diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7175273 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Git +.git +.gitignore +.gitattributes +.github + +# Python caches +__pycache__ +*.pyc +*.pyo +*.pyd +.pytest_cache +.coverage +htmlcov/ +*.egg-info/ +dist/ +build/ + +# Virtual environments +venv/ +.venv/ +env/ +.env + +# Editor & OS artifacts +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Agent & temporary files +scratch/ +.gemini/ + +# Raw lidar data (mounted via volume, never baked into image) +data/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4fe4bf4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,96 @@ +# ============================================================================ +# FlyGuard: Bionic Obstacle Detection Core (LCT-2026, Case 05) +# Hesai Pandar128 LiDAR (128 beams, 10 Hz / 100ms) +# Universal Production Image: NVIDIA CUDA 12.x / Ada Lovelace / Graceful CPU Fallback +# ============================================================================ + +# ---------------------------------------------------------------------------- +# Stage 1: Optional lightweight CPU-only image (build with: --target cpu) +# ---------------------------------------------------------------------------- +FROM python:3.11-slim-bookworm AS cpu + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH="/app:/app/tools" \ + FLYGUARD_DATA="/data" \ + FLYGUARD_DEVICE="cpu" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgomp1 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -m -u 1000 -s /bin/bash flyguard && \ + mkdir -p /app /data /app/artifacts && \ + chown -R flyguard:flyguard /app /data + +WORKDIR /app +COPY --chown=flyguard:flyguard requirements.txt /app/ +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +COPY --chown=flyguard:flyguard . /app/ +RUN chmod +x /app/docker-entrypoint.sh + +USER flyguard +VOLUME ["/data", "/app/artifacts"] +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["default"] + + +# ---------------------------------------------------------------------------- +# Stage 2: Universal Production Image with NVIDIA GPU acceleration & CPU fallback +# (Default target when building with: docker build -t flyguard:latest .) +# ---------------------------------------------------------------------------- +FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04 AS production + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH="/app:/app/tools" \ + FLYGUARD_DATA="/data" \ + FLYGUARD_DEVICE="auto" \ + NVIDIA_VISIBLE_DEVICES=all \ + NVIDIA_DRIVER_CAPABILITIES=compute,utility + +# Install Python 3.11, pip, and system runtime libraries (OpenMP for LightGBM/Torch) +RUN apt-get update && apt-get install -y --no-install-recommends \ + software-properties-common \ + ca-certificates \ + libgomp1 \ + curl \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get update && apt-get install -y --no-install-recommends \ + python3.11 \ + python3.11-distutils \ + && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \ + && ln -sf /usr/bin/python3.11 /usr/bin/python3 \ + && ln -sf /usr/bin/python3.11 /usr/bin/python \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Create non-root user for container security compliance +RUN useradd -m -u 1000 -s /bin/bash flyguard && \ + mkdir -p /app /data /app/artifacts && \ + chown -R flyguard:flyguard /app /data + +WORKDIR /app + +# Cache layer: install Python dependencies with PyTorch CUDA 12.1+ wheels +COPY --chown=flyguard:flyguard requirements-gpu.txt /app/ +RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \ + pip install --no-cache-dir -r requirements-gpu.txt + +# Copy source code and artifacts +COPY --chown=flyguard:flyguard . /app/ +RUN chmod +x /app/docker-entrypoint.sh + +USER flyguard +VOLUME ["/data", "/app/artifacts"] + +# Container healthcheck: verifies Python runtime and graceful device detection +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python3 -c "import flyguard; from flyguard.device import get_device_info; print('healthy', get_device_info())" || exit 1 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["default"] diff --git a/Dockerfile.gpu b/Dockerfile.gpu new file mode 100644 index 0000000..2fcd26e --- /dev/null +++ b/Dockerfile.gpu @@ -0,0 +1,57 @@ +# ============================================================================ +# FlyGuard GPU: NVIDIA RTX / CUDA 12 Production Image (Кейс 05, ЛЦТ-2026) +# Архитектура: Ada Lovelace (RTX 4070 Ti Super 16GB) / Ampere / Turing +# Полная совместимость с Dockerfile (Stage: production) +# ============================================================================ +FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04 AS production + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH="/app:/app/tools" \ + FLYGUARD_DATA="/data" \ + FLYGUARD_DEVICE="auto" \ + NVIDIA_VISIBLE_DEVICES=all \ + NVIDIA_DRIVER_CAPABILITIES=compute,utility + +# Install Python 3.11, pip, and system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + software-properties-common \ + ca-certificates \ + libgomp1 \ + curl \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get update && apt-get install -y --no-install-recommends \ + python3.11 \ + python3.11-distutils \ + && curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \ + && ln -sf /usr/bin/python3.11 /usr/bin/python3 \ + && ln -sf /usr/bin/python3.11 /usr/bin/python \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Create non-root user and directories +RUN useradd -m -u 1000 -s /bin/bash flyguard && \ + mkdir -p /app /data /app/artifacts && \ + chown -R flyguard:flyguard /app /data + +WORKDIR /app + +# Cache layer: copy only requirements first +COPY --chown=flyguard:flyguard requirements-gpu.txt /app/ + +# Install python dependencies with PyTorch CUDA 12 +RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \ + pip install --no-cache-dir -r requirements-gpu.txt + +# Copy application source code +COPY --chown=flyguard:flyguard . /app/ +RUN chmod +x /app/docker-entrypoint.sh + +USER flyguard +VOLUME ["/data", "/app/artifacts"] + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python3 -c "import flyguard; from flyguard.device import get_device_info; print('healthy', get_device_info())" || exit 1 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["default"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..816b6fe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,237 @@ +services: + # ============================================================================ + # FlyGuard: Универсальный производственный запуск (NVIDIA GPU / CUDA 12) + # Поддерживает Ada Lovelace (RTX 4070 Ti Super 16GB) / Ampere / CPU Fallback + # ============================================================================ + + # --- Главный сервис общего запуска конвейера по всем бэгам лидара --- + pipeline: + build: + context: . + dockerfile: Dockerfile + image: flyguard:latest + container_name: flyguard-pipeline + command: ["pipeline", "--all", "--verbose"] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data:ro + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - FLYGUARD_DEVICE=auto + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + shm_size: '8gb' + + # --- Полный прогон тестового набора ядра и роутинга устройств --- + test: + image: flyguard:latest + container_name: flyguard-test + command: ["test"] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data:ro + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - FLYGUARD_DEVICE=auto + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + shm_size: '8gb' + + # --- Диагностика доступности GPU и характеристик оборудования --- + info: + image: flyguard:latest + container_name: flyguard-info + command: ["info"] + environment: + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + + # --- Оценка метрик детекции (Folds Cross-Validation / AUC) --- + evaluate: + image: flyguard:latest + container_name: flyguard-evaluate + command: ["evaluate", "--mbon-dir", "artifacts/mbon_folds", "--device", "auto"] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data:ro + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + shm_size: '8gb' + + # --- Генерация синтетического бенчмарка с GPU DoG и MBON --- + benchmark: + image: flyguard:latest + container_name: flyguard-benchmark + command: [ + "benchmark", + "--memory", "artifacts/mushroom_body.npz", + "--mbon-dir", "artifacts/mbon_folds", + "--augment", + "--device", "auto", + "--out", "artifacts/benchmark_gpu.json" + ] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data:ro + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + shm_size: '8gb' + + # --- Обучение MBON Readout на 50 000 клеток Кеньона на GPU --- + train-mbon: + image: flyguard:latest + container_name: flyguard-train-mbon + command: [ + "train-mbon", + "--device", "auto", + "--n-kc", "50000", + "--active", "100", + "--epochs", "100", + "--save-folds", "artifacts/mbon_folds_50k", + "--out", "artifacts/mbon_readout_50k.npz" + ] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + shm_size: '8gb' + + # --- Обучение классификатора треков TrackReadout --- + train-track: + image: flyguard:latest + container_name: flyguard-train-track + command: [ + "train-track", + "--device", "auto", + "--epochs", "300", + "--baseline", + "--save-folds", "artifacts/track_folds", + "--out", "artifacts/track_readout.npz" + ] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + shm_size: '8gb' + + # --- Интерактивная Bash-сессия разработчика --- + shell: + image: flyguard:latest + container_name: flyguard-shell + command: ["bash"] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + stdin_open: true + tty: true + shm_size: '8gb' + + # ============================================================================ + # СЕРВИСЫ ДЛЯ ЧИСТОГО CPU (Stand-alone без NVIDIA GPU) + # ============================================================================ + cpu-test: + build: + context: . + dockerfile: Dockerfile + target: cpu + image: flyguard:cpu + container_name: flyguard-cpu-test + command: ["test"] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data:ro + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - FLYGUARD_DEVICE=cpu + shm_size: '2gb' + + cpu-pipeline: + image: flyguard:cpu + container_name: flyguard-cpu-pipeline + command: ["pipeline", "--all"] + volumes: + - ./artifacts:/app/artifacts + - ./data:/data:ro + environment: + - PYTHONUNBUFFERED=1 + - FLYGUARD_DATA=/data + - FLYGUARD_DEVICE=cpu + shm_size: '2gb' diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..2c3e7d3 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -e + +# ============================================================================== +# FlyGuard: Bionic Obstacle Detection Core (LCT-2026, Case 05) +# Hesai Pandar128 LiDAR (128 beams, 10 Hz / 100ms) +# Universal Entrypoint: NVIDIA GPU Acceleration (CUDA 12.x) with CPU Fallback +# ============================================================================== + +print_banner() { + echo "============================================================================" + echo " FlyGuard · Блиц-обнаружение препятствий в тоннеле метро (Кейс 05, ЛЦТ-2026)" + echo " Hesai Pandar128 (128 лучей, 10 Гц / 100 мс) · NVIDIA CUDA 12 / CPU Fallback" + echo "============================================================================" +} + +print_device_info() { + python3 -c " +try: + import flyguard.device as dev + info = dev.get_device_info() + if info['cuda_available']: + print(f'==> [HARDWARE] NVIDIA GPU: {info[\"device_name\"]} (Compute {info[\"compute_capability\"]}, {info[\"total_memory_gb\"]} GB VRAM)') + print(f'==> [ACCELERATION] CUDA 12: АКТИВНО (PyTorch {info[\"torch_version\"]})') + else: + print('==> [HARDWARE] CPU Mode: CUDA недоступна или не передана в контейнер.') + print('==> [FALLBACK] Активирован автоматический Graceful Fallback на CPU.') +except Exception as e: + print(f'==> Проверка окружения: {e}') +" +} + +CMD="${1:-default}" + +case "$CMD" in + default|run) + print_banner + print_device_info + DATA_DIR="${FLYGUARD_DATA:-/data}" + FOUND_BAG=$(find "$DATA_DIR" -name '*.db3' 2>/dev/null | head -n 1 || true) + if [ -d "$DATA_DIR" ] && [ -n "$FOUND_BAG" ]; then + echo "==> Обнаружены бэги лидара в $DATA_DIR. Запуск конвейера FlyGuard по всем записям..." + exec python3 tools/run_pipeline.py --all \ + --memory artifacts/mushroom_body.npz \ + --readout artifacts/mbon_readout.npz \ + --device auto \ + --verbose + else + echo "==> В $DATA_DIR не найдены файлы .db3 (датасет не смонтирован через -v /путь/к/data:/data)." + echo "==> Выполняется системный прогон верификационных тестов и роутинга..." + exec python3 tests/run_tests.py + fi + ;; + pipeline|run-pipeline) + shift + print_banner + print_device_info + exec python3 tools/run_pipeline.py \ + --memory artifacts/mushroom_body.npz \ + --readout artifacts/mbon_readout.npz \ + --device auto "$@" + ;; + benchmark|make-benchmark) + shift + print_banner + print_device_info + exec python3 tools/make_benchmark.py \ + --memory artifacts/mushroom_body.npz \ + --mbon-dir artifacts/mbon_folds \ + --device auto "$@" + ;; + evaluate) + shift + print_banner + print_device_info + exec python3 tools/evaluate.py \ + --mbon-dir artifacts/mbon_folds \ + --device auto "$@" + ;; + train-mbon) + shift + print_banner + print_device_info + exec python3 tools/train_mbon.py \ + --device auto "$@" + ;; + train-track) + shift + print_banner + print_device_info + exec python3 tools/train_track.py \ + --device auto "$@" + ;; + test|tests|run-tests) + shift + print_banner + print_device_info + exec python3 tests/run_tests.py "$@" + ;; + pytest) + shift + exec pytest tests "$@" + ;; + info|device|gpu|gpu-info) + print_banner + python3 -c " +import json +import flyguard.device as dev +print(json.dumps(dev.get_device_info(), indent=2, ensure_ascii=False)) +" + ;; + bash|sh) + shift + exec /bin/bash "$@" + ;; + *) + exec "$@" + ;; +esac diff --git a/docker-run.sh b/docker-run.sh new file mode 100755 index 0000000..4fc8b38 --- /dev/null +++ b/docker-run.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# ============================================================================== +# FlyGuard: Удобный скрипт сборки и запуска Docker-контейнера +# Автоматически определяет наличие NVIDIA GPU и передает флаги --gpus all +# ============================================================================== +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IMAGE_NAME="flyguard:latest" +DATA_DIR="${FLYGUARD_DATA:-$DIR/data}" +ARTIFACTS_DIR="$DIR/artifacts" + +mkdir -p "$DATA_DIR" "$ARTIFACTS_DIR" + +# Определение наличия драйвера NVIDIA +GPU_FLAG="" +if command -v nvidia-smi &>/dev/null; then + GPU_FLAG="--gpus all" +fi + +usage() { + echo "Использование: $0 [команда]" + echo "" + echo "Команды:" + echo " build Собрать универсальный образ с поддержкой NVIDIA GPU ($IMAGE_NAME)" + echo " build-cpu Собрать облегченный CPU-образ (flyguard:cpu)" + echo " run Общий запуск конвейера (по умолчанию обработка всех бэгов)" + echo " pipeline Запуск конвейера с дополнительными аргументами" + echo " test Запуск всех 40+ тестов верификации" + echo " benchmark Генерация бенчмарка с ускорением на GPU" + echo " evaluate Оценка качества моделей и кросс-валидация" + echo " train-mbon Обучение клеток Кеньона (50 000 KC) на GPU" + echo " train-track Обучение классификатора треков" + echo " info Вывод телеметрии и статуса GPU из контейнера" + echo " shell Интерактивная командная строка (bash) внутри контейнера" + echo "" + echo "Примеры:" + echo " $0 build" + echo " $0 run" + echo " $0 pipeline --all --verbose" + echo " $0 train-mbon --device cuda --n-kc 50000" +} + +CMD="${1:-run}" + +case "$CMD" in + build) + echo "==> Сборка универсального образа FlyGuard ($IMAGE_NAME)..." + docker build -t "$IMAGE_NAME" -f "$DIR/Dockerfile" "$DIR" + ;; + build-cpu) + echo "==> Сборка облегченного CPU-образа FlyGuard (flyguard:cpu)..." + docker build --target cpu -t flyguard:cpu -f "$DIR/Dockerfile" "$DIR" + ;; + run|default) + shift 2>/dev/null || true + echo "==> Запуск контейнера FlyGuard (GPU: ${GPU_FLAG:-CPU Fallback})..." + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data:ro \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" "$@" + ;; + pipeline) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data:ro \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" pipeline "$@" + ;; + test|tests) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data:ro \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" test "$@" + ;; + benchmark) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data:ro \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" benchmark "$@" + ;; + evaluate) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data:ro \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" evaluate "$@" + ;; + train-mbon) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" train-mbon "$@" + ;; + train-track) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" train-track "$@" + ;; + info|gpu|device) + docker run --rm $GPU_FLAG "$IMAGE_NAME" info + ;; + shell|bash) + shift + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" bash "$@" + ;; + help|--help|-h) + usage + exit 0 + ;; + *) + docker run --rm -it $GPU_FLAG \ + -v "$DATA_DIR":/data:ro \ + -v "$ARTIFACTS_DIR":/app/artifacts \ + --shm-size=8g \ + "$IMAGE_NAME" "$@" + ;; +esac diff --git a/docs/DOCKER.md b/docs/DOCKER.md new file mode 100644 index 0000000..ba2d481 --- /dev/null +++ b/docs/DOCKER.md @@ -0,0 +1,106 @@ +# Руководство по запуску FlyGuard в Docker (NVIDIA GPU + Graceful CPU Fallback) + +FlyGuard поставляется в виде универсального производственного Docker-образа с поддержкой аппаратного ускорения **NVIDIA GPU (CUDA 12.x)** и автоматического гибридного переключения на **CPU (Graceful Fallback)** при отсутствии или сбое графического процессора. + +--- + +## 1. Быстрый запуск через helper-скрипт `docker-run.sh` + +Скрипт [`docker-run.sh`](file:///home/wpng1337/Brainrot_Muxa/docker-run.sh) автоматически проверяет наличие `nvidia-smi` и подключает флаги `--gpus all`: + +```bash +# Сборка универсального образа (flyguard:latest) +./docker-run.sh build + +# Общий запуск конвейера (по всем бэгам в data/) +./docker-run.sh run + +# Проверка видимости GPU и драйвера внутри контейнера +./docker-run.sh info + +# Прогон всех 41 верификационных unit-тестов +./docker-run.sh test + +# Обучение клеток Кеньона (50 000 KC) на GPU +./docker-run.sh train-mbon --device cuda --n-kc 50000 + +# Генерация бенчмарка дальности с аугментациями +./docker-run.sh benchmark +``` + +--- + +## 2. Сборка образов напрямую через `docker build` + +### Универсальный образ с GPU-ускорением (по умолчанию) +```bash +docker build -t flyguard:latest . +``` +* **База**: `nvidia/cuda:12.4.1-runtime-ubuntu22.04` +* **Python**: 3.11, PyTorch CUDA 12.1+, cuDNN, OpenMP `libgomp1` +* **Поддерживаемые архитектуры**: Ada Lovelace (RTX 4070 Ti Super 16GB), Ampere, Hopper, Turing. +* **Отказоустойчивость**: если запущен без `--gpus all`, прозрачно работает на CPU. + +### Облегченный CPU-образ (опционально, ~200 МБ) +```bash +docker build --target cpu -t flyguard:cpu . +``` + +--- + +## 3. Запуск через Docker Compose + +В [`docker-compose.yml`](file:///home/wpng1337/Brainrot_Muxa/docker-compose.yml) настроены готовые сервисы с автоматической резервацией GPU: + +```bash +# 1. Главный сервис: сквозная обработка данных лидара +docker compose up +# или +docker compose run --rm pipeline + +# 2. Проверка оборудования +docker compose run --rm info + +# 3. Полный набор unit-тестов +docker compose run --rm test + +# 4. Высокопроизводительное обучение MBON на 50k клеток +docker compose run --rm train-mbon + +# 5. Оценка метрик детекции и ложных тревог +docker compose run --rm evaluate + +# 6. Интерактивная отладка в bash +docker compose run --rm shell +``` + +--- + +## 4. Прямой запуск через `docker run` + +### Запуск с аппаратным ускорением NVIDIA GPU: +```bash +docker run --rm -it \ + --gpus all \ + -v /path/to/lidar/data:/data:ro \ + -v $(pwd)/artifacts:/app/artifacts \ + --shm-size=8g \ + flyguard:latest pipeline --all --verbose +``` + +### Запуск на CPU (стенд жюри без GPU / fallback): +```bash +docker run --rm -it \ + -v /path/to/lidar/data:/data:ro \ + -v $(pwd)/artifacts:/app/artifacts \ + --shm-size=4g \ + flyguard:latest pipeline --all --verbose +``` + +--- + +## 5. Архитектура Graceful Fallback на CPU + +Конвейер использует гибридную схему вычислений: +1. **Статический выбор**: при инициализации `device = "cuda" if is_cuda_available() else "cpu"`. +2. **Динамический рантайм-откат**: если в процессе работы видеокарта возвращает ошибку (CUDA OOM, отвал драйвера), модуль [`flyguard/device.py`](file:///home/wpng1337/Brainrot_Muxa/flyguard/device.py) фиксирует сбой через `notify_cuda_error(exc)`, текущий кадр мгновенно досчитывается на CPU через NumPy/SciPy, а конвейер переходит в режим CPU без падения программы. diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..b4dd304 --- /dev/null +++ b/docs/IMPROVEMENT_PLAN.md @@ -0,0 +1,254 @@ +# Комплексный план развития FlyGuard · Хакатон ЛЦТ-2026 (Кейс 05) + +Документ фиксирует согласованную архитектурную стратегию и детальный план доработок ядра `FlyGuard` для максимизации качества решения на хакатоне ЛЦТ-2026 («Обнаружение посторонних объектов в тоннеле метро по данным 3D-лидара»). + +--- + +## 1. Контекст и текущее состояние + +Система [`FlyGuard`](../flyguard/pipeline.py) построена на принципах зрительной системы и грибовидных тел дрозофилы: +``` +облако точек 0.3–0.9 млн, 10 Гц + │ + ├─ RETINA омматидиальная решётка → дальностный образ 128 × N + ├─ HALTERES плоскость рельсов: крен, тангаж, высота сенсора + ├─ LAMINA диспаритет 1/R → ON/OFF, центр-окружение на 3 масштабах + ├─ MEDULLA / LP T4/T5 → LPTC: скорость поезда без одометрии + ├─ LOBULA LC11: кандидаты; разрез компоненты по контрасту + ├─ MUSHROOM BODY KC → APL → MBON: новизна формы (без меток) + │ + обученное считывание MBON (с метками из физики) + ├─ FAN-SHAPED BODY накопление лучей в координатах пути + ├─ CENTRAL COMPLEX накопление улик в координатах пути, треки + └─ DESCENDING два порога с гистерезисом → решение +``` + +### Текущие измеренные показатели (Ryzen 7600X, Hesai Pandar128): +* **Реальный объект на 55 м (`doubleT_obstacle`)**: **99.5 %** кадров с детекцией. +* **Задержка кадра на CPU**: **32–33 мс** при лимите 100 мс (запас ~3×). +* **Ложные тревоги (leave-one-bag-out)**: **8.0 трека/км** (11.0 % кадров). +* **Дальность обнаружения стоящего человека**: рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.33**. + +--- + +### Главные узкие места, выявленные в ходе анализа: + +1. **Застревание улики трека на 120–160 м**: + На 150 м кандидат появляется в ~62 % кадров, но от предмета приходит лишь 4–6 лучей. Покадровое считывание MBON выдаёт малое $p \approx 0.05$ (из-за априорной редкости), улика прирастает на $+gain \cdot w \approx +0.017$, а утечка за промах отнимает $-leak = -0.12$. В итоге $evidence$ колеблется около нуля, и порог $0.5$ никогда не пробивается. +2. **Артефакты синтетики в [`TrackReadout`](../flyguard/track_readout.py)**: + Признаки $s_{std}$ и $u_{std}$ перевернулись на реальном объекте ($0.365$ м у человека против $0.013$ м у фона) из-за того, что в синтетике предмет спавнился строго неподвижно относительно оценки движения, а в реальности поезд стоял, а человек двигался. Признаки $closed$ и $age$ заучили фиксированный спавн с 200 м. В текущей версии `track_blend = 0.0` (выключен). +3. **Неиспользуемая неопределённость коридора [`Corridor.sigma`](../flyguard/geometry.py)**: + Ось коридора на кривой за горизонтом видимости (62–107 м) продолжается по касательной и расходится с истинным путем до 1.19 м на 150 м. Полуширина габарита жестко зафиксирована на $1.6$ м, поэтому крайние предметы выпадают из маски габарита ещё до кластеризации. +4. **Потеря кандидатов вдали (38 % пропусков на 120–160 м)**: + Порог $min\_rays\_far = 3$ отсекает кадры с 1–2 лучами. При этом накопитель [`FanBody`](../flyguard/fan_body.py) за 15 кадров аккумулирует в вокселе десятки попаданий, но никак не помогает формированию покадрового кандидата. +5. **Скачок ложных тревог на новой линии ($8.0 \to 20.3$ треков/км)**: + На незнакомом участке без обученной долговременной памяти штатные лотки и кронштейны получают высокую новизну. Габарит сейчас однороден и одинаково доверяет предметам на путях и конструкциям на стенах. +6. **Слипание с платформой на 30–50 м**: + При смещении предмета $+0.9$ м к кромке платформы он объединяется с ней в связную компоненту длиной $>15$ м и выбрасывается правилом $max\_depth$. +7. **Отсутствие лежащего человека**: + В [`catalogue`](../flyguard/synth.py) нет класса лежащего человека, а нижняя граница габарита $h_{lo} = 0.28$ м срезает упавшего на рельсы пассажира. + +--- + +## 2. Поэтапный план улучшений + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ ФАЗА 1: Устранение артефактов данных и синтетики │ + │ • Аугментация спавна, движения, стоянки в make_benchmark │ + │ • Добавление «человек_лежа» (1.8×0.5×0.3 м) │ + │ • Честное переобучение и валидация TrackReadout │ + └──────────────────────────────┬──────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ ФАЗА 2: Пробитие дальней зоны (100–160 м) │ + │ • Двухканальное решение в DescendingNeurons (soft-warning) │ + │ • Динамическое расширение габарита по Corridor.sigma(d) │ + │ • Обратная связь FanBody → Lobula (порог 2 луча по опоре) │ + └──────────────────────────────┬──────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ ФАЗА 3: Обобщаемость на новый тоннель и ближняя зона │ + │ • Иерархический габарит (опасная колея vs стены/лотки) │ + │ • Вычитание плоскости платформы (h~1.1 м, |u|>1.2 м) │ + │ • Понижение h_lo до 0.16 м в межрельсовой зоне │ + └──────────────────────────────┬──────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ ФАЗА 4: ROS2 Экспорт, Замеры и Протокол Стенда Жюри │ + │ • Модуль экспорта: 3D Bounding Boxes, Threat Level, RViz │ + │ • Сквозной Leave-one-bag-out замер и стресс-тест на CPU │ + └─────────────────────────────────────────────────────────────┘ +``` + +--- + +### Фаза 1: Устранение артефактов данных и синтетики + +**Цель:** сделать выборку для обучения [`TrackReadout`](../flyguard/track_readout.py) инвариантной к реальным условиям, исключить переобучение на искусственные признаки генератора. + +#### 1.1. Аугментация генерации в [`tools/make_benchmark.py`](../tools/make_benchmark.py) +* **Случайная дальность спавна**: вместо жесткой фиксации на $d_{start} = 200$ м задавать $d_{start} \sim \mathcal{U}(40, 200)$ м. Это разрушает фиктивную корреляцию между истинностью объекта и признаками $closed = \ln(d_{start}/d)$ и $age$. +* **Поперечное и продольное блуждание объекта**: + * Случайная скорость движения человека: $v_{lat} \sim \mathcal{U}(-0.4, 0.4)$ м/с. + * Реалистичный шум положения $\delta s \sim \mathcal{N}(0, \sigma_s^2)$ с $\sigma_s = 0.15$ м (моделирует погрешность одометрии и скольжение лучей по телу). + * Благодаря этому $s_{std}$ и $u_{std}$ перестают быть нулевыми для синтетических объектов. +* **Сценарии со стоящим поездом**: + * Генерация препятствий в кадры со стоящим поездом ($v = 0$, например, кадры бэга `doubleT_obstacle`), чтобы модель не считала нулевую скорость признаком отсутствия опасности. + +#### 1.2. Добавление класса «человек_лежа» в [`catalogue`](../flyguard/synth.py) +* Добавить геометрию: + ```python + "человек_лежа": ObjectModel("человек_лежа", [ + Box(length=1.80, width=0.50, height=0.30, h_base=0.0) + ], reflectivity=0.35) + ``` +* В [`_quality`](../flyguard/central_complex.py) учесть, что вытянутость вдоль пути ($depth \approx 1.8$ м) при малой высоте ($h \le 0.35$ м) и расположении в межрельсовой зоне ($|u| \le 0.75$ м) не должна штрафоваться множителем $compact$. + +#### 1.3. Переобучение [`TrackReadout`](../tools/train_track.py) +* Собрать обновлённый кэш треков: + ```bash + python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \ + --mbon-dir artifacts/mbon_folds --tracks-out data/cache/track_set_aug.npz + ``` +* Обучить [`TrackReadout`](../tools/train_track.py) с контролем ROC AUC по всем полосам (целевой AUC $>0.90$ на полосе 110–160 м). +* Проверить валидацию на `doubleT_obstacle`: оценка $p_{track}$ для реального объекта должна составлять $> 0.95$. + +--- + +### Фаза 2: Пробитие дальней зоны (100–160 м) + +**Цель:** гарантировать, что появляющиеся на 120–160 м кандидаты и треки превращаются в надежное раннее предупреждение, не застревая в утечке $leak$. + +#### 2.1. Двухканальное решение в [`DescendingNeurons`](../flyguard/descending.py) +Биологическая аналогия: разделение Giant Fiber (DNp01, жесткий аварийный порог) и DNp02/DNp11 (раннее предупреждение). + +```python +# 1. Аварийный канал (Giant Fiber / DNp01) — экстренное торможение +is_emergency = (t.evidence >= self.emergency_evidence) and (d <= stopping_distance) + +# 2. Канал раннего предупреждения (DNp02 / DNp11) — ранний soft-warning +is_far_warning = (d > 90.0) and (t.hits >= 2) and (t.p_track >= 0.65) +is_near_warning = (d <= 90.0) and (t.evidence >= self.warn_evidence) + +decision.detected = is_emergency or is_far_warning or is_near_warning +decision.emergency = is_emergency +``` + +* **Безопасность**: далёкий ложный трек физически не может вызвать экстренное торможение (`emergency = False`), но даёт заблаговременное оповещение на 150 м. + +#### 2.2. Динамический габарит по [`Corridor.sigma(d)`](../flyguard/geometry.py) +В [`geometry.py`](../flyguard/geometry.py) уже заложен расчет неопределенности оси: +$$\sigma(d) = base + rate \cdot d + 0.02 \cdot \max(d - d_{max\_seen}, 0)$$ +На 150 м $\sigma(d) \approx 1.1$ м. + +В функции `gauge_mask` в [`flyguard/lobula.py`](../flyguard/lobula.py): +$$W_{eff}(d) = half\_width + k_\sigma \cdot \sigma(d), \quad k_\sigma = 0.8$$ +* На 50 м: $W_{eff} = 1.6 + 0.8 \cdot 0.45 \approx 1.96$ м. +* На 150 м: $W_{eff} = 1.6 + 0.8 \cdot 1.10 \approx 2.48$ м. +* На кривых радиуса 1300 м объект у внешнего рельса больше не вываливается за границу габарита. Стены тоннеля, попадающие в расширенную зону, отсекаются признаками $containment$ и низким $p_{mbon}$. + +#### 2.3. Обратная связь [`FanBody`](../flyguard/fan_body.py) $\to$ [`find_candidates`](../flyguard/lobula.py) +[`FanBody`](../flyguard/fan_body.py) аккумулирует лучи в воксельной сетке пути $2.0 \times 0.2 \times 0.25$ м. +В [`find_candidates`](../flyguard/lobula.py) вводится адаптивный порог числа лучей: +```python +# Если в данной точке пространства накопитель уже видит стабильную опору: +support = fan.support_at(d_comp, u_comp) if fan is not None else 0.0 +need_rays = 2 if (d_comp >= 90.0 and support >= 0.6) else (3 if d_comp >= 90.0 else 4) +``` +* Снижает пропуски кандидатов на 120–160 м с 38 % до $< 15$ % для слабых эхо. + +--- + +### Фаза 3: Обобщаемость на незнакомой линии и ближняя зона + +**Цель:** снизить ложные тревоги на незнакомой линии с 20.3 до $<10$ треков/км и закрыть потерю объектов возле платформ на 30–50 м. + +#### 3.1. Иерархический двухзонный габарит (Core vs Peripheral) +Сечение пути разделяется на две зоны ответственности: + +$$\text{Зона 1 (Колея)}: |u| \le 0.90\,\text{м}, \quad h \in [0.16, 1.90]\,\text{м}$$ +$$\text{Зона 2 (Периферия)}: 0.90 < |u| \le 1.60\,\text{м} \quad \text{или} \quad h > 1.90\,\text{м}$$ + +1. **Для Зоны 1 (Колея)**: + * Нижний порог $h_{lo}$ снижается до **$0.16$ м** (захватывает лежащего человека). + * Множитель компактности в [`_quality`](../flyguard/central_complex.py) не штрафует протяженность $depth$ до $2.0$ м при малой высоте ($h \le 0.35$ м). + * Максимальный приоритет подтверждения трека ($gain = 0.40$). +2. **Для Зоны 2 (Периферия)**: + * Пространство кабельных лотков, сигналов и кронштейнов. + * Требование опоры на полотно: $grounded \le 0.3$ для висящих на стенах объектов подавляет их прирост улики. + * Для формирования трека требуется $p_{mbon} \ge 0.50$. + * **Результат**: на незнакомой линии ложные срабатывания от кабелей и ниш падают с $20.3$ до $< 10.0$ на км. + +#### 3.2. Геометрическая сегментация платформы на 30–50 м +Платформа в метро имеет стандартизованные параметры: высота $1.10 \pm 0.05$ м над рельсом, удаление $|u| \ge 1.35$ м. +В [`find_candidates`](../flyguard/lobula.py): +```python +# Маска горизонтальной поверхности платформы: +platform_mask = (np.abs(lat) > 1.30) & (tf.h >= 1.05) & (tf.h <= 1.25) +# Исключаем лучи настила платформы из контекста связности: +clustering_context = context & ~platform_mask +``` +* Препятствие у края габарита ($u = +0.9$ м) больше не сшивается с настилом платформы в длинную 20-метровую стену и не отбрасывается по $max\_depth$. + +--- + +### Фаза 4: ROS2-интеграция, API и валидация + +**Цель:** подготовить модуль для интеграции с ребятами, делающими ROS2-узел, сохранив ядро 100 % независимым и легковесным (чистый CPU, NumPy/SciPy). + +#### 4.1. Модуль экспорта для ROS2 ([`flyguard/export.py`](../flyguard/pipeline.py)) +Обеспечивает чистый интерфейс между ядром и ROS2-нодой: +* Преобразование треков [`Track`](../flyguard/central_complex.py) в 3D Bounding Boxes в координатах сенсора `(x, y, z, dx, dy, dz, yaw)`. +* Расчет метрик безопасности: + * `distance_along_track` ($d$, м) + * `lateral_offset` ($u$, м) + * `ttc` (Time to Collision, с) + * `threat_level`: `CLEAR`, `WARNING`, `EMERGENCY`. +* Генератор структуры для публикации `visualization_msgs/MarkerArray`: + * Bounding boxes с цветовым кодированием (красный — авария, желтый — предупреждение). + * 3D линия оси коридора тоннеля. + * Текстовые метки с расстоянием и скоростью сближения над объектами. + +#### 4.2. Контроль рантайма на CPU +* **Зависимости**: строго `numpy` и `scipy` в рантайме. +* **Бюджет времени**: + * Новые операции (lookup в FanBody, проверка 2 зон, маска платформы) выполняются на готовых массивах NumPy и добавляют $< 1.2$ мс. + * Ожидаемая медиана на Ryzen 7600X: **$\approx 34$ мс**, на стенде жюри (i7-9700E): **$\approx 70–75$ мс** при лимите **100 мс**. + +--- + +## 3. Сводная таблица ожидаемых метрик + +| Метрика | До улучшений | После реализации | Ключевой фактор роста | +|---|:---:|:---:|---| +| **Реальный объект на 55 м (`doubleT_obstacle`)** | 99.5 % | **> 99.5 %** | Сохранение надежности канала Giant Fiber | +| **P@150 м (стоящий человек)** | 0.33 | **> 0.50** | Двухканальное решение по $p_{track}$ + $\sigma(d)$ | +| **P@100 м (сидящий человек / чемодан)** | 0.34 / 0.21 | **> 0.55 / 0.40** | Порог 2 лучей при накоплении в [`FanBody`](../flyguard/fan_body.py) | +| **P@50 м (лежащий человек)** | 0.0 % (срезался) | **> 0.75** | $h_{lo} = 0.16$ м в колее + класс в каталоге | +| **Ложные тревоги (незнакомая линия)** | 20.3 на км | **< 10.0 на км** | Двухзонный габарит (периферия фильтрует лотки) | +| **Обнаружение у платформ (30–50 м)** | 66 % (потеря 34 %) | **> 90 %** | Сегментация настила платформы до кластеризации | +| **Время кадра (CPU)** | 33 мс | **< 36 мс** | Векторизованный NumPy/SciPy без оверхеда | + +--- + +## 4. Дорожная карта выполнения (Action Checklist) + +- [ ] **Шаг 1 (Данные и синтетика)**: + * [x] В [`flyguard/synth.py`](../flyguard/synth.py) добавлен класс `человек_лежа`. + * [x] В [`flyguard/central_complex.py`](../flyguard/central_complex.py) скорректирован расчет `compact` в `_quality` для лежащих на путях объектов. + * [ ] В [`tools/make_benchmark.py`](../tools/make_benchmark.py) добавить случайный $d_{start}$, поперечный дрейф $v_{lat}$, шум $\delta s$ и статичные кадры. + * [ ] Пересобрать кэш `track_set.npz` и переобучить [`TrackReadout`](../tools/train_track.py). +- [x] **Шаг 2 (Дальняя зона)**: + * [x] В [`flyguard/descending.py`](../flyguard/descending.py) имплементирован двухканальный триггер soft-warning (DNp02/DNp11) для $d > 90$ м. + * [x] В [`flyguard/lobula.py`](../flyguard/lobula.py) применён динамический габарит $W_{eff}(d) = half\_width + k_\sigma \cdot \max(\sigma(d) - 0.25, 0.0)$. + * [x] Оптимизированы параметры `warn_far = 0.35` и `leak_far = 0.06` для предотвращения вымывания улики на дальности. +- [x] **Шаг 3 (Зонирование и платформы)**: + * [x] Двухзонный габарит: колея ($|u| \le 0.85$ м, $h_{lo\_core} = 0.16$ м) и периферия ($h_{lo} = 0.28$ м). + * [x] Геометрическая фильтрация настила платформ ($|u| \ge 1.30$ м, $h \in [1.05, 1.25]$ м) для устранения слипания объектов со стенкой. +- [ ] **Шаг 4 (ROS2 и валидация)**: + * [x] Создан модуль экспорта [`flyguard/export.py`](../flyguard/export.py): расчет `BoundingBox3D`, `ThreatLevel` (`CLEAR`/`WARNING`/`EMERGENCY`), `TTC` и генерация структур для `visualization_msgs/MarkerArray`. + * [x] Добавлены и успешно пройдены 35 модульных тестов (`tests/test_pipeline.py`). + * [ ] Запустить сквозную валидацию и зафиксировать метрики в `docs/EXPERIMENTS.md`. diff --git a/requirements-gpu.txt b/requirements-gpu.txt new file mode 100644 index 0000000..ae36809 --- /dev/null +++ b/requirements-gpu.txt @@ -0,0 +1,10 @@ +# Зависимости для запуска FlyGuard с поддержкой NVIDIA GPU (CUDA 12.x / Ada Lovelace RTX 4070 Ti Super) +--extra-index-url https://download.pytorch.org/whl/cu121 +numpy>=1.24 +scipy>=1.10 +torch>=2.2.0 +torchvision>=0.17.0 +lightgbm>=4.0 +catboost>=1.2 +matplotlib>=3.7 +pytest>=7.0 diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..b9d89bf --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Автономный запуск тестов без внешних зависимостей (включая pytest).""" +import inspect +import math +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + + +class PytestMock: + class raises: + def __init__(self, exc_type, match=None): + self.exc_type = exc_type + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + raise AssertionError(f"Expected {self.exc_type} but nothing was raised") + return issubclass(exc_type, self.exc_type) + + @staticmethod + def approx(expected, rel=1e-6, abs=1e-6): + class Approx: + def __eq__(self, other): + return math.isclose(other, expected, rel_tol=rel, abs_tol=abs) + + def __repr__(self): + return f"approx({expected})" + + return Approx() + + class mark: + @staticmethod + def skipif(cond, reason=""): + def dec(f): + f.__skip__ = cond + return f + + return dec + + +sys.modules.setdefault("pytest", PytestMock()) + +import tests.test_pipeline as tp + + +def main(): + passed = failed = skipped = 0 + test_names = sorted([n for n in dir(tp) if n.startswith("test_")]) + for name in test_names: + fn = getattr(tp, name) + if getattr(fn, "__skip__", False): + print(f"SKIP {name}") + skipped += 1 + continue + try: + sig = inspect.signature(fn) + kwargs = {} + if "tmp_path" in sig.parameters: + with tempfile.TemporaryDirectory() as td: + kwargs["tmp_path"] = Path(td) + fn(**kwargs) + else: + fn() + print(f"PASS {name}") + passed += 1 + except Exception as e: + print(f"FAIL {name}: {e}") + failed += 1 + + print(f"\nИтог: {passed} passed, {failed} failed, {skipped} skipped") + if failed > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/flyguard_ros2_node.py b/tools/flyguard_ros2_node.py new file mode 100644 index 0000000..6f68801 --- /dev/null +++ b/tools/flyguard_ros2_node.py @@ -0,0 +1,216 @@ +""" +ROS 2 узел для биоинспирированного пайплайна FlyGuard (ЛЦТ-2026 Кейс 05). +Связывает входные данные 3D-лидара с ядром FlyGuard и публикует детекции/маркеры. +""" + +import sys +from pathlib import Path +import numpy as np + +# Автоматически добавляем корень репозитория в sys.path +ROOT_DIR = Path(__file__).resolve().parent.parent +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) + +import rclpy +from rclpy.node import Node + +# Импорт сообщений ROS 2 +from sensor_msgs.msg import PointCloud2 +from std_msgs.msg import String +from vision_msgs.msg import Detection3DArray, Detection3D, BoundingBox3D +from visualization_msgs.msg import MarkerArray, Marker +from geometry_msgs.msg import Vector3 + +import sensor_msgs_py.point_cloud2 as pc2 + +# Импорт ядра FlyGuard +try: + from flyguard.pipeline import FlyGuard, Params + from flyguard.mushroom_body import MushroomBody +except ImportError as e: + FlyGuard = None + Params = None + MushroomBody = None + + +class FlyGuardNode(Node): + def __init__(self): + super().__init__('flyguard_node') + + # --- Объявление параметров --- + self.declare_parameter('lidar_topic', '/pandar_points') + self.declare_parameter('frame_id', 'pandar') + self.declare_parameter('memory_path', '') + self.declare_parameter('fov_deg', 30.0) + + self.lidar_topic = self.get_parameter('lidar_topic').get_parameter_value().string_value + self.frame_id = self.get_parameter('frame_id').get_parameter_value().string_value + memory_path = self.get_parameter('memory_path').get_parameter_value().string_value + fov_deg = self.get_parameter('fov_deg').get_parameter_value().double_value + + # --- Инициализация ядра FlyGuard --- + if FlyGuard is not None: + self.get_logger().info("Инициализация ядра FlyGuard...") + memory = MushroomBody.load(memory_path) if memory_path else None + params = Params(fov_deg=fov_deg) + self.fg = FlyGuard(params, memory=memory) + else: + self.get_logger().error("Модуль flyguard.pipeline не найден!") + self.fg = None + + # --- Подписчики и Публикаторы --- + self.sub_cloud = self.create_subscription( + PointCloud2, + self.lidar_topic, + self.pointcloud_callback, + 10 + ) + + self.pub_threat = self.create_publisher(String, 'flyguard/threat_level', 10) + self.pub_boxes = self.create_publisher(Detection3DArray, 'flyguard/bounding_boxes', 10) + self.pub_markers = self.create_publisher(MarkerArray, 'flyguard/markers', 10) + + self.get_logger().info(f"FlyGuard ROS 2 Node запущен! Топик: {self.lidar_topic}") + + def pointcloud_callback(self, msg: PointCloud2): + """Обработка каждого кадра лидара (10 Гц)""" + start_time = self.get_clock().now() + + # 1. Читаем точки из PointCloud2 в structured numpy array + # (сохраняет стандартную структуру PointCloud) + pc_data = pc2.read_points(msg, skip_nans=True) + if pc_data.size == 0: + return + + # 2. Вызываем ядро FlyGuard + if self.fg is None: + return + + res = self.fg.process(pc_data) + if res is None: + return + + d = res.decision + tracks = getattr(self.fg.cx, 'tracks', []) + + # 3. Определяем уровень угрозы + if d.detected: + threat_level = "EMERGENCY" if getattr(d, 'emergency', False) else "WARNING" + else: + threat_level = "CLEAR" + + # 4. Публикация статуса + threat_msg = String() + threat_msg.data = threat_level + self.pub_threat.publish(threat_msg) + + # 5. Публикация маркеров и рамок + stamp = msg.header.stamp + self.publish_detections(tracks, stamp) + self.publish_rviz_markers(tracks, threat_level, d, stamp) + + elapsed_ms = (self.get_clock().now() - start_time).nanoseconds / 1e6 + self.get_logger().debug(f"Кадр за {elapsed_ms:.1f} мс | Статус: {threat_level} | Треков: {len(tracks)}") + + def publish_detections(self, tracks, stamp): + """Формирование vision_msgs/Detection3DArray""" + det_array = Detection3DArray() + det_array.header.stamp = stamp + det_array.header.frame_id = self.frame_id + + for trk in tracks: + det = Detection3D() + det.header = det_array.header + + bbox = BoundingBox3D() + bbox.center.position.x = float(getattr(trk, 'x', 0.0)) + bbox.center.position.y = float(getattr(trk, 'y', 0.0)) + bbox.center.position.z = float(getattr(trk, 'z', 0.0)) + + bbox.size.x = float(getattr(trk, 'dx', 0.5)) + bbox.size.y = float(getattr(trk, 'dy', 0.5)) + bbox.size.z = float(getattr(trk, 'dz', 1.5)) + + det.bbox = bbox + det_array.detections.append(det) + + self.pub_boxes.publish(det_array) + + def publish_rviz_markers(self, tracks, threat_level: str, decision, stamp): + """Генерация маркеров для RViz2""" + marker_array = MarkerArray() + + delete_all = Marker() + delete_all.action = Marker.DELETEALL + marker_array.markers.append(delete_all) + + for i, trk in enumerate(tracks): + # Куб вокруг объекта + box_marker = Marker() + box_marker.header.stamp = stamp + box_marker.header.frame_id = self.frame_id + box_marker.ns = "flyguard_boxes" + box_marker.id = i + box_marker.type = Marker.CUBE + box_marker.action = Marker.ADD + + x = float(getattr(trk, 'x', 0.0)) + y = float(getattr(trk, 'y', 0.0)) + z = float(getattr(trk, 'z', 0.0)) + + box_marker.pose.position.x = x + box_marker.pose.position.y = y + box_marker.pose.position.z = z + + dx = float(getattr(trk, 'dx', 0.5)) + dy = float(getattr(trk, 'dy', 0.5)) + dz = float(getattr(trk, 'dz', 1.5)) + box_marker.scale = Vector3(x=dx, y=dy, z=dz) + + if threat_level == "EMERGENCY": + box_marker.color.r, box_marker.color.g, box_marker.color.b, box_marker.color.a = 1.0, 0.0, 0.0, 0.6 + elif threat_level == "WARNING": + box_marker.color.r, box_marker.color.g, box_marker.color.b, box_marker.color.a = 1.0, 0.9, 0.0, 0.5 + else: + box_marker.color.r, box_marker.color.g, box_marker.color.b, box_marker.color.a = 0.0, 1.0, 0.0, 0.4 + + marker_array.markers.append(box_marker) + + # Текст с дистанцией над объектом + text_marker = Marker() + text_marker.header.stamp = stamp + text_marker.header.frame_id = self.frame_id + text_marker.ns = "flyguard_labels" + text_marker.id = 1000 + i + text_marker.type = Marker.TEXT_VIEW_FACING + text_marker.action = Marker.ADD + + text_marker.pose.position.x = x + text_marker.pose.position.y = y + text_marker.pose.position.z = z + dz / 2.0 + 0.3 + text_marker.scale.z = 0.4 + + dist = getattr(trk, 'distance', np.hypot(x, y)) + text_marker.text = f"D: {dist:.1f}m" + + text_marker.color.r, text_marker.color.g, text_marker.color.b, text_marker.color.a = 1.0, 1.0, 1.0, 1.0 + marker_array.markers.append(text_marker) + + self.pub_markers.publish(marker_array) + + +def main(args=None): + rclpy.init(args=args) + node = FlyGuardNode() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() \ No newline at end of file