{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "83c2b1e7-b401-4a15-adf0-d43cebf9ad81",
   "metadata": {},
   "source": [
    "# **Algorithm project** # \n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "0b4f92e5-ac40-4491-983d-890c4f0f6694",
   "metadata": {},
   "source": [
    "## <u>Project context </u>\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5f483b31-1246-4f00-ae39-718ef92ce9eb",
   "metadata": {},
   "source": [
    "The French Environment and Energy Management Agency (ADEME) has launched a call for expressions of interest to develop mobility solutions adapted to different territories. CesiCDP, in collaboration with partners, specializes in Intelligent Multimodal Mobility. As part of this call, the CesiCDP team is working on the management of delivery routes to "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "fc23daaf-9f25-4c5a-bf6c-300a7ed8d623",
   "metadata": {},
   "source": [
    "Our aim is to develop an algorithm that will enable us to pass through all the delivery points in an optimized time."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "d633beb7-8f26-46d4-9cd9-1d0093e5b5c3",
   "metadata": {},
   "source": [
    "## <u>Selected constraint</u>\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "35fc1c3c-d7a9-4423-a948-aa00ab51dbf4",
   "metadata": {},
   "source": [
    "The constraint we chose was the following:\n",
    "- To have several trucks available simultaneously to make deliveries."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "ba356166-604a-4627-ac0f-93b76eb7a22d",
   "metadata": {},
   "source": [
    "## <u>Formulation of the problem </u>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "c4d6888b-14e6-4745-880f-0a063ebf7476",
   "metadata": {},
   "source": [
    "Consider a graph $G=(V,E)$, where $V$ is the set of cities (or delivery points) and $E$ is the set of roads between cities. There are $k$ trucks available to make deliveries.\n",
    "\n",
    "The problem is to find a route for each truck, so that all deliveries are made in the shortest possible time, both to and from the depot.\n",
    "\n",
    "The problem we have with the above constraints is the VRP (Vehicle Routing Problem)."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "6d392f68",
   "metadata": {},
   "source": [
    "## <u> Problem constraints </u>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "f95dba22",
   "metadata": {},
   "source": [
    "List of problem constraints:\n",
    "\n",
    "- All customers must be served\n",
    "- A customer can only be served by one vehicle.\n",
    "- When leaving a customer, a vehicle can only go to one other customer.\n",
    "\n",
    "We will therefore assign each customer to a route served by a single vehicle."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "c1ca5507",
   "metadata": {},
   "source": [
    "## <u>Demonstrating the complexity of the vehicle routing problem (VRP) </u>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "37632b4b",
   "metadata": {},
   "source": [
    "#### **Introduction**\n",
    "\n",
    "The Vehicle Routing Problem (VRP) is an extension of the Traveling Salesman Problem (TSP), and is known to be an NP-hard problem. We will demonstrate the complexity of VRP based on the Hamiltonian chain problem, which is known to be NP-complete."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "6a63522a",
   "metadata": {},
   "source": [
    "#### **Preliminary definitions**\n",
    "\n",
    "- Hamiltonian chain problem**: the problem consists in determining whether a given undirected graph has a Hamiltonian chain, i.e. a path that visits each vertex exactly once.\n",
    "\n",
    "- Traveling Salesman Problem (TSP)**: the problem consists in finding the shortest possible circuit that visits each city in a given set of cities and returns to the original city.\n",
    "\n",
    "- Vehicle Routing Problem (VRP)**: the problem consists in delivering a series of customers with several vehicles while minimizing the total cost, such as delivery time or distance traveled."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "589a1874",
   "metadata": {},
   "source": [
    "#### **Proof of the complexity of the TSP**.\n",
    "\n",
    "The TSP is an extension of the Hamiltonian chain problem. In fact, a special case of the TSP is the Hamiltonian chain problem, in which all edges have the same weight (or cost). In this case, finding the shortest circuit in the TSP is equivalent to finding a Hamiltonian chain in the graph. Since the Hamiltonian chain problem is NP-complete, the TSP must be at least as difficult, so the TSP is NP-complete."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "92658e81",
   "metadata": {},
   "source": [
    "#### **Proof of the complexity of the VRP**.\n",
    "\n",
    "The VRP is an extension of the TSP. In fact, a special case of the VRP is the TSP where there is only one vehicle available to make deliveries. In this case, finding the solution to the VRP is the same as finding the solution to the TSP.\n",
    "\n",
    "Since the TSP is NP-complete, the VRP must be at least as difficult, so the VRP is NP-difficult. Furthermore, VRP introduces additional constraints, such as multiple vehicles and potentially vehicle capacities and time windows, making it even more complex."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "a4106346",
   "metadata": {},
   "source": [
    "#### **Conclusion**\n",
    "\n",
    "In conclusion, we have shown that VRP is an NP-hard problem by reducing it to the NP-complete TSP problem, which in turn can be reduced to the NP-complete Hamiltonian chain problem. This demonstration highlights the difficulty of solving the VRP, particularly for large instances. In practice, (meta)heuristic and approximate methods are often used to solve the VRP, as we shall see later."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "16b3b8a7-4658-4509-a511-7a395654e6f1",
   "metadata": {},
   "source": [
    "## <u> Mathematical modeling </u>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "b54bfa8c",
   "metadata": {},
   "source": [
    "#### **Set and parameters**"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "637eb295",
   "metadata": {},
   "source": [
    "$V=\\{0,1,2,...,n_v\\}$ : the set of cities, where 0 is the base (or depot), $1,2,...,n_v$ are the delivery cities. $n_v+1$ will be the depot for the return. <br>\n",
    "$K=\\{1,2,...,k\\}$ : all trucks. <br>\n",
    "$E$ represents the arcs between two customers $i,j \\in V$ <br>\n",
    "$G=(V,E)$ : the graph with V as vertices (cities) and E as edges <br>\n",
    "$d_{ij}$​ : distance (or travel time) from city i to city $j$ (travel cost) <br>\n",
    "$M$ : a great constant."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "1219e4f2",
   "metadata": {},
   "source": [
    "#### **Decision variables**"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "a6d398fa",
   "metadata": {},
   "source": [
    "$x_{ijk}$​ : binary variable worth 1 if truck $k$ moves from city $i$ to city $j$, and 0 otherwise."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "e635cf14",
   "metadata": {},
   "source": [
    "#### **Objective function**"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "56652859",
   "metadata": {},
   "source": [
    "$$\\min \\sum_{k∈K} \\sum_{⁡i∈V} \\sum_{⁡j∈V} d_{ij} x_{ijk} $$"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "a1465000",
   "metadata": {},
   "source": [
    "#### **VRP constraints**\n",
    "\n",
    "- Each city is visited once and only once:\n",
    "$$\\sum_{⁡k \\in K} \\sum_{⁡j \\in V} x_{ijk} = 1, \\forall i \\in V, i \\ne 0$$\n",
    "\n",
    "- If a truck visits a city, it must leave it:\n",
    "$$\\sum_{i \\in V} x_{ijk} = \\sum_{j \\in V} x_{ijk}, \\forall k \\in K, \\forall i \\in V, \\forall j \\in V $$\n",
    "<br> \n",
    "\n",
    "- Sub-tour elimination constraint (to ensure that each truck completes a full tour) :\n",
    "$$\\sum_{i \\in S, j \\notin S} x_{ijk} \\geq 1, \\forall k \\in K, \\forall \\; subset \\; S \\; de \\; V, 0 \\in S, S \\ne V $$"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7670fdf4-884c-4352-83fa-eed0c8b50074",
   "metadata": {},
   "source": [
    "## <u>Resolution algorithm </u>"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "45133ac2",
   "metadata": {},
   "source": [
    "#### Importing the necessary libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "794657d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.cluster import KMeans\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import random, time, math\n",
    "from tests.clustering import split_tour_across_clusters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "c177ac22",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "module 'networkx.linalg.graphmatrix' has no attribute 'to_numpy_matrix'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "Cell \u001b[1;32mIn[17], line 41\u001b[0m\n\u001b[0;32m     38\u001b[0m     \u001b[39mreturn\u001b[39;00m adjacency_matrix\n\u001b[0;32m     40\u001b[0m \u001b[39m# Générer la matrice d'adjacence pondérée\u001b[39;00m\n\u001b[1;32m---> 41\u001b[0m weighted_adjacency_matrix \u001b[39m=\u001b[39m generate_weighted_adjacency_matrix(graph)\n\u001b[0;32m     43\u001b[0m \u001b[39m# Afficher la matrice d'adjacence pondérée\u001b[39;00m\n\u001b[0;32m     44\u001b[0m \u001b[39mprint\u001b[39m(weighted_adjacency_matrix)\n",
      "Cell \u001b[1;32mIn[17], line 36\u001b[0m, in \u001b[0;36mgenerate_weighted_adjacency_matrix\u001b[1;34m(graph)\u001b[0m\n\u001b[0;32m     34\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mgenerate_weighted_adjacency_matrix\u001b[39m(graph):\n\u001b[0;32m     35\u001b[0m     \u001b[39m# Créer une matrice d'adjacence pondérée à partir du graphe\u001b[39;00m\n\u001b[1;32m---> 36\u001b[0m     adjacency_matrix \u001b[39m=\u001b[39m graphmatrix\u001b[39m.\u001b[39;49mto_numpy_matrix(graph, weight\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39mdistance\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[0;32m     38\u001b[0m     \u001b[39mreturn\u001b[39;00m adjacency_matrix\n",
      "\u001b[1;31mAttributeError\u001b[0m: module 'networkx.linalg.graphmatrix' has no attribute 'to_numpy_matrix'"
     ]
    }
   ],
   "source": [
    "\n",
    "import random\n",
    "import numpy as np\n",
    "import networkx as nx\n",
    "import matplotlib.pyplot as plt\n",
    "import time as time\n",
    "\n",
    "from networkx.linalg import graphmatrix\n",
    "\n",
    "def generate_graph(num_nodes):\n",
    "    G = nx.Graph()\n",
    "    G.add_nodes_from(range(num_nodes + 1))\n",
    "    \n",
    "    for node in G.nodes():\n",
    "        connected_nodes = sorted(set(G.nodes()) - {node})\n",
    "        if len(connected_nodes) < 2:\n",
    "            continue\n",
    "        distance1 = random.randint(1, 10)\n",
    "        distance2 = random.randint(1, 10)\n",
    "        random_nodes = random.sample(connected_nodes, 2)\n",
    "        G.add_edges_from([(node, random_nodes[0], {'distance': distance1}),\n",
    "                          (node, random_nodes[1], {'distance': distance2})])\n",
    "\n",
    "    while not nx.is_connected(G):\n",
    "        node1, node2 = random.sample(G.nodes(), 2)\n",
    "        if not G.has_edge(node1, node2):\n",
    "            distance = random.randint(1, 10)\n",
    "            G.add_edge(node1, node2, distance=distance)\n",
    "\n",
    "    return G\n",
    "\n",
    "graph = generate_graph(12)\n",
    "A = nx.adjacency_matrix(graph)\n",
    "\n",
    "def generate_weighted_adjacency_matrix(graph):\n",
    "    # Créer une matrice d'adjacence pondérée à partir du graphe\n",
    "    adjacency_matrix = graphmatrix.to_numpy_matrix(graph, weight='distance')\n",
    "\n",
    "    return adjacency_matrix\n",
    "\n",
    "# Générer la matrice d'adjacence pondérée\n",
    "weighted_adjacency_matrix = generate_weighted_adjacency_matrix(graph)\n",
    "\n",
    "# Afficher la matrice d'adjacence pondérée\n",
    "print(weighted_adjacency_matrix)\n",
    "\n",
    "\n",
    "def generate_distance_matrix(graph):\n",
    "    num_nodes = graph.number_of_nodes()\n",
    "    distance_array = np.full((num_nodes, num_nodes), float('inf'))  # Initialiser avec l'infini\n",
    "    for edge in graph.edges(data=True):\n",
    "        i, j, data = edge\n",
    "        distance_array[i][j] = data['distance']\n",
    "        distance_array[j][i] = data['distance']  # Pour un graphe non orienté\n",
    "    np.fill_diagonal(distance_array, 0)  # Remplir la diagonale avec des zéros\n",
    "    return distance_array\n",
    "\n",
    "# Générer la matrice de distances\n",
    "distance_matrix = generate_distance_matrix(graph)\n",
    "\n",
    "\n",
    "# Afficher la matrice de distances\n",
    "print(distance_matrix)\n",
    "\n",
    "\n",
    "\n",
    "# Dessiner le graphe\n",
    "nx.draw(graph, with_labels=True)\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "9aac4453",
   "metadata": {},
   "source": [
    "# On applique l'algorithme des fourmis (ACO) sur notre graphe\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "23e32e2a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Meilleure solution : [[6, 5, 4, 8, 1, 11, 0], [9, 3, 7, 12, 10, 2, 0]]\n",
      "Distance totale : 81.0\n"
     ]
    }
   ],
   "source": [
    "import concurrent.futures\n",
    "num_trucks = 2  # Nombre de camions disponibles\n",
    "\n",
    "\n",
    "# Fonction d'évaluation de la qualité d'une solution (ici, la distance totale)\n",
    "def evaluate_solution(solution, distances):\n",
    "    total_distance = 0\n",
    "    num_nodes = len(solution)\n",
    "\n",
    "    for i in range(num_nodes - 1):\n",
    "        current_node = solution[i]\n",
    "        next_node = solution[i + 1]\n",
    "        total_distance += distances[current_node][next_node]\n",
    "\n",
    "\n",
    "    # Ajouter la distance de retour au dépôt\n",
    "    total_distance += distances[solution[-1]][solution[0]]\n",
    "\n",
    "    return total_distance\n",
    "\n",
    "    \n",
    "def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):\n",
    "    num_nodes = len(distances)\n",
    "    \n",
    "    pheromone = np.ones((num_nodes, num_nodes))  # Matrice de phéromones initiale\n",
    "    best_solution = None\n",
    "    best_distance = float('inf')\n",
    "\n",
    "    start_time = time.time()\n",
    "\n",
    "    for iteration in range(num_iterations):\n",
    "        # Construction de solutions par les fourmis\n",
    "        solutions = []\n",
    "\n",
    "        for ant in range(num_ants):\n",
    "            visited = set()\n",
    "            current_node = random.randint(0, num_nodes - 1)\n",
    "            visited.add(current_node)\n",
    "            solution = [current_node]\n",
    "\n",
    "            while len(visited) < num_nodes:\n",
    "                next_node = None\n",
    "                probabilities = []\n",
    "\n",
    "                # Calcul des probabilités de choisir chaque prochain nœud\n",
    "                for node in range(num_nodes):\n",
    "                    if node not in visited and (node !=0 or len(visited) == num_nodes - 1):\n",
    "                        pheromone_value = pheromone[current_node][node]\n",
    "                        distance_value = distances[current_node][node]\n",
    "                        probability = (pheromone_value ** alpha) * ((1 / distance_value) ** beta)\n",
    "                        probabilities.append((node, probability))\n",
    "\n",
    "                total_probability = sum(prob for _, prob in probabilities)\n",
    "                probabilities = [(node, prob / total_probability) for node, prob in probabilities]\n",
    "\n",
    "            \n",
    "                roulette_wheel = random.random()\n",
    "                probability_sum = 0\n",
    "\n",
    "\n",
    "                for node, probability in probabilities:\n",
    "                    \n",
    "                    probability_sum += probability\n",
    "                    if probability_sum >= roulette_wheel:\n",
    "                        next_node = node\n",
    "                        break\n",
    "\n",
    "                visited.add(next_node)\n",
    "                solution.append(next_node)\n",
    "                current_node = next_node\n",
    "\n",
    "            # Ajouter le retour au dépôt central à la fin du trajet\n",
    "            solution.append(0)\n",
    "\n",
    "            solutions.append(solution)\n",
    "\n",
    "        # Évaluation des solutions et mise à jour de la meilleure solution\n",
    "        for solution in solutions:\n",
    "            distance = evaluate_solution(solution, distances)\n",
    "            if distance < best_distance:\n",
    "                best_solution = solution\n",
    "                best_distance = distance\n",
    "\n",
    "        # Mise à jour des phéromones\n",
    "        pheromone *= evaporation_rate  # Évaporation des phéromones existantes\n",
    "\n",
    "        for solution in solutions:\n",
    "            delta_pheromone = 1 / evaluate_solution(solution, distances)\n",
    "            for i in range(num_nodes - 1):\n",
    "                node1 = solution[i]\n",
    "                node2 = solution[i + 1]\n",
    "                pheromone[node1][node2] += delta_pheromone\n",
    "                pheromone[node2][node1] += delta_pheromone\n",
    "\n",
    "    # Séparer la meilleure solution en trajets pour chaque camion\n",
    "    truck_solutions = []\n",
    "    num_nodes_per_truck = num_nodes // num_trucks\n",
    "\n",
    "    for i in range(num_trucks):\n",
    "        start_index = i * num_nodes_per_truck\n",
    "        end_index = start_index + num_nodes_per_truck\n",
    "        truck_solution = best_solution[start_index:end_index]\n",
    "        truck_solutions.append(truck_solution + [0])\n",
    "        \n",
    "\n",
    "    return truck_solutions, best_distance\n",
    "\n",
    "\n",
    "def trucks_thread(i, num_nodes_per_truck, best_solution, truck_solutions):\n",
    "    start_index = i * num_nodes_per_truck\n",
    "    end_index = start_index + num_nodes_per_truck\n",
    "    truck_solution = best_solution[start_index:end_index]\n",
    "    truck_solutions.append(truck_solution)\n",
    "    return truck_solutions\n",
    "\n",
    "\n",
    "num_ants = 10\n",
    "num_iterations = 100\n",
    "evaporation_rate = 0.5\n",
    "alpha = 1\n",
    "beta = 1\n",
    "\n",
    "best_truck_solutions, best_distance = ant_colony_optimization(distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks)\n",
    "\n",
    "\n",
    "print(\"Meilleure solution :\", best_truck_solutions)\n",
    "print(\"Distance totale :\", best_distance)\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "6928bbbd",
   "metadata": {},
   "outputs": [
    {
     "ename": "AttributeError",
     "evalue": "module 'networkx' has no attribute 'from_numpy_matrix'",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
      "Cell \u001b[1;32mIn[11], line 114\u001b[0m\n\u001b[0;32m    111\u001b[0m \u001b[39m# Pour chaque solution de camion, nous devons trouver le chemin le plus court du dépôt au premier nœud du chemin\u001b[39;00m\n\u001b[0;32m    112\u001b[0m \u001b[39mfor\u001b[39;00m i, truck_solution \u001b[39min\u001b[39;00m \u001b[39menumerate\u001b[39m(best_truck_solutions):\n\u001b[0;32m    113\u001b[0m     \u001b[39m# Créer un graphe à partir de la matrice de distance\u001b[39;00m\n\u001b[1;32m--> 114\u001b[0m     G \u001b[39m=\u001b[39m nx\u001b[39m.\u001b[39;49mfrom_numpy_matrix(distance_matrix)\n\u001b[0;32m    116\u001b[0m     \u001b[39m# 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\u001b[39;00m\n\u001b[0;32m    117\u001b[0m     shortest_path \u001b[39m=\u001b[39m nx\u001b[39m.\u001b[39mdijkstra_path(G, \u001b[39m0\u001b[39m, truck_solution[\u001b[39m1\u001b[39m])\n",
      "\u001b[1;31mAttributeError\u001b[0m: module 'networkx' has no attribute 'from_numpy_matrix'"
     ]
    }
   ],
   "source": [
    "num_trucks = 2  # Nombre de camions disponibles\n",
    "\n",
    "def evaluate_solution(solution, distances):\n",
    "    total_distance = 0\n",
    "    num_nodes = len(solution)\n",
    "\n",
    "    # Calculer la distance entre chaque paire consécutive de nœuds dans la solution\n",
    "    for i in range(num_nodes - 1):\n",
    "        current_node = solution[i]\n",
    "        next_node = solution[i + 1]\n",
    "        total_distance += distances[current_node][next_node]\n",
    "\n",
    "    # Ajouter la distance de retour au dépôt\n",
    "    total_distance += distances[solution[-1]][solution[0]]\n",
    "\n",
    "    return total_distance\n",
    "\n",
    "def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):\n",
    "    num_nodes = len(distances)\n",
    "    \n",
    "    pheromone = np.ones((num_nodes, num_nodes))  # Matrice de phéromones initiale\n",
    "    best_solution = None\n",
    "    best_distance = float('inf')\n",
    "\n",
    "    for iteration in range(num_iterations):\n",
    "        # Construction de solutions par les fourmis\n",
    "        solutions = []\n",
    "\n",
    "        for ant in range(num_ants):\n",
    "            visited = set()\n",
    "            current_node = 0  # Initialiser current_node à 0\n",
    "            visited.add(current_node)\n",
    "            solution = [current_node]\n",
    "\n",
    "            while len(visited) < num_nodes:\n",
    "                next_node = None\n",
    "                probabilities = []\n",
    "\n",
    "                # Calcul des probabilités de choisir chaque prochain nœud\n",
    "                for node in range(num_nodes):\n",
    "                    if node not in visited and node < len(pheromone) and current_node < len(pheromone):\n",
    "                        pheromone_value = pheromone[current_node][node]\n",
    "                        distance_value = distances[current_node][node]\n",
    "                        probability = (pheromone_value ** alpha) * ((1 / distance_value) ** beta)\n",
    "                        probabilities.append((node, probability))\n",
    "\n",
    "                total_probability = sum(prob for _, prob in probabilities)\n",
    "                probabilities = [(node, prob / total_probability) for node, prob in probabilities]\n",
    "\n",
    "                roulette_wheel = random.random()\n",
    "                probability_sum = 0\n",
    "\n",
    "                for node, probability in probabilities:\n",
    "                    probability_sum += probability\n",
    "                    if probability_sum >= roulette_wheel:\n",
    "                        next_node = node\n",
    "                        break\n",
    "\n",
    "                visited.add(next_node)\n",
    "                solution.append(next_node)\n",
    "                current_node = next_node\n",
    "\n",
    "            # Ajouter le retour au dépôt central à la fin du trajet\n",
    "            solution.append(0)\n",
    "\n",
    "            solutions.append(solution)\n",
    "\n",
    "        # Évaluation des solutions et mise à jour de la meilleure solution\n",
    "        for solution in solutions:\n",
    "            distance = evaluate_solution(solution, distances)\n",
    "            if distance < best_distance:\n",
    "                best_solution = solution\n",
    "                best_distance = distance\n",
    "\n",
    "        # Mise à jour des phéromones\n",
    "        pheromone *= evaporation_rate  # Évaporation des phéromones existantes\n",
    "\n",
    "        for solution in solutions:\n",
    "            delta_pheromone = 1 / evaluate_solution(solution, distances)\n",
    "            for i in range(num_nodes - 1):\n",
    "                node1 = solution[i]\n",
    "                node2 = solution[i + 1]\n",
    "                pheromone[node1][node2] += delta_pheromone\n",
    "                pheromone[node2][node1] += delta_pheromone\n",
    "\n",
    "    # Séparer la meilleure solution en trajets pour chaque camion\n",
    "    truck_solutions = []\n",
    "    num_nodes_per_truck = num_nodes // num_trucks\n",
    "\n",
    "    for i in range(num_trucks):\n",
    "        start_index = i * num_nodes_per_truck\n",
    "        end_index = start_index + num_nodes_per_truck\n",
    "        truck_solution = best_solution[start_index:end_index]\n",
    "        truck_solutions.append(truck_solution + [0])\n",
    "        \n",
    "\n",
    "    return truck_solutions, best_distance\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "num_ants = 10\n",
    "num_iterations = 100\n",
    "evaporation_rate = 0.5\n",
    "alpha = 1\n",
    "beta = 1\n",
    "\n",
    "best_truck_solutions,best_distance = ant_colony_optimization(distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks)\n",
    "\n",
    "# Pour chaque solution de camion, nous devons trouver le chemin le plus court du dépôt au premier nœud du chemin\n",
    "for i, truck_solution in enumerate(best_truck_solutions):\n",
    "    # Créer un graphe à partir de la matrice de distance\n",
    "    G = nx.from_numpy_matrix(distance_matrix)\n",
    "    \n",
    "    # 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\n",
    "    shortest_path = nx.dijkstra_path(G, 0, truck_solution[1])\n",
    "    \n",
    "    # Remplacer le premier nœud du chemin du camion par le chemin le plus court trouvé\n",
    "    best_truck_solutions[i] = [0] + shortest_path + truck_solution[2:]\n",
    "\n",
    "# Calculer la distance totale pour chaque solution de camion\n",
    "total_distances = [evaluate_solution(truck_solution, distance_matrix) for truck_solution in best_truck_solutions]\n",
    "\n",
    "# Afficher les solutions et les distances totales pour chaque camion\n",
    "for i, (truck_solution, total_distance) in enumerate(zip(best_truck_solutions, total_distances)):\n",
    "    print(f\"Camion {i+1} :\")\n",
    "    print(\"Solution :\", truck_solution)\n",
    "    print(\"Distance totale :\", total_distance)\n",
    "    print()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "421c05e1",
   "metadata": {},
   "source": []
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "a6ffa6c9",
   "metadata": {},
   "source": [
    "\n",
    "def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):\n",
    "    num_nodes = len(distances)\n",
    "    \n",
    "    pheromone = np.ones((num_nodes, num_nodes))  # Matrice de phéromones initiale\n",
    "    best_solution = None\n",
    "    best_distance = float('inf')\n",
    "\n",
    "    for iteration in range(num_iterations):\n",
    "        # Construction de solutions par les fourmis\n",
    "        solutions = []\n",
    "\n",
    "        for ant in range(num_ants):\n",
    "            visited = set()\n",
    "            current_node = 0  # Tous les camions partent du dépôt\n",
    "            visited.add(current_node)\n",
    "            solution = [current_node]\n",
    "\n",
    "            while len(visited) < num_nodes:\n",
    "                next_node = None\n",
    "                probabilities = []\n",
    "\n",
    "                # Calcul des probabilités de choisir chaque prochain nœud\n",
    "                for node in range(num_nodes):\n",
    "                    if node not in visited:\n",
    "                        pheromone_value = pheromone[current_node][node]\n",
    "                        distance_value = distances[current_node][node]\n",
    "                        probability = (pheromone_value ** alpha) * ((1 / distance_value) ** beta)\n",
    "                        probabilities.append((node, probability))\n",
    "\n",
    "                total_probability = sum(prob for _, prob in probabilities)\n",
    "                probabilities = [(node, prob / total_probability) for node, prob in probabilities]\n",
    "\n",
    "                roulette_wheel = random.random()\n",
    "                probability_sum = 0\n",
    "\n",
    "                for node, probability in probabilities:\n",
    "                    probability_sum += probability\n",
    "                    if probability_sum >= roulette_wheel:\n",
    "                        next_node = node\n",
    "                        break\n",
    "\n",
    "                visited.add(next_node)\n",
    "                solution.append(next_node)\n",
    "                current_node = next_node\n",
    "\n",
    "            # Ajouter le retour au dépôt central à la fin du trajet\n",
    "            solution.append(0)\n",
    "\n",
    "            solutions.append(solution)\n",
    "\n",
    "        # Évaluation des solutions et mise à jour de la meilleure solution\n",
    "        for solution in solutions:\n",
    "            distance = evaluate_solution(solution, distances)\n",
    "            if distance < best_distance:\n",
    "                best_solution = solution\n",
    "                best_distance = distance\n",
    "\n",
    "        # Mise à jour des phéromones\n",
    "        pheromone *= evaporation_rate  # Évaporation des phéromones existantes\n",
    "\n",
    "        for solution in solutions:\n",
    "            delta_pheromone = 1 / evaluate_solution(solution, distances)\n",
    "            for i in range(num_nodes - 1):\n",
    "                node1 = solution[i]\n",
    "                node2 = solution[i + 1]\n",
    "                pheromone[node1][node2] += delta_pheromone\n",
    "                pheromone[node2][node1] += delta_pheromone\n",
    "\n",
    "    # Séparer la meilleure solution en trajets pour chaque camion\n",
    "    truck_solutions = []\n",
    "    num_nodes_per_truck = num_nodes // num_trucks\n",
    "\n",
    "    for i in range(num_trucks):\n",
    "        start_index = i * num_nodes_per_truck\n",
    "        end_index = start_index + num_nodes_per_truck\n",
    "        truck_solution = best_solution[start_index:end_index]\n",
    "        truck_solutions.append(truck_solution + [0])\n",
    "        \n",
    "\n",
    "    return truck_solutions, best_distance\n",
    "\n",
    "num_ants = 10\n",
    "num_iterations = 100\n",
    "evaporation_rate = 0.5\n",
    "alpha = 1\n",
    "beta = 1\n",
    "\n",
    "best_truck_solutions,best_distance = ant_colony_optimization(distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks)\n",
    "\n",
    "# Pour chaque solution de camion, nous devons trouver le chemin le plus court du dépôt au premier nœud du chemin\n",
    "for i, truck_solution in enumerate(best_truck_solutions):\n",
    "    # Créer un graphe à partir de la matrice de distance\n",
    "    G = nx.from_numpy_matrix(distance_matrix)\n",
    "    \n",
    "    # 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\n",
    "    shortest_path = nx.dijkstra_path(G, 0, truck_solution[1])\n",
    "    \n",
    "    # Remplacer le premier nœud du chemin du camion par le chemin le plus court trouvé\n",
    "    best_truck_solutions[i] = [0] + shortest_path + truck_solution[2:]\n",
    "\n",
    "# Calculer la distance totale pour chaque solution de camion\n",
    "total_distances = [evaluate_solution(truck_solution, distance_matrix) for truck_solution in best_truck_solutions]\n",
    "\n",
    "# Afficher les solutions et les distances totales pour chaque camion\n",
    "for i, (truck_solution, total_distance) in enumerate(zip(best_truck_solutions, total_distances)):\n",
    "    print(f\"Camion {i+1} :\")\n",
    "    print(\"Solution :\", truck_solution)\n",
    "    print(\"Distance totale :\", total_distance)\n",
    "    print()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "29df0137",
   "metadata": {},
   "source": [
    "# En rajoutant la contrainte de Time Window pour une instance de VRPTW\n",
    "\n",
    "Dans un premier temps on va attribuer des fenêtres de temps pour chaque clients (ville dans notre graphe)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8266e414-d350-4101-8f15-9cc05ee02934",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "20\n",
      "Client 0 : (0, inf)\n",
      "Client 1 : (44, 94)\n",
      "Client 2 : (99, 111)\n",
      "Client 3 : (80, 125)\n",
      "Client 4 : (85, 96)\n",
      "Client 5 : (71, 85)\n",
      "Client 6 : (34, 69)\n",
      "Client 7 : (20, 30)\n",
      "Client 8 : (46, 56)\n",
      "Client 9 : (90, 109)\n",
      "Client 10 : (74, 116)\n",
      "Client 11 : (95, 112)\n",
      "Client 12 : (93, 131)\n",
      "Client 13 : (46, 82)\n",
      "Client 14 : (86, 124)\n",
      "Client 15 : (87, 129)\n",
      "Client 16 : (13, 23)\n",
      "Client 17 : (0, 28)\n",
      "Client 18 : (77, 108)\n",
      "Client 19 : (76, 105)\n",
      "Client 20 : (78, 94)\n",
      "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]\n",
      "[(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)]\n"
     ]
    }
   ],
   "source": [
    "\n",
    "def assign_time_windows(graph):\n",
    "    # Créer un dictionnaire pour stocker les fenêtres de temps des clients\n",
    "    time_windows = {}\n",
    "\n",
    "    # Définir la fenêtre de temps pour le dépôt central (nœud 0)\n",
    "    time_windows[0] = (0, float('inf'))\n",
    "\n",
    "    # Assigner une fenêtre de temps à chaque client\n",
    "    for node in graph.nodes():\n",
    "        if node !=0 and node != graph.number_of_nodes() :\n",
    "            # Générer une fenêtre de temps aléatoire pour chaque client\n",
    "            start_time = random.randint(0, 100)\n",
    "            end_time = start_time + random.randint(10, 50)\n",
    "            time_windows[node] = (start_time, end_time)\n",
    "           \n",
    "\n",
    "    return time_windows\n",
    "\n",
    "# Attribuer les fenêtres de temps aux clients\n",
    "time_windows = assign_time_windows(graph)\n",
    "\n",
    "print(max(graph.nodes()))\n",
    "# Afficher les fenêtres de temps assignées\n",
    "for node, window in time_windows.items():\n",
    "    print(\"Client\", node, \":\", window)\n",
    "    \n",
    "#paramètres ACO\n",
    "\n",
    "print(graph.nodes())\n",
    "print(graph.edges())\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "747541f5",
   "metadata": {},
   "source": [
    "# On va ensuite réadapter l'algorithme pour prendre en compte les time Windows "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "901e6b93",
   "metadata": {},
   "outputs": [],
   "source": [
    "import concurrent.futures\n",
    "import random\n",
    "import numpy as np\n",
    "import time\n",
    "import heapq\n",
    "\n",
    "\n",
    "# Fonction d'évaluation de la qualité d'une solution (ici, la distance totale)\n",
    "def evaluate_solution(solution, distances, time_windows, dijkstra_distances):\n",
    "    total_distance = 0\n",
    "    total_delay = 0\n",
    "    arrival_time = 0\n",
    "    num_nodes = len(solution)\n",
    "    waiting_times = []\n",
    "\n",
    "    total_distance += dijkstra_distances[solution[1]]\n",
    "\n",
    "    for i in range(num_nodes - 1):\n",
    "        current_node = solution[i]\n",
    "        next_node = solution[i + 1]\n",
    "        total_distance += distances[current_node][next_node]\n",
    "        arrival_time += distances[current_node][next_node]\n",
    "\n",
    "        if arrival_time < time_windows[next_node][0]:\n",
    "            waiting_time = time_windows[next_node][0] - arrival_time\n",
    "            waiting_times.append((next_node, waiting_time))\n",
    "            arrival_time = time_windows[next_node][0]\n",
    "        elif arrival_time > time_windows[next_node][1]:\n",
    "            total_delay += arrival_time - time_windows[next_node][1]\n",
    "\n",
    "    # Ajouter la distance de retour au dépôt\n",
    "    total_distance += distances[solution[-1]][solution[0]]\n",
    "\n",
    "    return total_distance, total_delay, waiting_times\n",
    "\n",
    "    \n",
    "# Fonction pour l'algorithme de Dijkstra\n",
    "def calculate_dijkstra(start_node, distances):\n",
    "    num_nodes = len(distances)\n",
    "    shortest_distances = [float('inf')] * num_nodes\n",
    "    shortest_distances[start_node] = 0\n",
    "    shortest_paths = [[] for _ in range(num_nodes)]\n",
    "    shortest_paths[start_node] = [start_node]\n",
    "    priority_queue = [(0, start_node)]\n",
    "    while priority_queue:\n",
    "        current_distance, current_node = heapq.heappop(priority_queue)\n",
    "        if current_distance == shortest_distances[current_node]:\n",
    "            for neighbor, distance in enumerate(distances[current_node]):\n",
    "                new_distance = current_distance + distance\n",
    "                if new_distance < shortest_distances[neighbor]:\n",
    "                    shortest_distances[neighbor] = new_distance\n",
    "                    shortest_paths[neighbor] = shortest_paths[current_node] + [neighbor]\n",
    "                    heapq.heappush(priority_queue, (new_distance, neighbor))\n",
    "    return shortest_distances, shortest_paths\n",
    "\n",
    "\n",
    "dijkstra_distances, dijkstra_paths = calculate_dijkstra(0, distance_matrix)\n",
    "\n",
    "\n",
    "def ant_colony_optimization(distances, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks):\n",
    "    num_nodes = len(distances)\n",
    "    pheromone = np.ones((num_nodes, num_nodes))  \n",
    "    best_solution = None\n",
    "    best_distance = float('inf')\n",
    "    best_delay = float('inf')\n",
    "    best_score = float('inf')  \n",
    "    best_waiting_times = None\n",
    "\n",
    "    for iteration in range(num_iterations):\n",
    "        solutions = []\n",
    "        for ant in range(num_ants):\n",
    "            visited = set()\n",
    "            current_node = random.randint(0, num_nodes - 1)\n",
    "            visited.add(current_node)\n",
    "            solution = [current_node]\n",
    "            while len(visited) < num_nodes:\n",
    "                next_node = None\n",
    "                probabilities = []\n",
    "                arrival_time = 0\n",
    "                for node in range(num_nodes):\n",
    "                    if node not in visited and (node !=0 or len(visited) == num_nodes - 1):\n",
    "                        pheromone_value = pheromone[current_node][node]\n",
    "                        distance_value = distances[current_node][node]\n",
    "                        wait_time = max(0, time_windows[node][0]- (arrival_time + distance_value))\n",
    "                        probability = (pheromone_value ** alpha) * ((1 / (distance_value + wait_time)) ** beta)\n",
    "                        probabilities.append((node, probability))\n",
    "                total_probability = sum(prob for _, prob in probabilities)\n",
    "                probabilities = [(node, prob / total_probability) for node, prob in probabilities]\n",
    "                roulette_wheel = random.random()\n",
    "                probability_sum = 0\n",
    "                for node, probability in probabilities:\n",
    "                    probability_sum += probability\n",
    "                    if probability_sum >= roulette_wheel:\n",
    "                        next_node = node\n",
    "                        break\n",
    "                visited.add(next_node)\n",
    "                solution.append(next_node)\n",
    "                current_node = next_node\n",
    "            solution.append(0)\n",
    "            solutions.append(solution)\n",
    "\n",
    "        for solution in solutions:\n",
    "            distance, delay, waiting_times = evaluate_solution(solution, distances, time_windows, dijkstra_distances)\n",
    "            score = distance + delay\n",
    "            if score < best_score:\n",
    "                best_solution = solution\n",
    "                best_distance = distance\n",
    "                best_delay = delay\n",
    "                best_waiting_times = waiting_times\n",
    "        pheromone *= evaporation_rate\n",
    "        for solution in solutions:\n",
    "            delta_pheromone = 1 / (evaluate_solution(solution, distances, time_windows, dijkstra_distances)[0] + 0.01)\n",
    "            for i in range(num_nodes - 1):\n",
    "                node1 = solution[i]\n",
    "                node2 = solution[i + 1]\n",
    "                pheromone[node1][node2] += delta_pheromone\n",
    "                pheromone[node2][node1] += delta_pheromone\n",
    "    truck_solutions = []\n",
    "    num_nodes_per_truck = num_nodes // num_trucks\n",
    "    for i in range(num_trucks):\n",
    "        start_index = i * num_nodes_per_truck\n",
    "        end_index = start_index + num_nodes_per_truck\n",
    "        truck_solution = best_solution[start_index:end_index]\n",
    "    \n",
    "        # Ajoutez le chemin le plus court entre le nœud 0 et le premier nœud du chemin\n",
    "        dijkstra_path_to_first_node = dijkstra_paths[truck_solution[0]]\n",
    "        truck_solution = dijkstra_path_to_first_node + truck_solution\n",
    "    \n",
    "    # Ajoutez le chemin le plus court entre le dernier nœud du chemin et le dépôt (nœud 0)\n",
    "        dijkstra_path_to_depot = dijkstra_paths[truck_solution[-1]]\n",
    "        truck_solution = truck_solution + dijkstra_path_to_depot\n",
    "\n",
    "        truck_solutions.append(truck_solution)\n",
    "    return truck_solutions, best_distance, best_waiting_times\n",
    "\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "f24a5980",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Meilleures solutions :\n",
      "Camion 1 : [0, 9, 9, 8, 17, 7, 1, 13, 14, 0, 14]\n",
      "Camion 2 : [0, 12, 12, 18, 4, 5, 20, 19, 15, 0, 15]\n",
      "Camion 3 : [0, 10, 10, 3, 2, 16, 6, 11, 0, 0]\n",
      "Distance totale : 48.0\n"
     ]
    }
   ],
   "source": [
    "num_thread = 1\n",
    "num_ants = 10\n",
    "num_iterations = 100\n",
    "evaporation_rate = 0.5\n",
    "alpha = 1\n",
    "beta = 1\n",
    "\n",
    "num_trucks = 3  # Nombre de camions disponibles\n",
    "\n",
    "\n",
    "\n",
    "with concurrent.futures.ThreadPoolExecutor(max_workers=num_thread) as executor:\n",
    "    futures = [executor.submit(ant_colony_optimization, distance_matrix, num_ants, num_iterations, evaporation_rate, alpha, beta, num_trucks) for _ in range(num_thread)]\n",
    "\n",
    "    for future in concurrent.futures.as_completed(futures):\n",
    "        best_truck_solutions, best_distance, best_waiting_times = future.result()\n",
    "        print(\"Meilleures solutions :\")\n",
    "        for i, truck_solution in enumerate(best_truck_solutions):\n",
    "            print(f\"Camion {i+1} : {truck_solution}\")\n",
    "        print(\"Distance totale :\", best_distance)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
