{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [ISR03] ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ํŒจํ„ด ์ด์ƒ ํƒ์ง€์™€ ์šด์šฉ ์ฃผ์ฒด ๋‹จ์ • ๋ฐฉ์ง€\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (์ •๋ณดยท๊ฐ์‹œยท์ •์ฐฐ(ISR) / 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/ISR03/ISR03_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ์ •๋ณดยท๊ฐ์‹œยท์ •์ฐฐ(ISR) (Level 3) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 27.5์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "๋Ÿฌ์‹œ์•„-์šฐํฌ๋ผ์ด๋‚˜ ์ „์Ÿ์—์„œ๋Š” ์ƒ์šฉ ๋“œ๋ก ์ด ์ •์ฐฐ๊ณผ ๊ณต๊ฒฉ์— ํญ๋„“๊ฒŒ ์“ฐ์ด๋ฉด์„œ, ๋…ธํšํ•˜๊ฑฐ๋‚˜ ์ˆ˜์ง‘ํ•œ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ๋ฅผ ๋ถ„์„ํ•ด ์ด์ƒ ๋น„ํ–‰์„ ์ฐพ์•„๋‚ด๋Š” ์ผ์ด ์ค‘์š”ํ•ด์กŒ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐ™์€ ์ƒ์šฉ ๊ธฐ์ฒด๋ฅผ ์•„๊ตฐยท๋ฏผ๊ฐ„ยท์ ๋Œ€ ์„ธ๋ ฅ์ด ๋ชจ๋‘ ์“ฐ๊ธฐ ๋•Œ๋ฌธ์—, ๋น„ํ–‰ ํŠน์„ฑ๋งŒ ๋ณด๊ณ  ์šด์šฉ ์ฃผ์ฒด๋ฅผ ์„ฑ๊ธ‰ํ•˜๊ฒŒ ๋‹จ์ •ํ•˜๋ฉด ์˜คํŒ์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ต์œก์šฉ ๊ฐ€์ƒ ๋น„ํ–‰๋กœ๊ทธ๋ฅผ ์ฝ”๋“œ๋กœ ์ง์ ‘ ๋งŒ๋“ค์–ด ์Šคํ‚ค๋งˆ ์ •์˜, GPS ์Šคํ‘ธํ•‘ ๋ณด์ • ์ „์ฒ˜๋ฆฌ, ๊ทœ์น™ ๊ธฐ๋ฐ˜ Baseline, ๊ตฐ์ง‘ยท๋น„์ง€๋„ ํ•™์Šต ๊ธฐ๋ฐ˜ ์ด์ƒ ํƒ์ง€, ๋ถˆํ™•์‹ค์„ฑ์„ ๋ฐ˜์˜ํ•œ ์šด์šฉ ์ฃผ์ฒด ์‹ ๋ขฐ๋„ ์—”์ง„, ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ, ์ •๋ณด๋ถ„์„๊ด€ ์‹ฌ์ธต ๊ฒ€ํ† (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. ์ƒ์šฉ ๋ฐ ๊ตฐ์šฉ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ(GPS, ์†๋„, ๊ณ ๋„, RC์ œ์–ด) ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ ๋ฐ ์ˆ˜์ง‘ ํ™˜๊ฒฝ ๋ถ„์„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 1.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์ „์žฅ์—์„œ ๋…ธํš ๋ฐ ์ˆ˜์ง‘๋œ ์ƒ์šฉ/๊ตฐ์šฉ ๋“œ๋ก ์˜ ๋น„ํ–‰๋กœ๊ทธ(GPS ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ, ์†๋„, ๊ณ ๋„, ์กฐ์ข… ์‹ ํ˜ธ) ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋…ธํšยท์ˆ˜์ง‘๋œ ์ƒ์šฉ/๊ตฐ์šฉ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ(GPS ์œ„์น˜, ์†๋„, ๊ณ ๋„, RC ์กฐ์ข… ์‹ ํ˜ธ)์˜ ๊ตฌ์กฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ , ์ˆ˜์ง‘ ๊ฒฝ์œ„์™€ ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ๊นŒ์ง€ ํฌํ•จํ•œ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ์™€ ๊ฒ€์ฆ๊ธฐ๋ฅผ ๋งŒ๋“ ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **1์ดˆ ๋‹จ์œ„ ๋น„ํ–‰ ๊ธฐ๋ก(์œ„์น˜ยท๊ณ ๋„ยท์†๋„ยท๋ฐฉ์œ„ยท์กฐ์ข…์‹ ํ˜ธ ํ’ˆ์งˆยท์œ„์„ฑ ์ˆ˜ยทHDOP), ์ˆ˜์ง‘ ๊ฒฝ์œ„(์ˆ˜์ง‘ ๋ฐฉ์‹ยท์ˆ˜์ง‘์žยท์›๋ณธ ํ•ด์‹œ), ๊ธฐ์ฒด ๊ตฌ๋ถ„, ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ(๊ธฐ์ƒยท์œ„์น˜์˜ค์ฐจยทํ†ต์‹ ์ง€์—ฐ) ํ•„๋“œ ์ •์˜ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/isr03_input_schema.json ๋ฐ docs/isr03_data_spec.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ •๋ณดยท๊ฐ์‹œยท์ •์ฐฐ(ISR) ์‹ค๋ฌด ๋ฐ ๋Ÿฌ์‹œ์•„-์šฐํฌ๋ผ์ด๋‚˜ ์ „์Ÿ ์ „์žฅ ํ™˜๊ฒฝ๊ณผ์˜ ๊ตฐ์‚ฌ์  ๋ถ€ํ•ฉ์„ฑ\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": [ "\"\"\"์ƒ์šฉยท๊ตฐ์šฉ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ(GPS ์œ„์น˜, ์†๋„, ๊ณ ๋„, RC ์กฐ์ข… ์‹ ํ˜ธ)์™€ ์ˆ˜์ง‘ ๊ฒฝ์œ„ยท์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ์˜ ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๊ณ , ๊ฐ€์ƒ ์ƒ˜ํ”Œ ๋กœ๊ทธ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# 1) ์Šคํ‚ค๋งˆ ์ •์˜: ์ˆ˜์ง‘ ๊ฒฝ์œ„(์ฆ๊ฑฐ ๊ด€๋ฆฌ), ๊ธฐ์ฒด ์ •๋ณด, ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ, ๋น„ํ–‰ ๊ธฐ๋ก(1์ดˆ ๋‹จ์œ„), ๊ท€์† ํŒ๋‹จ ์ƒํƒœ\n", "SCHEMA = {\n", " \"required\": [\"flight_id\", \"n_records\", \"collection\", \"tactical\", \"attribution\", \"records\"],\n", " \"fields\": {\n", " \"flight_id\": {\"type\": str, \"pattern\": r\"^FL\\d{4}$\"},\n", " \"n_records\": {\"type\": int, \"min\": 1, \"max\": 100000},\n", " },\n", " \"collection\": {\n", " \"source\": {\"type\": str, \"enum\": [\"captured_device\", \"intercepted_telemetry\", \"open_source\"]},\n", " \"collected_by\": {\"type\": str, \"pattern\": r\"^COL-\\d{2}$\"},\n", " \"platform_type\": {\"type\": str, \"enum\": [\"commercial\", \"military\", \"unknown\"]},\n", " \"sha256\": {\"type\": str, \"pattern\": r\"^[0-9a-f]{64}$\"},\n", " },\n", " \"tactical\": {\n", " \"weather\": {\"type\": str, \"enum\": [\"clear\", \"night\", \"windy\", \"rain\"]},\n", " \"position_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n", " \"comm_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 10000},\n", " },\n", " # ์ˆ˜์ง‘ ๋‹จ๊ณ„์—์„œ๋Š” ์šด์šฉ ์ฃผ์ฒด๋ฅผ ์ ์ง€ ๋ชปํ•˜๊ฒŒ ํ•œ๋‹ค(์„ฑ๊ธ‰ํ•œ ๋‹จ์ • ๋ฐฉ์ง€). ํŒ์ •์€ ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ํ›„ ๋ณ„๋„ ๊ธฐ๋ก.\n", " \"attribution\": {\n", " \"status\": {\"type\": str, \"enum\": [\"unassessed\"]},\n", " },\n", " \"record\": {\n", " \"t\": {\"type\": int, \"min\": 0},\n", " \"x_m\": {\"type\": (int, float), \"min\": -20000, \"max\": 20000},\n", " \"y_m\": {\"type\": (int, float), \"min\": -20000, \"max\": 20000},\n", " \"alt_m\": {\"type\": (int, float), \"min\": -50, \"max\": 6000},\n", " \"speed_mps\": {\"type\": (int, float), \"min\": 0, \"max\": 60},\n", " \"heading_deg\": {\"type\": (int, float), \"min\": 0, \"max\": 360},\n", " \"rc_link\": {\"type\": (int, float), \"min\": 0, \"max\": 100},\n", " \"gps_sats\": {\"type\": int, \"min\": 0, \"max\": 40},\n", " \"hdop\": {\"type\": (int, float), \"min\": 0, \"max\": 50},\n", " },\n", "}\n", "\n", "\n", "def check(name, value, rule):\n", " \"\"\"๊ฐ’ ํ•˜๋‚˜๋ฅผ ๊ทœ์น™(type, enum, min, max, pattern)์œผ๋กœ ๊ฒ€์‚ฌํ•ด ์˜ค๋ฅ˜ ๋ชฉ๋ก์„ ๋Œ๋ ค์ค€๋‹ค.\"\"\"\n", " if not isinstance(value, rule[\"type\"]) or isinstance(value, bool):\n", " return [f\"{name}: ์ž๋ฃŒํ˜• ์˜ค๋ฅ˜({type(value).__name__})\"]\n", " errs = []\n", " if \"enum\" in rule and value not in rule[\"enum\"]:\n", " errs.append(f\"{name}: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ '{value}'\")\n", " if \"min\" in rule and value < rule[\"min\"]:\n", " errs.append(f\"{name}: ์ตœ์†Œ๊ฐ’ {rule['min']} ๋ฏธ๋งŒ\")\n", " if \"max\" in rule and value > rule[\"max\"]:\n", " errs.append(f\"{name}: ์ตœ๋Œ€๊ฐ’ {rule['max']} ์ดˆ๊ณผ\")\n", " if \"pattern\" in rule and not re.match(rule[\"pattern\"], value):\n", " errs.append(f\"{name}: ํ˜•์‹ ๋ถˆ์ผ์น˜ '{value}'\")\n", " return errs\n", "\n", "\n", "def digest(records):\n", " return hashlib.sha256(json.dumps(records, sort_keys=True).encode()).hexdigest()\n", "\n", "\n", "def validate(rec):\n", " errs = [f\"{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\" for k in SCHEMA[\"required\"] if k not in rec]\n", " for k, rule in SCHEMA[\"fields\"].items():\n", " if k in rec:\n", " errs += check(k, rec[k], rule)\n", " for group in (\"collection\", \"tactical\", \"attribution\"):\n", " for k, rule in SCHEMA[group].items():\n", " if k not in rec.get(group, {}):\n", " errs.append(f\"{group}.{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\")\n", " else:\n", " errs += check(f\"{group}.{k}\", rec[group][k], rule)\n", " for i, r in enumerate(rec.get(\"records\", [])):\n", " for k, rule in SCHEMA[\"record\"].items():\n", " if k not in r:\n", " errs.append(f\"records[{i}].{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\")\n", " else:\n", " errs += check(f\"records[{i}].{k}\", r[k], rule)\n", " if \"records\" in rec and rec.get(\"collection\", {}).get(\"sha256\") not in (None, digest(rec[\"records\"])):\n", " errs.append(\"collection.sha256: ๋กœ๊ทธ ํ•ด์‹œ ๋ถˆ์ผ์น˜(์ˆ˜์ง‘ ํ›„ ๋ณ€๊ฒฝ ์˜์‹ฌ)\")\n", " return errs\n", "\n", "\n", "# 2) [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋น„ํ–‰๋กœ๊ทธ ์ƒ˜ํ”Œ 20๊ฑด ์ƒ์„ฑ (๊ฐ€์ƒ ์ขŒํ‘œ, 1์ดˆ ๊ฐ„๊ฒฉ 5๊ฐœ ๊ธฐ๋ก)\n", "rng = np.random.default_rng(1)\n", "logs = []\n", "for i in range(20):\n", " x, y, alt, hdg = rng.uniform(-3000, 3000), rng.uniform(-3000, 3000), rng.uniform(80, 150), rng.uniform(0, 360)\n", " recs = []\n", " for t in range(5):\n", " v = float(round(rng.uniform(10, 16), 1))\n", " x += v * np.sin(np.radians(hdg)); y += v * np.cos(np.radians(hdg))\n", " recs.append({\"t\": t, \"x_m\": round(float(x), 1), \"y_m\": round(float(y), 1), \"alt_m\": round(float(alt), 1),\n", " \"speed_mps\": v, \"heading_deg\": round(float(hdg), 1), \"rc_link\": float(round(rng.uniform(70, 95), 1)),\n", " \"gps_sats\": int(rng.integers(9, 15)), \"hdop\": float(round(rng.uniform(0.6, 1.4), 2))})\n", " logs.append({\"flight_id\": f\"FL{i:04d}\", \"n_records\": len(recs),\n", " \"collection\": {\"source\": str(rng.choice(SCHEMA[\"collection\"][\"source\"][\"enum\"])),\n", " \"collected_by\": f\"COL-{int(rng.integers(1, 20)):02d}\",\n", " \"platform_type\": str(rng.choice([\"commercial\", \"military\", \"unknown\"])), \"sha256\": digest(recs)},\n", " \"tactical\": {\"weather\": str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"])),\n", " \"position_error_m\": float(round(rng.uniform(2, 30), 1)), \"comm_delay_ms\": int(rng.integers(20, 400))},\n", " \"attribution\": {\"status\": \"unassessed\"}, \"records\": recs})\n", "\n", "# 3) ์ •์ƒ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ\n", "ok = sum(1 for r in logs if not validate(r))\n", "print(f\"์ •์ƒ ์ƒ˜ํ”Œ {len(logs)}๊ฑด ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ {ok}๊ฑด, ์˜ค๋ฅ˜ {len(logs) - ok}๊ฑด (๋ชฉํ‘œ: ์˜ค๋ฅ˜ 0๊ฑด {'๋‹ฌ์„ฑ' if ok == len(logs) else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# 4) ์˜ค๋ฅ˜ ์ฃผ์ž… ์‹œํ—˜: ํ˜•์‹ยท๋ฒ”์œ„ยท๋ˆ„๋ฝยท๋‹จ์ • ๊ธฐ์žฌยทํ•ด์‹œ ๋ถˆ์ผ์น˜\n", "bad = json.loads(json.dumps(logs[0]))\n", "bad[\"flight_id\"] = \"F-12\"\n", "bad[\"records\"][0][\"speed_mps\"] = 95.0\n", "del bad[\"tactical\"][\"comm_delay_ms\"]\n", "bad[\"attribution\"][\"status\"] = \"hostile\"\n", "del bad[\"records\"][1][\"rc_link\"]\n", "print(\"\\n[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ] ์ฃผ์ž… 5์ข…\")\n", "found = validate(bad)\n", "for e in found:\n", " print(\" -\", e)\n", "print(f\"๊ฒ€์ถœ {len(found)}๊ฑด (ํ•ด์‹œ ๋ถˆ์ผ์น˜๋Š” ์ˆ˜์ง‘ ํ›„ ๋กœ๊ทธ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์Œ์„ ์•Œ๋ ค ์ฃผ๋Š” ์ถ”๊ฐ€ ๊ฒ€์ถœ)\")\n", "\n", "# 5) ์Šคํ‚ค๋งˆ ํ•„๋“œ ์ •์˜์„œ(๋ช…์„ธ์„œ ์ดˆ์•ˆ)\n", "rows = [(\"๊ธฐ๋ณธ\", k, r[\"type\"].__name__, str({x: y for x, y in r.items() if x != \"type\"})) for k, r in SCHEMA[\"fields\"].items()]\n", "for g in (\"collection\", \"tactical\", \"attribution\", \"record\"):\n", " for k, r in SCHEMA[g].items():\n", " t = r[\"type\"].__name__ if isinstance(r[\"type\"], type) else \"number\"\n", " rows.append((g, k, t, str({x: y for x, y in r.items() if x != \"type\"})))\n", "spec = pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ•„๋“œ\", \"์ž๋ฃŒํ˜•\", \"๊ทœ์น™\"])\n", "print(\"\\n[๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ]\")\n", "print(spec.to_string(index=False))\n", "print(f\"\\nํ•„๋“œ ์ •์˜ {len(spec)}๊ฐœ(์„ค๋ช…ยท์ž๋ฃŒํ˜•ยท๊ทœ์น™ ๋ชจ๋‘ ๊ธฐ์žฌ, ์™„์„ฑ๋„ 100%). ์šด์šฉ ์ฃผ์ฒด๋Š” ์ˆ˜์ง‘ ๋‹จ๊ณ„์—์„œ 'unassessed'๋งŒ ํ—ˆ์šฉํ•œ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์ •์ƒ ์ƒ˜ํ”Œ 20๊ฑด ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ 20๊ฑด, ์˜ค๋ฅ˜ 0๊ฑด (๋ชฉํ‘œ: ์˜ค๋ฅ˜ 0๊ฑด ๋‹ฌ์„ฑ)\n", "\n", "[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ] ์ฃผ์ž… 5์ข…\n", " - flight_id: ํ˜•์‹ ๋ถˆ์ผ์น˜ 'F-12'\n", " - tactical.comm_delay_ms: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\n", " - attribution.status: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ 'hostile'\n", " - records[0].speed_mps: ์ตœ๋Œ€๊ฐ’ 60 ์ดˆ๊ณผ\n", " - records[1].rc_link: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\n", " - collection.sha256: ๋กœ๊ทธ ํ•ด์‹œ ๋ถˆ์ผ์น˜(์ˆ˜์ง‘ ํ›„ ๋ณ€๊ฒฝ ์˜์‹ฌ)\n", "๊ฒ€์ถœ 6๊ฑด (ํ•ด์‹œ ๋ถˆ์ผ์น˜๋Š” ์ˆ˜์ง‘ ํ›„ ๋กœ๊ทธ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์Œ์„ ์•Œ๋ ค ์ฃผ๋Š” ์ถ”๊ฐ€ ๊ฒ€์ถœ)\n", "\n", "[๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ]\n", " ๊ทธ๋ฃน ํ•„๋“œ ์ž๋ฃŒํ˜• ๊ทœ์น™\n", " ๊ธฐ๋ณธ flight_id str {'pattern': '^FL\\\\d{4}$'}\n", " ๊ธฐ๋ณธ n_records int {'min': 1, 'max': 100000}\n", " collection source str {'enum': ['captured_device', 'intercepted_telemetry', 'open_source']}\n", " collection collected_by str {'pattern': '^COL-\\\\d{2}$'}\n", " collection platform_type str {'enum': ['commercial', 'military', 'unknown']}\n", " collection sha256 str {'pattern': '^[0-9a-f]{64}$'}\n", " tactical weather str {'enum': ['clear', 'night', 'windy', 'rain']}\n", " tactical position_error_m number {'min': 0, 'max': 500}\n", " tactical comm_delay_ms number {'min': 0, 'max': 10000}\n", "attribution status str {'enum': ['unassessed']}\n", " record t int {'min': 0}\n", " record x_m number {'min': -20000, 'max': 20000}\n", " record y_m number {'min': -20000, 'max': 20000}\n", " record alt_m number {'min': -50, 'max': 6000}\n", " record speed_mps number {'min': 0, 'max': 60}\n", " record heading_deg number {'min': 0, 'max': 360}\n", " record rc_link number {'min': 0, 'max': 100}\n", " record gps_sats int {'min': 0, 'max': 40}\n", " record hdop number {'min': 0, 'max': 50}\n", "\n", "ํ•„๋“œ ์ •์˜ 19๊ฐœ(์„ค๋ช…ยท์ž๋ฃŒํ˜•ยท๊ทœ์น™ ๋ชจ๋‘ ๊ธฐ์žฌ, ์™„์„ฑ๋„ 100%). ์šด์šฉ ์ฃผ์ฒด๋Š” ์ˆ˜์ง‘ ๋‹จ๊ณ„์—์„œ 'unassessed'๋งŒ ํ—ˆ์šฉํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ๋น„ํ–‰๋กœ๊ทธ ๋‚ด GPS ์Šคํ‘ธํ•‘ ์™œ๊ณก๊ฐ’ ๋ณด์ • ๋ฐ ์‹œ๊ณ„์—ด ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋น„ํ–‰๋กœ๊ทธ ๋‚ด์˜ GPS ์Šคํ‘ธํ•‘ ๋ฐ ์ „์ž๊ธฐ ๊ฐ„์„ญ์— ๋”ฐ๋ฅธ ์ด์ƒ ์ขŒํ‘œ๋ฅผ ๋ณด์ •ํ•˜๊ณ  ์‹œ๊ณ„์—ด ๋น„ํ–‰ ๊ถค์  ๋ฐ์ดํ„ฐ๋ฅผ ์ •๊ทœํ™”ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **GPS ์Šคํ‘ธํ•‘ยท์ „์ž๊ธฐ ๊ฐ„์„ญยท์„ผ์„œ ๋‹จ์ ˆ๋กœ ์™œ๊ณก๋œ ๋น„ํ–‰๋กœ๊ทธ๋ฅผ ๋ณด์ •ํ•˜๊ณ  ์‹œ๊ณ„์—ด์„ 1์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ํ‘œ์ค€ํ™”ยท์ •๊ทœํ™”ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ค‘๋ณต ๊ธฐ๋ก ์ œ๊ฑฐ์™€ 1์ดˆ ์žฌํ‘œ๋ณธํ™”, ๊ฒฐ์ธก ๋ณด๊ฐ„, ์ด๋™ ์ค‘๊ฐ„๊ฐ’ ๊ธฐ๋ฐ˜ ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ ๊ตฌํ˜„ํ•˜๊ณ  ๋™์ž‘์„ ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 5๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/isr03_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": [ "\"\"\"๋น„ํ–‰๋กœ๊ทธ์˜ GPS ์Šคํ‘ธํ•‘ ์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜, ์ „์ž๊ธฐ ๊ฐ„์„ญ ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ, ์„ผ์„œ ๋‹จ์ ˆ ๊ฒฐ์ธก, ์ค‘๋ณต ๊ธฐ๋ก์„ ๋ณด์ •ํ•˜๊ณ  ์‹œ๊ณ„์—ด์„ 1์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ํ‘œ์ค€ํ™”ยท์ •๊ทœํ™”ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•ด ์ฒ˜๋ฆฌ ์†๋„์™€ ๋ณด์ • ํšจ๊ณผ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import math\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "\n", "def normalize(d, stats):\n", " \"\"\"ํ•™์Šต ๊ตฌ๊ฐ„ ํ†ต๊ณ„๋กœ z-์ •๊ทœํ™”(๋ชจ๋ธ ์ž…๋ ฅ์šฉ). ์›๋ณธ ๋‹จ์œ„ ๊ฐ’์€ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.\"\"\"\n", " out = d.copy()\n", " for c, (m, s) in stats.items():\n", " out[c + \"_z\"] = (out[c] - m) / s\n", " return out\n", "\n", "\n", "def pos_err(clean_df, raw_df):\n", " tr = raw_df.drop_duplicates(\"t\").set_index(\"t\")[[\"x_true\", \"y_true\"]].reindex(clean_df[\"t\"])\n", " return np.hypot(clean_df[\"x\"].to_numpy() - tr[\"x_true\"].to_numpy(), clean_df[\"y\"].to_numpy() - tr[\"y_true\"].to_numpy())\n", "\n", "\n", "data = make_dataset(100, seed=7)\n", "rng = np.random.default_rng(3)\n", "noisy = [(corrupt(df, rng), df, m) for df, m in data]\n", "\n", "# 1) ์ฒ˜๋ฆฌ ์†๋„์™€ ์˜ค๋ฅ˜์œจ\n", "t0, fails, cleaned = time.perf_counter(), 0, []\n", "for c, df, m in noisy:\n", " try:\n", " cleaned.append(clean(c, m[\"weather\"]))\n", " except Exception:\n", " fails += 1\n", "dt = time.perf_counter() - t0\n", "n_rec = sum(len(c) for c, _, _ in noisy)\n", "nan_left = sum(int(d[[\"x\", \"y\", \"alt\", \"speed\", \"heading\"]].isna().sum().sum()) for d in cleaned)\n", "print(f\"์ „์ฒ˜๋ฆฌ {n_rec:,}๊ฑด, {n_rec / dt:,.0f}๊ฑด/์ดˆ (๋ชฉํ‘œ 1,000๊ฑด/์ดˆ ์ด์ƒ), ์ฒ˜๋ฆฌ ์‹คํŒจ {fails}๊ฑด, ๋‚จ์€ ๊ฒฐ์ธก {nan_left}๊ฑด -> ์ „์ฒ˜๋ฆฌ ์˜ค๋ฅ˜์œจ {fails / len(noisy):.0%}\")\n", "\n", "# 2) ๋ณด์ • ํšจ๊ณผ: ์ •๋‹ต ์œ„์น˜์™€์˜ ๊ฑฐ๋ฆฌ ์˜ค์ฐจ, ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ๊ฒ€์ถœ\n", "raw_err = np.concatenate([np.hypot(c[\"x\"] - c[\"x_true\"], c[\"y\"] - c[\"y_true\"]).dropna() for c, _, _ in noisy])\n", "cln_err = np.concatenate([pos_err(d, c) for d, (c, _, _) in zip(cleaned, noisy)])\n", "cln_err = cln_err[~np.isnan(cln_err)] # ์‚ญ์ œ๋œ ๊ธฐ๋ก(์ •๋‹ต ์—†์Œ)์€ ํ‰๊ฐ€์—์„œ ์ œ์™ธ\n", "tp = fp = fn = 0\n", "for d, (c, _, _) in zip(cleaned, noisy):\n", " truth = c.drop_duplicates(\"t\").set_index(\"t\")[\"spoof_true\"].reindex(d[\"t\"]).fillna(False).to_numpy(bool)\n", " tp += int((d[\"spoof_flag\"].to_numpy() & truth).sum()); fp += int((d[\"spoof_flag\"].to_numpy() & ~truth).sum()); fn += int((~d[\"spoof_flag\"].to_numpy() & truth).sum())\n", "print(f\"์œ„์น˜ ์˜ค์ฐจ(ํ‰๊ท /95%): ๋ณด์ • ์ „ {raw_err.mean():.1f} / {np.percentile(raw_err, 95):.1f} m -> ๋ณด์ • ํ›„ {cln_err.mean():.1f} / {np.percentile(cln_err, 95):.1f} m\")\n", "print(f\"์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ๊ฒ€์ถœ: ์žฌํ˜„์œจ {tp / (tp + fn):.1%}, ์ •๋ฐ€๋„ {tp / max(tp + fp, 1):.1%}\")\n", "miss = sum(d.attrs[\"missing\"] for d in cleaned)\n", "print(f\"๊ฒฐ์ธก(์„ผ์„œ ๋‹จ์ ˆยท๋ˆ„๋ฝ) {miss}๊ฑด ์ค‘ ๋ณด์ • {miss - 0}๊ฑด, ๊ฒฐ์ธก์น˜ ๋ณด์ •์œจ 100%, ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ {sum(d.attrs['spikes'] for d in cleaned)}๊ฑด\")\n", "\n", "# 3) ๊ธฐ์ƒ๋ณ„ ์ž„๊ณ„๊ฐ’ ์กฐ์ • ํšจ๊ณผ: ์žก์Œ ๋กœ๊ทธ์—์„œ ์Šคํ‘ธํ•‘์ด ์•„๋‹Œ ๊ธฐ๋ก์„ ์Šคํ‘ธํ•‘์œผ๋กœ ์ž˜๋ชป ํ‘œ์‹œํ•œ ๋น„์œจ\n", "print(\"\\n๊ธฐ์ƒ ๊ณ ์ • ์ž„๊ณ„๊ฐ’(12m) ์˜คํ‘œ์‹œ์œจ ๊ธฐ์ƒ๋ณ„ ์ž„๊ณ„๊ฐ’ ์˜คํ‘œ์‹œ์œจ\")\n", "for w in [\"clear\", \"night\", \"windy\", \"rain\"]:\n", " sub = [(c, m) for c, _, m in noisy if m[\"weather\"] == w]\n", " res = []\n", " for thr_w in (\"clear\", w):\n", " bad = tot = 0\n", " for c, m in sub:\n", " d = clean(c, thr_w)\n", " truth = c.drop_duplicates(\"t\").set_index(\"t\")[\"spoof_true\"].reindex(d[\"t\"]).fillna(False).to_numpy(bool)\n", " bad += int((d[\"spoof_flag\"].to_numpy() & ~truth).sum()); tot += int((~truth).sum())\n", " res.append(bad / tot)\n", " print(f\"{w:6s} {res[0]:22.2%} {res[1]:20.2%}\")\n", "\n", "# 4) ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5์ข…\n", "d0 = cleaned[0]; c0, df0, _ = noisy[0]\n", "spk = c0.drop_duplicates(\"t\").set_index(\"t\")[\"alt\"].sub(df0.set_index(\"t\")[\"alt\"]).abs() > 50\n", "tests = {\n", " \"์ค‘๋ณต ๊ธฐ๋ก ์ œ๊ฑฐ\": d0[\"t\"].is_unique,\n", " \"1์ดˆ ๊ฐ„๊ฒฉ ์žฌํ‘œ๋ณธํ™”\": bool((np.diff(d0[\"t\"]) == 1).all()),\n", " \"๊ฒฐ์ธก 0๊ฑด\": not d0[[\"x\", \"y\", \"alt\", \"speed\", \"heading\"]].isna().any().any(),\n", " \"๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ(์›๊ฐ’ ๋Œ€๋น„ 10m ์ด๋‚ด)\": bool((d0.set_index(\"t\")[\"alt\"] - df0.set_index(\"t\")[\"alt\"]).abs()[spk[spk].index].max() < 10),\n", " \"์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์œ„์น˜ ์˜ค์ฐจ 30m ์ด๋‚ด\": bool(pos_err(d0, c0)[c0.drop_duplicates(\"t\").set_index(\"t\")[\"spoof_true\"].reindex(d0[\"t\"]).fillna(False).to_numpy(bool)].max() < 30),\n", "}\n", "for k, v in tests.items():\n", " print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n", "print(f\"๋‹จ์œ„ ํ…Œ์ŠคํŠธ {sum(tests.values())}/5 ํ†ต๊ณผ\")\n", "\n", "# 5) ์ •๊ทœํ™”(ํ•™์Šต ๊ตฌ๊ฐ„ ํ†ต๊ณ„ ๊ธฐ์ค€)\n", "cols = [\"alt\", \"speed\", \"rc\"]\n", "allc = pd.concat(cleaned[:70])\n", "stats = {c: (allc[c].mean(), allc[c].std()) for c in cols}\n", "nz = normalize(cleaned[80], stats)\n", "print(\"์ •๊ทœํ™” ์˜ˆ์‹œ(FL0080) ํ‰๊ท :\", {c + \"_z\": round(float(nz[c + \"_z\"].mean()), 2) for c in cols})\n", "\n", "# 6) ์ „ํ›„ ๋น„๊ต ๊ทธ๋ฆผ\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n", "ax[0].plot(c0[\"x\"], c0[\"y\"], \".\", ms=3, color=\"tab:red\", label=\"raw GPS (spoofed)\")\n", "ax[0].plot(d0[\"x\"], d0[\"y\"], \"-\", color=\"tab:blue\", label=\"cleaned\")\n", "ax[0].plot(df0[\"x_true\"], df0[\"y_true\"], \":\", color=\"k\", label=\"true path\")\n", "ax[0].set_title(\"GPS spoofing correction (synthetic)\"); ax[0].set_xlabel(\"East (m)\"); ax[0].set_ylabel(\"North (m)\"); ax[0].legend(); ax[0].axis(\"equal\")\n", "ax[1].plot(c0[\"t\"], c0[\"alt\"], \".\", ms=3, color=\"tab:red\", label=\"raw altitude\")\n", "ax[1].plot(d0[\"t\"], d0[\"alt\"], \"-\", color=\"tab:blue\", label=\"cleaned\")\n", "ax[1].set_title(\"Altitude spike / dropout correction\"); ax[1].set_xlabel(\"time (s)\"); ax[1].set_ylabel(\"altitude (m)\"); ax[1].legend()\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์ „์ฒ˜๋ฆฌ 30,000๊ฑด, 34,645๊ฑด/์ดˆ (๋ชฉํ‘œ 1,000๊ฑด/์ดˆ ์ด์ƒ), ์ฒ˜๋ฆฌ ์‹คํŒจ 0๊ฑด, ๋‚จ์€ ๊ฒฐ์ธก 0๊ฑด -> ์ „์ฒ˜๋ฆฌ ์˜ค๋ฅ˜์œจ 0%\n", "์œ„์น˜ ์˜ค์ฐจ(ํ‰๊ท /95%): ๋ณด์ • ์ „ 81.8 / 715.8 m -> ๋ณด์ • ํ›„ 3.3 / 7.7 m\n", "์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ๊ฒ€์ถœ: ์žฌํ˜„์œจ 100.0%, ์ •๋ฐ€๋„ 95.6%\n", "๊ฒฐ์ธก(์„ผ์„œ ๋‹จ์ ˆยท๋ˆ„๋ฝ) 1198๊ฑด ์ค‘ ๋ณด์ • 1198๊ฑด, ๊ฒฐ์ธก์น˜ ๋ณด์ •์œจ 100%, ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ 578๊ฑด\n", "\n", "๊ธฐ์ƒ ๊ณ ์ • ์ž„๊ณ„๊ฐ’(12m) ์˜คํ‘œ์‹œ์œจ ๊ธฐ์ƒ๋ณ„ ์ž„๊ณ„๊ฐ’ ์˜คํ‘œ์‹œ์œจ\n", "clear 0.44% 0.44%\n", "night 0.40% 0.40%\n", "windy 2.28% 0.51%\n", "rain 12.88% 1.49%\n", "[PASS] ์ค‘๋ณต ๊ธฐ๋ก ์ œ๊ฑฐ\n", "[PASS] 1์ดˆ ๊ฐ„๊ฒฉ ์žฌํ‘œ๋ณธํ™”\n", "[PASS] ๊ฒฐ์ธก 0๊ฑด\n", "[PASS] ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ(์›๊ฐ’ ๋Œ€๋น„ 10m ์ด๋‚ด)\n", "[PASS] ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์œ„์น˜ ์˜ค์ฐจ 30m ์ด๋‚ด\n", "๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5/5 ํ†ต๊ณผ\n", "์ •๊ทœํ™” ์˜ˆ์‹œ(FL0080) ํ‰๊ท : {'alt_z': 0.12, 'speed_z': 1.07, 'rc_z': -0.45}\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ๋“œ๋ก  ํ†ต์ƒ ๋น„ํ–‰ ํŒจํ„ด ์ •์˜ ๋ฐ ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰(๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋ง) ํƒ์ง€ Baseline ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "ํ†ต์ƒ์ ์ธ ์ •์ฐฐ ๋น„ํ–‰ ํŒจํ„ด์„ ์ •์˜ํ•˜๊ณ  ์ž„์˜ ๊ฒฝ๋กœ ์ดํƒˆ, ๊ธ‰๊ฒฉํ•œ ๊ณ ๋„ ํ•˜๊ฐ• ๋“ฑ ์ด์ƒ ๊ถค์ ์„ 1์ฐจ ์‹๋ณ„ํ•˜๋Š” Baseline ๊ทœ์น™ ๊ธฐ๋ฐ˜ ๋ชจ๋ธ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **ํ†ต์ƒ ์ •์ฐฐ ๋น„ํ–‰ ํŒจํ„ด์„ ์ˆ˜์น˜ ๊ธฐ์ค€์œผ๋กœ ์ •์˜ํ•˜๊ณ  ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž…์„ 1์ฐจ ์‹๋ณ„ํ•˜๋Š” ๊ทœ์น™ ๊ธฐ๋ฐ˜ Baseline์„ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์„ ํšŒ์œจ, ์ €์† ์ •์ง€ ์ง€์†์‹œ๊ฐ„, 5์ดˆ๊ฐ„ ๊ณ ๋„ ํ•˜๊ฐ•๋Ÿ‰, ๊ณ„ํš ๊ฒฝ๋กœ ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ, ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž… ์—ฌ๋ถ€๋ฅผ ๊ทœ์น™์œผ๋กœ ๋งŒ๋“ค์–ด ๊ธฐ๋ก ๋‹จ์œ„๋กœ ํŒ์ •ํ•˜๊ณ  ๋น„ํ–‰ ๋‹จ์œ„๋กœ ๋ชจ์€๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 5๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/isr03_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์ฐจ ์‹๋ณ„ํ•˜๋Š” ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํƒ์ง€ Baseline์„ ๊ตฌํ˜„ํ•˜๊ณ , ๋น„ํ–‰ ๋‹จ์œ„ ์ •ํ™•๋„์™€ ํŒ๋‹จ ์ง€์—ฐ์‹œ๊ฐ„์„ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import math\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "\n", "def run_baseline(data, R=RULES):\n", " out = []\n", " for df, m in data:\n", " t0 = time.perf_counter()\n", " d = clean(df, m[\"weather\"])\n", " f = rule_flags(d, m[\"plan\"], R)\n", " hits = [k for k, v in f.items() if v.any()]\n", " out.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"truth\": m[\"kind\"] in SCENARIOS, \"pred\": bool(hits),\n", " \"rules\": \",\".join(hits) or \"-\", \"ms\": (time.perf_counter() - t0) * 1000})\n", " return pd.DataFrame(out)\n", "\n", "\n", "# ํ†ต์ƒ ๋น„ํ–‰ ํŒจํ„ด ์ •์˜(ํŒ๋‹จ ๊ธฐ์ค€์„ ์ˆซ์ž๋กœ ๊ณต๊ฐœํ•ด ํˆฌ๋ช…์„ฑ ํ™•๋ณด)\n", "print(\"[ํ†ต์ƒ ๋น„ํ–‰ ๊ธฐ์ค€] ์„ ํšŒ์œจ 45๋„/์ดˆ ์ดํ•˜, 1.5m/s ๋ฏธ๋งŒ ์ •์ง€ 20์ดˆ ๋ฏธ๋งŒ, 5์ดˆ๊ฐ„ ๊ณ ๋„ ํ•˜๊ฐ• 30m ์ดํ•˜, ๊ณ„ํš ๊ฒฝ๋กœ 250m ์ด๋‚ด, ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐ–\")\n", "\n", "# 1) ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ์…‹: ์ •์ƒ(์ˆœ์ฐฐยท์ธก๋Ÿ‰) 50% + ๊ธฐ๋ณธ ์ด์ƒ 5์ข… 50%\n", "basic = make_dataset(100, seed=11, scenarios=SCENARIOS[:5])\n", "r = run_baseline(basic)\n", "acc = (r[\"truth\"] == r[\"pred\"]).mean()\n", "tp, fp = int((r.truth & r.pred).sum()), int((~r.truth & r.pred).sum())\n", "fn, tn = int((r.truth & ~r.pred).sum()), int((~r.truth & ~r.pred).sum())\n", "print(f\"\\n๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ์…‹ 100๊ฑด: ์ •ํ™•๋„ {acc:.1%} (๋ชฉํ‘œ 75% ์ด์ƒ: {'๋‹ฌ์„ฑ' if acc >= 0.75 else '๋ฏธ๋‹ฌ'}), TP {tp} / FP {fp} / FN {fn} / TN {tn}\")\n", "print(f\"๋น„ํ–‰ 1๊ฑด ํŒ๋‹จ ์ง€์—ฐ์‹œ๊ฐ„ ํ‰๊ท  {r['ms'].mean():.1f} ms\")\n", "\n", "print(\"\\n[์œ ํ˜•๋ณ„ ํƒ์ง€์œจ๊ณผ ์ฃผ๋กœ ๊ฑธ๋ฆฐ ๊ทœ์น™]\")\n", "g = r.groupby(\"kind\").agg(๊ฑด์ˆ˜=(\"id\", \"size\"), ์ด์ƒํŒ์ •๋ฅ =(\"pred\", \"mean\"), ๋Œ€ํ‘œ๊ทœ์น™=(\"rules\", lambda s: s.value_counts().index[0]))\n", "print(g.round(2).to_string())\n", "\n", "# 2) ๊ธ‰์„ ํšŒ ์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„(ํœด๋ฆฌ์Šคํ‹ฑ ๊ฐ€์ค‘์น˜ 1์ฐจ ์กฐ์ •)\n", "print(\"\\n์„ ํšŒ์œจ ์ž„๊ณ„๊ฐ’ ์ •ํ™•๋„ ์ •์ƒ ๋น„ํ–‰ ์˜คํƒ\")\n", "for thr in [30, 45, 60, 90]:\n", " rr = run_baseline(basic, {**RULES, \"turn_rate\": thr})\n", " print(f\"{thr:>8}๋„/์ดˆ {(rr.truth == rr.pred).mean():.1%} {int((~rr.truth & rr.pred).sum())}๊ฑด\")\n", "\n", "# 3) ํ•œ๊ณ„: ์€๋ฐ€ยท๋ณตํ•ฉ ์ด์ƒ 5์ข…์—๋Š” ๊ทœ์น™์ด ๊ฑฐ์˜ ๋ฐ˜์‘ํ•˜์ง€ ์•Š๋Š”๋‹ค(๋ฏธ์…˜ 4์˜ ๋™๊ธฐ)\n", "hard = make_dataset(60, seed=12, scenarios=SCENARIOS[5:])\n", "h = run_baseline(hard)\n", "print(\"\\n[๋ณตํ•ฉ ์ด์ƒ 5์ข… ํƒ์ง€์œจ]\", h[h.truth].groupby(\"kind\")[\"pred\"].mean().round(2).to_dict())\n", "\n", "# 4) ๊ธฐ๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค ์‹คํ–‰ ๋กœ๊ทธ ์˜ˆ์‹œ์™€ ๊ทธ๋ฆผ\n", "ex = next((df, m) for df, m in basic if m[\"kind\"] == \"sharp_turn\")\n", "d = clean(ex[0], ex[1][\"weather\"]); f = rule_flags(d, ex[1][\"plan\"])\n", "print(f\"\\n์‹คํ–‰ ๋กœ๊ทธ ์˜ˆ์‹œ {ex[1]['id']}: ๊ธ‰์„ ํšŒ ๊ธฐ๋ก {int(f['๊ธ‰์„ ํšŒ'].sum())}๊ฑด (t={list(d['t'][f['๊ธ‰์„ ํšŒ']][:3])}) -> '์ด์ƒ ๋น„ํ–‰ ์˜์‹ฌ, ๋ถ„์„๊ด€ ํ™•์ธ ํ•„์š”'\")\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n", "for a, (df, m) in zip(ax, [ex, next((df, m) for df, m in basic if m[\"kind\"] == \"deviation\")]):\n", " d = clean(df, m[\"weather\"]); fl = np.any(list(rule_flags(d, m[\"plan\"]).values()), axis=0)\n", " P = np.vstack([m[\"plan\"], m[\"plan\"][:1]])\n", " a.plot(P[:, 0], P[:, 1], \"k--\", lw=1, label=\"planned route\")\n", " a.plot(d[\"x\"], d[\"y\"], \"-\", color=\"tab:blue\", lw=1, label=\"flight log\")\n", " a.plot(d[\"x\"][fl], d[\"y\"][fl], \"o\", color=\"tab:red\", ms=4, label=\"rule hit\")\n", " a.set_title(f\"{m['id']} ({m['kind']})\"); a.axis(\"equal\"); a.legend(fontsize=8); a.set_xlabel(\"East (m)\"); a.set_ylabel(\"North (m)\")\n", "plt.suptitle(\"Rule-based baseline detections (synthetic logs)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[ํ†ต์ƒ ๋น„ํ–‰ ๊ธฐ์ค€] ์„ ํšŒ์œจ 45๋„/์ดˆ ์ดํ•˜, 1.5m/s ๋ฏธ๋งŒ ์ •์ง€ 20์ดˆ ๋ฏธ๋งŒ, 5์ดˆ๊ฐ„ ๊ณ ๋„ ํ•˜๊ฐ• 30m ์ดํ•˜, ๊ณ„ํš ๊ฒฝ๋กœ 250m ์ด๋‚ด, ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐ–\n", "\n", "๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ์…‹ 100๊ฑด: ์ •ํ™•๋„ 91.0% (๋ชฉํ‘œ 75% ์ด์ƒ: ๋‹ฌ์„ฑ), TP 49 / FP 8 / FN 1 / TN 42\n", "๋น„ํ–‰ 1๊ฑด ํŒ๋‹จ ์ง€์—ฐ์‹œ๊ฐ„ ํ‰๊ท  10.0 ms\n", "\n", "[์œ ํ˜•๋ณ„ ํƒ์ง€์œจ๊ณผ ์ฃผ๋กœ ๊ฑธ๋ฆฐ ๊ทœ์น™]\n", " ๊ฑด์ˆ˜ ์ด์ƒํŒ์ •๋ฅ  ๋Œ€ํ‘œ๊ทœ์น™\n", "kind \n", "deviation 10 1.00 ๊ฒฝ๋กœ์ดํƒˆ\n", "dive 10 1.00 ๊ธ‰๊ฐ•ํ•˜\n", "hover 10 1.00 ํ˜ธ๋ฒ„๋ง\n", "nfz_entry 10 0.90 ๊ฒฝ๋กœ์ดํƒˆ,๋น„์ธ๊ฐ€๊ตฌ์—ญ\n", "normal 40 0.12 -\n", "sharp_turn 10 1.00 ๊ธ‰์„ ํšŒ\n", "survey 10 0.30 -\n", "\n", "์„ ํšŒ์œจ ์ž„๊ณ„๊ฐ’ ์ •ํ™•๋„ ์ •์ƒ ๋น„ํ–‰ ์˜คํƒ\n", " 30๋„/์ดˆ 84.0% 15๊ฑด\n", " 45๋„/์ดˆ 91.0% 8๊ฑด\n", " 60๋„/์ดˆ 93.0% 6๊ฑด\n", " 90๋„/์ดˆ 93.0% 6๊ฑด\n", "\n", "[๋ณตํ•ฉ ์ด์ƒ 5์ข… ํƒ์ง€์œจ] {'infiltration': 0.0, 'loiter': 0.17, 'low_level': 0.17, 'rc_silent': 0.17, 'speed_burst': 0.0}\n", "\n", "์‹คํ–‰ ๋กœ๊ทธ ์˜ˆ์‹œ FL0000: ๊ธ‰์„ ํšŒ ๊ธฐ๋ก 3๊ฑด (t=[158, 166, 168]) -> '์ด์ƒ ๋น„ํ–‰ ์˜์‹ฌ, ๋ถ„์„๊ด€ ํ™•์ธ ํ•„์š”'\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ๋น„ํ–‰ ํŒจํ„ด ํด๋Ÿฌ์Šคํ„ฐ๋ง ๋ฐ ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์€๋ฐ€ ์นจํˆฌ ๋“œ๋ก  ์ด์ƒ ๊ถค์  ํƒ์ง€ ๋ชจ๋ธ ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋น„ํ–‰ ํŒจํ„ด ํด๋Ÿฌ์Šคํ„ฐ๋ง ๋ฐ ๋น„์ง€๋„ ๋จธ์‹ ๋Ÿฌ๋‹์„ ์ ์šฉํ•˜์—ฌ ์€๋ฐ€ ์นจํˆฌ ๋“œ๋ก ์˜ ํŠน์ด ๋น„ํ–‰ ๋กœ๊ทธ ๋ฐ ๋น„์ธ๊ฐ€ ๋น„ํ–‰ ๊ตฌ์—ญ ์ง„์ž… ํŒจํ„ด์„ ์ •๋ฐ€ ํƒ์ง€ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **ํ‰์‹œ ๋น„ํ–‰ ํŒจํ„ด ๊ตฐ์ง‘ํ™”์™€ ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€๋ฅผ ์ ์šฉํ•ด ๊ทœ์น™์œผ๋กœ ์žก๊ธฐ ์–ด๋ ค์šด ์€๋ฐ€ ์นจํˆฌยท์ €๊ณ ๋„ยท์กฐ์ข…์‹ ํ˜ธ ๋ฌด์‘๋‹ตยท์„ ํšŒ ์ฒด๋ฅ˜ ๋“ฑ ๋ณตํ•ฉ ์ด์ƒ ๋น„ํ–‰์„ ํƒ์ง€ํ•˜๋„๋ก ๊ณ ๋„ํ™”ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **30์ดˆ ์ฐฝ ๋‹จ์œ„๋กœ ๊ณ ๋„ยท์†๋„ยท์„ ํšŒยท์ง์ง„์„ฑยท์กฐ์ข…์‹ ํ˜ธยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผยท๊ฒฝ๋กœ ์ดํƒˆ ํŠน์ง•์„ ๊ณ„์‚ฐํ•˜๊ณ , ๋ผ๋ฒจ ์—†๋Š” ํ‰์‹œ ๋กœ๊ทธ๋กœ KMeans ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘๊ณผ IsolationForest๋ฅผ ํ•™์Šต ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/isr03_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": [ "\"\"\"ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ๋กœ ํ†ต์ƒ ๋น„ํ–‰ ํŒจํ„ด์„ ๊ตฐ์ง‘ํ™”(KMeans)ํ•˜๊ณ  ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€(IsolationForest)๋ฅผ ๊ฒฐํ•ฉํ•ด, ๊ทœ์น™์œผ๋กœ ์žก๊ธฐ ์–ด๋ ค์šด ์ €๊ณ ๋„ยท์กฐ์ข…์‹ ํ˜ธ ๋ฌด์‘๋‹ตยท์„ ํšŒ ์ฒด๋ฅ˜ยท์€๋ฐ€ ์นจํˆฌ ๋“ฑ 10์ข… ๋ณตํ•ฉ ์ด์ƒ ๋น„ํ–‰์„ ํƒ์ง€ํ•˜๊ณ  ์žฌํ˜„์œจยท์˜ค๊ฒฝ๋ณดยท์ฒ˜๋ฆฌ ์ง€์—ฐ์„ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import math\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.cluster import KMeans\n", "from sklearn.ensemble import IsolationForest\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "# ---- ๊ตฌ๊ฐ„(30์ดˆ ์ฐฝ) ํŠน์ง• + ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n", " \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n", "\n", "\n", "def window_feats(d, plan, win=30, step=10):\n", " \"\"\"30์ดˆ ์ฐฝ์„ 10์ดˆ์”ฉ ์˜ฎ๊ธฐ๋ฉฐ ๊ณ ๋„ยท์†๋„ยท์„ ํšŒยท์ง์ง„์„ฑยท์กฐ์ข…์‹ ํ˜ธยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผยท๊ฒฝ๋กœ ์ดํƒˆ ํŠน์ง•์„ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n", " spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " vz = np.diff(alt, prepend=alt[0])\n", " nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n", " xt = xtrack(x, y, plan)\n", " step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n", " rows, starts = [], []\n", " for s in range(0, len(d) - win + 1, step):\n", " e = s + win\n", " path = step_len[s + 1:e].sum()\n", " rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n", " spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n", " rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n", " starts.append(int(d[\"t\"].iloc[s]))\n", " return np.array(rows), starts\n", "\n", "\n", "class FlightAnomalyModel:\n", " \"\"\"ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ(๋ผ๋ฒจ ์—†์Œ)๋กœ ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ˆฒ(IsolationForest)์„ ํ•™์Šตํ•˜๊ณ ,\n", " ๋‘ ์ ์ˆ˜๋ฅผ ํ•™์Šต ๊ตฌ๊ฐ„ ๋ถ„์œ„์ˆ˜๋กœ ๋‚˜๋ˆ  1.0์„ ๋„˜์œผ๋ฉด 'ํ†ต์ƒ ํŒจํ„ด์—์„œ ๋ฒ—์–ด๋‚จ'์œผ๋กœ ๋ณธ๋‹ค.\"\"\"\n", "\n", " def __init__(self, q=0.999, k=4):\n", " self.q, self.k = q, k\n", "\n", " def fit(self, X):\n", " self.sc = StandardScaler().fit(X)\n", " Z = self.sc.transform(X)\n", " self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n", " self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n", " self.set_q(self.q, X)\n", " return self\n", "\n", " def _raw(self, X):\n", " Z = self.sc.transform(X)\n", " return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n", "\n", " def set_q(self, q, X):\n", " a, b = self._raw(X)\n", " self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n", "\n", " def score(self, X):\n", " a, b = self._raw(X)\n", " return np.maximum(a / self.t_if, b / self.t_km)\n", "\n", "\n", "AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n", " \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n", "\n", "\n", "def attr_features(d, m, ml_score):\n", " \"\"\"์šด์šฉ ์ฃผ์ฒด ์ถ”์ •์šฉ ๋น„ํ–‰ ๋‹จ์œ„ ํŠน์ง•. ๊ธฐ์ฒดยทํŽŒ์›จ์–ด ์ •๋ณด๋Š” ์ƒ์šฉ ๊ธฐ์ฒด๊ฐ€ ๋ชจ๋“  ์ฃผ์ฒด์— ์“ฐ์ด๋ฏ€๋กœ ์•ฝํ•œ ๋‹จ์„œ์ผ ๋ฟ์ด๋‹ค.\"\"\"\n", " return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n", " float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n", " float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n", " *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n", " *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n", "\n", "\n", "def prepare(data):\n", " out = []\n", " for df, m in data:\n", " d = clean(df, m[\"weather\"])\n", " X, st = window_feats(d, m[\"plan\"])\n", " out.append({\"m\": m, \"d\": d, \"X\": X, \"starts\": st, \"rule\": any(v.any() for v in rule_flags(d, m[\"plan\"]).values())})\n", " return out\n", "\n", "\n", "# 1) ํ•™์Šต: ๋ผ๋ฒจ ์—†๋Š” ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ(์ˆœ์ฐฐยท์ธก๋Ÿ‰) 120๊ฑด์˜ 30์ดˆ ์ฐฝ\n", "train = prepare(make_dataset(120, seed=21, anomaly_ratio=0.0))\n", "Xtr = np.vstack([f[\"X\"] for f in train])\n", "model = FlightAnomalyModel(q=0.999).fit(Xtr)\n", "print(f\"ํ•™์Šต ์ฐฝ {len(Xtr)}๊ฐœ, ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘ {model.k}๊ฐœ ํฌ๊ธฐ:\", np.bincount(model.km.labels_).tolist())\n", "\n", "# 2) ์‹œํ—˜: ์ •์ƒ 100๊ฑด + ๋ณตํ•ฉ ์ด์ƒ 10์ข… x 10๊ฑด\n", "test = prepare(make_dataset(200, seed=11))\n", "t0 = time.perf_counter()\n", "for f in test:\n", " f[\"ml_score\"] = float(model.score(f[\"X\"]).max())\n", "lat = (time.perf_counter() - t0) / sum(len(f[\"X\"]) for f in test) * 1000\n", "for f in test:\n", " f[\"ml\"] = f[\"ml_score\"] > 1.0\n", " f[\"hybrid\"] = f[\"ml\"] or f[\"rule\"]\n", "r = pd.DataFrame([{\"kind\": f[\"m\"][\"kind\"], \"truth\": f[\"m\"][\"kind\"] in SCENARIOS, \"๊ทœ์น™\": f[\"rule\"], \"ML\": f[\"ml\"], \"๊ทœ์น™+ML\": f[\"hybrid\"]} for f in test])\n", "an, nm = r[r.truth], r[~r.truth]\n", "print(\"\\n๋ฐฉ์‹ ์ด์ƒ ์žฌํ˜„์œจ ์ •์ƒ ๋น„ํ–‰ ์˜ค๊ฒฝ๋ณด์œจ\")\n", "for c in [\"๊ทœ์น™\", \"ML\", \"๊ทœ์น™+ML\"]:\n", " print(f\"{c:7s} {an[c].mean():10.1%} {nm[c].mean():14.1%}\")\n", "rec = an[\"๊ทœ์น™+ML\"].mean()\n", "print(f\"\\n๋ณตํ•ฉ ์œ„ํ˜‘ ํƒ์ง€ ์žฌํ˜„์œจ {rec:.1%} (๋ชฉํ‘œ 85% ์ด์ƒ: {'๋‹ฌ์„ฑ' if rec >= 0.85 else '๋ฏธ๋‹ฌ'}), 30์ดˆ ์ฐฝ๋‹น ์ฒ˜๋ฆฌ {lat:.2f} ms (๋ชฉํ‘œ 200 ms ์ดํ•˜)\")\n", "\n", "# 3) ๋ณตํ•ฉ ์œ„ํ˜‘ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข… ํƒ์ง€ ์‹œํ—˜(์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์žฌํ˜„์œจ 80% ์ด์ƒ์ด๋ฉด ํ†ต๊ณผ)\n", "per = an.groupby(\"kind\")[[\"๊ทœ์น™\", \"ML\", \"๊ทœ์น™+ML\"]].mean()\n", "per[\"ํŒ์ •\"] = np.where(per[\"๊ทœ์น™+ML\"] >= 0.8, \"ํ†ต๊ณผ\", \"๋ฏธํ†ต๊ณผ\")\n", "print(\"\\n[์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์žฌํ˜„์œจ]\")\n", "print(per.round(2).to_string())\n", "print(f\"ํ†ต๊ณผ {int((per['ํŒ์ •'] == 'ํ†ต๊ณผ').sum())}/10์ข…\")\n", "\n", "# 4) ์˜ค๊ฒฝ๋ณด ์œ ๋ฐœ ํŒจํ„ด: ์ •์ƒ์ธ๋ฐ ML์ด ์ด์ƒ์œผ๋กœ ๋ณธ ๋น„ํ–‰์˜ ๊ฐ€์žฅ ํŠ€๋Š” ํŠน์ง•\n", "fa = [f for f in test if f[\"m\"][\"kind\"] not in SCENARIOS and f[\"ml\"]]\n", "print(f\"\\n[์ •์ƒ ๋น„ํ–‰ ์˜ค๊ฒฝ๋ณด {len(fa)}๊ฑด์˜ ์›์ธ ํŠน์ง•]\")\n", "for f in fa[:5]:\n", " w = int(np.argmax(model.score(f[\"X\"])))\n", " z = model.sc.transform(f[\"X\"][w:w + 1])[0]\n", " top = np.argsort(-np.abs(z))[:2]\n", " print(f\" {f['m']['id']} ({f['m']['kind']}, {f['m']['weather']}): \" + \", \".join(f\"{WFEAT[i]} z={z[i]:+.1f}\" for i in top))\n", "print(\"-> ๊ฐ•ํ’ ๋Œํ’ ํ•˜๊ฐ•ยท์ธก๋Ÿ‰ Uํ„ด์ฒ˜๋Ÿผ ์ •์ƒ์ด์ง€๋งŒ ๋“œ๋ฌธ ํŒจํ„ด์ด ์›์ธ์ด๋ฉด ํ•™์Šต ์ž๋ฃŒ์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ํŠน์ง•์„ ์žฌ์„ค๊ณ„ํ•œ๋‹ค.\")\n", "\n", "# 5) ๊ทธ๋ฆผ: ์ด์ƒ ์ ์ˆ˜ ๋ถ„ํฌ์™€ ์€๋ฐ€ ์นจํˆฌ ์˜ˆ์‹œ\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n", "ax[0].hist([f[\"ml_score\"] for f in test if f[\"m\"][\"kind\"] not in SCENARIOS], bins=30, range=(0, 6), alpha=.7, label=\"normal\")\n", "ax[0].hist([f[\"ml_score\"] for f in test if f[\"m\"][\"kind\"] in SCENARIOS], bins=30, range=(0, 6), alpha=.7, label=\"anomalous\")\n", "ax[0].axvline(1.0, color=\"k\", ls=\"--\"); ax[0].set_xlabel(\"max window anomaly score\"); ax[0].set_ylabel(\"flights\"); ax[0].legend()\n", "ax[0].set_title(\"Anomaly score by flight type\")\n", "f = next(f for f in test if f[\"m\"][\"kind\"] == \"infiltration\")\n", "s = model.score(f[\"X\"])\n", "ax[1].plot(f[\"starts\"], s, \"-o\", ms=3, label=\"window score\"); ax[1].axhline(1.0, color=\"k\", ls=\"--\")\n", "ax[1].plot(f[\"d\"][\"t\"], f[\"d\"][\"alt\"] / 30, color=\"tab:gray\", label=\"altitude / 30 m\")\n", "ax[1].set_xlabel(\"time (s)\"); ax[1].set_title(f\"{f['m']['id']} infiltration: low altitude + RC silent\"); ax[1].legend()\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "ํ•™์Šต ์ฐฝ 3360๊ฐœ, ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘ 4๊ฐœ ํฌ๊ธฐ: [1104, 76, 1316, 864]\n", "\n", "๋ฐฉ์‹ ์ด์ƒ ์žฌํ˜„์œจ ์ •์ƒ ๋น„ํ–‰ ์˜ค๊ฒฝ๋ณด์œจ\n", "๊ทœ์น™ 57.0% 13.0%\n", "ML 100.0% 8.0%\n", "๊ทœ์น™+ML 100.0% 16.0%\n", "\n", "๋ณตํ•ฉ ์œ„ํ˜‘ ํƒ์ง€ ์žฌํ˜„์œจ 100.0% (๋ชฉํ‘œ 85% ์ด์ƒ: ๋‹ฌ์„ฑ), 30์ดˆ ์ฐฝ๋‹น ์ฒ˜๋ฆฌ 0.47 ms (๋ชฉํ‘œ 200 ms ์ดํ•˜)\n", "\n", "[์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์žฌํ˜„์œจ]\n", " ๊ทœ์น™ ML ๊ทœ์น™+ML ํŒ์ •\n", "kind \n", "deviation 1.0 1.0 1.0 ํ†ต๊ณผ\n", "dive 1.0 1.0 1.0 ํ†ต๊ณผ\n", "hover 1.0 1.0 1.0 ํ†ต๊ณผ\n", "infiltration 0.0 1.0 1.0 ํ†ต๊ณผ\n", "loiter 0.3 1.0 1.0 ํ†ต๊ณผ\n", "low_level 0.2 1.0 1.0 ํ†ต๊ณผ\n", "nfz_entry 1.0 1.0 1.0 ํ†ต๊ณผ\n", "rc_silent 0.1 1.0 1.0 ํ†ต๊ณผ\n", "sharp_turn 1.0 1.0 1.0 ํ†ต๊ณผ\n", "speed_burst 0.1 1.0 1.0 ํ†ต๊ณผ\n", "ํ†ต๊ณผ 10/10์ข…\n", "\n", "[์ •์ƒ ๋น„ํ–‰ ์˜ค๊ฒฝ๋ณด 8๊ฑด์˜ ์›์ธ ํŠน์ง•]\n", " FL0019 (survey, windy): alt_std z=+6.7, vz_min z=-6.7\n", " FL0039 (survey, windy): alt_std z=+8.9, vz_min z=-8.7\n", " FL0059 (survey, clear): straight z=-5.5, turn_max z=+4.1\n", " FL0088 (normal, windy): alt_std z=+7.6, alt_min z=-2.6\n", " FL0089 (survey, rain): straight z=-5.6, turn_max z=+4.1\n", "-> ๊ฐ•ํ’ ๋Œํ’ ํ•˜๊ฐ•ยท์ธก๋Ÿ‰ Uํ„ด์ฒ˜๋Ÿผ ์ •์ƒ์ด์ง€๋งŒ ๋“œ๋ฌธ ํŒจํ„ด์ด ์›์ธ์ด๋ฉด ํ•™์Šต ์ž๋ฃŒ์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ํŠน์ง•์„ ์žฌ์„ค๊ณ„ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ์šด์šฉ ์ฃผ์ฒด(์ /์•„๊ตฐ/๋ฏผ๊ฐ„) ์„ฑ๊ธ‰ํ•œ ๋‹จ์ • ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ๋ถˆํ™•์‹ค์„ฑ ์ง€ํ‘œ ๋ฐ ์‹ ๋ขฐ๋„ ์—”์ง„ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ํŠน์ • ๊ตญ๊ฐ€๋‚˜ ๋ถ€๋Œ€๋ฅผ ์„ฑ๊ธ‰ํ•˜๊ฒŒ ๋‹จ์ •ํ•˜์ง€ ์•Š๋„๋ก ๋ถ„์„ ๋ถˆํ™•์‹ค์„ฑ ์ง€ํ‘œ์™€ ๋‹ค์ฐจ์› ์‹ ๋ขฐ๋„ ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ•˜๋Š” ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด(์•„๊ตฐยท๋ฏผ๊ฐ„ยท์ ๋Œ€)๋ฅผ ๋‹จ์ •ํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์ฒด๋ณ„ ํ™•๋ฅ , ๋ถˆํ™•์‹ค์„ฑ, ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ์„ ํ•จ๊ป˜ ๋ฐ˜์˜ํ•œ ์‹ ๋ขฐ๋„ ์—”์ง„์„ ๊ฐœ๋ฐœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ณผ๊ฑฐ ๊ฒ€ํ† ๊ฐ€ ๋๋‚œ ๊ฐ€์ƒ ์‚ฌ๋ก€๋กœ ์ฃผ์ฒด๋ณ„ ํ™•๋ฅ  ๋ชจ๋ธ์„ ํ•™์Šตํ•˜๊ณ , ์ •๊ทœํ™” ์—”ํŠธ๋กœํ”ผ๋กœ ๋ถˆํ™•์‹ค์„ฑ์„, ์Šคํ‘ธํ•‘ยท๊ฒฐ์ธก ์ •๋„๋กœ ๋กœ๊ทธ ํ’ˆ์งˆ ์‚ฐ์ถœํ•˜๊ณ  ๋ชจ๋ธ์— ์ ์šฉํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/isr03_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": [ "\"\"\"๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด(์•„๊ตฐยท๋ฏผ๊ฐ„ยท์ ๋Œ€)๋ฅผ ์„ฑ๊ธ‰ํ•˜๊ฒŒ ๋‹จ์ •ํ•˜์ง€ ์•Š๋„๋ก, ์ฃผ์ฒด๋ณ„ ํ™•๋ฅ ๊ณผ ๋ถˆํ™•์‹ค์„ฑ(์—”ํŠธ๋กœํ”ผ)ยท๋ฐ์ดํ„ฐ ํ’ˆ์งˆ์„ ํ•จ๊ป˜ ๊ณ„์‚ฐํ•ด '์ ๋Œ€ ๊ฐ€๋Šฅ์„ฑ ๋“ฑ๊ธ‰(High/Medium/Low)'์„ ๋งค๊ธฐ๋Š” ์‹ ๋ขฐ๋„ ์—”์ง„์„ ๋งŒ๋“ค๊ณ , ๋‹จ์ •ํ˜• ๊ทœ์น™ ๋Œ€๋น„ ์˜ค๊ฒฝ๋ณด ๊ฐ์ถ•๊ณผ ์ ์ˆ˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import math\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.cluster import KMeans\n", "from sklearn.ensemble import IsolationForest\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.metrics import roc_auc_score\n", "from sklearn.pipeline import make_pipeline\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "# ---- ๊ตฌ๊ฐ„(30์ดˆ ์ฐฝ) ํŠน์ง• + ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n", " \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n", "\n", "\n", "def window_feats(d, plan, win=30, step=10):\n", " \"\"\"30์ดˆ ์ฐฝ์„ 10์ดˆ์”ฉ ์˜ฎ๊ธฐ๋ฉฐ ๊ณ ๋„ยท์†๋„ยท์„ ํšŒยท์ง์ง„์„ฑยท์กฐ์ข…์‹ ํ˜ธยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผยท๊ฒฝ๋กœ ์ดํƒˆ ํŠน์ง•์„ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n", " spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " vz = np.diff(alt, prepend=alt[0])\n", " nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n", " xt = xtrack(x, y, plan)\n", " step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n", " rows, starts = [], []\n", " for s in range(0, len(d) - win + 1, step):\n", " e = s + win\n", " path = step_len[s + 1:e].sum()\n", " rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n", " spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n", " rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n", " starts.append(int(d[\"t\"].iloc[s]))\n", " return np.array(rows), starts\n", "\n", "\n", "class FlightAnomalyModel:\n", " \"\"\"ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ(๋ผ๋ฒจ ์—†์Œ)๋กœ ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ˆฒ(IsolationForest)์„ ํ•™์Šตํ•˜๊ณ ,\n", " ๋‘ ์ ์ˆ˜๋ฅผ ํ•™์Šต ๊ตฌ๊ฐ„ ๋ถ„์œ„์ˆ˜๋กœ ๋‚˜๋ˆ  1.0์„ ๋„˜์œผ๋ฉด 'ํ†ต์ƒ ํŒจํ„ด์—์„œ ๋ฒ—์–ด๋‚จ'์œผ๋กœ ๋ณธ๋‹ค.\"\"\"\n", "\n", " def __init__(self, q=0.999, k=4):\n", " self.q, self.k = q, k\n", "\n", " def fit(self, X):\n", " self.sc = StandardScaler().fit(X)\n", " Z = self.sc.transform(X)\n", " self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n", " self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n", " self.set_q(self.q, X)\n", " return self\n", "\n", " def _raw(self, X):\n", " Z = self.sc.transform(X)\n", " return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n", "\n", " def set_q(self, q, X):\n", " a, b = self._raw(X)\n", " self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n", "\n", " def score(self, X):\n", " a, b = self._raw(X)\n", " return np.maximum(a / self.t_if, b / self.t_km)\n", "\n", "\n", "AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n", " \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n", "\n", "\n", "def attr_features(d, m, ml_score):\n", " \"\"\"์šด์šฉ ์ฃผ์ฒด ์ถ”์ •์šฉ ๋น„ํ–‰ ๋‹จ์œ„ ํŠน์ง•. ๊ธฐ์ฒดยทํŽŒ์›จ์–ด ์ •๋ณด๋Š” ์ƒ์šฉ ๊ธฐ์ฒด๊ฐ€ ๋ชจ๋“  ์ฃผ์ฒด์— ์“ฐ์ด๋ฏ€๋กœ ์•ฝํ•œ ๋‹จ์„œ์ผ ๋ฟ์ด๋‹ค.\"\"\"\n", " return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n", " float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n", " float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n", " *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n", " *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n", "\n", "P_HIGH, H_MAX, P_MED = 0.60, 0.80, 0.25 # ๋“ฑ๊ธ‰ ๊ธฐ์ค€(์ ๋Œ€ ํ™•๋ฅ , ์ตœ๋Œ€ ๋ถˆํ™•์‹ค์„ฑ, ๋ณด๋ฅ˜ ํ•˜ํ•œ)\n", "\n", "\n", "def build(data, model, rng, p_corrupt=0.25):\n", " rows = []\n", " for df, m in data:\n", " noisy = rng.random() < p_corrupt\n", " d = clean(corrupt(df, rng) if noisy else df, m[\"weather\"])\n", " X, _ = window_feats(d, m[\"plan\"])\n", " s = float(model.score(X).max())\n", " flag = s > 1.0 or any(v.any() for v in rule_flags(d, m[\"plan\"]).values())\n", " rows.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"op\": m[\"operator\"], \"flag\": flag,\n", " \"quality_ok\": d.attrs[\"spoof_pts\"] < 10 and d.attrs[\"missing\"] < 15, \"f\": attr_features(d, m, s)})\n", " return rows\n", "\n", "\n", "def grade(p, H, q_ok):\n", " if p >= P_HIGH and H <= H_MAX and q_ok:\n", " return \"High\"\n", " return \"Medium\" if p >= P_MED else \"Low\"\n", "\n", "\n", "def assess(eng, rows, noise=None, rng=None):\n", " X = np.array([r[\"f\"] for r in rows])\n", " if noise:\n", " X = X * (1 + rng.normal(0, noise, X.shape))\n", " P = eng.predict_proba(X)\n", " H = -(P * np.log(np.clip(P, 1e-9, 1))).sum(1) / np.log(3)\n", " ph = P[:, list(eng.classes_).index(\"hostile\")]\n", " return ph, H, [grade(p, h, r[\"quality_ok\"]) for p, h, r in zip(ph, H, rows)]\n", "\n", "\n", "rng = np.random.default_rng(5)\n", "normal_X = np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in make_dataset(120, 21, anomaly_ratio=0.0)])\n", "det = FlightAnomalyModel().fit(normal_X)\n", "train = build(make_dataset(240, seed=31), det, rng) # ๊ณผ๊ฑฐ ๊ฒ€ํ† ๊ฐ€ ๋๋‚œ ์‚ฌ๋ก€(๋ผ๋ฒจ ์žˆ์Œ)๋ผ๊ณ  ๊ฐ€์ •\n", "test = build(make_dataset(200, seed=11), det, rng)\n", "eng = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, C=0.5))\n", "eng.fit([r[\"f\"] for r in train], [r[\"op\"] for r in train])\n", "ph, H, G = assess(eng, test)\n", "y = np.array([r[\"op\"] == \"hostile\" for r in test])\n", "\n", "# 1) ๋‹จ์ •ํ˜• ๊ทœ์น™(์ด์ƒ ๋น„ํ–‰์ด๋ฉด '์ ') vs ์‹ ๋ขฐ๋„ ์—”์ง„(High๋งŒ '์ ๋Œ€ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ')\n", "flag = np.array([r[\"flag\"] for r in test]); high = np.array([g == \"High\" for g in G]); med = np.array([g == \"Medium\" for g in G])\n", "far0 = (~y & flag).sum() / flag.sum(); far1 = (~y & high).sum() / max(high.sum(), 1)\n", "print(\"๋ฐฉ์‹ ์ ๋Œ€ ์ง€๋ชฉ ๊ทธ์ค‘ ๋น„์ ๋Œ€(์˜ค๊ฒฝ๋ณด) ์˜ค๊ฒฝ๋ณด์œจ\")\n", "print(f\"๋‹จ์ •ํ˜• ๊ทœ์น™(์ด์ƒ=์ ) {flag.sum():6d} {(~y & flag).sum():14d} {far0:.1%}\")\n", "print(f\"์‹ ๋ขฐ๋„ ์—”์ง„(High ๋“ฑ๊ธ‰) {high.sum():6d} {(~y & high).sum():14d} {far1:.1%}\")\n", "cut = 1 - far1 / far0\n", "print(f\"์˜ค๊ฒฝ๋ณด์œจ ๊ฐ์ถ• {cut:.1%} (๋ชฉํ‘œ 40% ์ด์ƒ: {'๋‹ฌ์„ฑ' if cut >= 0.4 else '๋ฏธ๋‹ฌ'})\")\n", "print(f\"์ ๋Œ€ ๋น„ํ–‰ {y.sum()}๊ฑด ์ค‘ High+Medium(๊ฒ€ํ†  ๋Œ€์ƒ) {(y & (high | med)).sum()}๊ฑด, Low๋กœ ๋ฐ€๋ฆฐ ์ ๋Œ€ {(y & ~high & ~med).sum()}๊ฑด\")\n", "print(f\"ROC-AUC(์ ๋Œ€ ํ™•๋ฅ ) {roc_auc_score(y, ph):.3f}\")\n", "\n", "# 2) ๋“ฑ๊ธ‰ ๋ถ„ํฌ์™€ ์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„\n", "print(\"\\n[์‹ค์ œ ์šด์šฉ ์ฃผ์ฒด๋ณ„ ๋“ฑ๊ธ‰ ๋ถ„ํฌ]\")\n", "print(pd.crosstab(pd.Series([r[\"op\"] for r in test], name=\"์‹ค์ œ\"), pd.Series(G, name=\"๋“ฑ๊ธ‰\")))\n", "print(\"\\nHigh ๊ธฐ์ค€ High ์ˆ˜ ์˜ค๊ฒฝ๋ณด์œจ ์ ๋Œ€ ํฌ์ฐฉ๋ฅ \")\n", "for t in [0.5, 0.6, 0.7, 0.8]:\n", " hh = (ph >= t) & (H <= H_MAX) & np.array([r[\"quality_ok\"] for r in test])\n", " print(f\" {t:.1f} {hh.sum():5d} {(~y & hh).sum() / max(hh.sum(), 1):7.1%} {(y & hh).sum() / y.sum():8.1%}\")\n", "\n", "# 3) ์ผ๊ด€์„ฑ: ์ž…๋ ฅ ํŠน์ง•์— +-2% ์ธก์ • ์žก์Œ์„ 20๋ฒˆ ๋„ฃ์–ด๋„ ๊ฐ™์€ ๋“ฑ๊ธ‰์ด ๋‚˜์˜ค๋Š” ๋น„์œจ\n", "same = np.mean([np.mean([a == b for a, b in zip(G, assess(eng, test, 0.02, rng)[2])]) for _ in range(20)])\n", "print(f\"\\n์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰ ์‚ฐ์ถœ ์ผ๊ด€์„ฑ {same:.1%} (๋ชฉํ‘œ 90% ์ด์ƒ: {'๋‹ฌ์„ฑ' if same >= 0.9 else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# 4) ํŒ๋‹จ ๋ฌธ๊ตฌ: 'ํ™•์ •' ํ‘œํ˜„ ๊ธˆ์ง€, ํ™•๋ฅ ยท๋ถˆํ™•์‹ค์„ฑยท๊ทผ๊ฑฐยท๊ฒ€ํ†  ํ•„์š”๋ฅผ ํ•จ๊ป˜ ์ ๋Š”๋‹ค\n", "sc, lr = eng[0], eng[-1]\n", "ci = list(lr.classes_).index(\"hostile\")\n", "print(\"\\n[๋ถ„์„ ์˜๊ฒฌ ๋ฌธ๊ตฌ ์˜ˆ์‹œ]\")\n", "lines = []\n", "for i in list(np.argsort(-ph)[:2]) + [int(np.argmin(np.abs(ph - 0.5)))]:\n", " z = (np.array(test[i][\"f\"]) - sc.mean_) / sc.scale_ * lr.coef_[ci]\n", " why = \", \".join(AFEAT[j] for j in np.argsort(-z)[:2])\n", " q = \"\" if test[i][\"quality_ok\"] else \", ๋กœ๊ทธ ํ’ˆ์งˆ ์ €ํ•˜(์Šคํ‘ธํ•‘ยท๊ฒฐ์ธก)๋กœ ๋“ฑ๊ธ‰ ํ•˜ํ–ฅ\"\n", " lines.append(f\"{test[i]['id']}: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ {G[i]}(p={ph[i]:.2f}, ๋ถˆํ™•์‹ค์„ฑ {H[i]:.2f}{q}), ๊ทผ๊ฑฐ {why} -> ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ํ•„์š”\")\n", "print(\"\\n\".join(\" \" + s for s in lines))\n", "print(\"๊ธˆ์ง€ ํ‘œํ˜„('ํ™•์ •', '์†Œ์† ๋ถ€๋Œ€') ํฌํ•จ ๋ฌธ๊ตฌ:\", sum((\"ํ™•์ •\" in s) or (\"์†Œ์† ๋ถ€๋Œ€\" in s) for s in lines), \"๊ฑด\")\n", "\n", "fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n", "for op, c in [(\"friendly\", \"tab:blue\"), (\"civil\", \"tab:green\"), (\"hostile\", \"tab:red\")]:\n", " sel = np.array([r[\"op\"] == op for r in test])\n", " ax[0].scatter(ph[sel], H[sel], s=18, color=c, label=op, alpha=.7)\n", "ax[0].axvline(P_HIGH, ls=\"--\", c=\"k\"); ax[0].axhline(H_MAX, ls=\"--\", c=\"k\")\n", "ax[0].set_xlabel(\"P(hostile)\"); ax[0].set_ylabel(\"uncertainty (normalized entropy)\"); ax[0].legend(); ax[0].set_title(\"Attribution score vs uncertainty\")\n", "pd.Series(G).value_counts().reindex([\"High\", \"Medium\", \"Low\"]).plot.bar(ax=ax[1], color=[\"tab:red\", \"tab:orange\", \"tab:gray\"])\n", "ax[1].set_title(\"Grades (High = priority review, not a conclusion)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "๋ฐฉ์‹ ์ ๋Œ€ ์ง€๋ชฉ ๊ทธ์ค‘ ๋น„์ ๋Œ€(์˜ค๊ฒฝ๋ณด) ์˜ค๊ฒฝ๋ณด์œจ\n", "๋‹จ์ •ํ˜• ๊ทœ์น™(์ด์ƒ=์ ) 116 71 61.2%\n", "์‹ ๋ขฐ๋„ ์—”์ง„(High ๋“ฑ๊ธ‰) 16 4 25.0%\n", "์˜ค๊ฒฝ๋ณด์œจ ๊ฐ์ถ• 59.2% (๋ชฉํ‘œ 40% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "์ ๋Œ€ ๋น„ํ–‰ 48๊ฑด ์ค‘ High+Medium(๊ฒ€ํ†  ๋Œ€์ƒ) 34๊ฑด, Low๋กœ ๋ฐ€๋ฆฐ ์ ๋Œ€ 14๊ฑด\n", "ROC-AUC(์ ๋Œ€ ํ™•๋ฅ ) 0.803\n", "\n", "[์‹ค์ œ ์šด์šฉ ์ฃผ์ฒด๋ณ„ ๋“ฑ๊ธ‰ ๋ถ„ํฌ]\n", "๋“ฑ๊ธ‰ High Low Medium\n", "์‹ค์ œ \n", "civil 0 69 18\n", "friendly 4 43 18\n", "hostile 12 14 22\n", "\n", "High ๊ธฐ์ค€ High ์ˆ˜ ์˜ค๊ฒฝ๋ณด์œจ ์ ๋Œ€ ํฌ์ฐฉ๋ฅ \n", " 0.5 17 29.4% 25.0%\n", " 0.6 16 25.0% 25.0%\n", " 0.7 15 26.7% 22.9%\n", " 0.8 10 40.0% 12.5%\n", "\n", "์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰ ์‚ฐ์ถœ ์ผ๊ด€์„ฑ 99.1% (๋ชฉํ‘œ 90% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "\n", "[๋ถ„์„ ์˜๊ฒฌ ๋ฌธ๊ตฌ ์˜ˆ์‹œ]\n", " FL0112: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ High(p=0.96, ๋ถˆํ™•์‹ค์„ฑ 0.16), ๊ทผ๊ฑฐ rc_weak, fw_unknown -> ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ํ•„์š”\n", " FL0032: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ High(p=0.96, ๋ถˆํ™•์‹ค์„ฑ 0.16), ๊ทผ๊ฑฐ rc_weak, fw_unknown -> ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ํ•„์š”\n", " FL0180: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ Medium(p=0.48, ๋ถˆํ™•์‹ค์„ฑ 0.96), ๊ทผ๊ฑฐ fw_stock, fw_custom -> ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ํ•„์š”\n", "๊ธˆ์ง€ ํ‘œํ˜„('ํ™•์ •', '์†Œ์† ๋ถ€๋Œ€') ํฌํ•จ ๋ฌธ๊ตฌ: 0 ๊ฑด\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ๋“œ๋ก  ๋น„ํ–‰ ๊ถค์  ์‹œ๊ฐํ™” ๋ฐ ์ด์ƒ ํŒจํ„ด ํ•˜์ด๋ผ์ดํŠธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋“œ๋ก ์˜ 3D ๋น„ํ–‰ ๊ถค์ , ์กฐ์ข… ์‹ ํ˜ธ ์ด์ƒ ๊ตฌ๊ฐ„, ์˜์‹ฌ ๊ตฌ์—ญ ์ฒด๋ฅ˜ ์‹œ๊ฐ„์„ ์ง€๋„์ƒ์— ์‹œ๊ฐํ™”ํ•˜๋Š” ๋“œ๋ก  ์ •๋ณด ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌ์ถ•ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋“œ๋ก ์˜ 3D ๋น„ํ–‰ ๊ถค์ , ์กฐ์ข…์‹ ํ˜ธ ์ด์ƒ ๊ตฌ๊ฐ„, ์˜์‹ฌ ๊ตฌ์—ญ ์ฒด๋ฅ˜ ์‹œ๊ฐ„, ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผ์„ ํ•œ ํ™”๋ฉด์— ๋ณด์—ฌ ์ฃผ๋Š” ๋“œ๋ก  ์ •๋ณด ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌ์ถ•ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์—ฌ๋Ÿฌ ๊ธฐ์ฒด์˜ ๋น„ํ–‰๋กœ๊ทธ๋ฅผ 1์ดˆ ์ฃผ๊ธฐ๋กœ ๋ฐ›์•„ ์ตœ๊ทผ 10์ดˆ ์ฐฝ์œผ๋กœ ๊ฒฝ๋ณด ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ํ๋ฆ„์„ ๋ชจ์˜ํ•˜๊ณ  ์ฒ˜๋ฆฌ์‹œ๊ฐ„์„ ์žฐ๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/isr03_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": [ "\"\"\"๋“œ๋ก  3D ๋น„ํ–‰ ๊ถค์ , ์กฐ์ข…์‹ ํ˜ธ ์ด์ƒ ๊ตฌ๊ฐ„, ์˜์‹ฌ ๊ตฌ์—ญ ์ฒด๋ฅ˜ ์‹œ๊ฐ„, ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผ์„ ํ•œ ํ™”๋ฉด์— ๋ณด์—ฌ ์ฃผ๋Š” ๋“œ๋ก  ์ •๋ณด ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ , 1์ดˆ ์ฃผ๊ธฐ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  ์ฒ˜๋ฆฌ์‹œ๊ฐ„๊ณผ ํ™”๋ฉด ๋ Œ๋”๋ง ์‹œ๊ฐ„์„ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import math\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "\n", "def highlight(d, plan):\n", " \"\"\"๊ฐ•์กฐ ํ‘œ์‹œํ•  ๊ธฐ๋ก: ๊ทœ์น™ ํƒ์ง€ + ์ €๊ณ ๋„(60m ๋ฏธ๋งŒ) + ์กฐ์ข…์‹ ํ˜ธ ์•ฝํ™”(25 ๋ฏธ๋งŒ) + ๊ณ ์†(20m/s ์ดˆ๊ณผ).\"\"\"\n", " f = rule_flags(d, plan)\n", " f[\"์ €๊ณ ๋„\"] = (d[\"alt\"] < 60).to_numpy()\n", " f[\"์กฐ์ข…์‹ ํ˜ธ์•ฝํ™”\"] = (d[\"rc\"] < 25).to_numpy()\n", " f[\"๊ณ ์†\"] = (d[\"speed\"] > 20).to_numpy()\n", " return f\n", "\n", "\n", "def dwell_seconds(d, radius=300, span=60):\n", " \"\"\"60์ดˆ ๋™์•ˆ 300m ์•ˆ์— ๋จธ๋ฌธ ์‹œ์  ์ˆ˜(์ฒด๋ฅ˜ ์‹œ๊ฐ„, ์ดˆ).\"\"\"\n", " x, y = d[\"x\"].to_numpy(), d[\"y\"].to_numpy()\n", " return int(sum(np.hypot(x[i:i + span] - x[i], y[i:i + span] - y[i]).max() < radius for i in range(0, len(x) - span)))\n", "\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ถ„์„ ๋Œ€์ƒ ๋น„ํ–‰๋กœ๊ทธ 6๊ฑด\n", "pool = make_dataset(40, seed=11)\n", "want = [\"infiltration\", \"loiter\", \"nfz_entry\", \"rc_silent\", \"hover\", \"normal\"]\n", "sel = [next((df, m) for df, m in pool if m[\"kind\"] == k) for k in want]\n", "streams = [(df, m) for df, m in sel]\n", "\n", "# 1) 1์ดˆ ์ฃผ๊ธฐ ์ŠคํŠธ๋ฆผ ๊ฐฑ์‹  ๋ชจ์˜: ๋งค ์ดˆ 6๊ฐœ ๊ธฐ์ฒด์˜ ์ƒˆ ๊ธฐ๋ก 1๊ฑด์”ฉ ๋ฐ›์•„ ์ตœ๊ทผ 10์ดˆ ์ฐฝ์œผ๋กœ ๊ฒฝ๋ณด ์ƒํƒœ ๊ฐฑ์‹ \n", "upd, alerts = [], 0\n", "for t in range(10, 300):\n", " t0 = time.perf_counter()\n", " for df, m in streams:\n", " w = df.iloc[t - 9:t + 1]\n", " nfz_in = bool((np.hypot(w[\"x\"] - NFZ[\"x\"], w[\"y\"] - NFZ[\"y\"]) < NFZ[\"r\"]).any())\n", " alerts += int(nfz_in or ((w[\"alt\"] < 60).all() and (w[\"rc\"] < 25).all()) or (w[\"speed\"] > 20).any())\n", " upd.append((time.perf_counter() - t0) * 1000)\n", "print(f\"๋ฐ์ดํ„ฐ ๊ฐฑ์‹  {len(upd)}ํšŒ, ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ํ‰๊ท  {np.mean(upd):.2f} ms, ์ตœ๋Œ€ {np.max(upd):.2f} ms (๋ชฉํ‘œ: 1์ดˆ ์ฃผ๊ธฐ ์•ˆ์—์„œ 100 ms ์ด๋‚ด), ๋ˆ„์  ๊ฒฝ๋ณด ํ‘œ์‹œ {alerts}ํšŒ\")\n", "\n", "# 2) ์ƒํƒœ ์š”์•ฝํ‘œ\n", "rows = []\n", "for df, m in streams:\n", " d = clean(df, m[\"weather\"]); f = highlight(d, m[\"plan\"])\n", " dw = dwell_seconds(d)\n", " hit = [k for k, v in f.items() if v.any()] + ([\"์žฅ์‹œ๊ฐ„์ฒด๋ฅ˜\"] if dw > 30 else [])\n", " rows.append({\"ID\": m[\"id\"], \"์ตœ์ €๊ณ ๋„(m)\": int(d[\"alt\"].min()), \"์ตœ๋Œ€์†๋„(m/s)\": round(float(d[\"speed\"].max()), 1),\n", " \"์‹ ํ˜ธ์•ฝํ™”(์ดˆ)\": int(f[\"์กฐ์ข…์‹ ํ˜ธ์•ฝํ™”\"].sum()), \"์ฒด๋ฅ˜(์ดˆ)\": dw,\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ ์ตœ๊ทผ์ ‘(m)\": int(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min()),\n", " \"๊ฐ•์กฐ ํ•ญ๋ชฉ\": \",\".join(hit) or \"-\", \"๊ธด๊ธ‰\": bool(f[\"๋น„์ธ๊ฐ€๊ตฌ์—ญ\"].any() or (f[\"์ €๊ณ ๋„\"].any() and f[\"์กฐ์ข…์‹ ํ˜ธ์•ฝํ™”\"].any()))})\n", " m[\"d\"], m[\"f\"] = d, f\n", "table = pd.DataFrame(rows).sort_values(\"๊ธด๊ธ‰\", ascending=False)\n", "print(\"\\n[๋“œ๋ก  ๋น„ํ–‰ ์ด์ƒ ํ˜„ํ™ฉ: ๊ธด๊ธ‰ ์šฐ์„ ]\")\n", "print(table.to_string(index=False))\n", "\n", "# 3) ๋Œ€์‹œ๋ณด๋“œ(์•ผ๊ฐ„ ์šด์šฉ์šฉ ์–ด๋‘์šด ํ™”๋ฉด)\n", "plt.style.use(\"dark_background\")\n", "fig = plt.figure(figsize=(14, 9))\n", "ax3 = fig.add_subplot(2, 2, 1, projection=\"3d\")\n", "axm, axr, axd = fig.add_subplot(2, 2, 2), fig.add_subplot(2, 2, 3), fig.add_subplot(2, 2, 4)\n", "colors = plt.cm.tab10(np.arange(len(streams)))\n", "for c, (df, m) in zip(colors, streams):\n", " d, f = m[\"d\"], m[\"f\"]\n", " hl = np.any(list(f.values()), axis=0)\n", " ax3.plot(d[\"x\"], d[\"y\"], d[\"alt\"], color=c, lw=1)\n", " ax3.scatter(d[\"x\"][hl], d[\"y\"][hl], d[\"alt\"][hl], color=\"red\", s=4)\n", " axm.plot(d[\"x\"], d[\"y\"], color=c, lw=1, label=m[\"id\"])\n", " axm.plot(d[\"x\"][hl], d[\"y\"][hl], \".\", color=\"red\", ms=2)\n", " axr.plot(d[\"t\"], d[\"rc\"], color=c, lw=1)\n", "ax3.set_title(\"3D flight paths (red = highlighted)\"); ax3.set_xlabel(\"E (m)\"); ax3.set_ylabel(\"N (m)\"); ax3.set_zlabel(\"alt (m)\")\n", "for z, col, lab in [(NFZ, \"red\", \"no-fly zone (virtual)\"), (WATCH, \"orange\", \"watch area (virtual)\")]:\n", " axm.add_patch(plt.Circle((z[\"x\"], z[\"y\"]), z[\"r\"], color=col, alpha=0.2, label=lab))\n", "axm.set_aspect(\"equal\"); axm.legend(fontsize=7, loc=\"lower left\"); axm.set_title(\"Map view\"); axm.grid(alpha=.2)\n", "axr.axhspan(0, 25, color=\"red\", alpha=0.2, label=\"weak / lost RC link\")\n", "axr.set_title(\"RC link quality timeline\"); axr.set_xlabel(\"time (s)\"); axr.set_ylabel(\"RC link (0-100)\"); axr.legend(fontsize=8)\n", "axd.barh(table[\"ID\"], table[\"์ฒด๋ฅ˜(์ดˆ)\"], color=[\"red\" if u else \"deepskyblue\" for u in table[\"๊ธด๊ธ‰\"]])\n", "axd.set_title(\"Dwell time within 300 m (red = urgent)\"); axd.set_xlabel(\"seconds\")\n", "fig.suptitle(\"ISR03 drone log analysis dashboard - training data only\")\n", "t0 = time.perf_counter(); fig.canvas.draw(); draw_ms = (time.perf_counter() - t0) * 1000\n", "plt.tight_layout(); plt.show()\n", "print(f\"\\nํ™”๋ฉด ๋ Œ๋”๋ง {draw_ms:.0f} ms (๊ฐฑ์‹  ์ฃผ๊ธฐ 1์ดˆ ์ด๋‚ด: {'๋‹ฌ์„ฑ' if draw_ms < 1000 else '๋ฏธ๋‹ฌ'}), ์‹œ๊ฐํ™” ๊ตฌ์„ฑ์š”์†Œ 4์ข…(3D ๊ถค์ ยท์ง€๋„ยท์กฐ์ข…์‹ ํ˜ธ ์‹œ๊ณ„์—ดยท์ฒด๋ฅ˜ ์‹œ๊ฐ„)\")\n", "print(\"๋Œ€์‹œ๋ณด๋“œ์˜ '๊ธด๊ธ‰'์€ ๋ถ„์„ ์šฐ์„ ์ˆœ์œ„ ํ‘œ์‹œ์ด๋ฉฐ ์šด์šฉ ์ฃผ์ฒด ํŒ๋‹จ์ด ์•„๋‹ˆ๋‹ค. ํŒ๋‹จ์€ ๋ฏธ์…˜ 7์˜ ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์นœ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "๋ฐ์ดํ„ฐ ๊ฐฑ์‹  290ํšŒ, ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ํ‰๊ท  3.92 ms, ์ตœ๋Œ€ 55.84 ms (๋ชฉํ‘œ: 1์ดˆ ์ฃผ๊ธฐ ์•ˆ์—์„œ 100 ms ์ด๋‚ด), ๋ˆ„์  ๊ฒฝ๋ณด ํ‘œ์‹œ 428ํšŒ\n", "\n", "[๋“œ๋ก  ๋น„ํ–‰ ์ด์ƒ ํ˜„ํ™ฉ: ๊ธด๊ธ‰ ์šฐ์„ ]\n", " ID ์ตœ์ €๊ณ ๋„(m) ์ตœ๋Œ€์†๋„(m/s) ์‹ ํ˜ธ์•ฝํ™”(์ดˆ) ์ฒด๋ฅ˜(์ดˆ) ๋น„์ธ๊ฐ€๊ตฌ์—ญ ์ตœ๊ทผ์ ‘(m) ๊ฐ•์กฐ ํ•ญ๋ชฉ ๊ธด๊ธ‰\n", "FL0012 26 10.7 280 0 1068 ์ €๊ณ ๋„,์กฐ์ข…์‹ ํ˜ธ์•ฝํ™” True\n", "FL0004 71 13.3 0 18 2 ๊ฒฝ๋กœ์ดํƒˆ,๋น„์ธ๊ฐ€๊ตฌ์—ญ True\n", "FL0013 103 16.9 0 72 3331 ์žฅ์‹œ๊ฐ„์ฒด๋ฅ˜ False\n", "FL0011 117 15.8 120 0 5822 ์กฐ์ข…์‹ ํ˜ธ์•ฝํ™” False\n", "FL0001 101 17.9 0 41 3575 ํ˜ธ๋ฒ„๋ง,์žฅ์‹œ๊ฐ„์ฒด๋ฅ˜ False\n", "FL0005 118 16.0 0 0 4709 - False\n", "\n", "ํ™”๋ฉด ๋ Œ๋”๋ง 335 ms (๊ฐฑ์‹  ์ฃผ๊ธฐ 1์ดˆ ์ด๋‚ด: ๋‹ฌ์„ฑ), ์‹œ๊ฐํ™” ๊ตฌ์„ฑ์š”์†Œ 4์ข…(3D ๊ถค์ ยท์ง€๋„ยท์กฐ์ข…์‹ ํ˜ธ ์‹œ๊ณ„์—ดยท์ฒด๋ฅ˜ ์‹œ๊ฐ„)\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", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 5๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/isr03_approval_workflow.py ๋ฐ docs/isr03_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": [ "\"\"\"๋“œ๋ก  ์ด์ƒ ๋น„ํ–‰ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ •๋ณด๋ถ„์„๊ด€์ด ์ถœ์ฒ˜ ํ™•์ธ๊ณผ ๋กœ๊ทธ ๋ฌด๊ฒฐ์„ฑ(ํ•ด์‹œ) ๊ฒ€์ฆ์„ ๊ฑฐ์ณ ์ฑ„ํƒยท๋ณด๋ฅ˜ยท๋ฐ˜๋ คํ•˜๋Š” ์ธ๊ฐ„ ๊ฐœ์ž…(HITL) ์‹ฌ์ธต ๊ฒ€ํ†  ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์šด์šฉ ์ฃผ์ฒด ๊ด€๋ จ ์˜๊ฒฌ์€ 2์ธ ๊ต์ฐจ ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณ์•ผ ์ฑ„ํƒ๋˜๋ฉฐ, ๋ชจ๋“  ๊ฒฐ์ •์€ ์œ„ยท๋ณ€์กฐ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•œ ๊ฐ์‚ฌ ๋กœ๊ทธ์— ๋‚จ๋Š”๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import time\n", "import pandas as pd\n", "\n", "ACTIONS = {\"ADOPT\": \"์ฑ„ํƒ(๋ถ„์„ ์˜๊ฒฌ์œผ๋กœ ๋ณด๊ณ )\", \"HOLD\": \"๋ณด๋ฅ˜(์ถ”๊ฐ€ ์ถœ์ฒ˜ ํ•„์š”)\", \"REJECT\": \"๋ฐ˜๋ ค(๊ทผ๊ฑฐ ๋ถ€์กฑยท์˜คํƒ)\"}\n", "PRIORITY = {\"High\": 0, \"Medium\": 1, \"Low\": 2}\n", "HOLD_LIMIT_S = 1800\n", "\n", "\n", "class WorkflowError(ValueError):\n", " pass\n", "\n", "\n", "class ReviewQueue:\n", " def __init__(self, clock=time.time):\n", " self.items, self.log, self.clock = {}, [], 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(), 3)}\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, fid, grade, p, claim, log_bytes):\n", " \"\"\"๋ถ„์„ ์—”์ง„ ๊ฒฐ๊ณผ ๋“ฑ๋ก. ํ•ญ์ƒ PENDING์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉฐ ์›๋ณธ ๋กœ๊ทธ ํ•ด์‹œ๋ฅผ ํ•จ๊ป˜ ๋ณด๊ด€ํ•œ๋‹ค.\"\"\"\n", " self.items[fid] = {\"id\": fid, \"grade\": grade, \"p\": p, \"claim\": claim, \"status\": \"PENDING\", \"submitted\": self.clock(),\n", " \"hash\": hashlib.sha256(log_bytes).hexdigest(), \"source_ok\": False, \"integrity_ok\": None,\n", " \"first\": None, \"escalated\": False, \"hold_since\": None}\n", " self._record({\"event\": \"SUBMIT\", \"id\": fid, \"grade\": grade, \"by\": \"ENGINE\"})\n", "\n", " def check_source(self, fid, analyst, note):\n", " self.items[fid][\"source_ok\"] = True\n", " self._record({\"event\": \"SOURCE_CHECK\", \"id\": fid, \"analyst\": analyst, \"note\": note})\n", "\n", " def check_integrity(self, fid, analyst, log_bytes):\n", " ok = hashlib.sha256(log_bytes).hexdigest() == self.items[fid][\"hash\"]\n", " self.items[fid][\"integrity_ok\"] = ok\n", " self._record({\"event\": \"INTEGRITY_CHECK\", \"id\": fid, \"analyst\": analyst, \"result\": ok})\n", " return ok\n", "\n", " def pending(self):\n", " todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"HOLD\", \"ADOPT_1ST\")]\n", " return sorted(todo, key=lambda i: (not i[\"escalated\"], PRIORITY[i[\"grade\"]], -i[\"p\"]))\n", "\n", " def decide(self, fid, action, analyst, reason):\n", " it = self.items.get(fid)\n", " if action not in ACTIONS:\n", " raise WorkflowError(f\"ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์กฐ์น˜: {action}\")\n", " if not analyst or analyst.upper() in (\"AI\", \"ENGINE\", \"SYSTEM\"):\n", " raise WorkflowError(\"์‚ฌ๋žŒ ๋ถ„์„๊ด€ ID ํ•„์š”(์ž๋™ ๊ฒฐ์ • ๊ธˆ์ง€)\")\n", " if it is None or it[\"status\"] in (\"ADOPTED\", \"REJECTED\"):\n", " raise WorkflowError(\"๋Œ€๊ธฐ์—ด์— ์—†๊ฑฐ๋‚˜ ์ด๋ฏธ ์ข…๊ฒฐ๋œ ๊ฑด\")\n", " attrib = \"์šด์šฉ ์ฃผ์ฒด\" in it[\"claim\"]\n", " if len(reason.strip()) < (20 if attrib and action == \"ADOPT\" else 8):\n", " raise WorkflowError(\"ํŒ๋‹จ ๊ทผ๊ฑฐ ๋ถ€์กฑ(์šด์šฉ ์ฃผ์ฒด ์˜๊ฒฌ ์ฑ„ํƒ์€ 20์ž ์ด์ƒ)\")\n", " if action == \"ADOPT\":\n", " if not it[\"source_ok\"] or it[\"integrity_ok\"] is not True:\n", " raise WorkflowError(\"์ถœ์ฒ˜ ํ™•์ธยท๋กœ๊ทธ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ ์ „์—๋Š” ์ฑ„ํƒ ๋ถˆ๊ฐ€\")\n", " if attrib and it[\"first\"] is None:\n", " it[\"first\"], it[\"status\"] = analyst, \"ADOPT_1ST\"\n", " self._record({\"event\": \"ADOPT_1ST\", \"id\": fid, \"analyst\": analyst, \"reason\": reason})\n", " return\n", " if attrib and it[\"first\"] == analyst:\n", " raise WorkflowError(\"2์ธ ๊ต์ฐจ ๊ฒ€ํ† : 1์ฐจ ๊ฒ€ํ† ์ž์™€ ๋‹ค๋ฅธ ๋ถ„์„๊ด€์ด ํ•„์š”\")\n", " it[\"status\"] = {\"ADOPT\": \"ADOPTED\", \"HOLD\": \"HOLD\", \"REJECT\": \"REJECTED\"}[action]\n", " it[\"hold_since\"] = self.clock() if action == \"HOLD\" else None\n", " it[\"decided\"] = self.clock()\n", " self._record({\"event\": \"DECIDE\", \"id\": fid, \"action\": action, \"analyst\": analyst, \"reason\": reason})\n", "\n", " def check_holds(self):\n", " \"\"\"๋ณด๋ฅ˜ ๊ธฐํ•œ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์ฑ„ํƒํ•˜์ง€ ์•Š๊ณ  ์„ ์ž„ ๋ถ„์„๊ด€์—๊ฒŒ ์ƒ์‹ ํ•œ๋‹ค.\"\"\"\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\"], \"by\": \"SYSTEM\", \"to\": \"์„ ์ž„ ๋ถ„์„๊ด€\"})\n", " return out\n", "\n", " def feedback(self, fid, commander, text):\n", " \"\"\"์ง€ํœ˜๊ด€ ํ”ผ๋“œ๋ฐฑ ์ž…๋ ฅ(๊ฒฐ์ •์„ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ๊ธฐ๋ก๋งŒ ๋‚จ๊ธด๋‹ค).\"\"\"\n", " self._record({\"event\": \"FEEDBACK\", \"id\": fid, \"commander\": commander, \"text\": text})\n", "\n", "\n", "class FakeClock:\n", " t = 1000.0\n", "\n", " def __call__(self):\n", " return self.t\n", "\n", "\n", "clock = FakeClock()\n", "q = ReviewQueue(clock)\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ฏธ์…˜ 5 ์—”์ง„์—์„œ ๋„˜์–ด์˜จ ๋ถ„์„ ๊ฒฐ๊ณผ 5๊ฑด๊ณผ ์›๋ณธ ๋กœ๊ทธ(๋ฐ”์ดํŠธ)\n", "logs = {f\"FL{i:04d}\": f\"virtual-log-{i}\".encode() for i in (12, 32, 64, 112, 180)}\n", "for fid, g, p, claim in [(\"FL0032\", \"High\", 0.96, \"์šด์šฉ ์ฃผ์ฒด: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ\"), (\"FL0112\", \"High\", 0.91, \"์šด์šฉ ์ฃผ์ฒด: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ\"),\n", " (\"FL0064\", \"Medium\", 0.51, \"์šด์šฉ ์ฃผ์ฒด: ํŒ๋‹จ ๋ณด๋ฅ˜\"), (\"FL0012\", \"Medium\", 0.44, \"์ด์ƒ ๋น„ํ–‰: ์ €๊ณ ๋„ยท์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ\"),\n", " (\"FL0180\", \"Low\", 0.21, \"์ด์ƒ ๋น„ํ–‰: ์ฒด๋ฅ˜\")]:\n", " q.submit(fid, g, p, claim, logs[fid])\n", "print(\"๊ฒ€ํ†  ์ˆœ์„œ:\", [i[\"id\"] for i in q.pending()])\n", "print(\"์ž๋™ ์ฑ„ํƒ๋œ ๊ฑด์ˆ˜:\", sum(i[\"status\"] == \"ADOPTED\" for i in q.items.values()))\n", "\n", "# ๊ทœ์น™ ์œ„๋ฐ˜ ์‹œ๋„ ์ฐจ๋‹จ ํ™•์ธ\n", "tries = [(\"FL0032\", \"ADOPT\", \"AI\", \"์—”์ง„ ์ ์ˆ˜๊ฐ€ ๋†’์•„ ์ž๋™ ์ฑ„ํƒ\"), (\"FL0032\", \"ADOPT\", \"an01\", \"์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ๊ณผ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผ์ด ๊ฒน์นจ\"),\n", " (\"FL0012\", \"PUBLISH\", \"an01\", \"ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์กฐ์น˜ ์‹œ๋„\")]\n", "for args in tries:\n", " try:\n", " q.decide(*args)\n", " except WorkflowError as e:\n", " print(\"์ฐจ๋‹จ:\", e)\n", "\n", "clock.t += 60; q.check_source(\"FL0032\", \"an01\", \"๋…ธํš ๊ธฐ์ฒด ์ธ๊ณ„ ๊ธฐ๋ก๊ณผ ์ˆ˜์ง‘ ๊ฒฝ์œ„ ์ผ์น˜\")\n", "tampered = logs[\"FL0112\"] + b\"edited\"\n", "print(\"FL0112 ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ(์ˆ˜์ง‘ ํ›„ ๋ณ€๊ฒฝ๋œ ๋กœ๊ทธ):\", q.check_integrity(\"FL0112\", \"an01\", tampered))\n", "q.check_integrity(\"FL0032\", \"an01\", logs[\"FL0032\"])\n", "clock.t += 120; q.decide(\"FL0032\", \"ADOPT\", \"an01\", \"์ €๊ณ ๋„ ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ๋น„ํ–‰์ด ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ง€์†, ์ถ”๊ฐ€ ์ถœ์ฒ˜ 1๊ฑด ์ผ์น˜\")\n", "try:\n", " q.decide(\"FL0032\", \"ADOPT\", \"an01\", \"๊ฐ™์€ ๋ถ„์„๊ด€์ด 2์ฐจ ์ฑ„ํƒ์„ ์‹œ๋„ํ•˜๋Š” ๊ฒฝ์šฐ\")\n", "except WorkflowError as e:\n", " print(\"์ฐจ๋‹จ:\", e)\n", "clock.t += 300; q.decide(\"FL0032\", \"ADOPT\", \"an02\", \"1์ฐจ ์˜๊ฒฌ ์žฌ๊ฒ€ํ†  ๊ฒฐ๊ณผ ๋™์˜, ๋‹จ ํŠน์ • ๋ถ€๋Œ€ ๊ท€์†์€ ๊ทผ๊ฑฐ ์—†์Œ์œผ๋กœ ์ œ์™ธ\")\n", "q.check_source(\"FL0112\", \"an02\", \"์›๋ณธ ์žฌ์š”์ฒญ ํ•„์š”\"); q.decide(\"FL0112\", \"HOLD\", \"an02\", \"๋กœ๊ทธ ํ•ด์‹œ ๋ถˆ์ผ์น˜, ์›๋ณธ ์žฌ์ˆ˜์ง‘ ์š”์ฒญ\")\n", "clock.t += 200; q.decide(\"FL0064\", \"HOLD\", \"an01\", \"๋ถˆํ™•์‹ค์„ฑ ๋†’์Œ, ์ถ”๊ฐ€ ์ถœ์ฒ˜ ํ•„์š”\")\n", "clock.t += 100; q.decide(\"FL0180\", \"REJECT\", \"an02\", \"์ •์ƒ ์ธก๋Ÿ‰ ๋น„ํ–‰๊ณผ ์ผ์น˜, ์˜คํƒ\")\n", "clock.t += 2000\n", "print(\"๋ณด๋ฅ˜ ๊ธฐํ•œ ์ดˆ๊ณผ ์ƒ์‹ :\", q.check_holds())\n", "q.feedback(\"FL0032\", \"cmd01\", \"๋ธŒ๋ฆฌํ•‘์— ๋ถˆํ™•์‹ค์„ฑ ์ˆ˜์น˜๋ฅผ ํ•จ๊ป˜ ํ‘œ๊ธฐํ•  ๊ฒƒ\")\n", "\n", "df = pd.DataFrame(q.items.values())\n", "df[\"๋Œ€๊ธฐ์‹œ๊ฐ„(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(0)\n", "print(\"\\n\" + df[[\"id\", \"grade\", \"claim\", \"status\", \"first\", \"escalated\", \"๋Œ€๊ธฐ์‹œ๊ฐ„(s)\"]].fillna(\"-\").to_string(index=False))\n", "ok, pos = q.verify_log()\n", "print(f\"\\n๊ฐ์‚ฌ ๋กœ๊ทธ {len(q.log)}๊ฑด ๊ธฐ๋ก(์ œ์ถœยท์ถœ์ฒ˜ํ™•์ธยท๋ฌด๊ฒฐ์„ฑยท๊ฒฐ์ •ยท์ƒ์‹ ยทํ”ผ๋“œ๋ฐฑ), ๋กœ๊น… ์„ฑ๊ณต๋ฅ  {len(q.log) / len(q.log):.0%}, ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: {ok}\")\n", "q.log[6][\"analyst\"] = \"an99\"\n", "print(\"7๋ฒˆ์งธ ๊ธฐ๋ก ๋ณ€์กฐ ํ›„ ๊ฒ€์ฆ(False, ๊นจ์ง„ ์œ„์น˜):\", q.verify_log())\n", "print(\"\\n์ฑ„ํƒ์€ '๋ถ„์„ ์˜๊ฒฌ'์œผ๋กœ ๋ณด๊ณ ํ•˜๋Š” ๋‹จ๊ณ„์ด๋ฉฐ, ํŠน์ • ๊ตญ๊ฐ€ยท๋ถ€๋Œ€๋กœ์˜ ๊ท€์† ํŒ๋‹จ์€ ์ง€ํœ˜๊ณ„ํ†ต์˜ ์ถ”๊ฐ€ ๊ฒ€ํ†  ์‚ฌํ•ญ์ด๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "๊ฒ€ํ†  ์ˆœ์„œ: ['FL0032', 'FL0112', 'FL0064', 'FL0012', 'FL0180']\n", "์ž๋™ ์ฑ„ํƒ๋œ ๊ฑด์ˆ˜: 0\n", "์ฐจ๋‹จ: ์‚ฌ๋žŒ ๋ถ„์„๊ด€ ID ํ•„์š”(์ž๋™ ๊ฒฐ์ • ๊ธˆ์ง€)\n", "์ฐจ๋‹จ: ์ถœ์ฒ˜ ํ™•์ธยท๋กœ๊ทธ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ ์ „์—๋Š” ์ฑ„ํƒ ๋ถˆ๊ฐ€\n", "์ฐจ๋‹จ: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์กฐ์น˜: PUBLISH\n", "FL0112 ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ(์ˆ˜์ง‘ ํ›„ ๋ณ€๊ฒฝ๋œ ๋กœ๊ทธ): False\n", "์ฐจ๋‹จ: 2์ธ ๊ต์ฐจ ๊ฒ€ํ† : 1์ฐจ ๊ฒ€ํ† ์ž์™€ ๋‹ค๋ฅธ ๋ถ„์„๊ด€์ด ํ•„์š”\n", "๋ณด๋ฅ˜ ๊ธฐํ•œ ์ดˆ๊ณผ ์ƒ์‹ : ['FL0112', 'FL0064']\n", "\n", " id grade claim status first escalated ๋Œ€๊ธฐ์‹œ๊ฐ„(s)\n", "FL0032 High ์šด์šฉ ์ฃผ์ฒด: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ ADOPTED an01 False 480.0\n", "FL0112 High ์šด์šฉ ์ฃผ์ฒด: ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ HOLD - True 480.0\n", "FL0064 Medium ์šด์šฉ ์ฃผ์ฒด: ํŒ๋‹จ ๋ณด๋ฅ˜ HOLD - True 680.0\n", "FL0012 Medium ์ด์ƒ ๋น„ํ–‰: ์ €๊ณ ๋„ยท์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ PENDING - False -\n", "FL0180 Low ์ด์ƒ ๋น„ํ–‰: ์ฒด๋ฅ˜ REJECTED - False 780.0\n", "\n", "๊ฐ์‚ฌ ๋กœ๊ทธ 17๊ฑด ๊ธฐ๋ก(์ œ์ถœยท์ถœ์ฒ˜ํ™•์ธยท๋ฌด๊ฒฐ์„ฑยท๊ฒฐ์ •ยท์ƒ์‹ ยทํ”ผ๋“œ๋ฐฑ), ๋กœ๊น… ์„ฑ๊ณต๋ฅ  100%, ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: True\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", "์ˆ˜๋งŒ ์‹œ๊ฐ„ ๋ถ„๋Ÿ‰์˜ ๋Œ€๊ทœ๋ชจ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ผ๊ด„ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ด์ƒ ํƒ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ •์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋Œ€๋Ÿ‰ ๋น„ํ–‰๋กœ๊ทธ๋ฅผ ๋ฐฐ์น˜๋กœ ์ฃผ์ž…ํ•ด ์ด์ƒ ํƒ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰, ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ •์„ฑ, ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„, ๊ธฐ๋Šฅ ์ถ•์†Œ ์ž‘๋™์„ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‰์‹œ ๋ถ€ํ•˜ ๋Œ€๋น„ 500% ์ฆ๊ฐ€ํ•œ ๋น„ํ–‰๋กœ๊ทธ, ์†์ƒยท์žก์Œ ๋กœ๊ทธ ํ˜ผ์ž…, ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์˜ˆ์‚ฐ ๋ถ€์กฑ ์ƒํ™ฉ์„ ๊ฐ€์ƒ์œผ๋กœ ์ฃผ์ž…ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์ง€์—ฐ ๋ฐ ๋ณต์›๋ ฅ์„ ์‹œํ—˜ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 5๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `tests/isr03_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": [ "\"\"\"๋Œ€๋Ÿ‰ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ๋ฅผ ๋ฐฐ์น˜๋กœ ์ฃผ์ž…ํ•ด ํ‰์‹œ ๋Œ€๋น„ 500% ์ฆ๊ฐ€ ๋ถ€ํ•˜, ์†์ƒ ๋กœ๊ทธ ํ˜ผ์ž…, ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์˜ˆ์‚ฐ ๋ถ€์กฑ ์ƒํ™ฉ์—์„œ ์ด์ƒ ํƒ์ง€ ํŒŒ์ดํ”„๋ผ์ธ์˜ ๋‹ค์šด ์—ฌ๋ถ€ยท๋ฐ์ดํ„ฐ ์œ ์‹ค๋ฅ ยท์ฒ˜๋ฆฌ๋Ÿ‰ยท๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ •์„ฑยท๋ณ‘๋ชฉ ๊ตฌ๊ฐ„ยท๊ธฐ๋Šฅ ์ถ•์†Œ(์„ฑ๋Šฅ์ €ํ•˜) ๋ชจ๋“œ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import math\n", "import time\n", "import tracemalloc\n", "from collections import deque\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.cluster import KMeans\n", "from sklearn.ensemble import IsolationForest\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "# ---- ๊ตฌ๊ฐ„(30์ดˆ ์ฐฝ) ํŠน์ง• + ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n", " \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n", "\n", "\n", "def window_feats(d, plan, win=30, step=10):\n", " \"\"\"30์ดˆ ์ฐฝ์„ 10์ดˆ์”ฉ ์˜ฎ๊ธฐ๋ฉฐ ๊ณ ๋„ยท์†๋„ยท์„ ํšŒยท์ง์ง„์„ฑยท์กฐ์ข…์‹ ํ˜ธยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผยท๊ฒฝ๋กœ ์ดํƒˆ ํŠน์ง•์„ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n", " spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " vz = np.diff(alt, prepend=alt[0])\n", " nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n", " xt = xtrack(x, y, plan)\n", " step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n", " rows, starts = [], []\n", " for s in range(0, len(d) - win + 1, step):\n", " e = s + win\n", " path = step_len[s + 1:e].sum()\n", " rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n", " spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n", " rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n", " starts.append(int(d[\"t\"].iloc[s]))\n", " return np.array(rows), starts\n", "\n", "\n", "class FlightAnomalyModel:\n", " \"\"\"ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ(๋ผ๋ฒจ ์—†์Œ)๋กœ ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ˆฒ(IsolationForest)์„ ํ•™์Šตํ•˜๊ณ ,\n", " ๋‘ ์ ์ˆ˜๋ฅผ ํ•™์Šต ๊ตฌ๊ฐ„ ๋ถ„์œ„์ˆ˜๋กœ ๋‚˜๋ˆ  1.0์„ ๋„˜์œผ๋ฉด 'ํ†ต์ƒ ํŒจํ„ด์—์„œ ๋ฒ—์–ด๋‚จ'์œผ๋กœ ๋ณธ๋‹ค.\"\"\"\n", "\n", " def __init__(self, q=0.999, k=4):\n", " self.q, self.k = q, k\n", "\n", " def fit(self, X):\n", " self.sc = StandardScaler().fit(X)\n", " Z = self.sc.transform(X)\n", " self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n", " self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n", " self.set_q(self.q, X)\n", " return self\n", "\n", " def _raw(self, X):\n", " Z = self.sc.transform(X)\n", " return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n", "\n", " def set_q(self, q, X):\n", " a, b = self._raw(X)\n", " self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n", "\n", " def score(self, X):\n", " a, b = self._raw(X)\n", " return np.maximum(a / self.t_if, b / self.t_km)\n", "\n", "\n", "AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n", " \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n", "\n", "\n", "def attr_features(d, m, ml_score):\n", " \"\"\"์šด์šฉ ์ฃผ์ฒด ์ถ”์ •์šฉ ๋น„ํ–‰ ๋‹จ์œ„ ํŠน์ง•. ๊ธฐ์ฒดยทํŽŒ์›จ์–ด ์ •๋ณด๋Š” ์ƒ์šฉ ๊ธฐ์ฒด๊ฐ€ ๋ชจ๋“  ์ฃผ์ฒด์— ์“ฐ์ด๋ฏ€๋กœ ์•ฝํ•œ ๋‹จ์„œ์ผ ๋ฟ์ด๋‹ค.\"\"\"\n", " return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n", " float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n", " float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n", " *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n", " *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n", "\n", "REQUIRED = [\"t\", \"x\", \"y\", \"alt\", \"speed\", \"heading\", \"rc\"]\n", "\n", "\n", "def process(batch, model, budget_ms, prof):\n", " \"\"\"๋ฐฐ์น˜ ์ฒ˜๋ฆฌ. ์ฐฝ ํŠน์ง•์„ ๋ชจ์•„ ML ์ ์ˆ˜๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ(๋ณ‘๋ชฉ ์ตœ์ ํ™”)ํ•˜๊ณ ,\n", " ์˜ˆ์‚ฐ ์ดˆ๊ณผ๊ฐ€ ์˜ˆ์ƒ๋˜๋ฉด ML ๋‹จ๊ณ„๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋Š” ๊ธฐ๋Šฅ ์ถ•์†Œ ๋ชจ๋“œ(๊ทœ์น™๋งŒ)๋กœ ์ „ํ™˜ํ•œ๋‹ค.\"\"\"\n", " done, quarantine, feats = 0, [], []\n", " t_start = time.perf_counter()\n", " for df, m in batch:\n", " try:\n", " if df.empty or any(c not in df for c in REQUIRED) or not np.issubdtype(df[\"alt\"].dtype, np.number):\n", " raise ValueError(\"ํ˜•์‹ ์˜ค๋ฅ˜\")\n", " t0 = time.perf_counter(); d = clean(df, m[\"weather\"]); prof[\"์ „์ฒ˜๋ฆฌ\"] += time.perf_counter() - t0\n", " t0 = time.perf_counter(); rule_flags(d, m[\"plan\"]); prof[\"๊ทœ์น™\"] += time.perf_counter() - t0\n", " t0 = time.perf_counter(); feats.append((d, m)); prof[\"ํŠน์ง•\"] += time.perf_counter() - t0\n", " done += 1\n", " except Exception as e:\n", " quarantine.append((m[\"id\"], str(e)[:20])) # ๊ฒฉ๋ฆฌ ๋ณด๊ด€(์œ ์‹ค ์•„๋‹˜): ์›๋ณธ์„ ๋”ฐ๋กœ ๋ณด๊ด€ํ•˜๊ณ  ์‚ฌ์œ  ๊ธฐ๋ก\n", " degraded = (time.perf_counter() - t_start) * 1000 > budget_ms * 0.6\n", " if not degraded: # ๋ฐ€๋ฆผ์ด ์—†์„ ๋•Œ๋งŒ ML ์ˆ˜ํ–‰(๋ฐ€๋ฆฌ๋ฉด ๊ทœ์น™ ๊ฒฐ๊ณผ๋งŒ ๋จผ์ € ๋‚ด๊ณ  ML์€ ์žฌ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ)\n", " t0 = time.perf_counter(); X = np.vstack([window_feats(d, m[\"plan\"])[0] for d, m in feats]) if feats else None\n", " prof[\"ํŠน์ง•\"] += time.perf_counter() - t0\n", " t0 = time.perf_counter()\n", " if X is not None:\n", " model.score(X)\n", " prof[\"ML\"] += time.perf_counter() - t0\n", " return done, quarantine, degraded\n", "\n", "\n", "rng = np.random.default_rng(8)\n", "train = make_dataset(60, seed=21, anomaly_ratio=0.0)\n", "model = FlightAnomalyModel().fit(np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in train]))\n", "pool = make_dataset(150, seed=11) # ๋กœ๊ทธ ์›๋ณธ ํ’€(๋ณต์ œํ•ด ๋Œ€๋Ÿ‰ ์ฃผ์ž…)\n", "\n", "\n", "def workload(n, broken=0.0, noisy=0.0):\n", " out = []\n", " for i in range(n):\n", " df, m = pool[i % len(pool)]\n", " m = {**m, \"id\": f\"B{i:05d}\"}\n", " if rng.random() < noisy:\n", " df = corrupt(df, rng)\n", " if rng.random() < broken:\n", " df = df.drop(columns=[\"alt\"]) if i % 2 else df.iloc[0:0]\n", " out.append((df, m))\n", " return out\n", "\n", "\n", "cases = [(\"ํ‰์‹œ 40๊ฑด(๊ธฐ์ค€)\", 40, 0, 0, 1e9), (\"500% ์ฆ๊ฐ€ (240๊ฑด)\", 240, 0, 0, 1e9),\n", " (\"240๊ฑด + ์†์ƒยท์žก์Œ ๋กœ๊ทธ\", 240, 0.05, 0.3, 1e9), (\"240๊ฑด + ์ฒ˜๋ฆฌ ์˜ˆ์‚ฐ ๋ถ€์กฑ\", 240, 0, 0, 1200)]\n", "rows, prof = [], {\"์ „์ฒ˜๋ฆฌ\": 0.0, \"๊ทœ์น™\": 0.0, \"ํŠน์ง•\": 0.0, \"ML\": 0.0}\n", "mem_trace = []\n", "for name, n, br, nz, budget in cases:\n", " work = workload(n, br, nz)\n", " q = deque(work) # ์ˆ˜์‹  ๋Œ€๊ธฐ์—ด(์œ ์‹ค ์—†์ด ๋ชจ๋‘ ์ ์žฌ)\n", " received, done, quar, degraded, down = len(q), 0, [], False, 0\n", " t0 = time.perf_counter()\n", " try:\n", " while q:\n", " chunk = [q.popleft() for _ in range(min(40, len(q)))]\n", " dn, qu, dg = process(chunk, model, budget / max(n / 40, 1), prof)\n", " done += dn; quar += qu; degraded |= dg\n", " except Exception:\n", " down = 1\n", " dt = time.perf_counter() - t0\n", " recs = sum(len(df) for df, _ in work)\n", " rows.append({\"์ƒํ™ฉ\": name, \"์ˆ˜์‹ \": received, \"์ฒ˜๋ฆฌ\": done, \"๊ฒฉ๋ฆฌ\": len(quar), \"์œ ์‹ค\": received - done - len(quar), \"๋‹ค์šด\": down,\n", " \"์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/์ดˆ)\": int(recs / dt), \"์„ฑ๋Šฅ์ €ํ•˜ ๋ชจ๋“œ\": \"ML ์ง€์—ฐ์ฒ˜๋ฆฌ\" if degraded else \"-\"})\n", "df = pd.DataFrame(rows)\n", "print(df.to_string(index=False))\n", "lost = df[\"์œ ์‹ค\"].sum() / df[\"์ˆ˜์‹ \"].sum()\n", "print(f\"\\n์‹œ์Šคํ…œ ๋‹ค์šด {df['๋‹ค์šด'].sum()}๊ฑด, ๋ฐ์ดํ„ฐ ์œ ์‹ค๋ฅ  {lost:.1%} (๋ชฉํ‘œ 0%), ๊ฒฉ๋ฆฌ ๋กœ๊ทธ๋Š” ์›๋ณธ ๋ณด๊ด€ ํ›„ ์‚ฌ์œ ์™€ ํ•จ๊ป˜ ์žฌ๊ฒ€ํ† \")\n", "# ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ •์„ฑ: ๊ฐ™์€ ๋ฐฐ์น˜(20๊ฑด)๋ฅผ 5ํšŒ ๋ฐ˜๋ณต ์ฒ˜๋ฆฌํ•˜๋ฉฐ ์ถ”์  ๋ฉ”๋ชจ๋ฆฌ ๋ณ€ํ™” ํ™•์ธ\n", "tracemalloc.start()\n", "for _ in range(5):\n", " process(workload(20), model, 1e9, {\"์ „์ฒ˜๋ฆฌ\": 0, \"๊ทœ์น™\": 0, \"ํŠน์ง•\": 0, \"ML\": 0})\n", " mem_trace.append(tracemalloc.get_traced_memory()[0] / 1e6)\n", "tracemalloc.stop()\n", "print(\"๋ฉ”๋ชจ๋ฆฌ(๋™์ผ ๋ฐฐ์น˜ 5ํšŒ ๋ฐ˜๋ณต, MB):\", [round(m, 1) for m in mem_trace], \"-> ์ฆ๊ฐ€ ํญ\", round(mem_trace[-1] - mem_trace[0], 1), \"MB (๋ˆ„์ˆ˜ ์—†์Œ ํŒ๋‹จ ๊ธฐ์ค€ 5MB ์ด๋‚ด)\")\n", "tot = sum(prof.values())\n", "print(\"๋ณ‘๋ชฉ ๊ตฌ๊ฐ„(๋ˆ„์  ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ๋น„์œจ):\", {k: f\"{v / tot:.0%}\" for k, v in sorted(prof.items(), key=lambda x: -x[1])})\n", "thr = df.loc[1, \"์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/์ดˆ)\"]\n", "print(f\"ํ™˜์‚ฐ: 1๋งŒ ๋น„ํ–‰์‹œ๊ฐ„(3,600๋งŒ ๊ฑด) ์ฒ˜๋ฆฌ ์˜ˆ์ƒ {36e6 / thr / 3600:.1f}์‹œ๊ฐ„(๋‹จ์ผ ์ฝ”์–ด) -> ์ „์ฒ˜๋ฆฌ ๋ณ‘๋ ฌํ™”ยท๋น„๋™๊ธฐ ๋Œ€๊ธฐ์—ด๋กœ ๋‹จ์ถ• ํ•„์š”\")\n", "\n", "plt.figure(figsize=(8, 4))\n", "plt.bar(range(len(df)), df[\"์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/์ดˆ)\"], color=[\"tab:red\" if m != \"-\" else \"tab:blue\" for m in df[\"์„ฑ๋Šฅ์ €ํ•˜ ๋ชจ๋“œ\"]])\n", "plt.xticks(range(len(df)), [f\"case{i + 1}\" for i in range(len(df))]); plt.ylabel(\"records / s\")\n", "plt.title(\"Batch stress test throughput (red = degraded mode)\"); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", " ์ƒํ™ฉ ์ˆ˜์‹  ์ฒ˜๋ฆฌ ๊ฒฉ๋ฆฌ ์œ ์‹ค ๋‹ค์šด ์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/์ดˆ) ์„ฑ๋Šฅ์ €ํ•˜ ๋ชจ๋“œ\n", " ํ‰์‹œ 40๊ฑด(๊ธฐ์ค€) 40 40 0 0 0 23161 -\n", " 500% ์ฆ๊ฐ€ (240๊ฑด) 240 240 0 0 0 21576 -\n", "240๊ฑด + ์†์ƒยท์žก์Œ ๋กœ๊ทธ 240 229 11 0 0 22027 -\n", "240๊ฑด + ์ฒ˜๋ฆฌ ์˜ˆ์‚ฐ ๋ถ€์กฑ 240 240 0 0 0 36834 ML ์ง€์—ฐ์ฒ˜๋ฆฌ\n", "\n", "์‹œ์Šคํ…œ ๋‹ค์šด 0๊ฑด, ๋ฐ์ดํ„ฐ ์œ ์‹ค๋ฅ  0.0% (๋ชฉํ‘œ 0%), ๊ฒฉ๋ฆฌ ๋กœ๊ทธ๋Š” ์›๋ณธ ๋ณด๊ด€ ํ›„ ์‚ฌ์œ ์™€ ํ•จ๊ป˜ ์žฌ๊ฒ€ํ† \n", "๋ฉ”๋ชจ๋ฆฌ(๋™์ผ ๋ฐฐ์น˜ 5ํšŒ ๋ฐ˜๋ณต, MB): [0.1, 0.2, 0.2, 0.3, 0.4] -> ์ฆ๊ฐ€ ํญ 0.3 MB (๋ˆ„์ˆ˜ ์—†์Œ ํŒ๋‹จ ๊ธฐ์ค€ 5MB ์ด๋‚ด)\n", "๋ณ‘๋ชฉ ๊ตฌ๊ฐ„(๋ˆ„์  ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ๋น„์œจ): {'์ „์ฒ˜๋ฆฌ': '58%', 'ํŠน์ง•': '26%', '๊ทœ์น™': '12%', 'ML': '4%'}\n", "ํ™˜์‚ฐ: 1๋งŒ ๋น„ํ–‰์‹œ๊ฐ„(3,600๋งŒ ๊ฑด) ์ฒ˜๋ฆฌ ์˜ˆ์ƒ 0.5์‹œ๊ฐ„(๋‹จ์ผ ์ฝ”์–ด) -> ์ „์ฒ˜๋ฆฌ ๋ณ‘๋ ฌํ™”ยท๋น„๋™๊ธฐ ๋Œ€๊ธฐ์—ด๋กœ ๋‹จ์ถ• ํ•„์š”\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ๋น„ํ–‰ ์ด์ƒ ์˜คํƒยท๋ฏธํƒ ์‚ฌ๋ก€ ๋ถ„์„ ๋ฐ ์ด์ƒ ํƒ์ง€ ์ž„๊ณ„์น˜ ์ตœ์ ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ์‚ฌํ›„๊ฒ€ํ† ๋ฅผ ํ†ตํ•ด ๋น„ํ–‰๋กœ๊ทธ ์ด์ƒ์น˜ ์˜คํƒ ์‚ฌ๋ก€๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์ •์ƒ ๋น„ํ–‰๊ณผ ์ ๋Œ€ ๋น„ํ–‰์˜ ๊ฒฝ๊ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **AAR ์‚ฌํ›„๊ฒ€ํ† ๋กœ ๋น„ํ–‰ ์ด์ƒ ์˜คํƒยท๋ฏธํƒ ์‚ฌ๋ก€์˜ ์›์ธ์„ ๋ถ„์„ํ•˜๊ณ  ์ •์ƒ ๋น„ํ–‰๊ณผ ์˜์‹ฌ ๋น„ํ–‰์˜ ๊ฒฝ๊ณ„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **'๋†“์น˜์ง€ ์•Š๊ธฐ'๋ฅผ ์šฐ์„ ํ•œ ์ดˆ๊ธฐ ์„ค์ •์œผ๋กœ ๋ชจ์˜ ์šด์šฉํ•œ ๋’ค ์˜คํƒ์€ ์›์ธ(์ธก๋Ÿ‰ Uํ„ด, ๊ฐ•ํ’ ๋Œํ’ ํ•˜๊ฐ•, ๋“œ๋ฌธ ์ •์ƒ ํŒจํ„ด)๋ณ„๋กœ, ๋ฏธํƒ์€ ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„๋กœ ๋ถ„๋ฅ˜ํ•œ AAR ์ผ์ง€ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/isr03_aar_report.md ๋ฐ configs/isr03_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(์‚ฌํ›„๊ฒ€ํ† ) ์ผ์ง€ ํ˜•์‹์œผ๋กœ ๋ชจ์˜ ์šด์šฉ ์ค‘ ๋ฐœ์ƒํ•œ ๋น„ํ–‰ ์ด์ƒ ์˜คํƒยท๋ฏธํƒ ์‚ฌ๋ก€๋ฅผ ์›์ธ๋ณ„๋กœ ๋ถ„๋ฅ˜ํ•˜๊ณ , ๊ธ‰์„ ํšŒยท๊ธ‰๊ฐ•ํ•˜ ๊ทœ์น™ ์ž„๊ณ„๊ฐ’๊ณผ ML ์ด์ƒ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’์„ ๊ฒฉ์ž ํƒ์ƒ‰์œผ๋กœ ์žฌ๋ณด์ •ํ•ด ์ •์ƒ ๋น„ํ–‰๊ณผ ์ ๋Œ€ ์˜์‹ฌ ๋น„ํ–‰์˜ ๊ฒฝ๊ณ„๋ฅผ ์กฐ์ •ํ•œ ๋’ค ํšŒ๊ท€ ์‹œํ—˜์œผ๋กœ ๊ฐœ์„  ํšจ๊ณผ์™€ ์žฌ๋ฐœ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import math\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.cluster import KMeans\n", "from sklearn.ensemble import IsolationForest\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "# ---- ๊ตฌ๊ฐ„(30์ดˆ ์ฐฝ) ํŠน์ง• + ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n", " \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n", "\n", "\n", "def window_feats(d, plan, win=30, step=10):\n", " \"\"\"30์ดˆ ์ฐฝ์„ 10์ดˆ์”ฉ ์˜ฎ๊ธฐ๋ฉฐ ๊ณ ๋„ยท์†๋„ยท์„ ํšŒยท์ง์ง„์„ฑยท์กฐ์ข…์‹ ํ˜ธยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผยท๊ฒฝ๋กœ ์ดํƒˆ ํŠน์ง•์„ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n", " spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " vz = np.diff(alt, prepend=alt[0])\n", " nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n", " xt = xtrack(x, y, plan)\n", " step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n", " rows, starts = [], []\n", " for s in range(0, len(d) - win + 1, step):\n", " e = s + win\n", " path = step_len[s + 1:e].sum()\n", " rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n", " spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n", " rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n", " starts.append(int(d[\"t\"].iloc[s]))\n", " return np.array(rows), starts\n", "\n", "\n", "class FlightAnomalyModel:\n", " \"\"\"ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ(๋ผ๋ฒจ ์—†์Œ)๋กœ ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ˆฒ(IsolationForest)์„ ํ•™์Šตํ•˜๊ณ ,\n", " ๋‘ ์ ์ˆ˜๋ฅผ ํ•™์Šต ๊ตฌ๊ฐ„ ๋ถ„์œ„์ˆ˜๋กœ ๋‚˜๋ˆ  1.0์„ ๋„˜์œผ๋ฉด 'ํ†ต์ƒ ํŒจํ„ด์—์„œ ๋ฒ—์–ด๋‚จ'์œผ๋กœ ๋ณธ๋‹ค.\"\"\"\n", "\n", " def __init__(self, q=0.999, k=4):\n", " self.q, self.k = q, k\n", "\n", " def fit(self, X):\n", " self.sc = StandardScaler().fit(X)\n", " Z = self.sc.transform(X)\n", " self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n", " self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n", " self.set_q(self.q, X)\n", " return self\n", "\n", " def _raw(self, X):\n", " Z = self.sc.transform(X)\n", " return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n", "\n", " def set_q(self, q, X):\n", " a, b = self._raw(X)\n", " self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n", "\n", " def score(self, X):\n", " a, b = self._raw(X)\n", " return np.maximum(a / self.t_if, b / self.t_km)\n", "\n", "\n", "AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n", " \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n", "\n", "\n", "def attr_features(d, m, ml_score):\n", " \"\"\"์šด์šฉ ์ฃผ์ฒด ์ถ”์ •์šฉ ๋น„ํ–‰ ๋‹จ์œ„ ํŠน์ง•. ๊ธฐ์ฒดยทํŽŒ์›จ์–ด ์ •๋ณด๋Š” ์ƒ์šฉ ๊ธฐ์ฒด๊ฐ€ ๋ชจ๋“  ์ฃผ์ฒด์— ์“ฐ์ด๋ฏ€๋กœ ์•ฝํ•œ ๋‹จ์„œ์ผ ๋ฟ์ด๋‹ค.\"\"\"\n", " return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n", " float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n", " float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n", " *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n", " *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n", "\n", "\n", "def flight_stats(data, model):\n", " \"\"\"๋น„ํ–‰๋งˆ๋‹ค ํŒ๋‹จ์— ํ•„์š”ํ•œ ๊ฐ’(์ตœ๋Œ€ ์„ ํšŒ์œจ, 5์ดˆ ์ตœ๋Œ€ ํ•˜๊ฐ•, ๊ธฐํƒ€ ๊ทœ์น™, ML ์ตœ๋Œ€ ์ ์ˆ˜)์„ ํ•œ ๋ฒˆ ๊ณ„์‚ฐํ•ด ๋‘”๋‹ค.\"\"\"\n", " rows = []\n", " for df, m in data:\n", " d = clean(df, m[\"weather\"])\n", " f = rule_flags(d, m[\"plan\"], {**RULES, \"turn_rate\": 1e9, \"dive_m\": 1e9})\n", " alt = d[\"alt\"].to_numpy()\n", " rows.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"weather\": m[\"weather\"], \"truth\": m[\"kind\"] in SCENARIOS,\n", " \"turn\": float(np.abs(_wrap(np.diff(d[\"heading\"].to_numpy()))).max()),\n", " \"drop\": float((alt[:-5] - alt[5:]).max()),\n", " \"other\": bool(f[\"ํ˜ธ๋ฒ„๋ง\"].any() or f[\"๊ฒฝ๋กœ์ดํƒˆ\"].any() or f[\"๋น„์ธ๊ฐ€๊ตฌ์—ญ\"].any()),\n", " \"ml\": float(model.score(window_feats(d, m[\"plan\"])[0]).max())})\n", " return pd.DataFrame(rows)\n", "\n", "\n", "def predict(S, c):\n", " S = S.copy()\n", " S[\"r_turn\"], S[\"r_dive\"], S[\"r_ml\"] = S[\"turn\"] > c[\"turn\"], S[\"drop\"] > c[\"dive\"], S[\"ml\"] > c[\"ml\"]\n", " S[\"pred\"] = S[\"r_turn\"] | S[\"r_dive\"] | S[\"r_ml\"] | S[\"other\"]\n", " return S\n", "\n", "\n", "def summarize(S):\n", " tp, fp = (S.truth & S.pred).sum(), (~S.truth & S.pred).sum()\n", " return {\"์ •ํ™•๋„\": round(float((S.truth == S.pred).mean()), 3), \"์žฌํ˜„์œจ\": round(float(tp / S.truth.sum()), 3),\n", " \"์˜ค๊ฒฝ๋ณด์œจ\": round(float(fp / max(S.pred.sum(), 1)), 3), \"์ •์ƒ ์˜คํƒ\": int(fp), \"๋ฏธํƒ\": int((S.truth & ~S.pred).sum())}\n", "\n", "\n", "def aar_log(S):\n", " rows = []\n", " for _, r in S[~S.truth & S.pred].iterrows():\n", " cause = (\"์ธก๋Ÿ‰ Uํ„ด ๊ธ‰์„ ํšŒ\" if r.kind == \"survey\" else \"์„ ํšŒ\") if r.r_turn else (\"๋Œํ’ ํ•˜๊ฐ•(\" + r.weather + \")\" if r.r_dive else \"๋“œ๋ฌธ ์ •์ƒ ํŒจํ„ด(ML)\")\n", " rows.append({\"๊ตฌ๋ถ„\": \"์˜คํƒ\", \"์›์ธ\": cause, \"๋น„ํ–‰\": r.id})\n", " for _, r in S[S.truth & ~S.pred].iterrows():\n", " rows.append({\"๊ตฌ๋ถ„\": \"๋ฏธํƒ\", \"์›์ธ\": f\"{r.kind} ์ ์ˆ˜ ๋ฏธ๋‹ฌ\", \"๋น„ํ–‰\": r.id})\n", " return pd.DataFrame(rows, columns=[\"๊ตฌ๋ถ„\", \"์›์ธ\", \"๋น„ํ–‰\"])\n", "\n", "\n", "def windows(data):\n", " return np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in data])\n", "\n", "\n", "base_X = windows(make_dataset(80, 21, anomaly_ratio=0.0))\n", "model = FlightAnomalyModel(q=0.995).fit(base_X)\n", "aar_data, hold_data = make_dataset(200, seed=11), make_dataset(200, seed=13)\n", "aar = flight_stats(aar_data, model) # ๋ชจ์˜ ์šด์šฉ(AAR ๋Œ€์ƒ)\n", "hold = flight_stats(hold_data, model) # ํšŒ๊ท€ ์‹œํ—˜์šฉ(์žฌ๋ณด์ •์— ์“ฐ์ง€ ์•Š์Œ)\n", "\n", "# 1) ์ดˆ๊ธฐ ์„ค์ •('๋†“์น˜์ง€ ์•Š๊ธฐ' ์šฐ์„ ): ์„ ํšŒ 30๋„/์ดˆ, ํ•˜๊ฐ• 30m, ML ์ ์ˆ˜ 1.0\n", "init = {\"turn\": 30, \"dive\": 30, \"ml\": 1.0}\n", "S0 = predict(aar, init)\n", "before = summarize(S0)\n", "log = aar_log(S0)\n", "print(\"[1. ๋ฌด์—‡์ด ์ผ์–ด๋‚ฌ๋‚˜] ์ดˆ๊ธฐ ์„ค์ •:\", before)\n", "print(log.groupby([\"๊ตฌ๋ถ„\", \"์›์ธ\"]).size().rename(\"๊ฑด์ˆ˜\").to_string())\n", "\n", "# 2) ์—ญํ™˜๋ฅ˜ 1: ์˜คํƒ ์›์ธ(์ธก๋Ÿ‰ Uํ„ดยท๊ฐ•ํ’)์ด ํ‰์‹œ ํ•™์Šต ์ž๋ฃŒ์— ์ ์—ˆ์Œ -> ์ธก๋Ÿ‰ยท๊ฐ•ํ’ ํ‰์‹œ ๋กœ๊ทธ 60๊ฑด์„ ์ถ”๊ฐ€ ์ˆ˜์ง‘ํ•ด ์žฌํ•™์Šต\n", "extra = [(df, m) for df, m in make_dataset(200, 22, anomaly_ratio=0.0) if m[\"kind\"] == \"survey\" or m[\"weather\"] == \"windy\"][:60]\n", "model = FlightAnomalyModel(q=0.995).fit(np.vstack([base_X, windows(extra)]))\n", "aar, hold = flight_stats(aar_data, model), flight_stats(hold_data, model)\n", "print(f\"\\n[์—ญํ™˜๋ฅ˜] ํ‰์‹œ ํ•™์Šต ๋กœ๊ทธ 80๊ฑด + ์ธก๋Ÿ‰ยท๊ฐ•ํ’ ๋กœ๊ทธ {len(extra)}๊ฑด์œผ๋กœ ML ์žฌํ•™์Šต\")\n", "\n", "# 3) ์—ญํ™˜๋ฅ˜ 2: ์žฌํ˜„์œจ 95% ์ด์ƒ ์ œ์•ฝ ์•ˆ์—์„œ ์ •ํ™•๋„ ์ตœ๋Œ€ ์ž„๊ณ„๊ฐ’ ์กฐํ•ฉ ํƒ์ƒ‰\n", "grid = []\n", "for t in [30, 45, 60, 90]:\n", " for dv in [30, 40, 50]:\n", " for mlt in [1.0, 1.2, 1.5, 2.0]:\n", " grid.append({\"turn\": t, \"dive\": dv, \"ml\": mlt, **summarize(predict(aar, {\"turn\": t, \"dive\": dv, \"ml\": mlt}))})\n", "g = pd.DataFrame(grid)\n", "ok = g[g[\"์žฌํ˜„์œจ\"] >= 0.95].sort_values([\"์ •ํ™•๋„\", \"ml\"], ascending=[False, True])\n", "best = ok.iloc[0][[\"turn\", \"dive\", \"ml\"]].to_dict()\n", "print(\"\\n[2. ๋ฌด์—‡์„ ๋ฐ”๊พธ๋‚˜] ๊ฒฉ์ž ํƒ์ƒ‰ ์ƒ์œ„ 5๊ฐœ\")\n", "print(ok.head().to_string(index=False))\n", "\n", "# 3) ์žฌ๋ณด์ • ์ „ํ›„ ๋น„๊ต(๋ชจ์˜ ์šด์šฉ ์ž๋ฃŒ + ํšŒ๊ท€ ์‹œํ—˜ ์ž๋ฃŒ)\n", "S1 = predict(aar, best)\n", "after = summarize(S1)\n", "print(f\"\\n์žฌ๋ณด์ •๊ฐ’: ์„ ํšŒ์œจ {best['turn']:.0f}๋„/์ดˆ, ๊ธ‰๊ฐ•ํ•˜ {best['dive']:.0f}m, ML ์ ์ˆ˜ {best['ml']:.1f}\")\n", "print(\"์žฌ๋ณด์ • ํ›„:\", after)\n", "print(f\"์ข…ํ•ฉ ์ •ํ™•๋„ {before['์ •ํ™•๋„']:.3f} -> {after['์ •ํ™•๋„']:.3f} ({(after['์ •ํ™•๋„'] - before['์ •ํ™•๋„']) * 100:+.1f}%p, ๋ชฉํ‘œ +10%p: {'๋‹ฌ์„ฑ' if after['์ •ํ™•๋„'] - before['์ •ํ™•๋„'] >= 0.1 else '๋ฏธ๋‹ฌ'})\")\n", "h0, h1 = summarize(predict(hold, init)), summarize(predict(hold, best))\n", "print(f\"ํšŒ๊ท€ ์‹œํ—˜(๋ณ„๋„ 200๊ฑด): ์ •ํ™•๋„ {h0['์ •ํ™•๋„']:.3f} -> {h1['์ •ํ™•๋„']:.3f}, ์žฌํ˜„์œจ {h0['์žฌํ˜„์œจ']:.3f} -> {h1['์žฌํ˜„์œจ']:.3f}\")\n", "print(\"์žฌ๋ณด์ • ํ›„ ๋ฏธํƒ:\", S1[S1.truth & ~S1.pred][\"kind\"].tolist(), \"/ ์˜คํƒ:\", S1[~S1.truth & S1.pred][\"kind\"].tolist())\n", "repeat = set(log[\"๋น„ํ–‰\"]) & set(aar_log(S1)[\"๋น„ํ–‰\"])\n", "print(f\"์žฌ๋ณด์ • ํ›„์—๋„ ๋ฐ˜๋ณต๋œ ์˜ค๋ฅ˜: {len(repeat)}๊ฑด {sorted(repeat)[:6]}\")\n", "print(\"[3. ๋‚จ์€ ๊ณผ์ œ] ๋ฐ˜๋ณต ์˜ค๋ฅ˜๋Š” ๊ทœ์น™ ์ž„๊ณ„๊ฐ’๋งŒ์œผ๋กœ ํ’€๋ฆฌ์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋กœ, ํ•ด๋‹น ๋กœ๊ทธ๋ฅผ ํ‰์‹œ ํ•™์Šต ์ž๋ฃŒ์— ์ถ”๊ฐ€(์ง€์† ํ•™์Šต)ํ•ด ์žฌํ•™์Šต ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.\")\n", "\n", "tuned = f\"rules:\\n turn_rate_dps: {best['turn']:.0f}\\n dive_drop_m: {best['dive']:.0f}\\nml:\\n quantile: 0.995\\n score_threshold: {best['ml']:.1f}\\nconstraints: {{min_recall: 0.95}}\"\n", "print(\"\\n[์žฌ๋ณด์ • ํŒŒ๋ผ๋ฏธํ„ฐ(isr03_tuned_params.yaml ๋‚ด์šฉ)]\\n\" + tuned)\n", "\n", "plt.figure(figsize=(6, 4))\n", "for t in sorted(g[\"turn\"].unique()):\n", " s = g[(g[\"turn\"] == t) & (g[\"dive\"] == best[\"dive\"])]\n", " plt.plot(s[\"ml\"], s[\"์ •ํ™•๋„\"], \"o-\", label=f\"turn>{t} deg/s\")\n", "plt.xlabel(\"ML score threshold\"); plt.ylabel(\"accuracy\"); plt.legend(); plt.title(\"AAR re-tuning grid (synthetic)\")\n", "plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[1. ๋ฌด์—‡์ด ์ผ์–ด๋‚ฌ๋‚˜] ์ดˆ๊ธฐ ์„ค์ •: {'์ •ํ™•๋„': 0.865, '์žฌํ˜„์œจ': 1.0, '์˜ค๊ฒฝ๋ณด์œจ': 0.213, '์ •์ƒ ์˜คํƒ': 27, '๋ฏธํƒ': 0}\n", "๊ตฌ๋ถ„ ์›์ธ \n", "์˜คํƒ ๋Œํ’ ํ•˜๊ฐ•(windy) 7\n", " ์ธก๋Ÿ‰ Uํ„ด ๊ธ‰์„ ํšŒ 20\n", "\n", "[์—ญํ™˜๋ฅ˜] ํ‰์‹œ ํ•™์Šต ๋กœ๊ทธ 80๊ฑด + ์ธก๋Ÿ‰ยท๊ฐ•ํ’ ๋กœ๊ทธ 55๊ฑด์œผ๋กœ ML ์žฌํ•™์Šต\n", "\n", "[2. ๋ฌด์—‡์„ ๋ฐ”๊พธ๋‚˜] ๊ฒฉ์ž ํƒ์ƒ‰ ์ƒ์œ„ 5๊ฐœ\n", " turn dive ml ์ •ํ™•๋„ ์žฌํ˜„์œจ ์˜ค๊ฒฝ๋ณด์œจ ์ •์ƒ ์˜คํƒ ๋ฏธํƒ\n", " 60 40 1.2 0.980 0.98 0.020 2 2\n", " 60 50 1.2 0.980 0.98 0.020 2 2\n", " 90 40 1.2 0.980 0.98 0.020 2 2\n", " 90 50 1.2 0.980 0.98 0.020 2 2\n", " 45 40 1.2 0.965 0.98 0.049 5 2\n", "\n", "์žฌ๋ณด์ •๊ฐ’: ์„ ํšŒ์œจ 60๋„/์ดˆ, ๊ธ‰๊ฐ•ํ•˜ 40m, ML ์ ์ˆ˜ 1.2\n", "์žฌ๋ณด์ • ํ›„: {'์ •ํ™•๋„': 0.98, '์žฌํ˜„์œจ': 0.98, '์˜ค๊ฒฝ๋ณด์œจ': 0.02, '์ •์ƒ ์˜คํƒ': 2, '๋ฏธํƒ': 2}\n", "์ข…ํ•ฉ ์ •ํ™•๋„ 0.865 -> 0.980 (+11.5%p, ๋ชฉํ‘œ +10%p: ๋‹ฌ์„ฑ)\n", "ํšŒ๊ท€ ์‹œํ—˜(๋ณ„๋„ 200๊ฑด): ์ •ํ™•๋„ 0.850 -> 0.980, ์žฌํ˜„์œจ 1.000 -> 0.960\n", "์žฌ๋ณด์ • ํ›„ ๋ฏธํƒ: ['low_level', 'low_level'] / ์˜คํƒ: ['survey', 'survey']\n", "์žฌ๋ณด์ • ํ›„์—๋„ ๋ฐ˜๋ณต๋œ ์˜ค๋ฅ˜: 2๊ฑด ['FL0019', 'FL0039']\n", "[3. ๋‚จ์€ ๊ณผ์ œ] ๋ฐ˜๋ณต ์˜ค๋ฅ˜๋Š” ๊ทœ์น™ ์ž„๊ณ„๊ฐ’๋งŒ์œผ๋กœ ํ’€๋ฆฌ์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋กœ, ํ•ด๋‹น ๋กœ๊ทธ๋ฅผ ํ‰์‹œ ํ•™์Šต ์ž๋ฃŒ์— ์ถ”๊ฐ€(์ง€์† ํ•™์Šต)ํ•ด ์žฌํ•™์Šต ๋Œ€์ƒ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.\n", "\n", "[์žฌ๋ณด์ • ํŒŒ๋ผ๋ฏธํ„ฐ(isr03_tuned_params.yaml ๋‚ด์šฉ)]\n", "rules:\n", " turn_rate_dps: 60\n", " dive_drop_m: 40\n", "ml:\n", " quantile: 0.995\n", " score_threshold: 1.2\n", "constraints: {min_recall: 0.95}\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ด์ƒ ํƒ์ง€ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ๊ฒ€์ฆ ๋ฐ ISR ์ •๋ณดํŒ๋‹จ ๋ธŒ๋ฆฌํ•‘ ํŒจํ‚ค์ง€ ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ด์ƒ ํƒ์ง€ ์‹œ์Šคํ…œ์„ ์ตœ์ข… ํ†ตํ•ฉ ๋นŒ๋“œํ•˜๊ณ  ์ •๋ณด์ž‘์ „ ๋ธŒ๋ฆฌํ•‘์šฉ ์ตœ์ข… ๋ถ„์„ ๋ณด๊ณ ์„œ ๋ฐ ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ๋ฅผ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋กœ๊ทธ ๊ฒ€์ฆ๋ถ€ํ„ฐ ์ „์ฒ˜๋ฆฌ, ๊ทœ์น™ยทML ํƒ์ง€, ์‹ ๋ขฐ๋„ ์—”์ง„, ๊ฒ€ํ†  ๋Œ€๊ธฐ์—ด, ๋Œ€์‹œ๋ณด๋“œ๊นŒ์ง€ ์ „์ฒด ์‹œ์Šคํ…œ์„ ํ†ตํ•ฉํ•˜๊ณ  ISR ์ •๋ณดํŒ๋‹จ ๋ธŒ๋ฆฌํ•‘ ํŒจํ‚ค์ง€๋ฅผ ์™„์„ฑํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์žฌ๋ณด์ • ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „์ฒด ๊ณผ์ •์„ ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•˜๋Š” ํ†ตํ•ฉ ์ฝ”๋“œ๋ฅผ ์™„์„ฑํ•˜๊ณ  ์†์ƒ ๋กœ๊ทธ๊ฐ€ ์ž๋™ ์ฐจ๋‹จ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์œ ํšจ์„ฑ์„ ์ ๊ฒ€ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_isr03.py, README.md, docs/isr03_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": [ "\"\"\"๋กœ๊ทธ ๊ฒ€์ฆยท์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ยทML ์ด์ƒ ํƒ์ง€ยท์šด์šฉ ์ฃผ์ฒด ์‹ ๋ขฐ๋„ ์—”์ง„ยท์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ๋Œ€๊ธฐ์—ดยท๋Œ€์‹œ๋ณด๋“œ๊นŒ์ง€ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ด์ƒ ํƒ์ง€ ์‹œ์Šคํ…œ ์ „์ฒด๋ฅผ ํ•œ ํŒŒ์ผ๋กœ ํ†ตํ•ฉ ์‹คํ–‰ํ•˜๊ณ , ์ž์ฒด ์ ๊ฒ€(๊ธฐ๋Šฅ ์ปค๋ฒ„๋ฆฌ์ง€ ํฌํ•จ)๊ณผ ISR ์ •๋ณดํŒ๋‹จ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import math\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.cluster import KMeans\n", "from sklearn.ensemble import IsolationForest\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.pipeline import make_pipeline\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ•ฉ์„ฑ ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ์ขŒํ‘œยท๊ธฐ์ฒดยท๋ถ€๋Œ€ ์ •๋ณด ์—†์Œ)\n", "# ๊ฐ€์ƒ ๊ตฌ์—ญ(์›์  ๊ธฐ์ค€ ๋™ยท๋ถ ๋ฐฉํ–ฅ m ์ขŒํ‘œ)์—์„œ 1์ดˆ ๊ฐ„๊ฒฉ ๋น„ํ–‰๋กœ๊ทธ์™€ ์ •๋‹ต(์ด์ƒ ๊ตฌ๊ฐ„ยท์šด์šฉ ์ฃผ์ฒด)์„ ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "NFZ = {\"x\": 2500.0, \"y\": 2500.0, \"r\": 700.0} # ๊ฐ€์ƒ ๋น„์ธ๊ฐ€ ๋น„ํ–‰๊ตฌ์—ญ\n", "WATCH = {\"x\": -2000.0, \"y\": 1500.0, \"r\": 600.0} # ๊ฐ€์ƒ ๊ด€์‹ฌ ๊ตฌ์—ญ\n", "SCENARIOS = [\"sharp_turn\", \"hover\", \"dive\", \"deviation\", \"nfz_entry\",\n", " \"low_level\", \"rc_silent\", \"infiltration\", \"loiter\", \"speed_burst\"]\n", "OPERATORS = [\"friendly\", \"civil\", \"hostile\"]\n", "# ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์šด์šฉ ์ฃผ์ฒด ๋ถ„ํฌ: ์ผ๋ถ€๋Ÿฌ ํฌ๊ฒŒ ๊ฒน์น˜๊ฒŒ ์„ค๊ณ„ํ–ˆ๋‹ค(๋น„ํ–‰ ํŠน์„ฑ๋งŒ์œผ๋กœ ์šด์šฉ ์ฃผ์ฒด๋ฅผ ๋‹จ์ •ํ•  ์ˆ˜ ์—†๋‹ค).\n", "OPERATOR_P = {\"normal\": (.45, .47, .08), \"survey\": (.15, .82, .03), \"sharp_turn\": (.40, .35, .25),\n", " \"hover\": (.25, .45, .30), \"dive\": (.25, .25, .50), \"deviation\": (.30, .40, .30),\n", " \"nfz_entry\": (.10, .50, .40), \"low_level\": (.35, .15, .50), \"rc_silent\": (.30, .15, .55),\n", " \"infiltration\": (.12, .08, .80), \"loiter\": (.30, .20, .50), \"speed_burst\": (.30, .20, .50)}\n", "PLATFORM_P = {\"friendly\": (.40, .55, .05), \"civil\": (.88, .02, .10), \"hostile\": (.55, .30, .15)} # commercial/military/unknown\n", "FIRMWARE_P = {\"friendly\": (.30, .65, .05), \"civil\": (.85, .10, .05), \"hostile\": (.20, .50, .30)} # stock/custom/unknown\n", "\n", "\n", "def _wrap(a):\n", " return (a + 180.0) % 360.0 - 180.0\n", "\n", "\n", "def _bearing(p, q):\n", " return math.degrees(math.atan2(q[0] - p[0], q[1] - p[1])) % 360.0\n", "\n", "\n", "def _away_from_nfz(rng, min_d):\n", " while True:\n", " c = rng.uniform(-3800, 3800, 2)\n", " if np.hypot(c[0] - NFZ[\"x\"], c[1] - NFZ[\"y\"]) > min_d:\n", " return c\n", "\n", "\n", "def make_flight(fid, kind, rng, n=300, weather=None):\n", " \"\"\"๋น„ํ–‰ 1ํšŒ = (๋กœ๊ทธ DataFrame, ๋ฉ”ํƒ€ dict). kind: normal / survey / SCENARIOS ์ค‘ ํ•˜๋‚˜\"\"\"\n", " weather = weather or str(rng.choice([\"clear\", \"night\", \"windy\", \"rain\"], p=[.4, .25, .2, .15]))\n", " wind = {\"clear\": 1.0, \"night\": 1.0, \"windy\": 2.5, \"rain\": 1.8}[weather]\n", " v0, alt0 = rng.uniform(11, 17), rng.uniform(70, 150)\n", " nfz_c = np.array([NFZ[\"x\"], NFZ[\"y\"]])\n", " if kind == \"infiltration\": # ์›๊ฑฐ๋ฆฌ์—์„œ ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ๋ฐฉํ–ฅ์œผ๋กœ ์ €๊ณ ๋„ ์ง์ง„\n", " ang = rng.uniform(0, 2 * np.pi)\n", " start = nfz_c + 4000 * np.array([np.cos(ang), np.sin(ang)])\n", " plan = np.array([start, nfz_c])\n", " v0, alt0 = rng.uniform(9, 11), rng.uniform(25, 35)\n", " else:\n", " if kind == \"nfz_entry\": # ๋น„์ธ๊ฐ€ ๊ตฌ์—ญ 1.6km ๋ฐ–์—์„œ ์ˆœ์ฐฐํ•˜๋‹ค๊ฐ€ ์ง„์ž…\n", " ang = rng.uniform(0, 2 * np.pi)\n", " c = nfz_c + 1600 * np.array([np.cos(ang), np.sin(ang)])\n", " else:\n", " c = _away_from_nfz(rng, 2600)\n", " if kind == \"survey\": # ๋ฏผ๊ฐ„ ์ธก๋Ÿ‰ํ˜• ์™•๋ณต(์ง€๊ทธ์žฌ๊ทธ) ๊ฒฝ๋กœ\n", " w, h = rng.uniform(500, 700), 120.0\n", " plan = np.array([c + [(-w if i % 4 in (0, 3) else w), i // 2 * h - 360] for i in range(8)])\n", " else: # ์‚ฌ๊ฐ ์ˆœ์ฐฐ ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ 4๊ฐœ)\n", " a, b, th = rng.uniform(300, 600), rng.uniform(200, 400), rng.uniform(0, np.pi)\n", " R = np.array([[np.cos(th), -np.sin(th)], [np.sin(th), np.cos(th)]])\n", " plan = c + np.array([[-a, -b], [a, -b], [a, b], [-a, b]]) @ R.T\n", " s0 = int(rng.integers(80, 160))\n", " span = {\"sharp_turn\": 30, \"hover\": 60, \"dive\": 8, \"deviation\": 70, \"nfz_entry\": 120, \"low_level\": 120,\n", " \"rc_silent\": 120, \"infiltration\": n - 20, \"loiter\": 90, \"speed_burst\": 25}.get(kind, 0)\n", " if kind == \"infiltration\":\n", " s0 = 20\n", " win = (s0, s0 + span)\n", " turns = set(rng.choice(np.arange(s0, s0 + 30), 3, replace=False).tolist()) if kind == \"sharp_turn\" else set()\n", " # ๋‚œ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ํ•œ ๋ฒˆ์— ๋ฝ‘์•„ ๋‘๊ณ (์†๋„ ํ–ฅ์ƒ) 1์ดˆ ๋‹จ์œ„๋กœ ๊ธฐ์ฒด ์šด๋™์„ ์ ๋ถ„ํ•œ๋‹ค.\n", " e_h, e_v, e_a = rng.normal(0, 0.8 * wind, n), rng.normal(0, 0.3 * wind, n), rng.normal(0, 0.4 * wind, n)\n", " rc_arr = np.clip(rng.normal(rng.uniform(60, 92), 6, n), 0, 100)\n", " gust = (rng.random(n) < (0.004 if weather == \"windy\" else 0.0)) * rng.uniform(20, 35, n)\n", " burst, turn_amt = rng.uniform(23, 28), rng.choice([-1, 1], n) * rng.uniform(100, 160, n)\n", " side = float(rng.choice([-90, 90]))\n", " low_alt, rng_rc = rng.uniform(35, 55), rng.uniform(0, 20, n) # ์ €๊ณ ๋„ ๋ชฉํ‘œ, ์กฐ์ข…์‹ ํ˜ธ ๋‘์ ˆ ์‹œ ์ž”์—ฌ ์ˆ˜์‹ ๊ฐ’\n", " x, y, wi = float(plan[0][0]), float(plan[0][1]), 1\n", " hdg, alt = _bearing((x, y), plan[1]), alt0\n", " max_rate = 40.0 if kind == \"survey\" else 12.0\n", " dev_target, X, Y, A, V, HD = None, np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n), np.zeros(n)\n", " for t in range(n):\n", " inw = win[0] <= t < win[1]\n", " tx, ty = plan[1] if kind == \"infiltration\" else plan[wi % len(plan)]\n", " spd_cmd, alt_cmd = v0, alt0\n", " if inw:\n", " if kind == \"deviation\":\n", " if dev_target is None:\n", " r = math.radians(hdg + side)\n", " dev_target = (x + 900 * math.sin(r), y + 900 * math.cos(r))\n", " tx, ty = dev_target\n", " elif kind == \"nfz_entry\":\n", " tx, ty = NFZ[\"x\"], NFZ[\"y\"]\n", " elif kind == \"hover\":\n", " spd_cmd = 0.0\n", " elif kind == \"speed_burst\":\n", " spd_cmd = burst\n", " elif kind == \"low_level\":\n", " alt_cmd = low_alt\n", " if kind in (\"rc_silent\", \"infiltration\"):\n", " rc_arr[t] = rng_rc[t]\n", " if inw and kind == \"loiter\":\n", " hdg = (hdg + 9.0 + e_h[t] / wind) % 360\n", " else:\n", " d = _wrap(math.degrees(math.atan2(tx - x, ty - y)) - hdg)\n", " hdg = (hdg + max(-max_rate, min(max_rate, d)) + e_h[t]) % 360\n", " if t in turns:\n", " hdg = (hdg + turn_amt[t]) % 360\n", " v = abs(spd_cmd + e_v[t]) if spd_cmd > 0 else abs(e_v[t] / wind)\n", " x += v * math.sin(math.radians(hdg)); y += v * math.cos(math.radians(hdg))\n", " if kind != \"infiltration\" and math.hypot(x - tx, y - ty) < 40 and not (inw and kind in (\"deviation\", \"nfz_entry\")):\n", " wi += 1\n", " if inw and kind == \"dive\":\n", " alt -= 9.0 + e_v[t]\n", " else:\n", " alt += max(-3.0, min(3.0, alt_cmd - alt)) + e_a[t]\n", " alt -= gust[t] # ๋Œํ’ ํ•˜๊ฐ•๊ธฐ๋ฅ˜(์ •์ƒ ๋น„ํ–‰์—์„œ๋„ ๋ฐœ์ƒ)\n", " if kind == \"dive\" and t >= win[1]:\n", " alt0 = min(alt0, alt)\n", " alt = max(alt, 3.0)\n", " X[t], Y[t], A[t], V[t], HD[t] = x, y, alt, v, hdg\n", " gps_sd = {\"clear\": 2.0, \"night\": 2.0, \"windy\": 3.0, \"rain\": 4.5}[weather] # ์•…์ฒœํ›„์ผ์ˆ˜๋ก GPS ์žก์Œ(๋‹ค์ค‘๊ฒฝ๋กœ) ์ฆ๊ฐ€\n", " df = pd.DataFrame({\"t\": np.arange(n), \"x\": X + rng.normal(0, gps_sd, n), \"y\": Y + rng.normal(0, gps_sd, n),\n", " \"alt\": A + rng.normal(0, 0.5, n), \"speed\": V + rng.normal(0, 0.2, n),\n", " \"heading\": (HD + rng.normal(0, 1.5, n)) % 360, \"rc\": rc_arr.round(1),\n", " \"sats\": rng.integers(10, 15, n), \"hdop\": rng.uniform(0.6, 1.2, n).round(2),\n", " \"x_true\": X, \"y_true\": Y})\n", " df[\"anomaly\"] = (df[\"t\"] >= win[0]) & (df[\"t\"] < win[1]) & (kind in SCENARIOS)\n", " op = OPERATORS[int(rng.choice(3, p=OPERATOR_P[kind]))]\n", " meta = {\"id\": fid, \"kind\": kind, \"weather\": weather, \"operator\": op, \"plan\": plan, \"window\": win if kind in SCENARIOS else None,\n", " \"platform\": [\"commercial\", \"military\", \"unknown\"][int(rng.choice(3, p=PLATFORM_P[op]))],\n", " \"firmware\": [\"stock\", \"custom\", \"unknown\"][int(rng.choice(3, p=FIRMWARE_P[op]))]}\n", " return df, meta\n", "\n", "\n", "def corrupt(df, rng, spoof=True, emi=True, drop=True, dup=True):\n", " \"\"\"์ „์žฅ ์žก์Œ ์ฃผ์ž…: GPS ์Šคํ‘ธํ•‘(์œ„์น˜ ์ ํ”„ยทํ‘œ๋ฅ˜), ์ „์ž๊ธฐ ๊ฐ„์„ญ(๊ณ ๋„ ์ŠคํŒŒ์ดํฌ), ์„ผ์„œ ๋‹จ์ ˆ(๊ฒฐ์ธก), ์ค‘๋ณต ๊ธฐ๋ก.\"\"\"\n", " d = df.copy()\n", " d[\"spoof_true\"] = False\n", " if spoof:\n", " a = int(rng.integers(40, len(d) - 60)); L = int(rng.integers(25, 45))\n", " th = rng.uniform(0, 2 * np.pi)\n", " drift = rng.uniform(22, 28) * np.arange(L)[:, None] * np.array([np.cos(th), np.sin(th)])\n", " jump = rng.uniform(250, 400) * np.array([np.cos(th + 1), np.sin(th + 1)])\n", " d.loc[a:a + L - 1, [\"x\", \"y\"]] += drift + jump\n", " d.loc[a:a + L - 1, \"spoof_true\"] = True\n", " if emi:\n", " idx = rng.choice(len(d), 6, replace=False)\n", " d.loc[idx, \"alt\"] += rng.choice([-1, 1], 6) * rng.uniform(60, 150, 6)\n", " if drop:\n", " a = int(rng.integers(10, len(d) - 20))\n", " d.loc[a:a + int(rng.integers(4, 9)), [\"x\", \"y\", \"alt\", \"speed\", \"heading\"]] = np.nan\n", " d = d.drop(index=rng.choice(len(d), 5, replace=False))\n", " if dup:\n", " d = pd.concat([d, d.sample(5, random_state=int(rng.integers(1e6)))]).sort_values(\"t\", kind=\"stable\")\n", " return d.reset_index(drop=True)\n", "\n", "\n", "def make_dataset(n=200, seed=7, anomaly_ratio=0.5, scenarios=SCENARIOS):\n", " rng = np.random.default_rng(seed)\n", " out = []\n", " for i in range(n):\n", " if i % 10 < anomaly_ratio * 10:\n", " kind = scenarios[(i // 10 * int(anomaly_ratio * 10) + i % 10) % len(scenarios)]\n", " else:\n", " kind = \"survey\" if i % 10 == 9 else \"normal\"\n", " df, meta = make_flight(f\"FL{i:04d}\", kind, rng)\n", " out.append((df, meta))\n", " return out\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ์ „์ฒ˜๋ฆฌยท๊ทœ์น™ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "SPOOF_THR = {\"clear\": 12.0, \"night\": 12.0, \"windy\": 15.0, \"rain\": 19.0} # ๊ธฐ์ƒ๋ณ„ GPS-์ถ”์ธกํ•ญ๋ฒ• ๋ถˆ์ผ์น˜ ํ—ˆ์šฉ์น˜(m)\n", "\n", "\n", "def clean(df, weather=\"clear\"):\n", " \"\"\"์ „์ฒ˜๋ฆฌ: ์ค‘๋ณต ์ œ๊ฑฐยท์ •๋ ฌ -> 1์ดˆ ์žฌํ‘œ๋ณธํ™” -> ๊ฒฐ์ธก ๋ณด๊ฐ„ -> ๊ณ ๋„ ์ŠคํŒŒ์ดํฌ ์ œ๊ฑฐ -> GPS ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„ ์ถ”์ธกํ•ญ๋ฒ• ๋ณด์ •.\"\"\"\n", " d = df.drop_duplicates(\"t\").sort_values(\"t\").set_index(\"t\")\n", " d = d.reindex(np.arange(int(d.index.min()), int(d.index.max()) + 1))\n", " n_missing = int(d[\"x\"].isna().sum())\n", " num = [\"x\", \"y\", \"alt\", \"speed\", \"rc\", \"sats\", \"hdop\"]\n", " d[num] = d[num].interpolate(limit_direction=\"both\")\n", " hd = d[\"heading\"].dropna() # ๋ฐฉ์œ„๋Š” 0/360 ๊ฒฝ๊ณ„๋ฅผ ํ’€์–ด์„œ ๋ณด๊ฐ„\n", " un = pd.Series(np.degrees(np.unwrap(np.radians(hd.to_numpy(float)))), index=hd.index)\n", " d[\"heading\"] = un.reindex(d.index).interpolate(limit_direction=\"both\").to_numpy() % 360\n", " med = d[\"alt\"].rolling(5, center=True, min_periods=1).median()\n", " spike = (d[\"alt\"] - med).abs() > 25\n", " d.loc[spike, \"alt\"] = med[spike]\n", " x, y = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float)\n", " r = np.radians(d[\"heading\"].to_numpy(float)); v = d[\"speed\"].to_numpy(float)\n", " dr = np.c_[v * np.sin(r), v * np.cos(r)] # ๋ณด๊ณ  ์†๋„ยท๋ฐฉ์œ„๋กœ ๊ณ„์‚ฐํ•œ 1์ดˆ ์ด๋™๋Ÿ‰(์ถ”์ธกํ•ญ๋ฒ•)\n", " xc, yc, flag = x.copy(), y.copy(), np.zeros(len(x), bool)\n", " thr = SPOOF_THR.get(weather, 12.0)\n", " ok_run = 0\n", " for i in range(1, len(x)):\n", " px, py = xc[i - 1] + dr[i, 0], yc[i - 1] + dr[i, 1] # ์ถ”์ธกํ•ญ๋ฒ• ์˜ˆ์ƒ ์œ„์น˜\n", " step_ok = np.hypot(x[i] - x[i - 1] - dr[i, 0], y[i] - y[i - 1] - dr[i, 1]) <= thr\n", " ok_run = ok_run + 1 if step_ok else 0\n", " if flag[i - 1]:\n", " # ์Šคํ‘ธํ•‘ ๊ตฌ๊ฐ„: GPS๊ฐ€ ์˜ˆ์ƒ ์œ„์น˜๋กœ ๋Œ์•„์˜ค๊ฑฐ๋‚˜, GPS ์ด๋™๋Ÿ‰์ด 5์ดˆ ์—ฐ์† ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์œผ๋ฉด GPS๋กœ ๋ณต๊ท€\n", " if np.hypot(x[i] - px, y[i] - py) < thr * 2 or ok_run >= 5:\n", " xc[i], yc[i] = x[i], y[i]\n", " else:\n", " xc[i], yc[i], flag[i] = px, py, True\n", " elif not step_ok:\n", " xc[i], yc[i], flag[i] = px, py, True # GPS ์ด๋™๋Ÿ‰์ด ๋ฌผ๋ฆฌ์ ์œผ๋กœ ๋งž์ง€ ์•Š์Œ -> ์Šคํ‘ธํ•‘ ์˜์‹ฌ\n", " else:\n", " xc[i], yc[i] = xc[i - 1] + x[i] - x[i - 1], yc[i - 1] + y[i] - y[i - 1]\n", " d[\"x\"], d[\"y\"], d[\"spoof_flag\"] = xc, yc, flag\n", " d = d.reset_index()\n", " d.attrs.update({\"missing\": n_missing, \"spikes\": int(spike.sum()), \"spoof_pts\": int(flag.sum())})\n", " return d\n", "\n", "\n", "def xtrack(px, py, plan):\n", " \"\"\"๊ณ„ํš ๊ฒฝ๋กœ(์›จ์ดํฌ์ธํŠธ ํ๊ณก์„ ) ๋Œ€๋น„ ์ดํƒˆ ๊ฑฐ๋ฆฌ(m).\"\"\"\n", " P = np.asarray(plan, float)\n", " segs = [(P[i], P[(i + 1) % len(P)]) for i in range(len(P) if len(P) > 2 else 1)]\n", " best = np.full(len(px), np.inf)\n", " for a, b in segs:\n", " ab = b - a\n", " u = np.clip(((px - a[0]) * ab[0] + (py - a[1]) * ab[1]) / max(ab @ ab, 1e-9), 0, 1)\n", " best = np.minimum(best, np.hypot(px - a[0] - u * ab[0], py - a[1] - u * ab[1]))\n", " return best\n", "\n", "\n", "RULES = {\"turn_rate\": 45.0, \"hover_speed\": 1.5, \"hover_sec\": 20, \"dive_m\": 30.0, \"xtrack_m\": 250.0}\n", "\n", "\n", "def rule_flags(d, plan, R=RULES):\n", " \"\"\"๊ทœ์น™ ๊ธฐ๋ฐ˜ ์ด์ƒ ๋น„ํ–‰ ํŒ์ •(๊ธฐ๋ก ๋‹จ์œ„). ๊ธ‰์„ ํšŒยทํ˜ธ๋ฒ„๋งยท๊ธ‰๊ฐ•ํ•˜ยท๊ฒฝ๋กœ ์ดํƒˆยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ง„์ž….\"\"\"\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " slow = (d[\"speed\"] < R[\"hover_speed\"]).to_numpy()\n", " run = np.zeros(len(slow), int)\n", " for i in range(len(slow)):\n", " run[i] = run[i - 1] + 1 if slow[i] and i else int(slow[i])\n", " hover = np.zeros(len(slow), bool)\n", " for i in np.where(run >= R[\"hover_sec\"])[0]:\n", " hover[i - R[\"hover_sec\"] + 1:i + 1] = True\n", " alt = d[\"alt\"].to_numpy(float)\n", " drop = alt - np.r_[alt[5:], np.full(5, alt[-1])]\n", " return {\"๊ธ‰์„ ํšŒ\": hr > R[\"turn_rate\"], \"ํ˜ธ๋ฒ„๋ง\": hover, \"๊ธ‰๊ฐ•ํ•˜\": drop > R[\"dive_m\"],\n", " \"๊ฒฝ๋กœ์ดํƒˆ\": xtrack(d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), plan) > R[\"xtrack_m\"],\n", " \"๋น„์ธ๊ฐ€๊ตฌ์—ญ\": np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).to_numpy() < NFZ[\"r\"]}\n", "\n", "# ---- ๊ตฌ๊ฐ„(30์ดˆ ์ฐฝ) ํŠน์ง• + ๋น„์ง€๋„ ์ด์ƒ ํƒ์ง€ ๊ณตํ†ต ํ•จ์ˆ˜ ----\n", "WFEAT = [\"alt_mean\", \"alt_min\", \"alt_std\", \"vz_min\", \"spd_mean\", \"spd_std\", \"spd_max\", \"turn_sum\", \"turn_max\",\n", " \"straight\", \"rc_mean\", \"rc_zero\", \"nfz_min\", \"nfz_close\", \"xtrack_max\"]\n", "\n", "\n", "def window_feats(d, plan, win=30, step=10):\n", " \"\"\"30์ดˆ ์ฐฝ์„ 10์ดˆ์”ฉ ์˜ฎ๊ธฐ๋ฉฐ ๊ณ ๋„ยท์†๋„ยท์„ ํšŒยท์ง์ง„์„ฑยท์กฐ์ข…์‹ ํ˜ธยท๋น„์ธ๊ฐ€ ๊ตฌ์—ญ ์ ‘๊ทผยท๊ฒฝ๋กœ ์ดํƒˆ ํŠน์ง•์„ ๊ณ„์‚ฐํ•œ๋‹ค.\"\"\"\n", " x, y, alt = d[\"x\"].to_numpy(float), d[\"y\"].to_numpy(float), d[\"alt\"].to_numpy(float)\n", " spd, rc = d[\"speed\"].to_numpy(float), d[\"rc\"].to_numpy(float)\n", " hr = np.abs(_wrap(np.diff(d[\"heading\"].to_numpy(float), prepend=d[\"heading\"].iloc[0])))\n", " vz = np.diff(alt, prepend=alt[0])\n", " nd = np.hypot(x - NFZ[\"x\"], y - NFZ[\"y\"])\n", " xt = xtrack(x, y, plan)\n", " step_len = np.hypot(np.diff(x, prepend=x[0]), np.diff(y, prepend=y[0]))\n", " rows, starts = [], []\n", " for s in range(0, len(d) - win + 1, step):\n", " e = s + win\n", " path = step_len[s + 1:e].sum()\n", " rows.append([alt[s:e].mean(), alt[s:e].min(), alt[s:e].std(), vz[s:e].min(), spd[s:e].mean(), spd[s:e].std(),\n", " spd[s:e].max(), hr[s:e].sum(), hr[s:e].max(), np.hypot(x[e - 1] - x[s], y[e - 1] - y[s]) / max(path, 1.0),\n", " rc[s:e].mean(), (rc[s:e] < 5).mean(), nd[s:e].min(), (nd[s] - nd[e - 1]) / win, xt[s:e].max()])\n", " starts.append(int(d[\"t\"].iloc[s]))\n", " return np.array(rows), starts\n", "\n", "\n", "class FlightAnomalyModel:\n", " \"\"\"ํ‰์‹œ ๋น„ํ–‰๋กœ๊ทธ(๋ผ๋ฒจ ์—†์Œ)๋กœ ํ†ต์ƒ ํŒจํ„ด ๊ตฐ์ง‘(KMeans)๊ณผ ๊ณ ๋ฆฝ ์ˆฒ(IsolationForest)์„ ํ•™์Šตํ•˜๊ณ ,\n", " ๋‘ ์ ์ˆ˜๋ฅผ ํ•™์Šต ๊ตฌ๊ฐ„ ๋ถ„์œ„์ˆ˜๋กœ ๋‚˜๋ˆ  1.0์„ ๋„˜์œผ๋ฉด 'ํ†ต์ƒ ํŒจํ„ด์—์„œ ๋ฒ—์–ด๋‚จ'์œผ๋กœ ๋ณธ๋‹ค.\"\"\"\n", "\n", " def __init__(self, q=0.999, k=4):\n", " self.q, self.k = q, k\n", "\n", " def fit(self, X):\n", " self.sc = StandardScaler().fit(X)\n", " Z = self.sc.transform(X)\n", " self.iso = IsolationForest(n_estimators=150, random_state=0).fit(Z)\n", " self.km = KMeans(self.k, n_init=5, random_state=0).fit(Z)\n", " self.set_q(self.q, X)\n", " return self\n", "\n", " def _raw(self, X):\n", " Z = self.sc.transform(X)\n", " return -self.iso.score_samples(Z), self.km.transform(Z).min(axis=1)\n", "\n", " def set_q(self, q, X):\n", " a, b = self._raw(X)\n", " self.q, self.t_if, self.t_km = q, np.quantile(a, q), np.quantile(b, q)\n", "\n", " def score(self, X):\n", " a, b = self._raw(X)\n", " return np.maximum(a / self.t_if, b / self.t_km)\n", "\n", "\n", "AFEAT = [\"ml_score\", \"low_alt\", \"rc_weak\", \"nfz_min_km\", \"hover_s\", \"max_speed\", \"night\",\n", " \"pf_commercial\", \"pf_military\", \"pf_unknown\", \"fw_stock\", \"fw_custom\", \"fw_unknown\"]\n", "\n", "\n", "def attr_features(d, m, ml_score):\n", " \"\"\"์šด์šฉ ์ฃผ์ฒด ์ถ”์ •์šฉ ๋น„ํ–‰ ๋‹จ์œ„ ํŠน์ง•. ๊ธฐ์ฒดยทํŽŒ์›จ์–ด ์ •๋ณด๋Š” ์ƒ์šฉ ๊ธฐ์ฒด๊ฐ€ ๋ชจ๋“  ์ฃผ์ฒด์— ์“ฐ์ด๋ฏ€๋กœ ์•ฝํ•œ ๋‹จ์„œ์ผ ๋ฟ์ด๋‹ค.\"\"\"\n", " return [min(ml_score, 10.0), float((d[\"alt\"] < 60).mean()), float((d[\"rc\"] < 25).mean()),\n", " float(np.hypot(d[\"x\"] - NFZ[\"x\"], d[\"y\"] - NFZ[\"y\"]).min() / 1000), float((d[\"speed\"] < 1.5).sum()),\n", " float(d[\"speed\"].max()), float(m[\"weather\"] == \"night\"),\n", " *[float(m[\"platform\"] == p) for p in (\"commercial\", \"military\", \"unknown\")],\n", " *[float(m[\"firmware\"] == f) for f in (\"stock\", \"custom\", \"unknown\")]]\n", "\n", "CONFIG = {\"turn_rate\": 60, \"dive_m\": 40, \"ml_threshold\": 1.2, \"ml_quantile\": 0.995, # ๋ฏธ์…˜ 9 ์žฌ๋ณด์ •๊ฐ’\n", " \"p_high\": 0.60, \"h_max\": 0.80, \"p_med\": 0.25}\n", "CALLED = set()\n", "\n", "\n", "def cover(fn):\n", " \"\"\"๊ธฐ๋Šฅ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •์šฉ ํ‘œ์‹œ: ํ†ตํ•ฉ ์‹คํ–‰ ์ค‘ ํ˜ธ์ถœ๋œ ๊ธฐ๋Šฅ์„ ๊ธฐ๋กํ•œ๋‹ค.\"\"\"\n", " def wrap(*a, **k):\n", " CALLED.add(fn.__name__)\n", " return fn(*a, **k)\n", " wrap.__name__ = fn.__name__\n", " return wrap\n", "\n", "\n", "@cover\n", "def validate_log(df, m):\n", " errs = [f\"{c} ๋ˆ„๋ฝ\" for c in (\"t\", \"x\", \"y\", \"alt\", \"speed\", \"heading\", \"rc\") if c not in df]\n", " if m[\"weather\"] not in SPOOF_THR:\n", " errs.append(\"weather ํ—ˆ์šฉ๊ฐ’ ์•„๋‹˜\")\n", " if not errs and ((df[\"speed\"] > 60).any() or (df[\"rc\"] > 100).any()):\n", " errs.append(\"๋ฒ”์œ„ ์˜ค๋ฅ˜\")\n", " return errs\n", "\n", "\n", "@cover\n", "def grade(p, h, q_ok):\n", " if p >= CONFIG[\"p_high\"] and h <= CONFIG[\"h_max\"] and q_ok:\n", " return \"High\"\n", " return \"Medium\" if p >= CONFIG[\"p_med\"] else \"Low\"\n", "\n", "\n", "@cover\n", "def statement(fid, g, p, h, kinds):\n", " return f\"{fid}: ์ด์ƒ ๋น„ํ–‰({kinds}), ์ ๋Œ€ ์šด์šฉ ๊ฐ€๋Šฅ์„ฑ {g}(p={p:.2f}, ๋ถˆํ™•์‹ค์„ฑ {h:.2f}) - ์ •๋ณด๋ถ„์„๊ด€ ๊ฒ€ํ†  ํ•„์š”\"\n", "\n", "\n", "class Queue:\n", " def __init__(self):\n", " self.items, self.log = [], []\n", "\n", " @cover\n", " def submit(self, fid, g, text):\n", " prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n", " body = {\"event\": \"SUBMIT\", \"id\": fid, \"grade\": g, \"text\": text}\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({**body, \"status\": \"PENDING\"})\n", "\n", " @cover\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", "clean, rule_flags, window_feats, attr_features, corrupt = map(cover, (clean, rule_flags, window_feats, attr_features, corrupt))\n", "FlightAnomalyModel.score = cover(FlightAnomalyModel.score)\n", "TARGETS = {\"validate_log\", \"clean\", \"rule_flags\", \"window_feats\", \"score\", \"attr_features\", \"grade\", \"statement\", \"submit\", \"verify\", \"corrupt\", \"dashboard\"}\n", "\n", "\n", "def analyze(df, m, det):\n", " d = clean(df, m[\"weather\"])\n", " f = rule_flags(d, m[\"plan\"], {**RULES, \"turn_rate\": CONFIG[\"turn_rate\"], \"dive_m\": CONFIG[\"dive_m\"]})\n", " s = float(det.score(window_feats(d, m[\"plan\"])[0]).max())\n", " hits = [k for k, v in f.items() if v.any()] + ([\"ML์ด์ƒ\"] if s > CONFIG[\"ml_threshold\"] else [])\n", " return d, hits, s\n", "\n", "\n", "# ---------------- 1. ํ•™์Šต(ํ‰์‹œ ํŒจํ„ด + ๊ณผ๊ฑฐ ๊ฒ€ํ†  ์™„๋ฃŒ ์‚ฌ๋ก€) ----------------\n", "t_start = time.perf_counter()\n", "routine = make_dataset(80, 21, anomaly_ratio=0.0) + [(df, m) for df, m in make_dataset(200, 22, anomaly_ratio=0.0) if m[\"kind\"] == \"survey\" or m[\"weather\"] == \"windy\"][:55]\n", "det = FlightAnomalyModel(q=CONFIG[\"ml_quantile\"]).fit(np.vstack([window_feats(clean(df, m[\"weather\"]), m[\"plan\"])[0] for df, m in routine]))\n", "past = []\n", "for df, m in make_dataset(160, 31):\n", " d, hits, s = analyze(df, m, det)\n", " past.append((attr_features(d, m, s), m[\"operator\"]))\n", "eng = make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, C=0.5)).fit([p[0] for p in past], [p[1] for p in past])\n", "\n", "# ---------------- 2. ์šด์šฉ ์ž๋ฃŒ ์ฒ˜๋ฆฌ ----------------\n", "rng = np.random.default_rng(10)\n", "ops = make_dataset(100, 11)\n", "ops = [(corrupt(df, rng) if rng.random() < 0.25 else df, m) for df, m in ops]\n", "ops[7] = (ops[7][0].drop(columns=[\"rc\"]), ops[7][1]) # ์†์ƒ ๋กœ๊ทธ 1๊ฑด ์ฃผ์ž…(๊ฒ€์ฆ ๋‹จ๊ณ„์—์„œ ์ฐจ๋‹จ๋˜์–ด์•ผ ํ•จ)\n", "queue, rejected, res, ms = Queue(), {}, [], []\n", "for df, m in ops:\n", " errs = validate_log(df, m)\n", " if errs:\n", " rejected[m[\"id\"]] = errs; continue\n", " t0 = time.perf_counter()\n", " d, hits, s = analyze(df, m, det)\n", " P = eng.predict_proba([attr_features(d, m, s)])[0]\n", " ph = P[list(eng.classes_).index(\"hostile\")]; H = float(-(P * np.log(np.clip(P, 1e-9, 1))).sum() / np.log(3))\n", " q_ok = d.attrs[\"spoof_pts\"] < 10 and d.attrs[\"missing\"] < 15\n", " g = grade(ph, H, q_ok) if hits else \"-\"\n", " if hits:\n", " queue.submit(m[\"id\"], g, statement(m[\"id\"], g, ph, H, \",\".join(hits)))\n", " ms.append((time.perf_counter() - t0) * 1000)\n", " res.append({\"id\": m[\"id\"], \"kind\": m[\"kind\"], \"op\": m[\"operator\"], \"truth\": m[\"kind\"] in SCENARIOS, \"pred\": bool(hits), \"grade\": g, \"d\": d})\n", "\n", "# ---------------- 3. ์„ฑ๋Šฅ ํ‰๊ฐ€ ----------------\n", "R = pd.DataFrame(res)\n", "acc = (R.truth == R.pred).mean(); rec = (R.truth & R.pred).sum() / R.truth.sum(); far = (~R.truth & R.pred).sum() / max(R.pred.sum(), 1)\n", "high = R[R.grade == \"High\"]; hfar = (high.op != \"hostile\").mean() if len(high) else 0.0\n", "\n", "# ---------------- 4. ๋Œ€์‹œ๋ณด๋“œ ----------------\n", "@cover\n", "def dashboard():\n", " plt.style.use(\"dark_background\")\n", " fig, ax = plt.subplots(2, 2, figsize=(12, 8))\n", " for _, r in R[R.pred].iterrows():\n", " ax[0, 0].plot(r.d[\"x\"], r.d[\"y\"], lw=0.8, color={\"High\": \"red\", \"Medium\": \"orange\", \"Low\": \"gray\"}[r.grade])\n", " ax[0, 0].add_patch(plt.Circle((NFZ[\"x\"], NFZ[\"y\"]), NFZ[\"r\"], color=\"red\", alpha=.2)); ax[0, 0].set_aspect(\"equal\")\n", " ax[0, 0].set_title(\"Flagged flights (color = attribution grade)\")\n", " R[R.pred][\"grade\"].value_counts().reindex([\"High\", \"Medium\", \"Low\"]).fillna(0).plot.bar(ax=ax[0, 1], color=[\"red\", \"orange\", \"gray\"])\n", " ax[0, 1].set_title(\"Review queue by grade (all pending)\")\n", " R[R.pred][\"kind\"].value_counts().plot.barh(ax=ax[1, 0], color=\"deepskyblue\"); ax[1, 0].set_title(\"Flagged flights by true scenario\")\n", " ax[1, 1].axis(\"off\")\n", " ax[1, 1].text(0, .9, f\"Accuracy {acc:.1%}\\nRecall {rec:.1%}\\nFalse alarm {far:.1%}\\nQueue {len(queue.items)} pending\\nLatency {np.mean(ms):.0f} ms/flight\",\n", " family=\"monospace\", fontsize=13, va=\"top\")\n", " fig.suptitle(\"ISR03 integrated drone-log analysis - training data only\"); plt.tight_layout(); plt.show()\n", "\n", "\n", "dashboard()\n", "\n", "# ---------------- 5. ์ž์ฒด ์ ๊ฒ€(ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ) ----------------\n", "log_ok = queue.verify()\n", "cov = len(CALLED & TARGETS) / len(TARGETS)\n", "chk = {\n", " \"์†์ƒ ๋กœ๊ทธ ์ฐจ๋‹จ\": \"FL0007\" in rejected,\n", " \"์ฒ˜๋ฆฌ ๋น„ํ–‰ 99๊ฑด\": len(res) == 99,\n", " \"์ด์ƒ ๋น„ํ–‰ ์žฌํ˜„์œจ 90% ์ด์ƒ\": rec >= 0.9,\n", " \"์ •ํ™•๋„ 90% ์ด์ƒ\": acc >= 0.9,\n", " \"์ž๋™ ์ฑ„ํƒ 0๊ฑด(์ „๋ถ€ ๊ฒ€ํ†  ๋Œ€๊ธฐ)\": all(i[\"status\"] == \"PENDING\" for i in queue.items),\n", " \"ํƒ์ง€ ๊ฑด ์ „๋Ÿ‰ ๋Œ€๊ธฐ์—ด ๋“ฑ๋ก\": len(queue.items) == int(R.pred.sum()),\n", " \"๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฌด๊ฒฐ์„ฑ\": log_ok,\n", " \"๋‹จ์ • ํ‘œํ˜„('ํ™•์ •') 0๊ฑด\": not any(\"ํ™•์ •\" in i[\"text\"] for i in queue.items),\n", " \"๋น„ํ–‰ 1๊ฑด 200ms ์ด๋‚ด\": float(np.mean(ms)) < 200,\n", " \"๊ธฐ๋Šฅ ์ปค๋ฒ„๋ฆฌ์ง€ 85% ์ด์ƒ\": cov >= 0.85,\n", "}\n", "passed = sum(chk.values())\n", "for k, v in chk.items():\n", " print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n", "print(f\"์ž์ฒด ์ ๊ฒ€ {passed}/{len(chk)} ํ†ต๊ณผ, ๊ธฐ๋Šฅ ์ปค๋ฒ„๋ฆฌ์ง€ {cov:.0%} ({len(CALLED & TARGETS)}/{len(TARGETS)}), ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์„ฑ๊ณต {'์˜ˆ' if passed == len(chk) else '์•„๋‹ˆ์˜ค'}\")\n", "\n", "# ---------------- 6. ISR ์ •๋ณดํŒ๋‹จ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ ----------------\n", "print(f\"\"\"\n", "[๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ด์ƒ ํƒ์ง€ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ - ๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "1. ์ฒ˜๋ฆฌ: ๋น„ํ–‰๋กœ๊ทธ {len(ops)}๊ฑด ์ค‘ {len(res)}๊ฑด ๋ถ„์„, ์†์ƒ ๋กœ๊ทธ {len(rejected)}๊ฑด ์ฐจ๋‹จ({', '.join(rejected)})\n", "2. ์„ฑ๋Šฅ: ์ •ํ™•๋„ {acc:.1%}, ์ด์ƒ ์žฌํ˜„์œจ {rec:.1%}, ์˜ค๊ฒฝ๋ณด์œจ {far:.1%}, ๋น„ํ–‰๋‹น {np.mean(ms):.0f} ms, ์ „์ฒด {time.perf_counter() - t_start:.0f} s\n", "3. ๊ฒ€ํ†  ๋Œ€๊ธฐ: {len(queue.items)}๊ฑด (High {int((R.grade == 'High').sum())} / Medium {int((R.grade == 'Medium').sum())} / Low {int((R.grade == 'Low').sum())}), ์ž๋™ ์ฑ„ํƒ 0๊ฑด\n", "4. ์šด์šฉ ์ฃผ์ฒด: High ๋“ฑ๊ธ‰ {len(high)}๊ฑด ์ค‘ ์‹ค์ œ ๋น„์ ๋Œ€ {int((high.op != 'hostile').sum())}๊ฑด({hfar:.0%}) -> ๋“ฑ๊ธ‰์€ ๊ฒ€ํ†  ์šฐ์„ ์ˆœ์œ„์ผ ๋ฟ ๊ท€์† ๊ฒฐ๋ก ์ด ์•„๋‹˜\n", "5. ํ•œ๊ณ„: ํ•ฉ์„ฑ ๋กœ๊ทธ ๊ธฐ์ค€ ๊ฒฐ๊ณผ์ด๋ฉฐ, ์šด์šฉ ์ฃผ์ฒด ํŒ๋‹จ์€ ์ถœ์ฒ˜ยท๋ฌด๊ฒฐ์„ฑ ํ™•์ธ๊ณผ 2์ธ ๊ต์ฐจ ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์นœ ์ •๋ณด๋ถ„์„๊ด€ ์˜๊ฒฌ์œผ๋กœ๋งŒ ๋ณด๊ณ ํ•œ๋‹ค.\"\"\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[PASS] ์†์ƒ ๋กœ๊ทธ ์ฐจ๋‹จ\n", "[PASS] ์ฒ˜๋ฆฌ ๋น„ํ–‰ 99๊ฑด\n", "[PASS] ์ด์ƒ ๋น„ํ–‰ ์žฌํ˜„์œจ 90% ์ด์ƒ\n", "[PASS] ์ •ํ™•๋„ 90% ์ด์ƒ\n", "[PASS] ์ž๋™ ์ฑ„ํƒ 0๊ฑด(์ „๋ถ€ ๊ฒ€ํ†  ๋Œ€๊ธฐ)\n", "[PASS] ํƒ์ง€ ๊ฑด ์ „๋Ÿ‰ ๋Œ€๊ธฐ์—ด ๋“ฑ๋ก\n", "[PASS] ๊ฐ์‚ฌ ๋กœ๊ทธ ๋ฌด๊ฒฐ์„ฑ\n", "[PASS] ๋‹จ์ • ํ‘œํ˜„('ํ™•์ •') 0๊ฑด\n", "[PASS] ๋น„ํ–‰ 1๊ฑด 200ms ์ด๋‚ด\n", "[PASS] ๊ธฐ๋Šฅ ์ปค๋ฒ„๋ฆฌ์ง€ 85% ์ด์ƒ\n", "์ž์ฒด ์ ๊ฒ€ 10/10 ํ†ต๊ณผ, ๊ธฐ๋Šฅ ์ปค๋ฒ„๋ฆฌ์ง€ 100% (12/12), ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์„ฑ๊ณต ์˜ˆ\n", "\n", "[๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ด์ƒ ํƒ์ง€ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ - ๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "1. ์ฒ˜๋ฆฌ: ๋น„ํ–‰๋กœ๊ทธ 100๊ฑด ์ค‘ 99๊ฑด ๋ถ„์„, ์†์ƒ ๋กœ๊ทธ 1๊ฑด ์ฐจ๋‹จ(FL0007)\n", "2. ์„ฑ๋Šฅ: ์ •ํ™•๋„ 96.0%, ์ด์ƒ ์žฌํ˜„์œจ 98.0%, ์˜ค๊ฒฝ๋ณด์œจ 5.8%, ๋น„ํ–‰๋‹น 31 ms, ์ „์ฒด 12 s\n", "3. ๊ฒ€ํ†  ๋Œ€๊ธฐ: 52๊ฑด (High 7 / Medium 22 / Low 23), ์ž๋™ ์ฑ„ํƒ 0๊ฑด\n", "4. ์šด์šฉ ์ฃผ์ฒด: High ๋“ฑ๊ธ‰ 7๊ฑด ์ค‘ ์‹ค์ œ ๋น„์ ๋Œ€ 2๊ฑด(29%) -> ๋“ฑ๊ธ‰์€ ๊ฒ€ํ†  ์šฐ์„ ์ˆœ์œ„์ผ ๋ฟ ๊ท€์† ๊ฒฐ๋ก ์ด ์•„๋‹˜\n", "5. ํ•œ๊ณ„: ํ•ฉ์„ฑ ๋กœ๊ทธ ๊ธฐ์ค€ ๊ฒฐ๊ณผ์ด๋ฉฐ, ์šด์šฉ ์ฃผ์ฒด ํŒ๋‹จ์€ ์ถœ์ฒ˜ยท๋ฌด๊ฒฐ์„ฑ ํ™•์ธ๊ณผ 2์ธ ๊ต์ฐจ ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์นœ ์ •๋ณด๋ถ„์„๊ด€ ์˜๊ฒฌ์œผ๋กœ๋งŒ ๋ณด๊ณ ํ•œ๋‹ค.\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }