Commit ·
96bba34
0
Parent(s):
feat: polish mobile responsive layout across all dashboard screens
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +27 -0
- .gitattributes +10 -0
- .gitignore +11 -0
- .python-version +1 -0
- Dockerfile +77 -0
- Dockerfile.backend +59 -0
- Makefile +85 -0
- README.es.md +359 -0
- README.md +25 -0
- configs/baseline.yaml +12 -0
- configs/compare.yaml +17 -0
- configs/eval.yaml +31 -0
- configs/lstm.yaml +24 -0
- configs/preprocessing.yaml +48 -0
- configs/serve.yaml +5 -0
- configs/transformer.yaml +27 -0
- configs/tune.yaml +44 -0
- configs/vae.yaml +25 -0
- data/.gitkeep +0 -0
- data/processed/anomalies.npy +3 -0
- data/processed/scaler.npz +3 -0
- data/processed/test.npy +3 -0
- data/processed/train.npy +3 -0
- data/processed/val.npy +3 -0
- docker-compose.yml +33 -0
- docs/REPORT.es.md +463 -0
- docs/REPORT.md +440 -0
- docs/assets/eda/flight_phases.png +3 -0
- docs/assets/eda/heading_polar.png +3 -0
- docs/assets/eda/nulls_per_column.png +3 -0
- docs/assets/eda/trajectories_map.png +3 -0
- docs/assets/img1.png +3 -0
- docs/assets/img2.png +3 -0
- docs/assets/img3.png +3 -0
- docs/assets/img4.png +3 -0
- docs/assets/img5.png +3 -0
- docs/assets/img6.png +3 -0
- docs/assets/logo.png +3 -0
- docs/assets/qr-placeholder.png +3 -0
- docs/assets/tower-day.png +3 -0
- docs/assets/tower-night.png +3 -0
- docs/medium_post.en.md +205 -0
- docs/medium_post.md +205 -0
- frontend/.dockerignore +8 -0
- frontend/.gitignore +5 -0
- frontend/Dockerfile +27 -0
- frontend/index.html +20 -0
- frontend/nginx.conf +31 -0
- frontend/package.json +28 -0
- frontend/pnpm-lock.yaml +1160 -0
.dockerignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.gitignore
|
| 3 |
+
|
| 4 |
+
.venv
|
| 5 |
+
__pycache__
|
| 6 |
+
*.py[cod]
|
| 7 |
+
*.egg-info
|
| 8 |
+
.ruff_cache
|
| 9 |
+
.ipynb_checkpoints
|
| 10 |
+
.pytest_cache
|
| 11 |
+
.mypy_cache
|
| 12 |
+
|
| 13 |
+
frontend/node_modules
|
| 14 |
+
frontend/dist
|
| 15 |
+
frontend/.vite
|
| 16 |
+
|
| 17 |
+
data/raw
|
| 18 |
+
mlruns
|
| 19 |
+
notebooks
|
| 20 |
+
|
| 21 |
+
.vscode
|
| 22 |
+
.idea
|
| 23 |
+
.DS_Store
|
| 24 |
+
|
| 25 |
+
Dockerfile*
|
| 26 |
+
docker-compose*.yml
|
| 27 |
+
.dockerignore
|
.gitattributes
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data/processed/*.npy filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
data/processed/*.npz filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
models/*.pt filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
.ipynb_checkpoints/
|
| 5 |
+
.ruff_cache/
|
| 6 |
+
*.egg-info/
|
| 7 |
+
/data/raw
|
| 8 |
+
/mlruns/
|
| 9 |
+
.DS_Store
|
| 10 |
+
.vscode/
|
| 11 |
+
.idea/
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11
|
Dockerfile
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS frontend
|
| 2 |
+
|
| 3 |
+
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
| 4 |
+
|
| 5 |
+
WORKDIR /web
|
| 6 |
+
|
| 7 |
+
COPY frontend/package.json frontend/pnpm-lock.yaml ./
|
| 8 |
+
RUN pnpm install --frozen-lockfile
|
| 9 |
+
|
| 10 |
+
COPY frontend/ ./
|
| 11 |
+
RUN pnpm build
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
FROM python:3.11-slim AS backend-builder
|
| 15 |
+
|
| 16 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 17 |
+
PYTHONUNBUFFERED=1 \
|
| 18 |
+
PIP_NO_CACHE_DIR=1 \
|
| 19 |
+
UV_LINK_MODE=copy \
|
| 20 |
+
UV_PROJECT_ENVIRONMENT=/opt/venv
|
| 21 |
+
|
| 22 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 23 |
+
build-essential \
|
| 24 |
+
curl \
|
| 25 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 26 |
+
|
| 27 |
+
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /usr/local/bin/uv
|
| 28 |
+
|
| 29 |
+
WORKDIR /app
|
| 30 |
+
|
| 31 |
+
COPY pyproject.toml uv.lock ./
|
| 32 |
+
COPY src ./src
|
| 33 |
+
|
| 34 |
+
RUN uv sync --frozen --no-dev
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
FROM python:3.11-slim AS runtime
|
| 38 |
+
|
| 39 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 40 |
+
PYTHONUNBUFFERED=1 \
|
| 41 |
+
PATH="/opt/venv/bin:$PATH" \
|
| 42 |
+
HOME=/home/user \
|
| 43 |
+
SADAR_PROCESSED_DIR=/app/data/processed \
|
| 44 |
+
SADAR_MODELS_DIR=/app/models \
|
| 45 |
+
SADAR_REPORTS_DIR=/app/reports \
|
| 46 |
+
SADAR_SERVE_CONFIG=/app/configs/serve.yaml \
|
| 47 |
+
SADAR_FRONTEND_DIR=/app/frontend/dist \
|
| 48 |
+
PORT=7860
|
| 49 |
+
|
| 50 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 51 |
+
libgomp1 \
|
| 52 |
+
curl \
|
| 53 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 54 |
+
&& useradd --create-home --uid 1000 --shell /bin/bash user
|
| 55 |
+
|
| 56 |
+
WORKDIR /app
|
| 57 |
+
|
| 58 |
+
COPY --from=backend-builder /opt/venv /opt/venv
|
| 59 |
+
COPY --from=backend-builder /app/src /app/src
|
| 60 |
+
COPY --from=frontend /web/dist /app/frontend/dist
|
| 61 |
+
|
| 62 |
+
COPY configs ./configs
|
| 63 |
+
COPY pyproject.toml ./
|
| 64 |
+
COPY models ./models
|
| 65 |
+
COPY reports ./reports
|
| 66 |
+
COPY data/processed/scaler.npz data/processed/test.npy data/processed/val.npy ./data/processed/
|
| 67 |
+
|
| 68 |
+
RUN chown -R user:user /app
|
| 69 |
+
|
| 70 |
+
USER user
|
| 71 |
+
|
| 72 |
+
EXPOSE 7860
|
| 73 |
+
|
| 74 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
| 75 |
+
CMD curl -fsS http://localhost:${PORT}/api/health || exit 1
|
| 76 |
+
|
| 77 |
+
CMD ["sh", "-c", "uvicorn sadar.serve.app:app --host 0.0.0.0 --port ${PORT}"]
|
Dockerfile.backend
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim AS builder
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1 \
|
| 5 |
+
PIP_NO_CACHE_DIR=1 \
|
| 6 |
+
UV_LINK_MODE=copy \
|
| 7 |
+
UV_PROJECT_ENVIRONMENT=/opt/venv
|
| 8 |
+
|
| 9 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
+
build-essential \
|
| 11 |
+
curl \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /usr/local/bin/uv
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
COPY pyproject.toml uv.lock ./
|
| 19 |
+
COPY src ./src
|
| 20 |
+
|
| 21 |
+
RUN uv sync --frozen --no-dev
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
FROM python:3.11-slim AS runtime
|
| 25 |
+
|
| 26 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 27 |
+
PYTHONUNBUFFERED=1 \
|
| 28 |
+
PATH="/opt/venv/bin:$PATH" \
|
| 29 |
+
SADAR_PROCESSED_DIR=/app/data/processed \
|
| 30 |
+
SADAR_MODELS_DIR=/app/models \
|
| 31 |
+
SADAR_REPORTS_DIR=/app/reports \
|
| 32 |
+
SADAR_SERVE_CONFIG=/app/configs/serve.yaml
|
| 33 |
+
|
| 34 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 35 |
+
libgomp1 \
|
| 36 |
+
curl \
|
| 37 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 38 |
+
&& groupadd --system --gid 1000 sadar \
|
| 39 |
+
&& useradd --system --uid 1000 --gid sadar --create-home sadar
|
| 40 |
+
|
| 41 |
+
WORKDIR /app
|
| 42 |
+
|
| 43 |
+
COPY --from=builder /opt/venv /opt/venv
|
| 44 |
+
COPY --from=builder /app/src /app/src
|
| 45 |
+
|
| 46 |
+
COPY configs ./configs
|
| 47 |
+
COPY pyproject.toml ./
|
| 48 |
+
|
| 49 |
+
RUN mkdir -p /app/data/processed /app/models /app/reports \
|
| 50 |
+
&& chown -R sadar:sadar /app
|
| 51 |
+
|
| 52 |
+
USER sadar
|
| 53 |
+
|
| 54 |
+
EXPOSE 8000
|
| 55 |
+
|
| 56 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
| 57 |
+
CMD curl -fsS http://localhost:8000/api/health || exit 1
|
| 58 |
+
|
| 59 |
+
CMD ["uvicorn", "sadar.serve.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
Makefile
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DEFAULT_GOAL := help
|
| 2 |
+
.PHONY: help install notebook preprocess baseline train-lstm train-transformer train-vae tune-vae evaluate compare serve web dev artifacts mlflow-ui lint clean docker-build docker-up docker-down docker-logs
|
| 3 |
+
|
| 4 |
+
PROCESSED_DIR ?= data/processed
|
| 5 |
+
MODELS_DIR ?= models
|
| 6 |
+
VAE_CHECKPOINT ?= $(MODELS_DIR)/vae_lstm.pt
|
| 7 |
+
SCALER ?= $(PROCESSED_DIR)/scaler.npz
|
| 8 |
+
|
| 9 |
+
help: ## Show this help
|
| 10 |
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
| 11 |
+
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
|
| 12 |
+
|
| 13 |
+
install: ## Create the Python environment with uv
|
| 14 |
+
uv sync
|
| 15 |
+
|
| 16 |
+
notebook: ## Open Jupyter for the notebooks (EDA, etc.)
|
| 17 |
+
uv run jupyter lab
|
| 18 |
+
|
| 19 |
+
preprocess: ## Run the preprocessing pipeline (raw -> tensors)
|
| 20 |
+
uv run sadar-preprocess
|
| 21 |
+
|
| 22 |
+
baseline: ## Fit and evaluate the Isolation Forest baseline
|
| 23 |
+
uv run sadar-baseline
|
| 24 |
+
|
| 25 |
+
train-lstm: ## Train the LSTM autoencoder on the normal flights
|
| 26 |
+
uv run sadar-train-lstm
|
| 27 |
+
|
| 28 |
+
train-transformer: ## Train the Transformer autoencoder on the normal flights
|
| 29 |
+
uv run sadar-train-transformer
|
| 30 |
+
|
| 31 |
+
train-vae: ## Train the VAE-LSTM on the normal flights
|
| 32 |
+
uv run sadar-train-vae
|
| 33 |
+
|
| 34 |
+
tune-vae: ## Search VAE-LSTM hyperparameters with Optuna
|
| 35 |
+
uv run sadar-tune-vae
|
| 36 |
+
|
| 37 |
+
evaluate: ## Evaluate a trained autoencoder against synthetic anomalies
|
| 38 |
+
uv run sadar-evaluate
|
| 39 |
+
|
| 40 |
+
compare: ## Compare every detector and select the final model
|
| 41 |
+
uv run sadar-compare
|
| 42 |
+
|
| 43 |
+
artifacts: ## Ensure the preprocessed tensors and the VAE checkpoint exist
|
| 44 |
+
@if [ ! -f "$(SCALER)" ]; then \
|
| 45 |
+
echo "[artifacts] $(SCALER) missing, running preprocess..."; \
|
| 46 |
+
$(MAKE) preprocess; \
|
| 47 |
+
fi
|
| 48 |
+
@if [ ! -f "$(VAE_CHECKPOINT)" ]; then \
|
| 49 |
+
echo "[artifacts] $(VAE_CHECKPOINT) missing, trying to restore from MLflow..."; \
|
| 50 |
+
uv run python -m sadar.models.restore || true; \
|
| 51 |
+
fi
|
| 52 |
+
@if [ ! -f "$(VAE_CHECKPOINT)" ]; then \
|
| 53 |
+
echo "[artifacts] still missing, training VAE-LSTM..."; \
|
| 54 |
+
$(MAKE) train-vae; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
serve: artifacts ## Run the inference API for the dashboard
|
| 58 |
+
uv run uvicorn sadar.serve.app:app --port 8000
|
| 59 |
+
|
| 60 |
+
web: ## Run the frontend dev server (Vite)
|
| 61 |
+
pnpm -C frontend dev
|
| 62 |
+
|
| 63 |
+
dev: artifacts ## Run the backend API and the frontend dev server together
|
| 64 |
+
@trap 'kill 0' EXIT; uv run uvicorn sadar.serve.app:app --port 8000 & pnpm -C frontend dev
|
| 65 |
+
|
| 66 |
+
mlflow-ui: ## Open the MLflow experiment tracking UI
|
| 67 |
+
uv run mlflow ui --backend-store-uri file:mlruns
|
| 68 |
+
|
| 69 |
+
lint: ## Python linter
|
| 70 |
+
uv run ruff check src
|
| 71 |
+
|
| 72 |
+
clean: ## Clean caches and artifacts
|
| 73 |
+
rm -rf .ruff_cache **/__pycache__
|
| 74 |
+
|
| 75 |
+
docker-build: ## Build the backend and frontend container images
|
| 76 |
+
docker compose build
|
| 77 |
+
|
| 78 |
+
docker-up: ## Start the stack in the background (backend on :8000, frontend on :5180)
|
| 79 |
+
docker compose up -d
|
| 80 |
+
|
| 81 |
+
docker-down: ## Stop the stack and remove the containers
|
| 82 |
+
docker compose down
|
| 83 |
+
|
| 84 |
+
docker-logs: ## Tail the logs of the running stack
|
| 85 |
+
docker compose logs -f
|
README.es.md
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
|
| 3 |
+
<img src="docs/assets/logo.png" alt="SADAR" width="220" />
|
| 4 |
+
|
| 5 |
+
<h1>SADAR</h1>
|
| 6 |
+
|
| 7 |
+
<p><b>Smart Anomaly Detection for Aviation Routes</b></p>
|
| 8 |
+
|
| 9 |
+
<p>Detección temprana de comportamiento de vuelo no conforme sobre Madrid-Barajas (LEMD).</p>
|
| 10 |
+
|
| 11 |
+
<p>
|
| 12 |
+
<img src="docs/assets/qr-placeholder.png" alt="QR" width="220" onerror="this.style.display='none'" />
|
| 13 |
+
</p>
|
| 14 |
+
|
| 15 |
+
<p>
|
| 16 |
+
<a href="README.md"><img src="https://img.shields.io/badge/English-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="English" /></a>
|
| 17 |
+
·
|
| 18 |
+
<a href="README.es.md"><img src="https://img.shields.io/badge/Espa%C3%B1ol-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="Español" /></a>
|
| 19 |
+
</p>
|
| 20 |
+
|
| 21 |
+
<p>
|
| 22 |
+
<img src="https://img.shields.io/badge/Python-3.11-0a1014?logo=python&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 23 |
+
<img src="https://img.shields.io/badge/PyTorch-2.x-0a1014?logo=pytorch&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 24 |
+
<img src="https://img.shields.io/badge/FastAPI-0.136-0a1014?logo=fastapi&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 25 |
+
<img src="https://img.shields.io/badge/React-18-0a1014?logo=react&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 26 |
+
<img src="https://img.shields.io/badge/Vite-5-0a1014?logo=vite&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 27 |
+
<img src="https://img.shields.io/badge/MLflow-3.x-0a1014?logo=mlflow&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 28 |
+
<img src="https://img.shields.io/badge/Optuna-4.x-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 29 |
+
<img src="https://img.shields.io/badge/uv-package%20mgr-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 30 |
+
<img src="https://img.shields.io/badge/Hugging%20Face-Spaces-0a1014?logo=huggingface&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 31 |
+
<img src="https://img.shields.io/badge/Docker-compose-0a1014?logo=docker&logoColor=7fd1c6&labelColor=0d1a1f" />
|
| 32 |
+
</p>
|
| 33 |
+
|
| 34 |
+
<p>
|
| 35 |
+
<a href="https://medium.com/@devrup404/cuando-el-modelo-m%C3%A1s-complejo-no-es-el-que-detecta-mejor-db64f1195ae9"><img src="https://img.shields.io/badge/Medium-Leer%20el%20art%C3%ADculo-0a1014?logo=medium&logoColor=7fd1c6&labelColor=0d1a1f" alt="Leer en Medium" /></a>
|
| 36 |
+
</p>
|
| 37 |
+
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
### Descripción del proyecto
|
| 43 |
+
|
| 44 |
+
**SADAR** (Smart Anomaly Detection for Aviation Routes) es un sistema de deep learning que aprende cómo es una aproximación o despegue **normal** en Madrid-Barajas (LEMD) y marca cualquier trayectoria que se desvíe del patrón aprendido: motores y al aire (go-arounds), rutas anómalas, pérdidas de transpondedor o maniobras compatibles con una emergencia.
|
| 45 |
+
|
| 46 |
+
Está construido sobre tres autoencoders profundos entrenados con unos 20.000 vuelos reales de ADS-B de la red OpenSky: un **autoencoder LSTM**, un **autoencoder Transformer** y un **VAE-LSTM**. Los tres se entrenan en igualdad de condiciones y se comparan cabeza a cabeza; el **VAE-LSTM** es el que se sirve en producción porque combina la mejor PR-AUC con la menor latencia de detección.
|
| 47 |
+
|
| 48 |
+
> **Alcance.** SADAR se entrena exclusivamente con operaciones normales de LEMD y funciona como monitor de conformidad de trayectorias: aprende el patrón normal y señala cualquier desviación para revisión humana. No es un predictor de eventos catastróficos.
|
| 49 |
+
|
| 50 |
+
### Lo destacado
|
| 51 |
+
|
| 52 |
+
- **Deep learning de una sola clase** sobre 7 variables dinámicas (posición, altitud, velocidad, seno/coseno del rumbo, régimen vertical).
|
| 53 |
+
- **Selección comparativa**: baseline Isolation Forest vs LSTM vs Transformer vs VAE-LSTM con el mismo preprocesado y las mismas métricas.
|
| 54 |
+
- **Banco de anomalías sintéticas** con cinco tipos de inyección (desviación de ruta, salto de altitud, anomalía de velocidad, holding, congelado de transpondedor) en varias intensidades, mide **PR-AUC** y **latencia de detección**.
|
| 55 |
+
- **Dashboard estilo torre de control** con React + Vite: radar en vivo, ranking de anomalías, simulador con sliders y página de métricas.
|
| 56 |
+
- **Totalmente reproducible**: `uv` para Python, `pnpm` para el front, configs YAML, semillas fijas.
|
| 57 |
+
|
| 58 |
+
### Stack
|
| 59 |
+
|
| 60 |
+
| Capa | Herramientas |
|
| 61 |
+
|---|---|
|
| 62 |
+
| Modelos | PyTorch · scikit-learn (baseline) · Optuna · MLflow |
|
| 63 |
+
| Datos | pandas · pyarrow · pyproj · NumPy |
|
| 64 |
+
| Backend | FastAPI · uvicorn · uv |
|
| 65 |
+
| Frontend | React 18 · Vite 5 · TypeScript · framer-motion |
|
| 66 |
+
| Despliegue | Docker · Hugging Face Spaces |
|
| 67 |
+
|
| 68 |
+
### Capturas de la demo
|
| 69 |
+
|
| 70 |
+
<details>
|
| 71 |
+
<summary><b>1. Consola torre - radar en directo sobre LEMD</b></summary>
|
| 72 |
+
|
| 73 |
+

|
| 74 |
+
|
| 75 |
+
Scope en tiempo real centrado en Barajas. Cada aeronave se colorea según su estado (en seguimiento / en alerta) y el panel lateral muestra todos los vuelos del espacio aéreo con su indicativo y su estado.
|
| 76 |
+
|
| 77 |
+
</details>
|
| 78 |
+
|
| 79 |
+
<details>
|
| 80 |
+
<summary><b>2. Simulador - inyecta una anomalía y observa la alerta</b></summary>
|
| 81 |
+
|
| 82 |
+

|
| 83 |
+
|
| 84 |
+
La estrella de la demo. Eliges un vuelo normal, seleccionas el tipo de anomalía (desviación de ruta, salto de altitud, velocidad, holding, corte de transpondedor), ajustas intensidad y momento, y la inyectas en vivo. El score de la derecha cruza el umbral y se reporta la latencia en segundos.
|
| 85 |
+
|
| 86 |
+
</details>
|
| 87 |
+
|
| 88 |
+
<details>
|
| 89 |
+
<summary><b>3. Métricas - comparación de modelos y detector elegido</b></summary>
|
| 90 |
+
|
| 91 |
+

|
| 92 |
+
|
| 93 |
+
Reporta la PR-AUC y el desempeño por anomalía de baseline, LSTM, Transformer y VAE-LSTM, además del motivo por el que se selecciona el **VAE-LSTM** como modelo de producción.
|
| 94 |
+
|
| 95 |
+
</details>
|
| 96 |
+
|
| 97 |
+
<details>
|
| 98 |
+
<summary><b>4. Métricas - desglose por tipo de anomalía</b></summary>
|
| 99 |
+
|
| 100 |
+

|
| 101 |
+
|
| 102 |
+
PR-AUC detallada por tipo de anomalía e intensidad. Sirve para justificar qué detector funciona mejor en cada modo de fallo.
|
| 103 |
+
|
| 104 |
+
</details>
|
| 105 |
+
|
| 106 |
+
<details>
|
| 107 |
+
<summary><b>5. Consola torre - instantánea completa del tráfico de LEMD</b></summary>
|
| 108 |
+
|
| 109 |
+

|
| 110 |
+
|
| 111 |
+
Vista más amplia del radar con toda la flota visible, bloque de datos del vuelo y, abajo a la derecha, el histograma en vivo del anomaly score.
|
| 112 |
+
|
| 113 |
+
</details>
|
| 114 |
+
|
| 115 |
+
<details>
|
| 116 |
+
<summary><b>6. Consola torre - alerta levantada sobre un vuelo</b></summary>
|
| 117 |
+
|
| 118 |
+

