From a0a3d76b787bbee80a4bb6cd9fcba61e9b0fae58 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Thu, 9 Jan 2025 09:42:11 +0100 Subject: [PATCH 01/32] cleaning up --- backend/src/utils/cluster_manager.py | 3 + backend/src/utils/landmarks_manager.py | 98 ++++++++++---------------- backend/src/utils/test.py | 27 +++++++ 3 files changed, 66 insertions(+), 62 deletions(-) create mode 100644 backend/src/utils/test.py diff --git a/backend/src/utils/cluster_manager.py b/backend/src/utils/cluster_manager.py index ed79c86..9b9570b 100644 --- a/backend/src/utils/cluster_manager.py +++ b/backend/src/utils/cluster_manager.py @@ -224,6 +224,9 @@ class ClusterManager: for elem in result.elements(): location = (elem.centerLat(), elem.centerLon()) + # Skip if element has neither name or location + if elem.tag('name') is None : + continue if location[0] is None : location = (elem.lat(), elem.lon()) if location[0] is None : diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index 92d01bb..5330bb7 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -261,97 +261,71 @@ class LandmarkManager: if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes': continue - elem_type = landmarktype # Add the landmark type as 'sightseeing, - n_tags = len(elem.tags().keys()) # Add number of tags - score = n_tags**self.tag_exponent # Add score - website_url = None - image_url = None - name_en = None + elem_type = landmarktype # Add the landmark type as 'sightseeing, + n_tags = len(elem.tags().keys()) # Add number of tags + score = n_tags**self.tag_exponent # Add score + duration = 5 # Set base duration to 5 minutes + skip = False # Set skipping parameter to false + tag_values = set(elem.tags().values()) # Store tag values - # Adjust scoring, browse through tag keys - skip = False + + # Use simple tags : + image_url = elem.tag('image') + website_url = elem.tag('website') + if website_url is None : + website_url = elem.tag('wikipedia') + name_en = elem.tag('name:en') + + if elem_type != "nature" and elem.tag('leisure') == "park": + elem_type = "nature" + + # Skip element if it is an administrative boundary or a disused thing or it is an appartement and useless amenities + if elem.tag('boundary') is not None or elem.tag('disused') is not None: + continue + if 'apartments' in elem.tags().values(): + continue + if elem.tag('historic') is not None and elem.tag('historic') in ['manor', 'optical_telegraph', 'pound', 'shieling', 'wayside_cross']: + continue + + # Adjust scoring, browse through tag keys using wildcards for tag_key in elem.tags().keys(): if "pay" in tag_key: # payment options are misleading and should not count for the scoring. score += self.pay_bonus - if "disused" in tag_key: - # skip disused amenities - skip = True - break - if "building:" in tag_key: # do not count the building description as being particularly useful n_tags -= 1 - - - if "boundary" in tag_key: - # skip "areas" like administrative boundaries and stuff - skip = True - break - - if "historic" in tag_key and elem.tag('historic') in ['manor', 'optical_telegraph', 'pound', 'shieling', 'wayside_cross']: - # skip useless amenities - skip = True - break - - if "name" in tag_key : - score += self.name_bonus if "wiki" in tag_key: # wikipedia entries count more score += self.wikipedia_bonus - if "image" in tag_key: - # images must count more - score += self.image_bonus - - if elem_type != "nature": - if "leisure" in tag_key and elem.tag('leisure') == "park": - elem_type = "nature" - if landmarktype != "shopping": if "shop" in tag_key: skip = True break - - if tag_key == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: - skip = True - break - - # Extract image, website and english name - if tag_key in ['website', 'contact:website']: - website_url = elem.tag(tag_key) - if tag_key == 'image': - image_url = elem.tag('image') - if tag_key =='name:en': - name_en = elem.tag('name:en') + # if tag_key == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: + # skip = True + # break if skip: continue - # Don't visit random apartments - if 'apartments' in elem.tags().values(): - continue - score = score_function(score) - if "place_of_worship" in elem.tags().values() : - if "cathedral" not in elem.tags().values() : - score = score * self.church_coeff - duration = 5 - else : + + if "place_of_worship" in tag_values : + if 'cathedral' in tag_values : duration = 10 + else : + score *= self.church_coeff - elif 'viewpoint' in elem.tags().values() : + elif 'viewpoint' in tag_values : # viewpoints must count more score = score * self.viewpoint_bonus - duration = 10 - elif "museum" in elem.tags().values() or "aquarium" in elem.tags().values() or "planetarium" in elem.tags().values(): + elif "museum" in tag_values or "aquarium" in tag_values or "planetarium" in tag_values: duration = 60 - - else: - duration = 5 # finally create our own landmark object landmark = Landmark( diff --git a/backend/src/utils/test.py b/backend/src/utils/test.py new file mode 100644 index 0000000..aca019b --- /dev/null +++ b/backend/src/utils/test.py @@ -0,0 +1,27 @@ +from OSMPythonTools.overpass import Overpass, overpassQueryBuilder + + + +overpass = Overpass() +query = overpassQueryBuilder( + bbox = (45.7300, 4.7900, 45.8000, 4.8600), + elementType = ['way'], + # selector can in principle be a list already, + # but it generates the intersection of the queries + # we want the union + selector = '"historic"="building"', + includeCenter = True, + out = 'body' + ) + + +res = overpass.query(query) + + +# for elem in res.elements() : +elem = res.elements()[1] + +tags = elem.tags() + +test = elem.tag('sgehs') +print(test) From 11bbf34375c82d5982e8c2c79186e656050e4855 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Thu, 9 Jan 2025 16:58:38 +0100 Subject: [PATCH 02/32] better logs --- backend/src/main.py | 16 ++++++++++++- backend/src/tests/test_main.py | 23 ++++++++++++++---- backend/src/tests/test_utils.py | 33 ++++++++++++-------------- backend/src/utils/landmarks_manager.py | 24 ++++++++----------- backend/src/utils/optimizer.py | 3 --- backend/src/utils/refiner.py | 2 +- backend/src/utils/test.py | 27 --------------------- 7 files changed, 60 insertions(+), 68 deletions(-) delete mode 100644 backend/src/utils/test.py diff --git a/backend/src/main.py b/backend/src/main.py index c8c1f40..b1c865b 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,6 +1,7 @@ """Main app for backend api""" import logging +import time from fastapi import FastAPI, HTTPException, Query from contextlib import asynccontextmanager @@ -81,6 +82,7 @@ def new_trip(preferences: Preferences, must_do=True, n_tags=0) + start_time = time.time() # Generate the landmarks from the start location landmarks, landmarks_short = manager.generate_landmarks_list( center_coordinates = start, @@ -91,6 +93,9 @@ def new_trip(preferences: Preferences, landmarks_short.insert(0, start_landmark) landmarks_short.append(end_landmark) + t_generate_landmarks = time.time() - start_time + start_time = time.time() + # First stage optimization try: base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) @@ -99,11 +104,20 @@ def new_trip(preferences: Preferences, except TimeoutError as exc: raise HTTPException(status_code=500, detail="Optimzation took too long") from exc + t_first_stage = time.time() - start_time + start_time = time.time() + # Second stage optimization refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_minute, preferences.detour_tolerance_minute) + t_second_stage = time.time() - start_time + logger.debug(f'Generating landmarks : {round(t_generate_landmarks,3)} seconds') + logger.debug(f'First stage optimization : {round(t_first_stage,3)} seconds') + logger.debug(f'Second stage optimization : {round(t_second_stage,3)} seconds') + logger.info(f'Total computation time : {round(t_generate_landmarks + t_first_stage + t_generate_landmarks,3)} seconds') + linked_tour = LinkedLandmarks(refined_tour) # upon creation of the trip, persistence of both the trip and its landmarks is ensured. trip = Trip.from_linked_landmarks(linked_tour, cache_client) @@ -165,7 +179,7 @@ def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) - raise HTTPException(status_code=406, detail="Coordinates not provided or invalid") if not (-90 <= location[0] <= 90 or -180 <= location[1] <= 180): raise HTTPException(status_code=422, detail="Start coordinates not in range") - + toilets_manager = ToiletsManager(location, radius) try : diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 25f58d4..0759f1e 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -1,7 +1,7 @@ """Collection of tests to ensure correct implementation and track progress. """ from fastapi.testclient import TestClient -import pytest +import pytest, time from .test_utils import landmarks_to_osmid, load_trip_landmarks, log_trip_details from ..main import app @@ -20,7 +20,9 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name client: request: """ + start_time = time.time() # Start timer duration_minutes = 15 + response = client.post( "/trip/new", json={ @@ -35,6 +37,9 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name result = response.json() landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) + # Get computation time + comp_time = time.time() - start_time + # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) @@ -43,6 +48,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 len(landmarks) > 2 # check that there is something to visit + assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" def test_bellecour(client, request) : # pylint: disable=redefined-outer-name @@ -53,7 +59,9 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name client: request: """ + start_time = time.time() # Start timer duration_minutes = 120 + response = client.post( "/trip/new", json={ @@ -67,7 +75,9 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name ) result = response.json() landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) - osm_ids = landmarks_to_osmid(landmarks) + + # Get computation time + comp_time = time.time() - start_time # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) @@ -79,8 +89,7 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name # checks : assert response.status_code == 200 # check for successful planning assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 - assert 136200148 in osm_ids # check for Cathédrale St. Jean in trip - # assert response.status_code == 2000 # check for successful planning + assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" @@ -92,7 +101,9 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name client: request: """ + start_time = time.time() # Start timer duration_minutes = 240 + response = client.post( "/trip/new", json={ @@ -107,12 +118,16 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name result = response.json() landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) + # Get computation time + comp_time = time.time() - start_time + # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) # checks : 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" # def test_new_trip_single_prefs(client): # response = client.post( diff --git a/backend/src/tests/test_utils.py b/backend/src/tests/test_utils.py index 73f4ec7..2f266ce 100644 --- a/backend/src/tests/test_utils.py +++ b/backend/src/tests/test_utils.py @@ -34,26 +34,25 @@ def fetch_landmark(client, landmark_uuid: str): dict: Landmark data fetched from the API. """ logger = logging.getLogger(__name__) - response = client.get(f"/landmark/{landmark_uuid}") + response = client.get(f'/landmark/{landmark_uuid}') if response.status_code != 200: raise HTTPException(status_code=500, - detail=f"Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}") + detail=f'Failed to fetch landmark with UUID {landmark_uuid}: {response.status_code}') try: json_data = response.json() - logger.info(f"API Response: {json_data}") + logger.info(f'API Response: {json_data}') 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") - + # Try validating against the Landmark model here to ensure consistency try: landmark = Landmark(**json_data) except ValidationError as ve: - logging.error(f"Validation error: {ve}") + logging.error(f'Validation error: {ve}') raise HTTPException(status_code=500, detail="Invalid data format received from API") - if "detail" in json_data: raise HTTPException(status_code=500, detail=json_data["detail"]) @@ -75,23 +74,21 @@ def fetch_landmark_cache(landmark_uuid: str): # Try to fetch the landmark data from the cache try: - landmark = cache_client.get(f"landmark_{landmark_uuid}") + landmark = cache_client.get(f'landmark_{landmark_uuid}') if not landmark : - logger.warning(f"Cache miss for landmark UUID: {landmark_uuid}") - raise HTTPException(status_code=404, detail=f"Landmark with UUID {landmark_uuid} not found in cache.") - + logger.warning(f'Cache miss for landmark UUID: {landmark_uuid}') + raise HTTPException(status_code=404, detail=f'Landmark with UUID {landmark_uuid} not found in cache.') + # Validate that the fetched data is a dictionary if not isinstance(landmark, Landmark): - logger.error(f"Invalid cache data format for landmark UUID: {landmark_uuid}. Expected dict, got {type(landmark).__name__}.") + logger.error(f'Invalid cache data format for landmark UUID: {landmark_uuid}. Expected dict, got {type(landmark).__name__}.') raise HTTPException(status_code=500, detail="Invalid cache data format.") return landmark - + except Exception as exc: - logger.error(f"Unexpected error occurred while fetching landmark UUID {landmark_uuid}: {exc}") + logger.error(f'Unexpected error occurred while fetching landmark UUID {landmark_uuid}: {exc}') raise HTTPException(status_code=500, detail="An unexpected error occurred while fetching the landmark from the cache") from exc - - def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> list[Landmark]: @@ -122,14 +119,14 @@ def load_trip_landmarks(client, first_uuid: str, from_cache=None) -> list[Landma def log_trip_details(request, landmarks: list[Landmark], duration: int, target_duration: int) : """ Allows to show the detailed trip in the html test report. - + Args: request: landmarks (list): the ordered list of visited landmarks duration (int): the total duration of this trip target_duration(int): the target duration of this trip """ - trip_string = [f"{landmark.name} ({landmark.attractiveness} | {landmark.duration}) - {landmark.time_to_reach_next}" for landmark in landmarks] + trip_string = [f'{landmark.name} ({landmark.attractiveness} | {landmark.duration}) - {landmark.time_to_reach_next}' for landmark in landmarks] # Pass additional info to pytest for reporting request.node.trip_details = trip_string diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index 5330bb7..b7eb240 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -212,9 +212,6 @@ class LandmarkManager: for sel in dict_to_selector_list(amenity_selector): self.logger.debug(f"Current selector: {sel}") - # query_conditions = ['count_tags()>5'] - # if landmarktype == 'shopping' : # use this later for shopping clusters - # element_types = ['node'] element_types = ['way', 'relation'] if 'viewpoint' in sel : @@ -269,7 +266,7 @@ class LandmarkManager: tag_values = set(elem.tags().values()) # Store tag values - # Use simple tags : + # Retrieve image, name and website : image_url = elem.tag('image') website_url = elem.tag('website') if website_url is None : @@ -278,6 +275,9 @@ class LandmarkManager: if elem_type != "nature" and elem.tag('leisure') == "park": elem_type = "nature" + + if elem.tag('wikipedia') is not None : + score += self.wikipedia_bonus # Skip element if it is an administrative boundary or a disused thing or it is an appartement and useless amenities if elem.tag('boundary') is not None or elem.tag('disused') is not None: @@ -297,20 +297,16 @@ class LandmarkManager: # do not count the building description as being particularly useful n_tags -= 1 - if "wiki" in tag_key: - # wikipedia entries count more - score += self.wikipedia_bonus - - if landmarktype != "shopping": - if "shop" in tag_key: - skip = True - break + # if landmarktype != "shopping": + # if "shop" in tag_key: + # skip = True + # break # if tag_key == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']: # skip = True # break - if skip: - continue + # if skip: + # continue score = score_function(score) diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index d7f354e..becdc76 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -8,9 +8,6 @@ from ..structs.landmark import Landmark from .get_time_separation import get_time from ..constants import OPTIMIZER_PARAMETERS_PATH - - - class Optimizer: diff --git a/backend/src/utils/refiner.py b/backend/src/utils/refiner.py index 1f9c755..e208d0b 100644 --- a/backend/src/utils/refiner.py +++ b/backend/src/utils/refiner.py @@ -333,7 +333,7 @@ class Refiner : minor_landmarks = self.get_minor_landmarks(all_landmarks, base_tour, self.detour_corridor_width) - self.logger.info(f"Using {len(minor_landmarks)} minor landmarks around the predicted path") + self.logger.debug(f"Using {len(minor_landmarks)} minor landmarks around the predicted path") # Full set of visitable landmarks. full_set = self.integrate_landmarks(minor_landmarks, base_tour) # could probably be optimized with less overhead diff --git a/backend/src/utils/test.py b/backend/src/utils/test.py deleted file mode 100644 index aca019b..0000000 --- a/backend/src/utils/test.py +++ /dev/null @@ -1,27 +0,0 @@ -from OSMPythonTools.overpass import Overpass, overpassQueryBuilder - - - -overpass = Overpass() -query = overpassQueryBuilder( - bbox = (45.7300, 4.7900, 45.8000, 4.8600), - elementType = ['way'], - # selector can in principle be a list already, - # but it generates the intersection of the queries - # we want the union - selector = '"historic"="building"', - includeCenter = True, - out = 'body' - ) - - -res = overpass.query(query) - - -# for elem in res.elements() : -elem = res.elements()[1] - -tags = elem.tags() - -test = elem.tag('sgehs') -print(test) From c6cebd0fdfc7bd2f90a456277d74751b0b85603a Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Fri, 10 Jan 2025 15:46:10 +0100 Subject: [PATCH 03/32] speeding up optimizer --- backend/src/utils/optimizer.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index becdc76..e5d4811 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -41,7 +41,7 @@ class Optimizer: 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. + tuple[list[int], list[int]]: A tuple containing a new row for A and new value for ub. """ for i, elem in enumerate(resx): @@ -165,19 +165,21 @@ class Optimizer: - def init_ub_dist(self, landmarks: list[Landmark], max_time: int): + def init_ub_time(self, landmarks: list[Landmark], max_time: int): """ - Initialize the objective function coefficients and inequality constraints for the optimization problem. + Initialize the objective function coefficients and inequality constraints. - 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. + 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. + 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 + ... @@ -191,7 +193,7 @@ class Optimizer: for j, spot2 in enumerate(landmarks) : t = get_time(spot1.location, spot2.location) + spot1.duration dist_table[j] = t - closest = sorted(dist_table)[:25] + closest = sorted(dist_table)[:15] for i, dist in enumerate(dist_table) : if dist not in closest : dist_table[i] = 32700 @@ -452,7 +454,7 @@ class Optimizer: 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 + c, A_ub, b_ub = self.init_ub_time(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 From 73373e0fc34412feab8edde69714f4a68642fd58 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Fri, 10 Jan 2025 15:59:44 +0100 Subject: [PATCH 04/32] linting --- backend/.pylintrc | 2 +- backend/src/cache.py | 2 +- backend/src/logging_config.py | 3 +-- backend/src/main.py | 2 +- backend/src/structs/landmark.py | 8 +++++--- backend/src/tests/test_cache.py | 2 +- backend/src/tests/test_main.py | 6 +++--- backend/src/tests/test_toilets.py | 11 +++++------ backend/src/tests/test_utils.py | 6 +++--- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/backend/.pylintrc b/backend/.pylintrc index db084dd..706cf02 100644 --- a/backend/.pylintrc +++ b/backend/.pylintrc @@ -402,7 +402,7 @@ preferred-modules= # The type of string formatting that logging methods do. `old` means using % # formatting, `new` is for `{}` formatting. -logging-format-style=old +logging-format-style=new # Logging modules to check that the string format arguments are in logging # function parameter format. diff --git a/backend/src/cache.py b/backend/src/cache.py index 97f9bb7..dffc222 100644 --- a/backend/src/cache.py +++ b/backend/src/cache.py @@ -70,6 +70,6 @@ else: MEMCACHED_HOST_PATH, timeout=1, allow_unicode_keys=True, - encoding='utf-8', + encoding='utf-8', serde=serde.pickle_serde ) diff --git a/backend/src/logging_config.py b/backend/src/logging_config.py index c43a246..c59d6d3 100644 --- a/backend/src/logging_config.py +++ b/backend/src/logging_config.py @@ -22,7 +22,7 @@ def configure_logging(): loki_url = "http://localhost:3100/loki/api/v1/push" if loki_url is None: raise ValueError("LOKI_URL environment variable is not set") - + loki_handler = LokiLoggerHandler( url = loki_url, labels = {'app': 'anyway', 'environment': 'staging' if is_debug else 'production'} @@ -55,4 +55,3 @@ def configure_logging(): logging.getLogger('uvicorn').handlers = logging_handlers logging.getLogger('uvicorn.access').handlers = logging_handlers logging.getLogger('uvicorn.error').handlers = logging_handlers - diff --git a/backend/src/main.py b/backend/src/main.py index b1c865b..b7f796c 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -2,8 +2,8 @@ import logging import time -from fastapi import FastAPI, HTTPException, Query from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException, Query from .logging_config import configure_logging from .structs.landmark import Landmark, Toilets diff --git a/backend/src/structs/landmark.py b/backend/src/structs/landmark.py index da0f122..b320f49 100644 --- a/backend/src/structs/landmark.py +++ b/backend/src/structs/landmark.py @@ -136,7 +136,9 @@ class Toilets(BaseModel) : str: A formatted string with the toilets location. """ return f'Toilets @{self.location}' - + class Config: - # This allows us to easily convert the model to and from dictionaries - from_attributes = True \ No newline at end of file + """ + This allows us to easily convert the model to and from dictionaries + """ + from_attributes = True diff --git a/backend/src/tests/test_cache.py b/backend/src/tests/test_cache.py index d2cfaca..6994a6c 100644 --- a/backend/src/tests/test_cache.py +++ b/backend/src/tests/test_cache.py @@ -13,7 +13,7 @@ def client(): return TestClient(app) -def test_cache(client, request): # pylint: disable=redefined-outer-name +def test_cache(client): # pylint: disable=redefined-outer-name """ Test n°1 : Custom test in Turckheim to ensure small villages are also supported. diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 0759f1e..c2e49cf 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -1,9 +1,9 @@ """Collection of tests to ensure correct implementation and track progress. """ - +import time from fastapi.testclient import TestClient -import pytest, time +import pytest -from .test_utils import landmarks_to_osmid, load_trip_landmarks, log_trip_details +from .test_utils import load_trip_landmarks, log_trip_details from ..main import app @pytest.fixture(scope="module") diff --git a/backend/src/tests/test_toilets.py b/backend/src/tests/test_toilets.py index 29d9f06..90ea5ea 100644 --- a/backend/src/tests/test_toilets.py +++ b/backend/src/tests/test_toilets.py @@ -6,11 +6,13 @@ import pytest from ..structs.landmark import Toilets from ..main import app + @pytest.fixture(scope="module") def client(): """Client used to call the app.""" return TestClient(app) + @pytest.mark.parametrize( "location,radius,status_code", [ @@ -39,8 +41,6 @@ def test_invalid_input(client, location, radius, status_code): # pylint: disa assert response.status_code == status_code - - @pytest.mark.parametrize( "location,status_code", [ @@ -66,11 +66,10 @@ def test_no_toilets(client, location, status_code): # pylint: disable=redefin toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] # checks : - assert response.status_code == 200 # check for successful planning + assert response.status_code == status_code # check for successful planning assert isinstance(toilets_list, list) # check that the return type is a list - @pytest.mark.parametrize( "location,status_code", [ @@ -97,6 +96,6 @@ def test_toilets(client, location, status_code): # pylint: disable=redefined- toilets_list = [Toilets.model_validate(toilet) for toilet in response.json()] # checks : - assert response.status_code == 200 # check for successful planning + assert response.status_code == status_code # check for successful planning assert isinstance(toilets_list, list) # check that the return type is a list - assert len(toilets_list) > 0 \ No newline at end of file + assert len(toilets_list) > 0 diff --git a/backend/src/tests/test_utils.py b/backend/src/tests/test_utils.py index 2f266ce..68d4fb8 100644 --- a/backend/src/tests/test_utils.py +++ b/backend/src/tests/test_utils.py @@ -45,19 +45,19 @@ def fetch_landmark(client, landmark_uuid: str): logger.info(f'API Response: {json_data}') except ValueError as e: logger.error(f'Failed to parse response as JSON: {response.text}') - raise HTTPException(status_code=500, detail="Invalid response format from API") + raise HTTPException(status_code=500, detail="Invalid response format from API") from e # Try validating against the Landmark model here to ensure consistency try: landmark = Landmark(**json_data) except ValidationError as ve: logging.error(f'Validation error: {ve}') - raise HTTPException(status_code=500, detail="Invalid data format received from API") + raise HTTPException(status_code=500, detail="Invalid data format received from API") from ve if "detail" in json_data: raise HTTPException(status_code=500, detail=json_data["detail"]) - return Landmark(**json_data) + return landmark def fetch_landmark_cache(landmark_uuid: str): From 41976e3e85928ebdc24603ec0cae727174ad0c96 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Fri, 10 Jan 2025 16:07:10 +0100 Subject: [PATCH 05/32] corrected timing --- backend/src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main.py b/backend/src/main.py index b7f796c..6d05911 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -116,7 +116,7 @@ def new_trip(preferences: Preferences, logger.debug(f'Generating landmarks : {round(t_generate_landmarks,3)} seconds') logger.debug(f'First stage optimization : {round(t_first_stage,3)} seconds') logger.debug(f'Second stage optimization : {round(t_second_stage,3)} seconds') - logger.info(f'Total computation time : {round(t_generate_landmarks + t_first_stage + t_generate_landmarks,3)} seconds') + logger.info(f'Total computation time : {round(t_generate_landmarks + t_first_stage + t_second_stage,3)} seconds') linked_tour = LinkedLandmarks(refined_tour) # upon creation of the trip, persistence of both the trip and its landmarks is ensured. From 4fae658dbb31b603a70424ed2f95187a4508016a Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Tue, 14 Jan 2025 11:59:23 +0100 Subject: [PATCH 06/32] better array handling in the optimizer --- backend/src/main.py | 5 +- backend/src/tests/test_main.py | 87 ++++- backend/src/utils/cluster_manager.py | 2 +- backend/src/utils/landmarks_manager.py | 31 +- backend/src/utils/optimizer.py | 477 ++++++++++++------------- backend/src/utils/refiner.py | 44 +-- 6 files changed, 366 insertions(+), 280 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index 6d05911..03a03c6 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -100,10 +100,11 @@ def new_trip(preferences: Preferences, try: base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short) except ArithmeticError as exc: - raise HTTPException(status_code=500, detail="No solution found") from exc + raise HTTPException(status_code=500) from exc except TimeoutError as exc: raise HTTPException(status_code=500, detail="Optimzation took too long") from exc - + except Exception as exc: + raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(exc)}") from exc t_first_stage = time.time() - start_time start_time = time.time() diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index c2e49cf..54f6202 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -35,8 +35,10 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name } ) result = response.json() + print(result) landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) + # Get computation time comp_time = time.time() - start_time @@ -49,7 +51,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" - + assert 2==3 def test_bellecour(client, request) : # pylint: disable=redefined-outer-name """ @@ -91,7 +93,88 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name 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" +''' +def test_Paris(client, request) : # pylint: disable=redefined-outer-name + """ + Test n°2 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area. + + Args: + client: + request: + """ + start_time = time.time() # Start timer + duration_minutes = 300 + + response = client.post( + "/trip/new", + json={ + "preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, + "nature": {"type": "nature", "score": 5}, + "shopping": {"type": "shopping", "score": 5}, + "max_time_minute": duration_minutes, + "detour_tolerance_minute": 0}, + "start": [48.86248803298562, 2.346451131285925] + } + ) + result = response.json() + landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) + + # Get computation time + comp_time = time.time() - start_time + + # Add details to report + log_trip_details(request, landmarks, result['total_time'], duration_minutes) + + for elem in landmarks : + print(elem) + print(elem.osm_id) + + # checks : + 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" + + +def test_New_York(client, request) : # pylint: disable=redefined-outer-name + """ + Test n°2 : Custom test in New York (les Halles) centre to ensure proper decision making in crowded area. + + Args: + client: + request: + """ + start_time = time.time() # Start timer + duration_minutes = 600 + + response = client.post( + "/trip/new", + json={ + "preferences": {"sightseeing": {"type": "sightseeing", "score": 5}, + "nature": {"type": "nature", "score": 5}, + "shopping": {"type": "shopping", "score": 5}, + "max_time_minute": duration_minutes, + "detour_tolerance_minute": 0}, + "start": [40.72592726802, -73.9920434795] + } + ) + result = response.json() + landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) + + # Get computation time + comp_time = time.time() - start_time + + # Add details to report + log_trip_details(request, landmarks, result['total_time'], duration_minutes) + + for elem in landmarks : + print(elem) + print(elem.osm_id) + + # checks : + 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" def test_shopping(client, request) : # pylint: disable=redefined-outer-name """ @@ -128,7 +211,7 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name 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" - +''' # def test_new_trip_single_prefs(client): # response = client.post( # "/trip/new", diff --git a/backend/src/utils/cluster_manager.py b/backend/src/utils/cluster_manager.py index 9b9570b..6dbc7a7 100644 --- a/backend/src/utils/cluster_manager.py +++ b/backend/src/utils/cluster_manager.py @@ -280,6 +280,6 @@ class ClusterManager: filtered_cluster_labels.append(np.full((label_counts[label],), label)) # Replicate the label # update the cluster points and labels with the filtered data - self.cluster_points = np.vstack(filtered_cluster_points) + self.cluster_points = np.vstack(filtered_cluster_points) # ValueError here self.cluster_labels = np.concatenate(filtered_cluster_labels) diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index b7eb240..37a69f6 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -1,3 +1,4 @@ +"""Module used to import data from OSM and arrange them in categories.""" import math, yaml, logging from OSMPythonTools.overpass import Overpass, overpassQueryBuilder from OSMPythonTools.cachingStrategy import CachingStrategy, JSON @@ -79,7 +80,7 @@ class LandmarkManager: # Create a bbox using the around technique bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1]))) - + # list for sightseeing if preferences.sightseeing.score != 0: score_function = lambda score: score * 10 * preferences.sightseeing.score / 5 @@ -101,7 +102,7 @@ class LandmarkManager: if preferences.shopping.score != 0: 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) - + # set time for all shopping activites : for landmark in current_landmarks : landmark.duration = 30 all_landmarks.update(current_landmarks) @@ -110,7 +111,7 @@ class LandmarkManager: shopping_manager = ClusterManager(bbox, 'shopping') shopping_clusters = shopping_manager.generate_clusters() all_landmarks.update(shopping_clusters) - + landmarks_constrained = take_most_important(all_landmarks, self.N_important) @@ -152,7 +153,7 @@ class LandmarkManager: elementType=['node', 'way', 'relation'] ) - try: + 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") @@ -242,28 +243,28 @@ class LandmarkManager: name = elem.tag('name') location = (elem.centerLat(), elem.centerLon()) osm_type = elem.type() # Add type: 'way' or 'relation' - osm_id = elem.id() # Add OSM id + osm_id = elem.id() # Add OSM id # TODO: exclude these from the get go # handle unprecise and no-name locations if name is None or location[0] is None: - if osm_type == 'node' and 'viewpoint' in elem.tags().values(): + if osm_type == 'node' and 'viewpoint' in elem.tags().values(): name = 'Viewpoint' name_en = 'Viewpoint' location = (elem.lat(), elem.lon()) - else : + else : continue # skip if part of another building if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes': continue - - elem_type = landmarktype # Add the landmark type as 'sightseeing, + + elem_type = landmarktype # Add the landmark type as 'sightseeing, n_tags = len(elem.tags().keys()) # Add number of tags score = n_tags**self.tag_exponent # Add score duration = 5 # Set base duration to 5 minutes 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 # Retrieve image, name and website : @@ -275,7 +276,7 @@ class LandmarkManager: if elem_type != "nature" and elem.tag('leisure') == "park": elem_type = "nature" - + if elem.tag('wikipedia') is not None : score += self.wikipedia_bonus @@ -309,9 +310,9 @@ class LandmarkManager: # continue score = score_function(score) - + if "place_of_worship" in tag_values : - if 'cathedral' in tag_values : + if 'cathedral' in tag_values : duration = 10 else : score *= self.church_coeff @@ -319,7 +320,7 @@ class LandmarkManager: elif 'viewpoint' in tag_values : # viewpoints must count more score = score * self.viewpoint_bonus - + elif "museum" in tag_values or "aquarium" in tag_values or "planetarium" in tag_values: duration = 60 @@ -339,7 +340,7 @@ class LandmarkManager: website_url = website_url ) return_list.append(landmark) - + self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}") return return_list diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index e5d4811..4ea7b5b 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -29,7 +29,232 @@ class Optimizer: self.average_walking_speed = parameters['average_walking_speed'] self.max_landmarks = parameters['max_landmarks'] self.overshoot = parameters['overshoot'] + + + def init_ub_time(self, landmarks: list[Landmark], max_time: int): + """ + Initialize the objective function coefficients and inequality constraints. + -> Adds 1 row of constraints + + 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 + 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. + """ + L = len(landmarks) + + # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... + c = np.zeros(L, dtype=np.int16) + + # Coefficients of inequality constraints (left-hand side) + A_first = np.zeros((L, L), dtype=np.int16) + + for i, spot1 in enumerate(landmarks) : + c[i] = -spot1.attractiveness + for j in range(i+1, L) : + if i !=j : + t = get_time(spot1.location, landmarks[j].location) + spot1.duration + A_first[i,j] = t + A_first[j,i] = t + + # Now sort and modify A_ub for each row + if L > 22 : + for i in range(L): + # Get indices of the 20 smallest values in row i + closest_indices = np.argpartition(A_first[i, :], 20)[:20] + + # Create a mask for non-closest landmarks + mask = np.ones(L, dtype=bool) + mask[closest_indices] = False + + # Set non-closest landmarks to 32700 + A_first[i, mask] = 32765 + # Replicate the objective function 'c' for each decision variable (L times) + 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): + """ + Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks. + -> Adds L-1 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. + """ + # First constraint: each landmark is visited exactly once + A = np.zeros((L-1, L*L), dtype=np.int8) + b = [] + for i in range(1, L-1): + A[i-1, L*i:L*(i+1)] = np.ones(L, dtype=np.int8) + b.append(1) + + # Second constraint: cap the total number of visits + A[-1, :] = np.ones(L*L, dtype=np.int8) + b.append(max_landmarks+2) + return A, b + + + def break_sym(self, L): + """ + Generate constraints to prevent simultaneous travel between two landmarks + in both directions. Constraint to not have d14 and d41 simultaneously. + Does not prevent cyclic paths with more elements + -> Adds a variable number of 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. + """ + b = [] + upper_ind = np.triu_indices(L,0,L) + up_ind_x = upper_ind[0] + up_ind_y = upper_ind[1] + + A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) + for i, _ in enumerate(up_ind_x[1:]) : + if up_ind_x[i] != up_ind_y[i] : + A[i, up_ind_x[i]*L + up_ind_y[i]] = 1 + A[i, up_ind_y[i]*L + up_ind_x[i]] = 1 + b.append(1) + + return A[~np.all(A == 0, axis=1)], 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.). + -> Adds 1 row of constraints + + 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 = np.zeros((L, L), dtype=np.int8) + + # Set diagonal elements to 1 (to prevent staying in the same position) + np.fill_diagonal(l, 1) + + return l.flatten(), [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. + -> 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_do'. + + 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) : + 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) : + 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) + b.append(0) + + return A[~np.all(A == 0, axis=1)], 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. + -> 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 @@ -164,236 +389,6 @@ class Optimizer: return order, all_journeys_nodes - - def init_ub_time(self, landmarks: list[Landmark], max_time: int): - """ - Initialize the objective function coefficients and inequality constraints. - - 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)[:15] - 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. @@ -455,28 +450,33 @@ class Optimizer: # 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 - 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) + + 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)) b_ub += b + A, b = self.break_sym(L) # break the 'zig-zag' symmetry - A_ub = np.vstack((A_ub, A), dtype=np.int16) + A_ub = np.vstack((A_ub, A)) 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 + if len(b) > 0 : + 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 + 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 # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) x_bounds = [(0, 1)]*L*L @@ -484,7 +484,7 @@ class Optimizer: # 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 + # Raise error if no solution is found. FIXME: for now this throws the internal server error if not res.success : raise ArithmeticError("No solution could be found, the problem is overconstrained. Try with a longer trip (>30 minutes).") @@ -505,7 +505,6 @@ class Optimizer: 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 : diff --git a/backend/src/utils/refiner.py b/backend/src/utils/refiner.py index e208d0b..054b95f 100644 --- a/backend/src/utils/refiner.py +++ b/backend/src/utils/refiner.py @@ -1,7 +1,9 @@ -import yaml, logging - -from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull +"""Allows to refine the tour by adding more landmarks and making the path easier to follow.""" +import logging from math import pi +import yaml +from shapely import buffer, LineString, Point, Polygon, MultiPoint, concave_hull + from ..structs.landmark import Landmark from . import take_most_important, get_time_separation @@ -13,7 +15,7 @@ from ..constants import OPTIMIZER_PARAMETERS_PATH class Refiner : logger = logging.getLogger(__name__) - + detour_factor: float # detour factor of straight line vs real distance in cities detour_corridor_width: float # width of the corridor around the path average_walking_speed: float # average walking speed of adult @@ -45,7 +47,7 @@ class Refiner : """ corrected_width = (180*width)/(6371000*pi) - + path = self.create_linestring(landmarks) obj = buffer(path, corrected_width, join_style="mitre", cap_style="square", mitre_limit=2) @@ -70,7 +72,7 @@ class Refiner : return LineString(points) - # Check if some coordinates are in area. Used for the corridor + # Check if some coordinates are in area. Used for the corridor def is_in_area(self, area: Polygon, coordinates) -> bool : """ Check if a given point is within a specified area. @@ -86,7 +88,7 @@ class Refiner : return point.within(area) - # Function to determine if two landmarks are close to each other + # Function to determine if two landmarks are close to each other def is_close_to(self, location1: tuple[float], location2: tuple[float]): """ Determine if two locations are close to each other by rounding their coordinates to 3 decimal places. @@ -119,7 +121,7 @@ class Refiner : Returns: list[Landmark]: The rearranged list of landmarks with grouped nearby visits. """ - + i = 1 while i < len(tour): j = i+1 @@ -131,9 +133,9 @@ class Refiner : break # Move to the next i-th element after rearrangement j += 1 i += 1 - + return tour - + def integrate_landmarks(self, sub_list: list[Landmark], main_list: list[Landmark]) : """ Inserts 'sub_list' of Landmarks inside the 'main_list' by leaving the ends untouched. @@ -166,24 +168,24 @@ class Refiner : should be visited, and the second element is a `Polygon` representing the path connecting all landmarks. """ - + # Step 1: Find 'start' and 'finish' landmarks start_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'start') finish_idx = next(i for i, lm in enumerate(landmarks) if lm.type == 'finish') - + start_landmark = landmarks[start_idx] finish_landmark = landmarks[finish_idx] - + # Step 2: Create a list of unvisited landmarks excluding 'start' and 'finish' unvisited_landmarks = [lm for i, lm in enumerate(landmarks) if i not in [start_idx, finish_idx]] - + # Step 3: Initialize the path with the 'start' landmark path = [start_landmark] coordinates = [landmarks[start_idx].location] current_landmark = start_landmark - + # Step 4: Use nearest neighbor heuristic to visit all landmarks while unvisited_landmarks: nearest_landmark = min(unvisited_landmarks, key=lambda lm: get_time_separation.get_time(current_landmark.location, lm.location)) @@ -224,7 +226,7 @@ class Refiner : for visited in visited_landmarks : visited_names.append(visited.name) - + for landmark in all_landmarks : if self.is_in_area(area, landmark.location) and landmark.name not in visited_names: second_order_landmarks.append(landmark) @@ -256,7 +258,7 @@ class Refiner : coords_dict[landmark.location] = landmark tour_poly = Polygon(coords) - + better_tour_poly = tour_poly.buffer(0) try : xs, ys = better_tour_poly.exterior.xy @@ -299,7 +301,7 @@ class Refiner : # Rearrange only if polygon still not simple if not better_tour_poly.is_simple : better_tour = self.rearrange(better_tour) - + return better_tour @@ -330,7 +332,7 @@ class Refiner : # No need to refine if no detour is taken # if detour == 0: # return base_tour - + minor_landmarks = self.get_minor_landmarks(all_landmarks, base_tour, self.detour_corridor_width) self.logger.debug(f"Using {len(minor_landmarks)} minor landmarks around the predicted path") @@ -341,7 +343,7 @@ class Refiner : # Generate a new tour with the optimizer. new_tour = self.optimizer.solve_optimization( max_time = max_time + detour, - landmarks = full_set, + landmarks = full_set, max_landmarks = self.max_landmarks_refiner ) @@ -357,7 +359,7 @@ class Refiner : # Find shortest path using the nearest neighbor heuristic. better_tour, better_poly = self.find_shortest_path_through_all_landmarks(new_tour) - # Fix the tour using Polygons if the path looks weird. + # Fix the tour using Polygons if the path looks weird. # Conditions : circular trip and invalid polygon. if base_tour[0].location == base_tour[-1].location and not better_poly.is_valid : better_tour = self.fix_using_polygon(better_tour) From ecd505a9ceda0bf33230b1585204e35472a97cbf Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Tue, 14 Jan 2025 18:23:58 +0100 Subject: [PATCH 07/32] massive numpy optimization and more tests --- backend/report.html | 1094 ++++++++++++++++++++++ backend/src/main.py | 5 +- backend/src/tests/test_main.py | 38 +- backend/src/tests/test_utils.py | 2 +- backend/src/utils/cluster_manager.py | 20 +- backend/src/utils/get_time_separation.py | 30 +- backend/src/utils/landmarks_manager.py | 25 +- backend/src/utils/optimizer.py | 473 ++++++---- 8 files changed, 1440 insertions(+), 247 deletions(-) create mode 100644 backend/report.html diff --git a/backend/report.html b/backend/report.html new file mode 100644 index 0000000..c0a91b2 --- /dev/null +++ b/backend/report.html @@ -0,0 +1,1094 @@ + + + + + Backend Testing Report + + + + +

