{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# MILP Energy Lab - solution notebook\n",
        "\n",
        "Derived from the PyPSA 'Modular Expansion with Unit Commitment' tutorial\n",
        "(CC BY 4.0, PyPSA contributors). This notebook performs a fresh real\n",
        "default solve, hand-computable reference assertions, an infeasible case, a\n",
        "shedding case, a startup-cost case, and a tiny AGILAB batch with\n",
        "matching one/two worker results when capacity allows."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import json\n",
        "import os\n",
        "import sys\n",
        "from pathlib import Path\n",
        "\n",
        "PROJECT_ROOT = Path(globals().get('PROJECT_ROOT', Path.cwd()))\n",
        "sys.path.insert(0, str(PROJECT_ROOT))\n",
        "\n",
        "from energy_core import default_settings, solve_scenario, make_batch, cpu_limits\n",
        "from energy_runner import run_benchmark\n",
        "\n",
        "print('PROJECT_ROOT =', PROJECT_ROOT)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Fresh default solve + hand-computable upstream reference.\n",
        "settings = default_settings()\n",
        "result = solve_scenario(settings)\n",
        "print('status:', result['status'], 'objective:', result['objective'])\n",
        "\n",
        "demand = [4000, 6000, 5000, 800]\n",
        "active = [20, 30, 25, 4]\n",
        "expected_cost = sum(demand) + 200 * max(active) + sum(active)\n",
        "assert result['status'] == 'optimal'\n",
        "assert abs(result['objective'] - expected_cost) < 1e-5, result['objective']\n",
        "assert result['dispatch'] == [float(v) for v in demand]\n",
        "assert result['active_modules'] == active\n",
        "assert abs(result['capacity_mw'] - 6000) < 1e-6\n",
        "assert abs(result['modules'] - 30) < 1e-6\n",
        "assert str(result['solver']['name']).lower() == 'highs'\n",
        "assert result['solver']['threads'] == 1\n",
        "print('Reference objective (21879) verified:', expected_cost)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Infeasible case: capacity-limited, no shedding.\n",
        "infeasible = solve_scenario(settings | {'max_modules': 20})\n",
        "assert infeasible['status'] == 'infeasible'\n",
        "assert infeasible['objective'] is None\n",
        "assert infeasible['dispatch'] == []\n",
        "print('Infeasible case OK')\n",
        "\n",
        "# Shedding case: shortage made feasible by shedding.\n",
        "shed = solve_scenario(settings | {'max_modules': 20, 'allow_shedding': True})\n",
        "assert shed['status'] in ('optimal', 'feasible')\n",
        "assert sum(shed['shed']) >= 3000 - 1e-5\n",
        "print('Shedding case OK, shed total:', sum(shed['shed']))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Startup-cost tradeoff (12h): startup cost must affect commitment.\n",
        "startup = solve_scenario(settings | {'hours': 12, 'startup_cost': 500.0})\n",
        "assert startup['status'] == 'optimal'\n",
        "expected_active = [20] + [30] * 9 + [25, 4]\n",
        "expected_obj = 3 * (4000 + 6000 + 5000 + 800) + 200 * 30 + 500 * 30 + sum(expected_active)\n",
        "assert abs(startup['objective'] - expected_obj) < 1e-5, startup['objective']\n",
        "assert startup['active_modules'] == expected_active\n",
        "print('Startup optimum (68719) verified:', expected_obj)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Tiny AGILAB batch: matching one/two worker results if capacity allows.\n",
        "env = cpu_limits()\n",
        "workers = min(2, env['effective_cpus'])\n",
        "batch = make_batch(settings, 4)\n",
        "benchmark = run_benchmark(batch, workers) if workers > 1 else None\n",
        "if benchmark is not None:\n",
        "    one, many = benchmark['sequential'], benchmark['parallel']\n",
        "    assert benchmark['comparison']['matches']\n",
        "    assert one['batch'] == many['batch']\n",
        "    assert len(many['rows']) == 4\n",
        "    assert sorted(r['case'] for r in many['rows']) == list(range(4))\n",
        "    assert len({r['pid'] for r in many['rows']}) == 2\n",
        "    print('AGILAB batch OK, speedup:', benchmark['comparison']['speedup'])\n",
        "else:\n",
        "    print('Single CPU: scaling skipped')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Persist project results for the numerical verifier.\n",
        "results = {\n",
        "    'results': {\n",
        "        'reference_objective': result['objective'],\n",
        "        'reference_modules': result['modules'],\n",
        "        'reference_active': result['active_modules'],\n",
        "        'infeasible_status': infeasible['status'],\n",
        "        'shed_total': sum(shed['shed']),\n",
        "        'startup_objective': startup['objective'],\n",
        "        'benchmark_matches': (benchmark['comparison']['matches'] if benchmark else None),\n",
        "    }\n",
        "}\n",
        "Path('results.json').write_text(json.dumps(results, allow_nan=False))\n",
        "Path('metrics.json').write_text(json.dumps({\n",
        "    'reference_objective': result['objective'],\n",
        "    'startup_objective': startup['objective'],\n",
        "}, allow_nan=False))\n",
        "print('Wrote results.json and metrics.json')"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.11"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