|
| 119 |
+
|
| 120 |
+
Una aeronave marcada se pone en rojo y el controlador recibe el score, el umbral y la clasificación sugerida en el panel derecho.
|
| 121 |
+
|
| 122 |
+
</details>
|
| 123 |
+
|
| 124 |
+
### Arquitectura
|
| 125 |
+
|
| 126 |
+
```
|
| 127 |
+
┌──────────────────┐ ┌──────────────────┐
|
| 128 |
+
│ Parquet ADS-B │ │ Banco anomalías │
|
| 129 |
+
│ (OpenSky / LEMD)│ │ sintéticas │
|
| 130 |
+
└────────┬─────────┘ └────────┬─────────┘
|
| 131 |
+
│ │
|
| 132 |
+
┌────────▼────────┐ │
|
| 133 |
+
│ Preprocesado │ │
|
| 134 |
+
│ · limpieza │ │
|
| 135 |
+
│ · resample 10s │ │
|
| 136 |
+
│ · XY relativos │ │
|
| 137 |
+
│ · sin/cos rumbo │ │
|
| 138 |
+
│ · estandarizado │ │
|
| 139 |
+
└────────┬────────┘ │
|
| 140 |
+
│ │
|
| 141 |
+
┌────────▼────────────────────────────▼────────┐
|
| 142 |
+
│ Entrenamiento (solo vuelos normales) │
|
| 143 |
+
│ ┌──────────┐ ┌─────────────┐ ┌────────────┐ │
|
| 144 |
+
│ │ Isolation│ │ LSTM-AE │ │ Transformer│ │
|
| 145 |
+
│ │ Forest │ │ │ │ AE │ │
|
| 146 |
+
│ └──────────┘ └─────────────┘ └────────────┘ │
|
| 147 |
+
│ ┌─────────────────┐ │
|
| 148 |
+
│ │ VAE-LSTM ★ │ ← elegido │
|
| 149 |
+
│ └─────────────────┘ │
|
| 150 |
+
└────────┬─────────────────────────────────────┘
|
| 151 |
+
│ checkpoint + scaler + umbral
|
| 152 |
+
┌────────▼─────────┐
|
| 153 |
+
│ Servicio FastAPI│ /api/health · /api/scene
|
| 154 |
+
│ (uvicorn) │ /api/flights · /api/metrics
|
| 155 |
+
└────────┬─────────┘ /api/simulate
|
| 156 |
+
│
|
| 157 |
+
┌────────▼─────────┐
|
| 158 |
+
│ React + Vite │ Radar · Simulador · Métricas
|
| 159 |
+
│ dashboard │
|
| 160 |
+
└──────────────────┘
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### Ejecutarlo en local
|
| 164 |
+
|
| 165 |
+
Requisitos: **Python 3.11**, [`uv`](https://docs.astral.sh/uv/) para las deps de Python, **Node 20+** y [`pnpm`](https://pnpm.io/) para el frontend.
|
| 166 |
+
|
| 167 |
+
Dashboard en `http://localhost:5180`, API en `http://localhost:8000`.
|
| 168 |
+
|
| 169 |
+
<details>
|
| 170 |
+
<summary><b>macOS / Linux - con Makefile (recomendado)</b></summary>
|
| 171 |
+
|
| 172 |
+
```bash
|
| 173 |
+
uv sync
|
| 174 |
+
pnpm -C frontend install
|
| 175 |
+
make dev
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
`make dev` arranca el backend FastAPI y el frontend Vite juntos con hot reload.
|
| 179 |
+
|
| 180 |
+
</details>
|
| 181 |
+
|
| 182 |
+
<details>
|
| 183 |
+
<summary><b>macOS / Linux - sin Makefile</b></summary>
|
| 184 |
+
|
| 185 |
+
Dos terminales:
|
| 186 |
+
|
| 187 |
+
```bash
|
| 188 |
+
# terminal 1: backend
|
| 189 |
+
uv sync
|
| 190 |
+
uv run uvicorn sadar.serve.app:app --port 8000
|
| 191 |
+
|
| 192 |
+
# terminal 2: frontend
|
| 193 |
+
pnpm -C frontend install
|
| 194 |
+
pnpm -C frontend dev
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
</details>
|
| 198 |
+
|
| 199 |
+
<details>
|
| 200 |
+
<summary><b>Windows (PowerShell)</b></summary>
|
| 201 |
+
|
| 202 |
+
PowerShell no trae `make`; arranca los dos servicios en dos terminales.
|
| 203 |
+
|
| 204 |
+
```powershell
|
| 205 |
+
# instalación una sola vez
|
| 206 |
+
uv sync
|
| 207 |
+
pnpm -C frontend install
|
| 208 |
+
|
| 209 |
+
# terminal 1: backend
|
| 210 |
+
uv run uvicorn sadar.serve.app:app --port 8000
|
| 211 |
+
|
| 212 |
+
# terminal 2: frontend
|
| 213 |
+
pnpm -C frontend dev
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
Si no tienes `pnpm`: `npm install -g pnpm`. Si no tienes `uv`: `winget install astral-sh.uv`.
|
| 217 |
+
|
| 218 |
+
</details>
|
| 219 |
+
|
| 220 |
+
<details>
|
| 221 |
+
<summary><b>Windows con Make (Chocolatey / scoop / WSL)</b></summary>
|
| 222 |
+
|
| 223 |
+
```powershell
|
| 224 |
+
choco install make # o: scoop install make
|
| 225 |
+
make dev
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
O ejecuta todo dentro de WSL Ubuntu y sigue las instrucciones de macOS / Linux.
|
| 229 |
+
|
| 230 |
+
</details>
|
| 231 |
+
|
| 232 |
+
### Ejecutarlo con Docker
|
| 233 |
+
|
| 234 |
+
Igual en cualquier sistema operativo, solo necesitas Docker Desktop / Docker Engine.
|
| 235 |
+
|
| 236 |
+
<details>
|
| 237 |
+
<summary><b>Un contenedor con Docker Compose</b></summary>
|
| 238 |
+
|
| 239 |
+
```bash
|
| 240 |
+
docker compose up --build
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
Abre `http://localhost:5180`. Modelos, tensores preprocesados e informes se montan en sólo lectura desde `./models`, `./data/processed` y `./reports`.
|
| 244 |
+
|
| 245 |
+
</details>
|
| 246 |
+
|
| 247 |
+
<details>
|
| 248 |
+
<summary><b>Imagen única (la misma que en producción / Hugging Face Spaces)</b></summary>
|
| 249 |
+
|
| 250 |
+
```bash
|
| 251 |
+
docker build -t sadar .
|
| 252 |
+
docker run --rm -p 7860:7860 sadar
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
Abre `http://localhost:7860`. Los modelos, tensores e informes van dentro de la imagen; no necesitas volúmenes.
|
| 256 |
+
|
| 257 |
+
</details>
|
| 258 |
+
|
| 259 |
+
### Desplegar en Hugging Face Spaces
|
| 260 |
+
|
| 261 |
+
El repositorio trae un [Dockerfile](Dockerfile) listo para HF Spaces (una sola imagen, el frontend servido por FastAPI en el puerto 7860).
|
| 262 |
+
|
| 263 |
+
```bash
|
| 264 |
+
git lfs install
|
| 265 |
+
git clone https://huggingface.co/spaces/<tu-usuario>/sadar
|
| 266 |
+
cd sadar
|
| 267 |
+
# copia los archivos del repositorio SADAR dentro
|
| 268 |
+
git lfs track "*.npy" "*.npz" "*.pt"
|
| 269 |
+
git add . && git commit -m "feat: initial deploy" && git push
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
### Estructura del repositorio
|
| 273 |
+
|
| 274 |
+
```
|
| 275 |
+
SADAR/
|
| 276 |
+
├── src/sadar/ Paquete Python (importable como `sadar`)
|
| 277 |
+
│ ├── data/ Pipeline de datos (parquet crudo → tensores)
|
| 278 |
+
│ │ ├── pipeline.py Punto de entrada: `sadar-preprocess`
|
| 279 |
+
│ │ ├── io.py Carga de parquet, dedupe, manifests
|
| 280 |
+
│ │ ├── cleaning.py Nulos, huecos, eliminación de go-arounds
|
| 281 |
+
│ │ ├── features.py XY relativo a pista, sin/cos del rumbo
|
| 282 |
+
│ │ ├── scaling.py Estandarización por feature (fit solo en train)
|
| 283 |
+
│ │ ├── splitting.py Split sin leak por flight_id y fecha
|
| 284 |
+
│ │ └── windowing.py Ventanas de longitud fija
|
| 285 |
+
│ ├── models/ Modelos y entrenadores
|
| 286 |
+
│ │ ├── baseline.py Baseline Isolation Forest
|
| 287 |
+
│ │ ├── lstm_autoencoder.py Arquitectura LSTM-AE
|
| 288 |
+
│ │ ├── transformer_autoencoder.py Arquitectura Transformer-AE
|
| 289 |
+
│ │ ├── vae_lstm.py Arquitectura VAE-LSTM (elegida)
|
| 290 |
+
│ │ ├── training.py Loop común, device, checkpoint I/O
|
| 291 |
+
│ │ ├── train_lstm.py Entrada: `sadar-train-lstm`
|
| 292 |
+
│ │ ├── train_transformer.py Entrada: `sadar-train-transformer`
|
| 293 |
+
│ │ ├── train_vae.py Entrada: `sadar-train-vae`
|
| 294 |
+
│ │ └── tune_vae.py Búsqueda Optuna: `sadar-tune-vae`
|
| 295 |
+
│ ├── eval/ Evaluación y banco sintético
|
| 296 |
+
│ │ ├── synthetic.py Inyección de anomalías (5 tipos, varias intensidades)
|
| 297 |
+
│ │ ├── evaluate.py Evaluación por modelo: `sadar-evaluate`
|
| 298 |
+
│ │ └── compare.py Informe comparativo: `sadar-compare`
|
| 299 |
+
│ └── serve/ Inferencia en producción
|
| 300 |
+
│ ├── app.py App FastAPI (también sirve la SPA en prod)
|
| 301 |
+
│ └── inference.py ConformanceService (carga modelo + scaler)
|
| 302 |
+
│
|
| 303 |
+
├── frontend/ Dashboard React + Vite (pnpm)
|
| 304 |
+
│ ├── src/
|
| 305 |
+
│ │ ├── pages/ Dashboard, Simulador, Métricas, Presentación
|
| 306 |
+
│ │ ├── components/ RadarScope, paneles, gráficas
|
| 307 |
+
│ │ ├── api.ts Cliente tipado para /api/*
|
| 308 |
+
│ │ └── i18n.tsx Traducciones EN / ES
|
| 309 |
+
│ ├── public/ Logo y estáticos
|
| 310 |
+
│ ├── vite.config.ts Proxy de dev /api → :8000
|
| 311 |
+
│ └── package.json
|
| 312 |
+
│
|
| 313 |
+
├── configs/ Configuración YAML
|
| 314 |
+
│ ├── preprocessing.yaml
|
| 315 |
+
│ ├── baseline.yaml
|
| 316 |
+
│ ├── lstm.yaml · transformer.yaml · vae.yaml
|
| 317 |
+
│ ├── tune.yaml · eval.yaml · compare.yaml
|
| 318 |
+
│ └── serve.yaml Selecciona el checkpoint de producción
|
| 319 |
+
│
|
| 320 |
+
├── models/ Checkpoints entrenados (Git LFS)
|
| 321 |
+
│ ├── lstm_autoencoder.pt
|
| 322 |
+
│ ├── transformer_autoencoder.pt
|
| 323 |
+
│ └── vae_lstm.pt ★ servido en producción
|
| 324 |
+
│
|
| 325 |
+
├── data/
|
| 326 |
+
│ ├── raw/ Parquet ADS-B (fuera del repositorio, $SADAR_DATA_DIR)
|
| 327 |
+
│ └── processed/ Tensores estandarizados (Git LFS)
|
| 328 |
+
│ ├── scaler.npz
|
| 329 |
+
│ ├── train.npy · val.npy · test.npy
|
| 330 |
+
│ └── anomalies.npy
|
| 331 |
+
│
|
| 332 |
+
├── reports/ Métricas generadas
|
| 333 |
+
│ ├── model_comparison.json
|
| 334 |
+
│ └── vae_tuning.json
|
| 335 |
+
│
|
| 336 |
+
├── notebooks/ EDA
|
| 337 |
+
├── docs/assets/ Capturas y logo del README
|
| 338 |
+
│
|
| 339 |
+
├── Dockerfile Imagen de producción (HF Spaces, un contenedor)
|
| 340 |
+
├── docker-compose.yml Stack local de dos contenedores
|
| 341 |
+
├── pyproject.toml · uv.lock Deps Python gestionadas con uv
|
| 342 |
+
├── Makefile `make dev`, `make preprocess`, ...
|
| 343 |
+
└── README.md · README.es.md Este archivo (EN / ES)
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
### Limitaciones
|
| 347 |
+
|
| 348 |
+
- El dataset no contiene incidentes reales catastróficos, SADAR valida una metodología, no eventos históricos.
|
| 349 |
+
- No se dispone de planes de vuelo, la "ruta prevista" se aproxima como "patrón normal aprendido".
|
| 350 |
+
- Los huecos de transpondedor son mayoritariamente cobertura del receptor, se usan como característica, no como etiqueta.
|
| 351 |
+
- Solo hay cuatro vuelos reales con squawk de emergencia, se usan para validar, jamás para entrenar.
|
| 352 |
+
|
| 353 |
+
---
|
| 354 |
+
|
| 355 |
+
<div align="center">
|
| 356 |
+
<sub><b>SADAR</b> · Smart Anomaly Detection for Aviation Routes</sub>
|
| 357 |
+
<br />
|
| 358 |
+
<sub>Monitorización de conformidad de vuelos con deep learning sobre Madrid-Barajas (LEMD) · 2026</sub>
|
| 359 |
+
</div>
|
README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SADAR Flight Conformance Monitor
|
| 3 |
+
emoji: 🛬
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: Anomaly detection on ADS-B trajectories at Madrid LEMD.
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# SADAR — Hugging Face Space card
|
| 14 |
+
|
| 15 |
+
This file is the README used **only** when the project is deployed to a Hugging Face
|
| 16 |
+
Space. The YAML front-matter at the top is the Space card that Hugging Face needs to
|
| 17 |
+
configure the container (SDK, port, title, colors, license).
|
| 18 |
+
|
| 19 |
+
When deploying to Hugging Face Spaces, copy this file to the repository root as
|
| 20 |
+
`README.md` (overwriting the GitHub one) before pushing to the Space remote. The GitHub
|
| 21 |
+
`README.md` does not include this front-matter on purpose, so it renders cleanly on
|
| 22 |
+
github.com.
|
| 23 |
+
|
| 24 |
+
For the project description, screenshots, architecture and run instructions, see the
|
| 25 |
+
main `README.md` at the repository root.
|
configs/baseline.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 5 |
+
|
| 6 |
+
isolation_forest:
|
| 7 |
+
n_estimators: 200
|
| 8 |
+
max_samples: auto
|
| 9 |
+
contamination: auto
|
| 10 |
+
|
| 11 |
+
threshold:
|
| 12 |
+
val_percentile: 99.0
|
configs/compare.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
eval_config: configs/eval.yaml
|
| 2 |
+
|
| 3 |
+
models:
|
| 4 |
+
- name: LSTM
|
| 5 |
+
checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/lstm_autoencoder.pt
|
| 6 |
+
- name: Transformer
|
| 7 |
+
checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/transformer_autoencoder.pt
|
| 8 |
+
- name: VAE-LSTM
|
| 9 |
+
checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/vae_lstm.pt
|
| 10 |
+
|
| 11 |
+
ensemble:
|
| 12 |
+
enabled: true
|
| 13 |
+
reducers: [mean, max]
|
| 14 |
+
|
| 15 |
+
output:
|
| 16 |
+
path: ${oc.env:SADAR_REPORTS_DIR,reports}/model_comparison.json
|
| 17 |
+
curves_dir: ${oc.env:SADAR_REPORTS_DIR,reports}/figures
|
configs/eval.yaml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 5 |
+
|
| 6 |
+
preprocessing_config: configs/preprocessing.yaml
|
| 7 |
+
baseline_config: configs/baseline.yaml
|
| 8 |
+
|
| 9 |
+
model:
|
| 10 |
+
checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/lstm_autoencoder.pt
|
| 11 |
+
device: auto
|
| 12 |
+
|
| 13 |
+
synthetic:
|
| 14 |
+
sample_size: 2000
|
| 15 |
+
onset_fraction: 0.5
|
| 16 |
+
route_deviation:
|
| 17 |
+
magnitudes_m: [20000, 40000, 80000]
|
| 18 |
+
altitude:
|
| 19 |
+
magnitudes_m: [300, 800, 1500]
|
| 20 |
+
speed:
|
| 21 |
+
factors: [1.6, 2.2, 0.4]
|
| 22 |
+
holding:
|
| 23 |
+
turn_periods_s: [240, 120]
|
| 24 |
+
freeze:
|
| 25 |
+
enabled: true
|
| 26 |
+
|
| 27 |
+
threshold:
|
| 28 |
+
val_percentile: 99.0
|
| 29 |
+
|
| 30 |
+
latency:
|
| 31 |
+
normal_step_percentile: 99.0
|
configs/lstm.yaml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 5 |
+
|
| 6 |
+
model:
|
| 7 |
+
hidden_size: 64
|
| 8 |
+
latent_size: 16
|
| 9 |
+
num_layers: 1
|
| 10 |
+
dropout: 0.1
|
| 11 |
+
|
| 12 |
+
training:
|
| 13 |
+
epochs: 50
|
| 14 |
+
batch_size: 256
|
| 15 |
+
learning_rate: 0.001
|
| 16 |
+
weight_decay: 0.0001
|
| 17 |
+
patience: 7
|
| 18 |
+
lr_factor: 0.5
|
| 19 |
+
lr_patience: 3
|
| 20 |
+
device: auto
|
| 21 |
+
|
| 22 |
+
output:
|
| 23 |
+
dir: ${oc.env:SADAR_MODELS_DIR,models}
|
| 24 |
+
checkpoint: lstm_autoencoder.pt
|
configs/preprocessing.yaml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
raw_dir: ${oc.env:SADAR_DATA_DIR,data/raw}
|
| 5 |
+
file_glob: lemd_*.parquet
|
| 6 |
+
airport: LEMD
|
| 7 |
+
|
| 8 |
+
runway:
|
| 9 |
+
ref_lat: 40.4936
|
| 10 |
+
ref_lon: -3.5668
|
| 11 |
+
projection_epsg: 32630
|
| 12 |
+
|
| 13 |
+
resample:
|
| 14 |
+
seconds: 10
|
| 15 |
+
|
| 16 |
+
cleaning:
|
| 17 |
+
min_points_per_flight: 40
|
| 18 |
+
max_gap_seconds: 120
|
| 19 |
+
max_missing_fraction: 0.5
|
| 20 |
+
remove_go_arounds: true
|
| 21 |
+
remove_emergency_squawks: true
|
| 22 |
+
emergency_squawks: [7500, 7600, 7700]
|
| 23 |
+
interpolate_columns: [lat, lon, baroaltitude, velocity, heading, vertrate]
|
| 24 |
+
go_around:
|
| 25 |
+
near_runway_m: 5000
|
| 26 |
+
descend_vertrate: -1.0
|
| 27 |
+
climb_vertrate: 2.0
|
| 28 |
+
min_climb_points: 3
|
| 29 |
+
|
| 30 |
+
features:
|
| 31 |
+
columns: [x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]
|
| 32 |
+
altitude_source: baroaltitude
|
| 33 |
+
|
| 34 |
+
windowing:
|
| 35 |
+
length: 60
|
| 36 |
+
stride: 30
|
| 37 |
+
|
| 38 |
+
split:
|
| 39 |
+
train_years: [2017, 2018, 2019]
|
| 40 |
+
eval_years: [2020]
|
| 41 |
+
val_fraction: 0.5
|
| 42 |
+
|
| 43 |
+
scaling:
|
| 44 |
+
method: standard
|
| 45 |
+
|
| 46 |
+
output:
|
| 47 |
+
dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 48 |
+
format: npy
|
configs/serve.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
eval_config: configs/eval.yaml
|
| 2 |
+
|
| 3 |
+
checkpoint: ${oc.env:SADAR_MODELS_DIR,models}/vae_lstm.pt
|
| 4 |
+
|
| 5 |
+
metrics: ${oc.env:SADAR_REPORTS_DIR,reports}/model_comparison.json
|
configs/transformer.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 5 |
+
|
| 6 |
+
model:
|
| 7 |
+
d_model: 64
|
| 8 |
+
nhead: 4
|
| 9 |
+
num_layers: 2
|
| 10 |
+
dim_feedforward: 128
|
| 11 |
+
latent_size: 16
|
| 12 |
+
dropout: 0.1
|
| 13 |
+
max_len: 256
|
| 14 |
+
|
| 15 |
+
training:
|
| 16 |
+
epochs: 50
|
| 17 |
+
batch_size: 256
|
| 18 |
+
learning_rate: 0.001
|
| 19 |
+
weight_decay: 0.0001
|
| 20 |
+
patience: 7
|
| 21 |
+
lr_factor: 0.5
|
| 22 |
+
lr_patience: 3
|
| 23 |
+
device: auto
|
| 24 |
+
|
| 25 |
+
output:
|
| 26 |
+
dir: ${oc.env:SADAR_MODELS_DIR,models}
|
| 27 |
+
checkpoint: transformer_autoencoder.pt
|
configs/tune.yaml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 5 |
+
|
| 6 |
+
preprocessing_config: configs/preprocessing.yaml
|
| 7 |
+
|
| 8 |
+
n_trials: 20
|
| 9 |
+
|
| 10 |
+
search_space:
|
| 11 |
+
hidden_size: [32, 64, 128]
|
| 12 |
+
latent_size: [8, 16, 32]
|
| 13 |
+
num_layers: [1, 2]
|
| 14 |
+
dropout: [0.0, 0.3]
|
| 15 |
+
beta: [0.0001, 0.1]
|
| 16 |
+
learning_rate: [0.0003, 0.003]
|
| 17 |
+
weight_decay: [0.000001, 0.001]
|
| 18 |
+
|
| 19 |
+
synthetic:
|
| 20 |
+
sample_size: 1500
|
| 21 |
+
onset_fraction: 0.5
|
| 22 |
+
route_deviation:
|
| 23 |
+
magnitudes_m: [20000, 40000, 80000]
|
| 24 |
+
altitude:
|
| 25 |
+
magnitudes_m: [300, 800, 1500]
|
| 26 |
+
speed:
|
| 27 |
+
factors: [1.6, 2.2, 0.4]
|
| 28 |
+
holding:
|
| 29 |
+
turn_periods_s: [240, 120]
|
| 30 |
+
freeze:
|
| 31 |
+
enabled: true
|
| 32 |
+
|
| 33 |
+
training:
|
| 34 |
+
epochs: 30
|
| 35 |
+
batch_size: 256
|
| 36 |
+
learning_rate: 0.001
|
| 37 |
+
weight_decay: 0.0001
|
| 38 |
+
patience: 6
|
| 39 |
+
lr_factor: 0.5
|
| 40 |
+
lr_patience: 3
|
| 41 |
+
device: auto
|
| 42 |
+
|
| 43 |
+
output:
|
| 44 |
+
path: ${oc.env:SADAR_REPORTS_DIR,reports}/vae_tuning.json
|
configs/vae.yaml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
|
| 3 |
+
data:
|
| 4 |
+
processed_dir: ${oc.env:SADAR_PROCESSED_DIR,data/processed}
|
| 5 |
+
|
| 6 |
+
model:
|
| 7 |
+
hidden_size: 64
|
| 8 |
+
latent_size: 8
|
| 9 |
+
num_layers: 1
|
| 10 |
+
dropout: 0.1488
|
| 11 |
+
beta: 0.0001193
|
| 12 |
+
|
| 13 |
+
training:
|
| 14 |
+
epochs: 50
|
| 15 |
+
batch_size: 256
|
| 16 |
+
learning_rate: 0.001436
|
| 17 |
+
weight_decay: 0.0000054741
|
| 18 |
+
patience: 7
|
| 19 |
+
lr_factor: 0.5
|
| 20 |
+
lr_patience: 3
|
| 21 |
+
device: auto
|
| 22 |
+
|
| 23 |
+
output:
|
| 24 |
+
dir: ${oc.env:SADAR_MODELS_DIR,models}
|
| 25 |
+
checkpoint: vae_lstm.pt
|
data/.gitkeep
ADDED
|
File without changes
|
data/processed/anomalies.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bc9b10f15b6b20da2d6c3ae51336d15f68e92d60802a0539d4ae2b27a5ea5171
|
| 3 |
+
size 1807808
|
data/processed/scaler.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:50bb65a11f9b41d2d6673b47fd83803cd847f19a5bde5860c05ba271e2891db4
|
| 3 |
+
size 556
|
data/processed/test.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1b66afdc544b8a34762131901244c30f7702983be692c321959638d6db9ffb66
|
| 3 |
+
size 13083968
|
data/processed/train.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ac25b59731b49c1dc19180684a35198074f7e65fdc6cadd9149f42ab2902429
|
| 3 |
+
size 102493568
|
data/processed/val.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8fc07743435ec2facb7c32649180b1d30d4d30892dbd933b75ce598b63f822a7
|
| 3 |
+
size 12900848
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: sadar
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
backend:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: Dockerfile.backend
|
| 8 |
+
image: sadar-backend:latest
|
| 9 |
+
container_name: sadar-backend
|
| 10 |
+
environment:
|
| 11 |
+
SADAR_PROCESSED_DIR: /app/data/processed
|
| 12 |
+
SADAR_MODELS_DIR: /app/models
|
| 13 |
+
SADAR_REPORTS_DIR: /app/reports
|
| 14 |
+
SADAR_SERVE_CONFIG: /app/configs/serve.yaml
|
| 15 |
+
volumes:
|
| 16 |
+
- ./data/processed:/app/data/processed:ro
|
| 17 |
+
- ./models:/app/models:ro
|
| 18 |
+
- ./reports:/app/reports:ro
|
| 19 |
+
ports:
|
| 20 |
+
- "8000:8000"
|
| 21 |
+
restart: unless-stopped
|
| 22 |
+
|
| 23 |
+
frontend:
|
| 24 |
+
build:
|
| 25 |
+
context: ./frontend
|
| 26 |
+
dockerfile: Dockerfile
|
| 27 |
+
image: sadar-frontend:latest
|
| 28 |
+
container_name: sadar-frontend
|
| 29 |
+
depends_on:
|
| 30 |
+
- backend
|
| 31 |
+
ports:
|
| 32 |
+
- "5180:80"
|
| 33 |
+
restart: unless-stopped
|
docs/REPORT.es.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SADAR
|
| 2 |
+
|
| 3 |
+
**Smart Anomaly Detection for Aviation Routes**
|
| 4 |
+
|
| 5 |
+
Memoria técnica
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
**Proyecto:** SADAR · Flight Conformance Monitor
|
| 10 |
+
**Dominio:** Vigilancia de tráfico aéreo · datos ADS-B
|
| 11 |
+
**Aeropuerto de estudio:** Madrid-Barajas (LEMD)
|
| 12 |
+
**Enfoque:** Aprendizaje profundo de una sola clase para detección de anomalías en trayectorias
|
| 13 |
+
**Fecha:** 2026
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Resumen ejecutivo
|
| 18 |
+
|
| 19 |
+
SADAR es un sistema de aprendizaje profundo que aprende el patrón normal de las
|
| 20 |
+
operaciones de aproximación y despegue en Madrid-Barajas (LEMD) a partir de datos de
|
| 21 |
+
vigilancia ADS-B y señala en tiempo real cualquier trayectoria que se aparte de ese
|
| 22 |
+
patrón. El sistema está concebido como **monitor de conformidad de vuelos**: no clasifica
|
| 23 |
+
eventos concretos, asigna a cada vuelo del espacio aéreo una puntuación interpretable de
|
| 24 |
+
anomalía para que un operador pueda revisar los casos que quedan fuera de la
|
| 25 |
+
distribución aprendida.
|
| 26 |
+
|
| 27 |
+
Se entrenan tres autoencoders secuenciales en igualdad de condiciones sobre unos 20.000
|
| 28 |
+
vuelos reales obtenidos de la red OpenSky (un autoencoder LSTM, un autoencoder
|
| 29 |
+
Transformer y un VAE-LSTM), junto con un baseline no secuencial basado en Isolation
|
| 30 |
+
Forest. Los cuatro detectores se evalúan sobre el mismo conjunto de test y sobre un banco
|
| 31 |
+
sintético con cinco tipos de inyección y varias intensidades. Se selecciona el
|
| 32 |
+
**VAE-LSTM** como modelo de producción: obtiene la mejor PR-AUC sobre datos reales
|
| 33 |
+
(0,299) y la mejor ROC-AUC media en el banco sintético (0,792). También se evalúan dos
|
| 34 |
+
estrategias de ensemble (consenso y más sensible) y se descartan, porque ninguna mejora
|
| 35 |
+
al mejor modelo individual.
|
| 36 |
+
|
| 37 |
+
El modelo seleccionado se sirve a través de una aplicación FastAPI y de un dashboard
|
| 38 |
+
React + Vite que combina un radar en vivo estilo torre de control, un ranking de
|
| 39 |
+
anomalías, una página de métricas y un simulador interactivo. Todo el sistema es
|
| 40 |
+
reproducible desde una única imagen Docker y está listo para desplegarse en Hugging Face
|
| 41 |
+
Spaces.
|
| 42 |
+
|
| 43 |
+
---
|
| 44 |
+
|
| 45 |
+
## 1. Motivación y alcance
|
| 46 |
+
|
| 47 |
+
La monitorización de trayectorias es una función central de la gestión del tráfico
|
| 48 |
+
aéreo. Hoy se apoya en reglas deterministas y en supervisión humana: separación,
|
| 49 |
+
perfiles de descenso, esperas o códigos de transpondedor de emergencia se vigilan
|
| 50 |
+
contra umbrales fijos. Esas reglas funcionan bien para las situaciones para las que
|
| 51 |
+
fueron diseñadas, pero no pueden anticipar comportamientos que sean inusuales sin ser
|
| 52 |
+
una violación de manual.
|
| 53 |
+
|
| 54 |
+
SADAR explora un enfoque complementario, basado en datos. En lugar de codificar reglas,
|
| 55 |
+
el sistema **aprende** cómo es una operación normal en LEMD a partir de una muestra
|
| 56 |
+
amplia de vuelos reales, y trata cada nueva trayectoria como más o menos compatible con
|
| 57 |
+
esa distribución. La salida es una puntuación continua de conformidad que el operador
|
| 58 |
+
puede ordenar, filtrar y umbralizar.
|
| 59 |
+
|
| 60 |
+
El alcance del proyecto es deliberadamente acotado:
|
| 61 |
+
|
| 62 |
+
- **Qué hace.** Detecta desviaciones respecto al patrón normal de tráfico de un único
|
| 63 |
+
aeropuerto (LEMD) a partir de un conjunto fijo de variables dinámicas extraídas de las
|
| 64 |
+
observaciones ADS-B.
|
| 65 |
+
- **Qué no hace.** Predecir eventos operativos concretos, sustituir al controlador,
|
| 66 |
+
emitir recomendaciones de seguridad certificadas, ni generalizar sin reentrenamiento a
|
| 67 |
+
aeropuertos con procedimientos distintos.
|
| 68 |
+
|
| 69 |
+
Esta formulación como **monitor de conformidad** es la que hace tratable el problema con
|
| 70 |
+
los datos disponibles: solo requiere ejemplos de vuelo normal, que son abundantes, y
|
| 71 |
+
deja clara la interpretación de la salida ("esta trayectoria no se parece a las que el
|
| 72 |
+
modelo ha visto"), sin sobrevender.
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 2. Datos
|
| 77 |
+
|
| 78 |
+
### 2.1 Fuente
|
| 79 |
+
|
| 80 |
+
El dataset se construye a partir de observaciones ADS-B captadas por la red OpenSky en
|
| 81 |
+
la zona terminal de Madrid-Barajas. Cubre aproximadamente 18 días de operaciones
|
| 82 |
+
muestreados entre junio de 2017 y marzo de 2020. Cada aeronave se reporta una vez cada
|
| 83 |
+
10 segundos, la cadencia natural de ADS-B.
|
| 84 |
+
|
| 85 |
+
| Propiedad | Valor |
|
| 86 |
+
|---|---|
|
| 87 |
+
| Trayectorias totales | ≈ 20.000 (≈ 950 a 1.200 por día) |
|
| 88 |
+
| Filas por día | 140.000 a 240.000 |
|
| 89 |
+
| Columnas | 21 |
|
| 90 |
+
| Puntos por vuelo (mín · mediana · máx) | 40 · 188 · 1.246 |
|
| 91 |
+
|
| 92 |
+
### 2.2 Variables
|
| 93 |
+
|
| 94 |
+
El esquema en bruto contiene información posicional (latitud, longitud, tiempo),
|
| 95 |
+
dinámica (altitud barométrica y geométrica, velocidad respecto al suelo, rumbo, régimen
|
| 96 |
+
vertical), campos derivados (distancia a pista, fase de vuelo), identidad (ICAO24,
|
| 97 |
+
indicativo, identificador de vuelo) y estado del transpondedor (squawk, indicador de
|
| 98 |
+
"en tierra", indicador de alerta, SPI y último contacto). Un campo, `operation`, está
|
| 99 |
+
vacío en todo el dataset y se descarta.
|
| 100 |
+
|
| 101 |
+
### 2.3 Calidad y limpieza
|
| 102 |
+
|
| 103 |
+
Un análisis de perfilado sobre el dataset completo revela varios problemas que se
|
| 104 |
+
gestionan en el pipeline de preprocesado:
|
| 105 |
+
|
| 106 |
+
- **Valores ausentes.** `geoaltitude` falta en el 34,9 % de las filas, `baroaltitude` en
|
| 107 |
+
el 19,6 %, los campos cinemáticos (`velocity`, `heading`, `vertrate`) en torno al
|
| 108 |
+
19 %. El resto se queda por debajo del 4 %.
|
| 109 |
+
- **Archivo duplicado.** Un día llega dos veces con nombres distintos y se deduplica por
|
| 110 |
+
hash de contenido.
|
| 111 |
+
- **Días vacíos o erróneos.** Algunos días llegan sin filas o con errores en el
|
| 112 |
+
manifest y se descartan.
|
| 113 |
+
- **Huecos de cobertura.** Cerca del 8 % de los vuelos en aire muestran un salto
|
| 114 |
+
superior a 120 s entre observaciones consecutivas. Casi siempre son artefactos de
|
| 115 |
+
cobertura del receptor, no eventos operativos, por lo que se utilizan como
|
| 116 |
+
característica, jamás como etiqueta.
|
| 117 |
+
|
| 118 |
+
Tras la limpieza, el dataset contiene un conjunto estable de operaciones normales en
|
| 119 |
+
LEMD que abarca varias estaciones e incluye el inicio de 2020, lo que sirve además como
|
| 120 |
+
cambio de distribución natural para poner a prueba el split temporal.
|
| 121 |
+
|
| 122 |
+
### 2.4 Observaciones destacadas
|
| 123 |
+
|
| 124 |
+
El análisis exploratorio confirma un pequeño conjunto de casos raros que se reservan
|
| 125 |
+
únicamente para validación:
|
| 126 |
+
|
| 127 |
+
- Cuatro vuelos llevan un código de transpondedor de emergencia en algún punto.
|
| 128 |
+
- Aproximadamente un centenar de vuelos muestran un patrón claro de motor y al aire
|
| 129 |
+
(aproximación seguida de ascenso sostenido).
|
| 130 |
+
|
| 131 |
+
Ninguno de estos vuelos entra en el conjunto de entrenamiento. Se utilizan como prueba
|
| 132 |
+
de cordura del modelo desplegado.
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## 3. Formulación del problema
|
| 137 |
+
|
| 138 |
+
El dataset contiene muchos vuelos normales y casi ninguna anomalía etiquetada. Eso
|
| 139 |
+
descarta la clasificación supervisada y apunta al **aprendizaje de una sola clase**:
|
| 140 |
+
entrenar un modelo que capture la distribución de trayectorias normales y puntuar los
|
| 141 |
+
nuevos vuelos según su compatibilidad con esa distribución.
|
| 142 |
+
|
| 143 |
+
En la práctica, los tres modelos profundos son **autoencoders** entrenados solo con
|
| 144 |
+
vuelos normales. El objetivo de entrenamiento es reconstruir la ventana de entrada a
|
| 145 |
+
partir de una representación latente comprimida; en inferencia se utiliza el **error de
|
| 146 |
+
reconstrucción** como puntuación de conformidad. Una trayectoria parecida a los patrones
|
| 147 |
+
normales se reconstruye con precisión y recibe un score bajo; una trayectoria que se
|
| 148 |
+
aparta de esos patrones es más difícil de reconstruir y recibe un score alto.
|
| 149 |
+
|
| 150 |
+
Esta formulación encaja con el contexto operativo por tres motivos. Sólo necesita datos
|
| 151 |
+
normales, que son los únicos disponibles a escala. Produce una puntuación continua e
|
| 152 |
+
interpretable en lugar de una etiqueta dura, lo que es la salida adecuada para una
|
| 153 |
+
herramienta de apoyo a un operador humano. Y se comporta bien ante modos de fallo
|
| 154 |
+
desconocidos: cualquier trayectoria que el modelo no haya aprendido a reconstruir
|
| 155 |
+
emerge como anómala con independencia de qué es lo que la hace inusual.
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## 4. Pipeline de preprocesado
|
| 160 |
+
|
| 161 |
+
El pipeline transforma los parquet en bruto en tensores de longitud fija ya
|
| 162 |
+
estandarizados y listos para entrenar. Cada paso está implementado como una función
|
| 163 |
+
pequeña y testeable bajo `src/sadar/data/`.
|
| 164 |
+
|
| 165 |
+
1. **Carga y fusión.** Se leen todos los parquet bajo `$SADAR_DATA_DIR` con pyarrow, se
|
| 166 |
+
concatenan y se deduplican. Los días vacíos o erróneos se descartan.
|
| 167 |
+
2. **Transformación de coordenadas.** La latitud y la longitud se proyectan a un sistema
|
| 168 |
+
métrico relativo a pista con `pyproj`. Así el modelo es robusto a la curvatura de las
|
| 169 |
+
coordenadas y recibe metros como unidad.
|
| 170 |
+
3. **Codificación del rumbo.** El rumbo se codifica como `(sin(rumbo), cos(rumbo))` para
|
| 171 |
+
evitar la discontinuidad entre 0 y 360 grados.
|
| 172 |
+
4. **Tratamiento de ausentes.** Los huecos cortos dentro de una trayectoria por lo demás
|
| 173 |
+
completa se rellenan con interpolación lineal. Las trayectorias con demasiados
|
| 174 |
+
ausentes se descartan.
|
| 175 |
+
5. **Resampleado.** Cada vuelo se resamplea a una rejilla uniforme de 10 s, que coincide
|
| 176 |
+
con la cadencia nominal de ADS-B.
|
| 177 |
+
6. **Ventanado.** Cada trayectoria resampleada se trocea en ventanas de longitud fija.
|
| 178 |
+
La longitud de ventana es un hiperparámetro definido en
|
| 179 |
+
`configs/preprocessing.yaml`.
|
| 180 |
+
7. **Estandarización.** Cada variable se normaliza a media cero y varianza unidad. El
|
| 181 |
+
escalador se ajusta **solo con el conjunto de entrenamiento** para evitar fugas, y se
|
| 182 |
+
guarda como `data/processed/scaler.npz`, de manera que la misma transformación se
|
| 183 |
+
aplica en inferencia.
|
| 184 |
+
8. **Split sin fugas.** La división se hace conjuntamente por `flight_id` y por fecha:
|
| 185 |
+
ningún vuelo aparece en más de un conjunto, y el test es estrictamente posterior en
|
| 186 |
+
el tiempo al entrenamiento. Los años 2017 a 2019 se usan para entrenamiento y
|
| 187 |
+
validación; 2020 se reserva para test. Este split realista es lo que permite tomar
|
| 188 |
+
las cifras de latencia con confianza.
|
| 189 |
+
9. **Limpieza del set de entrenamiento.** Los pocos vuelos con squawks de emergencia y
|
| 190 |
+
los go-around sospechosos se eliminan del entrenamiento. Sólo trayectorias
|
| 191 |
+
"normales" entran al modelo.
|
| 192 |
+
|
| 193 |
+
La entrada de todos los modelos profundos es una ventana de siete variables:
|
| 194 |
+
`[x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]`.
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 5. Modelos
|
| 199 |
+
|
| 200 |
+
### 5.1 Baseline Isolation Forest
|
| 201 |
+
|
| 202 |
+
Detector de referencia, no secuencial. Cada ventana se resume por estadísticos por
|
| 203 |
+
variable (media, desviación, mínimo, máximo) y se alimenta a un Isolation Forest. El
|
| 204 |
+
baseline sirve para cuantificar cuánto añade realmente el modelado secuencial.
|
| 205 |
+
|
| 206 |
+
### 5.2 Autoencoder LSTM
|
| 207 |
+
|
| 208 |
+
Un encoder LSTM de dos capas comprime la ventana de entrada en un vector latente, y un
|
| 209 |
+
decoder LSTM espejado la reconstruye. Se entrena de extremo a extremo con MSE y el loop
|
| 210 |
+
de entrenamiento compartido descrito más abajo. Es la arquitectura caballo de batalla:
|
| 211 |
+
estable, rápida en inferencia y alineada con la bibliografía sobre modelado de
|
| 212 |
+
trayectorias ADS-B.
|
| 213 |
+
|
| 214 |
+
### 5.3 Autoencoder Transformer
|
| 215 |
+
|
| 216 |
+
El mismo esqueleto encoder-decoder, pero con atención multi-cabeza en lugar de
|
| 217 |
+
recurrencia. El Transformer captura dependencias a largo plazo dentro de la ventana sin
|
| 218 |
+
el cuello de botella secuencial de las LSTM, a cambio de más parámetros y una superficie
|
| 219 |
+
de hiperparámetros mayor.
|
| 220 |
+
|
| 221 |
+
### 5.4 VAE-LSTM (modelo de producción seleccionado)
|
| 222 |
+
|
| 223 |
+
Un autoencoder variacional construido sobre encoder y decoder LSTM. En lugar de producir
|
| 224 |
+
un único vector latente, el encoder produce una distribución posterior; el decoder
|
| 225 |
+
reconstruye a partir de una muestra. La función de pérdida es la ELBO habitual: MSE de
|
| 226 |
+
reconstrucción más un término KL que regulariza la posterior hacia una gaussiana
|
| 227 |
+
estándar, escalado por un factor beta. Esto aporta una interpretación probabilística al
|
| 228 |
+
score y se comporta mejor en el paso de selección de umbral.
|
| 229 |
+
|
| 230 |
+
### 5.5 Ensembles (evaluados y descartados)
|
| 231 |
+
|
| 232 |
+
Como parte de la comparación se evalúan dos estrategias de ensemble. Tras calibrar cada
|
| 233 |
+
modelo con z-score sobre las normales de validación, los tres modelos profundos se
|
| 234 |
+
combinan por la **media** de sus scores (consenso) o por el **máximo** (más sensible).
|
| 235 |
+
Ninguna combinación supera al mejor modelo individual en el test real, así que el
|
| 236 |
+
ensemble queda documentado y descartado. El detector final es el VAE-LSTM en solitario.
|
| 237 |
+
|
| 238 |
+
### 5.6 Infraestructura de entrenamiento compartida
|
| 239 |
+
|
| 240 |
+
Los tres modelos profundos comparten el mismo loop de entrenamiento, implementado una
|
| 241 |
+
sola vez en `src/sadar/models/training.py`. El loop usa:
|
| 242 |
+
|
| 243 |
+
- **Adam** con weight decay,
|
| 244 |
+
- planificador **ReduceLROnPlateau**,
|
| 245 |
+
- **early stopping** sobre la pérdida de validación,
|
| 246 |
+
- **MLflow** para tracking (losses, hiperparámetros, mejor checkpoint),
|
| 247 |
+
- **Optuna** para búsqueda de hiperparámetros, usado sobre todo en el VAE-LSTM.
|
| 248 |
+
|
| 249 |
+
Cada arquitectura tiene su propio fichero YAML bajo `configs/`. La reproducibilidad se
|
| 250 |
+
garantiza con semillas fijas y guardando junto al checkpoint la configuración exacta de
|
| 251 |
+
preprocesado utilizada.
|
| 252 |
+
|
| 253 |
+
---
|
| 254 |
+
|
| 255 |
+
## 6. Banco de anomalías sintéticas
|
| 256 |
+
|
| 257 |
+
Como las anomalías reales son escasas y carecen de etiqueta, se construye un banco
|
| 258 |
+
controlado sobre los vuelos normales del test. Cada escenario parte de una ventana
|
| 259 |
+
normal real e inyecta una perturbación conocida y etiquetada que se aplica de forma
|
| 260 |
+
gradual a partir de la mitad de la ventana. Se implementan cinco tipos de inyección,
|
| 261 |
+
cada uno en varias intensidades:
|
| 262 |
+
|
| 263 |
+
| Tipo | Descripción |
|
| 264 |
+
|---|---|
|
| 265 |
+
| Desviación de ruta | Salida lateral del corredor, parametrizada en metros. |
|
| 266 |
+
| Anomalía de altitud | Desplazamiento de altitud inconsistente con la fase. |
|
| 267 |
+
| Anomalía de velocidad | Aceleración o deceleración multiplicativa. |
|
| 268 |
+
| Holding | Giro de 360 ° repetido con periodo configurable. |
|
| 269 |
+
| Congelado de transpondedor | La señal se mantiene fija desde un punto dado. |
|
| 270 |
+
|
| 271 |
+
El banco produce etiquetas de verdad (cada inyección sabe exactamente cuándo empieza) y
|
| 272 |
+
por tanto permite dos métricas que los datos reales no pueden dar: **PR-AUC por tipo de
|
| 273 |
+
anomalía** y **latencia de detección** (mediana de segundos entre el inicio de la
|
| 274 |
+
inyección y el primer instante en que el score por paso cruza el umbral de alerta).
|
| 275 |
+
|
| 276 |
+
El banco sintético se utiliza únicamente para evaluación. Ninguna ventana sintética
|
| 277 |
+
entra jamás en el entrenamiento.
|
| 278 |
+
|
| 279 |
+
---
|
| 280 |
+
|
| 281 |
+
## 7. Evaluación
|
| 282 |
+
|
| 283 |
+
### 7.1 Conjunto de test real
|
| 284 |
+
|
| 285 |
+
Los cuatro detectores se puntúan sobre el mismo split de test. La métrica principal es
|
| 286 |
+
**PR-AUC**, más informativa que la ROC-AUC dado el fuerte desbalance de clases en datos
|
| 287 |
+
reales.
|
| 288 |
+
|
| 289 |
+
| Modelo | ROC-AUC | PR-AUC | ROC-AUC media sintética | Latencia mediana |
|
| 290 |
+
|---|---|---|---|---|
|
| 291 |
+
| Baseline Isolation Forest | 0,515 | 0,133 | 0,593 | n/d |
|
| 292 |
+
| Autoencoder LSTM | 0,648 | 0,260 | 0,779 | 115 s |
|
| 293 |
+
| Autoencoder Transformer | 0,614 | 0,227 | 0,743 | 115 s |
|
| 294 |
+
| **VAE-LSTM (seleccionado)** | **0,659** | **0,299** | **0,792** | 120 s |
|
| 295 |
+
| Ensemble (media) | 0,652 | 0,273 | 0,778 | n/d |
|
| 296 |
+
| Ensemble (máximo) | 0,658 | 0,278 | 0,774 | n/d |
|
| 297 |
+
|
| 298 |
+
Se desprenden tres conclusiones:
|
| 299 |
+
|
| 300 |
+
- Los modelos profundos aportan valor sustancial sobre el baseline (la PR-AUC
|
| 301 |
+
aproximadamente se duplica).
|
| 302 |
+
- El VAE-LSTM es el mejor en conjunto: mayor PR-AUC sobre datos reales, mayor ROC-AUC
|
| 303 |
+
media sintética y latencia competitiva.
|
| 304 |
+
- Las dos estrategias de ensemble no superan al mejor modelo individual y no se
|
| 305 |
+
retienen.
|
| 306 |
+
|
| 307 |
+
### 7.2 Banco sintético (ROC-AUC por anomalía)
|
| 308 |
+
|
| 309 |
+
El VAE-LSTM es el mejor en todos los tipos de anomalía. El patrón es consistente: las
|
| 310 |
+
perturbaciones grandes y sostenidas son más fáciles que las sutiles, y las maniobras
|
| 311 |
+
estructuradas (holding, cambios fuertes de velocidad) son más fáciles que los
|
| 312 |
+
desplazamientos pequeños.
|
| 313 |
+
|
| 314 |
+
| Anomalía | Intensidad | Baseline | LSTM | Transformer | VAE-LSTM |
|
| 315 |
+
|---|---|---|---|---|---|
|
| 316 |
+
| Desviación de ruta | 20.000 m | 0,513 | 0,556 | 0,543 | 0,558 |
|
| 317 |
+
| Desviación de ruta | 40.000 m | 0,537 | 0,684 | 0,657 | 0,700 |
|
| 318 |
+
| Desviación de ruta | 80.000 m | 0,618 | 0,883 | 0,865 | 0,899 |
|
| 319 |
+
| Desviación de altitud | 300 m | 0,502 | 0,512 | 0,505 | 0,514 |
|
| 320 |
+
| Desviación de altitud | 800 m | 0,518 | 0,581 | 0,531 | 0,593 |
|
| 321 |
+
| Desviación de altitud | 1.500 m | 0,561 | 0,726 | 0,606 | 0,752 |
|
| 322 |
+
| Factor de velocidad | x1,6 | 0,620 | 0,901 | 0,835 | 0,927 |
|
| 323 |
+
| Factor de velocidad | x2,2 | 0,743 | 0,985 | 0,965 | 0,989 |
|
| 324 |
+
| Factor de velocidad | x0,4 | 0,673 | 0,901 | 0,884 | 0,925 |
|
| 325 |
+
| Holding | 240 s/giro | 0,614 | 0,972 | 0,979 | 0,978 |
|
| 326 |
+
| Holding | 120 s/giro | 0,634 | 0,975 | 0,985 | 0,984 |
|
| 327 |
+
| Congelado transpondedor | fijo | 0,582 | 0,668 | 0,558 | 0,686 |
|
| 328 |
+
|
| 329 |
+
### 7.3 Curvas y matrices de confusión
|
| 330 |
+
|
| 331 |
+
Para cada detector se generan curvas precision-recall y ROC, junto con una matriz de
|
| 332 |
+
confusión tomada al umbral del percentil 99 de validación. Las figuras se guardan como
|
| 333 |
+
PNG en `reports/figures/` y los arrays subyacentes se almacenan dentro de
|
| 334 |
+
`reports/model_comparison.json`, de modo que el dashboard puede renderizarlas sin
|
| 335 |
+
recalcular.
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
## 8. Inferencia y servicio
|
| 340 |
+
|
| 341 |
+
El VAE-LSTM seleccionado se sirve mediante una aplicación FastAPI compacta
|
| 342 |
+
(`src/sadar/serve/app.py`). En el arranque el servicio carga:
|
| 343 |
+
|
| 344 |
+
- el checkpoint del modelo de producción,
|
| 345 |
+
- el escalador ajustado con los datos de entrenamiento,
|
| 346 |
+
- el informe de métricas precomputado,
|
| 347 |
+
- y una muestra curada de trayectorias para que el dashboard pueda funcionar sin los
|
| 348 |
+
datos brutos.
|
| 349 |
+
|
| 350 |
+
La superficie HTTP es deliberadamente reducida. El dashboard se comunica con el backend
|
| 351 |
+
a través de cinco endpoints:
|
| 352 |
+
|
| 353 |
+
| Endpoint | Función |
|
| 354 |
+
|---|---|
|
| 355 |
+
| `GET /api/health` | Sonda de vida (también devuelve un identificador de build). |
|
| 356 |
+
| `GET /api/scene` | Escena de radar actual: aeronaves, scores, umbral. |
|
| 357 |
+
| `GET /api/flights` | Lista ordenada de vuelos monitorizados con metadatos. |
|
| 358 |
+
| `GET /api/metrics` | Métricas comparativas y curvas precomputadas. |
|
| 359 |
+
| `POST /api/simulate` | Inyecta una anomalía en un vuelo y devuelve la respuesta. |
|
| 360 |
+
|
| 361 |
+
En producción, el mismo proceso FastAPI también sirve la SPA de React ya compilada (la
|
| 362 |
+
SPA se monta en `/`, la API queda bajo `/api`). Todo el sistema corre en un único
|
| 363 |
+
contenedor.
|
| 364 |
+
|
| 365 |
+
---
|
| 366 |
+
|
| 367 |
+
## 9. Frontend
|
| 368 |
+
|
| 369 |
+
El dashboard es una aplicación React 18 + Vite + TypeScript diseñada para que parezca y
|
| 370 |
+
se sienta como una consola de torre de control: fondo oscuro, bloques de datos en
|
| 371 |
+
monoespaciada, criticidad codificada por color (verde y cian para normal, ámbar y rojo
|
| 372 |
+
para alerta). Expone cuatro pantallas:
|
| 373 |
+
|
| 374 |
+
1. **Consola torre.** Un radar en directo centrado en LEMD con un panel lateral que
|
| 375 |
+
lista todos los vuelos monitorizados y su estado actual. Las aeronaves en seguimiento
|
| 376 |
+
o en alerta se diferencian por color.
|
| 377 |
+
2. **Simulador.** El corazón interactivo de la demo. El operador elige un vuelo, un tipo
|
| 378 |
+
de anomalía, una intensidad y un instante de inicio, e inyecta la perturbación en
|
| 379 |
+
tiempo real. El score de la derecha reacciona a la inyección y la latencia de
|
| 380 |
+
detección se reporta en segundos.
|
| 381 |
+
3. **Métricas.** El informe comparativo completo: curvas ROC y PR, desglose de PR-AUC
|
| 382 |
+
por anomalía, justificación de la selección del modelo y resumen del detector final.
|
| 383 |
+
4. **Presentación.** Un recorrido guiado utilizado para presentaciones en vivo del
|
| 384 |
+
proyecto.
|
| 385 |
+
|
| 386 |
+
La aplicación está internacionalizada en inglés y español mediante un pequeño contexto
|
| 387 |
+
de traducción.
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
## 10. Reproducibilidad y despliegue
|
| 392 |
+
|
| 393 |
+
El proyecto está diseñado para que el pipeline completo (preprocesado, entrenamiento,
|
| 394 |
+
evaluación, servicio) pueda reproducirse desde cero.
|
| 395 |
+
|
| 396 |
+
- **Entorno Python.** Fijado con `uv` y `pyproject.toml`; un único comando
|
| 397 |
+
(`uv sync`) reconstruye el entorno.
|
| 398 |
+
- **Entorno de frontend.** Gestionado con `pnpm` y `pnpm-lock.yaml`.
|
| 399 |
+
- **Configuración.** Todos los parámetros (rutas, longitud de ventana, dimensiones del
|
| 400 |
+
modelo, plan de entrenamiento, umbrales de evaluación) viven en ficheros YAML bajo
|
| 401 |
+
`configs/`. El código no lee rutas literales.
|
| 402 |
+
- **Semillas.** Una única semilla se fija al inicio de cada entry point y se propaga a
|
| 403 |
+
NumPy, scikit-learn y PyTorch.
|
| 404 |
+
- **Tracking.** MLflow registra todas las ejecuciones en local; Optuna guarda su
|
| 405 |
+
historial de búsqueda junto a los hiperparámetros resultantes.
|
| 406 |
+
- **Containerización.** Un único Dockerfile construye una imagen autocontenida que
|
| 407 |
+
incluye el modelo entrenado, el escalador, los informes precomputados y la SPA. La
|
| 408 |
+
misma imagen se utiliza en local (`docker compose up`) y en producción (Hugging Face
|
| 409 |
+
Spaces).
|
| 410 |
+
|
| 411 |
+
Un `Makefile` expone el flujo como targets con nombre: `make install`,
|
| 412 |
+
`make preprocess`, `make train-lstm`, `make train-transformer`, `make train-vae`,
|
| 413 |
+
`make tune-vae`, `make compare`, `make serve`, `make dev`, `make docker-up`.
|
| 414 |
+
|
| 415 |
+
---
|
| 416 |
+
|
| 417 |
+
## 11. Limitaciones y trabajo futuro
|
| 418 |
+
|
| 419 |
+
### 11.1 Limitaciones
|
| 420 |
+
|
| 421 |
+
- **Alcance.** El dataset cubre un solo aeropuerto (LEMD) y un periodo acotado. No se
|
| 422 |
+
espera que el modelo transfiera a otras terminales sin reentrenamiento.
|
| 423 |
+
- **Sin planes de vuelo.** La "ruta prevista" se aproxima como "patrón normal
|
| 424 |
+
aprendido". Un vuelo que se aparte de su plan de vuelo pero permanezca dentro del
|
| 425 |
+
patrón normal del tráfico podría no ser marcado.
|
| 426 |
+
- **Huecos de cobertura.** Los huecos del transpondedor en ADS-B están dominados por la
|
| 427 |
+
cobertura del receptor. Se usan como característica de entrada, no como etiqueta.
|
| 428 |
+
- **Desbalance de clases.** Los eventos anómalos reales son extremadamente raros. Las
|
| 429 |
+
cifras de PR-AUC sobre el test real se calculan con un conjunto positivo pequeño; el
|
| 430 |
+
banco sintético es el referente cuantitativo principal.
|
| 431 |
+
|
| 432 |
+
### 11.2 Trabajo futuro
|
| 433 |
+
|
| 434 |
+
- Ampliar el dataset a más aeropuertos y validar la generalización entre terminales.
|
| 435 |
+
- Incorporar los planes de vuelo, cuando estén disponibles, como señal auxiliar.
|
| 436 |
+
- Combinar el score de conformidad con un clasificador posterior entrenado sobre
|
| 437 |
+
eventos operativos etiquetados (motor y al aire, aproximaciones frustradas, holding)
|
| 438 |
+
para producir alertas más interpretables.
|
| 439 |
+
- Explorar ensembles probabilísticos que ponderen cada modelo por su verosimilitud
|
| 440 |
+
calibrada en lugar de hacerlo con un reductor fijo.
|
| 441 |
+
|
| 442 |
+
---
|
| 443 |
+
|
| 444 |
+
## 12. Conclusiones
|
| 445 |
+
|
| 446 |
+
SADAR muestra que un enfoque de aprendizaje profundo de una sola clase es una base
|
| 447 |
+
viable para la monitorización de conformidad de trayectorias en un aeropuerto
|
| 448 |
+
importante. Tres autoencoders secuenciales entrenados con los mismos datos y evaluados
|
| 449 |
+
sobre el mismo banco arrojan una elección de modelo clara y defendible: el
|
| 450 |
+
**VAE-LSTM** combina la mejor PR-AUC sobre datos reales, el mejor desempeño medio en el
|
| 451 |
+
banco sintético y una latencia mediana de detección competitiva, mejorando con holgura
|
| 452 |
+
a un baseline no secuencial. El sistema es completamente reproducible, se empaqueta en
|
| 453 |
+
un único contenedor y se sirve a través de un dashboard construido en torno a un caso
|
| 454 |
+
de uso real.
|
| 455 |
+
|
| 456 |
+
El resultado no es un producto cerrado, pero sí un prototipo sólido de cómo la
|
| 457 |
+
monitorización de conformidad basada en datos puede complementar las herramientas
|
| 458 |
+
basadas en reglas que usan hoy los operadores del tráfico aéreo.
|
| 459 |
+
|
| 460 |
+
---
|
| 461 |
+
|
| 462 |
+
**Repositorio.** Smart Anomaly Detection for Aviation Routes (SADAR).
|
| 463 |
+
Madrid-Barajas (LEMD) · 2026.
|
docs/REPORT.md
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SADAR
|
| 2 |
+
|
| 3 |
+
**Smart Anomaly Detection for Aviation Routes**
|
| 4 |
+
|
| 5 |
+
Technical Report
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
**Project:** SADAR · Flight Conformance Monitor
|
| 10 |
+
**Domain:** Air traffic surveillance · ADS-B data
|
| 11 |
+
**Airport of study:** Madrid-Barajas (LEMD)
|
| 12 |
+
**Approach:** One-class deep learning for trajectory anomaly detection
|
| 13 |
+
**Date:** 2026
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Executive summary
|
| 18 |
+
|
| 19 |
+
SADAR is a deep-learning system that learns the normal pattern of approach and departure
|
| 20 |
+
operations at Madrid-Barajas (LEMD) from ADS-B surveillance data and surfaces, in real time,
|
| 21 |
+
any trajectory that deviates from that pattern. The system is built as a **flight
|
| 22 |
+
conformance monitor**: it does not classify specific events, it raises an interpretable
|
| 23 |
+
anomaly score for every flight in the airspace so that an operator can review the cases
|
| 24 |
+
that fall outside the learned distribution.
|
| 25 |
+
|
| 26 |
+
Three sequence autoencoders are trained under identical conditions on roughly 20.000 real
|
| 27 |
+
flights from the OpenSky Network (an LSTM autoencoder, a Transformer autoencoder and a
|
| 28 |
+
VAE-LSTM), together with a non-sequential Isolation Forest baseline. The four detectors
|
| 29 |
+
are evaluated on the same held-out test set and on a synthetic anomaly bench with five
|
| 30 |
+
injection types and several intensities. The **VAE-LSTM** is selected as the production
|
| 31 |
+
model: it achieves the highest PR-AUC on real held-out data (0.299) and the highest
|
| 32 |
+
synthetic mean ROC-AUC (0.792). Two ensemble strategies (consensus and most-sensitive) are
|
| 33 |
+
also evaluated and discarded because neither beats the best individual model.
|
| 34 |
+
|
| 35 |
+
The selected model is wrapped in a FastAPI inference service and served through a
|
| 36 |
+
React + Vite dashboard that combines a tower-style live radar, a ranked anomaly list, a
|
| 37 |
+
metrics page and a fully interactive simulator. The whole system is reproducible from a
|
| 38 |
+
single Docker image and ships ready for deployment on Hugging Face Spaces.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 1. Motivation and scope
|
| 43 |
+
|
| 44 |
+
Trajectory monitoring is a core function of air traffic management. Today it relies on
|
| 45 |
+
deterministic rules and human supervision: separation, descent profiles, holding patterns
|
| 46 |
+
and emergency squawk codes are all monitored against fixed thresholds. These rules work
|
| 47 |
+
well for the situations they were designed for, but they cannot anticipate behavior that
|
| 48 |
+
is unusual without being a textbook violation.
|
| 49 |
+
|
| 50 |
+
SADAR explores a complementary, data-driven approach. Instead of encoding rules, the
|
| 51 |
+
system **learns** what a normal operation at LEMD looks like from a large sample of real
|
| 52 |
+
flights, and treats every new trajectory as more or less compatible with that learned
|
| 53 |
+
distribution. The output is a continuous conformance score that an operator can rank,
|
| 54 |
+
filter and threshold.
|
| 55 |
+
|
| 56 |
+
The scope of the project is deliberately narrow and well defined:
|
| 57 |
+
|
| 58 |
+
- **What it does.** Detects deviations from the normal traffic pattern at one airport
|
| 59 |
+
(LEMD) based on a fixed set of dynamic features extracted from ADS-B reports.
|
| 60 |
+
- **What it does not do.** Predict specific operational events, replace controllers,
|
| 61 |
+
produce certified safety recommendations or generalize without retraining to airports
|
| 62 |
+
with different procedures.
|
| 63 |
+
|
| 64 |
+
This framing as a **conformance monitor** is what makes the problem tractable with the
|
| 65 |
+
data available: it requires only examples of normal flight, which are abundant, and it
|
| 66 |
+
keeps the interpretation of the output clear ("this trajectory is unlike the ones the
|
| 67 |
+
model has seen") without overclaiming.
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 2. Data
|
| 72 |
+
|
| 73 |
+
### 2.1 Source
|
| 74 |
+
|
| 75 |
+
The dataset is built from ADS-B reports collected by the OpenSky Network around the
|
| 76 |
+
Madrid-Barajas terminal area. It covers approximately 18 days of operations sampled
|
| 77 |
+
between June 2017 and March 2020. Each aircraft is reported once every 10 seconds, which
|
| 78 |
+
is the natural cadence of ADS-B.
|
| 79 |
+
|
| 80 |
+
| Property | Value |
|
| 81 |
+
|---|---|
|
| 82 |
+
| Total trajectories | ≈ 20.000 (≈ 950 to 1.200 per day) |
|
| 83 |
+
| Rows per day | 140.000 to 240.000 |
|
| 84 |
+
| Columns | 21 |
|
| 85 |
+
| Points per flight (min · median · max) | 40 · 188 · 1.246 |
|
| 86 |
+
|
| 87 |
+
### 2.2 Variables
|
| 88 |
+
|
| 89 |
+
The raw schema includes positional information (latitude, longitude, time), dynamics
|
| 90 |
+
(barometric and geometric altitude, ground velocity, heading, vertical rate), derived
|
| 91 |
+
fields (distance to runway, flight phase), identity (ICAO24, callsign, flight id) and
|
| 92 |
+
transponder state (squawk, on-ground flag, alert flag, special-purpose indicator, last
|
| 93 |
+
contact). One field, `operation`, is entirely missing and is dropped.
|
| 94 |
+
|
| 95 |
+
### 2.3 Quality and cleaning
|
| 96 |
+
|
| 97 |
+
A profiling pass over the full dataset surfaces several issues that are handled in the
|
| 98 |
+
preprocessing pipeline:
|
| 99 |
+
|
| 100 |
+
- **Missing values.** `geoaltitude` is missing in 34.9 % of rows, `baroaltitude` in
|
| 101 |
+
19.6 %, kinematic fields (`velocity`, `heading`, `vertrate`) around 19 %. The rest are
|
| 102 |
+
below 4 %.
|
| 103 |
+
- **Duplicate file.** One day is shipped twice with different filenames and is
|
| 104 |
+
deduplicated by content hash.
|
| 105 |
+
- **Empty or errored days.** A handful of days arrive with zero rows or with manifest
|
| 106 |
+
errors and are dropped.
|
| 107 |
+
- **Coverage gaps.** About 8 % of airborne flights show a gap greater than 120 s between
|
| 108 |
+
consecutive reports. These gaps are almost always receiver-coverage artifacts, not
|
| 109 |
+
operational events, and are used as a feature, never as a label.
|
| 110 |
+
|
| 111 |
+
After cleaning, the dataset contains a stable set of normal LEMD operations spanning
|
| 112 |
+
multiple seasons and including the start of 2020, which doubles as a natural distribution
|
| 113 |
+
shift to stress-test the temporal split.
|
| 114 |
+
|
| 115 |
+
### 2.4 Notable observations
|
| 116 |
+
|
| 117 |
+
The exploratory analysis confirms a small set of rare cases that are kept aside for
|
| 118 |
+
validation only:
|
| 119 |
+
|
| 120 |
+
- Four flights carry an emergency squawk code at some point of their trajectory.
|
| 121 |
+
- Roughly one hundred flights show a clear go-around pattern (an approach followed by a
|
| 122 |
+
sustained climb).
|
| 123 |
+
|
| 124 |
+
None of these flights enter the training set. They serve as a sanity check for the
|
| 125 |
+
deployed model.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 3. Problem formulation
|
| 130 |
+
|
| 131 |
+
The dataset contains many normal flights and almost no labeled anomalies. This rules out
|
| 132 |
+
supervised classification and points to **one-class learning**: train a model that
|
| 133 |
+
captures the distribution of normal trajectories and score new flights by how compatible
|
| 134 |
+
they are with that distribution.
|
| 135 |
+
|
| 136 |
+
Concretely, all three deep models are **autoencoders** trained on normal flights only.
|
| 137 |
+
The training objective is to reconstruct the input window from a compressed latent
|
| 138 |
+
representation; at inference time the **reconstruction error** is used as the conformance
|
| 139 |
+
score. A trajectory that resembles the normal patterns is reconstructed accurately and
|
| 140 |
+
gets a low score; a trajectory that departs from those patterns is harder to reconstruct
|
| 141 |
+
and gets a high score.
|
| 142 |
+
|
| 143 |
+
This formulation has three properties that match the operational context. It needs only
|
| 144 |
+
normal data, which is the only data available at scale. It produces a continuous,
|
| 145 |
+
interpretable score instead of a hard label, which is the right output for a tool that
|
| 146 |
+
supports a human operator. And it gracefully handles unknown failure modes: any
|
| 147 |
+
trajectory the model has not learned to reconstruct will surface as anomalous regardless
|
| 148 |
+
of what makes it unusual.
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## 4. Preprocessing pipeline
|
| 153 |
+
|
| 154 |
+
The preprocessing pipeline turns raw parquet files into fixed-length, standardized
|
| 155 |
+
tensors ready for training. Every step is implemented as a small, testable function under
|
| 156 |
+
`src/sadar/data/`.
|
| 157 |
+
|
| 158 |
+
1. **Load and merge.** All parquet files under `$SADAR_DATA_DIR` are read with pyarrow,
|
| 159 |
+
concatenated and deduplicated. Empty or errored days are dropped.
|
| 160 |
+
2. **Coordinate transform.** Latitude and longitude are projected to a runway-relative
|
| 161 |
+
metric system using `pyproj`. This makes the model robust to the curvature of
|
| 162 |
+
coordinates and gives the network meters as input units.
|
| 163 |
+
3. **Heading encoding.** Heading is encoded as `(sin(heading), cos(heading))` to avoid
|
| 164 |
+
the discontinuity at 0 / 360 degrees.
|
| 165 |
+
4. **Missing-value handling.** Short gaps inside an otherwise complete trajectory are
|
| 166 |
+
filled with linear interpolation. Trajectories with too many missing samples are
|
| 167 |
+
discarded.
|
| 168 |
+
5. **Resampling.** Every flight is resampled to a uniform 10 s grid, which matches the
|
| 169 |
+
nominal cadence of ADS-B.
|
| 170 |
+
6. **Windowing.** Each resampled trajectory is sliced into fixed-length windows. The
|
| 171 |
+
window length is a hyperparameter, set in `configs/preprocessing.yaml`.
|
| 172 |
+
7. **Standardization.** Each feature is normalized to zero mean and unit variance. The
|
| 173 |
+
scaler is fit **only on the training split** to avoid leakage and saved as
|
| 174 |
+
`data/processed/scaler.npz`, so the exact same transform is applied at inference time.
|
| 175 |
+
8. **Leak-free split.** The split is done jointly by `flight_id` and by date: no flight
|
| 176 |
+
ever appears in more than one split, and the test split is strictly later in time than
|
| 177 |
+
the training split. Years 2017 to 2019 are used for training and validation; 2020 is
|
| 178 |
+
reserved for testing. This realistic split is what allows latency numbers to be taken
|
| 179 |
+
at face value.
|
| 180 |
+
9. **Training set cleaning.** The few flights with emergency squawks and the suspected
|
| 181 |
+
go-arounds are removed from the training split. Only "normal" trajectories enter the
|
| 182 |
+
training set.
|
| 183 |
+
|
| 184 |
+
The input to all deep models is a window of seven features:
|
| 185 |
+
`[x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]`.
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
## 5. Models
|
| 190 |
+
|
| 191 |
+
### 5.1 Isolation Forest baseline
|
| 192 |
+
|
| 193 |
+
A non-sequential reference detector. Each window is summarized by per-feature statistics
|
| 194 |
+
(mean, standard deviation, min, max) and fed to an Isolation Forest. The baseline is
|
| 195 |
+
useful to quantify how much sequential modeling actually adds.
|
| 196 |
+
|
| 197 |
+
### 5.2 LSTM autoencoder
|
| 198 |
+
|
| 199 |
+
A two-layer LSTM encoder compresses the input window into a latent vector, and a mirror
|
| 200 |
+
LSTM decoder reconstructs it. Trained end-to-end with MSE loss and the shared training
|
| 201 |
+
loop described below. This is the workhorse architecture: stable to train, fast to score
|
| 202 |
+
and well aligned with the literature on ADS-B trajectory modeling.
|
| 203 |
+
|
| 204 |
+
### 5.3 Transformer autoencoder
|
| 205 |
+
|
| 206 |
+
The same encoder-decoder skeleton, but with multi-head self-attention instead of
|
| 207 |
+
recurrence. The Transformer captures long-range dependencies inside the window without
|
| 208 |
+
the sequential bottleneck of LSTMs, at the cost of more parameters and a larger
|
| 209 |
+
hyperparameter surface.
|
| 210 |
+
|
| 211 |
+
### 5.4 VAE-LSTM (selected production model)
|
| 212 |
+
|
| 213 |
+
A variational autoencoder built on LSTM encoder and decoder. Instead of producing a single
|
| 214 |
+
latent vector, the encoder produces a posterior distribution; the decoder reconstructs
|
| 215 |
+
from a sample drawn from it. The training loss is the standard ELBO: reconstruction MSE
|
| 216 |
+
plus a KL term that regularizes the posterior toward a unit Gaussian, scaled by a
|
| 217 |
+
beta factor. This adds a principled, probabilistic interpretation to the score and
|
| 218 |
+
behaves better at the threshold-selection step.
|
| 219 |
+
|
| 220 |
+
### 5.5 Ensembling (evaluated and discarded)
|
| 221 |
+
|
| 222 |
+
Two ensemble strategies are evaluated as part of the comparison. After per-model
|
| 223 |
+
z-score calibration on validation normals, the three deep models are combined either by
|
| 224 |
+
their **mean** score (consensus) or by their **max** score (most-sensitive). Neither
|
| 225 |
+
combination beats the best individual model on the real test set, so the ensemble is
|
| 226 |
+
documented and discarded. The final detector is the VAE-LSTM alone.
|
| 227 |
+
|
| 228 |
+
### 5.6 Shared training infrastructure
|
| 229 |
+
|
| 230 |
+
All three deep models share the same training loop, implemented once in
|
| 231 |
+
`src/sadar/models/training.py`. The loop uses:
|
| 232 |
+
|
| 233 |
+
- **Adam** with weight decay,
|
| 234 |
+
- a **ReduceLROnPlateau** scheduler,
|
| 235 |
+
- **early stopping** on the validation loss,
|
| 236 |
+
- **MLflow** for tracking (losses, hyperparameters, best checkpoint),
|
| 237 |
+
- **Optuna** for hyperparameter search, used here mainly for the VAE-LSTM.
|
| 238 |
+
|
| 239 |
+
Each architecture has its own YAML configuration file under `configs/`. Reproducibility
|
| 240 |
+
is enforced by fixed seeds and by storing the exact preprocessing config alongside the
|
| 241 |
+
checkpoint.
|
| 242 |
+
|
| 243 |
+
---
|
| 244 |
+
|
| 245 |
+
## 6. Synthetic anomaly bench
|
| 246 |
+
|
| 247 |
+
Because real anomalies are rare and unlabeled, a controlled benchmark is built on top of
|
| 248 |
+
normal test flights. Each scenario starts from a real normal window and injects a known,
|
| 249 |
+
labeled perturbation that ramps in halfway through the window. Five types of injection
|
| 250 |
+
are implemented, each at multiple intensities:
|
| 251 |
+
|
| 252 |
+
| Type | Description |
|
| 253 |
+
|---|---|
|
| 254 |
+
| Route deviation | Lateral departure from the corridor, parameterized in meters. |
|
| 255 |
+
| Altitude anomaly | Altitude offset inconsistent with the flight phase. |
|
| 256 |
+
| Speed anomaly | Multiplicative acceleration or deceleration. |
|
| 257 |
+
| Holding | A repeated 360 ° turn at a configurable period. |
|
| 258 |
+
| Transponder freeze | The signal is held constant from a chosen point. |
|
| 259 |
+
|
| 260 |
+
The bench produces ground-truth labels (each injection knows exactly when it starts) and
|
| 261 |
+
therefore enables two metrics that real data cannot give us: **PR-AUC per anomaly type**
|
| 262 |
+
and **detection latency** (median seconds between the injection onset and the first
|
| 263 |
+
moment the per-step score crosses the alert threshold).
|
| 264 |
+
|
| 265 |
+
The synthetic bench is used only for evaluation. No synthetic window ever enters the
|
| 266 |
+
training set.
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
## 7. Evaluation
|
| 271 |
+
|
| 272 |
+
### 7.1 Real test set
|
| 273 |
+
|
| 274 |
+
The four detectors are scored on the same held-out test split. The headline metric is
|
| 275 |
+
**PR-AUC**, which is more informative than ROC-AUC under the strong class imbalance
|
| 276 |
+
present in real data.
|
| 277 |
+
|
| 278 |
+
| Model | ROC-AUC | PR-AUC | Synthetic mean ROC-AUC | Median latency |
|
| 279 |
+
|---|---|---|---|---|
|
| 280 |
+
| Isolation Forest baseline | 0.515 | 0.133 | 0.593 | n/a |
|
| 281 |
+
| LSTM autoencoder | 0.648 | 0.260 | 0.779 | 115 s |
|
| 282 |
+
| Transformer autoencoder | 0.614 | 0.227 | 0.743 | 115 s |
|
| 283 |
+
| **VAE-LSTM (selected)** | **0.659** | **0.299** | **0.792** | 120 s |
|
| 284 |
+
| Ensemble (mean) | 0.652 | 0.273 | 0.778 | n/a |
|
| 285 |
+
| Ensemble (max) | 0.658 | 0.278 | 0.774 | n/a |
|
| 286 |
+
|
| 287 |
+
Three conclusions stand out:
|
| 288 |
+
|
| 289 |
+
- The deep models add substantial value over the baseline (PR-AUC roughly doubles).
|
| 290 |
+
- The VAE-LSTM is the best overall: highest PR-AUC on real data, highest synthetic mean
|
| 291 |
+
ROC-AUC, competitive latency.
|
| 292 |
+
- The two ensemble strategies do not beat the best individual model and are not retained.
|
| 293 |
+
|
| 294 |
+
### 7.2 Synthetic bench (per-anomaly ROC-AUC)
|
| 295 |
+
|
| 296 |
+
The VAE-LSTM performs best across all anomaly types. The pattern is consistent: large,
|
| 297 |
+
sustained perturbations are easier than subtle ones, and structured maneuvers (holding,
|
| 298 |
+
high-magnitude speed change) are easier than small drifts.
|
| 299 |
+
|
| 300 |
+
| Anomaly | Intensity | Baseline | LSTM | Transformer | VAE-LSTM |
|
| 301 |
+
|---|---|---|---|---|---|
|
| 302 |
+
| Route deviation | 20.000 m | 0.513 | 0.556 | 0.543 | 0.558 |
|
| 303 |
+
| Route deviation | 40.000 m | 0.537 | 0.684 | 0.657 | 0.700 |
|
| 304 |
+
| Route deviation | 80.000 m | 0.618 | 0.883 | 0.865 | 0.899 |
|
| 305 |
+
| Altitude offset | 300 m | 0.502 | 0.512 | 0.505 | 0.514 |
|
| 306 |
+
| Altitude offset | 800 m | 0.518 | 0.581 | 0.531 | 0.593 |
|
| 307 |
+
| Altitude offset | 1.500 m | 0.561 | 0.726 | 0.606 | 0.752 |
|
| 308 |
+
| Speed factor | x1.6 | 0.620 | 0.901 | 0.835 | 0.927 |
|
| 309 |
+
| Speed factor | x2.2 | 0.743 | 0.985 | 0.965 | 0.989 |
|
| 310 |
+
| Speed factor | x0.4 | 0.673 | 0.901 | 0.884 | 0.925 |
|
| 311 |
+
| Holding | 240 s/turn | 0.614 | 0.972 | 0.979 | 0.978 |
|
| 312 |
+
| Holding | 120 s/turn | 0.634 | 0.975 | 0.985 | 0.984 |
|
| 313 |
+
| Transponder freeze | stuck | 0.582 | 0.668 | 0.558 | 0.686 |
|
| 314 |
+
|
| 315 |
+
### 7.3 Curves and confusion matrices
|
| 316 |
+
|
| 317 |
+
Precision-recall and ROC curves are produced for every detector, together with a
|
| 318 |
+
confusion matrix taken at the validation-99-percentile threshold. The figures are
|
| 319 |
+
saved as PNGs under `reports/figures/` and the underlying arrays are stored inside
|
| 320 |
+
`reports/model_comparison.json`, so the dashboard can render them without recomputing.
|
| 321 |
+
|
| 322 |
+
---
|
| 323 |
+
|
| 324 |
+
## 8. Serving
|
| 325 |
+
|
| 326 |
+
The selected VAE-LSTM is served by a small FastAPI application
|
| 327 |
+
(`src/sadar/serve/app.py`). At startup, the service loads:
|
| 328 |
+
|
| 329 |
+
- the checkpoint of the production model,
|
| 330 |
+
- the scaler fit on the training data,
|
| 331 |
+
- the precomputed metrics report,
|
| 332 |
+
- and a curated sample of trajectories so the dashboard can run without raw data.
|
| 333 |
+
|
| 334 |
+
The HTTP surface is intentionally small. The dashboard talks to the backend through five
|
| 335 |
+
endpoints:
|
| 336 |
+
|
| 337 |
+
| Endpoint | Purpose |
|
| 338 |
+
|---|---|
|
| 339 |
+
| `GET /api/health` | Liveness probe (also serves a build identifier). |
|
| 340 |
+
| `GET /api/scene` | Current radar scene: aircraft, scores, threshold. |
|
| 341 |
+
| `GET /api/flights` | Ranked list of monitored flights with metadata. |
|
| 342 |
+
| `GET /api/metrics` | Pre-computed comparative metrics and curves. |
|
| 343 |
+
| `POST /api/simulate` | Inject an anomaly on a chosen flight and return the response. |
|
| 344 |
+
|
| 345 |
+
In production, the same FastAPI process also serves the built React app (the SPA is
|
| 346 |
+
mounted on `/`, the API stays under `/api`). The whole system runs in a single
|
| 347 |
+
container.
|
| 348 |
+
|
| 349 |
+
---
|
| 350 |
+
|
| 351 |
+
## 9. Frontend
|
| 352 |
+
|
| 353 |
+
The dashboard is a React 18 + Vite + TypeScript application designed to look and feel
|
| 354 |
+
like a tower control console: dark background, monospace data blocks, criticality
|
| 355 |
+
encoded by color (green/cyan for normal, amber/red for alert). It exposes four screens:
|
| 356 |
+
|
| 357 |
+
1. **Tower console.** A live radar scope centered on LEMD with a side panel listing every
|
| 358 |
+
tracked flight and its current state. Tracked vs alerted aircraft are color-coded.
|
| 359 |
+
2. **Simulator.** The interactive heart of the demo. The operator picks a flight, an
|
| 360 |
+
anomaly type, an intensity and an onset, and injects the perturbation in real time.
|
| 361 |
+
The score on the right reacts to the injection and the detection latency is reported
|
| 362 |
+
in seconds.
|
| 363 |
+
3. **Metrics.** The full comparative report: ROC and PR curves, per-anomaly PR-AUC
|
| 364 |
+
breakdown, model selection rationale and final-detector summary.
|
| 365 |
+
4. **Presentation.** A guided walkthrough used for live presentations of the project.
|
| 366 |
+
|
| 367 |
+
The app is internationalized in English and Spanish through a small translation context.
|
| 368 |
+
|
| 369 |
+
---
|
| 370 |
+
|
| 371 |
+
## 10. Reproducibility and deployment
|
| 372 |
+
|
| 373 |
+
The project is engineered so that the entire pipeline (preprocessing, training,
|
| 374 |
+
evaluation, serving) can be reproduced from scratch.
|
| 375 |
+
|
| 376 |
+
- **Python environment.** Locked with `uv` and `pyproject.toml`; one command (`uv sync`)
|
| 377 |
+
rebuilds the environment.
|
| 378 |
+
- **Frontend environment.** Managed with `pnpm` and `pnpm-lock.yaml`.
|
| 379 |
+
- **Configuration.** All knobs (paths, window length, model dimensions, training
|
| 380 |
+
schedule, evaluation thresholds) live in YAML files under `configs/`. The code reads no
|
| 381 |
+
literal paths.
|
| 382 |
+
- **Seeds.** A single seed is set at the top of every entry point; it propagates to
|
| 383 |
+
NumPy, scikit-learn and PyTorch.
|
| 384 |
+
- **Tracking.** MLflow logs all training runs locally; Optuna stores its search history
|
| 385 |
+
alongside the resulting hyperparameters.
|
| 386 |
+
- **Containerization.** A single Dockerfile builds a self-contained image that bundles
|
| 387 |
+
the trained model, the scaler, the precomputed reports and the SPA. The same image is
|
| 388 |
+
used locally (`docker compose up`) and in production (Hugging Face Spaces).
|
| 389 |
+
|
| 390 |
+
A `Makefile` exposes the workflow as named targets: `make install`, `make preprocess`,
|
| 391 |
+
`make train-lstm`, `make train-transformer`, `make train-vae`, `make tune-vae`,
|
| 392 |
+
`make compare`, `make serve`, `make dev`, `make docker-up`.
|
| 393 |
+
|
| 394 |
+
---
|
| 395 |
+
|
| 396 |
+
## 11. Limitations and future work
|
| 397 |
+
|
| 398 |
+
### 11.1 Limitations
|
| 399 |
+
|
| 400 |
+
- **Scope.** The dataset covers one airport (LEMD) and a limited time span. The model is
|
| 401 |
+
not expected to transfer to other terminals without retraining.
|
| 402 |
+
- **No flight plans.** "Intended route" is approximated as "learned normal pattern". A
|
| 403 |
+
flight that deviates from its filed route but stays inside the normal traffic pattern
|
| 404 |
+
may not be flagged.
|
| 405 |
+
- **Coverage gaps.** Transponder gaps in ADS-B are dominated by receiver coverage. They
|
| 406 |
+
are used as an input feature, not as a label.
|
| 407 |
+
- **Class imbalance.** Real anomalous events are extremely rare. The PR-AUC numbers on
|
| 408 |
+
the real test set are computed on a small positive set; the synthetic bench is the
|
| 409 |
+
primary quantitative benchmark.
|
| 410 |
+
|
| 411 |
+
### 11.2 Future work
|
| 412 |
+
|
| 413 |
+
- Extend the dataset to additional airports and validate cross-airport generalization.
|
| 414 |
+
- Incorporate filed flight plans, when available, as an auxiliary signal.
|
| 415 |
+
- Combine the conformance score with a downstream classifier trained on labeled
|
| 416 |
+
operational events (go-arounds, missed approaches, holding) for more interpretable
|
| 417 |
+
alerts.
|
| 418 |
+
- Explore probabilistic ensembles that weight each model by its calibrated likelihood
|
| 419 |
+
rather than by a fixed reducer.
|
| 420 |
+
|
| 421 |
+
---
|
| 422 |
+
|
| 423 |
+
## 12. Conclusions
|
| 424 |
+
|
| 425 |
+
SADAR shows that a one-class deep-learning approach is a viable foundation for trajectory
|
| 426 |
+
conformance monitoring at a major airport. Three sequence autoencoders trained on the
|
| 427 |
+
same data and evaluated on the same bench produce a clear, defensible model choice: the
|
| 428 |
+
**VAE-LSTM** combines the best PR-AUC on real data, the best mean performance on the
|
| 429 |
+
synthetic bench and a competitive median detection latency, while improving substantially
|
| 430 |
+
over a non-sequential baseline. The system is fully reproducible, packaged as a single
|
| 431 |
+
container and served through a dashboard built around a real operational use case.
|
| 432 |
+
|
| 433 |
+
The result is not a finished product, but it is a solid prototype of how data-driven
|
| 434 |
+
conformance monitoring could complement the rule-based tools that air traffic operators
|
| 435 |
+
use today.
|
| 436 |
+
|
| 437 |
+
---
|
| 438 |
+
|
| 439 |
+
**Repository.** Smart Anomaly Detection for Aviation Routes (SADAR).
|
| 440 |
+
Madrid-Barajas (LEMD) · 2026.
|
docs/assets/eda/flight_phases.png
ADDED
|
Git LFS Details
|
docs/assets/eda/heading_polar.png
ADDED
|
Git LFS Details
|
docs/assets/eda/nulls_per_column.png
ADDED
|
Git LFS Details
|
docs/assets/eda/trajectories_map.png
ADDED
|
Git LFS Details
|
docs/assets/img1.png
ADDED
|
Git LFS Details
|
docs/assets/img2.png
ADDED
|
Git LFS Details
|
docs/assets/img3.png
ADDED
|
Git LFS Details
|
docs/assets/img4.png
ADDED
|
Git LFS Details
|
docs/assets/img5.png
ADDED
|
Git LFS Details
|
docs/assets/img6.png
ADDED
|
Git LFS Details
|
docs/assets/logo.png
ADDED
|
Git LFS Details
|
docs/assets/qr-placeholder.png
ADDED
|
Git LFS Details
|
docs/assets/tower-day.png
ADDED
|
Git LFS Details
|
docs/assets/tower-night.png
ADDED
|
Git LFS Details
|
docs/medium_post.en.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# When the most complex model is not the one that detects best
|
| 2 |
+
|
| 3 |
+
*A comparison of LSTM, Transformer and VAE-LSTM autoencoders over 19,000 real flights at Madrid-Barajas. And why validation loss is a poor advisor.*
|
| 4 |
+
|
| 5 |
+

|
| 6 |
+
|
| 7 |
+
> **Code:** [github.com/iRuperth/SADAR](https://github.com/iRuperth/SADAR)
|
| 8 |
+
> **Live demo:** [huggingface.co/spaces/devrup404/sadar](https://huggingface.co/spaces/devrup404/sadar)
|
| 9 |
+
>
|
| 10 |
+
> The repository is public and open to collaborations, suggestions and forks. Any technical discussion, *issue* or *pull request* is welcome.
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
The most interesting data point in the dataset is not a hijacking. It is an aircraft that pretended to be one for thirty seconds, by mistake.
|
| 15 |
+
|
| 16 |
+
On July 31, 2017, an Iberia Airbus flew over Madrid with the hijack code active on its transponder during three points of its trajectory. Then it went back to normal. The flight was IBE2845, the transmitted code was 7500, and in the secondary surveillance system that code means unlawful interference.
|
| 17 |
+
|
| 18 |
+
It was not a hijacking. It was almost certainly a transponder fault: three points out of the 171 that make up the complete flight record, 800 meters from the runway, with no other operational anomaly. That flight, logged in a parquet file of 3.4 million rows, marks the beginning of this article. And it also marks the technical question I took into the project: can a model learn what a normal flight looks like and raise a flag when one stops looking like one?
|
| 19 |
+
|
| 20 |
+
This article presents SADAR, an anomaly detection system for flight trajectories trained on eighteen days of real traffic from Adolfo Suárez Madrid-Barajas airport. It compares three deep autoencoders against a classical baseline, evaluates the system on a synthetic anomaly bench and discusses with candor where it works and where it does not.
|
| 21 |
+
|
| 22 |
+
Let us start with the problem.
|
| 23 |
+
|
| 24 |
+
## Problem statement
|
| 25 |
+
|
| 26 |
+
Incident detection in civil aviation can rarely be framed as a supervised problem: the data is dominated by normal operation and the few labeled events are ambiguous. The standard alternative is **conformance monitoring**: learn the distribution of normal behavior and flag trajectories that deviate from it. This is the framing SADAR adopts.
|
| 27 |
+
|
| 28 |
+
The distinction matters. Saying "this flight deviates from the learned pattern" is defensible with data. Saying "this flight is an incident" requires operational context that ADS-B does not provide. SADAR operates strictly under the first statement.
|
| 29 |
+
|
| 30 |
+
## Data
|
| 31 |
+
|
| 32 |
+
The data comes from OpenSky Network, a collaborative network of ADS-B receivers. The set covers 18 non-consecutive days between June 2017 and March 2020, centered on the airspace of Barajas. Each aircraft reports a position every 10 seconds, with 21 columns per point: position and dynamics (latitude, longitude, barometric and geometric altitude, velocity, heading, vertical rate), identity (callsign, icao24, flight_id) and transponder state (squawk, alert, onground).
|
| 33 |
+
|
| 34 |
+
The raw figures after merging and deduplication:
|
| 35 |
+
|
| 36 |
+
- 3,429,638 rows.
|
| 37 |
+
- 19,057 distinct flights.
|
| 38 |
+
- Median of 175 points per flight, maximum 1,606.
|
| 39 |
+
- Median duration of 30.2 minutes.
|
| 40 |
+
|
| 41 |
+
The first finding of the exploratory analysis was the gaps. GPS altitude (`geoaltitude`) shows 24.79 % missing values. Velocity and heading, around 13 %. These gaps are not errors: they correspond to areas of lower ground receiver coverage. I discarded GPS altitude and kept barometric altitude, more complete, treating the gaps as a property of the problem rather than a fault to correct.
|
| 42 |
+
|
| 43 |
+

|
| 44 |
+
|
| 45 |
+
The second finding came from the heading histogram. Plotted on a polar chart, the two parallel runways of Barajas appear as two opposite lobes.
|
| 46 |
+
|
| 47 |
+

|
| 48 |
+
|
| 49 |
+
The airport is identifiable in the data before projecting it onto a map.
|
| 50 |
+
|
| 51 |
+
That circular property of heading motivated its encoding as a sine and cosine pair: for an autoencoder, 359° and 1° must remain adjacent, and a linear encoding prevents that.
|
| 52 |
+
|
| 53 |
+

|
| 54 |
+
|
| 55 |
+
## Real events in the set
|
| 56 |
+
|
| 57 |
+
The dataset contains only four flights with an emergency code on the transponder. They deserve detailed description because their scarcity determines the framing of the problem:
|
| 58 |
+
|
| 59 |
+
- **IBE2845** (Iberia, July 31, 2017). Squawk 7500 for three points out of 171, transmitted 800 meters from the runway. Consistent with a transponder fault.
|
| 60 |
+
- **ELY395** (El Al, October 2, 2017). Squawk 7700 for five points, declared 800 meters from the runway. The aircraft landed. The most plausible case of a real emergency.
|
| 61 |
+
- **RYR61AD** (Ryanair, January 28, 2019). Squawk 7600 (communications failure). One point. Landed.
|
| 62 |
+
- **SWR202Y** (Swiss, February 3, 2020). Squawk 7700. One point. Landed.
|
| 63 |
+
|
| 64 |
+
Four labels in a universe of non-labels. This proportion rules out supervised learning: no classifier is trained on four positives. The four flights were set aside as a qualitative validation set, grouped in 1,076 labeled windows in `data/processed/anomalies.npy`, and do not appear at any point of training.
|
| 65 |
+
|
| 66 |
+
## Methodology
|
| 67 |
+
|
| 68 |
+
### One-class learning
|
| 69 |
+
|
| 70 |
+
Without enough labels, the framing is **one-class learning** through autoencoders. The network learns to reconstruct normal trajectories. The reconstruction error operates as an anomaly score: an input similar to those in training produces a low error; an unusual input raises it.
|
| 71 |
+
|
| 72 |
+
I trained four detectors under identical conditions (same preprocessing, same split, same threshold) to allow a rigorous comparison:
|
| 73 |
+
|
| 74 |
+
| # | Model | Idea | Weights |
|
| 75 |
+
|---|---|---|---|
|
| 76 |
+
| 0 | Isolation Forest | Per-window summary statistics, not sequential | baseline |
|
| 77 |
+
| 1 | LSTM autoencoder | Sequential reconstruction | 224 KB |
|
| 78 |
+
| 2 | Transformer autoencoder | Attention over the full window | 635 KB |
|
| 79 |
+
| 3 | VAE-LSTM | Probabilistic encoding of the latent space | 222 KB |
|
| 80 |
+
|
| 81 |
+
### Preprocessing
|
| 82 |
+
|
| 83 |
+
Four preprocessing decisions deserve specific mention because they determine the final quality of a trajectory detector.
|
| 84 |
+
|
| 85 |
+
**Metric coordinates relative to the runway.** Latitude and longitude in degrees are inadequate for modeling movement over an airport: distances depend on latitude, and the model does not incorporate geodesy. The pipeline projects coordinates to UTM zone 30 (EPSG:32630), centered on the runway of Barajas, and operates in meters.
|
| 86 |
+
|
| 87 |
+
**Heading as the (sin, cos) pair.** A circular variable that linear encoding does not represent faithfully. Trigonometric encoding ensures that values close on the circle are also close in feature space.
|
| 88 |
+
|
| 89 |
+
**Split by date and by flight_id.** Training on 2017-2019, validation and test on 2020. Each flight appears in a single split. This blocks both temporal leakage and instance leakage simultaneously.
|
| 90 |
+
|
| 91 |
+
**Cleaning of the training set.** I excluded from training the 100 flights with a go-around pattern and the four with an emergency squawk. The training set must represent the most conservative version of "normal flight". Leaving go-arounds in makes the model learn to reconstruct them without difficulty, nullifying their value as anomalies.
|
| 92 |
+
|
| 93 |
+
The final windows have 60 steps of 10 seconds (10 minutes per window), with 50 % overlap. Seven features per step:
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
[x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
The resulting set: 61,008 training windows, 7,679 validation, 7,788 test and 1,076 labeled as anomalous. The scaler standardizes features by fitting only on the training set.
|
| 100 |
+
|
| 101 |
+
### Architectures
|
| 102 |
+
|
| 103 |
+
**Isolation Forest** over summary statistics (mean, standard deviation, minimum and maximum per feature). A non-sequential baseline whose function is to establish the reference line that deep learning must surpass.
|
| 104 |
+
|
| 105 |
+
**LSTM autoencoder**: encoder with a single LSTM layer (hidden 64), linear bottleneck to a latent of dimension 16, symmetric decoder that unrolls from the repeated latent.
|
| 106 |
+
|
| 107 |
+
**Transformer autoencoder**: two encoder layers with four attention heads (d_model 64), sinusoidal positional encoding, same bottleneck to latent 16.
|
| 108 |
+
|
| 109 |
+
**VAE-LSTM**: LSTM encoder that produces the mean and log-variance, reparameterization so that gradients flow through the sampling, LSTM decoder from z. The loss combines reconstruction error (MSE) with the Kullback-Leibler divergence weighted by a beta coefficient of 0.0001193, selected by Optuna.
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
std = torch.exp(0.5 * logvar)
|
| 113 |
+
eps = torch.randn_like(std)
|
| 114 |
+
z = mu + std * eps
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
All models share the same training loop: Adam optimizer with weight decay, ReduceLROnPlateau scheduler, early stopping with patience 7, fixed seed 42, complete tracking with MLflow.
|
| 118 |
+
|
| 119 |
+
### Synthetic anomaly bench
|
| 120 |
+
|
| 121 |
+
Four real events do not allow stable quantitative evaluation. To address this limitation, controlled anomalies are injected over normal flights from the test set. Five families, all with a linear ramp at onset to allow the measurement of **detection latency** (seconds from the start of the perturbation to the first threshold crossing):
|
| 122 |
+
|
| 123 |
+
- **Route deviation**: lateral deformation, magnitudes of 20, 40 and 80 km.
|
| 124 |
+
- **Altitude anomaly**: offset of 300, 800 or 1,500 m.
|
| 125 |
+
- **Speed anomaly**: multiplicative factors of 0.4x, 1.6x and 2.2x.
|
| 126 |
+
- **Holding**: continuous turns with a period of 120 or 240 seconds.
|
| 127 |
+
- **Transponder freeze**: the ADS-B retains the last value for the rest of the window.
|
| 128 |
+
|
| 129 |
+
Validation against an in-house simulator might seem circular. It is standard practice in *safety engineering*: the simulator was designed before the models and describes well-defined failure modes, not patterns learned from the model. The set produces 12 variants (combinations of type and intensity) and 24,000 synthetic windows per evaluated model.
|
| 130 |
+
|
| 131 |
+
So much for the setup. Next, whether it works.
|
| 132 |
+
|
| 133 |
+
## Results
|
| 134 |
+
|
| 135 |
+
Results on the 2020 test set:
|
| 136 |
+
|
| 137 |
+
| Model | Real ROC-AUC | Real PR-AUC | Synthetic ROC | Median latency |
|
| 138 |
+
|---|---|---|---|---|
|
| 139 |
+
| Isolation Forest | 0.515 | 0.133 | 0.593 | n/a |
|
| 140 |
+
| LSTM-AE | 0.648 | 0.260 | 0.779 | 115 s |
|
| 141 |
+
| Transformer-AE | 0.614 | 0.227 | 0.743 | 115 s |
|
| 142 |
+
| **VAE-LSTM** | **0.659** | **0.299** | **0.792** | 120 s |
|
| 143 |
+
|
| 144 |
+
The deep models nearly double the PR-AUC of the baseline. This difference justifies the investment in deep learning: if the LSTM had not beaten the Isolation Forest, the rest of the work would not have made sense.
|
| 145 |
+
|
| 146 |
+

|
| 147 |
+
|
| 148 |
+

|
| 149 |
+
|
| 150 |
+
One observation deserves attention.
|
| 151 |
+
|
| 152 |
+
The Transformer won the validation loss (0.038 against 0.053 for the VAE and 0.072 for the LSTM). And lost the PR-AUC. Something did not add up.
|
| 153 |
+
|
| 154 |
+
The explanation is direct: an architecture with enough capacity also reconstructs anomalies with some fidelity, which reduces the separability between normal and anomalous as measured by reconstruction error. In reconstruction-based detection, validation loss is a poor proxy for performance. We know it and keep looking at it anyway.
|
| 155 |
+
|
| 156 |
+
I also evaluated two ensembles, by mean and by maximum of the z-normalized scores. Neither beat the individual VAE (PR-AUC 0.273 and 0.278 against 0.299), and I discarded them. The ensemble was an attempt to close with a push. It did not deliver. It is honest to report it.
|
| 157 |
+
|
| 158 |
+
The numbers tell one part. The other lies in what does not show up in the table.
|
| 159 |
+
|
| 160 |
+
## Discussion
|
| 161 |
+
|
| 162 |
+
The results show a detection profile that is **heterogeneous by anomaly type**. The final model easily detects loud perturbations: ROC-AUC 0.984 in holding with a 120 s period, 0.989 in acceleration with factor 2.2, 0.899 in route deviation of 80 km. Subtle perturbations (300 m of altitude, small drifts) require close to two minutes and fall into the 0.51-0.55 range.
|
| 163 |
+
|
| 164 |
+
The most relevant case of the discussion is the **transponder freeze**.
|
| 165 |
+
|
| 166 |
+
The VAE detects it in 1.2 % of cases. One out of every eighty attempts.
|
| 167 |
+
|
| 168 |
+
The explanation is structural: an aircraft in stable cruise whose values freeze produces a pattern very close to that of an aircraft in stable cruise that keeps transmitting. The reconstruction error remains low and the score does not cross the threshold. It is a limitation of the reconstruction approach, not of training.
|
| 169 |
+
|
| 170 |
+

|
| 171 |
+
|
| 172 |
+
This heterogeneity defines the scope of the system. SADAR is a detector of **anomalous maneuvers**, not a universal incident detector. Its operational value lies in those deviations that the traditional rule-based network (systems such as STCA or MSAW) does not cover, not in replacing those systems.
|
| 173 |
+
|
| 174 |
+
Before closing, it is worth being explicit about what the system does not do.
|
| 175 |
+
|
| 176 |
+
## Limitations
|
| 177 |
+
|
| 178 |
+
The system assumes certain limitations documented explicitly in the technical report.
|
| 179 |
+
|
| 180 |
+
- **Absence of flight plans**. SADAR does not have the planned route for each flight. The "deviation from the expected route" is approximated by "deviation from the mean pattern". A flight that departs from its assigned plan but stays within the mean pattern will not be flagged.
|
| 181 |
+
- **Temporal coverage**. The set consists of 18 non-consecutive days between 2017 and 2020. Modeling demand, seasonality or continuous temporal prediction falls outside the scope.
|
| 182 |
+
- **Transponder freeze**. Within-window detection rate of 1.2 %. Any serious extension of the system must address this case specifically (derivative signals, change-point detection, dedicated model).
|
| 183 |
+
- **Coverage gaps**. 24.2 % of in-air flights present gaps longer than 120 seconds in reception. These gaps are receiver artifacts, not operational incidents. Treating them as positive labels would generate systematic noise.
|
| 184 |
+
|
| 185 |
+
## Deployed system
|
| 186 |
+
|
| 187 |
+
The final model is served through an API built on FastAPI that mounts a React + Vite + TypeScript frontend on the same port, allowing a *single-image* deployment on Hugging Face Spaces. The dashboard consists of three screens:
|
| 188 |
+
|
| 189 |
+
1. **Tower console**: live radar over Barajas with the day's flights and their scores, visual alert when one crosses the threshold.
|
| 190 |
+
2. **Simulator**: controlled injection of anomalies over a reference flight, with visualization of the deformed trajectory, the score and the latency.
|
| 191 |
+
3. **Metrics**: model comparison table, precision-recall and ROC curves, confusion matrices, breakdown by anomaly type.
|
| 192 |
+
|
| 193 |
+
## Stack and reproducibility
|
| 194 |
+
|
| 195 |
+
Python 3.11, PyTorch, FastAPI, MLflow and Optuna on the backend. Vite, React 18 and TypeScript on the frontend. Packaged in a *single-image* Docker container. Reproducibility guaranteed by fixed seeds, YAML configurations that resolve environment variables, and checkpoints stored also as MLflow artifacts.
|
| 196 |
+
|
| 197 |
+
## Conclusions and future work
|
| 198 |
+
|
| 199 |
+
SADAR shows that conformance monitoring of flight trajectories is tractable with deep autoencoders trained exclusively on normal operation, with defensible metrics and a reproducible evaluation protocol. The comparison between three model families under identical conditions allows the choice of VAE-LSTM to be justified not only by its final PR-AUC, but by its overall detection and latency profile.
|
| 200 |
+
|
| 201 |
+
The Transformer was not the right choice for this problem. I leave it documented because discarded options also count.
|
| 202 |
+
|
| 203 |
+
Immediate future work points to three directions. First, replace the Transformer with a TCN or a convolutional autoencoder to bound capacity without losing temporal modeling. Second, incorporate derivatives and change-point detection to address the transponder freeze. Third, evaluate the incorporation of flight plans when available, which would reframe the problem as plan-conditioned prediction rather than approximation to the mean pattern.
|
| 204 |
+
|
| 205 |
+
IBE2845 landed without incident on July 31, 2017 and its three red points received no further attention. Glitch, anomaly or incident, reality rarely arrives labeled, and building systems that operate in that terrain begins by accepting it.
|
docs/medium_post.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cuando el modelo más complejo no es el que detecta mejor
|
| 2 |
+
|
| 3 |
+
*Comparativa entre LSTM, Transformer y VAE-LSTM sobre 19.000 vuelos reales de Barajas. Y por qué la pérdida de validación no es buena consejera.*
|
| 4 |
+
|
| 5 |
+

|
| 6 |
+
|
| 7 |
+
> **Código:** [github.com/iRuperth/SADAR](https://github.com/iRuperth/SADAR)
|
| 8 |
+
> **Demo en vivo:** [huggingface.co/spaces/devrup404/sadar](https://huggingface.co/spaces/devrup404/sadar)
|
| 9 |
+
>
|
| 10 |
+
> El repositorio es público y está abierto a colaboraciones, sugerencias y forks. Cualquier discusión técnica, *issue* o *pull request* es bienvenida.
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
El dato más interesante del dataset no es un secuestro. Es un avión que fingió serlo durante treinta segundos por error.
|
| 15 |
+
|
| 16 |
+
El 31 de julio de 2017, un Airbus de Iberia voló sobre Madrid con el código de secuestro activado en el transpondedor durante tres puntos de su trayectoria. Después volvió a la normalidad. El vuelo era el IBE2845, el código transmitido era el 7500, y en el sistema secundario de vigilancia ese código significa interferencia ilícita.
|
| 17 |
+
|
| 18 |
+
No era un secuestro. Era casi con total seguridad un fallo del transpondedor: tres puntos sobre los 171 que componen el registro completo del vuelo, a 800 metros de la pista, sin más anomalía operativa. Aquel vuelo, anotado en un fichero parquet de 3,4 millones de filas, marca el inicio de este artículo. Y marca también la pregunta técnica que me llevé al proyecto: ¿puede un modelo aprender qué aspecto tiene un vuelo normal y avisar cuando uno deja de parecerlo?
|
| 19 |
+
|
| 20 |
+
Este artículo presenta SADAR, un sistema de detección de anomalías en trayectorias aéreas entrenado sobre dieciocho días de tráfico real procedente del aeropuerto Adolfo Suárez Madrid-Barajas. Compara tres autoencoders profundos contra un baseline clásico, evalúa el sistema sobre un banco de anomalías sintéticas y discute con franqueza dónde funciona y dónde no.
|
| 21 |
+
|
| 22 |
+
Empecemos por el problema.
|
| 23 |
+
|
| 24 |
+
## Planteamiento
|
| 25 |
+
|
| 26 |
+
La detección de incidentes en aviación civil rara vez puede formularse como un problema supervisado: los datos están dominados por operación normal y los pocos eventos etiquetados son ambiguos. La alternativa estándar es la **monitorización de conformidad**: aprender la distribución del comportamiento normal y señalar las trayectorias que se apartan de ella. Esta es la formulación que adopta SADAR.
|
| 27 |
+
|
| 28 |
+
La distinción importa. Decir "este vuelo se aparta del patrón aprendido" es defendible con datos. Decir "este vuelo es un incidente" requiere contexto operacional que el ADS-B no proporciona. SADAR opera estrictamente en el primer enunciado.
|
| 29 |
+
|
| 30 |
+
## Datos
|
| 31 |
+
|
| 32 |
+
Los datos provienen de OpenSky Network, una red colaborativa de receptores ADS-B. El conjunto cubre 18 días no consecutivos entre junio de 2017 y marzo de 2020, centrados en el espacio aéreo de Barajas. Cada aeronave reporta una posición cada 10 segundos, con 21 columnas por punto: posición y dinámica (latitud, longitud, altitud barométrica y geométrica, velocidad, rumbo, régimen vertical), identidad (callsign, icao24, flight_id) y estado del transpondedor (squawk, alert, onground).
|
| 33 |
+
|
| 34 |
+
Las cifras crudas, tras la unión y deduplicación:
|
| 35 |
+
|
| 36 |
+
- 3.429.638 filas.
|
| 37 |
+
- 19.057 vuelos distintos.
|
| 38 |
+
- Mediana de 175 puntos por vuelo, máximo 1.606.
|
| 39 |
+
- Mediana de duración de 30,2 minutos.
|
| 40 |
+
|
| 41 |
+
El primer hallazgo del análisis exploratorio fueron los huecos. La altitud GPS (`geoaltitude`) presenta un 24,79 % de valores ausentes. La velocidad y el rumbo, alrededor del 13 %. Estos huecos no son errores: corresponden a zonas de menor cobertura del receptor terrestre. Descarté la altitud GPS y me quedé con la barométrica, más completa, tratando los huecos como una propiedad del problema y no como un fallo a corregir.
|
| 42 |
+
|
| 43 |
+

|
| 44 |
+
|
| 45 |
+
El segundo hallazgo provino del histograma del rumbo. Representado en un gráfico polar, las dos pistas paralelas de Barajas aparecen como dos lóbulos opuestos.
|
| 46 |
+
|
| 47 |
+

|
| 48 |
+
|
| 49 |
+
El aeropuerto es identificable en los datos antes de proyectarlos sobre un mapa.
|
| 50 |
+
|
| 51 |
+
Esa propiedad circular del rumbo motivó su codificación como par seno y coseno: para un autoencoder, 359° y 1° deben quedar adyacentes, y una codificación lineal lo impide.
|
| 52 |
+
|
| 53 |
+

|
| 54 |
+
|
| 55 |
+
## Eventos reales en el conjunto
|
| 56 |
+
|
| 57 |
+
El dataset contiene únicamente cuatro vuelos con código de emergencia en el transpondedor. Conviene describirlos con detalle, porque su escasez determina el planteamiento del problema:
|
| 58 |
+
|
| 59 |
+
- **IBE2845** (Iberia, 31 de julio de 2017). Squawk 7500 durante tres puntos sobre 171, transmitido a 800 metros de pista. Compatible con un fallo del transpondedor.
|
| 60 |
+
- **ELY395** (El Al, 2 de octubre de 2017). Squawk 7700 durante cinco puntos, declarado a 800 metros de pista. El avión aterrizó. Es el caso más plausible de emergencia real.
|
| 61 |
+
- **RYR61AD** (Ryanair, 28 de enero de 2019). Squawk 7600 (fallo de comunicaciones). Un punto. Aterrizó.
|
| 62 |
+
- **SWR202Y** (Swiss, 3 de febrero de 2020). Squawk 7700. Un punto. Aterrizó.
|
| 63 |
+
|
| 64 |
+
Cuatro etiquetas en un universo de no etiquetas. Esta proporción descarta el aprendizaje supervisado: ningún clasificador se entrena con cuatro positivos. Los cuatro vuelos se reservaron como conjunto de validación cualitativa, agrupados en 1.076 ventanas etiquetadas en `data/processed/anomalies.npy`, y no aparecen en ningún momento del entrenamiento.
|
| 65 |
+
|
| 66 |
+
## Metodología
|
| 67 |
+
|
| 68 |
+
### Aprendizaje de una sola clase
|
| 69 |
+
|
| 70 |
+
Sin etiquetas suficientes, el planteamiento es **aprendizaje de una sola clase** mediante autoencoders. La red aprende a reconstruir trayectorias normales. El error de reconstrucción opera como score de anomalía: una entrada parecida a las del entrenamiento produce un error bajo; una entrada inusual lo eleva.
|
| 71 |
+
|
| 72 |
+
Entrené cuatro detectores bajo idénticas condiciones (mismo preprocesado, mismo split, mismo umbral) para permitir una comparación rigurosa:
|
| 73 |
+
|
| 74 |
+
| # | Modelo | Idea | Pesos |
|
| 75 |
+
|---|---|---|---|
|
| 76 |
+
| 0 | Isolation Forest | Estadísticas resumen por ventana, no secuencial | baseline |
|
| 77 |
+
| 1 | LSTM autoencoder | Reconstrucción secuencial | 224 KB |
|
| 78 |
+
| 2 | Transformer autoencoder | Atención sobre la ventana completa | 635 KB |
|
| 79 |
+
| 3 | VAE-LSTM | Codificación probabilística del espacio latente | 222 KB |
|
| 80 |
+
|
| 81 |
+
### Preprocesado
|
| 82 |
+
|
| 83 |
+
Cuatro decisiones de preprocesado merecen mención específica porque determinan la calidad final de un detector de trayectorias.
|
| 84 |
+
|
| 85 |
+
**Coordenadas métricas relativas a la pista.** Latitud y longitud en grados resultan inadecuadas para modelar movimiento sobre un aeropuerto: las distancias dependen de la latitud, y el modelo no incorpora geodesia. El pipeline proyecta las coordenadas a UTM zona 30 (EPSG:32630), centradas en la pista de Barajas, y opera en metros.
|
| 86 |
+
|
| 87 |
+
**Rumbo como par (sin, cos).** Variable circular que la codificación lineal no representa con fidelidad. La codificación trigonométrica asegura que valores próximos en el círculo lo sean también en el espacio de features.
|
| 88 |
+
|
| 89 |
+
**Split por fecha y por flight_id.** Entrenamiento sobre 2017-2019, validación y test sobre 2020. Cada vuelo aparece en un único split. Esto bloquea simultáneamente la fuga temporal y la de instancia.
|
| 90 |
+
|
| 91 |
+
**Limpieza del conjunto de entrenamiento.** Excluí del entrenamiento los 100 vuelos con patrón de go-around y los cuatro con squawk de emergencia. El conjunto de entrenamiento debe representar la versión más conservadora de "vuelo normal". Mantener go-arounds dentro hace que el modelo aprenda a reconstruirlos sin dificultad, anulando su valor como anomalías.
|
| 92 |
+
|
| 93 |
+
Las ventanas finales tienen 60 pasos de 10 segundos (10 minutos por ventana), con solapamiento del 50 %. Siete features por paso:
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
[x_rel, y_rel, baroaltitude, velocity, sin_hdg, cos_hdg, vertrate]
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
El conjunto resultante: 61.008 ventanas de entrenamiento, 7.679 de validación, 7.788 de test y 1.076 etiquetadas como anómalas. El escalador estandariza las features ajustándose únicamente sobre el conjunto de entrenamiento.
|
| 100 |
+
|
| 101 |
+
### Arquitecturas
|
| 102 |
+
|
| 103 |
+
**Isolation Forest** sobre estadísticas resumen (media, desviación, mínimo y máximo por feature). Baseline no secuencial cuya función es establecer la línea de referencia que el deep learning debe superar.
|
| 104 |
+
|
| 105 |
+
**LSTM autoencoder**: encoder con una capa LSTM (hidden 64), cuello de botella lineal a un latente de dimensión 16, decoder simétrico que desenrolla desde el latente repetido.
|
| 106 |
+
|
| 107 |
+
**Transformer autoencoder**: dos capas de encoder con cuatro cabezas de atención (d_model 64), codificación posicional sinusoidal, mismo cuello de botella a latente 16.
|
| 108 |
+
|
| 109 |
+
**VAE-LSTM**: encoder LSTM que produce la media y el logaritmo de la varianza, reparametrización para que los gradientes fluyan a través del muestreo, decoder LSTM desde z. La pérdida combina el error de reconstrucción (MSE) con la divergencia de Kullback-Leibler ponderada por un coeficiente beta de 0,0001193, seleccionado por Optuna.
|
| 110 |
+
|
| 111 |
+
```python
|
| 112 |
+
std = torch.exp(0.5 * logvar)
|
| 113 |
+
eps = torch.randn_like(std)
|
| 114 |
+
z = mu + std * eps
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Todos los modelos comparten el mismo loop de entrenamiento: optimizador Adam con weight decay, scheduler ReduceLROnPlateau, early stopping con paciencia 7, semilla fija a 42, tracking completo con MLflow.
|
| 118 |
+
|
| 119 |
+
### Banco de anomalías sintéticas
|
| 120 |
+
|
| 121 |
+
Cuatro eventos reales no permiten una evaluación cuantitativa estable. Para suplir esta limitación, se inyectan anomalías controladas sobre vuelos normales del conjunto de test. Cinco familias, todas con rampa lineal de inicio para permitir la medición de la **latencia de detección** (segundos desde el inicio de la perturbación hasta el primer cruce del umbral):
|
| 122 |
+
|
| 123 |
+
- **Desviación de ruta**: deformación lateral, magnitudes de 20, 40 y 80 km.
|
| 124 |
+
- **Anomalía de altitud**: offset de 300, 800 o 1.500 m.
|
| 125 |
+
- **Anomalía de velocidad**: factores multiplicativos de 0,4x, 1,6x y 2,2x.
|
| 126 |
+
- **Holding**: giros continuos con período de 120 o 240 segundos.
|
| 127 |
+
- **Congelación del transpondedor**: el ADS-B retiene el último valor durante el resto de la ventana.
|
| 128 |
+
|
| 129 |
+
La validación contra un simulador propio podría parecer circular. Es una práctica estándar en *safety engineering*: el simulador se diseñó antes que los modelos y describe modos de fallo bien definidos, no patrones aprendidos del modelo. El conjunto produce 12 variantes (combinaciones de tipo e intensidad) y 24.000 ventanas sintéticas por modelo evaluado.
|
| 130 |
+
|
| 131 |
+
Hasta aquí, el planteamiento. Lo siguiente es ver si funciona.
|
| 132 |
+
|
| 133 |
+
## Resultados
|
| 134 |
+
|
| 135 |
+
Resultados sobre el conjunto de test de 2020:
|
| 136 |
+
|
| 137 |
+
| Modelo | ROC-AUC real | PR-AUC real | ROC sintético | Latencia mediana |
|
| 138 |
+
|---|---|---|---|---|
|
| 139 |
+
| Isolation Forest | 0,515 | 0,133 | 0,593 | n/d |
|
| 140 |
+
| LSTM-AE | 0,648 | 0,260 | 0,779 | 115 s |
|
| 141 |
+
| Transformer-AE | 0,614 | 0,227 | 0,743 | 115 s |
|
| 142 |
+
| **VAE-LSTM** | **0,659** | **0,299** | **0,792** | 120 s |
|
| 143 |
+
|
| 144 |
+
Los modelos profundos prácticamente duplican la PR-AUC del baseline. Esta diferencia justifica la inversión en deep learning: si el LSTM no hubiera batido al Isolation Forest, el resto del trabajo no habría tenido sentido.
|
| 145 |
+
|
| 146 |
+

|
| 147 |
+
|
| 148 |
+

|
| 149 |
+
|
| 150 |
+
Una observación merece atención.
|
| 151 |
+
|
| 152 |
+
El Transformer ganó la pérdida de validación (0,038 frente a 0,053 del VAE y 0,072 del LSTM). Y perdió la PR-AUC. Algo no cuadraba.
|
| 153 |
+
|
| 154 |
+
La explicación es directa: una arquitectura con suficiente capacidad reconstruye también con cierta fidelidad las anomalías, lo que reduce la separabilidad entre normal y anómalo medida por el error de reconstrucción. En detección por reconstrucción, la pérdida de validación es un mal proxy del rendimiento. Lo sabemos y aun así la seguimos mirando.
|
| 155 |
+
|
| 156 |
+
Evalué además dos ensembles, por promedio y por máximo de los scores z-normalizados. Ninguno superó al VAE individual (PR-AUC 0,273 y 0,278 frente a 0,299), y los descarté. El ensemble fue un intento de cerrar con un empujón. No lo dio. Es honesto reportarlo.
|
| 157 |
+
|
| 158 |
+
Los números cuentan una parte. La otra está en lo que no sale en la tabla.
|
| 159 |
+
|
| 160 |
+
## Discusión
|
| 161 |
+
|
| 162 |
+
Los resultados muestran un perfil de detección **heterogéneo por tipo de anomalía**. El modelo final detecta con facilidad las perturbaciones ruidosas: ROC-AUC 0,984 en holding con periodo de 120 s, 0,989 en aceleración con factor 2,2, 0,899 en desviación de ruta de 80 km. Las perturbaciones sutiles (300 m de altitud, derivas pequeñas) requieren tiempos cercanos a los dos minutos y bajan al rango 0,51-0,55.
|
| 163 |
+
|
| 164 |
+
El caso más relevante de la discusión es la **congelación del transpondedor**.
|
| 165 |
+
|
| 166 |
+
El VAE lo detecta en el 1,2 % de los casos. Uno de cada ochenta intentos.
|
| 167 |
+
|
| 168 |
+
La explicación es estructural: un avión en crucero estable cuyos valores se congelan produce un patrón muy próximo al de un avión en crucero estable que continúa transmitiendo. El error de reconstrucción permanece bajo y el score no llega a cruzar el umbral. Es una limitación del enfoque de reconstrucción, no del entrenamiento.
|
| 169 |
+
|
| 170 |
+

|
| 171 |
+
|
| 172 |
+
Esta heterogeneidad delimita el alcance del sistema. SADAR es un detector de **maniobras anómalas**, no un detector universal de incidentes. Su valor operativo está en aquellas desviaciones que la red de reglas tradicional (sistemas tipo STCA o MSAW) no cubre, no en sustituir esos sistemas.
|
| 173 |
+
|
| 174 |
+
Antes de cerrar, conviene ser explícito en lo que el sistema no hace.
|
| 175 |
+
|
| 176 |
+
## Limitaciones
|
| 177 |
+
|
| 178 |
+
El sistema asume ciertas limitaciones documentadas explícitamente en el informe técnico.
|
| 179 |
+
|
| 180 |
+
- **Ausencia de planes de vuelo**. SADAR no dispone de la ruta prevista de cada vuelo. La "desviación respecto a la ruta esperada" se aproxima por "desviación respecto al patrón medio". Un vuelo que se aparte de su plan asignado pero permanezca dentro del patrón medio no será marcado.
|
| 181 |
+
- **Cobertura temporal**. El conjunto consta de 18 días no consecutivos entre 2017 y 2020. Modelar demanda, estacionalidad o predicción temporal continua queda fuera del alcance.
|
| 182 |
+
- **Congelación del transpondedor**. Tasa de detección dentro de ventana del 1,2 %. Cualquier extensión seria del sistema debe atacar este caso de forma específica (señales derivadas, detección de cambio de régimen, modelo dedicado).
|
| 183 |
+
- **Huecos de cobertura**. El 24,2 % de los vuelos en aire presentan gaps superiores a 120 segundos en la recepción. Estos huecos son artefactos del receptor, no incidentes operativos. Tratarlos como etiquetas positivas generaría ruido sistemático.
|
| 184 |
+
|
| 185 |
+
## Sistema desplegado
|
| 186 |
+
|
| 187 |
+
El modelo final se sirve a través de una API construida sobre FastAPI que monta un frontend React + Vite + TypeScript en el mismo puerto, lo que permite un despliegue *single-image* en Hugging Face Spaces. El dashboard consta de tres pantallas:
|
| 188 |
+
|
| 189 |
+
1. **Tower console**: radar en vivo sobre Barajas con los vuelos del día y sus scores, alerta visual al cruzar el umbral.
|
| 190 |
+
2. **Simulator**: inyección controlada de anomalías sobre un vuelo de referencia, con visualización de la trayectoria deformada, el score y la latencia.
|
| 191 |
+
3. **Metrics**: tabla comparativa de modelos, curvas precision-recall y ROC, matrices de confusión, desglose por tipo de anomalía.
|
| 192 |
+
|
| 193 |
+
## Stack y reproducibilidad
|
| 194 |
+
|
| 195 |
+
Python 3.11, PyTorch, FastAPI, MLflow y Optuna en el backend. Vite, React 18 y TypeScript en el frontend. Empaquetado en Docker *single-image*. Reproducibilidad garantizada por semillas fijas, configuraciones YAML que resuelven variables de entorno, y los checkpoints almacenados también como artifacts de MLflow.
|
| 196 |
+
|
| 197 |
+
## Conclusiones y trabajo futuro
|
| 198 |
+
|
| 199 |
+
SADAR demuestra que la monitorización de conformidad de trayectorias aéreas es abordable mediante autoencoders profundos entrenados exclusivamente sobre operación normal, con métricas defendibles y un protocolo de evaluación reproducible. La comparación entre tres familias de modelos bajo idénticas condiciones permite justificar la elección del VAE-LSTM no solo por su PR-AUC final, sino por su perfil global de detección y latencia.
|
| 200 |
+
|
| 201 |
+
El Transformer no fue la elección correcta para este problema. Lo dejo documentado porque los descartes también cuentan.
|
| 202 |
+
|
| 203 |
+
El trabajo futuro inmediato apunta a tres líneas. Primero, sustituir el Transformer por un TCN o un autoencoder convolucional para acotar la capacidad sin perder modelado temporal. Segundo, incorporar derivadas y detección de cambio de régimen para atacar la congelación del transpondedor. Tercero, evaluar la incorporación de planes de vuelo cuando estén disponibles, lo que replantearía el problema como predicción condicionada por el plan en lugar de aproximación al patrón medio.
|
| 204 |
+
|
| 205 |
+
El IBE2845 aterrizó sin incidentes el 31 de julio de 2017 y sus tres puntos en rojo no recibieron mayor atención. Glitch, anomalía o incidente, la realidad rara vez llega etiquetada, y construir sistemas que operen en ese terreno empieza por aceptarlo.
|
frontend/.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.vite
|
| 4 |
+
.DS_Store
|
| 5 |
+
.env
|
| 6 |
+
.env.*
|
| 7 |
+
Dockerfile
|
| 8 |
+
.dockerignore
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
dist-ssr
|
| 4 |
+
*.local
|
| 5 |
+
.vite
|
frontend/Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS builder
|
| 2 |
+
|
| 3 |
+
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
| 4 |
+
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
COPY package.json pnpm-lock.yaml ./
|
| 8 |
+
|
| 9 |
+
RUN pnpm install --frozen-lockfile
|
| 10 |
+
|
| 11 |
+
COPY . .
|
| 12 |
+
|
| 13 |
+
ARG VITE_API_BASE=/api
|
| 14 |
+
ENV VITE_API_BASE=$VITE_API_BASE
|
| 15 |
+
|
| 16 |
+
RUN pnpm build
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
FROM nginx:1.27-alpine AS runtime
|
| 20 |
+
|
| 21 |
+
COPY --from=builder /app/dist /usr/share/nginx/html
|
| 22 |
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
| 23 |
+
|
| 24 |
+
EXPOSE 80
|
| 25 |
+
|
| 26 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
| 27 |
+
CMD wget -q -O - http://localhost/ >/dev/null || exit 1
|
frontend/index.html
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<link rel="icon" type="image/png" href="/sadar-mark.png" />
|
| 7 |
+
<link rel="apple-touch-icon" href="/sadar-mark.png" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link
|
| 11 |
+
href="https://fonts.googleapis.com/css2?family=B612+Mono:wght@400;700&display=swap"
|
| 12 |
+
rel="stylesheet"
|
| 13 |
+
/>
|
| 14 |
+
<title>SADAR Flight Conformance Monitor</title>
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<div id="root"></div>
|
| 18 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 19 |
+
</body>
|
| 20 |
+
</html>
|
frontend/nginx.conf
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
server {
|
| 2 |
+
listen 80;
|
| 3 |
+
server_name _;
|
| 4 |
+
|
| 5 |
+
root /usr/share/nginx/html;
|
| 6 |
+
index index.html;
|
| 7 |
+
|
| 8 |
+
gzip on;
|
| 9 |
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
| 10 |
+
gzip_min_length 1024;
|
| 11 |
+
|
| 12 |
+
location /api/ {
|
| 13 |
+
proxy_pass http://backend:8000;
|
| 14 |
+
proxy_http_version 1.1;
|
| 15 |
+
proxy_set_header Host $host;
|
| 16 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 17 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 18 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 19 |
+
proxy_read_timeout 120s;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
location / {
|
| 23 |
+
try_files $uri $uri/ /index.html;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
location ~* \.(?:js|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|gif|webp|ico|map)$ {
|
| 27 |
+
expires 7d;
|
| 28 |
+
access_log off;
|
| 29 |
+
add_header Cache-Control "public, max-age=604800, immutable";
|
| 30 |
+
}
|
| 31 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sadar-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.1.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"typecheck": "tsc --noEmit"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"framer-motion": "^11.5.4",
|
| 14 |
+
"react": "^18.3.1",
|
| 15 |
+
"react-dom": "^18.3.1",
|
| 16 |
+
"react-router-dom": "^6.26.2"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@types/react": "^18.3.10",
|
| 20 |
+
"@types/react-dom": "^18.3.0",
|
| 21 |
+
"@vitejs/plugin-react": "^4.3.2",
|
| 22 |
+
"typescript": "^5.6.2",
|
| 23 |
+
"vite": "^5.4.8"
|
| 24 |
+
},
|
| 25 |
+
"pnpm": {
|
| 26 |
+
"onlyBuiltDependencies": ["esbuild"]
|
| 27 |
+
}
|
| 28 |
+
}
|
frontend/pnpm-lock.yaml
ADDED
|
@@ -0,0 +1,1160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lockfileVersion: '9.0'
|
| 2 |
+
|
| 3 |
+
settings:
|
| 4 |
+
autoInstallPeers: true
|
| 5 |
+
excludeLinksFromLockfile: false
|
| 6 |
+
|
| 7 |
+
importers:
|
| 8 |
+
|
| 9 |
+
.:
|
| 10 |
+
dependencies:
|
| 11 |
+
framer-motion:
|
| 12 |
+
specifier: ^11.5.4
|
| 13 |
+
version: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
| 14 |
+
react:
|
| 15 |
+
specifier: ^18.3.1
|
| 16 |
+
version: 18.3.1
|
| 17 |
+
react-dom:
|
| 18 |
+
specifier: ^18.3.1
|
| 19 |
+
version: 18.3.1(react@18.3.1)
|
| 20 |
+
react-router-dom:
|
| 21 |
+
specifier: ^6.26.2
|
| 22 |
+
version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
| 23 |
+
devDependencies:
|
| 24 |
+
'@types/react':
|
| 25 |
+
specifier: ^18.3.10
|
| 26 |
+
version: 18.3.29
|
| 27 |
+
'@types/react-dom':
|
| 28 |
+
specifier: ^18.3.0
|
| 29 |
+
version: 18.3.7(@types/react@18.3.29)
|
| 30 |
+
'@vitejs/plugin-react':
|
| 31 |
+
specifier: ^4.3.2
|
| 32 |
+
version: 4.7.0(vite@5.4.21)
|
| 33 |
+
typescript:
|
| 34 |
+
specifier: ^5.6.2
|
| 35 |
+
version: 5.9.3
|
| 36 |
+
vite:
|
| 37 |
+
specifier: ^5.4.8
|
| 38 |
+
version: 5.4.21
|
| 39 |
+
|
| 40 |
+
packages:
|
| 41 |
+
|
| 42 |
+
'@babel/code-frame@7.29.7':
|
| 43 |
+
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
| 44 |
+
engines: {node: '>=6.9.0'}
|
| 45 |
+
|
| 46 |
+
'@babel/compat-data@7.29.7':
|
| 47 |
+
resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
|
| 48 |
+
engines: {node: '>=6.9.0'}
|
| 49 |
+
|
| 50 |
+
'@babel/core@7.29.7':
|
| 51 |
+
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
|
| 52 |
+
engines: {node: '>=6.9.0'}
|
| 53 |
+
|
| 54 |
+
'@babel/generator@7.29.7':
|
| 55 |
+
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
|
| 56 |
+
engines: {node: '>=6.9.0'}
|
| 57 |
+
|
| 58 |
+
'@babel/helper-compilation-targets@7.29.7':
|
| 59 |
+
resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
|
| 60 |
+
engines: {node: '>=6.9.0'}
|
| 61 |
+
|
| 62 |
+
'@babel/helper-globals@7.29.7':
|
| 63 |
+
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
| 64 |
+
engines: {node: '>=6.9.0'}
|
| 65 |
+
|
| 66 |
+
'@babel/helper-module-imports@7.29.7':
|
| 67 |
+
resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
|
| 68 |
+
engines: {node: '>=6.9.0'}
|
| 69 |
+
|
| 70 |
+
'@babel/helper-module-transforms@7.29.7':
|
| 71 |
+
resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
|
| 72 |
+
engines: {node: '>=6.9.0'}
|
| 73 |
+
peerDependencies:
|
| 74 |
+
'@babel/core': ^7.0.0
|
| 75 |
+
|
| 76 |
+
'@babel/helper-plugin-utils@7.29.7':
|
| 77 |
+
resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
|
| 78 |
+
engines: {node: '>=6.9.0'}
|
| 79 |
+
|
| 80 |
+
'@babel/helper-string-parser@7.29.7':
|
| 81 |
+
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
| 82 |
+
engines: {node: '>=6.9.0'}
|
| 83 |
+
|
| 84 |
+
'@babel/helper-validator-identifier@7.29.7':
|
| 85 |
+
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
| 86 |
+
engines: {node: '>=6.9.0'}
|
| 87 |
+
|
| 88 |
+
'@babel/helper-validator-option@7.29.7':
|
| 89 |
+
resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
|
| 90 |
+
engines: {node: '>=6.9.0'}
|
| 91 |
+
|
| 92 |
+
'@babel/helpers@7.29.7':
|
| 93 |
+
resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
|
| 94 |
+
engines: {node: '>=6.9.0'}
|
| 95 |
+
|
| 96 |
+
'@babel/parser@7.29.7':
|
| 97 |
+
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
| 98 |
+
engines: {node: '>=6.0.0'}
|
| 99 |
+
hasBin: true
|
| 100 |
+
|
| 101 |
+
'@babel/plugin-transform-react-jsx-self@7.29.7':
|
| 102 |
+
resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
|
| 103 |
+
engines: {node: '>=6.9.0'}
|
| 104 |
+
peerDependencies:
|
| 105 |
+
'@babel/core': ^7.0.0-0
|
| 106 |
+
|
| 107 |
+
'@babel/plugin-transform-react-jsx-source@7.29.7':
|
| 108 |
+
resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
|
| 109 |
+
engines: {node: '>=6.9.0'}
|
| 110 |
+
peerDependencies:
|
| 111 |
+
'@babel/core': ^7.0.0-0
|
| 112 |
+
|
| 113 |
+
'@babel/template@7.29.7':
|
| 114 |
+
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
| 115 |
+
engines: {node: '>=6.9.0'}
|
| 116 |
+
|
| 117 |
+
'@babel/traverse@7.29.7':
|
| 118 |
+
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
|
| 119 |
+
engines: {node: '>=6.9.0'}
|
| 120 |
+
|
| 121 |
+
'@babel/types@7.29.7':
|
| 122 |
+
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
| 123 |
+
engines: {node: '>=6.9.0'}
|
| 124 |
+
|
| 125 |
+
'@esbuild/aix-ppc64@0.21.5':
|
| 126 |
+
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
| 127 |
+
engines: {node: '>=12'}
|
| 128 |
+
cpu: [ppc64]
|
| 129 |
+
os: [aix]
|
| 130 |
+
|
| 131 |
+
'@esbuild/android-arm64@0.21.5':
|
| 132 |
+
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
|
| 133 |
+
engines: {node: '>=12'}
|
| 134 |
+
cpu: [arm64]
|
| 135 |
+
os: [android]
|
| 136 |
+
|
| 137 |
+
'@esbuild/android-arm@0.21.5':
|
| 138 |
+
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
|
| 139 |
+
engines: {node: '>=12'}
|
| 140 |
+
cpu: [arm]
|
| 141 |
+
os: [android]
|
| 142 |
+
|
| 143 |
+
'@esbuild/android-x64@0.21.5':
|
| 144 |
+
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
|
| 145 |
+
engines: {node: '>=12'}
|
| 146 |
+
cpu: [x64]
|
| 147 |
+
os: [android]
|
| 148 |
+
|
| 149 |
+
'@esbuild/darwin-arm64@0.21.5':
|
| 150 |
+
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
|
| 151 |
+
engines: {node: '>=12'}
|
| 152 |
+
cpu: [arm64]
|
| 153 |
+
os: [darwin]
|
| 154 |
+
|
| 155 |
+
'@esbuild/darwin-x64@0.21.5':
|
| 156 |
+
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
|
| 157 |
+
engines: {node: '>=12'}
|
| 158 |
+
cpu: [x64]
|
| 159 |
+
os: [darwin]
|
| 160 |
+
|
| 161 |
+
'@esbuild/freebsd-arm64@0.21.5':
|
| 162 |
+
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
|
| 163 |
+
engines: {node: '>=12'}
|
| 164 |
+
cpu: [arm64]
|
| 165 |
+
os: [freebsd]
|
| 166 |
+
|
| 167 |
+
'@esbuild/freebsd-x64@0.21.5':
|
| 168 |
+
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
|
| 169 |
+
engines: {node: '>=12'}
|
| 170 |
+
cpu: [x64]
|
| 171 |
+
os: [freebsd]
|
| 172 |
+
|
| 173 |
+
'@esbuild/linux-arm64@0.21.5':
|
| 174 |
+
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
|
| 175 |
+
engines: {node: '>=12'}
|
| 176 |
+
cpu: [arm64]
|
| 177 |
+
os: [linux]
|
| 178 |
+
|
| 179 |
+
'@esbuild/linux-arm@0.21.5':
|
| 180 |
+
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
|
| 181 |
+
engines: {node: '>=12'}
|
| 182 |
+
cpu: [arm]
|
| 183 |
+
os: [linux]
|
| 184 |
+
|
| 185 |
+
'@esbuild/linux-ia32@0.21.5':
|
| 186 |
+
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
|
| 187 |
+
engines: {node: '>=12'}
|
| 188 |
+
cpu: [ia32]
|
| 189 |
+
os: [linux]
|
| 190 |
+
|
| 191 |
+
'@esbuild/linux-loong64@0.21.5':
|
| 192 |
+
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
|
| 193 |
+
engines: {node: '>=12'}
|
| 194 |
+
cpu: [loong64]
|
| 195 |
+
os: [linux]
|
| 196 |
+
|
| 197 |
+
'@esbuild/linux-mips64el@0.21.5':
|
| 198 |
+
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
|
| 199 |
+
engines: {node: '>=12'}
|
| 200 |
+
cpu: [mips64el]
|
| 201 |
+
os: [linux]
|
| 202 |
+
|
| 203 |
+
'@esbuild/linux-ppc64@0.21.5':
|
| 204 |
+
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
|
| 205 |
+
engines: {node: '>=12'}
|
| 206 |
+
cpu: [ppc64]
|
| 207 |
+
os: [linux]
|
| 208 |
+
|
| 209 |
+
'@esbuild/linux-riscv64@0.21.5':
|
| 210 |
+
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
|
| 211 |
+
engines: {node: '>=12'}
|
| 212 |
+
cpu: [riscv64]
|
| 213 |
+
os: [linux]
|
| 214 |
+
|
| 215 |
+
'@esbuild/linux-s390x@0.21.5':
|
| 216 |
+
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
|
| 217 |
+
engines: {node: '>=12'}
|
| 218 |
+
cpu: [s390x]
|
| 219 |
+
os: [linux]
|
| 220 |
+
|
| 221 |
+
'@esbuild/linux-x64@0.21.5':
|
| 222 |
+
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
|
| 223 |
+
engines: {node: '>=12'}
|
| 224 |
+
cpu: [x64]
|
| 225 |
+
os: [linux]
|
| 226 |
+
|
| 227 |
+
'@esbuild/netbsd-x64@0.21.5':
|
| 228 |
+
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
|
| 229 |
+
engines: {node: '>=12'}
|
| 230 |
+
cpu: [x64]
|
| 231 |
+
os: [netbsd]
|
| 232 |
+
|
| 233 |
+
'@esbuild/openbsd-x64@0.21.5':
|
| 234 |
+
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
|
| 235 |
+
engines: {node: '>=12'}
|
| 236 |
+
cpu: [x64]
|
| 237 |
+
os: [openbsd]
|
| 238 |
+
|
| 239 |
+
'@esbuild/sunos-x64@0.21.5':
|
| 240 |
+
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
|
| 241 |
+
engines: {node: '>=12'}
|
| 242 |
+
cpu: [x64]
|
| 243 |
+
os: [sunos]
|
| 244 |
+
|
| 245 |
+
'@esbuild/win32-arm64@0.21.5':
|
| 246 |
+
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
|
| 247 |
+
engines: {node: '>=12'}
|
| 248 |
+
cpu: [arm64]
|
| 249 |
+
os: [win32]
|
| 250 |
+
|
| 251 |
+
'@esbuild/win32-ia32@0.21.5':
|
| 252 |
+
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
|
| 253 |
+
engines: {node: '>=12'}
|
| 254 |
+
cpu: [ia32]
|
| 255 |
+
os: [win32]
|
| 256 |
+
|
| 257 |
+
'@esbuild/win32-x64@0.21.5':
|
| 258 |
+
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
|
| 259 |
+
engines: {node: '>=12'}
|
| 260 |
+
cpu: [x64]
|
| 261 |
+
os: [win32]
|
| 262 |
+
|
| 263 |
+
'@jridgewell/gen-mapping@0.3.13':
|
| 264 |
+
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
| 265 |
+
|
| 266 |
+
'@jridgewell/remapping@2.3.5':
|
| 267 |
+
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
| 268 |
+
|
| 269 |
+
'@jridgewell/resolve-uri@3.1.2':
|
| 270 |
+
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
| 271 |
+
engines: {node: '>=6.0.0'}
|
| 272 |
+
|
| 273 |
+
'@jridgewell/sourcemap-codec@1.5.5':
|
| 274 |
+
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
| 275 |
+
|
| 276 |
+
'@jridgewell/trace-mapping@0.3.31':
|
| 277 |
+
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
| 278 |
+
|
| 279 |
+
'@remix-run/router@1.23.3':
|
| 280 |
+
resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==}
|
| 281 |
+
engines: {node: '>=14.0.0'}
|
| 282 |
+
|
| 283 |
+
'@rolldown/pluginutils@1.0.0-beta.27':
|
| 284 |
+
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
| 285 |
+
|
| 286 |
+
'@rollup/rollup-android-arm-eabi@4.60.4':
|
| 287 |
+
resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==}
|
| 288 |
+
cpu: [arm]
|
| 289 |
+
os: [android]
|
| 290 |
+
|
| 291 |
+
'@rollup/rollup-android-arm64@4.60.4':
|
| 292 |
+
resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==}
|
| 293 |
+
cpu: [arm64]
|
| 294 |
+
os: [android]
|
| 295 |
+
|
| 296 |
+
'@rollup/rollup-darwin-arm64@4.60.4':
|
| 297 |
+
resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==}
|
| 298 |
+
cpu: [arm64]
|
| 299 |
+
os: [darwin]
|
| 300 |
+
|
| 301 |
+
'@rollup/rollup-darwin-x64@4.60.4':
|
| 302 |
+
resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==}
|
| 303 |
+
cpu: [x64]
|
| 304 |
+
os: [darwin]
|
| 305 |
+
|
| 306 |
+
'@rollup/rollup-freebsd-arm64@4.60.4':
|
| 307 |
+
resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==}
|
| 308 |
+
cpu: [arm64]
|
| 309 |
+
os: [freebsd]
|
| 310 |
+
|
| 311 |
+
'@rollup/rollup-freebsd-x64@4.60.4':
|
| 312 |
+
resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==}
|
| 313 |
+
cpu: [x64]
|
| 314 |
+
os: [freebsd]
|
| 315 |
+
|
| 316 |
+
'@rollup/rollup-linux-arm-gnueabihf@4.60.4':
|
| 317 |
+
resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==}
|
| 318 |
+
cpu: [arm]
|
| 319 |
+
os: [linux]
|
| 320 |
+
libc: [glibc]
|
| 321 |
+
|
| 322 |
+
'@rollup/rollup-linux-arm-musleabihf@4.60.4':
|
| 323 |
+
resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==}
|
| 324 |
+
cpu: [arm]
|
| 325 |
+
os: [linux]
|
| 326 |
+
libc: [musl]
|
| 327 |
+
|
| 328 |
+
'@rollup/rollup-linux-arm64-gnu@4.60.4':
|
| 329 |
+
resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==}
|
| 330 |
+
cpu: [arm64]
|
| 331 |
+
os: [linux]
|
| 332 |
+
libc: [glibc]
|
| 333 |
+
|
| 334 |
+
'@rollup/rollup-linux-arm64-musl@4.60.4':
|
| 335 |
+
resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==}
|
| 336 |
+
cpu: [arm64]
|
| 337 |
+
os: [linux]
|
| 338 |
+
libc: [musl]
|
| 339 |
+
|
| 340 |
+
'@rollup/rollup-linux-loong64-gnu@4.60.4':
|
| 341 |
+
resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==}
|
| 342 |
+
cpu: [loong64]
|
| 343 |
+
os: [linux]
|
| 344 |
+
libc: [glibc]
|
| 345 |
+
|
| 346 |
+
'@rollup/rollup-linux-loong64-musl@4.60.4':
|
| 347 |
+
resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==}
|
| 348 |
+
cpu: [loong64]
|
| 349 |
+
os: [linux]
|
| 350 |
+
libc: [musl]
|
| 351 |
+
|
| 352 |
+
'@rollup/rollup-linux-ppc64-gnu@4.60.4':
|
| 353 |
+
resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==}
|
| 354 |
+
cpu: [ppc64]
|
| 355 |
+
os: [linux]
|
| 356 |
+
libc: [glibc]
|
| 357 |
+
|
| 358 |
+
'@rollup/rollup-linux-ppc64-musl@4.60.4':
|
| 359 |
+
resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==}
|
| 360 |
+
cpu: [ppc64]
|
| 361 |
+
os: [linux]
|
| 362 |
+
libc: [musl]
|
| 363 |
+
|
| 364 |
+
'@rollup/rollup-linux-riscv64-gnu@4.60.4':
|
| 365 |
+
resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==}
|
| 366 |
+
cpu: [riscv64]
|
| 367 |
+
os: [linux]
|
| 368 |
+
libc: [glibc]
|
| 369 |
+
|
| 370 |
+
'@rollup/rollup-linux-riscv64-musl@4.60.4':
|
| 371 |
+
resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==}
|
| 372 |
+
cpu: [riscv64]
|
| 373 |
+
os: [linux]
|
| 374 |
+
libc: [musl]
|
| 375 |
+
|
| 376 |
+
'@rollup/rollup-linux-s390x-gnu@4.60.4':
|
| 377 |
+
resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==}
|
| 378 |
+
cpu: [s390x]
|
| 379 |
+
os: [linux]
|
| 380 |
+
libc: [glibc]
|
| 381 |
+
|
| 382 |
+
'@rollup/rollup-linux-x64-gnu@4.60.4':
|
| 383 |
+
resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==}
|
| 384 |
+
cpu: [x64]
|
| 385 |
+
os: [linux]
|
| 386 |
+
libc: [glibc]
|
| 387 |
+
|
| 388 |
+
'@rollup/rollup-linux-x64-musl@4.60.4':
|
| 389 |
+
resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==}
|
| 390 |
+
cpu: [x64]
|
| 391 |
+
os: [linux]
|
| 392 |
+
libc: [musl]
|
| 393 |
+
|
| 394 |
+
'@rollup/rollup-openbsd-x64@4.60.4':
|
| 395 |
+
resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==}
|
| 396 |
+
cpu: [x64]
|
| 397 |
+
os: [openbsd]
|
| 398 |
+
|
| 399 |
+
'@rollup/rollup-openharmony-arm64@4.60.4':
|
| 400 |
+
resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==}
|
| 401 |
+
cpu: [arm64]
|
| 402 |
+
os: [openharmony]
|
| 403 |
+
|
| 404 |
+
'@rollup/rollup-win32-arm64-msvc@4.60.4':
|
| 405 |
+
resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==}
|
| 406 |
+
cpu: [arm64]
|
| 407 |
+
os: [win32]
|
| 408 |
+
|
| 409 |
+
'@rollup/rollup-win32-ia32-msvc@4.60.4':
|
| 410 |
+
resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==}
|
| 411 |
+
cpu: [ia32]
|
| 412 |
+
os: [win32]
|
| 413 |
+
|
| 414 |
+
'@rollup/rollup-win32-x64-gnu@4.60.4':
|
| 415 |
+
resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==}
|
| 416 |
+
cpu: [x64]
|
| 417 |
+
os: [win32]
|
| 418 |
+
|
| 419 |
+
'@rollup/rollup-win32-x64-msvc@4.60.4':
|
| 420 |
+
resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==}
|
| 421 |
+
cpu: [x64]
|
| 422 |
+
os: [win32]
|
| 423 |
+
|
| 424 |
+
'@types/babel__core@7.20.5':
|
| 425 |
+
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
| 426 |
+
|
| 427 |
+
'@types/babel__generator@7.27.0':
|
| 428 |
+
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
|
| 429 |
+
|
| 430 |
+
'@types/babel__template@7.4.4':
|
| 431 |
+
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
|
| 432 |
+
|
| 433 |
+
'@types/babel__traverse@7.28.0':
|
| 434 |
+
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
| 435 |
+
|
| 436 |
+
'@types/estree@1.0.8':
|
| 437 |
+
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
| 438 |
+
|
| 439 |
+
'@types/prop-types@15.7.15':
|
| 440 |
+
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
| 441 |
+
|
| 442 |
+
'@types/react-dom@18.3.7':
|
| 443 |
+
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
|
| 444 |
+
peerDependencies:
|
| 445 |
+
'@types/react': ^18.0.0
|
| 446 |
+
|
| 447 |
+
'@types/react@18.3.29':
|
| 448 |
+
resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==}
|
| 449 |
+
|
| 450 |
+
'@vitejs/plugin-react@4.7.0':
|
| 451 |
+
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
| 452 |
+
engines: {node: ^14.18.0 || >=16.0.0}
|
| 453 |
+
peerDependencies:
|
| 454 |
+
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
| 455 |
+
|
| 456 |
+
baseline-browser-mapping@2.10.32:
|
| 457 |
+
resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==}
|
| 458 |
+
engines: {node: '>=6.0.0'}
|
| 459 |
+
hasBin: true
|
| 460 |
+
|
| 461 |
+
browserslist@4.28.2:
|
| 462 |
+
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
|
| 463 |
+
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
| 464 |
+
hasBin: true
|
| 465 |
+
|
| 466 |
+
caniuse-lite@1.0.30001793:
|
| 467 |
+
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
| 468 |
+
|
| 469 |
+
convert-source-map@2.0.0:
|
| 470 |
+
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
| 471 |
+
|
| 472 |
+
csstype@3.2.3:
|
| 473 |
+
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
| 474 |
+
|
| 475 |
+
debug@4.4.3:
|
| 476 |
+
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
| 477 |
+
engines: {node: '>=6.0'}
|
| 478 |
+
peerDependencies:
|
| 479 |
+
supports-color: '*'
|
| 480 |
+
peerDependenciesMeta:
|
| 481 |
+
supports-color:
|
| 482 |
+
optional: true
|
| 483 |
+
|
| 484 |
+
electron-to-chromium@1.5.364:
|
| 485 |
+
resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==}
|
| 486 |
+
|
| 487 |
+
esbuild@0.21.5:
|
| 488 |
+
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
| 489 |
+
engines: {node: '>=12'}
|
| 490 |
+
hasBin: true
|
| 491 |
+
|
| 492 |
+
escalade@3.2.0:
|
| 493 |
+
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
| 494 |
+
engines: {node: '>=6'}
|
| 495 |
+
|
| 496 |
+
framer-motion@11.18.2:
|
| 497 |
+
resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==}
|
| 498 |
+
peerDependencies:
|
| 499 |
+
'@emotion/is-prop-valid': '*'
|
| 500 |
+
react: ^18.0.0 || ^19.0.0
|
| 501 |
+
react-dom: ^18.0.0 || ^19.0.0
|
| 502 |
+
peerDependenciesMeta:
|
| 503 |
+
'@emotion/is-prop-valid':
|
| 504 |
+
optional: true
|
| 505 |
+
react:
|
| 506 |
+
optional: true
|
| 507 |
+
react-dom:
|
| 508 |
+
optional: true
|
| 509 |
+
|
| 510 |
+
fsevents@2.3.3:
|
| 511 |
+
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
| 512 |
+
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
| 513 |
+
os: [darwin]
|
| 514 |
+
|
| 515 |
+
gensync@1.0.0-beta.2:
|
| 516 |
+
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
| 517 |
+
engines: {node: '>=6.9.0'}
|
| 518 |
+
|
| 519 |
+
js-tokens@4.0.0:
|
| 520 |
+
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
| 521 |
+
|
| 522 |
+
jsesc@3.1.0:
|
| 523 |
+
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
| 524 |
+
engines: {node: '>=6'}
|
| 525 |
+
hasBin: true
|
| 526 |
+
|
| 527 |
+
json5@2.2.3:
|
| 528 |
+
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
| 529 |
+
engines: {node: '>=6'}
|
| 530 |
+
hasBin: true
|
| 531 |
+
|
| 532 |
+
loose-envify@1.4.0:
|
| 533 |
+
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
| 534 |
+
hasBin: true
|
| 535 |
+
|
| 536 |
+
lru-cache@5.1.1:
|
| 537 |
+
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
| 538 |
+
|
| 539 |
+
motion-dom@11.18.1:
|
| 540 |
+
resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==}
|
| 541 |
+
|
| 542 |
+
motion-utils@11.18.1:
|
| 543 |
+
resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==}
|
| 544 |
+
|
| 545 |
+
ms@2.1.3:
|
| 546 |
+
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
| 547 |
+
|
| 548 |
+
nanoid@3.3.12:
|
| 549 |
+
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
| 550 |
+
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
| 551 |
+
hasBin: true
|
| 552 |
+
|
| 553 |
+
node-releases@2.0.46:
|
| 554 |
+
resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
|
| 555 |
+
engines: {node: '>=18'}
|
| 556 |
+
|
| 557 |
+
picocolors@1.1.1:
|
| 558 |
+
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
| 559 |
+
|
| 560 |
+
postcss@8.5.15:
|
| 561 |
+
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
| 562 |
+
engines: {node: ^10 || ^12 || >=14}
|
| 563 |
+
|
| 564 |
+
react-dom@18.3.1:
|
| 565 |
+
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
| 566 |
+
peerDependencies:
|
| 567 |
+
react: ^18.3.1
|
| 568 |
+
|
| 569 |
+
react-refresh@0.17.0:
|
| 570 |
+
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
| 571 |
+
engines: {node: '>=0.10.0'}
|
| 572 |
+
|
| 573 |
+
react-router-dom@6.30.4:
|
| 574 |
+
resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==}
|
| 575 |
+
engines: {node: '>=14.0.0'}
|
| 576 |
+
peerDependencies:
|
| 577 |
+
react: '>=16.8'
|
| 578 |
+
react-dom: '>=16.8'
|
| 579 |
+
|
| 580 |
+
react-router@6.30.4:
|
| 581 |
+
resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==}
|
| 582 |
+
engines: {node: '>=14.0.0'}
|
| 583 |
+
peerDependencies:
|
| 584 |
+
react: '>=16.8'
|
| 585 |
+
|
| 586 |
+
react@18.3.1:
|
| 587 |
+
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
| 588 |
+
engines: {node: '>=0.10.0'}
|
| 589 |
+
|
| 590 |
+
rollup@4.60.4:
|
| 591 |
+
resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==}
|
| 592 |
+
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
| 593 |
+
hasBin: true
|
| 594 |
+
|
| 595 |
+
scheduler@0.23.2:
|
| 596 |
+
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
| 597 |
+
|
| 598 |
+
semver@6.3.1:
|
| 599 |
+
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
| 600 |
+
hasBin: true
|
| 601 |
+
|
| 602 |
+
source-map-js@1.2.1:
|
| 603 |
+
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
| 604 |
+
engines: {node: '>=0.10.0'}
|
| 605 |
+
|
| 606 |
+
tslib@2.8.1:
|
| 607 |
+
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
| 608 |
+
|
| 609 |
+
typescript@5.9.3:
|
| 610 |
+
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
| 611 |
+
engines: {node: '>=14.17'}
|
| 612 |
+
hasBin: true
|
| 613 |
+
|
| 614 |
+
update-browserslist-db@1.2.3:
|
| 615 |
+
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
| 616 |
+
hasBin: true
|
| 617 |
+
peerDependencies:
|
| 618 |
+
browserslist: '>= 4.21.0'
|
| 619 |
+
|
| 620 |
+
vite@5.4.21:
|
| 621 |
+
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
|
| 622 |
+
engines: {node: ^18.0.0 || >=20.0.0}
|
| 623 |
+
hasBin: true
|
| 624 |
+
peerDependencies:
|
| 625 |
+
'@types/node': ^18.0.0 || >=20.0.0
|
| 626 |
+
less: '*'
|
| 627 |
+
lightningcss: ^1.21.0
|
| 628 |
+
sass: '*'
|
| 629 |
+
sass-embedded: '*'
|
| 630 |
+
stylus: '*'
|
| 631 |
+
sugarss: '*'
|
| 632 |
+
terser: ^5.4.0
|
| 633 |
+
peerDependenciesMeta:
|
| 634 |
+
'@types/node':
|
| 635 |
+
optional: true
|
| 636 |
+
less:
|
| 637 |
+
optional: true
|
| 638 |
+
lightningcss:
|
| 639 |
+
optional: true
|
| 640 |
+
sass:
|
| 641 |
+
optional: true
|
| 642 |
+
sass-embedded:
|
| 643 |
+
optional: true
|
| 644 |
+
stylus:
|
| 645 |
+
optional: true
|
| 646 |
+
sugarss:
|
| 647 |
+
optional: true
|
| 648 |
+
terser:
|
| 649 |
+
optional: true
|
| 650 |
+
|
| 651 |
+
yallist@3.1.1:
|
| 652 |
+
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
| 653 |
+
|
| 654 |
+
snapshots:
|
| 655 |
+
|
| 656 |
+
'@babel/code-frame@7.29.7':
|
| 657 |
+
dependencies:
|
| 658 |
+
'@babel/helper-validator-identifier': 7.29.7
|
| 659 |
+
js-tokens: 4.0.0
|
| 660 |
+
picocolors: 1.1.1
|
| 661 |
+
|
| 662 |
+
'@babel/compat-data@7.29.7': {}
|
| 663 |
+
|
| 664 |
+
'@babel/core@7.29.7':
|
| 665 |
+
dependencies:
|
| 666 |
+
'@babel/code-frame': 7.29.7
|
| 667 |
+
'@babel/generator': 7.29.7
|
| 668 |
+
'@babel/helper-compilation-targets': 7.29.7
|
| 669 |
+
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
|
| 670 |
+
'@babel/helpers': 7.29.7
|
| 671 |
+
'@babel/parser': 7.29.7
|
| 672 |
+
'@babel/template': 7.29.7
|
| 673 |
+
'@babel/traverse': 7.29.7
|
| 674 |
+
'@babel/types': 7.29.7
|
| 675 |
+
'@jridgewell/remapping': 2.3.5
|
| 676 |
+
convert-source-map: 2.0.0
|
| 677 |
+
debug: 4.4.3
|
| 678 |
+
gensync: 1.0.0-beta.2
|
| 679 |
+
json5: 2.2.3
|
| 680 |
+
semver: 6.3.1
|
| 681 |
+
transitivePeerDependencies:
|
| 682 |
+
- supports-color
|
| 683 |
+
|
| 684 |
+
'@babel/generator@7.29.7':
|
| 685 |
+
dependencies:
|
| 686 |
+
'@babel/parser': 7.29.7
|
| 687 |
+
'@babel/types': 7.29.7
|
| 688 |
+
'@jridgewell/gen-mapping': 0.3.13
|
| 689 |
+
'@jridgewell/trace-mapping': 0.3.31
|
| 690 |
+
jsesc: 3.1.0
|
| 691 |
+
|
| 692 |
+
'@babel/helper-compilation-targets@7.29.7':
|
| 693 |
+
dependencies:
|
| 694 |
+
'@babel/compat-data': 7.29.7
|
| 695 |
+
'@babel/helper-validator-option': 7.29.7
|
| 696 |
+
browserslist: 4.28.2
|
| 697 |
+
lru-cache: 5.1.1
|
| 698 |
+
semver: 6.3.1
|
| 699 |
+
|
| 700 |
+
'@babel/helper-globals@7.29.7': {}
|
| 701 |
+
|
| 702 |
+
'@babel/helper-module-imports@7.29.7':
|
| 703 |
+
dependencies:
|
| 704 |
+
'@babel/traverse': 7.29.7
|
| 705 |
+
'@babel/types': 7.29.7
|
| 706 |
+
transitivePeerDependencies:
|
| 707 |
+
- supports-color
|
| 708 |
+
|
| 709 |
+
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
| 710 |
+
dependencies:
|
| 711 |
+
'@babel/core': 7.29.7
|
| 712 |
+
'@babel/helper-module-imports': 7.29.7
|
| 713 |
+
'@babel/helper-validator-identifier': 7.29.7
|
| 714 |
+
'@babel/traverse': 7.29.7
|
| 715 |
+
transitivePeerDependencies:
|
| 716 |
+
- supports-color
|
| 717 |
+
|
| 718 |
+
'@babel/helper-plugin-utils@7.29.7': {}
|
| 719 |
+
|
| 720 |
+
'@babel/helper-string-parser@7.29.7': {}
|
| 721 |
+
|
| 722 |
+
'@babel/helper-validator-identifier@7.29.7': {}
|
| 723 |
+
|
| 724 |
+
'@babel/helper-validator-option@7.29.7': {}
|
| 725 |
+
|
| 726 |
+
'@babel/helpers@7.29.7':
|
| 727 |
+
dependencies:
|
| 728 |
+
'@babel/template': 7.29.7
|
| 729 |
+
'@babel/types': 7.29.7
|
| 730 |
+
|
| 731 |
+
'@babel/parser@7.29.7':
|
| 732 |
+
dependencies:
|
| 733 |
+
'@babel/types': 7.29.7
|
| 734 |
+
|
| 735 |
+
'@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
|
| 736 |
+
dependencies:
|
| 737 |
+
'@babel/core': 7.29.7
|
| 738 |
+
'@babel/helper-plugin-utils': 7.29.7
|
| 739 |
+
|
| 740 |
+
'@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
|
| 741 |
+
dependencies:
|
| 742 |
+
'@babel/core': 7.29.7
|
| 743 |
+
'@babel/helper-plugin-utils': 7.29.7
|
| 744 |
+
|
| 745 |
+
'@babel/template@7.29.7':
|
| 746 |
+
dependencies:
|
| 747 |
+
'@babel/code-frame': 7.29.7
|
| 748 |
+
'@babel/parser': 7.29.7
|
| 749 |
+
'@babel/types': 7.29.7
|
| 750 |
+
|
| 751 |
+
'@babel/traverse@7.29.7':
|
| 752 |
+
dependencies:
|
| 753 |
+
'@babel/code-frame': 7.29.7
|
| 754 |
+
'@babel/generator': 7.29.7
|
| 755 |
+
'@babel/helper-globals': 7.29.7
|
| 756 |
+
'@babel/parser': 7.29.7
|
| 757 |
+
'@babel/template': 7.29.7
|
| 758 |
+
'@babel/types': 7.29.7
|
| 759 |
+
debug: 4.4.3
|
| 760 |
+
transitivePeerDependencies:
|
| 761 |
+
- supports-color
|
| 762 |
+
|
| 763 |
+
'@babel/types@7.29.7':
|
| 764 |
+
dependencies:
|
| 765 |
+
'@babel/helper-string-parser': 7.29.7
|
| 766 |
+
'@babel/helper-validator-identifier': 7.29.7
|
| 767 |
+
|
| 768 |
+
'@esbuild/aix-ppc64@0.21.5':
|
| 769 |
+
optional: true
|
| 770 |
+
|
| 771 |
+
'@esbuild/android-arm64@0.21.5':
|
| 772 |
+
optional: true
|
| 773 |
+
|
| 774 |
+
'@esbuild/android-arm@0.21.5':
|
| 775 |
+
optional: true
|
| 776 |
+
|
| 777 |
+
'@esbuild/android-x64@0.21.5':
|
| 778 |
+
optional: true
|
| 779 |
+
|
| 780 |
+
'@esbuild/darwin-arm64@0.21.5':
|
| 781 |
+
optional: true
|
| 782 |
+
|
| 783 |
+
'@esbuild/darwin-x64@0.21.5':
|
| 784 |
+
optional: true
|
| 785 |
+
|
| 786 |
+
'@esbuild/freebsd-arm64@0.21.5':
|
| 787 |
+
optional: true
|
| 788 |
+
|
| 789 |
+
'@esbuild/freebsd-x64@0.21.5':
|
| 790 |
+
optional: true
|
| 791 |
+
|
| 792 |
+
'@esbuild/linux-arm64@0.21.5':
|
| 793 |
+
optional: true
|
| 794 |
+
|
| 795 |
+
'@esbuild/linux-arm@0.21.5':
|
| 796 |
+
optional: true
|
| 797 |
+
|
| 798 |
+
'@esbuild/linux-ia32@0.21.5':
|
| 799 |
+
optional: true
|
| 800 |
+
|
| 801 |
+
'@esbuild/linux-loong64@0.21.5':
|
| 802 |
+
optional: true
|
| 803 |
+
|
| 804 |
+
'@esbuild/linux-mips64el@0.21.5':
|
| 805 |
+
optional: true
|
| 806 |
+
|
| 807 |
+
'@esbuild/linux-ppc64@0.21.5':
|
| 808 |
+
optional: true
|
| 809 |
+
|
| 810 |
+
'@esbuild/linux-riscv64@0.21.5':
|
| 811 |
+
optional: true
|
| 812 |
+
|
| 813 |
+
'@esbuild/linux-s390x@0.21.5':
|
| 814 |
+
optional: true
|
| 815 |
+
|
| 816 |
+
'@esbuild/linux-x64@0.21.5':
|
| 817 |
+
optional: true
|
| 818 |
+
|
| 819 |
+
'@esbuild/netbsd-x64@0.21.5':
|
| 820 |
+
optional: true
|
| 821 |
+
|
| 822 |
+
'@esbuild/openbsd-x64@0.21.5':
|
| 823 |
+
optional: true
|
| 824 |
+
|
| 825 |
+
'@esbuild/sunos-x64@0.21.5':
|
| 826 |
+
optional: true
|
| 827 |
+
|
| 828 |
+
'@esbuild/win32-arm64@0.21.5':
|
| 829 |
+
optional: true
|
| 830 |
+
|
| 831 |
+
'@esbuild/win32-ia32@0.21.5':
|
| 832 |
+
optional: true
|
| 833 |
+
|
| 834 |
+
'@esbuild/win32-x64@0.21.5':
|
| 835 |
+
optional: true
|
| 836 |
+
|
| 837 |
+
'@jridgewell/gen-mapping@0.3.13':
|
| 838 |
+
dependencies:
|
| 839 |
+
'@jridgewell/sourcemap-codec': 1.5.5
|
| 840 |
+
'@jridgewell/trace-mapping': 0.3.31
|
| 841 |
+
|
| 842 |
+
'@jridgewell/remapping@2.3.5':
|
| 843 |
+
dependencies:
|
| 844 |
+
'@jridgewell/gen-mapping': 0.3.13
|
| 845 |
+
'@jridgewell/trace-mapping': 0.3.31
|
| 846 |
+
|
| 847 |
+
'@jridgewell/resolve-uri@3.1.2': {}
|
| 848 |
+
|
| 849 |
+
'@jridgewell/sourcemap-codec@1.5.5': {}
|
| 850 |
+
|
| 851 |
+
'@jridgewell/trace-mapping@0.3.31':
|
| 852 |
+
dependencies:
|
| 853 |
+
'@jridgewell/resolve-uri': 3.1.2
|
| 854 |
+
'@jridgewell/sourcemap-codec': 1.5.5
|
| 855 |
+
|
| 856 |
+
'@remix-run/router@1.23.3': {}
|
| 857 |
+
|
| 858 |
+
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
| 859 |
+
|
| 860 |
+
'@rollup/rollup-android-arm-eabi@4.60.4':
|
| 861 |
+
optional: true
|
| 862 |
+
|
| 863 |
+
'@rollup/rollup-android-arm64@4.60.4':
|
| 864 |
+
optional: true
|
| 865 |
+
|
| 866 |
+
'@rollup/rollup-darwin-arm64@4.60.4':
|
| 867 |
+
optional: true
|
| 868 |
+
|
| 869 |
+
'@rollup/rollup-darwin-x64@4.60.4':
|
| 870 |
+
optional: true
|
| 871 |
+
|
| 872 |
+
'@rollup/rollup-freebsd-arm64@4.60.4':
|
| 873 |
+
optional: true
|
| 874 |
+
|
| 875 |
+
'@rollup/rollup-freebsd-x64@4.60.4':
|
| 876 |
+
optional: true
|
| 877 |
+
|
| 878 |
+
'@rollup/rollup-linux-arm-gnueabihf@4.60.4':
|
| 879 |
+
optional: true
|
| 880 |
+
|
| 881 |
+
'@rollup/rollup-linux-arm-musleabihf@4.60.4':
|
| 882 |
+
optional: true
|
| 883 |
+
|
| 884 |
+
'@rollup/rollup-linux-arm64-gnu@4.60.4':
|
| 885 |
+
optional: true
|
| 886 |
+
|
| 887 |
+
'@rollup/rollup-linux-arm64-musl@4.60.4':
|
| 888 |
+
optional: true
|
| 889 |
+
|
| 890 |
+
'@rollup/rollup-linux-loong64-gnu@4.60.4':
|
| 891 |
+
optional: true
|
| 892 |
+
|
| 893 |
+
'@rollup/rollup-linux-loong64-musl@4.60.4':
|
| 894 |
+
optional: true
|
| 895 |
+
|
| 896 |
+
'@rollup/rollup-linux-ppc64-gnu@4.60.4':
|
| 897 |
+
optional: true
|
| 898 |
+
|
| 899 |
+
'@rollup/rollup-linux-ppc64-musl@4.60.4':
|
| 900 |
+
optional: true
|
| 901 |
+
|
| 902 |
+
'@rollup/rollup-linux-riscv64-gnu@4.60.4':
|
| 903 |
+
optional: true
|
| 904 |
+
|
| 905 |
+
'@rollup/rollup-linux-riscv64-musl@4.60.4':
|
| 906 |
+
optional: true
|
| 907 |
+
|
| 908 |
+
'@rollup/rollup-linux-s390x-gnu@4.60.4':
|
| 909 |
+
optional: true
|
| 910 |
+
|
| 911 |
+
'@rollup/rollup-linux-x64-gnu@4.60.4':
|
| 912 |
+
optional: true
|
| 913 |
+
|
| 914 |
+
'@rollup/rollup-linux-x64-musl@4.60.4':
|
| 915 |
+
optional: true
|
| 916 |
+
|
| 917 |
+
'@rollup/rollup-openbsd-x64@4.60.4':
|
| 918 |
+
optional: true
|
| 919 |
+
|
| 920 |
+
'@rollup/rollup-openharmony-arm64@4.60.4':
|
| 921 |
+
optional: true
|
| 922 |
+
|
| 923 |
+
'@rollup/rollup-win32-arm64-msvc@4.60.4':
|
| 924 |
+
optional: true
|
| 925 |
+
|
| 926 |
+
'@rollup/rollup-win32-ia32-msvc@4.60.4':
|
| 927 |
+
optional: true
|
| 928 |
+
|
| 929 |
+
'@rollup/rollup-win32-x64-gnu@4.60.4':
|
| 930 |
+
optional: true
|
| 931 |
+
|
| 932 |
+
'@rollup/rollup-win32-x64-msvc@4.60.4':
|
| 933 |
+
optional: true
|
| 934 |
+
|
| 935 |
+
'@types/babel__core@7.20.5':
|
| 936 |
+
dependencies:
|
| 937 |
+
'@babel/parser': 7.29.7
|
| 938 |
+
'@babel/types': 7.29.7
|
| 939 |
+
'@types/babel__generator': 7.27.0
|
| 940 |
+
'@types/babel__template': 7.4.4
|
| 941 |
+
'@types/babel__traverse': 7.28.0
|
| 942 |
+
|
| 943 |
+
'@types/babel__generator@7.27.0':
|
| 944 |
+
dependencies:
|
| 945 |
+
'@babel/types': 7.29.7
|
| 946 |
+
|
| 947 |
+
'@types/babel__template@7.4.4':
|
| 948 |
+
dependencies:
|
| 949 |
+
'@babel/parser': 7.29.7
|
| 950 |
+
'@babel/types': 7.29.7
|
| 951 |
+
|
| 952 |
+
'@types/babel__traverse@7.28.0':
|
| 953 |
+
dependencies:
|
| 954 |
+
'@babel/types': 7.29.7
|
| 955 |
+
|
| 956 |
+
'@types/estree@1.0.8': {}
|
| 957 |
+
|
| 958 |
+
'@types/prop-types@15.7.15': {}
|
| 959 |
+
|
| 960 |
+
'@types/react-dom@18.3.7(@types/react@18.3.29)':
|
| 961 |
+
dependencies:
|
| 962 |
+
'@types/react': 18.3.29
|
| 963 |
+
|
| 964 |
+
'@types/react@18.3.29':
|
| 965 |
+
dependencies:
|
| 966 |
+
'@types/prop-types': 15.7.15
|
| 967 |
+
csstype: 3.2.3
|
| 968 |
+
|
| 969 |
+
'@vitejs/plugin-react@4.7.0(vite@5.4.21)':
|
| 970 |
+
dependencies:
|
| 971 |
+
'@babel/core': 7.29.7
|
| 972 |
+
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
|
| 973 |
+
'@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
|
| 974 |
+
'@rolldown/pluginutils': 1.0.0-beta.27
|
| 975 |
+
'@types/babel__core': 7.20.5
|
| 976 |
+
react-refresh: 0.17.0
|
| 977 |
+
vite: 5.4.21
|
| 978 |
+
transitivePeerDependencies:
|
| 979 |
+
- supports-color
|
| 980 |
+
|
| 981 |
+
baseline-browser-mapping@2.10.32: {}
|
| 982 |
+
|
| 983 |
+
browserslist@4.28.2:
|
| 984 |
+
dependencies:
|
| 985 |
+
baseline-browser-mapping: 2.10.32
|
| 986 |
+
caniuse-lite: 1.0.30001793
|
| 987 |
+
electron-to-chromium: 1.5.364
|
| 988 |
+
node-releases: 2.0.46
|
| 989 |
+
update-browserslist-db: 1.2.3(browserslist@4.28.2)
|
| 990 |
+
|
| 991 |
+
caniuse-lite@1.0.30001793: {}
|
| 992 |
+
|
| 993 |
+
convert-source-map@2.0.0: {}
|
| 994 |
+
|
| 995 |
+
csstype@3.2.3: {}
|
| 996 |
+
|
| 997 |
+
debug@4.4.3:
|
| 998 |
+
dependencies:
|
| 999 |
+
ms: 2.1.3
|
| 1000 |
+
|
| 1001 |
+
electron-to-chromium@1.5.364: {}
|
| 1002 |
+
|
| 1003 |
+
esbuild@0.21.5:
|
| 1004 |
+
optionalDependencies:
|
| 1005 |
+
'@esbuild/aix-ppc64': 0.21.5
|
| 1006 |
+
'@esbuild/android-arm': 0.21.5
|
| 1007 |
+
'@esbuild/android-arm64': 0.21.5
|
| 1008 |
+
'@esbuild/android-x64': 0.21.5
|
| 1009 |
+
'@esbuild/darwin-arm64': 0.21.5
|
| 1010 |
+
'@esbuild/darwin-x64': 0.21.5
|
| 1011 |
+
'@esbuild/freebsd-arm64': 0.21.5
|
| 1012 |
+
'@esbuild/freebsd-x64': 0.21.5
|
| 1013 |
+
'@esbuild/linux-arm': 0.21.5
|
| 1014 |
+
'@esbuild/linux-arm64': 0.21.5
|
| 1015 |
+
'@esbuild/linux-ia32': 0.21.5
|
| 1016 |
+
'@esbuild/linux-loong64': 0.21.5
|
| 1017 |
+
'@esbuild/linux-mips64el': 0.21.5
|
| 1018 |
+
'@esbuild/linux-ppc64': 0.21.5
|
| 1019 |
+
'@esbuild/linux-riscv64': 0.21.5
|
| 1020 |
+
'@esbuild/linux-s390x': 0.21.5
|
| 1021 |
+
'@esbuild/linux-x64': 0.21.5
|
| 1022 |
+
'@esbuild/netbsd-x64': 0.21.5
|
| 1023 |
+
'@esbuild/openbsd-x64': 0.21.5
|
| 1024 |
+
'@esbuild/sunos-x64': 0.21.5
|
| 1025 |
+
'@esbuild/win32-arm64': 0.21.5
|
| 1026 |
+
'@esbuild/win32-ia32': 0.21.5
|
| 1027 |
+
'@esbuild/win32-x64': 0.21.5
|
| 1028 |
+
|
| 1029 |
+
escalade@3.2.0: {}
|
| 1030 |
+
|
| 1031 |
+
framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
| 1032 |
+
dependencies:
|
| 1033 |
+
motion-dom: 11.18.1
|
| 1034 |
+
motion-utils: 11.18.1
|
| 1035 |
+
tslib: 2.8.1
|
| 1036 |
+
optionalDependencies:
|
| 1037 |
+
react: 18.3.1
|
| 1038 |
+
react-dom: 18.3.1(react@18.3.1)
|
| 1039 |
+
|
| 1040 |
+
fsevents@2.3.3:
|
| 1041 |
+
optional: true
|
| 1042 |
+
|
| 1043 |
+
gensync@1.0.0-beta.2: {}
|
| 1044 |
+
|
| 1045 |
+
js-tokens@4.0.0: {}
|
| 1046 |
+
|
| 1047 |
+
jsesc@3.1.0: {}
|
| 1048 |
+
|
| 1049 |
+
json5@2.2.3: {}
|
| 1050 |
+
|
| 1051 |
+
loose-envify@1.4.0:
|
| 1052 |
+
dependencies:
|
| 1053 |
+
js-tokens: 4.0.0
|
| 1054 |
+
|
| 1055 |
+
lru-cache@5.1.1:
|
| 1056 |
+
dependencies:
|
| 1057 |
+
yallist: 3.1.1
|
| 1058 |
+
|
| 1059 |
+
motion-dom@11.18.1:
|
| 1060 |
+
dependencies:
|
| 1061 |
+
motion-utils: 11.18.1
|
| 1062 |
+
|
| 1063 |
+
motion-utils@11.18.1: {}
|
| 1064 |
+
|
| 1065 |
+
ms@2.1.3: {}
|
| 1066 |
+
|
| 1067 |
+
nanoid@3.3.12: {}
|
| 1068 |
+
|
| 1069 |
+
node-releases@2.0.46: {}
|
| 1070 |
+
|
| 1071 |
+
picocolors@1.1.1: {}
|
| 1072 |
+
|
| 1073 |
+
postcss@8.5.15:
|
| 1074 |
+
dependencies:
|
| 1075 |
+
nanoid: 3.3.12
|
| 1076 |
+
picocolors: 1.1.1
|
| 1077 |
+
source-map-js: 1.2.1
|
| 1078 |
+
|
| 1079 |
+
react-dom@18.3.1(react@18.3.1):
|
| 1080 |
+
dependencies:
|
| 1081 |
+
loose-envify: 1.4.0
|
| 1082 |
+
react: 18.3.1
|
| 1083 |
+
scheduler: 0.23.2
|
| 1084 |
+
|
| 1085 |
+
react-refresh@0.17.0: {}
|
| 1086 |
+
|
| 1087 |
+
react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
| 1088 |
+
dependencies:
|
| 1089 |
+
'@remix-run/router': 1.23.3
|
| 1090 |
+
react: 18.3.1
|
| 1091 |
+
react-dom: 18.3.1(react@18.3.1)
|
| 1092 |
+
react-router: 6.30.4(react@18.3.1)
|
| 1093 |
+
|
| 1094 |
+
react-router@6.30.4(react@18.3.1):
|
| 1095 |
+
dependencies:
|
| 1096 |
+
'@remix-run/router': 1.23.3
|
| 1097 |
+
react: 18.3.1
|
| 1098 |
+
|
| 1099 |
+
react@18.3.1:
|
| 1100 |
+
dependencies:
|
| 1101 |
+
loose-envify: 1.4.0
|
| 1102 |
+
|
| 1103 |
+
rollup@4.60.4:
|
| 1104 |
+
dependencies:
|
| 1105 |
+
'@types/estree': 1.0.8
|
| 1106 |
+
optionalDependencies:
|
| 1107 |
+
'@rollup/rollup-android-arm-eabi': 4.60.4
|
| 1108 |
+
'@rollup/rollup-android-arm64': 4.60.4
|
| 1109 |
+
'@rollup/rollup-darwin-arm64': 4.60.4
|
| 1110 |
+
'@rollup/rollup-darwin-x64': 4.60.4
|
| 1111 |
+
'@rollup/rollup-freebsd-arm64': 4.60.4
|
| 1112 |
+
'@rollup/rollup-freebsd-x64': 4.60.4
|
| 1113 |
+
'@rollup/rollup-linux-arm-gnueabihf': 4.60.4
|
| 1114 |
+
'@rollup/rollup-linux-arm-musleabihf': 4.60.4
|
| 1115 |
+
'@rollup/rollup-linux-arm64-gnu': 4.60.4
|
| 1116 |
+
'@rollup/rollup-linux-arm64-musl': 4.60.4
|
| 1117 |
+
'@rollup/rollup-linux-loong64-gnu': 4.60.4
|
| 1118 |
+
'@rollup/rollup-linux-loong64-musl': 4.60.4
|
| 1119 |
+
'@rollup/rollup-linux-ppc64-gnu': 4.60.4
|
| 1120 |
+
'@rollup/rollup-linux-ppc64-musl': 4.60.4
|
| 1121 |
+
'@rollup/rollup-linux-riscv64-gnu': 4.60.4
|
| 1122 |
+
'@rollup/rollup-linux-riscv64-musl': 4.60.4
|
| 1123 |
+
'@rollup/rollup-linux-s390x-gnu': 4.60.4
|
| 1124 |
+
'@rollup/rollup-linux-x64-gnu': 4.60.4
|
| 1125 |
+
'@rollup/rollup-linux-x64-musl': 4.60.4
|
| 1126 |
+
'@rollup/rollup-openbsd-x64': 4.60.4
|
| 1127 |
+
'@rollup/rollup-openharmony-arm64': 4.60.4
|
| 1128 |
+
'@rollup/rollup-win32-arm64-msvc': 4.60.4
|
| 1129 |
+
'@rollup/rollup-win32-ia32-msvc': 4.60.4
|
| 1130 |
+
'@rollup/rollup-win32-x64-gnu': 4.60.4
|
| 1131 |
+
'@rollup/rollup-win32-x64-msvc': 4.60.4
|
| 1132 |
+
fsevents: 2.3.3
|
| 1133 |
+
|
| 1134 |
+
scheduler@0.23.2:
|
| 1135 |
+
dependencies:
|
| 1136 |
+
loose-envify: 1.4.0
|
| 1137 |
+
|
| 1138 |
+
semver@6.3.1: {}
|
| 1139 |
+
|
| 1140 |
+
source-map-js@1.2.1: {}
|
| 1141 |
+
|
| 1142 |
+
tslib@2.8.1: {}
|
| 1143 |
+
|
| 1144 |
+
typescript@5.9.3: {}
|
| 1145 |
+
|
| 1146 |
+
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
| 1147 |
+
dependencies:
|
| 1148 |
+
browserslist: 4.28.2
|
| 1149 |
+
escalade: 3.2.0
|
| 1150 |
+
picocolors: 1.1.1
|
| 1151 |
+
|
| 1152 |
+
vite@5.4.21:
|
| 1153 |
+
dependencies:
|
| 1154 |
+
esbuild: 0.21.5
|
| 1155 |
+
postcss: 8.5.15
|
| 1156 |
+
rollup: 4.60.4
|
| 1157 |
+
optionalDependencies:
|
| 1158 |
+
fsevents: 2.3.3
|
| 1159 |
+
|
| 1160 |
+
yallist@3.1.1: {}
|