SatQuery / docs /architecture /01-system-overview.md
thundercode's picture
release: add docs/architecture/01-system-overview.md
7ce3b93 verified
|
Raw
History Blame Contribute Delete
8.37 kB
# 01 β€” System Overview
**Parent:** [Architecture hub](README.md) Β· **Status tags:** `IMPLEMENTED` Β· `VERIFIED` Β·
`MEASURED` Β· `NOT RUN`
---
## 1. What the system is
SatQuery AI takes a natural-language question and one or two image assets, and returns a typed
`ResultEnvelope` containing an answer, a list of evidence items, an execution trace, and a
confidence breakdown.
It is a **router + specialists** architecture. There is no single end-to-end model that "does
satellite QA". Instead:
1. a **router** reads the question and predicts *which capability* is being requested;
2. a **controller** validates the request, dispatches exactly one specialist, and assembles the
result;
3. the **specialist** computes the actual answer using a frozen backbone plus a small trained module;
4. an **evidence engine** aggregates, orders, deduplicates and bounds what the specialist produced;
5. a **confidence** stage attaches a calibrated number, or honestly declines to.
### 1.1 Why not one end-to-end model
| Constraint | Consequence |
|---|---|
| The system must run on **CPU** | End-to-end VLM inference at usable quality needs a GPU; small per-task modules do not. |
| Different tasks have **incompatible outputs** | `change` returns a spatial change map; `caption` returns prose; `optical_sar` returns a class distribution. One head cannot emit all three. |
| Tasks have **different data and metrics** | Each specialist is trained and evaluated on its own split with its own protocol. |
| **Truthfulness** | Per-task metrics are auditable. A single end-to-end number would hide which component failed. |
The cost of this design is that there is **no system-level accuracy number** β€” because there is no
single model to measure. That absence is stated rather than papered over.
## 2. The six capabilities
Declared in `configs/base.yaml` (`router.tasks`) and mirrored in the live
`GET /api/capabilities` response. All six report `available: true` in the deployed system.
| Task | Assets | Output | Backbone (frozen) | Trained module |
|---|---|---|---|---|
| `vqa` | 1 | short answer | SmolVLM-500M-Instruct | (unadapted; LoRA exists but is rejected) |
| `caption` | 1 | prose caption | SmolVLM-500M-Instruct | (unadapted) |
| `grounding` | 1 | boxes / regions | RemoteCLIP ViT-B/32 | grounding head |
| `change` | 2 (equal shape) | change map + regions | STANet-style (ResNet-18 + PAM) | change head |
| `change_vqa` | 2 | short answer | β€” (cached change features) | change-VQA head |
| `optical_sar` | 2 (GeoTIFF pair) | class distribution | CROMA-base | fusion head |
`Task` in `core/schemas.py` also carries `unsupported` β€” the router's explicit "this is not a
satellite-imagery question" class. The router's label space is therefore **6 classes**
(`router/label_space.py`: `vqa, caption, grounding, change, optical_sar, unsupported`), and
`change_vqa` is reached through the change family rather than being a separate router class.
> **Note the asymmetry.** `router/label_space.py` lists **six** task classes; the capabilities
> endpoint lists **six** tasks but a *different* six β€” `change_vqa` appears in capabilities and
> `unsupported` does not. This is intentional: `unsupported` is a routing outcome, not a servable
> capability. `core/schemas.py::Task` carries all seven values.
## 3. Component inventory
Every path below is real and is the authoritative location.
| Layer | Module | Responsibility |
|---|---|---|
| **Contracts** | `core/schemas.py` | the binding typed contract: `Task`, `Intent`, `Evidence`, `SpecialistResult`, `ResultEnvelope`, `ExecutionTrace`, … |
| **Config** | `core/config.py` | load, deep-merge, validate, hash the registry; `get_config()` singleton |
| **Errors** | `core/errors.py` | the error taxonomy (`ConfigError`, `ModelLoadError`, `WorkflowPlanError`, …) |
| **Planning** | `core/planner.py` | turn an `Intent` into a concrete workflow |
| **Registry** | `core/registry.py` | specialist registration / lookup |
| **Controller** | `core/controller.py` | the nine-state FSM; the only thing that dispatches |
| **Router** | `router/encoder.py` | frozen MiniLM embedding, cached |
| | `router/adapter.py` | the 5-head `IntentAdapter` (the only trainable router part) |
| | `router/classifier.py` | learned classification + confidence threshold |
| | `router/fallback.py` | deterministic lexical fallback (`lexical_route`) |
| | `router/label_space.py` | the ontology, single source of truth |
| | `router/dataset.py`, `router/train.py` | dataset generation and training |
| **Specialists** | `specialists/base.py` | the specialist interface |
| | `specialists/vqa/{model,inference,prompts}.py` | SmolVLM VQA |
| | `specialists/grounding/{remoteclip,head,inference,specialist}.py` | RemoteCLIP + head |
| | `specialists/change/{stanet,specialist,postprocess,vqa_specialist}.py` | change detection + change-VQA |
| | `specialists/optical_sar/{croma,fusion_head,inference,specialist,sensor_adapter,radiometry,prompts}.py` | CROMA fusion |
| **Evidence** | `evidence/engine.py` | aggregation: dedup β†’ sort β†’ renumber β†’ cap |
| | `evidence/confidence.py` | temperature scaling, honest pass-through |
| **Inference app** | `app/space_app.py` | `build_space_app()`; the four-endpoint JSON contract |
| | `app/serving.py` | the composition root (`build_serving_controller()`) |
| | `app/deployment.py` | deployment helpers |
| **Gateway** | `gateway/` | the Render orchestrator |
| **Frontend** | `frontend/` | the static site |
## 4. The frozen-backbone strategy
Four backbones, all pinned by revision in `configs/base.yaml`, all fetched from the Hub on first use:
| Role | Repository | Revision | Why frozen |
|---|---|---|---|
| Router encoder | `sentence-transformers/all-MiniLM-L6-v2` | `1110a243fdf4` | embeddings are cached; the adapter trains on cached vectors in 0.28 s on CPU |
| VLM | `HuggingFaceTB/SmolVLM-500M-Instruct` | `a7da5b986cb5` | a 500M VLM cannot be fine-tuned end-to-end on CPU |
| Grounding | `chendelong/RemoteCLIP` | `bf1d8a3ccf2d` | provides the visual-language embedding space; only the head is trained |
| Optical-SAR | `antofuller/CROMA` | `0dd28e3d633b` | provides optical/SAR/joint embeddings; only the fusion head is trained |
Two consequences:
1. **The system is small.** The six trained artifacts total ~125 MiB. Everything else is public
weights.
2. **Backbones are not redistributed.** The release publishes only the six trained modules, each
with its backbone dependency documented.
## 5. The design vocabulary
`docs/ARCHITECTURE_FREEZE.md` section 5 assigns one verb per layer. This is not decoration β€” it
resolves real ambiguities about *where* a decision belongs:
| Layer | Verb | Consequence |
|---|---|---|
| Router | *understands* | its output (`Intent`) is **advisory only** β€” the controller decides |
| Policy engine / planner | *decides* | picks the workflow; may override the router |
| Specialists | *compute* | produce evidence; never decide routing |
| VLM | *explains* | produces prose; never produces a confidence number |
| Evidence engine | *proves* | aggregates; never re-derives a specialist's claim |
The `Intent` schema makes the first row explicit in code:
```python
class Intent(BaseModel):
"""Output of the learned router. Advisory only β€” the controller decides."""
```
## 6. What is deliberately absent
| Absent | Status |
|---|---|
| Database / persistence | by design β€” the gateway is stateless |
| Authentication / users | by design |
| Job queue | by design β€” inference is synchronous |
| GPU requirement | by design β€” CPU-first, `.to(device)` everywhere |
| Gradio GUI | `app/space_app.py` serves JSON only |
| Chain-of-thought in traces | by design β€” observable facts only |
| System-level end-to-end benchmark | **NOT RUN β€” none exists** |
| Router test-split evaluation | **NOT RUN** |
## 7. Evidence for this document
| Claim | Source |
|---|---|
| six tasks, config-declared | `configs/base.yaml` Β§`router.tasks` |
| router label space is 6 classes | `router/label_space.py` Β§`TASK_CLASSES` |
| `Intent` is advisory | `core/schemas.py` Β§`Intent` docstring |
| layer verbs | `docs/ARCHITECTURE_FREEZE.md` Β§5 (quoted in `evidence/engine.py`) |
| frozen backbones + revisions | `configs/base.yaml` |
| six trained artifacts | `release/repo/models/manifest.json` (generated) |