mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 08:41:12 +00:00
a11ed15187
ADR 0002: hexagonal/ports-and-adapters architecture — components/ layout, protocol.py per component, composition root, core/ for concentrated math. ADR 0003: @dataclass(slots=True, frozen=True) on hot path; Pydantic retained only at REST/config/DB boundaries. Pose/GPSPoint migration deferred to Phase 2. ADR 0004: Stage 2 as independent iteration — own phases 1-6, own requirements, stage1 code treated as MVP starting capital. PROJECT.md: Stage 2 Key Decisions updated from Pending → Accepted with Phase 1 implementation notes, deferred work list, and final architecture summary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
53 lines
3.5 KiB
Markdown
53 lines
3.5 KiB
Markdown
# ADR 0003 — `@dataclass(slots=True, frozen=True)` on Hot Path; Pydantic at Boundaries Only
|
|
|
|
**Date:** 2026-05-11
|
|
**Status:** Accepted (partially implemented — Phase 1 scaffolded; full migration Phase 2)
|
|
**Supersedes:** —
|
|
**Implemented in:** Phase 1 scaffold; Phase 2 full migration
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
Stage 1 and the parallel `try02` branch both used Pydantic models (`BaseModel`) for per-frame data types: `FrameState`, `IMUSample`, `PositionEstimate`, `VOEstimate`, `SatelliteAnchor`. Pydantic v2 is fast, but on the per-frame path at 0.7fps with Jetson's shared 8GB CPU/GPU pool, every `model_validate()` or `__init__` triggers field validation, type coercion, and `__dict__` allocation — none of which we need for internal pipeline types whose values come from trusted numpy operations.
|
|
|
|
try02's design doc noted this overhead but kept Pydantic for "consistency." We rejected this trade-off.
|
|
|
|
Pydantic remains genuinely valuable at system boundaries: REST API request/response parsing (FastAPI), config loading (pydantic-settings), and DB schema validation (SQLAlchemy models). At those boundaries, external input is untrusted and validation catches bugs early. On the per-frame path, input comes from our own numpy operations — validation is redundant overhead.
|
|
|
|
## Decision
|
|
|
|
**Hot-path data types** use `@dataclass(slots=True, frozen=True)` from Python 3.10+:
|
|
- `FrameState` — per-frame snapshot passed through the pipeline
|
|
- `IMUSample` — raw IMU measurement from MAVLink
|
|
- `PositionEstimate` — output of ESKF, input to GPS_INPUT encoding
|
|
- `VOEstimate` — output of visual odometry backend
|
|
- `SatelliteAnchor` — accepted satellite match result
|
|
|
|
These live in `src/gps_denied/hot_types/`. Old schema paths (`gps_denied.schemas.eskf`, `gps_denied.schemas.vo`, etc.) are shimmed to re-export from `hot_types` for test compatibility.
|
|
|
|
**Boundary types** keep Pydantic:
|
|
- FastAPI request/response schemas (`src/gps_denied/schemas/`)
|
|
- `AppSettings` / `RuntimeConfig` (pydantic-settings)
|
|
- `AsyncSQLAlchemy` models
|
|
- `Pose` — special case (see below)
|
|
|
|
## Consequences
|
|
|
|
**Positive:**
|
|
- `slots=True` eliminates `__dict__` per instance — reduces per-frame allocations on a memory-constrained target.
|
|
- `frozen=True` prevents accidental mutation deep in the pipeline — catches bugs at assignment time rather than as silent state corruption.
|
|
- `dataclasses.replace()` for modified copies is explicit and cheap.
|
|
- No validation overhead on trusted internal data.
|
|
|
|
**Negative / Exceptions:**
|
|
- **`Pose` stays Pydantic** in Phase 1. `core/factor_graph.py` mutates `pose.position` at lines 182, 207, 230, 297 using `pose.position[0] = x` style assignment. Converting `Pose` to a frozen dataclass requires rewriting 4 mutation sites to use `dataclasses.replace()`. Deferred to Phase 2 to avoid breaking the regression floor during the Phase 1 rename wave.
|
|
- **`GPSPoint` stays Pydantic** — it appears in REST responses and is already at a boundary. No change needed.
|
|
- `dataclasses.replace()` is more verbose than Pydantic's `.model_copy(update={...})`. Acceptable trade-off.
|
|
|
|
## Implementation Notes
|
|
|
|
- `src/gps_denied/hot_types/` scaffolded in Plan 01-01 with 5 types + `__init__.py`.
|
|
- Old schema files (`schemas/eskf.py`, `schemas/vo.py`, `schemas/satellite.py`, `schemas/metric.py`, `schemas/rotation.py`) converted to re-export shims pointing to `hot_types`.
|
|
- Phase 2 work: migrate all `Pose` mutation sites to `dataclasses.replace()`; remove schema shims; update tests to import from `hot_types` directly.
|