first steps toward pulp
This commit is contained in:
parent
d62dddd424
commit
3fe6056f3c
@ -26,3 +26,4 @@ fastapi-cli = "*"
|
|||||||
scikit-learn = "*"
|
scikit-learn = "*"
|
||||||
pyqt6 = "*"
|
pyqt6 = "*"
|
||||||
loki-logger-handler = "*"
|
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."""
|
"""Client used to call the app."""
|
||||||
return TestClient(app)
|
return TestClient(app)
|
||||||
|
|
||||||
|
'''
|
||||||
def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
|
Test n°1 : Custom test in Turckheim to ensure small villages are also supported.
|
||||||
@ -21,7 +21,7 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
|||||||
request:
|
request:
|
||||||
"""
|
"""
|
||||||
start_time = time.time() # Start timer
|
start_time = time.time() # Start timer
|
||||||
duration_minutes = 15
|
duration_minutes = 20
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/trip/new",
|
"/trip/new",
|
||||||
@ -45,8 +45,8 @@ def test_turckheim(client, request): # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
# for elem in landmarks :
|
for elem in landmarks :
|
||||||
# print(elem)
|
print(elem)
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
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 comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
assert 2==3
|
assert 2==3
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
||||||
"""
|
"""
|
||||||
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
Test n°2 : Custom test in Lyon centre to ensure proper decision making in crowded area.
|
||||||
@ -87,16 +89,16 @@ def test_bellecour(client, request) : # pylint: disable=redefined-outer-name
|
|||||||
# Add details to report
|
# Add details to report
|
||||||
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
log_trip_details(request, landmarks, result['total_time'], duration_minutes)
|
||||||
|
|
||||||
# for elem in landmarks :
|
for elem in landmarks :
|
||||||
# print(elem)
|
print(elem)
|
||||||
|
|
||||||
# checks :
|
# checks :
|
||||||
assert response.status_code == 200 # check for successful planning
|
assert response.status_code == 200 # check for successful planning
|
||||||
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
assert comp_time < 30, f"Computation time exceeded 30 seconds: {comp_time:.2f} seconds"
|
||||||
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
assert duration_minutes*0.8 < int(result['total_time']) < duration_minutes*1.2
|
||||||
# assert 2 == 3
|
assert 2 == 3
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
def test_Paris(client, request) : # pylint: disable=redefined-outer-name
|
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.
|
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 yaml, logging
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pulp as pl
|
||||||
from scipy.optimize import linprog
|
from scipy.optimize import linprog
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
|
||||||
@ -9,6 +9,9 @@ from .get_time_separation import get_time
|
|||||||
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
from ..constants import OPTIMIZER_PARAMETERS_PATH
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger('pulp').setLevel(level=logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
class Optimizer:
|
class Optimizer:
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -62,7 +65,7 @@ class Optimizer:
|
|||||||
b_ub[0] = round(max_time*self.overshoot)
|
b_ub[0] = round(max_time*self.overshoot)
|
||||||
|
|
||||||
for i, spot1 in enumerate(landmarks) :
|
for i, spot1 in enumerate(landmarks) :
|
||||||
c[i] = -spot1.attractiveness
|
c[i] = spot1.attractiveness
|
||||||
for j in range(i+1, L) :
|
for j in range(i+1, L) :
|
||||||
if i !=j :
|
if i !=j :
|
||||||
t = get_time(spot1.location, landmarks[j].location)
|
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.
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
"""
|
"""
|
||||||
# First constraint: each landmark is visited exactly once
|
# First constraint: each landmark is visited exactly once
|
||||||
# A = np.zeros((L-1, L*L), dtype=np.int8)
|
|
||||||
# b = []
|
|
||||||
# Fill-in row 2 until row L-2
|
# Fill-in row 2 until row L-2
|
||||||
for i in range(1, L-1):
|
for i in range(1, L-1):
|
||||||
A[i, L*i:L*(i+1)] = np.ones(L, dtype=np.int16)
|
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
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and
|
||||||
the right-hand side of the inequality constraints.
|
the right-hand side of the inequality constraints.
|
||||||
"""
|
"""
|
||||||
# b = []
|
|
||||||
upper_ind = np.triu_indices(L,0,L)
|
upper_ind = np.triu_indices(L,0,L)
|
||||||
up_ind_x = upper_ind[0]
|
up_ind_x = upper_ind[0]
|
||||||
up_ind_y = upper_ind[1]
|
up_ind_y = upper_ind[1]
|
||||||
|
|
||||||
# A = np.zeros((len(up_ind_x[1:]),L*L), dtype=np.int8)
|
|
||||||
# Fill-in rows L to 2*L-1
|
# Fill-in rows L to 2*L-1
|
||||||
incr = 0
|
incr = 0
|
||||||
for i in range(int((L*L+L)/2)) :
|
for i in range(int((L*L+L)/2)) :
|
||||||
@ -144,14 +143,12 @@ class Optimizer:
|
|||||||
b[L+incr] = 1
|
b[L+incr] = 1
|
||||||
incr += 1
|
incr += 1
|
||||||
|
|
||||||
# return A[~np.all(A == 0, axis=1)], b
|
|
||||||
|
|
||||||
|
|
||||||
def init_eq_not_stay(self, landmarks: list):
|
def init_eq_not_stay(self, landmarks: list):
|
||||||
"""
|
"""
|
||||||
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
Generate constraints to prevent staying in the same position (e.g., removing d11, d22, d33, etc.).
|
||||||
-> Adds 1 row of constraints
|
-> Adds 1 row of constraints
|
||||||
-> Pre-allocates A_eq for the rest of the computations with (L+2 + dynamic incr) rows
|
-> Pre-allocates A_eq for the rest of the computations with (L+ 2 + dynamic incr) rows
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
L (int): Number of landmarks.
|
L (int): Number of landmarks.
|
||||||
@ -220,7 +217,6 @@ class Optimizer:
|
|||||||
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
tuple[np.ndarray, list[int]]: Inequality constraint coefficients and the right-hand side of the inequality constraints.
|
||||||
"""
|
"""
|
||||||
ones = np.ones(L, dtype=np.int8)
|
ones = np.ones(L, dtype=np.int8)
|
||||||
|
|
||||||
# Fill-in rows 4 to L+2
|
# Fill-in rows 4 to L+2
|
||||||
for i in range(1, L-1) : # Prevent stacked ones
|
for i in range(1, L-1) : # Prevent stacked ones
|
||||||
for j in range(L) :
|
for j in range(L) :
|
||||||
@ -291,7 +287,7 @@ class Optimizer:
|
|||||||
if i in vertices_visited :
|
if i in vertices_visited :
|
||||||
h[i*L:i*L+L] = ones
|
h[i*L:i*L+L] = ones
|
||||||
|
|
||||||
return h, np.array([len(vertices_visited)-1])
|
return h, len(vertices_visited)-1
|
||||||
|
|
||||||
|
|
||||||
# Prevents the creation of the same circle (both directions)
|
# Prevents the creation of the same circle (both directions)
|
||||||
@ -320,7 +316,7 @@ class Optimizer:
|
|||||||
l[0, g*L + s] = 1
|
l[0, g*L + s] = 1
|
||||||
l[1, s*L + g] = 1
|
l[1, s*L + g] = 1
|
||||||
|
|
||||||
return l, np.zeros(2, dtype=np.int8)
|
return l, [0, 0]
|
||||||
|
|
||||||
|
|
||||||
def is_connected(self, resx) :
|
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
|
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
|
N = len(resx) # length of res
|
||||||
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||||
|
|
||||||
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
nonzeroind = np.nonzero(resx)[0] # the return is a little funny so I use the [0]
|
||||||
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
nonzero_tup = np.unravel_index(nonzeroind, (L,L))
|
||||||
|
|
||||||
ind_a = nonzero_tup[0] # removed .tolist()
|
ind_a = nonzero_tup[0]
|
||||||
ind_b = nonzero_tup[1]
|
ind_b = nonzero_tup[1]
|
||||||
|
|
||||||
# Extract all journeys
|
# Extract all journeys
|
||||||
@ -414,9 +412,7 @@ class Optimizer:
|
|||||||
Returns:
|
Returns:
|
||||||
list[int]: A list containing the visit order.
|
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
|
N = len(resx) # length of res
|
||||||
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
L = int(np.sqrt(N)) # number of landmarks. CAST INTO INT but should not be a problem because N = L**2 by def.
|
||||||
@ -454,7 +450,6 @@ class Optimizer:
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def link_list(self, order: list[int], landmarks: list[Landmark])->list[Landmark] :
|
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.
|
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:
|
Returns:
|
||||||
list[Landmark]]: The updated linked list of landmarks with travel times
|
list[Landmark]]: The updated linked list of landmarks with travel times
|
||||||
"""
|
"""
|
||||||
|
|
||||||
L = []
|
L = []
|
||||||
j = 0
|
j = 0
|
||||||
while j < len(order)-1 :
|
while j < len(order)-1 :
|
||||||
@ -489,7 +483,6 @@ class Optimizer:
|
|||||||
return L
|
return L
|
||||||
|
|
||||||
|
|
||||||
# Main optimization pipeline
|
|
||||||
def solve_optimization(
|
def solve_optimization(
|
||||||
self,
|
self,
|
||||||
max_time: int,
|
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.")
|
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)
|
# SET BOUNDS FOR DECISION VARIABLE (x can only be 0 or 1)
|
||||||
x_bounds = [(0, 1)]*L*L
|
x_bounds = [(0, 1)]*L*L
|
||||||
|
|
||||||
# Solve linear programming problem
|
# Solve linear programming problem with PulP
|
||||||
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)
|
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.")
|
self.logger.debug("First results are out. Looking out for circles and correcting.")
|
||||||
|
|
||||||
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
# Raise error if no solution is found. FIXME: for now this throws the internal server error
|
||||||
if not res.success :
|
if status != 'Optimal' :
|
||||||
self.logger.error("The problem is overconstrained, no solution on first try.")
|
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
|
# If there is a solution, we're good to go, just check for connectiveness
|
||||||
circles = self.is_connected(res.x)
|
circles = self.is_connected(solution)
|
||||||
#nodes, edges = is_connected(res.x)
|
|
||||||
i = 0
|
i = 0
|
||||||
timeout = 80
|
timeout = 80
|
||||||
while circles is not None and i < timeout:
|
while circles is not None and i < timeout:
|
||||||
i += 1
|
i += 1
|
||||||
# print(f"Iteration {i} of fixing circles")
|
# print(f"Iteration {i} of fixing circles")
|
||||||
A, b = self.prevent_config(res.x)
|
print("ok1")
|
||||||
A_ub = np.vstack((A_ub, A))
|
A, b = self.prevent_config(solution)
|
||||||
b_ub = np.concatenate((b_ub, b))
|
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 :
|
for circle in circles :
|
||||||
A, b = self.prevent_circle(circle, L)
|
A, b = self.prevent_circle(circle, L)
|
||||||
A_eq = np.vstack((A_eq, A))
|
prob += (pl.lpSum([A[0][j] * x[j // L][j % L] for j in range(L*L)]) == b[0])
|
||||||
b_eq = np.concatenate((b_eq, b))
|
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.')
|
self.logger.error(f'Unexpected error after {timeout} iterations of fixing circles.')
|
||||||
raise ArithmeticError("Solving failed because of overconstrained problem")
|
raise ArithmeticError("Solving failed because of overconstrained problem")
|
||||||
circles = self.is_connected(res.x)
|
|
||||||
#nodes, edges = is_connected(res.x)
|
circles = self.is_connected(solution)
|
||||||
if circles is None :
|
if circles is None :
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -594,8 +597,8 @@ class Optimizer:
|
|||||||
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
raise TimeoutError(f"Optimization took too long. No solution found after {timeout} iterations.")
|
||||||
|
|
||||||
# Sort the landmarks in the order of the solution
|
# Sort the landmarks in the order of the solution
|
||||||
order = self.get_order(res.x)
|
order = self.get_order(solution)
|
||||||
tour = [landmarks[i] for i in order]
|
tour = [landmarks[i] for i in order]
|
||||||
|
|
||||||
self.logger.debug(f"Re-optimized {i} times, score: {int(-res.fun)}")
|
self.logger.debug(f"Re-optimized {i} times, score: {int(pl.value(prob.objective))}")
|
||||||
return tour
|
return tour
|
||||||
|
Loading…
x
Reference in New Issue
Block a user