{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Aufgabenblatt 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 138,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Imports\n",
    "\n",
    "import networkx as nx\n",
    "from random import randint, sample\n",
    "import numpy as np"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Aufgabe 1\n",
    "\n",
    "Schreiben Sie eine Funktion, die zufällig einen Graphen mit einer ubergebenen Anzahl an\n",
    "Kanten und Knoten erstellt. Verwenden sie hierfür die `networkx`-Bibliothek."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 20844 characters]",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def generate_graph(nodes: int, edges: int) -> nx.Graph:\n",
    "    G = nx.Graph()\n",
    "    G.add_nodes_from(range(nodes))\n",
    "    G.add_edges_from([(randint(0, nodes-1), randint(0, nodes-1)) for _ in range(edges)])\n",
    "    return G\n",
    "\n",
    "G = generate_graph(10,18)\n",
    "nx.draw(G, with_labels=True)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Aufgabe 2\n",
    "\n",
    "Nun soll eine Funktion implementiert werden, die die Fitness für eine Lösung berechnet,\n",
    "machen Sie sich an dieser Stelle Gedanken, wie die Lösungen repräsentiert werden sollen und\n",
    "ob ungültige Lösungen berücksichtigt werden müssen."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 140,
   "metadata": {},
   "outputs": [],
   "source": [
    "def fitness(G: nx.Graph, solution : list) -> int:\n",
    "    for a in solution:\n",
    "        for b in solution:\n",
    "            if a == b:\n",
    "                continue\n",
    "            if not G.has_edge(a, b):\n",
    "                return 0\n",
    "            \n",
    "    return len(solution)        "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Aufgabe 3\n",
    "\n",
    "Implementieren Sie einen evolutionären Algorithmus, der den Lösungsraum nach optimalen\n",
    "Lösungen durchsucht. Verwenden Sie bei der Reproduktion Mutation sowie auch Crossover.\n",
    "Uberprüfen Sie außerdem, ob ihr Algorithmus wirklich eine maximale Clique findet. Dazu\n",
    "kann die `find_clique()`-Funktion von `networkx` verwendet werden."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 141,
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng()\n",
    "\n",
    "def clique(G: nx.Graph, generations: int = 50, population_size: int = 10, selection_size: int = 3) -> list:\n",
    "\n",
    "    # Initial population\n",
    "    population = [sample(list(G.nodes), randint(1, G.number_of_nodes())) for _ in range(population_size)]\n",
    "\n",
    "    # Evolution\n",
    "    for _ in range(generations):\n",
    "\n",
    "        # Death penalty (regenerate solution until valid)\n",
    "        for i, individual in enumerate(population):\n",
    "            while fitness(G, individual) == 0:\n",
    "                individual = sample(list(G.nodes), randint(1, G.number_of_nodes()))\n",
    "            population[i] = individual\n",
    "\n",
    "        # Select individuals with highest fitness for reproduction\n",
    "        population = sorted(population, key=lambda p: fitness(G, p), reverse=True)\n",
    "        selection = population[:selection_size]\n",
    "\n",
    "        # Reproduce\n",
    "        offsprings = []\n",
    "        for _ in range(population_size - selection_size):\n",
    "\n",
    "            mother, father = sample(selection, 2)\n",
    "\n",
    "            # Each offspring consists of nodes contained in the parent.\n",
    "            # The number of nodes is randomly chosen as a value between len(mother) and len(father).\n",
    "            num_nodes = randint(*sorted((len(mother), len(father))))\n",
    "            offspring = sample(list(set(mother + father)), num_nodes)\n",
    "\n",
    "            # Mutate offspring by randomly removing or adding nodes\n",
    "            m = int(rng.normal())\n",
    "            if m < 0: # Remove m nodes\n",
    "                offspring = sample(offspring, max(1, num_nodes + m))\n",
    "            elif m > 0: # Add m nodes if possible\n",
    "                available_nodes = list(set(G.nodes) - set(offspring))\n",
    "                offspring += sample(available_nodes, min(len(available_nodes), m))\n",
    "\n",
    "            offsprings.append(offspring)\n",
    "\n",
    "        # Create new population\n",
    "        population = selection + offsprings\n",
    "\n",
    "    # Return best individual of final population\n",
    "    return max(population, key=lambda p: fitness(G, p))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Aufgabe 4\n",
    "\n",
    "Plotten Sie den in Aufgabe 1 generierten Graphen. Knoten, die zu der maximalen Clique\n",
    "gehören, die der EA bestimmt hat, sollen dabei farblich hervorgehoben werden."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 142,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fitness of solution: 4\n",
      "Number of nodes in solution: 4\n",
      "Correct solution found: True\n",
      "Correct number of nodes in maximum clique: 4\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 30836 characters]",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def draw_solution(G: nx.Graph, solution: list):\n",
    "    pos = nx.spring_layout(G)\n",
    "\n",
    "    # Filter largest cliques from nx.find_cliques\n",
    "    clique_number = max(len(c) for c in nx.find_cliques(G))\n",
    "    cliques = [set(clique) for clique in nx.find_cliques(G) if len(clique) == clique_number]\n",
    "\n",
    "    # Draw nodes\n",
    "    nx.draw_networkx_nodes(G, pos, nodelist=[n for n in G.nodes if n not in solution], node_color=\"tab:blue\")\n",
    "    nx.draw_networkx_nodes(G, pos, nodelist=solution, node_color=\"tab:red\")\n",
    "\n",
    "    # Draw edges\n",
    "    nx.draw_networkx_edges(G, pos)\n",
    "    nx.draw_networkx_labels(G, pos, {n:n for n in G.nodes})\n",
    "\n",
    "    # Information\n",
    "    print(f\"Fitness of solution: {fitness(G, solution)}\")\n",
    "    print(f\"Number of nodes in solution: {len(solution)}\")\n",
    "    print(f\"Correct solution found: {set(solution) in cliques}\")\n",
    "    print(f\"Correct number of nodes in maximum clique: {clique_number}\")\n",
    "\n",
    "\n",
    "\n",
    "G = generate_graph(10,40)\n",
    "solution = clique(G, generations=200, population_size=20, selection_size=3)\n",
    "\n",
    "draw_solution(G, solution)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 143,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test run: iteration=(1000/1000), success=960, failure=40, rate=0.96\r"
     ]
    }
   ],
   "source": [
    "# Test evolutional algorithm\n",
    "\n",
    "iterations = 1000\n",
    "success = 0\n",
    "failure = 0\n",
    "\n",
    "for i in range(iterations):\n",
    "    \n",
    "    # Generate graph and solve\n",
    "    G = generate_graph(10,40)\n",
    "    solution = clique(G, generations=50, population_size=20, selection_size=5)\n",
    "    #solution = solve(G, 30, 50)\n",
    "\n",
    "    # Check if solution correct\n",
    "    clique_number = max(len(c) for c in nx.find_cliques(G))\n",
    "    cliques = [set(clique) for clique in nx.find_cliques(G) if len(clique) == clique_number]\n",
    "\n",
    "    if set(solution) in cliques:\n",
    "        success += 1\n",
    "    else:\n",
    "        failure += 1\n",
    "\n",
    "    # Print results\n",
    "    print(f\"Test run: iteration=({i+1}/{iterations}), {success=}, {failure=}, rate={success/(i+1):.2f}\", end=\"\\r\")"
   ]
  }
 ],
 "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.10.1"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "b0fa6594d8f4cbf19f97940f81e996739fb7646882a419484c72d19e05852a7e"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
