a3-algorithmique-avancee/Projet_algo.ipynb
2023-06-15 16:14:12 +02:00

43 KiB
Raw Blame History

Projet algorithme

Contexte du projet:

L'ADEME a lancé un appel à manifestation d'intérêt pour développer des solutions de mobilité adaptées à différents territoires. CesiCDP, en collaboration avec des partenaires, s'est spécialisé dans la Mobilité Multimodale Intelligente. Dans le cadre de cet appel, l'équipe de CesiCDP travaille sur la gestion de tournées de livraison pour minimiser les trajets et optimiser l'efficacité. L'objectif est d'obtenir de nouveaux marchés et des financements pour poursuivre le développement de l'entreprise. Des contraintes supplémentaires seront ajoutées pour rendre le problème plus réaliste et intéressant pour l'ADEME.

Notre but est de réaliser un algorithme permettant de passer par tous les points de livraisons avec un temps optimisé.

Contraintes choisies :

Les contraintes que nous avons choisi sont les suivantes:

  • Avoir plusieurs camions disponible simultanément pour effectuer les livraisons.
  • Fenêtre de temps de livraison pour chaque object

Formulation du problème

Soit un graphe $G=(V,E)$, où $V$ est l'ensemble des villes (ou points de livraison) et $E$ est l'ensemble des routes entre les villes. Chaque ville $v∈V$ a une fenêtre de temps de livraison $[a_v,b_v]$, où $a_v$ est le début de la fenêtre et $b_v$ est la fin de la fenêtre. Il y a $k$ camions disponibles pour effectuer les livraisons.

Le problème consiste à trouver une tournée pour chaque camion, de manière à ce que toutes les livraisons soient effectuées dans leurs fenêtres de temps respectives et que la date de retour du dernier camion à la base soit minimisée.

