Merge branch 'feature/optimizer-use-fastapi'
This commit is contained in:
		
							
								
								
									
										164
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | |||||||
|  | # osm-cache | ||||||
|  | cache/ | ||||||
|  | # Byte-compiled / optimized / DLL files | ||||||
|  | __pycache__/ | ||||||
|  | *.py[cod] | ||||||
|  | *$py.class | ||||||
|  |  | ||||||
|  | # C extensions | ||||||
|  | *.so | ||||||
|  |  | ||||||
|  | # Distribution / packaging | ||||||
|  | .Python | ||||||
|  | build/ | ||||||
|  | develop-eggs/ | ||||||
|  | dist/ | ||||||
|  | downloads/ | ||||||
|  | eggs/ | ||||||
|  | .eggs/ | ||||||
|  | lib/ | ||||||
|  | lib64/ | ||||||
|  | parts/ | ||||||
|  | sdist/ | ||||||
|  | var/ | ||||||
|  | wheels/ | ||||||
|  | share/python-wheels/ | ||||||
|  | *.egg-info/ | ||||||
|  | .installed.cfg | ||||||
|  | *.egg | ||||||
|  | MANIFEST | ||||||
|  |  | ||||||
|  | # PyInstaller | ||||||
|  | #  Usually these files are written by a python script from a template | ||||||
|  | #  before PyInstaller builds the exe, so as to inject date/other infos into it. | ||||||
|  | *.manifest | ||||||
|  | *.spec | ||||||
|  |  | ||||||
|  | # Installer logs | ||||||
|  | pip-log.txt | ||||||
|  | pip-delete-this-directory.txt | ||||||
|  |  | ||||||
|  | # Unit test / coverage reports | ||||||
|  | htmlcov/ | ||||||
|  | .tox/ | ||||||
|  | .nox/ | ||||||
|  | .coverage | ||||||
|  | .coverage.* | ||||||
|  | .cache | ||||||
|  | nosetests.xml | ||||||
|  | coverage.xml | ||||||
|  | *.cover | ||||||
|  | *.py,cover | ||||||
|  | .hypothesis/ | ||||||
|  | .pytest_cache/ | ||||||
|  | cover/ | ||||||
|  |  | ||||||
|  | # Translations | ||||||
|  | *.mo | ||||||
|  | *.pot | ||||||
|  |  | ||||||
|  | # Django stuff: | ||||||
|  | *.log | ||||||
|  | local_settings.py | ||||||
|  | db.sqlite3 | ||||||
|  | db.sqlite3-journal | ||||||
|  |  | ||||||
|  | # Flask stuff: | ||||||
|  | instance/ | ||||||
|  | .webassets-cache | ||||||
|  |  | ||||||
|  | # Scrapy stuff: | ||||||
|  | .scrapy | ||||||
|  |  | ||||||
|  | # Sphinx documentation | ||||||
|  | docs/_build/ | ||||||
|  |  | ||||||
|  | # PyBuilder | ||||||
|  | .pybuilder/ | ||||||
|  | target/ | ||||||
|  |  | ||||||
|  | # Jupyter Notebook | ||||||
|  | .ipynb_checkpoints | ||||||
|  |  | ||||||
|  | # IPython | ||||||
|  | profile_default/ | ||||||
|  | ipython_config.py | ||||||
|  |  | ||||||
|  | # pyenv | ||||||
|  | #   For a library or package, you might want to ignore these files since the code is | ||||||
|  | #   intended to run in multiple environments; otherwise, check them in: | ||||||
|  | # .python-version | ||||||
|  |  | ||||||
|  | # pipenv | ||||||
|  | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | ||||||
|  | #   However, in case of collaboration, if having platform-specific dependencies or dependencies | ||||||
|  | #   having no cross-platform support, pipenv may install dependencies that don't work, or not | ||||||
|  | #   install all needed dependencies. | ||||||
|  | #Pipfile.lock | ||||||
|  |  | ||||||
|  | # poetry | ||||||
|  | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. | ||||||
|  | #   This is especially recommended for binary packages to ensure reproducibility, and is more | ||||||
|  | #   commonly ignored for libraries. | ||||||
|  | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control | ||||||
|  | #poetry.lock | ||||||
|  |  | ||||||
|  | # pdm | ||||||
|  | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. | ||||||
|  | #pdm.lock | ||||||
|  | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it | ||||||
|  | #   in version control. | ||||||
|  | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control | ||||||
|  | .pdm.toml | ||||||
|  | .pdm-python | ||||||
|  | .pdm-build/ | ||||||
|  |  | ||||||
|  | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm | ||||||
|  | __pypackages__/ | ||||||
|  |  | ||||||
|  | # Celery stuff | ||||||
|  | celerybeat-schedule | ||||||
|  | celerybeat.pid | ||||||
|  |  | ||||||
|  | # SageMath parsed files | ||||||
|  | *.sage.py | ||||||
|  |  | ||||||
|  | # Environments | ||||||
|  | .env | ||||||
|  | .venv | ||||||
|  | env/ | ||||||
|  | venv/ | ||||||
|  | ENV/ | ||||||
|  | env.bak/ | ||||||
|  | venv.bak/ | ||||||
|  |  | ||||||
|  | # Spyder project settings | ||||||
|  | .spyderproject | ||||||
|  | .spyproject | ||||||
|  |  | ||||||
|  | # Rope project settings | ||||||
|  | .ropeproject | ||||||
|  |  | ||||||
|  | # mkdocs documentation | ||||||
|  | /site | ||||||
|  |  | ||||||
|  | # mypy | ||||||
|  | .mypy_cache/ | ||||||
|  | .dmypy.json | ||||||
|  | dmypy.json | ||||||
|  |  | ||||||
|  | # Pyre type checker | ||||||
|  | .pyre/ | ||||||
|  |  | ||||||
|  | # pytype static type analyzer | ||||||
|  | .pytype/ | ||||||
|  |  | ||||||
|  | # Cython debug symbols | ||||||
|  | cython_debug/ | ||||||
|  |  | ||||||
|  | # PyCharm | ||||||
|  | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can | ||||||
|  | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore | ||||||
|  | #  and can be added to the global gitignore or merged into this file.  For a more nuclear | ||||||
|  | #  option (not recommended) you can uncomment the following to ignore the entire idea folder. | ||||||
|  | #.idea/ | ||||||
| @@ -1,14 +1,12 @@ | |||||||
| [[source]] | [[source]] | ||||||
| url = "https://pypi.org/simple" | url = "https://pypi.org/simple" | ||||||
| verify_ssl = true | verify_ssl = true | ||||||
| name = "pypi" | name = "pypi" | ||||||
|  |  | ||||||
| [packages] | [packages] | ||||||
| numpy = "*" | numpy = "*" | ||||||
| scipy = "*" | scipy = "*" | ||||||
| fastapi = "*" | fastapi = "*" | ||||||
|  | osmpythontools = "*" | ||||||
| [dev-packages] |  | ||||||
|  | [dev-packages] | ||||||
| [requires] |  | ||||||
| python_version = "3.12" |  | ||||||
|   | |||||||
							
								
								
									
										2369
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2369
									
								
								backend/Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										0
									
								
								backend/app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/app/__pycache__/__init__.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/app/__pycache__/__init__.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/app/__pycache__/main.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/app/__pycache__/main.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								backend/app/dependencies.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/app/dependencies.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | import app.src | ||||||
							
								
								
									
										34
									
								
								backend/app/main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								backend/app/main.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | from .src.optimizer import solve_optimization | ||||||
