{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# ๐ฏ [OPS02] ๋ค์ค ์ผ์ ๊ฒฝ๋ณด ์ค๋ณต ํญ์ ํตํฉ ์๋ฎฌ๋ ์ดํฐ\n",
"### Google Colab ์ค์ต ํ๊ฒฝ (์์ ยท์ ํฌ ์ํ / Level 3)\n",
"\n",
"[](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/OPS02/OPS02_solution.ipynb)\n",
"\n",
"---\n",
"\n",
"## ๐ 1. ํ๋ก์ ํธ ๊ฐ์ ๋ฐ ๋ฐฐ๊ฒฝ\n",
"\n",
"| ํญ๋ชฉ | ๋ด์ฉ |\n",
"| :--- | :--- |\n",
"| **๊ณผ์ ๋ถ๋ฅ** | ์์ ยท์ ํฌ ์ํ (Level 3) |\n",
"| **ํ์ต ์์ ์๊ฐ** | 10๊ฐ ๋ฏธ์
(5์ผ / ์ด 27.5์๊ฐ) |\n",
"| **ํ๊ฒฝ ์ฌ์** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n",
"\n",
"### ๐ ํ๋ก์ ํธ ๋ชฉํ\n",
"๋ฏธ๊ตญ-์ด๋ ๊ฐ ๋ฌด๋ ฅ ์ถฉ๋ ์ฌ๋ก์์๋ ๋๋ก ยท๋ฏธ์ฌ์ผ์ด ์ฌ๋ฌ ๋ฐฉํฅ์์ ๋์์ ๋ค์ด์ค๋ฉด์ ๋ ์ด๋, ๊ดํ(EO/IR), ์ํฅ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๊ฐ์ ๊ฒฝ๋ณดํด ์ํฉํ์ ์ค๋ณต ํญ์ ์ด ์์์ง๊ณ , ์ด ๋๋ฌธ์ ์ํ ์๋ฅผ ์๋ชป ์ธ๊ฑฐ๋ ๋์์ด ๋ฆ์ด์ง๋ ๋ฌธ์ ๊ฐ ์ฃผ๋ชฉ๋ฐ์๋ค. ๋ณธ ํ๋ก์ ํธ์์๋ ๊ฐ์ ๋ฐฉ์ด ์ง์ญ์ ๋ฐฐ์นํ ๊ฐ์ ์ผ์ 4๊ธฐ์ ๊ฐ์ ๋นํ์ฒด ๊ถค์ ์ผ๋ก ๊ต์ก์ฉ ๊ฒฝ๋ณด ๋ฐ์ดํฐ๋ฅผ ์ฝ๋๋ก ๋ง๋ค์ด, ์ฐ๋ ๊ท๊ฒฉ๊ณผ ํตํฉ ์คํค๋ง ์ค๊ณ, ์๊ฐ ๋๊ธฐํ์ ENU ์ขํ ๋ณํ ์ ์ฒ๋ฆฌ, ์ต๋จ๊ฑฐ๋ฆฌ ๋งค์นญ Baseline, ํ์ฅ ์นผ๋ง ํํฐ(EKF) ํญ์ ์ถ์ ยทํตํฉ, ์ตํฉ ์ ๋ขฐ๋ ์ง์, ์ ์ ์ํฉํ(COP), ์กฐ์๊ด HITL ๊ฒํ , ํต์ ๋จ์ ยท์ฌ๋ฐ ์คํธ๋ ์ค ์ํ, AAR ์ฌ๋ณด์ , ์ต์ข
ํตํฉ๊น์ง ๋ค์ค ์ผ์ ์ค๋ณต ํญ์ ํตํฉ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ๋จ๊ณ๋ณ๋ก ๊ตฌํํ๋ค. ์ต์ข
๋ชฉํ๋ ๋นํ์ฒด ํ๋๋ฅผ ํ๋์ ํญ์ ์ผ๋ก ์ ํํ ๋ณด์ฌ ์ฃผ์ด ์งํ๊ด์ ์ธ์ง ๋ถํ๋ฅผ ์ค์ด๊ณ , ํญ์ ํตํฉยท๋ถ๋ฆฌ ๊ฐ์ ํ๋จ์ ์กฐ์๊ด์ด ๊ทผ๊ฑฐ์ ํจ๊ป ํ์ ํ๋๋ก ์ค๊ณํ๋ ๊ฒ์ด๋ค.\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. ๋ค์ข
์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ์ฐ๋ ๊ท๊ฒฉ ๋ถ์ ๋ฐ ์ค๋ณต ํญ์ ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ\n",
"**์์์๊ฐ**: ์ฝ 1.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ฏธ๊ตญ-์ด๋ ๋ถ์ ๋ฑ ์ ์ ๋ฐฉ๊ณต ํ๊ฒฝ์ ๋ถ์ํ์ฌ ๋ ์ด๋, EO/IR ๊ดํ์ผ์, ์ํฅ ํ์ง๊ธฐ ๋ฑ ์ด๊ธฐ์ข
์ผ์์ ๋ฐ์ดํฐ ์ฐ๋ ๊ท๊ฒฉ๊ณผ ์ค๋ณต ํญ์ (Track) ํตํฉ ์คํค๋ง๋ฅผ ์ค๊ณํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ ์ด๋ยทEO/IRยท์ํฅ ์ผ์์ ๊ฒฝ๋ณด ์ฐ๋ ๊ท๊ฒฉ์ ๋ถ์ํ๊ณ , ์์ ๊ฒฝ๋ณด์ ํตํฉ ํญ์ ์ ํจ๊ป ๋ด๋ ๋ฐ์ดํฐ์
์คํค๋ง์ ๊ฒ์ฆ๊ธฐ๋ฅผ ๋ง๋ ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ฐ์ ์ผ์ 4๊ธฐ์ ์ ํ, ์์น, ์ต๋ ํ์ง๊ฑฐ๋ฆฌ, ๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ, ํ์งํ๋ฅ ์ ๋ฑ๋ก๋ถ๋ก ์ ๋ฆฌํ๊ณ ๊ท๊ฒฉํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/ops02_input_schema.json ๋ฐ docs/ops02_data_spec.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์์ ยท์ ํฌ ์ํ ์ค๋ฌด ๋ฐ ๋ฏธ๊ตญ-์ด๋ ์ ์ ์ ์ฅ ํ๊ฒฝ๊ณผ์ ๊ตฐ์ฌ์ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์คํค๋ง ํ๋ ์ ์ ์์ฑ๋ 100%, ์ํ ๋ฐ์ดํฐ ๊ฒ์ฆ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์
๋ ฅ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์คํฌ๋ฆฝํธ ํต๊ณผ ๋ฐ ๋ช
์ธ์ ๋ฌธ์ํ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: JSON Schema ๊ฒ์ฆ ๋๊ตฌ ๋ฐ ์ํ ๋ฐ์ดํฐ ํ์ฑ ๋จ์ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ ์ด๋ยทEO/IRยท์ํฅ ์ผ์ ๊ฒฝ๋ณด ๋ฉ์์ง์ ํตํฉ ํญ์ (Fused Track)์ ๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ ์ํ๊ณ , ๊ฐ์ ๊ฒฝ๋ณด ์ํ 20๊ฑด์ ๊ฒ์ฆํ๋ฉฐ ์ค๋ฅ ์ฃผ์
์ํ์ผ๋ก ๊ฒ์ฆ๊ธฐ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import json\n",
"import re\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 1] ๋ค์ข
์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ์ฐ๋ ๊ท๊ฒฉ ๋ถ์ ๋ฐ ์ค๋ณต ํญ์ ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# 1) ์คํค๋ง ์ ์: ์ผ์ ๊ฒฝ๋ณด(์์), ์ ์ ํ๋ผ๋ฏธํฐ, ํตํฉ ํญ์ (์ถ๋ ฅ)\n",
"SCHEMA = {\n",
" \"report\": {\n",
" \"msg_id\": {\"type\": str, \"pattern\": r\"^M\\d{5}$\"},\n",
" \"sensor\": {\"type\": str, \"enum\": list(SENSORS)},\n",
" \"stype\": {\"type\": str, \"enum\": [\"radar\", \"eoir\", \"acoustic\"]},\n",
" \"t_sensor\": {\"type\": (int, float), \"min\": -5, \"max\": 86400},\n",
" \"range_km\": {\"type\": (int, float), \"min\": 0, \"max\": 60},\n",
" \"az_deg\": {\"type\": (int, float), \"min\": 0, \"max\": 360},\n",
" \"sr\": {\"type\": (int, float), \"min\": 0.001, \"max\": 5},\n",
" \"saz\": {\"type\": (int, float), \"min\": 0.01, \"max\": 10},\n",
" },\n",
" \"tactical\": {\n",
" \"weather\": {\"type\": str, \"enum\": list(WEATHER)},\n",
" \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n",
" \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 5000},\n",
" },\n",
" \"fused_track\": {\n",
" \"track_id\": {\"type\": str, \"pattern\": r\"^FT\\d{3}$\"},\n",
" \"state_enu\": {\"type\": list, \"length\": 4},\n",
" \"sensors\": {\"type\": list, \"min_len\": 1},\n",
" \"confidence\": {\"type\": (int, float), \"min\": 0, \"max\": 1},\n",
" \"status\": {\"type\": str, \"enum\": [\"TENTATIVE\", \"CONFIRMED\", \"HOLD\", \"SPLIT\", \"MERGED\"]},\n",
" \"source_msgs\": {\"type\": list, \"min_len\": 1},\n",
" },\n",
"}\n",
"\n",
"\n",
"def check(name, value, rule):\n",
" # [TODO : ํ์ ์ค์ต 1] ๋ค์ข
์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ์ฐ๋ ๊ท๊ฒฉ ๋ถ์ ๋ฐ ์ค๋ณต ํญ์ ๋ฐ์ดํฐ์
์คํค๋ง ์ค๊ณ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def validate(rec, group):\n",
" errs = []\n",
" for k, rule in SCHEMA[group].items():\n",
" errs += [f\"{k}: ํ์ ํ๋ ๋๋ฝ\"] if k not in rec else check(k, rec[k], rule)\n",
" if group == \"report\" and not errs and SENSORS[rec[\"sensor\"]][\"type\"] != rec[\"stype\"]:\n",
" errs.append(f\"stype: ์ผ์ {rec['sensor']}์ ์ ํ๊ณผ ๋ถ์ผ์น\")\n",
" return errs\n",
"\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ์๋๋ฆฌ์ค์์ ๊ฒฝ๋ณด ์ํ 20๊ฑด ์ถ์ถ\n",
"scn = make_scenario(seed=1, n=4, T=10)\n",
"rng = np.random.default_rng(1)\n",
"samples = [{k: r[k] for k in SCHEMA[\"report\"]} for r in rng.choice(scn[\"reports\"], 20, replace=False)]\n",
"tactical = {\"weather\": \"clear\", \"position_error_m\": 35.0, \"comm_delay_ms\": 120}\n",
"track = {\"track_id\": \"FT001\", \"state_enu\": [3.2, 4.1, 0.05, -0.02], \"sensors\": [\"RDR-A\", \"EOIR-C\"],\n",
" \"confidence\": 0.87, \"status\": \"CONFIRMED\", \"source_msgs\": [\"M00012\", \"M00015\"]}\n",
"n_err = sum(len(validate(s, \"report\")) for s in samples) + len(validate(tactical, \"tactical\")) + len(validate(track, \"fused_track\"))\n",
"print(f\"๊ฒฝ๋ณด ์ํ {len(samples)}๊ฑด + ์ ์ ํ๋ผ๋ฏธํฐ 1๊ฑด + ํตํฉ ํญ์ 1๊ฑด ๊ฒ์ฆ: ์ค๋ฅ {n_err}๊ฑด\")\n",
"\n",
"# 3) ์ค๋ฅ ์ฃผ์
์ํ\n",
"bad = json.loads(json.dumps(samples[0]))\n",
"bad[\"msg_id\"] = \"MSG-1\" # ํ์ ์ค๋ฅ\n",
"bad[\"sensor\"] = \"RDR-Z\" # ๋ฑ๋ก๋์ง ์์ ์ผ์\n",
"bad[\"az_deg\"] = 412.0 # ๋ฒ์ ์ค๋ฅ\n",
"del bad[\"sr\"] # ํ์ ํ๋ ๋๋ฝ\n",
"bad_trk = dict(track, sensors=[], status=\"AUTO_FIRE\") # ์ผ์ ์๋ ํญ์ , ํ์ฉ๋์ง ์์ ์ํ\n",
"print(\"\\n[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\")\n",
"for e in validate(bad, \"report\") + validate(bad_trk, \"fused_track\"):\n",
" print(\" -\", e)\n",
"\n",
"# 4) ๋ช
์ธ์ ์์ฝ: ํ๋ ์ ์ + ์ผ์ ๋ฑ๋ก๋ถ\n",
"rows = []\n",
"for g, fields in SCHEMA.items():\n",
" for k, r in fields.items():\n",
" t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n",
" rows.append((g, k, t, \", \".join(f\"{x}={y}\" for x, y in r.items() if x != \"type\")))\n",
"spec = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ๋\", \"์๋ฃํ\", \"๊ท์น\"])\n",
"print(f\"\\n[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ] ํ๋ {len(spec)}๊ฐ ์ ์ (์ ์ ์์ฑ๋ {spec['๊ท์น'].str.len().gt(0).mean():.0%})\")\n",
"print(spec.to_string(index=False))\n",
"reg = pd.DataFrame(SENSORS).T[[\"type\", \"pos\", \"max_r\", \"sr\", \"saz\", \"pd\"]]\n",
"reg.columns = [\"์ ํ\", \"์์น(ENU km)\", \"์ต๋๊ฑฐ๋ฆฌ(km)\", \"๊ฑฐ๋ฆฌ์ค์ฐจ(km)\", \"๋ฐฉ์์ค์ฐจ(๋)\", \"ํ์งํ๋ฅ \"]\n",
"print(\"\\n[๊ฐ์ ์ผ์ ๋ฑ๋ก๋ถ]\")\n",
"print(reg.to_string())\n",
"\n",
"# 5) ์ค๋ณต ๊ฒฝ๋ณด ํํฉ: ๊ฐ์ ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ช ๊ฐ ์ผ์๊ฐ ๋์์ ๋ณด๊ณ ํ๋๊ฐ\n",
"df = pd.DataFrame(scn[\"reports\"])\n",
"df[\"sec\"] = df[\"t_sensor\"].round().astype(int)\n",
"dup = df[df[\"truth\"] != \"FA\"].groupby([\"truth\", \"sec\"])[\"sensor\"].nunique()\n",
"print(f\"\\n๋นํ์ฒด 1๊ฐ๋น ๊ฐ์ ์ด์ ํ๊ท {dup.mean():.1f}๊ฐ ์ผ์๊ฐ ๊ฒฝ๋ณด(์ต๋ {dup.max()}๊ฐ) -> ํตํฉ ํญ์ ์คํค๋ง๊ฐ ํ์ํ ์ด์ \")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
01 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒฝ๋ณด ์ํ 20๊ฑด + ์ ์ ํ๋ผ๋ฏธํฐ 1๊ฑด + ํตํฉ ํญ์ 1๊ฑด ๊ฒ์ฆ: ์ค๋ฅ 0๊ฑด\n",
"\n",
"[์ค๋ฅ ์ฃผ์
๊ฒฐ๊ณผ]\n",
" - msg_id: ํ์ ๋ถ์ผ์น 'MSG-1'\n",
" - sensor: ํ์ฉ๋์ง ์์ ๊ฐ 'RDR-Z'\n",
" - az_deg: ์ต๋๊ฐ 360 ์ด๊ณผ\n",
" - sr: ํ์ ํ๋ ๋๋ฝ\n",
" - sensors: ์ต์ 1๊ฐ ํ์\n",
" - status: ํ์ฉ๋์ง ์์ ๊ฐ 'AUTO_FIRE'\n",
"\n",
"[๋ฐ์ดํฐ ๋ช
์ธ ์์ฝ] ํ๋ 17๊ฐ ์ ์ (์ ์ ์์ฑ๋ 100%)\n",
" ๊ทธ๋ฃน ํ๋ ์๋ฃํ ๊ท์น\n",
" report msg_id str pattern=^M\\d{5}$\n",
" report sensor str enum=['RDR-A', 'RDR-B', 'EOIR-C', 'ACU-D']\n",
" report stype str enum=['radar', 'eoir', 'acoustic']\n",
" report t_sensor number min=-5, max=86400\n",
" report range_km number min=0, max=60\n",
" report az_deg number min=0, max=360\n",
" report sr number min=0.001, max=5\n",
" report saz number min=0.01, max=10\n",
" tactical weather str enum=['clear', 'night', 'fog', 'rain']\n",
" tactical position_error_m number min=0, max=500\n",
" tactical comm_delay_ms number min=0, max=5000\n",
"fused_track track_id str pattern=^FT\\d{3}$\n",
"fused_track state_enu list length=4\n",
"fused_track sensors list min_len=1\n",
"fused_track confidence number min=0, max=1\n",
"fused_track status str enum=['TENTATIVE', 'CONFIRMED', 'HOLD', 'SPLIT', 'MERGED']\n",
"fused_track source_msgs list min_len=1\n",
"\n",
"[๊ฐ์ ์ผ์ ๋ฑ๋ก๋ถ]\n",
" ์ ํ ์์น(ENU km) ์ต๋๊ฑฐ๋ฆฌ(km) ๊ฑฐ๋ฆฌ์ค์ฐจ(km) ๋ฐฉ์์ค์ฐจ(๋) ํ์งํ๋ฅ \n",
"RDR-A radar (0.0, 0.0) 40.0 0.08 0.4 0.95\n",
"RDR-B radar (16.0, -6.0) 40.0 0.1 0.5 0.93\n",
"EOIR-C eoir (5.0, 9.0) 18.0 0.4 0.15 0.88\n",
"ACU-D acoustic (10.0, 2.0) 9.0 0.6 2.0 0.8\n",
"\n",
"๋นํ์ฒด 1๊ฐ๋น ๊ฐ์ ์ด์ ํ๊ท 2.9๊ฐ ์ผ์๊ฐ ๊ฒฝ๋ณด(์ต๋ 4๊ฐ) -> ํตํฉ ํญ์ ์คํค๋ง๊ฐ ํ์ํ ์ด์ \n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
02. ์ด์ข
์ผ์ ์๊ฐ ๋๊ธฐํ(Time-sync) ๋ฐ ๊ณต๊ฐ ์ขํ๊ณ(ENU/ECEF) ๋ณํ ์ ์ฒ๋ฆฌ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ด๊ธฐ์ข
์ผ์ ๊ฐ์ ์๊ฐ ๋๊ธฐํ(Time-sync) ์ค์ฐจ๋ฅผ ๋ณด์ ํ๊ณ ๊ฐ ์ผ์์ ๊ตญ์ ์ขํ๊ณ๋ฅผ ๊ณตํต ์ ์ ์ขํ๊ณ(ENU/ECEF)๋ก ๋ณํํ๋ ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์๋ง๋ค ๋ค๋ฅธ ์๊ณ ์ค์ฐจ๋ฅผ ๋ณด์ ํ๊ณ ์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์)๋ฅผ ๊ณตํต ์ ์ ์ขํ(ENU)๋ก ๋ฐ๊พธ๋ ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๋ชจ๋์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ธฐ์ค ์ ํธ(heartbeat)์ ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ์ผ๋ก ์ผ์๋ณ ์๊ณ ์ค์ฐจ๋ฅผ ์ถ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/ops02_preprocessor.py ๋ฐ ์ ์ฒ๋ฆฌ ์ ํ ๋ฐ์ดํฐ์
` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ํ๊ฒฝ ํน์ ์ ๋
ธ์ด์ฆ(์คํธํ, ๋ฐ์ฌํ, ์ผ์ ๋จ์ ๋ฑ) ์ ๊ฑฐ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ฐ์ดํฐ ์ ์ ์ฒ๋ฆฌ ์๋ 1,000๊ฑด/์ด ์ด์, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ ์ ํ์ดํ๋ผ์ธ ์๋ํ ๋ฐ ๋จ์ ํ
์คํธ ์ผ์ด์ค 5์ข
ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: pytest ๊ธฐ๋ฐ ์ ์ฒ๋ฆฌ ์ ํฉ์ฑ ํ
์คํธ ๋ฐ ๊ฒฐ์ธก์น ๋ณด์ ์จ ํ์ธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ด์ข
์ผ์ ๊ฒฝ๋ณด์ ์ ์ (์ค๋ณตยท๊ฒฐ์ธกยท๋ฐ์ฌํ ์ ๊ฑฐ), ์๊ฐ ๋๊ธฐํ(Time-sync) ์ค์ฐจ ๋ณด์ , ์ผ์ ๊ตญ์ ๊ทน์ขํ -> ๊ณตํต ENU ์ขํ ๋ณํ(๊ฐ์ ๊ธฐ์ค์ ์ ์๊ฒฝ๋ยทECEF ๋ณํ ํฌํจ)์ ๊ตฌํํ๊ณ ์ฒ๋ฆฌ ์๋ยท์ ํ๋๋ฅผ ๋จ์ ์ํ์ผ๋ก ํ์ธํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 2] ์ด์ข
์ผ์ ์๊ฐ ๋๊ธฐํ(Time-sync) ๋ฐ ๊ณต๊ฐ ์ขํ๊ณ(ENU/ECEF) ๋ณํ ์ ์ฒ๋ฆฌ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 2] ์ด์ข
์ผ์ ์๊ฐ ๋๊ธฐํ(Time-sync) ๋ฐ ๊ณต๊ฐ ์ขํ๊ณ(ENU/ECEF) ๋ณํ ์ ์ฒ๋ฆฌ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# 1) ์๊ฒฝ๋(LLA) <-> ECEF <-> ENU ๋ณํ (WGS84 ํ์์ฒด ๊ณต์)\n",
"A, E2 = 6378137.0, 6.69437999014e-3\n",
"ORIGIN = (10.0, 20.0, 0.0) # [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ฐ์ ๊ธฐ์ค์ ์๊ฒฝ๋(์ค์ ์์น ์๋)\n",
"\n",
"\n",
"def lla_to_ecef(lat, lon, h):\n",
" la, lo = np.radians(lat), np.radians(lon)\n",
" n = A / np.sqrt(1 - E2 * np.sin(la) ** 2)\n",
" return np.array([(n + h) * np.cos(la) * np.cos(lo), (n + h) * np.cos(la) * np.sin(lo), (n * (1 - E2) + h) * np.sin(la)])\n",
"\n",
"\n",
"def _rot(lat, lon):\n",
" la, lo = np.radians(lat), np.radians(lon)\n",
" return np.array([[-np.sin(lo), np.cos(lo), 0],\n",
" [-np.sin(la) * np.cos(lo), -np.sin(la) * np.sin(lo), np.cos(la)],\n",
" [np.cos(la) * np.cos(lo), np.cos(la) * np.sin(lo), np.sin(la)]])\n",
"\n",
"\n",
"def ecef_to_enu(p, origin=ORIGIN):\n",
" return _rot(*origin[:2]) @ (p - lla_to_ecef(*origin))\n",
"\n",
"\n",
"def enu_to_ecef(enu, origin=ORIGIN):\n",
" return _rot(*origin[:2]).T @ enu + lla_to_ecef(*origin)\n",
"\n",
"\n",
"def ecef_to_lla(p):\n",
" lon = np.arctan2(p[1], p[0]); r = np.hypot(p[0], p[1]); lat = np.arctan2(p[2], r * (1 - E2))\n",
" for _ in range(5):\n",
" n = A / np.sqrt(1 - E2 * np.sin(lat) ** 2)\n",
" h = r / np.cos(lat) - n\n",
" lat = np.arctan2(p[2], r * (1 - E2 * n / (n + h)))\n",
" return np.degrees(lat), np.degrees(lon), h\n",
"\n",
"\n",
"print(\"[์ผ์ ์ค์น ์ง์ (๊ฐ์ ๊ธฐ์ค์ ๊ธฐ์ค, ์ค์ ์์น ์๋): ๊ฐ์ ENU -> ์๊ฒฝ๋ -> ECEF -> ENU ์๋ณต ๋ณํ]\")\n",
"rt = []\n",
"for sid, s in SENSORS.items():\n",
" enu = np.array([s[\"pos\"][0] * 1000, s[\"pos\"][1] * 1000, 30.0])\n",
" lla = ecef_to_lla(enu_to_ecef(enu))\n",
" back = ecef_to_enu(lla_to_ecef(*lla))\n",
" rt.append(np.linalg.norm(back - enu))\n",
" print(f\"{sid:7s} ์๋ {lla[0]:.5f} ๊ฒฝ๋ {lla[1]:.5f} ๊ณ ๋ {lla[2]:6.1f} m ์๋ณต ์ค์ฐจ {rt[-1] * 1000:.3f} mm\")\n",
"\n",
"# 2) [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์์ ๊ฒฝ๋ณด + ์ ์ ์ก์ ์ฃผ์
(์ฌ์ ์ก ์ค๋ณต, ๊ฒฐ์ธก, ๋ฐ์ฌํ ๊ฑฐ๋ฆฌ๊ฐ, ์ค์ฐจ๊ฐ ๋๋ฝ)\n",
"scn = make_scenario(seed=2, n=8, T=60, special=\"fast\")\n",
"rng = np.random.default_rng(2)\n",
"raw = [dict(r) for r in scn[\"reports\"]]\n",
"n0 = len(raw)\n",
"raw += [dict(raw[i]) for i in rng.choice(n0, int(0.03 * n0), replace=False)] # ์ฌ์ ์ก ์ค๋ณต 3%\n",
"for i in rng.choice(n0, int(0.02 * n0), replace=False):\n",
" raw[i][\"az_deg\"] = None # ์ธก์ ๊ฐ ๊ฒฐ์ธก 2%\n",
"for i in rng.choice(n0, int(0.01 * n0), replace=False):\n",
" raw[i][\"range_km\"] = SENSORS[raw[i][\"sensor\"]][\"max_r\"] * rng.uniform(1.5, 3) # ๋ฐ์ฌํ(๋ค์ค๊ฒฝ๋ก) 1%\n",
"miss_sigma = rng.choice(n0, int(0.05 * n0), replace=False)\n",
"for i in miss_sigma:\n",
" raw[i][\"sr\"] = None # ์ค์ฐจ๊ฐ ๋๋ฝ 5% -> ๋ฑ๋ก๋ถ ๊ฐ์ผ๋ก ๋ณด์ \n",
"rng.shuffle(raw) # ๋์ฐฉ ์์ ๋ค์์\n",
"\n",
"t0 = time.perf_counter()\n",
"filled = 0\n",
"for r in raw:\n",
" if r[\"sr\"] is None:\n",
" r[\"sr\"] = SENSORS[r[\"sensor\"]][\"sr\"]; filled += 1\n",
"cleaned, why = clean(raw)\n",
"off = estimate_offsets(scn[\"heartbeats\"])\n",
"enu = sorted((to_enu(r, off) for r in cleaned), key=lambda r: r[\"t\"])\n",
"dt = time.perf_counter() - t0\n",
"print(f\"\\n์์ ๊ฒฝ๋ณด {len(raw)}๊ฑด -> ์ ์ {len(enu)}๊ฑด, ์ ๊ฑฐ ์ฌ์ {why}, ์ค์ฐจ๊ฐ ๋ณด์ {filled}๊ฑด(๋ณด์ ๋ฅ {filled / len(miss_sigma):.0%})\")\n",
"print(f\"์ฒ๋ฆฌ ์๋ {len(raw) / dt:,.0f} ๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์: {'๋ฌ์ฑ' if len(raw) / dt >= 1000 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"# 3) ์๊ฐ ๋๊ธฐํ: ๊ธฐ์ค ์ ํธ๋ก ์ถ์ ํ ์ผ์ ์๊ณ ์ค์ฐจ\n",
"print(\"\\n์ผ์ ์ค์ ์๊ณ์ค์ฐจ(s) ์ถ์ ๊ฐ(s) ์ถ์ ์ค์ฐจ(s)\")\n",
"for sid, s in SENSORS.items():\n",
" print(f\"{sid:7s} {s['clock']:14.3f} {off[sid]:10.3f} {abs(off[sid] - s['clock']):11.3f}\")\n",
"\n",
"\n",
"def along_bias(reps, use_t):\n",
" \"\"\"๊ณ ์ ํ์ ๊ฒฝ๋ณด๊ฐ ์ ๋ต ์์น๋ณด๋ค ์งํ ๋ฐฉํฅ์ผ๋ก ์ผ๋ง๋ ์์๊ฑฐ๋ ๋ค์ฒ์ง๋์ง(m, ํ๊ท ). ์๊ณ ์ค์ฐจ x ์๋๋งํผ ์๊ธด๋ค.\"\"\"\n",
" e = []\n",
" for r in reps:\n",
" if scn[\"kinds\"].get(r[\"truth\"]) == \"fast\":\n",
" tr, ts = scn[\"targets\"][r[\"truth\"]], np.arange(scn[\"T\"])\n",
" g = np.array([np.interp(r[use_t], ts, tr[:, j]) for j in range(4)])\n",
" v = g[2:] / np.linalg.norm(g[2:])\n",
" e.append(np.dot([r[\"x\"] - g[0], r[\"y\"] - g[1]], v))\n",
" return float(np.mean(e)) * 1000\n",
"\n",
"\n",
"print(\"\\n[๊ณ ์ ํ์ ๊ฒฝ๋ณด์ ์งํ ๋ฐฉํฅ ์์น ํธํฅ: ์๊ณ ์ค์ฐจ x ์๋๋งํผ ์ด๊ธ๋๋ค]\")\n",
"print(\"์ผ์ ๋๊ธฐํ ์ (m) ๋๊ธฐํ ํ(m)\")\n",
"bias = {}\n",
"for sid in SENSORS:\n",
" rs = [r for r in enu if r[\"sensor\"] == sid]\n",
" bias[sid] = (along_bias(rs, \"t_sensor\"), along_bias(rs, \"t\"))\n",
" print(f\"{sid:7s} {bias[sid][0]:+11.0f} {bias[sid][1]:+12.0f}\")\n",
"\n",
"# 4) ๋จ์ ์ํ 5์ข
(pytest ์์ด assert๋ก ์ํ)\n",
"tests = {\n",
" \"ENU ์๋ณต ๋ณํ ์ค์ฐจ 1 mm ๋ฏธ๋ง\": max(rt) < 1e-3,\n",
" \"๋ฐฉ์ 0๋ = ์ ๋ถ(+y) ๋ฐฉํฅ\": np.allclose([to_enu({\"sensor\": \"RDR-A\", \"t_sensor\": 0, \"range_km\": 5, \"az_deg\": 0}, {})[k] for k in \"xy\"], [0, 5]),\n",
" \"์ค๋ณต ๋ฉ์์ง ์ ๋ ์ ๊ฑฐ\": len({r[\"msg_id\"] for r in enu}) == len(enu),\n",
" \"๋ฐ์ฌํ(์ต๋๊ฑฐ๋ฆฌ ์ด๊ณผ) ์ ๋ ์ ๊ฑฐ\": all(r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1 for r in enu),\n",
" \"์๊ณ์ค์ฐจ ์ถ์ ์ค์ฐจ 0.05์ด ๋ฏธ๋ง\": all(abs(off[s] - SENSORS[s][\"clock\"]) < 0.05 for s in SENSORS),\n",
" \"์ถ๋ ฅ ์๊ฐ ์ค๋ฆ์ฐจ์ ์ ๋ ฌ\": all(a[\"t\"] <= b[\"t\"] for a, b in zip(enu, enu[1:])),\n",
"}\n",
"for k, v in tests.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"print(f\"๋จ์ ์ํ {sum(tests.values())}/{len(tests)} ํต๊ณผ, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ {1 - sum(tests.values()) / len(tests):.0%}\")\n",
"\n",
"# 5) ๊ทธ๋ฆผ: ์ผ์๋ณ ์งํ ๋ฐฉํฅ ์์น ํธํฅ(๋๊ธฐํ ์ /ํ)\n",
"sids = list(SENSORS)\n",
"x = np.arange(len(sids))\n",
"plt.figure(figsize=(7, 4))\n",
"plt.bar(x - 0.2, [bias[s][0] for s in sids], 0.4, label=\"before time-sync\", color=\"tab:gray\")\n",
"plt.bar(x + 0.2, [bias[s][1] for s in sids], 0.4, label=\"after time-sync\", color=\"tab:blue\")\n",
"plt.axhline(0, color=\"k\", lw=0.8)\n",
"plt.xticks(x, sids); plt.ylabel(\"along-track bias (m)\"); plt.legend()\n",
"plt.title(\"Fast-target along-track bias by sensor (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
02 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ผ์ ์ค์น ์ง์ (๊ฐ์ ๊ธฐ์ค์ ๊ธฐ์ค, ์ค์ ์์น ์๋): ๊ฐ์ ENU -> ์๊ฒฝ๋ -> ECEF -> ENU ์๋ณต ๋ณํ]\n",
"RDR-A ์๋ 10.00000 ๊ฒฝ๋ 20.00000 ๊ณ ๋ 30.0 m ์๋ณต ์ค์ฐจ 0.000 mm\n",
"RDR-B ์๋ 9.94572 ๊ฒฝ๋ 20.14591 ๊ณ ๋ 52.9 m ์๋ณต ์ค์ฐจ 0.000 mm\n",
"EOIR-C ์๋ 10.08136 ๊ฒฝ๋ 20.04562 ๊ณ ๋ 38.4 m ์๋ณต ์ค์ฐจ 0.000 mm\n",
"ACU-D ์๋ 10.01807 ๊ฒฝ๋ 20.09121 ๊ณ ๋ 38.2 m ์๋ณต ์ค์ฐจ 0.000 mm\n",
"\n",
"์์ ๊ฒฝ๋ณด 1815๊ฑด -> ์ ์ 1712๊ฑด, ์ ๊ฑฐ ์ฌ์ {'์ค๋ณต': 52, '๊ฒฐ์ธก': 35, '๋ฒ์ ๋ฐ': 16}, ์ค์ฐจ๊ฐ ๋ณด์ 88๊ฑด(๋ณด์ ๋ฅ 100%)\n",
"์ฒ๋ฆฌ ์๋ 112,246 ๊ฑด/์ด (๋ชฉํ 1,000๊ฑด/์ด ์ด์: ๋ฌ์ฑ)\n",
"\n",
"์ผ์ ์ค์ ์๊ณ์ค์ฐจ(s) ์ถ์ ๊ฐ(s) ์ถ์ ์ค์ฐจ(s)\n",
"RDR-A 0.000 -0.009 0.009\n",
"RDR-B 0.420 0.418 0.002\n",
"EOIR-C -0.650 -0.656 0.006\n",
"ACU-D 0.930 0.938 0.008\n",
"\n",
"[๊ณ ์ ํ์ ๊ฒฝ๋ณด์ ์งํ ๋ฐฉํฅ ์์น ํธํฅ: ์๊ณ ์ค์ฐจ x ์๋๋งํผ ์ด๊ธ๋๋ค]\n",
"์ผ์ ๋๊ธฐํ ์ (m) ๋๊ธฐํ ํ(m)\n",
"RDR-A -6 -8\n",
"RDR-B -71 -6\n",
"EOIR-C +95 -10\n",
"ACU-D -179 -33\n",
"[PASS] ENU ์๋ณต ๋ณํ ์ค์ฐจ 1 mm ๋ฏธ๋ง\n",
"[PASS] ๋ฐฉ์ 0๋ = ์ ๋ถ(+y) ๋ฐฉํฅ\n",
"[PASS] ์ค๋ณต ๋ฉ์์ง ์ ๋ ์ ๊ฑฐ\n",
"[PASS] ๋ฐ์ฌํ(์ต๋๊ฑฐ๋ฆฌ ์ด๊ณผ) ์ ๋ ์ ๊ฑฐ\n",
"[PASS] ์๊ณ์ค์ฐจ ์ถ์ ์ค์ฐจ 0.05์ด ๋ฏธ๋ง\n",
"[PASS] ์ถ๋ ฅ ์๊ฐ ์ค๋ฆ์ฐจ์ ์ ๋ ฌ\n",
"๋จ์ ์ํ 6/6 ํต๊ณผ, ์ ์ฒ๋ฆฌ ์ค๋ฅ์จ 0%\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
03. ์ต๋จ๊ฑฐ๋ฆฌ ๋งค์นญ ๊ธฐ๋ฐ ๋จ์ผ ํญ์ -์ผ์ ๊ฒฝ๋ณด 1์ฐจ ์ตํฉ(Fusion) ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ต๋จ ๊ฑฐ๋ฆฌ(Euclidean/Mahalanobis) ๋งค์นญ ๊ธฐ๋ฐ์ผ๋ก ๋์ผ ๋นํ์ฒด์์ ๋ฐ์ํ ๋ค์ค ์ผ์ ๊ฒฝ๋ณด๋ฅผ 1์ฐจ ํด๋ฌ์คํฐ๋งํ๋ ํญ์ ์ตํฉ(Fusion) Baseline ๋ก์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋งค ์ค์บ๋ง๋ค ์ฌ๋ฌ ์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ต๋จ๊ฑฐ๋ฆฌ(์ ํด๋ฆฌ๋ยท๋งํ ๋ผ๋
ธ๋น์ค) ๋งค์นญ์ผ๋ก ๋ฌถ์ด ๋นํ์ฒด 1๊ฐ๋น 1๊ฐ์ ์ตํฉ ๊ฒฝ๋ณด๋ฅผ ๋ง๋๋ Baseline์ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ ๋ฐ๋๊ฐ ๋์ ์ผ์ ๊ฒฝ๋ณด๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ค๋ฅธ ์ผ์ ๊ฒฝ๋ณด๋ฅผ ๊ฑฐ๋ฆฌ ๊ฒ์ดํธ ์์์ ๋ฌถ๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/ops02_baseline_model.py ๋ฐ ๊ธฐ๋ณธ ์ถ๋ก ๊ฒฐ๊ณผ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์๊ณ ๋ฆฌ์ฆ์ ๋
ผ๋ฆฌ์ ํ๋ฆ ๋ฐ ๊ตฐ์ฌ์ ํ๋จ ๊ธฐ์ค์ ํฌ๋ช
์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๊ธฐ๋ณธ ๋ฐ์ดํฐ์
๋์ ํ์ง/์ฒ๋ฆฌ ์ ํ๋ 75% ์ด์ ๋ฌ์ฑ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋จ์ ๊ธฐ๋ฅ ๋์ ํ์ธ ๋ฐ ๊ธฐ๋ณธ ์๋๋ฆฌ์ค ์คํ ๋ก๊ทธ ์์ฑ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๊ธฐ์ค ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
๋์ ์ถ๋ก ๊ฒฐ๊ณผ ์ค์ฐจ์จ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋งค ์ค์บ(1์ด)๋ง๋ค ์ฌ๋ฌ ์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ต๋จ๊ฑฐ๋ฆฌ(์ ํด๋ฆฌ๋/๋งํ ๋ผ๋
ธ๋น์ค) ๋งค์นญ์ผ๋ก ๋ฌถ์ด ๋นํ์ฒด 1๊ฐ๋น 1๊ฐ์ ์ตํฉ ๊ฒฝ๋ณด๋ก ๋ง๋๋ 1์ฐจ ํญ์ ์ตํฉ(Fusion) Baseline์ ๊ตฌํํ๊ณ , ํตํฉ ์ ํ๋์ ํ๊ณ๋ฅผ ์ธก์ ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 3] ์ต๋จ๊ฑฐ๋ฆฌ ๋งค์นญ ๊ธฐ๋ฐ ๋จ์ผ ํญ์ -์ผ์ ๊ฒฝ๋ณด 1์ฐจ ์ตํฉ(Fusion) ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 3] ์ต๋จ๊ฑฐ๋ฆฌ ๋งค์นญ ๊ธฐ๋ฐ ๋จ์ผ ํญ์ -์ผ์ ๊ฒฝ๋ณด 1์ฐจ ์ตํฉ(Fusion) ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"\n",
"def fuse_scan(reps, gate, metric):\n",
" \"\"\"์ ๋ฐํ ์ผ์ ๊ฒฝ๋ณด๋ถํฐ ์ฐจ๋ก๋ก, ๊ฐ์ ์ผ์๊ฐ ์์ง ์๋ ๊ฐ์ฅ ๊ฐ๊น์ด ๋ฌถ์์ ๋ฃ๋๋ค(๊ฒ์ดํธ ๋ฐ์ด๋ฉด ์ ๋ฌถ์).\n",
" ๋ฌถ์ ์์น๋ ์ธก์ ๊ณต๋ถ์ฐ์ ์ญ์(์ ๋ณด๋) ๊ฐ์ค ํ๊ท ์ผ๋ก ๊ฐฑ์ ํ๋ค.\"\"\"\n",
" clusters = []\n",
" for r in sorted(reps, key=lambda r: SENSORS[r[\"sensor\"]][\"sr\"]):\n",
" p, C = np.array([r[\"x\"], r[\"y\"]]), meas_cov(r)\n",
" best, bd = None, gate\n",
" for c in clusters:\n",
" if r[\"sensor\"] in c[\"sensors\"]:\n",
" continue\n",
" d = p - c[\"pos\"]\n",
" dist = np.hypot(*d) if metric == \"euclid\" else np.sqrt(d @ np.linalg.solve(C + c[\"cov\"], d))\n",
" if dist < bd:\n",
" best, bd = c, dist\n",
" if best is None:\n",
" clusters.append({\"pos\": p, \"cov\": C, \"sensors\": {r[\"sensor\"]}, \"members\": [r], \"dist\": []})\n",
" else:\n",
" info = np.linalg.inv(best[\"cov\"]) + np.linalg.inv(C)\n",
" best[\"pos\"] = np.linalg.solve(info, np.linalg.solve(best[\"cov\"], best[\"pos\"]) + np.linalg.solve(C, p))\n",
" best[\"cov\"] = np.linalg.inv(info)\n",
" best[\"sensors\"].add(r[\"sensor\"]); best[\"members\"].append(r); best[\"dist\"].append(round(float(bd), 2))\n",
" return clusters\n",
"\n",
"\n",
"def evaluate_scans(reps, gate, metric):\n",
" \"\"\"ํตํฉ ์ ํ๋ = (์ค์บ, ์ค์ ๋นํ์ฒด) ์ค ์ ํํ 1๊ฐ์ ์์ํ ๋ฌถ์์ผ๋ก ํตํฉ๋ ๋น์จ.\"\"\"\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" ok = tot = n_cl = n_multi_fa = 0\n",
" t0 = time.perf_counter()\n",
" for k, rs in by.items():\n",
" cl = fuse_scan(rs, gate, metric)\n",
" n_cl += len(cl)\n",
" n_multi_fa += sum(1 for c in cl if len(c[\"sensors\"]) >= 2 and all(m[\"truth\"] == \"FA\" for m in c[\"members\"]))\n",
" for g in {r[\"truth\"] for r in rs if r[\"truth\"] != \"FA\"}:\n",
" hold = [c for c in cl if any(m[\"truth\"] == g for m in c[\"members\"])]\n",
" tot += 1\n",
" ok += len(hold) == 1 and all(m[\"truth\"] == g for m in hold[0][\"members\"])\n",
" ms = (time.perf_counter() - t0) * 1000 / len(by)\n",
" return {\"๊ฒฝ๋ณด\": len(reps), \"์ตํฉ ๋ฌถ์\": n_cl, \"ํตํฉ ์ ํ๋\": round(ok / tot, 3), \"๋ค์ค์ผ์ ๊ฑฐ์ง๋ฌถ์\": n_multi_fa, \"์ค์บ๋น ms\": round(ms, 2)}\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๊ธฐ์ยท๊ธฐ๋์ด ๋ค๋ฅธ ๊ฐ์ ์๋๋ฆฌ์ค 4๊ฐ\n",
"scns = [make_scenario(30 + i, n=6, weather=w, special=sp) for i, (w, sp) in enumerate([(\"clear\", None), (\"rain\", None), (\"clear\", \"turn\"), (\"fog\", \"formation\")])]\n",
"reps = [prepare(s) for s in scns]\n",
"\n",
"rows = []\n",
"for name, gate, metric in [(\"์ ํด๋ฆฌ๋ 1.0km\", 1.0, \"euclid\"), (\"๋งํ ๋ผ๋
ธ๋น์ค 3.0\", 3.0, \"mahal\")]:\n",
" for s, r in zip(scns, reps):\n",
" rows.append({\"๋ฐฉ์\": name, \"๊ธฐ์\": s[\"weather\"], **evaluate_scans(r, gate, metric)})\n",
"df = pd.DataFrame(rows)\n",
"print(df.to_string(index=False))\n",
"summ = df.groupby(\"๋ฐฉ์\")[[\"ํตํฉ ์ ํ๋\", \"์ค์บ๋น ms\"]].mean().round(3)\n",
"print(\"\\n\" + summ.to_string())\n",
"best = summ[\"ํตํฉ ์ ํ๋\"].max()\n",
"print(f\"\\n๋ชฉํ(ํตํฉ ์ ํ๋ 75% ์ด์) ๋ฌ์ฑ ์ฌ๋ถ: {'๋ฌ์ฑ' if best >= 0.75 else '๋ฏธ๋ฌ'} (์ต๊ณ {best:.1%})\")\n",
"\n",
"# ๊ฒ์ดํธ ํฌ๊ธฐ ๋ฏผ๊ฐ๋(๋ง์ ์๋๋ฆฌ์ค)\n",
"print(\"\\n[๊ฒ์ดํธ ๋ฏผ๊ฐ๋] ์์ผ๋ฉด ๊ฐ์ ๋นํ์ฒด๊ฐ ์ชผ๊ฐ์ง๊ณ , ํฌ๋ฉด ๋ค๋ฅธ ๋นํ์ฒดยท์ค๊ฒฝ๋ณด๊ฐ ์์ธ๋ค\")\n",
"for metric, gates in [(\"euclid\", [0.3, 0.6, 1.0, 1.5, 2.5]), (\"mahal\", [1.5, 2.0, 3.0, 4.0, 6.0])]:\n",
" print(metric, \" \".join(f\"{g}:{evaluate_scans(reps[0], g, metric)['ํตํฉ ์ ํ๋']:.3f}\" for g in gates))\n",
"\n",
"# ์๊ฐ ๋๊ธฐํ ํจ๊ณผ\n",
"e0 = evaluate_scans(prepare(scns[2], sync=False), 3.0, \"mahal\")[\"ํตํฉ ์ ํ๋\"]\n",
"print(f\"\\n์๊ฐ ๋๊ธฐํ ์๋ต ์(๊ธฐ๋ ์๋๋ฆฌ์ค) ํตํฉ ์ ํ๋ {e0:.3f} -> ๋๊ธฐํ ์ ์ฉ {df.iloc[6]['ํตํฉ ์ ํ๋']:.3f}\")\n",
"\n",
"# ํ๋จ ๊ทผ๊ฑฐ(ํฌ๋ช
์ฑ): ํ ์ค์บ์ ์ตํฉ ๊ฒฐ๊ณผ\n",
"k = 25\n",
"cl = fuse_scan([r for r in reps[0] if int(round(r[\"t\"])) == k], 3.0, \"mahal\")\n",
"print(f\"\\n[์ค์บ {k}์ด ์ตํฉ ๊ฒฐ๊ณผ ์์: ๋ฌถ์ {len(cl)}๊ฐ]\")\n",
"for c in sorted(cl, key=lambda c: -len(c[\"sensors\"]))[:5]:\n",
" print(f\"์์น({c['pos'][0]:6.2f},{c['pos'][1]:6.2f}) ์ผ์ {sorted(c['sensors'])} ๊ฒฐํฉ ๊ฑฐ๋ฆฌ {c['dist']} ์ ๋ต {sorted({m['truth'] for m in c['members']})}\")\n",
"print(\"ํ๊ณ: ์ค์บ๋ง๋ค ๋
๋ฆฝ์ ์ผ๋ก ๋ฌถ์ด ์๋ ์ ๋ณด๋ฅผ ์ฐ์ง ์์ผ๋ฏ๋ก, ๊ฐ๊น์ด ํธ๋ยท๊ต์ฐจ ํ์ ๊ณผ ๋จ์ผ ์ผ์ ์ค๊ฒฝ๋ณด๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ด๋ ต๋ค(๋ฏธ์
4์์ ์นผ๋ง ํํฐ๋ก ๋ณด์).\")\n",
"\n",
"# ๊ทธ๋ฆผ: ์์ ๊ฒฝ๋ณด vs ์ตํฉ ๋ฌถ์\n",
"fig, ax = plt.subplots(1, 2, figsize=(11, 5), sharex=True, sharey=True)\n",
"colors = dict(zip(SENSORS, [\"tab:blue\", \"tab:green\", \"tab:orange\", \"tab:red\"]))\n",
"for r in [r for r in reps[0] if int(round(r[\"t\"])) == k]:\n",
" ax[0].scatter(r[\"x\"], r[\"y\"], color=colors[r[\"sensor\"]], s=18)\n",
"for sid, c in colors.items():\n",
" ax[0].scatter([], [], color=c, label=sid)\n",
"for c in cl:\n",
" ax[1].scatter(*c[\"pos\"], s=40 + 40 * len(c[\"sensors\"]), color=\"tab:purple\" if len(c[\"sensors\"]) > 1 else \"lightgray\", edgecolor=\"k\")\n",
"for g, tr in scns[0][\"targets\"].items():\n",
" ax[1].plot(tr[k, 0], tr[k, 1], \"k+\", ms=10)\n",
"ax[0].set_title(f\"Raw alerts at t={k}s ({sum(1 for r in reps[0] if int(round(r['t'])) == k)})\"); ax[0].legend(fontsize=8)\n",
"ax[1].set_title(f\"Fused clusters ({len(cl)}), + = truth, size = #sensors\")\n",
"for a in ax:\n",
" a.set_xlabel(\"East (km)\"); a.set_ylabel(\"North (km)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
03 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
" ๋ฐฉ์ ๊ธฐ์ ๊ฒฝ๋ณด ์ตํฉ ๋ฌถ์ ํตํฉ ์ ํ๋ ๋ค์ค์ผ์ ๊ฑฐ์ง๋ฌถ์ ์ค์บ๋น ms\n",
"์ ํด๋ฆฌ๋ 1.0km clear 1425 586 0.906 0 1.44\n",
"์ ํด๋ฆฌ๋ 1.0km rain 1284 703 0.950 0 1.22\n",
"์ ํด๋ฆฌ๋ 1.0km clear 1409 585 0.903 0 1.49\n",
"์ ํด๋ฆฌ๋ 1.0km fog 1327 594 0.811 0 1.35\n",
"๋งํ ๋ผ๋
ธ๋น์ค 3.0 clear 1425 568 0.953 0 2.27\n",
"๋งํ ๋ผ๋
ธ๋น์ค 3.0 rain 1284 693 0.978 0 2.36\n",
"๋งํ ๋ผ๋
ธ๋น์ค 3.0 clear 1409 570 0.947 0 2.72\n",
"๋งํ ๋ผ๋
ธ๋น์ค 3.0 fog 1327 577 0.886 0 2.82\n",
"\n",
" ํตํฉ ์ ํ๋ ์ค์บ๋น ms\n",
"๋ฐฉ์ \n",
"๋งํ ๋ผ๋
ธ๋น์ค 3.0 0.941 2.542\n",
"์ ํด๋ฆฌ๋ 1.0km 0.893 1.375\n",
"\n",
"๋ชฉํ(ํตํฉ ์ ํ๋ 75% ์ด์) ๋ฌ์ฑ ์ฌ๋ถ: ๋ฌ์ฑ (์ต๊ณ 94.1%)\n",
"\n",
"[๊ฒ์ดํธ ๋ฏผ๊ฐ๋] ์์ผ๋ฉด ๊ฐ์ ๋นํ์ฒด๊ฐ ์ชผ๊ฐ์ง๊ณ , ํฌ๋ฉด ๋ค๋ฅธ ๋นํ์ฒดยท์ค๊ฒฝ๋ณด๊ฐ ์์ธ๋ค\n",
"euclid 0.3:0.300 0.6:0.642 1.0:0.906 1.5:0.972 2.5:0.964\n",
"mahal 1.5:0.411 2.0:0.675 3.0:0.953 4.0:0.989 6.0:0.983\n",
"\n",
"์๊ฐ ๋๊ธฐํ ์๋ต ์(๊ธฐ๋ ์๋๋ฆฌ์ค) ํตํฉ ์ ํ๋ 0.881 -> ๋๊ธฐํ ์ ์ฉ 0.947\n",
"\n",
"[์ค์บ 25์ด ์ตํฉ ๊ฒฐ๊ณผ ์์: ๋ฌถ์ 13๊ฐ]\n",
"์์น( 14.85, 2.39) ์ผ์ ['ACU-D', 'EOIR-C', 'RDR-A', 'RDR-B'] ๊ฒฐํฉ ๊ฑฐ๋ฆฌ [0.63, 2.08, 0.35] ์ ๋ต ['TGT05']\n",
"์์น( 7.30, 9.20) ์ผ์ ['EOIR-C', 'RDR-A', 'RDR-B'] ๊ฒฐํฉ ๊ฑฐ๋ฆฌ [1.19, 1.7] ์ ๋ต ['TGT04']\n",
"์์น( 10.20, -2.85) ์ผ์ ['EOIR-C', 'RDR-A', 'RDR-B'] ๊ฒฐํฉ ๊ฑฐ๋ฆฌ [1.55, 1.18] ์ ๋ต ['TGT00']\n",
"์์น( 12.85, -0.54) ์ผ์ ['EOIR-C', 'RDR-A', 'RDR-B'] ๊ฒฐํฉ ๊ฑฐ๋ฆฌ [1.58, 0.28] ์ ๋ต ['TGT01']\n",
"์์น( 0.08, 1.81) ์ผ์ ['EOIR-C', 'RDR-A', 'RDR-B'] ๊ฒฐํฉ ๊ฑฐ๋ฆฌ [0.8, 1.71] ์ ๋ต ['TGT03']\n",
"ํ๊ณ: ์ค์บ๋ง๋ค ๋
๋ฆฝ์ ์ผ๋ก ๋ฌถ์ด ์๋ ์ ๋ณด๋ฅผ ์ฐ์ง ์์ผ๋ฏ๋ก, ๊ฐ๊น์ด ํธ๋ยท๊ต์ฐจ ํ์ ๊ณผ ๋จ์ผ ์ผ์ ์ค๊ฒฝ๋ณด๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ด๋ ต๋ค(๋ฏธ์
4์์ ์นผ๋ง ํํฐ๋ก ๋ณด์).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
04. ์นผ๋ง ํํฐ(Kalman Filter) ๊ธฐ๋ฐ ๋ค์ค ์ผ์ ๊ฒฝ๋ณด ์ค๋ณต ํญ์ ์ถ์ ๋ฐ ๊ถค์ ํตํฉ ๊ณ ๋ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"ํ์ฅ ์นผ๋ง ํํฐ(EKF)๋ฅผ ์ ์ฉํ์ฌ ์ผ์๋ณ ์ธก์ ์ค์ฐจ๋ฅผ ๋ณด์ ํ๊ณ ๊ธ๊ฒฉํ ๊ธฐ๋์ ์ํํ๋ ๋นํ์ฒด์ ์ค๋ณต ํญ์ ์ ๋จ์ผ ๊ถค์ ์ผ๋ก ์ ๋ฐ ์ถ์ ยทํตํฉํ๋ ์๊ณ ๋ฆฌ์ฆ์ ๊ณ ๋ํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **ํ์ฅ ์นผ๋ง ํํฐ(EKF)์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN)์ผ๋ก ๋ค์ค ์ผ์ ๊ฒฝ๋ณด๋ฅผ ํ๋์ ํญ์ ์ผ๋ก ์ถ์ ยทํตํฉํ๊ณ ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค์์ ๊ฒ์ฆํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ํ(์์นยท์๋)๋ฅผ ์์ธกํ๊ณ ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ ํ๋ฉฐ, ๋งํ ๋ผ๋
ธ๋น์ค ๊ฒ์ดํธ์ ํ ๋น์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ํญ์ ์ ์ฐ๊ฒฐ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/ops02_advanced_detector.py ๋ฐ ์์ธ ํ์ง ํ
์คํธ ๊ฒฐ๊ณผ` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ธฐ๋ง/๊ต๋ ๊ณต๊ฒฉ์ ๋ํ ๋ฐฉ์ด ๋ก์ง์ ํ์ค์ฑ ๋ฐ ์ ๋ขฐ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋ณตํฉ ์ํ ํ์ง ์ฌํ์จ(Recall) 85% ์ด์, ์ฒ๋ฆฌ ์ง์ฐ 200ms ์ดํ\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์ ๋ํ ํ์ง ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํฉ์ฑ ์ํ ๋ฐ์ดํฐ์
์ฃผ์
์ ์ด์์น ํ์ง ์ ํ๋ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํ์ฅ ์นผ๋ง ํํฐ(EKF)์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN)์ผ๋ก ์ฌ๋ฌ ์ผ์ ๊ฒฝ๋ณด๋ฅผ ํ๋์ ํญ์ ์ผ๋ก ์ถ์ ยทํตํฉํ๋ ์ถ์ ๊ธฐ๋ฅผ ๊ณ ๋ํํ๊ณ , ์ ํยท๊ต์ฐจยทํธ๋ยท๊ณ ์ยท๊ธฐ๋ง ํญ์ ยท์ฌ๋ฐยทํต์ ๋จ์ ๋ฑ ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
์์ ์ฌํ์จยท์ฒ๋ฆฌ ์ง์ฐยท๊ฑฐ์ง ํญ์ ํจํด์ ๊ฒ์ฆํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 4] ์นผ๋ง ํํฐ(Kalman Filter) ๊ธฐ๋ฐ ๋ค์ค ์ผ์ ๊ฒฝ๋ณด ์ค๋ณต ํญ์ ์ถ์ ๋ฐ ๊ถค์ ํตํฉ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 4] ์นผ๋ง ํํฐ(Kalman Filter) ๊ธฐ๋ฐ ๋ค์ค ์ผ์ ๊ฒฝ๋ณด ์ค๋ณต ํญ์ ์ถ์ ๋ฐ ๊ถค์ ํตํฉ ๊ณ ๋ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# ---- ํ์ฅ ์นผ๋ง ํํฐ(EKF) ๋ค์ค ์ผ์ ํญ์ ์ถ์ ๊ธฐ ๊ณตํต ํจ์ ----\n",
"# ์ํ x = [๋(km), ๋ถ(km), ๋์(km/s), ๋ถ์(km/s)], ์ธก์ z = ์ผ์ ๊ธฐ์ค [๊ฑฐ๋ฆฌ(km), ๋ฐฉ์(rad)]\n",
"def ekf_predict(x, P, dt, q):\n",
" F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], float)\n",
" G = np.array([[dt ** 2 / 2, 0], [0, dt ** 2 / 2], [dt, 0], [0, dt]])\n",
" return F @ x, F @ P @ F.T + q * G @ G.T\n",
"\n",
"\n",
"def h_polar(x, spos):\n",
" dx, dy = x[0] - spos[0], x[1] - spos[1]\n",
" r2 = max(dx * dx + dy * dy, 1e-9); r = np.sqrt(r2)\n",
" return np.array([r, np.arctan2(dx, dy)]), np.array([[dx / r, dy / r, 0, 0], [dy / r2, -dx / r2, 0, 0]])\n",
"\n",
"\n",
"def innovation(x, P, rep):\n",
" s = SENSORS[rep[\"sensor\"]]\n",
" zp, H = h_polar(x, s[\"pos\"])\n",
" y = np.array([rep[\"range_km\"], np.radians(rep[\"az_deg\"])]) - zp\n",
" y[1] = (y[1] + np.pi) % (2 * np.pi) - np.pi # ๋ฐฉ์ ์ฐจ์ด๋ -pi~pi๋ก ๊ฐ์ผ๋ค\n",
" S = H @ P @ H.T + np.diag([rep[\"sr\"] ** 2, np.radians(rep[\"saz\"]) ** 2])\n",
" return y, H, S, float(y @ np.linalg.solve(S, y))\n",
"\n",
"\n",
"class Tracker:\n",
" \"\"\"์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ค์บ(1์ด) ๋จ์๋ก ๋ฐ์ EKF ํญ์ ์ ๊ฐฑ์ ํ๋ค. ๊ฐ์ ์ค์บ ์์์ ์ผ์๋ณ๋ก\n",
" ์์ฐจ ๊ฐฑ์ ํ๋ฏ๋ก, ์ฌ๋ฌ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ณด๋ฉด ํ๋์ ํญ์ ์ผ๋ก ํตํฉ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self, gate=9.21, q=4e-4, confirm=(3, 5), max_miss=4, merge_gate=6.0):\n",
" self.gate, self.q, self.confirm, self.max_miss, self.merge_gate = gate, q, confirm, max_miss, merge_gate\n",
" self.tracks, self.dead, self.nid, self.n_merged = [], [], 0, 0\n",
"\n",
" def _new(self, rep, k):\n",
" C = meas_cov(rep)\n",
" P = np.zeros((4, 4)); P[:2, :2] = C; P[2, 2] = P[3, 3] = 0.25 ** 2\n",
" self.tracks.append({\"id\": f\"FT{self.nid:03d}\", \"x\": np.array([rep[\"x\"], rep[\"y\"], 0, 0]), \"P\": P, \"k\": k,\n",
" \"scans\": [k], \"sensors\": {rep[\"sensor\"]: 1}, \"truths\": [rep[\"truth\"]], \"nis\": [],\n",
" \"hist\": [(k, rep[\"x\"], rep[\"y\"])], \"confirmed\": False, \"miss\": 0, \"born\": k, \"upd\": True})\n",
" self.nid += 1\n",
"\n",
" def _assign(self, tracks, rs, sid):\n",
" \"\"\"๊ฒ์ดํธ(๋งํ ๋ผ๋
ธ๋น์ค ๊ฑฐ๋ฆฌ) ์์์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN). ํ ๋น๋ ๋ณด๊ณ ์ ๋ฒํธ ์งํฉ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" if not tracks or not rs:\n",
" return set()\n",
" spos = SENSORS[sid][\"pos\"]\n",
" Z = np.array([[r[\"range_km\"], np.radians(r[\"az_deg\"])] for r in rs])\n",
" R = np.diag([rs[0][\"sr\"] ** 2, np.radians(rs[0][\"saz\"]) ** 2])\n",
" cost = np.full((len(tracks), len(rs)), 1e6)\n",
" for i, tr in enumerate(tracks):\n",
" zp, H = h_polar(tr[\"x\"], spos)\n",
" Y = Z - zp\n",
" Y[:, 1] = (Y[:, 1] + np.pi) % (2 * np.pi) - np.pi\n",
" Si = np.linalg.inv(H @ tr[\"P\"] @ H.T + R)\n",
" d2 = np.einsum(\"ij,jk,ik->i\", Y, Si, Y)\n",
" cost[i] = np.where(d2 < self.gate, d2, 1e6)\n",
" used = set()\n",
" for i, j in zip(*linear_sum_assignment(cost)):\n",
" if cost[i, j] < self.gate:\n",
" tr, r = tracks[i], rs[j]\n",
" y, H, S, d2 = innovation(tr[\"x\"], tr[\"P\"], r)\n",
" K = tr[\"P\"] @ H.T @ np.linalg.inv(S)\n",
" tr[\"x\"] = tr[\"x\"] + K @ y\n",
" tr[\"P\"] = (np.eye(4) - K @ H) @ tr[\"P\"]\n",
" tr[\"nis\"].append(d2); tr[\"truths\"].append(r[\"truth\"])\n",
" tr[\"sensors\"][sid] = tr[\"sensors\"].get(sid, 0) + 1\n",
" tr[\"upd\"] = True; used.add(j)\n",
" return used\n",
"\n",
" def _merge(self):\n",
" \"\"\"๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ฐ๋ผ๊ฐ๋ ์ค๋ณต ํญ์ (์์นยท์๋๊ฐ ํต๊ณ์ ์ผ๋ก ๊ฐ์)์ ์ค๋๋ ํญ์ ์ผ๋ก ํตํฉํ๋ค.\"\"\"\n",
" drop = set()\n",
" for a in range(len(self.tracks)):\n",
" for b in range(a + 1, len(self.tracks)):\n",
" A, B = self.tracks[a], self.tracks[b]\n",
" if A[\"id\"] in drop or B[\"id\"] in drop:\n",
" continue\n",
" d = A[\"x\"] - B[\"x\"]\n",
" d2 = float(d[:2] @ np.linalg.solve(A[\"P\"][:2, :2] + B[\"P\"][:2, :2], d[:2]))\n",
" if d2 < self.merge_gate and np.hypot(d[2], d[3]) < 0.05:\n",
" keep, lose = (A, B) if len(A[\"scans\"]) >= len(B[\"scans\"]) else (B, A)\n",
" for sid, n in lose[\"sensors\"].items():\n",
" keep[\"sensors\"][sid] = keep[\"sensors\"].get(sid, 0) + n\n",
" keep[\"truths\"] += lose[\"truths\"]; keep[\"scans\"] = sorted(set(keep[\"scans\"]) | set(lose[\"scans\"]))\n",
" keep[\"merged\"] = keep.get(\"merged\", 0) + 1\n",
" drop.add(lose[\"id\"])\n",
" self.tracks = [t for t in self.tracks if t[\"id\"] not in drop]\n",
" self.n_merged += len(drop)\n",
"\n",
" def step(self, k, reps):\n",
" for tr in self.tracks: # 1) ์์ธก\n",
" tr[\"x\"], tr[\"P\"] = ekf_predict(tr[\"x\"], tr[\"P\"], k - tr[\"k\"], self.q)\n",
" tr[\"k\"], tr[\"upd\"] = k, False\n",
" for sid in sorted({r[\"sensor\"] for r in reps}, key=lambda s: SENSORS[s][\"sr\"]): # 2) ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ \n",
" rs = [r for r in reps if r[\"sensor\"] == sid]\n",
" used = self._assign([t for t in self.tracks if t[\"confirmed\"]], rs, sid) # ํ์ ํญ์ ์ฐ์ \n",
" rest = [r for j, r in enumerate(rs) if j not in used]\n",
" used2 = self._assign([t for t in self.tracks if not t[\"confirmed\"]], rest, sid)\n",
" for j, r in enumerate(rest):\n",
" if j not in used2:\n",
" self._new(r, k)\n",
" if self.merge_gate > 0:\n",
" self._merge()\n",
" keep = []\n",
" for tr in self.tracks: # 3) ํ์ ยท์ญ์ ๊ด๋ฆฌ\n",
" if tr[\"upd\"]:\n",
" tr[\"miss\"] = 0\n",
" if tr[\"scans\"][-1] != k:\n",
" tr[\"scans\"].append(k)\n",
" tr[\"hist\"].append((k, tr[\"x\"][0], tr[\"x\"][1]))\n",
" else:\n",
" tr[\"miss\"] += 1\n",
" m, n = self.confirm\n",
" if not tr[\"confirmed\"] and sum(1 for s in tr[\"scans\"] if s > k - n) >= m:\n",
" tr[\"confirmed\"] = True\n",
" limit = self.max_miss if tr[\"confirmed\"] else 1\n",
" (keep if tr[\"miss\"] <= limit else self.dead).append(tr)\n",
" self.tracks = keep\n",
"\n",
" def all_tracks(self):\n",
" return self.dead + self.tracks\n",
"\n",
"\n",
"def run_tracker(reps, **kw):\n",
" trk = Tracker(**kw)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.step(k, by.get(k, []))\n",
" return trk.all_tracks()\n",
"\n",
"\n",
"def label_track(tr):\n",
" vals, cnt = np.unique(tr[\"truths\"], return_counts=True)\n",
" return str(vals[np.argmax(cnt)]), float(cnt.max() / cnt.sum())\n",
"\n",
"\n",
"def evaluate(tracks, scn, min_reports=5):\n",
" \"\"\"์ ๋ต ์ฐ๊ฒฐ ํ๊ฐ: ์ฌํ์จ, ํญ์ ๋ถํ (ํ ํ์ ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ, ์์น ์ค์ฐจ.\"\"\"\n",
" conf = [t for t in tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"label\"], t[\"purity\"] = label_track(t)\n",
" cnt = pd.Series([r[\"truth\"] for r in scn[\"reports\"] if r[\"truth\"].startswith(\"TGT\")]).value_counts()\n",
" truths = [k for k, v in cnt.items() if v >= min_reports]\n",
" per = {g: [t for t in conf if t[\"label\"] == g] for g in truths}\n",
" found = [g for g in truths if per[g]]\n",
" split = sum(max(0, len(v) - 1) for v in per.values())\n",
" false = [t for t in conf if not t[\"label\"].startswith(\"TGT\")]\n",
" merge = [t for t in conf if t[\"label\"].startswith(\"TGT\") and t[\"purity\"] < 0.7]\n",
" err = []\n",
" for g in found:\n",
" for t in per[g]:\n",
" for k, x, y in t[\"hist\"][3:]:\n",
" if 0 <= k < scn[\"T\"]:\n",
" err.append(np.hypot(x - scn[\"targets\"][g][k, 0], y - scn[\"targets\"][g][k, 1]))\n",
" n = len(truths)\n",
" acc = len(found) / max(n + split + len(false) + len(merge), 1)\n",
" return {\"์ ๋ตํ์ \": n, \"ํ์งํ์ \": len(found), \"์ฌํ์จ\": round(len(found) / max(n, 1), 3), \"๋ถํ \": split,\n",
" \"๊ฑฐ์งํญ์ \": len(false), \"๋ณํฉ์ค๋ฅ\": len(merge), \"์์นRMSE(km)\": round(float(np.sqrt(np.mean(np.square(err)))) if err else np.nan, 3),\n",
" \"ํญ์ ์ ํ๋\": round(acc, 3)}\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
\n",
"SCEN = [(\"S01 ๊ธฐ๋ณธ(๋ง์)\", dict(seed=401)), (\"S02 ์ ํ ๊ธฐ๋\", dict(seed=402, special=\"turn\")),\n",
" (\"S03 ๊ต์ฐจ ํ์ \", dict(seed=403, special=\"cross\")), (\"S04 ๊ทผ์ ํธ๋\", dict(seed=404, special=\"formation\")),\n",
" (\"S05 ๊ณ ์ ํ์ \", dict(seed=405, special=\"fast\")), (\"S06 ๊ธฐ๋ง ํญ์ (EO/IR)\", dict(seed=406, ghost=True)),\n",
" (\"S07 ๋ ์ด๋A ์ฌ๋ฐ\", dict(seed=407, noise_mult={\"RDR-A\": 4.0})), (\"S08 ๋ ์ด๋B ๋จ์ \", dict(seed=408, dropout=(\"RDR-B\", 15, 45))),\n",
" (\"S09 ์ฐ์ฒ+์ค๊ฒฝ๋ณด ์ฆ๊ฐ\", dict(seed=409, weather=\"rain\", fa_mult=2.0)), (\"S10 ์๊ฐ+์ ํ+๊ธฐ๋ง\", dict(seed=410, weather=\"fog\", special=\"turn\", ghost=True))]\n",
"\n",
"rows, lat_all, falses, trues = [], [], [], []\n",
"for name, kw in SCEN:\n",
" scn = make_scenario(**kw)\n",
" reps = prepare(scn)\n",
" t0 = time.perf_counter()\n",
" trk = run_tracker(reps)\n",
" ms = (time.perf_counter() - t0) * 1000 / scn[\"T\"]\n",
" ev = evaluate(trk, scn)\n",
" lat_all.append(ms)\n",
" for t in trk:\n",
" if t[\"confirmed\"] and not t[\"label\"].startswith(\"TGT\"):\n",
" falses.append({\"์๋๋ฆฌ์ค\": name.split()[0], \"์์ธ\": t[\"label\"], \"์ผ์์\": len(t[\"sensors\"]), \"์ผ์\": \"+\".join(sorted(t[\"sensors\"])),\n",
" \"์๋ช
(s)\": len(t[\"scans\"]), \"์ค์บ๋น ๋ณด๊ณ \": len(t[\"truths\"]) / len(t[\"scans\"])})\n",
" elif t[\"confirmed\"]:\n",
" trues.append({\"์๋ช
(s)\": len(t[\"scans\"]), \"์ค์บ๋น ๋ณด๊ณ \": len(t[\"truths\"]) / len(t[\"scans\"]), \"์ผ์์\": len(t[\"sensors\"])})\n",
" rows.append({\"์๋๋ฆฌ์ค\": name, \"ํ์ \": ev[\"์ ๋ตํ์ \"], \"์ฌํ์จ\": ev[\"์ฌํ์จ\"], \"๋ถํ \": ev[\"๋ถํ \"], \"๊ฑฐ์งํญ์ \": ev[\"๊ฑฐ์งํญ์ \"],\n",
" \"์์นRMSE(km)\": ev[\"์์นRMSE(km)\"], \"ms/์ค์บ\": round(ms, 1), \"ํต๊ณผ\": \"PASS\" if ev[\"์ฌํ์จ\"] >= 0.85 else \"FAIL\"})\n",
"R = pd.DataFrame(rows)\n",
"print(\"[๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
EKF ์ถ์ ยทํตํฉ ๊ฒฐ๊ณผ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\")\n",
"print(R.to_string(index=False))\n",
"rec = R[\"์ฌํ์จ\"].mean()\n",
"print(f\"\\nํ๊ท ์ฌํ์จ {rec:.1%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if rec >= 0.85 else '๋ฏธ๋ฌ'}), ์๋๋ฆฌ์ค ํต๊ณผ {(R['ํต๊ณผ'] == 'PASS').sum()}/10\")\n",
"print(f\"์ค์บ๋น ์ฒ๋ฆฌ ์ง์ฐ ์ต๋ {max(lat_all):.1f} ms (๋ชฉํ 200ms ์ดํ: {'๋ฌ์ฑ' if max(lat_all) <= 200 else '๋ฏธ๋ฌ'})\")\n",
"\n",
"F = pd.DataFrame(falses)\n",
"print(f\"\\n[๊ฑฐ์ง ํญ์ ํจํด] ์ด {len(F)}๊ฐ\")\n",
"if len(F):\n",
" print(F.groupby([\"์์ธ\", \"์ผ์์\"]).size().rename(\"๊ฐ์\").reset_index().to_string(index=False))\n",
" T_ = pd.DataFrame(trues)\n",
" print(f\"์ค์ ํญ์ : ํ๊ท ์๋ช
{T_['์๋ช
(s)'].mean():.0f}s, ์ค์บ๋น ๋ณด๊ณ {T_['์ค์บ๋น ๋ณด๊ณ '].mean():.1f}๊ฑด, ์ผ์ {T_['์ผ์์'].mean():.1f}์ข
\")\n",
" print(f\"๊ฑฐ์ง ํญ์ : ํ๊ท ์๋ช
{F['์๋ช
(s)'].mean():.0f}s, ์ค์บ๋น ๋ณด๊ณ {F['์ค์บ๋น ๋ณด๊ณ '].mean():.1f}๊ฑด, ์ผ์ {F['์ผ์์'].mean():.1f}์ข
\")\n",
" print(\"์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: ์ฐ์ฒยท์ค๊ฒฝ๋ณด ์ฆ๊ฐ ์ ํด๋ฌํฐ๊ฐ ์ฐ์ฐํ ์ฐ์ ์ฐ๊ฒฐ๋ ์งง์ ํญ์ , EO/IR ๋จ๋
๊ธฐ๋ง ํญ์ -> ์๋ช
ยท์ค์บ๋น ๋ณด๊ณ ยท์ผ์ ์ข
๋ฅ ์๋ฅผ ์ ๋ขฐ๋ ํน์ง์ผ๋ก ์ถ๊ฐ(๋ฏธ์
5)\")\n",
"\n",
"# ๊ธฐ๋ง ํญ์ ์๋๋ฆฌ์ค์์ ๊ถค์ ๊ทธ๋ฆผ\n",
"scn = make_scenario(seed=410, weather=\"fog\", special=\"turn\", ghost=True)\n",
"trk = run_tracker(prepare(scn)); evaluate(trk, scn)\n",
"plt.figure(figsize=(6, 6))\n",
"for tid, tr in scn[\"targets\"].items():\n",
" plt.plot(tr[:, 0], tr[:, 1], \"k-\", lw=0.8)\n",
"for t in trk:\n",
" if t[\"confirmed\"]:\n",
" h = np.array(t[\"hist\"])\n",
" plt.plot(h[:, 1], h[:, 2], \"--\", c=\"tab:red\" if not t[\"label\"].startswith(\"TGT\") else \"tab:blue\", lw=1.2)\n",
"for sid, s in SENSORS.items():\n",
" plt.plot(*s[\"pos\"], \"^\", c=\"tab:green\"); plt.text(s[\"pos\"][0] + 0.3, s[\"pos\"][1], sid, fontsize=8)\n",
"plt.xlabel(\"East (km)\"); plt.ylabel(\"North (km)\"); plt.title(\"EKF fused tracks (blue) / false tracks (red) - synthetic\")\n",
"plt.axis(\"equal\"); plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
04 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๋ณตํฉ ์ํ ์๋๋ฆฌ์ค 10์ข
EKF ์ถ์ ยทํตํฉ ๊ฒฐ๊ณผ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
" ์๋๋ฆฌ์ค ํ์ ์ฌํ์จ ๋ถํ ๊ฑฐ์งํญ์ ์์นRMSE(km) ms/์ค์บ ํต๊ณผ\n",
" S01 ๊ธฐ๋ณธ(๋ง์) 6 1.0 0 0 0.043 7.9 PASS\n",
" S02 ์ ํ ๊ธฐ๋ 6 1.0 0 0 0.041 6.0 PASS\n",
" S03 ๊ต์ฐจ ํ์ 6 1.0 0 0 0.039 6.9 PASS\n",
" S04 ๊ทผ์ ํธ๋ 6 1.0 0 1 0.041 5.3 PASS\n",
" S05 ๊ณ ์ ํ์ 6 1.0 0 1 0.040 5.7 PASS\n",
"S06 ๊ธฐ๋ง ํญ์ (EO/IR) 6 1.0 0 2 0.041 6.9 PASS\n",
" S07 ๋ ์ด๋A ์ฌ๋ฐ 6 1.0 0 1 0.067 7.8 PASS\n",
" S08 ๋ ์ด๋B ๋จ์ 6 1.0 0 0 0.055 4.7 PASS\n",
" S09 ์ฐ์ฒ+์ค๊ฒฝ๋ณด ์ฆ๊ฐ 6 1.0 0 5 0.054 14.5 PASS\n",
" S10 ์๊ฐ+์ ํ+๊ธฐ๋ง 6 1.0 0 2 0.048 7.3 PASS\n",
"\n",
"ํ๊ท ์ฌํ์จ 100.0% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ), ์๋๋ฆฌ์ค ํต๊ณผ 10/10\n",
"์ค์บ๋น ์ฒ๋ฆฌ ์ง์ฐ ์ต๋ 14.5 ms (๋ชฉํ 200ms ์ดํ: ๋ฌ์ฑ)\n",
"\n",
"[๊ฑฐ์ง ํญ์ ํจํด] ์ด 12๊ฐ\n",
" ์์ธ ์ผ์์ ๊ฐ์\n",
" FA 2 6\n",
" FA 3 2\n",
" FA 4 1\n",
"GHOST 1 3\n",
"์ค์ ํญ์ : ํ๊ท ์๋ช
60s, ์ค์บ๋น ๋ณด๊ณ 3.1๊ฑด, ์ผ์ 3.8์ข
\n",
"๊ฑฐ์ง ํญ์ : ํ๊ท ์๋ช
10s, ์ค์บ๋น ๋ณด๊ณ 1.0๊ฑด, ์ผ์ 2.1์ข
\n",
"์ค๊ฒฝ๋ณด ์ ๋ฐ ํจํด: ์ฐ์ฒยท์ค๊ฒฝ๋ณด ์ฆ๊ฐ ์ ํด๋ฌํฐ๊ฐ ์ฐ์ฐํ ์ฐ์ ์ฐ๊ฒฐ๋ ์งง์ ํญ์ , EO/IR ๋จ๋
๊ธฐ๋ง ํญ์ -> ์๋ช
ยท์ค์บ๋น ๋ณด๊ณ ยท์ผ์ ์ข
๋ฅ ์๋ฅผ ์ ๋ขฐ๋ ํน์ง์ผ๋ก ์ถ๊ฐ(๋ฏธ์
5)\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
05. ์ผ์๋ณ ์คํ ๊ฐ์ค์น ๋ฐ์ ๋ฐ ํญ์ ํตํฉ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์๋ณ ์ ๋ขฐ๋ ๊ฐ์ค์น์ ๊ธฐ์ ํ๊ฒฝ ์์ธ์ ๋ฐ์ํ์ฌ ํตํฉ๋ ํญ์ ์ ์ง์ ์ฌ๋ถ ๋ฐ ์ตํฉ ์ ๋ขฐ๋ ์ง์๋ฅผ ๊ณ์ฐํ๋ ์์ง์ ๊ฐ๋ฐํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์๋ณ ์ ๋ขฐ๋ ๊ฐ์ค์น์ ๊ธฐ์ ์์ธ์ ๋ฐ์ํด ํตํฉ ํญ์ ์ ์ง์๋ฅผ ๋ํ๋ด๋ ์ตํฉ ์ ๋ขฐ๋ ์ง์์ High/Med/Low ๋ฑ๊ธ์ ์ฐ์ถํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๊ธฐ์๋ณ ์ผ์ ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค๋ฅผ ๋ง๋ค๊ณ ํญ์ ์๋ช
, ์ค์บ๋น ๋ณด๊ณ ์, ์ผ์ ์ข
๋ฅ ์, ๊ฐ์ค ์ผ์ ์ง์ง๋, ์ธก์ ์ ํฉ๋(NIS), EO/IR ๋จ๋
์ฌ๋ถ๋ฅผ ํน์ง์ผ๋ก ์ฐ์ถํ๊ณ ๋ชจ๋ธ์ ์ ์ฉํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/ops02_confidence_engine.py` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ๋ขฐ๋ ๋ฑ๊ธ(High/Med/Low) ๋ถ์ฌ ๊ธฐ์ค์ ๊ตฐ์ฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด์จ(FAR) 40% ์ด์ ๊ฐ์ถ, ์ ๋ขฐ๋ ์ค์ฝ์ด ์ฐ์ถ ์ผ๊ด์ฑ 90% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ค๊ฒฝ๋ณด ํํฐ๋ง ์ ํ ๋น๊ต ์งํ ์ฐ์ถ ๋ฐ ํตํฉ ํ
์คํธ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ROC-AUC ๊ณก์ ๋ถ์ ๋ฐ ์๊ณ๊ฐ ๋ฏผ๊ฐ๋ ํ
์คํธ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"ํตํฉ ํญ์ ๋ง๋ค ์ผ์๋ณ ์ ๋ขฐ๋ ๊ฐ์ค์น(๊ธฐ์ ๋ฐ์), ํญ์ ์๋ช
, ์ค์บ๋น ๋ณด๊ณ ์, ์ผ์ ์ข
๋ฅ ์, ์ธก์ ์ ํฉ๋(NIS)๋ฅผ ํน์ง์ผ๋ก ์ตํฉ ์ ๋ขฐ๋ ์ง์์ High/Med/Low ๋ฑ๊ธ์ ์ฐ์ถํด, ๋ชจ๋ ํ์ ํญ์ ์ ๊ฒฝ๋ณดํ๋ ๋ฐฉ์ ๋๋น ์ค๊ฒฝ๋ณด์จ ๊ฐ์ถ๊ณผ ๋ฑ๊ธ ์ผ๊ด์ฑ์ ํ์ธํ๋ค.\"\"\"\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import roc_auc_score\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 5] ์ผ์๋ณ ์คํ ๊ฐ์ค์น ๋ฐ์ ๋ฐ ํญ์ ํตํฉ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 5] ์ผ์๋ณ ์คํ ๊ฐ์ค์น ๋ฐ์ ๋ฐ ํญ์ ํตํฉ ์ ๋ขฐ๋ ์ง์ ์ฐ์ถ ์์ง ๊ฐ๋ฐ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# ---- ํ์ฅ ์นผ๋ง ํํฐ(EKF) ๋ค์ค ์ผ์ ํญ์ ์ถ์ ๊ธฐ ๊ณตํต ํจ์ ----\n",
"# ์ํ x = [๋(km), ๋ถ(km), ๋์(km/s), ๋ถ์(km/s)], ์ธก์ z = ์ผ์ ๊ธฐ์ค [๊ฑฐ๋ฆฌ(km), ๋ฐฉ์(rad)]\n",
"def ekf_predict(x, P, dt, q):\n",
" F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], float)\n",
" G = np.array([[dt ** 2 / 2, 0], [0, dt ** 2 / 2], [dt, 0], [0, dt]])\n",
" return F @ x, F @ P @ F.T + q * G @ G.T\n",
"\n",
"\n",
"def h_polar(x, spos):\n",
" dx, dy = x[0] - spos[0], x[1] - spos[1]\n",
" r2 = max(dx * dx + dy * dy, 1e-9); r = np.sqrt(r2)\n",
" return np.array([r, np.arctan2(dx, dy)]), np.array([[dx / r, dy / r, 0, 0], [dy / r2, -dx / r2, 0, 0]])\n",
"\n",
"\n",
"def innovation(x, P, rep):\n",
" s = SENSORS[rep[\"sensor\"]]\n",
" zp, H = h_polar(x, s[\"pos\"])\n",
" y = np.array([rep[\"range_km\"], np.radians(rep[\"az_deg\"])]) - zp\n",
" y[1] = (y[1] + np.pi) % (2 * np.pi) - np.pi # ๋ฐฉ์ ์ฐจ์ด๋ -pi~pi๋ก ๊ฐ์ผ๋ค\n",
" S = H @ P @ H.T + np.diag([rep[\"sr\"] ** 2, np.radians(rep[\"saz\"]) ** 2])\n",
" return y, H, S, float(y @ np.linalg.solve(S, y))\n",
"\n",
"\n",
"class Tracker:\n",
" \"\"\"์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ค์บ(1์ด) ๋จ์๋ก ๋ฐ์ EKF ํญ์ ์ ๊ฐฑ์ ํ๋ค. ๊ฐ์ ์ค์บ ์์์ ์ผ์๋ณ๋ก\n",
" ์์ฐจ ๊ฐฑ์ ํ๋ฏ๋ก, ์ฌ๋ฌ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ณด๋ฉด ํ๋์ ํญ์ ์ผ๋ก ํตํฉ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self, gate=9.21, q=4e-4, confirm=(3, 5), max_miss=4, merge_gate=6.0):\n",
" self.gate, self.q, self.confirm, self.max_miss, self.merge_gate = gate, q, confirm, max_miss, merge_gate\n",
" self.tracks, self.dead, self.nid, self.n_merged = [], [], 0, 0\n",
"\n",
" def _new(self, rep, k):\n",
" C = meas_cov(rep)\n",
" P = np.zeros((4, 4)); P[:2, :2] = C; P[2, 2] = P[3, 3] = 0.25 ** 2\n",
" self.tracks.append({\"id\": f\"FT{self.nid:03d}\", \"x\": np.array([rep[\"x\"], rep[\"y\"], 0, 0]), \"P\": P, \"k\": k,\n",
" \"scans\": [k], \"sensors\": {rep[\"sensor\"]: 1}, \"truths\": [rep[\"truth\"]], \"nis\": [],\n",
" \"hist\": [(k, rep[\"x\"], rep[\"y\"])], \"confirmed\": False, \"miss\": 0, \"born\": k, \"upd\": True})\n",
" self.nid += 1\n",
"\n",
" def _assign(self, tracks, rs, sid):\n",
" \"\"\"๊ฒ์ดํธ(๋งํ ๋ผ๋
ธ๋น์ค ๊ฑฐ๋ฆฌ) ์์์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN). ํ ๋น๋ ๋ณด๊ณ ์ ๋ฒํธ ์งํฉ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" if not tracks or not rs:\n",
" return set()\n",
" spos = SENSORS[sid][\"pos\"]\n",
" Z = np.array([[r[\"range_km\"], np.radians(r[\"az_deg\"])] for r in rs])\n",
" R = np.diag([rs[0][\"sr\"] ** 2, np.radians(rs[0][\"saz\"]) ** 2])\n",
" cost = np.full((len(tracks), len(rs)), 1e6)\n",
" for i, tr in enumerate(tracks):\n",
" zp, H = h_polar(tr[\"x\"], spos)\n",
" Y = Z - zp\n",
" Y[:, 1] = (Y[:, 1] + np.pi) % (2 * np.pi) - np.pi\n",
" Si = np.linalg.inv(H @ tr[\"P\"] @ H.T + R)\n",
" d2 = np.einsum(\"ij,jk,ik->i\", Y, Si, Y)\n",
" cost[i] = np.where(d2 < self.gate, d2, 1e6)\n",
" used = set()\n",
" for i, j in zip(*linear_sum_assignment(cost)):\n",
" if cost[i, j] < self.gate:\n",
" tr, r = tracks[i], rs[j]\n",
" y, H, S, d2 = innovation(tr[\"x\"], tr[\"P\"], r)\n",
" K = tr[\"P\"] @ H.T @ np.linalg.inv(S)\n",
" tr[\"x\"] = tr[\"x\"] + K @ y\n",
" tr[\"P\"] = (np.eye(4) - K @ H) @ tr[\"P\"]\n",
" tr[\"nis\"].append(d2); tr[\"truths\"].append(r[\"truth\"])\n",
" tr[\"sensors\"][sid] = tr[\"sensors\"].get(sid, 0) + 1\n",
" tr[\"upd\"] = True; used.add(j)\n",
" return used\n",
"\n",
" def _merge(self):\n",
" \"\"\"๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ฐ๋ผ๊ฐ๋ ์ค๋ณต ํญ์ (์์นยท์๋๊ฐ ํต๊ณ์ ์ผ๋ก ๊ฐ์)์ ์ค๋๋ ํญ์ ์ผ๋ก ํตํฉํ๋ค.\"\"\"\n",
" drop = set()\n",
" for a in range(len(self.tracks)):\n",
" for b in range(a + 1, len(self.tracks)):\n",
" A, B = self.tracks[a], self.tracks[b]\n",
" if A[\"id\"] in drop or B[\"id\"] in drop:\n",
" continue\n",
" d = A[\"x\"] - B[\"x\"]\n",
" d2 = float(d[:2] @ np.linalg.solve(A[\"P\"][:2, :2] + B[\"P\"][:2, :2], d[:2]))\n",
" if d2 < self.merge_gate and np.hypot(d[2], d[3]) < 0.05:\n",
" keep, lose = (A, B) if len(A[\"scans\"]) >= len(B[\"scans\"]) else (B, A)\n",
" for sid, n in lose[\"sensors\"].items():\n",
" keep[\"sensors\"][sid] = keep[\"sensors\"].get(sid, 0) + n\n",
" keep[\"truths\"] += lose[\"truths\"]; keep[\"scans\"] = sorted(set(keep[\"scans\"]) | set(lose[\"scans\"]))\n",
" keep[\"merged\"] = keep.get(\"merged\", 0) + 1\n",
" drop.add(lose[\"id\"])\n",
" self.tracks = [t for t in self.tracks if t[\"id\"] not in drop]\n",
" self.n_merged += len(drop)\n",
"\n",
" def step(self, k, reps):\n",
" for tr in self.tracks: # 1) ์์ธก\n",
" tr[\"x\"], tr[\"P\"] = ekf_predict(tr[\"x\"], tr[\"P\"], k - tr[\"k\"], self.q)\n",
" tr[\"k\"], tr[\"upd\"] = k, False\n",
" for sid in sorted({r[\"sensor\"] for r in reps}, key=lambda s: SENSORS[s][\"sr\"]): # 2) ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ \n",
" rs = [r for r in reps if r[\"sensor\"] == sid]\n",
" used = self._assign([t for t in self.tracks if t[\"confirmed\"]], rs, sid) # ํ์ ํญ์ ์ฐ์ \n",
" rest = [r for j, r in enumerate(rs) if j not in used]\n",
" used2 = self._assign([t for t in self.tracks if not t[\"confirmed\"]], rest, sid)\n",
" for j, r in enumerate(rest):\n",
" if j not in used2:\n",
" self._new(r, k)\n",
" if self.merge_gate > 0:\n",
" self._merge()\n",
" keep = []\n",
" for tr in self.tracks: # 3) ํ์ ยท์ญ์ ๊ด๋ฆฌ\n",
" if tr[\"upd\"]:\n",
" tr[\"miss\"] = 0\n",
" if tr[\"scans\"][-1] != k:\n",
" tr[\"scans\"].append(k)\n",
" tr[\"hist\"].append((k, tr[\"x\"][0], tr[\"x\"][1]))\n",
" else:\n",
" tr[\"miss\"] += 1\n",
" m, n = self.confirm\n",
" if not tr[\"confirmed\"] and sum(1 for s in tr[\"scans\"] if s > k - n) >= m:\n",
" tr[\"confirmed\"] = True\n",
" limit = self.max_miss if tr[\"confirmed\"] else 1\n",
" (keep if tr[\"miss\"] <= limit else self.dead).append(tr)\n",
" self.tracks = keep\n",
"\n",
" def all_tracks(self):\n",
" return self.dead + self.tracks\n",
"\n",
"\n",
"def run_tracker(reps, **kw):\n",
" trk = Tracker(**kw)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.step(k, by.get(k, []))\n",
" return trk.all_tracks()\n",
"\n",
"\n",
"def label_track(tr):\n",
" vals, cnt = np.unique(tr[\"truths\"], return_counts=True)\n",
" return str(vals[np.argmax(cnt)]), float(cnt.max() / cnt.sum())\n",
"\n",
"\n",
"def evaluate(tracks, scn, min_reports=5):\n",
" \"\"\"์ ๋ต ์ฐ๊ฒฐ ํ๊ฐ: ์ฌํ์จ, ํญ์ ๋ถํ (ํ ํ์ ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ, ์์น ์ค์ฐจ.\"\"\"\n",
" conf = [t for t in tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"label\"], t[\"purity\"] = label_track(t)\n",
" cnt = pd.Series([r[\"truth\"] for r in scn[\"reports\"] if r[\"truth\"].startswith(\"TGT\")]).value_counts()\n",
" truths = [k for k, v in cnt.items() if v >= min_reports]\n",
" per = {g: [t for t in conf if t[\"label\"] == g] for g in truths}\n",
" found = [g for g in truths if per[g]]\n",
" split = sum(max(0, len(v) - 1) for v in per.values())\n",
" false = [t for t in conf if not t[\"label\"].startswith(\"TGT\")]\n",
" merge = [t for t in conf if t[\"label\"].startswith(\"TGT\") and t[\"purity\"] < 0.7]\n",
" err = []\n",
" for g in found:\n",
" for t in per[g]:\n",
" for k, x, y in t[\"hist\"][3:]:\n",
" if 0 <= k < scn[\"T\"]:\n",
" err.append(np.hypot(x - scn[\"targets\"][g][k, 0], y - scn[\"targets\"][g][k, 1]))\n",
" n = len(truths)\n",
" acc = len(found) / max(n + split + len(false) + len(merge), 1)\n",
" return {\"์ ๋ตํ์ \": n, \"ํ์งํ์ \": len(found), \"์ฌํ์จ\": round(len(found) / max(n, 1), 3), \"๋ถํ \": split,\n",
" \"๊ฑฐ์งํญ์ \": len(false), \"๋ณํฉ์ค๋ฅ\": len(merge), \"์์นRMSE(km)\": round(float(np.sqrt(np.mean(np.square(err)))) if err else np.nan, 3),\n",
" \"ํญ์ ์ ํ๋\": round(acc, 3)}\n",
"\n",
"# ์ผ์ ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค(๊ธฐ์๋ณ) [๊ต์ก์ฉ๊ฐ์์๋ฃ]: ๊ธฐ๋ณธ ์ ๋ขฐ๋ x ๊ธฐ์ ํ์ง ๊ณ์\n",
"BASE_W = {\"radar\": 1.0, \"eoir\": 0.8, \"acoustic\": 0.6}\n",
"WMAT = {w: {sid: round(BASE_W[s[\"type\"]] * WEATHER[w][s[\"type\"]] / WEATHER[w][\"fa\"], 2) for sid, s in SENSORS.items()} for w in WEATHER}\n",
"\n",
"\n",
"def track_features(trk, weather):\n",
" rows = []\n",
" for t in trk:\n",
" if not t[\"confirmed\"]:\n",
" continue\n",
" n = len(t[\"scans\"])\n",
" rows.append({\"id\": t[\"id\"], \"life\": min(n, 10) / 10, \"rps\": len(t[\"truths\"]) / n, \"nsens\": len(t[\"sensors\"]),\n",
" \"wsup\": sum(WMAT[weather][s] * min(c / n, 1) for s, c in t[\"sensors\"].items()),\n",
" \"nis\": float(np.median(t[\"nis\"])) if t[\"nis\"] else 9.0, \"eoir_only\": float(set(t[\"sensors\"]) == {\"EOIR-C\"}),\n",
" \"label\": t[\"label\"], \"true\": t[\"label\"].startswith(\"TGT\")})\n",
" return pd.DataFrame(rows)\n",
"\n",
"\n",
"FEAT = [\"life\", \"rps\", \"nsens\", \"wsup\", \"nis\", \"eoir_only\"]\n",
"\n",
"\n",
"def dataset(seeds, fa=2.0, **kw):\n",
" out = []\n",
" for i, sd in enumerate(seeds):\n",
" w = [\"clear\", \"night\", \"fog\", \"rain\"][i % 4]\n",
" scn = make_scenario(sd, weather=w, fa_mult=fa, ghost=(i % 3 == 0), special=[None, \"turn\", \"cross\", \"formation\"][i % 4], **kw)\n",
" reps = prepare(scn)\n",
" for ts in (8, 15, 30): # ์ค์๊ฐ ํ๋จ ์์ : ํด๋น ์๊ฐ๊น์ง์ ๋ณด๊ณ ๋ง ์ฌ์ฉ\n",
" trk = run_tracker([r for r in reps if r[\"t\"] <= ts]); evaluate(trk, scn)\n",
" df = track_features(trk, w); df[\"weather\"] = w; df[\"scn\"] = sd; df[\"ts\"] = ts; out.append(df)\n",
" return pd.concat(out, ignore_index=True)\n",
"\n",
"\n",
"print(\"[์ผ์ ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค]\")\n",
"print(pd.DataFrame(WMAT).T.to_string())\n",
"tr = dataset(range(500, 508))\n",
"te = dataset(range(600, 606), fa=4.0, noise_mult={\"RDR-B\": 3.0}) # ํ๊ฐ: ์ค๊ฒฝ๋ณด 4๋ฐฐ + ๋ ์ด๋B ์ฌ๋ฐ(ํ์ต๋ณด๋ค ๊ฐํน)\n",
"eng = LogisticRegression(max_iter=1000, C=1.0).fit(tr[FEAT], tr[\"true\"])\n",
"\n",
"\n",
"def grade(F):\n",
" s = eng.predict_proba(F[FEAT])[:, 1]\n",
" g = np.where(s >= 0.8, \"High\", np.where(s >= 0.5, \"Med\", \"Low\"))\n",
" return s, g\n",
"\n",
"\n",
"te[\"score\"], te[\"grade\"] = grade(te)\n",
"false_n = (~te[\"true\"]).sum()\n",
"far_old = false_n / len(te) # ๊ธฐ์กด: ํ์ ํญ์ ์ ๋ถ ๊ฒฝ๋ณด\n",
"alarm = te[\"grade\"] == \"High\"\n",
"far_new = (~te[\"true\"] & alarm).sum() / max(alarm.sum(), 1)\n",
"red = 1 - far_new / far_old\n",
"print(f\"\\nํ๊ฐ ํญ์ (์์ ๋ณ) {len(te)}๊ฐ(์ค์ {te['true'].sum()} / ๊ฑฐ์ง {false_n}), ์๋๋ฆฌ์ค 6๊ฐ x ํ๋จ ์์ 3๊ฐ(8ยท15ยท30์ด), ๊ธฐ์ 4์ข
, ์ค๊ฒฝ๋ณด 4๋ฐฐ, ๋ ์ด๋B ์ฌ๋ฐ, ๊ธฐ๋ง ํญ์ ํฌํจ\")\n",
"print(f\"์ค๊ฒฝ๋ณด์จ(๊ฒฝ๋ณด ์ค ๊ฑฐ์ง ํญ์ ๋น์จ): ๊ธฐ์กด {far_old:.1%} -> High ๊ฒฝ๋ณด {far_new:.1%}, ๊ฐ์ถ {red:.0%} (๋ชฉํ 40% ์ด์: {'๋ฌ์ฑ' if red >= 0.4 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ค์ ํญ์ High ๋น์จ {(te['true'] & alarm).sum() / te['true'].sum():.1%}, ์ค์ ํญ์ Low ํ์ {(te['true'] & (te['grade'] == 'Low')).sum()}๊ฑด, ROC-AUC {roc_auc_score(te['true'], te['score']):.3f}\")\n",
"print(\"\\n๋ฑ๊ธ๋ณ ํญ์ ์:\")\n",
"print(pd.crosstab(te[\"grade\"], te[\"true\"].map({True: \"์ค์ \", False: \"๊ฑฐ์ง\"})).reindex([\"High\", \"Med\", \"Low\"]).fillna(0).astype(int).to_string())\n",
"\n",
"coef = pd.Series(eng.coef_[0], index=FEAT).round(2)\n",
"print(\"\\nํน์ง ๊ฐ์ค์น(๋ก์ง์คํฑ):\", {k: float(v) for k, v in coef.items()})\n",
"\n",
"# ์ผ๊ด์ฑ: ํน์ง์ ยฑ5% ์ธก์ ํ๋ค๋ฆผ์ 10ํ ์ฃผ์ด๋ ๋ฑ๊ธ์ด ์ ์ง๋๋ ๋น์จ\n",
"rng = np.random.default_rng(7)\n",
"same = []\n",
"for _ in range(10):\n",
" P = te.copy()\n",
" for c in [\"life\", \"rps\", \"wsup\", \"nis\"]:\n",
" P[c] = P[c] * rng.uniform(0.95, 1.05, len(P))\n",
" same.append((grade(P)[1] == te[\"grade\"]).mean())\n",
"cons = float(np.mean(same))\n",
"print(f\"\\n์ ๋ขฐ๋ ๋ฑ๊ธ ์ผ๊ด์ฑ(ํน์ง ยฑ5% ํ๋ค๋ฆผ 10ํ): {cons:.1%} (๋ชฉํ 90% ์ด์: {'๋ฌ์ฑ' if cons >= 0.9 else '๋ฏธ๋ฌ'})\")\n",
"print(\"\\n[์๊ณ๊ฐ ๋ฏผ๊ฐ๋] High ๊ธฐ์ค ๊ฒฝ๋ณด ์ ๊ฒฝ๋ณด ์ค ๊ฑฐ์ง\")\n",
"for th in [0.6, 0.7, 0.8, 0.9]:\n",
" a = te[\"score\"] >= th\n",
" print(f\" {th:.1f} {a.sum():4d} {(~te['true'] & a).sum() / max(a.sum(), 1):6.1%}\")\n",
"print(\"๋ฑ๊ธ ์ด์ฉ: High=ํตํฉ ํญ์ ๊ฒฝ๋ณด / Med=์กฐ์๊ด ํ์ธ(๋ฏธ์
7) / Low=๊ฐ์ ์ ์ง(์ญ์ ํ์ง ์์). ๊ต์ ์ฌ๋ถ๋ ๊ต์ ์์นยท์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\")\n",
"\n",
"plt.figure(figsize=(6, 3.5))\n",
"plt.hist([te[\"score\"][te[\"true\"]], te[\"score\"][~te[\"true\"]]], bins=20, label=[\"true track\", \"false track\"], color=[\"tab:blue\", \"tab:red\"])\n",
"plt.axvline(0.8, c=\"k\", ls=\"--\"); plt.axvline(0.5, c=\"gray\", ls=\":\"); plt.xlabel(\"fusion confidence\"); plt.legend(); plt.title(\"Fusion confidence (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
05 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ผ์ ์ ๋ขฐ๋ ๊ฐ์ค์น ๋งคํธ๋ฆญ์ค]\n",
" RDR-A RDR-B EOIR-C ACU-D\n",
"clear 1.00 1.00 0.80 0.60\n",
"night 0.91 0.91 0.65 0.55\n",
"fog 0.83 0.83 0.37 0.50\n",
"rain 0.61 0.61 0.35 0.22\n",
"\n",
"ํ๊ฐ ํญ์ (์์ ๋ณ) 263๊ฐ(์ค์ 114 / ๊ฑฐ์ง 149), ์๋๋ฆฌ์ค 6๊ฐ x ํ๋จ ์์ 3๊ฐ(8ยท15ยท30์ด), ๊ธฐ์ 4์ข
, ์ค๊ฒฝ๋ณด 4๋ฐฐ, ๋ ์ด๋B ์ฌ๋ฐ, ๊ธฐ๋ง ํญ์ ํฌํจ\n",
"์ค๊ฒฝ๋ณด์จ(๊ฒฝ๋ณด ์ค ๊ฑฐ์ง ํญ์ ๋น์จ): ๊ธฐ์กด 56.7% -> High ๊ฒฝ๋ณด 2.6%, ๊ฐ์ถ 95% (๋ชฉํ 40% ์ด์: ๋ฌ์ฑ)\n",
"์ค์ ํญ์ High ๋น์จ 97.4%, ์ค์ ํญ์ Low ํ์ 0๊ฑด, ROC-AUC 0.998\n",
"\n",
"๋ฑ๊ธ๋ณ ํญ์ ์:\n",
"true ๊ฑฐ์ง ์ค์ \n",
"grade \n",
"High 3 111\n",
"Med 18 3\n",
"Low 128 0\n",
"\n",
"ํน์ง ๊ฐ์ค์น(๋ก์ง์คํฑ): {'life': -0.32, 'rps': 1.21, 'nsens': 1.32, 'wsup': 1.39, 'nis': -0.76, 'eoir_only': -0.61}\n",
"\n",
"์ ๋ขฐ๋ ๋ฑ๊ธ ์ผ๊ด์ฑ(ํน์ง ยฑ5% ํ๋ค๋ฆผ 10ํ): 99.8% (๋ชฉํ 90% ์ด์: ๋ฌ์ฑ)\n",
"\n",
"[์๊ณ๊ฐ ๋ฏผ๊ฐ๋] High ๊ธฐ์ค ๊ฒฝ๋ณด ์ ๊ฒฝ๋ณด ์ค ๊ฑฐ์ง\n",
" 0.6 128 11.7%\n",
" 0.7 120 5.8%\n",
" 0.8 114 2.6%\n",
" 0.9 113 2.7%\n",
"๋ฑ๊ธ ์ด์ฉ: High=ํตํฉ ํญ์ ๊ฒฝ๋ณด / Med=์กฐ์๊ด ํ์ธ(๋ฏธ์
7) / Low=๊ฐ์ ์ ์ง(์ญ์ ํ์ง ์์). ๊ต์ ์ฌ๋ถ๋ ๊ต์ ์์นยท์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
06. ๋ค์ค ์ผ์ ์ตํฉ ํญ์ ๋ฐ ์ผ์๋ณ ์์ ๊ฒฝ๋ณด ๋น๊ต ์ ์ ์ํฉํ(COP) ๋์๋ณด๋ ๊ตฌํ\n",
"**์์์๊ฐ**: ์ฝ 2.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ค์ค ์ผ์์ ์์ ๊ฒฝ๋ณด์ ์ตํฉ๋ ํตํฉ ํญ์ ์ ์ค์๊ฐ์ผ๋ก ๋น๊ต ์๊ฐํํ์ฌ ์งํ๊ด์ ์ผ์ ์ค๋ณต ์ธ์ง ๋ถํ๋ฅผ ์ค์ด๋ ์ ์ ์ํฉํ ๋์๋ณด๋๋ฅผ ๊ตฌ์ถํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์๋ณ ์์ ๊ฒฝ๋ณด์ ์ตํฉ ํญ์ ์ ๋๋ํ ๋ณด์ฌ ์ฃผ๋ ์ผ๊ฐ ๋ชจ๋ ์ ์ ์ํฉํ(COP)์ ๊ตฌํํด ์งํ๊ด์ ์ค๋ณต ์ธ์ง ๋ถํ๋ฅผ ์ค์ธ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ค์บ๋ง๋ค ์์ ๊ฒฝ๋ณด ์ง๋, ์ตํฉ ํญ์ ์ง๋, ํ์ ํญ๋ชฉ ์ ์ถ์ด, ํญ์ ๋ชฉ๋ก 4๊ฐ ํจ๋์ ๊ฐฑ์ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/ops02_dashboard.py (๋๋ ์น UI ์ปดํฌ๋ํธ)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ ์ ์์ ์ํฉํ์ผ๋ก์์ ์ง๊ด์ฑ ๋ฐ ์ธ์ง ๋ถํ ๊ฐ์๋\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ํ๋ฉด ๊ฐฑ์ ์ฃผ๊ธฐ 1์ด ์ด๋ด, UI ์๋ต์ฑ 100ms ์ด๋ด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ฃผ์ ์งํ ์๊ฐํ ์ปดํฌ๋ํธ 4์ข
์ด์ ๋ ๋๋ง ๋ฐ ์ค์๊ฐ ๋ฐ์ดํฐ ์ฐ๋\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ฌ์ฉ์ ์ธํฐ๋์
ํ
์คํธ ๋ฐ ๋ชจ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ ๋ ๋๋ง ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ค์บ(1์ด)๋ง๋ค ์ผ์๋ณ ์์ ๊ฒฝ๋ณด์ EKF ์ตํฉ ํญ์ ์ ๋๋ํ ๊ฐฑ์ ํ๋ ์ผ๊ฐ ๋ชจ๋ ์ ์ ์ํฉํ(COP)์ ๊ตฌํํ๊ณ , ์์ ๊ฒฝ๋ณด ๋๋น ํ์ ํญ๋ชฉ ๊ฐ์(์ธ์ง ๋ถํ), ๋ฐฉ์ด ์ง์ญ ์ ๊ทผ ๊ธด๊ธ ๊ฐ์กฐ, ํ๋ฉด ๊ฐฑ์ ์๊ฐ๊ณผ ํญ์ ์ ํ ์๋ต ์๊ฐ์ ์ธก์ ํ๋ค.\"\"\"\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 6] ๋ค์ค ์ผ์ ์ตํฉ ํญ์ ๋ฐ ์ผ์๋ณ ์์ ๊ฒฝ๋ณด ๋น๊ต ์ ์ ์ํฉํ(COP) ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 6] ๋ค์ค ์ผ์ ์ตํฉ ํญ์ ๋ฐ ์ผ์๋ณ ์์ ๊ฒฝ๋ณด ๋น๊ต ์ ์ ์ํฉํ(COP) ๋์๋ณด๋ ๊ตฌํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# ---- ํ์ฅ ์นผ๋ง ํํฐ(EKF) ๋ค์ค ์ผ์ ํญ์ ์ถ์ ๊ธฐ ๊ณตํต ํจ์ ----\n",
"# ์ํ x = [๋(km), ๋ถ(km), ๋์(km/s), ๋ถ์(km/s)], ์ธก์ z = ์ผ์ ๊ธฐ์ค [๊ฑฐ๋ฆฌ(km), ๋ฐฉ์(rad)]\n",
"def ekf_predict(x, P, dt, q):\n",
" F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], float)\n",
" G = np.array([[dt ** 2 / 2, 0], [0, dt ** 2 / 2], [dt, 0], [0, dt]])\n",
" return F @ x, F @ P @ F.T + q * G @ G.T\n",
"\n",
"\n",
"def h_polar(x, spos):\n",
" dx, dy = x[0] - spos[0], x[1] - spos[1]\n",
" r2 = max(dx * dx + dy * dy, 1e-9); r = np.sqrt(r2)\n",
" return np.array([r, np.arctan2(dx, dy)]), np.array([[dx / r, dy / r, 0, 0], [dy / r2, -dx / r2, 0, 0]])\n",
"\n",
"\n",
"def innovation(x, P, rep):\n",
" s = SENSORS[rep[\"sensor\"]]\n",
" zp, H = h_polar(x, s[\"pos\"])\n",
" y = np.array([rep[\"range_km\"], np.radians(rep[\"az_deg\"])]) - zp\n",
" y[1] = (y[1] + np.pi) % (2 * np.pi) - np.pi # ๋ฐฉ์ ์ฐจ์ด๋ -pi~pi๋ก ๊ฐ์ผ๋ค\n",
" S = H @ P @ H.T + np.diag([rep[\"sr\"] ** 2, np.radians(rep[\"saz\"]) ** 2])\n",
" return y, H, S, float(y @ np.linalg.solve(S, y))\n",
"\n",
"\n",
"class Tracker:\n",
" \"\"\"์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ค์บ(1์ด) ๋จ์๋ก ๋ฐ์ EKF ํญ์ ์ ๊ฐฑ์ ํ๋ค. ๊ฐ์ ์ค์บ ์์์ ์ผ์๋ณ๋ก\n",
" ์์ฐจ ๊ฐฑ์ ํ๋ฏ๋ก, ์ฌ๋ฌ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ณด๋ฉด ํ๋์ ํญ์ ์ผ๋ก ํตํฉ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self, gate=9.21, q=4e-4, confirm=(3, 5), max_miss=4, merge_gate=6.0):\n",
" self.gate, self.q, self.confirm, self.max_miss, self.merge_gate = gate, q, confirm, max_miss, merge_gate\n",
" self.tracks, self.dead, self.nid, self.n_merged = [], [], 0, 0\n",
"\n",
" def _new(self, rep, k):\n",
" C = meas_cov(rep)\n",
" P = np.zeros((4, 4)); P[:2, :2] = C; P[2, 2] = P[3, 3] = 0.25 ** 2\n",
" self.tracks.append({\"id\": f\"FT{self.nid:03d}\", \"x\": np.array([rep[\"x\"], rep[\"y\"], 0, 0]), \"P\": P, \"k\": k,\n",
" \"scans\": [k], \"sensors\": {rep[\"sensor\"]: 1}, \"truths\": [rep[\"truth\"]], \"nis\": [],\n",
" \"hist\": [(k, rep[\"x\"], rep[\"y\"])], \"confirmed\": False, \"miss\": 0, \"born\": k, \"upd\": True})\n",
" self.nid += 1\n",
"\n",
" def _assign(self, tracks, rs, sid):\n",
" \"\"\"๊ฒ์ดํธ(๋งํ ๋ผ๋
ธ๋น์ค ๊ฑฐ๋ฆฌ) ์์์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN). ํ ๋น๋ ๋ณด๊ณ ์ ๋ฒํธ ์งํฉ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" if not tracks or not rs:\n",
" return set()\n",
" spos = SENSORS[sid][\"pos\"]\n",
" Z = np.array([[r[\"range_km\"], np.radians(r[\"az_deg\"])] for r in rs])\n",
" R = np.diag([rs[0][\"sr\"] ** 2, np.radians(rs[0][\"saz\"]) ** 2])\n",
" cost = np.full((len(tracks), len(rs)), 1e6)\n",
" for i, tr in enumerate(tracks):\n",
" zp, H = h_polar(tr[\"x\"], spos)\n",
" Y = Z - zp\n",
" Y[:, 1] = (Y[:, 1] + np.pi) % (2 * np.pi) - np.pi\n",
" Si = np.linalg.inv(H @ tr[\"P\"] @ H.T + R)\n",
" d2 = np.einsum(\"ij,jk,ik->i\", Y, Si, Y)\n",
" cost[i] = np.where(d2 < self.gate, d2, 1e6)\n",
" used = set()\n",
" for i, j in zip(*linear_sum_assignment(cost)):\n",
" if cost[i, j] < self.gate:\n",
" tr, r = tracks[i], rs[j]\n",
" y, H, S, d2 = innovation(tr[\"x\"], tr[\"P\"], r)\n",
" K = tr[\"P\"] @ H.T @ np.linalg.inv(S)\n",
" tr[\"x\"] = tr[\"x\"] + K @ y\n",
" tr[\"P\"] = (np.eye(4) - K @ H) @ tr[\"P\"]\n",
" tr[\"nis\"].append(d2); tr[\"truths\"].append(r[\"truth\"])\n",
" tr[\"sensors\"][sid] = tr[\"sensors\"].get(sid, 0) + 1\n",
" tr[\"upd\"] = True; used.add(j)\n",
" return used\n",
"\n",
" def _merge(self):\n",
" \"\"\"๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ฐ๋ผ๊ฐ๋ ์ค๋ณต ํญ์ (์์นยท์๋๊ฐ ํต๊ณ์ ์ผ๋ก ๊ฐ์)์ ์ค๋๋ ํญ์ ์ผ๋ก ํตํฉํ๋ค.\"\"\"\n",
" drop = set()\n",
" for a in range(len(self.tracks)):\n",
" for b in range(a + 1, len(self.tracks)):\n",
" A, B = self.tracks[a], self.tracks[b]\n",
" if A[\"id\"] in drop or B[\"id\"] in drop:\n",
" continue\n",
" d = A[\"x\"] - B[\"x\"]\n",
" d2 = float(d[:2] @ np.linalg.solve(A[\"P\"][:2, :2] + B[\"P\"][:2, :2], d[:2]))\n",
" if d2 < self.merge_gate and np.hypot(d[2], d[3]) < 0.05:\n",
" keep, lose = (A, B) if len(A[\"scans\"]) >= len(B[\"scans\"]) else (B, A)\n",
" for sid, n in lose[\"sensors\"].items():\n",
" keep[\"sensors\"][sid] = keep[\"sensors\"].get(sid, 0) + n\n",
" keep[\"truths\"] += lose[\"truths\"]; keep[\"scans\"] = sorted(set(keep[\"scans\"]) | set(lose[\"scans\"]))\n",
" keep[\"merged\"] = keep.get(\"merged\", 0) + 1\n",
" drop.add(lose[\"id\"])\n",
" self.tracks = [t for t in self.tracks if t[\"id\"] not in drop]\n",
" self.n_merged += len(drop)\n",
"\n",
" def step(self, k, reps):\n",
" for tr in self.tracks: # 1) ์์ธก\n",
" tr[\"x\"], tr[\"P\"] = ekf_predict(tr[\"x\"], tr[\"P\"], k - tr[\"k\"], self.q)\n",
" tr[\"k\"], tr[\"upd\"] = k, False\n",
" for sid in sorted({r[\"sensor\"] for r in reps}, key=lambda s: SENSORS[s][\"sr\"]): # 2) ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ \n",
" rs = [r for r in reps if r[\"sensor\"] == sid]\n",
" used = self._assign([t for t in self.tracks if t[\"confirmed\"]], rs, sid) # ํ์ ํญ์ ์ฐ์ \n",
" rest = [r for j, r in enumerate(rs) if j not in used]\n",
" used2 = self._assign([t for t in self.tracks if not t[\"confirmed\"]], rest, sid)\n",
" for j, r in enumerate(rest):\n",
" if j not in used2:\n",
" self._new(r, k)\n",
" if self.merge_gate > 0:\n",
" self._merge()\n",
" keep = []\n",
" for tr in self.tracks: # 3) ํ์ ยท์ญ์ ๊ด๋ฆฌ\n",
" if tr[\"upd\"]:\n",
" tr[\"miss\"] = 0\n",
" if tr[\"scans\"][-1] != k:\n",
" tr[\"scans\"].append(k)\n",
" tr[\"hist\"].append((k, tr[\"x\"][0], tr[\"x\"][1]))\n",
" else:\n",
" tr[\"miss\"] += 1\n",
" m, n = self.confirm\n",
" if not tr[\"confirmed\"] and sum(1 for s in tr[\"scans\"] if s > k - n) >= m:\n",
" tr[\"confirmed\"] = True\n",
" limit = self.max_miss if tr[\"confirmed\"] else 1\n",
" (keep if tr[\"miss\"] <= limit else self.dead).append(tr)\n",
" self.tracks = keep\n",
"\n",
" def all_tracks(self):\n",
" return self.dead + self.tracks\n",
"\n",
"\n",
"def run_tracker(reps, **kw):\n",
" trk = Tracker(**kw)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.step(k, by.get(k, []))\n",
" return trk.all_tracks()\n",
"\n",
"\n",
"def label_track(tr):\n",
" vals, cnt = np.unique(tr[\"truths\"], return_counts=True)\n",
" return str(vals[np.argmax(cnt)]), float(cnt.max() / cnt.sum())\n",
"\n",
"\n",
"def evaluate(tracks, scn, min_reports=5):\n",
" \"\"\"์ ๋ต ์ฐ๊ฒฐ ํ๊ฐ: ์ฌํ์จ, ํญ์ ๋ถํ (ํ ํ์ ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ, ์์น ์ค์ฐจ.\"\"\"\n",
" conf = [t for t in tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"label\"], t[\"purity\"] = label_track(t)\n",
" cnt = pd.Series([r[\"truth\"] for r in scn[\"reports\"] if r[\"truth\"].startswith(\"TGT\")]).value_counts()\n",
" truths = [k for k, v in cnt.items() if v >= min_reports]\n",
" per = {g: [t for t in conf if t[\"label\"] == g] for g in truths}\n",
" found = [g for g in truths if per[g]]\n",
" split = sum(max(0, len(v) - 1) for v in per.values())\n",
" false = [t for t in conf if not t[\"label\"].startswith(\"TGT\")]\n",
" merge = [t for t in conf if t[\"label\"].startswith(\"TGT\") and t[\"purity\"] < 0.7]\n",
" err = []\n",
" for g in found:\n",
" for t in per[g]:\n",
" for k, x, y in t[\"hist\"][3:]:\n",
" if 0 <= k < scn[\"T\"]:\n",
" err.append(np.hypot(x - scn[\"targets\"][g][k, 0], y - scn[\"targets\"][g][k, 1]))\n",
" n = len(truths)\n",
" acc = len(found) / max(n + split + len(false) + len(merge), 1)\n",
" return {\"์ ๋ตํ์ \": n, \"ํ์งํ์ \": len(found), \"์ฌํ์จ\": round(len(found) / max(n, 1), 3), \"๋ถํ \": split,\n",
" \"๊ฑฐ์งํญ์ \": len(false), \"๋ณํฉ์ค๋ฅ\": len(merge), \"์์นRMSE(km)\": round(float(np.sqrt(np.mean(np.square(err)))) if err else np.nan, 3),\n",
" \"ํญ์ ์ ํ๋\": round(acc, 3)}\n",
"\n",
"ALERT_R = 4.0 # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ ๋ฐ๊ฒฝ(km) ์ด๋ด ์ ๊ทผ ์ ๊ธด๊ธ ๊ฐ์กฐ\n",
"COLORS = {\"RDR-A\": \"deepskyblue\", \"RDR-B\": \"cyan\", \"EOIR-C\": \"yellow\", \"ACU-D\": \"violet\"}\n",
"\n",
"\n",
"def grade(t):\n",
" n = len(t[\"scans\"])\n",
" if len(t[\"sensors\"]) >= 2 and len(t[\"truths\"]) / n >= 1.5 and n >= 5:\n",
" return \"High\"\n",
" return \"Med\"\n",
"\n",
"\n",
"scn = make_scenario(seed=611, n=6, T=40, weather=\"night\", ghost=True)\n",
"reps = prepare(scn)\n",
"by = {}\n",
"for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
"trk = Tracker()\n",
"\n",
"plt.style.use(\"dark_background\") # ์ผ๊ฐ ์์ ๋ชจ๋\n",
"fig, ax = plt.subplots(2, 2, figsize=(11, 8))\n",
"upd, n_raw, n_fused, emerg_log = [], [], [], []\n",
"for k in range(scn[\"T\"]):\n",
" t0 = time.perf_counter()\n",
" rk = by.get(k, [])\n",
" trk.step(k, rk)\n",
" conf = [t for t in trk.tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"grade\"] = grade(t)\n",
" t[\"emerg\"] = np.hypot(*(t[\"x\"][:2] - CENTER)) < ALERT_R and t[\"grade\"] == \"High\"\n",
" n_raw.append(len(rk)); n_fused.append(len(conf))\n",
" for a in ax.flat:\n",
" a.clear()\n",
" for sid in SENSORS: # ํจ๋ 1: ์ผ์๋ณ ์์ ๊ฒฝ๋ณด\n",
" pts = np.array([[r[\"x\"], r[\"y\"]] for r in rk if r[\"sensor\"] == sid]).reshape(-1, 2)\n",
" ax[0, 0].scatter(pts[:, 0], pts[:, 1], s=10, c=COLORS[sid], label=sid)\n",
" ax[0, 0].set_title(f\"Raw sensor alerts t={k}s ({len(rk)})\"); ax[0, 0].legend(fontsize=7, loc=\"upper right\")\n",
" for t in conf: # ํจ๋ 2: ์ตํฉ ํญ์ \n",
" h = np.array(t[\"hist\"])\n",
" c = \"red\" if t[\"emerg\"] else (\"lime\" if t[\"grade\"] == \"High\" else \"orange\")\n",
" ax[0, 1].plot(h[-10:, 1], h[-10:, 2], c=c, lw=2 if t[\"emerg\"] else 1)\n",
" ax[0, 1].text(t[\"x\"][0], t[\"x\"][1], t[\"id\"], fontsize=7, color=c)\n",
" ax[0, 1].add_patch(plt.Circle(CENTER, ALERT_R, fill=False, ls=\"--\", color=\"red\"))\n",
" ax[0, 1].set_title(f\"Fused tracks ({len(conf)})\")\n",
" for a in ax[0]:\n",
" a.set_xlim(-10, 26); a.set_ylim(-14, 18); a.set_aspect(\"equal\")\n",
" ax[1, 0].plot(n_raw, label=\"raw alerts\"); ax[1, 0].plot(n_fused, label=\"fused tracks\"); ax[1, 0].legend(); ax[1, 0].set_title(\"Items on screen per scan\")\n",
" ax[1, 1].axis(\"off\"); ax[1, 1].set_title(\"Track list (operator review)\")\n",
" for i, t in enumerate(sorted(conf, key=lambda t: (not t[\"emerg\"], t[\"grade\"] != \"High\"))[:8]):\n",
" ax[1, 1].text(0, 0.9 - i * 0.11, f\"{t['id']} {t['grade']:4s} sensors {len(t['sensors'])} pos({t['x'][0]:5.1f},{t['x'][1]:5.1f})\"\n",
" + (\" !! APPROACH\" if t[\"emerg\"] else \"\"), color=\"red\" if t[\"emerg\"] else \"white\", fontsize=8, family=\"monospace\")\n",
" if any(t[\"emerg\"] for t in conf):\n",
" emerg_log.append(k)\n",
" for sp in ax[0, 1].spines.values():\n",
" sp.set_color(\"red\"); sp.set_linewidth(3)\n",
" fig.canvas.draw()\n",
" upd.append((time.perf_counter() - t0) * 1000)\n",
"\n",
"# ํญ์ ์ ํ ์๋ต์ฑ: ๋ชฉ๋ก์์ ํญ์ ์ ๊ณ ๋ฅด๋ฉด ๊ธฐ์ฌ ์ผ์ยท์ต๊ทผ ๊ถค์ ยท์๋๋ฅผ ๋ณด์ฌ ์ฃผ๋ ์ฝ๋ฐฑ\n",
"def on_select(tid):\n",
" t = next(t for t in trk.all_tracks() if t[\"id\"] == tid)\n",
" sp = np.hypot(t[\"x\"][2], t[\"x\"][3]) * 1000\n",
" return {\"id\": tid, \"grade\": t.get(\"grade\"), \"sensors\": dict(t[\"sensors\"]), \"speed_mps\": round(sp), \"last_pos\": [round(float(v), 2) for v in t[\"x\"][:2]]}\n",
"\n",
"\n",
"resp = []\n",
"for t in [t for t in trk.tracks if t[\"confirmed\"]]:\n",
" t0 = time.perf_counter(); info = on_select(t[\"id\"]); resp.append((time.perf_counter() - t0) * 1000)\n",
"\n",
"ev = evaluate(trk.all_tracks(), scn)\n",
"red = 1 - np.mean(n_fused[5:]) / np.mean(n_raw[5:])\n",
"print(\"[์ ์ ์ํฉํ(COP) - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ๊ฐ์ ๋ฐฉ์ด ์ง์ญยท์ผ๊ฐ]\")\n",
"print(f\"ํจ๋ 4์ข
(์์ ๊ฒฝ๋ณด, ์ตํฉ ํญ์ , ํ์ ํญ๋ชฉ ์ถ์ด, ํญ์ ๋ชฉ๋ก) {len(upd)}ํ ๊ฐฑ์ \")\n",
"print(f\"ํ๋ฉด ๊ฐฑ์ ์๊ฐ ์ต๋ {max(upd):.0f} ms, ํ๊ท {np.mean(upd):.0f} ms (๋ชฉํ 1์ด ์ด๋ด: {'๋ฌ์ฑ' if max(upd) < 1000 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"ํญ์ ์ ํ ์๋ต ์ต๋ {max(resp):.2f} ms (๋ชฉํ 100ms ์ด๋ด: {'๋ฌ์ฑ' if max(resp) < 100 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"์ค์บ๋น ํ์ ํญ๋ชฉ: ์์ ๊ฒฝ๋ณด ํ๊ท {np.mean(n_raw[5:]):.1f}๊ฐ -> ์ตํฉ ํญ์ {np.mean(n_fused[5:]):.1f}๊ฐ (์ธ์ง ๋ถํ {red:.0%} ๊ฐ์)\")\n",
"fk = [t for t in trk.all_tracks() if t[\"confirmed\"] and not t[\"label\"].startswith(\"TGT\")]\n",
"print(f\"์ตํฉ ๊ฒฐ๊ณผ: ์ ๋ต ํ์ {ev['์ ๋ตํ์ ']}๊ฐ ์ค {ev['ํ์งํ์ ']}๊ฐ ์ถ์ , ๊ฑฐ์ง ํญ์ {ev['๊ฑฐ์งํญ์ ']}๊ฐ ์ค Med ํ์ {sum(t.get('grade') == 'Med' for t in fk)}๊ฐ\")\n",
"print(f\"๋ฐฉ์ด ์ง์ญ ์ ๊ทผ ๊ธด๊ธ ๊ฐ์กฐ ์ค์บ: {len(emerg_log)}ํ (์ฒซ ๋ฐ์ t={emerg_log[0] if emerg_log else '-'}s)\")\n",
"print(\"์ ํ ์์:\", info)\n",
"print(\"์ํฉํ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ํญ์ ๋ถ๋ฆฌยทํตํฉ ํ์ ์ ์กฐ์๊ด ๊ฒํ (๋ฏธ์
7), ๊ต์ ์ฌ๋ถ๋ ๊ต์ ์์นยท์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
06 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[์ ์ ์ํฉํ(COP) - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ๊ฐ์ ๋ฐฉ์ด ์ง์ญยท์ผ๊ฐ]\n",
"ํจ๋ 4์ข
(์์ ๊ฒฝ๋ณด, ์ตํฉ ํญ์ , ํ์ ํญ๋ชฉ ์ถ์ด, ํญ์ ๋ชฉ๋ก) 40ํ ๊ฐฑ์ \n",
"ํ๋ฉด ๊ฐฑ์ ์๊ฐ ์ต๋ 381 ms, ํ๊ท 284 ms (๋ชฉํ 1์ด ์ด๋ด: ๋ฌ์ฑ)\n",
"ํญ์ ์ ํ ์๋ต ์ต๋ 0.09 ms (๋ชฉํ 100ms ์ด๋ด: ๋ฌ์ฑ)\n",
"์ค์บ๋น ํ์ ํญ๋ชฉ: ์์ ๊ฒฝ๋ณด ํ๊ท 23.9๊ฐ -> ์ตํฉ ํญ์ 7.2๊ฐ (์ธ์ง ๋ถํ 70% ๊ฐ์)\n",
"์ตํฉ ๊ฒฐ๊ณผ: ์ ๋ต ํ์ 6๊ฐ ์ค 6๊ฐ ์ถ์ , ๊ฑฐ์ง ํญ์ 2๊ฐ ์ค Med ํ์ 2๊ฐ\n",
"๋ฐฉ์ด ์ง์ญ ์ ๊ทผ ๊ธด๊ธ ๊ฐ์กฐ ์ค์บ: 13ํ (์ฒซ ๋ฐ์ t=27s)\n",
"์ ํ ์์: {'id': 'FT006', 'grade': 'Med', 'sensors': {'EOIR-C': 34}, 'speed_mps': 119, 'last_pos': [1.87, 5.33]}\n",
"์ํฉํ์ ํ๋จ ๋ณด์กฐ์ด๋ฉฐ, ํญ์ ๋ถ๋ฆฌยทํตํฉ ํ์ ์ ์กฐ์๊ด ๊ฒํ (๋ฏธ์
7), ๊ต์ ์ฌ๋ถ๋ ๊ต์ ์์นยท์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
07. ์ค๋ณต ํญ์ ๊ฐ์ ๋ถ๋ฆฌ ๋ฐ ํตํฉ ํ์ ์ ์ํ ์กฐ์๊ด HITL ๊ฒํ /์น์ธ ์ฒด๊ณ ๊ตฌ์ถ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ์ ๊ฐ ์์ถฉ ์ ๋ณด ๋ฐ์ ์ ์กฐ์๊ด์ด ์ง์ ํญ์ ๋ถ๋ฆฌ, ๊ฐ์ ํตํฉ, ์ถ์ ๋ณด๋ฅ๋ฅผ ๊ฒฐ์ ํ ์ ์๋ HITL ๊ฒํ /์น์ธ ์ฒด๊ณ๋ฅผ ๊ตฌํํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ผ์ ๊ฐ ์์ถฉ ์ ๋ณด๋ก ์๊ธด ํญ์ ์ถฉ๋์ ์กฐ์๊ด์ด ์ง์ ํตํฉยท๋ถ๋ฆฌยท๋ณด๋ฅยท์ ์ง๋ก ๊ฒฐ์ ํ๋ HITL ๊ฒํ /์น์ธ ์ฒด๊ณ๋ฅผ ๊ตฌํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์ตํฉ ์์ง์ ์ค๋ณต ์์ฌ, ๋ณํฉ ์์ฌ, ๋จ์ผ ์ผ์ ํญ์ ์ ์ถฉ๋๋ก ์ ์๋ง ํ๊ณ ํญ์ ์ํ๋ ๋ฐ๊พธ์ง ์๋๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/ops02_approval_workflow.py ๋ฐ docs/ops02_hitl_guide.md` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์งํ ์ฑ
์ ๋ถ๊ณ์ ๋ช
ํํ ๋ฐ ์ฑ๊ธํ ์๋ ๋จ์ ๋ฐฉ์ง ์ฒด๊ณ ๋ถํฉ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ์ธ๊ฐ ๊ฒํ ํ(Queue) ๋๊ธฐ์๊ฐ ๋ฐ ์น์ธ/๋ฐ๋ ค ์ฒ๋ฆฌ ๋ก๊น
์ฑ๊ณต๋ฅ 100%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์น์ธ/๋ฐ๋ ค ์ธํฐํ์ด์ค ๋์ ๋ฐ ๊ฐ์ฌ ๋ก๊ทธ(Audit Log) ์์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ชจ์ ์กฐ์์ ์น์ธ/๋ฐ๋ ค ์๋ฎฌ๋ ์ด์
๋ฐ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# [TODO : ํ์ ์ค์ต 7] ์ค๋ณต ํญ์ ๊ฐ์ ๋ถ๋ฆฌ ๋ฐ ํตํฉ ํ์ ์ ์ํ ์กฐ์๊ด HITL ๊ฒํ /์น์ธ ์ฒด๊ณ ๊ตฌ์ถ\n",
"# ๋ฏธ์
์ง์นจ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค์ ํ์ธํ๊ณ ์ฝ๋๋ฅผ ์คํ ๋ฐ ํ๋ํ์ธ์.\n",
"\"\"\"์ผ์ ๊ฐ ์์ถฉ ์ ๋ณด๋ก ์๊ธด ํญ์ ์ถฉ๋(์ค๋ณต ์์ฌ, ๋ณํฉ ์์ฌ, ๋จ์ผ ์ผ์ ํญ์ )์ ์กฐ์๊ด ๊ฒํ ๋๊ธฐ์ด์ ์ฌ๋ฆฌ๊ณ , ๊ฐ์ ํตํฉยท๋ถ๋ฆฌยท์ถ์ ๋ณด๋ฅยทํํ ์ ์ง๋ฅผ ์กฐ์๊ด์ด ๊ทผ๊ฑฐ์ ํจ๊ป ์ง์ ๊ฒฐ์ ํ๋๋ก ํ๋ HITL ๊ฒํ /์น์ธ ์ฒด๊ณ์ ํด์ ์ฒด์ธ ๊ฐ์ฌ ๋ก๊ทธ๋ฅผ ๊ตฌํํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"ACTIONS = {\"MERGE\": \"๊ฐ์ ํตํฉ\", \"SPLIT\": \"ํญ์ ๋ถ๋ฆฌ\", \"HOLD\": \"์ถ์ ๋ณด๋ฅ(๊ฐ์ ์ ์ง)\", \"KEEP\": \"ํํ ์ ์ง\"}\n",
"SEVERITY = {\"DUP\": 0, \"MIXED\": 1, \"SINGLE\": 2} # ์ค๋ณต ์์ฌ > ๋ณํฉ ์์ฌ > ๋จ์ผ ์ผ์\n",
"HOLD_LIMIT_S = 60\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ(km)\n",
"\n",
"\n",
"class WorkflowError(ValueError):\n",
" pass\n",
"\n",
"\n",
"class ReviewQueue:\n",
" def __init__(self, tracks, clock):\n",
" self.tracks, self.items, self.log, self.clock, self.feedback = tracks, {}, [], clock, []\n",
"\n",
" def _record(self, body):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {**body, \"ts\": round(self.clock(), 1)}\n",
" h = hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()\n",
" self.log.append({**body, \"prev\": prev, \"hash\": h})\n",
"\n",
" def verify_log(self):\n",
" prev = \"GENESIS\"\n",
" for n, rec in enumerate(self.log, start=1):\n",
" body = {k: v for k, v in rec.items() if k not in (\"prev\", \"hash\")}\n",
" if rec[\"prev\"] != prev or rec[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False, n\n",
" prev = rec[\"hash\"]\n",
" return True, -1\n",
"\n",
" def submit(self, cid, kind, tids, note):\n",
" \"\"\"์์คํ
์ ์ถฉ๋์ '์ ์'๋ง ํ๊ณ ํญ์ ์ํ๋ ๋ฐ๊พธ์ง ์๋๋ค(PENDING).\"\"\"\n",
" d = min(np.hypot(*(np.array(self.tracks[t][\"pos\"]) - CENTER)) for t in tids)\n",
" self.items[cid] = {\"id\": cid, \"kind\": kind, \"tids\": tids, \"note\": note, \"dist_km\": round(float(d), 1), \"status\": \"PENDING\",\n",
" \"submitted\": self.clock(), \"hold_since\": None, \"escalated\": False}\n",
" self._record({\"event\": \"SUBMIT\", \"id\": cid, \"kind\": kind, \"tids\": tids, \"by\": \"FUSION\"})\n",
"\n",
" def pending(self):\n",
" todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\")]\n",
" return sorted(todo, key=lambda i: (not i[\"escalated\"], SEVERITY[i[\"kind\"]], i[\"dist_km\"], i[\"submitted\"]))\n",
"\n",
" def decide(self, cid, action, operator, reason):\n",
" if action not in ACTIONS:\n",
" raise WorkflowError(f\"ํ์ฉ๋์ง ์์ ์กฐ์น: {action}\")\n",
" if not operator:\n",
" raise WorkflowError(\"์กฐ์๊ด ID ํ์\")\n",
" it = self.items.get(cid)\n",
" if it is None or it[\"status\"] in (\"DONE\",):\n",
" raise WorkflowError(\"๋๊ธฐ์ด์ ์๊ฑฐ๋ ์ด๋ฏธ ์ข
๊ฒฐ๋ ์ถฉ๋\")\n",
" if len(reason.strip()) < 5:\n",
" raise WorkflowError(\"ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\")\n",
" T = [self.tracks[t] for t in it[\"tids\"]]\n",
" if action == \"MERGE\":\n",
" if len(T) < 2:\n",
" raise WorkflowError(\"ํตํฉ์ ํญ์ 2๊ฐ ์ด์ ํ์\")\n",
" gap = np.hypot(*(np.array(T[0][\"pos\"]) - np.array(T[1][\"pos\"])))\n",
" if gap > 1.0 and len(reason.strip()) < 15:\n",
" raise WorkflowError(f\"๊ฐ๊ฒฉ {gap:.1f}km ํญ์ ํตํฉ์ ๊ทผ๊ฑฐ 15์ ์ด์ ํ์(์คํตํฉ ๋ฐฉ์ง)\")\n",
" T[1][\"status\"] = f\"MERGED->{T[0]['id']}\"; T[0][\"sensors\"] = sorted(set(T[0][\"sensors\"]) | set(T[1][\"sensors\"]))\n",
" elif action == \"SPLIT\":\n",
" if len(T[0][\"sensors\"]) < 2:\n",
" raise WorkflowError(\"๋จ์ผ ์ผ์ ํญ์ ์ ๋ถ๋ฆฌ ๋์์ด ์๋\")\n",
" new = f\"{T[0]['id']}b\"\n",
" self.tracks[new] = {\"id\": new, \"pos\": T[0][\"pos\"], \"sensors\": T[0][\"sensors\"][1:], \"status\": \"SPLIT\"}\n",
" T[0][\"sensors\"] = T[0][\"sensors\"][:1]\n",
" elif action == \"HOLD\":\n",
" for t in T:\n",
" t[\"status\"] = \"HOLD\"\n",
" it[\"status\"] = \"HOLD\" if action == \"HOLD\" else \"DONE\"\n",
" it[\"hold_since\"] = self.clock() if action == \"HOLD\" else None\n",
" it[\"decided\"], it[\"action\"] = self.clock(), action\n",
" self._record({\"event\": \"DECIDE\", \"id\": cid, \"action\": action, \"operator\": operator, \"reason\": reason})\n",
"\n",
" def check_holds(self):\n",
" out = []\n",
" for i in self.items.values():\n",
" if i[\"status\"] == \"HOLD\" and not i[\"escalated\"] and self.clock() - i[\"hold_since\"] > HOLD_LIMIT_S:\n",
" i[\"escalated\"] = True; out.append(i[\"id\"])\n",
" self._record({\"event\": \"ESCALATE\", \"id\": i[\"id\"], \"to\": \"๋ฐฉ๊ณต ์ํฉ์ฅ๊ต\", \"by\": \"SYSTEM\"})\n",
" return out\n",
"\n",
" def commander_feedback(self, cid, text):\n",
" self.feedback.append({\"id\": cid, \"text\": text}); self._record({\"event\": \"FEEDBACK\", \"id\": cid, \"text\": text})\n",
"\n",
"\n",
"class FakeClock:\n",
" t = 0.0\n",
"\n",
" def __call__(self):\n",
" return self.t\n",
"\n",
"\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ตํฉ ์์ง์ด ๋๊ธด ๊ฐ์ ํญ์ ๊ณผ ์ถฉ๋ 6๊ฑด\n",
"tracks = {t: {\"id\": t, \"pos\": p, \"sensors\": s, \"status\": \"ACTIVE\"} for t, p, s in [\n",
" (\"FT003\", (9.1, 3.0), [\"RDR-A\", \"RDR-B\"]), (\"FT007\", (9.4, 3.2), [\"EOIR-C\", \"ACU-D\"]), (\"FT010\", (14.0, -3.0), [\"RDR-A\", \"RDR-B\", \"EOIR-C\"]),\n",
" (\"FT012\", (3.5, 12.0), [\"EOIR-C\"]), (\"FT015\", (12.0, 6.0), [\"RDR-A\"]), (\"FT016\", (13.5, 6.5), [\"RDR-B\"]), (\"FT020\", (6.0, 0.5), [\"RDR-A\", \"ACU-D\"])]}\n",
"clock = FakeClock()\n",
"q = ReviewQueue(tracks, clock)\n",
"for cid, kind, tids, note in [(\"C01\", \"DUP\", [\"FT003\", \"FT007\"], \"0.4km ๊ฐ๊ฒฉ, ์๋ ์ ์ฌ\"), (\"C02\", \"MIXED\", [\"FT010\"], \"EO/IR ๋ฐฉ์๊ฐ ๋ ์ด๋์ 2๋ ์ด๊ธ๋จ\"),\n",
" (\"C03\", \"SINGLE\", [\"FT012\"], \"EO/IR ๋จ๋
30์ด ์ ์ง\"), (\"C04\", \"DUP\", [\"FT015\", \"FT016\"], \"1.6km ๊ฐ๊ฒฉ, ๋ ์ด๋๋ณ ๋จ๋
\"),\n",
" (\"C05\", \"SINGLE\", [\"FT020\"], \"์ํฅ ์์ค ํ ๋ ์ด๋ ๋จ๋
\"), (\"C06\", \"MIXED\", [\"FT020\"], \"์ํฅยท๋ ์ด๋ ์์น ๋ถ์ผ์น\")]:\n",
" clock.t += 3; q.submit(cid, kind, tids, note)\n",
"print(\"๊ฒํ ์์(์ถฉ๋ ์ ํยท๋ฐฉ์ด ์ง์ญ ๊ฑฐ๋ฆฌ ์):\", [i[\"id\"] for i in q.pending()])\n",
"print(\"์๋ ์ ์ฉ๋ ์ถฉ๋ ์:\", sum(i[\"status\"] != \"PENDING\" for i in q.items.values()))\n",
"\n",
"blocked = 0\n",
"for cid, a, op, r in [(\"C01\", \"MERGE\", \"\", \"๊ทผ๊ฑฐ ์ถฉ๋ถ\"), (\"C02\", \"AUTO\", \"op1\", \"์๋ ์ฒ๋ฆฌ\"), (\"C03\", \"SPLIT\", \"op1\", \"๋จ๋
ํญ์ ๋ถ๋ฆฌ ์๋\"),\n",
" (\"C04\", \"MERGE\", \"op1\", \"์์น ์ ์ฌ\"), (\"C05\", \"KEEP\", \"op1\", \"ํ์ธ\")]:\n",
" try:\n",
" q.decide(cid, a, op, r)\n",
" except WorkflowError as e:\n",
" blocked += 1; print(f\"์ฐจ๋จ({cid} {a}): {e}\")\n",
"\n",
"clock.t += 10; q.decide(\"C01\", \"MERGE\", \"op1\", \"์๋ยท๊ณ ๋ ์ถ์ ์ผ์น, ๊ฐ์ ๋นํ์ฒด๋ก ํ๋จ\")\n",
"clock.t += 6; q.decide(\"C02\", \"SPLIT\", \"op2\", \"EO/IR ์์์ 2๊ธฐ ๊ทผ์ ๋นํ ํ์ธ\")\n",
"clock.t += 5; q.decide(\"C04\", \"MERGE\", \"op1\", \"๋ ์ด๋ AยทB ์๊ณ ๋ณด์ ํ ๊ถค์ ์ผ์น, ๋์ผ ํ์ ์ผ๋ก ํ๋จ\")\n",
"clock.t += 4; q.decide(\"C03\", \"HOLD\", \"op2\", \"๊ธฐ๋ง ๊ฐ๋ฅ์ฑ, ๋ ์ด๋ ์ฌํ์ธ ์์ฒญ\")\n",
"clock.t += 3; q.decide(\"C05\", \"KEEP\", \"op2\", \"๋ ์ด๋ ์ฐ์ ์ถ์ ์ ์\")\n",
"clock.t += 90\n",
"print(\"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ :\", q.check_holds(), \"/ ์์ ํ ๊ฒํ ์์:\", [i[\"id\"] for i in q.pending()])\n",
"q.commander_feedback(\"C03\", \"EO/IR ๋จ๋
ํญ์ ์ 30์ด ์ด์ ์ง์ ์ ์ํฉ์ฅ๊ต์๊ฒ ์ฆ์ ๋ณด๊ณ \")\n",
"\n",
"df = pd.DataFrame(q.items.values())\n",
"df[\"๋๊ธฐ(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(1)\n",
"print(\"\\n\" + df[[\"id\", \"kind\", \"tids\", \"dist_km\", \"status\", \"action\", \"escalated\", \"๋๊ธฐ(s)\"]].fillna(\"-\").to_string(index=False))\n",
"print(\"\\nํญ์ ์ํ:\", {k: v[\"status\"] for k, v in tracks.items()})\n",
"n_dec = sum(r[\"event\"] == \"DECIDE\" for r in q.log)\n",
"n_auto = sum(r[\"event\"] == \"DECIDE\" and not r.get(\"operator\") for r in q.log)\n",
"n_exp = sum(\"decided\" in i for i in q.items.values())\n",
"print(f\"\\n์๋ ์ ์ฉ {n_auto}๊ฑด / ๊ท์น ์๋ฐ ์ฐจ๋จ {blocked}๊ฑด / ๊ฒฐ์ ๋ก๊ทธ {n_dec}๊ฑด(๊ฒฐ์ {n_exp}๊ฑด, ๋ก๊น
์ฑ๊ณต๋ฅ {n_dec / n_exp:.0%})\")\n",
"print(f\"ํ๊ท ๊ฒํ ๋๊ธฐ {df['๋๊ธฐ(s)'].mean():.1f}s, ๊ฐ์ฌ ๋ก๊ทธ {len(q.log)}๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: {q.verify_log()}\")\n",
"q.log[6][\"action\"] = \"KEEP\"\n",
"print(\"7๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น):\", q.verify_log())\n",
"print(\"\\n์ฑ
์ ๋ถ๊ณ: ์ตํฉ ์์ง=์ถฉ๋ ์ ์, ์กฐ์๊ด=ํตํฉยท๋ถ๋ฆฌยท๋ณด๋ฅ ๊ฒฐ์ , ์ํฉ์ฅ๊ต=์์ ๊ฑด ํ๋จ. ํญ์ ๊ฒฐ์ ์ ๊ต์ ๊ฒฐ์ ์ด ์๋๋ฉฐ ๊ต์ ์ ๊ต์ ์์นยท์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
07 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"๊ฒํ ์์(์ถฉ๋ ์ ํยท๋ฐฉ์ด ์ง์ญ ๊ฑฐ๋ฆฌ ์): ['C01', 'C04', 'C06', 'C02', 'C05', 'C03']\n",
"์๋ ์ ์ฉ๋ ์ถฉ๋ ์: 0\n",
"์ฐจ๋จ(C01 MERGE): ์กฐ์๊ด ID ํ์\n",
"์ฐจ๋จ(C02 AUTO): ํ์ฉ๋์ง ์์ ์กฐ์น: AUTO\n",
"์ฐจ๋จ(C03 SPLIT): ๋จ์ผ ์ผ์ ํญ์ ์ ๋ถ๋ฆฌ ๋์์ด ์๋\n",
"์ฐจ๋จ(C04 MERGE): ๊ฐ๊ฒฉ 1.6km ํญ์ ํตํฉ์ ๊ทผ๊ฑฐ 15์ ์ด์ ํ์(์คํตํฉ ๋ฐฉ์ง)\n",
"์ฐจ๋จ(C05 KEEP): ํ๋จ ๊ทผ๊ฑฐ 5์ ์ด์ ํ์\n",
"๋ณด๋ฅ ๊ธฐํ ์ด๊ณผ ์์ : ['C03'] / ์์ ํ ๊ฒํ ์์: ['C03', 'C06']\n",
"\n",
" id kind tids dist_km status action escalated ๋๊ธฐ(s)\n",
"C01 DUP [FT003, FT007] 1.5 DONE MERGE False 25.0\n",
"C02 MIXED [FT010] 7.8 DONE SPLIT False 28.0\n",
"C03 SINGLE [FT012] 11.0 HOLD HOLD True 34.0\n",
"C04 DUP [FT015, FT016] 5.7 DONE MERGE False 27.0\n",
"C05 SINGLE [FT020] 2.5 DONE KEEP False 31.0\n",
"C06 MIXED [FT020] 2.5 PENDING - False -\n",
"\n",
"ํญ์ ์ํ: {'FT003': 'ACTIVE', 'FT007': 'MERGED->FT003', 'FT010': 'ACTIVE', 'FT012': 'HOLD', 'FT015': 'ACTIVE', 'FT016': 'MERGED->FT015', 'FT020': 'ACTIVE', 'FT010b': 'SPLIT'}\n",
"\n",
"์๋ ์ ์ฉ 0๊ฑด / ๊ท์น ์๋ฐ ์ฐจ๋จ 5๊ฑด / ๊ฒฐ์ ๋ก๊ทธ 5๊ฑด(๊ฒฐ์ 5๊ฑด, ๋ก๊น
์ฑ๊ณต๋ฅ 100%)\n",
"ํ๊ท ๊ฒํ ๋๊ธฐ 29.0s, ๊ฐ์ฌ ๋ก๊ทธ 13๊ฑด, ํด์ ์ฒด์ธ ๊ฒ์ฆ: (True, -1)\n",
"7๋ฒ์งธ ๊ธฐ๋ก ๋ณ์กฐ ํ ๊ฒ์ฆ(False, ๊นจ์ง ์์น): (False, 7)\n",
"\n",
"์ฑ
์ ๋ถ๊ณ: ์ตํฉ ์์ง=์ถฉ๋ ์ ์, ์กฐ์๊ด=ํตํฉยท๋ถ๋ฆฌยท๋ณด๋ฅ ๊ฒฐ์ , ์ํฉ์ฅ๊ต=์์ ๊ฑด ํ๋จ. ํญ์ ๊ฒฐ์ ์ ๊ต์ ๊ฒฐ์ ์ด ์๋๋ฉฐ ๊ต์ ์ ๊ต์ ์์นยท์งํ๊ณํต์ ๋ฐ๋ฅธ๋ค.\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
08. ๋ค์ ์ผ์ ํต์ ๋จ์ ๋ฐ ์ ์์ ์ฌ๋ฐ ํ๊ฒฝ ๋ชจ์ ํญ์ ์ตํฉ ํ๊ณ์น ์คํธ๋ ์ค ํ
์คํธ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"์ผ๋ถ ๋ ์ด๋ ์ผ์ ํผ๊ฒฉ ํต์ ๋จ์ ๋ฐ ์ ์์ ์ฌ๋ฐ ํ๊ฒฝ์ ๊ฐ์ ์๋ฎฌ๋ ์ด์
ํ์ฌ ํญ์ ์ตํฉ ์์ง์ ๊ฒฐํจ ํ์ฉ(Fault-tolerant) ๋ณต์๋ ฅ์ ํ
์คํธํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ ์ด๋ ํต์ ๋จ์ ๊ณผ ์ ์์ ์ฌ๋ฐ ํ๊ฒฝ์์ ํญ์ ์ตํฉ์ ๊ฒฐํจ ํ์ฉ ํ๊ณ๋ฅผ ์ฐพ๊ณ , ํธ๋ํฝ ๊ธ์ฆ ๋ถํ์์๋ ์์ ํ๊ฒ ๋์ํ๋์ง ์ํํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **๋ ์ด๋ ๋จ์ , ๋จ์ผยท๋ณต์ ๋ ์ด๋ ์ฌ๋ฐ ์ธ๊ธฐ๋ฅผ ๋จ๊ณ์ ์ผ๋ก ๋์ฌ ์ฌํ์จ, ๋ถํ , ๊ฑฐ์ง ํญ์ , ์์น ์ค์ฐจ, ํญ์ ์ ํ๋๋ฅผ ๋น๊ต ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `tests/ops02_stress_test.py ๋ฐ ์คํธ๋ ์ค ํ
์คํธ ๋ถ์ ๋ณด๊ณ ์` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ทน๋จ ์ํฉ์์์ ์์ ์คํจ(Fail-safe) ๋ฐ ๊ธฐ๋ฅ ์ถ์ ์๋ ์ ์ ์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: ๋์ ํธ๋ํฝ 500% ์ฆ๊ฐ ์ ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค๋ฅ 0%\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ๋๋ ์๋ฎฌ๋ ์ด์
๋ถํ ์ฃผ์
ํ
์คํธ ํต๊ณผ ๋ฐ ๋ณ๋ชฉ ๊ตฌ๊ฐ ํ๋กํ์ผ๋ง ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ๋ถํ ํ
์คํธ ๋๊ตฌ๋ฅผ ํตํ ์ฒ๋ฆฌ๋(Throughput) ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"๋ ์ด๋ ํต์ ๋จ์ ๊ณผ ์ ์์ ์ฌ๋ฐ ์ธ๊ธฐ๋ฅผ ๋จ๊ณ์ ์ผ๋ก ๋์ฌ ํญ์ ์ตํฉ ์ฑ๋ฅ์ ํ๊ณ๋ฅผ ์ฐพ๊ณ , ๋นํ์ฒดยท์ค๊ฒฝ๋ณด ํธ๋ํฝ์ ํ์์ 5๋ฐฐ๋ก ๋๋ฆฌ๋ฉฐ ์์ ๋ฉ์์ง๋ฅผ ์์ ๋ถํ ์ํ์์ ์์คํ
๋ค์ดยท๋ฐ์ดํฐ ์ ์ค ์์ด ๊ธฐ๋ฅ ์ถ์ ๋ชจ๋๋ก ๋ฒํฐ๋์ง์ ๋ณ๋ชฉ ํจ์๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import cProfile\n",
"import pstats\n",
"import time\n",
"import tracemalloc\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 8] ๋ค์ ์ผ์ ํต์ ๋จ์ ๋ฐ ์ ์์ ์ฌ๋ฐ ํ๊ฒฝ ๋ชจ์ ํญ์ ์ตํฉ ํ๊ณ์น ์คํธ๋ ์ค ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 8] ๋ค์ ์ผ์ ํต์ ๋จ์ ๋ฐ ์ ์์ ์ฌ๋ฐ ํ๊ฒฝ ๋ชจ์ ํญ์ ์ตํฉ ํ๊ณ์น ์คํธ๋ ์ค ํ
์คํธ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# ---- ํ์ฅ ์นผ๋ง ํํฐ(EKF) ๋ค์ค ์ผ์ ํญ์ ์ถ์ ๊ธฐ ๊ณตํต ํจ์ ----\n",
"# ์ํ x = [๋(km), ๋ถ(km), ๋์(km/s), ๋ถ์(km/s)], ์ธก์ z = ์ผ์ ๊ธฐ์ค [๊ฑฐ๋ฆฌ(km), ๋ฐฉ์(rad)]\n",
"def ekf_predict(x, P, dt, q):\n",
" F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], float)\n",
" G = np.array([[dt ** 2 / 2, 0], [0, dt ** 2 / 2], [dt, 0], [0, dt]])\n",
" return F @ x, F @ P @ F.T + q * G @ G.T\n",
"\n",
"\n",
"def h_polar(x, spos):\n",
" dx, dy = x[0] - spos[0], x[1] - spos[1]\n",
" r2 = max(dx * dx + dy * dy, 1e-9); r = np.sqrt(r2)\n",
" return np.array([r, np.arctan2(dx, dy)]), np.array([[dx / r, dy / r, 0, 0], [dy / r2, -dx / r2, 0, 0]])\n",
"\n",
"\n",
"def innovation(x, P, rep):\n",
" s = SENSORS[rep[\"sensor\"]]\n",
" zp, H = h_polar(x, s[\"pos\"])\n",
" y = np.array([rep[\"range_km\"], np.radians(rep[\"az_deg\"])]) - zp\n",
" y[1] = (y[1] + np.pi) % (2 * np.pi) - np.pi # ๋ฐฉ์ ์ฐจ์ด๋ -pi~pi๋ก ๊ฐ์ผ๋ค\n",
" S = H @ P @ H.T + np.diag([rep[\"sr\"] ** 2, np.radians(rep[\"saz\"]) ** 2])\n",
" return y, H, S, float(y @ np.linalg.solve(S, y))\n",
"\n",
"\n",
"class Tracker:\n",
" \"\"\"์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ค์บ(1์ด) ๋จ์๋ก ๋ฐ์ EKF ํญ์ ์ ๊ฐฑ์ ํ๋ค. ๊ฐ์ ์ค์บ ์์์ ์ผ์๋ณ๋ก\n",
" ์์ฐจ ๊ฐฑ์ ํ๋ฏ๋ก, ์ฌ๋ฌ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ณด๋ฉด ํ๋์ ํญ์ ์ผ๋ก ํตํฉ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self, gate=9.21, q=4e-4, confirm=(3, 5), max_miss=4, merge_gate=6.0):\n",
" self.gate, self.q, self.confirm, self.max_miss, self.merge_gate = gate, q, confirm, max_miss, merge_gate\n",
" self.tracks, self.dead, self.nid, self.n_merged = [], [], 0, 0\n",
"\n",
" def _new(self, rep, k):\n",
" C = meas_cov(rep)\n",
" P = np.zeros((4, 4)); P[:2, :2] = C; P[2, 2] = P[3, 3] = 0.25 ** 2\n",
" self.tracks.append({\"id\": f\"FT{self.nid:03d}\", \"x\": np.array([rep[\"x\"], rep[\"y\"], 0, 0]), \"P\": P, \"k\": k,\n",
" \"scans\": [k], \"sensors\": {rep[\"sensor\"]: 1}, \"truths\": [rep[\"truth\"]], \"nis\": [],\n",
" \"hist\": [(k, rep[\"x\"], rep[\"y\"])], \"confirmed\": False, \"miss\": 0, \"born\": k, \"upd\": True})\n",
" self.nid += 1\n",
"\n",
" def _assign(self, tracks, rs, sid):\n",
" \"\"\"๊ฒ์ดํธ(๋งํ ๋ผ๋
ธ๋น์ค ๊ฑฐ๋ฆฌ) ์์์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN). ํ ๋น๋ ๋ณด๊ณ ์ ๋ฒํธ ์งํฉ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" if not tracks or not rs:\n",
" return set()\n",
" spos = SENSORS[sid][\"pos\"]\n",
" Z = np.array([[r[\"range_km\"], np.radians(r[\"az_deg\"])] for r in rs])\n",
" R = np.diag([rs[0][\"sr\"] ** 2, np.radians(rs[0][\"saz\"]) ** 2])\n",
" cost = np.full((len(tracks), len(rs)), 1e6)\n",
" for i, tr in enumerate(tracks):\n",
" zp, H = h_polar(tr[\"x\"], spos)\n",
" Y = Z - zp\n",
" Y[:, 1] = (Y[:, 1] + np.pi) % (2 * np.pi) - np.pi\n",
" Si = np.linalg.inv(H @ tr[\"P\"] @ H.T + R)\n",
" d2 = np.einsum(\"ij,jk,ik->i\", Y, Si, Y)\n",
" cost[i] = np.where(d2 < self.gate, d2, 1e6)\n",
" used = set()\n",
" for i, j in zip(*linear_sum_assignment(cost)):\n",
" if cost[i, j] < self.gate:\n",
" tr, r = tracks[i], rs[j]\n",
" y, H, S, d2 = innovation(tr[\"x\"], tr[\"P\"], r)\n",
" K = tr[\"P\"] @ H.T @ np.linalg.inv(S)\n",
" tr[\"x\"] = tr[\"x\"] + K @ y\n",
" tr[\"P\"] = (np.eye(4) - K @ H) @ tr[\"P\"]\n",
" tr[\"nis\"].append(d2); tr[\"truths\"].append(r[\"truth\"])\n",
" tr[\"sensors\"][sid] = tr[\"sensors\"].get(sid, 0) + 1\n",
" tr[\"upd\"] = True; used.add(j)\n",
" return used\n",
"\n",
" def _merge(self):\n",
" \"\"\"๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ฐ๋ผ๊ฐ๋ ์ค๋ณต ํญ์ (์์นยท์๋๊ฐ ํต๊ณ์ ์ผ๋ก ๊ฐ์)์ ์ค๋๋ ํญ์ ์ผ๋ก ํตํฉํ๋ค.\"\"\"\n",
" drop = set()\n",
" for a in range(len(self.tracks)):\n",
" for b in range(a + 1, len(self.tracks)):\n",
" A, B = self.tracks[a], self.tracks[b]\n",
" if A[\"id\"] in drop or B[\"id\"] in drop:\n",
" continue\n",
" d = A[\"x\"] - B[\"x\"]\n",
" d2 = float(d[:2] @ np.linalg.solve(A[\"P\"][:2, :2] + B[\"P\"][:2, :2], d[:2]))\n",
" if d2 < self.merge_gate and np.hypot(d[2], d[3]) < 0.05:\n",
" keep, lose = (A, B) if len(A[\"scans\"]) >= len(B[\"scans\"]) else (B, A)\n",
" for sid, n in lose[\"sensors\"].items():\n",
" keep[\"sensors\"][sid] = keep[\"sensors\"].get(sid, 0) + n\n",
" keep[\"truths\"] += lose[\"truths\"]; keep[\"scans\"] = sorted(set(keep[\"scans\"]) | set(lose[\"scans\"]))\n",
" keep[\"merged\"] = keep.get(\"merged\", 0) + 1\n",
" drop.add(lose[\"id\"])\n",
" self.tracks = [t for t in self.tracks if t[\"id\"] not in drop]\n",
" self.n_merged += len(drop)\n",
"\n",
" def step(self, k, reps):\n",
" for tr in self.tracks: # 1) ์์ธก\n",
" tr[\"x\"], tr[\"P\"] = ekf_predict(tr[\"x\"], tr[\"P\"], k - tr[\"k\"], self.q)\n",
" tr[\"k\"], tr[\"upd\"] = k, False\n",
" for sid in sorted({r[\"sensor\"] for r in reps}, key=lambda s: SENSORS[s][\"sr\"]): # 2) ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ \n",
" rs = [r for r in reps if r[\"sensor\"] == sid]\n",
" used = self._assign([t for t in self.tracks if t[\"confirmed\"]], rs, sid) # ํ์ ํญ์ ์ฐ์ \n",
" rest = [r for j, r in enumerate(rs) if j not in used]\n",
" used2 = self._assign([t for t in self.tracks if not t[\"confirmed\"]], rest, sid)\n",
" for j, r in enumerate(rest):\n",
" if j not in used2:\n",
" self._new(r, k)\n",
" if self.merge_gate > 0:\n",
" self._merge()\n",
" keep = []\n",
" for tr in self.tracks: # 3) ํ์ ยท์ญ์ ๊ด๋ฆฌ\n",
" if tr[\"upd\"]:\n",
" tr[\"miss\"] = 0\n",
" if tr[\"scans\"][-1] != k:\n",
" tr[\"scans\"].append(k)\n",
" tr[\"hist\"].append((k, tr[\"x\"][0], tr[\"x\"][1]))\n",
" else:\n",
" tr[\"miss\"] += 1\n",
" m, n = self.confirm\n",
" if not tr[\"confirmed\"] and sum(1 for s in tr[\"scans\"] if s > k - n) >= m:\n",
" tr[\"confirmed\"] = True\n",
" limit = self.max_miss if tr[\"confirmed\"] else 1\n",
" (keep if tr[\"miss\"] <= limit else self.dead).append(tr)\n",
" self.tracks = keep\n",
"\n",
" def all_tracks(self):\n",
" return self.dead + self.tracks\n",
"\n",
"\n",
"def run_tracker(reps, **kw):\n",
" trk = Tracker(**kw)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.step(k, by.get(k, []))\n",
" return trk.all_tracks()\n",
"\n",
"\n",
"def label_track(tr):\n",
" vals, cnt = np.unique(tr[\"truths\"], return_counts=True)\n",
" return str(vals[np.argmax(cnt)]), float(cnt.max() / cnt.sum())\n",
"\n",
"\n",
"def evaluate(tracks, scn, min_reports=5):\n",
" \"\"\"์ ๋ต ์ฐ๊ฒฐ ํ๊ฐ: ์ฌํ์จ, ํญ์ ๋ถํ (ํ ํ์ ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ, ์์น ์ค์ฐจ.\"\"\"\n",
" conf = [t for t in tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"label\"], t[\"purity\"] = label_track(t)\n",
" cnt = pd.Series([r[\"truth\"] for r in scn[\"reports\"] if r[\"truth\"].startswith(\"TGT\")]).value_counts()\n",
" truths = [k for k, v in cnt.items() if v >= min_reports]\n",
" per = {g: [t for t in conf if t[\"label\"] == g] for g in truths}\n",
" found = [g for g in truths if per[g]]\n",
" split = sum(max(0, len(v) - 1) for v in per.values())\n",
" false = [t for t in conf if not t[\"label\"].startswith(\"TGT\")]\n",
" merge = [t for t in conf if t[\"label\"].startswith(\"TGT\") and t[\"purity\"] < 0.7]\n",
" err = []\n",
" for g in found:\n",
" for t in per[g]:\n",
" for k, x, y in t[\"hist\"][3:]:\n",
" if 0 <= k < scn[\"T\"]:\n",
" err.append(np.hypot(x - scn[\"targets\"][g][k, 0], y - scn[\"targets\"][g][k, 1]))\n",
" n = len(truths)\n",
" acc = len(found) / max(n + split + len(false) + len(merge), 1)\n",
" return {\"์ ๋ตํ์ \": n, \"ํ์งํ์ \": len(found), \"์ฌํ์จ\": round(len(found) / max(n, 1), 3), \"๋ถํ \": split,\n",
" \"๊ฑฐ์งํญ์ \": len(false), \"๋ณํฉ์ค๋ฅ\": len(merge), \"์์นRMSE(km)\": round(float(np.sqrt(np.mean(np.square(err)))) if err else np.nan, 3),\n",
" \"ํญ์ ์ ํ๋\": round(acc, 3)}\n",
"\n",
"# 1) ๊ฒฐํจ ํ์ฉ ํ๊ณ: ์ผ์ ๋จ์ ยท์ฌ๋ฐ ๋จ๊ณ๋ณ ์ตํฉ ์ฑ๋ฅ\n",
"print(\"[๊ฒฐํจ ํ์ฉ ํ๊ณ - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ์๋๋ฆฌ์ค๋ณ ํ์ 6๊ฐ x 60์ด]\")\n",
"cases = [(\"์ ์\", {}), (\"RDR-B ๋จ์ 30์ด\", dict(dropout=(\"RDR-B\", 15, 45))), (\"RDR-A ๋จ์ 60์ด\", dict(dropout=(\"RDR-A\", 0, 60))),\n",
" (\"RDR-A ์ฌ๋ฐ x3\", dict(noise_mult={\"RDR-A\": 3.0})), (\"๋ ์ด๋ 2๋ ์ฌ๋ฐ x3\", dict(noise_mult={\"RDR-A\": 3.0, \"RDR-B\": 3.0})),\n",
" (\"๋ ์ด๋ 2๋ ์ฌ๋ฐ x6\", dict(noise_mult={\"RDR-A\": 6.0, \"RDR-B\": 6.0})), (\"RDR-A ๋จ์ +RDR-B ์ฌ๋ฐ x6\", dict(dropout=(\"RDR-A\", 0, 60), noise_mult={\"RDR-B\": 6.0}))]\n",
"rows = []\n",
"for i, (nm, kw) in enumerate(cases):\n",
" evs = [evaluate(run_tracker(prepare(s)), s) for s in [make_scenario(800 + 10 * i + j, **kw) for j in range(2)]]\n",
" rows.append({\"์กฐ๊ฑด\": nm, \"์ฌํ์จ\": np.mean([e[\"์ฌํ์จ\"] for e in evs]), \"๋ถํ \": sum(e[\"๋ถํ \"] for e in evs),\n",
" \"๊ฑฐ์งํญ์ \": sum(e[\"๊ฑฐ์งํญ์ \"] for e in evs), \"์์นRMSE(km)\": round(np.mean([e[\"์์นRMSE(km)\"] for e in evs]), 3),\n",
" \"ํญ์ ์ ํ๋\": round(np.mean([e[\"ํญ์ ์ ํ๋\"] for e in evs]), 3)})\n",
"L = pd.DataFrame(rows)\n",
"print(L.to_string(index=False))\n",
"lim = L[L[\"ํญ์ ์ ํ๋\"] < 0.8][\"์กฐ๊ฑด\"].tolist()\n",
"print(\"ํญ์ ์ ํ๋ 80% ๋ฏธ๋ง(ํ๊ณ):\", lim if lim else \"์์\", \"-> ํด๋น ์กฐ๊ฑด์ ์ํฉํ์ '์ตํฉ ์ ๋ขฐ ์ ํ' ํ์, ํตํฉยท๋ถ๋ฆฌ ๊ฒฐ์ ์ ์กฐ์๊ด ๊ฒํ ๋ก ์ ํ\")\n",
"\n",
"\n",
"# 2) ๋ถํ ์ํ: ํ์(ํ์ 6, ์ค๊ฒฝ๋ณด 1๋ฐฐ) vs 500%(ํ์ 30, ์ค๊ฒฝ๋ณด 5๋ฐฐ), ์์ ๋ฉ์์ง 2% ์ฃผ์
\n",
"def safe_prepare(scn, rng):\n",
" raw = list(scn[\"reports\"])\n",
" for k in np.flatnonzero(rng.random(len(raw)) < 0.02): # ์์ ๋ฉ์์ง: ๊ฒฐ์ธกยท๋ฏธ๋ฑ๋ก ์ผ์ยท์ค๋ณต\n",
" bad = dict(raw[k])\n",
" kind = k % 3\n",
" if kind == 0:\n",
" bad[\"range_km\"] = None\n",
" elif kind == 1:\n",
" bad[\"sensor\"] = \"UNKNOWN\"\n",
" bad[\"msg_id\"] = raw[k][\"msg_id\"] if kind == 2 else bad[\"msg_id\"] + \"x\"\n",
" raw.append(bad)\n",
" reg = [r for r in raw if r.get(\"sensor\") in SENSORS]\n",
" q_unknown = len(raw) - len(reg)\n",
" reps, why = clean(reg)\n",
" off = estimate_offsets(scn[\"heartbeats\"])\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"]), len(raw), q_unknown + sum(why.values())\n",
"\n",
"\n",
"def run_load(n, fa, seed, budget_ms=1000.0, degrade_ms=150.0):\n",
" rng = np.random.default_rng(seed)\n",
" scn = make_scenario(seed, n=n, T=40, fa_mult=fa)\n",
" reps, total, quar = safe_prepare(scn, rng)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" trk, done, crash, degraded, worst = Tracker(), 0, 0, 0, 0.0\n",
" tracemalloc.start(); m0 = tracemalloc.get_traced_memory()[0]\n",
" last = 0.0\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.merge_gate = 0.0 if last > degrade_ms else 6.0 # ๊ธฐ๋ฅ ์ถ์: ์ง์ ์ค์บ์ด ๋๋ฆฌ๋ฉด ์ค๋ณต ํตํฉ ๋จ๊ณ๋ฅผ 1์ค์บ ์๋ต\n",
" degraded += trk.merge_gate == 0.0\n",
" t0 = time.perf_counter()\n",
" try:\n",
" trk.step(k, by.get(k, [])); done += len(by.get(k, []))\n",
" except Exception:\n",
" crash += 1\n",
" last = (time.perf_counter() - t0) * 1000; worst = max(worst, last)\n",
" mem = (tracemalloc.get_traced_memory()[0] - m0) / 1e6; tracemalloc.stop()\n",
" ev = evaluate(trk.all_tracks(), scn)\n",
" return {\"์
๋ ฅ\": total, \"์ฒ๋ฆฌ\": done, \"๊ฒฉ๋ฆฌ\": quar, \"์ ์ค\": total - done - quar, \"๋ค์ด\": crash, \"๊ธฐ๋ฅ์ถ์ ์ค์บ\": degraded,\n",
" \"์ต๋ ms/์ค์บ\": round(worst, 1), \"๋ฉ๋ชจ๋ฆฌ์ฆ๊ฐMB\": round(mem, 2), \"์ฌํ์จ\": ev[\"์ฌํ์จ\"]}\n",
"\n",
"\n",
"print(\"\\n[๋ถํ ์ํ]\")\n",
"res = {\"ํ์\": run_load(6, 1.0, 901), \"500%\": run_load(30, 5.0, 902)}\n",
"print(pd.DataFrame(list(res.values()), index=list(res)).to_string())\n",
"hi = res[\"500%\"]\n",
"print(f\"ํธ๋ํฝ {hi['์
๋ ฅ'] / res['ํ์']['์
๋ ฅ']:.1f}๋ฐฐ: ์์คํ
๋ค์ด {hi['๋ค์ด']}๊ฑด, ๋ฐ์ดํฐ ์ ์ค {hi['์ ์ค'] / hi['์
๋ ฅ']:.1%} \"\n",
" f\"(๋ชฉํ 0๊ฑด/0%: {'๋ฌ์ฑ' if hi['๋ค์ด'] == 0 and hi['์ ์ค'] == 0 else '๋ฏธ๋ฌ'}), ์ค์บ ์ต๋ {hi['์ต๋ ms/์ค์บ']} ms(1์ด ์ฃผ๊ธฐ ์ด๋ด: {'์' if hi['์ต๋ ms/์ค์บ'] < 1000 else '์๋์ค'})\")\n",
"\n",
"# 3) ๋ณ๋ชฉ ํ๋กํ์ผ๋ง(500% ์กฐ๊ฑด)\n",
"scn = make_scenario(902, n=30, T=20, fa_mult=5.0)\n",
"reps = prepare(scn)\n",
"pr = cProfile.Profile(); pr.enable(); run_tracker(reps); pr.disable()\n",
"st = pstats.Stats(pr).stats\n",
"tot = {f\"{fn[2]}\": v[2] for fn, v in st.items()} # ํจ์๋ณ ์์ฒด ์คํ ์๊ฐ\n",
"top = sorted(((k, v) for k, v in tot.items() if not k.startswith(\"<\")), key=lambda x: -x[1])[:4]\n",
"cum = {fn[2]: v[3] for fn, v in st.items() if fn[2] in (\"_assign\", \"_merge\", \"step\", \"ekf_predict\")}\n",
"allt = cum.get(\"step\", 1e-9)\n",
"print(\"\\n[๋ณ๋ชฉ ํ๋กํ์ผ] ๋จ๊ณ๋ณ ๋์ ์๊ฐ ๋น์จ:\", {k: f\"{v / allt:.0%}\" for k, v in cum.items() if k != \"step\"})\n",
"print(\"์์ฒด ์๊ฐ ์์ ํจ์:\", [f\"{k}({v * 1000:.0f}ms)\" for k, v in top])\n",
"bn = max((k for k in cum if k != \"step\"), key=cum.get)\n",
"print(f\"๋ณ๋ชฉ: {bn}\", \"-> ๊ฐ์ ๋ฐฉํฅ: ์ค๋ณต ํตํฉ(_merge)์ ๊ฒฉ์ ๋ถํ ๋ก ๋น๊ต ์์ ์ค์ด๊ณ , ํ ๋น(_assign)์ ํญ์ ๋ณ ์ญํ๋ ฌ ๋ฐ๋ณต์ ๋ฒกํฐํํ๋ค. ๊ธฐ๋ฅ ์ถ์ ์์๋ ๊ฒฝ๋ณด๋ ๋ฒ๋ฆฌ์ง ์๋๋ค(Fail-safe).\")\n",
"\n",
"plt.figure(figsize=(7, 3.5))\n",
"plt.plot(L[\"์กฐ๊ฑด\"], L[\"์ฌํ์จ\"], \"o-\", label=\"recall\"); plt.plot(L[\"์กฐ๊ฑด\"], L[\"ํญ์ ์ ํ๋\"], \"s--\", label=\"track accuracy\")\n",
"plt.axhline(0.8, c=\"k\", ls=\":\"); plt.xticks(range(len(L)), [f\"C{i}\" for i in range(len(L))]); plt.legend(); plt.title(\"Fusion limit under dropout / jamming (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
08 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[๊ฒฐํจ ํ์ฉ ํ๊ณ - ๊ต์ก์ฉ๊ฐ์์๋ฃ, ์๋๋ฆฌ์ค๋ณ ํ์ 6๊ฐ x 60์ด]\n",
" ์กฐ๊ฑด ์ฌํ์จ ๋ถํ ๊ฑฐ์งํญ์ ์์นRMSE(km) ํญ์ ์ ํ๋\n",
" ์ ์ 1.0 0 0 0.048 1.000\n",
" RDR-B ๋จ์ 30์ด 1.0 0 0 0.052 1.000\n",
" RDR-A ๋จ์ 60์ด 1.0 0 0 0.070 1.000\n",
" RDR-A ์ฌ๋ฐ x3 1.0 0 0 0.062 1.000\n",
" ๋ ์ด๋ 2๋ ์ฌ๋ฐ x3 1.0 1 1 0.099 0.804\n",
" ๋ ์ด๋ 2๋ ์ฌ๋ฐ x6 1.0 1 5 0.174 0.675\n",
"RDR-A ๋จ์ +RDR-B ์ฌ๋ฐ x6 1.0 2 5 0.198 0.634\n",
"ํญ์ ์ ํ๋ 80% ๋ฏธ๋ง(ํ๊ณ): ['๋ ์ด๋ 2๋ ์ฌ๋ฐ x6', 'RDR-A ๋จ์ +RDR-B ์ฌ๋ฐ x6'] -> ํด๋น ์กฐ๊ฑด์ ์ํฉํ์ '์ตํฉ ์ ๋ขฐ ์ ํ' ํ์, ํตํฉยท๋ถ๋ฆฌ ๊ฒฐ์ ์ ์กฐ์๊ด ๊ฒํ ๋ก ์ ํ\n",
"\n",
"[๋ถํ ์ํ]\n",
" ์
๋ ฅ ์ฒ๋ฆฌ ๊ฒฉ๋ฆฌ ์ ์ค ๋ค์ด ๊ธฐ๋ฅ์ถ์ ์ค์บ ์ต๋ ms/์ค์บ ๋ฉ๋ชจ๋ฆฌ์ฆ๊ฐMB ์ฌํ์จ\n",
"ํ์ 953 928 25 0 0 0 35.5 0.20 1.0\n",
"500% 4661 4581 80 0 0 19 335.9 1.11 1.0\n",
"ํธ๋ํฝ 4.9๋ฐฐ: ์์คํ
๋ค์ด 0๊ฑด, ๋ฐ์ดํฐ ์ ์ค 0.0% (๋ชฉํ 0๊ฑด/0%: ๋ฌ์ฑ), ์ค์บ ์ต๋ 335.9 ms(1์ด ์ฃผ๊ธฐ ์ด๋ด: ์)\n",
"\n",
"[๋ณ๋ชฉ ํ๋กํ์ผ] ๋จ๊ณ๋ณ ๋์ ์๊ฐ ๋น์จ: {'_assign': '29%', '_merge': '69%', 'ekf_predict': '1%'}\n",
"์์ฒด ์๊ฐ ์์ ํจ์: ['_merge(409ms)', 'solve(396ms)', '_assign(190ms)', '_commonType(143ms)']\n",
"๋ณ๋ชฉ: _merge -> ๊ฐ์ ๋ฐฉํฅ: ์ค๋ณต ํตํฉ(_merge)์ ๊ฒฉ์ ๋ถํ ๋ก ๋น๊ต ์์ ์ค์ด๊ณ , ํ ๋น(_assign)์ ํญ์ ๋ณ ์ญํ๋ ฌ ๋ฐ๋ณต์ ๋ฒกํฐํํ๋ค. ๊ธฐ๋ฅ ์ถ์ ์์๋ ๊ฒฝ๋ณด๋ ๋ฒ๋ฆฌ์ง ์๋๋ค(Fail-safe).\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
09. AAR ์ผ์ง ๊ธฐ๋ฐ ํญ์ ์๊ณก/๋ถํ (Track Split) ์ค๋ฅ ๋ถ์ ๋ฐ ์ตํฉ ์๊ณ๊ฐ ์ต์ ํ\n",
"**์์์๊ฐ**: ์ฝ 3.0์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"AAR ์ผ์ง๋ฅผ ๋ถ์ํ์ฌ ํญ์ ์ด ๋๋ก ์ชผ๊ฐ์ง๋ ํธ๋ ๋ถํ (Track Split) ๋ฐ ๊ฑฐ์ง ํญ์ ์ค๋ฅ ์ฌ๋ก๋ฅผ ์๋ณํ๊ณ ์ตํฉ ์๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ๋ณด์ ํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **์ด๊ธฐ ๋ฐฐ์น ํ๋ผ๋ฏธํฐ์ ๋ชจ์ AAR ์ผ์ง์์ ํญ์ ๋ถํ ยท๊ฑฐ์ง ํญ์ ์ฌ๋ก๋ฅผ ๋ถ์ํ๊ณ ์ตํฉ ์๊ณ๊ฐ์ ์ฌ๋ณด์ ํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **AAR ์ผ์ง์ ์ค๋ฅ๋ฅผ ๋ถํ , ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ๋ก ๋ถ๋ฅํด ์กฐ๊ฑด๋ณ๋ก ์ง๊ณ ์ํํ๊ณ ๊ฒ์ฆํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `docs/ops02_aar_report.md ๋ฐ configs/ops02_tuned_params.yaml` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ์ค๋ฅ ์์ธ ๊ท๋ช
์ ๊ฐ๊ด์ฑ ๋ฐ ๊ฐ์ ์กฐ์น์ ๋
ผ๋ฆฌ์ ํ๋น์ฑ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: AAR ํผ๋๋ฐฑ ์ ์ฉ ํ ์ข
ํฉ ์ ํ๋ 10%p ์ด์ ํฅ์, ์ฌ๋ฐ ์ค๋ฅ 0๊ฑด\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: AAR ๋ณด๊ณ ์ ์์ฑ ๋ฐ ํ๋ผ๋ฏธํฐ ํ๋ ์ ํ ํ๊ท ํ
์คํธ ํต๊ณผ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ํ๊ท ํ
์คํธ(Regression Test) ์ค์ํธ ์ ์ฒด ์๋ ์คํ ๋ฐ ๋น๊ต\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ด๊ธฐ ๋ฐฐ์น ํ๋ผ๋ฏธํฐ๋ก ์ด์ฉํ ๋ชจ์ AAR ์ผ์ง์์ ํญ์ ๋ถํ (Track Split)ยท๊ฑฐ์ง ํญ์ ยท๋ณํฉ ์ค๋ฅ ์ฌ๋ก๋ฅผ ์ฐพ์ ์์ธ์ ๋ถ์ํ๊ณ , ํ๋ก์ธ์ค ์ก์ยท๊ฒ์ดํธยทํ์ ๊ท์นยท์ญ์ ๊ธฐ์คยท์ค๋ณต ํตํฉ ๊ฒ์ดํธ๋ฅผ ์ฌํ์ํด ์ตํฉ ์๊ณ๊ฐ์ ์ฌ๋ณด์ ํ ๋ค ์ ์๋๋ฆฌ์ค ํ๊ท ์ํ์ผ๋ก ์ ํ๋ ํฅ์๊ณผ ์ฌ๋ฐ ์ค๋ฅ๋ฅผ ํ์ธํ๋ค.\"\"\"\n",
"import itertools\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ์ผ์ง ๊ธฐ๋ฐ ํญ์ ์๊ณก/๋ถํ (Track Split) ์ค๋ฅ ๋ถ์ ๋ฐ ์ตํฉ ์๊ณ๊ฐ ์ต์ ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 9] AAR ์ผ์ง ๊ธฐ๋ฐ ํญ์ ์๊ณก/๋ถํ (Track Split) ์ค๋ฅ ๋ถ์ ๋ฐ ์ตํฉ ์๊ณ๊ฐ ์ต์ ํ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# ---- ํ์ฅ ์นผ๋ง ํํฐ(EKF) ๋ค์ค ์ผ์ ํญ์ ์ถ์ ๊ธฐ ๊ณตํต ํจ์ ----\n",
"# ์ํ x = [๋(km), ๋ถ(km), ๋์(km/s), ๋ถ์(km/s)], ์ธก์ z = ์ผ์ ๊ธฐ์ค [๊ฑฐ๋ฆฌ(km), ๋ฐฉ์(rad)]\n",
"def ekf_predict(x, P, dt, q):\n",
" F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], float)\n",
" G = np.array([[dt ** 2 / 2, 0], [0, dt ** 2 / 2], [dt, 0], [0, dt]])\n",
" return F @ x, F @ P @ F.T + q * G @ G.T\n",
"\n",
"\n",
"def h_polar(x, spos):\n",
" dx, dy = x[0] - spos[0], x[1] - spos[1]\n",
" r2 = max(dx * dx + dy * dy, 1e-9); r = np.sqrt(r2)\n",
" return np.array([r, np.arctan2(dx, dy)]), np.array([[dx / r, dy / r, 0, 0], [dy / r2, -dx / r2, 0, 0]])\n",
"\n",
"\n",
"def innovation(x, P, rep):\n",
" s = SENSORS[rep[\"sensor\"]]\n",
" zp, H = h_polar(x, s[\"pos\"])\n",
" y = np.array([rep[\"range_km\"], np.radians(rep[\"az_deg\"])]) - zp\n",
" y[1] = (y[1] + np.pi) % (2 * np.pi) - np.pi # ๋ฐฉ์ ์ฐจ์ด๋ -pi~pi๋ก ๊ฐ์ผ๋ค\n",
" S = H @ P @ H.T + np.diag([rep[\"sr\"] ** 2, np.radians(rep[\"saz\"]) ** 2])\n",
" return y, H, S, float(y @ np.linalg.solve(S, y))\n",
"\n",
"\n",
"class Tracker:\n",
" \"\"\"์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ค์บ(1์ด) ๋จ์๋ก ๋ฐ์ EKF ํญ์ ์ ๊ฐฑ์ ํ๋ค. ๊ฐ์ ์ค์บ ์์์ ์ผ์๋ณ๋ก\n",
" ์์ฐจ ๊ฐฑ์ ํ๋ฏ๋ก, ์ฌ๋ฌ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ณด๋ฉด ํ๋์ ํญ์ ์ผ๋ก ํตํฉ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self, gate=9.21, q=4e-4, confirm=(3, 5), max_miss=4, merge_gate=6.0):\n",
" self.gate, self.q, self.confirm, self.max_miss, self.merge_gate = gate, q, confirm, max_miss, merge_gate\n",
" self.tracks, self.dead, self.nid, self.n_merged = [], [], 0, 0\n",
"\n",
" def _new(self, rep, k):\n",
" C = meas_cov(rep)\n",
" P = np.zeros((4, 4)); P[:2, :2] = C; P[2, 2] = P[3, 3] = 0.25 ** 2\n",
" self.tracks.append({\"id\": f\"FT{self.nid:03d}\", \"x\": np.array([rep[\"x\"], rep[\"y\"], 0, 0]), \"P\": P, \"k\": k,\n",
" \"scans\": [k], \"sensors\": {rep[\"sensor\"]: 1}, \"truths\": [rep[\"truth\"]], \"nis\": [],\n",
" \"hist\": [(k, rep[\"x\"], rep[\"y\"])], \"confirmed\": False, \"miss\": 0, \"born\": k, \"upd\": True})\n",
" self.nid += 1\n",
"\n",
" def _assign(self, tracks, rs, sid):\n",
" \"\"\"๊ฒ์ดํธ(๋งํ ๋ผ๋
ธ๋น์ค ๊ฑฐ๋ฆฌ) ์์์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN). ํ ๋น๋ ๋ณด๊ณ ์ ๋ฒํธ ์งํฉ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" if not tracks or not rs:\n",
" return set()\n",
" spos = SENSORS[sid][\"pos\"]\n",
" Z = np.array([[r[\"range_km\"], np.radians(r[\"az_deg\"])] for r in rs])\n",
" R = np.diag([rs[0][\"sr\"] ** 2, np.radians(rs[0][\"saz\"]) ** 2])\n",
" cost = np.full((len(tracks), len(rs)), 1e6)\n",
" for i, tr in enumerate(tracks):\n",
" zp, H = h_polar(tr[\"x\"], spos)\n",
" Y = Z - zp\n",
" Y[:, 1] = (Y[:, 1] + np.pi) % (2 * np.pi) - np.pi\n",
" Si = np.linalg.inv(H @ tr[\"P\"] @ H.T + R)\n",
" d2 = np.einsum(\"ij,jk,ik->i\", Y, Si, Y)\n",
" cost[i] = np.where(d2 < self.gate, d2, 1e6)\n",
" used = set()\n",
" for i, j in zip(*linear_sum_assignment(cost)):\n",
" if cost[i, j] < self.gate:\n",
" tr, r = tracks[i], rs[j]\n",
" y, H, S, d2 = innovation(tr[\"x\"], tr[\"P\"], r)\n",
" K = tr[\"P\"] @ H.T @ np.linalg.inv(S)\n",
" tr[\"x\"] = tr[\"x\"] + K @ y\n",
" tr[\"P\"] = (np.eye(4) - K @ H) @ tr[\"P\"]\n",
" tr[\"nis\"].append(d2); tr[\"truths\"].append(r[\"truth\"])\n",
" tr[\"sensors\"][sid] = tr[\"sensors\"].get(sid, 0) + 1\n",
" tr[\"upd\"] = True; used.add(j)\n",
" return used\n",
"\n",
" def _merge(self):\n",
" \"\"\"๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ฐ๋ผ๊ฐ๋ ์ค๋ณต ํญ์ (์์นยท์๋๊ฐ ํต๊ณ์ ์ผ๋ก ๊ฐ์)์ ์ค๋๋ ํญ์ ์ผ๋ก ํตํฉํ๋ค.\"\"\"\n",
" drop = set()\n",
" for a in range(len(self.tracks)):\n",
" for b in range(a + 1, len(self.tracks)):\n",
" A, B = self.tracks[a], self.tracks[b]\n",
" if A[\"id\"] in drop or B[\"id\"] in drop:\n",
" continue\n",
" d = A[\"x\"] - B[\"x\"]\n",
" d2 = float(d[:2] @ np.linalg.solve(A[\"P\"][:2, :2] + B[\"P\"][:2, :2], d[:2]))\n",
" if d2 < self.merge_gate and np.hypot(d[2], d[3]) < 0.05:\n",
" keep, lose = (A, B) if len(A[\"scans\"]) >= len(B[\"scans\"]) else (B, A)\n",
" for sid, n in lose[\"sensors\"].items():\n",
" keep[\"sensors\"][sid] = keep[\"sensors\"].get(sid, 0) + n\n",
" keep[\"truths\"] += lose[\"truths\"]; keep[\"scans\"] = sorted(set(keep[\"scans\"]) | set(lose[\"scans\"]))\n",
" keep[\"merged\"] = keep.get(\"merged\", 0) + 1\n",
" drop.add(lose[\"id\"])\n",
" self.tracks = [t for t in self.tracks if t[\"id\"] not in drop]\n",
" self.n_merged += len(drop)\n",
"\n",
" def step(self, k, reps):\n",
" for tr in self.tracks: # 1) ์์ธก\n",
" tr[\"x\"], tr[\"P\"] = ekf_predict(tr[\"x\"], tr[\"P\"], k - tr[\"k\"], self.q)\n",
" tr[\"k\"], tr[\"upd\"] = k, False\n",
" for sid in sorted({r[\"sensor\"] for r in reps}, key=lambda s: SENSORS[s][\"sr\"]): # 2) ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ \n",
" rs = [r for r in reps if r[\"sensor\"] == sid]\n",
" used = self._assign([t for t in self.tracks if t[\"confirmed\"]], rs, sid) # ํ์ ํญ์ ์ฐ์ \n",
" rest = [r for j, r in enumerate(rs) if j not in used]\n",
" used2 = self._assign([t for t in self.tracks if not t[\"confirmed\"]], rest, sid)\n",
" for j, r in enumerate(rest):\n",
" if j not in used2:\n",
" self._new(r, k)\n",
" if self.merge_gate > 0:\n",
" self._merge()\n",
" keep = []\n",
" for tr in self.tracks: # 3) ํ์ ยท์ญ์ ๊ด๋ฆฌ\n",
" if tr[\"upd\"]:\n",
" tr[\"miss\"] = 0\n",
" if tr[\"scans\"][-1] != k:\n",
" tr[\"scans\"].append(k)\n",
" tr[\"hist\"].append((k, tr[\"x\"][0], tr[\"x\"][1]))\n",
" else:\n",
" tr[\"miss\"] += 1\n",
" m, n = self.confirm\n",
" if not tr[\"confirmed\"] and sum(1 for s in tr[\"scans\"] if s > k - n) >= m:\n",
" tr[\"confirmed\"] = True\n",
" limit = self.max_miss if tr[\"confirmed\"] else 1\n",
" (keep if tr[\"miss\"] <= limit else self.dead).append(tr)\n",
" self.tracks = keep\n",
"\n",
" def all_tracks(self):\n",
" return self.dead + self.tracks\n",
"\n",
"\n",
"def run_tracker(reps, **kw):\n",
" trk = Tracker(**kw)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.step(k, by.get(k, []))\n",
" return trk.all_tracks()\n",
"\n",
"\n",
"def label_track(tr):\n",
" vals, cnt = np.unique(tr[\"truths\"], return_counts=True)\n",
" return str(vals[np.argmax(cnt)]), float(cnt.max() / cnt.sum())\n",
"\n",
"\n",
"def evaluate(tracks, scn, min_reports=5):\n",
" \"\"\"์ ๋ต ์ฐ๊ฒฐ ํ๊ฐ: ์ฌํ์จ, ํญ์ ๋ถํ (ํ ํ์ ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ, ์์น ์ค์ฐจ.\"\"\"\n",
" conf = [t for t in tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"label\"], t[\"purity\"] = label_track(t)\n",
" cnt = pd.Series([r[\"truth\"] for r in scn[\"reports\"] if r[\"truth\"].startswith(\"TGT\")]).value_counts()\n",
" truths = [k for k, v in cnt.items() if v >= min_reports]\n",
" per = {g: [t for t in conf if t[\"label\"] == g] for g in truths}\n",
" found = [g for g in truths if per[g]]\n",
" split = sum(max(0, len(v) - 1) for v in per.values())\n",
" false = [t for t in conf if not t[\"label\"].startswith(\"TGT\")]\n",
" merge = [t for t in conf if t[\"label\"].startswith(\"TGT\") and t[\"purity\"] < 0.7]\n",
" err = []\n",
" for g in found:\n",
" for t in per[g]:\n",
" for k, x, y in t[\"hist\"][3:]:\n",
" if 0 <= k < scn[\"T\"]:\n",
" err.append(np.hypot(x - scn[\"targets\"][g][k, 0], y - scn[\"targets\"][g][k, 1]))\n",
" n = len(truths)\n",
" acc = len(found) / max(n + split + len(false) + len(merge), 1)\n",
" return {\"์ ๋ตํ์ \": n, \"ํ์งํ์ \": len(found), \"์ฌํ์จ\": round(len(found) / max(n, 1), 3), \"๋ถํ \": split,\n",
" \"๊ฑฐ์งํญ์ \": len(false), \"๋ณํฉ์ค๋ฅ\": len(merge), \"์์นRMSE(km)\": round(float(np.sqrt(np.mean(np.square(err)))) if err else np.nan, 3),\n",
" \"ํญ์ ์ ํ๋\": round(acc, 3)}\n",
"\n",
"INIT = dict(gate=5.99, q=5e-5, confirm=(2, 3), max_miss=1, merge_gate=0.0) # 1์ฐจ ๋ฐฐ์น ํ๋ผ๋ฏธํฐ(๊ฐ์)\n",
"AAR_SCN = [dict(seed=1001, special=\"turn\", weather=\"rain\"), dict(seed=1002, special=\"cross\", fa_mult=2.0),\n",
" dict(seed=1003, dropout=(\"RDR-A\", 20, 35), weather=\"fog\"), dict(seed=1004, special=\"fast\", ghost=True)]\n",
"\n",
"\n",
"def run(kw, params, T=40):\n",
" scn = make_scenario(T=T, **kw)\n",
" trk = run_tracker(prepare(scn), **params)\n",
" return evaluate(trk, scn), trk\n",
"\n",
"\n",
"def errors(trk):\n",
" \"\"\"์ค๋ฅ ์ฌ๋ก ๋ชฉ๋ก: ๋ถํ (๊ฐ์ ํ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ.\"\"\"\n",
" conf = [t for t in trk if t[\"confirmed\"]]\n",
" out, seen = [], {}\n",
" for t in sorted(conf, key=lambda t: t[\"born\"]):\n",
" if not t[\"label\"].startswith(\"TGT\"):\n",
" out.append((\"๊ฑฐ์งํญ์ \", t[\"label\"], len(t[\"sensors\"]), len(t[\"scans\"])))\n",
" elif t[\"purity\"] < 0.7:\n",
" out.append((\"๋ณํฉ์ค๋ฅ\", t[\"label\"], len(t[\"sensors\"]), len(t[\"scans\"])))\n",
" elif t[\"label\"] in seen:\n",
" out.append((\"๋ถํ \", t[\"label\"], len(t[\"sensors\"]), t[\"born\"] - seen[t[\"label\"]]))\n",
" seen.setdefault(t[\"label\"], t[\"born\"])\n",
" return out\n",
"\n",
"\n",
"print(\"[AAR ์ผ์ง ๋ถ์ - 1์ฐจ ๋ฐฐ์น ํ๋ผ๋ฏธํฐ]\", INIT)\n",
"log, acc0 = [], []\n",
"for i, kw in enumerate(AAR_SCN):\n",
" ev, trk = run(kw, INIT)\n",
" acc0.append(ev[\"ํญ์ ์ ํ๋\"])\n",
" for e in errors(trk):\n",
" log.append({\"์ผ์ง\": f\"AAR-{i + 1}\", \"์กฐ๊ฑด\": kw.get(\"special\") or kw.get(\"weather\") or \"-\", \"์ค๋ฅ\": e[0], \"ํ์ \": e[1], \"์ผ์์\": e[2], \"์๋ช
/๊ฐ๊ฒฉ(s)\": e[3]})\n",
"A = pd.DataFrame(log)\n",
"print(f\"์๋๋ฆฌ์ค {len(AAR_SCN)}๊ฐ ํ๊ท ํญ์ ์ ํ๋ {np.mean(acc0):.1%}, ์ค๋ฅ {len(A)}๊ฑด\")\n",
"print(A.groupby([\"์ผ์ง\", \"์กฐ๊ฑด\", \"์ค๋ฅ\"]).size().unstack(fill_value=0).to_string())\n",
"print(\"\\n[์์ธ ๋ถ์: ํ๋ผ๋ฏธํฐ ํ๋์ฉ๋ง ๋ฐ๊ฟ AAR ์ผ์ง ์ฌ์คํ] ๊ฐ์ค -> ์ ํ๋ / ๋ถํ / ๊ฑฐ์งํญ์ \")\n",
"HYP = [(\"gate\", 9.21, \"๊ฒ์ดํธ(5.99)๊ฐ ์ข์ ๊ธฐ๋ยท์ก์ ์ ๊ฒฝ๋ณด๊ฐ ํญ์ ๋ฐ์ผ๋ก ๋น ์ ธ ์ ํญ์ ์์ฑ\"),\n",
" (\"q\", 4e-4, \"ํ๋ก์ธ์ค ์ก์์ด ์์ ์ ํยท๊ณ ์ ๊ธฐ๋ ์์ธก์ด ์ด๊ธ๋จ\"),\n",
" (\"confirm\", (4, 6), \"ํ์ ๊ท์น(2/3)์ด ๋์จํด ํด๋ฌํฐยท๊ธฐ๋ง ๊ฒฝ๋ณด๊ฐ ๊ฑฐ์ง ํญ์ ์ผ๋ก ํ์ \"),\n",
" (\"max_miss\", 4, \"์ญ์ ๊ธฐ์ค์ด ์งง์ ๋จ์ ยท๋ฏธํ 1~2์ด์ ํญ์ ์ด ๋๊ฒผ๋ค ์ฌ์์ฑ\"),\n",
" (\"merge_gate\", 6.0, \"์ค๋ณต ํตํฉ ๋ฏธ์ฌ์ฉ์ผ๋ก ์ผ์๋ณ ํญ์ ์ด ํฉ์ณ์ง์ง ์์\")]\n",
"for k, v, why in HYP:\n",
" evs = [run(kw, {**INIT, k: v})[0] for kw in AAR_SCN]\n",
" acc = np.mean([e[\"ํญ์ ์ ํ๋\"] for e in evs])\n",
" print(f\" {k:10s}={str(v):8s} {acc:6.1%} / {sum(e['๋ถํ '] for e in evs):2d} / {sum(e['๊ฑฐ์งํญ์ '] for e in evs):2d} ({acc - np.mean(acc0):+.1%}p) {why}\")\n",
"print(\" -> ์ ํ๋ ๊ฐ์ ํญ์ด ํฐ ํ๋ผ๋ฏธํฐ๊ฐ ์ฃผ์์ธ์ด๋ค. ๊ฐ์ ์ด ์๊ฑฐ๋ ์
ํ๋๋ ๊ฐ์ค์ ๊ธฐ๊ฐํ๊ณ ์กฐํฉ ํ์์ผ๋ก ์ต์ข
๊ฐ์ ์ ํ๋ค.\")\n",
"\n",
"# ์ฌ๋ณด์ : ๋ณ๋ ํ๋ ์๋๋ฆฌ์ค์์ ๊ฒฉ์ ํ์\n",
"TUNE = [dict(seed=1101, special=\"turn\"), dict(seed=1102, fa_mult=2.0, weather=\"rain\"), dict(seed=1103, dropout=(\"RDR-B\", 10, 25), ghost=True)]\n",
"grid = list(itertools.product([9.21], [5e-5, 4e-4], [(2, 3), (3, 5), (4, 6)], [1, 4], [0.0, 6.0])) # ๊ฒ์ดํธ๋ ์นด์ด์ ๊ณฑ 99%(9.21)๋ก ๊ณ ์ \n",
"best = None\n",
"for g, q, c, mm, mg in grid:\n",
" p = dict(gate=g, q=q, confirm=c, max_miss=mm, merge_gate=mg)\n",
" a = np.mean([run(kw, p, T=30)[0][\"ํญ์ ์ ํ๋\"] for kw in TUNE])\n",
" if best is None or a > best[0]:\n",
" best = (a, p)\n",
"tuned = best[1]\n",
"print(f\"\\n๊ฒฉ์ ํ์ {len(grid)}๊ฐ ์กฐํฉ -> ํ๋ ์๋๋ฆฌ์ค ์ ํ๋ {best[0]:.1%}\")\n",
"yaml = \"# configs/ops02_tuned_params.yaml [๊ต์ก์ฉ๊ฐ์์๋ฃ]\\ntracker:\\n\" + \"\\n\".join(f\" {k}: {list(v) if isinstance(v, tuple) else v}\" for k, v in tuned.items())\n",
"yaml += \"\\n sync: heartbeat_median\\n sensor_order: precise_first\\nreview:\\n route_to_operator: [split_suspect, single_sensor_over_30s]\"\n",
"print(yaml)\n",
"\n",
"# ํ๊ท ์ํ: AAR ์๋๋ฆฌ์ค ์ฌ๋ฐ + ์ ์๋๋ฆฌ์ค ์ผ๋ฐํ\n",
"recur, fa_b, fa_a = 0, 0, 0\n",
"for kw in AAR_SCN:\n",
" ev0, trk0 = run(kw, INIT)\n",
" ev, trk = run(kw, tuned)\n",
" old = {(e[0], e[1]) for e in errors(trk0) if e[0] != \"๊ฑฐ์งํญ์ \"}\n",
" new = {(e[0], e[1]) for e in errors(trk) if e[0] != \"๊ฑฐ์งํญ์ \"}\n",
" recur += len(old & new); fa_b += ev0[\"๊ฑฐ์งํญ์ \"]; fa_a += ev[\"๊ฑฐ์งํญ์ \"]\n",
"NEW = [dict(seed=1201 + i, **c) for i, c in enumerate([dict(special=\"turn\"), dict(special=\"formation\"), dict(weather=\"rain\", fa_mult=2.0),\n",
" dict(dropout=(\"RDR-A\", 10, 30)), dict(ghost=True, weather=\"night\"), dict(special=\"fast\")])]\n",
"b = [run(kw, INIT)[0][\"ํญ์ ์ ํ๋\"] for kw in NEW]\n",
"a = [run(kw, tuned)[0][\"ํญ์ ์ ํ๋\"] for kw in NEW]\n",
"print(f\"\\n[ํ๊ท ์ํ] ์ ์๋๋ฆฌ์ค {len(NEW)}๊ฐ ํญ์ ์ ํ๋: ์ฌ๋ณด์ ์ {np.mean(b):.1%} -> ํ {np.mean(a):.1%} ({(np.mean(a) - np.mean(b)) * 100:+.1f}%p, ๋ชฉํ +10%p: {'๋ฌ์ฑ' if np.mean(a) - np.mean(b) >= 0.1 else '๋ฏธ๋ฌ'})\")\n",
"print(f\"AAR ์ผ์ง ์ค๋ฅ ์ฌ๋ฐ(๊ฐ์ ํ์ ๋ถํ ยท๋ณํฉ) {recur}๊ฑด (๋ชฉํ 0๊ฑด: {'๋ฌ์ฑ' if recur == 0 else '๋ฏธ๋ฌ'}), ๊ฐ์ ์ผ์ง ๊ฑฐ์ง ํญ์ {fa_b}๊ฑด -> {fa_a}๊ฑด\")\n",
"print(\"์๋๋ฆฌ์ค๋ณ ์ ํ๋(์ ->ํ):\", [f\"{x:.2f}->{y:.2f}\" for x, y in zip(b, a)])\n",
"print(\"์ง์ ํ์ต ์ฐ๊ณ: ์กฐ์๊ด ํตํฉยท๋ถ๋ฆฌ ๊ฒฐ์ (๋ฏธ์
7 ๋ก๊ทธ)์ ์ ๋ต ๋ผ๋ฒจ๋ก ๋์ -> ๋ถ๊ธฐ๋ณ ์ฌํ์ -> ๋ณธ ํ๊ท ์ํ ํต๊ณผ ์์๋ง ํ๋ผ๋ฏธํฐ ๊ต์ฒด\")\n",
"\n",
"plt.figure(figsize=(6, 3.5))\n",
"x = np.arange(len(NEW))\n",
"plt.bar(x - 0.2, b, 0.4, label=\"initial\"); plt.bar(x + 0.2, a, 0.4, label=\"retuned\")\n",
"plt.xticks(x, [\"turn\", \"formation\", \"rain+FA\", \"dropout\", \"ghost\", \"fast\"]); plt.ylabel(\"track accuracy\"); plt.legend(); plt.title(\"AAR recalibration (synthetic)\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
09 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[AAR ์ผ์ง ๋ถ์ - 1์ฐจ ๋ฐฐ์น ํ๋ผ๋ฏธํฐ] {'gate': 5.99, 'q': 5e-05, 'confirm': (2, 3), 'max_miss': 1, 'merge_gate': 0.0}\n",
"์๋๋ฆฌ์ค 4๊ฐ ํ๊ท ํญ์ ์ ํ๋ 33.8%, ์ค๋ฅ 47๊ฑด\n",
"์ค๋ฅ ๊ฑฐ์งํญ์ ๋ณํฉ์ค๋ฅ ๋ถํ \n",
"์ผ์ง ์กฐ๊ฑด \n",
"AAR-1 turn 6 1 5\n",
"AAR-2 cross 8 1 7\n",
"AAR-3 fog 4 0 5\n",
"AAR-4 fast 5 0 5\n",
"\n",
"[์์ธ ๋ถ์: ํ๋ผ๋ฏธํฐ ํ๋์ฉ๋ง ๋ฐ๊ฟ AAR ์ผ์ง ์ฌ์คํ] ๊ฐ์ค -> ์ ํ๋ / ๋ถํ / ๊ฑฐ์งํญ์ \n",
" gate =9.21 43.4% / 4 / 28 (+9.6%p) ๊ฒ์ดํธ(5.99)๊ฐ ์ข์ ๊ธฐ๋ยท์ก์ ์ ๊ฒฝ๋ณด๊ฐ ํญ์ ๋ฐ์ผ๋ก ๋น ์ ธ ์ ํญ์ ์์ฑ\n",
" q =0.0004 35.8% / 21 / 22 (+2.0%p) ํ๋ก์ธ์ค ์ก์์ด ์์ ์ ํยท๊ณ ์ ๊ธฐ๋ ์์ธก์ด ์ด๊ธ๋จ\n",
" confirm =(4, 6) 86.6% / 3 / 1 (+52.8%p) ํ์ ๊ท์น(2/3)์ด ๋์จํด ํด๋ฌํฐยท๊ธฐ๋ง ๊ฒฝ๋ณด๊ฐ ๊ฑฐ์ง ํญ์ ์ผ๋ก ํ์ \n",
" max_miss =4 34.8% / 23 / 22 (+1.0%p) ์ญ์ ๊ธฐ์ค์ด ์งง์ ๋จ์ ยท๋ฏธํ 1~2์ด์ ํญ์ ์ด ๋๊ฒผ๋ค ์ฌ์์ฑ\n",
" merge_gate=6.0 32.1% / 17 / 36 (-1.7%p) ์ค๋ณต ํตํฉ ๋ฏธ์ฌ์ฉ์ผ๋ก ์ผ์๋ณ ํญ์ ์ด ํฉ์ณ์ง์ง ์์\n",
" -> ์ ํ๋ ๊ฐ์ ํญ์ด ํฐ ํ๋ผ๋ฏธํฐ๊ฐ ์ฃผ์์ธ์ด๋ค. ๊ฐ์ ์ด ์๊ฑฐ๋ ์
ํ๋๋ ๊ฐ์ค์ ๊ธฐ๊ฐํ๊ณ ์กฐํฉ ํ์์ผ๋ก ์ต์ข
๊ฐ์ ์ ํ๋ค.\n",
"\n",
"๊ฒฉ์ ํ์ 24๊ฐ ์กฐํฉ -> ํ๋ ์๋๋ฆฌ์ค ์ ํ๋ 95.2%\n",
"# configs/ops02_tuned_params.yaml [๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"tracker:\n",
" gate: 9.21\n",
" q: 5e-05\n",
" confirm: [4, 6]\n",
" max_miss: 1\n",
" merge_gate: 0.0\n",
" sync: heartbeat_median\n",
" sensor_order: precise_first\n",
"review:\n",
" route_to_operator: [split_suspect, single_sensor_over_30s]\n",
"\n",
"[ํ๊ท ์ํ] ์ ์๋๋ฆฌ์ค 6๊ฐ ํญ์ ์ ํ๋: ์ฌ๋ณด์ ์ 29.7% -> ํ 92.1% (+62.4%p, ๋ชฉํ +10%p: ๋ฌ์ฑ)\n",
"AAR ์ผ์ง ์ค๋ฅ ์ฌ๋ฐ(๊ฐ์ ํ์ ๋ถํ ยท๋ณํฉ) 0๊ฑด (๋ชฉํ 0๊ฑด: ๋ฌ์ฑ), ๊ฐ์ ์ผ์ง ๊ฑฐ์ง ํญ์ 23๊ฑด -> 1๊ฑด\n",
"์๋๋ฆฌ์ค๋ณ ์ ํ๋(์ ->ํ): ['0.33->1.00', '0.35->1.00', '0.13->1.00', '0.32->1.00', '0.25->0.67', '0.40->0.86']\n",
"์ง์ ํ์ต ์ฐ๊ณ: ์กฐ์๊ด ํตํฉยท๋ถ๋ฆฌ ๊ฒฐ์ (๋ฏธ์
7 ๋ก๊ทธ)์ ์ ๋ต ๋ผ๋ฒจ๋ก ๋์ -> ๋ถ๊ธฐ๋ณ ์ฌํ์ -> ๋ณธ ํ๊ท ์ํ ํต๊ณผ ์์๋ง ํ๋ผ๋ฏธํฐ ๊ต์ฒด\n",
"```\n",
" \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## ๐ฏ ๋ฏธ์
10. ๋ค์ค ์ผ์ ์ค๋ณต ํญ์ ํตํฉ ์๋ฎฌ๋ ์ดํฐ ์ต์ข
๋น๋ ๋ฐ ํตํฉ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ ํจํค์ง ์์ฑ\n",
"**์์์๊ฐ**: ์ฝ 3.5์๊ฐ\n",
"\n",
"### ๐ ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ์ํ ์ง์นจ\n",
"#### ๋ฏธ์
๋ฐฐ๊ฒฝ ๋ฐ ๋น์์ฑ\n",
"\n",
"๋ค์ค ์ผ์ ์ค๋ณต ํญ์ ํตํฉ ์๋ฎฌ๋ ์ดํฐ ์ ์ฒด ๋ชจ๋์ ํตํฉ ๊ฒ์ฆํ๊ณ ํฉ๋ ๋ฐฉ๊ณต ์์ ์ด์ฉ์ ์ํ ๋ธ๋ฆฌํ ํจํค์ง์ ๋ฐฐํฌ ๊ฐ์ด๋๋ฅผ ์์ฑํ๋ค. ์ค์ ์ ์ ํ์ฅ์์๋ ๋จ์ ํ์ง์ ๊ทธ์น์ง ์๊ณ ์ ์ํ ์ํ ํ๋จ๊ณผ ์ ๋ขฐ๋ ๋์ ์์ฌ๊ฒฐ์ ์ด ํ์์ ์ด๋ฏ๋ก, **๋ฉ์์ง ๊ฒ์ฆ๋ถํฐ ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ, ํญ์ ์ตํฉ, ์ ๋ขฐ๋ ๋ฑ๊ธ, ์กฐ์๊ด ๊ฒํ ๋๊ธฐ์ด, ๊ฐ์ฌ ๋ก๊ทธ๊น์ง ํ ๋ฒ์ ์คํ๋๋ ํตํฉ ์๋ฎฌ๋ ์ดํฐ์ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ์ ์์ฑํ๋ค.** ๊ณผ์
์ ์์ํ์ฌ ์คํ์ ๋ฐฉ์งํ๊ณ ์์ ๋์๋ ฅ์ ๊ทน๋ํํด์ผ ํฉ๋๋ค.\n",
"\n",
"์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ณธ ๋ฏธ์
์์๋ **์์ ๋ฉ์์ง๋ฅผ ์ฐจ๋จํ๊ณ ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ๋ก ์ตํฉํ ๋ค, Med ํญ์ ์ ์ ๋ ์กฐ์๊ด ๊ฒํ ๋๊ธฐ์ด์ ์ฌ๋ฆฐ๋ค ๊ณผ์ ์ ์งํํ๋** ์ค์ต์ ์ฒด๊ณ์ ์ผ๋ก ์ํํฉ๋๋ค.\n",
"\n",
"ํ์ต์๋ ์ค์ต๊ฐ์ด๋์ ์ ์๋ ์ด 4๊ฐ ์ธ๋ถ ํ๋(๊ธฐ๋ณธ ๊ธฐ๋ฅ ๊ตฌํ โ ๊ฒฐ๊ณผ ์งํ ๊ฒ์ฆ โ ํ๋ผ๋ฏธํฐ ํ๋ ๋ฐ ๋์ ๊ณผ์ )์ ์์ฐจ์ ์ผ๋ก ์งํํ๋ฉฐ ํต์ฌ ํ์ ๊ธฐ์ค์ ๋ฌ์ฑํฉ๋๋ค.\n",
"\n",
"**์ด๋ฒ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_ops02.py, README.md, docs/ops02_final_report.pdf(md)` ํํ์ ๊ฒฐ๊ณผ๋ฌผ์ ์์ฑํ๋ค.\n",
"\n",
"### ๐ ํ๊ฐ ๋ฐ ๊ฒ์ฆ ๊ธฐ์ค (PASS ์กฐ๊ฑด)\n",
"#### ๋ฏธ์
ํ์ \n",
"\n",
"**ํ๊ฐ ๊ตฌ๋ถ:** ํ์\n",
"\n",
"**ํต์ฌ ํ๊ฐ์์ญ:** ๊ตฐ ์ค๋ฌด ์ฆ์ ๋์
๊ฐ๋ฅ์ฑ, ์ด์ฉ ์ ๋ขฐ๋ ๋ฐ ์ ๋ต์ ๊ธฐ๋ํจ๊ณผ\n",
"\n",
"#### ๋ฏธ์
PASS ์กฐ๊ฑด\n",
"\n",
"- ์ ๋ ๊ธฐ์ค: End-to-End ํตํฉ ํ์ดํ๋ผ์ธ ์คํ ์ฑ๊ณต๋ฅ 100%, ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง 85% ์ด์\n",
"\n",
"\n",
"- ์งํ ๊ธฐ์ค: ์ํด๋ฆญ ์คํ ์คํฌ๋ฆฝํธ ๋์ ํ์ธ ๋ฐ ์ต์ข
๋ฐํ ์ฐ์ถ๋ฌผ ์์ฑ ์๋ฃ\n",
"\n",
"\n",
"- ๊ฒ์ฆ ๋ฐฉ๋ฒ: ์ต์ข
๊ฒ์ฆ ์์ํ(ํ๊ฐ๊ด) ๋ชจ์ ์์ฐ ๋ฐ ์ ์ฒด ๊ธฐ๋ฅ ํตํฉ ๊ฒ์ฆ\n",
"\n",
"\n",
"#### ์ฑ์ ์ ํ์ ๋ฉ๋ชจ\n",
"\n",
"> ๋จ์ ์ฝ๋ ์คํ ์ฌ๋ถ๋ฟ๋ง ์๋๋ผ ์ถ๋ ฅ๋ ์์น์ ๊ทผ๊ฑฐ ์๋ฃ๊ฐ ๊ตฐ ์ค๋ฌด ์๊ตฌ์ฌํญ์ ๋ถํฉํ๋์ง ๋ฉด๋ฐํ ํ์ธํ๋ค.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\"\"\"์ผ์ ๋ฉ์์ง ๊ฒ์ฆยท์ ์ , ์๊ฐ ๋๊ธฐํยทENU ๋ณํ, ์ฌ๋ณด์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ์ฉํ EKF ํญ์ ์ตํฉ, ์ตํฉ ์ ๋ขฐ๋ ๋ฑ๊ธ, ์กฐ์๊ด ๊ฒํ ๋๊ธฐ์ดยท๊ฐ์ฌ ๋ก๊ทธ๊น์ง ํ ๋ฒ์ ์คํํ๋ ํตํฉ ์๋ฎฌ๋ ์ดํฐ๋ฅผ ๋ง๋ค๊ณ , ์์ฒด ์ ๊ฒ(ํจ์ ์ปค๋ฒ๋ฆฌ์ง ํฌํจ)๊ณผ ํตํฉ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ ์์ฝ์ ์ถ๋ ฅํ๋ค.\"\"\"\n",
"import hashlib\n",
"import json\n",
"import time\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from scipy.optimize import linear_sum_assignment\n",
"\n",
"# ---------------------------------------------------------------------------\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ๋ค์ค ์ผ์(๋ ์ด๋ยทEO/IRยท์ํฅ) ๊ฒฝ๋ณด ์์ฑ๊ธฐ (์ค์ ์ฅ๋น ์ ์ยท๋ฐฐ์น์ ๋ฌด๊ด)\n",
"# ๊ฐ์ ๋ฐฉ์ด ์ง์ญ(์์ ๊ธฐ์ค ๋/๋ถ km)์ ๋นํ์ฒด ๊ถค์ ์ ๋ง๋ค๊ณ , ์ผ์๋ง๋ค '์๊ธฐ ์์น ๊ธฐ์ค\n",
"# ๊ฑฐ๋ฆฌยท๋ฐฉ์'์ '์๊ธฐ ์๊ณ ๊ธฐ์ค ์๊ฐ'์ผ๋ก ๊ฒฝ๋ณด๋ฅผ ๋ธ๋ค. ๊ฐ์ ๋นํ์ฒด๊ฐ ์ฌ๋ฌ ์ผ์์ ๋์์\n",
"# ์กํ๋ฏ๋ก ๊ฒฝ๋ณด๊ฐ ์ค๋ณต๋๊ณ , ์ผ์๋ง๋ค ์๊ณ ์ค์ฐจยท์ธก์ ์ค์ฐจยท์ค๊ฒฝ๋ณด๊ฐ ๋ค๋ฅด๋ค.\n",
"# ---------------------------------------------------------------------------\n",
"SENSORS = {\n",
" \"RDR-A\": {\"type\": \"radar\", \"pos\": (0.0, 0.0), \"sr\": 0.08, \"saz\": 0.4, \"pd\": 0.95, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.0},\n",
" \"RDR-B\": {\"type\": \"radar\", \"pos\": (16.0, -6.0), \"sr\": 0.10, \"saz\": 0.5, \"pd\": 0.93, \"fa\": 1.0, \"max_r\": 40.0, \"clock\": 0.42},\n",
" \"EOIR-C\": {\"type\": \"eoir\", \"pos\": (5.0, 9.0), \"sr\": 0.40, \"saz\": 0.15, \"pd\": 0.88, \"fa\": 0.5, \"max_r\": 18.0, \"clock\": -0.65},\n",
" \"ACU-D\": {\"type\": \"acoustic\", \"pos\": (10.0, 2.0), \"sr\": 0.60, \"saz\": 2.0, \"pd\": 0.80, \"fa\": 0.6, \"max_r\": 9.0, \"clock\": 0.93},\n",
"}\n",
"CENTER = np.array([8.0, 2.0]) # ๊ฐ์ ๋ฐฉ์ด ์ง์ญ ์ค์ฌ\n",
"KINDS = {\"uav\": (0.03, 0.06), \"heli\": (0.04, 0.07), \"fast\": (0.15, 0.20)} # ์๋ ๋ฒ์(km/s, ์ถ์๊ฐ)\n",
"WEATHER = {\"clear\": {\"eoir\": 1.0, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.0},\n",
" \"night\": {\"eoir\": 0.9, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.1},\n",
" \"fog\": {\"eoir\": 0.55, \"acoustic\": 1.0, \"radar\": 1.0, \"fa\": 1.2},\n",
" \"rain\": {\"eoir\": 0.7, \"acoustic\": 0.6, \"radar\": 0.97, \"fa\": 1.6}}\n",
"\n",
"\n",
"def _targets(rng, n, T, special):\n",
" # [TODO : ํ์ ์ค์ต 10] ๋ค์ค ์ผ์ ์ค๋ณต ํญ์ ํตํฉ ์๋ฎฌ๋ ์ดํฐ ์ต์ข
๋น๋ ๋ฐ ํตํฉ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def make_scenario(seed, n=6, T=60, weather=\"clear\", fa_mult=1.0, dropout=None, ghost=False, special=None,\n",
" noise_mult=None, drift=0.0):\n",
" \"\"\"์๋๋ฆฌ์ค 1๊ฐ: ์ ๋ต ๊ถค์ , ์ผ์ ๊ฒฝ๋ณด(๋ณด๊ณ ) ๋ชฉ๋ก, ์๊ณ ๋๊ธฐํ์ฉ ๊ธฐ์ค ์ ํธ(heartbeat)๋ฅผ ๋ง๋ ๋ค.\n",
" dropout=(์ผ์, ์์, ๋): ํต์ ๋จ์ , ghost: ํ ์ผ์์๋ง ๋ณด์ด๋ ๊ฑฐ์ง ํญ์ (๊ธฐ๋งยท์คํธํ),\n",
" noise_mult={์ผ์: ๋ฐฐ์}: ์ฌ๋ฐ์ผ๋ก ์ธก์ ์ก์ ์ฆ๊ฐ, drift: ์ํฅ ์ผ์ ์๊ณ๊ฐ ์ด๋น drift์ด์ฉ ํํธ๋ฌ์ง.\"\"\"\n",
" rng = np.random.default_rng(seed)\n",
" targets, kinds = _targets(rng, n, T, special)\n",
" w = WEATHER[weather]\n",
" reports, hb, mid = [], [], 0\n",
" gpos = CENTER + np.array([-9.0, 7.0])\n",
" for t in range(T):\n",
" for sid, s in SENSORS.items():\n",
" clock = s[\"clock\"] + (drift * t if sid == \"ACU-D\" else 0.0)\n",
" if t % 10 == 0:\n",
" hb.append({\"sensor\": sid, \"t_ref\": float(t), \"t_sensor\": t + clock + rng.normal(0, 0.02)})\n",
" if dropout and sid == dropout[0] and dropout[1] <= t < dropout[2]:\n",
" continue\n",
" nm = (noise_mult or {}).get(sid, 1.0)\n",
" objs = [(tid, tr[t, :2]) for tid, tr in targets.items()]\n",
" if ghost and sid == \"EOIR-C\":\n",
" objs.append((\"GHOST\", gpos + 0.12 * t * np.array([0.6, -0.8])))\n",
" for tid, xy in objs:\n",
" d = xy - np.array(s[\"pos\"])\n",
" r = float(np.hypot(*d))\n",
" if r > s[\"max_r\"] or rng.random() > s[\"pd\"] * w[s[\"type\"]]:\n",
" continue\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(r + rng.normal(0, s[\"sr\"] * nm), 3),\n",
" \"az_deg\": round(float((np.degrees(np.arctan2(d[0], d[1])) + rng.normal(0, s[\"saz\"] * nm)) % 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": tid})\n",
" mid += 1\n",
" for _ in range(rng.poisson(s[\"fa\"] * fa_mult * w[\"fa\"] * (3 if nm > 1 else 1))): # ์ค๊ฒฝ๋ณด(ํด๋ฌํฐยท๋ฐ์ฌ)\n",
" reports.append({\"msg_id\": f\"M{mid:05d}\", \"sensor\": sid, \"stype\": s[\"type\"],\n",
" \"t_sensor\": round(t + clock + rng.normal(0, 0.02), 3),\n",
" \"range_km\": round(rng.uniform(0.5, s[\"max_r\"]), 3), \"az_deg\": round(rng.uniform(0, 360), 3),\n",
" \"sr\": s[\"sr\"] * nm, \"saz\": s[\"saz\"] * nm, \"truth\": \"FA\"})\n",
" mid += 1\n",
" return {\"targets\": targets, \"kinds\": kinds, \"reports\": reports, \"heartbeats\": hb, \"T\": T, \"weather\": weather}\n",
"# ---------------------------------------------------------------------------\n",
"\n",
"# ---- ์๊ณต๊ฐ ์ ์ฒ๋ฆฌ ๊ณตํต ํจ์: ์ ์ -> ์๊ฐ ๋๊ธฐํ -> ๊ณตํต ์ขํ(ENU, km) ๋ณํ ----\n",
"def estimate_offsets(heartbeats):\n",
" \"\"\"์ผ์๋ณ ์๊ณ ์ค์ฐจ = ๊ธฐ์ค ์ ํธ(heartbeat) ๋๋น ์ผ์ ์๊ฐ ์ฐจ์ด์ ์ค์๊ฐ(์ก์์ ๊ฐํจ).\"\"\"\n",
" # [TODO : ํ์ ์ค์ต 10] ๋ค์ค ์ผ์ ์ค๋ณต ํญ์ ํตํฉ ์๋ฎฌ๋ ์ดํฐ ์ต์ข
๋น๋ ๋ฐ ํตํฉ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ ํจํค์ง ์์ฑ ๊ด๋ จ ํต์ฌ ๋ก์ง์ ๊ตฌํํ์ธ์.\n",
" # ํํธ: ์๋จ ์ฃผ์ ๋ฐ ๋ฐ์ดํฐ ์คํค๋ง/์ง์นจ์ ์ฐธ๊ณ ํ์ฌ ์์ฑํฉ๋๋ค.\n",
" # <<< ์ฌ๊ธฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์ >>>\n",
" pass # ์์ฑ ํ pass๋ฅผ ์ ๊ฑฐํ์ธ์.\n",
"def clean(reports):\n",
" \"\"\"์ค๋ณต ๋ฉ์์งยท๊ฒฐ์ธกยท๋ฒ์ ๋ฐ(๋ฐ์ฌํ ๋ฑ ์ต๋ ํ์ง๊ฑฐ๋ฆฌ ์ด๊ณผ) ๊ฒฝ๋ณด๋ฅผ ๊ฑธ๋ฌ๋ธ๋ค. (์ ์ ๋ชฉ๋ก, ์ฌ์ ๋ณ ๊ฑด์)\"\"\"\n",
" seen, out, why = set(), [], {\"์ค๋ณต\": 0, \"๊ฒฐ์ธก\": 0, \"๋ฒ์ ๋ฐ\": 0}\n",
" for r in reports:\n",
" if r[\"msg_id\"] in seen:\n",
" why[\"์ค๋ณต\"] += 1; continue\n",
" seen.add(r[\"msg_id\"])\n",
" if any(r.get(k) is None or (isinstance(r.get(k), float) and np.isnan(r[k])) for k in (\"t_sensor\", \"range_km\", \"az_deg\")):\n",
" why[\"๊ฒฐ์ธก\"] += 1; continue\n",
" if not 0 <= r[\"range_km\"] <= SENSORS[r[\"sensor\"]][\"max_r\"] * 1.1:\n",
" why[\"๋ฒ์ ๋ฐ\"] += 1; continue\n",
" out.append(r)\n",
" return out, why\n",
"\n",
"\n",
"def to_enu(r, offsets):\n",
" \"\"\"์ผ์ ๊ตญ์ ๊ทน์ขํ(๊ฑฐ๋ฆฌยท๋ฐฉ์) -> ๊ณตํต ENU(๋ยท๋ถ km), ์ผ์ ์๊ฐ -> ๊ธฐ์ค ์๊ฐ.\"\"\"\n",
" s, a = SENSORS[r[\"sensor\"]], np.radians(r[\"az_deg\"])\n",
" return {**r, \"t\": r[\"t_sensor\"] - offsets.get(r[\"sensor\"], 0.0),\n",
" \"x\": s[\"pos\"][0] + r[\"range_km\"] * np.sin(a), \"y\": s[\"pos\"][1] + r[\"range_km\"] * np.cos(a)}\n",
"\n",
"\n",
"def meas_cov(r):\n",
" \"\"\"๊ฑฐ๋ฆฌยท๋ฐฉ์ ์ค์ฐจ๋ฅผ ENU ๊ณต๋ถ์ฐ(2x2)์ผ๋ก ๋ณํํ๋ค(1์ฐจ ๊ทผ์ฌ).\"\"\"\n",
" a, rr = np.radians(r[\"az_deg\"]), r[\"range_km\"]\n",
" J = np.array([[np.sin(a), rr * np.cos(a)], [np.cos(a), -rr * np.sin(a)]])\n",
" return J @ np.diag([r[\"sr\"] ** 2, np.radians(r[\"saz\"]) ** 2]) @ J.T\n",
"\n",
"\n",
"def prepare(scn, sync=True):\n",
" reps, _ = clean(scn[\"reports\"])\n",
" off = estimate_offsets(scn[\"heartbeats\"]) if sync else {}\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"])\n",
"\n",
"# ---- ํ์ฅ ์นผ๋ง ํํฐ(EKF) ๋ค์ค ์ผ์ ํญ์ ์ถ์ ๊ธฐ ๊ณตํต ํจ์ ----\n",
"# ์ํ x = [๋(km), ๋ถ(km), ๋์(km/s), ๋ถ์(km/s)], ์ธก์ z = ์ผ์ ๊ธฐ์ค [๊ฑฐ๋ฆฌ(km), ๋ฐฉ์(rad)]\n",
"def ekf_predict(x, P, dt, q):\n",
" F = np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], float)\n",
" G = np.array([[dt ** 2 / 2, 0], [0, dt ** 2 / 2], [dt, 0], [0, dt]])\n",
" return F @ x, F @ P @ F.T + q * G @ G.T\n",
"\n",
"\n",
"def h_polar(x, spos):\n",
" dx, dy = x[0] - spos[0], x[1] - spos[1]\n",
" r2 = max(dx * dx + dy * dy, 1e-9); r = np.sqrt(r2)\n",
" return np.array([r, np.arctan2(dx, dy)]), np.array([[dx / r, dy / r, 0, 0], [dy / r2, -dx / r2, 0, 0]])\n",
"\n",
"\n",
"def innovation(x, P, rep):\n",
" s = SENSORS[rep[\"sensor\"]]\n",
" zp, H = h_polar(x, s[\"pos\"])\n",
" y = np.array([rep[\"range_km\"], np.radians(rep[\"az_deg\"])]) - zp\n",
" y[1] = (y[1] + np.pi) % (2 * np.pi) - np.pi # ๋ฐฉ์ ์ฐจ์ด๋ -pi~pi๋ก ๊ฐ์ผ๋ค\n",
" S = H @ P @ H.T + np.diag([rep[\"sr\"] ** 2, np.radians(rep[\"saz\"]) ** 2])\n",
" return y, H, S, float(y @ np.linalg.solve(S, y))\n",
"\n",
"\n",
"class Tracker:\n",
" \"\"\"์ผ์ ๊ฒฝ๋ณด๋ฅผ ์ค์บ(1์ด) ๋จ์๋ก ๋ฐ์ EKF ํญ์ ์ ๊ฐฑ์ ํ๋ค. ๊ฐ์ ์ค์บ ์์์ ์ผ์๋ณ๋ก\n",
" ์์ฐจ ๊ฐฑ์ ํ๋ฏ๋ก, ์ฌ๋ฌ ์ผ์๊ฐ ๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ณด๋ฉด ํ๋์ ํญ์ ์ผ๋ก ํตํฉ๋๋ค.\"\"\"\n",
"\n",
" def __init__(self, gate=9.21, q=4e-4, confirm=(3, 5), max_miss=4, merge_gate=6.0):\n",
" self.gate, self.q, self.confirm, self.max_miss, self.merge_gate = gate, q, confirm, max_miss, merge_gate\n",
" self.tracks, self.dead, self.nid, self.n_merged = [], [], 0, 0\n",
"\n",
" def _new(self, rep, k):\n",
" C = meas_cov(rep)\n",
" P = np.zeros((4, 4)); P[:2, :2] = C; P[2, 2] = P[3, 3] = 0.25 ** 2\n",
" self.tracks.append({\"id\": f\"FT{self.nid:03d}\", \"x\": np.array([rep[\"x\"], rep[\"y\"], 0, 0]), \"P\": P, \"k\": k,\n",
" \"scans\": [k], \"sensors\": {rep[\"sensor\"]: 1}, \"truths\": [rep[\"truth\"]], \"nis\": [],\n",
" \"hist\": [(k, rep[\"x\"], rep[\"y\"])], \"confirmed\": False, \"miss\": 0, \"born\": k, \"upd\": True})\n",
" self.nid += 1\n",
"\n",
" def _assign(self, tracks, rs, sid):\n",
" \"\"\"๊ฒ์ดํธ(๋งํ ๋ผ๋
ธ๋น์ค ๊ฑฐ๋ฆฌ) ์์์ ์ ์ญ ์ต๊ทผ์ ํ ๋น(GNN). ํ ๋น๋ ๋ณด๊ณ ์ ๋ฒํธ ์งํฉ์ ๋๋ ค์ค๋ค.\"\"\"\n",
" if not tracks or not rs:\n",
" return set()\n",
" spos = SENSORS[sid][\"pos\"]\n",
" Z = np.array([[r[\"range_km\"], np.radians(r[\"az_deg\"])] for r in rs])\n",
" R = np.diag([rs[0][\"sr\"] ** 2, np.radians(rs[0][\"saz\"]) ** 2])\n",
" cost = np.full((len(tracks), len(rs)), 1e6)\n",
" for i, tr in enumerate(tracks):\n",
" zp, H = h_polar(tr[\"x\"], spos)\n",
" Y = Z - zp\n",
" Y[:, 1] = (Y[:, 1] + np.pi) % (2 * np.pi) - np.pi\n",
" Si = np.linalg.inv(H @ tr[\"P\"] @ H.T + R)\n",
" d2 = np.einsum(\"ij,jk,ik->i\", Y, Si, Y)\n",
" cost[i] = np.where(d2 < self.gate, d2, 1e6)\n",
" used = set()\n",
" for i, j in zip(*linear_sum_assignment(cost)):\n",
" if cost[i, j] < self.gate:\n",
" tr, r = tracks[i], rs[j]\n",
" y, H, S, d2 = innovation(tr[\"x\"], tr[\"P\"], r)\n",
" K = tr[\"P\"] @ H.T @ np.linalg.inv(S)\n",
" tr[\"x\"] = tr[\"x\"] + K @ y\n",
" tr[\"P\"] = (np.eye(4) - K @ H) @ tr[\"P\"]\n",
" tr[\"nis\"].append(d2); tr[\"truths\"].append(r[\"truth\"])\n",
" tr[\"sensors\"][sid] = tr[\"sensors\"].get(sid, 0) + 1\n",
" tr[\"upd\"] = True; used.add(j)\n",
" return used\n",
"\n",
" def _merge(self):\n",
" \"\"\"๊ฐ์ ๋นํ์ฒด๋ฅผ ๋ฐ๋ผ๊ฐ๋ ์ค๋ณต ํญ์ (์์นยท์๋๊ฐ ํต๊ณ์ ์ผ๋ก ๊ฐ์)์ ์ค๋๋ ํญ์ ์ผ๋ก ํตํฉํ๋ค.\"\"\"\n",
" drop = set()\n",
" for a in range(len(self.tracks)):\n",
" for b in range(a + 1, len(self.tracks)):\n",
" A, B = self.tracks[a], self.tracks[b]\n",
" if A[\"id\"] in drop or B[\"id\"] in drop:\n",
" continue\n",
" d = A[\"x\"] - B[\"x\"]\n",
" d2 = float(d[:2] @ np.linalg.solve(A[\"P\"][:2, :2] + B[\"P\"][:2, :2], d[:2]))\n",
" if d2 < self.merge_gate and np.hypot(d[2], d[3]) < 0.05:\n",
" keep, lose = (A, B) if len(A[\"scans\"]) >= len(B[\"scans\"]) else (B, A)\n",
" for sid, n in lose[\"sensors\"].items():\n",
" keep[\"sensors\"][sid] = keep[\"sensors\"].get(sid, 0) + n\n",
" keep[\"truths\"] += lose[\"truths\"]; keep[\"scans\"] = sorted(set(keep[\"scans\"]) | set(lose[\"scans\"]))\n",
" keep[\"merged\"] = keep.get(\"merged\", 0) + 1\n",
" drop.add(lose[\"id\"])\n",
" self.tracks = [t for t in self.tracks if t[\"id\"] not in drop]\n",
" self.n_merged += len(drop)\n",
"\n",
" def step(self, k, reps):\n",
" for tr in self.tracks: # 1) ์์ธก\n",
" tr[\"x\"], tr[\"P\"] = ekf_predict(tr[\"x\"], tr[\"P\"], k - tr[\"k\"], self.q)\n",
" tr[\"k\"], tr[\"upd\"] = k, False\n",
" for sid in sorted({r[\"sensor\"] for r in reps}, key=lambda s: SENSORS[s][\"sr\"]): # 2) ์ ๋ฐ ์ผ์๋ถํฐ ์์ฐจ ๊ฐฑ์ \n",
" rs = [r for r in reps if r[\"sensor\"] == sid]\n",
" used = self._assign([t for t in self.tracks if t[\"confirmed\"]], rs, sid) # ํ์ ํญ์ ์ฐ์ \n",
" rest = [r for j, r in enumerate(rs) if j not in used]\n",
" used2 = self._assign([t for t in self.tracks if not t[\"confirmed\"]], rest, sid)\n",
" for j, r in enumerate(rest):\n",
" if j not in used2:\n",
" self._new(r, k)\n",
" if self.merge_gate > 0:\n",
" self._merge()\n",
" keep = []\n",
" for tr in self.tracks: # 3) ํ์ ยท์ญ์ ๊ด๋ฆฌ\n",
" if tr[\"upd\"]:\n",
" tr[\"miss\"] = 0\n",
" if tr[\"scans\"][-1] != k:\n",
" tr[\"scans\"].append(k)\n",
" tr[\"hist\"].append((k, tr[\"x\"][0], tr[\"x\"][1]))\n",
" else:\n",
" tr[\"miss\"] += 1\n",
" m, n = self.confirm\n",
" if not tr[\"confirmed\"] and sum(1 for s in tr[\"scans\"] if s > k - n) >= m:\n",
" tr[\"confirmed\"] = True\n",
" limit = self.max_miss if tr[\"confirmed\"] else 1\n",
" (keep if tr[\"miss\"] <= limit else self.dead).append(tr)\n",
" self.tracks = keep\n",
"\n",
" def all_tracks(self):\n",
" return self.dead + self.tracks\n",
"\n",
"\n",
"def run_tracker(reps, **kw):\n",
" trk = Tracker(**kw)\n",
" by = {}\n",
" for r in reps:\n",
" by.setdefault(int(round(r[\"t\"])), []).append(r)\n",
" for k in range(min(by), max(by) + 1):\n",
" trk.step(k, by.get(k, []))\n",
" return trk.all_tracks()\n",
"\n",
"\n",
"def label_track(tr):\n",
" vals, cnt = np.unique(tr[\"truths\"], return_counts=True)\n",
" return str(vals[np.argmax(cnt)]), float(cnt.max() / cnt.sum())\n",
"\n",
"\n",
"def evaluate(tracks, scn, min_reports=5):\n",
" \"\"\"์ ๋ต ์ฐ๊ฒฐ ํ๊ฐ: ์ฌํ์จ, ํญ์ ๋ถํ (ํ ํ์ ์ ํญ์ 2๊ฐ ์ด์), ๊ฑฐ์ง ํญ์ , ๋ณํฉ ์ค๋ฅ, ์์น ์ค์ฐจ.\"\"\"\n",
" conf = [t for t in tracks if t[\"confirmed\"]]\n",
" for t in conf:\n",
" t[\"label\"], t[\"purity\"] = label_track(t)\n",
" cnt = pd.Series([r[\"truth\"] for r in scn[\"reports\"] if r[\"truth\"].startswith(\"TGT\")]).value_counts()\n",
" truths = [k for k, v in cnt.items() if v >= min_reports]\n",
" per = {g: [t for t in conf if t[\"label\"] == g] for g in truths}\n",
" found = [g for g in truths if per[g]]\n",
" split = sum(max(0, len(v) - 1) for v in per.values())\n",
" false = [t for t in conf if not t[\"label\"].startswith(\"TGT\")]\n",
" merge = [t for t in conf if t[\"label\"].startswith(\"TGT\") and t[\"purity\"] < 0.7]\n",
" err = []\n",
" for g in found:\n",
" for t in per[g]:\n",
" for k, x, y in t[\"hist\"][3:]:\n",
" if 0 <= k < scn[\"T\"]:\n",
" err.append(np.hypot(x - scn[\"targets\"][g][k, 0], y - scn[\"targets\"][g][k, 1]))\n",
" n = len(truths)\n",
" acc = len(found) / max(n + split + len(false) + len(merge), 1)\n",
" return {\"์ ๋ตํ์ \": n, \"ํ์งํ์ \": len(found), \"์ฌํ์จ\": round(len(found) / max(n, 1), 3), \"๋ถํ \": split,\n",
" \"๊ฑฐ์งํญ์ \": len(false), \"๋ณํฉ์ค๋ฅ\": len(merge), \"์์นRMSE(km)\": round(float(np.sqrt(np.mean(np.square(err)))) if err else np.nan, 3),\n",
" \"ํญ์ ์ ํ๋\": round(acc, 3)}\n",
"\n",
"CONFIG = {\"tracker\": dict(gate=9.21, q=5e-5, confirm=(4, 6), max_miss=1, merge_gate=0.0), # ๋ฏธ์
9 ์ฌ๋ณด์ ๊ฐ\n",
" \"high\": {\"min_sensors\": 2, \"min_rps\": 1.5}}\n",
"CALLED = set()\n",
"\n",
"\n",
"def traced(fn):\n",
" def w(*a, **k):\n",
" CALLED.add(fn.__name__); return fn(*a, **k)\n",
" w.__name__ = fn.__name__\n",
" return w\n",
"\n",
"\n",
"@traced\n",
"def validate(r):\n",
" errs = []\n",
" if r.get(\"sensor\") not in SENSORS:\n",
" errs.append(\"๋ฏธ๋ฑ๋ก ์ผ์\")\n",
" for k in (\"t_sensor\", \"range_km\", \"az_deg\"):\n",
" if not isinstance(r.get(k), (int, float)) or not np.isfinite(r[k]):\n",
" errs.append(f\"{k} ๊ฒฐ์ธกยทํ์\"); break\n",
" return errs\n",
"\n",
"\n",
"@traced\n",
"def ingest(scn):\n",
" ok, bad = [], {}\n",
" for r in scn[\"reports\"]:\n",
" e = validate(r)\n",
" (bad.__setitem__(r[\"msg_id\"], e) if e else ok.append(r))\n",
" reps, why = clean(ok)\n",
" off = estimate_offsets(scn[\"heartbeats\"])\n",
" return sorted((to_enu(r, off) for r in reps), key=lambda r: r[\"t\"]), bad, why, off\n",
"\n",
"\n",
"@traced\n",
"def fuse(reps):\n",
" return run_tracker(reps, **CONFIG[\"tracker\"])\n",
"\n",
"\n",
"@traced\n",
"def grade(t):\n",
" n = len(t[\"scans\"])\n",
" hi = len(t[\"sensors\"]) >= CONFIG[\"high\"][\"min_sensors\"] and len(t[\"truths\"]) / n >= CONFIG[\"high\"][\"min_rps\"]\n",
" return \"High\" if hi else \"Med\"\n",
"\n",
"\n",
"class Queue:\n",
" def __init__(self):\n",
" self.items, self.log = [], []\n",
"\n",
" @traced\n",
" def submit(self, rec):\n",
" prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n",
" body = {\"event\": \"SUBMIT\", \"id\": rec[\"id\"], \"grade\": rec[\"grade\"], \"reason\": rec[\"reason\"]}\n",
" self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()})\n",
" self.items.append({**rec, \"status\": \"PENDING\"}) # ์๋ ํตํฉยท๋ถ๋ฆฌยท๊ต์ ์ฐ๊ณ ์์\n",
"\n",
" @traced\n",
" def verify(self):\n",
" prev = \"GENESIS\"\n",
" for r in self.log:\n",
" body = {k: v for k, v in r.items() if k not in (\"prev\", \"hash\")}\n",
" if r[\"prev\"] != prev or r[\"hash\"] != hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest():\n",
" return False\n",
" prev = r[\"hash\"]\n",
" return True\n",
"\n",
"\n",
"@traced\n",
"def main(scn):\n",
" t0 = time.perf_counter()\n",
" reps, bad, why, off = ingest(scn)\n",
" trk = fuse(reps)\n",
" ev = evaluate(trk, scn)\n",
" q, table = Queue(), []\n",
" for t in [t for t in trk if t[\"confirmed\"]]:\n",
" g = grade(t)\n",
" rec = {\"id\": t[\"id\"], \"grade\": g, \"sensors\": \"+\".join(sorted(t[\"sensors\"])), \"scans\": len(t[\"scans\"]), \"label\": t[\"label\"]}\n",
" table.append(rec)\n",
" if g == \"Med\":\n",
" q.submit({**rec, \"reason\": \"๋จ์ผ ์ผ์ยท๋ณด๊ณ ๋ถ์กฑ(๊ธฐ๋งยทํด๋ฌํฐ ๊ฐ๋ฅ์ฑ)\"})\n",
" ms = (time.perf_counter() - t0) * 1000 / scn[\"T\"]\n",
" return pd.DataFrame(table), ev, bad, why, off, q, ms, len(reps)\n",
"\n",
"\n",
"T0 = time.perf_counter()\n",
"# [๊ต์ก์ฉ๊ฐ์์๋ฃ] ์ผ๊ฐ, ํ์ 8๊ฐ, ๊ธฐ๋ง ํญ์ , RDR-B 20์ด ๋จ์ , ์์ ๋ฉ์์ง 4๊ฑด\n",
"scn = make_scenario(1501, n=8, T=60, weather=\"night\", ghost=True, dropout=(\"RDR-B\", 25, 45))\n",
"for k, (f, v) in enumerate([(\"range_km\", None), (\"sensor\", \"RDR-Z\"), (\"az_deg\", float(\"nan\")), (\"t_sensor\", \"12:00\")]):\n",
" bad = dict(scn[\"reports\"][50 + k]); bad[f] = v; bad[\"msg_id\"] = f\"BAD{k}\"; scn[\"reports\"].append(bad)\n",
"D, ev, bad, why, off, q, ms, n_used = main(scn)\n",
"elapsed = time.perf_counter() - T0\n",
"true_hi = D[(D[\"grade\"] == \"High\")][\"label\"].str.startswith(\"TGT\")\n",
"\n",
"chk = {\n",
" \"์์ ๋ฉ์์ง 4๊ฑด ์ฐจ๋จ\": len(bad) == 4,\n",
" \"๋ฉ์์ง ๊ฑด์ ๋ณด์กด(์ ์ค 0)\": n_used + len(bad) + sum(why.values()) == len(scn[\"reports\"]),\n",
" \"์๊ณ ์ค์ฐจ ์ถ์ 4๊ฐ ์ผ์\": len(off) == 4,\n",
" \"ํ์ ์ฌํ์จ 90% ์ด์\": ev[\"์ฌํ์จ\"] >= 0.9,\n",
" \"High ํญ์ ์ค ๊ฑฐ์ง 0๊ฑด\": bool(true_hi.all()),\n",
" \"Med ํญ์ ์ ๋ ๊ฒํ ๋๊ธฐ\": len(q.items) == (D[\"grade\"] == \"Med\").sum(),\n",
" \"์๋ ์ ์ฉ 0๊ฑด\": all(i[\"status\"] == \"PENDING\" for i in q.items),\n",
" \"๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\": q.verify() and len(q.log) == len(q.items),\n",
" \"์ค์บ๋น 200ms ์ด๋ด\": ms < 200,\n",
" \"ํญ์ ๋ถํ 0๊ฑด\": ev[\"๋ถํ \"] == 0,\n",
"}\n",
"passed = sum(chk.values())\n",
"for k, v in chk.items():\n",
" print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n",
"funcs = [\"validate\", \"ingest\", \"fuse\", \"grade\", \"submit\", \"verify\", \"main\"]\n",
"cov = len(CALLED & set(funcs)) / len(funcs)\n",
"print(f\"์์ฒด ์ ๊ฒ {passed}/{len(chk)} ํต๊ณผ, E2E ์คํ ์ฑ๊ณต๋ฅ {'100%' if passed == len(chk) else f'{passed / len(chk):.0%}'}, ํจ์ ์ปค๋ฒ๋ฆฌ์ง {cov:.0%} (๋ชฉํ 85% ์ด์: {'๋ฌ์ฑ' if cov >= 0.85 else '๋ฏธ๋ฌ'})\")\n",
"print(\"์ฐจ๋จ ๋ฉ์์ง:\", bad)\n",
"print(\"\\n\" + D.drop(columns=\"label\").to_string(index=False))\n",
"\n",
"n_raw = len(scn[\"reports\"])\n",
"print(f\"\"\"\n",
"[ํตํฉ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ์ผ์ 4์ข
๊ฒฝ๋ณด {n_raw}๊ฑด ์์ , ์์ {len(bad)}๊ฑด ์ฐจ๋จ, ์ ์ ํ {n_used}๊ฑด ์ตํฉ, ์ ์ฒด {elapsed:.1f}s, ์ค์บ๋น {ms:.1f} ms\n",
"2. ์ตํฉ: ์์ ๊ฒฝ๋ณด {n_raw}๊ฑด -> ํตํฉ ํญ์ {len(D)}๊ฐ (High {(D['grade'] == 'High').sum()} / Med {(D['grade'] == 'Med').sum()}), ํ์ ์ฌํ์จ {ev['์ฌํ์จ']:.0%}, ๋ถํ {ev['๋ถํ ']}๊ฑด, ์์น ์ค์ฐจ {ev['์์นRMSE(km)']} km\n",
"3. ๊ฒํ : Med ํญ์ {len(q.items)}๊ฑด ์กฐ์๊ด ๊ฒํ ๋๊ธฐ(๊ธฐ๋งยทํด๋ฌํฐ ๊ฐ๋ฅ์ฑ), ์๋ ํตํฉยท๋ถ๋ฆฌ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ {'์ ์' if q.verify() else '์ด์'}\n",
"4. ์ด์ฉ: ๋ ์ด๋B ๋จ์ ๊ตฌ๊ฐ๋ ๋ค๋ฅธ ์ผ์๋ก ์ถ์ ์ ์ ์งํ๋ค. ํญ์ ๊ฒฐ์ ์ ์กฐ์๊ด์ด, ๊ต์ ์ฌ๋ถ๋ ๊ต์ ์์นยท์งํ๊ณํต์ด ์ ํ๋ค.\n",
"5. ํ๊ณยทํ์ฅ: ๊ฐ์ ์ผ์ยท๊ฐ์ ์ขํ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก ์ค์ ์ผ์ ์ ์ยท๋ฐฐ์น์ ๋ง์ถ ์ฌ๋ณด์ ์ด ํ์ํ๋ค. ์ธ์ ์ฒด๊ณ ์ฐ๋์ ํตํฉ ํญ์ (JSON) ํ์ค ๋ฉ์์ง API๋ก ํ์ฅํ๋ค.\"\"\")\n",
"\n",
"plt.figure(figsize=(6, 6))\n",
"for tid, tr in scn[\"targets\"].items():\n",
" plt.plot(tr[:, 0], tr[:, 1], \"k-\", lw=0.7)\n",
"for t in fuse(ingest(scn)[0]):\n",
" if t[\"confirmed\"]:\n",
" h = np.array(t[\"hist\"]); plt.plot(h[:, 1], h[:, 2], \"--\", c=\"tab:blue\" if grade(t) == \"High\" else \"tab:orange\")\n",
"for sid, s in SENSORS.items():\n",
" plt.plot(*s[\"pos\"], \"^\", c=\"tab:green\")\n",
"plt.xlabel(\"East (km)\"); plt.ylabel(\"North (km)\"); plt.title(\"Integrated fusion: High (blue) / Med review (orange) - synthetic\"); plt.axis(\"equal\")\n",
"plt.tight_layout(); plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"๐ก [์ฐธ๊ณ ] ๋ฏธ์
10 ์์ ์คํ ๊ฒฐ๊ณผ (Output)
\n",
"\n",
"```text\n",
"[PASS] ์์ ๋ฉ์์ง 4๊ฑด ์ฐจ๋จ\n",
"[PASS] ๋ฉ์์ง ๊ฑด์ ๋ณด์กด(์ ์ค 0)\n",
"[PASS] ์๊ณ ์ค์ฐจ ์ถ์ 4๊ฐ ์ผ์\n",
"[PASS] ํ์ ์ฌํ์จ 90% ์ด์\n",
"[PASS] High ํญ์ ์ค ๊ฑฐ์ง 0๊ฑด\n",
"[PASS] Med ํญ์ ์ ๋ ๊ฒํ ๋๊ธฐ\n",
"[PASS] ์๋ ์ ์ฉ 0๊ฑด\n",
"[PASS] ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ\n",
"[PASS] ์ค์บ๋น 200ms ์ด๋ด\n",
"[PASS] ํญ์ ๋ถํ 0๊ฑด\n",
"์์ฒด ์ ๊ฒ 10/10 ํต๊ณผ, E2E ์คํ ์ฑ๊ณต๋ฅ 100%, ํจ์ ์ปค๋ฒ๋ฆฌ์ง 100% (๋ชฉํ 85% ์ด์: ๋ฌ์ฑ)\n",
"์ฐจ๋จ ๋ฉ์์ง: {'BAD0': ['range_km ๊ฒฐ์ธกยทํ์'], 'BAD1': ['๋ฏธ๋ฑ๋ก ์ผ์'], 'BAD2': ['az_deg ๊ฒฐ์ธกยทํ์'], 'BAD3': ['t_sensor ๊ฒฐ์ธกยทํ์']}\n",
"\n",
" id grade sensors scans\n",
"FT012 Med EOIR-C 26\n",
"FT000 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT001 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT002 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT003 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT005 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT006 High ACU-D+EOIR-C+RDR-A+RDR-B 59\n",
"FT007 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT008 High ACU-D+EOIR-C+RDR-A+RDR-B 60\n",
"FT116 Med EOIR-C 21\n",
"\n",
"[ํตํฉ ๋ฐฉ๊ณต๋ง ๋ธ๋ฆฌํ ์์ฝ - ๊ต์ก์ฉ๊ฐ์์๋ฃ]\n",
"1. ์ฒ๋ฆฌ: ์ผ์ 4์ข
๊ฒฝ๋ณด 1655๊ฑด ์์ , ์์ 4๊ฑด ์ฐจ๋จ, ์ ์ ํ 1650๊ฑด ์ตํฉ, ์ ์ฒด 0.3s, ์ค์บ๋น 4.6 ms\n",
"2. ์ตํฉ: ์์ ๊ฒฝ๋ณด 1655๊ฑด -> ํตํฉ ํญ์ 10๊ฐ (High 8 / Med 2), ํ์ ์ฌํ์จ 100%, ๋ถํ 0๊ฑด, ์์น ์ค์ฐจ 0.044 km\n",
"3. ๊ฒํ : Med ํญ์ 2๊ฑด ์กฐ์๊ด ๊ฒํ ๋๊ธฐ(๊ธฐ๋งยทํด๋ฌํฐ ๊ฐ๋ฅ์ฑ), ์๋ ํตํฉยท๋ถ๋ฆฌ 0๊ฑด, ๊ฐ์ฌ ๋ก๊ทธ ๋ฌด๊ฒฐ์ฑ ์ ์\n",
"4. ์ด์ฉ: ๋ ์ด๋B ๋จ์ ๊ตฌ๊ฐ๋ ๋ค๋ฅธ ์ผ์๋ก ์ถ์ ์ ์ ์งํ๋ค. ํญ์ ๊ฒฐ์ ์ ์กฐ์๊ด์ด, ๊ต์ ์ฌ๋ถ๋ ๊ต์ ์์นยท์งํ๊ณํต์ด ์ ํ๋ค.\n",
"5. ํ๊ณยทํ์ฅ: ๊ฐ์ ์ผ์ยท๊ฐ์ ์ขํ ๊ธฐ์ค ๊ฒฐ๊ณผ๋ก ์ค์ ์ผ์ ์ ์ยท๋ฐฐ์น์ ๋ง์ถ ์ฌ๋ณด์ ์ด ํ์ํ๋ค. ์ธ์ ์ฒด๊ณ ์ฐ๋์ ํตํฉ ํญ์ (JSON) ํ์ค ๋ฉ์์ง API๋ก ํ์ฅํ๋ค.\n",
"```\n",
" \n"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}