Le problème que nous avons avec les contraintes ci-dessus est le problème du VRPTW (Vehicule Routing Problem avec la contrainte supplémentaire de fenêtres d'ouverture).

Contraintes du problème

Liste des contraintes du problème:

  • Tous les clients doivent être déservis
  • Un client ne peut être servi que par un et un seul véhicule.
  • En quittant un client, un véhicule peut aller que vers un seul autre client.
  • Un véhicule ne peut servir un client que s'il a assez de capacité pour servir le client.

On va donc affecter chaque client à une tournée effectué par un seul véhicule.

Modélisation mathématique

Ensemble et paramètres:

$V=\{0,1,2,...,n_v\}$ : l'ensemble des villes, où 0 est la base (ou le dépôt), $1,2,...,n_v$ sont les villes de livraison. $n_v+1$ sera le dépot pour le retour.
$K=\{1,2,...,k\}$ : l'ensemble des camions.
$E$ représente les arcs entre deux clients $i,j \in V$
$G=(V,E)$ : le graphe
$d_{ij}$ : la distance (ou le temps de trajet) de la ville i à la ville $j$ (coût de déplacement)
$[a_i,b_i]$ : la fenêtre de temps de livraison pour la ville $i$.
$t_{ij}$ : le temps de déplacement de la ville i à la ville j.
$M$ : une grande constante.

Variables de décision:

$x_{ijk}$ : variable binaire qui vaut 1 si le camion $k$ se déplace de la ville $i$ à la ville $j$, et 0 sinon.
$t_{ik}$ : le moment où le camion $k$ arrive à la ville $i$.

Fonction objective:

Minimiser $Z=max_{k∈K}t_{0k}$

Contraintes et modèle mathématique du VRP:

L'objectif est de minimiser la distance totale parcourue :

$$\min \sum_{k∈K} \sum_{i∈V} \sum_{j∈V} d_{ij} x_{ijk} $$

Chaque ville est visitée une fois et une seule fois : $$\sum_{k∈K} \sum_{j∈V} x_{ijk} = 1, ∀i \in V \{0\}$$

Si un camion se déplace de la ville ii à la ville $j$, alors le moment d'arrivée à la ville $j$ doit être plus grand que le moment d'arrivée à la ville $i$ plus le temps de trajet : $$t_{ik}+d_{ij} \leq t_{jk}+M(1x_{ijk}),∀i,j \in V,i \ne j,∀k \in K$$

Les contraintes de flux pour garantir que si un camion entre dans une ville, il doit également en sortir : $$i \in V,i \ne j∑xijk=i∈V,i \ne j∑xjik=yjk,∀j∈V,∀k∈K$$

Les $t_{ik}$ doivent être continues :

$t_{ik}\le 0 $

Dans le cas ou l'on veut rajouter les contraintes de fenêtres de livraison (Time Windows)

Les fenêtres de temps de livraison doivent être respectées : $$a_i \leq t_{ik} \leq b_i, ∀i \in V \{0 \},∀k \in K$$

Initialisation de la matrice

In [17]:
import random
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import time as time

from networkx.linalg import graphmatrix

def generate_graph(num_nodes):
    G = nx.Graph()
    G.add_nodes_from(range(num_nodes + 1))
    
    for node in G.nodes():
        connected_nodes = sorted(set(G.nodes()) - {node})
        if len(connected_nodes) < 2:
            continue
        distance1 = random.randint(1, 10)
        distance2 = random.randint(1, 10)
        random_nodes = random.sample(connected_nodes, 2)
        G.add_edges_from([(node, random_nodes[0], {'distance': distance1}),
                          (node, random_nodes[1], {'distance': distance2})])

    while not nx.is_connected(G):
        node1, node2 = random.sample(G.nodes(), 2)
        if not G.has_edge(node1, node2):
            distance = random.randint(1, 10)
            G.add_edge(node1, node2, distance=distance)

    return G

graph = generate_graph(12)
A = nx.adjacency_matrix(graph)

def generate_weighted_adjacency_matrix(graph):
    # Créer une matrice d'adjacence pondérée à partir du graphe
    adjacency_matrix = graphmatrix.to_numpy_matrix(graph, weight='distance')

    return adjacency_matrix

# Générer la matrice d'adjacence pondérée
weighted_adjacency_matrix = generate_weighted_adjacency_matrix(graph)

# Afficher la matrice d'adjacence pondérée
print(weighted_adjacency_matrix)


def generate_distance_matrix(graph):
    num_nodes = graph.number_of_nodes()
    distance_array = np.full((num_nodes, num_nodes), float('inf'))  # Initialiser avec l'infini
    for edge in graph.edges(data=True):
        i, j, data = edge
        distance_array[i][j] = data['distance']
        distance_array[j][i] = data['distance']  # Pour un graphe non orienté
    np.fill_diagonal(distance_array, 0)  # Remplir la diagonale avec des zéros
    return distance_array

# Générer la matrice de distances
distance_matrix = generate_distance_matrix(graph)


# Afficher la matrice de distances
print(distance_matrix)



# Dessiner le graphe
nx.draw(graph, with_labels=True)
plt.show()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[17], line 41
     38     return adjacency_matrix
     40 # Générer la matrice d'adjacence pondérée
---> 41 weighted_adjacency_matrix = generate_weighted_adjacency_matrix(graph)
     43 # Afficher la matrice d'adjacence pondérée
     44 print(weighted_adjacency_matrix)

Cell In[17], line 36, in generate_weighted_adjacency_matrix(graph)
     34 def generate_weighted_adjacency_matrix(graph):
     35     # Créer une matrice d'adjacence pondérée à partir du graphe
---> 36     adjacency_matrix = graphmatrix.to_numpy_matrix(graph, weight='distance')
     38     return adjacency_matrix

AttributeError: module 'networkx.linalg.graphmatrix' has no attribute 'to_numpy_matrix'

On applique l'algorithme des fourmis (ACO) sur notre graphe

In [10]:
import concurrent.futures
num_trucks = 2  # Nombre de camions disponibles


# Fonction d'évaluation de la qualité d'une solution (ici, la distance totale)
def evaluate_solution(solution, distances):
    total_distance = 0
    num_nodes = len(solution)

    for i in range(num_nodes - 1):
        current_node = solution[i]
        next_node = solution[i + 1]
        total_distance += distances[current_node][next_node]


    # Ajouter la distance de retour au dépôt
    total_distance += distances[solution[-1]][solution[0]]

    return total_distance

    
def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):
    num_nodes = len(distances)
    
    pheromone = np.ones((num_nodes, num_nodes))  # Matrice de phéromones initiale
    best_solution = None
    best_distance = float('inf')

    start_time = time.time()

    for iteration in range(num_iterations):
        # Construction de solutions par les fourmis
        solutions = []

        for ant in range(num_ants):
            visited = set()
            current_node = random.randint(0, num_nodes - 1)
            visited.add(current_node)
            solution = [current_node]

            while len(visited) < num_nodes:
                next_node = None
                probabilities = []

                # Calcul des probabilités de choisir chaque prochain nœud
                for node in range(num_nodes):
                    if node not in visited and (node !=0 or len(visited) == num_nodes - 1):
                        pheromone_value = pheromone[current_node][node]
                        distance_value = distances[current_node][node]
                        probability = (pheromone_value ** alpha) * ((1 / distance_value) ** beta)
                        probabilities.append((node, probability))

                total_probability = sum(prob for _, prob in probabilities)
                probabilities = [(node, prob / total_probability) for node, prob in probabilities]

            
                roulette_wheel = random.random()
                probability_sum = 0


                for node, probability in probabilities:
                    
                    probability_sum += probability
                    if probability_sum >= roulette_wheel:
                        next_node = node
                        break

                visited.add(next_node)
                solution.append(next_node)
                current_node = next_node

            # Ajouter le retour au dépôt central à la fin du trajet
            solution.append(0)

            solutions.append(solution)

        # Évaluation des solutions et mise à jour de la meilleure solution
        for solution in solutions:
            distance = evaluate_solution(solution, distances)
            if distance < best_distance:
                best_solution = solution
                best_distance = distance

        # Mise à jour des phéromones
        pheromone *= evaporation_rate  # Évaporation des phéromones existantes

        for solution in solutions:
            delta_pheromone = 1 / evaluate_solution(solution, distances)
            for i in range(num_nodes - 1):
                node1 = solution[i]
                node2 = solution[i + 1]
                pheromone[node1][node2] += delta_pheromone
                pheromone[node2][node1] += delta_pheromone

    # Séparer la meilleure solution en trajets pour chaque camion
    truck_solutions = []
    num_nodes_per_truck = num_nodes // num_trucks

    for i in range(num_trucks):
        start_index = i * num_nodes_per_truck
        end_index = start_index + num_nodes_per_truck
        truck_solution = best_solution[start_index:end_index]
        truck_solutions.append(truck_solution + [0])
        

    return truck_solutions, best_distance


