{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Importing necessary libraries"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.cluster import KMeans\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import random, time, math"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Function to gereate cities :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_cities(howmany, max_coordinates=1000):\n",
    "    return [random.sample(range(max_coordinates), 2) for _ in range(howmany)]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Function to split citites into multiple clusters depending on the number of trucks"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "def split_tour_across_clusters(cities, nb_truck):\n",
    "    if nb_truck == 1:\n",
    "        return {0: list(range(len(cities)))}\n",
    "\n",
    "    # clustering initial\n",
    "    kmeans = KMeans(n_clusters=nb_truck, random_state=0).fit(cities)\n",
    "    clusters = {i:[] for i in range(nb_truck)}\n",
    "\n",
    "    # assignation des indices des villes aux clusters\n",
    "    for i, label in enumerate(kmeans.labels_):\n",
    "        clusters[label].append(i)\n",
    "\n",
    "    while True:\n",
    "        # calcul des tailles de clusters\n",
    "        cluster_sizes = {i:len(clusters[i]) for i in range(nb_truck)}\n",
    "\n",
    "        # identification du cluster le plus grand et du plus petit\n",
    "        max_cluster = max(cluster_sizes, key=cluster_sizes.get)\n",
    "        min_cluster = min(cluster_sizes, key=cluster_sizes.get)\n",
    "\n",
    "        # s'il n'y a pas de grande disparité, on arrête la boucle\n",
    "        if cluster_sizes[max_cluster] - cluster_sizes[min_cluster] <= 1:\n",
    "            break\n",
    "\n",
    "        # calcul du centre de chaque cluster\n",
    "        cluster_centers = {i:np.mean([cities[index] for index in clusters[i]], axis=0) for i in range(nb_truck)}\n",
    "\n",
    "        # calcul des distances entre le centre du cluster le plus grand et les autres\n",
    "        distances = {i:np.linalg.norm(cluster_centers[max_cluster]-cluster_centers[i]) for i in range(nb_truck)}\n",
    "        del distances[max_cluster]  # on supprime la distance vers lui-même\n",
    "\n",
    "        if nb_truck >= 3:\n",
    "            # on identifie les 2 clusters les plus proches\n",
    "            closest_clusters = sorted(distances, key=distances.get)[:2]\n",
    "\n",
    "            \n",
    "            # parmi les deux clusters les plus proches, on choisit le plus petit\n",
    "            if cluster_sizes[closest_clusters[0]] <= cluster_sizes[closest_clusters[1]]:\n",
    "                target_cluster = closest_clusters[0]\n",
    "            else:\n",
    "                target_cluster = closest_clusters[1]\n",
    "        else:\n",
    "            closest_clusters = sorted(distances, key=distances.get)[:1]\n",
    "            target_cluster = closest_clusters[0]\n",
    "\n",
    "\n",
    "\n",
    "        # si le transfert va créer une plus grande disparité, on arrête la boucle\n",
    "        if cluster_sizes[target_cluster] >= cluster_sizes[max_cluster]:\n",
    "            break\n",
    "\n",
    "        # calcul des distances entre le centre du cluster cible et les villes du cluster le plus grand\n",
    "        distances_to_target = {index:np.linalg.norm(cluster_centers[target_cluster]-cities[index]) \n",
    "                               for index in clusters[max_cluster]}\n",
    "\n",
    "        # on identifie la ville la plus proche du centre du cluster cible\n",
    "        closest_city_index = min(distances_to_target, key=distances_to_target.get)\n",
    "\n",
    "        # on transfère la ville du cluster le plus grand au cluster cible\n",
    "        clusters[target_cluster].append(closest_city_index)\n",
    "        clusters[max_cluster].remove(closest_city_index)\n",
    "\n",
    "    # Ajout du point de départ et d'arrivée pour chaque cluster\n",
    "    depot_index = 0\n",
    "    for cluster in clusters.values():\n",
    "        if cluster[0] != depot_index:\n",
    "            cluster.insert(0, depot_index)\n",
    "        if cluster[-1] != depot_index:\n",
    "            cluster.append(depot_index)\n",
    "\n",
    "    return clusters"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Usual function to calculate distances"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def distance(city1, city2):\n",
    "    return math.sqrt((city1[0] - city2[0]) ** 2 + (city1[1] - city2[1]) ** 2)\n",
    "\n",
    "def total_distance(cities):\n",
    "    return sum([distance(cities[i - 1], cities[i]) for i in range(len(cities))])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Function to show graphics"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "previous_route = None\n",
    "\n",
    "def draw_cities(cities, previous_route, color='b', title=' '):\n",
    "    plt.title(title)\n",
    "    \n",
    "    # If there's a previous route, we remove it.\n",
    "    if previous_route is not None:\n",
    "        previous_route.remove()\n",
    "\n",
    "    x = [city[0] for city in cities]\n",
    "    y = [city[1] for city in cities]\n",
    "    x.append(x[0])\n",
    "    y.append(y[0])\n",
    "    \n",
    "    # We plot the route with the specified color and keep a reference to the Line2D object.\n",
    "    previous_route, = plt.plot(x, y, color=color, marker='x', linestyle='-')\n",
    "    \n",
    "    plt.draw()\n",
    "    plt.pause(0.005)\n",
    "\n",
    "    # We return the reference so we can remove this route when a new one is found.\n",
    "    return previous_route"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Function to calculate heuristic for each cluster"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def simulated_annealing(cities, color='b', temperature=100000, cooling_rate=0.9999, temperature_ok=0.001):\n",
    "    interration = 0\n",
    "    plt.ion()\n",
    "    current_solution = cities.copy()\n",
    "    best_solution = cities.copy()\n",
    "    previous_route = draw_cities(best_solution, None, color, 'Initial solution')\n",
    "    while temperature > temperature_ok:\n",
    "        new_solution = current_solution.copy()\n",
    "        # Swap two cities in the route\n",
    "        i = random.randint(0, len(new_solution) - 1)\n",
    "        j = random.randint(0, len(new_solution) - 1)\n",
    "        new_solution[i], new_solution[j] = new_solution[j], new_solution[i]\n",
    "        # Calculate the acceptance probability\n",
    "        current_energy = total_distance(current_solution)\n",
    "        new_energy = total_distance(new_solution)\n",
    "        delta = new_energy - current_energy\n",
    "        if delta < 0 or random.random() < math.exp(-delta / temperature):\n",
    "            current_solution = new_solution\n",
    "            if total_distance(current_solution) < total_distance(best_solution):\n",
    "                best_solution = current_solution\n",
    "                previous_route = draw_cities(best_solution, previous_route, color, 'Current best solution, total distance = ' + str(total_distance(best_solution))  + ', iteration = ' + str(interration))\n",
    "        # Cool down\n",
    "        temperature *= cooling_rate\n",
    "        interration += 1\n",
    "    plt.ioff()\n",
    "    return best_solution"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Main code"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "8\n",
      "8\n",
      "9\n",
      "\n",
      "---- TIME ----\n",
      "generate cities time:  0.0009984970092773438\n",
      "split cities time:  0.12865757942199707\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 38936 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 48040 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 44644 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 42188 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 43376 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 38812 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 38968 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Final solution for cluster  0 : [[214, 814], [477, 508], [500, 500], [500, 500], [80, 410], [30, 684], [82, 731], [169, 908]]\n",
      "Total distance:  1507.4366001497751\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 40956 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 49764 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 45484 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 55764 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 47364 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 47656 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 52500 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 51696 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 53156 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 43592 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 48648 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 49140 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Final solution for cluster  1 : [[421, 284], [500, 500], [500, 500], [685, 360], [692, 106], [473, 167], [85, 13], [389, 296]]\n",
      "Total distance:  1810.3866608133385\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 46712 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 46428 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 44476 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 43264 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 43288 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 44268 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 45404 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 45248 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Final solution for cluster  2 : [[500, 500], [500, 500], [804, 655], [955, 570], [987, 536], [894, 990], [628, 950], [590, 983], [540, 817]]\n",
      "Total distance:  1836.8325428173955\n"
     ]
    }
   ],
   "source": [
    "start_time_generate = time.time()\n",
    "cities = generate_cities(20)      # generate 100 cities\n",
    "cities[0] = [500, 500]          # placing depot at the center\n",
    "stop_time_generate = time.time()\n",
    "\n",
    "nb_truck = 3\n",
    "\n",
    "start_time_split = time.time()\n",
    "clusters = split_tour_across_clusters(cities, nb_truck)\n",
    "stop_time_split = time.time()\n",
    "\n",
    "for cluster in clusters.values():\n",
    "    print(len(cluster))\n",
    "print(\"\\n---- TIME ----\")\n",
    "print(\"generate cities time: \", stop_time_generate - start_time_generate)\n",
    "print(\"split cities time: \", stop_time_split - start_time_split)\n",
    "\n",
    "# create new figure for annealing paths\n",
    "plt.figure()\n",
    "colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k']\n",
    "\n",
    "for i, cluster_indices in enumerate(clusters.values()):\n",
    "    # Sélection d'une couleur pour le cluster\n",
    "    color = colors[i % len(colors)]\n",
    "    \n",
    "    # Récupération des coordonnées de la ville\n",
    "    cluster_cities = [cities[index] for index in cluster_indices]\n",
    "\n",
    "    # Appel de la fonction simulated_annealing avec la couleur choisie\n",
    "    best_route = simulated_annealing(cluster_cities, color)\n",
    "    print(\"Final solution for cluster \", i, \":\", best_route)\n",
    "    print(\"Total distance: \", total_distance(best_route))\n",
    "\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