|  | from .src.landmarks_manager import Landmark | ||||||
|  | from fastapi import FastAPI | ||||||
|  |  | ||||||
|  | app = FastAPI() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("/optimize/{max_steps}/{print_details}") | ||||||
|  | def main(max_steps: int, print_details: bool): | ||||||
|  |      | ||||||
|  |     # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS | ||||||
|  |     #max_steps = 16 | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # Initialize all landmarks (+ start and goal). Order matters here | ||||||
|  |     landmarks = [] | ||||||
|  |     landmarks.append(Landmark("départ", -1, (0, 0))) | ||||||
|  |     landmarks.append(Landmark("tour eiffel", 99, (0,2)))                           # PUT IN JSON | ||||||
|  |     landmarks.append(Landmark("arc de triomphe", 99, (0,4))) | ||||||
|  |     landmarks.append(Landmark("louvre", 99, (0,6))) | ||||||
|  |     landmarks.append(Landmark("montmartre", 99, (0,10))) | ||||||
|  |     landmarks.append(Landmark("concorde", 99, (0,8))) | ||||||
|  |     landmarks.append(Landmark("arrivée", -1, (0, 0))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     visiting_order = solve_optimization(landmarks, max_steps, print_details) | ||||||
|  |  | ||||||
|  |     #return visiting_order | ||||||
|  |  | ||||||
|  |     return("max steps :", max_steps, "\n", visiting_order) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """if __name__ == "__main__": | ||||||
|  |     main()""" | ||||||
							
								
								
									
										0
									
								
								backend/app/src/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/app/src/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/app/src/__pycache__/__init__.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/app/src/__pycache__/__init__.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/app/src/__pycache__/optimizer.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/app/src/__pycache__/optimizer.cpython-310.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										57
									
								
								backend/app/src/landmarks_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								backend/app/src/landmarks_manager.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | from OSMPythonTools.api import Api | ||||||
|  | from OSMPythonTools.overpass import Overpass | ||||||
|  | from dataclasses import dataclass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Defines the landmark class (aka some place there is to visit) | ||||||
|  | @dataclass | ||||||
|  | class Landmarkkkk : | ||||||
|  |     name : str | ||||||
|  |     attractiveness : int | ||||||
|  |     id : int | ||||||
|  |      | ||||||
|  | @dataclass | ||||||
|  | class Landmark : | ||||||
|  |     name : str | ||||||
|  |     attractiveness : int | ||||||
|  |     loc : tuple | ||||||
|  |  | ||||||
|  | # Converts a OSM id to a landmark | ||||||
|  | def add_from_id(id: int, score: int) : | ||||||
|  |  | ||||||
|  |     try : | ||||||
|  |         s = 'way/' + str(id)           # prepare string for query | ||||||
|  |         obj =  api.query(s)                             # object to add | ||||||
|  |     except : | ||||||
|  |         s = 'relation/' + str(id)           # prepare string for query | ||||||
|  |         obj =  api.query(s)                             # object to add | ||||||
|  |  | ||||||
|  |     return Landmarkkkk(obj.tag('name:fr'), score, id)      # create Landmark out of it | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # take a lsit of tuples (id, score) to generate a list of landmarks | ||||||
|  | def generate_landmarks(ids_and_scores: list) : | ||||||
|  |  | ||||||
|  |     L = [] | ||||||
|  |     for tup in ids_and_scores : | ||||||
|  |         L.append(add_from_id(tup[0], tup[1])) | ||||||
|  |  | ||||||
|  |     return L | ||||||
|  |  | ||||||
|  | api = Api() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | l = (7515426, 70) | ||||||
|  | t = (5013364, 100) | ||||||
|  | n = (201611261, 99) | ||||||
|  | a = (226413508, 50) | ||||||
|  | m = (23762981, 30) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ids_and_scores = [t, l, n, a, m] | ||||||
|  |  | ||||||
|  | landmarks = generate_landmarks(ids_and_scores) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | for obj in landmarks : | ||||||
|  |     print(obj) | ||||||
| @@ -1,23 +1,23 @@ | |||||||
| import fastapi | import fastapi | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @dataclass | @dataclass | ||||||
| class Destination: | class Destination: | ||||||
|     name: str |     name: str | ||||||
|     location: tuple |     location: tuple | ||||||
|     attractiveness: int |     attractiveness: int | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| d = Destination() | d = Destination() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_route() -> list[Destination]: | def get_route() -> list[Destination]: | ||||||
|     return {"route": "Hello World"} |     return {"route": "Hello World"} | ||||||
| 
 | 
 | ||||||
| endpoint = ("/get_route", get_route) | endpoint = ("/get_route", get_route) | ||||||
| end | end | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     fastapi.run() |     fastapi.run() | ||||||
							
								
								
									
										323
									
								
								backend/app/src/optimizer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								backend/app/src/optimizer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,323 @@ | |||||||
|  | from scipy.optimize import linprog | ||||||
|  | import numpy as np | ||||||
|  | from scipy.linalg import block_diag | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # landmarks = [Landmark_1, Landmark_2, ...] | ||||||
|  |  | ||||||
|  | # Convert the solution of the optimization into the list of edges to follow. Order is taken into account | ||||||
|  | def untangle(resx: list) : | ||||||
|  |     N = len(resx)                   # length of res | ||||||
|  |     L = int(np.sqrt(N))             # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def. | ||||||
|  |     n_edges = resx.sum()      # number of edges | ||||||
|  |  | ||||||
|  |     order = [] | ||||||
|  |     nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0] | ||||||
|  |  | ||||||
|  |     nonzero_tup = np.unravel_index(nonzeroind, (L,L)) | ||||||
|  |  | ||||||
|  |     indx = nonzero_tup[0].tolist() | ||||||
|  |     indy = nonzero_tup[1].tolist() | ||||||
|  |  | ||||||
|  |     vert = (indx[0], indy[0]) | ||||||
|  |  | ||||||
|  |     order.append(vert[0]) | ||||||
|  |     order.append(vert[1]) | ||||||
|  |      | ||||||
|  |     while len(order) < n_edges + 1 : | ||||||
|  |         ind = indx.index(vert[1]) | ||||||
|  |  | ||||||
|  |         vert = (indx[ind], indy[ind]) | ||||||
|  |  | ||||||
|  |         order.append(vert[1]) | ||||||
|  |  | ||||||
|  |     return order | ||||||
|  |  | ||||||
|  | # Just to print the result | ||||||
|  | def print_res(res, landmarks: list, P) : | ||||||
|  |     X = abs(res.x) | ||||||
|  |     order = untangle(X) | ||||||
|  |     things = [] | ||||||
|  |  | ||||||
|  |     """N = int(np.sqrt(len(X))) | ||||||
|  |     for i in range(N): | ||||||
|  |         print(X[i*N:i*N+N]) | ||||||
|  |     print("Optimal value:", -res.fun)  # Minimization, so we negate to get the maximum | ||||||
|  |     print("Optimal point:", res.x) | ||||||
|  |     for i,x in enumerate(X) : X[i] = round(x,0) | ||||||
|  |     print(order)""" | ||||||
|  |  | ||||||
|  |     if (X.sum()+1)**2 == len(X) :  | ||||||
|  |         print('\nAll landmarks can be visited within max_steps, the following order is suggested : ') | ||||||
|  |     else : | ||||||
|  |         print('Could not visit all the landmarks, the following order is suggested : ') | ||||||
|  |  | ||||||
|  |     for idx in order :  | ||||||
|  |         print('- ' + landmarks[idx].name) | ||||||
|  |         things.append(landmarks[idx].name) | ||||||
|  |  | ||||||
|  |     steps = path_length(P, abs(res.x)) | ||||||
|  |     print("\nSteps walked : " + str(steps)) | ||||||
|  |  | ||||||
|  |     return things | ||||||
|  |  | ||||||
|  | # Checks for cases of circular symmetry in the result | ||||||
|  | def has_circle(resx: list) : | ||||||
|  |     N = len(resx)                   # length of res | ||||||
|  |     L = int(np.sqrt(N))             # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def. | ||||||
|  |     n_edges = resx.sum()      # number of edges | ||||||
|  |  | ||||||
|  |      | ||||||
|  |     nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0] | ||||||
|  |  | ||||||
|  |     nonzero_tup = np.unravel_index(nonzeroind, (L,L)) | ||||||
|  |  | ||||||
|  |     indx = nonzero_tup[0].tolist() | ||||||
|  |     indy = nonzero_tup[1].tolist() | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     verts = [] | ||||||
|  |  | ||||||
|  |     for i, x in enumerate(indx) : | ||||||
|  |         verts.append((x, indy[i])) | ||||||
|  |  | ||||||
|  |      | ||||||
|  |     for vert in verts : | ||||||
|  |         visited = [] | ||||||
|  |         visited.append(vert) | ||||||
|  |  | ||||||
|  |         while len(visited) < n_edges + 1 : | ||||||
|  |  | ||||||
|  |             try :  | ||||||
|  |                 ind = indx.index(vert[1]) | ||||||
|  |  | ||||||
|  |                 vert = (indx[ind], indy[ind]) | ||||||
|  |  | ||||||
|  |                 if vert in visited : | ||||||
|  |                     return visited | ||||||
|  |                 else : | ||||||
|  |                     visited.append(vert) | ||||||
|  |             except : | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |     return [] | ||||||
|  |  | ||||||
|  | # Constraint to not have d14 and d41 simultaneously. Does not prevent circular symmetry with more elements | ||||||
|  | def break_sym(landmarks, A_ub, b_ub): | ||||||
|  |     L = len(landmarks) | ||||||
|  |     upper_ind = np.triu_indices(L,0,L) | ||||||
|  |  | ||||||
|  |     up_ind_x = upper_ind[0] | ||||||
|  |     up_ind_y = upper_ind[1] | ||||||
|  |  | ||||||
|  |     for i, _ in enumerate(up_ind_x) : | ||||||
|  |         l = [0]*L*L | ||||||
|  |         if up_ind_x[i] != up_ind_y[i] : | ||||||
|  |             l[up_ind_x[i]*L + up_ind_y[i]] = 1 | ||||||
|  |             l[up_ind_y[i]*L + up_ind_x[i]] = 1 | ||||||
|  |  | ||||||
|  |             A_ub = np.vstack((A_ub,l)) | ||||||
|  |             b_ub.append(1) | ||||||
|  |  | ||||||
|  |             """for i in range(7): | ||||||
|  |                 print(l[i*7:i*7+7]) | ||||||
|  |             print("\n")""" | ||||||
|  |  | ||||||
|  |     return A_ub, b_ub | ||||||
|  |  | ||||||
|  | # Constraint to not have circular paths. Want to go from start -> finish without unconnected loops | ||||||
|  | def break_circle(landmarks, A_ub, b_ub, circle) : | ||||||
|  |     N = len(landmarks) | ||||||
|  |     l = [0]*N*N | ||||||
|  |  | ||||||
|  |     for index in circle : | ||||||
|  |         x = index[0] | ||||||
|  |         y = index[1] | ||||||
|  |         l[x*N+y] = 1 | ||||||
|  |  | ||||||
|  |     A_ub = np.vstack((A_ub,l)) | ||||||
|  |     b_ub.append(len(circle)-1) | ||||||
|  |  | ||||||
|  |     """print("\n\nPREVENT CIRCLE") | ||||||
|  |     for i in range(7): | ||||||
|  |         print(l[i*7:i*7+7]) | ||||||
|  |     print("\n")""" | ||||||
|  |  | ||||||
|  |     return A_ub, b_ub | ||||||
|  |  | ||||||
|  | # Constraint to respect max number of travels | ||||||
|  | def respect_number(landmarks, A_ub, b_ub): | ||||||
|  |     h = [] | ||||||
|  |     for i in range(len(landmarks)) : h.append([1]*len(landmarks)) | ||||||
|  |     T = block_diag(*h) | ||||||
|  |     """for l in T : | ||||||
|  |         for i in range(7): | ||||||
|  |             print(l[i*7:i*7+7]) | ||||||
|  |         print("\n")""" | ||||||
|  |     return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks) | ||||||
|  |  | ||||||
|  | # Constraint to tie the problem together. Necessary but not sufficient to avoid circles | ||||||
|  | def respect_order(landmarks: list, A_eq, b_eq):  | ||||||
|  |     N = len(landmarks) | ||||||
|  |     for i in range(N-1) :     # Prevent stacked ones | ||||||
|  |         if i == 0 : | ||||||
|  |             continue | ||||||
|  |         else :  | ||||||
|  |             l = [0]*N | ||||||
|  |             l[i] = -1 | ||||||
|  |             l = l*N | ||||||
|  |             for j in range(N) : | ||||||
|  |                 l[i*N + j] = 1 | ||||||
|  |  | ||||||
|  |             A_eq = np.vstack((A_eq,l)) | ||||||
|  |             b_eq.append(0) | ||||||
|  |  | ||||||
|  |             """for i in range(7): | ||||||
|  |                 print(l[i*7:i*7+7]) | ||||||
|  |             print("\n")""" | ||||||
|  |  | ||||||
|  |     return A_eq, b_eq | ||||||
|  |  | ||||||
|  | # Compute manhattan distance between 2 locations | ||||||
|  | def manhattan_distance(loc1: tuple, loc2: tuple): | ||||||
|  |     x1, y1 = loc1 | ||||||
|  |     x2, y2 = loc2 | ||||||
|  |     return abs(x1 - x2) + abs(y1 - y2) | ||||||
|  |  | ||||||
|  | # Constraint to not stay in position | ||||||
|  | def init_eq_not_stay(landmarks):  | ||||||
|  |     L = len(landmarks) | ||||||
|  |     l = [0]*L*L | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     for i in range(L) : | ||||||
|  |         for j in range(L) : | ||||||
|  |             if j == i : | ||||||
|  |                 l[j + i*L] = 1 | ||||||
|  |     l[L-1] = 1      # cannot skip from start to finish | ||||||
|  |     #A_eq = np.array([np.array(xi) for xi in A_eq])                  # Must convert A_eq into an np array | ||||||
|  |     l = np.array(np.array(l)) | ||||||
|  |  | ||||||
|  |     """for i in range(7): | ||||||
|  |         print(l[i*7:i*7+7])""" | ||||||
|  |  | ||||||
|  |     return [l], [0] | ||||||
|  |  | ||||||
|  | # Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness | ||||||
|  | # We want to maximize the sightseeing :  max(c) st. A*x < b   and   A_eq*x = b_eq | ||||||
|  | def init_ub_dist(landmarks: list, max_steps: int): | ||||||
|  |     # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... | ||||||
|  |     c = [] | ||||||
|  |     # Coefficients of inequality constraints (left-hand side) | ||||||
|  |     A = [] | ||||||
|  |     for i, spot1 in enumerate(landmarks) : | ||||||
|  |         dist_table = [0]*len(landmarks) | ||||||
|  |         c.append(-spot1.attractiveness) | ||||||
|  |         for j, spot2 in enumerate(landmarks) : | ||||||
|  |             dist_table[j] = manhattan_distance(spot1.loc, spot2.loc) | ||||||
|  |         A.append(dist_table) | ||||||
|  |     c = c*len(landmarks) | ||||||
|  |     A_ub = [] | ||||||
|  |     for line in A : | ||||||
|  |         #print(line) | ||||||
|  |         A_ub += line | ||||||
|  |     return c, A_ub, [max_steps] | ||||||
|  |  | ||||||
|  | # Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1 | ||||||
|  | def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) : | ||||||
|  |     L = len(landmarks) | ||||||
|  |     H = 0       # sort of heuristic to get an idea of the number of steps needed | ||||||
|  |     for i in landmarks :  | ||||||
|  |         if i.name == "départ" : elem_prev = i              # list of all matches | ||||||
|  |     for i, elem in enumerate(landmarks) : | ||||||
|  |         if elem.attractiveness == -1 : | ||||||
|  |             l = [0]*L*L | ||||||
|  |             if elem.name != "arrivée" : | ||||||
|  |                 for j in range(L) : | ||||||
|  |                     l[j +i*L] = 1 | ||||||
|  |                      | ||||||
|  |             else :                          # This ensures we go to goal | ||||||
|  |                 for k in range(L-1) : | ||||||
|  |                         l[k*L+L-1] = 1   | ||||||
|  |  | ||||||
|  |             H += manhattan_distance(elem.loc, elem_prev.loc) | ||||||
|  |             elem_prev = elem | ||||||
|  |  | ||||||
|  |             """for i in range(7): | ||||||
|  |                 print(l[i*7:i*7+7]) | ||||||
|  |             print("\n")""" | ||||||
|  |  | ||||||
|  |             A_eq = np.vstack((A_eq,l)) | ||||||
|  |             b_eq.append(1) | ||||||
|  |     return A_eq, b_eq, H | ||||||
|  |  | ||||||
|  | # Computes the path length given path matrix (dist_table) and a result | ||||||
|  | def path_length(P: list, resx: list) : | ||||||
|  |     return np.dot(P, resx) | ||||||
|  |  | ||||||
|  | # Main optimization pipeline | ||||||
|  | def solve_optimization (landmarks, max_steps, printing_details) : | ||||||
|  |  | ||||||
|  |     # SET CONSTRAINTS FOR INEQUALITY | ||||||
|  |     c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other | ||||||
|  |     P = A_ub                                                        # store the paths for later. Needed to compute path length | ||||||
|  |     A_ub, b_ub = respect_number(landmarks, A_ub, b_ub)              # Respect max number of visits.  | ||||||
|  |  | ||||||
|  |     # TODO : Problems with circular symmetry | ||||||
|  |     A_ub, b_ub = break_sym(landmarks, A_ub, b_ub)                  # break the symmetry. Only use the upper diagonal values | ||||||
|  |  | ||||||
|  |     # SET CONSTRAINTS FOR EQUALITY | ||||||
|  |     A_eq, b_eq = init_eq_not_stay(landmarks)                       # Force solution not to stay in same place | ||||||
|  |     A_eq, b_eq, H = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal | ||||||
|  |  | ||||||
|  |     A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order of visit (only works when max_steps is limiting factor) | ||||||
|  |  | ||||||
|  |     # Bounds for variables (x can only be 0 or 1) | ||||||
|  |     x_bounds = [(0, 1)] * len(c) | ||||||
|  |  | ||||||
|  |     # 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 : | ||||||
|  |  | ||||||
|  |         # Override the max_steps using the heuristic | ||||||
|  |         for i, val in enumerate(b_ub) : | ||||||
|  |             if val == max_steps : b_ub[i] = H | ||||||
|  |  | ||||||
|  |         # Solve problem again : | ||||||
|  |         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 : | ||||||
|  |             s = "No solution could be found, even when increasing max_steps using the heuristic" | ||||||
|  |             return s | ||||||
|  |             #raise ValueError("No solution could be found, even when increasing max_steps using the heuristic") | ||||||
|  |      | ||||||
|  |     # If there is a solution, we're good to go, just check for | ||||||
|  |     else : | ||||||
|  |         circle = has_circle(res.x) | ||||||
|  |         i = 0 | ||||||
|  |  | ||||||
|  |         # Break the circular symmetry if needed | ||||||
|  |         while len(circle) != 0 : | ||||||
|  |             A_ub, b_ub = break_circle(landmarks, A_ub, b_ub, circle) | ||||||
|  |             res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq = b_eq, bounds=x_bounds, method='highs', integrality=3) | ||||||
|  |             circle = has_circle(res.x) | ||||||
|  |             i += 1 | ||||||
|  |  | ||||||
|  |         if printing_details is True : | ||||||
|  |             if i != 0 : | ||||||
|  |                 print(f"Neded to recompute paths {i} times because of unconnected loops...") | ||||||
|  |             X = print_res(res, landmarks, P) | ||||||
|  |             return X | ||||||
|  |         else : | ||||||
|  |             return untangle(res.x) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1,206 +0,0 @@ | |||||||
| from scipy.optimize import linprog |  | ||||||
| import numpy as np |  | ||||||
| from scipy.linalg import block_diag |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Defines the landmark class (aka some place there is to visit) |  | ||||||
| class landmark : |  | ||||||
|     def __init__(self, name: str, attractiveness: int, loc: tuple): |  | ||||||
|         self.name = name |  | ||||||
|         self.attractiveness = attractiveness |  | ||||||
|         self.loc = loc |  | ||||||
|  |  | ||||||
| # Convert the result (edges from j to k like d_25 = edge between vertex 2 and vertex 5) into the list of indices corresponding to the landmarks |  | ||||||
| def untangle(res: list) : |  | ||||||
|     N = len(res)                # length of res |  | ||||||
|     L = int(np.sqrt(N))         # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def. |  | ||||||
|     n_landmarks = res.sum()     # number of visited landmarks |  | ||||||
|     visit_order = [] |  | ||||||
|     cnt = 0 |  | ||||||
|  |  | ||||||
|     if n_landmarks % 2 == 1 :                                     # if odd number of visited checkpoints |  | ||||||
|         for i in range(L) : |  | ||||||
|             for j in range(L) : |  | ||||||
|                 if res[i*L + j] == 1 :              # if index is 1 |  | ||||||
|                     cnt += 1                        # increment counter |  | ||||||
|                     if cnt % 2 == 1 :               # if counter odd |  | ||||||
|                         visit_order.append(i) |  | ||||||
|                         visit_order.append(j) |  | ||||||
|     else :                                   # if even number of ones |  | ||||||
|         for i in range(L) : |  | ||||||
|             for j in range(L) : |  | ||||||
|                 if res[i*L + j] == 1 :              # if index is one |  | ||||||
|                     cnt += 1                        # increment counter |  | ||||||
|                     if j % (L-1) == 0 :             # if last node |  | ||||||
|                         visit_order.append(j)       # append only the last index |  | ||||||
|                         return visit_order          # return |  | ||||||
|                     if cnt % 2 == 1 :  |  | ||||||
|                         visit_order.append(i) |  | ||||||
|                         visit_order.append(j) |  | ||||||
|     return visit_order |  | ||||||
|  |  | ||||||
| # Just to print the result |  | ||||||
| def print_res(res: list, P) : |  | ||||||
|     X = abs(res.x) |  | ||||||
|     order = untangle(X) |  | ||||||
|  |  | ||||||
|     # print("Optimal value:", -res.fun)  # Minimization, so we negate to get the maximum |  | ||||||
|     # print("Optimal point:", res.x) |  | ||||||
|     # N = int(np.sqrt(len(X))) |  | ||||||
|     # for i in range(N): |  | ||||||
|     #     print(X[i*N:i*N+N]) |  | ||||||
|     # print(order) |  | ||||||
|  |  | ||||||
|     if (X.sum()+1)**2 == len(X) :  |  | ||||||
|         print('\nAll landmarks can be visited within max_steps, the following order is most likely not the fastest') |  | ||||||
|     else : |  | ||||||
|         print('Could not visit all the landmarks, the following order could be the fastest but not sure') |  | ||||||
|     print("Order of visit :") |  | ||||||
|     for i, elem in enumerate(landmarks) :  |  | ||||||
|         if i in order : print('- ' + elem.name) |  | ||||||
|  |  | ||||||
|     steps = path_length(P, abs(res.x)) |  | ||||||
|     print("\nSteps walked : " + str(steps)) |  | ||||||
|  |  | ||||||
| # Constraint to use only the upper triangular indices for travel |  | ||||||
| def break_sym(landmarks, A_eq, b_eq): |  | ||||||
|     L = len(landmarks) |  | ||||||
|     l = [0]*L*L |  | ||||||
|     for i in range(L) : |  | ||||||
|         for j in range(L) : |  | ||||||
|             if i >= j : |  | ||||||
|                 l[j+i*L] = 1 |  | ||||||
|  |  | ||||||
|     A_eq = np.vstack((A_eq,l)) |  | ||||||
|     b_eq.append(0) |  | ||||||
|  |  | ||||||
|     return A_eq, b_eq |  | ||||||
|  |  | ||||||
| # Constraint to respect max number of travels |  | ||||||
| def respect_number(landmarks, A_ub, b_ub): |  | ||||||
|     h = [] |  | ||||||
|     for i in range(len(landmarks)) : h.append([1]*len(landmarks)) |  | ||||||
|     T = block_diag(*h) |  | ||||||
|     return np.vstack((A_ub, T)), b_ub + [1]*len(landmarks) |  | ||||||
|  |  | ||||||
| # Constraint to tie the problem together and have a connected path |  | ||||||
| def respect_order(landmarks: list, A_eq, b_eq):  |  | ||||||
|     N = len(landmarks) |  | ||||||
|     for i in range(N-1) :     # Prevent stacked ones |  | ||||||
|         if i == 0 : |  | ||||||
|             continue |  | ||||||
|         else :  |  | ||||||
|             l = [0]*N |  | ||||||
|             l[i] = -1 |  | ||||||
|             l = l*N |  | ||||||
|             for j in range(N) : |  | ||||||
|                 l[i*N + j] = 1 |  | ||||||
|  |  | ||||||
|             A_eq = np.vstack((A_eq,l)) |  | ||||||
|             b_eq.append(0) |  | ||||||
|  |  | ||||||
|     return A_eq, b_eq |  | ||||||
|  |  | ||||||
| # Compute manhattan distance between 2 locations |  | ||||||
| def manhattan_distance(loc1: tuple, loc2: tuple): |  | ||||||
|     x1, y1 = loc1 |  | ||||||
|     x2, y2 = loc2 |  | ||||||
|     return abs(x1 - x2) + abs(y1 - y2) |  | ||||||
|  |  | ||||||
| # Constraint to not stay in position |  | ||||||
| def init_eq_not_stay(landmarks):  |  | ||||||
|     L = len(landmarks) |  | ||||||
|     l = [0]*L*L |  | ||||||
|     for i in range(L) : |  | ||||||
|         for j in range(L) : |  | ||||||
|             if j == i : |  | ||||||
|                 l[j + i*L] = 1 |  | ||||||
|     #A_eq = np.array([np.array(xi) for xi in A_eq])                  # Must convert A_eq into an np array |  | ||||||
|     l = np.array(np.array(l)) |  | ||||||
|     return [l], [0] |  | ||||||
|  |  | ||||||
| # Initialize A and c. Compute the distances from all landmarks to each other and store attractiveness |  | ||||||
| # We want to maximize the sightseeing :  max(c) st. A*x < b   and   A_eq*x = b_eq |  | ||||||
| def init_ub_dist(landmarks: list, max_steps: int): |  | ||||||
|     # Objective function coefficients. a*x1 + b*x2 + c*x3 + ... |  | ||||||
|     c = [] |  | ||||||
|     # Coefficients of inequality constraints (left-hand side) |  | ||||||
|     A = [] |  | ||||||
|     for i, spot1 in enumerate(landmarks) : |  | ||||||
|         dist_table = [0]*len(landmarks) |  | ||||||
|         c.append(-spot1.attractiveness) |  | ||||||
|         for j, spot2 in enumerate(landmarks) : |  | ||||||
|             dist_table[j] = manhattan_distance(spot1.loc, spot2.loc) |  | ||||||
|         A.append(dist_table) |  | ||||||
|     c = c*len(landmarks) |  | ||||||
|     A_ub = [] |  | ||||||
|     for line in A : |  | ||||||
|         A_ub += line |  | ||||||
|     return c, A_ub, [max_steps] |  | ||||||
|  |  | ||||||
| # Go through the landmarks and force the optimizer to use landmarks where attractiveness is set to -1 |  | ||||||
| def respect_user_mustsee(landmarks: list, A_eq: list, b_eq: list) : |  | ||||||
|     L = len(landmarks) |  | ||||||
|     for i, elem in enumerate(landmarks) : |  | ||||||
|         if elem.attractiveness == -1 : |  | ||||||
|             l = [0]*L*L |  | ||||||
|             if elem.name != "arrivée" : |  | ||||||
|                 for j in range(L) : |  | ||||||
|                     l[j +i*L] = 1 |  | ||||||
|             else :                          # This ensures we go to goal |  | ||||||
|                 for k in range(L-1) : |  | ||||||
|                         l[k*L+L-1] = 1 |  | ||||||
|             A_eq = np.vstack((A_eq,l)) |  | ||||||
|             b_eq.append(1) |  | ||||||
|     return A_eq, b_eq |  | ||||||
|  |  | ||||||
| # Computes the path length given path matrix (dist_table) and a result |  | ||||||
| def path_length(P: list, resx: list) : |  | ||||||
|     return np.dot(P, resx) |  | ||||||
|  |  | ||||||
| # Initialize all landmarks (+ start and goal). Order matters here |  | ||||||
| landmarks = [] |  | ||||||
| landmarks.append(landmark("départ", -1, (0, 0))) |  | ||||||
| landmarks.append(landmark("concorde", -1, (5,5))) |  | ||||||
| landmarks.append(landmark("tour eiffel", 99, (1,1)))                           # PUT IN JSON |  | ||||||
| landmarks.append(landmark("arc de triomphe", 99, (2,3))) |  | ||||||
| landmarks.append(landmark("louvre", 70, (4,2))) |  | ||||||
| landmarks.append(landmark("montmartre", 20, (0,2))) |  | ||||||
| landmarks.append(landmark("arrivée", -1, (0, 0))) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # CONSTRAINT TO RESPECT MAX NUMBER OF STEPS |  | ||||||
| max_steps = 25 |  | ||||||
|  |  | ||||||
| # SET CONSTRAINTS FOR INEQUALITY |  | ||||||
| c, A_ub, b_ub = init_ub_dist(landmarks, max_steps)              # Add the distances from each landmark to the other |  | ||||||
| P = A_ub                                                        # store the paths for later. Needed to compute path length |  | ||||||
| A_ub, b_ub = respect_number(landmarks, A_ub, b_ub)              # Respect max number of visits.  |  | ||||||
|  |  | ||||||
| # SET CONSTRAINTS FOR EQUALITY |  | ||||||
| A_eq, b_eq = init_eq_not_stay(landmarks)                       # Force solution not to stay in same place |  | ||||||
| A_eq, b_eq = respect_user_mustsee(landmarks, A_eq, b_eq)       # Check if there are user_defined must_see. Also takes care of start/goal |  | ||||||
| A_eq, b_eq = break_sym(landmarks, A_eq, b_eq)                  # break the symmetry. Only use the upper diagonal values |  | ||||||
| A_eq, b_eq = respect_order(landmarks, A_eq, b_eq)              # Respect order of visit (only works when max_steps is limiting factor) |  | ||||||
|  |  | ||||||
| # Bounds for variables (x can only be 0 or 1) |  | ||||||
| x_bounds = [(0, 1)] * len(c) |  | ||||||
|  |  | ||||||
| # 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 ValueError("No solution has been found, please adapt your max steps") |  | ||||||
|  |  | ||||||
| # Print result |  | ||||||
| print_res(res, P) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
		Reference in New Issue
	
	Block a user