def trucks_thread(i, num_nodes_per_truck, best_solution, truck_solutions):
    start_index = i * num_nodes_per_truck
    end_index = start_index + num_nodes_per_truck
    truck_solution = best_solution[start_index:end_index]
    truck_solutions.append(truck_solution)
    return truck_solutions


num_ants = 10
num_iterations = 100
evaporation_rate = 0.5
alpha = 1
beta = 1

best_truck_solutions, best_distance = ant_colony_optimization(distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks)


print("Meilleure solution :", best_truck_solutions)
print("Distance totale :", best_distance)
    
Meilleure solution : [[6, 5, 4, 8, 1, 11, 0], [9, 3, 7, 12, 10, 2, 0]]
Distance totale : 81.0
In [11]:
num_trucks = 2  # Nombre de camions disponibles

def evaluate_solution(solution, distances):
    total_distance = 0
    num_nodes = len(solution)

    # Calculer la distance entre chaque paire consécutive de nœuds dans la solution
    for i in range(num_nodes - 1):
        current_node = solution[i]
        next_node = solution[i + 1]
        total_distance += distances[current_node][next_node]

    # Ajouter la distance de retour au dépôt
    total_distance += distances[solution[-1]][solution[0]]

    return total_distance

def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):
    num_nodes = len(distances)
    
    pheromone = np.ones((num_nodes, num_nodes))  # Matrice de phéromones initiale
    best_solution = None
    best_distance = float('inf')

    for iteration in range(num_iterations):
        # Construction de solutions par les fourmis
        solutions = []

        for ant in range(num_ants):
            visited = set()
            current_node = 0  # Initialiser current_node à 0
            visited.add(current_node)
            solution = [current_node]

            while len(visited) < num_nodes:
                next_node = None
                probabilities = []

                # Calcul des probabilités de choisir chaque prochain nœud
                for node in range(num_nodes):
                    if node not in visited and node < len(pheromone) and current_node < len(pheromone):
                        pheromone_value = pheromone[current_node][node]
                        distance_value = distances[current_node][node]
                        probability = (pheromone_value ** alpha) * ((1 / distance_value) ** beta)
                        probabilities.append((node, probability))

                total_probability = sum(prob for _, prob in probabilities)
                probabilities = [(node, prob / total_probability) for node, prob in probabilities]

                roulette_wheel = random.random()
                probability_sum = 0

                for node, probability in probabilities:
                    probability_sum += probability
                    if probability_sum >= roulette_wheel:
                        next_node = node
                        break

                visited.add(next_node)
                solution.append(next_node)
                current_node = next_node

            # Ajouter le retour au dépôt central à la fin du trajet
            solution.append(0)

            solutions.append(solution)

        # Évaluation des solutions et mise à jour de la meilleure solution
        for solution in solutions:
            distance = evaluate_solution(solution, distances)
            if distance < best_distance:
                best_solution = solution
                best_distance = distance

        # Mise à jour des phéromones
        pheromone *= evaporation_rate  # Évaporation des phéromones existantes

        for solution in solutions:
            delta_pheromone = 1 / evaluate_solution(solution, distances)
            for i in range(num_nodes - 1):
                node1 = solution[i]
                node2 = solution[i + 1]
                pheromone[node1][node2] += delta_pheromone
                pheromone[node2][node1] += delta_pheromone

    # Séparer la meilleure solution en trajets pour chaque camion
    truck_solutions = []
    num_nodes_per_truck = num_nodes // num_trucks

    for i in range(num_trucks):
        start_index = i * num_nodes_per_truck
        end_index = start_index + num_nodes_per_truck
        truck_solution = best_solution[start_index:end_index]
        truck_solutions.append(truck_solution + [0])
        

    return truck_solutions, best_distance





