{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [OPS02] ๋‹ค์ค‘ ์„ผ์„œ ๊ฒฝ๋ณด ์ค‘๋ณต ํ•ญ์  ํ†ตํ•ฉ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์ž‘์ „ยท์ „ํˆฌ ์ˆ˜ํ–‰ / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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 }