{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 飞机降落问题\n",
    "\n",
    "**类别：** 排程\n",
    "\n",
    "使用 OptAgent 的 Python 接口描述变量、约束与目标。\n",
    "\n",
    "问题与原始示例来源：[Hexaly Code Templates](https://www.hexaly.com/templates/aircraft-landing-problem-2)。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 问题描述\n",
    "\n",
    "在**飞机降落问题**中,需要为一组飞机安排降落时间。每架飞机可以在目标时间附近的一个预定时间窗口内降落,且相邻两架飞机之间必须保持一定的间隔时间。每架飞机都需要支付提前或延迟降落的惩罚费用。目标是在满足所有约束条件的前提下,最小化总惩罚费用。\n",
    "\n",
    "### 学习要点\n",
    "\n",
    "- 使用 [列表决策变量](https://optagent.pages.dev/guide/modeling/) 表示降落顺序\n",
    "- 使用 [lambda 表达式](https://optagent.pages.dev/guide/modeling/) 递归地定义飞机的降落时间\n",
    "- 使用非线性算子,如[三元条件表达式](https://optagent.pages.dev/api/model-reference/#optagent.OptModel.iif)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据\n",
    "\n",
    "我们提供来自 [OR Library](http://people.brunel.ac.uk/~mastjjb/jeb/orlib/airlandinfo.html) 的算例。数据文件的格式如下:\n",
    "\n",
    "- 第一行:飞机数量与冻结时间\n",
    "- 从第二行起,每架飞机 i 的信息:\n",
    "\n",
    "- 出现时间\n",
    "- 最早降落时间\n",
    "- 目标降落时间\n",
    "- 最晚降落时间\n",
    "- 早于目标时间降落时,每单位时间的惩罚费用\n",
    "- 晚于目标时间降落时,每单位时间的惩罚费用\n",
    "- 对于每架飞机 j:在飞机 i 降落后,飞机 j 降落所需的间隔时间\n",
    "\n",
    "我们仅考虑问题的静态版本,忽略出现时间与冻结时间。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 建模思路\n",
    "\n",
    "飞机降落问题的 OptAgent 模型使用 [列表决策变量](https://optagent.pages.dev/guide/modeling/) 表示降落顺序。列表中的第 i 个元素对应第 i 个降落的飞机的索引。为了确保所有飞机均被调度,我们对列表变量施加排列约束。\n",
    "\n",
    "我们还引入了另一个决策变量:每架飞机的偏好降落时间。当一架飞机在前一架飞机之后必须等待才能降落时,它只能在目标时间之后降落。因此,一架飞机的偏好降落时间在最早降落时间与目标时间之间。\n",
    "\n",
    "[递归 lambda 函数](https://optagent.pages.dev/guide/modeling/) 将每架飞机的降落时间定义为偏好降落时间与该飞机在不违反与前机间隔时间的前提下所能降落的最早时间之间的最大值。\n",
    "\n",
    "目标是最小化由每架飞机提前或延迟降落所产生的惩罚费用。为了计算每架飞机的该费用,我们使用三元条件表达式,根据降落时间与目标时间的差值,选择提前费用或延迟费用。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python 实现\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aircraft-model",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "from optagent import OptModel, solve\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "def read_elem(filename):\n",
    "    with open(filename, encoding=\"utf-8\") as f:\n",
    "        return [str(elem) for elem in f.read().split()]\n",
    "\n",
    "\n",
    "def read_instance(instance_file):\n",
    "    file_it = iter(read_elem(instance_file))\n",
    "    nb_planes = int(next(file_it))\n",
    "    next(file_it)  # Skip freezeTime value\n",
    "    earliest_time_data = []\n",
    "    target_time_data = []\n",
    "    latest_time_data = []\n",
    "    earliness_cost_data = []\n",
    "    tardiness_cost_data = []\n",
    "    separation_time_data = []\n",
    "\n",
    "    for p in range(nb_planes):\n",
    "        next(file_it)  # Skip appearanceTime values\n",
    "        earliest_time_data.append(int(next(file_it)))\n",
    "        target_time_data.append(int(next(file_it)))\n",
    "        latest_time_data.append(int(next(file_it)))\n",
    "        earliness_cost_data.append(float(next(file_it)))\n",
    "        tardiness_cost_data.append(float(next(file_it)))\n",
    "        separation_time_data.append([0] * nb_planes)\n",
    "\n",
    "        for pp in range(nb_planes):\n",
    "            separation_time_data[p][pp] = int(next(file_it))\n",
    "\n",
    "    return (\n",
    "        nb_planes,\n",
    "        earliest_time_data,\n",
    "        target_time_data,\n",
    "        latest_time_data,\n",
    "        earliness_cost_data,\n",
    "        tardiness_cost_data,\n",
    "        separation_time_data,\n",
    "    )\n",
    "\n",
    "\n",
    "def main(instance_file, output_file=None, time_limit=20):\n",
    "    nb_planes, earliest_time_data, target_time_data, latest_time_data, earliness_cost_data, tardiness_cost_data, separation_time_data = read_instance(instance_file)\n",
    "    model = OptModel()\n",
    "\n",
    "    # List variable: landing_order[p] is the index of the p-th plane to land\n",
    "    landing_order = model.list(nb_planes)\n",
    "\n",
    "    # All planes must be scheduled\n",
    "    model.constraint(\n",
    "        model.count(landing_order) == nb_planes\n",
    "    )\n",
    "\n",
    "    # Create OptAgent arrays to be able to access them with an \"at\" operator\n",
    "    target_time = model.array(target_time_data)\n",
    "    latest_time = model.array(latest_time_data)\n",
    "    earliness_cost = model.array(earliness_cost_data)\n",
    "    tardiness_cost = model.array(tardiness_cost_data)\n",
    "    separation_time = model.array(separation_time_data)\n",
    "\n",
    "    # Int variable: preferred landing time for each plane\n",
    "    preferred_time_vars = [\n",
    "        model.int(\n",
    "            lb=earliest_time_data[p],\n",
    "            ub=target_time_data[p],\n",
    "        )\n",
    "        for p in range(nb_planes)\n",
    "    ]\n",
    "    preferred_time = model.array(preferred_time_vars)\n",
    "\n",
    "    # Landing time for each plane (recursive: respects separation with the previous plane)\n",
    "    landing_time_lambda = model.lambda_function(\n",
    "        lambda p, prev: model.max(\n",
    "            model.at(preferred_time, model.at(landing_order, p)),\n",
    "            model.iif(\n",
    "                p > 0,\n",
    "                prev\n",
    "                + model.at(\n",
    "                    separation_time,\n",
    "                    model.at(landing_order, p - 1),\n",
    "                    model.at(landing_order, p),\n",
    "                ),\n",
    "                0,\n",
    "            ),\n",
    "        )\n",
    "    )\n",
    "    landing_time = model.array(model.range(0, nb_planes), landing_time_lambda)\n",
    "\n",
    "    # Landing times must respect separation from every previous plane\n",
    "    for p in range(1, nb_planes):\n",
    "        last_separation_end = [\n",
    "            landing_time[previous_plane]\n",
    "            + model.at(\n",
    "                separation_time,\n",
    "                model.at(landing_order, previous_plane),\n",
    "                model.at(landing_order, p),\n",
    "            )\n",
    "            for previous_plane in range(p)\n",
    "        ]\n",
    "        model.constraint(landing_time[p] >= model.max(last_separation_end))\n",
    "\n",
    "    total_cost_terms = []\n",
    "    for p in range(nb_planes):\n",
    "        plane_index = model.at(landing_order, p)\n",
    "\n",
    "        # Constraint on latest landing time\n",
    "        model.constraint(\n",
    "            model.at(landing_time, p) <= model.at(latest_time, plane_index),\n",
    "        )\n",
    "\n",
    "        # Cost for each plane\n",
    "        difference_to_target_time = model.abs(\n",
    "            model.at(landing_time, p) - model.at(target_time, plane_index)\n",
    "        )\n",
    "        unit_cost = model.iif(\n",
    "            model.at(landing_time, p) < model.at(target_time, plane_index),\n",
    "            model.at(earliness_cost, plane_index),\n",
    "            model.at(tardiness_cost, plane_index),\n",
    "        )\n",
    "        total_cost_terms.append(unit_cost * difference_to_target_time)\n",
    "\n",
    "    total_cost = model.sum(total_cost_terms)\n",
    "\n",
    "    # Minimize the total cost\n",
    "    model.minimize(total_cost)\n",
    "\n",
    "    solution = solve(model, time_limit_s=float(time_limit))\n",
    "    result_values = {'landing_order': landing_order.value, 'total_cost': total_cost.value}\n",
    "\n",
    "    landing_sequence = [int(item) for item in result_values[\"landing_order\"]]\n",
    "    print(\n",
    "        f\"Nb planes = {nb_planes}; Total cost = {int(result_values['total_cost'])}; \"\n",
    "        f\"Status = {solution.feasible}\"\n",
    "    )\n",
    "    print(\"Landing order:\", \" \".join(str(p) for p in landing_sequence))\n",
    "\n",
    "    if output_file is not None:\n",
    "        result_text = (\n",
    "            f\"{int(result_values['total_cost'])}\\n\"\n",
    "            + \" \".join(str(p) for p in landing_sequence)\n",
    "            + \"\\n\"\n",
    "        )\n",
    "        Path(output_file).write_text(result_text, encoding=\"utf-8\")\n",
    "    return solution\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aircraft-runs",
   "metadata": {},
   "source": [
    "## 本地运行\n",
    "\n",
    "Notebook 直接调用 `main` 并显式传入实例路径。以下代码格相互独立，可以按需要单独运行；调整 `time_limit` 可以控制每个实例的求解时间。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aircraft-instance-dir",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "INSTANCE_DIR = Path.cwd() / \"instances\"\n",
    "print(\"Instances:\", INSTANCE_DIR)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aircraft-run-1",
   "metadata": {},
   "outputs": [],
   "source": [
    "solution_airland1 = main(INSTANCE_DIR / \"airland1.txt\", time_limit=1)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aircraft-run-5",
   "metadata": {},
   "outputs": [],
   "source": [
    "solution_airland5 = main(INSTANCE_DIR / \"airland5.txt\", time_limit=1)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aircraft-run-10",
   "metadata": {},
   "outputs": [],
   "source": [
    "solution_airland10 = main(INSTANCE_DIR / \"airland10.txt\", time_limit=1)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