num_ants = 10
num_iterations = 100
evaporation_rate = 0.5
alpha = 1
beta = 1

best_truck_solutions,best_distance = ant_colony_optimization(distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks)

# Pour chaque solution de camion, nous devons trouver le chemin le plus court du dépôt au premier nœud du chemin
for i, truck_solution in enumerate(best_truck_solutions):
    # Créer un graphe à partir de la matrice de distance
    G = nx.from_numpy_matrix(distance_matrix)
    
    # Utiliser l'algorithme de Dijkstra pour trouver le chemin le plus court du dépôt (nœud 0) au premier nœud du chemin du camion
    shortest_path = nx.dijkstra_path(G, 0, truck_solution[1])
    
    # Remplacer le premier nœud du chemin du camion par le chemin le plus court trouvé
    best_truck_solutions[i] = [0] + shortest_path + truck_solution[2:]

# Calculer la distance totale pour chaque solution de camion
total_distances = [evaluate_solution(truck_solution, distance_matrix) for truck_solution in best_truck_solutions]

# Afficher les solutions et les distances totales pour chaque camion
for i, (truck_solution, total_distance) in enumerate(zip(best_truck_solutions, total_distances)):
    print(f"Camion {i+1} :")
    print("Solution :", truck_solution)
    print("Distance totale :", total_distance)
    print()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[11], line 114
    111 # Pour chaque solution de camion, nous devons trouver le chemin le plus court du dépôt au premier nœud du chemin
    112 for i, truck_solution in enumerate(best_truck_solutions):
    113     # Créer un graphe à partir de la matrice de distance
--> 114     G = nx.from_numpy_matrix(distance_matrix)
    116     # Utiliser l'algorithme de Dijkstra pour trouver le chemin le plus court du dépôt (nœud 0) au premier nœud du chemin du camion
    117     shortest_path = nx.dijkstra_path(G, 0, truck_solution[1])

AttributeError: module 'networkx' has no attribute 'from_numpy_matrix'

