first steps toward pulp
This commit is contained in:
parent
d62dddd424
commit
3fe6056f3c
@ -26,3 +26,4 @@ fastapi-cli = "*"
|
||||
scikit-learn = "*"
|
||||
pyqt6 = "*"
|
||||
loki-logger-handler = "*"
|
||||
pulp = "*"
|
||||
|
1555
backend/Pipfile.lock
generated
1555
backend/Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user