Backend Testing Report

+

Report generated on 14-Jan-2025 at 16:05:24 by pytest-html + v4.1.1

+
+

Environment

+
+
+ + + + + +
+
+

Summary

+
+
+

23 tests took 00:01:59.

+

(Un)check the boxes to filter the results.

+
+ +
+
+
+
+ + 4 Failed, + + 19 Passed, + + 0 Skipped, + + 0 Expected failures, + + 0 Unexpected passes, + + 0 Errors, + + 0 Reruns +
+
+  /  +
+
+
+
+
+
+
+
+ + + + + + + + + + + + +
ResultTestDetailed tripTrip DurationTarget DurationExecution timeLinks
+ +
+
+ +
+ \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index 03a03c6..765cbcb 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -109,9 +109,12 @@ def new_trip(preferences: Preferences, start_time = time.time() # Second stage optimization - refined_tour = refiner.refine_optimization(landmarks, base_tour, + try : + refined_tour = refiner.refine_optimization(landmarks, base_tour, preferences.max_time_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 logger.debug(f'Generating landmarks : {round(t_generate_landmarks,3)} seconds') diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 54f6202..60e1278 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -35,7 +35,6 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name } ) result = response.json() - print(result) 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 log_trip_details(request, landmarks, result['total_time'], duration_minutes) + # for elem in landmarks : + # print(elem) + # checks : assert response.status_code == 200 # check for successful planning 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 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 2==3 + # assert 2==3 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 log_trip_details(request, landmarks, result['total_time'], duration_minutes) - for elem in landmarks : - print(elem) - print(elem.osm_id) + # for elem in landmarks : + # print(elem) # checks : 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 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 """ @@ -126,14 +127,13 @@ def test_Paris(client, request) : # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - for elem in landmarks : - print(elem) - print(elem.osm_id) + # for elem in landmarks : + # print(elem) # checks : 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 duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 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 log_trip_details(request, landmarks, result['total_time'], duration_minutes) - for elem in landmarks : - print(elem) - print(elem.osm_id) + # for elem in landmarks : + # print(elem) # checks : 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 duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 + 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 log_trip_details(request, landmarks, result['total_time'], duration_minutes) + # for elem in landmarks : + # print(elem) + # checks : 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 duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2 + + # def test_new_trip_single_prefs(client): # response = client.post( # "/trip/new", diff --git a/backend/src/tests/test_utils.py b/backend/src/tests/test_utils.py index 68d4fb8..56c3405 100644 --- a/backend/src/tests/test_utils.py +++ b/backend/src/tests/test_utils.py @@ -42,7 +42,7 @@ def fetch_landmark(client, landmark_uuid: str): try: json_data = response.json() - logger.info(f'API Response: {json_data}') + # logger.info(f'API Response: {json_data}') except ValueError as e: logger.error(f'Failed to parse response as JSON: {response.text}') raise HTTPException(status_code=500, detail="Invalid response format from API") from e diff --git a/backend/src/utils/cluster_manager.py b/backend/src/utils/cluster_manager.py index 6dbc7a7..4164ac3 100644 --- a/backend/src/utils/cluster_manager.py +++ b/backend/src/utils/cluster_manager.py @@ -12,6 +12,10 @@ from ..utils.get_time_separation import get_distance from ..constants import OSM_CACHE_DIR +# silence the overpass logger +logging.getLogger('OSMPythonTools').setLevel(level=logging.CRITICAL) + + class Cluster(BaseModel): """" A class representing an interesting area for shopping or sightseeing. @@ -102,7 +106,6 @@ class ClusterManager: points.append(coords) self.all_points = np.array(points) - self.valid = True # Apply DBSCAN to find clusters. Choose different settings for different cities. if self.cluster_type == 'shopping' and len(self.all_points) > 200 : @@ -114,12 +117,17 @@ class ClusterManager: labels = dbscan.fit_predict(self.all_points) - # Separate clustered points and noise points - self.cluster_points = self.all_points[labels != -1] - self.cluster_labels = labels[labels != -1] + # 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 + self.cluster_points = self.all_points[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 - self.filter_clusters() + else : + self.valid = False def generate_clusters(self) -> list[Landmark]: diff --git a/backend/src/utils/get_time_separation.py b/backend/src/utils/get_time_separation.py index c8bd509..1952d9d 100644 --- a/backend/src/utils/get_time_separation.py +++ b/backend/src/utils/get_time_separation.py @@ -23,23 +23,23 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: """ - if p1 == p2: - return 0 - else: - # Compute the distance in km along the surface of the Earth - # (assume spherical Earth) - # this is the haversine formula, stolen from stackoverflow - # in order to not use any external libraries - lat1, lon1 = radians(p1[0]), radians(p1[1]) - lat2, lon2 = radians(p2[0]), radians(p2[1]) + # if p1 == p2: + # return 0 + # else: + # Compute the distance in km along the surface of the Earth + # (assume spherical Earth) + # this is the haversine formula, stolen from stackoverflow + # in order to not use any external libraries + lat1, lon1 = radians(p1[0]), radians(p1[1]) + lat2, lon2 = radians(p2[0]), radians(p2[1]) - dlon = lon2 - lon1 - dlat = lat2 - lat1 + dlon = lon2 - lon1 + dlat = lat2 - lat1 - a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 - c = 2 * atan2(sqrt(a), sqrt(1 - a)) + a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 + c = 2 * atan2(sqrt(a), sqrt(1 - a)) - distance = EARTH_RADIUS_KM * c + distance = EARTH_RADIUS_KM * c # Consider the detour factor for average an average city walk_distance = distance * DETOUR_FACTOR @@ -47,7 +47,7 @@ def get_time(p1: tuple[float, float], p2: tuple[float, float]) -> int: # Time to walk this distance (in minutes) 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: diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index 37a69f6..15c49e9 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -53,6 +53,8 @@ class LandmarkManager: self.overpass = Overpass() 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]]: """ @@ -71,7 +73,7 @@ class LandmarkManager: - A list of all existing landmarks. - 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 reachable_bbox_side = min(max_walk_dist, self.max_bbox_side) @@ -83,25 +85,32 @@ class LandmarkManager: # list for sightseeing if preferences.sightseeing.score != 0: + self.logger.debug('Fetching sightseeing landmarks...') 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) all_landmarks.update(current_landmarks) + self.logger.debug('Fetching sightseeing clusters...') # special pipeline for historic neighborhoods neighborhood_manager = ClusterManager(bbox, 'sightseeing') historic_clusters = neighborhood_manager.generate_clusters() all_landmarks.update(historic_clusters) + self.logger.debug('Sightseeing clusters done') # list for nature if preferences.nature.score != 0: + self.logger.debug('Fetching nature landmarks...') 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) all_landmarks.update(current_landmarks) + # list for shopping if preferences.shopping.score != 0: + self.logger.debug('Fetching shopping landmarks...') 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) + self.logger.debug('Fetching shopping clusters...') # set time for all shopping activites : for landmark in current_landmarks : landmark.duration = 30 @@ -111,18 +120,19 @@ class LandmarkManager: shopping_manager = ClusterManager(bbox, 'shopping') shopping_clusters = shopping_manager.generate_clusters() all_landmarks.update(shopping_clusters) + self.logger.debug('Shopping clusters done') 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 - + """ 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 @@ -134,7 +144,7 @@ class LandmarkManager: 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] @@ -162,6 +172,7 @@ class LandmarkManager: return N_elem except: return 0 + """ # 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 # we need to split the selectors into separate queries and merge the results 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'] @@ -230,7 +241,7 @@ class LandmarkManager: includeCenter = True, out = 'center' ) - self.logger.debug(f"Query: {query}") + # self.logger.debug(f"Query: {query}") try: result = self.overpass.query(query) diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 4ea7b5b..2f4dd6a 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -35,11 +35,7 @@ class Optimizer: """ Initialize the objective function coefficients and inequality constraints. -> Adds 1 row of constraints - - 1 row - + L-1 rows - - -> Pre-allocates A_ub for the rest of the computations + -> Pre-allocates A_ub for the rest of the computations with 2*L rows This function computes the distances between all landmarks and stores their attractiveness to maximize sightseeing. The goal is to maximize @@ -59,36 +55,42 @@ class Optimizer: c = np.zeros(L, dtype=np.int16) # 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) : c[i] = -spot1.attractiveness for j in range(i+1, L) : if i !=j : t = get_time(spot1.location, landmarks[j].location) + spot1.duration - A_first[i,j] = t - A_first[j,i] = t + A_ub[0, i*L + j] = 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 if L > 22 : for i in range(L): - # Get indices of the 20 smallest values in row i - closest_indices = np.argpartition(A_first[i, :], 20)[:20] + # Get indices of the 4 smallest values in row i + 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 mask = np.ones(L, dtype=bool) mask[closest_indices] = False - # Set non-closest landmarks to 32700 - A_first[i, mask] = 32765 - - # Replicate the objective function 'c' for each decision variable (L times) - c = np.tile(c, L) # This correctly expands 'c' to L*L + # Set non-closest landmarks to 32765 + row_values[mask] = 32765 + A_ub[0, i*L:i*L+L] = row_values - return c, A_first.flatten(), [max_time*self.overshoot] + return c, A_ub, b_ub - 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. -> 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. """ # First constraint: each landmark is visited exactly once - A = np.zeros((L-1, L*L), dtype=np.int8) - b = [] + # A = np.zeros((L-1, L*L), dtype=np.int8) + # b = [] + # Fill-in row 2 until row L-2 for i in range(1, L-1): - A[i-1, L*i:L*(i+1)] = np.ones(L, dtype=np.int8) - b.append(1) + A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16) + b[i] = 1 + # Fill-in row L-1 # Second constraint: cap the total number of visits - A[-1, :] = np.ones(L*L, dtype=np.int8) - b.append(max_landmarks+2) - return A, b + A[L-1, :] = np.ones(L*L, dtype=np.int16) + b[L-1] = max_landmarks+2 - def break_sym(self, L): + def break_sym(self, A, b, L): """ Generate constraints to prevent simultaneous travel between two landmarks in both directions. Constraint to not have d14 and d41 simultaneously. 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: L (int): Number of landmarks. @@ -126,25 +129,27 @@ class Optimizer: tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. """ - b = [] + # b = [] upper_ind = np.triu_indices(L,0,L) up_ind_x = upper_ind[0] up_ind_y = upper_ind[1] - A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) - for i, _ in enumerate(up_ind_x[1:]) : + # A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) + # Fill-in rows L to 2*L-1 + for i in range(L) : if up_ind_x[i] != up_ind_y[i] : - A[i, up_ind_x[i]*L + up_ind_y[i]] = 1 - A[i, up_ind_y[i]*L + up_ind_x[i]] = 1 - b.append(1) + A[L+i, up_ind_x[i]*L + up_ind_y[i]] = 1 + A[L+i, up_ind_y[i]*L + up_ind_x[i]] = 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.). -> Adds 1 row of constraints + -> Pre-allocates A_eq for the rest of the computations with (L+2 + dynamic incr) rows Args: L (int): Number of landmarks. @@ -152,15 +157,78 @@ class Optimizer: Returns: 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) # Set diagonal elements to 1 (to prevent staying in the same position) 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. -> 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. """ L = len(landmarks) - A = np.zeros((L, L*L), dtype=np.int8) - b = [] - - 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 = [] + ones = np.ones(L, dtype=np.int8) + incr = 0 for i, elem in enumerate(landmarks) : 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) - b.append(0) - - return A[~np.all(A == 0, axis=1)], 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. - -> 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 + # First part of the dynamic infill + A[L+2+incr, i*L:i*L+L] = ones + b[L+2+incr] = 1 + incr += 1 + if elem.must_avoid is True and i not in [0, L-1]: + # Second part of the dynamic infill + A[L+2+incr, i*L:i*L+L] = ones + b[L+2+incr] = 0 + incr += 1 # Prevent the use of a particular solution @@ -282,13 +282,14 @@ class Optimizer: vertices_visited = ind_a vertices_visited.remove(0) - ones = [1]*L - h = [0]*N + ones = np.ones(L, dtype=np.int8) + h = np.zeros(L*L, dtype=np.int8) + for i in range(L) : if i in vertices_visited : 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) @@ -303,22 +304,21 @@ class Optimizer: Returns: 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]) : next = circle_vertices[i+1] - l1[node*L + next] = 1 - l2[next*L + node] = 1 + l[0, node*L + next] = 1 + l[1, next*L + node] = 1 s = circle_vertices[0] g = circle_vertices[-1] - l1[g*L + s] = 1 - l2[s*L + g] = 1 + l[0, g*L + s] = 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) : @@ -331,10 +331,90 @@ class Optimizer: Returns: 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 + + 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] # removed .tolist() + ind_b = nonzero_tup[1] + + # 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) + for a, b in zip(ind_a, ind_b): + graph[a].append(b) + + 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 + + + def get_order(self, resx): + """ + Determine the order of visits given the result of the optimization. + + Args: + resx (list): List of edge weights. + + Returns: + list[int]: A list containing the visit order. + """ # first round the results to have only 0-1 values - for i, elem in enumerate(resx): - resx[i] = round(elem) + 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. @@ -345,48 +425,32 @@ class Optimizer: 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) + 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 - # Step 2: Function to perform BFS/DFS to extract journeys - def get_journey(start): - journey_nodes = [] - visited = set() - stack = deque([start]) + 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 - 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) + # Switch roles, now look for next_node in ind_a + try: + current = next_node + except ValueError: + break # No further connections, end the path - return journey_nodes + return order - # 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 link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] : @@ -449,33 +513,34 @@ class Optimizer: L = len(landmarks) # 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 - - 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)) - b_ub += b - - A, b = self.break_sym(L) # break the 'zig-zag' symmetry - A_ub = np.vstack((A_ub, A)) - b_ub += b - + 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). + self.break_sym(A_ub, b_ub, L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. # 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 - if len(b) > 0 : - 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 - 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 + A_eq, b_eq = self.init_eq_not_stay(landmarks) # Force solution not to stay in same place + self.respect_start_finish(A_eq, b_eq, L) # Force start and finish positions + self.respect_order(A_eq, b_eq, L) # Respect order of visit (only works when max_time is limiting factor) + self.respect_user_must(A_eq, b_eq, landmarks) # Force to do/avoid landmarks set by user. + + self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") + + + + # A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal + # if len(b) > 0 : + # 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 + # 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 # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) @@ -484,39 +549,47 @@ class Optimizer: # 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) + 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 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).") # 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) i = 0 timeout = 80 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_ub = np.vstack((A_ub, A)) - b_ub += b - #A_ub, b_ub = prevent_circle(order, len(landmarks), A_ub, b_ub) + b_ub = np.concatenate((b_ub, b)) + for circle in circles : A, b = self.prevent_circle(circle, L) 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) + if not res.success : + self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.') 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) if circles is None : break - # print(i) - i += 1 - + 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.") - #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] - + self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}") return tour From dba988629d084ce2c586ccf1b0d4f51f85204b4c Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 08:58:17 +0100 Subject: [PATCH 08/32] formatting for tests --- backend/src/tests/test_main.py | 6 +- backend/src/utils/old_optimizer.py | 524 +++++++++++++++++++++++++++++ backend/src/utils/optimizer.py | 7 + 3 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 backend/src/utils/old_optimizer.py diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 60e1278..1a353f4 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -53,8 +53,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 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 """ Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area. @@ -214,7 +214,7 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name assert response.status_code == 200 # check for successful planning 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): # response = client.post( diff --git a/backend/src/utils/old_optimizer.py b/backend/src/utils/old_optimizer.py new file mode 100644 index 0000000..d7f354e --- /dev/null +++ b/backend/src/utils/old_optimizer.py @@ -0,0 +1,524 @@ +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 diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 2f4dd6a..7d4d345 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -525,6 +525,13 @@ class Optimizer: self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") + print(A_ub) + print('\n\n') + print(b_ub) + print('\n\n') + print(A_eq) + print('\n\n') + print(b_eq) # A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal From 14385342ccb4519ca5c5631a6db2fda0c1ef322c Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 09:35:25 +0100 Subject: [PATCH 09/32] better sym breaking --- backend/src/tests/test_main.py | 1 + backend/src/utils/optimizer.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 1a353f4..92b7be6 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -35,6 +35,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name } ) result = response.json() + print(result) landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 7d4d345..77b30de 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -35,7 +35,7 @@ class Optimizer: """ Initialize the objective function coefficients and inequality constraints. -> Adds 1 row of constraints - -> Pre-allocates A_ub for the rest of the computations with 2*L rows + -> Pre-allocates A_ub for the rest of the computations with L + (L*L-L)/2 rows This function computes the distances between all landmarks and stores their attractiveness to maximize sightseeing. The goal is to maximize @@ -55,8 +55,8 @@ class Optimizer: c = np.zeros(L, dtype=np.int16) # Coefficients of inequality constraints (left-hand side) - A_ub = np.zeros((2*L, L*L), dtype=np.int16) - b_ub = np.zeros(2*L, dtype=np.int16) + A_ub = np.zeros((L + int((L*L-L)/2), L*L), dtype=np.int16) + b_ub = np.zeros(L + int((L*L-L)/2), dtype=np.int16) # Fill in first row b_ub[0] = round(max_time*self.overshoot) @@ -65,9 +65,9 @@ class Optimizer: c[i] = -spot1.attractiveness for j in range(i+1, L) : if i !=j : - t = get_time(spot1.location, landmarks[j].location) + spot1.duration - A_ub[0, i*L + j] = t - A_ub[0, j*L + i] = t + t = get_time(spot1.location, landmarks[j].location) + A_ub[0, i*L + j] = t + spot1.duration + A_ub[0, j*L + i] = t + landmarks[j].duration # Expand 'c' to L*L for every decision variable c = np.tile(c, L) @@ -120,7 +120,7 @@ class Optimizer: Generate constraints to prevent simultaneous travel between two landmarks in both directions. Constraint to not have d14 and d41 simultaneously. Does not prevent cyclic paths with more elements - -> Adds L rows of constraints (some of which might be zero) + -> Adds (L*L-L)/2 rows of constraints (some of which might be zero) Args: L (int): Number of landmarks. @@ -136,7 +136,7 @@ class Optimizer: # A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) # Fill-in rows L to 2*L-1 - for i in range(L) : + for i in range(int((L*L-L)/2)) : if up_ind_x[i] != up_ind_y[i] : A[L+i, up_ind_x[i]*L + up_ind_y[i]] = 1 A[L+i, up_ind_y[i]*L + up_ind_x[i]] = 1 From 133f81ce3b2f35a8e43bdc51dda35e6a8a391e89 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 10:05:12 +0100 Subject: [PATCH 10/32] more print --- backend/src/tests/test_main.py | 2 +- backend/src/utils/optimizer.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 92b7be6..933eaad 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -54,7 +54,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 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 """ diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 77b30de..858d350 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -136,11 +136,13 @@ class Optimizer: # A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) # Fill-in rows L to 2*L-1 - for i in range(int((L*L-L)/2)) : + incr = 0 + for i in range(int((L*L+L)/2)) : if up_ind_x[i] != up_ind_y[i] : - A[L+i, up_ind_x[i]*L + up_ind_y[i]] = 1 - A[L+i, up_ind_y[i]*L + up_ind_x[i]] = 1 - b[L+i] = 1 + A[L+incr, up_ind_x[i]*L + up_ind_y[i]] = 1 + A[L+incr, up_ind_y[i]*L + up_ind_x[i]] = 1 + b[L+incr] = 1 + incr += 1 # return A[~np.all(A == 0, axis=1)], b @@ -525,13 +527,10 @@ class Optimizer: self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") - print(A_ub) - print('\n\n') - print(b_ub) - print('\n\n') print(A_eq) print('\n\n') print(b_eq) + print('\n\n') # A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal From d62dddd4240c4999d000c8a15185e3709ebab914 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 10:05:42 +0100 Subject: [PATCH 11/32] forgot assertion --- backend/src/tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 933eaad..92b7be6 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -54,7 +54,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 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 """ From 3fe6056f3c2aa89e5b47f56a326cc56c2d7d983c Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 16:48:32 +0100 Subject: [PATCH 12/32] first steps toward pulp --- backend/Pipfile | 1 + backend/Pipfile.lock | 1555 ++++++++++++++++---------------- backend/src/tests/test_main.py | 18 +- backend/src/utils/optimizer.py | 131 +-- 4 files changed, 843 insertions(+), 862 deletions(-) diff --git a/backend/Pipfile b/backend/Pipfile index 1f9a632..8a93488 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -26,3 +26,4 @@ fastapi-cli = "*" scikit-learn = "*" pyqt6 = "*" loki-logger-handler = "*" +pulp = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index b13bd13..796a51d 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6edd6644586e8814a0b4526adb3352dfc17828ca129de7a68c1d5929efe94daa" + "sha256": "91fbdaede2db7679f1f689c7e6b22598cb367723117f78a7760ad21cb4b5c3c9" }, "pipfile-spec": 6, "requires": {}, @@ -24,11 +24,11 @@ }, "anyio": { "hashes": [ - "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", - "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" + "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", + "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a" ], "markers": "python_version >= '3.9'", - "version": "==4.6.2.post1" + "version": "==4.8.0" }, "beautifulsoup4": { "hashes": [ @@ -40,130 +40,117 @@ }, "certifi": { "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", + "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" ], "markers": "python_version >= '3.6'", - "version": "==2024.8.30" + "version": "==2024.12.14" }, "charset-normalizer": { "hashes": [ - "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", - "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", - "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", - "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", - "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", - "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", - "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", - "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", - "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", - "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", - "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", - "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", - "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", - "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", - "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", - "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", - "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", - "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", - "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", - "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", - "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", - "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", - "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", - "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", - "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", - "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", - "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", - "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", - "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", - "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", - "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", - "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", - "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", - "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", - "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", - "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", - "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", - "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", - "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", - "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", - "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", - "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", - "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", - "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", - "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", - "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", - "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", - "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", - "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", - "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", - "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", - "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", - "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", - "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", - "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", - "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", - "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", - "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", - "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", - "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", - "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", - "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", - "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", - "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", - "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", - "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", - "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", - "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", - "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", - "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", - "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", - "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", - "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", - "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", - "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", - "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", - "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", - "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", - "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", - "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", - "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", - "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", - "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", - "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", - "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", - "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", - "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", - "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", - "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", - "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", - "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", - "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", - "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", - "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", - "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", - "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", - "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", - "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", - "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", - "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", - "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", - "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", - "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", - "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", - "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" + "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", + "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", + "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", + "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", + "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", + "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", + "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", + "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", + "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", + "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", + "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", + "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", + "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", + "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", + "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", + "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", + "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", + "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", + "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", + "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", + "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", + "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", + "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", + "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", + "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", + "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", + "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", + "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", + "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", + "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", + "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", + "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", + "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", + "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", + "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", + "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", + "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", + "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", + "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", + "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", + "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", + "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", + "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", + "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", + "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", + "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", + "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", + "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", + "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", + "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", + "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", + "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", + "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", + "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", + "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", + "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", + "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", + "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", + "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", + "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", + "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", + "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", + "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", + "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", + "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", + "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", + "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", + "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", + "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", + "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", + "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", + "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", + "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", + "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", + "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", + "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", + "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", + "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", + "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", + "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", + "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", + "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", + "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", + "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", + "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", + "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", + "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", + "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", + "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", + "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", + "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", + "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.4.0" + "markers": "python_version >= '3.7'", + "version": "==3.4.1" }, "click": { "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" ], "markers": "python_version >= '3.7'", - "version": "==8.1.7" + "version": "==8.1.8" }, "contourpy": { "hashes": [ @@ -235,85 +222,85 @@ }, "fastapi": { "hashes": [ - "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", - "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796" + "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", + "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.115.5" + "version": "==0.115.6" }, "fastapi-cli": { "hashes": [ - "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", - "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46" + "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", + "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.0.5" + "version": "==0.0.7" }, "fonttools": { "hashes": [ - "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e", - "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c", - "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f", - "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03", - "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c", - "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438", - "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998", - "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05", - "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60", - "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a", - "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189", - "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c", - "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6", - "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61", - "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd", - "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8", - "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a", - "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc", - "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b", - "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6", - "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81", - "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40", - "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71", - "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d", - "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9", - "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c", - "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7", - "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d", - "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f", - "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6", - "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18", - "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6", - "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b", - "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478", - "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967", - "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c", - "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69", - "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2", - "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4", - "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838", - "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6", - "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880", - "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe", - "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3", - "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2", - "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6", - "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246", - "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf", - "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c", - "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51" + "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7", + "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b", + "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261", + "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0", + "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02", + "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841", + "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45", + "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4", + "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b", + "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a", + "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048", + "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90", + "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd", + "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674", + "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72", + "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c", + "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07", + "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b", + "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de", + "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926", + "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e", + "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628", + "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca", + "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29", + "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa", + "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe", + "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427", + "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d", + "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765", + "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5", + "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d", + "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314", + "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b", + "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af", + "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831", + "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3", + "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56", + "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e", + "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276", + "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0", + "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851", + "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5", + "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54", + "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b", + "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f", + "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4", + "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977", + "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f", + "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35", + "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32" ], "markers": "python_version >= '3.8'", - "version": "==4.55.0" + "version": "==4.55.3" }, "geojson": { "hashes": [ - "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac", - "sha256:68a9771827237adb8c0c71f8527509c8f5bef61733aa434cefc9c9d4f0ebe8f3" + "sha256:69d14156469e13c79479672eafae7b37e2dcd19bdfd77b53f74fa8fe29910b52", + "sha256:b860baba1e8c6f71f8f5f6e3949a694daccf40820fa8f138b3f712bd85804903" ], "markers": "python_version >= '3.7'", - "version": "==3.1.0" + "version": "==3.2.0" }, "h11": { "hashes": [ @@ -389,132 +376,98 @@ }, "kiwisolver": { "hashes": [ - "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", - "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95", - "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", - "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", - "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d", - "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", - "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", - "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", - "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", - "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", - "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", - "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", - "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", - "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", - "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", - "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", - "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", - "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", - "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", - "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e", - "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", - "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", - "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", - "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", - "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", - "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", - "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", - "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5", - "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", - "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", - "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", - "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", - "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", - "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", - "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", - "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", - "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3", - "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a", - "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", - "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", - "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", - "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", - "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", - "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", - "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", - "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", - "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", - "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", - "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", - "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", - "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b", - "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", - "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", - "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", - "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", - "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", - "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", - "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", - "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", - "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", - "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", - "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", - "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", - "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933", - "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", - "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", - "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", - "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503", - "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", - "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", - "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", - "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", - "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", - "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", - "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf", - "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d", - "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", - "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", - "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", - "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", - "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2", - "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", - "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade", - "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a", - "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c", - "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", - "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00", - "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", - "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", - "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", - "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", - "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", - "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09", - "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", - "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", - "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89", - "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", - "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", - "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", - "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", - "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", - "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", - "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d", - "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935", - "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", - "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", - "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b", - "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", - "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", - "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", - "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", - "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", - "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", - "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052" + "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", + "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", + "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", + "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", + "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", + "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", + "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", + "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", + "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", + "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", + "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", + "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", + "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", + "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", + "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", + "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", + "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", + "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", + "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", + "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", + "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", + "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", + "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", + "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", + "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", + "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", + "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", + "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", + "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", + "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", + "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", + "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", + "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", + "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", + "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", + "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", + "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", + "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", + "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", + "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", + "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", + "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", + "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", + "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", + "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", + "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", + "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", + "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", + "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", + "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", + "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", + "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", + "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", + "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", + "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", + "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", + "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", + "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", + "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", + "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", + "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", + "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", + "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", + "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", + "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", + "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", + "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", + "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", + "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", + "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", + "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", + "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", + "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", + "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", + "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", + "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", + "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", + "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", + "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", + "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794" ], - "markers": "python_version >= '3.8'", - "version": "==1.4.7" + "markers": "python_version >= '3.10'", + "version": "==1.4.8" }, "loki-logger-handler": { "hashes": [ - "sha256:aa1a9c933282c134a1e4271aba3cbaa2a3660eab6ea415bad7a072444ab98aa8", - "sha256:f6114727a9e5e6f3f2058b9b5324d1cab6d1a04e802079f7b57a8aeb7bd0a112" + "sha256:0198c6ec0cda01e90a569b2ed2e1bb92d8bbfc19c0f9d47014238d9a0fa5df86", + "sha256:4ec0cfecaa8ba724f3d7d429cf5a505d5e53ff6ca2cf4e980e4262b12cb980fb" ], "index": "pypi", "markers": "python_version >= '2.7'", - "version": "==1.0.2" + "version": "==1.1.0" }, "lxml": { "hashes": [ @@ -670,50 +623,43 @@ }, "matplotlib": { "hashes": [ - "sha256:026bdf3137ab6022c866efa4813b6bbeddc2ed4c9e7e02f0e323a7bca380dfa0", - "sha256:031b7f5b8e595cc07def77ec5b58464e9bb67dc5760be5d6f26d9da24892481d", - "sha256:0a0a63cb8404d1d1f94968ef35738900038137dab8af836b6c21bb6f03d75465", - "sha256:0a361bd5583bf0bcc08841df3c10269617ee2a36b99ac39d455a767da908bbbc", - "sha256:10d3e5c7a99bd28afb957e1ae661323b0800d75b419f24d041ed1cc5d844a764", - "sha256:1c40c244221a1adbb1256692b1133c6fb89418df27bf759a31a333e7912a4010", - "sha256:203d18df84f5288973b2d56de63d4678cc748250026ca9e1ad8f8a0fd8a75d83", - "sha256:213d6dc25ce686516208d8a3e91120c6a4fdae4a3e06b8505ced5b716b50cc04", - "sha256:3119b2f16de7f7b9212ba76d8fe6a0e9f90b27a1e04683cd89833a991682f639", - "sha256:3fb0b37c896172899a4a93d9442ffdc6f870165f59e05ce2e07c6fded1c15749", - "sha256:41b016e3be4e740b66c79a031a0a6e145728dbc248142e751e8dab4f3188ca1d", - "sha256:4a8d279f78844aad213c4935c18f8292a9432d51af2d88bca99072c903948045", - "sha256:4e6eefae6effa0c35bbbc18c25ee6e0b1da44d2359c3cd526eb0c9e703cf055d", - "sha256:5f2a4ea08e6876206d511365b0bc234edc813d90b930be72c3011bbd7898796f", - "sha256:66d7b171fecf96940ce069923a08ba3df33ef542de82c2ff4fe8caa8346fa95a", - "sha256:687df7ceff57b8f070d02b4db66f75566370e7ae182a0782b6d3d21b0d6917dc", - "sha256:6be0ba61f6ff2e6b68e4270fb63b6813c9e7dec3d15fc3a93f47480444fd72f0", - "sha256:6e9de2b390d253a508dd497e9b5579f3a851f208763ed67fdca5dc0c3ea6849c", - "sha256:760a5e89ebbb172989e8273024a1024b0f084510b9105261b3b00c15e9c9f006", - "sha256:816a966d5d376bf24c92af8f379e78e67278833e4c7cbc9fa41872eec629a060", - "sha256:87ad73763d93add1b6c1f9fcd33af662fd62ed70e620c52fcb79f3ac427cf3a6", - "sha256:896774766fd6be4571a43bc2fcbcb1dcca0807e53cab4a5bf88c4aa861a08e12", - "sha256:8e0143975fc2a6d7136c97e19c637321288371e8f09cff2564ecd73e865ea0b9", - "sha256:90a85a004fefed9e583597478420bf904bb1a065b0b0ee5b9d8d31b04b0f3f70", - "sha256:9b081dac96ab19c54fd8558fac17c9d2c9cb5cc4656e7ed3261ddc927ba3e2c5", - "sha256:9d6b2e8856dec3a6db1ae51aec85c82223e834b228c1d3228aede87eee2b34f9", - "sha256:9f459c8ee2c086455744723628264e43c884be0c7d7b45d84b8cd981310b4815", - "sha256:9fa6e193c14d6944e0685cdb527cb6b38b0e4a518043e7212f214113af7391da", - "sha256:a42b9dc42de2cfe357efa27d9c50c7833fc5ab9b2eb7252ccd5d5f836a84e1e4", - "sha256:b651b0d3642991259109dc0351fc33ad44c624801367bb8307be9bfc35e427ad", - "sha256:b6c12514329ac0d03128cf1dcceb335f4fbf7c11da98bca68dca8dcb983153a9", - "sha256:c52f48eb75fcc119a4fdb68ba83eb5f71656999420375df7c94cc68e0e14686e", - "sha256:c96eeeb8c68b662c7747f91a385688d4b449687d29b691eff7068a4602fe6dc4", - "sha256:cd1077b9a09b16d8c3c7075a8add5ffbfe6a69156a57e290c800ed4d435bef1d", - "sha256:cd5dbbc8e25cad5f706845c4d100e2c8b34691b412b93717ce38d8ae803bcfa5", - "sha256:cf2a60daf6cecff6828bc608df00dbc794380e7234d2411c0ec612811f01969d", - "sha256:d3c93796b44fa111049b88a24105e947f03c01966b5c0cc782e2ee3887b790a3", - "sha256:d796272408f8567ff7eaa00eb2856b3a00524490e47ad505b0b4ca6bb8a7411f", - "sha256:e0fcb7da73fbf67b5f4bdaa57d85bb585a4e913d4a10f3e15b32baea56a67f0a", - "sha256:e14485bb1b83eeb3d55b6878f9560240981e7bbc7a8d4e1e8c38b9bd6ec8d2de", - "sha256:edd14cf733fdc4f6e6fe3f705af97676a7e52859bf0044aa2c84e55be739241c" + "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6", + "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908", + "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6", + "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2", + "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae", + "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea", + "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede", + "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59", + "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765", + "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12", + "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683", + "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593", + "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1", + "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c", + "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5", + "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a", + "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03", + "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef", + "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff", + "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25", + "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3", + "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06", + "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8", + "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e", + "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95", + "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf", + "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef", + "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278", + "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc", + "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442", + "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997", + "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a", + "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e", + "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363" ], - "markers": "python_version >= '3.9'", - "version": "==3.9.3" + "markers": "python_version >= '3.10'", + "version": "==3.10.0" }, "mdurl": { "hashes": [ @@ -757,65 +703,65 @@ }, "numpy": { "hashes": [ - "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", - "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", - "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", - "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", - "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", - "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", - "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", - "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", - "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", - "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", - "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", - "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", - "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", - "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", - "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", - "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", - "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", - "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", - "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", - "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", - "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", - "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", - "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", - "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", - "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", - "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", - "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", - "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", - "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", - "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", - "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", - "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", - "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", - "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", - "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", - "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", - "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", - "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", - "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", - "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", - "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", - "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", - "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", - "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", - "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", - "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", - "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", - "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", - "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", - "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", - "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", - "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", - "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", - "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", - "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4" + "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2", + "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5", + "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60", + "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71", + "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631", + "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8", + "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2", + "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16", + "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa", + "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591", + "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964", + "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821", + "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484", + "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957", + "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800", + "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918", + "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95", + "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0", + "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e", + "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d", + "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73", + "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59", + "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51", + "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355", + "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348", + "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e", + "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440", + "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675", + "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84", + "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046", + "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab", + "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712", + "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308", + "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315", + "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3", + "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008", + "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5", + "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2", + "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e", + "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7", + "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf", + "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab", + "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd", + "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf", + "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8", + "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb", + "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268", + "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d", + "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780", + "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716", + "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e", + "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528", + "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af", + "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7", + "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==2.1.3" + "version": "==2.2.1" }, "osmpythontools": { "hashes": [ @@ -882,207 +828,212 @@ }, "pillow": { "hashes": [ - "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", - "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", - "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", - "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", - "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", - "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", - "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", - "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", - "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", - "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", - "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", - "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", - "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", - "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", - "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", - "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", - "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", - "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", - "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", - "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", - "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", - "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", - "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", - "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", - "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", - "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", - "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", - "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", - "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", - "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", - "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", - "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", - "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", - "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", - "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", - "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", - "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", - "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", - "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", - "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", - "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", - "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", - "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", - "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", - "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", - "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", - "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", - "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", - "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", - "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", - "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", - "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", - "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", - "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", - "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", - "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", - "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", - "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", - "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", - "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", - "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", - "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", - "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", - "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", - "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", - "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", - "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", - "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", - "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", - "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", - "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", - "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", - "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", - "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", - "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" + "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", + "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", + "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", + "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", + "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", + "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f", + "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", + "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", + "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", + "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", + "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", + "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", + "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2", + "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", + "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884", + "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", + "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", + "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196", + "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", + "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", + "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", + "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", + "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", + "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", + "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", + "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", + "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8", + "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", + "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc", + "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", + "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", + "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", + "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", + "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", + "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", + "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", + "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", + "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", + "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", + "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", + "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", + "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", + "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade", + "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", + "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", + "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", + "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a", + "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", + "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", + "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", + "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", + "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6", + "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", + "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e", + "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", + "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", + "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", + "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", + "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", + "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", + "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", + "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", + "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", + "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", + "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", + "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5", + "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", + "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", + "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", + "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", + "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761" ], "markers": "python_version >= '3.9'", - "version": "==11.0.0" + "version": "==11.1.0" + }, + "pulp": { + "hashes": [ + "sha256:2e30e6c0ef2c0edac185220e3e53faca62eb786a9bd68465208f05bc63e850f3", + "sha256:ad6a9b566d8458f4d05f4bfe2cea59e32885dd1da6929a361be579222107987c" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.9.0" }, "pydantic": { "hashes": [ - "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", - "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e" + "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", + "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.10.2" + "version": "==2.10.5" }, "pydantic-core": { "hashes": [ - "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", - "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", - "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", - "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", - "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", - "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", - "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", - "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", - "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", - "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", - "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", - "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", - "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", - "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", - "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", - "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", - "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", - "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", - "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", - "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", - "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", - "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", - "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", - "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", - "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", - "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", - "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", - "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", - "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", - "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", - "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", - "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", - "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", - "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", - "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", - "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", - "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", - "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", - "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", - "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", - "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", - "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", - "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", - "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", - "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", - "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", - "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", - "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", - "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", - "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", - "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", - "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", - "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", - "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", - "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", - "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", - "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", - "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", - "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", - "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", - "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", - "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", - "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", - "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", - "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", - "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", - "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", - "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", - "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", - "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", - "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", - "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", - "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", - "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", - "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", - "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", - "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", - "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", - "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", - "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", - "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", - "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", - "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", - "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", - "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", - "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", - "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", - "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", - "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", - "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", - "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", - "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", - "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", - "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", - "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", - "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", - "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", - "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", - "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", - "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b" + "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278", + "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", + "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", + "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f", + "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", + "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", + "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54", + "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630", + "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", + "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", + "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", + "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", + "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", + "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", + "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", + "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", + "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", + "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd", + "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", + "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", + "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", + "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", + "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", + "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", + "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", + "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", + "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", + "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", + "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", + "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", + "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", + "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf", + "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", + "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", + "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76", + "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362", + "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", + "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", + "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320", + "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118", + "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96", + "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", + "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046", + "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", + "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", + "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", + "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", + "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67", + "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", + "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", + "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35", + "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", + "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", + "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b", + "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", + "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", + "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", + "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145", + "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", + "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", + "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", + "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", + "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", + "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", + "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5", + "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", + "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", + "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", + "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", + "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da", + "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", + "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", + "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993", + "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656", + "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4", + "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", + "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb", + "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d", + "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", + "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e", + "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", + "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc", + "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a", + "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9", + "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506", + "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b", + "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1", + "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", + "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", + "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", + "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", + "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", + "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", + "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", + "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308", + "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2", + "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228", + "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b", + "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", + "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad" ], "markers": "python_version >= '3.8'", - "version": "==2.27.1" + "version": "==2.27.2" }, "pygments": { "hashes": [ - "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", + "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" ], "markers": "python_version >= '3.8'", - "version": "==2.18.0" + "version": "==2.19.1" }, "pymemcache": { "hashes": [ @@ -1095,62 +1046,67 @@ }, "pyparsing": { "hashes": [ - "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", - "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" + "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", + "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a" ], "markers": "python_version >= '3.9'", - "version": "==3.2.0" + "version": "==3.2.1" }, "pyqt6": { "hashes": [ - "sha256:0adb7914c732ad1dee46d9cec838a98cb2b11bc38cc3b7b36fbd8701ae64bf47", - "sha256:2d771fa0981514cb1ee937633dfa64f14caa902707d9afffab66677f3a73e3da", - "sha256:3672a82ccd3a62e99ab200a13903421e2928e399fda25ced98d140313ad59cb9", - "sha256:7f397f4b38b23b5588eb2c0933510deb953d96b1f0323a916c4839c2a66ccccc", - "sha256:c2f202b7941aa74e5c7e1463a6f27d9131dbc1e6cabe85571d7364f5b3de7397", - "sha256:f053378e3aef6248fa612c8afddda17f942fb63f9fe8a9aeb2a6b6b4cbb0eba9", - "sha256:fa3954698233fe286a8afc477b84d8517f0788eb46b74da69d3ccc0170d3714c" + "sha256:3a4354816f11e812b727206a9ea6e79ff3774f1bb7228ad4b9318442d2c64ff9", + "sha256:452bae5840077bf0f146c798d7777f70d7bdd0c7dcfa9ee7a415c1daf2d10038", + "sha256:48bace7b87676bba5e6114482f3a20ca20be90c7f261b5d340464313f5f2bf5e", + "sha256:6d8628de4c2a050f0b74462e4c9cb97f839bf6ffabbca91711722ffb281570d9", + "sha256:8c5c05f5fdff31a5887dbc29b27615b09df467631238d7b449283809ffca6228", + "sha256:a9913d479f1ffee804bf7f232079baea4fb4b221a8f4890117588917a54ea30d", + "sha256:cf7123caea14e7ecf10bd12cae48e8d9970ef7caf627bc7d7988b0baa209adb3" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==6.7.1" + "markers": "python_version >= '3.9'", + "version": "==6.8.0" }, "pyqt6-qt6": { "hashes": [ - "sha256:36ea0892b8caeb983af3f285f45fb8dfbb93cfd972439f4e01b7efb2868f6230", - "sha256:50c7482bcdcf2bb78af257fb10ed8b582f8daf91d829782393bc50ac5a0a900c", - "sha256:8551732984fb36a5f4f3db51eafc4e8e6caf18617365830285306f2db17a94c2", - "sha256:cb525fdd393332de60887953029276a44de480fce1d785251ae639580f5e7246", - "sha256:f517a93b6b1a814d4aa6587adc312e812ebaf4d70415bb15cfb44268c5ad3f5f" + "sha256:006d786693d0511fbcf184a862edbd339c6ed1bb3bd9de363d73a19ed4b23dff", + "sha256:08065d595f1e6fc2dde9f4450eeff89082f4bad26f600a8e9b9cc5966716bfcf", + "sha256:1eb8460a1fdb38d0b2458c2974c01d471c1e59e4eb19ea63fc447aaba3ad530e", + "sha256:20843cb86bd94942d1cd99e39bf1aeabb875b241a35a8ab273e4bbbfa63776db", + "sha256:9f3790c4ce4dc576e48b8718d55fb8743057e6cbd53a6ca1dd253ffbac9b7287", + "sha256:a8bc2ed4ee5e7c6ff4dd1c7db0b27705d151fee5dc232bbd1bf17618f937f515", + "sha256:d6ca5d2b9d2ec0ee4a814b2175f641a5c4299cb80b45e0f5f8356632663f89b3" ], - "version": "==6.7.3" + "version": "==6.8.1" }, "pyqt6-sip": { "hashes": [ - "sha256:056af69d1d8d28d5968066ec5da908afd82fc0be07b67cf2b84b9f02228416ce", - "sha256:08dd81037a2864982ece2bf9891f3bf4558e247034e112993ea1a3fe239458cb", - "sha256:2559afa68825d08de09d71c42f3b6ad839dcc30f91e7c6d0785e07830d5541a5", - "sha256:2f74cf3d6d9cab5152bd9f49d570b2dfb87553ebb5c4919abfde27f5b9fd69d4", - "sha256:33d9b399fc9c9dc99496266842b0fb2735d924604774e97cf9b555667cc0fc59", - "sha256:6bce6bc5870d9e87efe5338b1ee4a7b9d7d26cdd16a79a5757d80b6f25e71edc", - "sha256:755beb5d271d081e56618fb30342cdd901464f721450495cb7cb0212764da89e", - "sha256:7a0bbc0918eab5b6351735d40cf22cbfa5aa2476b55e0d5fe881aeed7d871c29", - "sha256:7f84c472afdc7d316ff683f63129350d645ef82d9b3fd75a609b08472d1f7291", - "sha256:835ed22eab977f75fd77e60d4ff308a1fa794b1d0c04849311f36d2a080cdf3b", - "sha256:9ea9223c94906efd68148f12ae45b51a21d67e86704225ddc92bce9c54e4d93c", - "sha256:a5c086b7c9c7996ea9b7522646cc24eebbf3591ec9dd38f65c0a3fdb0dbeaac7", - "sha256:b1bf29e95f10a8a00819dac804ca7e5eba5fc1769adcd74c837c11477bf81954", - "sha256:b203b6fbae4a8f2d27f35b7df46200057033d9ecd9134bcf30e3eab66d43572c", - "sha256:beaddc1ec96b342f4e239702f91802706a80cb403166c2da318cec4ad8b790cb", - "sha256:cd81144b0770084e8005d3a121c9382e6f9bc8d0bb320dd618718ffe5090e0e6", - "sha256:cedd554c643e54c4c2e12b5874781a87441a1b405acf3650a4a2e1df42aae231", - "sha256:d8b22a6850917c68ce83fc152a8b606ecb2efaaeed35be53110468885d6cdd9d", - "sha256:dd168667addf01f8a4b0fa7755323e43e4cd12ca4bade558c61f713a5d48ba1a", - "sha256:f57275b5af774529f9838adcfb58869ba3ebdaf805daea113bb0697a96a3f3cb", - "sha256:fbb249b82c53180f1420571ece5dc24fea1188ba435923edd055599dffe7abfb" + "sha256:14f95c6352e3b85dc26bf59cfbf77a470ecbd5fcdcf00af4b648f0e1b9eefb9e", + "sha256:15be741d1ae8c82bb7afe9a61f3cf8c50457f7d61229a1c39c24cd6e8f4d86dc", + "sha256:1d322ded1d1fea339cc6ac65b768e72c69c486eebb7db6ccde061b5786d74cc5", + "sha256:1ec52e962f54137a19208b6e95b6bd9f7a403eb25d7237768a99306cd9db26d1", + "sha256:1fb405615970e85b622b13b4cad140ff1e4182eb8334a0b27a4698e6217b89b0", + "sha256:22d66256b800f552ade51a463510bf905f3cb318aae00ff4288fae4de5d0e600", + "sha256:2ab85aaf155828331399c59ebdd4d3b0358e42c08250e86b43d56d9873df148a", + "sha256:3c269052c770c09b61fce2f2f9ea934a67dfc65f443d59629b4ccc8f89751890", + "sha256:5004514b08b045ad76425cf3618187091a668d972b017677b1b4b193379ef553", + "sha256:552ff8fdc41f5769d3eccc661f022ed496f55f6e0a214c20aaf56e56385d61b6", + "sha256:5643c92424fe62cb0b33378fef3d28c1525f91ada79e8a15bd9a05414a09503d", + "sha256:56ce0afb19cd8a8c63ff93ae506dffb74f844b88adaa6673ebc0dec43af48a76", + "sha256:57b5312ef13c1766bdf69b317041140b184eb24a51e1e23ce8fc5386ba8dffb2", + "sha256:5d7726556d1ca7a7ed78e19ba53285b64a2a8f6ad7ff4cb18a1832efca1a3102", + "sha256:69a879cfc94f4984d180321b76f52923861cd5bf4969aa885eef7591ee932517", + "sha256:6e6c1e2592187934f4e790c0c099d0033e986dcef7bdd3c06e3895ffa995e9fc", + "sha256:8b2ac36d6e04db6099614b9c1178a2f87788c7ffc3826571fb63d36ddb4c401d", + "sha256:8c207528992d59b0801458aa6fcff118e5c099608ef0fc6ff8bccbdc23f29c04", + "sha256:976c7758f668806d4df7a8853f390ac123d5d1f73591ed368bdb8963574ff589", + "sha256:accab6974b2758296400120fdcc9d1f37785b2ea2591f00656e1776f058ded6c", + "sha256:c1942e107b0243ced9e510d507e0f27aeea9d6b13e0a1b7c06fd52a62e0d41f7", + "sha256:c800db3464481e87b1d2b84523b075df1e8fc7856c6f9623dc243f89be1cb604", + "sha256:e996d320744ca8342cad6f9454345330d4f06bce129812d032bda3bad6967c5c", + "sha256:fa27b51ae4c7013b3700cf0ecf46907d1333ae396fc6511311920485cbce094b" ], - "markers": "python_version >= '3.8'", - "version": "==13.8.0" + "markers": "python_version >= '3.9'", + "version": "==13.9.1" }, "python-dateutil": { "hashes": [ @@ -1176,12 +1132,12 @@ }, "pywikibot": { "hashes": [ - "sha256:4f2e3d8c00bf6410418dc78489a13d139354c832df312f78a693a27d0fffa53f", - "sha256:6543ac89a750db37a440373ec1cf4cbcd80b05742482ebeb0ee7e90bcfb590ba" + "sha256:c9ef1d35c7d1400f30b60090919e92d46f9de928803a66e6f0f6db1c7859ca6a", + "sha256:ef7e8b114c16d4ba1f4b5d74e53c5558730615a129406c1af96188936390c660" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==9.5.0" + "version": "==9.6.1" }, "pyyaml": { "hashes": [ @@ -1257,78 +1213,97 @@ "markers": "python_full_version >= '3.8.0'", "version": "==13.9.4" }, + "rich-toolkit": { + "hashes": [ + "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", + "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3" + ], + "markers": "python_version >= '3.8'", + "version": "==0.13.2" + }, "scikit-learn": { "hashes": [ - "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", - "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", - "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", - "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", - "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", - "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", - "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8", - "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", - "sha256:3bed4909ba187aca80580fe2ef370d9180dcf18e621a27c4cf2ef10d279a7efe", - "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1", - "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", - "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8", - "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", - "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9", - "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", - "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", - "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", - "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", - "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", - "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7", - "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", - "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", - "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", - "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", - "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", - "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a" + "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", + "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", + "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", + "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", + "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", + "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", + "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", + "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", + "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", + "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", + "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", + "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", + "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", + "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", + "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", + "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", + "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", + "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", + "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", + "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", + "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", + "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", + "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", + "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", + "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", + "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", + "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", + "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", + "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", + "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.5.2" + "version": "==1.6.1" }, "scipy": { "hashes": [ - "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", - "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", - "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37", - "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", - "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675", - "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d", - "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", - "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", - "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617", - "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", - "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", - "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", - "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", - "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", - "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad", - "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8", - "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0", - "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69", - "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", - "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3", - "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5", - "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", - "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2", - "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389", - "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", - "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", - "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2", - "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3", - "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", - "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", - "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", - "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", - "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2" + "sha256:033a75ddad1463970c96a88063a1df87ccfddd526437136b6ee81ff0312ebdf6", + "sha256:0458839c9f873062db69a03de9a9765ae2e694352c76a16be44f93ea45c28d2b", + "sha256:070d10654f0cb6abd295bc96c12656f948e623ec5f9a4eab0ddb1466c000716e", + "sha256:09c52320c42d7f5c7748b69e9f0389266fd4f82cf34c38485c14ee976cb8cb04", + "sha256:0ac102ce99934b162914b1e4a6b94ca7da0f4058b6d6fd65b0cef330c0f3346f", + "sha256:0fb57b30f0017d4afa5fe5f5b150b8f807618819287c21cbe51130de7ccdaed2", + "sha256:100193bb72fbff37dbd0bf14322314fc7cbe08b7ff3137f11a34d06dc0ee6b85", + "sha256:14eaa373c89eaf553be73c3affb11ec6c37493b7eaaf31cf9ac5dffae700c2e0", + "sha256:2114a08daec64980e4b4cbdf5bee90935af66d750146b1d2feb0d3ac30613692", + "sha256:21e10b1dd56ce92fba3e786007322542361984f8463c6d37f6f25935a5a6ef52", + "sha256:2722a021a7929d21168830790202a75dbb20b468a8133c74a2c0230c72626b6c", + "sha256:395be70220d1189756068b3173853029a013d8c8dd5fd3d1361d505b2aa58fa7", + "sha256:3fe1d95944f9cf6ba77aa28b82dd6bb2a5b52f2026beb39ecf05304b8392864b", + "sha256:491d57fe89927fa1aafbe260f4cfa5ffa20ab9f1435025045a5315006a91b8f5", + "sha256:4b17d4220df99bacb63065c76b0d1126d82bbf00167d1730019d2a30d6ae01ea", + "sha256:4c9d8fc81d6a3b6844235e6fd175ee1d4c060163905a2becce8e74cb0d7554ce", + "sha256:55cc79ce4085c702ac31e49b1e69b27ef41111f22beafb9b49fea67142b696c4", + "sha256:5b190b935e7db569960b48840e5bef71dc513314cc4e79a1b7d14664f57fd4ff", + "sha256:5bd8d27d44e2c13d0c1124e6a556454f52cd3f704742985f6b09e75e163d20d2", + "sha256:5dff14e75cdbcf07cdaa1c7707db6017d130f0af9ac41f6ce443a93318d6c6e0", + "sha256:5eb0ca35d4b08e95da99a9f9c400dc9f6c21c424298a0ba876fdc69c7afacedf", + "sha256:63b9b6cd0333d0eb1a49de6f834e8aeaefe438df8f6372352084535ad095219e", + "sha256:667f950bf8b7c3a23b4199db24cb9bf7512e27e86d0e3813f015b74ec2c6e3df", + "sha256:6b3e71893c6687fc5e29208d518900c24ea372a862854c9888368c0b267387ab", + "sha256:71ba9a76c2390eca6e359be81a3e879614af3a71dfdabb96d1d7ab33da6f2364", + "sha256:74bb864ff7640dea310a1377d8567dc2cb7599c26a79ca852fc184cc851954ac", + "sha256:82add84e8a9fb12af5c2c1a3a3f1cb51849d27a580cb9e6bd66226195142be6e", + "sha256:837299eec3d19b7e042923448d17d95a86e43941104d33f00da7e31a0f715d3c", + "sha256:900f3fa3db87257510f011c292a5779eb627043dd89731b9c461cd16ef76ab3d", + "sha256:9f151e9fb60fbf8e52426132f473221a49362091ce7a5e72f8aa41f8e0da4f25", + "sha256:af0b61c1de46d0565b4b39c6417373304c1d4f5220004058bdad3061c9fa8a95", + "sha256:bc7136626261ac1ed988dca56cfc4ab5180f75e0ee52e58f1e6aa74b5f3eacd5", + "sha256:be3deeb32844c27599347faa077b359584ba96664c5c79d71a354b80a0ad0ce0", + "sha256:c09aa9d90f3500ea4c9b393ee96f96b0ccb27f2f350d09a47f533293c78ea776", + "sha256:c352c1b6d7cac452534517e022f8f7b8d139cd9f27e6fbd9f3cbd0bfd39f5bef", + "sha256:c64ded12dcab08afff9e805a67ff4480f5e69993310e093434b10e85dc9d43e1", + "sha256:cdde8414154054763b42b74fe8ce89d7f3d17a7ac5dd77204f0e142cdc9239e9", + "sha256:ce3a000cd28b4430426db2ca44d96636f701ed12e2b3ca1f2b1dd7abdd84b39a", + "sha256:f735bc41bd1c792c96bc426dece66c8723283695f02df61dcc4d0a707a42fc54", + "sha256:f82fcf4e5b377f819542fbc8541f7b5fbcf1c0017d0df0bc22c781bf60abc4d8" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==1.14.1" + "version": "==1.15.1" }, "shapely": { "hashes": [ @@ -1389,11 +1364,11 @@ }, "six": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "version": "==1.17.0" }, "sniffio": { "hashes": [ @@ -1429,11 +1404,11 @@ }, "typer": { "hashes": [ - "sha256:af58f737f8d0c0c37b9f955a6d39000b9ff97813afcbeef56af5e37cf743b45a", - "sha256:f476233a25770ab3e7b2eebf7c68f3bc702031681a008b20167573a4b7018f09" + "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", + "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a" ], "markers": "python_version >= '3.7'", - "version": "==0.14.0" + "version": "==0.15.1" }, "typing-extensions": { "hashes": [ @@ -1537,22 +1512,22 @@ }, "urllib3": { "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", + "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" ], - "markers": "python_version >= '3.8'", - "version": "==2.2.3" + "markers": "python_version >= '3.9'", + "version": "==2.3.0" }, "uvicorn": { "extras": [ "standard" ], "hashes": [ - "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", - "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175" + "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", + "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9" ], - "markers": "python_version >= '3.8'", - "version": "==0.32.1" + "markers": "python_version >= '3.9'", + "version": "==0.34.0" }, "uvloop": { "hashes": [ @@ -1598,79 +1573,79 @@ }, "watchfiles": { "hashes": [ - "sha256:06d828fe2adc4ac8a64b875ca908b892a3603d596d43e18f7948f3fef5fc671c", - "sha256:074c7618cd6c807dc4eaa0982b4a9d3f8051cd0b72793511848fd64630174b17", - "sha256:09551237645d6bff3972592f2aa5424df9290e7a2e15d63c5f47c48cde585935", - "sha256:0fc3bf0effa2d8075b70badfdd7fb839d7aa9cea650d17886982840d71fdeabf", - "sha256:12ab123135b2f42517f04e720526d41448667ae8249e651385afb5cda31fedc0", - "sha256:13a4f9ee0cd25682679eea5c14fc629e2eaa79aab74d963bc4e21f43b8ea1877", - "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f", - "sha256:1e176b6b4119b3f369b2b4e003d53a226295ee862c0962e3afd5a1c15680b4e3", - "sha256:1ee5edc939f53466b329bbf2e58333a5461e6c7b50c980fa6117439e2c18b42d", - "sha256:1f73c2147a453315d672c1ad907abe6d40324e34a185b51e15624bc793f93cc6", - "sha256:1ff236d7a3f4b0a42f699a22fc374ba526bc55048a70cbb299661158e1bb5e1f", - "sha256:245fab124b9faf58430da547512d91734858df13f2ddd48ecfa5e493455ffccb", - "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041", - "sha256:28fb64b5843d94e2c2483f7b024a1280662a44409bedee8f2f51439767e2d107", - "sha256:29cf884ad4285d23453c702ed03d689f9c0e865e3c85d20846d800d4787de00f", - "sha256:2a825ba4b32c214e3855b536eb1a1f7b006511d8e64b8215aac06eb680642d84", - "sha256:2ac778a460ea22d63c7e6fb0bc0f5b16780ff0b128f7f06e57aaec63bd339285", - "sha256:2c2696611182c85eb0e755b62b456f48debff484b7306b56f05478b843ca8ece", - "sha256:2d9c0518fabf4a3f373b0a94bb9e4ea7a1df18dec45e26a4d182aa8918dee855", - "sha256:2de52b499e1ab037f1a87cb8ebcb04a819bf087b1015a4cf6dcf8af3c2a2613e", - "sha256:37566c844c9ce3b5deb964fe1a23378e575e74b114618d211fbda8f59d7b5dab", - "sha256:3d94fd83ed54266d789f287472269c0def9120a2022674990bd24ad989ebd7a0", - "sha256:48051d1c504448b2fcda71c5e6e3610ae45de6a0b8f5a43b961f250be4bdf5a8", - "sha256:487d15927f1b0bd24e7df921913399bb1ab94424c386bea8b267754d698f8f0e", - "sha256:4a3b33c3aefe9067ebd87846806cd5fc0b017ab70d628aaff077ab9abf4d06b3", - "sha256:4ff9c7e84e8b644a8f985c42bcc81457240316f900fc72769aaedec9d088055a", - "sha256:533a7cbfe700e09780bb31c06189e39c65f06c7f447326fee707fd02f9a6e945", - "sha256:53ae447f06f8f29f5ab40140f19abdab822387a7c426a369eb42184b021e97eb", - "sha256:550109001920a993a4383b57229c717fa73627d2a4e8fcb7ed33c7f1cddb0c85", - "sha256:5bbd0311588c2de7f9ea5cf3922ccacfd0ec0c1922870a2be503cc7df1ca8be7", - "sha256:5dccfc70480087567720e4e36ec381bba1ed68d7e5f368fe40c93b3b1eba0105", - "sha256:5f75cd42e7e2254117cf37ff0e68c5b3f36c14543756b2da621408349bd9ca7c", - "sha256:648e2b6db53eca6ef31245805cd528a16f56fa4cc15aeec97795eaf713c11435", - "sha256:774ef36b16b7198669ce655d4f75b4c3d370e7f1cbdfb997fb10ee98717e2058", - "sha256:8a2127cd68950787ee36753e6d401c8ea368f73beaeb8e54df5516a06d1ecd82", - "sha256:90004553be36427c3d06ec75b804233f8f816374165d5225b93abd94ba6e7234", - "sha256:905f69aad276639eff3893759a07d44ea99560e67a1cf46ff389cd62f88872a2", - "sha256:9122b8fdadc5b341315d255ab51d04893f417df4e6c1743b0aac8bf34e96e025", - "sha256:9272fdbc0e9870dac3b505bce1466d386b4d8d6d2bacf405e603108d50446940", - "sha256:936f362e7ff28311b16f0b97ec51e8f2cc451763a3264640c6ed40fb252d1ee4", - "sha256:947ccba18a38b85c366dafeac8df2f6176342d5992ca240a9d62588b214d731f", - "sha256:95dc785bc284552d044e561b8f4fe26d01ab5ca40d35852a6572d542adfeb4bc", - "sha256:95de85c254f7fe8cbdf104731f7f87f7f73ae229493bebca3722583160e6b152", - "sha256:9b4fb98100267e6a5ebaff6aaa5d20aea20240584647470be39fe4823012ac96", - "sha256:9c01446626574561756067f00b37e6b09c8622b0fc1e9fdbc7cbcea328d4e514", - "sha256:9c9a8d8fd97defe935ef8dd53d562e68942ad65067cd1c54d6ed8a088b1d931d", - "sha256:9e1d9284cc84de7855fcf83472e51d32daf6f6cecd094160192628bc3fee1b78", - "sha256:a0abf173975eb9dd17bb14c191ee79999e650997cc644562f91df06060610e62", - "sha256:a2218e78e2c6c07b1634a550095ac2a429026b2d5cbcd49a594f893f2bb8c936", - "sha256:a5a7a06cfc65e34fd0a765a7623c5ba14707a0870703888e51d3d67107589817", - "sha256:b2bca898c1dc073912d3db7fa6926cc08be9575add9e84872de2c99c688bac4e", - "sha256:b46e15c34d4e401e976d6949ad3a74d244600d5c4b88c827a3fdf18691a46359", - "sha256:b551c465a59596f3d08170bd7e1c532c7260dd90ed8135778038e13c5d48aa81", - "sha256:b555a93c15bd2c71081922be746291d776d47521a00703163e5fbe6d2a402399", - "sha256:bc338ce9f8846543d428260fa0f9a716626963148edc937d71055d01d81e1525", - "sha256:bedf84835069f51c7b026b3ca04e2e747ea8ed0a77c72006172c72d28c9f69fc", - "sha256:c3d258d78341d5d54c0c804a5b7faa66cd30ba50b2756a7161db07ce15363b8d", - "sha256:c83a6d33a9eda0af6a7470240d1af487807adc269704fe76a4972dd982d16236", - "sha256:c9a13ac46b545a7d0d50f7641eefe47d1597e7d1783a5d89e09d080e6dff44b0", - "sha256:cf517701a4a872417f4e02a136e929537743461f9ec6cdb8184d9a04f4843545", - "sha256:d2b39aa8edd9e5f56f99a2a2740a251dc58515398e9ed5a4b3e5ff2827060755", - "sha256:d3572d4c34c4e9c33d25b3da47d9570d5122f8433b9ac6519dca49c2740d23cd", - "sha256:d562a6114ddafb09c33246c6ace7effa71ca4b6a2324a47f4b09b6445ea78941", - "sha256:e1ed613ee107269f66c2df631ec0fc8efddacface85314d392a4131abe299f00", - "sha256:e3750434c83b61abb3163b49c64b04180b85b4dabb29a294513faec57f2ffdb7", - "sha256:eba98901a2eab909dbd79681190b9049acc650f6111fde1845484a4450761e98", - "sha256:f159ac795785cde4899e0afa539f4c723fb5dd336ce5605bc909d34edd00b79b", - "sha256:f8c4f3a1210ed099a99e6a710df4ff2f8069411059ffe30fa5f9467ebed1256b", - "sha256:fa13d604fcb9417ae5f2e3de676e66aa97427d888e83662ad205bed35a313176", - "sha256:fbd0ab7a9943bbddb87cbc2bf2f09317e74c77dc55b1f5657f81d04666c25269", - "sha256:ffd98a299b0a74d1b704ef0ed959efb753e656a4e0425c14e46ae4c3cbdd2919" + "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27", + "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", + "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", + "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", + "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", + "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a", + "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", + "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", + "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", + "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", + "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", + "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", + "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", + "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", + "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", + "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", + "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", + "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", + "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", + "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", + "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", + "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8", + "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff", + "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", + "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", + "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", + "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", + "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", + "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42", + "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", + "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", + "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3", + "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", + "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0", + "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", + "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", + "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", + "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", + "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", + "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", + "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb", + "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a", + "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e", + "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", + "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", + "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3", + "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", + "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", + "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43", + "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", + "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b", + "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", + "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", + "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", + "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", + "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", + "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", + "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", + "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", + "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", + "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf", + "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", + "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21", + "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", + "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", + "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a", + "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", + "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", + "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", + "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", + "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317" ], - "version": "==1.0.0" + "version": "==1.0.4" }, "websockets": { "hashes": [ @@ -1748,37 +1723,37 @@ }, "xarray": { "hashes": [ - "sha256:1ccace44573ddb862e210ad3ec204210654d2c750bec11bbe7d842dfc298591f", - "sha256:6ee94f63ddcbdd0cf3909d1177f78cdac756640279c0e32ae36819a89cdaba37" + "sha256:1a3011d00ca92a94ba31b297c2eccd310b87a7dacf5acc8d0468385d4a834342", + "sha256:8a69d17c1e4ad09664fd0bc2dbb398e7368eda25bd19456fb919a6eb6490fb72" ], "markers": "python_version >= '3.10'", - "version": "==2024.11.0" + "version": "==2025.1.1" } }, "develop": { "anyio": { "hashes": [ - "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", - "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" + "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", + "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a" ], "markers": "python_version >= '3.9'", - "version": "==4.6.2.post1" + "version": "==4.8.0" }, "astroid": { "hashes": [ - "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d", - "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8" + "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c", + "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b" ], "markers": "python_full_version >= '3.9.0'", - "version": "==3.3.5" + "version": "==3.3.8" }, "certifi": { "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", + "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" ], "markers": "python_version >= '3.6'", - "version": "==2024.8.30" + "version": "==2024.12.14" }, "dill": { "hashes": [ @@ -1816,12 +1791,12 @@ }, "httpx": { "hashes": [ - "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", - "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc" + "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", + "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.28.0" + "version": "==0.28.1" }, "idna": { "hashes": [ @@ -1849,11 +1824,11 @@ }, "jinja2": { "hashes": [ - "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", - "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", + "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], "markers": "python_version >= '3.7'", - "version": "==3.1.4" + "version": "==3.1.5" }, "markupsafe": { "hashes": [ @@ -1956,12 +1931,12 @@ }, "pylint": { "hashes": [ - "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9", - "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e" + "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a", + "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183" ], "index": "pypi", "markers": "python_full_version >= '3.9.0'", - "version": "==3.3.1" + "version": "==3.3.3" }, "pytest": { "hashes": [ diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 92b7be6..5d4e4a6 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -11,7 +11,7 @@ def client(): """Client used to call the app.""" return TestClient(app) - +''' def test_turckheim(client, request): # pylint: disable=redefined-outer-name """ Test n°1 : Custom test in Turckheim to ensure small villages are also supported. @@ -21,7 +21,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name request: """ start_time = time.time() # Start timer - duration_minutes = 15 + duration_minutes = 20 response = client.post( "/trip/new", @@ -45,8 +45,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - # for elem in landmarks : - # print(elem) + for elem in landmarks : + print(elem) # checks : assert response.status_code == 200 # check for successful planning @@ -56,6 +56,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds" assert 2==3 ''' + + def test_bellecour(client, request) : # pylint: disable=redefined-outer-name """ Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area. @@ -87,16 +89,16 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - # for elem in landmarks : - # print(elem) + for elem in landmarks : + print(elem) # checks : assert response.status_code == 200 # check for successful planning 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 - + assert 2 == 3 +''' def test_Paris(client, request) : # pylint: disable=redefined-outer-name """ Test n°2 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area. diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 858d350..c77b30b 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -1,6 +1,6 @@ import yaml, logging import numpy as np - +import pulp as pl from scipy.optimize import linprog from collections import defaultdict, deque @@ -9,6 +9,9 @@ from .get_time_separation import get_time from ..constants import OPTIMIZER_PARAMETERS_PATH +logging.getLogger('pulp').setLevel(level=logging.CRITICAL) + + class Optimizer: logger = logging.getLogger(__name__) @@ -62,7 +65,7 @@ class Optimizer: b_ub[0] = round(max_time*self.overshoot) for i, spot1 in enumerate(landmarks) : - c[i] = -spot1.attractiveness + c[i] = spot1.attractiveness for j in range(i+1, L) : if i !=j : t = get_time(spot1.location, landmarks[j].location) @@ -102,8 +105,6 @@ class Optimizer: 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 - # A = np.zeros((L-1, L*L), dtype=np.int8) - # b = [] # Fill-in row 2 until row L-2 for i in range(1, L-1): A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16) @@ -129,12 +130,10 @@ class Optimizer: tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. """ - # b = [] upper_ind = np.triu_indices(L,0,L) up_ind_x = upper_ind[0] up_ind_y = upper_ind[1] - # A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8) # Fill-in rows L to 2*L-1 incr = 0 for i in range(int((L*L+L)/2)) : @@ -144,14 +143,12 @@ class Optimizer: b[L+incr] = 1 incr += 1 - # return A[~np.all(A == 0, axis=1)], b - def init_eq_not_stay(self, landmarks: list): """ Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.). -> Adds 1 row of constraints - -> Pre-allocates A_eq for the rest of the computations with (L+2 + dynamic incr) rows + -> Pre-allocates A_eq for the rest of the computations with (L+ 2 + dynamic incr) rows Args: L (int): Number of landmarks. @@ -168,7 +165,7 @@ class Optimizer: 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) - + # Set diagonal elements to 1 (to prevent staying in the same position) np.fill_diagonal(l, 1) @@ -220,7 +217,6 @@ class Optimizer: 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) : @@ -291,7 +287,7 @@ class Optimizer: if i in vertices_visited : h[i*L:i*L+L] = ones - return h, np.array([len(vertices_visited)-1]) + return h, len(vertices_visited)-1 # Prevents the creation of the same circle (both directions) @@ -320,7 +316,7 @@ class Optimizer: l[0, g*L + s] = 1 l[1, s*L + g] = 1 - return l, np.zeros(2, dtype=np.int8) + return l, [0, 0] def is_connected(self, resx) : @@ -335,13 +331,15 @@ class Optimizer: """ resx = np.round(resx).astype(np.int8) # round all elements and cast them to int + print(f"resx = ") + # for r in resx : print(r) 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] # removed .tolist() + ind_a = nonzero_tup[0] ind_b = nonzero_tup[1] # Extract all journeys @@ -402,7 +400,7 @@ class Optimizer: stack.append(neighbor) return journey_nodes - + def get_order(self, resx): """ @@ -414,10 +412,8 @@ class Optimizer: Returns: list[int]: A list containing the visit order. """ + resx = np.round(resx).astype(np.uint8) # must contain only 0 and 1 - # 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. @@ -430,14 +426,14 @@ class Optimizer: 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 @@ -454,7 +450,6 @@ class Optimizer: return order - 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. @@ -466,7 +461,6 @@ class Optimizer: Returns: list[Landmark]]: The updated linked list of landmarks with travel times """ - L = [] j = 0 while j < len(order)-1 : @@ -485,11 +479,10 @@ class Optimizer: 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, @@ -527,65 +520,75 @@ class Optimizer: self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") - print(A_eq) - print('\n\n') - print(b_eq) - print('\n\n') - - - # A, b = self.respect_user_must_do(landmarks) # Check if there are user_defined must_see. Also takes care of start/goal - # if len(b) > 0 : - # 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 - # 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 - # 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) + # Solve linear programming problem with PulP + prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) + x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] + + # Add the objective function + prob += pl.lpSum([c[i] * x[i] for i in range(L*L)]) + + # Add Inequality Constraints (A_ub @ x <= b_ub) + for i in range(len(b_ub)): # Iterate over rows of A_ub + prob += (pl.lpSum([A_ub[i][j] * x[j] for j in range(L*L)]) <= b_ub[i]) + + # 5. Add Equality Constraints (A_eq @ x == b_eq) + for i in range(len(b_eq)): # Iterate over rows of A_eq + prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) + + # 6. Solve the problem + prob.solve(pl.PULP_CBC_CMD(msg=True)) + + # 7. Extract Results + status = pl.LpStatus[prob.status] + solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) + + print(status) 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 - if not res.success : + if status != 'Optimal' : 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. Please try again with more time or different preferences.") # If there is a solution, we're good to go, just check for connectiveness - circles = self.is_connected(res.x) - #nodes, edges = is_connected(res.x) + circles = self.is_connected(solution) + i = 0 timeout = 80 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_ub = np.vstack((A_ub, A)) - b_ub = np.concatenate((b_ub, b)) - + print("ok1") + A, b = self.prevent_config(solution) + print("ok2") + print(f"A: {A}") + print(f"b: {b}") + try : + prob += pl.lpSum([A[j] * x[j // L][j % L] for j in range(L * L)]) == b + except Exception as exc : + self.logger.error(f'Unexpected error occured', details=exc) + raise Exception from exc + + print("ok3") for circle in circles : A, b = self.prevent_circle(circle, L) - A_eq = np.vstack((A_eq, A)) - b_eq = np.concatenate((b_eq, b)) + prob += (pl.lpSum([A[0][j] * x[j // L][j % L] for j in range(L*L)]) == b[0]) + prob += (pl.lpSum([A[1][j] * x[j // L][j % L] for j in range(L*L)]) == b[1]) + print("ok4") + prob.solve(pl.PULP_CBC_CMD(msg=True)) - 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) + status = pl.LpStatus[prob.status] + solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) - if not res.success : + if status != 'Optimal' : self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.') raise ArithmeticError("Solving failed because of overconstrained problem") - circles = self.is_connected(res.x) - #nodes, edges = is_connected(res.x) + + circles = self.is_connected(solution) if circles is None : break @@ -594,8 +597,8 @@ class Optimizer: raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.") # Sort the landmarks in the order of the solution - order = self.get_order(res.x) + order = self.get_order(solution) 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(pl.value(prob.objective))}") return tour From 814da4b5f6d311e3b875dff57b02d455c844c988 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 17:11:29 +0100 Subject: [PATCH 13/32] working pulp --- backend/src/tests/test_main.py | 1 + backend/src/utils/optimizer.py | 47 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 5d4e4a6..c95b6f8 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -81,6 +81,7 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name } ) result = response.json() + print(result) landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) # Get computation time diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index c77b30b..7218971 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -2,6 +2,7 @@ import yaml, logging import numpy as np import pulp as pl from scipy.optimize import linprog +from scipy.sparse import lil_matrix, csr_matrix from collections import defaultdict, deque from ..structs.landmark import Landmark @@ -69,7 +70,7 @@ class Optimizer: for j in range(i+1, L) : if i !=j : t = get_time(spot1.location, landmarks[j].location) - A_ub[0, i*L + j] = t + spot1.duration + A_ub[0, i*L + j] = t + spot1.duration A_ub[0, j*L + i] = t + landmarks[j].duration # Expand 'c' to L*L for every decision variable @@ -255,7 +256,7 @@ class Optimizer: incr += 1 - # Prevent the use of a particular solution + # Prevent the use of a particular solution. TODO probably can be done faster just using resx def prevent_config(self, resx): """ Prevent the use of a particular solution by adding constraints to the optimization. @@ -330,9 +331,7 @@ class Optimizer: 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 - - print(f"resx = ") - # for r in resx : print(r) + 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. @@ -509,14 +508,21 @@ class Optimizer: # SET CONSTRAINTS FOR INEQUALITY c, A_ub, b_ub = self.init_ub_time(landmarks, max_time) # Adds the distances from each landmark to the other. + print("ok1") self.respect_number(A_ub, b_ub, L, max_landmarks) # Respects max number of visits (no more possible stops than landmarks). + print("ok2") self.break_sym(A_ub, b_ub, L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. # SET CONSTRAINTS FOR EQUALITY + print("ok3") A_eq, b_eq = self.init_eq_not_stay(landmarks) # Force solution not to stay in same place + print("ok4") self.respect_start_finish(A_eq, b_eq, L) # Force start and finish positions + print("ok5") self.respect_order(A_eq, b_eq, L) # Respect order of visit (only works when max_time is limiting factor) + print("ok6") self.respect_user_must(A_eq, b_eq, landmarks) # Force to do/avoid landmarks set by user. + print("ok7") self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") @@ -539,7 +545,7 @@ class Optimizer: prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) # 6. Solve the problem - prob.solve(pl.PULP_CBC_CMD(msg=True)) + prob.solve(pl.PULP_CBC_CMD(msg=False)) # 7. Extract Results status = pl.LpStatus[prob.status] @@ -559,32 +565,25 @@ class Optimizer: i = 0 timeout = 80 - while circles is not None and i < timeout: + while circles is not None : i += 1 # print(f"Iteration {i} of fixing circles") - print("ok1") - A, b = self.prevent_config(solution) - print("ok2") - print(f"A: {A}") - print(f"b: {b}") - try : - prob += pl.lpSum([A[j] * x[j // L][j % L] for j in range(L * L)]) == b - except Exception as exc : - self.logger.error(f'Unexpected error occured', details=exc) - raise Exception from exc - - print("ok3") + # l, b = self.prevent_config(solution) + # prob += (pl.lpSum([l[j] * x[j] for j in range(L*L)]) == b) + for circle in circles : A, b = self.prevent_circle(circle, L) - prob += (pl.lpSum([A[0][j] * x[j // L][j % L] for j in range(L*L)]) == b[0]) - prob += (pl.lpSum([A[1][j] * x[j // L][j % L] for j in range(L*L)]) == b[1]) - print("ok4") - prob.solve(pl.PULP_CBC_CMD(msg=True)) + prob += (pl.lpSum([A[0][j] * x[j] for j in range(L*L)]) == b[0]) + prob += (pl.lpSum([A[1][j] * x[j] for j in range(L*L)]) == b[1]) + prob.solve(pl.PULP_CBC_CMD(msg=False)) status = pl.LpStatus[prob.status] solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) if status != 'Optimal' : + self.logger.error("The problem is overconstrained, no solution after {i} cycles.") + raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.") + if i == timeout : self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.') raise ArithmeticError("Solving failed because of overconstrained problem") @@ -600,5 +599,5 @@ class Optimizer: order = self.get_order(solution) tour = [landmarks[i] for i in order] - self.logger.debug(f"Re-optimized {i} times, score: {int(pl.value(prob.objective))}") + self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(prob.objective))}") return tour From 3ebe0b7191146d536c5a80c903d962e88ab1f61c Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 19:55:48 +0100 Subject: [PATCH 14/32] good starting point, working pulp --- backend/src/tests/test_main.py | 28 ++++++++++++++-------------- backend/src/utils/optimizer.py | 12 +----------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index c95b6f8..bff5b00 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -11,7 +11,7 @@ def client(): """Client used to call the app.""" return TestClient(app) -''' + def test_turckheim(client, request): # pylint: disable=redefined-outer-name """ Test n°1 : Custom test in Turckheim to ensure small villages are also supported. @@ -45,8 +45,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - for elem in landmarks : - print(elem) + # for elem in landmarks : + # print(elem) # checks : assert response.status_code == 200 # check for successful planning @@ -54,8 +54,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 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 @@ -97,9 +97,9 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name assert response.status_code == 200 # check for successful planning 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 + # assert 2 == 3 + -''' def test_Paris(client, request) : # pylint: disable=redefined-outer-name """ Test n°2 : Custom test in Paris (les Halles) centre to ensure proper decision making in crowded area. @@ -131,8 +131,8 @@ def test_Paris(client, request) : # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - # for elem in landmarks : - # print(elem) + for elem in landmarks : + print(elem) # checks : assert response.status_code == 200 # check for successful planning @@ -171,8 +171,8 @@ def test_New_York(client, request) : # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - # for elem in landmarks : - # print(elem) + for elem in landmarks : + print(elem) # checks : assert response.status_code == 200 # check for successful planning @@ -211,14 +211,14 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - # for elem in landmarks : - # print(elem) + for elem in landmarks : + print(elem) # checks : assert response.status_code == 200 # check for successful planning 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): # response = client.post( diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 7218971..210559f 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -2,7 +2,6 @@ import yaml, logging import numpy as np import pulp as pl from scipy.optimize import linprog -from scipy.sparse import lil_matrix, csr_matrix from collections import defaultdict, deque from ..structs.landmark import Landmark @@ -508,21 +507,14 @@ class Optimizer: # SET CONSTRAINTS FOR INEQUALITY c, A_ub, b_ub = self.init_ub_time(landmarks, max_time) # Adds the distances from each landmark to the other. - print("ok1") self.respect_number(A_ub, b_ub, L, max_landmarks) # Respects max number of visits (no more possible stops than landmarks). - print("ok2") self.break_sym(A_ub, b_ub, L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. # SET CONSTRAINTS FOR EQUALITY - print("ok3") A_eq, b_eq = self.init_eq_not_stay(landmarks) # Force solution not to stay in same place - print("ok4") self.respect_start_finish(A_eq, b_eq, L) # Force start and finish positions - print("ok5") self.respect_order(A_eq, b_eq, L) # Respect order of visit (only works when max_time is limiting factor) - print("ok6") self.respect_user_must(A_eq, b_eq, landmarks) # Force to do/avoid landmarks set by user. - print("ok7") self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") @@ -545,14 +537,12 @@ class Optimizer: prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) # 6. Solve the problem - prob.solve(pl.PULP_CBC_CMD(msg=False)) + prob.solve(pl.PULP_CBC_CMD(msg=False, gapRel=0.3)) # 7. Extract Results status = pl.LpStatus[prob.status] solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) - print(status) - 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 From 2be7cd1e619f347ebd50dd0849af5ac781ebeee9 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Wed, 15 Jan 2025 21:12:47 +0100 Subject: [PATCH 15/32] some rpoblem --- backend/src/tests/test_main.py | 12 +- backend/src/utils/optimizer.py | 210 +++++++++++++++------------------ 2 files changed, 99 insertions(+), 123 deletions(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index bff5b00..710c21b 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -21,7 +21,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name request: """ start_time = time.time() # Start timer - duration_minutes = 20 + duration_minutes = 15 response = client.post( "/trip/new", @@ -45,8 +45,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - # for elem in landmarks : - # print(elem) + for elem in landmarks : + print(elem) # checks : assert response.status_code == 200 # check for successful planning @@ -54,9 +54,9 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 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 """ @@ -219,7 +219,7 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name 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): # response = client.post( # "/trip/new", diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 210559f..31bf343 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -21,7 +21,8 @@ class Optimizer: 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 - + prob: pl.LpProblem # linear optimization problem to solve + x: list[pl.LpVariable] # decision variables def __init__(self) : @@ -32,9 +33,12 @@ class Optimizer: self.average_walking_speed = parameters['average_walking_speed'] self.max_landmarks = parameters['max_landmarks'] self.overshoot = parameters['overshoot'] + + # Initalize the optimization problem + self.prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) - def init_ub_time(self, landmarks: list[Landmark], max_time: int): + def init_ub_time(self, L: int, landmarks: list[Landmark], max_time: int): """ Initialize the objective function coefficients and inequality constraints. -> Adds 1 row of constraints @@ -57,23 +61,20 @@ class Optimizer: # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... c = np.zeros(L, dtype=np.int16) - # Coefficients of inequality constraints (left-hand side) - A_ub = np.zeros((L + int((L*L-L)/2), L*L), dtype=np.int16) - b_ub = np.zeros(L + int((L*L-L)/2), dtype=np.int16) - - # Fill in first row - b_ub[0] = round(max_time*self.overshoot) + # inequality matrix and vector + A_ub = np.zeros(L*L, dtype=np.int16) + b_ub = round(max_time*self.overshoot) for i, spot1 in enumerate(landmarks) : c[i] = spot1.attractiveness for j in range(i+1, L) : if i !=j : t = get_time(spot1.location, landmarks[j].location) - A_ub[0, i*L + j] = t + spot1.duration - A_ub[0, j*L + i] = t + landmarks[j].duration + A_ub[i*L + j] = t + spot1.duration + A_ub[j*L + i] = t + landmarks[j].duration - # Expand 'c' to L*L for every decision variable - c = np.tile(c, L) + # Expand 'c' to L*L for every decision variable and ad + c = np.tile(c, L) # Now sort and modify A_ub for each row if L > 22 : @@ -90,10 +91,12 @@ class Optimizer: row_values[mask] = 32765 A_ub[0, i*L:i*L+L] = row_values - return c, A_ub, b_ub + # Add the objective and the distance constraint + self.prob += pl.lpSum([c[j] * self.x[j] for j in range(L*L)]) + self.prob += (pl.lpSum([A_ub[j] * self.x[j] for j in range(L*L)]) <= b_ub) - def respect_number(self, A, b, L, max_landmarks: int): + def respect_number(self, L: int, max_landmarks: int): """ Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks. -> Adds L-1 rows of constraints @@ -105,18 +108,16 @@ class Optimizer: 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 - # Fill-in row 2 until row L-2 - for i in range(1, L-1): - A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16) - b[i] = 1 + A_ub = np.zeros(L*L, dtype=np.int8) + for i in range(0, L-2): + A_ub[L*i:L*(i+1)] = np.ones(L, dtype=np.int16) + self.prob += (pl.lpSum([A_ub[j] * self.x[j] for j in range(L*L)]) <= 1) - # Fill-in row L-1 # Second constraint: cap the total number of visits - A[L-1, :] = np.ones(L*L, dtype=np.int16) - b[L-1] = max_landmarks+2 + self.prob += (pl.lpSum([1 * self.x[j] for j in range(L*L)]) <= max_landmarks+2) - def break_sym(self, A, b, L): + def break_sym(self, L: int): """ Generate constraints to prevent simultaneous travel between two landmarks in both directions. Constraint to not have d14 and d41 simultaneously. @@ -133,18 +134,17 @@ class Optimizer: upper_ind = np.triu_indices(L,0,L) up_ind_x = upper_ind[0] up_ind_y = upper_ind[1] + A = np.zeros(L*L, dtype=np.int8) # Fill-in rows L to 2*L-1 - incr = 0 for i in range(int((L*L+L)/2)) : if up_ind_x[i] != up_ind_y[i] : - A[L+incr, up_ind_x[i]*L + up_ind_y[i]] = 1 - A[L+incr, up_ind_y[i]*L + up_ind_x[i]] = 1 - b[L+incr] = 1 - incr += 1 + A[up_ind_x[i]*L + up_ind_y[i]] = 1 + A[up_ind_y[i]*L + up_ind_x[i]] = 1 + self.prob += (pl.lpSum([A[j] * self.x[j] for j in range(L*L)]) <= 1) - def init_eq_not_stay(self, landmarks: list): + def init_eq_not_stay(self, L: int): """ Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.). -> Adds 1 row of constraints @@ -156,28 +156,17 @@ class Optimizer: Returns: 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) + A_eq = np.zeros((L, L), dtype=np.int8) # Set diagonal elements to 1 (to prevent staying in the same position) - np.fill_diagonal(l, 1) + np.fill_diagonal(A_eq, 1) + A_eq = A_eq.flatten() - # Fill-in first row - A_eq[0,:] = l.flatten() - b_eq[0] = 0 - - return A_eq, b_eq + self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 1) # Constraint to ensure start at start and finish at goal - def respect_start_finish(self, A, b, L: int): + 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. @@ -189,22 +178,24 @@ class Optimizer: 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) + # Fill-in row 0. + A_eq = np.zeros((3,L*L), dtype=np.int8) + A_eq[0, :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 + # Fill-in row 1 + A_eq[1, k*L+L-1] = 1 # sets arrivals only for finish (vertical ones) + # Fill-in row 1 + A_eq[2, 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] + A_eq[2, L*(L-1):] = np.ones(L, dtype=np.int8) # prevents arrivals at start and departures from goal + b_eq= [1, 1, 0] - # return A, b + # Add the constraints to pulp + for i in range(3) : + self.prob += (pl.lpSum([A_eq[i][j] * self.x[j] for j in range(L*L)]) == b_eq[i]) - - def respect_order(self, A, b, L: int): + 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. @@ -216,17 +207,17 @@ class Optimizer: Returns: tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. """ + A_eq = np.zeros(L*L, dtype=np.int8) 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) + A_eq[i + j*L] = -1 + A_eq[i*L:(i+1)*L] = ones + self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 0) - def respect_user_must(self, A, b, landmarks: list[Landmark]) : + def respect_user_must(self, L: int, landmarks: list[Landmark]) : """ 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 @@ -238,21 +229,16 @@ class Optimizer: Returns: tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. """ - L = len(landmarks) ones = np.ones(L, dtype=np.int8) - incr = 0 + A_eq = np.zeros(L*L, dtype=np.int8) for i, elem in enumerate(landmarks) : if elem.must_do is True and i not in [0, L-1]: - # First part of the dynamic infill - A[L+2+incr, i*L:i*L+L] = ones - b[L+2+incr] = 1 - incr += 1 + A_eq[i*L:i*L+L] = ones + self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 1) if elem.must_avoid is True and i not in [0, L-1]: - # Second part of the dynamic infill - A[L+2+incr, i*L:i*L+L] = ones - b[L+2+incr] = 0 - incr += 1 + A_eq[i*L:i*L+L] = ones + self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 2) # Prevent the use of a particular solution. TODO probably can be done faster just using resx @@ -316,9 +302,11 @@ class Optimizer: l[0, g*L + s] = 1 l[1, s*L + g] = 1 - return l, [0, 0] - + # Add the constraints + self.prob += (pl.lpSum([l[0][j] * self.x[j] for j in range(L*L)]) == 0) + self.prob += (pl.lpSum([l[1][j] * self.x[j] for j in range(L*L)]) == 0) + def is_connected(self, resx) : """ Determine the order of visits and detect any circular paths in the given configuration. @@ -481,6 +469,27 @@ class Optimizer: return L + def pre_processing(self, L: int, landmarks: list[Landmark], max_time: int, max_landmarks: int | None) : + + if max_landmarks is None : + max_landmarks = self.max_landmarks + + # Define the problem + x_bounds = [(0, 1)]*L*L + self.x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] + + # Setup the inequality constraints + self.init_ub_time(L, landmarks, max_time) # Adds the distances from each landmark to the other. + self.respect_number(L, max_landmarks) # Respects max number of visits (no more possible stops than landmarks). + self.break_sym(L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. + + # Setup the equality constraints + self.init_eq_not_stay(L) # Force solution not to stay in same place + self.respect_start_finish(L) # Force start and finish positions + self.respect_order(L) # Respect order of visit (only works when max_time is limiting factor) + self.respect_user_must(L, landmarks) # Force to do/avoid landmarks set by user. + + def solve_optimization( self, max_time: int, @@ -500,48 +509,16 @@ class Optimizer: 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 - + # 1. Setup the optimization proplem. L = len(landmarks) + self.pre_processing(L, landmarks, max_time, max_landmarks) - # SET CONSTRAINTS FOR INEQUALITY - 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). - self.break_sym(A_ub, b_ub, L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. + # 2. Solve the problem + self.prob.solve(pl.PULP_CBC_CMD(msg=True, gapRel=0.1)) - # SET CONSTRAINTS FOR EQUALITY - A_eq, b_eq = self.init_eq_not_stay(landmarks) # Force solution not to stay in same place - self.respect_start_finish(A_eq, b_eq, L) # Force start and finish positions - self.respect_order(A_eq, b_eq, L) # Respect order of visit (only works when max_time is limiting factor) - self.respect_user_must(A_eq, b_eq, landmarks) # Force to do/avoid landmarks set by user. - - self.logger.debug(f"Optimizing with {A_ub.shape[0]} + {A_eq.shape[0]} = {A_ub.shape[0] + A_eq.shape[0]} constraints.") - - # SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1) - x_bounds = [(0, 1)]*L*L - - # Solve linear programming problem with PulP - prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) - x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] - - # Add the objective function - prob += pl.lpSum([c[i] * x[i] for i in range(L*L)]) - - # Add Inequality Constraints (A_ub @ x <= b_ub) - for i in range(len(b_ub)): # Iterate over rows of A_ub - prob += (pl.lpSum([A_ub[i][j] * x[j] for j in range(L*L)]) <= b_ub[i]) - - # 5. Add Equality Constraints (A_eq @ x == b_eq) - for i in range(len(b_eq)): # Iterate over rows of A_eq - prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) - - # 6. Solve the problem - prob.solve(pl.PULP_CBC_CMD(msg=False, gapRel=0.3)) - - # 7. Extract Results - status = pl.LpStatus[prob.status] - solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) + # 3. Extract Results + status = pl.LpStatus[self.prob.status] + solution = [pl.value(var) for var in self.x] # The values of the decision variables (will be 0 or 1) self.logger.debug("First results are out. Looking out for circles and correcting.") @@ -563,12 +540,11 @@ class Optimizer: for circle in circles : A, b = self.prevent_circle(circle, L) - prob += (pl.lpSum([A[0][j] * x[j] for j in range(L*L)]) == b[0]) - prob += (pl.lpSum([A[1][j] * x[j] for j in range(L*L)]) == b[1]) - prob.solve(pl.PULP_CBC_CMD(msg=False)) + + self.prob.solve(pl.PULP_CBC_CMD(msg=False)) - status = pl.LpStatus[prob.status] - solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) + status = pl.LpStatus[self.prob.status] + solution = [pl.value(var) for var in self.x] # The values of the decision variables (will be 0 or 1) if status != 'Optimal' : self.logger.error("The problem is overconstrained, no solution after {i} cycles.") @@ -589,5 +565,5 @@ class Optimizer: order = self.get_order(solution) tour = [landmarks[i] for i in order] - self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(prob.objective))}") + self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(self.prob.objective))}") return tour From e2e54f5205ee4bf378bac27d3efe55639a788e1e Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Thu, 16 Jan 2025 07:34:55 +0100 Subject: [PATCH 16/32] finally pulp is working ! --- backend/src/tests/test_main.py | 12 +- backend/src/utils/landmarks_manager.py | 2 +- backend/src/utils/optimizer.py | 200 ++++++++++++------------- 3 files changed, 107 insertions(+), 107 deletions(-) diff --git a/backend/src/tests/test_main.py b/backend/src/tests/test_main.py index 710c21b..813e798 100644 --- a/backend/src/tests/test_main.py +++ b/backend/src/tests/test_main.py @@ -21,7 +21,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name request: """ start_time = time.time() # Start timer - duration_minutes = 15 + duration_minutes = 20 response = client.post( "/trip/new", @@ -35,7 +35,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name } ) result = response.json() - print(result) + # print(result) landmarks = load_trip_landmarks(client, result['first_landmark_uuid']) @@ -45,8 +45,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name # Add details to report log_trip_details(request, landmarks, result['total_time'], duration_minutes) - for elem in landmarks : - print(elem) + # for elem in landmarks : + # print(elem) # checks : assert response.status_code == 200 # check for successful planning @@ -54,9 +54,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name 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 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 """ @@ -219,6 +218,7 @@ def test_shopping(client, request) : # pylint: disable=redefined-outer-name 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): # response = client.post( diff --git a/backend/src/utils/landmarks_manager.py b/backend/src/utils/landmarks_manager.py index 15c49e9..4cfc512 100644 --- a/backend/src/utils/landmarks_manager.py +++ b/backend/src/utils/landmarks_manager.py @@ -81,7 +81,7 @@ class LandmarkManager: all_landmarks = set() # Create a bbox using the around technique - bbox = tuple((f"around:{reachable_bbox_side/2}", str(center_coordinates[0]), str(center_coordinates[1]))) + bbox = tuple((f"around:{min(2000, reachable_bbox_side/2)}", str(center_coordinates[0]), str(center_coordinates[1]))) # list for sightseeing if preferences.sightseeing.score != 0: diff --git a/backend/src/utils/optimizer.py b/backend/src/utils/optimizer.py index 31bf343..d0f3d00 100644 --- a/backend/src/utils/optimizer.py +++ b/backend/src/utils/optimizer.py @@ -21,8 +21,6 @@ class Optimizer: 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 - prob: pl.LpProblem # linear optimization problem to solve - x: list[pl.LpVariable] # decision variables def __init__(self) : @@ -33,12 +31,9 @@ class Optimizer: self.average_walking_speed = parameters['average_walking_speed'] self.max_landmarks = parameters['max_landmarks'] self.overshoot = parameters['overshoot'] - - # Initalize the optimization problem - self.prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) - def init_ub_time(self, L: int, landmarks: list[Landmark], max_time: int): + def init_ub_time(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark], max_time: int): """ Initialize the objective function coefficients and inequality constraints. -> Adds 1 row of constraints @@ -80,7 +75,7 @@ class Optimizer: if L > 22 : for i in range(L): # Get indices of the 4 smallest values in row i - row_values = A_ub[0, i*L:i*L+L] + row_values = A_ub[i*L:i*L+L] closest_indices = np.argpartition(row_values, 22)[:22] # Create a mask for non-closest landmarks @@ -89,14 +84,14 @@ class Optimizer: # Set non-closest landmarks to 32765 row_values[mask] = 32765 - A_ub[0, i*L:i*L+L] = row_values + A_ub[i*L:i*L+L] = row_values - # Add the objective and the distance constraint - self.prob += pl.lpSum([c[j] * self.x[j] for j in range(L*L)]) - self.prob += (pl.lpSum([A_ub[j] * self.x[j] for j in range(L*L)]) <= b_ub) + # Add the objective and the 1 distance constraint + prob += pl.lpSum([c[j] * x[j] for j in range(L*L)]) + prob += (pl.lpSum([A_ub[j] * x[j] for j in range(L*L)]) <= b_ub) - def respect_number(self, L: int, max_landmarks: int): + def respect_number(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, max_landmarks: int): """ Generate constraints to ensure each landmark is visited only once and cap the total number of visited landmarks. -> Adds L-1 rows of constraints @@ -107,17 +102,15 @@ class Optimizer: Returns: 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 - A_ub = np.zeros(L*L, dtype=np.int8) - for i in range(0, L-2): - A_ub[L*i:L*(i+1)] = np.ones(L, dtype=np.int16) - self.prob += (pl.lpSum([A_ub[j] * self.x[j] for j in range(L*L)]) <= 1) + # L-2 constraints: each landmark is visited exactly once + for i in range(1, L-1): + prob += (pl.lpSum([x[L*i + j] for j in range(L)]) <= 1) - # Second constraint: cap the total number of visits - self.prob += (pl.lpSum([1 * self.x[j] for j in range(L*L)]) <= max_landmarks+2) + # 1 constraint: cap the total number of visits + prob += (pl.lpSum([1 * x[j] for j in range(L*L)]) <= max_landmarks+2) - def break_sym(self, L: int): + def break_sym(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): """ Generate constraints to prevent simultaneous travel between two landmarks in both directions. Constraint to not have d14 and d41 simultaneously. @@ -131,20 +124,18 @@ class Optimizer: 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) + upper_ind = np.triu_indices(L, 0, L) # Get the upper triangular indices up_ind_x = upper_ind[0] up_ind_y = upper_ind[1] - A = np.zeros(L*L, dtype=np.int8) - # Fill-in rows L to 2*L-1 - for i in range(int((L*L+L)/2)) : - if up_ind_x[i] != up_ind_y[i] : - A[up_ind_x[i]*L + up_ind_y[i]] = 1 - A[up_ind_y[i]*L + up_ind_x[i]] = 1 - self.prob += (pl.lpSum([A[j] * self.x[j] for j in range(L*L)]) <= 1) + # Loop over the upper triangular indices, excluding diagonal elements + for i in range(len(up_ind_x)): + if up_ind_x[i] != up_ind_y[i]: + # Add (L*L-L)/2 constraints to break symmetry + prob += (x[up_ind_x[i]*L + up_ind_y[i]] + x[up_ind_y[i]*L + up_ind_x[i]] <= 1) - def init_eq_not_stay(self, L: int): + def init_eq_not_stay(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): """ Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.). -> Adds 1 row of constraints @@ -162,11 +153,12 @@ class Optimizer: np.fill_diagonal(A_eq, 1) A_eq = A_eq.flatten() - self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 1) + # First equality constraint + prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 0) # Constraint to ensure start at start and finish at goal - def respect_start_finish(self, L: int): + def respect_start_finish(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): """ Generate constraints to ensure that the optimization starts at the designated start landmark and finishes at the goal landmark. @@ -193,9 +185,9 @@ class Optimizer: # Add the constraints to pulp for i in range(3) : - self.prob += (pl.lpSum([A_eq[i][j] * self.x[j] for j in range(L*L)]) == b_eq[i]) + prob += (pl.lpSum([A_eq[i][j] * x[j] for j in range(L*L)]) == b_eq[i]) - def respect_order(self, L: int): + def respect_order(self, prob: pl.LpProblem, x: pl.LpVariable, L: int): """ Generate constraints to tie the optimization problem together and prevent stacked ones, although this does not fully prevent circles. @@ -207,17 +199,24 @@ class Optimizer: Returns: tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints. """ - A_eq = np.zeros(L*L, dtype=np.int8) - 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_eq[i + j*L] = -1 - A_eq[i*L:(i+1)*L] = ones - self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 0) + # A_eq = np.zeros(L*L, dtype=np.int8) + # 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_eq[i + j*L] = -1 + # A_eq[i*L:(i+1)*L] = ones + # prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 0) + + # FIXME: weird 0 artifact in the coefficients popping up + # Loop through rows 1 to L-2 to prevent stacked ones + for i in range(1, L-1): + # Add the constraint that sums across each "row" or "block" in the decision variables + row_sum = -pl.lpSum(x[i + j*L] for j in range(L)) + pl.lpSum(x[i*L:(i+1)*L]) + prob += (row_sum == 0) - def respect_user_must(self, L: int, landmarks: list[Landmark]) : + def respect_user_must(self, prob: pl.LpProblem, x: pl.LpVariable, L: int, landmarks: list[Landmark]) : """ 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 @@ -235,49 +234,49 @@ class Optimizer: for i, elem in enumerate(landmarks) : if elem.must_do is True and i not in [0, L-1]: A_eq[i*L:i*L+L] = ones - self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 1) + prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 1) if elem.must_avoid is True and i not in [0, L-1]: A_eq[i*L:i*L+L] = ones - self.prob += (pl.lpSum([A_eq[j] * self.x[j] for j in range(L*L)]) == 2) + prob += (pl.lpSum([A_eq[j] * x[j] for j in range(L*L)]) == 2) # Prevent the use of a particular solution. TODO probably can be done faster just using resx - def prevent_config(self, resx): - """ - Prevent the use of a particular solution by adding constraints to the optimization. + # def prevent_config(self, prob: pl.LpProblem, x: pl.LpVariable, resx): + # """ + # Prevent the use of a particular solution by adding constraints to the optimization. - Args: - resx (list[float]): List of edge weights. + # Args: + # resx (list[float]): List of edge weights. - Returns: - tuple[list[int], list[int]]: A tuple containing a new row for A and new value for ub. - """ + # Returns: + # tuple[list[int], list[int]]: A tuple containing a new row for A and new value for ub. + # """ - for i, elem in enumerate(resx): - resx[i] = round(elem) + # for i, elem in enumerate(resx): + # resx[i] = round(elem) - N = len(resx) # Number of edges - L = int(np.sqrt(N)) # Number of landmarks + # 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)) + # 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) + # ind_a = nonzero_tup[0].tolist() + # vertices_visited = ind_a + # vertices_visited.remove(0) - ones = np.ones(L, dtype=np.int8) - h = np.zeros(L*L, dtype=np.int8) + # ones = np.ones(L, dtype=np.int8) + # h = np.zeros(L*L, dtype=np.int8) - for i in range(L) : - if i in vertices_visited : - h[i*L:i*L+L] = ones + # for i in range(L) : + # if i in vertices_visited : + # h[i*L:i*L+L] = ones - return h, len(vertices_visited)-1 + # return h, len(vertices_visited)-1 # Prevents the creation of the same circle (both directions) - def prevent_circle(self, circle_vertices: list, L: int) : + def prevent_circle(self, prob: pl.LpProblem, x: pl.LpVariable, circle_vertices: list, L: int) : """ Prevent circular paths by by adding constraints to the optimization. @@ -303,10 +302,10 @@ class Optimizer: l[1, s*L + g] = 1 # Add the constraints - self.prob += (pl.lpSum([l[0][j] * self.x[j] for j in range(L*L)]) == 0) - self.prob += (pl.lpSum([l[1][j] * self.x[j] for j in range(L*L)]) == 0) + prob += (pl.lpSum([l[0][j] * x[j] for j in range(L*L)]) == 0) + prob += (pl.lpSum([l[1][j] * x[j] for j in range(L*L)]) == 0) - + def is_connected(self, resx) : """ Determine the order of visits and detect any circular paths in the given configuration. @@ -474,21 +473,25 @@ class Optimizer: if max_landmarks is None : max_landmarks = self.max_landmarks + # Initalize the optimization problem + prob = pl.LpProblem("OptimizationProblem", pl.LpMaximize) + # Define the problem x_bounds = [(0, 1)]*L*L - self.x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] + x = [pl.LpVariable(f"x_{i}", lowBound=x_bounds[i][0], upBound=x_bounds[i][1], cat='Binary') for i in range(L*L)] # Setup the inequality constraints - self.init_ub_time(L, landmarks, max_time) # Adds the distances from each landmark to the other. - self.respect_number(L, max_landmarks) # Respects max number of visits (no more possible stops than landmarks). - self.break_sym(L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. + self.init_ub_time(prob, x, L, landmarks, max_time) # Adds the distances from each landmark to the other. + self.respect_number(prob, x, L, max_landmarks) # Respects max number of visits (no more possible stops than landmarks). + self.break_sym(prob, x, L) # Breaks the 'zig-zag' symmetry. Avoids d12 and d21 but not larger cirlces. # Setup the equality constraints - self.init_eq_not_stay(L) # Force solution not to stay in same place - self.respect_start_finish(L) # Force start and finish positions - self.respect_order(L) # Respect order of visit (only works when max_time is limiting factor) - self.respect_user_must(L, landmarks) # Force to do/avoid landmarks set by user. - + self.init_eq_not_stay(prob, x, L) # Force solution not to stay in same place + self.respect_start_finish(prob, x, L) # Force start and finish positions + self.respect_order(prob, x, L) # Respect order of visit (only works when max_time is limiting factor) + self.respect_user_must(prob, x, L, landmarks) # Force to do/avoid landmarks set by user. + + return prob, x def solve_optimization( self, @@ -511,14 +514,14 @@ class Optimizer: """ # 1. Setup the optimization proplem. L = len(landmarks) - self.pre_processing(L, landmarks, max_time, max_landmarks) + prob, x = self.pre_processing(L, landmarks, max_time, max_landmarks) # 2. Solve the problem - self.prob.solve(pl.PULP_CBC_CMD(msg=True, gapRel=0.1)) + prob.solve(pl.PULP_CBC_CMD(msg=False, gapRel=0.1)) # 3. Extract Results - status = pl.LpStatus[self.prob.status] - solution = [pl.value(var) for var in self.x] # The values of the decision variables (will be 0 or 1) + status = pl.LpStatus[prob.status] + solution = [pl.value(var) for var in x] # The values of the decision variables (will be 0 or 1) self.logger.debug("First results are out. Looking out for circles and correcting.") @@ -531,39 +534,36 @@ class Optimizer: circles = self.is_connected(solution) i = 0 - timeout = 80 + timeout = 40 while circles is not None : i += 1 # print(f"Iteration {i} of fixing circles") # l, b = self.prevent_config(solution) # prob += (pl.lpSum([l[j] * x[j] for j in range(L*L)]) == b) + 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.") + for circle in circles : - A, b = self.prevent_circle(circle, L) - - self.prob.solve(pl.PULP_CBC_CMD(msg=False)) + self.prevent_circle(prob, x, circle, L) - status = pl.LpStatus[self.prob.status] - solution = [pl.value(var) for var in self.x] # The values of the decision variables (will be 0 or 1) + # Solve the problem again + prob.solve(pl.PULP_CBC_CMD(msg=False)) + solution = [pl.value(var) for var in x] - if status != 'Optimal' : + if pl.LpStatus[prob.status] != 'Optimal' : self.logger.error("The problem is overconstrained, no solution after {i} cycles.") raise ArithmeticError("No solution could be found. Please try again with more time or different preferences.") - if i == timeout : - self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.') - raise ArithmeticError("Solving failed because of overconstrained problem") - + circles = self.is_connected(solution) if circles is None : break - 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.") - + # Sort the landmarks in the order of the solution order = self.get_order(solution) tour = [landmarks[i] for i in order] - self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(self.prob.objective))}") + self.logger.debug(f"Re-optimized {i} times, objective value : {int(pl.value(prob.objective))}") return tour From e5a4645f7a505aefbc4ec5f3179d79769677b4b8 Mon Sep 17 00:00:00 2001 From: Helldragon67 Date: Thu, 16 Jan 2025 10:15:24 +0100 Subject: [PATCH 17/32] faster pulp and more tests --- .gitea/workflows/backend_run_test.yaml | 2 +- backend/report.html | 10 +- backend/src/main.py | 17 +-- .../src/parameters/optimizer_parameters.yaml | 2 +- backend/src/tests/test_main.py | 141 ++++++++++++++++-- backend/src/utils/landmarks_manager.py | 2 +- backend/src/utils/refiner.py | 2 +- 7 files changed, 146 insertions(+), 30 deletions(-) diff --git a/.gitea/workflows/backend_run_test.yaml b/.gitea/workflows/backend_run_test.yaml index 82ad499..b5fa349 100644 --- a/.gitea/workflows/backend_run_test.yaml +++ b/.gitea/workflows/backend_run_test.yaml @@ -28,7 +28,7 @@ jobs: working-directory: backend - name: Run Tests - run: pipenv run pytest src --html=report.html --self-contained-html + run: pipenv run pytest src --html=report.html --self-contained-html --log-cli-level=INFO working-directory: backend - name: Upload HTML report diff --git a/backend/report.html b/backend/report.html index c0a91b2..ccaec2c 100644 --- a/backend/report.html +++ b/backend/report.html @@ -328,7 +328,7 @@ div.media {

Backend Testing Report

-

Report generated on 14-Jan-2025 at 16:05:24 by pytest-html +

Report generated on 16-Jan-2025 at 09:40:13 by pytest-html v4.1.1

Environment

@@ -382,7 +382,7 @@ div.media {

Summary

-

23 tests took 00:01:59.

+

26 tests took 00:06:14.

(Un)check the boxes to filter the results.