def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks): num_nodes = len(distances)

pheromone = np.ones((num_nodes, num_nodes))  # Matrice de phéromones initiale
best_solution = None
best_distance = float('inf')

for iteration in range(num_iterations):
    # Construction de solutions par les fourmis
    solutions = []

    for ant in range(num_ants):
        visited = set()
        current_node = 0  # Tous les camions partent du dépôt
        visited.add(current_node)
        solution = [current_node]

        while len(visited) < num_nodes:
            next_node = None
            probabilities = []

            # Calcul des probabilités de choisir chaque prochain nœud
            for node in range(num_nodes):
                if node not in visited:
                    pheromone_value = pheromone[current_node][node]
                    distance_value = distances[current_node][node]
                    probability = (pheromone_value ** alpha) * ((1 / distance_value) ** beta)
                    probabilities.append((node, probability))

            total_probability = sum(prob for _, prob in probabilities)
            probabilities = [(node, prob / total_probability) for node, prob in probabilities]

            roulette_wheel = random.random()
            probability_sum = 0

            for node, probability in probabilities:
                probability_sum += probability
                if probability_sum >= roulette_wheel:
                    next_node = node
                    break

            visited.add(next_node)
            solution.append(next_node)
            current_node = next_node

        # Ajouter le retour au dépôt central à la fin du trajet
        solution.append(0)

        solutions.append(solution)

    # Évaluation des solutions et mise à jour de la meilleure solution
    for solution in solutions:
        distance = evaluate_solution(solution, distances)
        if distance < best_distance:
            best_solution = solution
            best_distance = distance

    # Mise à jour des phéromones
    pheromone *= evaporation_rate  # Évaporation des phéromones existantes

    for solution in solutions:
        delta_pheromone = 1 / evaluate_solution(solution, distances)
        for i in range(num_nodes - 1):
            node1 = solution[i]
            node2 = solution[i + 1]
            pheromone[node1][node2] += delta_pheromone
            pheromone[node2][node1] += delta_pheromone

# Séparer la meilleure solution en trajets pour chaque camion
truck_solutions = []
num_nodes_per_truck = num_nodes // num_trucks

for i in range(num_trucks):
    start_index = i * num_nodes_per_truck
    end_index = start_index + num_nodes_per_truck
    truck_solution = best_solution[start_index:end_index]
    truck_solutions.append(truck_solution + [0])
    

return truck_solutions, best_distance

num_ants = 10 num_iterations = 100 evaporation_rate = 0.5 alpha = 1 beta = 1

best_truck_solutions,best_distance = ant_colony_optimization(distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks)

Pour chaque solution de camion, nous devons trouver le chemin le plus court du dépôt au premier nœud du chemin

for i, truck_solution in enumerate(best_truck_solutions): # Créer un graphe à partir de la matrice de distance G = nx.from_numpy_matrix(distance_matrix)

# Utiliser l'algorithme de Dijkstra pour trouver le chemin le plus court du dépôt (nœud 0) au premier nœud du chemin du camion
shortest_path = nx.dijkstra_path(G, 0, truck_solution[1])

# Remplacer le premier nœud du chemin du camion par le chemin le plus court trouvé
best_truck_solutions[i] = [0] + shortest_path + truck_solution[2:]

Calculer la distance totale pour chaque solution de camion

total_distances = [evaluate_solution(truck_solution, distance_matrix) for truck_solution in best_truck_solutions]

Afficher les solutions et les distances totales pour chaque camion

for i, (truck_solution, total_distance) in enumerate(zip(best_truck_solutions, total_distances)): print(f"Camion {i+1} :") print("Solution :", truck_solution) print("Distance totale :", total_distance) print()

En rajoutant la contrainte de Time Window pour une instance de VRPTW

Dans un premier temps on va attribuer des fenêtres de temps pour chaque clients (ville dans notre graphe)

