{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ๐ŸŽฏ [TRN01] ๋“œ๋ก  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ๊ธฐ๋ฐ˜ ์ˆ™๋ จ๋„ ๋ถ„์„๊ธฐ\n", "### Google Colab ์‹ค์Šต ํ™˜๊ฒฝ (๊ต์œกยทํ›ˆ๋ จ / Level 3)\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/edunlab/milicontent2/blob/main/uploads/TRN01/TRN01_solution.ipynb)\n", "\n", "---\n", "\n", "## ๐Ÿ“Œ 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ๋ฐ ๋ฐฐ๊ฒฝ\n", "\n", "| ํ•ญ๋ชฉ | ๋‚ด์šฉ |\n", "| :--- | :--- |\n", "| **๊ณผ์ • ๋ถ„๋ฅ˜** | ๊ต์œกยทํ›ˆ๋ จ (Level 3) |\n", "| **ํ•™์Šต ์†Œ์š” ์‹œ๊ฐ„** | 10๊ฐœ ๋ฏธ์…˜ (5์ผ / ์ด 27.5์‹œ๊ฐ„) |\n", "| **ํ™˜๊ฒฝ ์‚ฌ์–‘** | Python 3.10+, numpy, pandas, matplotlib, scipy, scikit-learn |\n", "\n", "### ๐Ÿ“– ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ\n", "๋Ÿฌ์‹œ์•„-์šฐํฌ๋ผ์ด๋‚˜ ์ „์Ÿ์—์„œ๋Š” ์†Œํ˜• ๋“œ๋ก  ์กฐ์ข… ์ธ๋ ฅ์„ ์งง์€ ๊ธฐ๊ฐ„์— ๋Œ€๋Ÿ‰์œผ๋กœ ์–‘์„ฑํ•ด์•ผ ํ–ˆ๊ณ , ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ›ˆ๋ จ์—์„œ ๋ˆ„๊ฐ€ ์–ด๋–ค ์กฐ์ž‘ ์•ฝ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ์ฐพ์•„ ๊ฐœ์ธ๋ณ„๋กœ ๋ณด์™„ํ•˜๋Š” ์ผ์ด ์ „ํˆฌ๋ ฅ ์œ ์ง€์˜ ๊ด€๊ฑด์ด ๋˜์—ˆ๋‹ค. ๋ณธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ต์œก์šฉ ๊ฐ€์ƒ ๋“œ๋ก  ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ(๊ฒฝ๋กœ ๋น„ํ–‰, ํ˜ธ๋ฒ„๋ง, ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ, ๋น„์ƒ ์ฐฉ๋ฅ™)๋ฅผ ์ฝ”๋“œ๋กœ ์ง์ ‘ ๋งŒ๋“ค์–ด, ๋กœ๊ทธ ์Šคํ‚ค๋งˆ ์ •์˜, ์žก์Œยท๊ฒฐ์ธก ์ „์ฒ˜๋ฆฌ, ๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ ๊ธฐ๋ฐ˜ Baseline ์ฑ„์ , ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„๋ฅผ ๋ฐ˜์˜ํ•œ ์ •๋ฐ€ ํ‰๊ฐ€ ์—”์ง„, ์กฐ์ž‘ ์Šต๊ด€ ๋ถ„์„๊ณผ ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰, 3D ์žฌ์ƒ ๋Œ€์‹œ๋ณด๋“œ, ๊ต๊ด€ 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. ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ(์Šคํ‹ฑ ์กฐ์ž‘, ์ž์„ธ๊ฐ, ๊ณ ๋„ ์œ ์ง€, ๊ฒฝ๋กœ ์˜ค์ฐจ) ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ ์ •์˜\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 1.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ(์Šคํ‹ฑ ์กฐ์ž‘, ์ž์„ธ๊ฐ, ๊ณ ๋„ ์œ ์ง€ ์˜ค์ฐจ) ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ(์Šคํ‹ฑ ์กฐ์ž‘, ์ž์„ธ๊ฐ, ๊ณ ๋„, ์œ„์น˜ยท๋ชฉํ‘œ ๊ฒฝ๋กœ)์™€ ๋น„ํ–‰ ์„ธ์…˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๊ณ  ์ž˜๋ชป๋œ ๋กœ๊ทธ๋ฅผ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ๊ฒ€์ฆ๊ธฐ๋ฅผ ๋งŒ๋“ ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์„ธ์…˜(๊ฐ€๋ช… ์กฐ์ข…์ž ID, ์ฝ”์Šค, ๊ธฐ๋ก ์ฃผ๊ธฐ, ๋น„ํ–‰ ์‹œ๊ฐ„), ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ(๋น„ํ–‰ ์กฐ๊ฑด, ํ’์†, GPS ์˜ค์ฐจ, ์˜์ƒ ์ง€์—ฐ), ๋ ˆ์ฝ”๋“œ(์Šคํ‹ฑ 3์ถ•, ์ž์„ธ๊ฐ, ๊ณ ๋„, ์œ„์น˜ยท๋ชฉํ‘œ) ํ•„๋“œ์™€ ํ—ˆ์šฉ ๋ฒ”์œ„ ์ •์˜ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `data/schema/trn01_input_schema.json ๋ฐ docs/trn01_data_spec.md` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ๊ต์œกยทํ›ˆ๋ จ ์‹ค๋ฌด ๋ฐ ๋Ÿฌ์‹œ์•„-์šฐํฌ๋ผ์ด๋‚˜ ์ „์Ÿ ์ „์žฅ ํ™˜๊ฒฝ๊ณผ์˜ ๊ตฐ์‚ฌ์  ๋ถ€ํ•ฉ์„ฑ\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ์Šคํ‚ค๋งˆ ํ•„๋“œ ์ •์˜ ์™„์„ฑ๋„ 100%, ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์˜ค๋ฅ˜ 0๊ฑด\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ ํ†ต๊ณผ ๋ฐ ๋ช…์„ธ์„œ ๋ฌธ์„œํ™” ์™„๋ฃŒ\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: JSON Schema ๊ฒ€์ฆ ๋„๊ตฌ ๋ฐ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ(์Šคํ‹ฑ ์กฐ์ž‘, ์ž์„ธ๊ฐ, ๊ณ ๋„, ์œ„์น˜ยท๋ชฉํ‘œ ๊ฒฝ๋กœ)์™€ ๋น„ํ–‰ ์„ธ์…˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(๊ฐ€๋ช… ์กฐ์ข…์ž, ๋น„ํ–‰ ์กฐ๊ฑด, ์ „์ˆ  ํŒŒ๋ผ๋ฏธํ„ฐ)์˜ ์Šคํ‚ค๋งˆ๋ฅผ ์ •์˜ํ•˜๊ณ  ๊ฐ€์ƒ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import json\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "SCHEMA = {\n", " \"session\": {\n", " \"session_id\": {\"type\": str, \"pattern\": r\"^S\\d{4}$\"},\n", " \"pilot\": {\"type\": str, \"pattern\": r\"^P\\d{3}$\"}, # ๊ฐ€๋ช… ID๋งŒ ํ—ˆ์šฉ(์‹ค๋ช…ยท๊ตฐ๋ฒˆ ๊ธˆ์ง€)\n", " \"sim_course\": {\"type\": str, \"enum\": [\"RECT40x30_HOVER_OBS_LAND\"]},\n", " \"rate_hz\": {\"type\": int, \"enum\": [10, 20, 50]},\n", " \"duration_s\": {\"type\": (int, float), \"min\": 10, \"max\": 1800},\n", " },\n", " \"tactical\": {\n", " \"condition\": {\"type\": str, \"enum\": list(COND)},\n", " \"wind_mps\": {\"type\": (int, float), \"min\": 0, \"max\": 20},\n", " \"gps_error_m\": {\"type\": (int, float), \"min\": 0, \"max\": 50},\n", " \"video_delay_ms\": {\"type\": (int, float), \"min\": 0, \"max\": 2000},\n", " },\n", " \"record\": {\n", " \"t\": {\"type\": (int, float), \"min\": 0},\n", " \"stick_roll\": {\"type\": (int, float), \"min\": -1, \"max\": 1},\n", " \"stick_pitch\": {\"type\": (int, float), \"min\": -1, \"max\": 1},\n", " \"throttle\": {\"type\": (int, float), \"min\": -1, \"max\": 1},\n", " \"roll_deg\": {\"type\": (int, float), \"min\": -90, \"max\": 90},\n", " \"pitch_deg\": {\"type\": (int, float), \"min\": -90, \"max\": 90},\n", " \"alt\": {\"type\": (int, float), \"min\": -1, \"max\": 500},\n", " \"x\": {\"type\": (int, float)}, \"y\": {\"type\": (int, float)},\n", " \"tx\": {\"type\": (int, float)}, \"ty\": {\"type\": (int, float)}, \"talt\": {\"type\": (int, float), \"min\": 0, \"max\": 500},\n", " },\n", "}\n", "\n", "\n", "def check(name, value, rule):\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 validate(sess):\n", " errs = []\n", " for g in (\"session\", \"tactical\"):\n", " for k, rule in SCHEMA[g].items():\n", " errs += [f\"{g}.{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\"] if k not in sess.get(g, {}) else check(f\"{g}.{k}\", sess[g][k], rule)\n", " prev = -1.0\n", " for i, rec in enumerate(sess.get(\"records\", [])):\n", " for k, rule in SCHEMA[\"record\"].items():\n", " errs += [f\"records[{i}].{k}: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\"] if k not in rec else check(f\"records[{i}].{k}\", rec[k], rule)\n", " if isinstance(rec.get(\"t\"), (int, float)) and rec[\"t\"] <= prev:\n", " errs.append(f\"records[{i}].t: ์‹œ๊ฐ ์—ญํ–‰/์ค‘๋ณต\")\n", " prev = rec.get(\"t\", prev)\n", " return errs\n", "\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์กฐ์ข…์ž 20๋ช… x ๋น„ํ–‰ 1ํšŒ -> ์„ธ์…˜ 20๊ฑด(๊ฐ ์„ธ์…˜์—์„œ ๋ ˆ์ฝ”๋“œ 30๊ฐœ๋งŒ ํ‘œ๋ณธ ๊ฒ€์ฆ)\n", "pilots = make_pilots(20, seed=7)\n", "log, truth = fly(pilots, \"day\", seed=1)\n", "rng = np.random.default_rng(2)\n", "sessions = []\n", "for j in range(20):\n", " d = one(log, j).iloc[::40][list(SCHEMA[\"record\"])].round(3)\n", " sessions.append({\"session\": {\"session_id\": f\"S{j + 1:04d}\", \"pilot\": pilots[\"pilot\"][j], \"sim_course\": \"RECT40x30_HOVER_OBS_LAND\",\n", " \"rate_hz\": 10, \"duration_s\": 120.0},\n", " \"tactical\": {\"condition\": \"day\", \"wind_mps\": float(round(rng.uniform(0, 3), 1)), \"gps_error_m\": 0.25,\n", " \"video_delay_ms\": int(rng.integers(50, 250))},\n", " \"records\": [{k: float(v) for k, v in r.items()} for r in d.to_dict(\"records\")]})\n", "ok = sum(1 for s in sessions if not validate(s))\n", "print(f\"์ •์ƒ ์„ธ์…˜ {len(sessions)}๊ฑด(๋ ˆ์ฝ”๋“œ {sum(len(s['records']) for s in sessions)}๊ฐœ) ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ {ok}๊ฑด, ์˜ค๋ฅ˜ {len(sessions) - ok}๊ฑด\")\n", "\n", "bad = json.loads(json.dumps(sessions[0]))\n", "bad[\"session\"][\"pilot\"] = \"KIM-01\" # ์‹ค๋ช…ํ˜• ID ์ž…๋ ฅ\n", "bad[\"tactical\"][\"condition\"] = \"storm\"\n", "del bad[\"tactical\"][\"video_delay_ms\"]\n", "bad[\"records\"][3][\"stick_roll\"] = 1.8\n", "bad[\"records\"][5][\"t\"] = bad[\"records\"][4][\"t\"]\n", "print(\"[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\")\n", "for e in validate(bad):\n", " print(\" -\", e)\n", "\n", "rows = []\n", "for g in SCHEMA:\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\"})[:45]))\n", "print(\"\\n[๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ]\")\n", "print(pd.DataFrame(rows, columns=[\"๊ทธ๋ฃน\", \"ํ•„๋“œ\", \"์ž๋ฃŒํ˜•\", \"๊ทœ์น™\"]).to_string(index=False))\n", "print(\"\\n๋น„ํ–‰ ๋‹จ๊ณ„: ๊ฒฝ๋กœ 0~60์ดˆ / ํ˜ธ๋ฒ„๋ง 60~85์ดˆ / ์žฅ์• ๋ฌผ ํšŒํ”ผ 85์ดˆ / ๋น„์ƒ ์ฐฉ๋ฅ™ ์ง€์‹œ 100์ดˆ, 10Hz ๊ธฐ๋ก\")\n", "print(\"๊ฐœ์ธ์ •๋ณด: ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… ID(P001~)๋กœ๋งŒ ์ €์žฅํ•˜๊ณ , ์‹ค๋ช…ยท๊ตฐ๋ฒˆยท์†Œ์†์€ ๋ณ„๋„ ๋Œ€์žฅ์—์„œ ๊ต๊ด€๋งŒ ๊ด€๋ฆฌํ•œ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 01 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์ •์ƒ ์„ธ์…˜ 20๊ฑด(๋ ˆ์ฝ”๋“œ 600๊ฐœ) ์ค‘ ๊ฒ€์ฆ ํ†ต๊ณผ 20๊ฑด, ์˜ค๋ฅ˜ 0๊ฑด\n", "[์˜ค๋ฅ˜ ์ฃผ์ž… ๊ฒฐ๊ณผ]\n", " - session.pilot: ํ˜•์‹ ๋ถˆ์ผ์น˜ 'KIM-01'\n", " - tactical.condition: ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๊ฐ’ 'storm'\n", " - tactical.video_delay_ms: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ\n", " - records[3].stick_roll: ์ตœ๋Œ€๊ฐ’ 1 ์ดˆ๊ณผ\n", " - records[5].t: ์‹œ๊ฐ ์—ญํ–‰/์ค‘๋ณต\n", "\n", "[๋ฐ์ดํ„ฐ ๋ช…์„ธ ์š”์•ฝ]\n", " ๊ทธ๋ฃน ํ•„๋“œ ์ž๋ฃŒํ˜• ๊ทœ์น™\n", " session session_id str {'pattern': '^S\\\\d{4}$'}\n", " session pilot str {'pattern': '^P\\\\d{3}$'}\n", " session sim_course str {'enum': ['RECT40x30_HOVER_OBS_LAND']}\n", " session rate_hz int {'enum': [10, 20, 50]}\n", " session duration_s number {'min': 10, 'max': 1800}\n", "tactical condition str {'enum': ['day', 'night', 'gust']}\n", "tactical wind_mps number {'min': 0, 'max': 20}\n", "tactical gps_error_m number {'min': 0, 'max': 50}\n", "tactical video_delay_ms number {'min': 0, 'max': 2000}\n", " record t number {'min': 0}\n", " record stick_roll number {'min': -1, 'max': 1}\n", " record stick_pitch number {'min': -1, 'max': 1}\n", " record throttle number {'min': -1, 'max': 1}\n", " record roll_deg number {'min': -90, 'max': 90}\n", " record pitch_deg number {'min': -90, 'max': 90}\n", " record alt number {'min': -1, 'max': 500}\n", " record x number {}\n", " record y number {}\n", " record tx number {}\n", " record ty number {}\n", " record talt number {'min': 0, 'max': 500}\n", "\n", "๋น„ํ–‰ ๋‹จ๊ณ„: ๊ฒฝ๋กœ 0~60์ดˆ / ํ˜ธ๋ฒ„๋ง 60~85์ดˆ / ์žฅ์• ๋ฌผ ํšŒํ”ผ 85์ดˆ / ๋น„์ƒ ์ฐฉ๋ฅ™ ์ง€์‹œ 100์ดˆ, 10Hz ๊ธฐ๋ก\n", "๊ฐœ์ธ์ •๋ณด: ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… ID(P001~)๋กœ๋งŒ ์ €์žฅํ•˜๊ณ , ์‹ค๋ช…ยท๊ตฐ๋ฒˆยท์†Œ์†์€ ๋ณ„๋„ ๋Œ€์žฅ์—์„œ ๊ต๊ด€๋งŒ ๊ด€๋ฆฌํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 02. ์‹œ๊ณ„์—ด ๋น„ํ–‰ ๊ถค์  ๋ฐ์ดํ„ฐ ์ •์ œ ๋ฐ ์ด์ƒ ์กฐ์ž‘ ๋…ธ์ด์ฆˆ ํ•„ํ„ฐ๋ง ์ „์ฒ˜๋ฆฌ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์‹œ๊ณ„์—ด ๋น„ํ–‰ ๊ถค์  ๋…ธ์ด์ฆˆ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ  ์Šคํ‹ฑ ์กฐ์ž‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ •๊ทœํ™”ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์‹œ๊ณ„์—ด ๋น„ํ–‰๋กœ๊ทธ์˜ ์‹œ๊ฐ ์ค‘๋ณตยท์—ญํ–‰, GPS ํА, ์„ผ์„œ ๋‹จ์ ˆ, ์Šคํ‹ฑ PWM ์›์‹œ๊ฐ’์„ ์ •๋ฆฌํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **10Hz ๊ฒฉ์ž๋กœ ์‹œ๊ฐ์„ ์ •๋ ฌํ•˜๊ณ  ์ค‘๋ณต์„ ์ œ๊ฑฐํ•œ ๋’ค, ์Šคํ‹ฑ PWM(1000~2000us)์„ -1~1๋กœ ์ •๊ทœํ™”ํ•˜๊ณ  ์ค‘๋ฆฝ ๋ฐ๋“œ๋ฐด๋“œ๋ฅผ ์ ์šฉ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/preprocess/trn01_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": [ "\"\"\"์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์›์‹œ ๋น„ํ–‰๋กœ๊ทธ์˜ ์‹œ๊ฐ ์ค‘๋ณตยท์—ญํ–‰ ์ •๋ฆฌ, ์Šคํ‹ฑ PWM ๊ฐ’(1000~2000us) ์ •๊ทœํ™”, GPS ํА(์ด์ƒ์น˜) ์ œ๊ฑฐ(Hampel ํ•„ํ„ฐ), ์„ผ์„œ ๋‹จ์ ˆ ๊ตฌ๊ฐ„ ๋ณด๊ฐ„์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ์„ ๊ตฌํ˜„ํ•˜๊ณ , ๋น„ํ–‰ ์กฐ๊ฑด(์ฃผ๊ฐ„/์•ผ๊ฐ„/๋Œํ’)๋ณ„ ์ž„๊ณ„๊ฐ’์„ ๋‹ฌ๋ฆฌ ์ ์šฉํ•ด ์ฒ˜๋ฆฌ ์†๋„์™€ ์œ„์น˜ ์˜ค์ฐจ ๊ฐœ์„ ์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "HAMPEL_K = {\"day\": 3.0, \"night\": 3.5, \"gust\": 4.5} # ์กฐ๊ฑด๋ณ„ ์ด์ƒ์น˜ ํŒ์ • ์ž„๊ณ„๊ฐ’(๋Œํ’์€ ์‹ค์ œ ํฐ ์›€์ง์ž„์ด ๋งŽ์•„ ๋А์Šจํ•˜๊ฒŒ)\n", "\n", "\n", "def make_raw(log, j, cond, rng):\n", " \"\"\"[๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์›์‹œ ๋กœ๊ทธ: PWM ์Šคํ‹ฑ๊ฐ’, GPS ํА 1%, ์„ผ์„œ ๋‹จ์ ˆ(0.5~2์ดˆ) 2ํšŒ, ์‹œ๊ฐ ์ค‘๋ณต 2%.\"\"\"\n", " d = one(log, j)\n", " raw = d[[\"t\", \"x\", \"y\", \"alt\", \"roll_deg\", \"pitch_deg\", \"true_x\", \"true_y\", \"true_alt\"]].copy()\n", " for s in (\"stick_roll\", \"stick_pitch\", \"throttle\"):\n", " raw[s + \"_pwm\"] = (1500 + 500 * d[s] + rng.normal(0, 4, len(d))).round().astype(int)\n", " k = rng.random(len(raw)) < 0.01\n", " for c in (\"x\", \"y\"):\n", " raw.loc[k, c] += rng.choice([-1, 1], k.sum()) * rng.uniform(6, 15, k.sum())\n", " for _ in range(2):\n", " a = int(rng.integers(50, len(raw) - 30)); raw.loc[a:a + int(rng.integers(5, 20)), [\"x\", \"y\", \"alt\"]] = np.nan\n", " dup = raw.sample(frac=0.02, random_state=int(rng.integers(1e6)))\n", " raw = pd.concat([raw, dup]).sample(frac=1.0, random_state=1).reset_index(drop=True) # ์ˆœ์„œ ์„ž์ž„ + ์ค‘๋ณต\n", " raw[\"t\"] = raw[\"t\"] + rng.normal(0, 0.003, len(raw)) # ์‹œ๊ฐ ํ”๋“ค๋ฆผ\n", " return raw\n", "\n", "\n", "def pwm_to_stick(p, dead=8):\n", " v = (np.asarray(p, float) - 1500) / 500\n", " v[np.abs(np.asarray(p) - 1500) < dead] = 0.0 # ์ค‘๋ฆฝ ๋ถ€๊ทผ ๋ฐ๋“œ๋ฐด๋“œ\n", " return np.clip(v, -1, 1)\n", "\n", "\n", "def hampel(x, win=7, k=3.0):\n", " s = pd.Series(x)\n", " med = s.rolling(win, center=True, min_periods=1).median()\n", " mad = (s - med).abs().rolling(win, center=True, min_periods=1).median() * 1.4826\n", " out = (s - med).abs() > k * (mad + 0.3) # 0.3m: GPS ์žก์Œ ์ˆ˜์ค€ ์ดํ•˜ ํ”๋“ค๋ฆผ์€ ์ด์ƒ์น˜๋กœ ๋ณด์ง€ ์•Š์Œ\n", " return np.where(out, np.nan, s), int(out.sum())\n", "\n", "\n", "def preprocess(raw, cond):\n", " d = raw.copy()\n", " d[\"k\"] = (d[\"t\"] / DT).round().astype(int) # 10Hz ๊ฒฉ์ž ๋ฒˆํ˜ธ๋กœ ์‹œ๊ฐ ์ •๋ ฌ\n", " d = d.sort_values(\"k\").drop_duplicates(\"k\").set_index(\"k\")\n", " d = d.reindex(np.arange(0, d.index.max() + 1))\n", " d[\"t\"] = np.round(d.index * DT, 1)\n", " n_missing = int(d[[\"x\", \"y\", \"alt\"]].isna().any(axis=1).sum())\n", " n_out = 0\n", " for c in (\"x\", \"y\", \"alt\"):\n", " d[c], n = hampel(d[c].values, k=HAMPEL_K[cond]); n_out += n\n", " d[[\"x\", \"y\", \"alt\"]] = d[[\"x\", \"y\", \"alt\"]].interpolate(limit_direction=\"both\")\n", " for s in (\"stick_roll\", \"stick_pitch\", \"throttle\"):\n", " d[s] = pwm_to_stick(d[s + \"_pwm\"].interpolate(limit_direction=\"both\").round())\n", " return d.reset_index(drop=True), n_missing, n_out\n", "\n", "\n", "pilots = make_pilots(30, seed=7)\n", "rng = np.random.default_rng(3)\n", "rows, n_rec, t_total = [], 0, 0.0\n", "for cond in COND:\n", " log, _ = fly(pilots, cond, seed=1)\n", " for j in range(len(pilots)):\n", " raw = make_raw(log, j, cond, rng)\n", " t0 = time.perf_counter()\n", " clean, miss, out = preprocess(raw, cond)\n", " t_total += time.perf_counter() - t0; n_rec += len(raw)\n", " e0 = np.sqrt((raw[\"x\"] - raw[\"true_x\"]) ** 2 + (raw[\"y\"] - raw[\"true_y\"]) ** 2).mean()\n", " e1 = np.sqrt((clean[\"x\"] - clean[\"true_x\"]) ** 2 + (clean[\"y\"] - clean[\"true_y\"]) ** 2).mean()\n", " rows.append({\"์กฐ๊ฑด\": cond, \"๊ฒฐ์ธกํ–‰\": miss, \"์ด์ƒ์น˜\": out, \"๋‚จ์€๊ฒฐ์ธก\": int(clean[[\"x\", \"y\", \"alt\"]].isna().sum().sum()),\n", " \"์œ„์น˜์˜ค์ฐจ ์ „(m)\": e0, \"์œ„์น˜์˜ค์ฐจ ํ›„(m)\": e1})\n", "df = pd.DataFrame(rows)\n", "print(f\"์›์‹œ ๋ ˆ์ฝ”๋“œ {n_rec:,}๊ฐœ(๋กœ๊ทธ {len(df)}๊ฐœ) ์ฒ˜๋ฆฌ, {n_rec / t_total:,.0f} ๋ ˆ์ฝ”๋“œ/์ดˆ (๋ชฉํ‘œ 1,000๊ฑด/์ดˆ ์ด์ƒ), ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ 0๊ฑด\")\n", "print(f\"๊ฒฐ์ธก ๋ณด์ •์œจ {1 - df['๋‚จ์€๊ฒฐ์ธก'].sum() / max(df['๊ฒฐ์ธกํ–‰'].sum(), 1):.0%} (๊ฒฐ์ธกํ–‰ {df['๊ฒฐ์ธกํ–‰'].sum()}๊ฐœ ๋ชจ๋‘ ๋ณด๊ฐ„)\")\n", "print(\"\\n[์กฐ๊ฑด๋ณ„ ์ „์ฒ˜๋ฆฌ ํšจ๊ณผ(๋กœ๊ทธ ํ‰๊ท )]\")\n", "print(df.groupby(\"์กฐ๊ฑด\")[[\"๊ฒฐ์ธกํ–‰\", \"์ด์ƒ์น˜\", \"์œ„์น˜์˜ค์ฐจ ์ „(m)\", \"์œ„์น˜์˜ค์ฐจ ํ›„(m)\"]].mean().round(2).to_string())\n", "print(\"์กฐ๊ฑด๋ณ„ Hampel ์ž„๊ณ„๊ฐ’:\", HAMPEL_K, \"-> ๋Œํ’ ์กฐ๊ฑด์€ ์‹ค์ œ ๊ธ‰๊ธฐ๋™์„ ์ด์ƒ์น˜๋กœ ์ง€์šฐ์ง€ ์•Š๋„๋ก ๋А์Šจํ•˜๊ฒŒ ๋‘”๋‹ค\")\n", "\n", "# ๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5์ข…\n", "raw = make_raw(fly(pilots.iloc[:1], \"day\", seed=9)[0], 0, \"day\", np.random.default_rng(0))\n", "clean, _, _ = preprocess(raw, \"day\")\n", "spike = np.r_[np.zeros(10), 50.0, np.zeros(10)]\n", "tests = {\n", " \"PWM 1000/1500/2000 -> -1/0/1\": np.allclose(pwm_to_stick([1000, 1500, 2000]), [-1, 0, 1]),\n", " \"๋ฐ๋“œ๋ฐด๋“œ(1504 -> 0)\": pwm_to_stick([1504])[0] == 0.0,\n", " \"๋‹จ์ผ ํА ๊ฐ’ ์ œ๊ฑฐ\": np.isnan(hampel(spike)[0][10]),\n", " \"์‹œ๊ฐ ์ค‘๋ณต ์ œ๊ฑฐยท๋‹จ์กฐ ์ฆ๊ฐ€\": clean[\"t\"].is_monotonic_increasing and clean[\"t\"].duplicated().sum() == 0,\n", " \"๊ฒฐ์ธก ์—†์Œ + 10Hz 1,200ํ–‰\": clean[[\"x\", \"y\", \"alt\"]].isna().sum().sum() == 0 and len(clean) == STEPS,\n", "}\n", "for k, v in tests.items():\n", " print(f\"[{'PASS' if v else 'FAIL'}] {k}\")\n", "print(f\"๋‹จ์œ„ ํ…Œ์ŠคํŠธ {sum(tests.values())}/{len(tests)} ํ†ต๊ณผ\")\n", "\n", "seg = slice(400, 700)\n", "r2 = raw.sort_values(\"t\").iloc[seg]\n", "plt.figure(figsize=(9, 3.5))\n", "plt.plot(r2[\"t\"], r2[\"y\"], \".\", ms=3, c=\"gray\", label=\"raw (spikes, gaps)\")\n", "plt.plot(clean[\"t\"].iloc[seg], clean[\"y\"].iloc[seg], c=\"tab:blue\", label=\"preprocessed\")\n", "plt.plot(clean[\"t\"].iloc[seg], clean[\"true_y\"].iloc[seg], \"k:\", label=\"true\")\n", "plt.xlabel(\"time (s)\"); plt.ylabel(\"y (m)\"); plt.legend(); plt.title(\"Flight log preprocessing (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 02 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์›์‹œ ๋ ˆ์ฝ”๋“œ 110,160๊ฐœ(๋กœ๊ทธ 90๊ฐœ) ์ฒ˜๋ฆฌ, 94,969 ๋ ˆ์ฝ”๋“œ/์ดˆ (๋ชฉํ‘œ 1,000๊ฑด/์ดˆ ์ด์ƒ), ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ 0๊ฑด\n", "๊ฒฐ์ธก ๋ณด์ •์œจ 100% (๊ฒฐ์ธกํ–‰ 2348๊ฐœ ๋ชจ๋‘ ๋ณด๊ฐ„)\n", "\n", "[์กฐ๊ฑด๋ณ„ ์ „์ฒ˜๋ฆฌ ํšจ๊ณผ(๋กœ๊ทธ ํ‰๊ท )]\n", " ๊ฒฐ์ธกํ–‰ ์ด์ƒ์น˜ ์œ„์น˜์˜ค์ฐจ ์ „(m) ์œ„์น˜์˜ค์ฐจ ํ›„(m)\n", "์กฐ๊ฑด \n", "day 25.37 24.63 0.47 0.31\n", "gust 26.17 21.87 0.45 0.33\n", "night 26.73 27.90 0.78 0.62\n", "์กฐ๊ฑด๋ณ„ Hampel ์ž„๊ณ„๊ฐ’: {'day': 3.0, 'night': 3.5, 'gust': 4.5} -> ๋Œํ’ ์กฐ๊ฑด์€ ์‹ค์ œ ๊ธ‰๊ธฐ๋™์„ ์ด์ƒ์น˜๋กœ ์ง€์šฐ์ง€ ์•Š๋„๋ก ๋А์Šจํ•˜๊ฒŒ ๋‘”๋‹ค\n", "[PASS] PWM 1000/1500/2000 -> -1/0/1\n", "[PASS] ๋ฐ๋“œ๋ฐด๋“œ(1504 -> 0)\n", "[PASS] ๋‹จ์ผ ํА ๊ฐ’ ์ œ๊ฑฐ\n", "[PASS] ์‹œ๊ฐ ์ค‘๋ณต ์ œ๊ฑฐยท๋‹จ์กฐ ์ฆ๊ฐ€\n", "[PASS] ๊ฒฐ์ธก ์—†์Œ + 10Hz 1,200ํ–‰\n", "๋‹จ์œ„ ํ…Œ์ŠคํŠธ 5/5 ํ†ต๊ณผ\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 03. ๋น„ํ–‰ ๊ฒฝ๋กœ ์ดํƒˆ๋„ ๋ฐ ํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ ๊ณ„์‚ฐ 1์ฐจ ์ˆ™๋ จ๋„ ์ฑ„์  Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋น„ํ–‰ ๊ฒฝ๋กœ ์ดํƒˆ๋„ ๋ฐ ํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ ๊ธฐ๋ฐ˜ 1์ฐจ ์ˆ™๋ จ๋„ ์ฑ„์  Baseline ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ฐœ๋ฐœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋น„ํ–‰ ๊ฒฝ๋กœ ์ดํƒˆ๋„์™€ ํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ, ๊ณ ๋„ ์œ ์ง€ ์˜ค์ฐจ๋กœ 1์ฐจ ์ˆ™๋ จ๋„ ์ ์ˆ˜์™€ ๋“ฑ๊ธ‰(A/B/C)์„ ๊ณ„์‚ฐํ•˜๋Š” Baseline ์ฑ„์  ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ฐœ๋ฐœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฒฝ๋กœ RMSE, ํ˜ธ๋ฒ„๋ง ํ‰๊ท  ์ดํƒˆยทํ”๋“ค๋ฆผ, ๊ณ ๋„ ์˜ค์ฐจ๋ฅผ ์กฐ๊ฑด๋ณ„๋กœ ํ‘œ์ค€ํ™”ํ•˜๊ณ  ๊ธฐ์ค€ ์ง‘๋‹จ์œผ๋กœ ๊ฐ€์ค‘์น˜๋ฅผ 1์ฐจ ํŠœ๋‹ํ•ด 0~100์  ์ฑ„์ ์‹ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/trn01_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": [ "\"\"\"๋น„ํ–‰ ๊ฒฝ๋กœ ์ดํƒˆ๋„(๊ฒฝ๋กœ RMSE)์™€ ํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(ํ‰๊ท  ์ดํƒˆยทํ”๋“ค๋ฆผ), ๊ณ ๋„ ์œ ์ง€ ์˜ค์ฐจ๋กœ 1์ฐจ ์ˆ™๋ จ๋„ ์ ์ˆ˜(0~100)์™€ ๋“ฑ๊ธ‰(A/B/C)์„ ๊ณ„์‚ฐํ•˜๋Š” Baseline ์ฑ„์  ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๊ณ , ๊ต๊ด€ ํ‰๊ฐ€ ๋“ฑ๊ธ‰๊ณผ์˜ ์ผ์น˜์œจ(์ฒ˜๋ฆฌ ์ •ํ™•๋„)๊ณผ ํŒ๋‹จ ์ง€์—ฐ์„ ์ธก์ •ํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import LinearRegression\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๊ธฐ์ค€(ํŠœ๋‹)์šฉ ์กฐ์ข…์ž 40๋ช…, ํ‰๊ฐ€์šฉ ์กฐ์ข…์ž 40๋ช… x ์กฐ๊ฑด 3์ข…(์ฃผ๊ฐ„ยท์•ผ๊ฐ„ยท๋Œํ’)\n", "train = make_dataset(40, seed=7)\n", "test = make_dataset(40, seed=8)\n", "\n", "# 1) ํœด๋ฆฌ์Šคํ‹ฑ ๊ฐ€์ค‘์น˜ 1์ฐจ ํŠœ๋‹: ์กฐ๊ฑด๋ณ„ ํ‘œ์ค€ํ™” ํŠน์ง• -> ๊ต๊ด€ ์ ์ˆ˜ ์„ ํ˜• ๊ทผ์‚ฌ\n", "ztr, zte = cond_z(train, train, BASE_F), cond_z(test, train, BASE_F)\n", "lr = LinearRegression().fit(ztr[BASE_F], train[\"instructor\"])\n", "print(\"์ฑ„์ ์‹: ์ ์ˆ˜ = {:.1f}\".format(lr.intercept_) + \"\".join(f\" {w:+.1f}x{f}\" for f, w in zip(BASE_F, lr.coef_)) + \" (ํŠน์ง•์€ ์กฐ๊ฑด๋ณ„ ํ‘œ์ค€ํ™” ๊ฐ’)\")\n", "\n", "t0 = time.perf_counter()\n", "test[\"base_score\"] = np.clip(lr.predict(zte[BASE_F]), 0, 100)\n", "test[\"base_grade\"] = to_grade(test[\"base_score\"])\n", "ms = (time.perf_counter() - t0) * 1000 / len(test)\n", "acc = (test[\"base_grade\"] == test[\"grade\"]).mean()\n", "print(f\"ํ‰๊ฐ€ ๋น„ํ–‰ {len(test)}ํšŒ: ๋“ฑ๊ธ‰ ์ผ์น˜์œจ(์ฒ˜๋ฆฌ ์ •ํ™•๋„) {acc:.1%} (๋ชฉํ‘œ 75% ์ด์ƒ: {'๋‹ฌ์„ฑ' if acc >= 0.75 else '๋ฏธ๋‹ฌ'}), \"\n", " f\"์ ์ˆ˜ ํ‰๊ท  ์˜ค์ฐจ {np.abs(test['base_score'] - test['instructor']).mean():.1f}์ , ์ฑ„์  ์ง€์—ฐ {ms:.3f} ms/๋น„ํ–‰\")\n", "\n", "# 2) ์กฐ๊ฑด๋ณ„ ์ •ํ™•๋„ + ์กฐ๊ฑด ํ‘œ์ค€ํ™”๋ฅผ ํ•˜์ง€ ์•Š์•˜์„ ๋•Œ ๋น„๊ต\n", "raw = LinearRegression().fit(train[BASE_F], train[\"instructor\"])\n", "test[\"raw_grade\"] = to_grade(raw.predict(test[BASE_F]))\n", "print(\"\\n์กฐ๊ฑด ํ‘œ์ค€ํ™” O ํ‘œ์ค€ํ™” X\")\n", "for c in COND:\n", " s = test[test[\"cond\"] == c]\n", " print(f\"{c:6s} {(s['base_grade'] == s['grade']).mean():8.1%} {(s['raw_grade'] == s['grade']).mean():8.1%}\")\n", "print(f\"์ „์ฒด {acc:8.1%} {(test['raw_grade'] == test['grade']).mean():8.1%}\")\n", "gust_raw = raw.predict(test[test[\"cond\"] == \"gust\"][BASE_F]).mean() - raw.predict(test[test[\"cond\"] == \"day\"][BASE_F]).mean()\n", "print(f\"-> ํ‘œ์ค€ํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ๋Œํ’ ๋น„ํ–‰ ์ ์ˆ˜๊ฐ€ ์ฃผ๊ฐ„๋ณด๋‹ค ํ‰๊ท  {gust_raw:+.1f}์  ๋‹ฌ๋ผ์ง„๋‹ค(์กฐ๊ฑด ํšจ๊ณผ๊ฐ€ ์ ์ˆ˜์— ์„ž์ž„). ํ‘œ์ค€ํ™”๋Š” ๊ฐ™์€ ์กฐ๊ฑด ๋™๋ฃŒ ๋Œ€๋น„ ๋น„๊ต๋‹ค.\")\n", "\n", "# 3) ํ˜ผ๋™ํ‘œ์™€ ํ•œ๊ณ„ ๋ถ„์„\n", "print(\"\\n[๊ต๊ด€ ๋“ฑ๊ธ‰ x Baseline ๋“ฑ๊ธ‰]\")\n", "print(pd.crosstab(test[\"grade\"].rename(\"๊ต๊ด€\"), test[\"base_grade\"].rename(\"Baseline\")).to_string())\n", "over = test[(test[\"grade\"] == \"C\") & (test[\"base_grade\"] != \"C\")]\n", "ev = [\"near_collision\", \"hard_landing\"]\n", "print(f\"\\n๊ฐ€์žฅ ๋งŽ์€ ์˜ค๋ฅ˜: ๊ต๊ด€ C -> Baseline B {len(over)}๊ฑด. ๊ต๊ด€ C ๋น„ํ–‰์˜ {test[test['grade'] == 'C'][ev].any(axis=1).mean():.0%}๊ฐ€\"\n", " f\" ์žฅ์• ๋ฌผ ๊ทผ์ ‘ยท๊ฒฝ์ฐฉ๋ฅ™์„ ํฌํ•จํ•˜์ง€๋งŒ Baseline ํŠน์ง•(๊ฒฝ๋กœยทํ˜ธ๋ฒ„๋งยท๊ณ ๋„)์—๋Š” ์ด ์ •๋ณด๊ฐ€ ์—†๋‹ค.\")\n", "print(\"-> ๊ฒฝ๋กœยทํ˜ธ๋ฒ„๋ง๋งŒ ๋ณด๋Š” Baseline์€ ๋Œ๋ฐœ ์ƒํ™ฉ ๋Œ€์‘(์žฅ์• ๋ฌผ ํšŒํ”ผ, ๋น„์ƒ ์ฐฉ๋ฅ™)์„ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•œ๋‹ค. ๋ฏธ์…˜ 4์—์„œ ๋ฐ˜์‘ ํŠน์ง•์„ ์ถ”๊ฐ€ํ•œ๋‹ค.\")\n", "\n", "plt.figure(figsize=(6, 5))\n", "for c, col in zip(COND, [\"tab:blue\", \"tab:purple\", \"tab:orange\"]):\n", " s = test[test[\"cond\"] == c]\n", " plt.scatter(s[\"instructor\"], s[\"base_score\"], s=15, c=col, label=c)\n", "plt.plot([20, 100], [20, 100], \"k--\"); plt.axhline(75, c=\"gray\", lw=0.5); plt.axhline(55, c=\"gray\", lw=0.5)\n", "plt.xlabel(\"instructor score\"); plt.ylabel(\"baseline AI score\"); plt.legend(); plt.title(\"Baseline skill score vs instructor (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 03 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์ฑ„์ ์‹: ์ ์ˆ˜ = 59.3 -0.4xpath_rmse -0.8xhover_drift +0.2xhover_std -13.0xalt_err (ํŠน์ง•์€ ์กฐ๊ฑด๋ณ„ ํ‘œ์ค€ํ™” ๊ฐ’)\n", "ํ‰๊ฐ€ ๋น„ํ–‰ 120ํšŒ: ๋“ฑ๊ธ‰ ์ผ์น˜์œจ(์ฒ˜๋ฆฌ ์ •ํ™•๋„) 75.8% (๋ชฉํ‘œ 75% ์ด์ƒ: ๋‹ฌ์„ฑ), ์ ์ˆ˜ ํ‰๊ท  ์˜ค์ฐจ 5.3์ , ์ฑ„์  ์ง€์—ฐ 0.025 ms/๋น„ํ–‰\n", "\n", "์กฐ๊ฑด ํ‘œ์ค€ํ™” O ํ‘œ์ค€ํ™” X\n", "day 70.0% 75.0%\n", "night 80.0% 70.0%\n", "gust 77.5% 82.5%\n", "์ „์ฒด 75.8% 75.8%\n", "-> ํ‘œ์ค€ํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ๋Œํ’ ๋น„ํ–‰ ์ ์ˆ˜๊ฐ€ ์ฃผ๊ฐ„๋ณด๋‹ค ํ‰๊ท  -4.0์  ๋‹ฌ๋ผ์ง„๋‹ค(์กฐ๊ฑด ํšจ๊ณผ๊ฐ€ ์ ์ˆ˜์— ์„ž์ž„). ํ‘œ์ค€ํ™”๋Š” ๊ฐ™์€ ์กฐ๊ฑด ๋™๋ฃŒ ๋Œ€๋น„ ๋น„๊ต๋‹ค.\n", "\n", "[๊ต๊ด€ ๋“ฑ๊ธ‰ x Baseline ๋“ฑ๊ธ‰]\n", "Baseline A B C\n", "๊ต๊ด€ \n", "A 15 6 0\n", "B 3 26 4\n", "C 0 16 50\n", "\n", "๊ฐ€์žฅ ๋งŽ์€ ์˜ค๋ฅ˜: ๊ต๊ด€ C -> Baseline B 16๊ฑด. ๊ต๊ด€ C ๋น„ํ–‰์˜ 95%๊ฐ€ ์žฅ์• ๋ฌผ ๊ทผ์ ‘ยท๊ฒฝ์ฐฉ๋ฅ™์„ ํฌํ•จํ•˜์ง€๋งŒ Baseline ํŠน์ง•(๊ฒฝ๋กœยทํ˜ธ๋ฒ„๋งยท๊ณ ๋„)์—๋Š” ์ด ์ •๋ณด๊ฐ€ ์—†๋‹ค.\n", "-> ๊ฒฝ๋กœยทํ˜ธ๋ฒ„๋ง๋งŒ ๋ณด๋Š” Baseline์€ ๋Œ๋ฐœ ์ƒํ™ฉ ๋Œ€์‘(์žฅ์• ๋ฌผ ํšŒํ”ผ, ๋น„์ƒ ์ฐฉ๋ฅ™)์„ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•œ๋‹ค. ๋ฏธ์…˜ 4์—์„œ ๋ฐ˜์‘ ํŠน์ง•์„ ์ถ”๊ฐ€ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 04. ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ ๋ฐ ๋น„์ƒ ์ฐฉ๋ฅ™ ๋ฐ˜์‘ ์†๋„ ๊ธฐ๋ฐ˜ ์ •๋ฐ€ ์ˆ™๋ จ๋„ ํ‰๊ฐ€ ์—”์ง„ ๊ณ ๋„ํ™”\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ ๋ฐ ๋น„์ƒ ์ฐฉ๋ฅ™ ๋ฐ˜์‘ ์†๋„๋ฅผ ๋ฐ˜์˜ํ•œ ์ •๋ฐ€ ์ˆ™๋ จ๋„ ํ‰๊ฐ€ ์—”์ง„์„ ๊ณ ๋„ํ™”ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ ๋ฐ˜์‘๊ณผ ๋น„์ƒ ์ฐฉ๋ฅ™ ์ ‘์ง€ ์†๋„, ์ž์„ธ ํ•œ๊ณ„, ๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ • ํŠน์ง•์„ ๋ฐ˜์˜ํ•œ ์ •๋ฐ€ ์ˆ™๋ จ๋„ ํ‰๊ฐ€ ์—”์ง„์œผ๋กœ ๊ณ ๋„ํ™”ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์žฅ์• ๋ฌผ ์ถœํ˜„ ํ›„ ์Šคํ‹ฑ ๋ฐ˜์‘์‹œ๊ฐ„๊ณผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ, ์ ‘์ง€ ์ง์ „ ํ•˜๊ฐ• ์†๋„, ์ž์„ธ 35๋„ ์ดˆ๊ณผ ์‹œ๊ฐ„, ์Šคํ‹ฑ ๋ณ€ํ™”์œจ, ํšŒํ”ผ ํ›„ ์ดˆ๊ณผ ์ด๋™์„ ํŠน์ง•์œผ๋กœ ์ถ”๊ฐ€ํ•˜๊ณ  ์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด(์žฅ์• ๋ฌผ ๊ทผ์ ‘ยท๊ฒฝ์ฐฉ๋ฅ™ยท์ž์„ธ ํ•œ๊ณ„) ํƒ์ง€ ๊ทœ์น™ ์ž‘์„ฑํ•˜์—ฌ ์›์ธ์„ ๋ถ„์„ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/models/trn01_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": [ "\"\"\"๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ ๋ฐ˜์‘์‹œ๊ฐ„ยทํšŒํ”ผ ๊ฑฐ๋ฆฌ, ๋น„์ƒ ์ฐฉ๋ฅ™ ์ ‘์ง€ ์†๋„, ์ž์„ธ ํ•œ๊ณ„ ์ดˆ๊ณผ, ๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ • ํŠน์ง•์„ ์ถ”๊ฐ€ํ•œ ์ •๋ฐ€ ์ˆ™๋ จ๋„ ํ‰๊ฐ€ ์—”์ง„์œผ๋กœ ๊ณ ๋„ํ™”ํ•˜๊ณ , ๋ณตํ•ฉ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…์—์„œ ์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด(์žฅ์• ๋ฌผ ๊ทผ์ ‘ยท๊ฒฝ์ฐฉ๋ฅ™ยท์ž์„ธ ํ•œ๊ณ„) ํƒ์ง€ ์žฌํ˜„์œจ, ๋“ฑ๊ธ‰ ์ผ์น˜์œจ, ๋กœ๊ทธ๋‹น ์ฒ˜๋ฆฌ ์ง€์—ฐ์„ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import LinearRegression, Ridge\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "train = make_dataset(40, seed=7)\n", "zt = cond_z(train, train, ADV_F)\n", "base = LinearRegression().fit(cond_z(train, train, BASE_F)[BASE_F], train[\"instructor\"])\n", "eng = Ridge(alpha=1.0).fit(zt[ADV_F], train[\"instructor\"])\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ณตํ•ฉ ์‹œ๋‚˜๋ฆฌ์˜ค 10์ข…: (์กฐ๊ฑด, ์˜์ƒ ์ง€์—ฐ ์ถ”๊ฐ€ ์ดˆ, ์žฅ์• ๋ฌผ ๋ฐฉํ–ฅ)\n", "SCEN = [(\"1 ์ฃผ๊ฐ„ ๊ธฐ๋ณธ\", \"day\", 0.0, None), (\"2 ์•ผ๊ฐ„\", \"night\", 0.0, None), (\"3 ๋Œํ’\", \"gust\", 0.0, None), (\"4 ์žฅ์• ๋ฌผ ์ขŒ์ธก\", \"day\", 0.0, -1),\n", " (\"5 ์žฅ์• ๋ฌผ ์šฐ์ธก\", \"day\", 0.0, 1), (\"6 ์˜์ƒ ์ง€์—ฐ 0.3์ดˆ\", \"day\", 0.3, None), (\"7 ๋Œํ’+์žฅ์• ๋ฌผ ์ขŒ์ธก\", \"gust\", 0.0, -1),\n", " (\"8 ์•ผ๊ฐ„+์˜์ƒ ์ง€์—ฐ\", \"night\", 0.3, None), (\"9 ๋Œํ’+์˜์ƒ ์ง€์—ฐ\", \"gust\", 0.3, None), (\"10 ์•ผ๊ฐ„+์žฅ์• ๋ฌผ ์šฐ์ธก\", \"night\", 0.0, 1)]\n", "pilots = make_pilots(40, seed=8)\n", "rows, all_det, ms = [], [], []\n", "for i, (name, cond, dly, side) in enumerate(SCEN):\n", " log, tr = fly(pilots, cond, seed=500 + i, delay_extra=dly, obstacle_side=side)\n", " t0 = time.perf_counter()\n", " f = features(log)\n", " df = pd.concat([tr.reset_index(drop=True), f], axis=1)\n", " df[\"score\"] = np.clip(eng.predict(cond_z(df, train, ADV_F)[ADV_F]), 0, 100)\n", " det = detect(df)\n", " ms.append((time.perf_counter() - t0) * 1000 / len(df))\n", " df[\"base\"] = np.clip(base.predict(cond_z(df, train, BASE_F)[BASE_F]), 0, 100)\n", " truth = df[list(RULES)].astype(bool)\n", " tp, pos = int((det & truth).values.sum()), int(truth.values.sum())\n", " all_det.append((det, truth))\n", " rows.append({\"์‹œ๋‚˜๋ฆฌ์˜ค\": name, \"์œ„ํ—˜์‚ฌ๊ฑด\": pos, \"ํƒ์ง€\": tp, \"์žฌํ˜„์œจ\": round(tp / max(pos, 1), 3), \"์˜คํƒ\": int((det & ~truth).values.sum()),\n", " \"Baseline ์ผ์น˜\": round((to_grade(df[\"base\"]) == df[\"grade\"]).mean(), 3), \"๊ณ ๋„ํ™” ์ผ์น˜\": round((to_grade(df[\"score\"]) == df[\"grade\"]).mean(), 3)})\n", "R = pd.DataFrame(rows)\n", "print(R.to_string(index=False))\n", "rec = R[\"ํƒ์ง€\"].sum() / R[\"์œ„ํ—˜์‚ฌ๊ฑด\"].sum()\n", "print(f\"\\n์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ {rec:.1%} (๋ชฉํ‘œ 85% ์ด์ƒ: {'๋‹ฌ์„ฑ' if rec >= 0.85 else '๋ฏธ๋‹ฌ'}), ๋กœ๊ทธ๋‹น ์ฒ˜๋ฆฌ {np.mean(ms):.1f} ms (์ตœ๋Œ€ {np.max(ms):.1f}, ๋ชฉํ‘œ 200 ms ์ดํ•˜)\")\n", "print(f\"๋“ฑ๊ธ‰ ์ผ์น˜์œจ Baseline {R['Baseline ์ผ์น˜'].mean():.1%} -> ๊ณ ๋„ํ™” ์—”์ง„ {R['๊ณ ๋„ํ™” ์ผ์น˜'].mean():.1%}, 10์ข… ์ค‘ ์žฌํ˜„์œจ 85% ์ด์ƒ {(R['์žฌํ˜„์œจ'] >= 0.85).sum()}๊ฐœ\")\n", "\n", "# ์‚ฌ๊ฑด๋ณ„ ์žฌํ˜„์œจยท์˜คํƒ(์˜ค๊ฒฝ๋ณด ์œ ๋ฐœ ํŒจํ„ด)\n", "det = pd.concat([d for d, _ in all_det]); tru = pd.concat([t for _, t in all_det])\n", "print(\"\\n์‚ฌ๊ฑด ์‹ค์ œ ์žฌํ˜„์œจ ์˜คํƒ\")\n", "for ev in RULES:\n", " print(f\"{ev:15s} {tru[ev].sum():4d} {(det[ev] & tru[ev]).sum() / max(tru[ev].sum(), 1):.2f} {(det[ev] & ~tru[ev]).sum():4d}\")\n", "print(\"์˜คํƒ ํŒจํ„ด: ์ ‘์ง€ ์†๋„๋Š” ๊ณ ๋„ ๋กœ๊ทธ๋ฅผ ๋ฏธ๋ถ„ํ•ด ์ถ”์ •ํ•˜๋ฏ€๋กœ ๊ณ ๋„ ์žก์Œ์ด ํฐ ๋น„ํ–‰์—์„œ ๊ณผ๋Œ€ ์ถ”์ •๋œ๋‹ค -> ์ ‘์ง€ ์ง์ „ 0.4์ดˆ ํ‰๊ท  ์‚ฌ์šฉ, ์ž„๊ณ„๊ฐ’ 2.0m/s๋กœ ์—ฌ์œ ๋ฅผ ๋‘ \")\n", "\n", "w = pd.Series(eng.coef_, index=ADV_F).sort_values()\n", "print(\"\\n์—”์ง„ ๊ฐ€์ค‘์น˜(์กฐ๊ฑด๋ณ„ ํ‘œ์ค€ํ™” ํŠน์ง• 1๋‹จ์œ„๋‹น ์ ์ˆ˜):\", \", \".join(f\"{k} {v:+.1f}\" for k, v in w.items()))\n", "\n", "plt.figure(figsize=(8, 4))\n", "plt.barh(w.index, w.values, color=[\"tab:red\" if v < 0 else \"tab:blue\" for v in w.values])\n", "plt.xlabel(\"score change per 1 SD\"); plt.title(\"Advanced skill engine weights (synthetic)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 04 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", " ์‹œ๋‚˜๋ฆฌ์˜ค ์œ„ํ—˜์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ ์˜คํƒ Baseline ์ผ์น˜ ๊ณ ๋„ํ™” ์ผ์น˜\n", " 1 ์ฃผ๊ฐ„ ๊ธฐ๋ณธ 47 47 1.000 3 0.725 0.800\n", " 2 ์•ผ๊ฐ„ 46 45 0.978 3 0.775 0.775\n", " 3 ๋Œํ’ 65 65 1.000 7 0.625 0.825\n", " 4 ์žฅ์• ๋ฌผ ์ขŒ์ธก 42 41 0.976 4 0.800 0.875\n", " 5 ์žฅ์• ๋ฌผ ์šฐ์ธก 37 36 0.973 7 0.825 0.850\n", "6 ์˜์ƒ ์ง€์—ฐ 0.3์ดˆ 52 49 0.942 3 0.675 0.625\n", " 7 ๋Œํ’+์žฅ์• ๋ฌผ ์ขŒ์ธก 56 56 1.000 6 0.750 0.800\n", " 8 ์•ผ๊ฐ„+์˜์ƒ ์ง€์—ฐ 54 51 0.944 5 0.650 0.675\n", " 9 ๋Œํ’+์˜์ƒ ์ง€์—ฐ 63 63 1.000 7 0.750 0.750\n", "10 ์•ผ๊ฐ„+์žฅ์• ๋ฌผ ์šฐ์ธก 46 44 0.957 5 0.825 0.800\n", "\n", "์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ 97.8% (๋ชฉํ‘œ 85% ์ด์ƒ: ๋‹ฌ์„ฑ), ๋กœ๊ทธ๋‹น ์ฒ˜๋ฆฌ 0.6 ms (์ตœ๋Œ€ 0.9, ๋ชฉํ‘œ 200 ms ์ดํ•˜)\n", "๋“ฑ๊ธ‰ ์ผ์น˜์œจ Baseline 74.0% -> ๊ณ ๋„ํ™” ์—”์ง„ 77.8%, 10์ข… ์ค‘ ์žฌํ˜„์œจ 85% ์ด์ƒ 10๊ฐœ\n", "\n", "์‚ฌ๊ฑด ์‹ค์ œ ์žฌํ˜„์œจ ์˜คํƒ\n", "near_collision 187 0.94 6\n", "hard_landing 256 1.00 44\n", "attitude_exceed 65 1.00 0\n", "์˜คํƒ ํŒจํ„ด: ์ ‘์ง€ ์†๋„๋Š” ๊ณ ๋„ ๋กœ๊ทธ๋ฅผ ๋ฏธ๋ถ„ํ•ด ์ถ”์ •ํ•˜๋ฏ€๋กœ ๊ณ ๋„ ์žก์Œ์ด ํฐ ๋น„ํ–‰์—์„œ ๊ณผ๋Œ€ ์ถ”์ •๋œ๋‹ค -> ์ ‘์ง€ ์ง์ „ 0.4์ดˆ ํ‰๊ท  ์‚ฌ์šฉ, ์ž„๊ณ„๊ฐ’ 2.0m/s๋กœ ์—ฌ์œ ๋ฅผ ๋‘ \n", "\n", "์—”์ง„ ๊ฐ€์ค‘์น˜(์กฐ๊ฑด๋ณ„ ํ‘œ์ค€ํ™” ํŠน์ง• 1๋‹จ์œ„๋‹น ์ ์ˆ˜): alt_err -6.7, jerk -3.1, touch_vz -2.6, n_risk -2.4, hover_drift -2.2, obs_rt -1.1, overshoot -1.1, att_exceed -0.4, path_rmse +0.2, obs_lateral +2.0, hover_std +2.2\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 05. ์กฐ์ข…์ž๋ณ„ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘, ๊ณผ๋ณด์ •) ๋ถ„์„ ๋ฐ ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰ ์‚ฐ์ถœ ๋ชจ๋“ˆ ๊ฐœ๋ฐœ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์กฐ์ข…์ž๋ณ„ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘, ๊ณผ๋ณด์ •)์„ ๋ถ„์„ํ•˜๊ณ  ์ˆ™๋ จ๋„ ๋“ฑ๊ธ‰ ๋ฐ ์‹ ๋ขฐ๋„ ์ ์ˆ˜๋ฅผ ์‚ฐ์ถœํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์กฐ์ข…์ž๋ณ„ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘, ๊ณผ๋ณด์ •)์„ ๋ถ„์„ํ•˜๊ณ  ์—ฌ๋Ÿฌ ๋น„ํ–‰์„ ๋ฌถ์–ด ์ˆ™๋ จ๋„ ๋“ฑ๊ธ‰๊ณผ ์‹ ๋ขฐ๋„ ๋“ฑ๊ธ‰(High/Medium/Low)์„ ์‚ฐ์ถœํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฐ™์€ ์กฐ๊ฑด์˜ ์Šต๊ด€ ์—†๋Š” ์กฐ์ข…์ž ๋Œ€๋น„ ์Šคํ‹ฑ ๋ณ€ํ™”์œจ๊ณผ ํšŒํ”ผ ํ›„ ์ดˆ๊ณผ ์ด๋™์œผ๋กœ ์Šต๊ด€ ํŒ์ •ํ•˜๊ณ  ์ •ํ™•๋„๋ฅผ ์ธก์ •ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/verification/trn01_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": [ "\"\"\"์กฐ์ข…์ž๋ณ„ ์—ฌ๋Ÿฌ ๋น„ํ–‰์˜ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘=์Šคํ‹ฑ ๋ณ€ํ™”์œจ, ๊ณผ๋ณด์ •=ํšŒํ”ผ ํ›„ ์ดˆ๊ณผ ์ด๋™)์„ ๋ถ„์„ํ•˜๊ณ , ๋น„ํ–‰ ์ ์ˆ˜๋ฅผ ๋ฌถ์–ด ์ˆ™๋ จ๋„ ๋“ฑ๊ธ‰๊ณผ ์‹ ๋ขฐ๋„(๋ถ€ํŠธ์ŠคํŠธ๋žฉ ๋“ฑ๊ธ‰ ํ™•๋ฅ )๋ฅผ ์‚ฐ์ถœํ•œ๋‹ค. ๋น„ํ–‰ 1ํšŒ๋กœ '์žฌ๊ต์œก ๋Œ€์ƒ'์„ ์ง€์ •ํ•˜๋˜ ๋ฐฉ์‹ ๋Œ€๋น„ ์˜ค์ง€์ •๋ฅ (FAR) ๊ฐ์ถ•, ROC-AUC, ์ž„๊ณ„๊ฐ’ ๋ฏผ๊ฐ๋„, ๋“ฑ๊ธ‰ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import Ridge\n", "from sklearn.metrics import roc_auc_score\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "train = make_dataset(40, seed=7)\n", "eng = Ridge(alpha=1.0).fit(cond_z(train, train, ADV_F)[ADV_F], train[\"instructor\"])\n", "base = Ridge(alpha=1.0).fit(cond_z(train, train, BASE_F)[BASE_F], train[\"instructor\"]) # ๋ฏธ์…˜ 3 Baseline ์ฑ„์ ์‹\n", "zt = cond_z(train, train, ADV_F) # ์Šต๊ด€ ํŒ์ • ๊ธฐ์ค€: ๊ฐ™์€ ์กฐ๊ฑด์—์„œ ์Šต๊ด€ ์—†๋Š” ์กฐ์ข…์ž์˜ ์ƒ์œ„ 10%\n", "jerk_th = zt.loc[~train[\"jerky\"], \"jerk\"].quantile(0.9)\n", "over_th = zt.loc[~train[\"overcorr\"], \"overshoot\"].quantile(0.9)\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ํ‰๊ฐ€ ์กฐ์ข…์ž 80๋ช… x ๋น„ํ–‰ 6ํšŒ(์ฃผ๊ฐ„ยท์•ผ๊ฐ„ยท๋Œํ’ ๊ฐ 2ํšŒ)\n", "test = make_dataset(80, seed=9, flights=2)\n", "test[\"score\"] = np.clip(eng.predict(cond_z(test, train, ADV_F)[ADV_F]), 0, 100)\n", "ze = cond_z(test, train, ADV_F)\n", "test[\"jerk_z\"], test[\"over_z\"] = ze[\"jerk\"], ze[\"overshoot\"]\n", "test[\"base\"] = np.clip(base.predict(cond_z(test, train, BASE_F)[BASE_F]), 0, 100)\n", "rng = np.random.default_rng(0)\n", "rows = []\n", "for pid, g in test.groupby(\"pilot\"):\n", " boot = [to_grade(rng.choice(g[\"score\"].values, len(g)).mean()).item() for _ in range(200)]\n", " grade = to_grade(g[\"score\"].mean()).item()\n", " conf = boot.count(grade) / len(boot)\n", " rows.append({\"pilot\": pid, \"์ ์ˆ˜\": g[\"score\"].mean(), \"์ ์ˆ˜SD\": g[\"score\"].std(), \"๋“ฑ๊ธ‰\": grade, \"์‹ ๋ขฐ๋„\": conf,\n", " \"์‹ ๋ขฐ๋“ฑ๊ธ‰\": \"High\" if conf >= 0.9 else (\"Medium\" if conf >= 0.7 else \"Low\"),\n", " \"๊ธ‰์กฐ์ž‘\": (g[\"jerk_z\"] > jerk_th).mean() >= 0.5, \"๊ณผ๋ณด์ •\": (g[\"over_z\"] > over_th).mean() >= 0.5,\n", " \"์‹ค์ œ๊ธ‰์กฐ์ž‘\": g[\"jerky\"].iloc[0], \"์‹ค์ œ๊ณผ๋ณด์ •\": g[\"overcorr\"].iloc[0],\n", " \"๊ต๊ด€๋“ฑ๊ธ‰\": to_grade(g[\"instructor\"].mean()).item(), \"๋‹จ์ผ๋น„ํ–‰์ ์ˆ˜\": g[\"base\"].iloc[int(rng.integers(len(g)))]})\n", "P = pd.DataFrame(rows)\n", "print(f\"์Šต๊ด€ ํŒ์ • ์ •ํ™•๋„: ๊ธ‰์กฐ์ž‘ {(P['๊ธ‰์กฐ์ž‘'] == P['์‹ค์ œ๊ธ‰์กฐ์ž‘']).mean():.0%}, ๊ณผ๋ณด์ • {(P['๊ณผ๋ณด์ •'] == P['์‹ค์ œ๊ณผ๋ณด์ •']).mean():.0%} (์กฐ์ข…์ž {len(P)}๋ช…)\")\n", "\n", "# ์žฌ๊ต์œก ๋Œ€์ƒ(C) ์ง€์ •: ๊ธฐ์กด(๋ฌด์ž‘์œ„ ๋น„ํ–‰ 1ํšŒ์˜ Baseline ์ ์ˆ˜ 55 ๋ฏธ๋งŒ) vs ์—”์ง„(6ํšŒ ํ‰๊ท  C + ์‹ ๋ขฐ๋„ Medium ์ด์ƒ, Low๋Š” ๊ต๊ด€ ํ™•์ธ์œผ๋กœ ๋ณด๋ƒ„)\n", "need = P[\"๊ต๊ด€๋“ฑ๊ธ‰\"] == \"C\"\n", "old = P[\"๋‹จ์ผ๋น„ํ–‰์ ์ˆ˜\"] < 55\n", "new = (P[\"๋“ฑ๊ธ‰\"] == \"C\") & (P[\"์‹ ๋ขฐ๋“ฑ๊ธ‰\"] != \"Low\")\n", "review = (P[\"๋“ฑ๊ธ‰\"] == \"C\") & (P[\"์‹ ๋ขฐ๋“ฑ๊ธ‰\"] == \"Low\")\n", "\n", "\n", "def far_rec(flag):\n", " return (flag & ~need).sum() / max(flag.sum(), 1), (flag & need).sum() / max(need.sum(), 1), int(flag.sum())\n", "\n", "\n", "f0, r0, n0 = far_rec(old)\n", "f1, r1, n1 = far_rec(new)\n", "print(pd.DataFrame([{\"๋ฐฉ์‹\": \"๊ธฐ์กด(๋น„ํ–‰ 1ํšŒ Baseline<55)\", \"์ง€์ •\": n0, \"์˜ค์ง€์ •๋ฅ \": round(f0, 3), \"์žฌํ˜„์œจ\": round(r0, 3)},\n", " {\"๋ฐฉ์‹\": \"์—”์ง„(6ํšŒ ํ‰๊ท  C, ์‹ ๋ขฐ Medium ์ด์ƒ)\", \"์ง€์ •\": n1, \"์˜ค์ง€์ •๋ฅ \": round(f1, 3), \"์žฌํ˜„์œจ\": round(r1, 3)}]).to_string(index=False))\n", "cut = 1 - f1 / max(f0, 1e-9)\n", "print(f\"์˜ค์ง€์ •๋ฅ (FAR) ๊ฐ์ถ• {cut:.1%} (๋ชฉํ‘œ 40% ์ด์ƒ: {'๋‹ฌ์„ฑ' if cut >= 0.4 else '๋ฏธ๋‹ฌ'}), ์‹ ๋ขฐ๋„ Low๋กœ ๊ต๊ด€ ํ™•์ธ์— ๋„˜๊ธด ์กฐ์ข…์ž {int(review.sum())}๋ช…\")\n", "print(f\"ROC-AUC(์žฌ๊ต์œก ํ•„์š” ํŒ๋ณ„, ํ‰๊ท  ์ ์ˆ˜ ๊ธฐ์ค€) {roc_auc_score(need, -P['์ ์ˆ˜']):.3f}\")\n", "\n", "print(\"\\n๊ธฐ์ค€์ ์ˆ˜ ์ง€์ • ์˜ค์ง€์ •๋ฅ  ์žฌํ˜„์œจ\")\n", "for th in [50, 55, 60, 65]:\n", " f, r, n = far_rec(P[\"์ ์ˆ˜\"] < th)\n", " print(f\" {th} {n:3d} {f:.3f} {r:.3f}\")\n", "\n", "# ๋“ฑ๊ธ‰ ์ผ๊ด€์„ฑ: ๋น„ํ–‰ 6ํšŒ ์ค‘ 5ํšŒ๋งŒ ๋ฌด์ž‘์œ„๋กœ ์จ๋„ ๊ฐ™์€ ๋“ฑ๊ธ‰์ด ๋‚˜์˜ค๋Š” ๋น„์œจ\n", "cons = []\n", "for pid, g in test.groupby(\"pilot\"):\n", " full = to_grade(g[\"score\"].mean()).item()\n", " cons += [to_grade(g[\"score\"].sample(5, random_state=k).mean()).item() == full for k in range(20)]\n", "print(f\"\\n๋“ฑ๊ธ‰ ์ผ๊ด€์„ฑ {np.mean(cons):.1%} (๋ชฉํ‘œ 90% ์ด์ƒ: {'๋‹ฌ์„ฑ' if np.mean(cons) >= 0.9 else '๋ฏธ๋‹ฌ'})\")\n", "print(\"[์‹ ๋ขฐ๋“ฑ๊ธ‰ ๋ถ„ํฌ]\", P[\"์‹ ๋ขฐ๋“ฑ๊ธ‰\"].value_counts().to_dict(), \"/ ๋“ฑ๊ธ‰ ์ผ์น˜(๊ต๊ด€ ๋Œ€๋น„)\", f\"{(P['๋“ฑ๊ธ‰'] == P['๊ต๊ด€๋“ฑ๊ธ‰']).mean():.0%}\")\n", "print(\"์‹ ๋ขฐ๋“ฑ๊ธ‰ ๊ธฐ์ค€: High(๋ถ€ํŠธ์ŠคํŠธ๋žฉ ๋™์ผ ๋“ฑ๊ธ‰ 90% ์ด์ƒ)=๊ฒฐ๊ณผ ํ†ต๋ณด, Medium(70~90%)=๊ต๊ด€ ๊ฒ€ํ†  ํ›„ ํ†ต๋ณด, Low=์ถ”๊ฐ€ ๋น„ํ–‰ ํ›„ ์žฌํ‰๊ฐ€\")\n", "\n", "P = P.sort_values(\"์ ์ˆ˜\")\n", "plt.figure(figsize=(10, 4))\n", "col = P[\"์‹ ๋ขฐ๋“ฑ๊ธ‰\"].map({\"High\": \"tab:green\", \"Medium\": \"tab:orange\", \"Low\": \"tab:red\"})\n", "plt.errorbar(range(len(P)), P[\"์ ์ˆ˜\"], yerr=P[\"์ ์ˆ˜SD\"] / np.sqrt(6) * 2, fmt=\"none\", ecolor=\"gray\")\n", "plt.scatter(range(len(P)), P[\"์ ์ˆ˜\"], c=col)\n", "plt.axhline(55, ls=\"--\", c=\"k\"); plt.axhline(75, ls=\"--\", c=\"k\")\n", "plt.xticks(range(len(P)), P[\"pilot\"], rotation=90, fontsize=6); plt.ylabel(\"mean skill score (+-2 SE)\")\n", "plt.title(\"Pilot skill score with uncertainty (color = confidence High/Medium/Low)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 05 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์Šต๊ด€ ํŒ์ • ์ •ํ™•๋„: ๊ธ‰์กฐ์ž‘ 95%, ๊ณผ๋ณด์ • 88% (์กฐ์ข…์ž 80๋ช…)\n", " ๋ฐฉ์‹ ์ง€์ • ์˜ค์ง€์ •๋ฅ  ์žฌํ˜„์œจ\n", " ๊ธฐ์กด(๋น„ํ–‰ 1ํšŒ Baseline<55) 27 0.111 0.750\n", "์—”์ง„(6ํšŒ ํ‰๊ท  C, ์‹ ๋ขฐ Medium ์ด์ƒ) 31 0.032 0.938\n", "์˜ค์ง€์ •๋ฅ (FAR) ๊ฐ์ถ• 71.0% (๋ชฉํ‘œ 40% ์ด์ƒ: ๋‹ฌ์„ฑ), ์‹ ๋ขฐ๋„ Low๋กœ ๊ต๊ด€ ํ™•์ธ์— ๋„˜๊ธด ์กฐ์ข…์ž 1๋ช…\n", "ROC-AUC(์žฌ๊ต์œก ํ•„์š” ํŒ๋ณ„, ํ‰๊ท  ์ ์ˆ˜ ๊ธฐ์ค€) 0.997\n", "\n", "๊ธฐ์ค€์ ์ˆ˜ ์ง€์ • ์˜ค์ง€์ •๋ฅ  ์žฌํ˜„์œจ\n", " 50 22 0.000 0.688\n", " 55 32 0.031 0.969\n", " 60 43 0.256 1.000\n", " 65 53 0.396 1.000\n", "\n", "๋“ฑ๊ธ‰ ์ผ๊ด€์„ฑ 97.6% (๋ชฉํ‘œ 90% ์ด์ƒ: ๋‹ฌ์„ฑ)\n", "[์‹ ๋ขฐ๋“ฑ๊ธ‰ ๋ถ„ํฌ] {'High': 66, 'Medium': 9, 'Low': 5} / ๋“ฑ๊ธ‰ ์ผ์น˜(๊ต๊ด€ ๋Œ€๋น„) 98%\n", "์‹ ๋ขฐ๋“ฑ๊ธ‰ ๊ธฐ์ค€: High(๋ถ€ํŠธ์ŠคํŠธ๋žฉ ๋™์ผ ๋“ฑ๊ธ‰ 90% ์ด์ƒ)=๊ฒฐ๊ณผ ํ†ต๋ณด, Medium(70~90%)=๊ต๊ด€ ๊ฒ€ํ†  ํ›„ ํ†ต๋ณด, Low=์ถ”๊ฐ€ ๋น„ํ–‰ ํ›„ ์žฌํ‰๊ฐ€\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 06. ์กฐ์ข…์ž ๋น„ํ–‰ ๊ถค์  3D ์žฌ์ƒ ๋ฐ ์ทจ์•ฝ ๊ตฌ๊ฐ„ ํ•˜์ด๋ผ์ดํŠธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 2.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "์กฐ์ข…์ž ๋น„ํ–‰ ๊ถค์  3D ์žฌ์ƒ ๋ฐ ์ทจ์•ฝ ๊ตฌ๊ฐ„ ํ•˜์ด๋ผ์ดํŠธ ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์กฐ์ข…์ž ๋น„ํ–‰ ๊ถค์ ์„ 3D๋กœ ์žฌ์ƒํ•˜๊ณ  ์ทจ์•ฝ ๊ตฌ๊ฐ„์„ ๊ฐ•์กฐํ•˜๋Š” ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **๊ฒฝ๋กœ ์ดํƒˆ 3m ์ดˆ๊ณผ, ์ž์„ธ 30๋„ ์ดˆ๊ณผ, ๊ธ‰์กฐ์ž‘ ๊ตฌ๊ฐ„์„ ์ทจ์•ฝ ๊ตฌ๊ฐ„์œผ๋กœ ์ฐพ์•„ 3D ๊ถค์ ์— ๋ถ‰๊ฒŒ ํ‘œ์‹œํ•˜๊ณ , ๋น„ํ–‰ ๋‹จ๊ณ„๋ณ„ ๊ฒฝ๋กœ ์ดํƒˆ ์ถ”์ด, ์Šคํ‹ฑ ์ž…๋ ฅ, ๋™๋ฃŒ ๋Œ€๋น„ ์•ฝ์  ํ”„๋กœํ•„์„ ํ•จ๊ป˜ ๋ณด์—ฌ ์ค€๋‹ค ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/ui/trn01_dashboard.py (๋˜๋Š” ์›น UI ์ปดํฌ๋„ŒํŠธ)` ํ˜•ํƒœ์˜ ๊ฒฐ๊ณผ๋ฌผ์„ ์™„์„ฑํ•œ๋‹ค.\n", "\n", "### ๐Ÿ” ํ‰๊ฐ€ ๋ฐ ๊ฒ€์ฆ ๊ธฐ์ค€ (PASS ์กฐ๊ฑด)\n", "#### ๋ฏธ์…˜ ํŒ์ •\n", "\n", "**ํ‰๊ฐ€ ๊ตฌ๋ถ„:** ํ•„์ˆ˜\n", "\n", "**ํ•ต์‹ฌ ํ‰๊ฐ€์˜์—ญ:** ์ „์ˆ  ์ž‘์ „ ์ƒํ™ฉํŒ์œผ๋กœ์„œ์˜ ์ง๊ด€์„ฑ ๋ฐ ์ธ์ง€ ๋ถ€ํ•˜ ๊ฐ์†Œ๋„\n", "\n", "#### ๋ฏธ์…˜ PASS ์กฐ๊ฑด\n", "\n", "- ์ •๋Ÿ‰ ๊ธฐ์ค€: ํ™”๋ฉด ๊ฐฑ์‹  ์ฃผ๊ธฐ 1์ดˆ ์ด๋‚ด, UI ์‘๋‹ต์„ฑ 100ms ์ด๋‚ด\n", "\n", "\n", "- ์ง„ํ–‰ ๊ธฐ์ค€: ์ฃผ์š” ์ง€ํ‘œ ์‹œ๊ฐํ™” ์ปดํฌ๋„ŒํŠธ 4์ข… ์ด์ƒ ๋ Œ๋”๋ง ๋ฐ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—ฐ๋™\n", "\n", "\n", "- ๊ฒ€์ฆ ๋ฐฉ๋ฒ•: ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ ๋ฐ ๋ชจ์˜ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ๋ Œ๋”๋ง ๊ฒ€์ฆ\n", "\n", "\n", "#### ์ฑ„์ ์ž ํŒ์ • ๋ฉ”๋ชจ\n", "\n", "> ๋‹จ์ˆœ ์ฝ”๋“œ ์‹คํ–‰ ์—ฌ๋ถ€๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ถœ๋ ฅ๋œ ์ˆ˜์น˜์™€ ๊ทผ๊ฑฐ ์ž๋ฃŒ๊ฐ€ ๊ตฐ ์‹ค๋ฌด ์š”๊ตฌ์‚ฌํ•ญ์— ๋ถ€ํ•ฉํ•˜๋Š”์ง€ ๋ฉด๋ฐ€ํžˆ ํ™•์ธํ•œ๋‹ค.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"์กฐ์ข…์ž 1๋ช…์˜ ๋น„ํ–‰ ๊ถค์ ์„ 3D๋กœ ์žฌ์ƒํ•˜๊ณ  ๊ฒฝ๋กœ ์ดํƒˆยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ ๊ตฌ๊ฐ„์„ ์ทจ์•ฝ ๊ตฌ๊ฐ„์œผ๋กœ ๊ฐ•์กฐํ•˜๋Š” ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ(์•ผ๊ฐ„ ๋ชจ๋“œ)๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์žฌ์ƒ 1์ดˆ๋งˆ๋‹ค ์ง€ํ‘œ๋ฅผ ๊ฐฑ์‹ ํ•˜๋Š” ์ฒ˜๋ฆฌ์‹œ๊ฐ„์„ ์ธก์ •ํ•˜๊ณ  ์ทจ์•ฝ ๊ตฌ๊ฐ„ ๋ชฉ๋ก์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "PHASES = [(0, 60, \"path\"), (60, 85, \"hover\"), (85, 100, \"obstacle\"), (100, 120, \"landing\")]\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์กฐ์ข…์ž 12๋ช… ์ค‘ ๊ฐ€์žฅ ์ ์ˆ˜๊ฐ€ ๋‚ฎ์€ ์กฐ์ข…์ž์˜ ๋Œํ’ ๋น„ํ–‰์„ ์žฌ์ƒ\n", "pilots = make_pilots(12, seed=4)\n", "log, truth = fly(pilots, \"gust\", seed=21)\n", "j = int(truth[\"instructor\"].idxmin())\n", "d = one(log, j)\n", "FE = features(log).assign(cond=\"gust\")\n", "f = FE.iloc[j]\n", "d[\"err\"] = np.hypot(d[\"x\"] - d[\"tx\"], d[\"y\"] - d[\"ty\"])\n", "d[\"att\"] = np.maximum(d[\"roll_deg\"].abs(), d[\"pitch_deg\"].abs())\n", "d[\"stick_rate\"] = np.r_[0, np.abs(np.diff(d[\"stick_roll\"])) / DT]\n", "\n", "\n", "def weak_flags(df):\n", " return {\"๊ฒฝ๋กœ์ดํƒˆ>3m\": df[\"err\"] > 3.0, \"์ž์„ธ>30๋„\": df[\"att\"] > 30, \"๊ธ‰์กฐ์ž‘(๋ณ€ํ™”์œจ>6/s)\": df[\"stick_rate\"] > 6}\n", "\n", "\n", "# 1) ์žฌ์ƒ ๋ชจ์˜: 1์ดˆ(10ํ”„๋ ˆ์ž„)๋งˆ๋‹ค ๋ˆ„์  ์ง€ํ‘œ ๊ฐฑ์‹  + ์ฒ˜๋ฆฌ์‹œ๊ฐ„ ์ธก์ •\n", "ms, kpis = [], []\n", "for k in range(10, len(d) + 1, 10):\n", " t0 = time.perf_counter()\n", " cur = d.iloc[:k]\n", " flags = weak_flags(cur)\n", " kpis.append({\"t\": cur[\"t\"].iloc[-1], \"err_now\": cur[\"err\"].iloc[-1], \"weak_ratio\": np.mean(np.any(np.c_[tuple(flags.values())], axis=1))})\n", " ms.append((time.perf_counter() - t0) * 1000)\n", "print(f\"์กฐ์ข…์ž {truth['pilot'][j]}(๊ต๊ด€ {truth['instructor'][j]}์ , ๋“ฑ๊ธ‰ {truth['grade'][j]}) ๋Œํ’ ๋น„ํ–‰ ์žฌ์ƒ: ๊ฐฑ์‹  {len(ms)}ํšŒ, \"\n", " f\"์ฒ˜๋ฆฌ ํ‰๊ท  {np.mean(ms):.2f} ms, ์ตœ๋Œ€ {np.max(ms):.2f} ms (๋ชฉํ‘œ 1์ดˆ ์ฃผ๊ธฐยท100 ms ์ด๋‚ด -> {'๋‹ฌ์„ฑ' if np.max(ms) < 100 else '๋ฏธ๋‹ฌ'})\")\n", "\n", "# 2) ์ทจ์•ฝ ๊ตฌ๊ฐ„ ๋ชฉ๋ก(์—ฐ์† 0.5์ดˆ ์ด์ƒ)\n", "segs = []\n", "for name, m in weak_flags(d).items():\n", " m = m.values.astype(int)\n", " edges = np.flatnonzero(np.diff(np.r_[0, m, 0]))\n", " for a, b in zip(edges[::2], edges[1::2]):\n", " if b - a >= 5:\n", " ph = next(p for s, e, p in PHASES if s <= d[\"t\"][a] < e)\n", " segs.append({\"๊ตฌ๋ถ„\": name, \"์‹œ์ž‘(s)\": round(d[\"t\"][a], 1), \"๊ธธ์ด(s)\": round((b - a) * DT, 1), \"๋‹จ๊ณ„\": ph})\n", "S = pd.DataFrame(segs).sort_values(\"์‹œ์ž‘(s)\")\n", "print(f\"\\n[์ทจ์•ฝ ๊ตฌ๊ฐ„ {len(S)}๊ฐœ - ์‹œ๊ฐ„์ˆœ ์ƒ์œ„ 10๊ฐœ]\")\n", "print(S.head(10).to_string(index=False))\n", "print(\"\\n๋‹จ๊ณ„๋ณ„ ์ทจ์•ฝ ์‹œ๊ฐ„(s):\", S.groupby(\"๋‹จ๊ณ„\")[\"๊ธธ์ด(s)\"].sum().round(1).to_dict())\n", "print(f\"๋Œ๋ฐœ ๋Œ€์‘: ์žฅ์• ๋ฌผ ๋ฐ˜์‘ {f['obs_rt']:.1f}s, 2.5์ดˆ ํ›„ ํšŒํ”ผ๊ฑฐ๋ฆฌ {f['obs_lateral']:.1f}m, ์ ‘์ง€ ์†๋„ {f['touch_vz']:.1f}m/s -> \"\n", " f\"์œ„ํ—˜ ์‚ฌ๊ฑด {', '.join(k for k, v in detect(FE.iloc[[j]]).iloc[0].items() if v) or '์—†์Œ'}\")\n", "\n", "# 3) ๋Œ€์‹œ๋ณด๋“œ(์•ผ๊ฐ„ ๋ชจ๋“œ, ๊ตฌ์„ฑ์š”์†Œ 4์ข…)\n", "plt.style.use(\"dark_background\")\n", "fig = plt.figure(figsize=(13, 8))\n", "ax = fig.add_subplot(2, 2, 1, projection=\"3d\")\n", "bad = np.any(np.c_[tuple(weak_flags(d).values())], axis=1)\n", "ax.plot(d[\"tx\"], d[\"ty\"], d[\"talt\"], \"--\", c=\"gray\", lw=1, label=\"course\")\n", "ax.scatter(d[\"x\"][~bad], d[\"y\"][~bad], d[\"alt\"][~bad], s=2, c=\"deepskyblue\", label=\"normal\")\n", "ax.scatter(d[\"x\"][bad], d[\"y\"][bad], d[\"alt\"][bad], s=6, c=\"red\", label=\"weak segment\")\n", "ax.set_xlabel(\"x (m)\"); ax.set_ylabel(\"y (m)\"); ax.set_zlabel(\"alt (m)\"); ax.legend(fontsize=7); ax.set_title(\"3D replay (red = weak)\")\n", "a2 = fig.add_subplot(2, 2, 2)\n", "a2.plot(d[\"t\"], d[\"err\"], c=\"lime\", lw=1); a2.axhline(3, c=\"red\", ls=\"--\")\n", "for s, e, p in PHASES:\n", " a2.axvspan(s, e, alpha=0.08 if p in (\"path\", \"obstacle\") else 0.16, color=\"white\"); a2.text(s + 1, a2.get_ylim()[1] * 0.9, p, fontsize=8)\n", "a2.set_title(\"Path deviation (m) by phase\")\n", "a3 = fig.add_subplot(2, 2, 3)\n", "a3.plot(d[\"t\"], d[\"stick_roll\"], lw=0.8, label=\"roll\"); a3.plot(d[\"t\"], d[\"stick_pitch\"], lw=0.8, label=\"pitch\"); a3.plot(d[\"t\"], d[\"throttle\"], lw=0.8, label=\"throttle\")\n", "a3.scatter(d[\"t\"][d[\"stick_rate\"] > 6], d[\"stick_roll\"][d[\"stick_rate\"] > 6], c=\"red\", s=10, label=\"abrupt\")\n", "a3.legend(fontsize=7, ncol=4); a3.set_title(\"Stick inputs\"); a3.set_xlabel(\"time (s)\")\n", "a4 = fig.add_subplot(2, 2, 4)\n", "z = cond_z(FE, FE, ADV_F).iloc[j][[\"path_rmse\", \"hover_std\", \"obs_rt\", \"touch_vz\", \"jerk\", \"overshoot\"]]\n", "a4.barh(z.index, z.values, color=[\"red\" if v > 1 else \"orange\" if v > 0 else \"deepskyblue\" for v in z.values])\n", "a4.axvline(0, c=\"white\", lw=0.5); a4.set_title(\"Weakness profile (z-score vs peers, right = worse)\")\n", "fig.suptitle(f\"Pilot {truth['pilot'][j]} flight replay dashboard (night mode) - training data only\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 06 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "์กฐ์ข…์ž P010(๊ต๊ด€ 33.7์ , ๋“ฑ๊ธ‰ C) ๋Œํ’ ๋น„ํ–‰ ์žฌ์ƒ: ๊ฐฑ์‹  120ํšŒ, ์ฒ˜๋ฆฌ ํ‰๊ท  0.50 ms, ์ตœ๋Œ€ 1.30 ms (๋ชฉํ‘œ 1์ดˆ ์ฃผ๊ธฐยท100 ms ์ด๋‚ด -> ๋‹ฌ์„ฑ)\n", "\n", "[์ทจ์•ฝ ๊ตฌ๊ฐ„ 34๊ฐœ - ์‹œ๊ฐ„์ˆœ ์ƒ์œ„ 10๊ฐœ]\n", " ๊ตฌ๋ถ„ ์‹œ์ž‘(s) ๊ธธ์ด(s) ๋‹จ๊ณ„\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 2.0 2.7 path\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 8.6 5.9 path\n", " ์ž์„ธ>30๋„ 11.5 1.8 path\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 14.9 3.4 path\n", " ์ž์„ธ>30๋„ 17.3 0.5 path\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 19.2 4.5 path\n", " ์ž์„ธ>30๋„ 20.4 1.5 path\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 25.6 2.5 path\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 29.1 1.2 path\n", "๊ฒฝ๋กœ์ดํƒˆ>3m 30.6 4.0 path\n", "\n", "๋‹จ๊ณ„๋ณ„ ์ทจ์•ฝ ์‹œ๊ฐ„(s): {'hover': 27.8, 'landing': 18.5, 'obstacle': 8.0, 'path': 63.7}\n", "๋Œ๋ฐœ ๋Œ€์‘: ์žฅ์• ๋ฌผ ๋ฐ˜์‘ 2.1s, 2.5์ดˆ ํ›„ ํšŒํ”ผ๊ฑฐ๋ฆฌ 1.1m, ์ ‘์ง€ ์†๋„ 2.7m/s -> ์œ„ํ—˜ ์‚ฌ๊ฑด near_collision, hard_landing, attitude_exceed\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 07. ๊ต๊ด€์ด ์ง์ ‘ ์กฐ์ข…์ž ์„ฑํ–ฅ์„ ์ง„๋‹จํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ž…๋ ฅํ•˜๋Š” HITL ํ‰๊ฐ€ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๊ต๊ด€์ด ์ง์ ‘ ์กฐ์ข…์ž ์„ฑํ–ฅ์„ ์ง„๋‹จํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ž…๋ ฅํ•˜๋Š” HITL ํ‰๊ฐ€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๊ต๊ด€์ด AI ์ œ์•ˆ ๋“ฑ๊ธ‰๊ณผ ์กฐ์ž‘ ์„ฑํ–ฅ ์ง„๋‹จ์„ ์ง์ ‘ ํ™•์ธยท์ˆ˜์ •ํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ž…๋ ฅํ•˜๋Š” HITL ํ‰๊ฐ€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์„ค๊ณ„ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **AI ์ œ์•ˆ์€ ํ•ญ์ƒ ๊ฒ€ํ†  ๋Œ€๊ธฐ๋กœ ๋“ฑ๋กํ•˜๊ณ  ์‹ ๋ขฐ๋„๊ฐ€ ๋‚ฎ์€ ์ œ์•ˆ๊ณผ C ๋“ฑ๊ธ‰์„ ๋จผ์ € ๋ณด๋„๋ก ์ˆœ์„œ๋ฅผ ์ • ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/hitl/trn01_approval_workflow.py ๋ฐ docs/trn01_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": [ "\"\"\"AI๊ฐ€ ์ œ์•ˆํ•œ ์ˆ™๋ จ๋„ ๋“ฑ๊ธ‰ยท์กฐ์ž‘ ์„ฑํ–ฅ์„ ๊ต๊ด€์ด ์ง์ ‘ ํ™•์ธยท์ˆ˜์ •ํ•˜๊ณ  ํ”ผ๋“œ๋ฐฑ์„ ์ž…๋ ฅํ•˜๋Š” HITL ํ‰๊ฐ€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. AI๋Š” ๋“ฑ๊ธ‰์„ ํ™•์ •ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๋ถˆ๋ฆฌํ•œ ๊ฒฐ์ •(์žฌ๊ต์œก ์ง€์ •ยท์ž๊ฒฉ ๋ณด๋ฅ˜)์€ ์ƒ์„ธ ๊ทผ๊ฑฐ์™€ ๋‘ ๋ฒˆ์งธ ๊ต๊ด€ ํ™•์ธ์„ ์š”๊ตฌํ•˜๊ณ , ๋ชจ๋“  ๊ฒฐ์ •์€ ์œ„ยท๋ณ€์กฐ ํ™•์ธ์ด ๊ฐ€๋Šฅํ•œ ๊ฐ์‚ฌ ๋กœ๊ทธ์— ๋‚จ๊ธด๋‹ค.\"\"\"\n", "import hashlib\n", "import json\n", "import pandas as pd\n", "\n", "ACTIONS = {\"CONFIRM\": \"AI ์ œ์•ˆ ๋“ฑ๊ธ‰ ํ™•์ •\", \"MODIFY\": \"๋“ฑ๊ธ‰ ์ˆ˜์ •\", \"REFLIGHT\": \"์ถ”๊ฐ€ ๋น„ํ–‰ ํ›„ ์žฌํ‰๊ฐ€\", \"RETRAIN\": \"์žฌ๊ต์œก ์ง€์ •\", \"QUAL_HOLD\": \"์ž๊ฒฉ ๋ถ€์—ฌ ๋ณด๋ฅ˜\"}\n", "CONF_ORDER = {\"Low\": 0, \"Medium\": 1, \"High\": 2}\n", "\n", "\n", "class ReviewError(ValueError):\n", " pass\n", "\n", "\n", "class FakeClock:\n", " t = 0.0\n", "\n", " def __call__(self):\n", " return self.t\n", "\n", "\n", "class ReviewQueue:\n", " def __init__(self, clock):\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(), 1)}\n", " self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode()).hexdigest()})\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, pilot, grade, conf, habits, evidence):\n", " \"\"\"AI ์ œ์•ˆ ๋“ฑ๋ก: ํ•ญ์ƒ PENDING์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉฐ AI๊ฐ€ ํ™•์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค.\"\"\"\n", " self.items[pilot] = {\"pilot\": pilot, \"ai_grade\": grade, \"conf\": conf, \"habits\": habits, \"evidence\": evidence,\n", " \"status\": \"PENDING\", \"final\": None, \"submitted\": self.clock(), \"approvals\": []}\n", " self._record({\"event\": \"AI_SUGGEST\", \"pilot\": pilot, \"grade\": grade, \"conf\": conf, \"by\": \"AI\"})\n", "\n", " def order(self):\n", " \"\"\"๊ฒ€ํ†  ์ˆœ์„œ: ์‹ ๋ขฐ๋„ ๋‚ฎ์€ ์ œ์•ˆ -> C ๋“ฑ๊ธ‰ -> ๋‚˜๋จธ์ง€(๊ต๊ด€์ด ํŒ๋‹จ์ด ํ•„์š”ํ•œ ๊ฑด๋ถ€ํ„ฐ ๋ณธ๋‹ค).\"\"\"\n", " todo = [i for i in self.items.values() if i[\"status\"] in (\"PENDING\", \"AWAIT_2ND\")]\n", " return sorted(todo, key=lambda i: (CONF_ORDER[i[\"conf\"]], i[\"ai_grade\"] != \"C\", i[\"pilot\"]))\n", "\n", " def decide(self, pilot, action, instructor, reason, new_grade=None):\n", " if action not in ACTIONS:\n", " raise ReviewError(f\"ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์กฐ์น˜: {action}\")\n", " if not instructor:\n", " raise ReviewError(\"๊ต๊ด€ ID ํ•„์š”\")\n", " it = self.items.get(pilot)\n", " if it is None or it[\"status\"] == \"FINAL\":\n", " raise ReviewError(\"๊ฒ€ํ†  ๋Œ€๊ธฐ ๊ฑด์ด ์•„๋‹˜\")\n", " if len(reason.strip()) < 10:\n", " raise ReviewError(\"์ง„๋‹จยทํ”ผ๋“œ๋ฐฑ 10์ž ์ด์ƒ ํ•„์š”\")\n", " if action == \"MODIFY\" and new_grade not in (\"A\", \"B\", \"C\"):\n", " raise ReviewError(\"์ˆ˜์ • ๋“ฑ๊ธ‰(A/B/C) ํ•„์š”\")\n", " if action in (\"RETRAIN\", \"QUAL_HOLD\") and len(reason.strip()) < 20:\n", " raise ReviewError(\"๋ถˆ๋ฆฌํ•œ ๊ฒฐ์ •์€ ๊ทผ๊ฑฐ 20์ž ์ด์ƒ ํ•„์š”\")\n", " if action == \"QUAL_HOLD\":\n", " if instructor in it[\"approvals\"]:\n", " raise ReviewError(\"๋‘ ๋ฒˆ์งธ ํ™•์ธ์€ ๋‹ค๋ฅธ ๊ต๊ด€์ด ํ•ด์•ผ ํ•จ\")\n", " it[\"approvals\"].append(instructor)\n", " self._record({\"event\": \"DECIDE\", \"pilot\": pilot, \"action\": action, \"by\": instructor, \"reason\": reason, \"step\": len(it[\"approvals\"])})\n", " if len(it[\"approvals\"]) < 2:\n", " it[\"status\"] = \"AWAIT_2ND\"\n", " return \"๋‘ ๋ฒˆ์งธ ๊ต๊ด€ ํ™•์ธ ๋Œ€๊ธฐ\"\n", " else:\n", " self._record({\"event\": \"DECIDE\", \"pilot\": pilot, \"action\": action, \"by\": instructor, \"reason\": reason, \"new_grade\": new_grade})\n", " it[\"status\"], it[\"action\"], it[\"decided\"] = \"FINAL\", action, self.clock()\n", " it[\"final\"] = new_grade if action == \"MODIFY\" else (None if action == \"REFLIGHT\" else it[\"ai_grade\"])\n", " return \"ํ™•์ •\"\n", "\n", "\n", "clock = FakeClock()\n", "rq = ReviewQueue(clock)\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋ฏธ์…˜ 5 ๊ฒฐ๊ณผ์—์„œ ๋„˜์–ด์˜จ AI ์ œ์•ˆ 6๊ฑด(๊ฐ€๋ช…)\n", "for p, g, c, h, e in [(\"P003\", \"A\", \"High\", [], \"๊ฒฝ๋กœ RMSE ํ•˜์œ„ 10%, ์ ‘์ง€ 0.4m/s\"), (\"P011\", \"C\", \"High\", [\"๊ธ‰์กฐ์ž‘\"], \"๊ฒฝ์ฐฉ๋ฅ™ 2ํšŒ, ์Šคํ‹ฑ ๋ณ€ํ™”์œจ ์ƒ์œ„ 5%\"),\n", " (\"P017\", \"B\", \"Low\", [\"๊ณผ๋ณด์ •\"], \"๋น„ํ–‰๋ณ„ ์ ์ˆ˜ ํŽธ์ฐจ ํผ(48~79)\"), (\"P024\", \"C\", \"Medium\", [\"๊ณผ๋ณด์ •\"], \"์žฅ์• ๋ฌผ ๊ทผ์ ‘ 3ํšŒ\"),\n", " (\"P030\", \"B\", \"High\", [], \"ํ˜ธ๋ฒ„๋ง ์•ˆ์ •, ์žฅ์• ๋ฌผ ๋ฐ˜์‘ 1.2s\"), (\"P038\", \"C\", \"Low\", [], \"๋Œํ’ ๋น„ํ–‰๋งŒ ๋‚ฎ์Œ\")]:\n", " clock.t += 1; rq.submit(p, g, c, h, e)\n", "print(\"๊ฒ€ํ†  ์ˆœ์„œ:\", [f\"{i['pilot']}({i['ai_grade']}/{i['conf']})\" for i in rq.order()])\n", "print(\"AI๊ฐ€ ํ™•์ •ํ•œ ๊ฑด์ˆ˜:\", sum(i[\"status\"] == \"FINAL\" for i in rq.items.values()))\n", "\n", "for args in [(\"P011\", \"RETRAIN\", \"\", \"๊ฒฝ์ฐฉ๋ฅ™ ๋ฐ˜๋ณต, ์ฐฉ๋ฅ™ ์ ˆ์ฐจ ์žฌ๊ต์œก\"), (\"P003\", \"CONFIRM\", \"inst01\", \"์ข‹์Œ\"), (\"P017\", \"MODIFY\", \"inst01\", \"ํŽธ์ฐจ๊ฐ€ ์ปค์„œ ๋“ฑ๊ธ‰ ์กฐ์ •ํ•จ\"),\n", " (\"P024\", \"RETRAIN\", \"inst02\", \"๊ทผ์ ‘ 3ํšŒ ์žฌ๊ต์œก\"), (\"P003\", \"AUTO\", \"inst01\", \"์ž๋™ ํ™•์ • ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค\")]:\n", " try:\n", " rq.decide(*args)\n", " except ReviewError as e:\n", " print(f\"์ฐจ๋‹จ: {args[0]} {args[1]} -> {e}\")\n", "\n", "clock.t += 120; print(\"P003:\", rq.decide(\"P003\", \"CONFIRM\", \"inst01\", \"๊ฒฝ๋กœยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •, ๋Œ๋ฐœ ๋Œ€์‘ ์–‘ํ˜ธ ํ™•์ธ\"))\n", "clock.t += 200; print(\"P017:\", rq.decide(\"P017\", \"REFLIGHT\", \"inst01\", \"์ ์ˆ˜ ํŽธ์ฐจ ํผ, ์ฃผ๊ฐ„ ๋น„ํ–‰ 2ํšŒ ์ถ”๊ฐ€ ํ›„ ์žฌํ‰๊ฐ€\"))\n", "clock.t += 150; print(\"P011:\", rq.decide(\"P011\", \"RETRAIN\", \"inst02\", \"๊ฒฝ์ฐฉ๋ฅ™ 2ํšŒ์™€ ๊ธ‰์กฐ์ž‘ ํ™•์ธ, ์ฐฉ๋ฅ™ ๊ฐ์† ์ ˆ์ฐจ ์žฌ๊ต์œก ์ง€์ •\"))\n", "clock.t += 90; print(\"P024 1์ฐจ:\", rq.decide(\"P024\", \"QUAL_HOLD\", \"inst02\", \"์žฅ์• ๋ฌผ ๊ทผ์ ‘ 3ํšŒ ๋ฐ˜๋ณต, ํšŒํ”ผ ๊ธฐ๋™ ์ˆ™๋‹ฌ ์ „ ์ž๊ฒฉ ๋ณด๋ฅ˜\"))\n", "clock.t += 10\n", "try:\n", " rq.decide(\"P024\", \"QUAL_HOLD\", \"inst02\", \"๊ฐ™์€ ๊ต๊ด€์ด ๋‘ ๋ฒˆ์งธ ํ™•์ธ๊นŒ์ง€ ์ง์ ‘ ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•จ\")\n", "except ReviewError as e:\n", " print(\"์ฐจ๋‹จ: P024 QUAL_HOLD ->\", e)\n", "clock.t += 300; print(\"P024 2์ฐจ:\", rq.decide(\"P024\", \"QUAL_HOLD\", \"inst05\", \"์˜์ƒ ์žฌํ™•์ธ ๊ฒฐ๊ณผ ํšŒํ”ผ ์ง€์—ฐ ๋™์˜, ๋ณด๋ฅ˜ ์œ ์ง€\"))\n", "clock.t += 60; print(\"P038:\", rq.decide(\"P038\", \"MODIFY\", \"inst03\", \"๋Œํ’ ์™ธ ๋น„ํ–‰์€ B ์ˆ˜์ค€, ๋Œํ’ ๋Œ€์‘๋งŒ ๋ณด์™„\", new_grade=\"B\"))\n", "\n", "df = pd.DataFrame(rq.items.values())\n", "df[\"๋Œ€๊ธฐ(s)\"] = (df[\"decided\"] - df[\"submitted\"]).round(0)\n", "print(\"\\n\" + df[[\"pilot\", \"ai_grade\", \"conf\", \"action\", \"final\", \"๋Œ€๊ธฐ(s)\"]].fillna(\"-\").to_string(index=False))\n", "fin = df[df[\"status\"] == \"FINAL\"]\n", "print(f\"๊ต๊ด€ ๊ฒฐ์ • {len(fin)}๊ฑด ์ค‘ AI ์ œ์•ˆ ๋“ฑ๊ธ‰๊ณผ ๋‹ค๋ฅธ ๊ฒฐ์ • {int((fin['final'] != fin['ai_grade']).sum())}๊ฑด(์ถ”๊ฐ€ ๋น„ํ–‰ ํฌํ•จ), ๋ฏธ๊ฒ€ํ†  {int((df['status'] != 'FINAL').sum())}๊ฑด์€ ๋Œ€๊ธฐ ์œ ์ง€\")\n", "print(f\"\\n๊ฐ์‚ฌ ๋กœ๊ทธ {len(rq.log)}๊ฑด ๊ธฐ๋ก(์ œ์•ˆ {len(df)}๊ฑด + ๊ฒฐ์ • {len(rq.log) - len(df)}๊ฑด, ๋กœ๊น… ์„ฑ๊ณต๋ฅ  100%), ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: {rq.verify_log()}\")\n", "rq.log[7][\"reason\"] = \"๊ทผ๊ฑฐ ์ˆ˜์ •\"\n", "print(\"8๋ฒˆ์งธ ๊ธฐ๋ก ๋ณ€์กฐ ํ›„ ๊ฒ€์ฆ(False, ๊นจ์ง„ ์œ„์น˜):\", rq.verify_log())\n", "\n", "form = {\"pilot\": \"P011\", \"strength\": \"(๊ต๊ด€ ์ž…๋ ฅ)\", \"weakness\": \"์ฐฉ๋ฅ™ ๊ฐ์† ๋Šฆ์Œ, ๊ธ‰์กฐ์ž‘\", \"next_training\": \"์ฐฉ๋ฅ™ ์ ˆ์ฐจ ๋ฐ˜๋ณต 3ํšŒ\", \"instructor\": \"inst02\", \"pilot_ack\": \"(์„œ๋ช…)\"}\n", "print(\"\\n[์กฐ์ข…์ž ํ”ผ๋“œ๋ฐฑ ์–‘์‹]\\n\" + json.dumps(form, ensure_ascii=False))\n", "print(\"AI ์ ์ˆ˜๋Š” ๊ต๊ด€ ํŒ๋‹จ ๋ณด์กฐ ์ž๋ฃŒ์ด๋ฉฐ ์ธ์‚ฌยท์ž๊ฒฉ ๊ฒฐ์ •์˜ ๋‹จ๋… ๊ทผ๊ฑฐ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ์ตœ์ข… ๋“ฑ๊ธ‰์˜ ์ฑ…์ž„์€ ๊ฒฐ์ •ํ•œ ๊ต๊ด€์—๊ฒŒ ์žˆ๋‹ค.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 07 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "๊ฒ€ํ†  ์ˆœ์„œ: ['P038(C/Low)', 'P017(B/Low)', 'P024(C/Medium)', 'P011(C/High)', 'P003(A/High)', 'P030(B/High)']\n", "AI๊ฐ€ ํ™•์ •ํ•œ ๊ฑด์ˆ˜: 0\n", "์ฐจ๋‹จ: P011 RETRAIN -> ๊ต๊ด€ ID ํ•„์š”\n", "์ฐจ๋‹จ: P003 CONFIRM -> ์ง„๋‹จยทํ”ผ๋“œ๋ฐฑ 10์ž ์ด์ƒ ํ•„์š”\n", "์ฐจ๋‹จ: P017 MODIFY -> ์ˆ˜์ • ๋“ฑ๊ธ‰(A/B/C) ํ•„์š”\n", "์ฐจ๋‹จ: P024 RETRAIN -> ์ง„๋‹จยทํ”ผ๋“œ๋ฐฑ 10์ž ์ด์ƒ ํ•„์š”\n", "์ฐจ๋‹จ: P003 AUTO -> ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์กฐ์น˜: AUTO\n", "P003: ํ™•์ •\n", "P017: ํ™•์ •\n", "P011: ํ™•์ •\n", "P024 1์ฐจ: ๋‘ ๋ฒˆ์งธ ๊ต๊ด€ ํ™•์ธ ๋Œ€๊ธฐ\n", "์ฐจ๋‹จ: P024 QUAL_HOLD -> ๋‘ ๋ฒˆ์งธ ํ™•์ธ์€ ๋‹ค๋ฅธ ๊ต๊ด€์ด ํ•ด์•ผ ํ•จ\n", "P024 2์ฐจ: ํ™•์ •\n", "P038: ํ™•์ •\n", "\n", "pilot ai_grade conf action final ๋Œ€๊ธฐ(s)\n", " P003 A High CONFIRM A 125.0\n", " P011 C High RETRAIN C 474.0\n", " P017 B Low REFLIGHT - 323.0\n", " P024 C Medium QUAL_HOLD C 872.0\n", " P030 B High - - -\n", " P038 C Low MODIFY B 930.0\n", "๊ต๊ด€ ๊ฒฐ์ • 5๊ฑด ์ค‘ AI ์ œ์•ˆ ๋“ฑ๊ธ‰๊ณผ ๋‹ค๋ฅธ ๊ฒฐ์ • 2๊ฑด(์ถ”๊ฐ€ ๋น„ํ–‰ ํฌํ•จ), ๋ฏธ๊ฒ€ํ†  1๊ฑด์€ ๋Œ€๊ธฐ ์œ ์ง€\n", "\n", "๊ฐ์‚ฌ ๋กœ๊ทธ 12๊ฑด ๊ธฐ๋ก(์ œ์•ˆ 6๊ฑด + ๊ฒฐ์ • 6๊ฑด, ๋กœ๊น… ์„ฑ๊ณต๋ฅ  100%), ํ•ด์‹œ ์ฒด์ธ ๊ฒ€์ฆ: (True, -1)\n", "8๋ฒˆ์งธ ๊ธฐ๋ก ๋ณ€์กฐ ํ›„ ๊ฒ€์ฆ(False, ๊นจ์ง„ ์œ„์น˜): (False, 8)\n", "\n", "[์กฐ์ข…์ž ํ”ผ๋“œ๋ฐฑ ์–‘์‹]\n", "{\"pilot\": \"P011\", \"strength\": \"(๊ต๊ด€ ์ž…๋ ฅ)\", \"weakness\": \"์ฐฉ๋ฅ™ ๊ฐ์† ๋Šฆ์Œ, ๊ธ‰์กฐ์ž‘\", \"next_training\": \"์ฐฉ๋ฅ™ ์ ˆ์ฐจ ๋ฐ˜๋ณต 3ํšŒ\", \"instructor\": \"inst02\", \"pilot_ack\": \"(์„œ๋ช…)\"}\n", "AI ์ ์ˆ˜๋Š” ๊ต๊ด€ ํŒ๋‹จ ๋ณด์กฐ ์ž๋ฃŒ์ด๋ฉฐ ์ธ์‚ฌยท์ž๊ฒฉ ๊ฒฐ์ •์˜ ๋‹จ๋… ๊ทผ๊ฑฐ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ์ตœ์ข… ๋“ฑ๊ธ‰์˜ ์ฑ…์ž„์€ ๊ฒฐ์ •ํ•œ ๊ต๊ด€์—๊ฒŒ ์žˆ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 08. 100๋ช… ์กฐ์ข…์ž ๋Œ€๋Ÿ‰ ๋น„ํ–‰๋กœ๊ทธ ๋ฐฐ์น˜ ๋ถ„์„ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๋ฐ ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ๋ง\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "100๋ช… ์กฐ์ข…์ž์˜ ๋Œ€๋Ÿ‰ ๋น„ํ–‰๋กœ๊ทธ ๋ฐฐ์น˜ ๋ถ„์„ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **์กฐ์ข…์ž 100๋ช…์˜ ๋Œ€๋Ÿ‰ ๋น„ํ–‰๋กœ๊ทธ ๋ฐฐ์น˜ ๋ถ„์„์—์„œ ์ฒ˜๋ฆฌ๋Ÿ‰ ์ฆ๊ฐ€์™€ ์†์ƒ ๋กœ๊ทธ, ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ์ƒํ™ฉ์— ๋Œ€ํ•œ ๋ณต์›๋ ฅ์„ ๊ฒ€์ฆํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **ํ‰์‹œ 100๊ฑด, 100๋ช… x 5ํšŒ 500๊ฑด, 500% ์ฆ๊ฐ€ 600๊ฑด, ์†์ƒ ๋กœ๊ทธ 5% ํ˜ผ์ž…, ๋ฌถ์Œ ์ฒ˜๋ฆฌ ์˜ˆ์™ธ ์ƒํ™ฉ ์ฃผ์ž…ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์ง€์—ฐ ๋ฐ ๋ณต์›๋ ฅ์„ ์‹œํ—˜ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `tests/trn01_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": [ "\"\"\"์กฐ์ข…์ž 100๋ช…์˜ ๋Œ€๋Ÿ‰ ๋น„ํ–‰๋กœ๊ทธ๋ฅผ ๋ฐฐ์น˜ ๋ถ„์„ํ•˜๋Š” ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ํ‰์‹œ 100๊ฑด์—์„œ 500% ์ฆ๊ฐ€(600๊ฑด), ์†์ƒ ๋กœ๊ทธ ํ˜ผ์ž…, ๋ฌถ์Œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ์ƒํ™ฉ์„ ์ฃผ์ž…ํ•ด ์‹œ์Šคํ…œ ๋‹ค์šด 0๊ฑดยท๋กœ๊ทธ ์œ ์‹ค 0๊ฑด(์ฑ„์ +๊ฒฉ๋ฆฌ=์ „์ฒด)๊ณผ ์ฒ˜๋ฆฌ๋Ÿ‰ยท๋ฉ”๋ชจ๋ฆฌยท๋ณ‘๋ชฉ ํ•จ์ˆ˜๋ฅผ ์ธก์ •ํ•˜๊ณ , ์˜ˆ์™ธ ์‹œ 1๊ฑด์”ฉ ์ฒ˜๋ฆฌ๋กœ ๊ธฐ๋Šฅ์„ ์ถ•์†Œํ•˜๋Š” ์•ˆ์ „ ์‹คํŒจ ๋™์ž‘์„ ํ™•์ธํ•œ๋‹ค.\"\"\"\n", "import cProfile\n", "import io\n", "import pstats\n", "import time\n", "import tracemalloc\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import Ridge\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "KEYS = [\"t\", \"x\", \"y\", \"alt\", \"stick_pitch\", \"stick_roll\", \"throttle\", \"roll_deg\", \"pitch_deg\", \"tx\", \"ty\", \"talt\"]\n", "\n", "\n", "def check_log(L):\n", " \"\"\"๋กœ๊ทธ 1๊ฑด ๊ฒ€์ฆ. ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์œ  ๋ฌธ์ž์—ด, ์—†์œผ๋ฉด None.\"\"\"\n", " if any(k not in L for k in KEYS):\n", " return \"ํ•„๋“œ ๋ˆ„๋ฝ\"\n", " if len(L[\"t\"]) != STEPS:\n", " return f\"๊ธธ์ด ์˜ค๋ฅ˜({len(L['t'])})\"\n", " if np.isnan(L[\"x\"]).mean() > 0.2:\n", " return \"๊ฒฐ์ธก 20% ์ดˆ๊ณผ\"\n", " if np.any(np.diff(L[\"t\"]) <= 0):\n", " return \"์‹œ๊ฐ ์—ญํ–‰\"\n", " if np.abs(L[\"stick_roll\"]).max() > 1.0001:\n", " return \"์Šคํ‹ฑ ๋ฒ”์œ„ ์ดˆ๊ณผ\"\n", " return None\n", "\n", "\n", "def score_chunk(logs, conds, eng, ref):\n", " stacked = {k: np.stack([L[k] for L in logs], axis=1) if k != \"t\" else T for k in KEYS}\n", " stacked = {k: (pd.DataFrame(v).interpolate(limit_direction=\"both\").values if v.ndim == 2 else v) for k, v in stacked.items()}\n", " f = features(stacked).assign(cond=conds)\n", " return np.clip(eng.predict(cond_z(f, ref, ADV_F)[ADV_F]), 0, 100)\n", "\n", "\n", "def run_batch(logs, conds, eng, ref, chunk=100, fail_chunk=None):\n", " scored, quarantine, degraded, down = {}, {}, 0, 0\n", " try:\n", " ok = []\n", " for i, L in enumerate(logs):\n", " why = check_log(L)\n", " (quarantine.__setitem__(i, why) if why else ok.append(i))\n", " for c0 in range(0, len(ok), chunk):\n", " idx = ok[c0:c0 + chunk]\n", " try:\n", " if fail_chunk is not None and c0 // chunk == fail_chunk:\n", " raise MemoryError(\"๋ชจ์˜ ์ž์› ๋ถ€์กฑ\")\n", " for i, s in zip(idx, score_chunk([logs[i] for i in idx], [conds[i] for i in idx], eng, ref)):\n", " scored[i] = s\n", " except Exception: # ๊ธฐ๋Šฅ ์ถ•์†Œ: 1๊ฑด์”ฉ ์ฒ˜๋ฆฌ, ๊ทธ๋ž˜๋„ ์‹คํŒจํ•˜๋ฉด ๊ฒฉ๋ฆฌ\n", " degraded += 1\n", " for i in idx:\n", " try:\n", " scored[i] = score_chunk([logs[i]], [conds[i]], eng, ref)[0]\n", " except Exception as e:\n", " quarantine[i] = f\"์ฒ˜๋ฆฌ ์‹คํŒจ({type(e).__name__})\"\n", " except Exception as e:\n", " down = 1; print(\"๋‹ค์šด:\", e)\n", " return scored, quarantine, degraded, down\n", "\n", "\n", "# ์—”์ง„ ์ค€๋น„(๋ฏธ์…˜ 4 ๋ฐฉ์‹)\n", "train = make_dataset(40, seed=7)\n", "eng = Ridge(alpha=1.0).fit(cond_z(train, train, ADV_F)[ADV_F], train[\"instructor\"])\n", "\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ์กฐ์ข…์ž 100๋ช… x ๋น„ํ–‰ 6ํšŒ = ๋กœ๊ทธ 600๊ฑด ์ƒ์„ฑ\n", "pilots = make_pilots(100, seed=12)\n", "LOGS, CONDS = [], []\n", "for f in range(6):\n", " cond = list(COND)[f % 3]\n", " log, _ = fly(pilots, cond, seed=900 + f)\n", " for j in range(len(pilots)):\n", " LOGS.append({k: (v if v.ndim == 1 else v[:, j].copy()) for k, v in log.items() if k in KEYS}); CONDS.append(cond)\n", "rng = np.random.default_rng(5)\n", "BAD = []\n", "for i in rng.choice(len(LOGS), 30, replace=False): # ์†์ƒ ๋กœ๊ทธ 5% ์ฃผ์ž…\n", " L = {k: v.copy() for k, v in LOGS[i].items()}\n", " kind = i % 4\n", " if kind == 0:\n", " del L[\"throttle\"]\n", " elif kind == 1:\n", " L[\"x\"][:400] = np.nan\n", " elif kind == 2:\n", " L = {k: v[:500] for k, v in L.items()}\n", " else:\n", " L[\"stick_roll\"][100] = 7.0\n", " BAD.append((i, L))\n", "CORRUPT = list(LOGS)\n", "for i, L in BAD:\n", " CORRUPT[i] = L\n", "\n", "CASES = [(\"ํ‰์‹œ 100๊ฑด\", LOGS[:100], CONDS[:100], None), (\"100๋ช… x 5ํšŒ 500๊ฑด\", LOGS[:500], CONDS[:500], None),\n", " (\"500% ์ฆ๊ฐ€ 600๊ฑด\", LOGS, CONDS, None), (\"600๊ฑด + ์†์ƒ 5%\", CORRUPT, CONDS, None),\n", " (\"600๊ฑด + ์†์ƒ + ๋ฌถ์Œ ์˜ˆ์™ธ\", CORRUPT, CONDS, 2)]\n", "rows = []\n", "for name, logs, conds, fail in CASES:\n", " t0 = time.perf_counter()\n", " sc, qu, dg, dn = run_batch(logs, conds, eng, train, fail_chunk=fail)\n", " dt = time.perf_counter() - t0\n", " rows.append({\"์ƒํ™ฉ\": name, \"๋กœ๊ทธ\": len(logs), \"์ฑ„์ \": len(sc), \"๊ฒฉ๋ฆฌ\": len(qu), \"์œ ์‹ค\": len(logs) - len(sc) - len(qu),\n", " \"๋‹ค์šด\": dn, \"๊ธฐ๋Šฅ์ถ•์†Œ ๋ฌถ์Œ\": dg, \"์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/s)\": round(len(logs) / dt), \"์†Œ์š”(s)\": round(dt, 2)})\n", "R = pd.DataFrame(rows)\n", "print(R.to_string(index=False))\n", "print(f\"\\n์‹œ์Šคํ…œ ๋‹ค์šด {R['๋‹ค์šด'].sum()}๊ฑด, ๋กœ๊ทธ ์œ ์‹ค {R['์œ ์‹ค'].sum()}๊ฑด(์ฑ„์ +๊ฒฉ๋ฆฌ=์ „์ฒด), ์ตœ์ € ์ฒ˜๋ฆฌ๋Ÿ‰ {R['์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/s)'].min()}๊ฑด/s\")\n", "_, qu, _, _ = run_batch(CORRUPT, CONDS, eng, train)\n", "print(\"๊ฒฉ๋ฆฌ ์‚ฌ์œ :\", pd.Series(list(qu.values())).value_counts().to_dict(), f\"/ ์ฃผ์ž… {len(BAD)}๊ฑด\")\n", "\n", "tracemalloc.start()\n", "mem = []\n", "for k in range(3):\n", " run_batch(LOGS[:200], CONDS[:200], eng, train)\n", " mem.append(tracemalloc.get_traced_memory()[0] / 1e6)\n", "peak = tracemalloc.get_traced_memory()[1] / 1e6\n", "tracemalloc.stop()\n", "print(f\"200๊ฑด ์ฒ˜๋ฆฌ ์ตœ๋Œ€ ๋ฉ”๋ชจ๋ฆฌ {peak:.0f} MB, ๋ฐ˜๋ณต ํ›„ ์ž”๋ฅ˜ {[round(m, 1) for m in mem]} MB -> {'๋ˆ„์ˆ˜ ์˜์‹ฌ' if mem[-1] > mem[0] * 1.5 + 1 else '๋ˆ„์ˆ˜ ์ง•ํ›„ ์—†์Œ'}\")\n", "\n", "pr = cProfile.Profile(); pr.enable()\n", "run_batch(LOGS[:300], CONDS[:300], eng, train)\n", "pr.disable()\n", "s = io.StringIO(); pstats.Stats(pr, stream=s).sort_stats(\"cumulative\").print_stats(30)\n", "top = [ln.split() for ln in s.getvalue().splitlines() if ln.strip()[:1].isdigit() and \".py\" in ln and \"m08\" in ln.split()[-1]]\n", "print(\"[๋ˆ„์  ์‹œ๊ฐ„ ์ƒ์œ„ ํ•จ์ˆ˜(์ด ํŒŒ์ผ)]\")\n", "for f in top[:4]:\n", " print(f\" {f[0]:>6}ํšŒ {f[3]}s {f[-1].split(':')[-1]}\")\n", "print(\"๊ฐœ์„  ๋ฐฉํ–ฅ: ๋น„ํ–‰๋ณ„ ๋ณด๊ฐ„(DataFrame.interpolate)์„ numpy ์ผ๊ด„ ๋ณด๊ฐ„์œผ๋กœ ๋ฐ”๊พธ๊ณ , ๋ฌถ์Œ ํฌ๊ธฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๋งž์ถฐ ์กฐ์ •ํ•œ๋‹ค.\")\n", "\n", "plt.figure(figsize=(8, 4))\n", "plt.bar([f\"case{i + 1}\" for i in range(len(R))], R[\"์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/s)\"], color=[\"tab:blue\"] * 3 + [\"tab:orange\", \"tab:red\"])\n", "plt.ylabel(\"logs per second\"); plt.title(\"Batch analysis stress test throughput (synthetic)\")\n", "plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 08 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", " ์ƒํ™ฉ ๋กœ๊ทธ ์ฑ„์  ๊ฒฉ๋ฆฌ ์œ ์‹ค ๋‹ค์šด ๊ธฐ๋Šฅ์ถ•์†Œ ๋ฌถ์Œ ์ฒ˜๋ฆฌ๋Ÿ‰(๊ฑด/s) ์†Œ์š”(s)\n", " ํ‰์‹œ 100๊ฑด 100 100 0 0 0 0 1297 0.08\n", " 100๋ช… x 5ํšŒ 500๊ฑด 500 500 0 0 0 0 1290 0.39\n", " 500% ์ฆ๊ฐ€ 600๊ฑด 600 600 0 0 0 0 1214 0.49\n", " 600๊ฑด + ์†์ƒ 5% 600 570 30 0 0 0 1258 0.48\n", "600๊ฑด + ์†์ƒ + ๋ฌถ์Œ ์˜ˆ์™ธ 600 570 30 0 0 1 264 2.28\n", "\n", "์‹œ์Šคํ…œ ๋‹ค์šด 0๊ฑด, ๋กœ๊ทธ ์œ ์‹ค 0๊ฑด(์ฑ„์ +๊ฒฉ๋ฆฌ=์ „์ฒด), ์ตœ์ € ์ฒ˜๋ฆฌ๋Ÿ‰ 264๊ฑด/s\n", "๊ฒฉ๋ฆฌ ์‚ฌ์œ : {'์Šคํ‹ฑ ๋ฒ”์œ„ ์ดˆ๊ณผ': 10, 'ํ•„๋“œ ๋ˆ„๋ฝ': 8, '๊ธธ์ด ์˜ค๋ฅ˜(500)': 6, '๊ฒฐ์ธก 20% ์ดˆ๊ณผ': 6} / ์ฃผ์ž… 30๊ฑด\n", "200๊ฑด ์ฒ˜๋ฆฌ ์ตœ๋Œ€ ๋ฉ”๋ชจ๋ฆฌ 22 MB, ๋ฐ˜๋ณต ํ›„ ์ž”๋ฅ˜ [0.0, 0.0, 0.0] MB -> ๋ˆ„์ˆ˜ ์ง•ํ›„ ์—†์Œ\n", "[๋ˆ„์  ์‹œ๊ฐ„ ์ƒ์œ„ ํ•จ์ˆ˜(์ด ํŒŒ์ผ)]\n", " 1ํšŒ 0.398s 193(run_batch)\n", " 3ํšŒ 0.376s 186(score_chunk)\n", " 3ํšŒ 0.171s 188()\n", " 3ํšŒ 0.088s 155(cond_z)\n", "๊ฐœ์„  ๋ฐฉํ–ฅ: ๋น„ํ–‰๋ณ„ ๋ณด๊ฐ„(DataFrame.interpolate)์„ numpy ์ผ๊ด„ ๋ณด๊ฐ„์œผ๋กœ ๋ฐ”๊พธ๊ณ , ๋ฌถ์Œ ํฌ๊ธฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ๋งž์ถฐ ์กฐ์ •ํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 09. AAR ๊ธฐ๋ฐ˜ ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜์™€ AI ์ˆ™๋ จ๋„ ์ ์ˆ˜ ๊ฐ„ ํŽธ์ฐจ ๋ถ„์„ ๋ฐ ๊ฐ€์ค‘์น˜ ์žฌํŠœ๋‹\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.0์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "AAR ๊ธฐ๋ฐ˜ ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜์™€ AI ์ ์ˆ˜ ๊ฐ„ ํŽธ์ฐจ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ๊ฐ€์ค‘์น˜๋ฅผ ์žฌ๋ณด์ •ํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **AAR๋กœ ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜์™€ AI ์ ์ˆ˜์˜ ํŽธ์ฐจ๋ฅผ ๋ถ„์„ํ•ด ์›์ธ์„ ๊ทœ๋ช…ํ•˜๊ณ  ์ฑ„์  ๊ฐ€์ค‘์น˜๋ฅผ ์žฌํŠœ๋‹ํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์ฃผ๊ฐ„ ๋น„ํ–‰๋งŒ์œผ๋กœ ๋งž์ถ˜ ์ดˆ๊ธฐ AI๋ฅผ ํ•œ ๊ธฐ์ˆ˜ ํ›ˆ๋ จ์— ์ ์šฉํ•ด ์กฐ๊ฑด๋ณ„ยท์œ„ํ—˜ ์‚ฌ๊ฑด ์ˆ˜๋ณ„ยท์Šต๊ด€๋ณ„ ํŽธ์ฐจ๋ฅผ ๋ถ„ํ•ดํ•˜๊ณ  ์›์ธ ์ •๋ฆฌํ•˜๊ณ  ๊ทœ๊ฒฉํ™”ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `docs/trn01_aar_report.md ๋ฐ configs/trn01_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(์‚ฌํ›„๊ฒ€ํ† )๋กœ ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜์™€ AI ์ˆ™๋ จ๋„ ์ ์ˆ˜์˜ ํŽธ์ฐจ๋ฅผ ๋น„ํ–‰ ์กฐ๊ฑดยท์œ„ํ—˜ ์‚ฌ๊ฑดยท์กฐ์ž‘ ์Šต๊ด€๋ณ„๋กœ ๋ถ„์„ํ•ด ์›์ธ์„ ๊ทœ๋ช…ํ•˜๊ณ , ํŠน์ง• ๊ตฌ์„ฑยท์กฐ๊ฑด ํ‘œ์ค€ํ™”ยท๊ฐ€์ค‘์น˜(๊ทœ์ œ ๊ฐ•๋„)๋ฅผ ๊ฒฉ์ž ํƒ์ƒ‰์œผ๋กœ ์žฌํŠœ๋‹ํ•œ ๋’ค ๋‹ค๋ฅธ ์กฐ์ข…์ž ์ง‘๋‹จ์œผ๋กœ ํšŒ๊ท€ ํ…Œ์ŠคํŠธํ•ด ๊ฐœ์„  ์ „ํ›„๋ฅผ ๋น„๊ตํ•œ๋‹ค.\"\"\"\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import Ridge\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "\n", "def fit(df, feats, norm, alpha):\n", " X = cond_z(df, df, feats)[feats] if norm else df[feats]\n", " return Ridge(alpha=alpha).fit(X, df[\"instructor\"])\n", "\n", "\n", "def predict(m, df, ref, feats, norm):\n", " X = cond_z(df, ref, feats)[feats] if norm else df[feats]\n", " return np.clip(m.predict(X), 0, 100)\n", "\n", "\n", "# ์ดˆ๊ธฐ ์šด์šฉ AI: ์ฃผ๊ฐ„ ๋น„ํ–‰๋งŒ์œผ๋กœ ๋งž์ถ˜ Baseline(๊ฒฝ๋กœยทํ˜ธ๋ฒ„๋งยท๊ณ ๋„, ์กฐ๊ฑด ํ‘œ์ค€ํ™” ์—†์Œ)\n", "day_train = make_dataset(40, seed=7, conds=(\"day\",))\n", "INIT = dict(feats=BASE_F, norm=False, alpha=1.0)\n", "init_m = fit(day_train, **INIT)\n", "\n", "# 1) ๋ฌด์—‡์ด ์ผ์–ด๋‚ฌ๋‚˜: ํ›ˆ๋ จ 1๊ฐœ ๊ธฐ์ˆ˜(์กฐ์ข…์ž 40๋ช… x 3์กฐ๊ฑด)์˜ ๊ต๊ด€ ์ ์ˆ˜์™€ AI ์ ์ˆ˜ ๋น„๊ต\n", "aar = make_dataset(40, seed=8)\n", "aar[\"ai\"] = predict(init_m, aar, day_train, INIT[\"feats\"], INIT[\"norm\"])\n", "aar[\"dev\"] = aar[\"ai\"] - aar[\"instructor\"]\n", "acc0 = (to_grade(aar[\"ai\"]) == aar[\"grade\"]).mean()\n", "print(f\"[1. ๋ฌด์—‡์ด ์ผ์–ด๋‚ฌ๋‚˜] ์ดˆ๊ธฐ AI ๋“ฑ๊ธ‰ ์ผ์น˜์œจ {acc0:.1%}, ์ ์ˆ˜ ํŽธ์ฐจ(AI-๊ต๊ด€) ํ‰๊ท  {aar['dev'].mean():+.1f}์ , ์ ˆ๋Œ€ ํŽธ์ฐจ {aar['dev'].abs().mean():.1f}์ \")\n", "\n", "# 2) ์™œ ์ผ์–ด๋‚ฌ๋‚˜: ํŽธ์ฐจ ์›์ธ ๋ถ„ํ•ด\n", "aar[\"์œ„ํ—˜์‚ฌ๊ฑด\"] = aar[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1).clip(0, 2).astype(str).str.replace(\"2\", \"2+\")\n", "print(\"\\n[2. ์™œ ์ผ์–ด๋‚ฌ๋‚˜] ์กฐ๊ฑด๋ณ„ ํ‰๊ท  ํŽธ์ฐจ:\", aar.groupby(\"cond\")[\"dev\"].mean().round(1).to_dict())\n", "ng = aar[aar[\"cond\"] != \"gust\"] # ์กฐ๊ฑด ํšจ๊ณผ๋ฅผ ๋บ€ ์ฃผ๊ฐ„ยท์•ผ๊ฐ„ ๋น„ํ–‰์œผ๋กœ ๋น„๊ต\n", "ev = ng.groupby(\"์œ„ํ—˜์‚ฌ๊ฑด\")[\"dev\"].mean().round(1)\n", "print(\"์ฃผ๊ฐ„ยท์•ผ๊ฐ„ ๋น„ํ–‰์˜ ์œ„ํ—˜ ์‚ฌ๊ฑด ์ˆ˜๋ณ„ ํ‰๊ท  ํŽธ์ฐจ:\", ev.to_dict())\n", "print(\"์ฃผ๊ฐ„ยท์•ผ๊ฐ„ ๋น„ํ–‰์˜ ์Šต๊ด€๋ณ„ ํ‰๊ท  ํŽธ์ฐจ: ๊ธ‰์กฐ์ž‘\", round(ng[ng[\"jerky\"]][\"dev\"].mean(), 1), \"/ ๊ณผ๋ณด์ •\", round(ng[ng[\"overcorr\"]][\"dev\"].mean(), 1),\n", " \"/ ์Šต๊ด€ ์—†์Œ\", round(ng[~ng[\"jerky\"] & ~ng[\"overcorr\"]][\"dev\"].mean(), 1))\n", "print(f\"-> ์›์ธ: (๊ฐ€) ์ฃผ๊ฐ„ ์ž๋ฃŒ๋กœ๋งŒ ๋งž์ถฐ ๋Œํ’ ๋น„ํ–‰์„ ํ‰๊ท  {aar[aar['cond'] == 'gust']['dev'].mean():.0f}์  ๊ณผ์†Œํ‰๊ฐ€(๊ฒฝ๋กœ ์ดํƒˆ์„ ์‹ค๋ ฅ ๋ถ€์กฑ์œผ๋กœ ์˜ค์ธ), \"\n", " f\"(๋‚˜) ์œ„ํ—˜ ์‚ฌ๊ฑด 2๊ฑด ์ด์ƒ ๋น„ํ–‰์„ 0๊ฑด ๋Œ€๋น„ {ev.get('2+', np.nan) - ev.get('0', np.nan):+.1f}์  ๊ณผ๋Œ€ํ‰๊ฐ€(์žฅ์• ๋ฌผยท์ฐฉ๋ฅ™ ๋Œ€์‘ ๋ฏธ๋ฐ˜์˜), (๋‹ค) ๊ธ‰์กฐ์ž‘ ์Šต๊ด€ ๋ฏธ๋ฐ˜์˜\")\n", "\n", "# 3) ๋ฌด์—‡์„ ๋ฐ”๊พธ๋‚˜: AAR ์ž๋ฃŒ(3์กฐ๊ฑด)๋กœ ๊ฒฉ์ž ํƒ์ƒ‰, ๊ธฐ์ˆ˜ ๋‚ด 2๊ฒน ๊ต์ฐจ๊ฒ€์ฆ์œผ๋กœ ์„ ํƒ\n", "pid = aar[\"pilot\"].str[1:].astype(int) % 2\n", "grid = []\n", "for name, feats in [(\"๊ธฐ๋ณธ\", BASE_F), (\"๊ณ ๋„ํ™”\", ADV_F)]:\n", " for norm in (False, True):\n", " for alpha in (0.1, 1.0, 10.0):\n", " accs = []\n", " for k in (0, 1):\n", " tr, va = aar[pid != k], aar[pid == k]\n", " m = fit(tr, feats, norm, alpha)\n", " accs.append((to_grade(predict(m, va, tr, feats, norm)) == va[\"grade\"]).mean())\n", " grid.append({\"ํŠน์ง•\": name, \"์กฐ๊ฑดํ‘œ์ค€ํ™”\": norm, \"alpha\": alpha, \"๊ต์ฐจ๊ฒ€์ฆ ์ผ์น˜์œจ\": round(np.mean(accs), 3)})\n", "g = pd.DataFrame(grid).sort_values(\"๊ต์ฐจ๊ฒ€์ฆ ์ผ์น˜์œจ\", ascending=False)\n", "print(\"\\n[3. ๋ฌด์—‡์„ ๋ฐ”๊พธ๋‚˜] ๊ฒฉ์ž 12๊ฐœ ์ค‘ ์ƒ์œ„ 4๊ฐœ\")\n", "print(g.head(4).to_string(index=False))\n", "best = g.iloc[0]\n", "TUNED = dict(feats=ADV_F if best[\"ํŠน์ง•\"] == \"๊ณ ๋„ํ™”\" else BASE_F, norm=bool(best[\"์กฐ๊ฑดํ‘œ์ค€ํ™”\"]), alpha=float(best[\"alpha\"]))\n", "tuned_m = fit(aar, **TUNED)\n", "\n", "# 4) ํšŒ๊ท€ ํ…Œ์ŠคํŠธ: ์ƒˆ ๊ธฐ์ˆ˜ 2๊ฐœ(ํ•™์Šต์— ์“ฐ์ง€ ์•Š์€ ์กฐ์ข…์ž)\n", "print(\"\\n[ํšŒ๊ท€ ํ…Œ์ŠคํŠธ] ๊ธฐ์ˆ˜ ์ดˆ๊ธฐ AI ์žฌํŠœ๋‹ ๋ณ€ํ™”\")\n", "reg, rep, near = [], [], []\n", "for sd in (21, 22):\n", " new = make_dataset(40, seed=sd)\n", " a0 = to_grade(predict(init_m, new, day_train, INIT[\"feats\"], INIT[\"norm\"]))\n", " a1 = to_grade(predict(tuned_m, new, aar, TUNED[\"feats\"], TUNED[\"norm\"]))\n", " r0, r1 = (a0 == new[\"grade\"]).mean(), (a1 == new[\"grade\"]).mean()\n", " both = (a0 != new[\"grade\"]) & (a1 != new[\"grade\"])\n", " reg.append((r0, r1)); rep.append(int(both.sum()))\n", " near += list((np.minimum(np.abs(new[\"instructor\"] - 55), np.abs(new[\"instructor\"] - 75)) <= 5)[both])\n", " print(f\" #{sd} {r0:6.1%} {r1:6.1%} {(r1 - r0) * 100:+5.1f}%p\")\n", "gain = np.mean([b - a for a, b in reg])\n", "print(f\"ํ‰๊ท  ํ–ฅ์ƒ {gain * 100:+.1f}%p (๋ชฉํ‘œ +10%p ์ด์ƒ: {'๋‹ฌ์„ฑ' if gain >= 0.10 else '๋ฏธ๋‹ฌ'}), ํšŒ๊ท€ ํ…Œ์ŠคํŠธ {sum(b >= a for a, b in reg)}/{len(reg)} ํ†ต๊ณผ\")\n", "print(f\"์žฌํŠœ๋‹ ํ›„์—๋„ ๋ฐ˜๋ณต๋œ ๋ถˆ์ผ์น˜ {sum(rep)}๊ฑด(์ „์ฒด {2 * len(new)}ํšŒ ์ค‘), ๊ทธ์ค‘ ๊ต๊ด€ ์ ์ˆ˜๊ฐ€ ๋“ฑ๊ธ‰ ๊ฒฝ๊ณ„(55ยท75์ ) ยฑ5์  ์ด๋‚ด์ธ ๋น„ํ–‰ {np.mean(near):.0%}\")\n", "\n", "w = pd.Series(tuned_m.coef_, index=TUNED[\"feats\"]).round(2)\n", "yaml = (f\"features: [{', '.join(TUNED['feats'])}]\\ncondition_normalization: {str(TUNED['norm']).lower()}\\nridge_alpha: {TUNED['alpha']}\\n\"\n", " f\"grade_cut: {{A: 75, B: 55}}\\nweights: {{{', '.join(f'{k}: {v}' for k, v in w.items())}}}\")\n", "print(\"\\n[์žฌํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ(trn01_tuned_params.yaml ๋‚ด์šฉ)]\\n\" + yaml)\n", "print(\"์ง€์† ๊ฐœ์„ : ๊ธฐ์ˆ˜๋งˆ๋‹ค ๊ต๊ด€ ์ ์ˆ˜๋ฅผ AAR ์ž๋ฃŒ๋กœ ๋ˆ„์ ํ•ด ๊ฐ™์€ ์ ˆ์ฐจ๋กœ ์žฌํŠœ๋‹ํ•˜๊ณ , ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ์‹œ์—๋งŒ ๊ต์ฒดํ•œ๋‹ค.\")\n", "\n", "plt.figure(figsize=(7, 4))\n", "for c, col in zip(COND, [\"tab:blue\", \"tab:purple\", \"tab:orange\"]):\n", " s = aar[aar[\"cond\"] == c]\n", " plt.scatter(s[\"instructor\"], s[\"dev\"], s=12, c=col, label=c)\n", "plt.axhline(0, c=\"k\"); plt.xlabel(\"instructor score\"); plt.ylabel(\"AI - instructor (initial AI)\")\n", "plt.legend(); plt.title(\"AAR: deviation of initial AI score (synthetic)\"); plt.tight_layout(); plt.show()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 09 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[1. ๋ฌด์—‡์ด ์ผ์–ด๋‚ฌ๋‚˜] ์ดˆ๊ธฐ AI ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 47.5%, ์ ์ˆ˜ ํŽธ์ฐจ(AI-๊ต๊ด€) ํ‰๊ท  -12.2์ , ์ ˆ๋Œ€ ํŽธ์ฐจ 21.1์ \n", "\n", "[2. ์™œ ์ผ์–ด๋‚ฌ๋‚˜] ์กฐ๊ฑด๋ณ„ ํ‰๊ท  ํŽธ์ฐจ: {'day': 2.7, 'gust': -40.1, 'night': 0.9}\n", "์ฃผ๊ฐ„ยท์•ผ๊ฐ„ ๋น„ํ–‰์˜ ์œ„ํ—˜ ์‚ฌ๊ฑด ์ˆ˜๋ณ„ ํ‰๊ท  ํŽธ์ฐจ: {'0': -9.8, '1': 0.6, '2+': 11.7}\n", "์ฃผ๊ฐ„ยท์•ผ๊ฐ„ ๋น„ํ–‰์˜ ์Šต๊ด€๋ณ„ ํ‰๊ท  ํŽธ์ฐจ: ๊ธ‰์กฐ์ž‘ 7.3 / ๊ณผ๋ณด์ • 3.6 / ์Šต๊ด€ ์—†์Œ 0.3\n", "-> ์›์ธ: (๊ฐ€) ์ฃผ๊ฐ„ ์ž๋ฃŒ๋กœ๋งŒ ๋งž์ถฐ ๋Œํ’ ๋น„ํ–‰์„ ํ‰๊ท  -40์  ๊ณผ์†Œํ‰๊ฐ€(๊ฒฝ๋กœ ์ดํƒˆ์„ ์‹ค๋ ฅ ๋ถ€์กฑ์œผ๋กœ ์˜ค์ธ), (๋‚˜) ์œ„ํ—˜ ์‚ฌ๊ฑด 2๊ฑด ์ด์ƒ ๋น„ํ–‰์„ 0๊ฑด ๋Œ€๋น„ +21.5์  ๊ณผ๋Œ€ํ‰๊ฐ€(์žฅ์• ๋ฌผยท์ฐฉ๋ฅ™ ๋Œ€์‘ ๋ฏธ๋ฐ˜์˜), (๋‹ค) ๊ธ‰์กฐ์ž‘ ์Šต๊ด€ ๋ฏธ๋ฐ˜์˜\n", "\n", "[3. ๋ฌด์—‡์„ ๋ฐ”๊พธ๋‚˜] ๊ฒฉ์ž 12๊ฐœ ์ค‘ ์ƒ์œ„ 4๊ฐœ\n", " ํŠน์ง• ์กฐ๊ฑดํ‘œ์ค€ํ™” alpha ๊ต์ฐจ๊ฒ€์ฆ ์ผ์น˜์œจ\n", "๊ณ ๋„ํ™” True 10.0 0.825\n", "๊ณ ๋„ํ™” True 0.1 0.817\n", "๊ณ ๋„ํ™” True 1.0 0.808\n", "๊ณ ๋„ํ™” False 0.1 0.783\n", "\n", "[ํšŒ๊ท€ ํ…Œ์ŠคํŠธ] ๊ธฐ์ˆ˜ ์ดˆ๊ธฐ AI ์žฌํŠœ๋‹ ๋ณ€ํ™”\n", " #21 45.0% 89.2% +44.2%p\n", " #22 40.0% 81.7% +41.7%p\n", "ํ‰๊ท  ํ–ฅ์ƒ +42.9%p (๋ชฉํ‘œ +10%p ์ด์ƒ: ๋‹ฌ์„ฑ), ํšŒ๊ท€ ํ…Œ์ŠคํŠธ 2/2 ํ†ต๊ณผ\n", "์žฌํŠœ๋‹ ํ›„์—๋„ ๋ฐ˜๋ณต๋œ ๋ถˆ์ผ์น˜ 21๊ฑด(์ „์ฒด 240ํšŒ ์ค‘), ๊ทธ์ค‘ ๊ต๊ด€ ์ ์ˆ˜๊ฐ€ ๋“ฑ๊ธ‰ ๊ฒฝ๊ณ„(55ยท75์ ) ยฑ5์  ์ด๋‚ด์ธ ๋น„ํ–‰ 86%\n", "\n", "[์žฌํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ(trn01_tuned_params.yaml ๋‚ด์šฉ)]\n", "features: [path_rmse, hover_drift, hover_std, alt_err, obs_rt, obs_lateral, touch_vz, att_exceed, jerk, overshoot, n_risk]\n", "condition_normalization: true\n", "ridge_alpha: 10.0\n", "grade_cut: {A: 75, B: 55}\n", "weights: {path_rmse: -0.59, hover_drift: 1.22, hover_std: 0.77, alt_err: -7.67, obs_rt: -0.51, obs_lateral: 1.08, touch_vz: -4.19, att_exceed: -0.54, jerk: -1.59, overshoot: -0.66, n_risk: -2.4}\n", "์ง€์† ๊ฐœ์„ : ๊ธฐ์ˆ˜๋งˆ๋‹ค ๊ต๊ด€ ์ ์ˆ˜๋ฅผ AAR ์ž๋ฃŒ๋กœ ๋ˆ„์ ํ•ด ๊ฐ™์€ ์ ˆ์ฐจ๋กœ ์žฌํŠœ๋‹ํ•˜๊ณ , ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ์‹œ์—๋งŒ ๊ต์ฒดํ•œ๋‹ค.\n", "```\n", "
\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## ๐ŸŽฏ ๋ฏธ์…˜ 10. ๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ˆ™๋ จ๋„ ๋ถ„์„ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ๋ฐ ๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ํŒจํ‚ค์ง€ ์™„์„ฑ\n", "**์†Œ์š”์‹œ๊ฐ„**: ์•ฝ 3.5์‹œ๊ฐ„\n", "\n", "### ๐Ÿ“‹ ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ์ˆ˜ํ–‰ ์ง€์นจ\n", "#### ๋ฏธ์…˜ ๋ฐฐ๊ฒฝ ๋ฐ ๋‹น์œ„์„ฑ\n", "\n", "๋“œ๋ก  ๋น„ํ–‰๋กœ๊ทธ ์ˆ™๋ จ๋„ ๋ถ„์„ ์‹œ์Šคํ…œ ํ†ตํ•ฉ ํŒจํ‚ค์ง€ ๋ฐ ๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ๋ณด๊ณ ์„œ๋ฅผ ์™„์„ฑํ•œ๋‹ค. ์‹ค์ œ ์ „์ˆ  ํ˜„์žฅ์—์„œ๋Š” ๋‹จ์ˆœ ํƒ์ง€์— ๊ทธ์น˜์ง€ ์•Š๊ณ  ์‹ ์†ํ•œ ์œ„ํ˜‘ ํŒ๋‹จ๊ณผ ์‹ ๋ขฐ๋„ ๋†’์€ ์˜์‚ฌ๊ฒฐ์ •์ด ํ•„์ˆ˜์ ์ด๋ฏ€๋กœ, **๋กœ๊ทธ ๊ฒ€์ฆ๋ถ€ํ„ฐ ์ „์ฒ˜๋ฆฌ, ํŠน์ง• ์ถ”์ถœ, ์žฌํŠœ๋‹ ์—”์ง„, ์œ„ํ—˜ ์กฐ์ž‘ ํƒ์ง€, ์กฐ์ข…์ž ๋“ฑ๊ธ‰ยท์‹ ๋ขฐ๋„, ๊ต๊ด€ ๊ฒ€ํ†  ๋Œ€๊ธฐ์—ด, ๋Œ€์‹œ๋ณด๋“œ๊นŒ์ง€ ํ†ตํ•ฉํ•˜๊ณ  ๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ํŒจํ‚ค์ง€๋ฅผ ์™„์„ฑํ•œ๋‹ค.** ๊ณผ์—…์„ ์™„์ˆ˜ํ•˜์—ฌ ์˜คํŒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์ „ ๋Œ€์‘๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ณธ ๋ฏธ์…˜์—์„œ๋Š” **์žฌํŠœ๋‹ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์‹ ๊ทœ ๊ธฐ์ˆ˜ ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๋ถ„์„ํ•˜๋Š” ํ†ตํ•ฉ ์ฝ”๋“œ๋ฅผ ์™„์„ฑํ•˜๊ณ  ์†์ƒ ๋กœ๊ทธ๊ฐ€ ๊ฒฉ๋ฆฌ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์œ ํšจ์„ฑ์„ ์ ๊ฒ€ํ•˜๋Š”** ์‹ค์Šต์„ ์ฒด๊ณ„์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "ํ•™์Šต์ž๋Š” ์‹ค์Šต๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ์ด 4๊ฐœ ์„ธ๋ถ€ ํ™œ๋™(๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โž” ๊ฒฐ๊ณผ ์ง€ํ‘œ ๊ฒ€์ฆ โž” ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๋ฐ ๋„์ „๊ณผ์ œ)์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ ํ•ต์‹ฌ ํŒ์ • ๊ธฐ์ค€์„ ๋‹ฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.\n", "\n", "**์ด๋ฒˆ ๊ฒฐ๊ณผ๋ฌผ:** `src/main_trn01.py, README.md, docs/trn01_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": [ "\"\"\"๋น„ํ–‰๋กœ๊ทธ ๊ฒ€์ฆยท์ „์ฒ˜๋ฆฌ๋ถ€ํ„ฐ ํŠน์ง• ์ถ”์ถœ, ์žฌํŠœ๋‹๋œ ์ˆ™๋ จ๋„ ์—”์ง„, ์œ„ํ—˜ ์กฐ์ž‘ ํƒ์ง€, ์กฐ์ข…์ž๋ณ„ ๋“ฑ๊ธ‰ยท์‹ ๋ขฐ๋„, ๊ต๊ด€ HITL ๊ฒ€ํ†  ๋Œ€๊ธฐ์—ด, ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๊นŒ์ง€ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์„ ํ•œ ํŒŒ์ผ๋กœ ํ†ตํ•ฉ ์‹คํ–‰ํ•˜๊ณ , ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ํฌํ•จํ•œ ์ž์ฒด ์ ๊ฒ€ ๊ฒฐ๊ณผ์™€ ๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ์„ ์ถœ๋ ฅํ•œ๋‹ค.\"\"\"\n", "import hashlib\n", "import inspect\n", "import json\n", "import sys\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.linear_model import Ridge\n", "\n", "# ---------------------------------------------------------------------------\n", "# [๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ] ๋“œ๋ก  ์กฐ์ข… ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋น„ํ–‰๋กœ๊ทธ ์ƒ์„ฑ๊ธฐ (์‹ค์ œ ํ›ˆ๋ จ์ƒยท์žฅ๋น„ ์ •๋ณด ์—†์Œ, ์กฐ์ข…์ž๋Š” ๊ฐ€๋ช… P001~)\n", "# ๋น„ํ–‰ 1ํšŒ = 120์ดˆ(10Hz): ๊ฒฝ๋กœ ๋น„ํ–‰(0~60์ดˆ) -> ํ˜ธ๋ฒ„๋ง(60~85์ดˆ) -> ๋Œ๋ฐœ ์žฅ์• ๋ฌผ ํšŒํ”ผ(85์ดˆ) -> ๋น„์ƒ ์ฐฉ๋ฅ™(100์ดˆ ์ง€์‹œ)\n", "# ์กฐ์ข…์ž๋งˆ๋‹ค ์ˆจ์€ ์ˆ™๋ จ๋„(skill)์™€ ์กฐ์ž‘ ์Šต๊ด€(๊ธ‰์กฐ์ž‘ jerky, ๊ณผ๋ณด์ • overcorr)์ด ์žˆ๊ณ , ๊ต๊ด€ ์ ์ˆ˜๋Š” ์ด๋ฅผ ๋ฐ˜์˜ํ•ด ๋งŒ๋“ ๋‹ค.\n", "# ---------------------------------------------------------------------------\n", "DT, STEPS = 0.1, 1200\n", "T = np.arange(STEPS) * DT\n", "COND = {\"day\": dict(wind=0.4, gust=0.0, gps=0.25, delay=0.0), \"night\": dict(wind=0.4, gust=0.0, gps=0.5, delay=0.15),\n", " \"gust\": dict(wind=1.2, gust=2.5, gps=0.25, delay=0.0)}\n", "OBST_T, LAND_T = 85.0, 100.0\n", "\n", "\n", "def make_pilots(n=40, seed=7):\n", " rng = np.random.default_rng(seed)\n", " skill = rng.beta(2.2, 2.0, n)\n", " return pd.DataFrame({\"pilot\": [f\"P{i + 1:03d}\" for i in range(n)], \"skill\": skill,\n", " \"jerky\": rng.random(n) < 0.15 + 0.35 * (1 - skill), \"overcorr\": rng.random(n) < 0.15 + 0.35 * (1 - skill)})\n", "\n", "\n", "def target_path(t):\n", " \"\"\"์‚ฌ๊ฐํ˜• ๊ฒฝ๋กœ(40x30m, ๊ณ ๋„ 20m)๋ฅผ 3m/s๋กœ ๋น„ํ–‰ -> 60์ดˆ ์ง€์ ์—์„œ ํ˜ธ๋ฒ„๋ง -> ์ฐฉ๋ฅ™ ์ง€์‹œ ํ›„ ๊ณ ๋„ 0.\"\"\"\n", " s = np.minimum(t, 60.0) * 3.0 % 140.0\n", " x = np.select([s < 40, s < 70, s < 110], [s, 40.0, 40 - (s - 70)], 0.0)\n", " y = np.select([s < 40, s < 70, s < 110], [0.0, s - 40, 30.0], 30 - (s - 110))\n", " z = np.where(t >= LAND_T, 0.0, 20.0)\n", " return np.stack([x, y, z], -1)\n", "\n", "\n", "def fly(pilots, cond=\"day\", seed=0, delay_extra=0.0, obstacle_side=None):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž์˜ ๋น„ํ–‰ 1ํšŒ๋ฅผ ๋™์‹œ์— ๋ชจ์˜ํ•œ๋‹ค(๋ฒกํ„ฐํ™”). ๋ฐ˜ํ™˜: ๋กœ๊ทธ ๋ฐฐ์—ด dict, ๋น„ํ–‰๋ณ„ ์ •๋‹ต ์ •๋ณด DataFrame.\"\"\"\n", " rng = np.random.default_rng(seed)\n", " c = COND[cond]\n", " delay_extra = delay_extra + c[\"delay\"]\n", " n = len(pilots)\n", " sk, jk, oc = pilots[\"skill\"].values, pilots[\"jerky\"].values, pilots[\"overcorr\"].values\n", " gain = (0.5 + 1.2 * sk) * np.where(oc, 1.5, 1.0)\n", " damp = np.where(oc, 0.25, 0.6 + 0.3 * sk)\n", " noise = 0.03 + 0.35 * (1 - sk)\n", " smooth = np.where(jk, 0.25, 0.75)\n", " delay = np.round((0.25 + 0.35 * (1 - sk) + delay_extra) / DT).astype(int)\n", " rt_obs = np.clip(0.5 + 1.5 * (1 - sk) + rng.normal(0, 0.2, n) + delay_extra, 0.3, 3.5)\n", " h_flare = np.clip(0.8 + 5.0 * sk - 1.5 * oc + rng.normal(0, 0.4, n), 0.3, None)\n", " side = rng.choice([-1, 1], n) if obstacle_side is None else np.full(n, obstacle_side)\n", " pos = np.tile(target_path(np.zeros(1))[0], (n, 1)).astype(float); vel = np.zeros((n, 3)); stick = np.zeros((n, 3))\n", " hist = np.zeros((STEPS, n, 3)); P = np.zeros((STEPS, n, 3)); S = np.zeros((STEPS, n, 3)); ATT = np.zeros((STEPS, n, 2))\n", " tgt_all = np.zeros((STEPS, n, 3)); vz_touch = np.full(n, np.nan); landed = np.zeros(n, bool)\n", " vmax = np.array([5.0, 5.0, 3.0])\n", " gust = np.zeros((n, 2))\n", " for i, t in enumerate(T):\n", " tgt = np.tile(target_path(np.array([t]))[0], (n, 1))\n", " if t >= OBST_T: # ์žฅ์• ๋ฌผ: 6m ์˜†์œผ๋กœ ํšŒํ”ผํ•ด์•ผ ํ•จ(์กฐ์ข…์ž๋Š” ๋ฐ˜์‘์‹œ๊ฐ„ ์ดํ›„ ์ธ์ง€)\n", " tgt_all[i] = tgt + np.c_[np.zeros(n), side * 6.0, np.zeros(n)]\n", " tgt[:, 1] += np.where(t >= OBST_T + rt_obs, side * 6.0, 0.0)\n", " else:\n", " tgt_all[i] = tgt\n", " hist[i] = pos\n", " seen = hist[np.maximum(i - delay, 0), np.arange(n)] # ๋ฐ˜์‘ ์ง€์—ฐ๋งŒํผ ๋Šฆ๊ฒŒ ๋ณธ ์œ„์น˜\n", " cmd = np.clip(gain[:, None] * (tgt - seen) / 4.0 - damp[:, None] * vel / vmax + rng.normal(0, 1, (n, 3)) * noise[:, None], -1, 1)\n", " if t >= LAND_T: # ๋น„์ƒ ์ฐฉ๋ฅ™: ํ•˜๊ฐ•ํ•˜๋‹ค๊ฐ€ h_flare ๋†’์ด์—์„œ ๊ฐ์†(์ˆ™๋ จ์ž์ผ์ˆ˜๋ก ์ผ์ฐ ๊ฐ์†)\n", " cmd[:, 2] = np.where(seen[:, 2] > h_flare, -1.0, -0.12 + rng.normal(0, 0.05, n))\n", " spikes = (jk[:, None] & (rng.random((n, 3)) < 0.02)) * rng.choice([-1, 1], (n, 3)) * 0.9\n", " stick = np.clip(smooth[:, None] * stick + (1 - smooth[:, None]) * cmd + spikes, -1, 1)\n", " gust = 0.95 * gust + rng.normal(0, c[\"gust\"] * 0.3, (n, 2))\n", " wind = np.c_[np.full(n, c[\"wind\"]) + gust[:, 0], gust[:, 1], np.zeros(n)]\n", " vel += (stick * vmax - vel) * DT / 0.6\n", " vel[landed] = 0.0\n", " pos = pos + (vel + wind * (~landed)[:, None]) * DT\n", " touch = (pos[:, 2] <= 0) & ~landed & (t >= LAND_T)\n", " vz_touch[touch] = -vel[touch, 2]; landed |= touch\n", " pos[:, 2] = np.maximum(pos[:, 2], 0.0)\n", " P[i], S[i] = pos, stick\n", " ATT[i] = np.c_[stick[:, 1] * 28 + gust[:, 1] * 1.5, -stick[:, 0] * 28 + gust[:, 0] * 1.5]\n", " g = rng.normal(0, c[\"gps\"], P.shape)\n", " log = {\"t\": T, \"x\": P[..., 0] + g[..., 0], \"y\": P[..., 1] + g[..., 1], \"alt\": P[..., 2] + g[..., 2] * 0.5,\n", " \"stick_pitch\": S[..., 0], \"stick_roll\": S[..., 1], \"throttle\": S[..., 2], \"roll_deg\": ATT[..., 0], \"pitch_deg\": ATT[..., 1],\n", " \"tx\": tgt_all[..., 0], \"ty\": tgt_all[..., 1], \"talt\": tgt_all[..., 2], \"true_x\": P[..., 0], \"true_y\": P[..., 1], \"true_alt\": P[..., 2]}\n", " k_obs = int((OBST_T + 2.5) / DT)\n", " lateral = np.abs(P[k_obs, :, 1] - P[int(OBST_T / DT), :, 1])\n", " truth = pd.DataFrame({\"pilot\": pilots[\"pilot\"].values, \"cond\": cond, \"skill\": sk, \"jerky\": jk, \"overcorr\": oc, \"rt_obs\": rt_obs,\n", " \"near_collision\": lateral < 2.5, \"vz_touch\": np.round(vz_touch, 2), \"hard_landing\": np.nan_to_num(vz_touch, nan=3.0) > 1.6,\n", " \"attitude_exceed\": (np.abs(ATT) > 35).any(axis=2).sum(axis=0) >= 10})\n", " risk = truth[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].sum(axis=1)\n", " inst = 40 + 50 * sk - 5 * jk - 5 * oc - 4 * risk + rng.normal(0, 3, n) # ๊ต๊ด€ ํ‰๊ฐ€ ์ ์ˆ˜(๊ฐ€์ƒ)\n", " truth[\"instructor\"] = np.clip(inst, 0, 100).round(1)\n", " truth[\"grade\"] = np.where(truth[\"instructor\"] >= 75, \"A\", np.where(truth[\"instructor\"] >= 55, \"B\", \"C\"))\n", " return log, truth\n", "\n", "\n", "def one(log, j):\n", " \"\"\"์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ์—์„œ j๋ฒˆ์งธ ์กฐ์ข…์ž 1๋ช…์˜ ๋กœ๊ทธ(DataFrame)๋ฅผ ๊บผ๋‚ธ๋‹ค.\"\"\"\n", " return pd.DataFrame({k: (v if v.ndim == 1 else v[:, j]) for k, v in log.items()})\n", "# ---------------------------------------------------------------------------\n", "\n", "# ---- ๋น„ํ–‰๋กœ๊ทธ ํŠน์ง• ์ถ”์ถœ ๊ณตํ†ต ํ•จ์ˆ˜(์—ฌ๋Ÿฌ ์กฐ์ข…์ž ๋กœ๊ทธ๋ฅผ ํ•œ ๋ฒˆ์— ๊ณ„์‚ฐ) ----\n", "from scipy.ndimage import uniform_filter1d\n", "\n", "BASE_F = [\"path_rmse\", \"hover_drift\", \"hover_std\", \"alt_err\"]\n", "ADV_F = BASE_F + [\"obs_rt\", \"obs_lateral\", \"touch_vz\", \"att_exceed\", \"jerk\", \"overshoot\", \"n_risk\"]\n", "RULES = {\"near_collision\": (\"obs_lateral\", \"<\", 2.5), \"hard_landing\": (\"touch_vz\", \">\", 2.0), \"attitude_exceed\": (\"att_exceed\", \">=\", 10)}\n", "\n", "\n", "def detect(df):\n", " \"\"\"์œ„ํ—˜ ์กฐ์ž‘ ์‚ฌ๊ฑด ํƒ์ง€ ๊ทœ์น™: ์žฅ์• ๋ฌผ 2.5์ดˆ ํ›„ ํšŒํ”ผ ๊ฑฐ๋ฆฌ 2.5m ๋ฏธ๋งŒ, ์ ‘์ง€ ์†๋„ 2.0m/s ์ดˆ๊ณผ, ์ž์„ธ 35๋„ ์ดˆ๊ณผ 1์ดˆ ์ด์ƒ.\"\"\"\n", " out = {}\n", " for ev, (f, op, th) in RULES.items():\n", " out[ev] = df[f] < th if op == \"<\" else (df[f] > th if op == \">\" else df[f] >= th)\n", " return pd.DataFrame(out)\n", "\n", "\n", "def features(log):\n", " \"\"\"๊ฒฝ๋กœ ์ดํƒˆ๋„ยทํ˜ธ๋ฒ„๋ง ์•ˆ์ •์„ฑ(๊ธฐ๋ณธ) + ์žฅ์• ๋ฌผ ๋ฐ˜์‘ยท์ฐฉ๋ฅ™ ์†๋„ยท์ž์„ธ ํ•œ๊ณ„ยท๊ธ‰์กฐ์ž‘ยท๊ณผ๋ณด์ •(๊ณ ๋„ํ™”) ํŠน์ง•.\"\"\"\n", " X, Y, A = (uniform_filter1d(log[k], 9, axis=0) for k in (\"x\", \"y\", \"alt\")) # GPS ์žก์Œ ์™„ํ™”(์ด๋™ํ‰๊ท  0.9์ดˆ)\n", " h = np.hypot(X - log[\"tx\"], Y - log[\"ty\"])\n", " cr, hv = slice(0, 600), slice(620, 850)\n", " k0, k1 = int(OBST_T / DT), int((OBST_T + 2.5) / DT)\n", " side = np.sign(log[\"ty\"][k0 + 1] - log[\"ty\"][k0 - 1])\n", " moved = (log[\"stick_roll\"][k0:k0 + 40] * side) > 0.3\n", " rt = np.where(moved.any(0), moved.argmax(0) * DT, 4.0)\n", " vz = -np.gradient(A, DT, axis=0)\n", " land = np.argmax((A < 0.4) & (T[:, None] >= LAND_T), axis=0)\n", " touch = np.array([vz[max(i - 6, 0):i - 2, j].mean() if i > 6 else 3.0 for j, i in enumerate(land)])\n", " ds = np.abs(np.diff(np.stack([log[\"stick_roll\"], log[\"stick_pitch\"]]), axis=1)) / DT\n", " return pd.DataFrame({\n", " \"path_rmse\": np.sqrt((h[cr] ** 2).mean(0)), \"hover_drift\": h[hv].mean(0), \"hover_std\": np.hypot(X[hv].std(0), Y[hv].std(0)),\n", " \"alt_err\": np.abs(A[:850] - log[\"talt\"][:850]).mean(0),\n", " \"obs_rt\": rt, \"obs_lateral\": np.abs(Y[k1] - Y[k0]), \"touch_vz\": touch,\n", " \"att_exceed\": (np.maximum(np.abs(log[\"roll_deg\"]), np.abs(log[\"pitch_deg\"])) > 35).sum(0),\n", " \"jerk\": np.percentile(ds, 95, axis=(0, 1)),\n", " \"overshoot\": np.clip(np.abs(Y[k0:k0 + 120] - Y[k0]).max(0) - 6.0, 0, None)}).pipe(\n", " lambda d: d.assign(n_risk=detect(d).sum(axis=1))) # overshoot: ํšŒํ”ผ ํ›„ ๋ชฉํ‘œ 6m๋ฅผ ๋„˜์–ด์„  ๊ฑฐ๋ฆฌ(๊ณผ๋ณด์ •)\n", "\n", "\n", "def make_dataset(n_pilots=40, seed=7, conds=(\"day\", \"night\", \"gust\"), flights=1):\n", " \"\"\"์กฐ์ข…์ž n๋ช… x ์กฐ๊ฑด x ๋ฐ˜๋ณต ๋น„ํ–‰ -> ๋น„ํ–‰๋ณ„ ํŠน์ง• + ์ •๋‹ต(๊ต๊ด€ ์ ์ˆ˜ยท๋“ฑ๊ธ‰ยท์œ„ํ—˜ ์‚ฌ๊ฑด) ํ‘œ.\"\"\"\n", " pilots = make_pilots(n_pilots, seed)\n", " out = []\n", " for c in conds:\n", " for f in range(flights):\n", " log, tr = fly(pilots, c, seed=seed * 100 + list(COND).index(c) * 10 + f)\n", " out.append(pd.concat([tr.reset_index(drop=True), features(log)], axis=1).assign(flight=f))\n", " return pd.concat(out, ignore_index=True)\n", "\n", "\n", "def cond_z(df, ref, cols):\n", " \"\"\"์กฐ๊ฑด๋ณ„ ๊ธฐ์ค€ ์ง‘๋‹จ(ref)์˜ ํ‰๊ท ยทํ‘œ์ค€ํŽธ์ฐจ๋กœ ํ‘œ์ค€ํ™”: ๋Œํ’์ฒ˜๋Ÿผ ์–ด๋ ค์šด ์กฐ๊ฑด์—์„œ ๋ฐ›๋Š” ๋ถˆ์ด์ต์„ ์ค„์ธ๋‹ค.\"\"\"\n", " z = df.copy()\n", " z[cols] = z[cols].astype(float)\n", " for c in df[\"cond\"].unique():\n", " r = ref[ref[\"cond\"] == c][cols]\n", " z.loc[df[\"cond\"] == c, cols] = (df.loc[df[\"cond\"] == c, cols] - r.mean()) / (r.std() + 1e-9)\n", " return z\n", "\n", "\n", "def to_grade(score):\n", " return np.where(score >= 75, \"A\", np.where(score >= 55, \"B\", \"C\"))\n", "\n", "CONFIG = {\"features\": ADV_F, \"condition_normalization\": True, \"ridge_alpha\": 10.0} # ๋ฏธ์…˜ 9 ์žฌํŠœ๋‹๊ฐ’\n", "KEYS = [\"x\", \"y\", \"alt\", \"stick_pitch\", \"stick_roll\", \"throttle\", \"roll_deg\", \"pitch_deg\", \"tx\", \"ty\", \"talt\"]\n", "\n", "\n", "def validate(L):\n", " if any(k not in L for k in KEYS) or len(L[\"x\"]) != STEPS:\n", " return \"ํ•„๋“œยท๊ธธ์ด ์˜ค๋ฅ˜\"\n", " if np.isnan(L[\"x\"]).mean() > 0.2:\n", " return \"๊ฒฐ์ธก 20% ์ดˆ๊ณผ\"\n", " return None\n", "\n", "\n", "def clean(L):\n", " out = {}\n", " for k in KEYS:\n", " s = pd.Series(L[k])\n", " if k in (\"x\", \"y\", \"alt\"):\n", " med = s.rolling(7, center=True, min_periods=1).median()\n", " s = s.mask((s - med).abs() > 3 * ((s - med).abs().rolling(7, center=True, min_periods=1).median() * 1.4826 + 0.3))\n", " out[k] = s.interpolate(limit_direction=\"both\").values\n", " return out\n", "\n", "\n", "class Review:\n", " def __init__(self):\n", " self.items, self.log = [], []\n", "\n", " def submit(self, rec):\n", " prev = self.log[-1][\"hash\"] if self.log else \"GENESIS\"\n", " body = {\"event\": \"AI_SUGGEST\", \"pilot\": rec[\"pilot\"], \"grade\": rec[\"grade\"], \"conf\": rec[\"conf\"]}\n", " self.log.append({**body, \"prev\": prev, \"hash\": hashlib.sha256((prev + json.dumps(body, sort_keys=True)).encode()).hexdigest()})\n", " self.items.append({**body, \"status\": \"PENDING\"})\n", "\n", "\n", "called = set()\n", "sys.setprofile(lambda f, e, a: called.add(f.f_code.co_name) if e == \"call\" and f.f_globals is globals() else None)\n", "t_start = time.perf_counter()\n", "\n", "# ---------------- 1. ์—”์ง„ ์ค€๋น„(AAR ์ž๋ฃŒ๋กœ ์žฌํŠœ๋‹๋œ ์„ค์ •) ----------------\n", "ref = make_dataset(40, seed=8)\n", "eng = Ridge(alpha=CONFIG[\"ridge_alpha\"]).fit(cond_z(ref, ref, ADV_F)[ADV_F], ref[\"instructor\"])\n", "\n", "# ---------------- 2. ์‹ ๊ทœ ๊ธฐ์ˆ˜ ๋กœ๊ทธ ์ˆ˜์ง‘(์กฐ์ข…์ž 30๋ช… x 3์กฐ๊ฑด) + ์†์ƒยท์žก์Œ ----------------\n", "pilots = make_pilots(30, seed=31)\n", "rng = np.random.default_rng(4)\n", "logs, conds, truth = [], [], []\n", "for c in COND:\n", " log, tr = fly(pilots, c, seed=700 + len(logs))\n", " for j in range(len(pilots)):\n", " L = {k: log[k][:, j].copy() for k in KEYS}\n", " sp = rng.random(STEPS) < 0.01\n", " L[\"x\"][sp] += 10.0 # GPS ํА\n", " L[\"y\"][300:310] = np.nan # ์„ผ์„œ ๋‹จ์ ˆ 1์ดˆ\n", " logs.append(L); conds.append(c)\n", " truth.append(tr)\n", "truth = pd.concat(truth, ignore_index=True)\n", "logs[7] = {k: v for k, v in logs[7].items() if k != \"throttle\"} # ์†์ƒ ๋กœ๊ทธ 2๊ฑด\n", "logs[40][\"x\"][:500] = np.nan\n", "\n", "# ---------------- 3. ๊ฒ€์ฆยท์ „์ฒ˜๋ฆฌยทํŠน์ง•ยท์ฑ„์  ----------------\n", "rejected = {i: r for i, L in enumerate(logs) if (r := validate(L))}\n", "ok = [i for i in range(len(logs)) if i not in rejected]\n", "t0 = time.perf_counter()\n", "cleaned = [clean(logs[i]) for i in ok]\n", "stack = {k: np.stack([c[k] for c in cleaned], axis=1) for k in KEYS}\n", "stack[\"t\"] = T\n", "F = features(stack).assign(cond=[conds[i] for i in ok])\n", "D = pd.concat([truth.iloc[ok].reset_index(drop=True), F.drop(columns=\"cond\")], axis=1)\n", "D[\"score\"] = np.clip(eng.predict(cond_z(F, ref, ADV_F)[ADV_F]), 0, 100)\n", "ev = detect(F)\n", "ms = (time.perf_counter() - t0) * 1000 / len(ok)\n", "\n", "# ---------------- 4. ์กฐ์ข…์ž ์ง‘๊ณ„ยท์‹ ๋ขฐ๋„ยท๊ต๊ด€ ๊ฒ€ํ†  ๋Œ€๊ธฐ์—ด ----------------\n", "rows = []\n", "for pid, g in D.groupby(\"pilot\"):\n", " boot = [to_grade(rng.choice(g[\"score\"].values, len(g)).mean()).item() for _ in range(100)]\n", " gr = to_grade(g[\"score\"].mean()).item(); cf = boot.count(gr) / 100\n", " rows.append({\"pilot\": pid, \"score\": g[\"score\"].mean(), \"grade\": gr, \"conf\": \"High\" if cf >= 0.9 else (\"Medium\" if cf >= 0.7 else \"Low\"),\n", " \"inst_grade\": to_grade(g[\"instructor\"].mean()).item(), \"risk\": int(ev.loc[g.index].values.sum())})\n", "P = pd.DataFrame(rows)\n", "rv = Review()\n", "for r in P.to_dict(\"records\"):\n", " rv.submit(r)\n", "sys.setprofile(None)\n", "\n", "# ---------------- 5. ์ž์ฒด ์ ๊ฒ€ ----------------\n", "mine = [n for n, o in list(globals().items()) if inspect.isfunction(o) and o.__module__ == \"__main__\"] + [\"submit\", \"__init__\"]\n", "cov = len(set(mine) & called) / len(set(mine))\n", "fl_acc = (to_grade(D[\"score\"]) == D[\"grade\"]).mean()\n", "pl_acc = (P[\"grade\"] == P[\"inst_grade\"]).mean()\n", "truth_ev = D[[\"near_collision\", \"hard_landing\", \"attitude_exceed\"]].astype(bool).values\n", "rec = (ev.values & truth_ev).sum() / max(truth_ev.sum(), 1)\n", "chk = {\n", " \"์†์ƒ ๋กœ๊ทธ 2๊ฑด ๊ฒฉ๋ฆฌ\": len(rejected) == 2,\n", " \"๋กœ๊ทธ ์œ ์‹ค 0๊ฑด(์ฑ„์ +๊ฒฉ๋ฆฌ=์ „์ฒด)\": len(ok) + len(rejected) == len(logs),\n", " \"์ „์ฒ˜๋ฆฌ ํ›„ ๊ฒฐ์ธก 0\": not any(np.isnan(v).any() for v in stack.values()),\n", " \"๋น„ํ–‰๋ณ„ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 75% ์ด์ƒ\": fl_acc >= 0.75,\n", " \"์กฐ์ข…์ž ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 85% ์ด์ƒ\": pl_acc >= 0.85,\n", " \"์œ„ํ—˜ ์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ 85% ์ด์ƒ\": rec >= 0.85,\n", " \"๋กœ๊ทธ๋‹น ์ฒ˜๋ฆฌ 200ms ์ด๋‚ด\": ms < 200,\n", " \"AI ํ™•์ • 0๊ฑด(์ „๋ถ€ ๊ต๊ด€ ๋Œ€๊ธฐ)\": all(i[\"status\"] == \"PENDING\" for i in rv.items),\n", " \"๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฑด์ˆ˜ ์ผ์น˜\": len(rv.log) == len(rv.items),\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%}, ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์„ฑ๊ณต {'์˜ˆ' if passed == len(chk) else '์•„๋‹ˆ์˜ค'}\")\n", "print(\"์กฐ์ข…์ž ๋“ฑ๊ธ‰ ๋ถ„ํฌ:\", P[\"grade\"].value_counts().reindex([\"A\", \"B\", \"C\"]).fillna(0).astype(int).to_dict(),\n", " \"/ ์‹ ๋ขฐ๋„:\", P[\"conf\"].value_counts().to_dict())\n", "\n", "# ---------------- 6. ๋Œ€์‹œ๋ณด๋“œ ----------------\n", "fig, ax = plt.subplots(2, 2, figsize=(12, 7))\n", "ax[0, 0].scatter(D[\"instructor\"], D[\"score\"], s=10, c=D[\"cond\"].map({\"day\": \"tab:blue\", \"night\": \"tab:purple\", \"gust\": \"tab:orange\"}))\n", "ax[0, 0].plot([20, 100], [20, 100], \"k--\"); ax[0, 0].set_xlabel(\"instructor\"); ax[0, 0].set_ylabel(\"AI\"); ax[0, 0].set_title(\"Flight score: AI vs instructor\")\n", "ax[0, 1].bar([\"near_collision\", \"hard_landing\", \"attitude\"], ev.sum().values, color=\"tab:red\"); ax[0, 1].set_title(\"Detected risk events\")\n", "Ps = P.sort_values(\"score\")\n", "ax[1, 0].bar(Ps[\"pilot\"], Ps[\"score\"], color=Ps[\"conf\"].map({\"High\": \"tab:green\", \"Medium\": \"tab:orange\", \"Low\": \"tab:red\"}))\n", "ax[1, 0].axhline(55, c=\"k\", ls=\"--\"); ax[1, 0].axhline(75, c=\"k\", ls=\"--\"); ax[1, 0].tick_params(axis=\"x\", rotation=90, labelsize=6)\n", "ax[1, 0].set_title(\"Pilot mean score (color = confidence)\")\n", "ax[1, 1].axis(\"off\")\n", "ax[1, 1].text(0, 0.9, f\"Logs {len(logs)} ({len(rejected)} rejected)\\nFlight acc {fl_acc:.1%}\\nPilot acc {pl_acc:.1%}\\n\"\n", " f\"Risk recall {rec:.1%}\\nPending {len(rv.items)} reviews\\nSelf-check {passed}/{len(chk)}\", family=\"monospace\", fontsize=13, va=\"top\")\n", "plt.suptitle(\"TRN01 integrated skill analyzer - training data only\"); plt.tight_layout(); plt.show()\n", "\n", "# ---------------- 7. ๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ ----------------\n", "c_list = P[P[\"grade\"] == \"C\"][\"pilot\"].tolist()\n", "print(f\"\"\"\n", "[๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ - ๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "1. ์ฒ˜๋ฆฌ: ๋น„ํ–‰๋กœ๊ทธ {len(logs)}๊ฑด ์ค‘ {len(ok)}๊ฑด ๋ถ„์„, ์†์ƒ {len(rejected)}๊ฑด ๊ฒฉ๋ฆฌ, ๋กœ๊ทธ๋‹น {ms:.1f} ms, ์ „์ฒด {time.perf_counter() - t_start:.1f}s\n", "2. ์„ฑ๋Šฅ: ๋น„ํ–‰๋ณ„ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ {fl_acc:.1%}, ์กฐ์ข…์ž ๋“ฑ๊ธ‰ ์ผ์น˜์œจ {pl_acc:.1%}, ์œ„ํ—˜ ์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ {rec:.1%}\n", "3. ๊ฒฐ๊ณผ: A {int((P['grade'] == 'A').sum())}๋ช… / B {int((P['grade'] == 'B').sum())}๋ช… / C {len(c_list)}๋ช…(์žฌ๊ต์œก ๊ฒ€ํ†  ๋Œ€์ƒ {', '.join(c_list[:5])}{' ์™ธ' if len(c_list) > 5 else ''}), ์ „์› ๊ต๊ด€ ๊ฒ€ํ†  ๋Œ€๊ธฐ\n", "4. ์šด์šฉ ์œ ์˜: AI ๋“ฑ๊ธ‰์€ ๊ต๊ด€ ํŒ๋‹จ ๋ณด์กฐ ์ž๋ฃŒ์ด๋ฉฐ, ์žฌ๊ต์œกยท์ž๊ฒฉ ๋ณด๋ฅ˜๋Š” ๊ต๊ด€ ๊ทผ๊ฑฐ ๊ธฐ๋ก๊ณผ ๋‘ ๋ฒˆ์งธ ๊ต๊ด€ ํ™•์ธ ํ›„ ํ™•์ •ํ•œ๋‹ค.\n", "5. ํ•œ๊ณ„ยทํ™•์žฅ: ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋กœ๊ทธ ๊ธฐ์ค€ ๊ฒฐ๊ณผ๋‹ค. ์‹ค์ œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋กœ๊ทธ ํ˜•์‹์— ๋งž์ถ˜ ์žฌ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋ฉฐ, ํ›ˆ๋ จ ๊ด€๋ฆฌ ์ฒด๊ณ„์™€๋Š” ๊ฒฐ๊ณผ ์กฐํšŒ API๋กœ ์—ฐ๋™ํ•œ๋‹ค.\"\"\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "๐Ÿ’ก [์ฐธ๊ณ ] ๋ฏธ์…˜ 10 ์˜ˆ์ƒ ์‹คํ–‰ ๊ฒฐ๊ณผ (Output)\n", "\n", "```text\n", "[PASS] ์†์ƒ ๋กœ๊ทธ 2๊ฑด ๊ฒฉ๋ฆฌ\n", "[PASS] ๋กœ๊ทธ ์œ ์‹ค 0๊ฑด(์ฑ„์ +๊ฒฉ๋ฆฌ=์ „์ฒด)\n", "[PASS] ์ „์ฒ˜๋ฆฌ ํ›„ ๊ฒฐ์ธก 0\n", "[PASS] ๋น„ํ–‰๋ณ„ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 75% ์ด์ƒ\n", "[PASS] ์กฐ์ข…์ž ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 85% ์ด์ƒ\n", "[PASS] ์œ„ํ—˜ ์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ 85% ์ด์ƒ\n", "[PASS] ๋กœ๊ทธ๋‹น ์ฒ˜๋ฆฌ 200ms ์ด๋‚ด\n", "[PASS] AI ํ™•์ • 0๊ฑด(์ „๋ถ€ ๊ต๊ด€ ๋Œ€๊ธฐ)\n", "[PASS] ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ฑด์ˆ˜ ์ผ์น˜\n", "[PASS] ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ 85% ์ด์ƒ\n", "์ž์ฒด ์ ๊ฒ€ 10/10 ํ†ต๊ณผ, ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€ 92%, ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰ ์„ฑ๊ณต ์˜ˆ\n", "์กฐ์ข…์ž ๋“ฑ๊ธ‰ ๋ถ„ํฌ: {'A': 4, 'B': 15, 'C': 11} / ์‹ ๋ขฐ๋„: {'High': 21, 'Medium': 6, 'Low': 3}\n", "\n", "[๊ต์œกํ›ˆ๋ จ ๋ธŒ๋ฆฌํ•‘ ์š”์•ฝ - ๊ต์œก์šฉ๊ฐ€์ƒ์ž๋ฃŒ]\n", "1. ์ฒ˜๋ฆฌ: ๋น„ํ–‰๋กœ๊ทธ 90๊ฑด ์ค‘ 88๊ฑด ๋ถ„์„, ์†์ƒ 2๊ฑด ๊ฒฉ๋ฆฌ, ๋กœ๊ทธ๋‹น 13.6 ms, ์ „์ฒด 5.9s\n", "2. ์„ฑ๋Šฅ: ๋น„ํ–‰๋ณ„ ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 79.5%, ์กฐ์ข…์ž ๋“ฑ๊ธ‰ ์ผ์น˜์œจ 93.3%, ์œ„ํ—˜ ์‚ฌ๊ฑด ํƒ์ง€ ์žฌํ˜„์œจ 97.4%\n", "3. ๊ฒฐ๊ณผ: A 4๋ช… / B 15๋ช… / C 11๋ช…(์žฌ๊ต์œก ๊ฒ€ํ†  ๋Œ€์ƒ P001, P007, P012, P015, P016 ์™ธ), ์ „์› ๊ต๊ด€ ๊ฒ€ํ†  ๋Œ€๊ธฐ\n", "4. ์šด์šฉ ์œ ์˜: AI ๋“ฑ๊ธ‰์€ ๊ต๊ด€ ํŒ๋‹จ ๋ณด์กฐ ์ž๋ฃŒ์ด๋ฉฐ, ์žฌ๊ต์œกยท์ž๊ฒฉ ๋ณด๋ฅ˜๋Š” ๊ต๊ด€ ๊ทผ๊ฑฐ ๊ธฐ๋ก๊ณผ ๋‘ ๋ฒˆ์งธ ๊ต๊ด€ ํ™•์ธ ํ›„ ํ™•์ •ํ•œ๋‹ค.\n", "5. ํ•œ๊ณ„ยทํ™•์žฅ: ๊ฐ€์ƒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋กœ๊ทธ ๊ธฐ์ค€ ๊ฒฐ๊ณผ๋‹ค. ์‹ค์ œ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ๋กœ๊ทธ ํ˜•์‹์— ๋งž์ถ˜ ์žฌ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋ฉฐ, ํ›ˆ๋ จ ๊ด€๋ฆฌ ์ฒด๊ณ„์™€๋Š” ๊ฒฐ๊ณผ ์กฐํšŒ API๋กœ ์—ฐ๋™ํ•œ๋‹ค.\n", "```\n", "
\n" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }