mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-23 03:06:37 +00:00
Add codebase map to .planning/codebase/
7 structured documents covering stack, integrations, architecture, structure, conventions, testing, and concerns. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
## Pattern Overview
|
||||||
|
|
||||||
|
**Overall:** Layered async service with component-injected processing pipeline
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- FastAPI HTTP layer delegates all logic to a singleton `FlightProcessor` orchestrator
|
||||||
|
- Core processing components are instantiated at app startup via lifespan and injected via `attach_components()`
|
||||||
|
- All components define ABC interfaces (`ISequentialVisualOdometry`, `IFactorGraphOptimizer`, etc.) with a single concrete implementation — enabling future substitution
|
||||||
|
- All inference engines are mocked behind `IModelManager` / `MockInferenceEngine`; no real GPU/TRT execution exists in code yet
|
||||||
|
- Database layer is async SQLAlchemy (aiosqlite default) with a thin `FlightRepository` DAO
|
||||||
|
- SSE streaming is fully wired: per-flight async queues, `EventSourceResponse` at `GET /flights/{id}/stream`
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
**API Layer:**
|
||||||
|
- Purpose: HTTP request routing, validation, auth-free (no JWT in code despite spec)
|
||||||
|
- Location: `src/gps_denied/api/`
|
||||||
|
- Contains: FastAPI routers, dependency injection wiring, `deps.py` singletons
|
||||||
|
- Depends on: `FlightProcessor`, `FlightRepository`, `SSEEventStreamer`
|
||||||
|
- Used by: External callers, other onboard systems
|
||||||
|
|
||||||
|
**Orchestration Layer:**
|
||||||
|
- Purpose: Manages per-flight state machine, invokes pipeline components in sequence
|
||||||
|
- Location: `src/gps_denied/core/processor.py`
|
||||||
|
- Contains: `FlightProcessor`, `TrackingState` enum (NORMAL/LOST/RECOVERY), `FrameResult`
|
||||||
|
- Depends on: All core components, `FlightRepository`, `SSEEventStreamer`
|
||||||
|
- Used by: API layer via dependency injection
|
||||||
|
|
||||||
|
**Core Pipeline Components:**
|
||||||
|
- Purpose: Individual processing stages, each behind an interface
|
||||||
|
- Location: `src/gps_denied/core/`
|
||||||
|
- Contains: `ImageInputPipeline`, `SequentialVisualOdometry`, `GlobalPlaceRecognition`, `MetricRefinement`, `FactorGraphOptimizer`, `RouteChunkManager`, `FailureRecoveryCoordinator`, `ImageRotationManager`, `CoordinateTransformer`, `ResultManager`, `SSEEventStreamer`, `SatelliteDataManager`, `ModelManager`
|
||||||
|
- Depends on: `IModelManager`, `FlightRepository` (some), schemas
|
||||||
|
- Used by: `FlightProcessor`
|
||||||
|
|
||||||
|
**Inference Layer:**
|
||||||
|
- Purpose: AI model lifecycle and inference dispatch
|
||||||
|
- Location: `src/gps_denied/core/models.py`
|
||||||
|
- Contains: `IModelManager`, `ModelManager`, `MockInferenceEngine`
|
||||||
|
- Depends on: `schemas/model.py`
|
||||||
|
- Used by: `SequentialVisualOdometry`, `GlobalPlaceRecognition`, `MetricRefinement`
|
||||||
|
|
||||||
|
**Database Layer:**
|
||||||
|
- Purpose: Async persistence, all SQL via SQLAlchemy ORM
|
||||||
|
- Location: `src/gps_denied/db/`
|
||||||
|
- Contains: `FlightRepository`, ORM models (`FlightRow`, `WaypointRow`, `GeofenceRow`, `FlightStateRow`, `FrameResultRow`, `HeadingRow`, `ImageRow`, `ChunkRow`)
|
||||||
|
- Depends on: SQLAlchemy async engine
|
||||||
|
- Used by: `FlightProcessor`, `ResultManager`, API deps
|
||||||
|
|
||||||
|
**Schema Layer:**
|
||||||
|
- Purpose: Pydantic models for validation and inter-component data contracts
|
||||||
|
- Location: `src/gps_denied/schemas/`
|
||||||
|
- Contains: Domain models (`GPSPoint`, `CameraParameters`), request/response schemas, VO/GPR/metric/satellite/rotation/chunk schemas, SSE event types
|
||||||
|
- Depends on: Nothing internal
|
||||||
|
- Used by: All layers
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
**Frame Processing (primary path):**
|
||||||
|
|
||||||
|
1. Client uploads image batch → `POST /flights/{id}/images/batch`
|
||||||
|
2. Router spawns `asyncio.create_task(_process_batch())`, returns 202 immediately
|
||||||
|
3. `_process_batch` calls `processor.process_frame(flight_id, frame_id, image)` per image
|
||||||
|
4. `FlightProcessor.process_frame`:
|
||||||
|
a. Calls `SequentialVisualOdometry.compute_relative_pose(prev, curr, cam)`
|
||||||
|
b. If VO succeeds: adds relative factor to `FactorGraphOptimizer`
|
||||||
|
c. State machine: NORMAL → LOST (on VO failure) → RECOVERY → NORMAL (on recovery)
|
||||||
|
d. On RECOVERY: `FailureRecoveryCoordinator.process_chunk_recovery()` calls GPR + MetricRefinement
|
||||||
|
e. In NORMAL: calls `GlobalPlaceRecognition.retrieve_candidate_tiles()` then `MetricRefinement.align_to_satellite()`
|
||||||
|
f. Runs incremental `FactorGraphOptimizer.optimize()`
|
||||||
|
g. Publishes `FrameResult` via `SSEEventStreamer.push_event()`
|
||||||
|
5. SSE clients receive real-time frame events
|
||||||
|
|
||||||
|
**Tracking Loss / Chunk Recovery:**
|
||||||
|
|
||||||
|
1. VO fails → `processor._flight_states[id] = LOST`
|
||||||
|
2. `FailureRecoveryCoordinator.handle_tracking_lost()` creates new chunk via `RouteChunkManager`
|
||||||
|
3. Next frame enters RECOVERY: `process_chunk_recovery()` runs GPR on chunk images
|
||||||
|
4. GPR finds candidate tiles → `MetricRefinement.align_chunk_to_satellite()` computes homography
|
||||||
|
5. If aligned: chunk anchored, state → NORMAL
|
||||||
|
6. If not aligned: chunk stays UNANCHORED, state stays RECOVERY
|
||||||
|
|
||||||
|
**Satellite Tile Fetch:**
|
||||||
|
|
||||||
|
1. `SatelliteDataManager.fetch_tile()` checks `diskcache` first
|
||||||
|
2. On miss: fetches from `https://mt1.google.com/vt/lyrs=s&x=...` via httpx
|
||||||
|
3. Decoded to numpy array, stored in diskcache
|
||||||
|
4. `fetch_tile_grid()` and `prefetch_route_corridor()` do parallel async fetches
|
||||||
|
|
||||||
|
**State Management:**
|
||||||
|
- Per-flight tracking state held in `FlightProcessor._flight_states: dict[str, TrackingState]`
|
||||||
|
- Per-flight previous frame cache in `FlightProcessor._prev_images: dict[str, np.ndarray]`
|
||||||
|
- Per-flight chunk state in `RouteChunkManager._chunks: dict[str, dict[str, ChunkHandle]]`
|
||||||
|
- Per-flight factor graph in `FactorGraphOptimizer._flights_state: dict[str, dict]`
|
||||||
|
- Per-flight SSE queues in `SSEEventStreamer._streams: dict[str, dict[str, Queue]]`
|
||||||
|
- All persistent state (waypoints, frame results, flight metadata) in SQLite via `FlightRepository`
|
||||||
|
|
||||||
|
## Key Abstractions
|
||||||
|
|
||||||
|
**TrackingState (State Machine):**
|
||||||
|
- Purpose: Three-state machine per flight controlling pipeline branch selection
|
||||||
|
- Location: `src/gps_denied/core/processor.py`
|
||||||
|
- States: NORMAL (VO active + drift correction) → LOST (VO failed, chunk created) → RECOVERY (GPR + metric) → NORMAL
|
||||||
|
- Note: Simplified vs. documented 5-state design; no IMU-only prediction state
|
||||||
|
|
||||||
|
**IModelManager / MockInferenceEngine:**
|
||||||
|
- Purpose: Decouples inference calls from model backend; enables mock-first development
|
||||||
|
- Location: `src/gps_denied/core/models.py`
|
||||||
|
- Pattern: All models auto-loaded as `MockInferenceEngine` when first accessed; no real TRT/ONNX loading
|
||||||
|
- Mock models: SuperPoint (500 random features), LightGlue (100 random matches), DINOv2 (4096-dim random descriptor), LiteSAM (random homography, 80% match probability)
|
||||||
|
|
||||||
|
**ChunkHandle / RouteChunkManager:**
|
||||||
|
- Purpose: Represents a disconnected trajectory segment between tracking losses
|
||||||
|
- Location: `src/gps_denied/core/chunk_manager.py`
|
||||||
|
- Lifecycle: UNANCHORED → MATCHING → ANCHORED or UNANCHORED → MERGED
|
||||||
|
|
||||||
|
**FactorGraphOptimizer:**
|
||||||
|
- Purpose: Maintains per-flight pose graph with relative (VO) and absolute (GPS/satellite) factors
|
||||||
|
- Location: `src/gps_denied/core/graph.py`
|
||||||
|
- Reality: GTSAM import is optional (`try: import gtsam`); concrete implementation is a mock using simple vector arithmetic
|
||||||
|
|
||||||
|
## Entry Points
|
||||||
|
|
||||||
|
**Application startup:**
|
||||||
|
- Location: `src/gps_denied/app.py`
|
||||||
|
- Triggers: `uvicorn` or `python -m gps_denied` (via `src/gps_denied/__main__.py`)
|
||||||
|
- Responsibilities: Creates FastAPI app, registers `/flights` router, wires lifespan (instantiates all pipeline components, stores on `app.state.pipeline_components`)
|
||||||
|
|
||||||
|
**Frame processing:**
|
||||||
|
- Location: `src/gps_denied/api/routers/flights.py` → `upload_image_batch`
|
||||||
|
- Triggers: `POST /flights/{id}/images/batch` multipart form
|
||||||
|
- Responsibilities: Validates batch, spawns background task, each frame calls `processor.process_frame()`
|
||||||
|
|
||||||
|
**SSE stream:**
|
||||||
|
- Location: `src/gps_denied/api/routers/flights.py` → `create_sse_stream`
|
||||||
|
- Triggers: `GET /flights/{id}/stream`
|
||||||
|
- Responsibilities: Returns `EventSourceResponse` wrapping async generator from `SSEEventStreamer`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Strategy:** Exception swallowing in processor with `logger.warning`; most component failures are non-fatal
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- VO failure: caught with `except Exception as exc`, logged, `vo_ok = False` → state machine handles
|
||||||
|
- Drift correction failure: caught with `except Exception as exc`, logged, frame continues without correction
|
||||||
|
- HTTP errors in satellite fetching: `httpx.HTTPError` caught, returns `None` (tile treated as missing)
|
||||||
|
- DB not-found: returns `None`, router converts to HTTP 404
|
||||||
|
- Batch upload errors: HTTP 422 with detail string
|
||||||
|
|
||||||
|
## Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Logging:** Standard `logging.getLogger(__name__)` in every module; no structured logging or log levels configuration in code
|
||||||
|
|
||||||
|
**Validation:** Pydantic models at API boundary; no internal validation between pipeline components
|
||||||
|
|
||||||
|
**Authentication:** Documented as JWT in solution spec; **not implemented** in code — no auth middleware, no JWT verification on any endpoint
|
||||||
|
|
||||||
|
**Coordinate System:** `CoordinateTransformer` (`src/gps_denied/core/coordinates.py`) handles ENU↔GPS conversion with real math; `pixel_to_gps` is a placeholder with fake scaling (1px = 0.1m)
|
||||||
|
|
||||||
|
**ESKF / MAVLink / cuVSLAM:** **Not present in code.** The solution document specifies all three in detail, but the codebase contains none of them. The implemented architecture is a ground-processing post-flight pipeline (images uploaded via REST), not the real-time onboard ESKF+cuVSLAM system described in `solution.md`.
|
||||||
|
|
||||||
|
## Divergence: Documented Design vs. Implemented Code
|
||||||
|
|
||||||
|
This is a critical architectural gap. The solution document describes a **real-time embedded system**; the code implements a **batch REST processing service**:
|
||||||
|
|
||||||
|
| Aspect | solution.md (documented) | Code (implemented) |
|
||||||
|
|--------|--------------------------|---------------------|
|
||||||
|
| Processing model | Real-time, 0.7fps camera stream | Batch HTTP upload, async background task |
|
||||||
|
| State estimator | ESKF (15-state, IMU-driven 5-10Hz) | FactorGraphOptimizer (mock GTSAM/pose graph) |
|
||||||
|
| Visual odometry | cuVSLAM Inertial mode | SuperPoint + LightGlue (mocked) via SequentialVisualOdometry |
|
||||||
|
| Satellite matching | LiteSAM/XFeat TRT Engine FP16 | LiteSAM via MockInferenceEngine (random homography) |
|
||||||
|
| Place recognition | Not mentioned as separate component | AnyLoc DINOv2 (GlobalPlaceRecognition, mocked) |
|
||||||
|
| GPS output | MAVLink GPS_INPUT via pymavlink UART | None — GPS positions computed but not sent anywhere |
|
||||||
|
| FC integration | pymavlink over UART | Not present |
|
||||||
|
| CUDA streams | Dual CUDA streams (Stream A/B) | Not present |
|
||||||
|
| Deployment | Jetson Orin Nano Super, systemd service | Local dev server (uvicorn, SQLite) |
|
||||||
|
| Auth | JWT on all endpoints | Not implemented |
|
||||||
|
|
||||||
|
The code is TRL ~2 for the actual target system. It is a functional prototype of the processing logic with all AI inference mocked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Architecture analysis: 2026-04-01*
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
# Codebase Concerns
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRITICAL — Blocks Core Functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CRITICAL-1: ESKF Does Not Exist
|
||||||
|
|
||||||
|
**Issue:** The ESKF (Error-State Kalman Filter) is the central position estimator per the architecture — it fuses IMU (5-10Hz), VO measurements (0.7Hz), and satellite corrections (0.07-0.14Hz), and feeds the GPS_INPUT MAVLink output. There is no `eskf.py` file anywhere in the codebase.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Missing: `src/gps_denied/core/eskf.py` (does not exist)
|
||||||
|
- Referenced in docs: `_docs/01_solution/solution.md` (lines 127-178)
|
||||||
|
|
||||||
|
**Impact:** Without ESKF:
|
||||||
|
- No IMU prediction between frames — system has no position estimate when no camera frame arrives
|
||||||
|
- No GPS_INPUT MAVLink messages can be sent to the flight controller (the primary AC requirement)
|
||||||
|
- The 5-10Hz output loop cannot function
|
||||||
|
- Confidence tier computation (HIGH/MEDIUM/LOW) for `fix_type` in GPS_INPUT is undefined
|
||||||
|
- Scale drift correction from satellite measurements is impossible
|
||||||
|
- The 400ms latency budget cannot be met with alternative approaches
|
||||||
|
- `FlightProcessor.process_frame` calls `self._graph.add_relative_factor` passing VO pose directly into the factor graph — this bypasses ESKF entirely
|
||||||
|
|
||||||
|
**Fix approach:** Implement `src/gps_denied/core/eskf.py` per the full specification in `_docs/01_solution/solution.md` lines 127-178. The 15-state error vector, F/Q matrices, dual measurement models (VO relative pose, satellite absolute), and GPS_INPUT field population must all be implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CRITICAL-2: MAVLink/GPS_INPUT Output Is Completely Missing
|
||||||
|
|
||||||
|
**Issue:** The acceptance criteria require the system to output MAVLink GPS_INPUT messages to the flight controller at 5-10Hz via UART. Neither `pymavlink` nor `mavsdk` is in the dependency list (`pyproject.toml`), and no code for MAVLink communication exists anywhere.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `pyproject.toml` — `pymavlink` and `mavsdk` absent from `dependencies`
|
||||||
|
- Missing: any `mavlink*.py`, `flight_controller*.py`, or GPS_INPUT sender
|
||||||
|
- Referenced in docs: `_docs/00_problem/acceptance_criteria.md` lines 23-24, `_docs/01_solution/solution.md` lines 224-242
|
||||||
|
|
||||||
|
**Impact:** The UAV flight controller receives no position replacement for GPS. The entire system purpose — feeding GPS_INPUT to ArduPilot/PX4 — cannot be fulfilled. All acceptance criteria around real-time positioning and failsafe behavior are unachievable.
|
||||||
|
|
||||||
|
**Fix approach:** Add `pymavlink` to `pyproject.toml`. Implement a MAVLink output component that reads ESKF state and covariance, maps them to GPS_INPUT fields (per the specification table in solution.md lines 224-242), and transmits over UART at 5-10Hz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CRITICAL-3: All ML Inference Is Mock — No Real TensorRT Engines
|
||||||
|
|
||||||
|
**Issue:** `ModelManager.load_model()` in `src/gps_denied/core/models.py` unconditionally instantiates `MockInferenceEngine` for all models: SuperPoint, LightGlue, LiteSAM, DINOv2. The comment reads "For prototype, we strictly use Mock". `optimize_to_tensorrt` and `fallback_to_onnx` are placeholders that return strings and `True` without any actual logic.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/models.py` lines 113-144
|
||||||
|
- `src/gps_denied/core/models.py:117`: `# For prototype, we strictly use Mock`
|
||||||
|
- `src/gps_denied/core/models.py:133-137`: `optimize_to_tensorrt` and `fallback_to_onnx` are stubs
|
||||||
|
|
||||||
|
**Impact:** All VO feature extraction (SuperPoint+LightGlue), satellite image matching (LiteSAM or XFeat), and global place recognition (DINOv2/AnyLoc) run on random number generators. No real position computation is possible. The system cannot achieve any accuracy target.
|
||||||
|
|
||||||
|
**Fix approach:** Replace `MockInferenceEngine` loading with actual TensorRT engine loading using the `tensorrt` Python API. Requires: (1) exporting PyTorch models to ONNX, (2) converting to TRT FP16 engines via `trtexec`, (3) implementing TRT context inference per-model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CRITICAL-4: cuVSLAM Not Implemented — Using OpenCV Essential Matrix Instead
|
||||||
|
|
||||||
|
**Issue:** The tech stack decision selected `PyCuVSLAM v15.0.0` for visual odometry (116fps on Orin Nano, built-in IMU fusion, loop closure). The actual implementation in `src/gps_denied/core/vo.py` uses SuperPoint + LightGlue feature matching via `cv2.findEssentialMat` and `cv2.recoverPose`. PyCuVSLAM is not imported anywhere.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/vo.py` — uses `cv2.findEssentialMat`, `cv2.recoverPose` (lines 92-120)
|
||||||
|
- `pyproject.toml` — `pycuvslam` absent from dependencies
|
||||||
|
- `_docs/01_solution/tech_stack.md` lines 60-66: cuVSLAM selected, risk documented
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- No IMU integration in VO — scale ambiguity is worse without inertial fusion
|
||||||
|
- No built-in loop closure
|
||||||
|
- `SequentialVisualOdometry.scale_ambiguous` is hardcoded `True` (line 147 of vo.py) — translation magnitude is unit-vector-only, making metric scale estimation impossible without satellite corrections at every step
|
||||||
|
- Performance on Orin Nano is unvalidated (OpenCV approach may exceed 400ms budget)
|
||||||
|
|
||||||
|
**Fix approach:** Either (a) integrate PyCuVSLAM in Inertial mode as designed, or (b) formally commit to SuperPoint+LightGlue with TRT engines and document the tradeoffs. Scale recovery must be addressed explicitly since `scale_ambiguous=True` is not handled anywhere in `processor.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CRITICAL-5: Faiss Index Is Synthetic Random Data
|
||||||
|
|
||||||
|
**Issue:** `GlobalPlaceRecognition.load_index()` in `src/gps_denied/core/gpr.py` generates 1000 random normalized vectors as the satellite tile descriptor database. It does not read the `index_path` parameter. The tile metadata contains random GPS coordinates near lat=49.0/lon=32.0 with no relation to actual operational area tiles.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/gpr.py` lines 74-105
|
||||||
|
- `src/gps_denied/core/gpr.py:80`: comment "Mock loading Faiss index. In reality, it reads index_path."
|
||||||
|
|
||||||
|
**Impact:** Re-localization after tracking loss returns random tile candidates, not actual satellite positions. The entire disconnected-segment recovery pipeline returns meaningless GPS coordinates. No real place recognition is possible.
|
||||||
|
|
||||||
|
**Fix approach:** Implement real Faiss index construction during offline pre-flight processing. DINOv2 descriptors must be extracted from actual satellite tiles, stored in a Faiss IndexFlatIP or IVF index, and loaded at runtime from `index_path`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CRITICAL-6: Satellite Tile Fetching Does Not Work Offline
|
||||||
|
|
||||||
|
**Issue:** `SatelliteDataManager.fetch_tile()` in `src/gps_denied/core/satellite.py` fetches tiles from `mt1.google.com` at runtime using `httpx`. The system architecture and restrictions require all tiles to be pre-loaded before flight; the Jetson has no internet during flight.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/satellite.py` lines 33-52
|
||||||
|
- `_docs/00_problem/restrictions.md` line 22: "Satellite imagery must be pre-loaded onto the companion computer before flight"
|
||||||
|
- `_docs/01_solution/tech_stack.md` lines 137-143: GeoHash-indexed directory storage was selected
|
||||||
|
|
||||||
|
**Impact:** All satellite matching fails in flight (no connectivity). The runtime tile-loading path hits a live URL that cannot be reached. The GeoHash-indexed directory storage design from tech_stack.md is not implemented.
|
||||||
|
|
||||||
|
**Fix approach:** Replace `SatelliteDataManager` with a GeoHash-indexed directory reader as specified. Offline pre-processing tool downloads tiles before flight. Runtime reads from local filesystem only, using `ESKF position ± 3σ` to select relevant tiles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HIGH — Significant Risk
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-1: Object GPS Localization Is Hardcoded Stub
|
||||||
|
|
||||||
|
**Issue:** `FlightProcessor.convert_object_to_gps()` in `src/gps_denied/core/processor.py` returns hardcoded `GPSPoint(lat=48.0, lon=37.0)` with `accuracy_meters=5.0` for any pixel on any frame. The real implementation requires the full pixel-to-ray-to-ground-plane-to-WGS84 transformation chain.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/processor.py` lines 409-417
|
||||||
|
- `src/gps_denied/core/coordinates.py` lines 62-78: `pixel_to_gps` uses "FAKE Math" with 0.1m/pixel approximation
|
||||||
|
|
||||||
|
**Impact:** Any onboard AI system requesting GPS coordinates of detected objects receives wrong data. The acceptance criterion "Other onboard AI systems can request GPS coordinates of objects" cannot be met.
|
||||||
|
|
||||||
|
**Fix approach:** Implement the full coordinate chain: pixel → camera ray (using K matrix from CameraParameters), camera-to-body via T_cam_body, body-to-NED via ESKF quaternion, ray-ground intersection at known altitude, NED-to-WGS84. See solution.md lines 187-215.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-2: Factor Graph Uses Mock Optimization — GTSAM Is Never Invoked
|
||||||
|
|
||||||
|
**Issue:** `FactorGraphOptimizer` in `src/gps_denied/core/graph.py` initializes GTSAM objects (`NonlinearFactorGraph`, `ISAM2`, `Values`) when GTSAM is available, but all method implementations are pure Python dict manipulations. The GTSAM objects are allocated in `_init_flight` but never used. `optimize()` sets `dirty=False` and returns hardcoded `converged=True, final_error=0.1`.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/graph.py` lines 116-195
|
||||||
|
- `src/gps_denied/core/graph.py:186`: comment "Real logic: state['isam'].update(...)"
|
||||||
|
- `src/gps_denied/core/graph.py:124-125`: comment "In a real environment, we'd add BetweenFactorPose3 to GTSAM"
|
||||||
|
|
||||||
|
**Impact:** Position optimization does not happen. Drift correction from satellite anchors is simulated as a direct position overwrite (line 164: `state["poses"][frame_id].position = enu`) without proper graph-theoretic smoothing of historical positions. The trajectory quality claimed by `mean_reprojection_error=0.5` is meaningless.
|
||||||
|
|
||||||
|
**Fix approach:** Implement real GTSAM ISAM2 incremental smoothing. Replace mock implementations with `BetweenFactorPose3` for VO factors, `GPSFactor` or `PriorFactorPose3` for satellite anchors, and ISAM2 update calls.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-3: Coordinate Transform Chain Is Fake
|
||||||
|
|
||||||
|
**Issue:** `CoordinateTransformer.pixel_to_gps()` in `src/gps_denied/core/coordinates.py` uses "FAKE Math" with a hardcoded 0.1m/pixel assumption. `image_object_to_gps()` uses fake camera parameters. `transform_points()` returns the input unchanged. The body-to-NED rotation from ESKF attitude quaternion is not used anywhere.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/coordinates.py` lines 62-78, 103-119
|
||||||
|
- `src/gps_denied/core/coordinates.py:66`: `# FAKE Math for mockup:`
|
||||||
|
- `src/gps_denied/core/coordinates.py:118`: `# Placeholder for cv2.perspectiveTransform`
|
||||||
|
|
||||||
|
**Impact:** All coordinate transformations produce wrong results. The requirement for sub-50m accuracy is impossible with a 0.1m/pixel approximation that ignores altitude, camera intrinsics, and UAV attitude.
|
||||||
|
|
||||||
|
**Fix approach:** Implement the full chain from solution.md lines 196-222. Use actual `CameraParameters.focal_length` and sensor dimensions to build K. Use ESKF quaternion for body-to-NED rotation. Use `altitude` from flight parameters for ray-ground intersection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-4: ImageRotationManager Constructor Signature Mismatch
|
||||||
|
|
||||||
|
**Issue:** `ImageRotationManager.__init__` in `src/gps_denied/core/rotation.py` accepts no arguments (`def __init__(self):`), but `app.py` line 31 instantiates it as `ImageRotationManager(mm)` passing a `ModelManager`. The constructor in `rotation.py` does not accept `mm` — this will raise `TypeError` at startup.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/rotation.py` line 24: `def __init__(self):`
|
||||||
|
- `src/gps_denied/app.py` line 31: `rotation = ImageRotationManager(mm)`
|
||||||
|
|
||||||
|
**Impact:** Application startup fails with `TypeError`. All processing is blocked.
|
||||||
|
|
||||||
|
**Fix approach:** Either update `ImageRotationManager.__init__` to accept an optional model manager parameter, or remove the `mm` argument from the `app.py` instantiation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-5: ResultManager.publish_waypoint_update Is a Silent No-Op
|
||||||
|
|
||||||
|
**Issue:** `ResultManager.publish_waypoint_update()` in `src/gps_denied/core/results.py` line 73 has a bare `pass` — it does nothing and returns `None` implicitly. It is `async` and its callers `await` it expecting a `bool` return.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/results.py` lines 71-73
|
||||||
|
|
||||||
|
**Impact:** Waypoint update SSE events are never emitted. The ground station receives no waypoint refinement updates, violating the AC requirement to stream position estimates to the ground station.
|
||||||
|
|
||||||
|
**Fix approach:** Implement waypoint update logic: retrieve frame result from DB, emit SSE `waypoint_update` event via `self.sse`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-6: Batch Validation Minimum Size Is Wrong
|
||||||
|
|
||||||
|
**Issue:** `ImageInputPipeline.validate_batch()` in `src/gps_denied/core/pipeline.py` line 54 requires a minimum of 10 images per batch (`if num_images < 10: errors.append("Batch is empty")`). At 0.7fps with 1430ms intervals, a batch of 1 or a few images is the expected normal operating mode.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/pipeline.py` lines 53-56
|
||||||
|
|
||||||
|
**Impact:** Every small batch upload (the expected use case) is rejected with "Batch is empty". The processing pipeline cannot ingest normal operational data.
|
||||||
|
|
||||||
|
**Fix approach:** Lower or remove the minimum batch size. The check should be `if num_images < 1` at minimum.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HIGH-7: SatelliteDataManager Uses diskcache During Flight — Conflicts With Pre-Loaded Tile Architecture
|
||||||
|
|
||||||
|
**Issue:** `SatelliteDataManager` depends on `diskcache` (`pyproject.toml` line 17) for runtime tile caching via disk-backed `dc.Cache`. This is a different architecture from the designed GeoHash-indexed directory structure. Using diskcache during flight creates write I/O overhead on the Jetson's storage at a time when latency is critical (<400ms budget).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/satellite.py` lines 17-22
|
||||||
|
- `pyproject.toml` line 17: `diskcache>=5.6`
|
||||||
|
|
||||||
|
**Impact:** The design called for O(1) directory lookup on pre-built GeoHash index. Runtime diskcache writes compete with flight-critical I/O and add latency unpredictability.
|
||||||
|
|
||||||
|
**Fix approach:** Implement pre-flight GeoHash indexing tool (offline). At runtime, use read-only directory lookup. Remove diskcache from the flight path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MEDIUM — Notable Issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-1: VO Scale Ambiguity Is Acknowledged But Never Resolved
|
||||||
|
|
||||||
|
**Issue:** `RelativePose.scale_ambiguous = True` is hardcoded in `src/gps_denied/core/vo.py` line 147. Nothing in `processor.py`, `graph.py`, or elsewhere handles scale recovery. Monocular VO from `recoverPose` yields unit-vector translations, not metric distances.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/vo.py` line 147
|
||||||
|
- `src/gps_denied/core/processor.py` lines 147-150: `rel_pose` passed to `add_relative_factor` without scale correction
|
||||||
|
|
||||||
|
**Impact:** Accumulated VO trajectory has no metric scale. The 100m cumulative drift budget cannot be assessed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-2: tech_stack.md Is Inconsistent With Current Architecture
|
||||||
|
|
||||||
|
**Issue:** `_docs/01_solution/solution.md` line 21 explicitly flags this: "tech_stack.md says 3fps (should be 0.7fps), LiteSAM at 480px (should be 1280px), missing EfficientLoFTR." The solution document acknowledges the inconsistency but marks it as "separate task — not addressed."
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `_docs/01_solution/tech_stack.md`
|
||||||
|
- `_docs/01_solution/solution.md` lines 21-22
|
||||||
|
|
||||||
|
**Impact:** Developers reading tech_stack.md get incorrect system parameters. Planning based on tech_stack.md (e.g. tile sizing at 480px resolution) will be wrong.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-3: GTSAM Dependency Version Is Pre-Release
|
||||||
|
|
||||||
|
**Issue:** `pyproject.toml` line 18: `gtsam>=4.3a0` — this pins to an alpha pre-release. The GTSAM Python bindings for ARM64 (aarch64) are not available on PyPI and require compilation from source on Jetson.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `pyproject.toml` line 18
|
||||||
|
|
||||||
|
**Impact:** `pip install` on Jetson will fail or install an incompatible version. The factor graph optimizer imports GTSAM with `try/except ImportError` fallback, so failure is silent — the mock implementation runs without error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-4: No IMU Input Path Exists
|
||||||
|
|
||||||
|
**Issue:** The ESKF prediction step runs at 5-10Hz from IMU data received via MAVLink `ATTITUDE` or `RAW_IMU` messages. No MAVLink listener, IMU data structure, or IMU ingestion queue exists anywhere in the codebase. Even when ESKF is implemented, it has no data source.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Missing: any `imu*.py`, `mavlink_listener*.py`, or IMU data queue
|
||||||
|
- `_docs/01_solution/solution.md` lines 146-152: IMU prediction specification
|
||||||
|
|
||||||
|
**Impact:** ESKF prediction step cannot function. GPS_INPUT output between camera frames (filling the 1430ms inter-frame gap) is impossible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-5: ProcessingStatus.processing_rate Is Always 0.0
|
||||||
|
|
||||||
|
**Issue:** `ImageInputPipeline.get_processing_status()` in `src/gps_denied/core/pipeline.py` line 203 hardcodes `processing_rate=0.0 # mock`. This value is presumably exposed via API and used for operator situational awareness.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/pipeline.py` line 203
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-6: get_image_by_sequence Uses Unreliable String Matching
|
||||||
|
|
||||||
|
**Issue:** `ImageInputPipeline.get_image_by_sequence()` in `src/gps_denied/core/pipeline.py` lines 173-176 uses `if str(sequence) in fn` — this means sequence `5` will match filenames `5.jpg`, `15.jpg`, `25.jpg`, `500.jpg` etc. This produces incorrect frame ordering.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/pipeline.py` lines 173-176
|
||||||
|
|
||||||
|
**Impact:** Frame sequence corruption in the processing pipeline. VO would compare non-consecutive frames.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-7: APIConfig.cors_origins Defaults to Wildcard
|
||||||
|
|
||||||
|
**Issue:** `APIConfig.cors_origins` in `src/gps_denied/config.py` line 32 defaults to `["*"]`. In a field deployment scenario, the SSE position stream (containing real-time UAV location) would be accessible from any origin.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/config.py` line 32
|
||||||
|
|
||||||
|
**Impact:** Security risk in production deployment. The security analysis requires JWT authentication on all endpoints, but CORS wildcard allows cross-origin access to the unauthenticated health endpoint and any future misconfigured endpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MEDIUM-8: APIConfig.reload Defaults to True in Production Config
|
||||||
|
|
||||||
|
**Issue:** `APIConfig.reload: bool = True` in `src/gps_denied/config.py` line 29. Uvicorn's `--reload` mode is for development only. It disables multi-process workers and causes the server to restart on any file change during flight.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/config.py` line 29
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LOW — Minor Issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-1: Rotation Sweep Integration in Processor Is Missing
|
||||||
|
|
||||||
|
**Issue:** `ImageRotationManager.try_rotation_steps()` performs a 360°/30° sweep to find UAV heading. `ImageRotationManager.requires_rotation_sweep()` returns `True` only on the first frame. Neither method is called in `FlightProcessor.process_frame()`. The rotation manager is attached but its heading-sweep capability is unused.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/rotation.py` lines 58-82, 130-139
|
||||||
|
- `src/gps_denied/core/processor.py` — no calls to `rotation.try_rotation_steps` or `rotation.requires_rotation_sweep`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-2: calculate_precise_angle Is Stubbed
|
||||||
|
|
||||||
|
**Issue:** `ImageRotationManager.calculate_precise_angle()` in `src/gps_denied/core/rotation.py` lines 84-94 has the real implementation commented out and returns `initial_angle` unchanged, described as "For simplicity in mock, just return initial."
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/rotation.py` lines 84-94
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-3: No Operator Re-Localization Request After 3 Consecutive Failures
|
||||||
|
|
||||||
|
**Issue:** The acceptance criteria require: "In case the system cannot determine the position of 3 consecutive frames by any means, it should send a re-localization request to the ground station operator via telemetry link." No consecutive-failure counter exists in `FlightProcessor` or `FailureRecoveryCoordinator`. The operator request mechanism (MAVLink NAMED_VALUE_FLOAT or custom message) is not implemented.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/processor.py` — no consecutive failure counter
|
||||||
|
- `src/gps_denied/core/recovery.py` — no failure count tracking
|
||||||
|
- `_docs/00_problem/acceptance_criteria.md` line 18
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-4: Mid-Flight Reboot Recovery Not Implemented
|
||||||
|
|
||||||
|
**Issue:** The acceptance criteria require the system to re-initialize from the flight controller's IMU-extrapolated position on companion computer reboot. No startup recovery sequence reads the FC position, initializes ESKF with high uncertainty, or validates against an initial satellite match.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/app.py` lifespan — no reboot recovery logic
|
||||||
|
- `_docs/00_problem/acceptance_criteria.md` lines 31-32
|
||||||
|
- `_docs/01_solution/solution.md` lines 14-15: estimated 35-70s recovery time documented but unimplemented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-5: Acceptance Tests Run Against Mock Infrastructure Only
|
||||||
|
|
||||||
|
**Issue:** All tests in `tests/test_acceptance.py` use `MockInferenceEngine`, mock repositories, and mock SSE streamers. The 5-second-per-frame performance test (AC-3) is meaningless since it measures random array generation speed. No test validates against real GPS data or real imagery.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `tests/test_acceptance.py` lines 37-51, 124-138
|
||||||
|
|
||||||
|
**Impact:** Tests pass with 100% mock coverage but provide no confidence in real-world accuracy or latency targets (400ms AC requirement).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-6: Telemetry to Ground Station Not Implemented
|
||||||
|
|
||||||
|
**Issue:** The acceptance criteria require position estimates and confidence scores to be streamed to the ground station via telemetry link at 1Hz. The current SSE stream goes to HTTP clients, not the radio telemetry link (MAVLink NAMED_VALUE_FLOAT per solution.md line 120-121).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/core/sse.py` — HTTP SSE only
|
||||||
|
- `_docs/00_problem/acceptance_criteria.md` lines 35-37
|
||||||
|
- Missing: any telemetry/radio link output
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LOW-7: No Security Controls Are Implemented
|
||||||
|
|
||||||
|
**Issue:** The security analysis (`_docs/01_solution/security_analysis.md`) specifies JWT bearer token authentication, TLS 1.3 on all endpoints, tile manifest SHA-256 verification, and Mahalanobis distance outlier rejection in ESKF. None of these are implemented in the current codebase. `APIConfig.cors_origins = ["*"]` is the only access control in place.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/gps_denied/api/deps.py` — no JWT verification present
|
||||||
|
- `src/gps_denied/config.py` — no TLS/JWT secret config
|
||||||
|
- `src/gps_denied/core/satellite.py` — no tile manifest integrity check
|
||||||
|
- `_docs/01_solution/security_analysis.md` — comprehensive spec, unimplemented
|
||||||
|
|
||||||
|
**Note:** The system is designed for conflict-zone deployment. Security controls are not optional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary by Priority
|
||||||
|
|
||||||
|
| Priority | Count | Blocking Category |
|
||||||
|
|----------|-------|-------------------|
|
||||||
|
| CRITICAL | 6 | Core functionality impossible |
|
||||||
|
| HIGH | 7 | System unstable or produces wrong output |
|
||||||
|
| MEDIUM | 8 | Significant operational gaps |
|
||||||
|
| LOW | 7 | Missing AC requirements and security controls |
|
||||||
|
|
||||||
|
**Overall TRL Assessment (implementation vs. documentation):**
|
||||||
|
|
||||||
|
The documentation describes a complete system at TRL ~3. The implementation is at TRL ~1: API scaffolding and data models exist, but all computation-critical components (ESKF, real ML inference, MAVLink output, Faiss index, offline tile pipeline) are stubs or entirely absent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Concerns audit: 2026-04-01*
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Coding Conventions
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
## Naming Patterns
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `snake_case.py` throughout — `coordinates.py`, `chunk_manager.py`, `processor.py`
|
||||||
|
- Test files: `test_<module>.py` prefix — `test_coordinates.py`, `test_api_flights.py`
|
||||||
|
- Schema files grouped by domain — `flight.py`, `vo.py`, `graph.py`, `metric.py`
|
||||||
|
|
||||||
|
**Classes:**
|
||||||
|
- `PascalCase` for all classes — `FlightProcessor`, `CoordinateTransformer`, `FactorGraphOptimizer`
|
||||||
|
- Interface/ABC classes prefixed with `I` — `IImageMatcher`, `ISequentialVisualOdometry`, `IModelManager`
|
||||||
|
- Error/Exception classes suffixed with `Error` — `OriginNotSetError`, `QueueFullError`, `ValidationError`
|
||||||
|
- Pydantic schema classes named after the concept they represent, suffixed with `Request`/`Response` for API boundaries — `FlightCreateRequest`, `FlightDetailResponse`, `BatchUpdateResponse`
|
||||||
|
- Config classes suffixed with `Config` or `Settings` — `DatabaseConfig`, `RecoveryConfig`, `AppSettings`
|
||||||
|
|
||||||
|
**Functions/Methods:**
|
||||||
|
- `snake_case` — `set_enu_origin`, `compute_relative_pose`, `retrieve_candidate_tiles`
|
||||||
|
- Async functions not distinguished by name from sync (no `async_` prefix); the `async def` keyword is the signal
|
||||||
|
- Private/internal methods prefixed with single underscore — `_init_flight`, `_cleanup_flight`, `_publish_frame_result`
|
||||||
|
|
||||||
|
**Variables:**
|
||||||
|
- `snake_case` — `flight_id`, `rel_pose`, `chunk_mgr`
|
||||||
|
- Per-flight in-memory dicts named `_<plural_noun>` keyed by `flight_id` — `_origins`, `_flight_states`, `_prev_images`, `_flight_cameras`
|
||||||
|
- Constants: uppercase not consistently enforced; numeric magic values appear in comments (e.g., `111319.5`)
|
||||||
|
|
||||||
|
**Type Parameters:**
|
||||||
|
- `PascalCase` — standard Python typing conventions
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
**Formatter/Linter:**
|
||||||
|
- `ruff` — configured in `pyproject.toml`
|
||||||
|
- Line length: 100 characters (`line-length = 100`)
|
||||||
|
- Target: Python 3.11 (`target-version = "py311"`)
|
||||||
|
- Ruff rule sets active: `E` (pycodestyle errors), `F` (Pyflakes), `I` (isort), `W` (pycodestyle warnings)
|
||||||
|
- No `B` (flake8-bugbear) or `UP` (pyupgrade) rules enabled
|
||||||
|
|
||||||
|
**Type Hints:**
|
||||||
|
- Type hints on all public method signatures — parameters and return types
|
||||||
|
- `from __future__ import annotations` used in modules with complex forward references (`processor.py`, `flight.py`, `config.py`)
|
||||||
|
- `Optional[T]` used alongside `T | None` syntax (inconsistency — both styles present)
|
||||||
|
- `dict[str, X]` and `list[X]` lowercase generics (Python 3.9+ style)
|
||||||
|
- Untyped component slots use `= None` without annotation — e.g., `self._vo = None` — intentional for lazy init
|
||||||
|
|
||||||
|
## Module Docstrings
|
||||||
|
|
||||||
|
Every source module starts with a one-line docstring naming the component:
|
||||||
|
```python
|
||||||
|
"""Coordinate Transformer (Component F13)."""
|
||||||
|
"""Core Flight Processor — Full Processing Pipeline (Stage 10)."""
|
||||||
|
"""Sequential Visual Odometry (Component F07)."""
|
||||||
|
```
|
||||||
|
Test files also carry docstrings stating what they test and which component ID they cover.
|
||||||
|
|
||||||
|
## Import Organization
|
||||||
|
|
||||||
|
**Order (enforced by ruff `I` rules):**
|
||||||
|
1. Standard library (`__future__`, `asyncio`, `logging`, `math`, `abc`)
|
||||||
|
2. Third-party (`fastapi`, `numpy`, `cv2`, `pydantic`, `sqlalchemy`)
|
||||||
|
3. Internal project (`gps_denied.*`)
|
||||||
|
|
||||||
|
**Internal imports:**
|
||||||
|
- Always absolute — `from gps_denied.core.coordinates import CoordinateTransformer`
|
||||||
|
- Schema imports collected into explicit multi-line blocks at top of file
|
||||||
|
- Lazy in-function imports used to avoid circular imports: `from gps_denied.schemas import Geofences` inside a method body in `processor.py`
|
||||||
|
|
||||||
|
## ABC/Interface Pattern
|
||||||
|
|
||||||
|
All major processing components define an Abstract Base Class in the same module:
|
||||||
|
```python
|
||||||
|
class ISequentialVisualOdometry(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def compute_relative_pose(...) -> RelativePose | None: ...
|
||||||
|
|
||||||
|
class SequentialVisualOdometry(ISequentialVisualOdometry):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
Components that depend on another component accept the interface type in `__init__`, enabling mock injection:
|
||||||
|
```python
|
||||||
|
class SequentialVisualOdometry(ISequentialVisualOdometry):
|
||||||
|
def __init__(self, model_manager: IModelManager): ...
|
||||||
|
```
|
||||||
|
This pattern appears in: `vo.py`, `models.py`, `rotation.py`.
|
||||||
|
|
||||||
|
`FlightProcessor` uses a post-construction injection method instead:
|
||||||
|
```python
|
||||||
|
def attach_components(self, vo=None, gpr=None, metric=None, ...): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pydantic Schema Conventions
|
||||||
|
|
||||||
|
- All schemas inherit from `pydantic.BaseModel`
|
||||||
|
- Validation constraints use `Field(..., ge=..., le=..., gt=..., min_length=..., max_length=...)`
|
||||||
|
- Default values use `Field(default_factory=...)` for mutable defaults
|
||||||
|
- `model_dump()` and `model_validate_json()` used (Pydantic v2 API)
|
||||||
|
- Config classes inherit from `pydantic_settings.BaseSettings` with `SettingsConfigDict(env_prefix=...)`
|
||||||
|
- Grouped into separate schema files by domain: `flight.py`, `vo.py`, `graph.py`, `metric.py`, `gpr.py`, `chunk.py`, `rotation.py`, `satellite.py`, `events.py`, `image.py`, `model.py`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Custom exceptions:**
|
||||||
|
- Defined in the module where they originate — `OriginNotSetError` in `coordinates.py`, `QueueFullError` / `ValidationError` in `pipeline.py`
|
||||||
|
- Inherit directly from `Exception` (no base project exception class)
|
||||||
|
|
||||||
|
**In async pipeline code:**
|
||||||
|
- `try/except Exception as exc` with `logger.warning(...)` — swallowed errors are logged, not re-raised:
|
||||||
|
```python
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("VO failed for frame %d: %s", frame_id, exc)
|
||||||
|
```
|
||||||
|
- HTTP layer raises `HTTPException` from FastAPI — no custom HTTP exception hierarchy
|
||||||
|
|
||||||
|
**Type-ignore suppressions:**
|
||||||
|
- `# type: ignore[union-attr]` used on SQLAlchemy `result.rowcount` accesses in `repository.py`
|
||||||
|
- `# type: ignore` on two FastAPI dependency parameters in `flights.py` (multipart form endpoint)
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
**Framework:** Python stdlib `logging`
|
||||||
|
|
||||||
|
**Setup per module:**
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage patterns:**
|
||||||
|
- `logger.warning(...)` for recoverable failures (VO failure, drift correction failure)
|
||||||
|
- `logger.info(...)` for state machine transitions: `"Flight %s → LOST at frame %d"`
|
||||||
|
- `logger.debug(...)` for high-frequency data: optimization convergence results
|
||||||
|
|
||||||
|
## Section Separators
|
||||||
|
|
||||||
|
Long modules use visual comment dividers to delineate sections:
|
||||||
|
```python
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# State Machine
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
```
|
||||||
|
and
|
||||||
|
```python
|
||||||
|
# =========================================================
|
||||||
|
# process_frame — central orchestration
|
||||||
|
# =========================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Function Design
|
||||||
|
|
||||||
|
**Size:** Methods tend to be 10-40 lines; `process_frame` is intentionally longer (~80 lines) as the central orchestrator.
|
||||||
|
|
||||||
|
**Parameters:** Typed, ordered: `self`, required positional, optional keyword with defaults.
|
||||||
|
|
||||||
|
**Return Values:**
|
||||||
|
- Async methods return domain schema objects or `None` on not-found
|
||||||
|
- Sync methods return `bool` for success/failure of CRUD operations
|
||||||
|
- `Optional[T]` / `T | None` used for nullable returns consistently
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
**Inline comments** used liberally to explain non-obvious math or mock logic:
|
||||||
|
```python
|
||||||
|
# 111319.5 meters per degree at equator
|
||||||
|
# FAKE Math for mockup:
|
||||||
|
# Very rough scaling: assume 1 pixel is ~0.1 meter
|
||||||
|
```
|
||||||
|
|
||||||
|
**Section-level comments** label major logical steps inside long methods:
|
||||||
|
```python
|
||||||
|
# ---- 1. Visual Odometry (frame-to-frame) ----
|
||||||
|
# ---- 2. State Machine transitions ----
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Design
|
||||||
|
|
||||||
|
**Exports:** No explicit `__all__` in any module; consumers import directly from sub-modules.
|
||||||
|
|
||||||
|
**Barrel Files:** `src/gps_denied/schemas/__init__.py` re-exports common types (`GPSPoint`, `CameraParameters`, `Geofences`, `Polygon`). Other packages use direct imports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Convention analysis: 2026-04-01*
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# External Integrations
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
## APIs & External Services
|
||||||
|
|
||||||
|
**Satellite Tile Provider:**
|
||||||
|
- Google Maps Slippy Tile API — satellite imagery for navigation reference
|
||||||
|
- URL pattern (actively in code): `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={zoom}`
|
||||||
|
- Used in: `src/gps_denied/core/satellite.py` (`SatelliteDataManager.fetch_tile`)
|
||||||
|
- Auth: none in current code — the URL is called unauthenticated via the undocumented tile CDN
|
||||||
|
- Config: `TILES_PROVIDER`, `TILES_API_KEY` env vars (key field exists in `TileProviderConfig` but not threaded into URL construction yet)
|
||||||
|
- Note: docs specify Google Maps Tile API with a paid API key; current code hits the public CDN endpoint without a key. This is a mismatch — the key config is wired in `src/gps_denied/config.py` but not used in `satellite.py`.
|
||||||
|
- Alternative providers documented (not implemented): Bing Maps, Mapbox Satellite
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
**Databases:**
|
||||||
|
- SQLite (async) — primary operational datastore
|
||||||
|
- Default connection: `sqlite+aiosqlite:///./flight_data.db` (relative to CWD)
|
||||||
|
- Configurable via `DB_URL` env var (any SQLAlchemy async-compatible URL)
|
||||||
|
- Client: SQLAlchemy >=2 async ORM (`src/gps_denied/db/engine.py`)
|
||||||
|
- Session factory: `async_sessionmaker` in `src/gps_denied/db/engine.py`
|
||||||
|
- Models: `src/gps_denied/db/models.py`
|
||||||
|
- Migrations: Alembic >=1.14 (migration scripts directory not confirmed in scan)
|
||||||
|
- SQLite pragmas: `PRAGMA foreign_keys=ON` enforced on every connection
|
||||||
|
|
||||||
|
**File Storage:**
|
||||||
|
- Local filesystem — satellite tile cache via diskcache
|
||||||
|
- Cache directory: `.satellite_cache` (default) or `TILES_CACHE_DIR` env var
|
||||||
|
- Size limit: 10GB default (configurable in `SatelliteDataManager.__init__`)
|
||||||
|
- Key scheme: `{flight_id}_{zoom}_{x}_{y}` (PNG-encoded bytes)
|
||||||
|
- Implementation: `src/gps_denied/core/satellite.py`
|
||||||
|
- Local filesystem — ML model weights
|
||||||
|
- Directory: `weights/` (configurable via `MODEL_WEIGHTS_DIR` env var)
|
||||||
|
|
||||||
|
**Caching:**
|
||||||
|
- diskcache >=5.6 — disk-based LRU cache for satellite tiles
|
||||||
|
- No in-memory cache layer (Redis/Memcached not used)
|
||||||
|
|
||||||
|
## Hardware Interfaces
|
||||||
|
|
||||||
|
**Camera (UAV):**
|
||||||
|
- Raw image frames ingested via REST API (`POST /flights/{id}/frames`) as multipart file uploads
|
||||||
|
- No direct camera SDK or hardware driver in codebase — hardware abstraction delegated to the flight controller or a separate capture process
|
||||||
|
- Frame format: image files (JPEG/PNG) decoded by OpenCV (`cv2.imdecode`)
|
||||||
|
|
||||||
|
**IMU:**
|
||||||
|
- IMU data referenced in architecture (`CameraParameters` schema, ESKF fusion design)
|
||||||
|
- No direct IMU hardware driver in current code — IMU input pathway not yet implemented
|
||||||
|
- Planned: multi-rate fusion at ~100Hz IMU vs ~3fps camera, implemented in custom ESKF
|
||||||
|
|
||||||
|
**GPU / CUDA (Jetson Orin Nano Super):**
|
||||||
|
- CUDA 12.6.10 — JetPack-bundled, accessed via OpenCV CUDA bindings and TensorRT
|
||||||
|
- TensorRT 10.3.0 — planned FP16 inference engine for SuperPoint, LightGlue, LiteSAM/XFeat
|
||||||
|
- Wrapped behind `IModelManager.optimize_to_tensorrt()` interface in `src/gps_denied/core/models.py`
|
||||||
|
- Current implementation uses `MockInferenceEngine` stubs
|
||||||
|
- NVIDIA VPI 3.2 — JetPack-bundled; planned as alternative to OpenCV CUDA for image resize (not yet used in code)
|
||||||
|
|
||||||
|
## Authentication & Identity
|
||||||
|
|
||||||
|
**Auth Provider:**
|
||||||
|
- None — no authentication on the REST API in current code
|
||||||
|
- CORS origins default to `["*"]` (`APIConfig` in `src/gps_denied/config.py`)
|
||||||
|
- Intended for local on-device operation (Jetson Orin Nano Super runs in air-gapped environment)
|
||||||
|
|
||||||
|
## Visual Odometry Engine
|
||||||
|
|
||||||
|
**PyCuVSLAM v15.0.0 (NVIDIA, closed-source):**
|
||||||
|
- Planned primary VO engine — SLAM with mono+IMU mode, loop closure, 116fps at 720p on Orin Nano
|
||||||
|
- Install: local aarch64 wheel from `bin/aarch64/` (not on PyPI)
|
||||||
|
- Abstracted behind `ISequentialVisualOdometry` interface in `src/gps_denied/core/vo.py`
|
||||||
|
- Not yet integrated — current `SequentialVisualOdometry` class uses SuperPoint+LightGlue via `ModelManager`
|
||||||
|
- Fallback if cuVSLAM fails: XFeat frame-to-frame matching (also not yet implemented)
|
||||||
|
|
||||||
|
## Factor Graph & Sensor Fusion
|
||||||
|
|
||||||
|
**GTSAM >=4.3a0:**
|
||||||
|
- Factor graph optimizer for route chunk pose graph optimization
|
||||||
|
- Used in `src/gps_denied/core/graph.py` (`FactorGraphOptimizer`)
|
||||||
|
- Import guarded with try/except — `HAS_GTSAM` flag allows graceful degradation
|
||||||
|
- Custom ESKF (16-state, NumPy/SciPy) is the planned primary fusion engine at 100Hz — not yet a separate file; referenced in docs only
|
||||||
|
|
||||||
|
## Streaming Protocol
|
||||||
|
|
||||||
|
**Server-Sent Events (SSE):**
|
||||||
|
- sse-starlette >=2.0 — `EventSourceResponse` used in `src/gps_denied/api/routers/flights.py`
|
||||||
|
- Streams real-time geolocalization results to a connected client during frame processing
|
||||||
|
- SSE utility helpers in `src/gps_denied/core/sse.py`
|
||||||
|
- No WebSocket or polling fallback
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
**GeoJSON (RFC 7946):**
|
||||||
|
- Primary coordinate output format (WGS84)
|
||||||
|
- `geojson` Python package planned (not yet in `pyproject.toml`)
|
||||||
|
- GPS coordinates returned in API responses via Pydantic schemas (`GPSPoint`, `ObjectGPSResponse`)
|
||||||
|
|
||||||
|
**CSV:**
|
||||||
|
- Documented as secondary export option; not yet implemented
|
||||||
|
|
||||||
|
## Tile Indexing Strategy
|
||||||
|
|
||||||
|
**Mercator / Slippy Tile Coordinates:**
|
||||||
|
- Web Mercator projection (XYZ tile scheme, same as Google Maps / OSM)
|
||||||
|
- Conversion utilities in `src/gps_denied/utils/mercator.py` (`latlon_to_tile`, `compute_tile_bounds`)
|
||||||
|
- Called from `src/gps_denied/core/satellite.py`
|
||||||
|
|
||||||
|
**GeoHash Indexing (planned, not yet implemented):**
|
||||||
|
- Docs specify `{geohash}/{zoom}_{x}_{y}.jpg` directory structure for pre-flight tile storage
|
||||||
|
- `pygeohash` package planned but not in `pyproject.toml`
|
||||||
|
- Current implementation uses diskcache flat-key scheme instead
|
||||||
|
|
||||||
|
## CI/CD & Deployment
|
||||||
|
|
||||||
|
**Hosting:**
|
||||||
|
- On-device: NVIDIA Jetson Orin Nano Super (air-gapped during flight)
|
||||||
|
- Development: x86_64 Linux with Docker+CUDA
|
||||||
|
|
||||||
|
**CI Pipeline:**
|
||||||
|
- Not detected in scanned directories (no `.github/`, `.gitlab-ci.yml`, etc. found)
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
**Required env vars (non-default critical settings):**
|
||||||
|
- `DB_URL` — override default SQLite path for production
|
||||||
|
- `TILES_API_KEY` — Google Maps API key (currently unused in tile fetch code — see mismatch note above)
|
||||||
|
- `MODEL_WEIGHTS_DIR` — path to TensorRT/ONNX model weight files
|
||||||
|
- `AREA_*` — operational area bounding box (defaults to Eastern Ukraine: lat 45-52, lon 22-40)
|
||||||
|
|
||||||
|
**Secrets location:**
|
||||||
|
- `.env` file in project root (not committed — standard pydantic-settings pattern)
|
||||||
|
|
||||||
|
## Webhooks & Callbacks
|
||||||
|
|
||||||
|
**Incoming:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Outgoing:**
|
||||||
|
- None — system is self-contained on the UAV; results streamed to connected client via SSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Integration audit: 2026-04-01*
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Technology Stack
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
**Primary:**
|
||||||
|
- Python 3.11+ — entire application (enforced in `pyproject.toml` via `requires-python = ">=3.11"`)
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- Target hardware: NVIDIA Jetson Orin Nano Super 8GB (ARM64/aarch64)
|
||||||
|
- Target OS: JetPack 6.2.2 (L4T / Ubuntu 22.04)
|
||||||
|
- CUDA 12.6.10, TensorRT 10.3.0, cuDNN 9.3 (all JetPack-bundled, not in pyproject.toml)
|
||||||
|
- Development/CI: x86_64 Linux with Docker+CUDA (cuVSLAM and TensorRT mocked)
|
||||||
|
|
||||||
|
**Package Manager:**
|
||||||
|
- pip / setuptools >=75
|
||||||
|
- Lockfile: not present (no `requirements.txt`, no lock file)
|
||||||
|
|
||||||
|
## Frameworks
|
||||||
|
|
||||||
|
**Core:**
|
||||||
|
- FastAPI (unpinned, >=0.115 per docs) — REST API framework with auto-generated OpenAPI docs
|
||||||
|
- Uvicorn (`uvicorn[standard]`, unpinned) — ASGI server
|
||||||
|
- sse-starlette >=2.0 — Server-Sent Events streaming (`EventSourceResponse` actively used in `src/gps_denied/api/routers/flights.py`)
|
||||||
|
|
||||||
|
**Data Validation / Settings:**
|
||||||
|
- Pydantic >=2 — schema definitions across all `src/gps_denied/schemas/*.py`
|
||||||
|
- pydantic-settings >=2 — env/YAML configuration in `src/gps_denied/config.py`
|
||||||
|
|
||||||
|
**Database:**
|
||||||
|
- SQLAlchemy >=2 (async) — ORM and query layer (`src/gps_denied/db/engine.py`, `src/gps_denied/db/models.py`)
|
||||||
|
- Alembic >=1.14 — database migrations
|
||||||
|
- aiosqlite >=0.20 — async SQLite driver (default DB URL: `sqlite+aiosqlite:///./flight_data.db`)
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- pytest >=8.0 — test runner (config in `pyproject.toml`, `testpaths = ["tests"]`)
|
||||||
|
- pytest-asyncio >=0.24 — async test support (`asyncio_mode = "auto"`)
|
||||||
|
- httpx >=0.28 (dev) — HTTP client for integration tests
|
||||||
|
|
||||||
|
**Linting / Formatting:**
|
||||||
|
- ruff >=0.9 — linting and formatting (`target-version = "py311"`, `line-length = 100`)
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
**HTTP Client:**
|
||||||
|
- httpx >=0.27 — async HTTP client used in `src/gps_denied/core/satellite.py` for tile fetching
|
||||||
|
|
||||||
|
**Computer Vision:**
|
||||||
|
- opencv-python-headless >=4.9 — image decode/encode, feature detection/matching, resize
|
||||||
|
- Actively imported in `src/gps_denied/core/satellite.py` and `src/gps_denied/core/vo.py`
|
||||||
|
- Note: docs specify a CUDA-enabled build must be compiled from source (`CUDA_ARCH_BIN=8.7`) on Jetson; pyproject.toml installs the headless CPU build as default
|
||||||
|
- numpy >=1.26 — array operations, ESKF math, image buffer handling (used throughout core modules)
|
||||||
|
|
||||||
|
**Tile Caching:**
|
||||||
|
- diskcache >=5.6 — disk-based tile cache with size limit in `src/gps_denied/core/satellite.py`
|
||||||
|
|
||||||
|
**Factor Graph Optimization:**
|
||||||
|
- gtsam >=4.3a0 — factor graph optimizer (`src/gps_denied/core/graph.py`)
|
||||||
|
- Import is wrapped in a try/except (`HAS_GTSAM` flag); the module degrades gracefully if not installed
|
||||||
|
|
||||||
|
**Multipart Upload:**
|
||||||
|
- python-multipart >=0.0.9 — required for FastAPI `UploadFile` in flight frame ingestion
|
||||||
|
|
||||||
|
## Dependencies Planned-but-Not-in-pyproject.toml
|
||||||
|
|
||||||
|
These appear in `_docs/01_solution/tech_stack.md` but are NOT listed in `pyproject.toml` or `requires.txt`:
|
||||||
|
|
||||||
|
| Package | Planned Purpose | Status |
|
||||||
|
|---------|----------------|--------|
|
||||||
|
| PyCuVSLAM v15.0.0 (aarch64 wheel) | Primary visual odometry engine | Planned — local wheel install from `bin/aarch64/` |
|
||||||
|
| scipy | Rotation matrices, ESKF spatial transforms | Planned — not yet in deps |
|
||||||
|
| torch (JetPack-compatible aarch64) | LiteSAM / XFeat model loading | Conditional — depends on day-1 benchmark |
|
||||||
|
| tensorrt 10.3.0 | TensorRT FP16 inference engine | JetPack-bundled, not pip-installed |
|
||||||
|
| pycuda | CUDA stream management | Planned, not yet added |
|
||||||
|
| geojson | GeoJSON output formatting | Planned, not yet added |
|
||||||
|
| pygeohash | GeoHash tile indexing | Planned, not yet added |
|
||||||
|
| filterpy v1.4.5 | ESKF reference/fallback | Planned, not yet added |
|
||||||
|
|
||||||
|
The VO interface in `src/gps_denied/core/vo.py` is written against an abstract `ISequentialVisualOdometry` — PyCuVSLAM implementation is a planned concrete subclass. Current `ModelManager` in `src/gps_denied/core/models.py` uses `MockInferenceEngine` stubs (SuperPoint/LightGlue mock returning random keypoints).
|
||||||
|
|
||||||
|
## ML Models (Planned)
|
||||||
|
|
||||||
|
| Model | Format | Purpose |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| SuperPoint | TensorRT FP16 / ONNX | Frame keypoint extraction (VO) |
|
||||||
|
| LightGlue | TensorRT FP16 / ONNX | Keypoint matching (VO) |
|
||||||
|
| LiteSAM (opt) OR XFeat semi-dense | TensorRT FP16 | Satellite image matching — selection via day-1 benchmark at 480px |
|
||||||
|
|
||||||
|
Weights expected at paths configured in `ModelPaths` settings (`src/gps_denied/config.py`): `weights/superpoint.pt`, `weights/lightglue.pt`, `weights/litesam.pt`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- Loaded from `.env` file (root) via pydantic-settings `AppSettings` in `src/gps_denied/config.py`
|
||||||
|
- Nested delimiter: `__` (e.g., `DB__URL=...`)
|
||||||
|
- Key env prefix groups: `DB_`, `API_`, `TILES_`, `MODEL_`, `AREA_`, `RECOVERY_`, `ROTATION_`
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
- `pyproject.toml` — project metadata, dependencies, ruff and pytest config
|
||||||
|
- No Dockerfile present in scanned directories
|
||||||
|
|
||||||
|
## Platform Requirements
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- Python 3.11+, pip
|
||||||
|
- x86_64 Linux with GPU (Docker+CUDA) for model export and unit tests
|
||||||
|
- cuVSLAM and TensorRT mocked in unit tests (per documented strategy)
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
- NVIDIA Jetson Orin Nano Super 8GB, JetPack 6.2.2
|
||||||
|
- No internet connectivity during flight (all tiles pre-fetched)
|
||||||
|
- OpenCV must be compiled from source with `CUDA_ARCH_BIN=8.7` for GPU preprocessing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Stack analysis: 2026-04-01*
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# Codebase Structure
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
gps-denied-onboard/
|
||||||
|
├── src/
|
||||||
|
│ └── gps_denied/ # Main package
|
||||||
|
│ ├── __init__.py # Package version
|
||||||
|
│ ├── __main__.py # Entry point (uvicorn runner)
|
||||||
|
│ ├── app.py # FastAPI app factory + lifespan
|
||||||
|
│ ├── config.py # Pydantic-settings configuration classes
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── deps.py # FastAPI dependency providers (singletons)
|
||||||
|
│ │ └── routers/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── flights.py # All /flights/* endpoints
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── processor.py # FlightProcessor — main orchestrator
|
||||||
|
│ │ ├── pipeline.py # ImageInputPipeline — image ingestion/queuing
|
||||||
|
│ │ ├── vo.py # SequentialVisualOdometry (SuperPoint+LightGlue)
|
||||||
|
│ │ ├── satellite.py # SatelliteDataManager (tile fetch/cache)
|
||||||
|
│ │ ├── gpr.py # GlobalPlaceRecognition (AnyLoc/DINOv2)
|
||||||
|
│ │ ├── metric.py # MetricRefinement (LiteSAM homography alignment)
|
||||||
|
│ │ ├── graph.py # FactorGraphOptimizer (GTSAM/mock pose graph)
|
||||||
|
│ │ ├── chunk_manager.py # RouteChunkManager (disconnected segments)
|
||||||
|
│ │ ├── recovery.py # FailureRecoveryCoordinator (tracking loss handling)
|
||||||
|
│ │ ├── rotation.py # ImageRotationManager (360° sweep, heading tracking)
|
||||||
|
│ │ ├── coordinates.py # CoordinateTransformer (ENU↔GPS, pixel→GPS)
|
||||||
|
│ │ ├── models.py # ModelManager + MockInferenceEngine
|
||||||
|
│ │ ├── results.py # ResultManager (DB + SSE publish)
|
||||||
|
│ │ └── sse.py # SSEEventStreamer (per-flight async queues)
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── engine.py # Async SQLAlchemy engine + session factory
|
||||||
|
│ │ ├── models.py # ORM table definitions (8 tables)
|
||||||
|
│ │ └── repository.py # FlightRepository DAO (all SQL operations)
|
||||||
|
│ ├── schemas/
|
||||||
|
│ │ ├── __init__.py # GPSPoint, CameraParameters, Polygon, Geofences
|
||||||
|
│ │ ├── flight.py # Flight CRUD request/response schemas
|
||||||
|
│ │ ├── image.py # ImageBatch, ImageData, ProcessedBatch schemas
|
||||||
|
│ │ ├── vo.py # Features, Matches, Motion, RelativePose
|
||||||
|
│ │ ├── gpr.py # TileCandidate, DatabaseMatch
|
||||||
|
│ │ ├── metric.py # AlignmentResult, ChunkAlignmentResult, Sim3Transform
|
||||||
|
│ │ ├── satellite.py # TileCoords, TileBounds
|
||||||
|
│ │ ├── graph.py # Pose, OptimizationResult, FactorGraphConfig
|
||||||
|
│ │ ├── chunk.py # ChunkHandle, ChunkStatus enum
|
||||||
|
│ │ ├── rotation.py # RotationResult, HeadingHistory
|
||||||
|
│ │ ├── model.py # InferenceEngine base schema
|
||||||
|
│ │ └── events.py # SSEMessage, SSEEventType, FrameProcessedEvent, etc.
|
||||||
|
│ └── utils/
|
||||||
|
│ └── mercator.py # Slippy map tile math (lat/lon↔tile x/y/bounds)
|
||||||
|
├── _docs/
|
||||||
|
│ ├── _autopilot_state.md # GSD planning state tracker
|
||||||
|
│ ├── 01_solution/
|
||||||
|
│ │ └── solution.md # Master architecture document (finalized draft 06)
|
||||||
|
│ └── 02_document/
|
||||||
|
│ └── tests/ # Test scenario specifications (43 scenarios)
|
||||||
|
├── .planning/
|
||||||
|
│ └── codebase/ # GSD mapping documents (this directory)
|
||||||
|
├── .venv/ # Python virtual environment (committed or local)
|
||||||
|
├── pyproject.toml # Project metadata and dependencies
|
||||||
|
└── flight_data.db # SQLite DB (runtime artifact, not committed)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Purposes
|
||||||
|
|
||||||
|
**`src/gps_denied/`:**
|
||||||
|
- Purpose: Top-level Python package; everything importable from here
|
||||||
|
- Key files: `app.py` (create_app factory), `config.py` (all settings), `__main__.py` (entrypoint)
|
||||||
|
|
||||||
|
**`src/gps_denied/api/`:**
|
||||||
|
- Purpose: HTTP API surface only — routing, request/response handling, dependency injection
|
||||||
|
- Contains: One router (`flights.py`) with 9 endpoints, `deps.py` with 3 singleton providers
|
||||||
|
- Key files: `routers/flights.py` (all endpoints), `deps.py` (ProcessorDep, SessionDep, RepoDep)
|
||||||
|
|
||||||
|
**`src/gps_denied/core/`:**
|
||||||
|
- Purpose: All processing logic — state machines, algorithms, pipeline components
|
||||||
|
- Module count: 14 files
|
||||||
|
- Key files: `processor.py` (orchestrator), `models.py` (inference abstraction), `graph.py` (pose graph)
|
||||||
|
- Note: Each component has a matching `I*` ABC interface defined in the same file
|
||||||
|
|
||||||
|
**`src/gps_denied/db/`:**
|
||||||
|
- Purpose: Database access only — no business logic
|
||||||
|
- Contains: Engine setup, ORM models, one repository class
|
||||||
|
- Key files: `models.py` (8 ORM tables), `repository.py` (all DB operations)
|
||||||
|
|
||||||
|
**`src/gps_denied/schemas/`:**
|
||||||
|
- Purpose: Pydantic data contracts; shared across API, core, and DB layers
|
||||||
|
- Contains: 12 schema files covering every domain concept
|
||||||
|
- Key files: `__init__.py` (GPSPoint, CameraParameters — imported everywhere), `flight.py` (largest — all REST schemas)
|
||||||
|
|
||||||
|
**`src/gps_denied/utils/`:**
|
||||||
|
- Purpose: Pure utility functions with no side effects
|
||||||
|
- Contains: `mercator.py` — Mercator projection math for tile coordinate conversion
|
||||||
|
- Used by: `satellite.py` exclusively
|
||||||
|
|
||||||
|
**`_docs/`:**
|
||||||
|
- Purpose: Architecture documentation and planning artifacts — not source code
|
||||||
|
- Key files: `01_solution/solution.md` (the authoritative design doc), `_autopilot_state.md` (GSD planning tracker)
|
||||||
|
|
||||||
|
## Key File Locations
|
||||||
|
|
||||||
|
**Entry Points:**
|
||||||
|
- `src/gps_denied/__main__.py`: Invoked by `python -m gps_denied`; starts uvicorn
|
||||||
|
- `src/gps_denied/app.py`: `create_app()` factory; `app` module-level instance used by uvicorn
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- `src/gps_denied/config.py`: All settings via `pydantic-settings`; reads from `.env` and env vars
|
||||||
|
- `DB_URL` → SQLAlchemy connection string (default: SQLite)
|
||||||
|
- `TILES_ZOOM_LEVEL`, `TILES_CACHE_DIR`, `TILES_API_KEY` → satellite tile provider
|
||||||
|
- `MODEL_WEIGHTS_DIR`, `MODEL_SUPERPOINT_PATH`, etc. → model file paths
|
||||||
|
- `RECOVERY_*`, `ROTATION_*` → algorithm thresholds
|
||||||
|
|
||||||
|
**Core Orchestration:**
|
||||||
|
- `src/gps_denied/core/processor.py`: `FlightProcessor` — only class that calls other pipeline components
|
||||||
|
|
||||||
|
**Dependency Wiring:**
|
||||||
|
- `src/gps_denied/api/deps.py`: Single location where singletons (`FlightProcessor`, `SSEEventStreamer`) are created and wired; `ProcessorDep` type alias used in all routers
|
||||||
|
|
||||||
|
**Database Schema:**
|
||||||
|
- `src/gps_denied/db/models.py`: All 8 ORM tables with cascade deletes; ground truth for DB schema
|
||||||
|
|
||||||
|
**Pydantic Contracts:**
|
||||||
|
- `src/gps_denied/schemas/__init__.py`: `GPSPoint` and `CameraParameters` — imported by nearly every module
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `snake_case.py` throughout
|
||||||
|
- Core components named after their function: `vo.py`, `gpr.py`, `metric.py`, `graph.py`
|
||||||
|
- Interfaces co-located with implementation in the same file (`ISequentialVisualOdometry` and `SequentialVisualOdometry` both in `vo.py`)
|
||||||
|
|
||||||
|
**Classes:**
|
||||||
|
- Implementation classes: `PascalCase` noun phrases (`FlightProcessor`, `RouteChunkManager`)
|
||||||
|
- Interfaces: `I` prefix + implementation name (`IRouteChunkManager`)
|
||||||
|
- ORM models: suffixed with `Row` (`FlightRow`, `WaypointRow`)
|
||||||
|
- Pydantic models: suffixed with `Request`/`Response` for API schemas; bare names for internal data
|
||||||
|
|
||||||
|
**Functions:**
|
||||||
|
- `snake_case`
|
||||||
|
- Async functions throughout API and DB layers; sync in core (except `process_frame` which is async)
|
||||||
|
|
||||||
|
**Constants/Enums:**
|
||||||
|
- `TrackingState`: `str, Enum` with lowercase values (`"normal"`, `"lost"`, `"recovery"`)
|
||||||
|
- `SSEEventType`: `str, Enum` with snake_case values
|
||||||
|
- `ChunkStatus`: `str, Enum`
|
||||||
|
|
||||||
|
## Where to Add New Code
|
||||||
|
|
||||||
|
**New API endpoint:**
|
||||||
|
- Add route function to `src/gps_denied/api/routers/flights.py`
|
||||||
|
- Add request/response Pydantic models to `src/gps_denied/schemas/flight.py`
|
||||||
|
- Add business logic method to `src/gps_denied/core/processor.py`
|
||||||
|
- If new router prefix needed: create `src/gps_denied/api/routers/{name}.py` and register in `app.py`
|
||||||
|
|
||||||
|
**New pipeline component:**
|
||||||
|
- Create `src/gps_denied/core/{component_name}.py`
|
||||||
|
- Define `I{ComponentName}(ABC)` interface and `{ComponentName}` implementation in same file
|
||||||
|
- Add schemas to `src/gps_denied/schemas/{component_name}.py`
|
||||||
|
- Add component instantiation to `app.py` lifespan block
|
||||||
|
- Inject via `FlightProcessor.attach_components()` and call from `process_frame()`
|
||||||
|
|
||||||
|
**New database table:**
|
||||||
|
- Add `{Name}Row(Base)` to `src/gps_denied/db/models.py`
|
||||||
|
- Add CRUD methods to `src/gps_denied/db/repository.py`
|
||||||
|
- Add foreign key + relationship to `FlightRow` if flight-scoped
|
||||||
|
|
||||||
|
**New configuration setting:**
|
||||||
|
- Add field to the appropriate `BaseSettings` subclass in `src/gps_denied/config.py`
|
||||||
|
- Use `ENV_PREFIX__FIELD_NAME` env var pattern
|
||||||
|
|
||||||
|
**New schema type:**
|
||||||
|
- Add to `src/gps_denied/schemas/{relevant_file}.py` or create new file
|
||||||
|
- If broadly shared (like `GPSPoint`), add to `src/gps_denied/schemas/__init__.py`
|
||||||
|
|
||||||
|
**Utility functions:**
|
||||||
|
- Add to `src/gps_denied/utils/mercator.py` if geospatial math
|
||||||
|
- Create `src/gps_denied/utils/{name}.py` for other pure utilities
|
||||||
|
|
||||||
|
## Special Directories
|
||||||
|
|
||||||
|
**`.venv/`:**
|
||||||
|
- Purpose: Python virtual environment
|
||||||
|
- Generated: Yes (via `python -m venv` or uv)
|
||||||
|
- Committed: Present in tree (check `.gitignore`)
|
||||||
|
|
||||||
|
**`_docs/`:**
|
||||||
|
- Purpose: Architecture docs, test specs, GSD planning artifacts
|
||||||
|
- Generated: No (hand-authored)
|
||||||
|
- Committed: Yes
|
||||||
|
|
||||||
|
**`.planning/`:**
|
||||||
|
- Purpose: GSD (Get Stuff Done) planning system — phase plans, codebase maps
|
||||||
|
- Generated: By GSD commands
|
||||||
|
- Committed: Yes (used by CI/planning tools)
|
||||||
|
|
||||||
|
**`.image_storage/` (runtime):**
|
||||||
|
- Purpose: Disk storage for uploaded UAV images per flight
|
||||||
|
- Created by: `ImageInputPipeline.store_images()`
|
||||||
|
- Path: Relative to process CWD; configurable via `ImageInputPipeline(storage_dir=...)`
|
||||||
|
|
||||||
|
**`.satellite_cache/` (runtime):**
|
||||||
|
- Purpose: diskcache directory for satellite tiles
|
||||||
|
- Created by: `SatelliteDataManager.__init__()`
|
||||||
|
- Size limit: 10GB default
|
||||||
|
|
||||||
|
## Module Dependency Graph (simplified)
|
||||||
|
|
||||||
|
```
|
||||||
|
api/routers/flights.py
|
||||||
|
→ api/deps.py
|
||||||
|
→ core/processor.py
|
||||||
|
→ core/pipeline.py (image ingestion)
|
||||||
|
→ core/results.py (DB + SSE publish)
|
||||||
|
→ core/sse.py (event streaming)
|
||||||
|
→ [via attach_components]
|
||||||
|
→ core/vo.py → core/models.py
|
||||||
|
→ core/gpr.py → core/models.py
|
||||||
|
→ core/metric.py → core/models.py
|
||||||
|
→ core/graph.py (optional: gtsam)
|
||||||
|
→ core/chunk_manager.py → core/graph.py
|
||||||
|
→ core/recovery.py → core/chunk_manager.py
|
||||||
|
→ core/gpr.py
|
||||||
|
→ core/metric.py
|
||||||
|
→ core/rotation.py
|
||||||
|
→ db/repository.py
|
||||||
|
→ db/models.py
|
||||||
|
→ core/sse.py
|
||||||
|
→ db/engine.py
|
||||||
|
|
||||||
|
core/satellite.py (standalone — not wired into processor yet)
|
||||||
|
core/coordinates.py (standalone — not wired into processor yet)
|
||||||
|
utils/mercator.py → only used by core/satellite.py
|
||||||
|
schemas/* → imported by all layers (no internal imports)
|
||||||
|
config.py → imported by db/engine.py, and anywhere settings needed
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notably standalone (not connected to processor):**
|
||||||
|
- `src/gps_denied/core/satellite.py`: `SatelliteDataManager` — instantiated nowhere in `app.py` or `processor.py`; satellite tiles fetched inline with mock in `processor.py`
|
||||||
|
- `src/gps_denied/core/coordinates.py`: `CoordinateTransformer` — not instantiated in the pipeline; `convert_object_to_gps` in processor returns a hardcoded stub (`GPSPoint(lat=48.0, lon=37.0)`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Structure analysis: 2026-04-01*
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# Testing Patterns
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-04-01
|
||||||
|
|
||||||
|
## Test Framework
|
||||||
|
|
||||||
|
**Runner:**
|
||||||
|
- `pytest` >= 8.0
|
||||||
|
- Config: `pyproject.toml` `[tool.pytest.ini_options]` section
|
||||||
|
- `testpaths = ["tests"]`
|
||||||
|
- `asyncio_mode = "auto"` — all async tests run automatically without per-test `@pytest.mark.asyncio` decorator (though some tests still apply it explicitly for clarity)
|
||||||
|
|
||||||
|
**Async Extension:**
|
||||||
|
- `pytest-asyncio` >= 0.24
|
||||||
|
|
||||||
|
**HTTP Client for API tests:**
|
||||||
|
- `httpx` >= 0.28 with `ASGITransport` — in-process FastAPI testing, no real server needed
|
||||||
|
|
||||||
|
**Run Commands:**
|
||||||
|
```bash
|
||||||
|
pytest # Run all tests
|
||||||
|
pytest tests/test_<module>.py # Run single file
|
||||||
|
pytest -v # Verbose with test names
|
||||||
|
pytest -x # Stop on first failure
|
||||||
|
# No coverage command configured; no pytest-cov in dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test File Organization
|
||||||
|
|
||||||
|
**Location:** All tests in `/tests/` flat directory — no sub-directories.
|
||||||
|
|
||||||
|
**Naming:** `test_<module>.py` maps to the component being tested:
|
||||||
|
|
||||||
|
| Test File | Component Tested | Component ID |
|
||||||
|
|-----------|-----------------|--------------|
|
||||||
|
| `test_coordinates.py` | `CoordinateTransformer` | F13 |
|
||||||
|
| `test_vo.py` | `SequentialVisualOdometry` | F07 |
|
||||||
|
| `test_gpr.py` | `GlobalPlaceRecognition` | F08 |
|
||||||
|
| `test_metric.py` | `MetricRefinement` | F09 |
|
||||||
|
| `test_graph.py` | `FactorGraphOptimizer` | F10 |
|
||||||
|
| `test_recovery.py` | `FailureRecoveryCoordinator` | F11 |
|
||||||
|
| `test_chunk_manager.py` | `RouteChunkManager` | F12 |
|
||||||
|
| `test_rotation.py` | `ImageRotationManager` | F06 |
|
||||||
|
| `test_pipeline.py` | `ImageInputPipeline` | F05 |
|
||||||
|
| `test_satellite.py` | `SatelliteDataManager` + `mercator` utils | F04, H06 |
|
||||||
|
| `test_models.py` | `ModelManager` | F16 |
|
||||||
|
| `test_processor_full.py` | `FlightProcessor` orchestration | F15 (Stage 10) |
|
||||||
|
| `test_acceptance.py` | Full pipeline acceptance scenarios | AC-1 through AC-6 |
|
||||||
|
| `test_api_flights.py` | REST API endpoints | HTTP integration |
|
||||||
|
| `test_schemas.py` | Pydantic schemas + config | Domain validation |
|
||||||
|
| `test_database.py` | `FlightRepository` + DB models | Persistence layer |
|
||||||
|
| `test_health.py` | `/health` endpoint | Smoke test |
|
||||||
|
|
||||||
|
No `conftest.py` exists — fixtures are defined locally per test file.
|
||||||
|
|
||||||
|
## Test Count Summary
|
||||||
|
|
||||||
|
| File | Test Functions |
|
||||||
|
|------|---------------|
|
||||||
|
| `test_schemas.py` | 16 (class-based: 6 `TestXxx` classes) |
|
||||||
|
| `test_database.py` | 9 |
|
||||||
|
| `test_acceptance.py` | 6 |
|
||||||
|
| `test_api_flights.py` | 5 |
|
||||||
|
| `test_vo.py` | 5 |
|
||||||
|
| `test_satellite.py` | 5 |
|
||||||
|
| `test_coordinates.py` | 4 |
|
||||||
|
| `test_graph.py` | 4 |
|
||||||
|
| `test_rotation.py` | 4 |
|
||||||
|
| `test_processor_full.py` | 4 |
|
||||||
|
| `test_pipeline.py` | 3 |
|
||||||
|
| `test_gpr.py` | 3 |
|
||||||
|
| `test_metric.py` | 3 |
|
||||||
|
| `test_models.py` | 3 |
|
||||||
|
| `test_chunk_manager.py` | 3 |
|
||||||
|
| `test_recovery.py` | 2 |
|
||||||
|
| `test_health.py` | 1 |
|
||||||
|
| **Total** | **~85 tests** |
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
**Fixture-based setup (dominant pattern):**
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
def transformer():
|
||||||
|
return CoordinateTransformer()
|
||||||
|
|
||||||
|
def test_gps_to_enu(transformer):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Class-based grouping (schemas only):**
|
||||||
|
```python
|
||||||
|
class TestGPSPoint:
|
||||||
|
def test_valid(self): ...
|
||||||
|
def test_lat_out_of_range(self): ...
|
||||||
|
```
|
||||||
|
Only `test_schemas.py` uses class-based grouping. All other files use module-level test functions.
|
||||||
|
|
||||||
|
**Async fixtures:**
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
async def session():
|
||||||
|
engine = create_async_engine("sqlite+aiosqlite://", echo=False)
|
||||||
|
...
|
||||||
|
async with async_session() as s:
|
||||||
|
yield s
|
||||||
|
await engine.dispose()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking Patterns
|
||||||
|
|
||||||
|
**`unittest.mock.MagicMock` / `AsyncMock`:**
|
||||||
|
Used to stub repository and SSE streamer in processor/acceptance tests:
|
||||||
|
```python
|
||||||
|
repo = MagicMock()
|
||||||
|
streamer = MagicMock()
|
||||||
|
streamer.push_event = AsyncMock()
|
||||||
|
proc = FlightProcessor(repo, streamer)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`monkeypatch`:**
|
||||||
|
Used to force specific VO outcomes or override alignment methods mid-test:
|
||||||
|
```python
|
||||||
|
monkeypatch.setattr(processor._vo, "compute_relative_pose", bad_vo)
|
||||||
|
monkeypatch.setattr(processor._recovery, "process_chunk_recovery", lambda *a, **k: False)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`MockInferenceEngine` (production mock, not test mock):**
|
||||||
|
Located in `src/gps_denied/core/models.py`. `ModelManager` always returns `MockInferenceEngine` instances — no real ML models are loaded at any point. The mock generates deterministic random numpy arrays of the expected shapes. This means all component tests run against fake inference, not real SuperPoint/LightGlue/LiteSAM.
|
||||||
|
|
||||||
|
**What is NOT mocked:**
|
||||||
|
- Coordinate math (`CoordinateTransformer`) — tested with real arithmetic
|
||||||
|
- SQLite database — in-memory `aiosqlite` is used for all DB tests (real ORM, real SQL)
|
||||||
|
- FastAPI app — `ASGITransport` runs the real app in-process
|
||||||
|
- Mercator utilities — tested with real computations
|
||||||
|
|
||||||
|
## Database Test Pattern
|
||||||
|
|
||||||
|
All DB tests and API tests use in-memory SQLite to avoid test pollution:
|
||||||
|
```python
|
||||||
|
engine = create_async_engine("sqlite+aiosqlite://", echo=False)
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
```
|
||||||
|
Foreign key cascade enforcement requires explicit SQLite pragma (applied via SQLAlchemy event):
|
||||||
|
```python
|
||||||
|
@event.listens_for(engine.sync_engine, "connect")
|
||||||
|
def _set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Test Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(override_get_session) -> AsyncClient:
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=ASGITransport(app=app), base_url="http://test"
|
||||||
|
) as ac:
|
||||||
|
yield ac
|
||||||
|
```
|
||||||
|
The `override_get_session` fixture patches `app.dependency_overrides[get_session]` with the in-memory SQLite session. Tests make real HTTP calls via `client.post(...)`, `client.get(...)`.
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
**No external fixtures or data files.** All test data is constructed inline:
|
||||||
|
- `GPSPoint`, `CameraParameters`, `Waypoint` objects instantiated directly in tests
|
||||||
|
- Images: `np.zeros(...)` or `np.random.randint(...)` numpy arrays
|
||||||
|
- FLIGHT_PAYLOAD dict defined at module level in `test_api_flights.py`
|
||||||
|
- Database CAM dict defined at module level in `test_database.py`
|
||||||
|
|
||||||
|
**No `fixtures/` or `data/` directory exists.**
|
||||||
|
|
||||||
|
## Acceptance Tests (`test_acceptance.py`)
|
||||||
|
|
||||||
|
Six scenarios implemented covering key pipeline behaviors:
|
||||||
|
|
||||||
|
| Test | Scenario |
|
||||||
|
|------|----------|
|
||||||
|
| `test_ac1_normal_flight` | 20 frames, no crash, 20 SSE events emitted |
|
||||||
|
| `test_ac2_tracking_loss_and_recovery` | VO fails on frame 5 → RECOVERY → back to NORMAL |
|
||||||
|
| `test_ac3_performance_per_frame` | max < 5s/frame, avg < 1s (mock pipeline) |
|
||||||
|
| `test_ac4_user_anchor_fix` | `add_absolute_factor(is_user_anchor=True)` updates trajectory |
|
||||||
|
| `test_ac5_sustained_throughput` | 50 frames < 30s total |
|
||||||
|
| `test_ac6_graph_optimization_convergence` | `optimize()` reports `converged=True` |
|
||||||
|
|
||||||
|
These use the fully wired `FlightProcessor` with all real components attached (but mock ML inference).
|
||||||
|
|
||||||
|
## Coverage Gaps
|
||||||
|
|
||||||
|
### Not Tested At Unit Level
|
||||||
|
|
||||||
|
**SSE streamer (`src/gps_denied/core/sse.py`):**
|
||||||
|
No dedicated `test_sse.py`. SSE push calls are verified only indirectly via `AsyncMock.assert_called_*` in processor tests.
|
||||||
|
|
||||||
|
**Results manager (`src/gps_denied/core/results.py`):**
|
||||||
|
No test file. Not exercised directly.
|
||||||
|
|
||||||
|
**App lifespan / startup (`src/gps_denied/app.py`):**
|
||||||
|
Component wiring in the FastAPI lifespan handler is not tested. API integration tests bypass lifespan by overriding the session dependency only.
|
||||||
|
|
||||||
|
**`pixel_to_gps` accuracy:**
|
||||||
|
`test_coordinates.py` tests the round-trip only with the mock (FAKE) math. The real ray-casting implementation is explicitly noted as a placeholder — no test verifies the correct geometric result.
|
||||||
|
|
||||||
|
**`image_object_to_gps`:**
|
||||||
|
Tested only that it doesn't crash and returns the origin (because the underlying `pixel_to_gps` is a fake). No accuracy assertion possible until real implementation exists.
|
||||||
|
|
||||||
|
**`transform_points`:**
|
||||||
|
`CoordinateTransformer.transform_points` is a stub that returns the input unchanged. No test covers it.
|
||||||
|
|
||||||
|
**MAVLink output / flight controller integration:**
|
||||||
|
Not testable from the current codebase — no `mavlink` module exists yet. Blackbox tests FT-P-05, FT-P-09 require SITL.
|
||||||
|
|
||||||
|
**Confidence scoring / `FlightStatusResponse.confidence` field:**
|
||||||
|
`FlightStatusResponse` has no `confidence` field in the schema; the SSE event dict sets `confidence` but it is a `float` (0.0–1.0), not the `HIGH`/`MEDIUM`/`LOW` tier described in blackbox tests FT-P-07, FT-P-08.
|
||||||
|
|
||||||
|
**IMU / ESKF dead reckoning:**
|
||||||
|
Not implemented. No tests. Referenced in blackbox FT-N-06.
|
||||||
|
|
||||||
|
**`/flights/{flightId}/delete` (bulk), `/flights` (list), `/flights/{flightId}/frames/{frameId}/object-to-gps`:**
|
||||||
|
`test_api_flights.py` only covers create, get detail, upload batch, user-fix, and status. Delete, list, and object-to-GPS endpoints are not covered by API integration tests.
|
||||||
|
|
||||||
|
### Blackbox Tests vs. Implemented Tests
|
||||||
|
|
||||||
|
The `_docs/02_document/tests/blackbox-tests.md` defines **14 positive** (FT-P-01 to FT-P-14) and **7 negative** (FT-N-01 to FT-N-07) scenarios = **21 planned blackbox tests**, none of which are implemented as runnable code. They require:
|
||||||
|
- SITL ArduPilot environment
|
||||||
|
- camera-replay Docker service
|
||||||
|
- satellite tile server
|
||||||
|
- MAVLink capture infrastructure
|
||||||
|
- ground truth `coordinates.csv` and `flight-sequence-60` dataset
|
||||||
|
|
||||||
|
All 21 blackbox scenarios are documentation-only at this point.
|
||||||
|
|
||||||
|
### Traceability Matrix Coverage
|
||||||
|
|
||||||
|
Per `_docs/02_document/tests/traceability-matrix.md`:
|
||||||
|
- **26 Acceptance Criteria**: 24/26 covered by planned blackbox tests; 2 explicitly not coverable (AC-25 MRE internal metric, AC-26 imagery age)
|
||||||
|
- **16 Restrictions**: 15/16 covered; 1 not coverable (RESTRICT-05 sunny weather)
|
||||||
|
- Overall planned blackbox coverage: 93% of requirements
|
||||||
|
|
||||||
|
The gap between **planned** (blackbox doc) and **implemented** (pytest suite) is large: all 21 blackbox scenarios remain unimplemented.
|
||||||
|
|
||||||
|
## Coverage Configuration
|
||||||
|
|
||||||
|
No `pytest-cov` in dependencies. No coverage targets defined. No `.coveragerc` or `coverage` configuration in `pyproject.toml`.
|
||||||
|
|
||||||
|
Coverage is not enforced in CI (no CI configuration file detected).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Testing analysis: 2026-04-01*
|
||||||
Reference in New Issue
Block a user