In [7]:
def assign_time_windows(graph):
    # Créer un dictionnaire pour stocker les fenêtres de temps des clients
    time_windows = {}

    # Définir la fenêtre de temps pour le dépôt central (nœud 0)
    time_windows[0] = (0, float('inf'))

    # Assigner une fenêtre de temps à chaque client
    for node in graph.nodes():
        if node !=0 and node != graph.number_of_nodes() :
            # Générer une fenêtre de temps aléatoire pour chaque client
            start_time = random.randint(0, 100)
            end_time = start_time + random.randint(10, 50)
            time_windows[node] = (start_time, end_time)
           

    return time_windows

# Attribuer les fenêtres de temps aux clients
time_windows = assign_time_windows(graph)

print(max(graph.nodes()))
# Afficher les fenêtres de temps assignées
for node, window in time_windows.items():
    print("Client", node, ":", window)
    
#paramètres ACO

print(graph.nodes())
print(graph.edges())
20
Client 0 : (0, inf)
Client 1 : (44, 94)
Client 2 : (99, 111)
Client 3 : (80, 125)
Client 4 : (85, 96)
Client 5 : (71, 85)
Client 6 : (34, 69)
Client 7 : (20, 30)
Client 8 : (46, 56)
Client 9 : (90, 109)
Client 10 : (74, 116)
Client 11 : (95, 112)
Client 12 : (93, 131)
Client 13 : (46, 82)
Client 14 : (86, 124)
Client 15 : (87, 129)
Client 16 : (13, 23)
Client 17 : (0, 28)
Client 18 : (77, 108)
Client 19 : (76, 105)
Client 20 : (78, 94)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
[(0, 19), (0, 16), (0, 5), (0, 10), (1, 18), (1, 8), (1, 11), (2, 18), (2, 6), (2, 3), (2, 9), (2, 10), (2, 14), (2, 17), (3, 7), (3, 8), (3, 16), (4, 15), (4, 5), (4, 13), (5, 8), (5, 7), (6, 16), (6, 19), (7, 15), (7, 19), (8, 13), (9, 20), (10, 15), (11, 14), (11, 18), (12, 15), (12, 17), (12, 16), (13, 19), (13, 17), (13, 20), (14, 20), (14, 15), (15, 18), (15, 20)]

On va ensuite réadapter l'algorithme pour prendre en compte les time Windows

In [15]:
import concurrent.futures
import random
import numpy as np
import time
import heapq


# Fonction d'évaluation de la qualité d'une solution (ici, la distance totale)
def evaluate_solution(solution, distances, time_windows, dijkstra_distances):
    total_distance = 0
    total_delay = 0
    arrival_time = 0
    num_nodes = len(solution)
    waiting_times = []

    total_distance += dijkstra_distances[solution[1]]

    for i in range(num_nodes - 1):
        current_node = solution[i]
        next_node = solution[i + 1]
        total_distance += distances[current_node][next_node]
        arrival_time += distances[current_node][next_node]

        if arrival_time < time_windows[next_node][0]:
            waiting_time = time_windows[next_node][0] - arrival_time
            waiting_times.append((next_node, waiting_time))
            arrival_time = time_windows[next_node][0]
        elif arrival_time > time_windows[next_node][1]:
            total_delay += arrival_time - time_windows[next_node][1]

    # Ajouter la distance de retour au dépôt
    total_distance += distances[solution[-1]][solution[0]]

    return total_distance, total_delay, waiting_times

    
# Fonction pour l'algorithme de Dijkstra
def calculate_dijkstra(start_node, distances):
    num_nodes = len(distances)
    shortest_distances = [float('inf')] * num_nodes
    shortest_distances[start_node] = 0
    shortest_paths = [[] for _ in range(num_nodes)]
    shortest_paths[start_node] = [start_node]
    priority_queue = [(0, start_node)]
    while priority_queue:
        current_distance, current_node = heapq.heappop(priority_queue)
        if current_distance == shortest_distances[current_node]:
            for neighbor, distance in enumerate(distances[current_node]):
                new_distance = current_distance + distance
                if new_distance < shortest_distances[neighbor]:
                    shortest_distances[neighbor] = new_distance
                    shortest_paths[neighbor] = shortest_paths[current_node] + [neighbor]
                    heapq.heappush(priority_queue, (new_distance, neighbor))
    return shortest_distances, shortest_paths


