{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [MOV02] ์์ก์ง์ ์ค์ ยท๋น์ฉ ๊ฐ์ํ Tool\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์์กยท๋ฌผ๋ฅยท์ฐฝ๊ณ / Level 2)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/MOV02/MOV02_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์์กยท๋ฌผ๋ฅยท์ฐฝ๊ณ (Level 2) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.0์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"NATO ๋๋งน์ ํ์ฌ๋ น๋ถ(ACT)๋ ๋ค๊ตญ์ ๊ตฐ์ ์ง์์์ ์์ก ์์ฒญ๋ถํฐ ๋์ฐฉ๊น์ง์ ์๊ฐ, ์ฒ๋ฆฌ ๊ธฐ๊ฐ, ๋ฌผ๋๋๊ณผ ์์ก๋ฅ๋ ฅ, ์ง์ฐ ์์ธ์ ํ ํ๋ฉด์์ ๋ณด๋ ๋ฌผ๋ฅยท์์ก ์ฑ๊ณผ ๊ฐ์ํ๋ฅผ ๊ฐ์กฐํด ์๋ค. ๋ณธ ํ๋ก์ ํธ๋ ์ด ๊ณต๊ฐ ์ฌ๋ก๋ฅผ ๋ฐํ์ผ๋ก ๊ต์ก์ฉ ๊ฐ์ ๋๋ก๋ง๊ณผ ์์ก ์์ฐ ์๋ฃ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด, ์์ก์ง์ ์ค์ ๊ณผ ๋น์ฉ์ ๊ทผ๊ฑฐ๋ก ๊ฒฝ๋ก์ ์์ฐ์ ๊ฒฐ์ ํ๋ ๊ณผ์ ์ ๊ตฌํํ๋ค. ํ์ต์๋ ์์ฐยท๋๋ก๋ง ์คํค๋ง ์ ์, ๋๋ก ํ๊ดดยทํต์ ๊ตฌ์ญ ์ ์ฒ๋ฆฌ, ๋ค์ต์คํธ๋ผ Baseline, ์ํ ํํผยท์ ์ฌ ํจ์จ ์ถ์ฒ, ์์กดํ๋ฅ ยท์ ์๋์ฐฉ ์ ๋ขฐ๋ ์ ์, ๊ด์ ๋์๋ณด๋, ์์กํต์ ๊ด ์น์ธ(HITL), ๋๋ก 50๊ณณ ์ฐจ๋จ ์คํธ๋ ์ค ์ํ, AAR ์ฌ๋ณด์ , ์ต์ข
ํตํฉ์ ์ฐจ๋ก๋ก ์ํํ๋ค. ์ต์ข
๋ชฉํ๋ ์์ก ๋ฆฌ๋ํ์๊ณผ ํคkm๋น ๋น์ฉ์ ๋ฎ์ถ๋ฉด์ ์ํ ๊ฒฝ๋ก์ ๊ฐํ ์ฌ๋ถ๋ ๋ฐ๋์ ์ฌ๋์ด ๊ฒฐ์ ํ๋๋ก ํ๋ ์์ก์ง์ ์ค์ ยท๋น์ฉ ๊ฐ์ํ ๋๊ตฌ๋ฅผ ์์ฑํ๋ ๊ฒ์ด๋ค.\n",
"\n",
"> **โ ๏ธ [๊ต์ก์ฉ ๊ฐ์ ๋ฐ์ดํฐ์
์์น]**\n",
"> - ๋ณธ ๋
ธํธ๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ(์ ์, ๋ก๊ทธ, ์๋๋ฆฌ์ค ๋ฑ)๋ ๋ณด์ ๊ท์ ์ ์ค์ํ์ฌ **์ฝ๋ ๋ด ๋์ ๊ธฐ๋ฐ ๊ฐ์ ๋ฐ์ดํฐ๋ก ์์ฑ**๋ฉ๋๋ค.\n",
"> - ์ค์ ๋ถ๋ ์์น, ์์ ์ธ๊ฐ, ์ค์ ์ ์ฅ ์ ๋ณด, ๊ตฐ์ฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ ์ผ์ฒด ํฌํจ๋์ง ์์ต๋๋ค.\n",
"\n",
"---\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ============================================================================\n",
"# [Google Colab ํ๊ฒฝ ์ค์ ] ํ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์น ๋ฐ ์ํฌํธ\n",
"# ============================================================================\n",
"!pip install -q numpy pandas matplotlib scipy scikit-learn\n",
"\n",
"import os\n",
"import sys\n",
"import time\n",
"import math\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import warnings\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"# ๊ทธ๋ํ ์๋ฌธ ํฐํธ ๊ธฐ๋ณธ ์ค์ (ํ๊ฒฝ ๋
๋ฆฝ์ฑ ๋ณด์ฅ)\n",
"plt.rcParams['font.sans-serif'] = 'DejaVu Sans'\n",
"plt.rcParams['axes.unicode_minus'] = False\n",
"\n",
"print(f\"์คํ ํ๊ฒฝ ์ค๋น ์๋ฃ (Python {sys.version.split()[0]})\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
01. ์ ์ฅ ์์ก ์์ฐ(ํธ๋ญ, ํธ๋ ์ผ๋ฌ, ์์ก๊ธฐ) ์ ์ ๋ฐ ๋๋ก๋ง ๋คํธ์ํฌ ๋ฐ์ดํฐ ์คํค๋ง ์ ์\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ฅ ์์ก ์์ฐ(ํธ๋ญ, ํธ๋ ์ผ๋ฌ) ์ ์ ๋ฐ ๋๋ก๋ง ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํธ๋ญยทํธ๋ ์ผ๋ฌยท์์ก๊ธฐ์ ๊ต์ก์ฉ ๊ฐ์ ์ ์๊ณผ ๋๋ก๋ง(๋
ธ๋ยท๊ฐ์ ยท๊ต๋ ํ์ค) ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ์์ก์ง์ ์ค์ ยท๋น์ฉ ๋ณด๊ณ ์
๋ฌด๋ฅผ AS-IS/TO-BE๋ก ๋น๊ตํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ฐ(์ ์ฌ๋ยท์๋ยทkm๋น ๋น์ฉ), ๋
ธ๋, ๊ฐ์ (๊ฑฐ๋ฆฌยท๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยทํตํ ์ฌ๋ถ), ์์ก ์์ฒญ์ ํ๋์ ํ์ฉ๊ฐ์ ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/mov02_as_is_to_be_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ํํ ์
๋ฌด(AS-IS)์ ๊ฐ์ ์
๋ฌด(TO-BE) ๋น๊ต์ ๋ช
ํ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ๋จ๊ณ ๋ถํด 10๋จ๊ณ ์ด์, ๋ณ๋ชฉ ๊ตฌ๊ฐ 3๊ฐ์ ์ด์ ๋์ถ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์
๋ฌด ์ ์์ ๋ฐ ์
์ถ๋ ฅ ๋ฐ์ดํฐ ๋ช
์ธ ์์ฑ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ค๋ฌด ํ๋ก์ธ์ค์์ ์ผ์น์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ ์ฅ ์์ก ์์ฐ(ํธ๋ญยทํธ๋ ์ผ๋ฌยท์์ก๊ธฐ) ์ ์๊ณผ ๋๋ก๋ง(๋
ธ๋ยท๊ฐ์ ) ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํด ๊ฐ์ ์ํ์ ๊ฒ์ฆํ๊ณ , ์์ก์ง์ ์ค์ ยท๋น์ฉ ๋ณด๊ณ ์
๋ฌด๋ฅผ AS-IS/TO-BE ๋จ๊ณ๋ก ๋ถํดํด ๋ณ๋ชฉ ๊ตฌ๊ฐ์ ๋์ถํ๋ค.\"\"\"\n",
"import copy\n",
"import heapq\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋๋ก๋งยท์์ก ์์ฐ ์์ฑ๊ธฐ (์ค์ ์ง๋ช
ยท๋ถ๋ยท์ฅ๋น ์ ์ ์์)\n",
"# ๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง(๋
ธ๋=๊ต์ฐจ์ , ๊ฐ์ =๋๋ก ๊ตฌ๊ฐ)์ ๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยท๊ฐ์ ์ํ ๊ตฌ์ญ์ ๋ถ์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ASSETS = { # ๊ต์ก์ฉ ๊ฐ์ ์ ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
" \"truck\": {\"name\": \"์คํ ํธ๋ญ\", \"cap_t\": 5.0, \"speed_kmh\": 50, \"cost_km\": 1.0, \"weight_t\": 10.0},\n",
" \"trailer\": {\"name\": \"๋ํ ํธ๋ ์ผ๋ฌ\", \"cap_t\": 20.0, \"speed_kmh\": 40, \"cost_km\": 2.4, \"weight_t\": 30.0},\n",
" \"airlift\": {\"name\": \"์์ก๊ธฐ\", \"cap_t\": 12.0, \"speed_kmh\": 300, \"cost_km\": 9.0, \"weight_t\": 0.0},\n",
"}\n",
"ROAD_FACTOR = {\"paved\": 1.0, \"unpaved\": 0.6} # ๋๋ก ๋ฑ๊ธ๋ณ ์๋ ๊ณ์\n",
"\n",
"\n",
"def make_network(seed=5, rows=6, cols=8, spacing=5.0):\n",
" \"\"\"๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง. ๋
ธ๋ ์ขํ(km)์ ๊ฐ์ ๋ชฉ๋ก(๊ฑฐ๋ฆฌยท๋๋ก๋ฑ๊ธยท๊ต๋ ํ์ค)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" nodes = {f\"N{r:02d}{c:02d}\": (c * spacing + rng.uniform(-1, 1), r * spacing + rng.uniform(-1, 1))\n",
" for r in range(rows) for c in range(cols)}\n",
" edges = []\n",
"\n",
" def add(a, b):\n",
" (x1, y1), (x2, y2) = nodes[a], nodes[b]\n",
" bridge = bool(rng.random() < 0.15)\n",
" edges.append({\"id\": f\"E{len(edges):03d}\", \"u\": a, \"v\": b,\n",
" \"km\": round(float(np.hypot(x2 - x1, y2 - y1) * rng.uniform(1.0, 1.25)), 2),\n",
" \"road\": \"paved\" if rng.random() < 0.65 else \"unpaved\", \"bridge\": bridge,\n",
" \"limit_t\": float(rng.choice([20, 40, 60])) if bridge else None, \"open\": True})\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if c + 1 < cols:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r:02d}{c + 1:02d}\")\n",
" if r + 1 < rows:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c:02d}\")\n",
" if r + 1 < rows and c + 1 < cols and rng.random() < 0.25:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c + 1:02d}\")\n",
" return nodes, edges\n",
"\n",
"\n",
"def make_threats(seed=9, n=3, xlim=(8, 28), ylim=(3, 22)):\n",
" \"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ(์ํ). p = ๊ตฌ๊ฐ 1ํ ํต๊ณผ ์ ํผํด ๊ฐ๋ฅ์ฑ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" return [{\"id\": f\"TZ{i + 1}\", \"x\": float(rng.uniform(*xlim)), \"y\": float(rng.uniform(*ylim)),\n",
" \"r\": float(rng.uniform(3, 5)), \"p\": float(round(rng.uniform(0.05, 0.2), 3))} for i in range(n)]\n",
"\n",
"\n",
"def edge_risk(e, nodes, threats):\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" s = np.linspace(0, 1, 6)\n",
" risk = 0.0\n",
" for t in threats:\n",
" if np.min(np.hypot(x1 + (x2 - x1) * s - t[\"x\"], y1 + (y2 - y1) * s - t[\"y\"])) < t[\"r\"]:\n",
" risk = max(risk, t[\"p\"])\n",
" return risk\n",
"\n",
"\n",
"def edge_minutes(e, asset=\"truck\"):\n",
" return e[\"km\"] / (ASSETS[asset][\"speed_kmh\"] * ROAD_FACTOR[e[\"road\"]]) * 60\n",
"\n",
"\n",
"def passable(e, gross_t):\n",
" return e[\"open\"] and (not e[\"bridge\"] or e[\"limit_t\"] >= gross_t)\n",
"\n",
"\n",
"def build_adj(nodes, edges, weight_fn):\n",
" \"\"\"weight_fn(e)๊ฐ None์ด๋ฉด ํตํ ๋ถ๊ฐ ๊ฐ์ ์ผ๋ก ๋ณด๊ณ ์ ์ธํ๋ค.\"\"\"\n",
" adj = {n: [] for n in nodes}\n",
" for e in edges:\n",
" w = weight_fn(e)\n",
" if w is not None:\n",
" adj[e[\"u\"]].append((e[\"v\"], w, e))\n",
" adj[e[\"v\"]].append((e[\"u\"], w, e))\n",
" return adj\n",
"\n",
"\n",
"def dijkstra(adj, src, dst):\n",
" \"\"\"๋ค์ต์คํธ๋ผ ์ต๋จ๊ฒฝ๋ก. (๋น์ฉ, ๋
ธ๋ ๊ฒฝ๋ก, ๊ฐ์ ๋ชฉ๋ก). ๋๋ฌ ๋ถ๊ฐ๋ฉด (inf, [], []).\"\"\"\n",
" dist, prev, done, pq = {src: 0.0}, {}, set(), [(0.0, src)]\n",
" while pq:\n",
" d, u = heapq.heappop(pq)\n",
" if u in done:\n",
" continue\n",
" done.add(u)\n",
" if u == dst:\n",
" break\n",
" for v, w, e in adj[u]:\n",
" if d + w < dist.get(v, float(\"inf\")):\n",
" dist[v], prev[v] = d + w, (u, e)\n",
" heapq.heappush(pq, (d + w, v))\n",
" if dst not in done:\n",
" return float(\"inf\"), [], []\n",
" path, es = [dst], []\n",
" while path[-1] != src:\n",
" u, e = prev[path[-1]]\n",
" es.append(e); path.append(u)\n",
" return dist[dst], path[::-1], es[::-1]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ์์ก ์์ฐ / ๋๋ก๋ง ๋
ธ๋ / ๋๋ก ๊ตฌ๊ฐ(๊ฐ์ ) / ์์ก ์์ฒญ\n",
"SCHEMA = {\n",
" \"asset\": {\"asset_id\": {\"type\": str, \"pattern\": r\"^(TRK|TRL|AIR)-\\d{3}$\"},\n",
" \"kind\": {\"type\": str, \"enum\": list(ASSETS)},\n",
" \"cap_t\": {\"type\": (int, float), \"min\": 0.5, \"max\": 50},\n",
" \"speed_kmh\": {\"type\": (int, float), \"min\": 5, \"max\": 900},\n",
" \"cost_km\": {\"type\": (int, float), \"min\": 0},\n",
" \"status\": {\"type\": str, \"enum\": [\"๊ฐ์ฉ\", \"์ ๋น\", \"์ดํ์ค\"]}},\n",
" \"node\": {\"node_id\": {\"type\": str, \"pattern\": r\"^N\\d{4}$\"},\n",
" \"x_km\": {\"type\": (int, float), \"min\": -100, \"max\": 100},\n",
" \"y_km\": {\"type\": (int, float), \"min\": -100, \"max\": 100}},\n",
" \"edge\": {\"id\": {\"type\": str, \"pattern\": r\"^E\\d{3}$\"},\n",
" \"u\": {\"type\": str, \"pattern\": r\"^N\\d{4}$\"}, \"v\": {\"type\": str, \"pattern\": r\"^N\\d{4}$\"},\n",
" \"km\": {\"type\": (int, float), \"min\": 0.1, \"max\": 200},\n",
" \"road\": {\"type\": str, \"enum\": list(ROAD_FACTOR)},\n",
" \"bridge\": {\"type\": bool}, \"open\": {\"type\": bool}},\n",
" \"request\": {\"req_id\": {\"type\": str, \"pattern\": r\"^REQ-\\d{4}$\"},\n",
" \"dest\": {\"type\": str, \"pattern\": r\"^N\\d{4}$\"},\n",
" \"cargo_t\": {\"type\": (int, float), \"min\": 0.1, \"max\": 200},\n",
" \"deadline_h\": {\"type\": (int, float), \"min\": 0.5, \"max\": 72}},\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" ok_type = isinstance(value, rule[\"type\"]) and (rule[\"type\"] is bool or not isinstance(value, bool))\n",
" if not ok_type:\n",
" return [f\"{name}: ์๋ฃํ ์ค๋ฅ({type(value).__name__})\"]\n",
" errs = []\n",
" if \"enum\" in rule and value not in rule[\"enum\"]:\n",
" errs.append(f\"{name}: ํ์ฉ๋์ง ์์ ๊ฐ '{value}'\")\n",
" if \"min\" in rule and value < rule[\"min\"]:\n",
" errs.append(f\"{name}: ์ต์๊ฐ {rule['min']} ๋ฏธ๋ง\")\n",
" if \"max\" in rule and value > rule[\"max\"]:\n",
" errs.append(f\"{name}: ์ต๋๊ฐ {rule['max']} ์ด๊ณผ\")\n",
" if \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n",
" errs.append(f\"{name}: ํ์ ๋ถ์ผ์น '{value}'\")\n",
" return errs\n",
"\n",
"\n",
"def validate(group, rec, node_ids=None):\n",
" errs = []\n",
" for k, rule in SCHEMA[group].items():\n",
" errs += [f\"{group}.{k}: ํ์ ํ๋ ๋๋ฝ\"] if k not in rec else check(f\"{group}.{k}\", rec[k], rule)\n",
" if group == \"edge\" and node_ids is not None: # ์ฐธ์กฐ ๋ฌด๊ฒฐ์ฑ: ์๋ ๋
ธ๋๋ฅผ ์๋ ๊ฐ์ \n",
" errs += [f\"edge.{k}: ์กด์ฌํ์ง ์๋ ๋
ธ๋ {rec[k]}\" for k in (\"u\", \"v\") if rec.get(k) not in node_ids]\n",
" if group == \"edge\" and rec.get(\"bridge\") and not rec.get(\"limit_t\"):\n",
" errs.append(\"edge.limit_t: ๊ต๋ ํ์ค ์ ํ ๋๋ฝ\")\n",
" return errs\n",
"\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ํ ์์ฑ: ์์ฐ 12๋, ๋๋ก๋ง 48๋
ธ๋, ์์ก ์์ฒญ 6๊ฑด\n",
"nodes, edges = make_network()\n",
"rng = np.random.default_rng(1)\n",
"prefix = {\"truck\": \"TRK\", \"trailer\": \"TRL\", \"airlift\": \"AIR\"}\n",
"assets = [{\"asset_id\": f\"{prefix[k]}-{i:03d}\", \"kind\": k, **{f: ASSETS[k][f] for f in (\"cap_t\", \"speed_kmh\", \"cost_km\")},\n",
" \"status\": str(rng.choice([\"๊ฐ์ฉ\", \"์ ๋น\", \"์ดํ์ค\"], p=[0.7, 0.15, 0.15]))}\n",
" for i, k in enumerate([\"truck\"] * 7 + [\"trailer\"] * 3 + [\"airlift\"] * 2)]\n",
"node_recs = [{\"node_id\": n, \"x_km\": round(x, 2), \"y_km\": round(y, 2)} for n, (x, y) in nodes.items()]\n",
"requests = [{\"req_id\": f\"REQ-{i:04d}\", \"dest\": f\"N{r:02d}07\", \"cargo_t\": float(round(rng.uniform(3, 30), 1)),\n",
" \"deadline_h\": float(rng.choice([2, 3, 4]))} for i, r in enumerate(range(6))]\n",
"samples = [(\"asset\", a) for a in assets] + [(\"node\", n) for n in node_recs] + [(\"edge\", e) for e in edges] + [(\"request\", q) for q in requests]\n",
"ids = set(nodes)\n",
"n_err = sum(1 for g, r in samples if validate(g, r, ids))\n",
"print(f\"์ ์ ์ํ {len(samples)}๊ฑด(์์ฐ {len(assets)}, ๋
ธ๋ {len(node_recs)}, ๊ฐ์ {len(edges)}, ์์ฒญ {len(requests)}) ๊ฒ์ฆ ์ค๋ฅ {n_err}๊ฑด\")\n",
"\n",
"# 3) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = [(\"asset\", {**assets[0], \"asset_id\": \"TRUCK-1\"}), (\"asset\", {**assets[1], \"kind\": \"tank\"}),\n",
" (\"edge\", {**edges[0], \"v\": \"N9999\"}), (\"edge\", {**{k: v for k, v in edges[1].items() if k != \"km\"}}),\n",
" (\"edge\", {**edges[2], \"bridge\": True, \"limit_t\": None}), (\"request\", {**requests[0], \"cargo_t\": -2.0})]\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"caught = 0\n",
"for g, r in bad:\n",
" e = validate(g, copy.deepcopy(r), ids)\n",
" caught += bool(e)\n",
" print(\" -\", \"; \".join(e))\n",
"print(f\"์ฃผ์
์ค๋ฅ {len(bad)}์ข
์ค {caught}์ข
๊ฒ์ถ\")\n",
"\n",
"# 4) ์์ก์ง์ ์ค์ ยท๋น์ฉ ๋ณด๊ณ ์
๋ฌด AS-IS ๋ถํด(๋จ๊ณ๋ณ ์์ ๋ถ, ๊ฐ์ ์ธก์ ๊ฐ)์ TO-BE ๊ฐ์ \n",
"steps = [(\"์์ก ์์ฒญ ์ ์(์ ํยท๋ฉ์ ์ )\", 20, \"์์ฒญ ์์ ํ์คํยท์๋ ๋ฑ๋ก\", 2),\n",
" (\"๊ฐ์ฉ ์์ฐ ํ์ธ(์ฐจ๋ ์ผ์ง ์๊ธฐ ๋์กฐ)\", 30, \"์์ฐ ์ํ ์คํค๋ง๋ก ์๋ ์กฐํ\", 2),\n",
" (\"๋๋ก ์ํฉ ํ์ธ(๊ตฌ๋ ๋ณด๊ณ ์ทจํฉ)\", 40, \"๋๋ก ์ ์ฝ ์๋ ๋ฐ์(๋ฏธ์
2)\", 5),\n",
" (\"๊ฒฝ๋ก ์ ์ (์ง๋ ์์์
)\", 35, \"์ต๋จยท์ํํํผ ๊ฒฝ๋ก ์๋ ์ถ์ฒ\", 3),\n",
" (\"๋ฐฐ์ฐจยท์ ์ฌ ๊ณํ ์์ฑ\", 25, \"์ ์ฌ ํจ์จ ๊ธฐ๋ฐ ์๋ ๋ฐฐ์ฐจ\", 5),\n",
" (\"์์กํต์ ๊ด ์น์ธ\", 15, \"HITL ์น์ธ ํ๋ฉด(๊ทผ๊ฑฐ ์๋ ์ฒจ๋ถ)\", 10),\n",
" (\"์ดํ ๊ธฐ๋ก ์๊ธฐ ์์ฑ\", 20, \"์ดํ ์ด๋ฒคํธ ์๋ ๊ธฐ๋ก\", 3),\n",
" (\"์ง์ฐยท์ฌ๊ณ ๋ณด๊ณ ์์ฑ\", 25, \"์ง์ฐ ์์ธ ์ฝ๋ ์ ํ์ ๊ธฐ๋ก\", 5),\n",
" (\"์ดํ ์ค์ ์ง๊ณ(์์
๋ณต์ฌยท๋ถ์ฌ๋ฃ๊ธฐ)\", 60, \"์ค์ ์๋ ์ง๊ณ\", 1),\n",
" (\"๋น์ฉ ์ฐ์ถ(์ ๋ฅยท๊ฑฐ๋ฆฌ ๋จ๊ฐ ๊ณ์ฐ)\", 45, \"์์ฐ๋ณ km ๋จ๊ฐ ์๋ ๊ณ์ฐ\", 1),\n",
" (\"์ค์ ยท๋น์ฉ ๋ณด๊ณ ์ ์์ฑ\", 50, \"๋์๋ณด๋ ํ๋ฉด ์บก์ฒ ๋์ ์๋ ์์ฝ\", 5),\n",
" (\"์๊ธ ๋ถ๋ ๋ณด๊ณ ยท์์ ์์ฒญ ๋ฐ์\", 30, \"๋จ์ผ ๋ฐ์ดํฐ ๊ธฐ์ค์ผ๋ก ์์ ์ต์ํ\", 10)]\n",
"proc = pd.DataFrame(steps, columns=[\"์
๋ฌด ๋จ๊ณ\", \"AS-IS(๋ถ)\", \"TO-BE ๊ฐ์ \", \"TO-BE(๋ถ)\"])\n",
"proc.index = range(1, len(proc) + 1)\n",
"thr = proc[\"AS-IS(๋ถ)\"].mean() + 0.5 * proc[\"AS-IS(๋ถ)\"].std()\n",
"proc[\"๋ณ๋ชฉ\"] = np.where(proc[\"AS-IS(๋ถ)\"] >= thr, \"๋ณ๋ชฉ\", \"\")\n",
"print(f\"\\n[AS-IS / TO-BE ์
๋ฌด ๋จ๊ณ {len(proc)}๋จ๊ณ] (๋ณ๋ชฉ ๊ธฐ์ค: ํ๊ท +0.5ํ์คํธ์ฐจ = {thr:.0f}๋ถ ์ด์)\")\n",
"print(proc[[\"์
๋ฌด ๋จ๊ณ\", \"AS-IS(๋ถ)\", \"TO-BE(๋ถ)\", \"๋ณ๋ชฉ\"]].to_string())\n",
"nb = int((proc[\"๋ณ๋ชฉ\"] == \"๋ณ๋ชฉ\").sum())\n",
"asis, tobe = proc[\"AS-IS(๋ถ)\"].sum(), proc[\"TO-BE(๋ถ)\"].sum()\n",
"print(f\"\\n์
๋ฌด ๋จ๊ณ {len(proc)}๋จ๊ณ(๋ชฉํ 10๋จ๊ณ ์ด์: {'๋ฌ์ฑ' if len(proc) >= 10 else '๋ฏธ๋ฌ'}), ๋ณ๋ชฉ {nb}๊ฐ์(๋ชฉํ 3๊ฐ์ ์ด์: {'๋ฌ์ฑ' if nb >= 3 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"1ํ ์ฒ๋ฆฌ ์๊ฐ AS-IS {asis}๋ถ -> TO-BE {tobe}๋ถ ({1 - tobe / asis:.0%} ๋จ์ถ ์์)\")\n",
"print(\"\\n[์คํค๋ง ๊ทธ๋ฃน๋ณ ํ๋ ์]\", {g: len(f) for g, f in SCHEMA.items()})\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"์ ์ ์ํ 157๊ฑด(์์ฐ 12, ๋
ธ๋ 48, ๊ฐ์ 91, ์์ฒญ 6) ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - asset.asset_id: ํ์ ๋ถ์ผ์น 'TRUCK-1'\n",
" - asset.kind: ํ์ฉ๋์ง ์์ ๊ฐ 'tank'\n",
" - edge.v: ์กด์ฌํ์ง ์๋ ๋
ธ๋ N9999\n",
" - edge.km: ํ์ ํ๋ ๋๋ฝ\n",
" - edge.limit_t: ๊ต๋ ํ์ค ์ ํ ๋๋ฝ\n",
" - request.cargo_t: ์ต์๊ฐ 0.1 ๋ฏธ๋ง\n",
"์ฃผ์
์ค๋ฅ 6์ข
์ค 6์ข
๊ฒ์ถ\n",
"\n",
"[AS-IS / TO-BE ์
๋ฌด ๋จ๊ณ 12๋จ๊ณ] (๋ณ๋ชฉ ๊ธฐ์ค: ํ๊ท +0.5ํ์คํธ์ฐจ = 40๋ถ ์ด์)\n",
" ์
๋ฌด ๋จ๊ณ AS-IS(๋ถ) TO-BE(๋ถ) ๋ณ๋ชฉ\n",
"1 ์์ก ์์ฒญ ์ ์(์ ํยท๋ฉ์ ์ ) 20 2 \n",
"2 ๊ฐ์ฉ ์์ฐ ํ์ธ(์ฐจ๋ ์ผ์ง ์๊ธฐ ๋์กฐ) 30 2 \n",
"3 ๋๋ก ์ํฉ ํ์ธ(๊ตฌ๋ ๋ณด๊ณ ์ทจํฉ) 40 5 ๋ณ๋ชฉ\n",
"4 ๊ฒฝ๋ก ์ ์ (์ง๋ ์์์
) 35 3 \n",
"5 ๋ฐฐ์ฐจยท์ ์ฌ ๊ณํ ์์ฑ 25 5 \n",
"6 ์์กํต์ ๊ด ์น์ธ 15 10 \n",
"7 ์ดํ ๊ธฐ๋ก ์๊ธฐ ์์ฑ 20 3 \n",
"8 ์ง์ฐยท์ฌ๊ณ ๋ณด๊ณ ์์ฑ 25 5 \n",
"9 ์ดํ ์ค์ ์ง๊ณ(์์
๋ณต์ฌยท๋ถ์ฌ๋ฃ๊ธฐ) 60 1 ๋ณ๋ชฉ\n",
"10 ๋น์ฉ ์ฐ์ถ(์ ๋ฅยท๊ฑฐ๋ฆฌ ๋จ๊ฐ ๊ณ์ฐ) 45 1 ๋ณ๋ชฉ\n",
"11 ์ค์ ยท๋น์ฉ ๋ณด๊ณ ์ ์์ฑ 50 5 ๋ณ๋ชฉ\n",
"12 ์๊ธ ๋ถ๋ ๋ณด๊ณ ยท์์ ์์ฒญ ๋ฐ์ 30 10 \n",
"\n",
"์
๋ฌด ๋จ๊ณ 12๋จ๊ณ(๋ชฉํ 10๋จ๊ณ ์ด์: ๋ฌ์ฑ), ๋ณ๋ชฉ 4๊ฐ์(๋ชฉํ 3๊ฐ์ ์ด์: ๋ฌ์ฑ)\n",
"1ํ ์ฒ๋ฆฌ ์๊ฐ AS-IS 395๋ถ -> TO-BE 52๋ถ (87% ๋จ์ถ ์์)\n",
"\n",
"[์คํค๋ง ๊ทธ๋ฃน๋ณ ํ๋ ์] {'asset': 6, 'node': 3, 'edge': 7, 'request': 4}\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ๋๋ก ํ๊ดด, ๊ต๋ ๋ถ๊ดด, ํต์ ๊ตฌ์ญ ๋ฑ ์ ์ฅ ์ ์ฝ ์กฐ๊ฑด ๊ทธ๋ํ ์ ์ฒ๋ฆฌ ๋ชจ๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋๋ก ํ๊ดด, ํต์ ๊ตฌ์ญ ๋ฑ ์ ์ฅ ์ ์ฝ ์กฐ๊ฑด ๊ทธ๋ํ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋๋ก ํ๊ดด, ๊ต๋ ๋ถ๊ดดยทํ์ค ์ ํ, ํต์ ๊ตฌ์ญ ๋ณด๊ณ ๋ฅผ ์ฌ๋ฌ ํ์์์ ์ฝ์ด ๋๋ก๋ง ๊ทธ๋ํ์ ๋ฐ์ํ๋ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **CSV, ์์
๋ด๋ณด๋ด๊ธฐ(ํญ ๊ตฌ๋ถ), JSON, ๊ณต๋ฌธ ํ
์คํธ ํ์์ ๊ฐ์ ๋ณด๊ณ ์๋ฅผ ๊ฐ๊ฐ ํ์ฑํด ํ์ค ์ ์ฝ ๋ ์ฝ๋๋ก ๋ฐ๊พผ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/connectors/mov02_data_loader.py ๋ฐ ์คํค๋ง ์ ์์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตญ๋ฐฉ ์์/์์ ๋ณ๊ฒฝ์ ๋ํ ์ ์ฐํ ํ์ฅ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ง์ ํ์ผ ํฌ๋งท(CSV, Excel, JSON ๋ฑ) ํ์ฑ ์ฑ๊ณต๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ฐ์ดํฐ ๋ก๋ ๋ฐ ์คํค๋ง ์ ํจ์ฑ ๊ฒ์ฌ ๋ชจ๋ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ค์ํ ํํ์ ์ํ ๊ตฐ์ ๋ฌธ์ ๋์ ํ์ฑ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋๋ก ํ๊ดดยท๊ต๋ ๋ถ๊ดดยทํ์ค ์ ํยทํต์ ๊ตฌ์ญ ๋ณด๊ณ ๋ฅผ ์ฌ๋ฌ ํ์(CSVยท์์
๋ด๋ณด๋ด๊ธฐยทJSONยท๊ณต๋ฌธ ํ
์คํธ)์ ๊ฐ์ ๋ฌธ์์์ ํ์ฑํด ๋๋ก๋ง ๊ทธ๋ํ์ ๋ฐ์ํ๋ ์ ์ฅ ์ ์ฝ ์กฐ๊ฑด ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๊ณ , ํ์ฑ ์ฑ๊ณต๋ฅ ๊ณผ ๋ฐ์ ํ ์ฐ๊ฒฐ์ฑ์ ํ์ธํ๋ค.\"\"\"\n",
"import heapq\n",
"import csv\n",
"import io\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.sparse import coo_matrix\n",
"from scipy.sparse.csgraph import connected_components\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋๋ก๋งยท์์ก ์์ฐ ์์ฑ๊ธฐ (์ค์ ์ง๋ช
ยท๋ถ๋ยท์ฅ๋น ์ ์ ์์)\n",
"# ๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง(๋
ธ๋=๊ต์ฐจ์ , ๊ฐ์ =๋๋ก ๊ตฌ๊ฐ)์ ๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยท๊ฐ์ ์ํ ๊ตฌ์ญ์ ๋ถ์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ASSETS = { # ๊ต์ก์ฉ ๊ฐ์ ์ ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
" \"truck\": {\"name\": \"์คํ ํธ๋ญ\", \"cap_t\": 5.0, \"speed_kmh\": 50, \"cost_km\": 1.0, \"weight_t\": 10.0},\n",
" \"trailer\": {\"name\": \"๋ํ ํธ๋ ์ผ๋ฌ\", \"cap_t\": 20.0, \"speed_kmh\": 40, \"cost_km\": 2.4, \"weight_t\": 30.0},\n",
" \"airlift\": {\"name\": \"์์ก๊ธฐ\", \"cap_t\": 12.0, \"speed_kmh\": 300, \"cost_km\": 9.0, \"weight_t\": 0.0},\n",
"}\n",
"ROAD_FACTOR = {\"paved\": 1.0, \"unpaved\": 0.6} # ๋๋ก ๋ฑ๊ธ๋ณ ์๋ ๊ณ์\n",
"\n",
"\n",
"def make_network(seed=5, rows=6, cols=8, spacing=5.0):\n",
" \"\"\"๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง. ๋
ธ๋ ์ขํ(km)์ ๊ฐ์ ๋ชฉ๋ก(๊ฑฐ๋ฆฌยท๋๋ก๋ฑ๊ธยท๊ต๋ ํ์ค)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" nodes = {f\"N{r:02d}{c:02d}\": (c * spacing + rng.uniform(-1, 1), r * spacing + rng.uniform(-1, 1))\n",
" for r in range(rows) for c in range(cols)}\n",
" edges = []\n",
"\n",
" def add(a, b):\n",
" (x1, y1), (x2, y2) = nodes[a], nodes[b]\n",
" bridge = bool(rng.random() < 0.15)\n",
" edges.append({\"id\": f\"E{len(edges):03d}\", \"u\": a, \"v\": b,\n",
" \"km\": round(float(np.hypot(x2 - x1, y2 - y1) * rng.uniform(1.0, 1.25)), 2),\n",
" \"road\": \"paved\" if rng.random() < 0.65 else \"unpaved\", \"bridge\": bridge,\n",
" \"limit_t\": float(rng.choice([20, 40, 60])) if bridge else None, \"open\": True})\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if c + 1 < cols:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r:02d}{c + 1:02d}\")\n",
" if r + 1 < rows:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c:02d}\")\n",
" if r + 1 < rows and c + 1 < cols and rng.random() < 0.25:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c + 1:02d}\")\n",
" return nodes, edges\n",
"\n",
"\n",
"def make_threats(seed=9, n=3, xlim=(8, 28), ylim=(3, 22)):\n",
" \"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ(์ํ). p = ๊ตฌ๊ฐ 1ํ ํต๊ณผ ์ ํผํด ๊ฐ๋ฅ์ฑ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" return [{\"id\": f\"TZ{i + 1}\", \"x\": float(rng.uniform(*xlim)), \"y\": float(rng.uniform(*ylim)),\n",
" \"r\": float(rng.uniform(3, 5)), \"p\": float(round(rng.uniform(0.05, 0.2), 3))} for i in range(n)]\n",
"\n",
"\n",
"def edge_risk(e, nodes, threats):\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" s = np.linspace(0, 1, 6)\n",
" risk = 0.0\n",
" for t in threats:\n",
" if np.min(np.hypot(x1 + (x2 - x1) * s - t[\"x\"], y1 + (y2 - y1) * s - t[\"y\"])) < t[\"r\"]:\n",
" risk = max(risk, t[\"p\"])\n",
" return risk\n",
"\n",
"\n",
"def edge_minutes(e, asset=\"truck\"):\n",
" return e[\"km\"] / (ASSETS[asset][\"speed_kmh\"] * ROAD_FACTOR[e[\"road\"]]) * 60\n",
"\n",
"\n",
"def passable(e, gross_t):\n",
" return e[\"open\"] and (not e[\"bridge\"] or e[\"limit_t\"] >= gross_t)\n",
"\n",
"\n",
"def build_adj(nodes, edges, weight_fn):\n",
" \"\"\"weight_fn(e)๊ฐ None์ด๋ฉด ํตํ ๋ถ๊ฐ ๊ฐ์ ์ผ๋ก ๋ณด๊ณ ์ ์ธํ๋ค.\"\"\"\n",
" adj = {n: [] for n in nodes}\n",
" for e in edges:\n",
" w = weight_fn(e)\n",
" if w is not None:\n",
" adj[e[\"u\"]].append((e[\"v\"], w, e))\n",
" adj[e[\"v\"]].append((e[\"u\"], w, e))\n",
" return adj\n",
"\n",
"\n",
"def dijkstra(adj, src, dst):\n",
" \"\"\"๋ค์ต์คํธ๋ผ ์ต๋จ๊ฒฝ๋ก. (๋น์ฉ, ๋
ธ๋ ๊ฒฝ๋ก, ๊ฐ์ ๋ชฉ๋ก). ๋๋ฌ ๋ถ๊ฐ๋ฉด (inf, [], []).\"\"\"\n",
" dist, prev, done, pq = {src: 0.0}, {}, set(), [(0.0, src)]\n",
" while pq:\n",
" d, u = heapq.heappop(pq)\n",
" if u in done:\n",
" continue\n",
" done.add(u)\n",
" if u == dst:\n",
" break\n",
" for v, w, e in adj[u]:\n",
" if d + w < dist.get(v, float(\"inf\")):\n",
" dist[v], prev[v] = d + w, (u, e)\n",
" heapq.heappush(pq, (d + w, v))\n",
" if dst not in done:\n",
" return float(\"inf\"), [], []\n",
" path, es = [dst], []\n",
" while path[-1] != src:\n",
" u, e = prev[path[-1]]\n",
" es.append(e); path.append(u)\n",
" return dist[dst], path[::-1], es[::-1]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"nodes, edges = make_network()\n",
"E = {e[\"id\"]: e for e in edges}\n",
"DEPOT, UNITS = \"N0000\", [f\"N{r:02d}07\" for r in range(6)]\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ฉ๋ชจ๋ฆฌ ์์ ๊ฐ์ ๋ณด๊ณ ๋ฌธ์ 4์ข
(ํ์ผ ๋์ ๋ฌธ์์ด๋ก ๋ชจ์)\n",
"CSV_DOC = \"\"\"edge_id,status,reason\n",
"E017,ํ๊ดด,ํฌ๊ฒฉ์ผ๋ก ๋
ธ๋ฉด ์ ์ค\n",
"E040,ํ๊ดด,์ง๋ขฐ ์์ฌ ์ ๊ณ \n",
"E058,์ ์,๋ณต๊ตฌ ์๋ฃ\n",
"E071,ํ๊ดด,์ฐจ๋ ์ํด ์ ์น\"\"\"\n",
"EXCEL_DOC = \"๊ตฌ๊ฐID\\t๊ต๋ํ์ค(t)\\t๋น๊ณ \\nE025\\t15\\t๊ต๊ฐ ์์\\nE064\\t20\\t์์ ๊ฐ์ค ๊ต๋\" # ์์
๋ด๋ณด๋ด๊ธฐ(ํญ ๊ตฌ๋ถ, ํ๊ธ ๋จธ๋ฆฌ๊ธ)\n",
"JSON_DOC = json.dumps({\"control_zones\": [{\"zone\": \"CZ-1\", \"x_km\": 17.5, \"y_km\": 12.0, \"r_km\": 2.5, \"reason\": \"ํ์๋ฐฉ ์ค์ผ ์์ฌ\"},\n",
" {\"zone\": \"CZ-2\", \"x_km\": 30.0, \"y_km\": 21.0, \"r_km\": 1.8, \"reason\": \"๋ฏผ๊ฐ์ธ ๋ํผ ํต๋ก\"}]}, ensure_ascii=False)\n",
"MEMO_DOC = \"\"\"[๊ฐ์ ๊ณต๋ฌธ] ์ ๋ชฉ: ๋๋ก ์ํฉ ๋ณ๊ฒฝ ํต๋ณด\n",
"1. ๊ต๋ E046 ๋ถ๊ดด๋ก ํตํ ๋ถ๊ฐ.\n",
"2. E083 ๊ตฌ๊ฐ ๊ต๋ ํ์ค ์ ํ 20ํค์ผ๋ก ํํฅ.\n",
"3. E999 ๊ตฌ๊ฐ ํ๊ดด ๋ณด๊ณ (๊ตฌ๊ฐ ๋ฒํธ ํ์ธ ํ์).\"\"\"\n",
"\n",
"ALIAS = {\"edge_id\": [\"edge_id\", \"๊ตฌ๊ฐID\", \"๊ตฌ๊ฐ\"], \"limit_t\": [\"limit_t\", \"๊ต๋ํ์ค(t)\", \"ํ์ค\"], \"status\": [\"status\", \"์ํ\"]}\n",
"\n",
"\n",
"def std_cols(df):\n",
" \"\"\"์์์ด ๋ฐ๋์ด๋(๋จธ๋ฆฌ๊ธ ๋ณ๊ฒฝ) ๋ณ์นญ ์ฌ์ ์ผ๋ก ํ์ค ์ปฌ๋ผ๋ช
์ ๋ง์ถ๋ค.\"\"\"\n",
" ren = {c: k for k, al in ALIAS.items() for c in df.columns if c.strip() in al}\n",
" return df.rename(columns=ren)\n",
"\n",
"\n",
"def parse(fmt, text):\n",
" \"\"\"ํ์๋ณ ํ์. ๊ฒฐ๊ณผ๋ ํ์ค ์ ์ฝ ๋ ์ฝ๋ ๋ชฉ๋ก {type, edge_id | zone, value, reason}.\"\"\"\n",
" if fmt == \"csv\":\n",
" df = std_cols(pd.DataFrame(list(csv.DictReader(io.StringIO(text)))))\n",
" return [{\"type\": \"close\" if s == \"ํ๊ดด\" else \"reopen\", \"edge_id\": e, \"reason\": r} for e, s, r in zip(df.edge_id, df.status, df.reason)]\n",
" if fmt == \"excel\":\n",
" df = std_cols(pd.DataFrame(list(csv.DictReader(io.StringIO(text), delimiter=\"\\t\"))))\n",
" return [{\"type\": \"limit\", \"edge_id\": e, \"value\": float(v), \"reason\": r} for e, v, r in zip(df.edge_id, df.limit_t, df[\"๋น๊ณ \"])]\n",
" if fmt == \"json\":\n",
" return [{\"type\": \"zone\", \"zone\": z[\"zone\"], \"value\": (z[\"x_km\"], z[\"y_km\"], z[\"r_km\"]), \"reason\": z[\"reason\"]}\n",
" for z in json.loads(text)[\"control_zones\"]]\n",
" if fmt == \"memo\":\n",
" out = []\n",
" for line in text.splitlines():\n",
" m = re.search(r\"(E\\d{3}).*?(๋ถ๊ดด|ํ๊ดด|ํตํ ๋ถ๊ฐ)\", line)\n",
" if m:\n",
" out.append({\"type\": \"close\", \"edge_id\": m.group(1), \"reason\": line.strip()}); continue\n",
" m = re.search(r\"(E\\d{3}).*?ํ์ค ์ ํ (\\d+)\\s*ํค\", line)\n",
" if m:\n",
" out.append({\"type\": \"limit\", \"edge_id\": m.group(1), \"value\": float(m.group(2)), \"reason\": line.strip()})\n",
" return out\n",
" raise ValueError(f\"์ง์ํ์ง ์๋ ํ์: {fmt}\")\n",
"\n",
"\n",
"def check_rec(r):\n",
" \"\"\"์คํค๋ง ์ ํจ์ฑ ๊ฒ์ฌ: ๊ตฌ๊ฐID ํ์ยท์กด์ฌ ์ฌ๋ถ, ํ์ค 1~100t, ํต์ ๊ตฌ์ญ ๋ฐ๊ฒฝ 0.1~10km.\"\"\"\n",
" if r[\"type\"] == \"zone\":\n",
" return [] if 0.1 <= r[\"value\"][2] <= 10 else [f\"{r['zone']} ๋ฐ๊ฒฝ ๋ฒ์ ์ค๋ฅ\"]\n",
" errs = [] if re.match(r\"^E\\d{3}$\", r[\"edge_id\"]) else [f\"{r['edge_id']} ํ์ ์ค๋ฅ\"]\n",
" if r[\"edge_id\"] not in E:\n",
" errs.append(f\"{r['edge_id']} ์กด์ฌํ์ง ์๋ ๊ตฌ๊ฐ\")\n",
" if r[\"type\"] == \"limit\" and not 1 <= r[\"value\"] <= 100:\n",
" errs.append(f\"{r['edge_id']} ํ์ค ๋ฒ์ ์ค๋ฅ\")\n",
" return errs\n",
"\n",
"\n",
"def apply(edges, recs, log):\n",
" \"\"\"์ ์ฝ ๋ ์ฝ๋๋ฅผ ๊ทธ๋ํ์ ๋ฐ์. ์๋ ๊ตฌ๊ฐ ID๋ ๋ฐ์ํ์ง ์๊ณ ๋ก๊ทธ์ ๋จ๊ธด๋ค.\"\"\"\n",
" new = [dict(e) for e in edges]\n",
" idx = {e[\"id\"]: e for e in new}\n",
" for r in recs:\n",
" if r[\"type\"] == \"zone\":\n",
" x, y, rad = r[\"value\"]\n",
" for e in new:\n",
" if edge_risk(e, nodes, [{\"x\": x, \"y\": y, \"r\": rad, \"p\": 1.0}]) > 0:\n",
" e[\"open\"] = False; e[\"zone\"] = r[\"zone\"]\n",
" log.append(f\"{r['zone']} ํต์ ๊ตฌ์ญ ๋ฐ์\")\n",
" elif r[\"edge_id\"] not in idx:\n",
" log.append(f\"{r['edge_id']} ์๋ ๊ตฌ๊ฐ ID - ๋ฐ์ ๋ณด๋ฅ(ํ์ธ ์์ฒญ)\")\n",
" elif r[\"type\"] == \"close\":\n",
" idx[r[\"edge_id\"]][\"open\"] = False\n",
" elif r[\"type\"] == \"reopen\":\n",
" idx[r[\"edge_id\"]][\"open\"] = True\n",
" elif r[\"type\"] == \"limit\":\n",
" idx[r[\"edge_id\"]].update(bridge=True, limit_t=r[\"value\"])\n",
" return new\n",
"\n",
"\n",
"docs = [(\"csv\", CSV_DOC), (\"excel\", EXCEL_DOC), (\"json\", JSON_DOC), (\"memo\", MEMO_DOC)]\n",
"recs, rows = [], []\n",
"for fmt, text in docs:\n",
" try:\n",
" r = parse(fmt, text); recs += r; rows.append({\"ํ์\": fmt, \"ํ์ฑ\": \"์ฑ๊ณต\", \"๋ ์ฝ๋\": len(r)})\n",
" except Exception as ex:\n",
" rows.append({\"ํ์\": fmt, \"ํ์ฑ\": f\"์คํจ({ex})\", \"๋ ์ฝ๋\": 0})\n",
"res = pd.DataFrame(rows)\n",
"rate = (res[\"ํ์ฑ\"] == \"์ฑ๊ณต\").mean()\n",
"print(res.to_string(index=False))\n",
"print(f\"ํ์ฑ ์ฑ๊ณต๋ฅ {rate:.0%} (๋ชฉํ 100%: {'๋ฌ์ฑ' if rate == 1 else '๋ฏธ๋ฌ'}), ์ ์ฝ ๋ ์ฝ๋ {len(recs)}๊ฑด\")\n",
"\n",
"log = []\n",
"after = apply(edges, recs, log)\n",
"print(\"\\n[๋ฐ์ ๋ก๊ทธ]\", \" / \".join(log))\n",
"print(f\"ํตํ ๋ถ๊ฐ ๊ตฌ๊ฐ {sum(not e['open'] for e in after)}๊ฐ, ํ์ค ์ ํ ๊ต๋ {sum(e['bridge'] for e in after)}๊ฐ (์๋ณธ {sum(e['bridge'] for e in edges)}๊ฐ)\")\n",
"\n",
"# 2) ๋ฐ์ ์ ํ ์ฐ๊ฒฐ์ฑยท๋๋ฌ์ฑ ์ ๊ฒ (ํธ๋ญ ์ด์ค๋ 15t, ํธ๋ ์ผ๋ฌ ์ด์ค๋ 50t ๊ฐ์ )\n",
"ids = list(nodes)\n",
"pos = {n: i for i, n in enumerate(ids)}\n",
"\n",
"\n",
"def n_components(es, gross):\n",
" ok = [e for e in es if passable(e, gross)]\n",
" g = coo_matrix((np.ones(len(ok)), ([pos[e[\"u\"]] for e in ok], [pos[e[\"v\"]] for e in ok])), shape=(len(ids), len(ids)))\n",
" return connected_components(g, directed=False)[0]\n",
"\n",
"\n",
"print(\"\\n์ฐจ์ข
๋ฐ์ ์ ์ฐ๊ฒฐ ์์ ๋ฐ์ ํ ์ฐ๊ฒฐ ์์ ๋๋ฌ ๊ฐ๋ฅ ๋ถ๋(ํ)\")\n",
"for kind, gross in [(\"truck\", 15), (\"trailer\", 50)]:\n",
" adj = build_adj(nodes, after, lambda e: edge_minutes(e, kind) if passable(e, gross) else None)\n",
" reach = sum(dijkstra(adj, DEPOT, u)[0] < float(\"inf\") for u in UNITS)\n",
" print(f\"{kind:8s} {n_components(edges, gross):16d} {n_components(after, gross):16d} {reach}/{len(UNITS)}\")\n",
"\n",
"# 3) ์์ ๋ณ๊ฒฝ ๋์ ์ํ: ๋จธ๋ฆฌ๊ธ์ด ๋ฐ๋ CSV๋ ๋ณ์นญ ์ฌ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋์ง ํ์ธ\n",
"changed = \"๊ตฌ๊ฐ,์ํ,reason\\nE017,ํ๊ดด,๋จธ๋ฆฌ๊ธ ๋ณ๊ฒฝ ์์\"\n",
"print(\"\\n๋จธ๋ฆฌ๊ธ ๋ณ๊ฒฝ ์์ ํ์ฑ:\", parse(\"csv\", changed))\n",
"errs = [x for r in recs for x in check_rec(r)]\n",
"print(f\"์คํค๋ง ๊ฒ์ฌ: ๋ ์ฝ๋ {len(recs)}๊ฑด ์ค ์ค๋ฅ {len(errs)}๊ฑด {errs} -> ํด๋น ๊ฑด์ ๋ฐ์ ๋ณด๋ฅ ํ ํ์ธ ์์ฒญ\")\n",
"\n",
"# 4) ๋ฐ์ ์ ํ ๊ทธ๋ํ ๊ทธ๋ฆผ\n",
"fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n",
"for a, es, title in [(ax[0], edges, \"Road network (before)\"), (ax[1], after, \"After battlefield constraints\")]:\n",
" for e in es:\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" col = \"red\" if not e[\"open\"] else (\"orange\" if e[\"bridge\"] else (\"tab:blue\" if e[\"road\"] == \"paved\" else \"tab:gray\"))\n",
" a.plot([x1, x2], [y1, y2], color=col, lw=2.5 if not e[\"open\"] else 1.5, ls=\"--\" if not e[\"open\"] else \"-\")\n",
" a.plot(*nodes[DEPOT], \"ks\", ms=10)\n",
" for u in UNITS:\n",
" a.plot(*nodes[u], \"g^\", ms=9)\n",
" a.set_title(title); a.set_xlabel(\"x (km)\"); a.set_ylabel(\"y (km)\"); a.set_aspect(\"equal\")\n",
"for r in recs:\n",
" if r[\"type\"] == \"zone\":\n",
" ax[1].add_patch(plt.Circle(r[\"value\"][:2], r[\"value\"][2], color=\"purple\", alpha=0.2))\n",
"ax[1].text(0, -4, \"red dashed = closed, orange = bridge (weight limit), purple = control zone\", fontsize=8)\n",
"plt.suptitle(\"MOV02 constraint preprocessing (synthetic network)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ํ์ ํ์ฑ ๋ ์ฝ๋\n",
" csv ์ฑ๊ณต 4\n",
"excel ์ฑ๊ณต 2\n",
" json ์ฑ๊ณต 2\n",
" memo ์ฑ๊ณต 3\n",
"ํ์ฑ ์ฑ๊ณต๋ฅ 100% (๋ชฉํ 100%: ๋ฌ์ฑ), ์ ์ฝ ๋ ์ฝ๋ 11๊ฑด\n",
"\n",
"[๋ฐ์ ๋ก๊ทธ] CZ-1 ํต์ ๊ตฌ์ญ ๋ฐ์ / CZ-2 ํต์ ๊ตฌ์ญ ๋ฐ์ / E999 ์๋ ๊ตฌ๊ฐ ID - ๋ฐ์ ๋ณด๋ฅ(ํ์ธ ์์ฒญ)\n",
"ํตํ ๋ถ๊ฐ ๊ตฌ๊ฐ 10๊ฐ, ํ์ค ์ ํ ๊ต๋ 17๊ฐ (์๋ณธ 14๊ฐ)\n",
"\n",
"์ฐจ์ข
๋ฐ์ ์ ์ฐ๊ฒฐ ์์ ๋ฐ์ ํ ์ฐ๊ฒฐ ์์ ๋๋ฌ ๊ฐ๋ฅ ๋ถ๋(ํ)\n",
"truck 1 2 6/6\n",
"trailer 1 2 6/6\n",
"\n",
"๋จธ๋ฆฌ๊ธ ๋ณ๊ฒฝ ์์ ํ์ฑ: [{'type': 'close', 'edge_id': 'E017', 'reason': '๋จธ๋ฆฌ๊ธ ๋ณ๊ฒฝ ์์'}]\n",
"์คํค๋ง ๊ฒ์ฌ: ๋ ์ฝ๋ 11๊ฑด ์ค ์ค๋ฅ 1๊ฑด ['E999 ์กด์ฌํ์ง ์๋ ๊ตฌ๊ฐ'] -> ํด๋น ๊ฑด์ ๋ฐ์ ๋ณด๋ฅ ํ ํ์ธ ์์ฒญ\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ๋ค์ต์คํธ๋ผ ๊ธฐ๋ฐ ์ต๋จ๊ฑฐ๋ฆฌ ๋ณด๊ธ ๊ฒฝ๋ก ํ์ 1์ฐจ Baseline ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ค์ต์คํธ๋ผ ๊ธฐ๋ฐ ์ต๋จ๊ฑฐ๋ฆฌ ๋ณด๊ธ ๊ฒฝ๋ก ํ์ Baseline ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ค์ต์คํธ๋ผ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ๋ณด๊ธ์์์ ์ง์ ๋ถ๋๊น์ง ์ต๋จ์๊ฐ ๊ฒฝ๋ก๋ฅผ ์ฐพ๋ Baseline๊ณผ ๊ฒฝ๋ก ๊ฒ์ฆ ๊ท์น ์์ง์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๊ฐ์ค์น๋ฅผ ์ฐจ์ข
๋ณ ์์์๊ฐ์ผ๋ก ๋๊ณ ํตํ ๋ถ๊ฐยทํ์ค ์ด๊ณผ ๊ตฌ๊ฐ์ ์ ์ธํด ์ต๋จ ๊ฒฝ๋ก๋ฅผ ๊ตฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/engine/mov02_rule_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ๊ท์ ๋ฐ ์ง์นจ ์กฐํญ๊ณผ์ ์ ํํ ์ผ์น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์
๋ฌด ๊ฒ์ฆ ๋ฃฐ 15์ข
์ด์ ์ฝ๋ฉ, ๋ฃฐ ์ฒ๋ฆฌ ์ง์ฐ์๊ฐ 50ms ์ดํ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ฃฐ ์์ง ๋จ์ ํ
์คํธ 100% ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ฒฝ๊ณ ์กฐ๊ฑด(Boundary condition)์ ๋ํ ๋ฃฐ ํ์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ค์ต์คํธ๋ผ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ๋ณด๊ธ์์์ ์ง์ ๋ถ๋๊น์ง ์ต๋จ์๊ฐ ๋ณด๊ธ ๊ฒฝ๋ก๋ฅผ ์ฐพ๋ Baseline์ ๊ตฌํํ๊ณ , ์ค์ ๋ฌธ์์ด๋ก ๋ถ๋ฆฌํ ๊ฒฝ๋ก ๊ฒ์ฆ ๊ท์น 15์ข
๊ณผ ๊ฒฝ๊ณ ์กฐ๊ฑด ๋จ์ ํ
์คํธ๋ก ๊ฒฐ๊ณผ๋ฅผ ํ์ ํ๋ค.\"\"\"\n",
"import heapq\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.sparse import csr_matrix\n",
"from scipy.sparse.csgraph import dijkstra as sp_dijkstra\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋๋ก๋งยท์์ก ์์ฐ ์์ฑ๊ธฐ (์ค์ ์ง๋ช
ยท๋ถ๋ยท์ฅ๋น ์ ์ ์์)\n",
"# ๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง(๋
ธ๋=๊ต์ฐจ์ , ๊ฐ์ =๋๋ก ๊ตฌ๊ฐ)์ ๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยท๊ฐ์ ์ํ ๊ตฌ์ญ์ ๋ถ์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ASSETS = { # ๊ต์ก์ฉ ๊ฐ์ ์ ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
" \"truck\": {\"name\": \"์คํ ํธ๋ญ\", \"cap_t\": 5.0, \"speed_kmh\": 50, \"cost_km\": 1.0, \"weight_t\": 10.0},\n",
" \"trailer\": {\"name\": \"๋ํ ํธ๋ ์ผ๋ฌ\", \"cap_t\": 20.0, \"speed_kmh\": 40, \"cost_km\": 2.4, \"weight_t\": 30.0},\n",
" \"airlift\": {\"name\": \"์์ก๊ธฐ\", \"cap_t\": 12.0, \"speed_kmh\": 300, \"cost_km\": 9.0, \"weight_t\": 0.0},\n",
"}\n",
"ROAD_FACTOR = {\"paved\": 1.0, \"unpaved\": 0.6} # ๋๋ก ๋ฑ๊ธ๋ณ ์๋ ๊ณ์\n",
"\n",
"\n",
"def make_network(seed=5, rows=6, cols=8, spacing=5.0):\n",
" \"\"\"๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง. ๋
ธ๋ ์ขํ(km)์ ๊ฐ์ ๋ชฉ๋ก(๊ฑฐ๋ฆฌยท๋๋ก๋ฑ๊ธยท๊ต๋ ํ์ค)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" nodes = {f\"N{r:02d}{c:02d}\": (c * spacing + rng.uniform(-1, 1), r * spacing + rng.uniform(-1, 1))\n",
" for r in range(rows) for c in range(cols)}\n",
" edges = []\n",
"\n",
" def add(a, b):\n",
" (x1, y1), (x2, y2) = nodes[a], nodes[b]\n",
" bridge = bool(rng.random() < 0.15)\n",
" edges.append({\"id\": f\"E{len(edges):03d}\", \"u\": a, \"v\": b,\n",
" \"km\": round(float(np.hypot(x2 - x1, y2 - y1) * rng.uniform(1.0, 1.25)), 2),\n",
" \"road\": \"paved\" if rng.random() < 0.65 else \"unpaved\", \"bridge\": bridge,\n",
" \"limit_t\": float(rng.choice([20, 40, 60])) if bridge else None, \"open\": True})\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if c + 1 < cols:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r:02d}{c + 1:02d}\")\n",
" if r + 1 < rows:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c:02d}\")\n",
" if r + 1 < rows and c + 1 < cols and rng.random() < 0.25:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c + 1:02d}\")\n",
" return nodes, edges\n",
"\n",
"\n",
"def make_threats(seed=9, n=3, xlim=(8, 28), ylim=(3, 22)):\n",
" \"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ(์ํ). p = ๊ตฌ๊ฐ 1ํ ํต๊ณผ ์ ํผํด ๊ฐ๋ฅ์ฑ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" return [{\"id\": f\"TZ{i + 1}\", \"x\": float(rng.uniform(*xlim)), \"y\": float(rng.uniform(*ylim)),\n",
" \"r\": float(rng.uniform(3, 5)), \"p\": float(round(rng.uniform(0.05, 0.2), 3))} for i in range(n)]\n",
"\n",
"\n",
"def edge_risk(e, nodes, threats):\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" s = np.linspace(0, 1, 6)\n",
" risk = 0.0\n",
" for t in threats:\n",
" if np.min(np.hypot(x1 + (x2 - x1) * s - t[\"x\"], y1 + (y2 - y1) * s - t[\"y\"])) < t[\"r\"]:\n",
" risk = max(risk, t[\"p\"])\n",
" return risk\n",
"\n",
"\n",
"def edge_minutes(e, asset=\"truck\"):\n",
" return e[\"km\"] / (ASSETS[asset][\"speed_kmh\"] * ROAD_FACTOR[e[\"road\"]]) * 60\n",
"\n",
"\n",
"def passable(e, gross_t):\n",
" return e[\"open\"] and (not e[\"bridge\"] or e[\"limit_t\"] >= gross_t)\n",
"\n",
"\n",
"def build_adj(nodes, edges, weight_fn):\n",
" \"\"\"weight_fn(e)๊ฐ None์ด๋ฉด ํตํ ๋ถ๊ฐ ๊ฐ์ ์ผ๋ก ๋ณด๊ณ ์ ์ธํ๋ค.\"\"\"\n",
" adj = {n: [] for n in nodes}\n",
" for e in edges:\n",
" w = weight_fn(e)\n",
" if w is not None:\n",
" adj[e[\"u\"]].append((e[\"v\"], w, e))\n",
" adj[e[\"v\"]].append((e[\"u\"], w, e))\n",
" return adj\n",
"\n",
"\n",
"def dijkstra(adj, src, dst):\n",
" \"\"\"๋ค์ต์คํธ๋ผ ์ต๋จ๊ฒฝ๋ก. (๋น์ฉ, ๋
ธ๋ ๊ฒฝ๋ก, ๊ฐ์ ๋ชฉ๋ก). ๋๋ฌ ๋ถ๊ฐ๋ฉด (inf, [], []).\"\"\"\n",
" dist, prev, done, pq = {src: 0.0}, {}, set(), [(0.0, src)]\n",
" while pq:\n",
" d, u = heapq.heappop(pq)\n",
" if u in done:\n",
" continue\n",
" done.add(u)\n",
" if u == dst:\n",
" break\n",
" for v, w, e in adj[u]:\n",
" if d + w < dist.get(v, float(\"inf\")):\n",
" dist[v], prev[v] = d + w, (u, e)\n",
" heapq.heappush(pq, (d + w, v))\n",
" if dst not in done:\n",
" return float(\"inf\"), [], []\n",
" path, es = [dst], []\n",
" while path[-1] != src:\n",
" u, e = prev[path[-1]]\n",
" es.append(e); path.append(u)\n",
" return dist[dst], path[::-1], es[::-1]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ๊ท์ ยท๊ธฐ์ค์ด ๋ฐ๋๋ฉด ์ฝ๋ ๋์ ์ด ์ค์ ๋ง ๊ณ ์น๋ค(YAML ํ์ ๋ฌธ์์ด)\n",
"RULES_YAML = \"\"\"\n",
"max_unpaved_ratio: 0.5\n",
"detour_factor: 2.5\n",
"max_threat_edges: 1\n",
"truck_count: 3\n",
"\"\"\"\n",
"CFG = {k.strip(): float(v) for k, v in (line.split(\":\") for line in RULES_YAML.strip().splitlines())}\n",
"\n",
"nodes, edges = make_network()\n",
"threats = make_threats()\n",
"for e in edges: # ๋ฏธ์
2 ์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ(ํ์ยทํ์ค ์ ํยทํต์ ๊ตฌ์ญ) ์์ฝ ๋ฐ์\n",
" if e[\"id\"] in (\"E017\", \"E040\", \"E046\", \"E071\"):\n",
" e[\"open\"] = False\n",
" if e[\"id\"] in (\"E025\", \"E064\", \"E083\"):\n",
" e.update(bridge=True, limit_t={\"E025\": 15.0, \"E064\": 20.0, \"E083\": 20.0}[e[\"id\"]])\n",
" if edge_risk(e, nodes, [{\"x\": 17.5, \"y\": 12.0, \"r\": 2.5, \"p\": 1}]) > 0:\n",
" e[\"open\"], e[\"zone\"] = False, \"CZ-1\"\n",
" e[\"risk\"] = edge_risk(e, nodes, threats)\n",
"DEPOT = \"N0000\"\n",
"ASSET = {\"kind\": \"truck\", \"status\": \"๊ฐ์ฉ\", **ASSETS[\"truck\"]}\n",
"\n",
"\n",
"def route(dest, cargo_t, kind=\"truck\"):\n",
" gross = ASSETS[kind][\"weight_t\"] + min(cargo_t, ASSETS[kind][\"cap_t\"])\n",
" adj = build_adj(nodes, edges, lambda e: edge_minutes(e, kind) if passable(e, gross) else None)\n",
" return dijkstra(adj, DEPOT, dest)\n",
"\n",
"\n",
"def rules(req, cost, path, es, asset=ASSET):\n",
" \"\"\"๊ฒฝ๋ก ๊ฒ์ฆ ๊ท์น 15์ข
. ๊ฒฐ๊ณผ: ๊ท์น๋ช
-> PASS / FAIL / WARN.\"\"\"\n",
" gross = asset[\"weight_t\"] + asset[\"cap_t\"]\n",
" km = sum(e[\"km\"] for e in es)\n",
" straight = np.hypot(*np.subtract(nodes[req[\"dest\"]], nodes[DEPOT]))\n",
" pairs = {frozenset((e[\"u\"], e[\"v\"])) for e in edges}\n",
" R = {\n",
" \"R01 ์ถ๋ฐ์ง=๋ณด๊ธ์\": path[:1] == [DEPOT],\n",
" \"R02 ๋์ฐฉ์ง=์์ฒญ ๋ถ๋\": path[-1:] == [req[\"dest\"]],\n",
" \"R03 ์ฐ์ ๋
ธ๋ ๊ฐ ๋๋ก ์กด์ฌ\": all(frozenset(p) in pairs for p in zip(path, path[1:])),\n",
" \"R04 ํ์ ๊ตฌ๊ฐ ๋ฏธํฌํจ\": all(e[\"open\"] for e in es),\n",
" \"R05 ๊ต๋ ํ์ค ์ด๋ด\": all(not e[\"bridge\"] or e[\"limit_t\"] >= gross for e in es),\n",
" \"R06 ํต์ ๊ตฌ์ญ ๋ฏธํต๊ณผ\": all(\"zone\" not in e for e in es),\n",
" \"R07 ๋
ธ๋ ์ค๋ณต ๋ฐฉ๋ฌธ ์์\": len(path) == len(set(path)),\n",
" \"R08 ๊ฑฐ๋ฆฌ ํฉ๊ณ ์ ํฉ\": abs(km - sum(e[\"km\"] for e in es)) < 1e-9 and len(es) == len(path) - 1,\n",
" \"R09 ์์์๊ฐ ๊ธฐํ ์ด๋ด\": cost <= req[\"deadline_h\"] * 60,\n",
" \"R10 ์ ์ฌ๋ ์์ก๋ฅ๋ ฅ ์ด๋ด\": req[\"cargo_t\"] <= asset[\"cap_t\"] * CFG[\"truck_count\"],\n",
" \"R11 ์์ฐ ์ํ ๊ฐ์ฉ\": asset[\"status\"] == \"๊ฐ์ฉ\",\n",
" \"R12 ๋นํฌ์ฅ ๋น์จ ๊ธฐ์ค ์ด๋ด\": sum(e[\"km\"] for e in es if e[\"road\"] == \"unpaved\") <= CFG[\"max_unpaved_ratio\"] * max(km, 1e-9),\n",
" \"R13 ์ฐํ ๊ณผ๋ค ์์\": km <= CFG[\"detour_factor\"] * straight,\n",
" \"R14 ๊ฒฝ๋ก ๊ตฌ๊ฐ 1๊ฐ ์ด์\": len(es) >= 1,\n",
" \"R15 ์ํ ๊ตฌ์ญ ํต๊ณผ ๊ธฐ์ค ์ด๋ด\": sum(e[\"risk\"] > 0 for e in es) <= CFG[\"max_threat_edges\"],\n",
" }\n",
" out = {k: \"PASS\" if v else \"FAIL\" for k, v in R.items()}\n",
" if out[\"R15 ์ํ ๊ตฌ์ญ ํต๊ณผ ๊ธฐ์ค ์ด๋ด\"] == \"FAIL\":\n",
" out[\"R15 ์ํ ๊ตฌ์ญ ํต๊ณผ ๊ธฐ์ค ์ด๋ด\"] = \"WARN\" # Baseline์ ์ํ์ ๊ณ ๋ คํ์ง ์์ผ๋ฏ๋ก ๊ฒฝ๊ณ ๋ง ํ์\n",
" return out\n",
"\n",
"\n",
"rng = np.random.default_rng(2)\n",
"reqs = [{\"req\": f\"REQ-{i:04d}\", \"dest\": f\"N{r:02d}07\", \"cargo_t\": float(round(rng.uniform(4, 14), 1)), \"deadline_h\": 1.5} for i, r in enumerate(range(6))]\n",
"rows, lat, all_res = [], [], []\n",
"for q in reqs:\n",
" cost, path, es = route(q[\"dest\"], q[\"cargo_t\"])\n",
" t0 = time.perf_counter()\n",
" res = rules(q, cost, path, es)\n",
" lat.append((time.perf_counter() - t0) * 1000)\n",
" all_res.append(res)\n",
" rows.append({\"์์ฒญ\": q[\"req\"], \"๋ชฉ์ ์ง\": q[\"dest\"], \"๊ฑฐ๋ฆฌ(km)\": round(sum(e[\"km\"] for e in es), 1), \"์์(๋ถ)\": round(cost, 1),\n",
" \"๊ตฌ๊ฐ ์\": len(es), \"์ํ ํต๊ณผ ๊ตฌ๊ฐ\": sum(e[\"risk\"] > 0 for e in es),\n",
" \"PASS\": sum(v == \"PASS\" for v in res.values()), \"WARN\": sum(v == \"WARN\" for v in res.values()), \"FAIL\": sum(v == \"FAIL\" for v in res.values())})\n",
"print(\"[Baseline ์ต๋จ์๊ฐ ๊ฒฝ๋ก ๊ฒฐ๊ณผ - ์คํ ํธ๋ญ]\")\n",
"print(pd.DataFrame(rows).to_string(index=False))\n",
"print(f\"\\n๊ฒ์ฆ ๊ท์น {len(all_res[0])}์ข
(๋ชฉํ 15์ข
์ด์), ๊ท์น ์ฒ๋ฆฌ ํ๊ท {np.mean(lat):.2f} ms / ์ต๋ {np.max(lat):.2f} ms (๋ชฉํ 50 ms ์ดํ: {'๋ฌ์ฑ' if max(lat) <= 50 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 1) ์ต์ ์ฑ ๊ฒ์ฆ: scipy ๋ค์ต์คํธ๋ผ์ ๋น์ฉ ๋น๊ต\n",
"ids = list(nodes); ix = {n: i for i, n in enumerate(ids)}\n",
"ok_e = [e for e in edges if passable(e, 10 + 5)]\n",
"w = [edge_minutes(e) for e in ok_e]\n",
"M = csr_matrix((w + w, ([ix[e[\"u\"]] for e in ok_e] + [ix[e[\"v\"]] for e in ok_e], [ix[e[\"v\"]] for e in ok_e] + [ix[e[\"u\"]] for e in ok_e])), shape=(len(ids),) * 2)\n",
"ref = sp_dijkstra(M, indices=ix[DEPOT])\n",
"same = sum(abs(ref[ix[q[\"dest\"]]] - route(q[\"dest\"], 5.0)[0]) < 1e-6 for q in reqs)\n",
"print(f\"์ต์ ์ฑ ๊ฒ์ฆ(scipy ๊ฒฐ๊ณผ์ ์ผ์น): {same}/{len(reqs)}\")\n",
"\n",
"# 2) ๊ฒฝ๊ณ ์กฐ๊ฑด ๋จ์ ํ
์คํธ\n",
"cost, path, es = route(\"N0507\", 5.0)\n",
"tests = {\n",
" \"์ ์ฌ๋=์์ก๋ฅ๋ ฅ ํ๊ณ(15t)\": rules({\"dest\": \"N0507\", \"cargo_t\": 15.0, \"deadline_h\": 9}, cost, path, es)[\"R10 ์ ์ฌ๋ ์์ก๋ฅ๋ ฅ ์ด๋ด\"] == \"PASS\",\n",
" \"์ ์ฌ๋ ํ๊ณ+0.1t\": rules({\"dest\": \"N0507\", \"cargo_t\": 15.1, \"deadline_h\": 9}, cost, path, es)[\"R10 ์ ์ฌ๋ ์์ก๋ฅ๋ ฅ ์ด๋ด\"] == \"FAIL\",\n",
" \"์์์๊ฐ=๊ธฐํ ์ ํํ ์ผ์น\": rules({\"dest\": \"N0507\", \"cargo_t\": 5, \"deadline_h\": cost / 60}, cost, path, es)[\"R09 ์์์๊ฐ ๊ธฐํ ์ด๋ด\"] == \"PASS\",\n",
" \"๋๋ฌ ๋ถ๊ฐ ๋ชฉ์ ์ง ์ฒ๋ฆฌ(๊ณ ๋ฆฝ ๋
ธ๋)\": dijkstra(build_adj(nodes, edges, lambda e: None if \"N0507\" in (e[\"u\"], e[\"v\"]) else 1.0), DEPOT, \"N0507\") == (float(\"inf\"), [], []),\n",
" \"์ถ๋ฐ์ง=๋์ฐฉ์ง\": dijkstra(build_adj(nodes, edges, lambda e: 1.0), DEPOT, DEPOT)[1] == [DEPOT],\n",
" \"์ ๋น ์ค ์์ฐ ์ฐจ๋จ\": rules({\"dest\": \"N0507\", \"cargo_t\": 5, \"deadline_h\": 9}, cost, path, es, {**ASSET, \"status\": \"์ ๋น\"})[\"R11 ์์ฐ ์ํ ๊ฐ์ฉ\"] == \"FAIL\",\n",
"}\n",
"for k, v in tests.items():\n",
" print(f\" [{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๋จ์ ํ
์คํธ ํต๊ณผ์จ {np.mean(list(tests.values())):.0%}\")\n",
"print(\"Baseline ํ๊ณ: ์๊ฐ๋ง ์ต์ํํด ์ํ ๊ตฌ์ญ์ ์ง๋๋ ๊ฒฝ๋ก(WARN)๊ฐ ๋์จ๋ค -> ๋ฏธ์
4์์ ์ํ ํํผยท์ ์ฌ ํจ์จ์ ๋ฐ์ํ๋ค.\")\n",
"\n",
"plt.figure(figsize=(8, 5.5))\n",
"for e in edges:\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" plt.plot([x1, x2], [y1, y2], color=\"lightgray\" if e[\"open\"] else \"red\", lw=1, ls=\"-\" if e[\"open\"] else \"--\")\n",
"for t in threats:\n",
" plt.gca().add_patch(plt.Circle((t[\"x\"], t[\"y\"]), t[\"r\"], color=\"red\", alpha=0.12))\n",
"for q, c in zip(reqs, plt.cm.tab10.colors):\n",
" _, path, _ = route(q[\"dest\"], q[\"cargo_t\"])\n",
" xy = np.array([nodes[n] for n in path]); plt.plot(xy[:, 0], xy[:, 1], color=c, lw=2.5, label=f\"to {q['dest']}\")\n",
"plt.plot(*nodes[DEPOT], \"ks\", ms=10, label=\"depot\")\n",
"plt.gca().set_aspect(\"equal\"); plt.legend(fontsize=8, loc=\"upper left\"); plt.xlabel(\"x (km)\"); plt.ylabel(\"y (km)\")\n",
"plt.title(\"Baseline shortest-time routes (red circles = threat zones, synthetic)\")\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[Baseline ์ต๋จ์๊ฐ ๊ฒฝ๋ก ๊ฒฐ๊ณผ - ์คํ ํธ๋ญ]\n",
" ์์ฒญ ๋ชฉ์ ์ง ๊ฑฐ๋ฆฌ(km) ์์(๋ถ) ๊ตฌ๊ฐ ์ ์ํ ํต๊ณผ ๊ตฌ๊ฐ PASS WARN FAIL\n",
"REQ-0000 N0007 44.2 58.9 8 2 14 1 0\n",
"REQ-0001 N0107 40.2 54.1 7 2 14 1 0\n",
"REQ-0002 N0207 44.4 53.2 8 3 14 1 0\n",
"REQ-0003 N0307 49.0 58.8 9 3 14 1 0\n",
"REQ-0004 N0407 53.3 67.4 10 3 14 1 0\n",
"REQ-0005 N0507 58.4 73.5 11 3 14 1 0\n",
"\n",
"๊ฒ์ฆ ๊ท์น 15์ข
(๋ชฉํ 15์ข
์ด์), ๊ท์น ์ฒ๋ฆฌ ํ๊ท 0.06 ms / ์ต๋ 0.11 ms (๋ชฉํ 50 ms ์ดํ: ๋ฌ์ฑ)\n",
"์ต์ ์ฑ ๊ฒ์ฆ(scipy ๊ฒฐ๊ณผ์ ์ผ์น): 6/6\n",
" [PASS] ์ ์ฌ๋=์์ก๋ฅ๋ ฅ ํ๊ณ(15t)\n",
" [PASS] ์ ์ฌ๋ ํ๊ณ+0.1t\n",
" [PASS] ์์์๊ฐ=๊ธฐํ ์ ํํ ์ผ์น\n",
" [PASS] ๋๋ฌ ๋ถ๊ฐ ๋ชฉ์ ์ง ์ฒ๋ฆฌ(๊ณ ๋ฆฝ ๋
ธ๋)\n",
" [PASS] ์ถ๋ฐ์ง=๋์ฐฉ์ง\n",
" [PASS] ์ ๋น ์ค ์์ฐ ์ฐจ๋จ\n",
"๋จ์ ํ
์คํธ ํต๊ณผ์จ 100%\n",
"Baseline ํ๊ณ: ์๊ฐ๋ง ์ต์ํํด ์ํ ๊ตฌ์ญ์ ์ง๋๋ ๊ฒฝ๋ก(WARN)๊ฐ ๋์จ๋ค -> ๋ฏธ์
4์์ ์ํ ํํผยท์ ์ฌ ํจ์จ์ ๋ฐ์ํ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์ ์ํ ๋ฐ๊ฒฝ ํํผ ๋ฐ ์์ฐ ์ ์ฌ ํจ์จ์ ๊ณ ๋ คํ ์ต์ ์์ก ๊ฒฝ๋ก ์ถ์ฒ ๋ชจ๋ธ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ํ ๋ฐ๊ฒฝ ํํผ ๋ฐ ์ ์ฌ ํจ์จ์ ๊ณ ๋ คํ ์ต์ ์์ก ๊ฒฝ๋ก ์ถ์ฒ ๋ชจ๋ธ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ ์ํ ๋ฐ๊ฒฝ ํํผ์ ์ ์ฌ ํจ์จ(์ฐจ์ข
์กฐํฉ)์ ํจ๊ป ๊ณ ๋ คํ ์ต์ ์์ก ๊ฒฝ๋ก ์ถ์ฒ ๋ชจ๋ธ๋ก ๊ณ ๋ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ๋น์ฉ์ ์์์๊ฐ๊ณผ ์ํ ์ํ ๊ฐ์ค์น๋ฅผ ํจ๊ป ๋ฃ๊ณ , ๊ณผ๊ฑฐ ์์กํต์ ๊ด ์น์ธ ์ฌ๋ก(๊ฐ์)๋ก ์ํ ๊ฐ์ค์น๋ฅผ ํ์ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ai/mov02_intelligence_module.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ถ์ฒ ์ฌ์ ์ ํฉ๋ฆฌ์ฑ ๋ฐ ์ค๋ฌด ์์ฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ถ์ฒ/๋ถ๋ฅ ์ ํ๋ 85% ์ด์ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: AI ์ถ๋ก ๋ชจ๋ ๊ตฌํ ๋ฐ ๋ฒค์น๋งํฌ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ณผ๊ฑฐ ์น์ธ/์ฒ๋ฆฌ ์๋ฃ ๋ฐ์ดํฐ์์ ์ถ์ฒ ์ผ์น๋ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ ํํผ ๋น์ฉ๊ณผ ์ ์ฌ ํจ์จ(์ฐจ์ข
์กฐํฉ)์ ํจ๊ป ๊ณ ๋ คํ๋ ์์ก ๊ฒฝ๋ก ์ถ์ฒ ๋ชจ๋ธ์ ๊ตฌํํ๋ค. ๊ณผ๊ฑฐ ์์กํต์ ๊ด ์น์ธ ์ฌ๋ก๋ก ์ํ ๊ฐ์ค์น๋ฅผ ํ์ตํด ์ถ์ฒ ์ผ์น๋(Top-1ยทTop-3)๋ฅผ ๊ฒ์ฆํ๊ณ , ์ถ์ฒ ์ฌ์ ์ ์ ์ฌ๋ ์ ์๋ฅผ ์ ์ํ๋ค.\"\"\"\n",
"import heapq\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋๋ก๋งยท์์ก ์์ฐ ์์ฑ๊ธฐ (์ค์ ์ง๋ช
ยท๋ถ๋ยท์ฅ๋น ์ ์ ์์)\n",
"# ๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง(๋
ธ๋=๊ต์ฐจ์ , ๊ฐ์ =๋๋ก ๊ตฌ๊ฐ)์ ๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยท๊ฐ์ ์ํ ๊ตฌ์ญ์ ๋ถ์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ASSETS = { # ๊ต์ก์ฉ ๊ฐ์ ์ ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
" \"truck\": {\"name\": \"์คํ ํธ๋ญ\", \"cap_t\": 5.0, \"speed_kmh\": 50, \"cost_km\": 1.0, \"weight_t\": 10.0},\n",
" \"trailer\": {\"name\": \"๋ํ ํธ๋ ์ผ๋ฌ\", \"cap_t\": 20.0, \"speed_kmh\": 40, \"cost_km\": 2.4, \"weight_t\": 30.0},\n",
" \"airlift\": {\"name\": \"์์ก๊ธฐ\", \"cap_t\": 12.0, \"speed_kmh\": 300, \"cost_km\": 9.0, \"weight_t\": 0.0},\n",
"}\n",
"ROAD_FACTOR = {\"paved\": 1.0, \"unpaved\": 0.6} # ๋๋ก ๋ฑ๊ธ๋ณ ์๋ ๊ณ์\n",
"\n",
"\n",
"def make_network(seed=5, rows=6, cols=8, spacing=5.0):\n",
" \"\"\"๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง. ๋
ธ๋ ์ขํ(km)์ ๊ฐ์ ๋ชฉ๋ก(๊ฑฐ๋ฆฌยท๋๋ก๋ฑ๊ธยท๊ต๋ ํ์ค)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" nodes = {f\"N{r:02d}{c:02d}\": (c * spacing + rng.uniform(-1, 1), r * spacing + rng.uniform(-1, 1))\n",
" for r in range(rows) for c in range(cols)}\n",
" edges = []\n",
"\n",
" def add(a, b):\n",
" (x1, y1), (x2, y2) = nodes[a], nodes[b]\n",
" bridge = bool(rng.random() < 0.15)\n",
" edges.append({\"id\": f\"E{len(edges):03d}\", \"u\": a, \"v\": b,\n",
" \"km\": round(float(np.hypot(x2 - x1, y2 - y1) * rng.uniform(1.0, 1.25)), 2),\n",
" \"road\": \"paved\" if rng.random() < 0.65 else \"unpaved\", \"bridge\": bridge,\n",
" \"limit_t\": float(rng.choice([20, 40, 60])) if bridge else None, \"open\": True})\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if c + 1 < cols:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r:02d}{c + 1:02d}\")\n",
" if r + 1 < rows:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c:02d}\")\n",
" if r + 1 < rows and c + 1 < cols and rng.random() < 0.25:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c + 1:02d}\")\n",
" return nodes, edges\n",
"\n",
"\n",
"def make_threats(seed=9, n=3, xlim=(8, 28), ylim=(3, 22)):\n",
" \"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ(์ํ). p = ๊ตฌ๊ฐ 1ํ ํต๊ณผ ์ ํผํด ๊ฐ๋ฅ์ฑ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" return [{\"id\": f\"TZ{i + 1}\", \"x\": float(rng.uniform(*xlim)), \"y\": float(rng.uniform(*ylim)),\n",
" \"r\": float(rng.uniform(3, 5)), \"p\": float(round(rng.uniform(0.05, 0.2), 3))} for i in range(n)]\n",
"\n",
"\n",
"def edge_risk(e, nodes, threats):\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" s = np.linspace(0, 1, 6)\n",
" risk = 0.0\n",
" for t in threats:\n",
" if np.min(np.hypot(x1 + (x2 - x1) * s - t[\"x\"], y1 + (y2 - y1) * s - t[\"y\"])) < t[\"r\"]:\n",
" risk = max(risk, t[\"p\"])\n",
" return risk\n",
"\n",
"\n",
"def edge_minutes(e, asset=\"truck\"):\n",
" return e[\"km\"] / (ASSETS[asset][\"speed_kmh\"] * ROAD_FACTOR[e[\"road\"]]) * 60\n",
"\n",
"\n",
"def passable(e, gross_t):\n",
" return e[\"open\"] and (not e[\"bridge\"] or e[\"limit_t\"] >= gross_t)\n",
"\n",
"\n",
"def build_adj(nodes, edges, weight_fn):\n",
" \"\"\"weight_fn(e)๊ฐ None์ด๋ฉด ํตํ ๋ถ๊ฐ ๊ฐ์ ์ผ๋ก ๋ณด๊ณ ์ ์ธํ๋ค.\"\"\"\n",
" adj = {n: [] for n in nodes}\n",
" for e in edges:\n",
" w = weight_fn(e)\n",
" if w is not None:\n",
" adj[e[\"u\"]].append((e[\"v\"], w, e))\n",
" adj[e[\"v\"]].append((e[\"u\"], w, e))\n",
" return adj\n",
"\n",
"\n",
"def dijkstra(adj, src, dst):\n",
" \"\"\"๋ค์ต์คํธ๋ผ ์ต๋จ๊ฒฝ๋ก. (๋น์ฉ, ๋
ธ๋ ๊ฒฝ๋ก, ๊ฐ์ ๋ชฉ๋ก). ๋๋ฌ ๋ถ๊ฐ๋ฉด (inf, [], []).\"\"\"\n",
" dist, prev, done, pq = {src: 0.0}, {}, set(), [(0.0, src)]\n",
" while pq:\n",
" d, u = heapq.heappop(pq)\n",
" if u in done:\n",
" continue\n",
" done.add(u)\n",
" if u == dst:\n",
" break\n",
" for v, w, e in adj[u]:\n",
" if d + w < dist.get(v, float(\"inf\")):\n",
" dist[v], prev[v] = d + w, (u, e)\n",
" heapq.heappush(pq, (d + w, v))\n",
" if dst not in done:\n",
" return float(\"inf\"), [], []\n",
" path, es = [dst], []\n",
" while path[-1] != src:\n",
" u, e = prev[path[-1]]\n",
" es.append(e); path.append(u)\n",
" return dist[dst], path[::-1], es[::-1]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"nodes, edges = make_network()\n",
"DEPOT, UNITS = \"N0000\", [f\"N{r:02d}07\" for r in range(6)]\n",
"for e in edges:\n",
" if e[\"id\"] in (\"E017\", \"E040\", \"E046\", \"E071\"):\n",
" e[\"open\"] = False\n",
"LAMS = [0.0, 0.3, 0.7, 1.5, 3.0] # ์ํ ํํผ ๊ฐ๋๋ณ ํ๋ณด ๊ฒฝ๋ก ์์ฑ\n",
"\n",
"\n",
"def candidates(dest, threats, kind=\"truck\", gross=15, k=5):\n",
" \"\"\"์ํ ํํผ ๊ฐ๋(lam)๋ฅผ ๋ฐ๊พธ๊ณ , ์ด๋ฏธ ์ฐพ์ ๊ฒฝ๋ก ๊ตฌ๊ฐ์ ๋ฒ์ ์ ์ฃผ์ด ์๋ก ๋ค๋ฅธ ํ๋ณด ๊ฒฝ๋ก(์ต๋ k๊ฐ)๋ฅผ ๋ง๋ ๋ค.\"\"\"\n",
" risk = {e[\"id\"]: edge_risk(e, nodes, threats) for e in edges}\n",
" out, seen, pen = [], set(), {}\n",
" for lam in LAMS + [0.0, 0.0, 0.3, 0.3]:\n",
" adj = build_adj(nodes, edges, lambda e: (edge_minutes(e, kind) + lam * risk[e[\"id\"]] * 100) * pen.get(e[\"id\"], 1.0)\n",
" if passable(e, gross) else None)\n",
" _, path, es = dijkstra(adj, DEPOT, dest)\n",
" for e in es:\n",
" pen[e[\"id\"]] = pen.get(e[\"id\"], 1.0) * 1.3\n",
" if path and tuple(path) not in seen and len(out) < k:\n",
" seen.add(tuple(path))\n",
" surv = float(np.prod([1 - risk[e[\"id\"]] for e in es]))\n",
" out.append({\"path\": path, \"min\": sum(edge_minutes(e, kind) for e in es), \"km\": sum(e[\"km\"] for e in es),\n",
" \"surv\": surv, \"risk_score\": -np.log(surv) * 100, \"threat_edges\": sum(risk[e[\"id\"]] > 0 for e in es)})\n",
" return out\n",
"\n",
"\n",
"def utility(c, w):\n",
" return c[\"min\"] + w * c[\"risk_score\"] # ๋ฎ์์๋ก ์ข์: ์์์๊ฐ(๋ถ) + ์ํ ๊ฐ์ค์น x ์ํ ์ ์\n",
"\n",
"\n",
"# 1) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ณผ๊ฑฐ ์์กํต์ ๊ด ์น์ธ ์ฌ๋ก 40๊ฑด: ์จ์ ์ ํธ(w=1.2)์ ํ๋จ ํธ์ฐจ(์ก์)๋ก ํ ๊ฒฝ๋ก๋ฅผ ์น์ธ\n",
"rng = np.random.default_rng(4)\n",
"cases = []\n",
"for i in range(60):\n",
" th = make_threats(seed=100 + i, n=int(rng.integers(2, 5)))\n",
" cand = candidates(str(rng.choice(UNITS)), th)\n",
" if len(cand) < 2:\n",
" continue\n",
" approved = int(np.argmin([utility(c, 1.2) + rng.normal(0, 1.0) for c in cand]))\n",
" cases.append({\"cand\": cand, \"approved\": approved})\n",
"train, test = cases[:len(cases) // 2], cases[len(cases) // 2:]\n",
"\n",
"\n",
"def agreement(cs, w, top=1):\n",
" return np.mean([c[\"approved\"] in np.argsort([utility(x, w) for x in c[\"cand\"]])[:top] for c in cs])\n",
"\n",
"\n",
"def loglik(cs, w, T=1.5):\n",
" \"\"\"์ ํ ํ๋ฅ P(i) ~ exp(-ํจ์ฉ/T) ๊ฐ์ ์ ๋ก๊ทธ์ฐ๋(์กฐ๊ฑด๋ถ ๋ก์ง). ์ผ์น๋๋ณด๋ค ๋งค๋๋ฌ์ ๊ฐ์ค์น ์ถ์ ์ด ์์ ์ ์ด๋ค.\"\"\"\n",
" ll = 0.0\n",
" for c in cs:\n",
" u = -np.array([utility(x, w) for x in c[\"cand\"]]) / T\n",
" ll += u[c[\"approved\"]] - (u.max() + np.log(np.exp(u - u.max()).sum()))\n",
" return ll\n",
"\n",
"\n",
"grid = np.round(np.arange(0, 3.01, 0.1), 1)\n",
"w_best = grid[int(np.argmax([loglik(train, w) for w in grid]))]\n",
"print(f\"๊ณผ๊ฑฐ ์น์ธ ์ฌ๋ก {len(cases)}๊ฑด(ํ์ต {len(train)} / ๊ฒ์ฆ {len(test)}), ํ์ต๋ ์ํ ๊ฐ์ค์น w = {w_best}\")\n",
"rows = [{\"๋ชจ๋ธ\": \"Baseline(์ต๋จ์๊ฐ, w=0)\", \"Top-1 ์ผ์น๋\": agreement(test, 0.0), \"Top-3 ํฌํจ๋ฅ \": agreement(test, 0.0, 3)},\n",
" {\"๋ชจ๋ธ\": f\"์ํ ํํผ ์ถ์ฒ(w={w_best})\", \"Top-1 ์ผ์น๋\": agreement(test, w_best), \"Top-3 ํฌํจ๋ฅ \": agreement(test, w_best, 3)}]\n",
"res = pd.DataFrame(rows)\n",
"print(res.to_string(index=False, float_format=lambda v: f\"{v:.1%}\"))\n",
"acc = rows[1][\"Top-1 ์ผ์น๋\"]\n",
"print(f\"์ถ์ฒ ์ ํ๋ {acc:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if acc >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 2) ํ์ฌ ์์ฒญ์ ๋ํ Top-3 ์ถ์ฒ๊ณผ ์ถ์ฒ ์ฌ์ \n",
"threats = make_threats()\n",
"dest, cargo = \"N0307\", 38.0\n",
"cand = candidates(dest, threats)\n",
"order = np.argsort([utility(c, w_best) for c in cand])[:3]\n",
"best_u = utility(cand[order[0]], w_best)\n",
"print(f\"\\n[์์ฒญ: {dest} ๋ณด๊ธ {cargo}t] ์ถ์ฒ Top-3 (์ ์ฌ๋ = 1์์ ๋๋น ํจ์ฉ ๋น์จ)\")\n",
"base = min(cand, key=lambda c: c[\"min\"])\n",
"for rank, i in enumerate(order, 1):\n",
" c = cand[i]\n",
" print(f\" {rank}์์ ์์ {c['min']:.0f}๋ถ, ๊ฑฐ๋ฆฌ {c['km']:.1f}km, ์ํ ํต๊ณผ {c['threat_edges']}๊ตฌ๊ฐ, ์์กดํ๋ฅ {c['surv']:.1%}, ์ ์ฌ๋ {best_u / utility(c, w_best):.2f}\")\n",
"top = cand[order[0]]\n",
"print(f\" ์ถ์ฒ ์ฌ์ : ์ต๋จ์๊ฐ ๊ฒฝ๋ก ๋๋น ์์ {top['min'] - base['min']:+.0f}๋ถ, ์ํ ํต๊ณผ {base['threat_edges']}->{top['threat_edges']}๊ตฌ๊ฐ, ์์กดํ๋ฅ {base['surv']:.1%}->{top['surv']:.1%}\")\n",
"\n",
"\n",
"# 3) ์ ์ฌ ํจ์จ: ์ฐจ์ข
์กฐํฉ(ํธ๋ญ 5t, ํธ๋ ์ผ๋ฌ 20t) ์ต์ ํ - ๊ต๋ ํ์ค ๋๋ฌธ์ ํธ๋ ์ผ๋ฌ ๊ฒฝ๋ก๊ฐ ๋ค๋ฅผ ์ ์์\n",
"def plan(cargo, allow_trailer=True):\n",
" best = None\n",
" for n_trl in range(0, 4 if allow_trailer else 1):\n",
" n_trk = int(max(0, np.ceil((cargo - 20 * n_trl) / 5)))\n",
" if n_trk > 10:\n",
" continue\n",
" cost = 0.0\n",
" if n_trl:\n",
" c = candidates(dest, threats, \"trailer\", 50)\n",
" if not c:\n",
" continue\n",
" cost += n_trl * min(c, key=lambda x: utility(x, w_best))[\"km\"] * ASSETS[\"trailer\"][\"cost_km\"]\n",
" cost += n_trk * top[\"km\"] * ASSETS[\"truck\"][\"cost_km\"]\n",
" cap = 20 * n_trl + 5 * n_trk\n",
" cand_plan = {\"ํธ๋ ์ผ๋ฌ\": n_trl, \"ํธ๋ญ\": n_trk, \"์์ก๋ฅ๋ ฅ(t)\": cap, \"์ ์ฌ ํจ์จ\": cargo / cap, \"๋น์ฉ(๋จ๊ฐx km)\": round(cost, 1)}\n",
" if best is None or cost < best[\"๋น์ฉ(๋จ๊ฐx km)\"]:\n",
" best = cand_plan\n",
" return best\n",
"\n",
"\n",
"p0, p1 = plan(cargo, False), plan(cargo)\n",
"print(\"\\n[์ ์ฌ ๊ณํ ๋น๊ต]\")\n",
"print(pd.DataFrame([{\"๋ฐฉ์\": \"ํธ๋ญ๋ง ๋ฐฐ์ฐจ\", **p0}, {\"๋ฐฉ์\": \"์ฐจ์ข
์กฐํฉ ์ต์ ํ\", **p1}]).to_string(index=False, float_format=lambda v: f\"{v:.1%}\" if v < 1 else f\"{v:.1f}\"))\n",
"print(f\"๋น์ฉ {1 - p1['๋น์ฉ(๋จ๊ฐx km)'] / p0['๋น์ฉ(๋จ๊ฐx km)']:.1%} ์ ๊ฐ, ํฌ์
์ฐจ๋ {p0['ํธ๋ญ']}๋ -> {p1['ํธ๋ญ'] + p1['ํธ๋ ์ผ๋ฌ']}๋\")\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n",
"for e in edges:\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" ax[0].plot([x1, x2], [y1, y2], color=\"lightgray\", lw=1)\n",
"for t in threats:\n",
" ax[0].add_patch(plt.Circle((t[\"x\"], t[\"y\"]), t[\"r\"], color=\"red\", alpha=0.15))\n",
"for rank, (i, col) in enumerate(zip(order, [\"tab:green\", \"tab:blue\", \"tab:orange\"]), 1):\n",
" xy = np.array([nodes[n] for n in cand[i][\"path\"]]); ax[0].plot(xy[:, 0], xy[:, 1], color=col, lw=3 - 0.6 * rank, label=f\"rank {rank}\")\n",
"xy = np.array([nodes[n] for n in base[\"path\"]]); ax[0].plot(xy[:, 0], xy[:, 1], \"k--\", lw=1, label=\"shortest-time\")\n",
"ax[0].set_aspect(\"equal\"); ax[0].legend(fontsize=8); ax[0].set_title(\"Threat-aware route recommendation (synthetic)\")\n",
"ax[1].plot(grid, [agreement(train, w) for w in grid], \"o-\", label=\"train\")\n",
"ax[1].plot(grid, [agreement(test, w) for w in grid], \"s-\", label=\"test\")\n",
"ax[1].axhline(0.85, ls=\"--\", c=\"gray\"); ax[1].set_xlabel(\"risk weight w\"); ax[1].set_ylabel(\"Top-1 agreement\"); ax[1].legend()\n",
"ax[1].set_title(\"Agreement with past approvals\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ณผ๊ฑฐ ์น์ธ ์ฌ๋ก 60๊ฑด(ํ์ต 30 / ๊ฒ์ฆ 30), ํ์ต๋ ์ํ ๊ฐ์ค์น w = 2.3\n",
" ๋ชจ๋ธ Top-1 ์ผ์น๋ Top-3 ํฌํจ๋ฅ \n",
"Baseline(์ต๋จ์๊ฐ, w=0) 43.3% 100.0%\n",
" ์ํ ํํผ ์ถ์ฒ(w=2.3) 90.0% 96.7%\n",
"์ถ์ฒ ์ ํ๋ 90.0% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์์ฒญ: N0307 ๋ณด๊ธ 38.0t] ์ถ์ฒ Top-3 (์ ์ฌ๋ = 1์์ ๋๋น ํจ์ฉ ๋น์จ)\n",
" 1์์ ์์ 60๋ถ, ๊ฑฐ๋ฆฌ 50.0km, ์ํ ํต๊ณผ 0๊ตฌ๊ฐ, ์์กดํ๋ฅ 100.0%, ์ ์ฌ๋ 1.00\n",
" 2์์ ์์ 66๋ถ, ๊ฑฐ๋ฆฌ 50.9km, ์ํ ํต๊ณผ 0๊ตฌ๊ฐ, ์์กดํ๋ฅ 100.0%, ์ ์ฌ๋ 0.91\n",
" 3์์ ์์ 73๋ถ, ๊ฑฐ๋ฆฌ 55.8km, ์ํ ํต๊ณผ 0๊ตฌ๊ฐ, ์์กดํ๋ฅ 100.0%, ์ ์ฌ๋ 0.82\n",
" ์ถ์ฒ ์ฌ์ : ์ต๋จ์๊ฐ ๊ฒฝ๋ก ๋๋น ์์ +1๋ถ, ์ํ ํต๊ณผ 3->0๊ตฌ๊ฐ, ์์กดํ๋ฅ 57.8%->100.0%\n",
"\n",
"[์ ์ฌ ๊ณํ ๋น๊ต]\n",
" ๋ฐฉ์ ํธ๋ ์ผ๋ฌ ํธ๋ญ ์์ก๋ฅ๋ ฅ(t) ์ ์ฌ ํจ์จ ๋น์ฉ(๋จ๊ฐx km)\n",
" ํธ๋ญ๋ง ๋ฐฐ์ฐจ 0 8 40 95.0% 400.0\n",
"์ฐจ์ข
์กฐํฉ ์ต์ ํ 2 0 40 95.0% 287.1\n",
"๋น์ฉ 28.2% ์ ๊ฐ, ํฌ์
์ฐจ๋ 8๋ -> 2๋\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์์ก์ ๋จ ์์กด ํ๋ฅ ๋ฐ ์ ์ ๋์ฐฉ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์์ก์ ๋จ ์์กด ํ๋ฅ ๋ฐ ์ ์ ๋์ฐฉ ์ ๋ขฐ๋ ์ ์ ์ฐ์ถ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ๋ณด ๊ฒฝ๋ก๋ง๋ค ์์ก์ ๋จ ์์กดํ๋ฅ ๊ณผ ์ ์ ๋์ฐฉ ์ ๋ขฐ๋๋ฅผ ๊ณ์ฐํด 0~100์ ์ ๋ขฐ๋ ์ ์์ ๋ฑ๊ธ์ ์ฐ์ถํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ํ ๊ตฌ๊ฐ ํต๊ณผ ์๋ก ์์กดํ๋ฅ ์, ๋๋ก ๋ฑ๊ธยท๊ต๋ ๋๊ธฐยท์ํ ๊ตฌ์ญ ๊ฐ์์ ๋ฐ์ํ ๋ชฌํ
์นด๋ฅผ๋ก ๋ชจ์๋ก ์ ์ ๋์ฐฉ ํ๋ฅ ์ ๊ตฌ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/mov02_app.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ์ค๋ฌด์ ์นํ์ ๋์์ธ ๋ฐ ์กฐ์ ํธ์์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํต์ฌ ํ๋ฉด 4์ข
(์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ) ๊ตฌํ ์๋ฃ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: UI ํ๋กํ ํ์
๋ก์ปฌ ์๋ฒ ๊ตฌ๋ ๋ฐ ์ ์ฒด ํ์ด์ง ๋ค๋น๊ฒ์ด์
ํ์ธ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ฌ์ฉ์ฑ ํ๊ฐ(UX Walkthrough) ๋ฐ ์ธํฐ๋์
ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํ๋ณด ์์ก ๊ฒฝ๋ก๋ง๋ค ์์ก์ ๋จ ์์กด ํ๋ฅ (์ํ ๊ตฌ์ญ ํต๊ณผ ๊ธฐ๋ฐ)๊ณผ ์ ์ ๋์ฐฉ ์ ๋ขฐ๋(๋ชฌํ
์นด๋ฅผ๋ก ์์์๊ฐ ๋ชจ์)๋ฅผ ๊ณ์ฐํด 0~100์ ์ ๋ขฐ๋ ์ ์์ ๋ฑ๊ธ์ ์ฐ์ถํ๋ ์์ง์ ๋ง๋ค๊ณ , ์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ 4๊ฐ ํ๋ฉด์ผ๋ก ํ์ํ๋ฉฐ ์์ธก ํ๋ฅ ์ ๋ณด์ ์ ํ๋๋ฅผ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import heapq\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋๋ก๋งยท์์ก ์์ฐ ์์ฑ๊ธฐ (์ค์ ์ง๋ช
ยท๋ถ๋ยท์ฅ๋น ์ ์ ์์)\n",
"# ๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง(๋
ธ๋=๊ต์ฐจ์ , ๊ฐ์ =๋๋ก ๊ตฌ๊ฐ)์ ๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยท๊ฐ์ ์ํ ๊ตฌ์ญ์ ๋ถ์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ASSETS = { # ๊ต์ก์ฉ ๊ฐ์ ์ ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
" \"truck\": {\"name\": \"์คํ ํธ๋ญ\", \"cap_t\": 5.0, \"speed_kmh\": 50, \"cost_km\": 1.0, \"weight_t\": 10.0},\n",
" \"trailer\": {\"name\": \"๋ํ ํธ๋ ์ผ๋ฌ\", \"cap_t\": 20.0, \"speed_kmh\": 40, \"cost_km\": 2.4, \"weight_t\": 30.0},\n",
" \"airlift\": {\"name\": \"์์ก๊ธฐ\", \"cap_t\": 12.0, \"speed_kmh\": 300, \"cost_km\": 9.0, \"weight_t\": 0.0},\n",
"}\n",
"ROAD_FACTOR = {\"paved\": 1.0, \"unpaved\": 0.6} # ๋๋ก ๋ฑ๊ธ๋ณ ์๋ ๊ณ์\n",
"\n",
"\n",
"def make_network(seed=5, rows=6, cols=8, spacing=5.0):\n",
" \"\"\"๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง. ๋
ธ๋ ์ขํ(km)์ ๊ฐ์ ๋ชฉ๋ก(๊ฑฐ๋ฆฌยท๋๋ก๋ฑ๊ธยท๊ต๋ ํ์ค)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" nodes = {f\"N{r:02d}{c:02d}\": (c * spacing + rng.uniform(-1, 1), r * spacing + rng.uniform(-1, 1))\n",
" for r in range(rows) for c in range(cols)}\n",
" edges = []\n",
"\n",
" def add(a, b):\n",
" (x1, y1), (x2, y2) = nodes[a], nodes[b]\n",
" bridge = bool(rng.random() < 0.15)\n",
" edges.append({\"id\": f\"E{len(edges):03d}\", \"u\": a, \"v\": b,\n",
" \"km\": round(float(np.hypot(x2 - x1, y2 - y1) * rng.uniform(1.0, 1.25)), 2),\n",
" \"road\": \"paved\" if rng.random() < 0.65 else \"unpaved\", \"bridge\": bridge,\n",
" \"limit_t\": float(rng.choice([20, 40, 60])) if bridge else None, \"open\": True})\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if c + 1 < cols:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r:02d}{c + 1:02d}\")\n",
" if r + 1 < rows:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c:02d}\")\n",
" if r + 1 < rows and c + 1 < cols and rng.random() < 0.25:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c + 1:02d}\")\n",
" return nodes, edges\n",
"\n",
"\n",
"def make_threats(seed=9, n=3, xlim=(8, 28), ylim=(3, 22)):\n",
" \"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ(์ํ). p = ๊ตฌ๊ฐ 1ํ ํต๊ณผ ์ ํผํด ๊ฐ๋ฅ์ฑ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" return [{\"id\": f\"TZ{i + 1}\", \"x\": float(rng.uniform(*xlim)), \"y\": float(rng.uniform(*ylim)),\n",
" \"r\": float(rng.uniform(3, 5)), \"p\": float(round(rng.uniform(0.05, 0.2), 3))} for i in range(n)]\n",
"\n",
"\n",
"def edge_risk(e, nodes, threats):\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" s = np.linspace(0, 1, 6)\n",
" risk = 0.0\n",
" for t in threats:\n",
" if np.min(np.hypot(x1 + (x2 - x1) * s - t[\"x\"], y1 + (y2 - y1) * s - t[\"y\"])) < t[\"r\"]:\n",
" risk = max(risk, t[\"p\"])\n",
" return risk\n",
"\n",
"\n",
"def edge_minutes(e, asset=\"truck\"):\n",
" return e[\"km\"] / (ASSETS[asset][\"speed_kmh\"] * ROAD_FACTOR[e[\"road\"]]) * 60\n",
"\n",
"\n",
"def passable(e, gross_t):\n",
" return e[\"open\"] and (not e[\"bridge\"] or e[\"limit_t\"] >= gross_t)\n",
"\n",
"\n",
"def build_adj(nodes, edges, weight_fn):\n",
" \"\"\"weight_fn(e)๊ฐ None์ด๋ฉด ํตํ ๋ถ๊ฐ ๊ฐ์ ์ผ๋ก ๋ณด๊ณ ์ ์ธํ๋ค.\"\"\"\n",
" adj = {n: [] for n in nodes}\n",
" for e in edges:\n",
" w = weight_fn(e)\n",
" if w is not None:\n",
" adj[e[\"u\"]].append((e[\"v\"], w, e))\n",
" adj[e[\"v\"]].append((e[\"u\"], w, e))\n",
" return adj\n",
"\n",
"\n",
"def dijkstra(adj, src, dst):\n",
" \"\"\"๋ค์ต์คํธ๋ผ ์ต๋จ๊ฒฝ๋ก. (๋น์ฉ, ๋
ธ๋ ๊ฒฝ๋ก, ๊ฐ์ ๋ชฉ๋ก). ๋๋ฌ ๋ถ๊ฐ๋ฉด (inf, [], []).\"\"\"\n",
" dist, prev, done, pq = {src: 0.0}, {}, set(), [(0.0, src)]\n",
" while pq:\n",
" d, u = heapq.heappop(pq)\n",
" if u in done:\n",
" continue\n",
" done.add(u)\n",
" if u == dst:\n",
" break\n",
" for v, w, e in adj[u]:\n",
" if d + w < dist.get(v, float(\"inf\")):\n",
" dist[v], prev[v] = d + w, (u, e)\n",
" heapq.heappush(pq, (d + w, v))\n",
" if dst not in done:\n",
" return float(\"inf\"), [], []\n",
" path, es = [dst], []\n",
" while path[-1] != src:\n",
" u, e = prev[path[-1]]\n",
" es.append(e); path.append(u)\n",
" return dist[dst], path[::-1], es[::-1]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"nodes, edges = make_network()\n",
"threats = make_threats()\n",
"DEPOT = \"N0000\"\n",
"for e in edges:\n",
" if e[\"id\"] in (\"E017\", \"E040\", \"E046\", \"E071\"):\n",
" e[\"open\"] = False\n",
" e[\"risk\"] = edge_risk(e, nodes, threats)\n",
"SIGMA = {\"paved\": 0.12, \"unpaved\": 0.30} # ๋๋ก ๋ฑ๊ธ๋ณ ์์์๊ฐ ๋ณ๋(๋ก๊ทธ์ ๊ท ํ์คํธ์ฐจ, ๊ฐ์ ๊ฐ)\n",
"\n",
"\n",
"def candidates(dest, lams=(0.0, 0.5, 1.5, 3.0, 0.0, 0.5), k=4):\n",
" \"\"\"์ํ ํํผ ๊ฐ๋๋ฅผ ๋ฐ๊พธ๊ณ ์ด๋ฏธ ์ด ๊ตฌ๊ฐ์ ๋ฒ์ ์ ์ฃผ์ด ์๋ก ๋ค๋ฅธ ํ๋ณด ๊ฒฝ๋ก๋ฅผ ์ต๋ k๊ฐ ๋ง๋ ๋ค.\"\"\"\n",
" out, seen, pen = [], set(), {}\n",
" for lam in lams:\n",
" adj = build_adj(nodes, edges, lambda e: (edge_minutes(e) + lam * e[\"risk\"] * 100) * pen.get(e[\"id\"], 1.0) if passable(e, 15) else None)\n",
" _, path, es = dijkstra(adj, DEPOT, dest)\n",
" for e in es:\n",
" pen[e[\"id\"]] = pen.get(e[\"id\"], 1.0) * 1.3\n",
" if path and tuple(path) not in seen and len(out) < k:\n",
" seen.add(tuple(path)); out.append({\"path\": path, \"es\": es})\n",
" return out\n",
"\n",
"\n",
"def simulate_minutes(es, n, rng):\n",
" \"\"\"๊ฒฝ๋ก ์์์๊ฐ ๋ชฌํ
์นด๋ฅผ๋ก: ๋๋ก ๋ฑ๊ธ๋ณ ๋ณ๋ + ๊ต๋ ํต๊ณผ ๋๊ธฐ + ์ํ ๊ตฌ์ญ ๊ฐ์.\"\"\"\n",
" total = np.zeros(n)\n",
" for e in es:\n",
" total += edge_minutes(e) * rng.lognormal(0, SIGMA[e[\"road\"]], n)\n",
" if e[\"bridge\"]:\n",
" total += rng.exponential(3, n)\n",
" if e[\"risk\"] > 0:\n",
" total += rng.uniform(0, 10, n)\n",
" return total\n",
"\n",
"\n",
"def score_route(c, deadline_min, n=3000, seed=0):\n",
" surv = float(np.prod([1 - e[\"risk\"] for e in c[\"es\"]]))\n",
" sims = simulate_minutes(c[\"es\"], n, np.random.default_rng(seed))\n",
" ontime = float((sims <= deadline_min).mean())\n",
" s = 100 * surv * ontime\n",
" return {\"์์กดํ๋ฅ \": surv, \"์ ์๋์ฐฉ\": ontime, \"์์์์(P50)\": float(np.median(sims)), \"์์์์(P90)\": float(np.percentile(sims, 90)),\n",
" \"์ ์\": round(s, 1), \"๋ฑ๊ธ\": \"์ ๋ขฐ\" if s >= 85 else (\"์ฃผ์\" if s >= 60 else \"์ํ\"), \"sims\": sims}\n",
"\n",
"\n",
"# ---------------- ํ๋ฉด 1. ์
๋ ฅ ----------------\n",
"REQ = {\"req_id\": \"REQ-0101\", \"dest\": \"N0307\", \"cargo_t\": 12.0, \"deadline_min\": 80}\n",
"print(\"[ํ๋ฉด 1: ์
๋ ฅ] ์์ก ์์ฒญ\", {k: v for k, v in REQ.items()}, \"| ๊ฐ์ ์ํ ๊ตฌ์ญ\", len(threats), \"๊ฐ\")\n",
"\n",
"# ---------------- ํ๋ฉด 2. ๋ถ์ ----------------\n",
"cands = candidates(REQ[\"dest\"])\n",
"rows = []\n",
"for i, c in enumerate(cands):\n",
" r = score_route(c, REQ[\"deadline_min\"], seed=i)\n",
" c.update(r)\n",
" rows.append({\"๊ฒฝ๋ก\": f\"R{i + 1}\", \"๊ตฌ๊ฐ ์\": len(c[\"es\"]), \"์ํ ํต๊ณผ\": sum(e[\"risk\"] > 0 for e in c[\"es\"]),\n",
" **{k: r[k] for k in (\"์์กดํ๋ฅ \", \"์ ์๋์ฐฉ\", \"์์์์(P50)\", \"์์์์(P90)\", \"์ ์\", \"๋ฑ๊ธ\")}})\n",
"table = pd.DataFrame(rows)\n",
"fmt = {\"์์กดํ๋ฅ \": \"{:.1%}\", \"์ ์๋์ฐฉ\": \"{:.1%}\", \"์์์์(P50)\": \"{:.0f}๋ถ\", \"์์์์(P90)\": \"{:.0f}๋ถ\"}\n",
"show = table.copy()\n",
"for k, f in fmt.items():\n",
" show[k] = show[k].map(f.format)\n",
"print(\"\\n[ํ๋ฉด 2: ๋ถ์] ํ๋ณด ๊ฒฝ๋ก๋ณ ์ ๋ขฐ๋ ์ ์ (์ ์ = 100 x ์์กดํ๋ฅ x ์ ์๋์ฐฉํ๋ฅ )\")\n",
"print(show.to_string(index=False))\n",
"\n",
"# ---------------- ํ๋ฉด 3. ๊ฒํ (์ํ ๋ฑ์งยทํํฐ) ----------------\n",
"badge = {\"์ ๋ขฐ\": \"์๋ฃ(์ถ์ฒ ๊ฐ๋ฅ)\", \"์ฃผ์\": \"์น์ธ๋๊ธฐ(ํต์ ๊ด ๊ฒํ )\", \"์ํ\": \"๋ฐ๋ ค(์ฌํ์)\"}\n",
"table[\"์ํ\"] = table[\"๋ฑ๊ธ\"].map(badge)\n",
"print(\"\\n[ํ๋ฉด 3: ๊ฒํ ] ์ํ๋ณ ํํฐ\")\n",
"for st in badge.values():\n",
" print(f\" {st}: {table.loc[table['์ํ'] == st, '๊ฒฝ๋ก'].tolist()}\")\n",
"\n",
"# ---------------- ํ๋ฉด 4. ๊ฒฐ๊ณผ ----------------\n",
"best = table.sort_values([\"์ ์\", \"์์์์(P50)\"], ascending=[False, True]).iloc[0]\n",
"print(f\"\\n[ํ๋ฉด 4: ๊ฒฐ๊ณผ] ์ถ์ฒ {best['๊ฒฝ๋ก']} - ์ ์ {best['์ ์']}({best['๋ฑ๊ธ']}), ์์กดํ๋ฅ {best['์์กดํ๋ฅ ']:.1%}, ์ ์๋์ฐฉ {best['์ ์๋์ฐฉ']:.1%}\")\n",
"print(\"ํต์ฌ ํ๋ฉด 4์ข
(์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ) ๊ตฌํ ์๋ฃ: 4/4\")\n",
"\n",
"# ---------------- ์์ธก ๋ณด์ (Calibration) ๊ฒ์ฆ ----------------\n",
"# ์๋ก ๋ค๋ฅธ ๋ชฉ์ ์งยท๊ธฐํ์ ๊ฐ์ ์์ก 60๊ฑด: ์์ธก ์ ์ํ๋ฅ vs ๋ณ๋ ๋์๋ก ๋ชจ์ํ '์ค์ ์ดํ ๊ฒฐ๊ณผ'\n",
"rng = np.random.default_rng(21)\n",
"pred, actual = [], []\n",
"for k in range(60):\n",
" c = candidates(f\"N{int(rng.integers(0, 6)):02d}07\")[int(rng.integers(0, 2))] if k % 2 else candidates(f\"N{int(rng.integers(0, 6)):02d}07\")[0]\n",
" dl = float(rng.uniform(60, 100))\n",
" pred.append(score_route(c, dl, n=2000, seed=k)[\"์ ์๋์ฐฉ\"])\n",
" actual.append(float(simulate_minutes(c[\"es\"], 1, rng)[0] <= dl))\n",
"pred, actual = np.array(pred), np.array(actual)\n",
"brier = np.mean((pred - actual) ** 2)\n",
"print(f\"\\n์ ์๋์ฐฉ ์์ธก ๋ณด์ ๊ฒ์ฆ(๊ฐ์ ์ดํ 60๊ฑด): Brier ์ ์ {brier:.3f} (0์ ๊ฐ๊น์ธ์๋ก ์ข์, ๋ฌด์ ๋ณด ์์ธก 0.25)\")\n",
"for lo, hi in [(0, 0.5), (0.5, 0.9), (0.9, 1.01)]:\n",
" m = (pred >= lo) & (pred < hi)\n",
" if m.any():\n",
" print(f\" ์์ธก {lo:.0%}~{min(hi, 1):.0%} ๊ตฌ๊ฐ {m.sum():2d}๊ฑด: ํ๊ท ์์ธก {pred[m].mean():.1%}, ์ค์ ์ ์์จ {actual[m].mean():.1%}\")\n",
"\n",
"fig, ax = plt.subplots(2, 2, figsize=(12, 8))\n",
"for e in edges:\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" ax[0, 0].plot([x1, x2], [y1, y2], color=\"lightgray\", lw=1)\n",
"for t in threats:\n",
" ax[0, 0].add_patch(plt.Circle((t[\"x\"], t[\"y\"]), t[\"r\"], color=\"red\", alpha=0.15))\n",
"for i, c in enumerate(cands):\n",
" xy = np.array([nodes[n] for n in c[\"path\"]]); ax[0, 0].plot(xy[:, 0], xy[:, 1], lw=2, label=f\"R{i + 1}\")\n",
"ax[0, 0].set_aspect(\"equal\"); ax[0, 0].legend(fontsize=8); ax[0, 0].set_title(\"1. Input: request and candidate routes\")\n",
"for i, c in enumerate(cands):\n",
" ax[0, 1].hist(c[\"sims\"], bins=40, alpha=0.5, label=f\"R{i + 1}\")\n",
"ax[0, 1].axvline(REQ[\"deadline_min\"], color=\"k\", ls=\"--\"); ax[0, 1].set_xlabel(\"travel time (min)\"); ax[0, 1].legend()\n",
"ax[0, 1].set_title(\"2. Analysis: Monte Carlo travel time\")\n",
"cols = {\"์ ๋ขฐ\": \"tab:green\", \"์ฃผ์\": \"tab:orange\", \"์ํ\": \"tab:red\"}\n",
"ax[1, 0].bar(table[\"๊ฒฝ๋ก\"], table[\"์ ์\"], color=[cols[g] for g in table[\"๋ฑ๊ธ\"]])\n",
"ax[1, 0].axhline(85, ls=\"--\", c=\"gray\"); ax[1, 0].axhline(60, ls=\":\", c=\"gray\"); ax[1, 0].set_ylim(0, 100)\n",
"ax[1, 0].set_title(\"3. Review: score (green=trusted, orange=review, red=reject)\")\n",
"ax[1, 1].plot([0, 1], [0, 1], \"k--\", lw=1)\n",
"bins = np.digitize(pred, [0.2, 0.4, 0.6, 0.8])\n",
"ax[1, 1].plot([pred[bins == b].mean() for b in np.unique(bins)], [actual[bins == b].mean() for b in np.unique(bins)], \"o-\")\n",
"ax[1, 1].set_xlabel(\"predicted on-time prob.\"); ax[1, 1].set_ylabel(\"observed on-time rate\"); ax[1, 1].set_title(\"4. Result: calibration check\")\n",
"plt.suptitle(\"MOV02 convoy reliability score engine (synthetic)\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[ํ๋ฉด 1: ์
๋ ฅ] ์์ก ์์ฒญ {'req_id': 'REQ-0101', 'dest': 'N0307', 'cargo_t': 12.0, 'deadline_min': 80} | ๊ฐ์ ์ํ ๊ตฌ์ญ 3 ๊ฐ\n",
"\n",
"[ํ๋ฉด 2: ๋ถ์] ํ๋ณด ๊ฒฝ๋ก๋ณ ์ ๋ขฐ๋ ์ ์ (์ ์ = 100 x ์์กดํ๋ฅ x ์ ์๋์ฐฉํ๋ฅ )\n",
"๊ฒฝ๋ก ๊ตฌ๊ฐ ์ ์ํ ํต๊ณผ ์์กดํ๋ฅ ์ ์๋์ฐฉ ์์์์(P50) ์์์์(P90) ์ ์ ๋ฑ๊ธ\n",
"R1 9 3 57.8% 67.4% 77๋ถ 86๋ถ 38.9 ์ํ\n",
"R2 10 0 100.0% 95.1% 69๋ถ 77๋ถ 95.1 ์ ๋ขฐ\n",
"R3 10 0 100.0% 75.3% 75๋ถ 85๋ถ 75.3 ์ฃผ์\n",
"R4 10 0 100.0% 54.2% 79๋ถ 89๋ถ 54.2 ์ํ\n",
"\n",
"[ํ๋ฉด 3: ๊ฒํ ] ์ํ๋ณ ํํฐ\n",
" ์๋ฃ(์ถ์ฒ ๊ฐ๋ฅ): ['R2']\n",
" ์น์ธ๋๊ธฐ(ํต์ ๊ด ๊ฒํ ): ['R3']\n",
" ๋ฐ๋ ค(์ฌํ์): ['R1', 'R4']\n",
"\n",
"[ํ๋ฉด 4: ๊ฒฐ๊ณผ] ์ถ์ฒ R2 - ์ ์ 95.1(์ ๋ขฐ), ์์กดํ๋ฅ 100.0%, ์ ์๋์ฐฉ 95.1%\n",
"ํต์ฌ ํ๋ฉด 4์ข
(์
๋ ฅ-๋ถ์-๊ฒํ -๊ฒฐ๊ณผ) ๊ตฌํ ์๋ฃ: 4/4\n",
"\n",
"์ ์๋์ฐฉ ์์ธก ๋ณด์ ๊ฒ์ฆ(๊ฐ์ ์ดํ 60๊ฑด): Brier ์ ์ 0.086 (0์ ๊ฐ๊น์ธ์๋ก ์ข์, ๋ฌด์ ๋ณด ์์ธก 0.25)\n",
" ์์ธก 0%~50% ๊ตฌ๊ฐ 21๊ฑด: ํ๊ท ์์ธก 23.3%, ์ค์ ์ ์์จ 19.0%\n",
" ์์ธก 50%~90% ๊ตฌ๊ฐ 14๊ฑด: ํ๊ท ์์ธก 72.6%, ์ค์ ์ ์์จ 78.6%\n",
" ์์ธก 90%~100% ๊ตฌ๊ฐ 25๊ฑด: ํ๊ท ์์ธก 98.4%, ์ค์ ์ ์์จ 100.0%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ์ ์ ์์ก ๊ฒฝ๋ก ๋ฐ ์์ฐ ๋ฐฐ์ ํํฉ ๊ฐ์ํ 2D/3D ์์ก ๊ด์ ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ ์ ์์ก ๊ฒฝ๋ก ๋ฐ ์์ฐ ๋ฐฐ์ ํํฉ ๊ฐ์ํ ๋์๋ณด๋๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์์ก ์์ฒญ๋ณ ์์ฐ ๋ฐฐ์ ๊ณผ ๊ฒฝ๋ก๋ฅผ ๊ด์ ๋์๋ณด๋๋ก ๊ฐ์ํํ๊ณ , ์
๋ ฅ ๊ฒ์ฆ๊ธฐ์ ์๋ฌ ์ฒ๋ฆฌ๊ธฐ๋ก ์์ธ ์ํฉ์์๋ ๋ฉ์ถ์ง ์๊ฒ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ฒญ์ ์ ์ฌ๋์ ๋ง๊ฒ ํธ๋ ์ผ๋ฌยทํธ๋ญ์ ๋ฐฐ์ ํ๊ณ ๊ฒฝ๋ก ์ง๋, ์์ฐ ๊ฐ๋๋ฅ , ์ดํ ์ผ์ (์ง์ฐ ํ์), ์์ฒญ๋ณ ๋น์ฉ์ 2x2 ๋์๋ณด๋๋ก ๊ทธ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/utils/mov02_validator.py ๋ฐ ์๋ฌ ํธ๋ค๋ฌ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ฌ์ฉ์์๊ฒ ์๊ธฐ ์ฌ์ด ์๋ฌ ๋ฉ์์ง ๋ฐ ์กฐ์น ๊ฐ์ด๋ ์ ๊ณต\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์์ธ ์ํฉ ๋ฐ์ ์ ํฌ๋์(Crash) 0๊ฑด, ์๋ฌ ๋ก๊ทธ ๊ธฐ๋ก๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์์ธ ์ฃผ์
ํ
์คํธ 10์ข
ํต๊ณผ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ ํ์ผ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋น์ ์ ๋ฐ์ดํฐ(๊ฒฐ์ธก, ํน์๋ฌธ์, ๋์ฉ๋) ์คํธ๋ ์ค ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์์ก ์์ฒญ์ ์์ฐ(ํธ๋ญยทํธ๋ ์ผ๋ฌ)๊ณผ ์ํ ํํผ ๊ฒฝ๋ก๋ฅผ ๋ฐฐ์ ํด ๊ด์ ๋์๋ณด๋(๊ฒฝ๋ก ์ง๋ยท์์ฐ ๊ฐ๋๋ฅ ยท์ดํ ์ผ์ ยท๋น์ฉ ์ค์ )๋ก ๊ฐ์ํํ๊ณ , ์
๋ ฅ ๊ฒ์ฆ๊ธฐยท์๋ฌ ์ฒ๋ฆฌ๊ธฐ๋ก ์์ธ ์ฃผ์
10์ข
์ ํฌ๋์ ์์ด ๊ธฐ๋กยท์๋ดํ๋ฉฐ ์ฌ์๋์ ๋กค๋ฐฑ์ ํ์ธํ๋ค.\"\"\"\n",
"import heapq\n",
"import json\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๋๋ก๋งยท์์ก ์์ฐ ์์ฑ๊ธฐ (์ค์ ์ง๋ช
ยท๋ถ๋ยท์ฅ๋น ์ ์ ์์)\n",
"# ๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง(๋
ธ๋=๊ต์ฐจ์ , ๊ฐ์ =๋๋ก ๊ตฌ๊ฐ)์ ๋๋ก ๋ฑ๊ธยท๊ต๋ ํ์คยท๊ฐ์ ์ํ ๊ตฌ์ญ์ ๋ถ์ธ๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"ASSETS = { # ๊ต์ก์ฉ ๊ฐ์ ์ ์(์ค์ ์ฅ๋น ์ ์ ์๋)\n",
" \"truck\": {\"name\": \"์คํ ํธ๋ญ\", \"cap_t\": 5.0, \"speed_kmh\": 50, \"cost_km\": 1.0, \"weight_t\": 10.0},\n",
" \"trailer\": {\"name\": \"๋ํ ํธ๋ ์ผ๋ฌ\", \"cap_t\": 20.0, \"speed_kmh\": 40, \"cost_km\": 2.4, \"weight_t\": 30.0},\n",
" \"airlift\": {\"name\": \"์์ก๊ธฐ\", \"cap_t\": 12.0, \"speed_kmh\": 300, \"cost_km\": 9.0, \"weight_t\": 0.0},\n",
"}\n",
"ROAD_FACTOR = {\"paved\": 1.0, \"unpaved\": 0.6} # ๋๋ก ๋ฑ๊ธ๋ณ ์๋ ๊ณ์\n",
"\n",
"\n",
"def make_network(seed=5, rows=6, cols=8, spacing=5.0):\n",
" \"\"\"๊ฒฉ์ํ ๊ฐ์ ๋๋ก๋ง. ๋
ธ๋ ์ขํ(km)์ ๊ฐ์ ๋ชฉ๋ก(๊ฑฐ๋ฆฌยท๋๋ก๋ฑ๊ธยท๊ต๋ ํ์ค)์ ๋๋ ค์ค๋ค.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" nodes = {f\"N{r:02d}{c:02d}\": (c * spacing + rng.uniform(-1, 1), r * spacing + rng.uniform(-1, 1))\n",
" for r in range(rows) for c in range(cols)}\n",
" edges = []\n",
"\n",
" def add(a, b):\n",
" (x1, y1), (x2, y2) = nodes[a], nodes[b]\n",
" bridge = bool(rng.random() < 0.15)\n",
" edges.append({\"id\": f\"E{len(edges):03d}\", \"u\": a, \"v\": b,\n",
" \"km\": round(float(np.hypot(x2 - x1, y2 - y1) * rng.uniform(1.0, 1.25)), 2),\n",
" \"road\": \"paved\" if rng.random() < 0.65 else \"unpaved\", \"bridge\": bridge,\n",
" \"limit_t\": float(rng.choice([20, 40, 60])) if bridge else None, \"open\": True})\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if c + 1 < cols:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r:02d}{c + 1:02d}\")\n",
" if r + 1 < rows:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c:02d}\")\n",
" if r + 1 < rows and c + 1 < cols and rng.random() < 0.25:\n",
" add(f\"N{r:02d}{c:02d}\", f\"N{r + 1:02d}{c + 1:02d}\")\n",
" return nodes, edges\n",
"\n",
"\n",
"def make_threats(seed=9, n=3, xlim=(8, 28), ylim=(3, 22)):\n",
" \"\"\"๊ฐ์ ์ํ ๊ตฌ์ญ(์ํ). p = ๊ตฌ๊ฐ 1ํ ํต๊ณผ ์ ํผํด ๊ฐ๋ฅ์ฑ(๊ต์ก์ฉ ๊ฐ์ ๊ฐ).\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" return [{\"id\": f\"TZ{i + 1}\", \"x\": float(rng.uniform(*xlim)), \"y\": float(rng.uniform(*ylim)),\n",
" \"r\": float(rng.uniform(3, 5)), \"p\": float(round(rng.uniform(0.05, 0.2), 3))} for i in range(n)]\n",
"\n",
"\n",
"def edge_risk(e, nodes, threats):\n",
" (x1, y1), (x2, y2) = nodes[e[\"u\"]], nodes[e[\"v\"]]\n",
" s = np.linspace(0, 1, 6)\n",
" risk = 0.0\n",
" for t in threats:\n",
" if np.min(np.hypot(x1 + (x2 - x1) * s - t[\"x\"], y1 + (y2 - y1) * s - t[\"y\"])) < t[\"r\"]:\n",
" risk = max(risk, t[\"p\"])\n",
" return risk\n",
"\n",
"\n",
"def edge_minutes(e, asset=\"truck\"):\n",
" return e[\"km\"] / (ASSETS[asset][\"speed_kmh\"] * ROAD_FACTOR[e[\"road\"]]) * 60\n",
"\n",
"\n",
"def passable(e, gross_t):\n",
" return e[\"open\"] and (not e[\"bridge\"] or e[\"limit_t\"] >= gross_t)\n",
"\n",
"\n",
"def build_adj(nodes, edges, weight_fn):\n",
" \"\"\"weight_fn(e)๊ฐ None์ด๋ฉด ํตํ ๋ถ๊ฐ ๊ฐ์ ์ผ๋ก ๋ณด๊ณ ์ ์ธํ๋ค.\"\"\"\n",
" adj = {n: [] for n in nodes}\n",
" for e in edges:\n",
" w = weight_fn(e)\n",
" if w is not None:\n",
" adj[e[\"u\"]].append((e[\"v\"], w, e))\n",
" adj[e[\"v\"]].append((e[\"u\"], w, e))\n",
" return adj\n",
"\n",
"\n",
"def dijkstra(adj, src, dst):\n",
" \"\"\"๋ค์ต์คํธ๋ผ ์ต๋จ๊ฒฝ๋ก. (๋น์ฉ, ๋
ธ๋ ๊ฒฝ๋ก, ๊ฐ์ ๋ชฉ๋ก). ๋๋ฌ ๋ถ๊ฐ๋ฉด (inf, [], []).\"\"\"\n",
" dist, prev, done, pq = {src: 0.0}, {}, set(), [(0.0, src)]\n",
" while pq:\n",
" d, u = heapq.heappop(pq)\n",
" if u in done:\n",
" continue\n",
" done.add(u)\n",
" if u == dst:\n",
" break\n",
" for v, w, e in adj[u]:\n",
" if d + w < dist.get(v, float(\"inf\")):\n",
" dist[v], prev[v] = d + w, (u, e)\n",
" heapq.heappush(pq, (d + w, v))\n",
" if dst not in done:\n",
" return float(\"inf\"), [], []\n",
" path, es = [dst], []\n",
" while path[-1] != src:\n",
" u, e = prev[path[-1]]\n",
" es.append(e); path.append(u)\n",
" return dist[dst], path[::-1], es[::-1]\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"nodes, edges = make_network()\n",
"threats = make_threats()\n",
"DEPOT = \"N0000\"\n",
"for e in edges:\n",
" if e[\"id\"] in (\"E017\", \"E040\", \"E046\", \"E071\"):\n",
" e[\"open\"] = False\n",
" e[\"risk\"] = edge_risk(e, nodes, threats)\n",
"for e in edges: # ๊ฐ์ ๊ณ ๋ฆฝ ์ง์ : N0506 ์ฃผ๋ณ ๋๋ก ์ ๋ถ ํ๊ดด\n",
" if \"N0506\" in (e[\"u\"], e[\"v\"]):\n",
" e[\"open\"] = False\n",
"\n",
"FLEET = {**{f\"T{i:02d}\": \"truck\" for i in range(1, 9)}, **{f\"L{i:02d}\": \"trailer\" for i in range(1, 3)}}\n",
"LOG = [] # ์๋ฌยท์ฒ๋ฆฌ ๋ก๊ทธ(๊ฐ์ฌ์ฉ)\n",
"\n",
"\n",
"class InputError(Exception):\n",
" def __init__(self, code, msg, guide):\n",
" super().__init__(msg); self.code, self.msg, self.guide = code, msg, guide\n",
"\n",
"\n",
"class TransientError(Exception):\n",
" pass\n",
"\n",
"\n",
"def validate(req, seen):\n",
" \"\"\"์
๋ ฅ ๊ฒ์ฆ: ํ์ ํ๋ยทํ์ยท๋ฒ์ยท์ค๋ณตยท์กด์ฌ ์ฌ๋ถ. ์๋ฐ ์ InputError(์ฝ๋, ๋ฉ์์ง, ์กฐ์น ๊ฐ์ด๋).\"\"\"\n",
" for k in (\"req_id\", \"dest\", \"cargo_t\", \"deadline_min\"):\n",
" if k not in req or req[k] is None:\n",
" raise InputError(\"V01\", f\"ํ์ ํญ๋ชฉ '{k}' ๋๋ฝ\", f\"์์ก ์์ฒญ์์ '{k}' ์นธ์ ์ฑ์ ๋ค์ ์ ์ถ\")\n",
" rid = str(req[\"req_id\"])\n",
" if not rid.replace(\"-\", \"\").isalnum() or len(rid) > 20:\n",
" raise InputError(\"V02\", f\"์์ฒญ๋ฒํธ ํ์ ์ค๋ฅ({rid[:12]}...)\", \"์๋ฌธยท์ซ์ยท'-'๋ง 20์ ์ด๋ด๋ก ์
๋ ฅ\")\n",
" if rid in seen:\n",
" raise InputError(\"V03\", f\"์ค๋ณต ์์ฒญ๋ฒํธ {rid}\", \"๊ธฐ์กด ์์ฒญ ์์ ํ๋ฉด์ ์ฌ์ฉํ๊ฑฐ๋ ์ ๋ฒํธ ๋ฐ๊ธ\")\n",
" try:\n",
" cargo, dl = float(req[\"cargo_t\"]), float(req[\"deadline_min\"])\n",
" except (TypeError, ValueError):\n",
" raise InputError(\"V04\", f\"์ซ์ ํ์ ์ค๋ฅ(cargo_t={req['cargo_t']!r})\", \"๋จ์ ์์ด ์ซ์๋ง ์
๋ ฅ(์: 12)\")\n",
" if not np.isfinite(cargo) or not np.isfinite(dl):\n",
" raise InputError(\"V05\", \"์ ์ฌ๋ยท๊ธฐํ ๊ฐ์ด ๋น์ด ์์(NaN)\", \"์ ์ฌ๋(t)๊ณผ ๋์ฐฉ ๊ธฐํ(๋ถ)์ ํ์ธ\")\n",
" if cargo <= 0 or dl <= 0:\n",
" raise InputError(\"V06\", f\"์์ยท0 ๊ฐ(cargo_t={cargo}, deadline={dl})\", \"0๋ณด๋ค ํฐ ๊ฐ์ผ๋ก ์
๋ ฅ\")\n",
" if req[\"dest\"] not in nodes:\n",
" raise InputError(\"V07\", f\"์กด์ฌํ์ง ์๋ ๋ชฉ์ ์ง {req['dest']}\", \"๋๋ก๋ง ๋
ธ๋ ๋ชฉ๋ก์์ ๋ชฉ์ ์ง ์ ํ\")\n",
" cap = sum(ASSETS[k][\"cap_t\"] for k in FLEET.values())\n",
" if cargo > cap:\n",
" raise InputError(\"V08\", f\"์ ์ฌ๋ {cargo}t > ๊ฐ์ฉ ์์ก๋ฅ๋ ฅ {cap}t\", \"๋ถํ ์์ก ์์ฒญ ๋๋ ์๊ธ ์ ๋ ์ง์ ์์ฒญ\")\n",
" return cargo, dl\n",
"\n",
"\n",
"def route(dest, kind, gross, fail_first=False, _state={\"n\": 0}):\n",
" if fail_first and _state[\"n\"] == 0: # ๊ฒฝ๋ก ๊ณ์ฐ ์๋น์ค ์ผ์ ์ฅ์ ๋ชจ์(์ฒซ ํธ์ถ๋ง ์คํจ)\n",
" _state[\"n\"] += 1\n",
" raise TransientError(\"๊ฒฝ๋ก ๊ณ์ฐ ์๊ฐ ์ด๊ณผ\")\n",
" adj = build_adj(nodes, edges, lambda e: edge_minutes(e, kind) + 150 * e[\"risk\"] if passable(e, gross) else None)\n",
" cost, path, es = dijkstra(adj, DEPOT, dest)\n",
" if not path:\n",
" raise InputError(\"R01\", f\"{dest}๊น์ง ํตํ ๊ฐ๋ฅํ ๊ฒฝ๋ก ์์\", \"๋๋ก ๋ณต๊ตฌ ์ ๋ณด ํ์ธ ํ ๊ณต์ค์์ก ๊ฒํ ๋ฅผ ํต์ ๊ด์ ์์ฒญ\")\n",
" return path, es\n",
"\n",
"\n",
"def assign(req, cargo, dl, state, fail_first=False):\n",
" \"\"\"์ ์ฌ๋์ ๋ง๊ฒ ์์ฐ์ ๊ณ ๋ฅด๊ณ (ํธ๋ ์ผ๋ฌ ์ฐ์ , ๋๋จธ์ง ํธ๋ญ) ๊ฒฝ๋ก๋ฅผ ๋ฐฐ์ ํ๋ค. ์คํจ ์ ํธ์ถ์๊ฐ ๋กค๋ฐฑ.\"\"\"\n",
" picks, left = [], cargo\n",
" for vid in sorted(FLEET, key=lambda v: FLEET[v] != \"trailer\"): # 10t ์ด๊ณผ๋ถ์ ํธ๋ ์ผ๋ฌ ์ฐ์ , ๋๋จธ์ง๋ ํธ๋ญ\n",
" if left <= 0:\n",
" break\n",
" if vid in state[\"busy\"] or (FLEET[vid] == \"trailer\" and left <= 10):\n",
" continue\n",
" picks.append(vid); state[\"busy\"].add(vid); left -= ASSETS[FLEET[vid]][\"cap_t\"] # ์ํ ๋ณ๊ฒฝ(๋กค๋ฐฑ ๋์)\n",
" if left > 0:\n",
" raise InputError(\"A01\", \"๊ฐ์ฉ ์์ฐ ๋ถ์กฑ\", \"๋๊ธฐ ํ ์ฌ๋ฐฐ์ ๋๋ ๋ค๋ฅธ ์์ฒญ๊ณผ ํตํฉ ์์ก\")\n",
" out = []\n",
" for vid in picks:\n",
" kind = FLEET[vid]\n",
" for attempt in range(3): # ์ผ์ ์ฅ์ ์๋ ์ฌ์๋(์ต๋ 3ํ)\n",
" try:\n",
" path, es = route(req[\"dest\"], kind, ASSETS[kind][\"weight_t\"] + min(ASSETS[kind][\"cap_t\"], cargo), fail_first and attempt == 0)\n",
" break\n",
" except TransientError as ex:\n",
" LOG.append({\"req\": req[\"req_id\"], \"level\": \"WARN\", \"code\": \"T01\", \"msg\": f\"{ex} -> ์ฌ์๋ {attempt + 1}ํ\"})\n",
" mins = sum(edge_minutes(e, kind) for e in es); km = sum(e[\"km\"] for e in es)\n",
" out.append({\"req\": req[\"req_id\"], \"vid\": vid, \"kind\": kind, \"dest\": req[\"dest\"], \"path\": path, \"km\": km, \"min\": mins,\n",
" \"cost\": km * ASSETS[kind][\"cost_km\"], \"threat\": sum(e[\"risk\"] > 0 for e in es), \"ontime\": mins <= dl})\n",
" return out\n",
"\n",
"\n",
"def process(batch):\n",
" state, seen, result = {\"busy\": set()}, set(), []\n",
" for req in batch:\n",
" snap = set(state[\"busy\"]) # ํธ๋์ญ์
์์ ์์ ์ค๋
์ท\n",
" try:\n",
" cargo, dl = validate(req, seen)\n",
" rows = assign(req, cargo, dl, state, fail_first=req.get(\"_flaky\", False))\n",
" seen.add(str(req[\"req_id\"])); result += rows\n",
" LOG.append({\"req\": req[\"req_id\"], \"level\": \"INFO\", \"code\": \"OK\", \"msg\": f\"๋ฐฐ์ {len(rows)}๋\"})\n",
" except InputError as ex:\n",
" state[\"busy\"] = snap # ๋กค๋ฐฑ: ๋ถ๋ถ ๋ฐฐ์ ํ ์์ฐ ๋ฐํ\n",
" LOG.append({\"req\": str(req.get(\"req_id\"))[:20], \"level\": \"ERROR\", \"code\": ex.code, \"msg\": ex.msg, \"guide\": ex.guide})\n",
" except Exception as ex: # ์์ ๋ชปํ ์ค๋ฅ๋ ํฌ๋์ ์์ด ๊ธฐ๋ก\n",
" state[\"busy\"] = snap\n",
" LOG.append({\"req\": str(req.get(\"req_id\"))[:20], \"level\": \"ERROR\", \"code\": \"X99\", \"msg\": repr(ex)[:60], \"guide\": \"์ด์ ๋ด๋น์์๊ฒ ๋ก๊ทธ ์ ๋ฌ\"})\n",
" return result, state\n",
"\n",
"\n",
"# ---------------- 1) ์ ์ ์์ฒญ ๋ฐฐ์ + ๊ด์ ๋์๋ณด๋ ----------------\n",
"NORMAL = [{\"req_id\": f\"REQ-02{i:02d}\", \"dest\": d, \"cargo_t\": c, \"deadline_min\": dl}\n",
" for i, (d, c, dl) in enumerate([(\"N0307\", 12, 90), (\"N0107\", 8, 80), (\"N0407\", 4, 70), (\"N0205\", 9, 75), (\"N0503\", 5, 60)])]\n",
"NORMAL[3][\"_flaky\"] = True # ๊ฒฝ๋ก ๊ณ์ฐ ์ผ์ ์ฅ์ ๊ฐ ๋๋ ์ฌ์๋๋ก ์ฒ๋ฆฌ๋๋์ง ํ์ธ\n",
"plan, st = process(NORMAL)\n",
"df = pd.DataFrame(plan)\n",
"print(\"[๊ด์ ํํฉํ] ์์ฒญ๋ณ ์์ฐ ๋ฐฐ์ ยท๊ฒฝ๋ก [๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"summ = df.groupby(\"req\").agg(์์ฐ=(\"vid\", lambda s: \",\".join(s)), ๋ชฉ์ ์ง=(\"dest\", \"first\"), ๊ฑฐ๋ฆฌkm=(\"km\", \"max\"),\n",
" ์์๋ถ=(\"min\", \"max\"), ์ํํต๊ณผ=(\"threat\", \"max\"), ์ ์=(\"ontime\", \"all\"), ๋น์ฉ=(\"cost\", \"sum\")).round(1)\n",
"print(summ.to_string())\n",
"util = len(st[\"busy\"]) / len(FLEET)\n",
"print(f\"์์ฐ ๊ฐ๋๋ฅ {util:.0%} ({len(st['busy'])}/{len(FLEET)}๋), ์ด ์ดํ๋น์ฉ {df['cost'].sum():.1f}(๋จ๊ฐx km), \"\n",
" f\"์ ์ ๋์ฐฉ ์์ {summ['์ ์'].mean():.0%}, ์ํ ๊ตฌ์ญ ํต๊ณผ ์ฐจ๋ {int((df['threat'] > 0).sum())}๋\")\n",
"\n",
"print(\"์ผ์ ์ฅ์ ์๋ ์ฌ์๋:\", [f\"{l['req']} {l['msg']}\" for l in LOG if l[\"code\"] == \"T01\"], \"-> ์ฌ์๋ ํ ์ ์ ๋ฐฐ์ \")\n",
"print(\"์ง์ฐ ์์ ์์ฒญ์ ๋ถ์์์ผ๋ก ํ์ํ๊ณ ํต์ ๊ด์๊ฒ ๊ธฐํ ์กฐ์ ๋๋ ์์ฐ ๋ณ๊ฒฝ์ ๊ฑด์ํ๋ค.\")\n",
"\n",
"# ---------------- 2) ์์ธ ์ฃผ์
ํ
์คํธ 10์ข
----------------\n",
"LOG.clear()\n",
"BAD = [{\"dest\": \"N0307\", \"cargo_t\": 3, \"deadline_min\": 60}, # 1 ํ์ ํญ๋ชฉ ๋๋ฝ\n",
" {\"req_id\": \"REQ-