{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 随机装箱问题\n",
    "\n",
    "**类别：** 装箱\n",
    "\n",
    "使用 OptAgent 的 Python 接口描述变量、约束与目标。\n",
    "\n",
    "问题与原始示例来源：[Hexaly Code Templates](https://www.hexaly.com/templates/stochastic-packing-problem)。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 问题描述\n",
    "\n",
    "在随机装箱问题中，需要将一组物品划分到若干箱子中。没有容量约束，每个箱子可以装入任何物品。该问题的随机性来源于物品的重量是随机的。在本例中，随机性通过不同的场景来表示，每个场景对应一组可能的物品重量。对于给定的物品到箱子的分配方案，每个场景都有一个与之对应的最大箱重。\n",
    "\n",
    "如何选取最合适的目标函数以最小化这种随机的最大重量是一个挑战。最小化平均最大重量可能掩盖有风险的场景，而最小化最坏场景下的最大重量又可能过于悲观。本例采用折中方案：最小化所有场景中最大箱重的第 90 百分位数。\n",
    "\n",
    "### 学习要点\n",
    "\n",
    "- 使用 OptAgent 的 `set` 决策变量表示各箱子所装物品\n",
    "- 使用 `partition` 约束确保每个物品恰好进入一个箱子\n",
    "- 使用集合 lambda 计算不同随机场景下每个箱子的总重量\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据\n",
    "\n",
    "本例在程序运行时随机生成实例。首先为每个物品选择一个均匀分布，然后对每个场景，从相应的均匀分布中独立采样得到各物品的重量。固定随机种子可以复现实例。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 建模思路\n",
    "\n",
    "每个箱子使用一个 `set` 决策变量表示其物品集合，并通过 `partition` 约束保证所有箱子构成物品的一个划分。\n",
    "\n",
    "对于每个场景，模型通过集合 lambda 汇总各箱子的物品重量，再取最大值作为该场景的最大箱重。将所有场景的最大箱重升序排序后，取索引 `ceil(0.9 * (场景数 - 1))` 对应的值作为第 90 百分位目标并最小化。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python 实现\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "import random\n",
    "from pathlib import Path\n",
    "\n",
    "from optagent import OptModel, solve\n",
    "\n",
    "\n",
    "def generate_scenarios(nb_items, nb_scenarios, seed):\n",
    "    rng = random.Random(seed)\n",
    "\n",
    "    item_distributions = []\n",
    "    for _ in range(nb_items):\n",
    "        item_min = rng.randint(10, 100)\n",
    "        item_max = item_min + rng.randint(0, 50)\n",
    "        item_distributions.append((item_min, item_max))\n",
    "\n",
    "    return [\n",
    "        [rng.randint(*distribution) for distribution in item_distributions]\n",
    "        for _ in range(nb_scenarios)\n",
    "    ]\n",
    "\n",
    "\n",
    "def main(\n",
    "    nb_items=10,\n",
    "    nb_bins=2,\n",
    "    nb_scenarios=3,\n",
    "    seed=42,\n",
    "    time_limit=2,\n",
    "    output_file=None,\n",
    "):\n",
    "    scenario_item_weights_data = generate_scenarios(\n",
    "        nb_items, nb_scenarios, seed\n",
    "    )\n",
    "\n",
    "    model = OptModel()\n",
    "\n",
    "    # bins[k] contains the items assigned to bin k.\n",
    "    bins = [model.set(nb_items) for k in range(nb_bins)]\n",
    "    model.constraint(model.partition(bins))\n",
    "\n",
    "    scenario_item_weights = model.array(scenario_item_weights_data)\n",
    "\n",
    "    def make_weight_lambda(scenario):\n",
    "        return model.lambda_function(\n",
    "            lambda item: scenario_item_weights[scenario, item]\n",
    "        )\n",
    "\n",
    "    scenario_max_weights = []\n",
    "    for scenario in range(nb_scenarios):\n",
    "        weight_lambda = make_weight_lambda(scenario)\n",
    "        bin_weights = [\n",
    "            model.sum(bin_items, weight_lambda) for bin_items in bins\n",
    "        ]\n",
    "        scenario_max_weights.append(model.max(bin_weights))\n",
    "\n",
    "    sorted_max_weights = model.sort(model.array(scenario_max_weights))\n",
    "    percentile_index = math.ceil(0.9 * (nb_scenarios - 1))\n",
    "    stochastic_max_weight = sorted_max_weights[percentile_index]\n",
    "    model.minimize(stochastic_max_weight)\n",
    "\n",
    "    solution = solve(model, time_limit_s=float(time_limit))\n",
    "    if not solution.feasible:\n",
    "        print(f\"No feasible packing found; Status = {solution.status}\")\n",
    "        return solution\n",
    "\n",
    "    bin_values = [sorted(int(item) for item in bin_items.value) for bin_items in bins]\n",
    "    display_lines = [\n",
    "        f\"90th percentile maximum weight = {stochastic_max_weight.value}; \"\n",
    "        f\"Status = {solution.status}\",\n",
    "        \"\",\n",
    "        \"Scenario item weights:\",\n",
    "    ]\n",
    "    display_lines.extend(\n",
    "        f\"{scenario}: {weights}\"\n",
    "        for scenario, weights in enumerate(scenario_item_weights_data)\n",
    "    )\n",
    "    display_lines.extend([\"\", \"Bins:\"])\n",
    "    display_lines.extend(\n",
    "        f\"{bin_index}: {items}\"\n",
    "        for bin_index, items in enumerate(bin_values)\n",
    "    )\n",
    "    result_text = \"\\n\".join(display_lines)\n",
    "    print(result_text)\n",
    "\n",
    "    if output_file is not None:\n",
    "        Path(output_file).write_text(result_text + \"\\n\", encoding=\"utf-8\")\n",
    "    return solution\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "solution = main(\n",
    "    nb_items=10,\n",
    "    nb_bins=2,\n",
    "    nb_scenarios=3,\n",
    "    seed=42,\n",
    "    time_limit=1,\n",
    ")\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