dijkstra_distances, dijkstra_paths = calculate_dijkstra(0, distance_matrix)


def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):
    num_nodes = len(distances)
    pheromone = np.ones((num_nodes, num_nodes))  
    best_solution = None
    best_distance = float('inf')
    best_delay = float('inf')
    best_score = float('inf')  
    best_waiting_times = None

    for iteration in range(num_iterations):
        solutions = []
        for ant in range(num_ants):
            visited = set()
            current_node = random.randint(0, num_nodes - 1)
            visited.add(current_node)
            solution = [current_node]
            while len(visited) < num_nodes:
                next_node = None
                probabilities = []
                arrival_time = 0
                for node in range(num_nodes):
                    if node not in visited and (node !=0 or len(visited) == num_nodes - 1):
                        pheromone_value = pheromone[current_node][node]
                        distance_value = distances[current_node][node]
                        wait_time = max(0, time_windows[node][0]- (arrival_time + distance_value))
                        probability = (pheromone_value ** alpha) * ((1 / (distance_value + wait_time)) ** beta)
                        probabilities.append((node, probability))
                total_probability = sum(prob for _, prob in probabilities)
                probabilities = [(node, prob / total_probability) for node, prob in probabilities]
                roulette_wheel = random.random()
                probability_sum = 0
                for node, probability in probabilities:
                    probability_sum += probability
                    if probability_sum >= roulette_wheel:
                        next_node = node
                        break
                visited.add(next_node)
                solution.append(next_node)
                current_node = next_node
            solution.append(0)
            solutions.append(solution)

        for solution in solutions:
            distance, delay, waiting_times = evaluate_solution(solution, distances, time_windows, dijkstra_distances)
            score = distance + delay
            if score < best_score:
                best_solution = solution
                best_distance = distance
                best_delay = delay
                best_waiting_times = waiting_times
        pheromone *= evaporation_rate
        for solution in solutions:
            delta_pheromone = 1 / (evaluate_solution(solution, distances, time_windows, dijkstra_distances)[0] + 0.01)
            for i in range(num_nodes - 1):
                node1 = solution[i]
                node2 = solution[i + 1]
                pheromone[node1][node2] += delta_pheromone
                pheromone[node2][node1] += delta_pheromone
    truck_solutions = []
    num_nodes_per_truck = num_nodes // num_trucks
    for i in range(num_trucks):
        start_index = i * num_nodes_per_truck
        end_index = start_index + num_nodes_per_truck
        truck_solution = best_solution[start_index:end_index]
    
        # Ajoutez le chemin le plus court entre le nœud 0 et le premier nœud du chemin
        dijkstra_path_to_first_node = dijkstra_paths[truck_solution[0]]
        truck_solution = dijkstra_path_to_first_node + truck_solution
    
    # Ajoutez le chemin le plus court entre le dernier nœud du chemin et le dépôt (nœud 0)
        dijkstra_path_to_depot = dijkstra_paths[truck_solution[-1]]
        truck_solution = truck_solution + dijkstra_path_to_depot

        truck_solutions.append(truck_solution)
    return truck_solutions, best_distance, best_waiting_times
In [16]:
num_thread = 1
num_ants = 10
num_iterations = 100
evaporation_rate = 0.5
alpha = 1
beta = 1

num_trucks = 3  # Nombre de camions disponibles



with concurrent.futures.ThreadPoolExecutor(max_workers=num_thread) as executor:
    futures = [executor.submit(ant_colony_optimization, distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks) for _ in range(num_thread)]

    for future in concurrent.futures.as_completed(futures):
        best_truck_solutions, best_distance, best_waiting_times = future.result()
        print("Meilleures solutions :")
        for i, truck_solution in enumerate(best_truck_solutions):
            print(f"Camion {i+1} : {truck_solution}")
        print("Distance totale :", best_distance)
Meilleures solutions :
Camion 1 : [0, 9, 9, 8, 17, 7, 1, 13, 14, 0, 14]
Camion 2 : [0, 12, 12, 18, 4, 5, 20, 19, 15, 0, 15]
Camion 3 : [0, 10, 10, 3, 2, 16, 6, 11, 0, 0]
Distance totale : 48.0