diff --git a/scripts/analyse-collaborative-complex.ipynb b/scripts/analyse-collaborative-complex.ipynb
new file mode 100644
index 0000000..660670b
--- /dev/null
+++ b/scripts/analyse-collaborative-complex.ipynb
@@ -0,0 +1,446 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 65,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "import os\n",
+ "\n",
+ "import re\n",
+ "import json\n",
+ "import statistics\n",
+ "import argparse\n",
+ "import itertools\n",
+ "from pathlib import Path\n",
+ "from dataclasses import dataclass\n",
+ "\n",
+ "# print(f\"{os.getcwd()=}\")\n",
+ "\n",
+ "from ldj import ldj\n",
+ "from utils import *\n",
+ "\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt\n",
+ "# from matplotlib.font_manager import FontProperties\n",
+ "import matplotlib.font_manager as fm\n",
+ "from matplotlib.patches import FancyBboxPatch\n",
+ "from matplotlib.patches import PathPatch\n",
+ "from matplotlib.path import get_path_collection_extents\n",
+ "import seaborn as sns\n",
+ "\n",
+ "from rich import print, pretty\n",
+ "from tabulate import tabulate\n",
+ "from typing import Iterable\n",
+ "import pretty_errors\n",
+ "from catppuccin import PALETTE\n",
+ "from IPython.display import display, HTML\n",
+ "\n",
+ "pretty.install()\n",
+ "\n",
+ "EXPERIMENT_DIR = Path(\"../experiments/collaborative-complex\")\n",
+ "assert EXPERIMENT_DIR.is_dir() and EXPERIMENT_DIR.exists()\n",
+ "\n",
+ "flavor = PALETTE.latte.colors\n",
+ "\n",
+ "data = dict()\n",
+ "\n",
+ "@dataclass\n",
+ "class Results:\n",
+ " with_tracking: dict\n",
+ " without_tracking: dict\n",
+ "\n",
+ "results = Results(dict(), dict())\n",
+ "\n",
+ "with open(EXPERIMENT_DIR / \"tracking-true.json\") as f:\n",
+ " results.with_tracking = json.load(f)\n",
+ "\n",
+ "with open(EXPERIMENT_DIR / \"tracking-false.json\") as f:\n",
+ " results.without_tracking = json.load(f)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 55,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@dataclass(frozen=True)\n",
+ "class Statistics:\n",
+ " mean: float\n",
+ " median: float\n",
+ " stdev: float\n",
+ " min: float\n",
+ " max: float\n",
+ "\n",
+ "\n",
+ " def display(self) -> None:\n",
+ " data = [\n",
+ " [\"Mean\", self.mean],\n",
+ " [\"Median\", self.median],\n",
+ " [\"Standard Deviation\", self.stdev],\n",
+ " [\"Min\", self.min],\n",
+ " [\"Max\", self.max]\n",
+ " ]\n",
+ " html_table = tabulate(data, headers=[\"Statistic\", \"Value\"], tablefmt=\"html\")\n",
+ " centered_html_table = f\"\"\"\n",
+ "
\n",
+ " {html_table}\n",
+ "
\n",
+ " \"\"\"\n",
+ " # display(HTML(html_table))\n",
+ " display(HTML(centered_html_table))\n",
+ " # print(tabulate(data, headers=[\"Statistic\", \"Value\"], tablefmt=\"html\"))\n",
+ "\n",
+ "\n",
+ "def compute_stats(data: list[float]) -> Statistics:\n",
+ " return Statistics(\n",
+ " mean=np.mean(data),\n",
+ " median=np.median(data),\n",
+ " stdev=np.std(data),\n",
+ " min=np.min(data),\n",
+ " max=np.max(data),\n",
+ " )\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 41,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@dataclass(frozen=True)\n",
+ "class PerpendicularPositionErrorResult:\n",
+ " errors: list[float]\n",
+ " rmses: list[float]\n",
+ "\n",
+ "\n",
+ "def perpendicular_position_error(exported_data: dict) -> PerpendicularPositionErrorResult:\n",
+ " errors: list[float] = []\n",
+ " rmses: list[float] = []\n",
+ "\n",
+ " for robot_id, robot_data in exported_data['robots'].items():\n",
+ " color: str = robot_data['color']\n",
+ " positions = np.array([p for p in robot_data['positions']])\n",
+ " mission = robot_data['mission']\n",
+ " waypoints = []\n",
+ " for route in mission['routes']:\n",
+ " waypoints.append(route['waypoints'][0])\n",
+ " for wp in route['waypoints'][1:]:\n",
+ " waypoints.append(wp)\n",
+ "\n",
+ " waypoints = np.array(waypoints)\n",
+ " waypoints = np.squeeze(waypoints)\n",
+ "\n",
+ " lines: list[LinePoints] = [LinePoints(start=start, end=end) for start, end in sliding_window(waypoints, 2)]\n",
+ " closest_projections = [closest_projection_onto_line_segments(p, lines) for p in positions]\n",
+ "\n",
+ " error: float = np.sum(np.linalg.norm(positions - closest_projections, axis=1))\n",
+ " rmse: float = np.sqrt(error / len(positions))\n",
+ "\n",
+ " errors.append(error)\n",
+ " rmses.append(rmse)\n",
+ "\n",
+ " return PerpendicularPositionErrorResult(errors=errors, rmses=rmses)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 59,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@dataclass(frozen=True)\n",
+ "class CollisionsResult:\n",
+ " interrobot: int\n",
+ " environment: int\n",
+ "\n",
+ "def collisions(exported_data: dict) -> CollisionsResult:\n",
+ " interrobot: int = len(exported_data['collisions']['robots'])\n",
+ " environment: int = len(exported_data['collisions']['environment'])\n",
+ " return CollisionsResult(interrobot=interrobot, environment=environment)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# With Tracking"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Makespan"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 66,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "makespan = 159.92 seconds\n",
+ "
\n"
+ ],
+ "text/plain": [
+ "makespan = \u001b[1;36m159.92\u001b[0m seconds\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "print(f\"makespan = {results.with_tracking['makespan']:.2f} seconds\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Perpendicular Position Error"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 70,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n"
+ ],
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ "
\n",
+ "\n",
+ "Statistic | Value |
\n",
+ "\n",
+ "\n",
+ "Mean | 113.23 |
\n",
+ "Median | 107.61 |
\n",
+ "Standard Deviation | 47.0414 |
\n",
+ "Min | 9.9365e-13 |
\n",
+ "Max | 316.921 |
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ " "
+ ],
+ "text/plain": [
+ "\u001b[1m<\u001b[0m\u001b[1;95mIPython.core.display.HTML\u001b[0m\u001b[39m object\u001b[0m\u001b[1m>\u001b[0m"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "pperror = perpendicular_position_error(results.with_tracking)\n",
+ "compute_stats(pperror.errors).display()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Collisions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 60,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n"
+ ],
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[1;35mCollisionsResult\u001b[0m\u001b[1m(\u001b[0m\u001b[33minterrobot\u001b[0m=\u001b[1;36m0\u001b[0m, \u001b[33menvironment\u001b[0m=\u001b[1;36m90\u001b[0m\u001b[1m)\u001b[0m"
+ ]
+ },
+ "execution_count": 60,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "collisions(results.with_tracking)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Without Tracking"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "makespan = 160.34 seconds\n",
+ "
\n"
+ ],
+ "text/plain": [
+ "makespan = \u001b[1;36m160.34\u001b[0m seconds\n"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "print(f\"makespan = {results.without_tracking['makespan']:.2f} seconds\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Perpendicular Position Error"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 71,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n"
+ ],
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ "
\n",
+ "\n",
+ "Statistic | Value |
\n",
+ "\n",
+ "\n",
+ "Mean | 113.23 |
\n",
+ "Median | 107.61 |
\n",
+ "Standard Deviation | 47.0414 |
\n",
+ "Min | 9.9365e-13 |
\n",
+ "Max | 316.921 |
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ " "
+ ],
+ "text/plain": [
+ "\u001b[1m<\u001b[0m\u001b[1;95mIPython.core.display.HTML\u001b[0m\u001b[39m object\u001b[0m\u001b[1m>\u001b[0m"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "pperror = perpendicular_position_error(results.without_tracking)\n",
+ "compute_stats(pperror.errors).display()\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Collisions"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 63,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n"
+ ],
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ "\u001b[1;35mCollisionsResult\u001b[0m\u001b[1m(\u001b[0m\u001b[33minterrobot\u001b[0m=\u001b[1;36m0\u001b[0m, \u001b[33menvironment\u001b[0m=\u001b[1;36m90\u001b[0m\u001b[1m)\u001b[0m"
+ ]
+ },
+ "execution_count": 63,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "collisions(results.without_tracking)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}