{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "Ο παρακάτω κώδικας αποτελείται από 3 κελιά.\n",
        "\n",
        "\n",
        "1.   Στο πρώτο κελί εισάγουμε τα απαραίτητα στοιχεία (πίνακα μετάβασης κ.τ.λ.) Θα μας κάνει μια προσομοίωση της κίνησης της αλυσίδας.\n",
        "2.   Το δεύτερο κελί έχει τα δεδομένα από το πρώτο κελί. Θα κάνει μια πλήρη μελέτη της αλυσίδας.\n",
        "3. Το τρίτο κελί θα εξάγει τα αποτελέσματα σε ένα αρχείο HTML το οποίο μπορείτε να δείτε σε οποιονδήποτε browser.\n",
        "\n"
      ],
      "metadata": {
        "id": "gfHfJS3hUbNQ"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Κίνηση Μαρκοβιανής Αλυσίδας\n",
        "\n",
        "\n",
        "import numpy as np\n",
        "import time\n",
        "import random\n",
        "import matplotlib.pyplot as plt\n",
        "import matplotlib.patches as patches\n",
        "from IPython import display # Απαραίτητο για δυναμική ενημέρωση στο Colab\n",
        "\n",
        "def get_states_from_user():\n",
        "    \"\"\"\n",
        "    Ζητάει από τον χρήστη να εισάγει τα ονόματα των καταστάσεων.\n",
        "    Επιστρέφει μια λίστα με τα ονόματα των καταστάσεων.\n",
        "    \"\"\"\n",
        "    while True:\n",
        "        states_input = input(\"Εισάγετε τα ονόματα των καταστάσεων, χωρισμένα με κόμμα (π.χ. Καλοκαιρία,Βροχή,Χιόνι): \")\n",
        "        states = [s.strip() for s in states_input.split(',') if s.strip()]\n",
        "        if len(states) >= 2:\n",
        "            print(f\"Καταστάσεις: {states}\")\n",
        "            return states\n",
        "        else:\n",
        "            print(\"Πρέπει να εισάγετε τουλάχιστον δύο καταστάσεις. Παρακαλώ δοκιμάστε ξανά.\")\n",
        "\n",
        "def get_transition_matrix_from_user(states):\n",
        "    \"\"\"\n",
        "    Ζητάει από τον χρήστη να εισάγει τον πίνακα μετάβασης.\n",
        "    Επιστρέφει έναν πίνακα NumPy.\n",
        "    \"\"\"\n",
        "    num_states = len(states)\n",
        "    transition_matrix = np.zeros((num_states, num_states))\n",
        "\n",
        "    print(\"\\nΤώρα θα εισάγετε τον πίνακα μετάβασης.\")\n",
        "    print(\"Για κάθε κατάσταση, εισάγετε τις πιθανότητες μετάβασης στις άλλες καταστάσεις.\")\n",
        "    print(\"Οι πιθανότητες πρέπει να χωρίζονται με κόμμα και το άθροισμά τους να είναι 1.\")\n",
        "    print(\"Παράδειγμα: Αν από 'Καλοκαιρία' μπορεί να πάει σε 'Καλοκαιρία'(0.8), 'Βροχή'(0.2), θα γράψετε 0.8,0.2\")\n",
        "\n",
        "    for i, current_state in enumerate(states):\n",
        "        while True:\n",
        "            row_input = input(f\"Πιθανότητες από '{current_state}' προς ({', '.join(states)}): \")\n",
        "            try:\n",
        "                probabilities = [float(p.strip()) for p in row_input.split(',') if p.strip()]\n",
        "                if len(probabilities) == num_states and np.isclose(sum(probabilities), 1.0):\n",
        "                    if all(p >= 0 for p in probabilities):\n",
        "                        transition_matrix[i] = probabilities\n",
        "                        break\n",
        "                    else:\n",
        "                        print(\"Οι πιθανότητες πρέπει να είναι μη αρνητικοί αριθμοί.\")\n",
        "                else:\n",
        "                    print(f\"Λάθος εισαγωγή. Εισάγετε ακριβώς {num_states} πιθανότητες που να αθροίζουν στο 1.0.\")\n",
        "            except ValueError:\n",
        "                print(\"Λάθος μορφή. Εισάγετε αριθμούς χωρισμένους με κόμμα (π.χ. 0.5,0.3,0.2).\")\n",
        "\n",
        "    print(\"\\nΟ πίνακας μετάβασης που εισάγατε είναι:\")\n",
        "    print(transition_matrix)\n",
        "    return transition_matrix\n",
        "\n",
        "def simulate_markov_chain_with_moving_marker_colab(transition_matrix, states, initial_state, num_steps, delay=1.0):\n",
        "    \"\"\"\n",
        "    Προσομοιώνει μια αλυσίδα Markov διακριτού χρόνου και εμφανίζει τις αλλαγές καταστάσεων\n",
        "    σε ένα διάγραμμα Matplotlib με κινούμενο δείκτη.\n",
        "    Είναι βελτιστοποιημένη για περιβάλλοντα Colab/Jupyter Notebooks.\n",
        "\n",
        "    Args:\n",
        "        transition_matrix (np.array): Ο πίνακας μετάβασης καταστάσεων.\n",
        "        states (list): Μια λίστα με τα ονόματα των καταστάσεων.\n",
        "        initial_state (str): Η αρχική κατάσταση.\n",
        "        num_steps (int): Ο αριθμός των βημάτων προσομοίωσης.\n",
        "        delay (float): Η καθυστέρηση σε δευτερόλεπτα μεταξύ των αλλαγών καταστάσεων.\n",
        "    \"\"\"\n",
        "\n",
        "    num_states = len(states)\n",
        "    state_positions = {}\n",
        "\n",
        "    # Υπολογισμός θέσεων για τους κύκλους των καταστάσεων σε κυκλική διάταξη\n",
        "    radius = 1.5\n",
        "    for i, state in enumerate(states):\n",
        "        angle = 2 * np.pi * i / num_states\n",
        "        x = radius * np.cos(angle)\n",
        "        y = radius * np.sin(angle)\n",
        "        state_positions[state] = (x, y)\n",
        "\n",
        "    current_state_index = states.index(initial_state)\n",
        "    print(f\"Ξεκινώντας προσομοίωση από την κατάσταση: **{initial_state}**\")\n",
        "\n",
        "    # Βρόχος προσομοίωσης\n",
        "    for step in range(num_steps):\n",
        "        # Καθαρίζουμε την προηγούμενη έξοδο για να εμφανίσουμε το ενημερωμένο γράφημα\n",
        "        display.clear_output(wait=True)\n",
        "\n",
        "        # Δημιουργούμε ένα νέο γράφημα σε κάθε βήμα για να εξασφαλίσουμε την ανανέωση\n",
        "        fig, ax = plt.subplots(figsize=(8, 8))\n",
        "        ax.set_xlim([-2.5, 2.5])\n",
        "        ax.set_ylim([-2.5, 2.5])\n",
        "        ax.set_aspect('equal', adjustable='box')\n",
        "        ax.axis('off') # Κρύβουμε τους άξονες για καθαρότερη οπτικοποίηση\n",
        "\n",
        "        node_color = 'lightblue' # Χρώμα για τους κόμβους των καταστάσεων\n",
        "\n",
        "        # Σχεδίαση των κύκλων (κόμβων) και των ονομάτων των καταστάσεων\n",
        "        for state, (x, y) in state_positions.items():\n",
        "            circle = patches.Circle((x, y), 0.3, fc=node_color, ec='black', lw=2, alpha=0.7)\n",
        "            ax.add_patch(circle)\n",
        "            ax.text(x, y, state, ha='center', va='center', fontsize=12, color='black')\n",
        "\n",
        "        # Σχεδίαση των βελών για τις πιθανές μεταβάσεις μεταξύ των καταστάσεων\n",
        "        for i in range(num_states):\n",
        "            for j in range(num_states):\n",
        "                # Εμφάνιση βέλους μόνο αν υπάρχει πιθανότητα μετάβασης και δεν είναι αυτο-βρόχος\n",
        "                if transition_matrix[i, j] > 0 and i != j:\n",
        "                    start_state = states[i]\n",
        "                    end_state = states[j]\n",
        "\n",
        "                    start_x, start_y = state_positions[start_state]\n",
        "                    end_x, end_y = state_positions[end_state]\n",
        "\n",
        "                    dx = end_x - start_x\n",
        "                    dy = end_y - start_y\n",
        "                    dist = np.sqrt(dx**2 + dy**2)\n",
        "\n",
        "                    # Υπολογισμός σημείων έναρξης/λήξης βέλους στην περίμετρο των κύκλων\n",
        "                    arrow_start_x = start_x + 0.3 * dx / dist\n",
        "                    arrow_start_y = start_y + 0.3 * dy / dist\n",
        "                    arrow_end_x = end_x - 0.3 * dx / dist\n",
        "                    arrow_end_y = end_y - 0.3 * dy / dist\n",
        "\n",
        "                    ax.annotate(\"\",\n",
        "                                xy=(arrow_end_x, arrow_end_y), xycoords='data',\n",
        "                                xytext=(arrow_start_x, arrow_start_y), textcoords='data',\n",
        "                                arrowprops=dict(arrowstyle=\"->\", color=\"gray\", lw=1, shrinkA=0, shrinkB=0))\n",
        "\n",
        "                    # Ετικέτα με την πιθανότητα πάνω στο βέλος\n",
        "                    mid_x = (arrow_start_x + arrow_end_x) / 2\n",
        "                    mid_y = (arrow_start_y + arrow_end_y) / 2\n",
        "                    ax.text(mid_x, mid_y, f'{transition_matrix[i, j]:.2f}',\n",
        "                            fontsize=9, color='darkgreen', ha='center', va='center',\n",
        "                            bbox=dict(boxstyle=\"round,pad=0.2\", fc=\"yellow\", ec=\"darkgreen\", lw=1, alpha=0.6))\n",
        "                # Χειρισμός αυτο-βρόχων (μετάβαση στην ίδια κατάσταση)\n",
        "                elif i == j and transition_matrix[i, j] > 0:\n",
        "                    x, y = state_positions[states[i]]\n",
        "                    # Δημιουργία ενός μικρού κύκλου για οπτικοποίηση του αυτο-βρόχου\n",
        "                    circle_loop = patches.Circle((x + 0.3, y + 0.3), 0.1, fc='none', ec='gray', lw=1, linestyle='--')\n",
        "                    ax.add_patch(circle_loop)\n",
        "                    ax.text(x + 0.4, y + 0.4, f'{transition_matrix[i, j]:.2f}', fontsize=9, color='darkgreen')\n",
        "\n",
        "        # Εντοπισμός της τρέχουσας κατάστασης για την τοποθέτηση του κινούμενου δείκτη\n",
        "        current_state_name = states[current_state_index]\n",
        "        marker_x, marker_y = state_positions[current_state_name]\n",
        "\n",
        "        # Σχεδίαση του κινούμενου δείκτη (μια μικρή κόκκινη κουκκίδα) στην τρέχουσα θέση\n",
        "        marker_radius = 0.1\n",
        "        marker = patches.Circle((marker_x, marker_y), marker_radius, fc='red', ec='darkred', lw=1, zorder=5) # zorder για να είναι πάνω από άλλα στοιχεία\n",
        "        ax.add_patch(marker)\n",
        "\n",
        "        # Εμφάνιση του γραφήματος για το τρέχον βήμα\n",
        "        plt.show()\n",
        "        plt.close(fig) # Κλείνουμε το figure για να απελευθερώσουμε τη μνήμη\n",
        "\n",
        "        # Λογική μετάβασης στην επόμενη κατάσταση\n",
        "        previous_state_name = states[current_state_index]\n",
        "        next_state_probabilities = transition_matrix[current_state_index]\n",
        "        # Επιλογή της επόμενης κατάστασης βάσει των πιθανοτήτων\n",
        "        next_state_index = random.choices(range(num_states), weights=next_state_probabilities, k=1)[0]\n",
        "        next_state_name = states[next_state_index]\n",
        "\n",
        "        print(f\"--- Βήμα {step + 1} ---\")\n",
        "        print(f\"Από: **{previous_state_name}** -> Προς: **{next_state_name}**\")\n",
        "\n",
        "        # Ενημέρωση της τρέχουσας κατάστασης για το επόμενο βήμα\n",
        "        current_state_index = next_state_index\n",
        "        time.sleep(delay) # Καθυστέρηση για παρακολούθηση σε \"πραγματικό χρόνο\"\n",
        "\n",
        "    print(\"\\n--- Η προσομοίωση ολοκληρώθηκε! ---\")\n",
        "\n",
        "# --- Κύριο μέρος του προγράμματος για είσοδο χρήστη και εκτέλεση ---\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    print(\"Καλώς ήρθατε στην προσομοίωση Μαρκοβιανής Αλυσίδας!\")\n",
        "\n",
        "    # Είσοδος καταστάσεων από τον χρήστη\n",
        "    user_states = get_states_from_user()\n",
        "\n",
        "    # Είσοδος πίνακα μετάβασης από τον χρήστη\n",
        "    user_transition_matrix = get_transition_matrix_from_user(user_states)\n",
        "\n",
        "    # Επιλογή αρχικής κατάστασης από τον χρήστη\n",
        "    while True:\n",
        "        initial_state_input = input(f\"Εισάγετε την αρχική κατάσταση ({', '.join(user_states)}): \")\n",
        "        if initial_state_input in user_states:\n",
        "            user_initial_state = initial_state_input\n",
        "            break\n",
        "        else:\n",
        "            print(\"Η επιλεγμένη αρχική κατάσταση δεν υπάρχει στις καταστάσεις που δώσατε. Παρακαλώ δοκιμάστε ξανά.\")\n",
        "\n",
        "    # Είσοδος αριθμού βημάτων από τον χρήστη\n",
        "    while True:\n",
        "        try:\n",
        "            num_steps_input = int(input(\"Εισάγετε τον αριθμό των βημάτων προσομοίωσης (π.χ. 20): \"))\n",
        "            if num_steps_input > 0:\n",
        "                user_num_steps = num_steps_input\n",
        "                break\n",
        "            else:\n",
        "                print(\"Ο αριθμός των βημάτων πρέπει να είναι θετικός ακέραιος.\")\n",
        "        except ValueError:\n",
        "            print(\"Μη έγκυρη είσοδος. Εισάγετε έναν ακέραιο αριθμό.\")\n",
        "\n",
        "    # Είσοδος καθυστέρησης από τον χρήστη\n",
        "    while True:\n",
        "        try:\n",
        "            delay_input = float(input(\"Εισάγετε την καθυστέρηση μεταξύ των βημάτων σε δευτερόλεπτα (π.χ. 1.5): \"))\n",
        "            if delay_input >= 0:\n",
        "                user_delay = delay_input\n",
        "                break\n",
        "            else:\n",
        "                print(\"Η καθυστέρηση πρέπει να είναι μη αρνητικός αριθμός.\")\n",
        "        except ValueError:\n",
        "            print(\"Μη έγκυρη είσοδος. Εισάγετε έναν αριθμό.\")\n",
        "\n",
        "    print(\"\\nΕκκίνηση προσομοίωσης με τα δεδομένα σας...\")\n",
        "    simulate_markov_chain_with_moving_marker_colab(user_transition_matrix, user_states, user_initial_state, user_num_steps, user_delay)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 685
        },
        "id": "mHghufSlHzQO",
        "outputId": "10164aff-7569-46e5-c3a1-449362d8b515"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<Figure size 800x800 with 1 Axes>"
            ],
            "image/png": "[encoded data, 37408 characters]\n"
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "--- Βήμα 10 ---\n",
            "Από: **2** -> Προς: **2**\n",
            "\n",
            "--- Η προσομοίωση ολοκληρώθηκε! ---\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [],
      "metadata": {
        "id": "_vURUhVCW_Id"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Μελέτη Μαρκοβιανής Αλυσίδας\n",
        "\n",
        "import numpy as np\n",
        "import math\n",
        "import graphviz\n",
        "from IPython.display import display # Νέα εισαγωγή για Colab/Jupyter\n",
        "\n",
        "class DiscreteTimeMarkovChain:\n",
        "    \"\"\"\n",
        "    Κλάση για την ανάλυση μιας Διακριτού Χρόνου Μαρκοβιανής Αλυσίδας (DTMC).\n",
        "    \"\"\"\n",
        "    def __init__(self, transition_matrix, state_names=None):\n",
        "        \"\"\"\n",
        "        Αρχικοποιεί τη Μαρκοβιανή Αλυσίδα.\n",
        "\n",
        "        Args:\n",
        "            transition_matrix (np.array): Ο τετραγωνικός πίνακας μετάβασης πιθανοτήτων.\n",
        "                                            Κάθε γραμμή πρέπει να αθροίζει στο 1.\n",
        "            state_names (list, optional): Λίστα με τα ονόματα των καταστάσεων.\n",
        "                                            Εάν δεν δοθεί, θα χρησιμοποιηθούν αριθμοί (0, 1, ...).\n",
        "        \"\"\"\n",
        "        self.P = np.array(transition_matrix, dtype=float)\n",
        "        self.num_states = self.P.shape[0]\n",
        "\n",
        "        if self.P.shape[0] != self.P.shape[1]:\n",
        "            raise ValueError(\"Ο πίνακας μετάβασης πρέπει να είναι τετραγωνικός.\")\n",
        "        if not np.allclose(self.P.sum(axis=1), 1.0):\n",
        "            raise ValueError(\"Οι γραμμές του πίνακα μετάβασης πρέπει να αθροίζουν στο 1.\")\n",
        "        if np.any(self.P < 0):\n",
        "            raise ValueError(\"Οι πιθανότητες μετάβασης δεν μπορούν να είναι αρνητικές.\")\n",
        "\n",
        "        self.state_names = state_names if state_names else [str(i) for i in range(self.num_states)]\n",
        "        if len(self.state_names) != self.num_states:\n",
        "            raise ValueError(\"Ο αριθμός των ονομάτων καταστάσεων δεν ταιριάζει με τον αριθμό των καταστάσεων.\")\n",
        "\n",
        "        # Υπολογισμός ταξινόμησης καταστάσεων κατά την αρχικοποίηση για χρήση σε άλλες μεθόδους\n",
        "        self._recurrent_states_indices, self._transient_states_indices = self._classify_states_internal()\n",
        "        self._recurrent_state_names = [self.state_names[i] for i in self._recurrent_states_indices]\n",
        "        self._transient_state_names = [self.state_names[i] for i in self._transient_states_indices]\n",
        "\n",
        "        # Υπολογισμός κλειστών αδιαχώριστων συνόλων κατά την αρχικοποίηση\n",
        "        self._closed_irreducible_sets_indices = self._find_closed_irreducible_sets_internal()\n",
        "\n",
        "\n",
        "        print(f\"Δημιουργήθηκε Μαρκοβιανή Αλυσίδα με {self.num_states} καταστάσεις: {', '.join(self.state_names)}\")\n",
        "        print(\"\\nΠίνακας Μετάβασης (P):\")\n",
        "        print(self.P)\n",
        "\n",
        "    def _is_communicating(self, i, j):\n",
        "        \"\"\"Ελέγχει αν η κατάσταση i επικοινωνεί με την κατάσταση j (i -> j).\"\"\"\n",
        "        if i == j:\n",
        "            return True # State always communicates with itself in 0 steps (or 1 if loop exists)\n",
        "        P_k = self.P.copy()\n",
        "        for _ in range(self.num_states * 2): # Check up to 2*N steps to be reasonably sure\n",
        "            if P_k[i, j] > 1e-9:\n",
        "                return True\n",
        "            P_k = np.dot(P_k, self.P)\n",
        "        return False\n",
        "\n",
        "    def _get_communicating_classes_internal(self):\n",
        "        \"\"\"\n",
        "        Βρίσκει τις κλάσες επικοινωνίας (communicating classes) της Μαρκοβιανής αλυσίδας.\n",
        "        Δύο καταστάσεις i, j επικοινωνούν αν i -> j και j -> i.\n",
        "        Εσωτερική μέθοδος για χρήση από άλλες μεθόδους.\n",
        "        \"\"\"\n",
        "        classes = []\n",
        "        visited = set()\n",
        "        for i in range(self.num_states):\n",
        "            if i not in visited:\n",
        "                current_class = {i}\n",
        "                for j in range(self.num_states):\n",
        "                    if self._is_communicating(i, j) and self._is_communicating(j, i):\n",
        "                        current_class.add(j)\n",
        "                classes.append(current_class)\n",
        "                visited.update(current_class)\n",
        "        # Filter out duplicates that might occur due to set construction order\n",
        "        unique_classes = []\n",
        "        for cls in classes:\n",
        "            if not any(cls == u_cls for u_cls in unique_classes):\n",
        "                unique_classes.append(cls)\n",
        "        return unique_classes\n",
        "\n",
        "    def get_communicating_classes(self):\n",
        "        \"\"\"\n",
        "        Εξωτερική μέθοδος για την εκτύπωση των κλάσεων επικοινωνίας.\n",
        "        \"\"\"\n",
        "        print(\"\\n--- Κλάσες Επικοινωνίας ---\")\n",
        "        communicating_classes = self._get_communicating_classes_internal()\n",
        "        for i, cls in enumerate(communicating_classes):\n",
        "            state_names_cls = [self.state_names[s] for s in cls]\n",
        "            print(f\"   Κλάση {i+1}: {{{', '.join(state_names_cls)}}}\")\n",
        "        return communicating_classes\n",
        "\n",
        "    def _classify_states_internal(self):\n",
        "        \"\"\"\n",
        "        Εσωτερική μέθοδος για την ταξινόμηση των καταστάσεων σε αναστρέψιμες (recurrent) και μεταβατικές (transient).\n",
        "        \"\"\"\n",
        "        communicating_classes = self._get_communicating_classes_internal()\n",
        "        recurrent_states_indices = []\n",
        "        transient_states_indices = []\n",
        "\n",
        "        # Find recurrent states first (they belong to closed classes)\n",
        "        for cls in communicating_classes:\n",
        "            is_closed = True\n",
        "            for member_state_index in cls:\n",
        "                for next_state_index in range(self.num_states):\n",
        "                    if self.P[member_state_index, next_state_index] > 1e-9 and next_state_index not in cls:\n",
        "                        is_closed = False\n",
        "                        break\n",
        "                if not is_closed:\n",
        "                    break\n",
        "            if is_closed:\n",
        "                recurrent_states_indices.extend(list(cls))\n",
        "\n",
        "        # Any state not recurrent is transient\n",
        "        for i in range(self.num_states):\n",
        "            if i not in recurrent_states_indices:\n",
        "                transient_states_indices.append(i)\n",
        "\n",
        "        return sorted(recurrent_states_indices), sorted(transient_states_indices)\n",
        "\n",
        "    def get_state_classification(self):\n",
        "        \"\"\"\n",
        "        Εξωτερική μέθοδος για την εκτύπωση της ταξινόμησης των καταστάσεων.\n",
        "        \"\"\"\n",
        "        print(\"\\n--- Ταξινόμηση Καταστάσεων ---\")\n",
        "        recurrent_states = [self.state_names[i] for i in self._recurrent_states_indices]\n",
        "        transient_states = [self.state_names[i] for i in self._transient_states_indices]\n",
        "        print(f\"   Αναστρέψιμες (Recurrent) Καταστάσεις: {recurrent_states}\")\n",
        "        print(f\"   Μεταβατικές (Transient) Καταστάσεις: {transient_states}\")\n",
        "        return {\"recurrent\": recurrent_states, \"transient\": transient_states}\n",
        "\n",
        "    def _find_closed_irreducible_sets_internal(self):\n",
        "        \"\"\"\n",
        "        Βρίσκει τα κλειστά και αδιαχώριστα σύνολα.\n",
        "        Αυτά είναι ταυτόχρονα και οι αναστρέψιμες κλάσες επικοινωνίας.\n",
        "        \"\"\"\n",
        "        communicating_classes = self._get_communicating_classes_internal()\n",
        "        closed_irreducible_sets_indices = []\n",
        "\n",
        "        for cls in communicating_classes:\n",
        "            is_closed = True\n",
        "            for member_state_index in cls:\n",
        "                for next_state_index in range(self.num_states):\n",
        "                    # If there's a transition out of the class\n",
        "                    if self.P[member_state_index, next_state_index] > 1e-9 and next_state_index not in cls:\n",
        "                        is_closed = False\n",
        "                        break\n",
        "                if not is_closed:\n",
        "                    break\n",
        "            if is_closed:\n",
        "                closed_irreducible_sets_indices.append(sorted(list(cls)))\n",
        "        return closed_irreducible_sets_indices\n",
        "\n",
        "    def get_closed_irreducible_sets(self):\n",
        "        \"\"\"\n",
        "        Εξωτερική μέθοδος για την εκτύπωση των κλειστών και αδιαχώριστων συνόλων.\n",
        "        \"\"\"\n",
        "        print(\"\\n--- Κλειστά και Αδιαχώριστα Σύνολα (Closed and Irreducible Sets) ---\")\n",
        "        if not self._closed_irreducible_sets_indices:\n",
        "            print(\"   Δεν υπάρχουν κλειστά και αδιαχώριστα σύνολα σε αυτήν την αλυσίδα.\")\n",
        "            return []\n",
        "\n",
        "        for i, cls_indices in enumerate(self._closed_irreducible_sets_indices):\n",
        "            state_names_cls = [self.state_names[s] for s in cls_indices]\n",
        "            print(f\"   Σύνολο {i+1}: {{{', '.join(state_names_cls)}}}\")\n",
        "        return self._closed_irreducible_sets_indices\n",
        "\n",
        "\n",
        "    def get_absorption_probabilities(self):\n",
        "        \"\"\"\n",
        "        Υπολογίζει την πιθανότητα απορρόφησης σε κάθε κλειστό/αναστρέψιμο σύνολο\n",
        "        ξεκινώντας από μια μεταβατική κατάσταση.\n",
        "        \"\"\"\n",
        "        if not self._transient_states_indices:\n",
        "            print(\"\\n--- Πιθανότητες Απορρόφησης ---\")\n",
        "            print(\"   Δεν υπάρχουν μεταβατικές καταστάσεις. Δεν απαιτείται υπολογισμός απορρόφησης.\")\n",
        "            return None\n",
        "\n",
        "        if not self._recurrent_states_indices:\n",
        "            print(\"\\n--- Πιθανότητες Απορρόφησης ---\")\n",
        "            print(\"   Δεν υπάρχουν αναστρέψιμες (απορροφητικές) καταστάσεις. Δεν μπορεί να συμβεί απορρόφηση.\")\n",
        "            return None\n",
        "\n",
        "        num_transient = len(self._transient_states_indices)\n",
        "        num_recurrent = len(self._recurrent_states_indices)\n",
        "\n",
        "        # Δημιουργία πίνακα P με τις γραμμές/στήλες σε σειρά T, R\n",
        "        # (δηλαδή, αρχικά οι μεταβατικές, μετά οι αναστρέψιμες)\n",
        "        # Αυτό είναι απαραίτητο για τη διάσπαση σε Q, R\n",
        "        sorted_indices = self._transient_states_indices + self._recurrent_states_indices\n",
        "        P_canonical = self.P[np.ix_(sorted_indices, sorted_indices)]\n",
        "\n",
        "        Q = P_canonical[0:num_transient, 0:num_transient]\n",
        "        R = P_canonical[0:num_transient, num_transient:num_transient + num_recurrent]\n",
        "\n",
        "        print(\"\\n--- Πιθανότητες Απορρόφησης από Μεταβατικές σε Αναστρέψιμες Καταστάσεις ---\")\n",
        "        print(\"   Μεταβατικές Καταστάσεις (indices):\", self._transient_states_indices)\n",
        "        print(\"   Αναστρέψιμες Καταστάσεις (indices):\", self._recurrent_states_indices)\n",
        "\n",
        "        try:\n",
        "            # Υπολογισμός του θεμελιώδους πίνακα N = (I - Q)^-1\n",
        "            I_minus_Q = np.identity(num_transient) - Q\n",
        "            N = np.linalg.inv(I_minus_Q)\n",
        "            # print(\"\\nΘεμελιώδης πίνακας (N):\")\n",
        "            # print(N)\n",
        "\n",
        "            # Υπολογισμός του πίνακα B = NR (πιθανότητες απορρόφησης)\n",
        "            B = np.dot(N, R)\n",
        "\n",
        "            # Τώρα, πρέπει να χαρτογραφήσουμε τις πιθανότητες απορρόφησης στα κλειστά αδιαχώριστα σύνολα\n",
        "            # Αντί για κάθε αναστρέψιμη κατάσταση, θέλουμε για κάθε κλειστό σύνολο\n",
        "            absorption_probs_by_set = {}\n",
        "            for t_idx_pos, t_orig_idx in enumerate(self._transient_states_indices):\n",
        "                absorption_probs_by_set[self.state_names[t_orig_idx]] = {}\n",
        "                for cls_idx_pos, cls_indices in enumerate(self._closed_irreducible_sets_indices):\n",
        "                    prob_to_set = 0.0\n",
        "                    for r_orig_idx in cls_indices:\n",
        "                        if r_orig_idx in self._recurrent_states_indices: # Ensure it's a recurrent state\n",
        "                            r_idx_pos = self._recurrent_states_indices.index(r_orig_idx)\n",
        "                            prob_to_set += B[t_idx_pos, r_idx_pos]\n",
        "                    absorption_probs_by_set[self.state_names[t_orig_idx]][f\"Σύνολο {cls_idx_pos+1} ({[self.state_names[s] for s in cls_indices]})\"] = prob_to_set\n",
        "\n",
        "            print(\"\\n   Πιθανότητες Απορρόφησης σε Κλειστό Σύνολο:\")\n",
        "            for transient_state_name, probs_dict in absorption_probs_by_set.items():\n",
        "                print(f\"     Από '{transient_state_name}':\")\n",
        "                for set_name, prob in probs_dict.items():\n",
        "                    print(f\"       Προς {set_name}: {prob:.4f}\")\n",
        "            return absorption_probs_by_set\n",
        "\n",
        "        except np.linalg.LinAlgError:\n",
        "            print(\"   Ο πίνακας (I-Q) δεν είναι αντιστρέψιμος. Δεν μπορούν να υπολογιστούν οι πιθανότητες απορρόφησης.\")\n",
        "            return None\n",
        "        except Exception as e:\n",
        "            print(f\"   Παρουσιάστηκε σφάλμα κατά τον υπολογισμό των πιθανοτήτων απορρόφησης: {e}\")\n",
        "            return None\n",
        "\n",
        "\n",
        "    def get_mean_absorption_times(self):\n",
        "        \"\"\"\n",
        "        Υπολογίζει τον μέσο χρόνο απορρόφησης από κάθε μεταβατική κατάσταση\n",
        "        στο σύνολο των αναστρέψιμων καταστάσεων.\n",
        "        \"\"\"\n",
        "        if not self._transient_states_indices:\n",
        "            print(\"\\n--- Μέσοι Χρόνοι Απορρόφησης ---\")\n",
        "            print(\"   Δεν υπάρχουν μεταβατικές καταστάσεις. Δεν απαιτείται υπολογισμός απορρόφησης.\")\n",
        "            return None\n",
        "\n",
        "        if not self._recurrent_states_indices:\n",
        "            print(\"\\n--- Μέσοι Χρόνοι Απορρόφησης ---\")\n",
        "            print(\"   Δεν υπάρχουν αναστρέψιμες (απορροφητικές) καταστάσεις. Δεν μπορεί να συμβεί απορρόφηση.\")\n",
        "            return None\n",
        "\n",
        "        num_transient = len(self._transient_states_indices)\n",
        "        # num_recurrent = len(self._recurrent_states_indices) # Not strictly needed here\n",
        "\n",
        "        # Δημιουργία πίνακα P με τις γραμμές/στήλες σε σειρά T, R\n",
        "        sorted_indices = self._transient_states_indices + self._recurrent_states_indices\n",
        "        P_canonical = self.P[np.ix_(sorted_indices, sorted_indices)]\n",
        "\n",
        "        Q = P_canonical[0:num_transient, 0:num_transient]\n",
        "\n",
        "        print(\"\\n--- Μέσοι Χρόνοι Απορρόφησης από Μεταβατικές Καταστάσεις ---\")\n",
        "        print(\"   Μεταβατικές Καταστάσεις (indices):\", self._transient_states_indices)\n",
        "\n",
        "        try:\n",
        "            I_minus_Q = np.identity(num_transient) - Q\n",
        "            N = np.linalg.inv(I_minus_Q) # Θεμελιώδης πίνακας\n",
        "\n",
        "            # Διάνυσμα 1 (όλοι άσσοι) με μέγεθος num_transient\n",
        "            ones_vector = np.ones(num_transient)\n",
        "\n",
        "            # Μέσοι χρόνοι απορρόφησης = N * 1\n",
        "            mean_absorption_times = np.dot(N, ones_vector)\n",
        "\n",
        "            for i, t_orig_idx in enumerate(self._transient_states_indices):\n",
        "                print(f\"   Από την '{self.state_names[t_orig_idx]}': {mean_absorption_times[i]:.4f} βήματα\")\n",
        "            return mean_absorption_times\n",
        "\n",
        "        except np.linalg.LinAlgError:\n",
        "            print(\"   Ο πίνακας (I-Q) δεν είναι αντιστρέψιμος. Δεν μπορούν να υπολογιστούν οι μέσοι χρόνοι απορρόφησης.\")\n",
        "            return None\n",
        "        except Exception as e:\n",
        "            print(f\"   Παρουσιάστηκε σφάλμα κατά τον υπολογισμό των μέσων χρόνων απορρόφησης: {e}\")\n",
        "            return None\n",
        "\n",
        "\n",
        "    # (Οι υπόλοιπες μέθοδοι παραμένουν ίδιες από την προηγούμενη απάντηση)\n",
        "\n",
        "    def get_nth_step_distribution(self, initial_distribution, n_steps):\n",
        "        \"\"\"\n",
        "        Υπολογίζει την κατανομή πιθανοτήτων μετά n βήματα.\n",
        "        \"\"\"\n",
        "        if len(initial_distribution) != self.num_states:\n",
        "            raise ValueError(\"Η αρχική κατανομή πρέπει να έχει μέγεθος ίσο με τον αριθμό των καταστάσεων.\")\n",
        "        if not np.isclose(np.sum(initial_distribution), 1.0):\n",
        "            raise ValueError(\"Η αρχική κατανομή πρέπει να αθροίζει στο 1.\")\n",
        "        if n_steps < 0:\n",
        "            raise ValueError(\"Ο αριθμός των βημάτων πρέπει να είναι μη αρνητικός.\")\n",
        "\n",
        "        P_n = np.linalg.matrix_power(self.P, n_steps)\n",
        "        distribution_n = np.dot(initial_distribution, P_n)\n",
        "        print(f\"\\n--- Κατανομή πιθανοτήτων μετά {n_steps} βήματα ---\")\n",
        "        for i, prob in enumerate(distribution_n):\n",
        "            print(f\"   P(X_{n_steps} = {self.state_names[i]}) = {prob:.4f}\")\n",
        "        return distribution_n\n",
        "\n",
        "    def get_steady_state_distribution(self):\n",
        "        \"\"\"\n",
        "        Υπολογίζει την οριακή (στατική) κατανομή της Μαρκοβιανής αλυσίδας.\n",
        "        \"\"\"\n",
        "        A = self.P.T - np.identity(self.num_states)\n",
        "        A[-1, :] = 1\n",
        "        b = np.zeros(self.num_states)\n",
        "        b[-1] = 1\n",
        "\n",
        "        print(\"\\n--- Υπολογισμός Οριακής (Στατικής) Κατανομής ---\")\n",
        "        try:\n",
        "            steady_state_distribution = np.linalg.solve(A, b)\n",
        "            if np.all(steady_state_distribution >= -1e-9) and np.isclose(np.sum(steady_state_distribution), 1.0):\n",
        "                print(\"   Βρέθηκε οριακή κατανομή (π):\")\n",
        "                for i, prob in enumerate(steady_state_distribution):\n",
        "                    print(f\"     π({self.state_names[i]}) = {prob:.4f}\")\n",
        "                return steady_state_distribution\n",
        "            else:\n",
        "                print(\"   Δεν βρέθηκε έγκυρη μοναδική οριακή κατανομή (πιθανόν αρνητικές πιθανότητες ή μη άθροισμα στο 1).\")\n",
        "                return None\n",
        "        except np.linalg.LinAlgError:\n",
        "            print(\"   Δεν υπάρχει μοναδική οριακή κατανομή (π.χ. αλυσίδα αναγώγιμη ή περιοδική).\")\n",
        "            return None\n",
        "\n",
        "    def get_mean_recurrence_time(self, state_index):\n",
        "        \"\"\"\n",
        "        Υπολογίζει τον μέσο χρόνο επιστροφής (mean recurrence time) για μια αναστρέψιμη κατάσταση.\n",
        "        \"\"\"\n",
        "        if not (0 <= state_index < self.num_states):\n",
        "            raise ValueError(\"Μη έγκυρος δείκτης κατάστασης.\")\n",
        "\n",
        "        print(f\"\\n--- Μέσος χρόνος επιστροφής για την κατάσταση '{self.state_names[state_index]}' ---\")\n",
        "        steady_state = self.get_steady_state_distribution() # This will print its own header\n",
        "        if steady_state is None:\n",
        "            print(f\"   Δεν μπορεί να υπολογιστεί ο μέσος χρόνος επιστροφής για {self.state_names[state_index]} χωρίς οριακή κατανομή.\")\n",
        "            return None\n",
        "\n",
        "        if steady_state[state_index] > 1e-9:\n",
        "            mean_recurrence_time = 1 / steady_state[state_index]\n",
        "            print(f\"   Μέσος χρόνος επιστροφής για την κατάσταση '{self.state_names[state_index]}': {mean_recurrence_time:.4f} βήματα\")\n",
        "            return mean_recurrence_time\n",
        "        else:\n",
        "            print(f\"   Η κατάσταση '{self.state_names[state_index]}' έχει μηδενική πιθανότητα στην οριακή κατανομή. Δεν έχει νόημα ο μέσος χρόνος επιστροφής ή είναι άπειρος.\")\n",
        "            return None\n",
        "\n",
        "    def get_mean_first_passage_times(self, target_state_index):\n",
        "        \"\"\"\n",
        "        Υπολογίζει τους μέσους χρόνους πρώτης διέλευσης (mean first passage times)\n",
        "        προς μια κατάσταση-στόχο από όλες τις άλλες καταστάσεις.\n",
        "        \"\"\"\n",
        "        if not (0 <= target_state_index < self.num_states):\n",
        "            raise ValueError(\"Μη έγκυρος δείκτης κατάστασης-στόχου.\")\n",
        "\n",
        "        non_target_states = [i for i in range(self.num_states) if i != target_state_index]\n",
        "        num_non_target = len(non_target_states)\n",
        "\n",
        "        print(f\"\\n--- Μέσοι Χρόνοι Πρώτης Διέλευσης προς την κατάσταση '{self.state_names[target_state_index]}' ---\")\n",
        "        if num_non_target == 0:\n",
        "            print(f\"   Η κατάσταση {self.state_names[target_state_index]} είναι η μόνη κατάσταση. Ο χρόνος για να φτάσει στον εαυτό της είναι 0.\")\n",
        "            return np.array([0.0])\n",
        "\n",
        "        A = np.zeros((num_non_target, num_non_target))\n",
        "        b = np.ones(num_non_target)\n",
        "\n",
        "        for i_idx, i in enumerate(non_target_states):\n",
        "            for j_idx, j in enumerate(non_target_states):\n",
        "                if i_idx == j_idx:\n",
        "                    A[i_idx, j_idx] = 1 - self.P[i, j]\n",
        "                else:\n",
        "                    A[i_idx, j_idx] = -self.P[i, j]\n",
        "            b[i_idx] += self.P[i, target_state_index]\n",
        "\n",
        "        try:\n",
        "            mean_first_passage_times_partial = np.linalg.solve(A, b)\n",
        "            full_mfpt = np.zeros(self.num_states)\n",
        "            full_mfpt[target_state_index] = 0.0\n",
        "\n",
        "            for i_idx, i in enumerate(non_target_states):\n",
        "                full_mfpt[i] = mean_first_passage_times_partial[i_idx]\n",
        "\n",
        "            for i, time in enumerate(full_mfpt):\n",
        "                print(f\"   Από την '{self.state_names[i]}': {time:.4f} βήματα\")\n",
        "            return full_mfpt\n",
        "\n",
        "        except np.linalg.LinAlgError:\n",
        "            print(f\"   Αδύνατος ο υπολογισμός των μέσων χρόνων πρώτης διέλευσης προς την κατάσταση '{self.state_names[target_state_index]}'.\")\n",
        "            return None\n",
        "    def get_fundamental_matrix_N(self):\n",
        "        \"\"\"\n",
        "        Υπολογίζει και επιστρέφει τον θεμελιώδη πίνακα N = (I - Q)^-1.\n",
        "        Επιστρέφει None αν δεν υπάρχουν μεταβατικές καταστάσεις ή αν ο (I-Q) δεν είναι αντιστρέψιμος.\n",
        "        \"\"\"\n",
        "        # Ensure states are classified first\n",
        "        if not self._transient_states_indices:\n",
        "            self._classify_states() # Re-run classification if needed\n",
        "\n",
        "        T = self._transient_states_indices\n",
        "        num_transient = len(T)\n",
        "\n",
        "        if num_transient == 0:\n",
        "            return None, []\n",
        "\n",
        "        Q = self.P[np.ix_(T, T)]\n",
        "\n",
        "        try:\n",
        "            I_minus_Q = np.identity(num_transient) - Q\n",
        "            N = np.linalg.inv(I_minus_Q)\n",
        "            return N, [self.state_names[idx] for idx in T]\n",
        "        except np.linalg.LinAlgError:\n",
        "            print(\"Προσοχή: Ο πίνακας (I - Q) δεν είναι αντιστρέψιμος. Δεν είναι δυνατός ο υπολογισμός του Θεμελιώδη Πίνακα N.\")\n",
        "            return None, []\n",
        "        except Exception as e:\n",
        "            print(f\"Σφάλμα κατά τον υπολογισμό του Θεμελιώδη Πίνακα N: {e}\")\n",
        "            return None, []\n",
        "\n",
        "    # Make sure your _classify_states method is robust and populates _recurrent_states_indices and _transient_states_indices\n",
        "    # Here's a robust _classify_states from our previous exchanges, ensure it's in your class:\n",
        "    def _classify_states(self):\n",
        "        self._recurrent_states_indices = []\n",
        "        self._transient_states_indices = []\n",
        "        self._closed_irreducible_sets = []\n",
        "\n",
        "        communicating_classes = self._get_communicating_classes()\n",
        "\n",
        "        for comm_class in communicating_classes:\n",
        "            is_closed = True\n",
        "            for state_idx in comm_class:\n",
        "                for next_state_idx in range(self.num_states):\n",
        "                    if self.P[state_idx, next_state_idx] > 0 and next_state_idx not in comm_class:\n",
        "                        is_closed = False\n",
        "                        break\n",
        "                if not is_closed:\n",
        "                    break\n",
        "\n",
        "            if is_closed:\n",
        "                self._recurrent_states_indices.extend(comm_class)\n",
        "                self._closed_irreducible_sets.append(sorted(comm_class))\n",
        "            else:\n",
        "                self._transient_states_indices.extend(comm_class)\n",
        "\n",
        "        # Ensure all states are classified. Any unclassified states are transient.\n",
        "        all_classified = set(self._recurrent_states_indices).union(set(self._transient_states_indices))\n",
        "        if len(all_classified) != self.num_states:\n",
        "            remaining_states = set(range(self.num_states)) - all_classified\n",
        "            self._transient_states_indices.extend(list(remaining_states))\n",
        "\n",
        "        self._recurrent_states_indices.sort() # Ensure sorted for consistent output\n",
        "        self._transient_states_indices.sort() # Ensure sorted for consistent output\n",
        "\n",
        "    # Also, ensure you have these helper methods if they are used by _classify_states\n",
        "    def _get_reachable_states(self, start_state_idx):\n",
        "        reachable = {start_state_idx}\n",
        "        queue = collections.deque([start_state_idx])\n",
        "        while queue:\n",
        "            current = queue.popleft()\n",
        "            for next_state in range(self.num_states):\n",
        "                if self.P[current, next_state] > 0 and next_state not in reachable:\n",
        "                    reachable.add(next_state)\n",
        "                    queue.append(next_state)\n",
        "        return reachable\n",
        "\n",
        "    def _get_communicating_classes(self):\n",
        "        classes = []\n",
        "        unvisited = set(range(self.num_states))\n",
        "\n",
        "        while unvisited:\n",
        "            start_node = unvisited.pop()\n",
        "            reachable_from_start = self._get_reachable_states(start_node)\n",
        "\n",
        "            reverse_adj = collections.defaultdict(list)\n",
        "            for i in range(self.num_states):\n",
        "                for j in range(self.num_states):\n",
        "                    if self.P[j, i] > 0:\n",
        "                        reverse_adj[i].append(j)\n",
        "\n",
        "            q_rev = collections.deque([start_node])\n",
        "            visited_rev = {start_node}\n",
        "            while q_rev:\n",
        "                curr_rev = q_rev.popleft()\n",
        "                for prev_state in reverse_adj[curr_rev]:\n",
        "                    if prev_state not in visited_rev:\n",
        "                        visited_rev.add(prev_state)\n",
        "                        q_rev.append(prev_state) # Corrected variable name prev_rev to prev_state\n",
        "            reachable_to_start = visited_rev\n",
        "\n",
        "            communicating_class = reachable_from_start.intersection(reachable_to_start)\n",
        "\n",
        "            if communicating_class:\n",
        "                classes.append(list(communicating_class))\n",
        "                unvisited -= communicating_class\n",
        "        return classes\n",
        "    def get_period(self, state_index):\n",
        "        \"\"\"\n",
        "        Υπολογίζει την περίοδος μιας κατάστασης.\n",
        "        \"\"\"\n",
        "        if not (0 <= state_index < self.num_states):\n",
        "            raise ValueError(\"Μη έγκυρος δείκτης κατάστασης.\")\n",
        "\n",
        "        print(f\"\\n--- Περίοδος της κατάστασης '{self.state_names[state_index]}' ---\")\n",
        "        possible_return_times = []\n",
        "        P_k = np.identity(self.num_states)\n",
        "\n",
        "        max_steps_check = 2 * self.num_states + 5 # Heuristic to find enough return times\n",
        "\n",
        "        for k in range(1, max_steps_check + 1):\n",
        "            P_k = np.dot(P_k, self.P)\n",
        "            if P_k[state_index, state_index] > 1e-9:\n",
        "                possible_return_times.append(k)\n",
        "\n",
        "        if not possible_return_times:\n",
        "            print(f\"   Η κατάσταση '{self.state_names[state_index]}' δεν μπορεί να επιστρέψει στον εαυτό της. Περίοδος: 0\")\n",
        "            return 0\n",
        "\n",
        "        # Calculate GCD of all possible return times\n",
        "        period = possible_return_times[0]\n",
        "        for t in possible_return_times[1:]:\n",
        "            period = math.gcd(period, t)\n",
        "\n",
        "        print(f\"   Περίοδος της κατάστασης '{self.state_names[state_index]}': {period}\")\n",
        "        return period\n",
        "\n",
        "    def draw_markov_chain(self):\n",
        "        \"\"\"\n",
        "        Δημιουργεί ένα οπτικό διάγραμμα της Μαρκοβιανής αλυσίδας χρησιμοποιώντας Graphviz\n",
        "        και το εμφανίζει απευθείας σε περιβάλλοντα όπως το Colab.\n",
        "        \"\"\"\n",
        "        dot = graphviz.Digraph(comment='Markov Chain', format='png') # Specify format for better display\n",
        "        dot.attr(rankdir='LR') # Left to Right layout\n",
        "\n",
        "        # Add nodes (states)\n",
        "        for i, name in enumerate(self.state_names):\n",
        "            dot.node(str(i), name, shape='circle', style='filled', fillcolor='lightblue') # Added styling\n",
        "\n",
        "        # Add edges (transitions)\n",
        "        for i in range(self.num_states):\n",
        "            for j in range(self.num_states):\n",
        "                prob = self.P[i, j]\n",
        "                if prob > 1e-9: # Only for non-zero probabilities\n",
        "                    # Adjust label for self-loops\n",
        "                    if i == j:\n",
        "                        dot.edge(str(i), str(j), label=f\"{prob:.2f}\",\n",
        "                                 minlen='2', # Make self-loops larger\n",
        "                                 dir='forward',\n",
        "                                 color='darkgreen',\n",
        "                                 style='dashed', # Dotted line for self-loops\n",
        "                                 constraint='false') # Don't use for layout calculations\n",
        "                    else:\n",
        "                        dot.edge(str(i), str(j), label=f\"{prob:.2f}\",\n",
        "                                 color='gray',\n",
        "                                 fontcolor='darkblue',\n",
        "                                 penwidth=str(0.5 + prob*2)) # Thicker lines for higher prob\n",
        "\n",
        "        print(f\"\\n--- Διάγραμμα Μαρκοβιανής Αλυσίδας ---\")\n",
        "        print(f\"   Το διάγραμμα θα εμφανιστεί παρακάτω.\")\n",
        "        try:\n",
        "            display(dot) # This displays the graph in Colab/Jupyter\n",
        "        except Exception as e:\n",
        "            print(f\"   Σφάλμα εμφάνισης διαγράμματος: {e}\")\n",
        "            print(f\"   Βεβαιωθείτε ότι το Graphviz είναι εγκατεστημένο και προσβάσιμο.\")\n",
        "\n",
        "\n",
        "    def perform_full_analysis(self, initial_distribution=None, n_steps_for_dist=5, target_state_for_mfpt=0, draw_graph=True):\n",
        "        \"\"\"\n",
        "        Πραγματοποιεί μια πλήρη ανάλυση της Μαρκοβιανής αλυσίδας, εκτυπώνοντας τα αποτελέσματα.\n",
        "\n",
        "        Args:\n",
        "            initial_distribution (np.array, optional): Αρχική κατανομή για την εμφάνιση της κατανομής μετά n βήματα.\n",
        "                                                        Αν δεν δοθεί, χρησιμοποιείται ομοιόμορφη κατανομή.\n",
        "            n_steps_for_dist (int, optional): Ο αριθμός των βημάτων για τον υπολογισμό της κατανομής.\n",
        "            target_state_for_mfpt (int, optional): Ο δείκτης της κατάστασης-στόχου για τους μέσους χρόνους πρώτης διέλευσης.\n",
        "            draw_graph (bool, optional): Αν θα σχεδιαστεί το γράφημα της αλυσίδας. Προεπιλογή True.\n",
        "        \"\"\"\n",
        "        print(\"\\n\" + \"=\"*60)\n",
        "        print(\"                   ΕΚΤΕΛΕΣΗ ΠΛΗΡΟΥΣ ΑΝΑΛΥΣΗΣ ΜΑΡΚΟΒΙΑΝΗΣ ΑΛΥΣΙΔΑΣ\")\n",
        "        print(\"=\"*60 + \"\\n\")\n",
        "\n",
        "        # 0. Σχεδίαση Μαρκοβιανής Αλυσίδας\n",
        "        if draw_graph:\n",
        "            self.draw_markov_chain()\n",
        "\n",
        "        # 1. Κατανομή πιθανοτήτων μετά n βήματα\n",
        "        if initial_distribution is None:\n",
        "            initial_distribution = np.full(self.num_states, 1.0 / self.num_states)\n",
        "            print(f\"\\nΧρησιμοποιήθηκε ομοιόμορφη αρχική κατανομή για την ανάλυση: {initial_distribution}\")\n",
        "\n",
        "        self.get_nth_step_distribution(initial_distribution, n_steps_for_dist)\n",
        "\n",
        "        # 2. Οριακή (Στατική) Κατανομή\n",
        "        steady_state = self.get_steady_state_distribution()\n",
        "\n",
        "        # 3. Ταξινόμηση Καταστάσεων (καλεί την εσωτερική μέθοδο κατά την αρχικοποίηση και εκτυπώνει)\n",
        "        self.get_state_classification()\n",
        "\n",
        "        # 4. Κλειστά και Αδιαχώριστα Σύνολα\n",
        "        self.get_closed_irreducible_sets()\n",
        "\n",
        "        # 5. Υπολογισμός Περιόδου για κάθε κατάσταση\n",
        "        print(\"\\n--- Ανάλυση Περιόδου για όλες τις Καταστάσεις ---\")\n",
        "        periods = {}\n",
        "        for i in range(self.num_states):\n",
        "            period_val = self.get_period(i)\n",
        "            periods[self.state_names[i]] = period_val\n",
        "\n",
        "        # 6. Μέσος Χρόνος Επιστροφής (μόνο για αναστρέψιμες καταστάσεις)\n",
        "        print(\"\\n--- Μέσοι Χρόνοι Επιστροφής για Αναστρέψιμες Καταστάσεις ---\")\n",
        "        if steady_state is not None:\n",
        "            for i in self._recurrent_states_indices: # Iterate through actual recurrent indices\n",
        "                state_name = self.state_names[i]\n",
        "                if steady_state[i] > 1e-9:\n",
        "                    mean_recurrence_time = 1 / steady_state[i]\n",
        "                    print(f\"   Μέσος χρόνος επιστροφής για την κατάσταση '{state_name}': {mean_recurrence_time:.4f} βήματα\")\n",
        "                else:\n",
        "                    print(f\"   Η κατάσταση '{state_name}' έχει μηδενική πιθανότητα στην οριακή κατανομή. Δεν έχει νόημα ο μέσος χρόνος επιστροφής ή είναι άπειρος.\")\n",
        "        else:\n",
        "            print(\"   Δεν υπάρχει μοναδική οριακή κατανομή, άρα δεν μπορούν να υπολογιστούν οι μέσοι χρόνοι επιστροφής.\")\n",
        "\n",
        "\n",
        "        # 7. Μέσοι Χρόνοι Πρώτης Διέλευσης\n",
        "        if 0 <= target_state_for_mfpt < self.num_states:\n",
        "            self.get_mean_first_passage_times(target_state_for_mfpt)\n",
        "        else:\n",
        "            print(f\"\\n--- Μέσοι Χρόνοι Πρώτης Διέλευσης ---\")\n",
        "            print(f\"   Ο δείκτης κατάστασης-στόχου ({target_state_for_mfpt}) για τους μέσους χρόνους πρώτης διέλευσης δεν είναι έγκυρος.\")\n",
        "\n",
        "        # ΝΕΕΣ ΛΕΙΤΟΥΡΓΙΕΣ\n",
        "        # 8. Πιθανότητες Απορρόφησης\n",
        "        self.get_absorption_probabilities()\n",
        "\n",
        "        # 9. Μέσοι Χρόνοι Απορρόφησης\n",
        "        self.get_mean_absorption_times()\n",
        "\n",
        "\n",
        "        print(\"\\n\" + \"=\"*60)\n",
        "        print(\"                   ΤΕΛΟΣ ΠΛΗΡΟΥΣ ΑΝΑΛΥΣΗΣ\")\n",
        "        print(\"=\"*60 + \"\\n\")\n",
        "\n",
        "\n",
        "# --- Το επόμενο cell θα είναι το παράδειγμα χρήσης ---\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    # Ensure graphviz is installed for visualization\n",
        "    # If running in Colab and it's not installed, uncomment the line below:\n",
        "    # !pip install graphviz\n",
        "\n",
        "    # This part now assumes user_states and user_transition_matrix\n",
        "    # have been defined by running the first cell.\n",
        "\n",
        "    # Option 1: Use the chain defined by the user in the first cell\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Ανάλυση Μαρκοβιανής Αλυσίδας από Είσοδο Χρήστη (από το Πρώτο Cell)\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "\n",
        "    try:\n",
        "        # We need to make sure user_states and user_transition_matrix are accessible.\n",
        "        # If the first cell defines them in its __main__ block, they might not be\n",
        "        # directly available here.\n",
        "        # The best way is to ensure the first cell's `if __name__ == \"__main__\":`\n",
        "        # block is structured so that `user_states` and `user_transition_matrix`\n",
        "        # are at a scope accessible by subsequent cells, or passed explicitly.\n",
        "        # For a simple Colab flow, running cell 1 then cell 2 works if these are globals.\n",
        "\n",
        "        # Create an instance of the Markov Chain using the user's input\n",
        "        user_mc_analysis = DiscreteTimeMarkovChain(user_transition_matrix, user_states)\n",
        "\n",
        "        # You might want to ask the user for these analysis parameters too,\n",
        "        # or use sensible defaults as shown.\n",
        "        analysis_n_steps = int(input(\"Εισάγετε τον αριθμό βημάτων για την ανάλυση κατανομής (π.χ. 10): \"))\n",
        "        analysis_target_state_index_input = input(f\"Εισάγετε το **όνομα** της κατάστασης-στόχου για MFPT ({', '.join(user_states)}): \")\n",
        "\n",
        "        try:\n",
        "            analysis_target_state_index = user_states.index(analysis_target_state_index_input)\n",
        "        except ValueError:\n",
        "            print(f\"Το όνομα κατάστασης '{analysis_target_state_index_input}' δεν βρέθηκε. Χρησιμοποιείται η πρώτη κατάσταση ως προεπιλογή για MFPT.\")\n",
        "            analysis_target_state_index = 0\n",
        "\n",
        "        # You'd need to define an initial_distribution. A common choice is uniform or from a specific start.\n",
        "        # For full analysis, let's use a uniform distribution unless the user_initial_state (from prev cell) is desired.\n",
        "        # For simplicity, we'll use a uniform distribution here for the analysis part.\n",
        "        initial_dist_for_analysis = np.full(len(user_states), 1.0 / len(user_states))\n",
        "\n",
        "        user_mc_analysis.perform_full_analysis(\n",
        "            initial_distribution=initial_dist_for_analysis,\n",
        "            n_steps_for_dist=analysis_n_steps,\n",
        "            target_state_for_mfpt=analysis_target_state_index\n",
        "        )\n",
        "\n",
        "    except NameError:\n",
        "        print(\"Σφάλμα: Οι μεταβλητές 'user_states' ή 'user_transition_matrix' δεν βρέθηκαν.\")\n",
        "        print(\"Παρακαλώ βεβαιωθείτε ότι έχετε εκτελέσει το πρώτο κελί και έχετε δώσει είσοδο.\")\n",
        "    except ValueError as e:\n",
        "        print(f\"Σφάλμα στην είσοδο ανάλυσης: {e}\")\n",
        "    except Exception as e:\n",
        "        print(f\"Παρουσιάστηκε απροσδόκητο σφάλμα: {e}\")\n",
        "\n",
        "\n",
        "    # Original example analyses remain for demonstration purposes (optional to keep)\n",
        "    # They are commented out by default to avoid clutter when running the user's chain.\n",
        "    \"\"\"\n",
        "    # Παράδειγμα 1: Καιρικές συνθήκες\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Παράδειγμα 1: Ανάλυση Καιρικών Συνθηκών (Απεριοδική, Αμείωτη)\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "    weather_matrix = np.array([\n",
        "        [0.7, 0.2, 0.1],\n",
        "        [0.3, 0.4, 0.3],\n",
        "        [0.2, 0.3, 0.5]\n",
        "    ])\n",
        "    weather_states = [\"Καλοκαιρία\", \"Συννεφιά\", \"Βροχή\"]\n",
        "\n",
        "    weather_mc = DiscreteTimeMarkovChain(weather_matrix, weather_states)\n",
        "    weather_mc.perform_full_analysis(initial_distribution=np.array([1.0, 0.0, 0.0]), n_steps_for_dist=5, target_state_for_mfpt=2)\n",
        "\n",
        "\n",
        "    # Παράδειγμα 2: Αλυσίδα με απορροφητική κατάσταση\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Παράδειγμα 2: Ανάλυση Αλυσίδας με Απορροφητική Κατάσταση (Αναγώγιμη)\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "    # State C is absorbing (P[2,2] = 1.0)\n",
        "    # States A, B are transient\n",
        "    abs_matrix = np.array([\n",
        "        [0.0, 0.5, 0.5], # From A: 0.5 to B, 0.5 to C\n",
        "        [0.6, 0.0, 0.4], # From B: 0.6 to A, 0.4 to C\n",
        "        [0.0, 0.0, 1.0]  # From C: 1.0 to C (absorbing)\n",
        "    ])\n",
        "    abs_states = [\"A\", \"B\", \"C\"]\n",
        "\n",
        "    abs_mc = DiscreteTimeMarkovChain(abs_matrix, abs_states)\n",
        "    abs_mc.perform_full_analysis(initial_distribution=np.array([1.0, 0.0, 0.0]), n_steps_for_dist=100, target_state_for_mfpt=2)\n",
        "\n",
        "\n",
        "    # Παράδειγμα 3: Αναγώγιμη (Reducible) αλυσίδα με δύο κλειστά σύνολα\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Παράδειγμα 3: Ανάλυση Αναγώγιμης Αλυσίδας (με δύο κλειστά αδιαχώριστα σύνολα)\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "    reducible_matrix = np.array([\n",
        "        [0.5, 0.5, 0.0, 0.0], # S1 -> S1, S2\n",
        "        [0.3, 0.7, 0.0, 0.0], # S2 -> S1, S2 (Set {S1, S2} is closed and irreducible)\n",
        "        [0.0, 0.0, 0.8, 0.2], # S3 -> S3, S4\n",
        "        [0.0, 0.0, 0.1, 0.9]  # S4 -> S3, S4 (Set {S3, S4} is closed and irreducible)\n",
        "    ])\n",
        "    reducible_states = [\"S1\", \"S2\", \"S3\", \"S4\"]\n",
        "\n",
        "    reducible_mc = DiscreteTimeMarkovChain(reducible_matrix, reducible_states)\n",
        "    reducible_mc.perform_full_analysis(n_steps_for_dist=10)\n",
        "\n",
        "\n",
        "    # Παράδειγμα 4: Περιοδική Αλυσίδα (Περίοδος 2)\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Παράδειγμα 4: Ανάλυση Περιοδικής Αλυσίδας (Περίοδος 2)\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "    periodic_matrix = np.array([\n",
        "        [0.0, 1.0],\n",
        "        [1.0, 0.0]\n",
        "    ])\n",
        "    periodic_states = [\"S0\", \"S1\"]\n",
        "\n",
        "    periodic_mc = DiscreteTimeMarkovChain(periodic_matrix, periodic_states)\n",
        "    periodic_mc.perform_full_analysis(initial_distribution=np.array([1.0, 0.0]), n_steps_for_dist=5)\n",
        "\n",
        "\n",
        "    # ΝΕΟ ΠΑΡΑΔΕΙΓΜΑ: Αλυσίδα με μεταβατικές και πολλαπλές απορροφητικές κλάσεις\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Νέο Παράδειγμα: Μεταβατικές και Πολλαπλές Απορροφητικές Κλάσες\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "    # States: T1, T2 (transient), R1, R2 (recurrent/absorbing classes)\n",
        "    complex_matrix = np.array([\n",
        "        # T1   T2   R1   R2\n",
        "        [0.2, 0.3, 0.4, 0.1],  # From T1\n",
        "        [0.5, 0.1, 0.2, 0.2],  # From T2\n",
        "        [0.0, 0.0, 1.0, 0.0],  # From R1 (absorbing)\n",
        "        [0.0, 0.0, 0.0, 1.0]   # From R2 (absorbing)\n",
        "    ])\n",
        "    complex_states = [\"T1\", \"T2\", \"R1\", \"R2\"]\n",
        "\n",
        "    complex_mc = DiscreteTimeMarkovChain(complex_matrix, complex_states)\n",
        "    complex_mc.perform_full_analysis(initial_distribution=np.array([1.0, 0.0, 0.0, 0.0]), n_steps_for_dist=5, target_state_for_mfpt=2)\n",
        "    \"\"\""
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 1000
        },
        "id": "R8Su7f_hm_QH",
        "outputId": "9ff1aaec-7d43-40b9-8311-07deac91a08c"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "######################################################################\n",
            "### Ανάλυση Μαρκοβιανής Αλυσίδας από Είσοδο Χρήστη (από το Πρώτο Cell)\n",
            "######################################################################\n",
            "\n",
            "Δημιουργήθηκε Μαρκοβιανή Αλυσίδα με 3 καταστάσεις: 1, 2, 3\n",
            "\n",
            "Πίνακας Μετάβασης (P):\n",
            "[[0.   1.   0.  ]\n",
            " [0.   0.66 0.34]\n",
            " [0.34 0.   0.66]]\n",
            "Εισάγετε τον αριθμό βημάτων για την ανάλυση κατανομής (π.χ. 10): 10\n",
            "Εισάγετε το **όνομα** της κατάστασης-στόχου για MFPT (1, 2, 3): 2\n",
            "\n",
            "============================================================\n",
            "                   ΕΚΤΕΛΕΣΗ ΠΛΗΡΟΥΣ ΑΝΑΛΥΣΗΣ ΜΑΡΚΟΒΙΑΝΗΣ ΑΛΥΣΙΔΑΣ\n",
            "============================================================\n",
            "\n",
            "\n",
            "--- Διάγραμμα Μαρκοβιανής Αλυσίδας ---\n",
            "   Το διάγραμμα θα εμφανιστεί παρακάτω.\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "image/svg+xml": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Generated by graphviz version 2.43.0 (0)\n -->\n<!-- Title: %3 Pages: 1 -->\n<svg width=\"238pt\" height=\"112pt\"\n viewBox=\"0.00 0.00 238.00 112.00\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 108)\">\n<title>%3</title>\n<polygon fill=\"white\" stroke=\"transparent\" points=\"-4,4 -4,-108 234,-108 234,4 -4,4\"/>\n<!-- 0 -->\n<g id=\"node1\" class=\"node\">\n<title>0</title>\n<ellipse fill=\"lightblue\" stroke=\"black\" cx=\"18\" cy=\"-18\" rx=\"18\" ry=\"18\"/>\n<text text-anchor=\"middle\" x=\"18\" y=\"-14.3\" font-family=\"Times,serif\" font-size=\"14.00\">1</text>\n</g>\n<!-- 1 -->\n<g id=\"node2\" class=\"node\">\n<title>1</title>\n<ellipse fill=\"lightblue\" stroke=\"black\" cx=\"115\" cy=\"-53\" rx=\"18\" ry=\"18\"/>\n<text text-anchor=\"middle\" x=\"115\" y=\"-49.3\" font-family=\"Times,serif\" font-size=\"14.00\">2</text>\n</g>\n<!-- 0&#45;&gt;1 -->\n<g id=\"edge1\" class=\"edge\">\n<title>0&#45;&gt;1</title>\n<path fill=\"none\" stroke=\"gray\" stroke-width=\"2.5\" d=\"M35.12,-23.94C49.58,-29.27 71.08,-37.19 87.98,-43.41\"/>\n<polygon fill=\"gray\" stroke=\"gray\" stroke-width=\"2.5\" points=\"87.21,-46.86 97.81,-47.03 89.63,-40.29 87.21,-46.86\"/>\n<text text-anchor=\"middle\" x=\"66.5\" y=\"-42.8\" font-family=\"Times,serif\" font-size=\"14.00\" fill=\"darkblue\">1.00</text>\n</g>\n<!-- 1&#45;&gt;1 -->\n<g id=\"edge2\" class=\"edge\">\n<title>1&#45;&gt;1</title>\n<path fill=\"none\" stroke=\"darkgreen\" stroke-dasharray=\"5,2\" d=\"M106.64,-69.29C104.62,-79.39 107.41,-89 115,-89 119.86,-89 122.76,-85.06 123.68,-79.57\"/>\n<polygon fill=\"darkgreen\" stroke=\"darkgreen\" points=\"127.17,-79.18 123.36,-69.29 120.17,-79.39 127.17,-79.18\"/>\n<text text-anchor=\"middle\" x=\"115\" y=\"-92.8\" font-family=\"Times,serif\" font-size=\"14.00\">0.66</text>\n</g>\n<!-- 2 -->\n<g id=\"node3\" class=\"node\">\n<title>2</title>\n<ellipse fill=\"lightblue\" stroke=\"black\" cx=\"212\" cy=\"-18\" rx=\"18\" ry=\"18\"/>\n<text text-anchor=\"middle\" x=\"212\" y=\"-14.3\" font-family=\"Times,serif\" font-size=\"14.00\">3</text>\n</g>\n<!-- 1&#45;&gt;2 -->\n<g id=\"edge3\" class=\"edge\">\n<title>1&#45;&gt;2</title>\n<path fill=\"none\" stroke=\"gray\" stroke-width=\"1.18\" d=\"M132.12,-47.06C146.58,-41.73 168.08,-33.81 184.98,-27.59\"/>\n<polygon fill=\"gray\" stroke=\"gray\" stroke-width=\"1.18\" points=\"186.63,-30.71 194.81,-23.97 184.21,-24.14 186.63,-30.71\"/>\n<text text-anchor=\"middle\" x=\"163.5\" y=\"-42.8\" font-family=\"Times,serif\" font-size=\"14.00\" fill=\"darkblue\">0.34</text>\n</g>\n<!-- 2&#45;&gt;0 -->\n<g id=\"edge4\" class=\"edge\">\n<title>2&#45;&gt;0</title>\n<path fill=\"none\" stroke=\"gray\" stroke-width=\"1.18\" d=\"M193.99,-16.03C178.21,-14.31 154.05,-11.94 133,-11 117.02,-10.28 112.98,-10.28 97,-11 80.06,-11.76 61.11,-13.44 46.13,-14.96\"/>\n<polygon fill=\"gray\" stroke=\"gray\" stroke-width=\"1.18\" points=\"45.59,-11.5 36.01,-16.03 46.32,-18.46 45.59,-11.5\"/>\n<text text-anchor=\"middle\" x=\"115\" y=\"-14.8\" font-family=\"Times,serif\" font-size=\"14.00\" fill=\"darkblue\">0.34</text>\n</g>\n<!-- 2&#45;&gt;2 -->\n<g id=\"edge5\" class=\"edge\">\n<title>2&#45;&gt;2</title>\n<path fill=\"none\" stroke=\"darkgreen\" stroke-dasharray=\"5,2\" d=\"M203.64,-34.29C201.62,-44.39 204.41,-54 212,-54 216.86,-54 219.76,-50.06 220.68,-44.57\"/>\n<polygon fill=\"darkgreen\" stroke=\"darkgreen\" points=\"224.17,-44.18 220.36,-34.29 217.17,-44.39 224.17,-44.18\"/>\n<text text-anchor=\"middle\" x=\"212\" y=\"-57.8\" font-family=\"Times,serif\" font-size=\"14.00\">0.66</text>\n</g>\n</g>\n</svg>\n",
            "text/plain": [
              "<graphviz.graphs.Digraph at 0x78b2bcc7cf80>"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "--- Κατανομή πιθανοτήτων μετά 10 βήματα ---\n",
            "   P(X_10 = 1) = 0.1453\n",
            "   P(X_10 = 2) = 0.4273\n",
            "   P(X_10 = 3) = 0.4274\n",
            "\n",
            "--- Υπολογισμός Οριακής (Στατικής) Κατανομής ---\n",
            "   Βρέθηκε οριακή κατανομή (π):\n",
            "     π(1) = 0.1453\n",
            "     π(2) = 0.4274\n",
            "     π(3) = 0.4274\n",
            "\n",
            "--- Ταξινόμηση Καταστάσεων ---\n",
            "   Αναστρέψιμες (Recurrent) Καταστάσεις: ['1', '2', '3']\n",
            "   Μεταβατικές (Transient) Καταστάσεις: []\n",
            "\n",
            "--- Κλειστά και Αδιαχώριστα Σύνολα (Closed and Irreducible Sets) ---\n",
            "   Σύνολο 1: {1, 2, 3}\n",
            "\n",
            "--- Ανάλυση Περιόδου για όλες τις Καταστάσεις ---\n",
            "\n",
            "--- Περίοδος της κατάστασης '1' ---\n",
            "   Περίοδος της κατάστασης '1': 1\n",
            "\n",
            "--- Περίοδος της κατάστασης '2' ---\n",
            "   Περίοδος της κατάστασης '2': 1\n",
            "\n",
            "--- Περίοδος της κατάστασης '3' ---\n",
            "   Περίοδος της κατάστασης '3': 1\n",
            "\n",
            "--- Μέσοι Χρόνοι Επιστροφής για Αναστρέψιμες Καταστάσεις ---\n",
            "   Μέσος χρόνος επιστροφής για την κατάσταση '1': 6.8824 βήματα\n",
            "   Μέσος χρόνος επιστροφής για την κατάσταση '2': 2.3400 βήματα\n",
            "   Μέσος χρόνος επιστροφής για την κατάσταση '3': 2.3400 βήματα\n",
            "\n",
            "--- Μέσοι Χρόνοι Πρώτης Διέλευσης προς την κατάσταση '2' ---\n",
            "   Από την '1': 2.0000 βήματα\n",
            "   Από την '2': 0.0000 βήματα\n",
            "   Από την '3': 4.9412 βήματα\n",
            "\n",
            "--- Πιθανότητες Απορρόφησης ---\n",
            "   Δεν υπάρχουν μεταβατικές καταστάσεις. Δεν απαιτείται υπολογισμός απορρόφησης.\n",
            "\n",
            "--- Μέσοι Χρόνοι Απορρόφησης ---\n",
            "   Δεν υπάρχουν μεταβατικές καταστάσεις. Δεν απαιτείται υπολογισμός απορρόφησης.\n",
            "\n",
            "============================================================\n",
            "                   ΤΕΛΟΣ ΠΛΗΡΟΥΣ ΑΝΑΛΥΣΗΣ\n",
            "============================================================\n",
            "\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [],
      "metadata": {
        "id": "3zgDozWvXDIh"
      }
    },
    {
      "cell_type": "code",
      "source": [],
      "metadata": {
        "id": "SkzcomxvW4Mt"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1RXo2NbYNxCr"
      },
      "outputs": [],
      "source": [
        "# ===== Colab Cell 1: Library (real-only Hermite/CH with symbolic n, t) =====\n",
        "# !pip -q install sympy\n",
        "\n",
        "import sympy as sp\n",
        "x = sp.Symbol('x')\n",
        "I = sp.I\n",
        "\n",
        "# ---------- helpers ----------\n",
        "def vec_col_major(M):\n",
        "    M = sp.Matrix(M)\n",
        "    n, m = M.shape\n",
        "    return sp.Matrix(n*m, 1, lambda i, _: M[i % n, i // n])\n",
        "\n",
        "def evaluate_on_matrix(A, coeffs):\n",
        "    \"\"\"\n",
        "    v(A) for v(x)=sum_{k=0}^{q-1} a_k x^k, with symbolic coefficients a_k(n) or a_k(t).\n",
        "    \"\"\"\n",
        "    A = sp.Matrix(A)\n",
        "    m = A.shape[0]\n",
        "    res = sp.zeros(m)\n",
        "    P = sp.eye(m)\n",
        "    for k, ak in enumerate(coeffs):\n",
        "        if k == 0:\n",
        "            res += ak * sp.eye(m)\n",
        "        else:\n",
        "            P = P * A\n",
        "            res += ak * P\n",
        "    return sp.simplify(res)\n",
        "\n",
        "# ---------- minimal polynomial via linear system B_A ----------\n",
        "def minimal_polynomial_via_linear(A):\n",
        "    A = sp.Matrix(A)\n",
        "    n = A.shape[0]\n",
        "    for q in range(1, n+1):\n",
        "        B_cols = [vec_col_major(A**k) for k in range(q)]\n",
        "        B = sp.Matrix.hstack(*B_cols)\n",
        "        b = vec_col_major(A**q)\n",
        "        sol = sp.linsolve((B, -b))\n",
        "        if sol != sp.EmptySet:\n",
        "            c = list(next(iter(sol)))\n",
        "            poly = x**q + sum(c[k] * x**k for k in range(q))\n",
        "            return sp.poly(sp.factor(poly), x).monic()\n",
        "    # fallback\n",
        "    try:\n",
        "        expr = A.minimal_polynomial(x)\n",
        "        return sp.poly(expr, x).monic()\n",
        "    except Exception:\n",
        "        return A.charpoly(x).as_poly().monic()\n",
        "\n",
        "\n",
        "def factor_minpoly(mA):\n",
        "    \"\"\"\n",
        "    Factor m_A over C → (roots, mults) with correct multiplicities.\n",
        "    \"\"\"\n",
        "    expr = sp.factor(mA.as_expr())\n",
        "    _, plist = sp.factor_list(expr)\n",
        "    roots, mults = [], []\n",
        "    for p, e in plist:\n",
        "        inner = sp.roots(p)\n",
        "        for lam, mr in inner.items():\n",
        "            roots.append(sp.nsimplify(lam))\n",
        "            mults.append(e * mr)\n",
        "    return roots, mults\n",
        "\n",
        "# ---------- split roots to real & conjugate pairs ----------\n",
        "def split_real_conj(roots, mults):\n",
        "    used = [False]*len(roots)\n",
        "    reals, pairs, fallback = [], [], False\n",
        "    for i, (ri, mi) in enumerate(zip(roots, mults)):\n",
        "        if used[i]:\n",
        "            continue\n",
        "        if sp.simplify(sp.im(ri)) == 0:\n",
        "            reals.append((sp.nsimplify(ri), mi))\n",
        "            used[i] = True\n",
        "        else:\n",
        "            ci = sp.conjugate(ri)\n",
        "            j_found = None\n",
        "            for j in range(i+1, len(roots)):\n",
        "                if not used[j] and sp.simplify(roots[j] - ci) == 0 and mults[j] == mi:\n",
        "                    j_found = j\n",
        "                    break\n",
        "            if j_found is None:\n",
        "                fallback = True\n",
        "                used[i] = True\n",
        "            else:\n",
        "                # keep representative with Im>0\n",
        "                if sp.im(ri) < 0:\n",
        "                    ri = ci\n",
        "                pairs.append((sp.nsimplify(ri), mi))\n",
        "                used[i] = True\n",
        "                used[j_found] = True\n",
        "    return reals, pairs, fallback\n",
        "\n",
        "# ---------- build REAL confluent-Vandermonde (polar for complex roots) ----------\n",
        "def real_hermite_system(A, roots, mults, mode, n=None, t=None):\n",
        "    \"\"\"\n",
        "    Build real system M a = y for v(x)=sum_{k=0}^{q-1} a_k x^k.\n",
        "    mode ∈ {'power','exp','inv'}, with symbolic n or t allowed.\n",
        "    \"\"\"\n",
        "    reals, pairs, fallback = split_real_conj(roots, mults)\n",
        "    q = sum(mults)\n",
        "    rows, rhs = [], []\n",
        "\n",
        "    fact = sp.factorial\n",
        "    ff   = sp.FallingFactorial # Corrected from sp.fallingfactorial\n",
        "\n",
        "    # real roots\n",
        "    for lam, r in reals:\n",
        "        for j in range(r):\n",
        "            row = [ (fact(k)//fact(k-j) * lam**(k-j) if k>=j else 0) for k in range(q) ]\n",
        "            rows.append(row)\n",
        "            if mode == 'power':\n",
        "                rhs.append(sp.simplify(ff(n, j) * lam**(n - j)))\n",
        "            elif mode == 'exp':\n",
        "                rhs.append(sp.simplify((t**j) * sp.exp(t*lam)))\n",
        "            elif mode == 'inv':\n",
        "                rhs.append(0 if sp.simplify(lam)==0 else ((-1)**j)*fact(j)/lam**(j+1))\n",
        "            else:\n",
        "                raise ValueError(\"mode?\")\n",
        "\n",
        "    # complex conjugate pairs → two real rows (cos/sin), polar form\n",
        "    for z, rmult in pairs:\n",
        "        rho, theta = sp.Abs(z), sp.arg(z)\n",
        "        for j in range(rmult):\n",
        "            rowR, rowI = [], []\n",
        "            for k in range(q):\n",
        "                if k < j:\n",
        "                    rowR.append(0); rowI.append(0)\n",
        "                else:\n",
        "                    m = k - j\n",
        "                    c = fact(k)//fact(k-j)\n",
        "                    rowR.append(c * rho**m * sp.cos(m*theta))\n",
        "                    rowI.append(c * rho**m * sp.sin(m*theta))\n",
        "            rows += [rowR, rowI]\n",
        "            if mode == 'power':\n",
        "                p  = n - j\n",
        "                coef = ff(n, j)\n",
        "                rhs += [ sp.simplify(coef * rho**p * sp.cos(p*theta)),\n",
        "                         sp.simplify(coef * rho**p * sp.sin(p*theta)) ]\n",
        "            elif mode == 'exp':\n",
        "                rhs += [ sp.simplify((t**j)*sp.exp(t*rho*sp.cos(theta))*sp.cos(t*rho*sp.sin(theta))),\n",
        "                         sp.simplify((t**j)*sp.exp(t*rho*sp.cos(theta))*sp.sin(t*rho*sp.sin(theta))) ]\n",
        "            elif mode == 'inv':\n",
        "                p   = -(j+1)\n",
        "                coef = ((-1)**j)*fact(j)\n",
        "                rhs += [ sp.simplify(coef * rho**p * sp.cos(p*theta)),\n",
        "                         sp.simplify(coef * rho**p * sp.sin(p*theta)) ]\n",
        "            else:\n",
        "                raise ValueError(\"mode?\")\n",
        "\n",
        "    M = sp.Matrix(rows)\n",
        "    y = sp.Matrix(rhs)\n",
        "    assert M.shape == (q, q), f\"bad size {M.shape} vs ({q},{q})\"\n",
        "    return M, y, fallback\n",
        "\n",
        "def matfun_real_hermite(A, mode, n=None, t=None):\n",
        "    \"\"\"\n",
        "    Compute f(A) with f(x)=x^n (symbolic n), f(x)=e^{tx} (symbolic t), or f(x)=1/x (Drazin kernel),\n",
        "    via minimal polynomial + REAL Hermite system (polar for complex roots).\n",
        "    \"\"\"\n",
        "    A = sp.Matrix(A)\n",
        "    mA = minimal_polynomial_via_linear(A)\n",
        "    roots, mults = factor_minpoly(mA)\n",
        "    M, y, _ = real_hermite_system(A, roots, mults, mode, n=n, t=t)\n",
        "    coeffs = list(M.LUsolve(y))         # symbolic dependence appears here\n",
        "    return evaluate_on_matrix(A, coeffs)\n",
        "\n",
        "# ---------- public wrappers ----------\n",
        "def power_via_CH_symbolic(A, n_symbol=None):\n",
        "    \"\"\"\n",
        "    Return A^n symbolically (n stays as a symbol).\n",
        "    \"\"\"\n",
        "    n = n_symbol or sp.Symbol('n', integer=True)\n",
        "    return sp.simplify(matfun_real_hermite(A, 'power', n=n))\n",
        "\n",
        "def exp_via_CH_symbolic(A, t_symbol=None):\n",
        "    \"\"\"\n",
        "    Return exp(t A) symbolically (t stays as a symbol).\n",
        "    \"\"\"\n",
        "    t = t_symbol or sp.Symbol('t', real=True)\n",
        "    return sp.simplify(matfun_real_hermite(A, 'exp', t=t))\n",
        "\n",
        "def drazin_via_hermite(A):\n",
        "    \"\"\"\n",
        "    Drazin inverse via f(x)=1/x for x≠0, f(0)=0 in the Hermite system (keeps everything real).\n",
        "    \"\"\"\n",
        "    return sp.simplify(matfun_real_hermite(A, 'inv'))"
      ]
    },
    {
      "cell_type": "code",
      "source": [],
      "metadata": {
        "id": "GJW6gO8eXivX"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "Παρακάτω υπολογίζουμε τη νιοστή δύναμη του πίνακα μετάβασης συμβολικά. Η μέθοδος που εφαρμόζουμε είναι να υπολογίσουμε πρώτα το ελάχιστο πολυώνυμο και έπειτα ακολουθούμε τη θεωρία των σημειώσεων."
      ],
      "metadata": {
        "id": "G_uUoaqnXjX1"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# ===== Colab Cell 3: Συμβολική δύναμη P^n με κλάσματα και όμορφο MathJax output =====\n",
        "\n",
        "from sympy import symbols, simplify, latex, nsimplify, Rational\n",
        "from IPython.display import display, Math\n",
        "\n",
        "# Έλεγχος ότι υπάρχει ο πίνακας από το πρώτο κελί\n",
        "try:\n",
        "    P = sp.Matrix(user_transition_matrix)\n",
        "except NameError:\n",
        "    raise NameError(\"❌ Δεν βρέθηκε η μεταβλητή 'user_transition_matrix'. Εκτελέστε πρώτα το Cell 1.\")\n",
        "\n",
        "# Μετατροπή όλων των στοιχείων σε ρητούς (κλάσματα)\n",
        "P = P.applyfunc(lambda x: nsimplify(x, rational=True))\n",
        "\n",
        "# Ορισμός του συμβολικού εκθέτη n\n",
        "n = symbols('n', integer=True)\n",
        "\n",
        "# Υπολογισμός συμβολικού P^n μέσω Hermite/CH (από το Cell 2)\n",
        "print(\"🔹 Υπολογισμός της νιοστής δύναμης του πίνακα μετάβασης:\")\n",
        "P_n = simplify(power_via_CH_symbolic(P, n))\n",
        "\n",
        "# Εμφάνιση του αποτελέσματος σε MathJax με ρητούς αριθμούς\n",
        "latex_expr = latex(P_n)\n",
        "display(Math(r\"P^n = \" + latex_expr))\n",
        "\n",
        "# Προαιρετικά: εμφάνιση για ορισμένες τιμές του n\n",
        "for k in [1, 2, 3]:\n",
        "    Pk = simplify(P_n.subs(n, k))\n",
        "    display(Math(rf\"P^{k} = {latex(Pk)}\"))\n"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 390
        },
        "id": "EZBfWbZqYSm1",
        "outputId": "eee997a2-8822-464b-d199-bd7add965b8f"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "🔹 Υπολογισμός της νιοστής δύναμης του πίνακα μετάβασης:\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Math object>"
            ],
            "text/latex": "$\\displaystyle P^n = \\left[\\begin{matrix}\\frac{2500^{- n} \\left(17 \\cdot 2500^{n} - 110 \\cdot 850^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} + 100 \\cdot 850^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}\\right)}{117} & \\frac{50^{1 - 2 n} \\left(50^{2 n} + 5 \\cdot 850^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} - 850^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}\\right)}{117} & \\frac{10 \\cdot 50^{- n} \\left(- 14 \\cdot 17^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} - 5 \\cdot 17^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} + 5 \\cdot 50^{n}\\right)}{117}\\\\\\frac{17 \\cdot 50^{- n} \\left(- 14 \\cdot 17^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} - 5 \\cdot 17^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} + 5 \\cdot 50^{n}\\right)}{585} & \\frac{2500^{- n} \\left(50^{2 n + 1} + 55 \\cdot 850^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} + 67 \\cdot 850^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}\\right)}{117} & \\frac{2500^{- n} \\left(250 \\cdot 50^{2 n} - 37 \\cdot 850^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} - 250 \\cdot 850^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}\\right)}{585}\\\\\\frac{17 \\cdot 2500^{- n} \\left(2500^{n} + 5 \\cdot 850^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} - 850^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}\\right)}{117} & \\frac{10 \\cdot 50^{- n} \\left(- 14 \\cdot 17^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} - 5 \\cdot 17^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} + 5 \\cdot 50^{n}\\right)}{117} & \\frac{2500^{- n} \\left(50^{2 n + 1} + 55 \\cdot 850^{n} \\sin{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)} + 67 \\cdot 850^{n} \\cos{\\left(n \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}\\right)}{117}\\end{matrix}\\right]$"
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Math object>"
            ],
            "text/latex": "$\\displaystyle P^1 = \\left[\\begin{matrix}0 & 1 & 0\\\\0 & \\frac{33}{50} & \\frac{17}{50}\\\\\\frac{17}{50} & 0 & \\frac{33}{50}\\end{matrix}\\right]$"
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Math object>"
            ],
            "text/latex": "$\\displaystyle P^2 = \\left[\\begin{matrix}0 & \\frac{33}{50} & \\frac{17}{50}\\\\\\frac{289}{2500} & \\frac{1089}{2500} & \\frac{561}{1250}\\\\\\frac{561}{2500} & \\frac{17}{50} & \\frac{1089}{2500}\\end{matrix}\\right]$"
          },
          "metadata": {}
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<IPython.core.display.Math object>"
            ],
            "text/latex": "$\\displaystyle P^3 = \\left[\\begin{matrix}\\frac{4913 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{146250} - \\frac{54043 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{1462500} + \\frac{17}{117} & \\frac{4913 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{58500} - \\frac{4913 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{292500} + \\frac{50}{117} & - \\frac{34391 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{731250} - \\frac{4913 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{292500} + \\frac{50}{117}\\\\- \\frac{584647 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{36562500} - \\frac{83521 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{14625000} + \\frac{17}{117} & \\frac{329171 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{14625000} + \\frac{54043 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{2925000} + \\frac{50}{117} & - \\frac{181781 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{73125000} - \\frac{4913 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{292500} + \\frac{50}{117}\\\\\\frac{83521 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{2925000} - \\frac{83521 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{14625000} + \\frac{17}{117} & - \\frac{34391 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{731250} - \\frac{4913 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{292500} + \\frac{50}{117} & \\frac{329171 \\cos{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{14625000} + \\frac{54043 \\sin{\\left(3 \\operatorname{atan}{\\left(\\frac{15}{8} \\right)} \\right)}}{2925000} + \\frac{50}{117}\\end{matrix}\\right]$"
          },
          "metadata": {}
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "import numpy as np\n",
        "import math\n",
        "import graphviz\n",
        "import io\n",
        "import base64\n",
        "import collections\n",
        "\n",
        "# It's assumed that the DiscreteTimeMarkovChain class is defined and executed\n",
        "# in a previous cell. This includes the helper functions like gcd, calculate_gcd_of_list.\n",
        "# For a self-contained example, you would include the class definition here,\n",
        "# but as per the request, we assume it's from \"the first cell\".\n",
        "\n",
        "# Ensure the DiscreteTimeMarkovChain class is available (dummy definition for standalone testing if needed)\n",
        "try:\n",
        "    _ = DiscreteTimeMarkovChain # This will raise NameError if not defined\n",
        "except NameError:\n",
        "    print(\"Προσοχή: Η κλάση 'DiscreteTimeMarkovChain' δεν βρέθηκε. Βεβαιωθείτε ότι το πρώτο κελί έχει εκτελεστεί.\")\n",
        "    print(\"Για λόγους δοκιμής, παρέχεται μια ελάχιστη ψεύτικη κλάση. Μην βασίζεστε σε αυτή για πραγματική λειτουργία.\")\n",
        "\n",
        "    # Minimal dummy class for testing this cell if the actual class is not loaded\n",
        "    # In a real scenario, the full class from your first cell must be present.\n",
        "    def gcd(a, b):\n",
        "        while b:\n",
        "            a, b = b, a % b\n",
        "        return a\n",
        "\n",
        "    def calculate_gcd_of_list(numbers):\n",
        "        if not numbers:\n",
        "            return 0\n",
        "        result = numbers[0]\n",
        "        for i in range(1, len(numbers)):\n",
        "            result = gcd(result, numbers[i])\n",
        "        return result\n",
        "\n",
        "    class DiscreteTimeMarkovChain:\n",
        "        def __init__(self, P, state_names):\n",
        "            self.P = np.array(P, dtype=float)\n",
        "            self.num_states = self.P.shape[0]\n",
        "            self.state_names = state_names\n",
        "            # Dummy classification for testing purposes if real class not loaded\n",
        "            # In a real scenario, _classify_states would populate these correctly\n",
        "            self._recurrent_states_indices = sorted([i for i in range(self.num_states) if i % 2 == 0])\n",
        "            self._transient_states_indices = sorted([i for i in range(self.num_states) if i % 2 != 0])\n",
        "            self._closed_irreducible_sets = [[i] for i in self._recurrent_states_indices] # simple sets\n",
        "\n",
        "        def _get_reachable_states(self, start_state_idx):\n",
        "            # Dummy implementation\n",
        "            return {i for i in range(self.num_states)}\n",
        "\n",
        "        def _get_communicating_classes(self):\n",
        "            # Dummy implementation\n",
        "            return [[i] for i in range(self.num_states)] # Each state is its own class\n",
        "\n",
        "        def _classify_states(self):\n",
        "            # Dummy - real logic in actual class\n",
        "            pass\n",
        "\n",
        "        def get_state_classification(self):\n",
        "            return {\"recurrent\": self._recurrent_states_indices, \"transient\": self._transient_states_indices}\n",
        "\n",
        "        def get_closed_irreducible_sets(self):\n",
        "            return self._closed_irreducible_sets\n",
        "\n",
        "        def get_steady_state_distribution(self):\n",
        "            # Dummy or simple calculation for aperiodic recurrent state\n",
        "            if self._recurrent_states_indices:\n",
        "                idx = self._recurrent_states_indices[0]\n",
        "                dist = np.zeros(self.num_states)\n",
        "                dist[idx] = 1.0 # Simple, not a full calculation\n",
        "                return dist\n",
        "            return None\n",
        "\n",
        "        def get_period(self, state_index):\n",
        "            return 1 # Dummy\n",
        "\n",
        "        def get_mean_first_passage_times(self, target_state_index):\n",
        "            return np.full(self.num_states, 5.0) # Dummy value\n",
        "\n",
        "        def get_absorption_probabilities(self):\n",
        "            return {\"Dummy_T0\": {\"Dummy_R0\": 0.5}} # Dummy\n",
        "\n",
        "        def get_mean_absorption_times(self):\n",
        "            return np.array([10.0]) # Dummy\n",
        "\n",
        "        def get_fundamental_matrix_N(self):\n",
        "            if not self._transient_states_indices:\n",
        "                return None, []\n",
        "            # Dummy N matrix (I - Q)^-1\n",
        "            num_trans = len(self._transient_states_indices)\n",
        "            if num_trans > 0:\n",
        "                N_dummy = np.identity(num_trans) * 2 # Example for a simple N\n",
        "                return N_dummy, [self.state_names[idx] for idx in self._transient_states_indices]\n",
        "            return None, []\n",
        "\n",
        "\n",
        "def export_analysis_to_html_with_mathjax(mc_instance, output_filename=\"markov_chain_analysis_mathjax.html\",\n",
        "                                         initial_distribution=None, n_steps_for_dist=5, target_state_for_mfpt_name=None):\n",
        "    \"\"\"\n",
        "    Εξάγει μια πλήρη ανάλυση της Μαρκοβιανής αλυσίδας σε αρχείο HTML με υποστήριξη MathJax για μαθηματικούς τύπους.\n",
        "    Περιλαμβάνει τον υπολογισμό και την εμφάνιση της κανονικής μορφής του πίνακα μετάβασης,\n",
        "    με τις αναστρέψιμες καταστάσεις να εμφανίζονται πρώτες, και τον μέσο χρόνο παραμονής στις μεταβατικές.\n",
        "\n",
        "    Args:\n",
        "        mc_instance (DiscreteTimeMarkovChain): Η στιγμιότυπο της Μαρκοβιανής αλυσίδας προς ανάλυση.\n",
        "        output_filename (str): Το όνομα του αρχείου HTML που θα δημιουργηθεί.\n",
        "        initial_distribution (np.array, optional): Αρχική κατανομή για τον υπολογισμό μετά n βήματα.\n",
        "                                                    Αν None, χρησιμοποιείται ομοιόμορφη.\n",
        "        n_steps_for_dist (int, optional): Αριθμός βημάτων για την κατανομή.\n",
        "        target_state_for_mfpt_name (str, optional): Το όνομα της κατάστασης-στόχου για τους μέσους χρόνους\n",
        "                                                    πρώτης διέλευσης. Αν None, χρησιμοποιείται η πρώτη κατάσταση.\n",
        "    \"\"\"\n",
        "    html_content = []\n",
        "    html_content.append(\"<!DOCTYPE html>\")\n",
        "    html_content.append(\"<html lang='el'>\")\n",
        "    html_content.append(\"<head>\")\n",
        "    html_content.append(\"    <meta charset='UTF-8'>\")\n",
        "    html_content.append(\"    <meta name='viewport' content='width=device-width, initial-scale=1.0'>\")\n",
        "    html_content.append(\"    <title>Ανάλυση Μαρκοβιανής Αλυσίδας (με MathJax)</title>\")\n",
        "    html_content.append(\"    <script type='text/javascript' id='MathJax-script' async src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'></script>\")\n",
        "    html_content.append(\"    <script>\")\n",
        "    html_content.append(\"        MathJax = {\")\n",
        "    html_content.append(\"            tex: {\")\n",
        "    html_content.append(\"                inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],\")\n",
        "    html_content.append(\"                displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],\")\n",
        "    html_content.append(\"                processEscapes: true\")\n",
        "    html_content.append(\"            },\")\n",
        "    html_content.append(\"            options: {\")\n",
        "    html_content.append(\"                ignoreHtmlClass: 'tex2jax_ignore',\")\n",
        "    html_content.append(\"                processHtmlClass: 'tex2jax_process'\")\n",
        "    html_content.append(\"            },\")\n",
        "    html_content.append(\"            startup: {\")\n",
        "    html_content.append(\"                typeset: 'all'\")\n",
        "    html_content.append(\"            }\")\n",
        "    html_content.append(\"        };\")\n",
        "    html_content.append(\"    </script>\")\n",
        "\n",
        "    html_content.append(\"    <style>\")\n",
        "    html_content.append(\"        body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; background-color: #f4f4f4; color: #333; }\")\n",
        "    html_content.append(\"        .container { max-width: 900px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }\")\n",
        "    html_content.append(\"        h1, h2, h3 { color: #0056b3; }\")\n",
        "    html_content.append(\"        table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }\")\n",
        "    html_content.append(\"        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\")\n",
        "    html_content.append(\"        th { background-color: #f2f2f2; }\")\n",
        "    html_content.append(\"        ul { list-style-type: disc; margin-left: 20px; }\")\n",
        "    html_content.append(\"        strong { color: #555; }\")\n",
        "    html_content.append(\"        code { background-color: #eee; padding: 2px 4px; border-radius: 4px; font-family: 'Courier New', monospace; }\")\n",
        "    html_content.append(\"        pre { background-color: #eee; padding: 10px; border-radius: 4px; overflow-x: auto; }\")\n",
        "    html_content.append(\"        .figure-container { text-align: center; margin-bottom: 20px; }\")\n",
        "    html_content.append(\"        .figure-container img, .figure-container svg { max-width: 100%; height: auto; border: 1px solid #ccc; border-radius: 5px; }\")\n",
        "    html_content.append(\"        .matrix-table { border-collapse: collapse; margin: 15px 0; font-family: 'Courier New', monospace; font-size: 0.9em; }\")\n",
        "    html_content.append(\"        .matrix-table td { border: 1px solid #ccc; padding: 5px 8px; text-align: right; }\")\n",
        "    html_content.append(\"        .matrix-table th { border: 1px solid #ccc; padding: 5px 8px; background-color: #f9f9f9; text-align: center; }\")\n",
        "    html_content.append(\"    </style>\")\n",
        "    html_content.append(\"</head>\")\n",
        "    html_content.append(\"<body>\")\n",
        "    html_content.append(\"    <div class='container'>\")\n",
        "    html_content.append(\"        <h1>Ανάλυση Μαρκοβιανής Αλυσίδας</h1>\")\n",
        "    html_content.append(\"        <p>Αυτό το έγγραφο περιέχει μια αυτόματη ανάλυση της Μαρκοβιανής Αλυσίδας που εισήχθη.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 1. Εισαγωγή Δεδομένων Αλυσίδας ---\n",
        "    html_content.append(\"        <h2>Εισαγωγή Δεδομένων Αλυσίδας</h2>\")\n",
        "    html_content.append(\"        <h3>Καταστάσεις</h3>\")\n",
        "    html_content.append(f\"        <p>Οι καταστάσεις της Μαρκοβιανής αλυσίδας είναι: <code>{', '.join(mc_instance.state_names)}</code>.</p>\")\n",
        "\n",
        "    html_content.append(\"        <h3>Πίνακας Μετάβασης (P)</h3>\")\n",
        "    html_content.append(\"        <p>Ο πίνακας μετάβασης <strong>$P$</strong> δίνεται παρακάτω:</p>\")\n",
        "    html_content.append(\"        $$\")\n",
        "    html_content.append(r\"        P = \\begin{pmatrix}\")\n",
        "    for row in mc_instance.P:\n",
        "        html_content.append(\" & \".join([f\"{prob:.2f}\" for prob in row]) + r\" \\\\\")\n",
        "    html_content.append(r\"        \\end{pmatrix}\")\n",
        "    html_content.append(\"        $$\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 2. Διάγραμμα Μαρκοβιανής Αλυσίδας ---\n",
        "    print(\"\\n--- Δημιουργία διαγράμματος SVG για ενσωμάτωση... ---\")\n",
        "    dot = graphviz.Digraph(comment='Markov Chain', format='svg')\n",
        "    dot.attr(rankdir='LR')\n",
        "\n",
        "    for i, name in enumerate(mc_instance.state_names):\n",
        "        dot.node(str(i), name, shape='circle', style='filled', fillcolor='lightblue')\n",
        "\n",
        "    for i in range(mc_instance.num_states):\n",
        "        for j in range(mc_instance.num_states):\n",
        "            prob = mc_instance.P[i, j]\n",
        "            if prob > 1e-9:\n",
        "                if i == j:\n",
        "                    dot.edge(str(i), str(j), label=f\"{prob:.2f}\",\n",
        "                             minlen='2', dir='forward', color='darkgreen', style='dashed', constraint='false')\n",
        "                else:\n",
        "                    dot.edge(str(i), str(j), label=f\"{prob:.2f}\",\n",
        "                             color='gray', fontcolor='darkblue', penwidth=str(0.5 + prob*2))\n",
        "\n",
        "    svg_source = dot.pipe(format='svg').decode('utf-8')\n",
        "\n",
        "    html_content.append(\"        <h2>Διάγραμμα Μαρκοβιανής Αλυσίδας</h2>\")\n",
        "    html_content.append(\"        <div class='figure-container'>\")\n",
        "    html_content.append(\"            <p>Το διάγραμμα της Μαρκοβιανής αλυσίδας είναι:</p>\")\n",
        "    html_content.append(svg_source)\n",
        "    html_content.append(\"            <p><em>Σχήμα 1: Διάγραμμα της Μαρκοβιανής Αλυσίδας.</em></p>\")\n",
        "    html_content.append(\"        </div>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 3. Κατανομή πιθανοτήτων μετά n βήματα ---\n",
        "    html_content.append(\"        <h2>Κατανομή Πιθανοτήτων μετά $n$ Βήματα</h2>\")\n",
        "    if initial_distribution is None:\n",
        "        initial_distribution = np.full(mc_instance.num_states, 1.0 / mc_instance.num_states)\n",
        "        html_content.append(f\"        <p>Χρησιμοποιήθηκε ομοιόμορφη αρχική κατανομή: <code>{', '.join([f'{p:.2f}' for p in initial_distribution])}</code>.</p>\")\n",
        "\n",
        "    P_n = np.linalg.matrix_power(mc_instance.P, n_steps_for_dist)\n",
        "    distribution_n = np.dot(initial_distribution, P_n)\n",
        "\n",
        "    html_content.append(f\"        <p>Η κατανομή πιθανοτήτων μετά <strong>${n_steps_for_dist}$</strong> βήματα είναι:</p>\")\n",
        "    html_content.append(\"        $$\")\n",
        "    html_content.append(r\"        \\pi_{\" + str(n_steps_for_dist) + r\"} = \\begin{pmatrix} \")\n",
        "    html_content.append(\" & \".join([f\"{prob:.4f}\" for prob in distribution_n]))\n",
        "    html_content.append(r\" \\end{pmatrix} $$\")\n",
        "    html_content.append(\"        <p>ή αναλυτικά:</p>\")\n",
        "    html_content.append(\"        <ul>\")\n",
        "    for i, prob in enumerate(distribution_n):\n",
        "        # Αλλάξτε το $ σε $$ στην αρχή και στο τέλος, και ίσως προσθέστε <p> για διάταξη\n",
        "        html_content.append(f\"            <li>$$P(X_{n_steps_for_dist} = \\\\text{{{mc_instance.state_names[i]}}}) = {prob:.4f}$$</li>\")\n",
        "    html_content.append(\"        </ul>\")\n",
        "\n",
        "    # --- 4. Οριακή (Στατική) Κατανομή ---\n",
        "    html_content.append(\"        <h2>Οριακή (Στατική) Κατανομή</h2>\")\n",
        "    steady_state_distribution = mc_instance.get_steady_state_distribution()\n",
        "\n",
        "    if steady_state_distribution is not None:\n",
        "        html_content.append(\"        <p>Η οριακή κατανομή $\\\\pi$ είναι:</p>\")\n",
        "        html_content.append(\"        $$\")\n",
        "        html_content.append(r\"        \\pi = \\begin{pmatrix} \")\n",
        "        html_content.append(\" & \".join([f\"{prob:.4f}\" for prob in steady_state_distribution]))\n",
        "        html_content.append(r\" \\end{pmatrix} $$\")\n",
        "        html_content.append(\"        <p>ή αναλυτικά:</p>\")\n",
        "        html_content.append(\"        <ul>\")\n",
        "        for i, prob in enumerate(steady_state_distribution):\n",
        "            html_content.append(f\"            <li>$\\\\pi(\\\\text{{{mc_instance.state_names[i]}}}) = {prob:.4f}$</li>\")\n",
        "        html_content.append(\"        </ul>\")\n",
        "    else:\n",
        "        html_content.append(\"        <p>Δεν βρέθηκε μοναδική οριακή κατανομή για αυτήν την Μαρκοβιανική αλυσίδα.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 5. Πίνακας Οριακών Πιθανοτήτων (P^1000) ---\n",
        "    html_content.append(\"        <h2>Πίνακας Οριακών Πιθανοτήτων (P^{1000})<br><small>(Μακροπρόθεσμη Συμπεριφορά)</small></h2>\")\n",
        "    html_content.append(\"        <p>Ο πίνακας $P^{1000}$ αντιπροσωπεύει τις πιθανότητες μετάβασης μετά από 1000 βήματα. Για αρκετά μεγάλους αριθμούς βημάτων, κάθε γραμμή αυτού του πίνακα συγκλίνει στην οριακή κατανομή της αλυσίδας, υποδεικνύοντας τη μακροπρόθεσμη συμπεριφορά της.</p>\")\n",
        "\n",
        "    P_1000 = np.linalg.matrix_power(mc_instance.P, 1000)\n",
        "\n",
        "    html_content.append(\"        $$\")\n",
        "    html_content.append(r\"        P^{1000} = \\begin{pmatrix}\")\n",
        "    for row in P_1000:\n",
        "        html_content.append(\" & \".join([f\"{prob:.4f}\" for prob in row]) + r\" \\\\\")\n",
        "    html_content.append(r\"        \\end{pmatrix}\")\n",
        "    html_content.append(\"        $$\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 6. Ταξινόμηση Καταστάσεων ---\n",
        "    html_content.append(\"        <h2>Ταξινόμηση Καταστάσεων</h2>\")\n",
        "    state_classification = mc_instance.get_state_classification()\n",
        "    recurrent_names = [mc_instance.state_names[i] for i in mc_instance._recurrent_states_indices]\n",
        "    transient_names = [mc_instance.state_names[i] for i in mc_instance._transient_states_indices]\n",
        "\n",
        "    html_content.append(f\"        <p>Οι <strong>αναστρέψιμες (Recurrent)</strong> καταστάσεις είναι: <code>{', '.join(recurrent_names)}</code>.</p>\")\n",
        "    html_content.append(f\"        <p>Οι <strong>μεταβατικές (Transient)</strong> καταστάσεις είναι: <code>{', '.join(transient_names)}</code>.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 7. Κλειστά και Αδιαχώριστα Σύνολα ---\n",
        "    html_content.append(\"        <h2>Κλειστά και Αδιαχώριστα Σύνολα</h2>\")\n",
        "    closed_irreducible_sets = mc_instance.get_closed_irreducible_sets()\n",
        "    if closed_irreducible_sets:\n",
        "        html_content.append(\"        <p>Τα κλειστά και αδιαχώριστα σύνολα είναι:</p>\")\n",
        "        html_content.append(\"        <ul>\")\n",
        "        for i, cls_indices in enumerate(closed_irreducible_sets):\n",
        "            state_names_cls = [mc_instance.state_names[s] for s in cls_indices]\n",
        "            html_content.append(f\"            <li>Σύνολο {i+1}: <code>{{{', '.join(state_names_cls)}}}</code></li>\")\n",
        "        html_content.append(\"        </ul>\")\n",
        "    else:\n",
        "        html_content.append(\"        <p>Δεν υπάρχουν κλειστά και αδιαχώριστα σύνολα σε αυτήν την αλυσίδα.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 8. Περίοδοι Καταστάσεων ---\n",
        "    html_content.append(\"        <h2>Περίοδοι Καταστάσεων</h2>\")\n",
        "    html_content.append(\"        <p>Οι περίοδοι για κάθε κατάσταση είναι:</p>\")\n",
        "    html_content.append(\"        <ul>\")\n",
        "    for i in range(mc_instance.num_states):\n",
        "        period_val = mc_instance.get_period(i)\n",
        "        html_content.append(f\"            <li>Περίοδος της <code>{mc_instance.state_names[i]}</code>: {period_val}</li>\")\n",
        "    html_content.append(\"        </ul>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 9. Μέσοι Χρόνοι Επιστροφής ---\n",
        "    html_content.append(\"        <h2>Μέσοι Χρόνοι Επιστροφής (Recurrence Times)</h2>\")\n",
        "    steady_state_distribution = mc_instance.get_steady_state_distribution()\n",
        "    if mc_instance._recurrent_states_indices and steady_state_distribution is not None:\n",
        "        html_content.append(\"        <p>Οι μέσοι χρόνοι επιστροφής για τις αναστρέψιμες καταστάσεις είναι:</p>\")\n",
        "        html_content.append(\"        <ul>\")\n",
        "        for i in mc_instance._recurrent_states_indices:\n",
        "            state_name = mc_instance.state_names[i]\n",
        "            if steady_state_distribution[i] > 1e-9:\n",
        "                mean_recurrence_time = 1 / steady_state_distribution[i]\n",
        "                html_content.append(f\"            <li>Από την $\\\\text{{{state_name}}}$ στην $\\\\text{{{state_name}}}$: ${mean_recurrence_time:.4f}$ βήματα</li>\")\n",
        "            else:\n",
        "                html_content.append(f\"            <li>Η κατάσταση <code>{state_name}</code> έχει μηδενική πιθανότητα στην οριακή κατανομή. Δεν έχει νόημα ο μέσος χρόνος επιστροφής ή είναι άπειρος.</li>\")\n",
        "        html_content.append(\"        </ul>\")\n",
        "    else:\n",
        "        html_content.append(\"        <p>Δεν υπάρχουν αναστρέψιμες καταστάσεις ή μοναδική οριακή κατανομή για τον υπολογισμό των μέσων χρόνων επιστροφής.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 10. Μέσοι Χρόνοι Πρώτης Διέλευσης ---\n",
        "    html_content.append(\"        <h2>Μέσοι Χρόνοι Πρώτης Διέλευσης (First Passage Times)</h2>\")\n",
        "    target_state_for_mfpt_index = 0\n",
        "    if target_state_for_mfpt_name:\n",
        "        try:\n",
        "            target_state_for_mfpt_index = mc_instance.state_names.index(target_state_for_mfpt_name)\n",
        "        except ValueError:\n",
        "            html_content.append(f\"        <p>Το όνομα κατάστασης-στόχου '<code>{target_state_for_mfpt_name}</code>' δεν βρέθηκε. Χρησιμοποιείται η πρώτη κατάσταση ως προεπιλογή.</p>\")\n",
        "            target_state_for_mfpt_name = mc_instance.state_names[0]\n",
        "    else:\n",
        "        target_state_for_mfpt_name = mc_instance.state_names[0]\n",
        "\n",
        "\n",
        "    html_content.append(f\"        <p>Υπολογισμός μέσων χρόνων πρώτης διέλευσης προς την κατάσταση $\\\\text{{{target_state_for_mfpt_name}}}$:</p>\")\n",
        "    mfpt_results = mc_instance.get_mean_first_passage_times(target_state_for_mfpt_index)\n",
        "\n",
        "    if mfpt_results is not None:\n",
        "        html_content.append(\"        <ul>\")\n",
        "        for i, time in enumerate(mfpt_results):\n",
        "            if math.isinf(time):\n",
        "                html_content.append(f\"            <li>Από την $\\\\text{{{mc_instance.state_names[i]}}}$: Άπειρα βήματα (δεν είναι προσβάσιμη)</li>\")\n",
        "            else:\n",
        "                html_content.append(f\"            <li>Από την $\\\\text{{{mc_instance.state_names[i]}}}$: ${time:.4f}$ βήματα</li>\")\n",
        "        html_content.append(\"        </ul>\")\n",
        "    else:\n",
        "        html_content.append(f\"        <p>Αδύνατος ο υπολογισμός των μέσων χρόνων πρώτης διέλευσης προς την κατάσταση <code>{target_state_for_mfpt_name}</code>.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 11. Πιθανότητες Απορρόφησης ---\n",
        "    html_content.append(\"        <h2>Πιθανότητες Απορρόφησης</h2>\")\n",
        "    absorption_probs = mc_instance.get_absorption_probabilities()\n",
        "\n",
        "    if absorption_probs:\n",
        "        html_content.append(\"        <p>Οι πιθανότητες απορρόφησης από μεταβατικές καταστάσεις σε κλειστά σύνολα είναι:</p>\")\n",
        "        html_content.append(\"        <ul>\")\n",
        "        for transient_state_name, probs_dict in absorption_probs.items():\n",
        "            html_content.append(f\"            <li>Από την <code>{transient_state_name}</code>:\")\n",
        "            html_content.append(\"            <ul>\")\n",
        "            for set_name, prob in probs_dict.items():\n",
        "                html_content.append(f\"                <li>Προς {set_name}: ${prob:.4f}$</li>\")\n",
        "            html_content.append(\"            </ul></li>\")\n",
        "        html_content.append(\"        </ul>\")\n",
        "    else:\n",
        "        html_content.append(\"        <p>Δεν υπάρχουν μεταβατικές καταστάσεις ή αδύνατος ο υπολογισμός πιθανοτήτων απορρόφησης.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 12. Μέσοι Χρόνοι Απορρόφησης ---\n",
        "    html_content.append(\"        <h2>Μέσοι Χρόνοι Απορρόφησης</h2>\")\n",
        "    mean_abs_times = mc_instance.get_mean_absorption_times()\n",
        "\n",
        "    if mean_abs_times is not None:\n",
        "        html_content.append(\"        <p>Οι μέσοι χρόνοι απορρόφησης από κάθε μεταβατική κατάσταση στο σύνολο των επαναληπτικών καταστάσεων είναι:</p>\")\n",
        "        html_content.append(\"        <ul>\")\n",
        "        for i, t_orig_idx in enumerate(mc_instance._transient_states_indices):\n",
        "            if math.isinf(mean_abs_times[i]):\n",
        "                 html_content.append(f\"            <li>Από την $\\\\text{{{mc_instance.state_names[t_orig_idx]}}}$: Άπειρα βήματα</li>\")\n",
        "            else:\n",
        "                 html_content.append(f\"            <li>Από την $\\\\text{{{mc_instance.state_names[t_orig_idx]}}}$: ${mean_abs_times[i]:.4f}$ βήματα</li>\")\n",
        "        html_content.append(\"        </ul>\")\n",
        "    else:\n",
        "        html_content.append(\"        <p>Δεν υπάρχουν μεταβατικές καταστάσεις ή αδύνατος ο υπολογισμός μέσων χρόνων απορρόφησης.</p>\")\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    # --- 13. Κανονική Μορφή Πίνακα Μετάβασης ---\n",
        "    html_content.append(\"        <h2>Κανονική Μορφή Πίνακα Μετάβασης</h2>\")\n",
        "\n",
        "    recurrent_indices = sorted(mc_instance._recurrent_states_indices) # Recurrent first\n",
        "    transient_indices = sorted(mc_instance._transient_states_indices) # Then transient\n",
        "\n",
        "    if not transient_indices and not recurrent_indices:\n",
        "        html_content.append(\"        <p>Δεν υπάρχουν καταστάσεις για τον υπολογισμό της κανονικής μορφής.</p>\")\n",
        "    else:\n",
        "        new_order_indices = recurrent_indices + transient_indices # New order: Recurrent then Transient\n",
        "        new_state_order_names = [mc_instance.state_names[i] for i in new_order_indices]\n",
        "\n",
        "        P_canonical = np.zeros_like(mc_instance.P, dtype=float)\n",
        "        for new_row_idx, old_row_idx in enumerate(new_order_indices):\n",
        "            for new_col_idx, old_col_idx in enumerate(new_order_indices):\n",
        "                P_canonical[new_row_idx, new_col_idx] = mc_instance.P[old_row_idx, old_col_idx]\n",
        "\n",
        "        num_recurrent = len(recurrent_indices)\n",
        "        num_transient = len(transient_indices)\n",
        "\n",
        "        html_content.append(\"        <h3>Αναδιαταγμένος Πίνακας Μετάβασης. Κανονική Μορφή</h3>\")\n",
        "        html_content.append(\"        <p>Οι καταστάσεις έχουν αναδιαταχθεί ως εξής: αναστρέψιμες πρώτα, μετά μεταβατικές:<br>\")\n",
        "        html_content.append(f\"        <code>{', '.join(new_state_order_names)}</code></p>\")\n",
        "        html_content.append(\"        <p>Η κανονική μορφή έχει τη μορφή:\")\n",
        "        html_content.append(\"        $$\")\n",
        "        html_content.append(r\"        P = \\begin{pmatrix}\")\n",
        "        html_content.append(r\"        S & 0 \\\\\") # S for recurrent to recurrent, 0 for recurrent to transient\n",
        "        html_content.append(r\"        R & Q\") # R for transient to recurrent, Q for transient to transient\n",
        "        html_content.append(r\"        \\end{pmatrix}\")\n",
        "        html_content.append(\"        $$\")\n",
        "        html_content.append(\"        και ο αναδιαταγμένος πίνακας είναι:</p>\")\n",
        "\n",
        "        html_content.append(\"        $$\")\n",
        "        html_content.append(r\"        P_{\\text{canonical}} = \\begin{pmatrix}\")\n",
        "        for r_idx, row in enumerate(P_canonical):\n",
        "            html_content.append(\" & \".join([f\"{prob:.2f}\" for prob in row]) + r\" \\\\\")\n",
        "            if r_idx == num_recurrent - 1 and num_transient > 0:\n",
        "                # Add a horizontal line to visually separate S/0 from R/Q\n",
        "                html_content.append(r\"\\hline\")\n",
        "        html_content.append(r\"        \\end{pmatrix}\")\n",
        "        html_content.append(\"        $$\")\n",
        "\n",
        "        html_content.append(\"        <h3>Υποπίνακες Κανονικής Μορφής</h3>\")\n",
        "\n",
        "        # Submatrix S (Recurrent to Recurrent)\n",
        "        S_matrix = None\n",
        "        if num_recurrent > 0:\n",
        "            S_matrix = P_canonical[:num_recurrent, :num_recurrent]\n",
        "            html_content.append(\"        <h4>Υποπίνακας $S$ Μεταβάσεις από Αναστρέψιμες σε Αναστρέψιμες Καταστάσεις</h4>\")\n",
        "            html_content.append(\"        $$\")\n",
        "            html_content.append(r\"        S = \\begin{pmatrix}\")\n",
        "            for row in S_matrix:\n",
        "                html_content.append(\" & \".join([f\"{prob:.2f}\" for prob in row]) + r\" \\\\\")\n",
        "            html_content.append(r\"        \\end{pmatrix}\")\n",
        "            html_content.append(\"        $$\")\n",
        "        else:\n",
        "            html_content.append(\"        <p>Δεν υπάρχουν αναστρέψιμες καταστάσεις, οπότε ο υποπίνακας $S$ δεν υπάρχει.</p>\")\n",
        "\n",
        "        # Submatrix 0 (Recurrent to Transient) - implicitly zeros\n",
        "        if num_recurrent > 0 and num_transient > 0:\n",
        "             html_content.append(\"        <h4>Υποπίνακας $0$ Μεταβάσεις από Αναστρέψιμες σε Μεταβατικές Καταστάσεις</h4>\")\n",
        "             html_content.append(\"        <p>Αυτός ο υποπίνακας αποτελείται από μηδενικές πιθανότητες, καθώς οι αναστρέψιμες καταστάσεις δεν οδηγούν σε μεταβατικές.</p>\")\n",
        "\n",
        "        # Submatrix R (Transient to Recurrent)\n",
        "        R_matrix = None\n",
        "        if num_transient > 0 and num_recurrent > 0:\n",
        "            R_matrix = P_canonical[num_recurrent:, :num_recurrent]\n",
        "            html_content.append(\"        <h4>Υποπίνακας $R$. Μεταβάσεις από Μεταβατικές σε Αναστρέψιμες Καταστάσεις</h4>\")\n",
        "            html_content.append(\"        $$\")\n",
        "            html_content.append(r\"        R = \\begin{pmatrix}\")\n",
        "            for row in R_matrix:\n",
        "                html_content.append(\" & \".join([f\"{prob:.2f}\" for prob in row]) + r\" \\\\\")\n",
        "            html_content.append(r\"        \\end{pmatrix}\")\n",
        "            html_content.append(\"        $$\")\n",
        "        else:\n",
        "            html_content.append(\"        <p>Δεν υπάρχουν αρκετές μεταβατικές ή αναστρέψιμες καταστάσεις για τον υποπίνακα $R$.</p>\")\n",
        "\n",
        "        # Submatrix Q (Transient to Transient)\n",
        "        Q_matrix = None\n",
        "        if num_transient > 0:\n",
        "            Q_matrix = P_canonical[num_recurrent:, num_recurrent:]\n",
        "            html_content.append(\"        <h4>Υποπίνακας $Q$. Μεταβάσεις από Μεταβατικές σε Μεταβατικές Καταστάσεις</h4>\")\n",
        "            html_content.append(\"        $$\")\n",
        "            html_content.append(r\"        Q = \\begin{pmatrix}\")\n",
        "            for row in Q_matrix:\n",
        "                html_content.append(\" & \".join([f\"{prob:.2f}\" for prob in row]) + r\" \\\\\")\n",
        "            html_content.append(r\"        \\end{pmatrix}\")\n",
        "            html_content.append(\"        $$\")\n",
        "        else:\n",
        "            html_content.append(\"        <p>Δεν υπάρχουν μεταβατικές καταστάσεις, οπότε ο υποπίνακας $Q$ δεν υπάρχει.</p>\")\n",
        "\n",
        "\n",
        "        # Fundamental Matrix N = (I - Q)^-1\n",
        "        N_fundamental, N_state_names = mc_instance.get_fundamental_matrix_N()\n",
        "\n",
        "        if N_fundamental is not None and N_fundamental.size > 0:\n",
        "            html_content.append(\"        <h3>Θεμελιώδης Πίνακας $N = (I - Q)^{-1}$</h3>\")\n",
        "            html_content.append(\"        <p>Ο Θεμελιώδης Πίνακας N είναι ο μέσος αριθμός φορών που η αλυσίδα βρίσκεται στην μεταβατική κατάσταση j, αν ξεκίνησε από την μεταβατική κατάσταση i, πριν την απορρόφηση).</p>\")\n",
        "            html_content.append(\"        $$\")\n",
        "            html_content.append(r\"        N = \\begin{pmatrix}\")\n",
        "            for row in N_fundamental:\n",
        "                html_content.append(\" & \".join([f\"{val:.2f}\" for val in row]) + r\" \\\\\")\n",
        "            html_content.append(r\"        \\end{pmatrix}\")\n",
        "            html_content.append(\"        $$\")\n",
        "\n",
        "            # Mean Sojourn Times in Transient States\n",
        "            html_content.append(\"        <h3>Μέσοι Χρόνοι Παραμονής στις Μεταβατικές Καταστάσεις</h3>\")\n",
        "            html_content.append(\"        <p>Ο **μέσος συνολικός χρόνος παραμονής** σε κάθε μεταβατική κατάσταση πριν την απορρόφηση δίνεται από το άθροισμα των στοιχείων της αντίστοιχης γραμμής του Θεμελιώδη Πίνακα $N$. Αυτό αντιπροσωπεύει τον αναμενόμενο αριθμό βημάτων που θα περάσει η αλυσίδα στην κατάσταση $j$ (στήλη) ξεκινώντας από την κατάσταση $i$ (γραμμή), αθροιστικά για όλες τις μεταβατικές καταστάσεις $j$.</p>\")\n",
        "            html_content.append(\"        <ul>\")\n",
        "            for i, row in enumerate(N_fundamental):\n",
        "                sojourn_time = np.sum(row)\n",
        "                html_content.append(f\"            <li>Από την $\\\\text{{{N_state_names[i]}}}$: ${sojourn_time:.4f}$ βήματα</li>\")\n",
        "            html_content.append(\"        </ul>\")\n",
        "\n",
        "        else:\n",
        "            html_content.append(\"        <p>Δεν υπάρχουν μεταβατικές καταστάσεις, οπότε ο Θεμελιώδης Πίνακας $N$ και οι μέσοι χρόνοι παραμονής δεν υπολογίζονται.</p>\")\n",
        "\n",
        "    html_content.append(\"        <hr>\")\n",
        "\n",
        "    html_content.append(\"    </div>\")\n",
        "    html_content.append(\"</body>\")\n",
        "    html_content.append(\"</html>\")\n",
        "\n",
        "    # Write to file\n",
        "    try:\n",
        "        with open(output_filename, \"w\", encoding=\"utf-8\") as f:\n",
        "            f.write(\"\\n\".join(html_content))\n",
        "        print(f\"\\nΗ ανάλυση εξήχθη επιτυχώς στο αρχείο '{output_filename}'\")\n",
        "        print(\"Σημείωση: Για τη σωστή εμφάνιση των μαθηματικών τύπων, απαιτείται σύνδεση στο διαδίκτυο (MathJax CDN).\")\n",
        "    except Exception as e:\n",
        "        print(f\"Σφάλμα κατά την εξαγωγή του αρχείου HTML: {e}\")\n",
        "\n",
        "# --- ΕΚΤΕΛΕΣΗ ΤΗΣ ΕΞΑΓΩΓΗΣ ---\n",
        "if __name__ == \"__main__\":\n",
        "    print(\"\\n\" + \"#\"*70)\n",
        "    print(\"### Εκτέλεση εξαγωγής ανάλυσης Μαρκοβιανής Αλυσίδας σε HTML\")\n",
        "    print(\"#\"*70 + \"\\n\")\n",
        "\n",
        "    try:\n",
        "        # Check if user_transition_matrix and user_states are defined from previous cells\n",
        "        if 'user_transition_matrix' not in globals() or 'user_states' not in globals():\n",
        "            raise NameError(\"Οι μεταβλητές 'user_transition_matrix' ή 'user_states' δεν βρέθηκαν.\")\n",
        "\n",
        "        # Instantiate the Markov Chain\n",
        "        mc_for_export = DiscreteTimeMarkovChain(user_transition_matrix, user_states)\n",
        "\n",
        "        export_n_steps = int(input(\"Εισάγετε τον αριθμό βημάτων για την ανάλυση κατανομής (π.χ. 10): \"))\n",
        "        export_target_state_name_input = input(f\"Εισάγετε το **όνομα** της κατάστασης-στόχου για MFPT ({', '.join(user_states)}): \")\n",
        "\n",
        "        initial_dist_for_export = np.full(len(user_states), 1.0 / len(user_states))\n",
        "\n",
        "        export_analysis_to_html_with_mathjax(\n",
        "            mc_for_export,\n",
        "            output_filename=\"markov_chain_analysis_mathjax.html\",\n",
        "            initial_distribution=initial_dist_for_export,\n",
        "            n_steps_for_dist=export_n_steps,\n",
        "            target_state_for_mfpt_name=export_target_state_name_input\n",
        "        )\n",
        "\n",
        "    except NameError as e:\n",
        "        print(f\"Σφάλμα: {e}\")\n",
        "        print(\"Παρακαλώ βεβαιωθείτε ότι έχετε εκτελέσει τα προηγούμενα κελιά και έχετε δώσει είσοδο.\")\n",
        "    except ValueError as e:\n",
        "        print(f\"Σφάλμα στην είσοδο ανάλυσης: {e}\")\n",
        "    except Exception as e:\n",
        "        print(f\"Παρουσιάστηκε απροσδόκητο σφάλμα: {e}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 529
        },
        "id": "FenVXRGExnKO",
        "outputId": "c9486464-7814-4562-a333-d3f19d00c998"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "######################################################################\n",
            "### Εκτέλεση εξαγωγής ανάλυσης Μαρκοβιανής Αλυσίδας σε HTML\n",
            "######################################################################\n",
            "\n",
            "Δημιουργήθηκε Μαρκοβιανή Αλυσίδα με 2 καταστάσεις: 1, 2\n",
            "\n",
            "Πίνακας Μετάβασης (P):\n",
            "[[0.9 0.1]\n",
            " [0.5 0.5]]\n"
          ]
        },
        {
          "output_type": "error",
          "ename": "KeyboardInterrupt",
          "evalue": "Interrupted by user",
          "traceback": [
            "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
            "\u001b[0;31mKeyboardInterrupt\u001b[0m                         Traceback (most recent call last)",
            "\u001b[0;32m/tmp/ipython-input-2901465728.py\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[1;32m    518\u001b[0m         \u001b[0mmc_for_export\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mDiscreteTimeMarkovChain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0muser_transition_matrix\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0muser_states\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    519\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 520\u001b[0;31m         \u001b[0mexport_n_steps\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Εισάγετε τον αριθμό βημάτων για την ανάλυση κατανομής (π.χ. 10): \"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    521\u001b[0m         \u001b[0mexport_target_state_name_input\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"Εισάγετε το **όνομα** της κατάστασης-στόχου για MFPT ({', '.join(user_states)}): \"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    522\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
            "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/ipykernel/kernelbase.py\u001b[0m in \u001b[0;36mraw_input\u001b[0;34m(self, prompt)\u001b[0m\n\u001b[1;32m   1175\u001b[0m                 \u001b[0;34m\"raw_input was called, but this frontend does not support input requests.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m   1176\u001b[0m             )\n\u001b[0;32m-> 1177\u001b[0;31m         return self._input_request(\n\u001b[0m\u001b[1;32m   1178\u001b[0m             \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprompt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m   1179\u001b[0m             \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_parent_ident\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"shell\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
            "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/ipykernel/kernelbase.py\u001b[0m in \u001b[0;36m_input_request\u001b[0;34m(self, prompt, ident, parent, password)\u001b[0m\n\u001b[1;32m   1217\u001b[0m             \u001b[0;32mexcept\u001b[0m \u001b[0mKeyboardInterrupt\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m   1218\u001b[0m                 \u001b[0;31m# re-raise KeyboardInterrupt, to truncate traceback\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1219\u001b[0;31m                 \u001b[0;32mraise\u001b[0m \u001b[0mKeyboardInterrupt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Interrupted by user\"\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m   1220\u001b[0m             \u001b[0;32mexcept\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m   1221\u001b[0m                 \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlog\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mwarning\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Invalid Message:\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexc_info\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
            "\u001b[0;31mKeyboardInterrupt\u001b[0m: Interrupted by user"
          ]
        }
      ]
    }
  ]
}