component decomposition is done

This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-11-24 14:09:23 +02:00
parent acec83018b
commit f50006d100
34 changed files with 8637 additions and 0 deletions
@@ -44,5 +44,6 @@
- Generate draw.io components diagram shows relations between components. - Generate draw.io components diagram shows relations between components.
## Notes ## Notes
Components should be semantically coherents. Do not spread similar functionality across multiple components
Do not put any code yet, only names, input and output. Do not put any code yet, only names, input and output.
Ask as many questions as possible to clarify all uncertainties. Ask as many questions as possible to clarify all uncertainties.
@@ -0,0 +1,306 @@
<mxfile host="65bd71144e">
<diagram name="ASTRAL-Next Components" id="astral-next-components">
<mxGraphModel dx="1440" dy="1201" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1800" pageHeight="2800" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="title" value="ASTRAL-Next System Architecture&#10;GPS-Denied Localization for UAVs&#10;29 Components: Route API (4) + GPS-Denied API (17) + Helpers (8)" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=20;fontStyle=1;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="200" y="20" width="1000" height="80" as="geometry"/>
</mxCell>
<mxCell id="route-api-lane" value="Route API (Separate Project)" style="swimlane;horizontal=1;whiteSpace=wrap;html=1;fontSize=14;fontStyle=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="200" y="110" width="600" height="350" as="geometry"/>
</mxCell>
<mxCell id="r01" value="R01&#10;Route REST API" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
<mxGeometry x="40" y="50" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="r02" value="R02&#10;Route Data Manager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
<mxGeometry x="200" y="50" width="140" height="60" as="geometry"/>
</mxCell>
<mxCell id="r03" value="R03&#10;Waypoint Validator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
<mxGeometry x="380" y="50" width="140" height="60" as="geometry"/>
</mxCell>
<mxCell id="r04" value="R04&#10;Route Database Layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
<mxGeometry x="200" y="150" width="140" height="60" as="geometry"/>
</mxCell>
<mxCell id="r01-r02" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r01" target="r02" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="r02-r03" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r02" target="r03" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="r02-r04" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r02" target="r04" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="route-db" value="Route DB&#10;(Separate Schema)" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="route-api-lane" vertex="1">
<mxGeometry x="225" y="240" width="90" height="80" as="geometry"/>
</mxCell>
<mxCell id="r04-db" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="route-api-lane" source="r04" target="route-db" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="gps-denied-lane" value="GPS-Denied API (Main Processing System)" style="swimlane;horizontal=1;whiteSpace=wrap;html=1;fontSize=14;fontStyle=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="130" y="500" width="1200" height="2200" as="geometry"/>
</mxCell>
<mxCell id="core-layer" value="Core API Layer" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="20" y="40" width="560" height="140" as="geometry"/>
</mxCell>
<mxCell id="g01" value="G01&#10;GPS-Denied REST API" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="core-layer" vertex="1">
<mxGeometry x="20" y="40" width="150" height="60" as="geometry"/>
</mxCell>
<mxCell id="g02" value="G02&#10;Flight Manager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="core-layer" vertex="1">
<mxGeometry x="200" y="40" width="150" height="60" as="geometry"/>
</mxCell>
<mxCell id="g03" value="G03&#10;Route API Client" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="core-layer" vertex="1">
<mxGeometry x="380" y="40" width="150" height="60" as="geometry"/>
</mxCell>
<mxCell id="g01-g02" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="core-layer" source="g01" target="g02" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="data-layer" value="Data Management" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="600" y="40" width="560" height="140" as="geometry"/>
</mxCell>
<mxCell id="g04" value="G04&#10;Satellite Data Manager&#10;(fetch, cache, grid)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="data-layer" vertex="1">
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g05" value="G05&#10;Image Input Pipeline&#10;(queue, validate, store)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="data-layer" vertex="1">
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g06" value="G06&#10;Image Rotation Mgr&#10;(30° sweep, heading)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="data-layer" vertex="1">
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="visual-layer" value="Visual Processing (Tri-Layer Architecture)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="20" y="200" width="560" height="140" as="geometry"/>
</mxCell>
<mxCell id="g07" value="G07&#10;Sequential VO&#10;(SuperPoint+LightGlue)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="visual-layer" vertex="1">
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g08" value="G08&#10;Global Place Recognition&#10;(AnyLoc DINOv2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="visual-layer" vertex="1">
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g09" value="G09&#10;Metric Refinement&#10;(LiteSAM)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="visual-layer" vertex="1">
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="state-layer" value="State Estimation &amp; Coordination" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="600" y="200" width="560" height="140" as="geometry"/>
</mxCell>
<mxCell id="g10" value="G10&#10;Factor Graph Optimizer&#10;(GTSAM iSAM2)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="state-layer" vertex="1">
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g11" value="G11&#10;Failure Recovery&#10;(Progressive 1→25)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="state-layer" vertex="1">
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g12" value="G12&#10;Coordinate Transform&#10;(Pixel↔GPS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="state-layer" vertex="1">
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="results-layer" value="Results &amp; Communication" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="20" y="360" width="380" height="140" as="geometry"/>
</mxCell>
<mxCell id="g13" value="G13&#10;Result Manager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="results-layer" vertex="1">
<mxGeometry x="20" y="40" width="150" height="60" as="geometry"/>
</mxCell>
<mxCell id="g14" value="G14&#10;SSE Event Streamer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="results-layer" vertex="1">
<mxGeometry x="200" y="40" width="150" height="60" as="geometry"/>
</mxCell>
<mxCell id="g13-g14" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="results-layer" source="g13" target="g14" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="infra-layer" value="Infrastructure" style="swimlane;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="420" y="360" width="740" height="140" as="geometry"/>
</mxCell>
<mxCell id="g15" value="G15&#10;Model Manager&#10;(TensorRT/ONNX)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
<mxGeometry x="20" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g16" value="G16&#10;Configuration Mgr" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
<mxGeometry x="200" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="g17" value="G17&#10;GPS-Denied DB Layer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
<mxGeometry x="380" y="40" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="gps-db" value="GPS-Denied DB&#10;(Separate Schema)&#10;Flights, Frame Results" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="infra-layer" vertex="1">
<mxGeometry x="570" y="30" width="140" height="90" as="geometry"/>
</mxCell>
<mxCell id="helpers-lane" value="Helper Components (Shared Utilities)" style="swimlane;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="20" y="520" width="1140" height="160" as="geometry"/>
</mxCell>
<mxCell id="h01" value="H01&#10;Camera Model" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="20" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="h02" value="H02&#10;GSD Calculator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="160" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="h03" value="H03&#10;Robust Kernels" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="300" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="h04" value="H04&#10;Faiss Index Mgr" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="440" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="h05" value="H05&#10;Performance Monitor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="580" y="40" width="130" height="60" as="geometry"/>
</mxCell>
<mxCell id="h06" value="H06&#10;Web Mercator Utils" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="730" y="40" width="130" height="60" as="geometry"/>
</mxCell>
<mxCell id="h07" value="H07&#10;Image Rotation Utils" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="880" y="40" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="h08" value="H08&#10;Batch Validator" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="helpers-lane" vertex="1">
<mxGeometry x="1020" y="40" width="100" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-title" value="Main Processing Flow" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="20" y="700" width="200" height="30" as="geometry"/>
</mxCell>
<mxCell id="flow-box" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1E1E1E;strokeColor=#FFFFFF;dashed=1;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="20" y="740" width="1140" height="600" as="geometry"/>
</mxCell>
<mxCell id="flow-1" value="1. Client uploads batch&#10;G01 → G05" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="40" y="760" width="160" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-2" value="2. Get next image&#10;G05 → G06" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="230" y="760" width="160" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-3" value="3. Rotation preprocessing&#10;G06 (30° sweep if needed)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="420" y="760" width="180" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-4" value="4. Sequential VO&#10;G07 (SuperPoint+LightGlue)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="630" y="760" width="180" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-5" value="5. Check confidence&#10;G11 Failure Recovery" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="840" y="760" width="180" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-6a" value="6a. IF GOOD: LiteSAM (1 tile)&#10;G09 drift correction" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="40" y="860" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-6b" value="6b. IF LOST: Progressive search&#10;G11 → G04 (1→4→9→16→25)&#10;G08 Global PR + G09 LiteSAM" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#CC6600;strokeColor=#FFB300;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="270" y="860" width="220" height="80" as="geometry"/>
</mxCell>
<mxCell id="flow-7" value="7. Factor Graph optimize&#10;G10 (fuse VO + GPS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="520" y="860" width="180" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-8" value="8. Coordinate transform&#10;G12 (Pixel → GPS)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="730" y="860" width="180" height="60" as="geometry"/>
</mxCell>
<mxCell id="flow-9" value="9. Publish results&#10;G13 → G03 (Route API)&#10;G13 → G14 (SSE to client)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="940" y="860" width="180" height="80" as="geometry"/>
</mxCell>
<mxCell id="arrow-1-2" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-1" target="flow-2" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-2-3" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-2" target="flow-3" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-3-4" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-3" target="flow-4" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-4-5" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-4" target="flow-5" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-5-6a" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-5" target="flow-6a" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-5-6b" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-5" target="flow-6b" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-6a-7" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-6a" target="flow-7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-6b-7" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-6b" target="flow-7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-7-8" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-7" target="flow-8" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-8-9" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="flow-8" target="flow-9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="user-input-title" value="User Input Recovery (when all search fails)" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="40" y="980" width="400" height="30" as="geometry"/>
</mxCell>
<mxCell id="user-1" value="G11 exhausted (grid=25)&#10;→ G14 send user_input_needed" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="40" y="1020" width="240" height="70" as="geometry"/>
</mxCell>
<mxCell id="user-2" value="Client responds&#10;G01 → G11 apply_user_anchor" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="310" y="1020" width="240" height="70" as="geometry"/>
</mxCell>
<mxCell id="user-3" value="G11 → G10 add_absolute_factor&#10;(high confidence)&#10;Processing resumes" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="580" y="1020" width="240" height="70" as="geometry"/>
</mxCell>
<mxCell id="arrow-user-1-2" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="user-1" target="user-2" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-user-2-3" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="user-2" target="user-3" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="async-title" value="Asynchronous Trajectory Refinement" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=14;fontStyle=1;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="40" y="1130" width="350" height="30" as="geometry"/>
</mxCell>
<mxCell id="async-1" value="G10 back-propagates&#10;new absolute factors" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6A1B9A;strokeColor=#BA68C8;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="40" y="1170" width="220" height="60" as="geometry"/>
</mxCell>
<mxCell id="async-2" value="G13 detect changed frames&#10;→ G03 batch_update_waypoints" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="290" y="1170" width="240" height="60" as="geometry"/>
</mxCell>
<mxCell id="async-3" value="G13 → G14&#10;send frame_refined events" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFA726;fontColor=#ffffff;" parent="gps-denied-lane" vertex="1">
<mxGeometry x="560" y="1170" width="220" height="60" as="geometry"/>
</mxCell>
<mxCell id="arrow-async-1-2" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="async-1" target="async-2" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arrow-async-2-3" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="gps-denied-lane" source="async-2" target="async-3" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="client" value="Client&#10;(GPS-Denied UI)" style="shape=actor;whiteSpace=wrap;html=1;fillColor=#1565C0;strokeColor=#64B5F6;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="10" y="560" width="90" height="80" as="geometry"/>
</mxCell>
<mxCell id="satellite-provider" value="Satellite Provider API&#10;/api/satellite/tiles/..." style="ellipse;shape=cloud;whiteSpace=wrap;html=1;fillColor=#E65100;strokeColor=#FFB300;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="900" y="290" width="250" height="140" as="geometry"/>
</mxCell>
<mxCell id="external-detector" value="External&lt;br&gt;Object Detector (Azaion.Inference)&lt;br&gt;(provides pixel coords)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C62828;strokeColor=#EF5350;dashed=1;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="1340" y="800" width="160" height="80" as="geometry"/>
</mxCell>
<mxCell id="client-g01" value="HTTP REST" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="client" target="g01" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="g03-route" value="Per-frame&#10;waypoint updates" style="edgeStyle=orthogonalEdgeStyle;curved=1;dashed=1;strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="g03" target="r01" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="g04-sat" value="Fetch tiles" style="strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="g04" target="satellite-provider" edge="1">
<mxGeometry x="0.3759" y="-10" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="g14-client" value="SSE Events" style="dashed=1;strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="g14" target="client" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="detector-g12" value="Object pixels → GPS" style="dashed=1;strokeColor=#FFFFFF;fontColor=#ffffff;" parent="1" source="external-detector" target="g12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="legend-title" value="Legend" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="100" y="2730" width="100" height="30" as="geometry"/>
</mxCell>
<mxCell id="legend-box" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1E1E1E;strokeColor=#FFFFFF;" parent="1" vertex="1">
<mxGeometry x="100" y="2770" width="700" height="120" as="geometry"/>
</mxCell>
<mxCell id="legend-1" value="Route API Components" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#B8860B;strokeColor=#FFD54F;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="120" y="2790" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="legend-2" value="GPS-Denied Core/API" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8B1A1A;strokeColor=#EF5350;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="280" y="2790" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="legend-3" value="Visual Processing" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;strokeColor=#42A5F5;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="440" y="2790" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="legend-4" value="Helper Components" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#689F38;strokeColor=#9CCC65;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="120" y="2840" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="legend-5" value="State Estimation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#388E3C;strokeColor=#66BB6A;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="280" y="2840" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="legend-6" value="Infrastructure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#424242;strokeColor=#BDBDBD;fontColor=#ffffff;" parent="1" vertex="1">
<mxGeometry x="440" y="2840" width="140" height="40" as="geometry"/>
</mxCell>
<mxCell id="legend-7" value="External Systems" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#C62828;strokeColor=#EF5350;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="600" y="2790" width="140" height="40" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

+364
View File
@@ -0,0 +1,364 @@
<!-- 31098ee5-58fb-474a-815e-fd9cbd17c063 9f609f9e-c80d-4c88-b618-3135b96a8333 -->
# ASTRAL-Next System Component Decomposition Plan
## Design Principle: Interface-Based Architecture
**CRITICAL REQUIREMENT**: Each component MUST implement a well-defined interface to ensure interchangeability with different implementations.
**Benefits**:
- Swap implementations (e.g., replace LiteSAM with TransFG, GTSAM with Ceres)
- Enable unit testing with mocks
- Support multiple backends (TensorRT vs ONNX, different databases)
- Facilitate future enhancements without breaking contracts
**Interface Specification**: Each component spec must define:
- Interface name (e.g., `ISatelliteDataManager`, `IMetricRefinement`)
- All public methods with strict contracts
- Input/output data structures
- Error conditions and exceptions
- Performance guarantees
---
## System Architecture Overview
**Two separate REST APIs in same repository:**
### Route API (Separate Project)
- Route/waypoint/geofence CRUD
- Shared by GPS-Denied and Mission Planner
- Does NOT call satellite provider
### GPS-Denied API (Main System)
- Tri-layer localization (SuperPoint+LightGlue, AnyLoc, LiteSAM)
- Calls satellite provider for tiles
- Rotation preprocessing (LiteSAM 45° limit)
- Per-frame Route API updates
- Progressive tile search (1→4→9→16→25)
---
## ROUTE API COMPONENTS (4 components)
### R01_route_rest_api
**Interface**: `IRouteRestAPI`
**Endpoints**: `POST /routes`, `GET /routes/{routeId}`, `PUT /routes/{routeId}/waypoints`, `DELETE /routes/{routeId}`
### R02_route_data_manager
**Interface**: `IRouteDataManager`
**API**: `save_route()`, `load_route()`, `update_waypoint()`, `delete_waypoint()`, `get_route_metadata()`
### R03_waypoint_validator
**Interface**: `IWaypointValidator`
**API**: `validate_waypoint()`, `validate_geofence()`, `check_bounds()`, `validate_route_continuity()`
### R04_route_database_layer
**Interface**: `IRouteDatabase`
**API**: `insert_route()`, `update_route()`, `query_routes()`, `get_waypoints()`
---
## GPS-DENIED API COMPONENTS (17 components)
### Core REST API Layer
**G01_gps_denied_rest_api**
**Interface**: `IGPSDeniedRestAPI`
**Endpoints**: `POST /gps-denied/flights`, `POST .../images/batch`, `POST .../user-fix`, `GET .../status`, `GET .../stream`
**G02_flight_manager**
**Interface**: `IFlightManager`
**API**: `create_flight()`, `get_flight_state()`, `link_to_route()`, `update_flight_status()`
**G03_route_api_client**
**Interface**: `IRouteAPIClient`
**API**: `update_route_waypoint()`, `get_route_info()`, `batch_update_waypoints()`
### Data Management
**G04_satellite_data_manager**
**Interface**: `ISatelliteDataManager`
**API**: `fetch_tile()`, `fetch_tile_grid()`, `prefetch_route_corridor()`, `progressive_fetch()`, `cache_tile()`, `get_cached_tile()`, `compute_tile_coords()`, `expand_search_grid()`
**Features**: Progressive retrieval, tile caching, grid calculations
**G05_image_input_pipeline**
**Interface**: `IImageInputPipeline`
**API**: `queue_batch()`, `process_next_batch()`, `validate_batch()`, `store_images()`, `get_next_image()`, `get_image_by_sequence()`
**Features**: FIFO queuing, validation, storage
**G06_image_rotation_manager**
**Interface**: `IImageRotationManager`
**API**: `rotate_image_360()`, `try_rotation_steps()`, `calculate_precise_angle()`, `get_current_heading()`, `update_heading()`, `detect_sharp_turn()`, `requires_rotation_sweep()`
**Features**: 30° rotation sweeps, heading tracking
### Visual Processing
**G07_sequential_visual_odometry**
**Interface**: `ISequentialVO`
**API**: `compute_relative_pose()`, `extract_features()`, `match_features()`, `estimate_motion()`
**G08_global_place_recognition**
**Interface**: `IGlobalPlaceRecognition`
**API**: `retrieve_candidate_tiles()`, `compute_location_descriptor()`, `query_database()`, `rank_candidates()`
**G09_metric_refinement**
**Interface**: `IMetricRefinement`
**API**: `align_to_satellite()`, `compute_homography()`, `extract_gps_from_alignment()`, `compute_match_confidence()`
### State Estimation
**G10_factor_graph_optimizer**
**Interface**: `IFactorGraphOptimizer`
**API**: `add_relative_factor()`, `add_absolute_factor()`, `add_altitude_prior()`, `optimize()`, `get_trajectory()`
**G11_failure_recovery_coordinator**
**Interface**: `IFailureRecoveryCoordinator`
**API**: `check_confidence()`, `detect_tracking_loss()`, `start_search()`, `expand_search_radius()`, `try_current_grid()`, `create_user_input_request()`, `apply_user_anchor()`
**G12_coordinate_transformer**
**Interface**: `ICoordinateTransformer`
**API**: `pixel_to_gps()`, `gps_to_pixel()`, `image_object_to_gps()`, `compute_gsd()`, `transform_points()`
### Results & Communication
**G13_result_manager**
**Interface**: `IResultManager`
**API**: `update_frame_result()`, `publish_to_route_api()`, `get_flight_results()`, `mark_refined()`
**G14_sse_event_streamer**
**Interface**: `ISSEEventStreamer`
**API**: `create_stream()`, `send_frame_result()`, `send_search_progress()`, `send_user_input_request()`, `send_refinement()`
### Infrastructure
**G15_model_manager**
**Interface**: `IModelManager`
**API**: `load_model()`, `get_inference_engine()`, `optimize_to_tensorrt()`, `fallback_to_onnx()`
**G16_configuration_manager**
**Interface**: `IConfigurationManager`
**API**: `load_config()`, `get_camera_params()`, `validate_config()`, `get_flight_config()`
**G17_gps_denied_database_layer**
**Interface**: `IGPSDeniedDatabase`
**API**: `save_flight_state()`, `load_flight_state()`, `query_processing_history()`
---
## HELPER COMPONENTS (8 components)
**H01_camera_model** - `ICameraModel`
**H02_gsd_calculator** - `IGSDCalculator`
**H03_robust_kernels** - `IRobustKernels`
**H04_faiss_index_manager** - `IFaissIndexManager`
**H05_performance_monitor** - `IPerformanceMonitor`
**H06_web_mercator_utils** - `IWebMercatorUtils`
**H07_image_rotation_utils** - `IImageRotationUtils`
**H08_batch_validator** - `IBatchValidator`
---
## Comprehensive Component Interaction Matrix
### System Initialization
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G02 | G15 | `load_model()` × 4 | Load SuperPoint, LightGlue, DINOv2, LiteSAM |
| G02 | G16 | `load_config()` | Load system configuration |
| G04 | G08 | Satellite tiles | G08 generates descriptors for Faiss |
| G08 | H04 | `build_index()` | Build satellite descriptor index |
| G08 | G15 | `get_inference_engine("DINOv2")` | Get model for descriptor generation |
### Flight Creation
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| Client | G01 | `POST /gps-denied/flights` | Create flight |
| G01 | G02 | `create_flight()` | Initialize flight state |
| G02 | G16 | `get_flight_config()` | Get camera params, altitude |
| G02 | G03 | `get_route_info()` | Fetch route metadata |
| G03 | Route API | `GET /routes/{routeId}` | HTTP call |
| G02 | G04 | `prefetch_route_corridor()` | Prefetch tiles |
| G04 | Satellite Provider | `GET /api/satellite/tiles/batch` | HTTP batch download |
| G04 | H06 | `compute_tile_bounds()` | Tile coordinate calculations |
| G02 | G17 | `save_flight_state()` | Persist flight metadata |
| Client | G01 | `GET .../stream` | Open SSE connection |
| G01 | G14 | `create_stream()` | Establish SSE channel |
### Image Upload
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| Client | G01 | `POST .../images/batch` | Upload 10-50 images |
| G01 | G05 | `queue_batch()` | Queue for processing |
| G05 | H08 | `validate_batch()` | Validate sequence, format |
| G05 | G17 | `store_images()` | Persist images |
### Per-Frame Processing (First Frame / Sharp Turn)
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G05 | G06 | `get_next_image()` | Get image for processing |
| G06 | G06 | `requires_rotation_sweep()` | Check if sweep needed |
| G06 | H07 | `rotate_image()` × 12 | Rotate in 30° steps |
| G06 | G09 | `align_to_satellite()` × 12 | Try LiteSAM each rotation |
| G09 | G04 | `get_cached_tile()` | Get expected tile |
| G09 | G15 | `get_inference_engine("LiteSAM")` | Get model |
| G06 | H07 | `calculate_rotation_from_points()` | Precise angle from homography |
| G06 | Internal | `update_heading()` | Store UAV heading |
### Per-Frame Processing (Sequential VO)
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G05 | G07 | `get_next_image()` | Provide image |
| G07 | G15 | `get_inference_engine("SuperPoint")` | Get feature extractor |
| G07 | G15 | `get_inference_engine("LightGlue")` | Get matcher |
| G07 | H05 | `start_timer()`, `end_timer()` | Monitor timing |
| G07 | G10 | `add_relative_factor()` | Add pose measurement |
### Tracking Good (Drift Correction)
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G07 | G11 | `check_confidence()` | Check tracking quality |
| G11 | G09 | `align_to_satellite()` | Align to 1 tile |
| G09 | G04 | `get_tile_grid(1)` | Get single tile |
| G09 | G10 | `add_absolute_factor()` | Add GPS measurement |
### Tracking Lost (Progressive Search)
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G07 | G11 | `check_confidence()` → FAIL | Low confidence |
| G11 | G06 | `requires_rotation_sweep()` | Trigger rotation sweep |
| G11 | G08 | `retrieve_candidate_tiles()` | Coarse localization |
| G08 | G15 | `get_inference_engine("DINOv2")` | Get model |
| G08 | H04 | `search()` | Query Faiss index |
| G08 | G04 | `get_tile_by_gps()` × 5 | Get candidate tiles |
| G11 | G04 | `expand_search_grid(4)` | Get 2×2 grid |
| G11 | G09 | `align_to_satellite()` | Try LiteSAM on 4 tiles |
| G11 (fail) | G04 | `expand_search_grid(9)` | Expand to 3×3 |
| G11 (fail) | G04 | `expand_search_grid(16)` | Expand to 4×4 |
| G11 (fail) | G04 | `expand_search_grid(25)` | Expand to 5×5 |
| G11 (fail) | G14 | `send_user_input_request()` | Request human help |
| G11 | G02 | `update_flight_status("BLOCKED")` | Block processing |
### Optimization & Results
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G10 | H03 | `huber_loss()`, `cauchy_loss()` | Apply robust kernels |
| G10 | Internal | `optimize()` | Run iSAM2 optimization |
| G10 | G12 | `get_trajectory()` | Get optimized poses |
| G12 | H01 | `project()`, `unproject()` | Camera operations |
| G12 | H02 | `compute_gsd()` | GSD calculations |
| G12 | H06 | `tile_to_latlon()` | Coordinate transforms |
| G12 | G13 | Frame GPS + object coords | Provide results |
| G13 | G03 | `update_route_waypoint()` | Per-frame Route API update |
| G03 | Route API | `PUT /routes/.../waypoints/...` | HTTP call |
| G13 | G14 | `send_frame_result()` | Publish to client |
| G14 | Client | SSE `frame_processed` | Real-time delivery |
| G13 | G17 | `save_flight_state()` | Persist state |
### User Input Recovery
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G14 | Client | SSE `user_input_needed` | Notify client |
| Client | G01 | `POST .../user-fix` | Provide anchor |
| G01 | G11 | `apply_user_anchor()` | Apply fix |
| G11 | G10 | `add_absolute_factor()` (high confidence) | Hard constraint |
| G10 | Internal | `optimize()` | Re-optimize |
| G11 | G02 | `update_flight_status("PROCESSING")` | Resume |
### Asynchronous Refinement
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G10 | Internal (background) | `optimize()` | Back-propagate anchors |
| G10 | G13 | `get_trajectory()` | Get refined poses |
| G13 | G03 | `batch_update_waypoints()` | Batch update Route API |
| G13 | G14 | `send_refinement()` × N | Send updates |
| G14 | Client | SSE `frame_refined` × N | Incremental updates |
### Cross-Cutting Concerns
| Source | Target | Method | Purpose |
|--------|--------|--------|---------|
| G16 | ALL | `get_*_config()` | Provide configuration |
| H05 | G07, G08, G09, G10, G11 | `start_timer()`, `end_timer()` | Performance monitoring |
---
## Interaction Coverage Verification
**Initialization**: G02→G15, G16, G17; G04→G08→H04
**Flight creation**: Client→G01→G02→G03,G04,G16,G17,G14
**Image upload**: Client→G01→G05→H08,G17
**Rotation sweep**: G06→H07,G09 (12 iterations)
**Sequential VO**: G07→G15,G10,H05
**Drift correction**: G11→G09→G04(1),G10
**Tracking loss**: G11→G06,G08,G04(progressive),G09,G14,G02
**Global PR**: G08→G15,H04,G04
**Optimization**: G10→H03,G12
**Coordinate transform**: G12→H01,H02,H06
**Results**: G12→G13→G03,G14,G17
**User input**: Client→G01→G11→G10,G02
**Refinement**: G10→G13→G03,G14
**Configuration**: G16→ALL
**Performance**: H05→processing components
**All major component interactions are covered.**
---
## Deliverables
**Component Count**: 29 total
- Route API: 4 (R01-R04)
- GPS-Denied API: 17 (G01-G17)
- Helpers: 8 (H01-H08)
**For each component**, create `docs/02_components/[project]_[##]_[component_name]/[component_name]_spec.md`:
1. **Interface Definition** (interface name, methods, contracts)
2. **Component Description** (responsibilities, scope)
3. **API Methods** (inputs, outputs, errors, which components call it, test cases)
4. **Integration Tests**
5. **Non-Functional Requirements** (performance, accuracy targets)
6. **Dependencies** (which components it calls)
7. **Data Models**
**Generate draw.io diagram** showing:
- Two API projects (Route API, GPS-Denied API)
- All 29 components
- Route API ↔ GPS-Denied API communication
- GPS-Denied → Satellite Provider calls
- Rotation preprocessing flow
- Progressive search expansion (1→4→9→16→25)
- Per-frame Route API update flow
- Helper component usage
### To-dos
- [x] Create 4 Route API specs with interfaces (REST, data manager, validator, DB)
- [x] Create GPS-Denied core API specs with interfaces (REST, flight manager, Route client)
- [x] Create data management specs with interfaces (satellite, image pipeline, rotation)
- [x] Create visual processing specs with interfaces (VO, place recognition, LiteSAM)
- [x] Create coordination specs with interfaces (factor graph, failure recovery, transformer)
- [x] Create results/infrastructure specs with interfaces (result manager, SSE, models, config, DB)
- [x] Create 8 helper specs with interfaces
- [x] Generate draw.io with all components, interactions, flows
@@ -0,0 +1,387 @@
# GPS-Denied REST API
## Interface Definition
**Interface Name**: `IGPSDeniedRestAPI`
### Interface Methods
```python
class IGPSDeniedRestAPI(ABC):
@abstractmethod
def create_flight(self, flight_data: FlightCreateRequest) -> FlightResponse:
pass
@abstractmethod
def upload_image_batch(self, flight_id: str, batch: ImageBatch) -> BatchResponse:
pass
@abstractmethod
def submit_user_fix(self, flight_id: str, fix_data: UserFixRequest) -> UserFixResponse:
pass
@abstractmethod
def get_flight_status(self, flight_id: str) -> FlightStatusResponse:
pass
@abstractmethod
def create_sse_stream(self, flight_id: str) -> SSEStream:
pass
```
## Component Description
### Responsibilities
- Expose REST API endpoints for GPS-Denied image processing pipeline
- Handle flight creation with satellite data prefetching
- Accept batch image uploads (10-50 images per request)
- Accept user-provided GPS fixes for blocked flights
- Provide real-time status updates
- Stream results via Server-Sent Events (SSE)
### Scope
- FastAPI-based REST endpoints
- Request/response validation
- Coordinate with Flight Manager for processing
- Multipart form data handling for image uploads
- SSE connection management
- Authentication and rate limiting
## API Methods
### `create_flight(flight_data: FlightCreateRequest) -> FlightResponse`
**REST Endpoint**: `POST /gps-denied/flights`
**Description**: Creates a new flight processing session, links to Route API, and prefetches satellite data.
**Called By**:
- Client applications (GPS-Denied UI)
**Input**:
```python
FlightCreateRequest:
route_id: str # UUID from Route API
start_gps: GPSPoint # Starting GPS coordinates (approximate)
camera_params: CameraParameters
rough_waypoints: List[GPSPoint] # Rough route for prefetching
```
**Output**:
```python
FlightResponse:
flight_id: str # UUID
status: str # "prefetching", "ready", "error"
message: Optional[str]
```
**Processing Flow**:
1. Validate request data
2. Call G02 Flight Manager → create_flight()
3. Flight Manager triggers satellite prefetch
4. Return flight_id immediately (prefetch is async)
**Error Conditions**:
- `400 Bad Request`: Invalid input data
- `404 Not Found`: route_id doesn't exist in Route API
- `500 Internal Server Error`: System error
**Test Cases**:
1. **Valid flight creation**: Returns 201 with flight_id
2. **Invalid route_id**: Returns 404
3. **Missing camera_params**: Returns 400
4. **Concurrent flight creation**: Multiple flights for same route → all succeed
---
### `upload_image_batch(flight_id: str, batch: ImageBatch) -> BatchResponse`
**REST Endpoint**: `POST /gps-denied/flights/{flightId}/images/batch`
**Description**: Uploads a batch of 10-50 UAV images for processing.
**Called By**:
- Client applications
**Input**:
```python
flight_id: str # Path parameter
ImageBatch: multipart/form-data
images: List[UploadFile] # 10-50 images
metadata: BatchMetadata
start_sequence: int
end_sequence: int
```
**Output**:
```python
BatchResponse:
accepted: bool
sequences: List[int] # [start, end]
next_expected: int
message: Optional[str]
```
**Processing Flow**:
1. Validate flight_id exists
2. Validate batch size (10-50 images)
3. Validate sequence numbers (strict sequential)
4. Pass to G05 Image Input Pipeline
5. Return immediately (processing is async)
**Error Conditions**:
- `400 Bad Request`: Invalid batch size, out-of-sequence images
- `404 Not Found`: flight_id doesn't exist
- `413 Payload Too Large`: Batch exceeds size limit
- `429 Too Many Requests`: Rate limit exceeded
**Test Cases**:
1. **Valid batch upload**: 20 images → returns 202 Accepted
2. **Out-of-sequence batch**: Sequence gap detected → returns 400
3. **Too many images**: 60 images → returns 400
4. **Large images**: 50 × 8MB images → successfully uploads
5. **Client-side resized images**: 2048×1536 → optimal processing
---
### `submit_user_fix(flight_id: str, fix_data: UserFixRequest) -> UserFixResponse`
**REST Endpoint**: `POST /gps-denied/flights/{flightId}/user-fix`
**Description**: Submits user-provided GPS anchor point to unblock failed localization.
**Called By**:
- Client applications (when user responds to `user_input_needed` event)
**Input**:
```python
UserFixRequest:
frame_id: int # Frame sequence number
uav_pixel: Tuple[float, float] # Pixel coordinates in UAV image
satellite_gps: GPSPoint # GPS corresponding to pixel location
```
**Output**:
```python
UserFixResponse:
accepted: bool
processing_resumed: bool
message: Optional[str]
```
**Processing Flow**:
1. Validate flight_id exists and is blocked
2. Pass to G11 Failure Recovery Coordinator
3. Coordinator applies anchor to Factor Graph
4. Resume processing pipeline
**Error Conditions**:
- `400 Bad Request`: Invalid fix data
- `404 Not Found`: flight_id or frame_id not found
- `409 Conflict`: Flight not in blocked state
**Test Cases**:
1. **Valid user fix**: Blocked flight → returns 200, processing resumes
2. **Fix for non-blocked flight**: Returns 409
3. **Invalid GPS coordinates**: Returns 400
4. **Multiple fixes**: Sequential fixes for different frames → all accepted
---
### `get_flight_status(flight_id: str) -> FlightStatusResponse`
**REST Endpoint**: `GET /gps-denied/flights/{flightId}/status`
**Description**: Retrieves current processing status of a flight.
**Called By**:
- Client applications (polling for status)
**Input**:
```python
flight_id: str
```
**Output**:
```python
FlightStatusResponse:
status: str # "prefetching", "processing", "blocked", "completed", "failed"
frames_processed: int
frames_total: int
current_frame: Optional[int]
current_heading: Optional[float] # UAV heading in degrees
blocked: bool
search_grid_size: Optional[int] # 1, 4, 9, 16, or 25
message: Optional[str]
```
**Error Conditions**:
- `404 Not Found`: flight_id doesn't exist
**Test Cases**:
1. **Processing flight**: Returns current progress
2. **Blocked flight**: Returns blocked=true with search_grid_size
3. **Completed flight**: Returns status="completed" with final counts
---
### `create_sse_stream(flight_id: str) -> SSEStream`
**REST Endpoint**: `GET /gps-denied/flights/{flightId}/stream`
**Description**: Opens Server-Sent Events connection for real-time result streaming.
**Called By**:
- Client applications
**Input**:
```python
flight_id: str
```
**Output**:
```python
SSE Stream with events:
- frame_processed
- frame_refined
- search_expanded
- user_input_needed
- processing_blocked
- route_api_updated
- route_completed
```
**Event Format**:
```json
{
"event": "frame_processed",
"data": {
"frame_id": 237,
"gps": {"lat": 48.123, "lon": 37.456},
"altitude": 800.0,
"confidence": 0.95,
"heading": 87.3,
"timestamp": "2025-11-24T10:30:00Z"
}
}
```
**Error Conditions**:
- `404 Not Found`: flight_id doesn't exist
- Connection closed on client disconnect
**Test Cases**:
1. **Connect to stream**: Opens SSE connection successfully
2. **Receive frame events**: Process 100 frames → receive 100 events
3. **Receive user_input_needed**: Blocked frame → event sent
4. **Client reconnect**: Replay missed events from last_event_id
## Integration Tests
### Test 1: Complete Flight Processing Flow
1. POST /gps-denied/flights
2. GET /gps-denied/flights/{flightId}/stream (open SSE)
3. POST /gps-denied/flights/{flightId}/images/batch × 40 (2000 images total)
4. Receive frame_processed events via SSE
5. Receive route_completed event
### Test 2: User Fix Flow
1. Create flight and process images
2. Receive user_input_needed event
3. POST /gps-denied/flights/{flightId}/user-fix
4. Receive processing_resumed event
5. Continue receiving frame_processed events
### Test 3: Multiple Concurrent Flights
1. Create 10 flights concurrently
2. Upload batches to all flights in parallel
3. Stream results from all flights simultaneously
4. Verify no cross-contamination
## Non-Functional Requirements
### Performance
- **create_flight**: < 500ms response (prefetch is async)
- **upload_image_batch**: < 2 seconds for 50 × 2MB images
- **submit_user_fix**: < 200ms response
- **get_flight_status**: < 100ms
- **SSE latency**: < 500ms from event generation to client receipt
### Scalability
- Support 100 concurrent flight processing sessions
- Handle 1000+ concurrent SSE connections
- Support 10,000 requests per minute
### Reliability
- Request timeout: 30 seconds for batch uploads
- SSE keepalive: Ping every 30 seconds
- Automatic SSE reconnection with event replay
- Graceful handling of client disconnects
### Security
- API key authentication
- Rate limiting: 100 requests/minute per client
- Max upload size: 500MB per batch
- CORS configuration for web clients
## Dependencies
### Internal Components
- **G02 Flight Manager**: For all flight operations
- **G05 Image Input Pipeline**: For batch processing
- **G11 Failure Recovery Coordinator**: For user fixes
- **G14 SSE Event Streamer**: For real-time streaming
### External Dependencies
- **FastAPI**: Web framework
- **Uvicorn**: ASGI server
- **Pydantic**: Validation
- **python-multipart**: Multipart form handling
## Data Models
### FlightCreateRequest
```python
class GPSPoint(BaseModel):
lat: float
lon: float
class CameraParameters(BaseModel):
focal_length: float # mm
sensor_width: float # mm
sensor_height: float # mm
resolution_width: int # pixels
resolution_height: int # pixels
distortion_coefficients: Optional[List[float]] = None
class FlightCreateRequest(BaseModel):
route_id: str
start_gps: GPSPoint
camera_params: CameraParameters
rough_waypoints: List[GPSPoint]
altitude: float # Predefined altitude in meters
```
### BatchMetadata
```python
class BatchMetadata(BaseModel):
start_sequence: int # e.g., 101
end_sequence: int # e.g., 150
batch_number: int # e.g., 3
```
### FlightStatusResponse
```python
class FlightStatusResponse(BaseModel):
status: str
frames_processed: int
frames_total: int
current_frame: Optional[int]
current_heading: Optional[float]
blocked: bool
search_grid_size: Optional[int]
message: Optional[str]
created_at: datetime
updated_at: datetime
```
@@ -0,0 +1,358 @@
# Flight Manager
## Interface Definition
**Interface Name**: `IFlightManager`
### Interface Methods
```python
class IFlightManager(ABC):
@abstractmethod
def create_flight(self, flight_data: FlightData) -> str:
pass
@abstractmethod
def get_flight_state(self, flight_id: str) -> Optional[FlightState]:
pass
@abstractmethod
def link_to_route(self, flight_id: str, route_id: str) -> bool:
pass
@abstractmethod
def update_flight_status(self, flight_id: str, status: FlightStatus) -> bool:
pass
@abstractmethod
def initialize_system(self) -> bool:
pass
```
## Component Description
### Responsibilities
- Manage flight lifecycle (creation, state tracking, completion)
- Link flights to Route API routes
- Initialize system components (models, configurations, satellite database)
- Coordinate satellite data prefetching
- Track flight processing status and statistics
- Manage flight metadata persistence
### Scope
- Central coordinator for flight processing sessions
- System initialization and resource management
- Flight state machine management
- Integration point between REST API and processing components
## API Methods
### `create_flight(flight_data: FlightData) -> str`
**Description**: Creates a new flight processing session, initializes state, and triggers satellite prefetching.
**Called By**:
- G01 GPS-Denied REST API
**Input**:
```python
FlightData:
route_id: str
start_gps: GPSPoint
camera_params: CameraParameters
rough_waypoints: List[GPSPoint]
altitude: float
```
**Output**:
```python
flight_id: str # UUID
```
**Processing Flow**:
1. Generate flight_id (UUID)
2. Get flight configuration from G16 Configuration Manager
3. Get route info from G03 Route API Client
4. Initialize flight state
5. Trigger G04 Satellite Data Manager → prefetch_route_corridor()
6. Save flight state to G17 Database Layer
7. Return flight_id
**Error Conditions**:
- `RouteNotFoundError`: route_id doesn't exist
- `ConfigurationError`: Invalid camera parameters
- `DatabaseError`: Failed to persist state
**Test Cases**:
1. **Valid flight creation**: Returns flight_id, state persisted
2. **Invalid route_id**: Raises RouteNotFoundError
3. **Prefetch triggered**: Satellite manager receives prefetch request
4. **Concurrent creation**: 10 flights created simultaneously → all succeed
---
### `get_flight_state(flight_id: str) -> Optional[FlightState]`
**Description**: Retrieves current flight state including processing statistics.
**Called By**:
- G01 GPS-Denied REST API (for status endpoint)
- G05 Image Input Pipeline (to check flight exists)
- G11 Failure Recovery Coordinator (for state updates)
**Input**:
```python
flight_id: str
```
**Output**:
```python
FlightState:
flight_id: str
route_id: str
status: str # "prefetching", "ready", "processing", "blocked", "completed", "failed"
frames_processed: int
frames_total: int
current_frame: Optional[int]
current_heading: Optional[float]
blocked: bool
search_grid_size: Optional[int]
created_at: datetime
updated_at: datetime
cache_reference: str # Satellite data cache identifier
```
**Error Conditions**:
- Returns `None`: Flight not found (not an error condition)
**Test Cases**:
1. **Get existing flight**: Returns complete FlightState
2. **Get non-existent flight**: Returns None
3. **Get during processing**: Returns accurate frame count
---
### `link_to_route(flight_id: str, route_id: str) -> bool`
**Description**: Links a flight to its Route API route for waypoint updates.
**Called By**:
- Internal (during create_flight)
**Input**:
```python
flight_id: str
route_id: str
```
**Output**:
```python
bool: True if linked, False if flight doesn't exist
```
**Processing Flow**:
1. Verify flight exists
2. Verify route exists via G03 Route API Client
3. Store linkage in flight state
4. Update database
**Error Conditions**:
- `RouteNotFoundError`: route_id invalid
**Test Cases**:
1. **Valid linkage**: Returns True
2. **Invalid flight_id**: Returns False
3. **Invalid route_id**: Raises RouteNotFoundError
---
### `update_flight_status(flight_id: str, status: FlightStatus) -> bool`
**Description**: Updates flight processing status (processing, blocked, completed, etc.).
**Called By**:
- G05 Image Input Pipeline (status transitions)
- G11 Failure Recovery Coordinator (blocked/resumed)
- G13 Result Manager (completed)
**Input**:
```python
flight_id: str
status: FlightStatus:
status_type: str # "processing", "blocked", "completed", "failed"
frames_processed: Optional[int]
current_frame: Optional[int]
current_heading: Optional[float]
blocked: Optional[bool]
search_grid_size: Optional[int]
message: Optional[str]
```
**Output**:
```python
bool: True if updated, False if flight not found
```
**Processing Flow**:
1. Load current flight state
2. Update relevant fields
3. Update updated_at timestamp
4. Persist to G17 Database Layer
5. Return success
**Error Conditions**:
- Returns False if flight not found
**Test Cases**:
1. **Update to processing**: status="processing" → updates successfully
2. **Update to blocked**: blocked=True, search_grid_size=9 → updates
3. **Resume from blocked**: blocked=False → processing continues
4. **Concurrent updates**: Multiple simultaneous updates → all persist correctly
---
### `initialize_system() -> bool`
**Description**: Initializes system components on startup (models, configurations, satellite database).
**Called By**:
- System startup (main application)
**Input**:
- None
**Output**:
```python
bool: True if initialization successful
```
**Processing Flow**:
1. Load system configuration from G16 Configuration Manager
2. Initialize ML models via G15 Model Manager:
- Load SuperPoint model
- Load LightGlue model
- Load DINOv2 model
- Load LiteSAM model
3. Initialize G08 Global Place Recognition → build satellite descriptor database
4. Initialize G04 Satellite Data Manager cache
5. Verify all components ready
**Error Conditions**:
- `InitializationError`: Component initialization failed
- `ModelLoadError`: ML model loading failed
**Test Cases**:
1. **Clean startup**: All models load successfully
2. **Missing model file**: Raises ModelLoadError
3. **Configuration error**: Raises InitializationError
4. **Partial initialization failure**: Cleanup and raise error
## Integration Tests
### Test 1: Flight Lifecycle
1. initialize_system()
2. create_flight() with valid data
3. get_flight_state() → verify "prefetching"
4. Wait for prefetch completion
5. update_flight_status("processing")
6. get_flight_state() → verify "processing"
7. update_flight_status("completed")
### Test 2: Multiple Concurrent Flights
1. create_flight() × 10 concurrently
2. update_flight_status() for all flights in parallel
3. get_flight_state() for all flights
4. Verify no state cross-contamination
### Test 3: System Initialization
1. initialize_system()
2. Verify all 4 models loaded
3. Verify satellite database ready
4. Create flight immediately after init → succeeds
## Non-Functional Requirements
### Performance
- **create_flight**: < 300ms (excluding prefetch which is async)
- **get_flight_state**: < 50ms
- **update_flight_status**: < 30ms
- **initialize_system**: < 30 seconds (one-time startup cost)
### Scalability
- Support 1000+ concurrent flight sessions
- Handle 100 status updates per second
- Maintain state for up to 10,000 flights (historical data)
### Reliability
- Graceful handling of component initialization failures
- Flight state persistence survives process restarts
- Transaction safety for concurrent updates
- Automatic cleanup of completed flights after 7 days
## Dependencies
### Internal Components
- **G03 Route API Client**: For route validation and metadata
- **G04 Satellite Data Manager**: For prefetch operations
- **G08 Global Place Recognition**: For descriptor database initialization
- **G15 Model Manager**: For ML model loading
- **G16 Configuration Manager**: For system configuration
- **G17 GPS-Denied Database Layer**: For state persistence
### External Dependencies
- None (coordinates with internal components)
## Data Models
### FlightData
```python
class FlightData(BaseModel):
route_id: str
start_gps: GPSPoint
camera_params: CameraParameters
rough_waypoints: List[GPSPoint]
altitude: float
```
### FlightState
```python
class FlightState(BaseModel):
flight_id: str
route_id: str
status: str
frames_processed: int = 0
frames_total: int = 0
current_frame: Optional[int] = None
current_heading: Optional[float] = None
blocked: bool = False
search_grid_size: Optional[int] = None
created_at: datetime
updated_at: datetime
cache_reference: str
camera_params: CameraParameters
altitude: float
start_gps: GPSPoint
```
### FlightStatus (Update DTO)
```python
class FlightStatus(BaseModel):
status_type: str
frames_processed: Optional[int] = None
current_frame: Optional[int] = None
current_heading: Optional[float] = None
blocked: Optional[bool] = None
search_grid_size: Optional[int] = None
message: Optional[str] = None
```
### SystemState
```python
class SystemState(BaseModel):
initialized: bool
models_loaded: Dict[str, bool] # {"SuperPoint": True, "LightGlue": True, ...}
satellite_db_ready: bool
active_flights_count: int
initialization_timestamp: datetime
```
@@ -0,0 +1,331 @@
# Route API Client
## Interface Definition
**Interface Name**: `IRouteAPIClient`
### Interface Methods
```python
class IRouteAPIClient(ABC):
@abstractmethod
def update_route_waypoint(self, route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool:
pass
@abstractmethod
def get_route_info(self, route_id: str) -> Optional[RouteInfo]:
pass
@abstractmethod
def batch_update_waypoints(self, route_id: str, waypoints: List[Waypoint]) -> BatchUpdateResult:
pass
```
## Component Description
### Responsibilities
- HTTP client for communicating with Route API
- Send per-frame GPS refinements to Route API
- Retrieve route metadata and waypoints
- Handle batch waypoint updates for trajectory refinements
- Manage connection pooling and retry logic
- Handle HTTP errors and timeouts
### Scope
- Synchronous HTTP client (requests library)
- Waypoint update operations
- Route metadata retrieval
- Error handling and retries
- Rate limiting and backpressure management
## API Methods
### `update_route_waypoint(route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool`
**Description**: Updates a single waypoint in Route API. Called per-frame after GPS calculation.
**Called By**:
- G13 Result Manager (per-frame update)
**Input**:
```python
route_id: str
waypoint_id: str # Frame sequence number
waypoint: Waypoint:
lat: float
lon: float
altitude: Optional[float]
confidence: float
timestamp: datetime
refined: bool # Always True for GPS-Denied updates
```
**Output**:
```python
bool: True if updated successfully, False on failure
```
**HTTP Request**:
```
PUT /routes/{route_id}/waypoints/{waypoint_id}
Content-Type: application/json
{
"lat": 48.123456,
"lon": 37.654321,
"altitude": 800.0,
"confidence": 0.95,
"timestamp": "2025-11-24T10:30:00Z",
"refined": true
}
```
**Error Handling**:
- **Retry**: 3 attempts with exponential backoff (1s, 2s, 4s)
- **Timeout**: 5 seconds per request
- **404 Not Found**: Log warning, return False
- **429 Too Many Requests**: Backoff and retry
- **500 Server Error**: Retry with backoff
**Error Conditions**:
- Returns `False`: Update failed after retries
- Logs errors but doesn't raise exceptions (non-critical path)
**Test Cases**:
1. **Successful update**: Returns True
2. **Route API unavailable**: Retries 3 times, returns False
3. **Waypoint not found**: Returns False
4. **Network timeout**: Retries, returns False if all fail
5. **High-frequency updates**: 100 updates/sec sustained
---
### `get_route_info(route_id: str) -> Optional[RouteInfo]`
**Description**: Retrieves route metadata including rough waypoints and geofences.
**Called By**:
- G02 Flight Manager (during flight creation)
**Input**:
```python
route_id: str
```
**Output**:
```python
RouteInfo:
route_id: str
name: str
description: str
points: List[GPSPoint] # Rough waypoints
geofences: Geofences
waypoint_count: int
created_at: datetime
```
**HTTP Request**:
```
GET /routes/{route_id}
```
**Error Handling**:
- **Retry**: 3 attempts for transient errors
- **Timeout**: 10 seconds
- **404 Not Found**: Return None
- **500 Server Error**: Retry with backoff
**Error Conditions**:
- Returns `None`: Route not found or error after retries
- `RouteAPIError`: Critical error retrieving route
**Test Cases**:
1. **Existing route**: Returns complete RouteInfo
2. **Non-existent route**: Returns None
3. **Large route**: 2000+ waypoints → returns successfully
4. **Concurrent requests**: 10 simultaneous requests → all succeed
---
### `batch_update_waypoints(route_id: str, waypoints: List[Waypoint]) -> BatchUpdateResult`
**Description**: Updates multiple waypoints in a single request. Used for trajectory refinements.
**Called By**:
- G13 Result Manager (asynchronous refinement updates)
**Input**:
```python
route_id: str
waypoints: List[Waypoint] # Refined waypoints
```
**Output**:
```python
BatchUpdateResult:
success: bool
updated_count: int
failed_ids: List[str]
```
**HTTP Request**:
```
PUT /routes/{route_id}/waypoints/batch
Content-Type: application/json
{
"waypoints": [
{
"id": "AD000237",
"lat": 48.123,
"lon": 37.654,
"altitude": 800.0,
"confidence": 0.97,
"timestamp": "2025-11-24T10:30:00Z",
"refined": true
},
...
]
}
```
**Error Handling**:
- **Partial success**: Some waypoints update, some fail
- **Retry**: 3 attempts for complete batch
- **Timeout**: 30 seconds (larger batches)
- Returns updated_count and failed_ids
**Error Conditions**:
- Returns `success=False` with failed_ids list
**Test Cases**:
1. **Batch update 100 waypoints**: All succeed
2. **Partial failure**: 5 waypoints fail → returns failed_ids
3. **Empty batch**: Returns success=True, updated_count=0
4. **Large batch**: 500 waypoints → splits into sub-batches
## Integration Tests
### Test 1: Per-Frame Update Flow
1. Create flight and process 100 frames
2. update_route_waypoint() × 100 sequentially
3. Verify all updates successful via get_route_info()
4. Verify waypoints marked as refined=True
### Test 2: Refinement Batch Update
1. Process route, track 200 frames needing refinement
2. batch_update_waypoints() with 200 waypoints
3. Verify all updates applied
4. Handle partial failures gracefully
### Test 3: Error Recovery
1. Simulate Route API downtime
2. Attempt update_route_waypoint() → retries 3 times
3. Route API comes back online
4. Next update succeeds
### Test 4: High-Frequency Updates
1. Send 200 waypoint updates sequentially
2. Measure throughput and success rate
3. Verify no rate limiting issues
4. Verify all updates persisted
## Non-Functional Requirements
### Performance
- **update_route_waypoint**: < 100ms average latency (critical path)
- **get_route_info**: < 200ms
- **batch_update_waypoints**: < 2 seconds for 100 waypoints
- **Throughput**: Support 100 waypoint updates per second
### Scalability
- Connection pool: 20-50 connections
- Handle 1000+ waypoint updates per flight (2000+ frame flight)
- Support concurrent updates from multiple flights
### Reliability
- **Retry strategy**: 3 attempts with exponential backoff
- **Circuit breaker**: Temporarily stop requests after 5 consecutive failures
- **Timeout management**: Progressive timeouts (5s, 10s, 30s)
- **Graceful degradation**: Continue processing even if Route API unavailable
### Monitoring
- Track success/failure rates
- Monitor latency percentiles (p50, p95, p99)
- Alert on high failure rates
- Log all HTTP errors
## Dependencies
### Internal Components
- None (external HTTP client)
### External Dependencies
- **Route API**: External REST API service
- **requests** or **httpx**: HTTP client library
- **tenacity**: Retry library
- **urllib3**: Connection pooling
## Data Models
### RouteInfo
```python
class GPSPoint(BaseModel):
lat: float
lon: float
class Geofences(BaseModel):
polygons: List[Polygon]
class RouteInfo(BaseModel):
route_id: str
name: str
description: str
points: List[GPSPoint]
geofences: Geofences
waypoint_count: int
created_at: datetime
```
### Waypoint
```python
class Waypoint(BaseModel):
id: str
lat: float
lon: float
altitude: Optional[float]
confidence: float
timestamp: datetime
refined: bool
```
### BatchUpdateResult
```python
class BatchUpdateResult(BaseModel):
success: bool
updated_count: int
failed_ids: List[str]
errors: Optional[Dict[str, str]] # waypoint_id -> error_message
```
### HTTPConfig
```python
class HTTPConfig(BaseModel):
route_api_base_url: str # e.g., "http://localhost:8000"
timeout: int = 5 # seconds
max_retries: int = 3
retry_backoff: float = 1.0 # seconds
connection_pool_size: int = 50
max_batch_size: int = 500
```
### Retry Strategy
```python
retry_strategy = {
"stop": "stop_after_attempt(3)",
"wait": "wait_exponential(multiplier=1, min=1, max=10)",
"retry": "retry_if_exception_type((ConnectionError, Timeout, HTTPError))",
"reraise": True
}
```
@@ -0,0 +1,551 @@
# Satellite Data Manager
## Interface Definition
**Interface Name**: `ISatelliteDataManager`
### Interface Methods
```python
class ISatelliteDataManager(ABC):
@abstractmethod
def fetch_tile(self, lat: float, lon: float, zoom: int) -> Optional[np.ndarray]:
pass
@abstractmethod
def fetch_tile_grid(self, center_lat: float, center_lon: float, grid_size: int, zoom: int) -> Dict[str, np.ndarray]:
pass
@abstractmethod
def prefetch_route_corridor(self, waypoints: List[GPSPoint], corridor_width_m: float, zoom: int) -> bool:
pass
@abstractmethod
def progressive_fetch(self, center_lat: float, center_lon: float, grid_sizes: List[int], zoom: int) -> Iterator[Dict[str, np.ndarray]]:
pass
@abstractmethod
def cache_tile(self, tile_coords: TileCoords, tile_data: np.ndarray) -> bool:
pass
@abstractmethod
def get_cached_tile(self, tile_coords: TileCoords) -> Optional[np.ndarray]:
pass
@abstractmethod
def get_tile_grid(self, center: TileCoords, grid_size: int) -> List[TileCoords]:
pass
@abstractmethod
def compute_tile_coords(self, lat: float, lon: float, zoom: int) -> TileCoords:
pass
@abstractmethod
def expand_search_grid(self, center: TileCoords, current_size: int, new_size: int) -> List[TileCoords]:
pass
@abstractmethod
def compute_tile_bounds(self, tile_coords: TileCoords) -> TileBounds:
pass
@abstractmethod
def clear_flight_cache(self, flight_id: str) -> bool:
pass
```
## Component Description
### Responsibilities
- Fetch satellite tiles from external provider API
- Manage local tile cache per flight
- Calculate tile coordinates and grid layouts
- Support progressive tile grid expansion (1→4→9→16→25)
- Handle Web Mercator projection calculations
- Coordinate corridor prefetching for flight routes
### Scope
- **HTTP client** for Satellite Provider API
- **Local caching** with disk storage
- **Grid calculations** for search patterns
- **Tile coordinate transformations** (GPS↔Tile coordinates)
- **Progressive retrieval** for "kidnapped robot" recovery
## API Methods
### `fetch_tile(lat: float, lon: float, zoom: int) -> Optional[np.ndarray]`
**Description**: Fetches a single satellite tile by GPS coordinates.
**Called By**:
- G09 Metric Refinement (single tile for drift correction)
- Internal (during prefetching)
**Input**:
```python
lat: float # Latitude
lon: float # Longitude
zoom: int # Zoom level (19 for 0.3m/pixel at Ukraine latitude)
```
**Output**:
```python
np.ndarray: Tile image (H×W×3 RGB) or None if failed
```
**HTTP Request**:
```
GET /api/satellite/tiles/latlon?lat={lat}&lon={lon}&zoom={zoom}
```
**Processing Flow**:
1. Convert GPS to tile coordinates
2. Check cache
3. If not cached, fetch from satellite provider
4. Cache tile
5. Return tile image
**Error Conditions**:
- Returns `None`: Tile unavailable, HTTP error
- Logs errors for monitoring
**Test Cases**:
1. **Cache hit**: Tile in cache → returns immediately
2. **Cache miss**: Fetches from API → caches → returns
3. **API error**: Returns None
4. **Invalid coordinates**: Returns None
---
### `fetch_tile_grid(center_lat: float, center_lon: float, grid_size: int, zoom: int) -> Dict[str, np.ndarray]`
**Description**: Fetches NxN grid of tiles centered on GPS coordinates.
**Called By**:
- G09 Metric Refinement (for progressive search)
- G11 Failure Recovery Coordinator
**Input**:
```python
center_lat: float
center_lon: float
grid_size: int # 1, 4 (2×2), 9 (3×3), 16 (4×4), or 25 (5×5)
zoom: int
```
**Output**:
```python
Dict[str, np.ndarray] # tile_id -> tile_image
```
**Processing Flow**:
1. Compute tile grid centered on coordinates
2. For each tile in grid:
- Check cache
- If not cached, fetch from API
3. Return dict of tiles
**HTTP Request** (if using batch endpoint):
```
GET /api/satellite/tiles/batch?tiles=[...]
```
**Error Conditions**:
- Returns partial dict if some tiles fail
- Empty dict if all tiles fail
**Test Cases**:
1. **2×2 grid**: Returns 4 tiles
2. **3×3 grid**: Returns 9 tiles
3. **5×5 grid**: Returns 25 tiles
4. **Partial failure**: Some tiles unavailable → returns available tiles
5. **All cached**: Fast retrieval without HTTP requests
---
### `prefetch_route_corridor(waypoints: List[GPSPoint], corridor_width_m: float, zoom: int) -> bool`
**Description**: Prefetches satellite tiles along route corridor for a flight.
**Called By**:
- G02 Flight Manager (during flight creation)
**Input**:
```python
waypoints: List[GPSPoint] # Rough route waypoints
corridor_width_m: float # Corridor width in meters (e.g., 1000m)
zoom: int
```
**Output**:
```python
bool: True if prefetch completed, False on error
```
**Processing Flow**:
1. For each waypoint pair:
- Calculate corridor polygon
- Determine tiles covering corridor
2. Fetch tiles (async, parallel)
3. Cache all tiles with flight_id reference
**Algorithm**:
- Use H06 Web Mercator Utils for tile calculations
- Parallel fetching (10-20 concurrent requests)
- Progress tracking for monitoring
**Error Conditions**:
- Returns `False`: Major error preventing prefetch
- Logs warnings for individual tile failures
**Test Cases**:
1. **Simple route**: 10 waypoints → prefetches 50-100 tiles
2. **Long route**: 50 waypoints → prefetches 200-500 tiles
3. **Partial failure**: Some tiles fail → continues, returns True
4. **Complete failure**: All tiles fail → returns False
---
### `progressive_fetch(center_lat: float, center_lon: float, grid_sizes: List[int], zoom: int) -> Iterator[Dict[str, np.ndarray]]`
**Description**: Progressively fetches expanding tile grids for "kidnapped robot" recovery.
**Called By**:
- G11 Failure Recovery Coordinator (progressive search)
**Input**:
```python
center_lat: float
center_lon: float
grid_sizes: List[int] # e.g., [1, 4, 9, 16, 25]
zoom: int
```
**Output**:
```python
Iterator yielding Dict[str, np.ndarray] for each grid size
```
**Processing Flow**:
1. For each grid_size in sequence:
- Fetch tile grid
- Yield tiles
- If match found by caller, iterator can be stopped
**Usage Pattern**:
```python
for tiles in progressive_fetch(lat, lon, [1, 4, 9, 16, 25], 19):
if litesam_match_found(tiles):
break # Stop expanding search
```
**Test Cases**:
1. **Progressive search**: Yields 1, then 4, then 9 tiles
2. **Early termination**: Match on 4 tiles → doesn't fetch 9, 16, 25
3. **Full search**: No match → fetches all grid sizes
---
### `cache_tile(tile_coords: TileCoords, tile_data: np.ndarray) -> bool`
**Description**: Caches a satellite tile to disk.
**Called By**:
- Internal (after fetching tiles)
**Input**:
```python
tile_coords: TileCoords:
x: int
y: int
zoom: int
tile_data: np.ndarray
```
**Output**:
```python
bool: True if cached successfully
```
**Processing Flow**:
1. Generate cache key from tile_coords
2. Serialize tile_data (PNG format)
3. Write to disk cache directory
4. Update cache index
**Error Conditions**:
- Returns `False`: Disk write error, space full
**Test Cases**:
1. **Cache new tile**: Writes successfully
2. **Overwrite existing**: Updates tile
3. **Disk full**: Returns False
---
### `get_cached_tile(tile_coords: TileCoords) -> Optional[np.ndarray]`
**Description**: Retrieves a cached tile from disk.
**Called By**:
- Internal (before fetching from API)
- G09 Metric Refinement (direct cache lookup)
**Input**:
```python
tile_coords: TileCoords
```
**Output**:
```python
Optional[np.ndarray]: Tile image or None if not cached
```
**Processing Flow**:
1. Generate cache key
2. Check if file exists
3. Load and deserialize
4. Return tile_data
**Error Conditions**:
- Returns `None`: Not cached, corrupted file
**Test Cases**:
1. **Cache hit**: Returns tile quickly
2. **Cache miss**: Returns None
3. **Corrupted cache**: Returns None, logs warning
---
### `get_tile_grid(center: TileCoords, grid_size: int) -> List[TileCoords]`
**Description**: Calculates tile coordinates for NxN grid centered on a tile.
**Called By**:
- Internal (for grid fetching)
- G11 Failure Recovery Coordinator
**Input**:
```python
center: TileCoords
grid_size: int # 1, 4, 9, 16, 25
```
**Output**:
```python
List[TileCoords] # List of tile coordinates in grid
```
**Algorithm**:
- For grid_size=9 (3×3): tiles from center-1 to center+1 in both x and y
- For grid_size=16 (4×4): asymmetric grid with center slightly off-center
**Test Cases**:
1. **1-tile grid**: Returns [center]
2. **4-tile grid (2×2)**: Returns 4 tiles
3. **9-tile grid (3×3)**: Returns 9 tiles centered
4. **25-tile grid (5×5)**: Returns 25 tiles
---
### `compute_tile_coords(lat: float, lon: float, zoom: int) -> TileCoords`
**Description**: Converts GPS coordinates to tile coordinates.
**Called By**:
- All methods that need tile coordinates from GPS
**Input**:
```python
lat: float
lon: float
zoom: int
```
**Output**:
```python
TileCoords:
x: int
y: int
zoom: int
```
**Algorithm** (Web Mercator):
```python
n = 2 ** zoom
x = int((lon + 180) / 360 * n)
lat_rad = lat * π / 180
y = int((1 - log(tan(lat_rad) + sec(lat_rad)) / π) / 2 * n)
```
**Test Cases**:
1. **Ukraine coordinates**: Produces valid tile coords
2. **Edge cases**: lat=0, lon=0, lat=90, lon=180
---
### `expand_search_grid(center: TileCoords, current_size: int, new_size: int) -> List[TileCoords]`
**Description**: Returns only NEW tiles when expanding from current grid to larger grid.
**Called By**:
- G11 Failure Recovery Coordinator (progressive search optimization)
**Input**:
```python
center: TileCoords
current_size: int # e.g., 4
new_size: int # e.g., 9
```
**Output**:
```python
List[TileCoords] # Only tiles not in current_size grid
```
**Purpose**: Avoid re-fetching tiles already tried in smaller grid.
**Test Cases**:
1. **4→9 expansion**: Returns 5 new tiles (9-4)
2. **9→16 expansion**: Returns 7 new tiles
3. **1→4 expansion**: Returns 3 new tiles
---
### `compute_tile_bounds(tile_coords: TileCoords) -> TileBounds`
**Description**: Computes GPS bounding box of a tile.
**Called By**:
- G09 Metric Refinement (for homography calculations)
- H06 Web Mercator Utils (shared calculation)
**Input**:
```python
tile_coords: TileCoords
```
**Output**:
```python
TileBounds:
nw: GPSPoint # North-West corner
ne: GPSPoint # North-East corner
sw: GPSPoint # South-West corner
se: GPSPoint # South-East corner
center: GPSPoint
gsd: float # Ground Sampling Distance (meters/pixel)
```
**Algorithm**:
- Inverse Web Mercator projection
- GSD calculation: `156543.03392 * cos(lat * π/180) / 2^zoom`
**Test Cases**:
1. **Zoom 19 at Ukraine**: GSD ≈ 0.3 m/pixel
2. **Tile bounds**: Valid GPS coordinates
---
### `clear_flight_cache(flight_id: str) -> bool`
**Description**: Clears cached tiles for a completed flight.
**Called By**:
- G02 Flight Manager (cleanup after flight completion)
**Input**:
```python
flight_id: str
```
**Output**:
```python
bool: True if cleared successfully
```
**Processing Flow**:
1. Find all tiles associated with flight_id
2. Delete tile files
3. Update cache index
**Test Cases**:
1. **Clear flight cache**: Removes all associated tiles
2. **Non-existent flight**: Returns True (no-op)
## Integration Tests
### Test 1: Prefetch and Retrieval
1. prefetch_route_corridor() with 20 waypoints
2. Verify tiles cached
3. get_cached_tile() for each tile → all hit cache
4. clear_flight_cache() → cache cleared
### Test 2: Progressive Search Simulation
1. progressive_fetch() with [1, 4, 9, 16, 25]
2. Simulate match on 9 tiles
3. Verify only 1, 4, 9 fetched (not 16, 25)
### Test 3: Grid Expansion
1. fetch_tile_grid(4) → 4 tiles
2. expand_search_grid(4, 9) → 5 new tiles
3. Verify no duplicate fetches
## Non-Functional Requirements
### Performance
- **fetch_tile**: < 200ms (cached: < 10ms)
- **fetch_tile_grid(9)**: < 1 second
- **prefetch_route_corridor**: < 30 seconds for 500 tiles
- **Cache lookup**: < 5ms
### Scalability
- Cache 10,000+ tiles per flight
- Support 100 concurrent tile fetches
- Handle 10GB+ cache size
### Reliability
- Retry failed HTTP requests (3 attempts)
- Graceful degradation on partial failures
- Cache corruption recovery
## Dependencies
### Internal Components
- **H06 Web Mercator Utils**: Tile coordinate calculations
### External Dependencies
- **Satellite Provider API**: HTTP tile source
- **requests** or **httpx**: HTTP client
- **numpy**: Image handling
- **opencv-python**: Image I/O
- **diskcache**: Persistent cache
## Data Models
### TileCoords
```python
class TileCoords(BaseModel):
x: int
y: int
zoom: int
```
### TileBounds
```python
class TileBounds(BaseModel):
nw: GPSPoint
ne: GPSPoint
sw: GPSPoint
se: GPSPoint
center: GPSPoint
gsd: float # meters/pixel
```
### CacheConfig
```python
class CacheConfig(BaseModel):
cache_dir: str = "./satellite_cache"
max_size_gb: int = 50
eviction_policy: str = "lru"
ttl_days: int = 30
```
@@ -0,0 +1,450 @@
# Image Input Pipeline
## Interface Definition
**Interface Name**: `IImageInputPipeline`
### Interface Methods
```python
class IImageInputPipeline(ABC):
@abstractmethod
def queue_batch(self, flight_id: str, batch: ImageBatch) -> bool:
pass
@abstractmethod
def process_next_batch(self, flight_id: str) -> Optional[ProcessedBatch]:
pass
@abstractmethod
def validate_batch(self, batch: ImageBatch) -> ValidationResult:
pass
@abstractmethod
def store_images(self, flight_id: str, images: List[ImageData]) -> bool:
pass
@abstractmethod
def get_next_image(self, flight_id: str) -> Optional[ImageData]:
pass
@abstractmethod
def get_image_by_sequence(self, flight_id: str, sequence: int) -> Optional[ImageData]:
pass
@abstractmethod
def get_image_metadata(self, flight_id: str, sequence: int) -> Optional[ImageMetadata]:
pass
@abstractmethod
def get_processing_status(self, flight_id: str) -> ProcessingStatus:
pass
```
## Component Description
### Responsibilities
- Unified image ingestion, validation, storage, and retrieval
- FIFO batch queuing for processing
- Validate consecutive naming (AD000001, AD000002, etc.)
- Validate sequence integrity (strict sequential ordering)
- Image persistence with indexed retrieval
- Metadata extraction (EXIF, dimensions)
### Scope
- Batch queue management
- Image validation
- Disk storage management
- Sequential processing coordination
- Metadata management
## API Methods
### `queue_batch(flight_id: str, batch: ImageBatch) -> bool`
**Description**: Queues a batch of images for processing (FIFO).
**Called By**:
- G01 GPS-Denied REST API (after upload)
**Input**:
```python
flight_id: str
batch: ImageBatch:
images: List[bytes] # Raw image data
filenames: List[str] # e.g., ["AD000101.jpg", "AD000102.jpg", ...]
start_sequence: int # 101
end_sequence: int # 150
```
**Output**:
```python
bool: True if queued successfully
```
**Processing Flow**:
1. Validate batch using H08 Batch Validator
2. Check sequence continuity (no gaps)
3. Add to FIFO queue for flight_id
4. Return immediately (async processing)
**Error Conditions**:
- `ValidationError`: Sequence gap, invalid naming
- `QueueFullError`: Queue capacity exceeded
**Test Cases**:
1. **Valid batch**: Queued successfully
2. **Sequence gap**: Batch 101-150, expecting 51-100 → error
3. **Invalid naming**: Non-consecutive names → error
4. **Queue full**: Returns error with backpressure signal
---
### `process_next_batch(flight_id: str) -> Optional[ProcessedBatch]`
**Description**: Dequeues and processes the next batch from FIFO queue.
**Called By**:
- Internal processing loop (background worker)
**Input**:
```python
flight_id: str
```
**Output**:
```python
ProcessedBatch:
images: List[ImageData]
batch_id: str
start_sequence: int
end_sequence: int
```
**Processing Flow**:
1. Dequeue next batch
2. Decompress/decode images
3. Extract metadata (EXIF, dimensions)
4. Store images to disk
5. Return ProcessedBatch for pipeline
**Error Conditions**:
- Returns `None`: Queue empty
- `ImageCorruptionError`: Invalid image data
**Test Cases**:
1. **Process batch**: Dequeues, returns ImageData list
2. **Empty queue**: Returns None
3. **Corrupted image**: Logs error, skips image
---
### `validate_batch(batch: ImageBatch) -> ValidationResult`
**Description**: Validates batch integrity and sequence continuity.
**Called By**:
- Internal (before queuing)
- H08 Batch Validator (delegated validation)
**Input**:
```python
batch: ImageBatch
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
1. **Batch size**: 10 <= len(images) <= 50
2. **Naming convention**: ADxxxxxx.jpg (6 digits)
3. **Sequence continuity**: Consecutive numbers
4. **File format**: JPEG or PNG
5. **Image dimensions**: 640x480 to 6252x4168
6. **File size**: < 10MB per image
**Test Cases**:
1. **Valid batch**: Returns valid=True
2. **Too few images**: 5 images → invalid
3. **Too many images**: 60 images → invalid
4. **Non-consecutive**: AD000101, AD000103 → invalid
5. **Invalid naming**: IMG_0001.jpg → invalid
---
### `store_images(flight_id: str, images: List[ImageData]) -> bool`
**Description**: Persists images to disk with indexed storage.
**Called By**:
- Internal (after processing batch)
**Input**:
```python
flight_id: str
images: List[ImageData]
```
**Output**:
```python
bool: True if stored successfully
```
**Storage Structure**:
```
/image_storage/
{flight_id}/
AD000001.jpg
AD000002.jpg
metadata.json
```
**Processing Flow**:
1. Create flight directory if not exists
2. Write each image to disk
3. Update metadata index
4. Persist to G17 Database Layer (metadata only)
**Error Conditions**:
- `StorageError`: Disk full, permission error
**Test Cases**:
1. **Store batch**: All images written successfully
2. **Disk full**: Returns False
3. **Verify storage**: Images retrievable after storage
---
### `get_next_image(flight_id: str) -> Optional[ImageData]`
**Description**: Gets the next image in sequence for processing.
**Called By**:
- G06 Image Rotation Manager
- G07 Sequential VO
- Processing pipeline (main loop)
**Input**:
```python
flight_id: str
```
**Output**:
```python
ImageData:
flight_id: str
sequence: int
filename: str
image: np.ndarray # Loaded image
metadata: ImageMetadata
```
**Processing Flow**:
1. Track current sequence number for flight
2. Load next image from disk
3. Increment sequence counter
4. Return ImageData
**Error Conditions**:
- Returns `None`: No more images
- `ImageNotFoundError`: Expected image missing
**Test Cases**:
1. **Get sequential images**: Returns images in order
2. **End of sequence**: Returns None
3. **Missing image**: Handles gracefully
---
### `get_image_by_sequence(flight_id: str, sequence: int) -> Optional[ImageData]`
**Description**: Retrieves a specific image by sequence number.
**Called By**:
- G11 Failure Recovery Coordinator (for user fix)
- G13 Result Manager (for refinement)
**Input**:
```python
flight_id: str
sequence: int
```
**Output**:
```python
Optional[ImageData]
```
**Processing Flow**:
1. Construct filename from sequence (ADxxxxxx.jpg)
2. Load from disk
3. Load metadata
4. Return ImageData
**Error Conditions**:
- Returns `None`: Image not found
**Test Cases**:
1. **Get specific image**: Returns correct image
2. **Invalid sequence**: Returns None
---
### `get_image_metadata(flight_id: str, sequence: int) -> Optional[ImageMetadata]`
**Description**: Retrieves metadata without loading full image (lightweight).
**Called By**:
- G02 Flight Manager (status checks)
- G13 Result Manager (metadata-only queries)
**Input**:
```python
flight_id: str
sequence: int
```
**Output**:
```python
ImageMetadata:
sequence: int
filename: str
dimensions: Tuple[int, int] # (width, height)
file_size: int # bytes
timestamp: datetime
exif_data: Optional[Dict]
```
**Test Cases**:
1. **Get metadata**: Returns quickly without loading image
2. **Missing image**: Returns None
---
### `get_processing_status(flight_id: str) -> ProcessingStatus`
**Description**: Gets current processing status for a flight.
**Called By**:
- G01 GPS-Denied REST API (status endpoint)
- G02 Flight Manager
**Input**:
```python
flight_id: str
```
**Output**:
```python
ProcessingStatus:
flight_id: str
total_images: int
processed_images: int
current_sequence: int
queued_batches: int
processing_rate: float # images/second
```
**Test Cases**:
1. **Get status**: Returns accurate counts
2. **During processing**: Updates in real-time
## Integration Tests
### Test 1: Batch Processing Flow
1. queue_batch() with 50 images
2. process_next_batch() → returns batch
3. store_images() → persists to disk
4. get_next_image() × 50 → retrieves all sequentially
5. Verify metadata
### Test 2: Multiple Batches
1. queue_batch() × 5 (250 images total)
2. process_next_batch() × 5
3. Verify FIFO order maintained
4. Verify sequence continuity
### Test 3: Error Handling
1. Queue batch with sequence gap
2. Verify validation error
3. Queue valid batch → succeeds
4. Simulate disk full → storage fails gracefully
## Non-Functional Requirements
### Performance
- **queue_batch**: < 100ms
- **process_next_batch**: < 2 seconds for 50 images
- **get_next_image**: < 50ms
- **get_image_by_sequence**: < 50ms
- **Processing throughput**: 10-20 images/second
### Scalability
- Support 3000 images per flight
- Handle 10 concurrent flights
- Manage 100GB+ image storage
### Reliability
- Crash recovery (resume processing from last sequence)
- Atomic batch operations
- Data integrity validation
## Dependencies
### Internal Components
- **H08 Batch Validator**: For validation logic
- **G17 Database Layer**: For metadata persistence
### External Dependencies
- **opencv-python**: Image I/O
- **Pillow**: Image processing
- **numpy**: Image arrays
## Data Models
### ImageBatch
```python
class ImageBatch(BaseModel):
images: List[bytes]
filenames: List[str]
start_sequence: int
end_sequence: int
batch_number: int
```
### ImageData
```python
class ImageData(BaseModel):
flight_id: str
sequence: int
filename: str
image: np.ndarray
metadata: ImageMetadata
```
### ImageMetadata
```python
class ImageMetadata(BaseModel):
sequence: int
filename: str
dimensions: Tuple[int, int]
file_size: int
timestamp: datetime
exif_data: Optional[Dict]
```
### ProcessingStatus
```python
class ProcessingStatus(BaseModel):
flight_id: str
total_images: int
processed_images: int
current_sequence: int
queued_batches: int
processing_rate: float
```
@@ -0,0 +1,410 @@
# Image Rotation Manager
## Interface Definition
**Interface Name**: `IImageRotationManager`
### Interface Methods
```python
class IImageRotationManager(ABC):
@abstractmethod
def rotate_image_360(self, image: np.ndarray, angle: float) -> np.ndarray:
pass
@abstractmethod
def try_rotation_steps(self, flight_id: str, image: np.ndarray, satellite_tile: np.ndarray) -> Optional[RotationResult]:
pass
@abstractmethod
def calculate_precise_angle(self, homography: np.ndarray, initial_angle: float) -> float:
pass
@abstractmethod
def get_current_heading(self, flight_id: str) -> Optional[float]:
pass
@abstractmethod
def update_heading(self, flight_id: str, heading: float) -> bool:
pass
@abstractmethod
def detect_sharp_turn(self, flight_id: str, new_heading: float) -> bool:
pass
@abstractmethod
def requires_rotation_sweep(self, flight_id: str) -> bool:
pass
```
## Component Description
### Responsibilities
- Handle UAV image rotation preprocessing for LiteSAM
- **Critical**: LiteSAM fails if images rotated >45°, requires preprocessing
- Perform 30° step rotation sweeps (12 rotations: 0°, 30°, 60°, ..., 330°)
- Track UAV heading angle across flight
- Calculate precise rotation angle from homography point correspondences
- Detect sharp turns requiring rotation sweep
- Pre-rotate images to known heading for subsequent frames
### Scope
- Image rotation operations
- UAV heading tracking and history
- Sharp turn detection
- Rotation sweep coordination with LiteSAM matching
- Precise angle calculation from homography
## API Methods
### `rotate_image_360(image: np.ndarray, angle: float) -> np.ndarray`
**Description**: Rotates an image by specified angle around center.
**Called By**:
- Internal (during rotation sweep)
- H07 Image Rotation Utils (may delegate to)
**Input**:
```python
image: np.ndarray # Input image (H×W×3)
angle: float # Rotation angle in degrees (0-360)
```
**Output**:
```python
np.ndarray # Rotated image (same dimensions)
```
**Processing Details**:
- Rotation around image center
- Preserves image dimensions
- Fills borders with black or extrapolation
**Error Conditions**:
- None (always returns rotated image)
**Test Cases**:
1. **Rotate 90°**: Image rotated correctly
2. **Rotate 0°**: Image unchanged
3. **Rotate 180°**: Image inverted
4. **Rotate 45°**: Diagonal rotation
---
### `try_rotation_steps(flight_id: str, image: np.ndarray, satellite_tile: np.ndarray) -> Optional[RotationResult]`
**Description**: Performs 30° rotation sweep, trying LiteSAM match for each rotation.
**Called By**:
- Internal (when requires_rotation_sweep() returns True)
- Main processing loop (first frame or sharp turn)
**Input**:
```python
flight_id: str
image: np.ndarray # UAV image
satellite_tile: np.ndarray # Satellite reference tile
```
**Output**:
```python
RotationResult:
matched: bool
initial_angle: float # Best matching step angle (0, 30, 60, ...)
precise_angle: float # Refined angle from homography
confidence: float
homography: np.ndarray
```
**Algorithm**:
```
For angle in [0°, 30°, 60°, 90°, 120°, 150°, 180°, 210°, 240°, 270°, 300°, 330°]:
rotated_image = rotate_image_360(image, angle)
result = LiteSAM.align_to_satellite(rotated_image, satellite_tile)
if result.matched and result.confidence > threshold:
precise_angle = calculate_precise_angle(result.homography, angle)
update_heading(flight_id, precise_angle)
return RotationResult(matched=True, initial_angle=angle, precise_angle=precise_angle, ...)
return None # No match found
```
**Processing Flow**:
1. For each 30° step:
- Rotate image
- Call G09 Metric Refinement (LiteSAM)
- Check if match found
2. If match found:
- Calculate precise angle from homography
- Update UAV heading
- Return result
3. If no match:
- Return None (triggers progressive search expansion)
**Error Conditions**:
- Returns `None`: No match found in any rotation
- This is expected behavior (leads to progressive search)
**Test Cases**:
1. **Match at 60°**: Finds match, returns result
2. **Match at 0°**: No rotation needed, finds match
3. **No match**: All 12 rotations tried, returns None
4. **Multiple matches**: Returns best confidence
---
### `calculate_precise_angle(homography: np.ndarray, initial_angle: float) -> float`
**Description**: Calculates precise rotation angle from homography matrix point shifts.
**Called By**:
- Internal (after LiteSAM match in rotation sweep)
**Input**:
```python
homography: np.ndarray # 3×3 homography matrix from LiteSAM
initial_angle: float # 30° step angle that matched
```
**Output**:
```python
float: Precise rotation angle (e.g., 62.3° refined from 60° step)
```
**Algorithm**:
1. Extract rotation component from homography
2. Calculate angle from rotation matrix
3. Refine initial_angle with delta from homography
**Uses**: H07 Image Rotation Utils for angle calculation
**Error Conditions**:
- Falls back to initial_angle if calculation fails
**Test Cases**:
1. **Refine 60°**: Returns 62.5° (small delta)
2. **Refine 0°**: Returns 3.2° (small rotation)
3. **Invalid homography**: Returns initial_angle
---
### `get_current_heading(flight_id: str) -> Optional[float]`
**Description**: Gets current UAV heading angle for a flight.
**Called By**:
- G06 Internal (to check if pre-rotation needed)
- Main processing loop (before LiteSAM)
- G11 Failure Recovery Coordinator (logging)
**Input**:
```python
flight_id: str
```
**Output**:
```python
Optional[float]: Heading angle in degrees (0-360), or None if not initialized
```
**Error Conditions**:
- Returns `None`: First frame, heading not yet determined
**Test Cases**:
1. **After first frame**: Returns heading angle
2. **Before first frame**: Returns None
3. **During flight**: Returns current heading
---
### `update_heading(flight_id: str, heading: float) -> bool`
**Description**: Updates UAV heading angle after successful match.
**Called By**:
- Internal (after rotation sweep match)
- Internal (after normal LiteSAM match with small rotation delta)
**Input**:
```python
flight_id: str
heading: float # New heading angle (0-360)
```
**Output**:
```python
bool: True if updated successfully
```
**Processing Flow**:
1. Normalize angle to 0-360 range
2. Add to heading history (last 10 headings)
3. Update current_heading for flight
4. Persist to database (optional)
**Test Cases**:
1. **Update heading**: Sets new heading
2. **Angle normalization**: 370° → 10°
3. **History tracking**: Maintains last 10 headings
---
### `detect_sharp_turn(flight_id: str, new_heading: float) -> bool`
**Description**: Detects if UAV made a sharp turn (>45° heading change).
**Called By**:
- Internal (before deciding if rotation sweep needed)
- Main processing loop
**Input**:
```python
flight_id: str
new_heading: float # Proposed new heading
```
**Output**:
```python
bool: True if sharp turn detected (>45° change)
```
**Algorithm**:
```python
current = get_current_heading(flight_id)
if current is None:
return False
delta = abs(new_heading - current)
if delta > 180: # Handle wraparound
delta = 360 - delta
return delta > 45
```
**Test Cases**:
1. **Small turn**: 60° → 75° → False (15° delta)
2. **Sharp turn**: 60° → 120° → True (60° delta)
3. **Wraparound**: 350° → 20° → False (30° delta)
4. **180° turn**: 0° → 180° → True
---
### `requires_rotation_sweep(flight_id: str) -> bool`
**Description**: Determines if rotation sweep is needed for current frame.
**Called By**:
- Main processing loop (before each frame)
- G11 Failure Recovery Coordinator (after tracking loss)
**Input**:
```python
flight_id: str
```
**Output**:
```python
bool: True if rotation sweep required
```
**Conditions for sweep**:
1. **First frame**: heading not initialized
2. **Sharp turn detected**: >45° heading change from VO
3. **Tracking loss**: LiteSAM failed to match in previous frame
4. **User flag**: Manual trigger (rare)
**Test Cases**:
1. **First frame**: Returns True
2. **Second frame, no turn**: Returns False
3. **Sharp turn detected**: Returns True
4. **Tracking loss**: Returns True
## Integration Tests
### Test 1: First Frame Rotation Sweep
1. First frame arrives (no heading set)
2. requires_rotation_sweep() → True
3. try_rotation_steps() → rotates 12 times
4. Match found at 60° step
5. calculate_precise_angle() → 62.3°
6. update_heading(62.3°)
7. Subsequent frames use 62.3° heading
### Test 2: Normal Frame Processing
1. Heading known (90°)
2. requires_rotation_sweep() → False
3. Pre-rotate image to 90°
4. LiteSAM match succeeds with small delta (+2.5°)
5. update_heading(92.5°)
### Test 3: Sharp Turn Detection
1. UAV heading 45°
2. Next frame shows 120° heading (from VO estimate)
3. detect_sharp_turn() → True (75° delta)
4. requires_rotation_sweep() → True
5. Perform rotation sweep → find match at 120° step
### Test 4: Tracking Loss Recovery
1. LiteSAM fails to match (no overlap after turn)
2. requires_rotation_sweep() → True
3. try_rotation_steps() with all 12 rotations
4. Match found → heading updated
## Non-Functional Requirements
### Performance
- **rotate_image_360**: < 20ms per rotation
- **try_rotation_steps**: < 1.2 seconds (12 rotations × 100ms LiteSAM)
- **calculate_precise_angle**: < 10ms
- **get_current_heading**: < 1ms
- **update_heading**: < 5ms
### Accuracy
- **Angle precision**: ±0.5° for precise angle calculation
- **Sharp turn detection**: 100% accuracy for >45° turns
### Reliability
- Rotation sweep always completes all 12 steps
- Graceful handling of no-match scenarios
- Heading history preserved across failures
## Dependencies
### Internal Components
- **G09 Metric Refinement**: For LiteSAM matching during rotation sweep
- **H07 Image Rotation Utils**: For image rotation and angle calculations
### External Dependencies
- **opencv-python**: Image rotation (`cv2.warpAffine`)
- **numpy**: Matrix operations
## Data Models
### RotationResult
```python
class RotationResult(BaseModel):
matched: bool
initial_angle: float # 30° step angle (0, 30, 60, ...)
precise_angle: float # Refined angle from homography
confidence: float
homography: np.ndarray
inlier_count: int
```
### HeadingHistory
```python
class HeadingHistory(BaseModel):
flight_id: str
current_heading: float
heading_history: List[float] # Last 10 headings
last_update: datetime
sharp_turns: int # Count of sharp turns detected
```
### RotationConfig
```python
class RotationConfig(BaseModel):
step_angle: float = 30.0 # Degrees
sharp_turn_threshold: float = 45.0 # Degrees
confidence_threshold: float = 0.7 # For accepting match
history_size: int = 10 # Number of headings to track
```
@@ -0,0 +1,316 @@
# Sequential Visual Odometry
## Interface Definition
**Interface Name**: `ISequentialVO`
### Interface Methods
```python
class ISequentialVO(ABC):
@abstractmethod
def compute_relative_pose(self, prev_image: np.ndarray, curr_image: np.ndarray) -> Optional[RelativePose]:
pass
@abstractmethod
def extract_features(self, image: np.ndarray) -> Features:
pass
@abstractmethod
def match_features(self, features1: Features, features2: Features) -> Matches:
pass
@abstractmethod
def estimate_motion(self, matches: Matches, camera_params: CameraParameters) -> Optional[Motion]:
pass
```
## Component Description
### Responsibilities
- SuperPoint feature extraction from UAV images
- LightGlue feature matching between consecutive frames
- Handle <5% overlap scenarios
- Estimate relative pose (translation + rotation) between frames
- Return relative pose factors for Factor Graph Optimizer
- Detect tracking loss (low inlier count)
### Scope
- Frame-to-frame visual odometry
- Feature-based motion estimation
- Handles low overlap and challenging agricultural environments
- Provides relative measurements for trajectory optimization
## API Methods
### `compute_relative_pose(prev_image: np.ndarray, curr_image: np.ndarray) -> Optional[RelativePose]`
**Description**: Computes relative camera pose between consecutive frames.
**Called By**:
- Main processing loop (per-frame)
**Input**:
```python
prev_image: np.ndarray # Previous frame (t-1)
curr_image: np.ndarray # Current frame (t)
```
**Output**:
```python
RelativePose:
translation: np.ndarray # (x, y, z) in meters
rotation: np.ndarray # 3×3 rotation matrix or quaternion
confidence: float # 0.0 to 1.0
inlier_count: int
total_matches: int
tracking_good: bool
```
**Processing Flow**:
1. extract_features(prev_image) → features1
2. extract_features(curr_image) → features2
3. match_features(features1, features2) → matches
4. estimate_motion(matches, camera_params) → motion
5. Return RelativePose
**Tracking Quality Indicators**:
- **Good tracking**: inlier_count > 50, inlier_ratio > 0.5
- **Degraded tracking**: inlier_count 20-50
- **Tracking loss**: inlier_count < 20
**Error Conditions**:
- Returns `None`: Tracking lost (insufficient matches)
**Test Cases**:
1. **Good overlap (>50%)**: Returns reliable pose
2. **Low overlap (5-10%)**: Still succeeds with LightGlue
3. **<5% overlap**: May return None (tracking loss)
4. **Agricultural texture**: Handles repetitive patterns
---
### `extract_features(image: np.ndarray) -> Features`
**Description**: Extracts SuperPoint keypoints and descriptors from image.
**Called By**:
- Internal (during compute_relative_pose)
- G08 Global Place Recognition (for descriptor caching)
**Input**:
```python
image: np.ndarray # Input image (H×W×3 or H×W)
```
**Output**:
```python
Features:
keypoints: np.ndarray # (N, 2) - (x, y) coordinates
descriptors: np.ndarray # (N, 256) - 256-dim descriptors
scores: np.ndarray # (N,) - detection confidence scores
```
**Processing Details**:
- Uses G15 Model Manager to get SuperPoint model
- Converts to grayscale if needed
- Non-maximum suppression for keypoint selection
- Typically extracts 500-2000 keypoints per image
**Performance**:
- Inference time: ~15ms with TensorRT on RTX 2060
**Error Conditions**:
- Never fails (returns empty features if image invalid)
**Test Cases**:
1. **FullHD image**: Extracts ~1000 keypoints
2. **High-res image (6252×4168)**: Extracts ~2000 keypoints
3. **Low-texture image**: Extracts fewer keypoints
---
### `match_features(features1: Features, features2: Features) -> Matches`
**Description**: Matches features using LightGlue attention-based matcher.
**Called By**:
- Internal (during compute_relative_pose)
**Input**:
```python
features1: Features # Previous frame features
features2: Features # Current frame features
```
**Output**:
```python
Matches:
matches: np.ndarray # (M, 2) - indices [idx1, idx2]
scores: np.ndarray # (M,) - match confidence scores
keypoints1: np.ndarray # (M, 2) - matched keypoints from frame 1
keypoints2: np.ndarray # (M, 2) - matched keypoints from frame 2
```
**Processing Details**:
- Uses G15 Model Manager to get LightGlue model
- Transformer-based attention mechanism
- "Dustbin" mechanism for unmatched features
- Adaptive depth (exits early for easy matches)
- **Critical**: Handles <5% overlap better than RANSAC
**Performance**:
- Inference time: ~35-100ms (adaptive depth)
- Faster for high-overlap, slower for low-overlap
**Test Cases**:
1. **High overlap**: Fast matching (~35ms), 500+ matches
2. **Low overlap (<5%)**: Slower (~100ms), 20-50 matches
3. **No overlap**: Few or no matches (< 10)
---
### `estimate_motion(matches: Matches, camera_params: CameraParameters) -> Optional[Motion]`
**Description**: Estimates camera motion from matched keypoints using Essential Matrix.
**Called By**:
- Internal (during compute_relative_pose)
**Input**:
```python
matches: Matches
camera_params: CameraParameters:
focal_length: float
principal_point: Tuple[float, float]
resolution: Tuple[int, int]
```
**Output**:
```python
Motion:
translation: np.ndarray # (x, y, z) - unit vector (scale ambiguous)
rotation: np.ndarray # 3×3 rotation matrix
inliers: np.ndarray # Boolean mask of inlier matches
inlier_count: int
```
**Algorithm**:
1. Normalize keypoint coordinates using camera intrinsics
2. Estimate Essential Matrix using RANSAC
3. Decompose Essential Matrix → [R, t]
4. Return motion with inlier mask
**Scale Ambiguity**:
- Monocular VO has inherent scale ambiguity
- Translation is unit vector (direction only)
- Scale resolved by:
- Altitude prior (from G10 Factor Graph)
- Absolute GPS measurements (from G09 LiteSAM)
**Error Conditions**:
- Returns `None`: Insufficient inliers (< 8 points for Essential Matrix)
**Test Cases**:
1. **Good matches**: Returns motion with high inlier count
2. **Low inliers**: May return None
3. **Degenerate motion**: Handles pure rotation
## Integration Tests
### Test 1: Normal Flight Sequence
1. Load consecutive frames with 50% overlap
2. compute_relative_pose() → returns valid pose
3. Verify translation direction reasonable
4. Verify inlier_count > 100
### Test 2: Low Overlap Scenario
1. Load frames with 5% overlap
2. compute_relative_pose() → still succeeds
3. Verify inlier_count > 20
4. Verify LightGlue finds matches despite low overlap
### Test 3: Tracking Loss
1. Load frames with 0% overlap (sharp turn)
2. compute_relative_pose() → returns None
3. Verify tracking_good = False
4. Trigger global place recognition
### Test 4: Agricultural Texture
1. Load images of wheat fields (repetitive texture)
2. compute_relative_pose() → SuperPoint handles better than SIFT
3. Verify match quality
## Non-Functional Requirements
### Performance
- **compute_relative_pose**: < 200ms total
- SuperPoint extraction: ~15ms × 2 = 30ms
- LightGlue matching: ~50ms
- Motion estimation: ~10ms
- **Frame rate**: 5-10 FPS processing (meets <5s requirement)
### Accuracy
- **Relative rotation**: ±2° error
- **Relative translation direction**: ±5° error
- **Inlier ratio**: >50% for good tracking
### Reliability
- Handle 100m spacing between frames
- Survive temporary tracking degradation
- Recover from brief occlusions
## Dependencies
### Internal Components
- **G15 Model Manager**: For SuperPoint and LightGlue models
- **G16 Configuration Manager**: For camera parameters
- **H01 Camera Model**: For coordinate normalization
- **H05 Performance Monitor**: For timing measurements
### External Dependencies
- **SuperPoint**: Feature extraction model
- **LightGlue**: Feature matching model
- **opencv-python**: Essential Matrix estimation
- **numpy**: Matrix operations
## Data Models
### Features
```python
class Features(BaseModel):
keypoints: np.ndarray # (N, 2)
descriptors: np.ndarray # (N, 256)
scores: np.ndarray # (N,)
```
### Matches
```python
class Matches(BaseModel):
matches: np.ndarray # (M, 2) - pairs of indices
scores: np.ndarray # (M,) - match confidence
keypoints1: np.ndarray # (M, 2)
keypoints2: np.ndarray # (M, 2)
```
### RelativePose
```python
class RelativePose(BaseModel):
translation: np.ndarray # (3,) - unit vector
rotation: np.ndarray # (3, 3) or (4,) quaternion
confidence: float
inlier_count: int
total_matches: int
tracking_good: bool
scale_ambiguous: bool = True
```
### Motion
```python
class Motion(BaseModel):
translation: np.ndarray # (3,)
rotation: np.ndarray # (3, 3)
inliers: np.ndarray # Boolean mask
inlier_count: int
```
@@ -0,0 +1,310 @@
# Global Place Recognition
## Interface Definition
**Interface Name**: `IGlobalPlaceRecognition`
### Interface Methods
```python
class IGlobalPlaceRecognition(ABC):
@abstractmethod
def retrieve_candidate_tiles(self, image: np.ndarray, top_k: int) -> List[TileCandidate]:
pass
@abstractmethod
def compute_location_descriptor(self, image: np.ndarray) -> np.ndarray:
pass
@abstractmethod
def query_database(self, descriptor: np.ndarray, top_k: int) -> List[DatabaseMatch]:
pass
@abstractmethod
def rank_candidates(self, candidates: List[TileCandidate]) -> List[TileCandidate]:
pass
@abstractmethod
def initialize_database(self, satellite_tiles: List[SatelliteTile]) -> bool:
pass
```
## Component Description
### Responsibilities
- AnyLoc (DINOv2 + VLAD) for coarse localization after tracking loss
- "Kidnapped robot" recovery after sharp turns
- Compute image descriptors robust to season/appearance changes
- Query Faiss index of satellite tile descriptors
- Return top-k candidate tile regions for progressive refinement
- Initialize satellite descriptor database during system startup
### Scope
- Global localization (not frame-to-frame)
- Appearance-based place recognition
- Handles domain gap (UAV vs satellite imagery)
- Semantic feature extraction (DINOv2)
- Efficient similarity search (Faiss)
## API Methods
### `retrieve_candidate_tiles(image: np.ndarray, top_k: int) -> List[TileCandidate]`
**Description**: Retrieves top-k candidate satellite tiles for a UAV image.
**Called By**:
- G11 Failure Recovery Coordinator (after tracking loss)
**Input**:
```python
image: np.ndarray # UAV image
top_k: int # Number of candidates (typically 5)
```
**Output**:
```python
List[TileCandidate]:
tile_id: str
gps_center: GPSPoint
similarity_score: float
rank: int
```
**Processing Flow**:
1. compute_location_descriptor(image) → descriptor
2. query_database(descriptor, top_k) → database_matches
3. Retrieve tile metadata for matches
4. rank_candidates() → sorted by similarity
5. Return top-k candidates
**Error Conditions**:
- Returns empty list: Database not initialized, query failed
**Test Cases**:
1. **UAV image over Ukraine**: Returns relevant tiles
2. **Different season**: DINOv2 handles appearance change
3. **Top-1 accuracy**: Correct tile in top-5 > 85%
---
### `compute_location_descriptor(image: np.ndarray) -> np.ndarray`
**Description**: Computes global descriptor using DINOv2 + VLAD aggregation.
**Called By**:
- Internal (during retrieve_candidate_tiles)
- System initialization (for satellite database)
**Input**:
```python
image: np.ndarray # UAV or satellite image
```
**Output**:
```python
np.ndarray: Descriptor vector (4096-dim or 8192-dim)
```
**Algorithm (AnyLoc)**:
1. Extract DINOv2 features (dense feature map)
2. Apply VLAD (Vector of Locally Aggregated Descriptors) aggregation
3. L2-normalize descriptor
4. Return compact global descriptor
**Processing Details**:
- Uses G15 Model Manager to get DINOv2 model
- Dense features: extracts from multiple spatial locations
- VLAD codebook: pre-trained cluster centers
- Semantic features: invariant to texture/color changes
**Performance**:
- Inference time: ~150ms for DINOv2 + VLAD
**Test Cases**:
1. **Same location, different season**: Similar descriptors
2. **Different locations**: Dissimilar descriptors
3. **UAV vs satellite**: Domain-invariant features
---
### `query_database(descriptor: np.ndarray, top_k: int) -> List[DatabaseMatch]`
**Description**: Queries Faiss index for most similar satellite tiles.
**Called By**:
- Internal (during retrieve_candidate_tiles)
**Input**:
```python
descriptor: np.ndarray # Query descriptor
top_k: int
```
**Output**:
```python
List[DatabaseMatch]:
index: int # Tile index in database
distance: float # L2 distance
similarity_score: float # Normalized score
```
**Processing Details**:
- Uses H04 Faiss Index Manager
- Index type: IVF (Inverted File) or HNSW for fast search
- Distance metric: L2 (Euclidean)
- Query time: ~10-50ms for 10,000+ tiles
**Error Conditions**:
- Returns empty list: Query failed
**Test Cases**:
1. **Query satellite database**: Returns top-5 matches
2. **Large database (10,000 tiles)**: Fast retrieval (<50ms)
---
### `rank_candidates(candidates: List[TileCandidate]) -> List[TileCandidate]`
**Description**: Re-ranks candidates based on additional heuristics.
**Called By**:
- Internal (during retrieve_candidate_tiles)
**Input**:
```python
candidates: List[TileCandidate] # Initial ranking by similarity
```
**Output**:
```python
List[TileCandidate] # Re-ranked list
```
**Re-ranking Factors**:
1. **Similarity score**: Primary factor
2. **Spatial proximity**: Prefer tiles near dead-reckoning estimate
3. **Previous trajectory**: Favor continuation of route
4. **Geofence constraints**: Within operational area
**Test Cases**:
1. **Spatial re-ranking**: Closer tile promoted
2. **Similar scores**: Spatial proximity breaks tie
---
### `initialize_database(satellite_tiles: List[SatelliteTile]) -> bool`
**Description**: Initializes satellite descriptor database during system startup.
**Called By**:
- G02 Flight Manager (during system initialization)
**Input**:
```python
List[SatelliteTile]:
tile_id: str
image: np.ndarray
gps_center: GPSPoint
bounds: TileBounds
```
**Output**:
```python
bool: True if database initialized successfully
```
**Processing Flow**:
1. For each satellite tile:
- compute_location_descriptor(tile.image) → descriptor
- Store descriptor with tile metadata
2. Build Faiss index using H04 Faiss Index Manager
3. Persist index to disk for fast startup
**Performance**:
- Initialization time: ~10-30 minutes for 10,000 tiles (one-time cost)
- Can be done offline and loaded at startup
**Test Cases**:
1. **Initialize with 1000 tiles**: Completes successfully
2. **Load pre-built index**: Fast startup (<10s)
## Integration Tests
### Test 1: Place Recognition Flow
1. Load UAV image from sharp turn
2. retrieve_candidate_tiles(top_k=5)
3. Verify correct tile in top-5
4. Pass candidates to G11 Failure Recovery
### Test 2: Season Invariance
1. Satellite tiles from summer
2. UAV images from autumn
3. retrieve_candidate_tiles() → correct match despite appearance change
### Test 3: Database Initialization
1. Prepare 500 satellite tiles
2. initialize_database(tiles)
3. Verify Faiss index built
4. Query with test image → returns matches
## Non-Functional Requirements
### Performance
- **retrieve_candidate_tiles**: < 200ms total
- Descriptor computation: ~150ms
- Database query: ~50ms
- **compute_location_descriptor**: ~150ms
- **query_database**: ~10-50ms
### Accuracy
- **Recall@5**: > 85% (correct tile in top-5)
- **Recall@1**: > 60% (correct tile is top-1)
### Scalability
- Support 10,000+ satellite tiles in database
- Fast query even with large database
## Dependencies
### Internal Components
- **G15 Model Manager**: For DINOv2 model
- **H04 Faiss Index Manager**: For similarity search
- **G04 Satellite Data Manager**: For tile metadata
### External Dependencies
- **DINOv2**: Foundation vision model
- **Faiss**: Similarity search library
- **numpy**: Array operations
## Data Models
### TileCandidate
```python
class TileCandidate(BaseModel):
tile_id: str
gps_center: GPSPoint
bounds: TileBounds
similarity_score: float
rank: int
spatial_score: Optional[float]
```
### DatabaseMatch
```python
class DatabaseMatch(BaseModel):
index: int
tile_id: str
distance: float
similarity_score: float
```
### SatelliteTile
```python
class SatelliteTile(BaseModel):
tile_id: str
image: np.ndarray
gps_center: GPSPoint
bounds: TileBounds
descriptor: Optional[np.ndarray]
```
@@ -0,0 +1,308 @@
# Metric Refinement
## Interface Definition
**Interface Name**: `IMetricRefinement`
### Interface Methods
```python
class IMetricRefinement(ABC):
@abstractmethod
def align_to_satellite(self, uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[AlignmentResult]:
pass
@abstractmethod
def compute_homography(self, uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[np.ndarray]:
pass
@abstractmethod
def extract_gps_from_alignment(self, homography: np.ndarray, tile_bounds: TileBounds, image_center: Tuple[int, int]) -> GPSPoint:
pass
@abstractmethod
def compute_match_confidence(self, alignment: AlignmentResult) -> float:
pass
```
## Component Description
### Responsibilities
- LiteSAM for precise UAV-to-satellite cross-view matching
- **Requires pre-rotated images** from Image Rotation Manager
- Compute homography mapping UAV image to satellite tile
- Extract absolute GPS coordinates from alignment
- Process against single tile (drift correction) or tile grid (progressive search)
- Achieve <20m accuracy requirement
### Scope
- Cross-view geo-localization (UAV↔satellite)
- Handles altitude variations (<1km)
- Multi-scale processing for different GSDs
- Domain gap (UAV downward vs satellite nadir view)
- **Critical**: Fails if rotation >45° (handled by G06)
## API Methods
### `align_to_satellite(uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[AlignmentResult]`
**Description**: Aligns UAV image to satellite tile, returning GPS location.
**Called By**:
- G06 Image Rotation Manager (during rotation sweep)
- G11 Failure Recovery Coordinator (progressive search)
- Main processing loop (drift correction with single tile)
**Input**:
```python
uav_image: np.ndarray # Pre-rotated UAV image
satellite_tile: np.ndarray # Reference satellite tile
```
**Output**:
```python
AlignmentResult:
matched: bool
homography: np.ndarray # 3×3 transformation matrix
gps_center: GPSPoint # UAV image center GPS
confidence: float
inlier_count: int
total_correspondences: int
```
**Processing Flow**:
1. Extract features from both images using LiteSAM encoder
2. Compute dense correspondence field
3. Estimate homography from correspondences
4. Validate match quality (inlier count, reprojection error)
5. If valid match:
- Extract GPS from homography
- Return AlignmentResult
6. If no match:
- Return None
**Match Criteria**:
- **Good match**: inlier_count > 30, confidence > 0.7
- **Weak match**: inlier_count 15-30, confidence 0.5-0.7
- **No match**: inlier_count < 15
**Error Conditions**:
- Returns `None`: No match found, rotation >45° (should be pre-rotated)
**Test Cases**:
1. **Good alignment**: Returns GPS within 20m of ground truth
2. **Altitude variation**: Handles GSD mismatch
3. **Rotation >45°**: Fails (by design, requires pre-rotation)
4. **Multi-scale**: Processes at multiple scales
---
### `compute_homography(uav_image: np.ndarray, satellite_tile: np.ndarray) -> Optional[np.ndarray]`
**Description**: Computes homography transformation from UAV to satellite.
**Called By**:
- Internal (during align_to_satellite)
**Input**:
```python
uav_image: np.ndarray
satellite_tile: np.ndarray
```
**Output**:
```python
Optional[np.ndarray]: 3×3 homography matrix or None
```
**Algorithm (LiteSAM)**:
1. Extract multi-scale features using TAIFormer
2. Compute correlation via Convolutional Token Mixer (CTM)
3. Generate dense correspondences
4. Estimate homography using RANSAC
5. Refine with non-linear optimization
**Homography Properties**:
- Maps pixels from UAV image to satellite image
- Accounts for: scale, rotation, perspective
- 8 DoF (degrees of freedom)
**Error Conditions**:
- Returns `None`: Insufficient correspondences
**Test Cases**:
1. **Valid correspondence**: Returns 3×3 matrix
2. **Insufficient features**: Returns None
---
### `extract_gps_from_alignment(homography: np.ndarray, tile_bounds: TileBounds, image_center: Tuple[int, int]) -> GPSPoint`
**Description**: Extracts GPS coordinates from homography and tile georeferencing.
**Called By**:
- Internal (during align_to_satellite)
- G06 Image Rotation Manager (for precise angle calculation)
**Input**:
```python
homography: np.ndarray # 3×3 matrix
tile_bounds: TileBounds # GPS bounds of satellite tile
image_center: Tuple[int, int] # Center pixel of UAV image
```
**Output**:
```python
GPSPoint:
lat: float
lon: float
```
**Algorithm**:
1. Apply homography to UAV image center point
2. Get pixel coordinates in satellite tile
3. Convert satellite pixel to GPS using tile_bounds and GSD
4. Return GPS coordinates
**Uses**: G04 Satellite Data Manager for tile_bounds, H02 GSD Calculator
**Test Cases**:
1. **Center alignment**: UAV center → correct GPS
2. **Corner alignment**: UAV corner → correct GPS
3. **Multiple points**: All points consistent
---
### `compute_match_confidence(alignment: AlignmentResult) -> float`
**Description**: Computes match confidence score from alignment quality.
**Called By**:
- Internal (during align_to_satellite)
- G11 Failure Recovery Coordinator (to decide if match acceptable)
**Input**:
```python
alignment: AlignmentResult
```
**Output**:
```python
float: Confidence score (0.0 to 1.0)
```
**Confidence Factors**:
1. **Inlier ratio**: inliers / total_correspondences
2. **Inlier count**: Absolute number of inliers
3. **Reprojection error**: Mean error of inliers (in pixels)
4. **Spatial distribution**: Inliers well-distributed vs clustered
**Thresholds**:
- **High confidence (>0.8)**: inlier_ratio > 0.6, inlier_count > 50, MRE < 0.5px
- **Medium confidence (0.5-0.8)**: inlier_ratio > 0.4, inlier_count > 30
- **Low confidence (<0.5)**: Reject match
**Test Cases**:
1. **Good match**: confidence > 0.8
2. **Weak match**: confidence 0.5-0.7
3. **Poor match**: confidence < 0.5
## Integration Tests
### Test 1: Single Tile Drift Correction
1. Load UAV image and expected satellite tile
2. Pre-rotate UAV image to known heading
3. align_to_satellite() → returns GPS
4. Verify GPS within 20m of ground truth
### Test 2: Progressive Search (4 tiles)
1. Load UAV image from sharp turn
2. Get 2×2 tile grid from G04
3. align_to_satellite() for each tile
4. First 3 tiles: No match
5. 4th tile: Match found → GPS extracted
### Test 3: Rotation Sensitivity
1. Rotate UAV image by 60° (not pre-rotated)
2. align_to_satellite() → returns None (fails as expected)
3. Pre-rotate to 60°
4. align_to_satellite() → succeeds
### Test 4: Multi-Scale Robustness
1. UAV at 500m altitude (GSD=0.1m/pixel)
2. Satellite at zoom 19 (GSD=0.3m/pixel)
3. LiteSAM handles scale difference → match succeeds
## Non-Functional Requirements
### Performance
- **align_to_satellite**: ~60ms per tile (TensorRT optimized)
- **Progressive search 25 tiles**: ~1.5 seconds total (25 × 60ms)
- Meets <5s per frame requirement
### Accuracy
- **GPS accuracy**: 60% of frames < 20m error, 80% < 50m error
- **Mean Reprojection Error (MRE)**: < 1.0 pixels
- **Alignment success rate**: > 95% when rotation correct
### Reliability
- Graceful failure when no match
- Robust to altitude variations (<1km)
- Handles seasonal appearance changes (to extent possible)
## Dependencies
### Internal Components
- **G15 Model Manager**: For LiteSAM model
- **G04 Satellite Data Manager**: For tile_bounds and GSD
- **H01 Camera Model**: For projection operations
- **H02 GSD Calculator**: For coordinate transformations
- **H05 Performance Monitor**: For timing
### External Dependencies
- **LiteSAM**: Cross-view matching model
- **opencv-python**: Homography estimation
- **numpy**: Matrix operations
## Data Models
### AlignmentResult
```python
class AlignmentResult(BaseModel):
matched: bool
homography: np.ndarray # (3, 3)
gps_center: GPSPoint
confidence: float
inlier_count: int
total_correspondences: int
reprojection_error: float # Mean error in pixels
```
### GPSPoint
```python
class GPSPoint(BaseModel):
lat: float
lon: float
```
### TileBounds
```python
class TileBounds(BaseModel):
nw: GPSPoint
ne: GPSPoint
sw: GPSPoint
se: GPSPoint
center: GPSPoint
gsd: float # Ground Sampling Distance (m/pixel)
```
### LiteSAMConfig
```python
class LiteSAMConfig(BaseModel):
model_path: str
confidence_threshold: float = 0.7
min_inliers: int = 15
max_reprojection_error: float = 2.0 # pixels
multi_scale_levels: int = 3
```
@@ -0,0 +1,362 @@
# Factor Graph Optimizer
## Interface Definition
**Interface Name**: `IFactorGraphOptimizer`
### Interface Methods
```python
class IFactorGraphOptimizer(ABC):
@abstractmethod
def add_relative_factor(self, frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool:
pass
@abstractmethod
def add_absolute_factor(self, frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool:
pass
@abstractmethod
def add_altitude_prior(self, frame_id: int, altitude: float, covariance: float) -> bool:
pass
@abstractmethod
def optimize(self, iterations: int) -> OptimizationResult:
pass
@abstractmethod
def get_trajectory(self) -> Dict[int, Pose]:
pass
@abstractmethod
def get_marginal_covariance(self, frame_id: int) -> np.ndarray:
pass
```
## Component Description
### Responsibilities
- GTSAM-based fusion of relative and absolute measurements
- Incremental optimization (iSAM2) for real-time performance
- Robust kernels (Huber/Cauchy) for 350m outlier handling
- Scale resolution through altitude priors and absolute GPS
- Trajectory smoothing and global consistency
- Back-propagation of refinements to previous frames
### Scope
- Non-linear least squares optimization
- Factor graph representation of SLAM problem
- Handles monocular scale ambiguity
- Real-time incremental updates
- Asynchronous batch refinement
## API Methods
### `add_relative_factor(frame_i: int, frame_j: int, relative_pose: RelativePose, covariance: np.ndarray) -> bool`
**Description**: Adds relative pose measurement between consecutive frames.
**Called By**:
- G07 Sequential VO (frame-to-frame odometry)
**Input**:
```python
frame_i: int # Previous frame ID
frame_j: int # Current frame ID (typically frame_i + 1)
relative_pose: RelativePose:
translation: np.ndarray # (3,) - in meters (scale from altitude prior)
rotation: np.ndarray # (3, 3) or quaternion
covariance: np.ndarray # (6, 6) - uncertainty
```
**Output**:
```python
bool: True if factor added successfully
```
**Processing Flow**:
1. Create BetweenFactor in GTSAM
2. Apply robust kernel (Huber) to handle outliers
3. Add to factor graph
4. Mark graph as needing optimization
**Robust Kernel**:
- **Huber loss**: Downweights large errors (>threshold)
- **Critical** for 350m outlier handling from tilt
**Test Cases**:
1. **Normal motion**: Factor added, contributes to optimization
2. **Large displacement** (350m outlier): Huber kernel reduces weight
3. **Consecutive factors**: Chain of relative factors builds trajectory
---
### `add_absolute_factor(frame_id: int, gps: GPSPoint, covariance: np.ndarray, is_user_anchor: bool) -> bool`
**Description**: Adds absolute GPS measurement for drift correction or user anchor.
**Called By**:
- G09 Metric Refinement (after LiteSAM alignment)
- G11 Failure Recovery Coordinator (user-provided anchors)
**Input**:
```python
frame_id: int
gps: GPSPoint:
lat: float
lon: float
covariance: np.ndarray # (2, 2) or (3, 3) - GPS uncertainty
is_user_anchor: bool # True for user-provided fixes (high confidence)
```
**Output**:
```python
bool: True if factor added
```
**Processing Flow**:
1. Convert GPS to local ENU coordinates (East-North-Up)
2. Create PriorFactor or UnaryFactor
3. Set covariance (low for user anchors, higher for LiteSAM)
4. Add to factor graph
5. Trigger optimization (immediate for user anchors)
**Covariance Settings**:
- **User anchor**: σ = 5m (high confidence)
- **LiteSAM match**: σ = 20-50m (depends on confidence)
**Test Cases**:
1. **LiteSAM GPS**: Adds absolute factor, corrects drift
2. **User anchor**: High confidence, immediately refines trajectory
3. **Multiple absolute factors**: Graph optimizes to balance all
---
### `add_altitude_prior(frame_id: int, altitude: float, covariance: float) -> bool`
**Description**: Adds altitude constraint to resolve monocular scale ambiguity.
**Called By**:
- Main processing loop (for each frame)
**Input**:
```python
frame_id: int
altitude: float # Predefined altitude in meters
covariance: float # Altitude uncertainty (e.g., 50m)
```
**Output**:
```python
bool: True if prior added
```
**Processing Flow**:
1. Create UnaryFactor for Z-coordinate
2. Set as soft constraint (not hard constraint)
3. Add to factor graph
**Purpose**:
- Resolves scale ambiguity in monocular VO
- Prevents scale drift (trajectory collapsing or exploding)
- Soft constraint allows adjustment based on absolute GPS
**Test Cases**:
1. **Without altitude prior**: Scale drifts over time
2. **With altitude prior**: Scale stabilizes
3. **Conflicting measurements**: Optimizer balances VO and altitude
---
### `optimize(iterations: int) -> OptimizationResult`
**Description**: Runs optimization to refine trajectory.
**Called By**:
- Main processing loop (incremental after each frame)
- Asynchronous refinement thread (batch optimization)
**Input**:
```python
iterations: int # Max iterations (typically 5-10 for incremental, 50-100 for batch)
```
**Output**:
```python
OptimizationResult:
converged: bool
final_error: float
iterations_used: int
optimized_frames: List[int] # Frames with updated poses
```
**Processing Details**:
- **Incremental** (iSAM2): Updates only affected nodes
- **Batch**: Re-optimizes entire trajectory when new absolute factors added
- **Robust M-estimation**: Automatically downweights outliers
**Optimization Algorithm** (Levenberg-Marquardt):
1. Linearize factor graph around current estimate
2. Solve linear system
3. Update pose estimates
4. Check convergence (error reduction < threshold)
**Test Cases**:
1. **Incremental optimization**: Fast (<100ms), local update
2. **Batch optimization**: Slower (~500ms), refines entire trajectory
3. **Convergence**: Error reduces, converges within iterations
---
### `get_trajectory() -> Dict[int, Pose]`
**Description**: Retrieves complete optimized trajectory.
**Called By**:
- G13 Result Manager (for publishing results)
- G12 Coordinate Transformer (for GPS conversion)
**Input**: None
**Output**:
```python
Dict[int, Pose]:
frame_id -> Pose:
position: np.ndarray # (x, y, z) in ENU
orientation: np.ndarray # Quaternion or rotation matrix
timestamp: datetime
```
**Processing Flow**:
1. Extract all pose estimates from graph
2. Convert to appropriate coordinate system
3. Return dictionary
**Test Cases**:
1. **After optimization**: Returns all frame poses
2. **Refined trajectory**: Poses updated after batch optimization
---
### `get_marginal_covariance(frame_id: int) -> np.ndarray`
**Description**: Gets uncertainty (covariance) of a pose estimate.
**Called By**:
- G11 Failure Recovery Coordinator (to detect high uncertainty)
**Input**:
```python
frame_id: int
```
**Output**:
```python
np.ndarray: (6, 6) covariance matrix [x, y, z, roll, pitch, yaw]
```
**Purpose**:
- Uncertainty quantification
- Trigger user input when uncertainty too high (> 50m radius)
**Test Cases**:
1. **Well-constrained pose**: Small covariance
2. **Unconstrained pose**: Large covariance
3. **After absolute factor**: Covariance reduces
## Integration Tests
### Test 1: Incremental Trajectory Building
1. Initialize graph with first frame
2. Add relative factors from VO × 100
3. Add altitude priors × 100
4. Optimize incrementally after each frame
5. Verify smooth trajectory
### Test 2: Drift Correction with Absolute GPS
1. Build trajectory with VO only (will drift)
2. Add absolute GPS factor at frame 50
3. Optimize → trajectory corrects
4. Verify frames 1-49 also corrected (back-propagation)
### Test 3: Outlier Handling
1. Add normal relative factors
2. Add 350m outlier factor (tilt error)
3. Optimize with robust kernel
4. Verify outlier downweighted, trajectory smooth
### Test 4: User Anchor Integration
1. Processing blocked at frame 237
2. User provides anchor (high confidence)
3. add_absolute_factor(is_user_anchor=True)
4. Optimize → trajectory snaps to anchor
## Non-Functional Requirements
### Performance
- **Incremental optimize**: < 100ms per frame (iSAM2)
- **Batch optimize**: < 500ms for 100 frames
- **get_trajectory**: < 10ms
- Real-time capable: 10 FPS processing
### Accuracy
- **Mean Reprojection Error (MRE)**: < 1.0 pixels
- **GPS accuracy**: Meet 80% < 50m, 60% < 20m criteria
- **Trajectory smoothness**: No sudden jumps (except user anchors)
### Reliability
- Numerical stability for 2000+ frame trajectories
- Graceful handling of degenerate configurations
- Robust to missing/corrupted measurements
## Dependencies
### Internal Components
- **H03 Robust Kernels**: For Huber/Cauchy loss functions
- **H02 GSD Calculator**: For coordinate conversions
### External Dependencies
- **GTSAM**: Graph optimization library
- **numpy**: Matrix operations
- **scipy**: Sparse matrix operations (optional)
## Data Models
### Pose
```python
class Pose(BaseModel):
frame_id: int
position: np.ndarray # (3,) - [x, y, z] in ENU
orientation: np.ndarray # (4,) quaternion or (3,3) rotation matrix
timestamp: datetime
covariance: Optional[np.ndarray] # (6, 6)
```
### RelativePose
```python
class RelativePose(BaseModel):
translation: np.ndarray # (3,)
rotation: np.ndarray # (3, 3) or (4,)
covariance: np.ndarray # (6, 6)
```
### OptimizationResult
```python
class OptimizationResult(BaseModel):
converged: bool
final_error: float
iterations_used: int
optimized_frames: List[int]
mean_reprojection_error: float
```
### FactorGraphConfig
```python
class FactorGraphConfig(BaseModel):
robust_kernel_type: str = "Huber" # or "Cauchy"
huber_threshold: float = 1.0 # pixels
cauchy_k: float = 0.1
isam2_relinearize_threshold: float = 0.1
isam2_relinearize_skip: int = 1
```
@@ -0,0 +1,404 @@
# Failure Recovery Coordinator
## Interface Definition
**Interface Name**: `IFailureRecoveryCoordinator`
### Interface Methods
```python
class IFailureRecoveryCoordinator(ABC):
@abstractmethod
def check_confidence(self, vo_result: RelativePose, litesam_result: Optional[AlignmentResult]) -> ConfidenceAssessment:
pass
@abstractmethod
def detect_tracking_loss(self, confidence: ConfidenceAssessment) -> bool:
pass
@abstractmethod
def start_search(self, flight_id: str, frame_id: int, estimated_gps: GPSPoint) -> SearchSession:
pass
@abstractmethod
def expand_search_radius(self, session: SearchSession) -> List[TileCoords]:
pass
@abstractmethod
def try_current_grid(self, session: SearchSession, tiles: Dict[str, np.ndarray]) -> Optional[AlignmentResult]:
pass
@abstractmethod
def mark_found(self, session: SearchSession, result: AlignmentResult) -> bool:
pass
@abstractmethod
def get_search_status(self, session: SearchSession) -> SearchStatus:
pass
@abstractmethod
def create_user_input_request(self, flight_id: str, frame_id: int, candidate_tiles: List[TileCandidate]) -> UserInputRequest:
pass
@abstractmethod
def apply_user_anchor(self, flight_id: str, frame_id: int, anchor: UserAnchor) -> bool:
pass
```
## Component Description
### Responsibilities
- Monitor confidence metrics (inlier count, MRE, covariance)
- Detect tracking loss and trigger recovery
- Coordinate progressive tile search (1→4→9→16→25)
- Handle human-in-the-loop when all strategies exhausted
- Block flight processing when awaiting user input
- Apply user-provided anchors to Factor Graph
### Scope
- Confidence monitoring
- Progressive search coordination
- User input request/response handling
- Recovery strategy orchestration
- Integration point for G04, G06, G08, G09, G10
## API Methods
### `check_confidence(vo_result: RelativePose, litesam_result: Optional[AlignmentResult]) -> ConfidenceAssessment`
**Description**: Assesses tracking confidence from VO and LiteSAM results.
**Called By**: Main processing loop (per frame)
**Input**:
```python
vo_result: RelativePose
litesam_result: Optional[AlignmentResult]
```
**Output**:
```python
ConfidenceAssessment:
overall_confidence: float # 0-1
vo_confidence: float
litesam_confidence: float
inlier_count: int
tracking_status: str # "good", "degraded", "lost"
```
**Confidence Metrics**:
- VO inlier count and ratio
- LiteSAM match confidence
- Factor graph marginal covariance
- Reprojection error
**Thresholds**:
- **Good**: VO inliers > 50, LiteSAM confidence > 0.7
- **Degraded**: VO inliers 20-50
- **Lost**: VO inliers < 20
**Test Cases**:
1. Good tracking → "good" status
2. Low overlap → "degraded"
3. Sharp turn → "lost"
---
### `detect_tracking_loss(confidence: ConfidenceAssessment) -> bool`
**Description**: Determines if tracking is lost.
**Called By**: Main processing loop
**Input**: `ConfidenceAssessment`
**Output**: `bool` - True if tracking lost
**Test Cases**:
1. Confidence good → False
2. Confidence lost → True
---
### `start_search(flight_id: str, frame_id: int, estimated_gps: GPSPoint) -> SearchSession`
**Description**: Initiates progressive search session.
**Called By**: Main processing loop (when tracking lost)
**Input**:
```python
flight_id: str
frame_id: int
estimated_gps: GPSPoint # Dead-reckoning estimate
```
**Output**:
```python
SearchSession:
session_id: str
flight_id: str
frame_id: int
center_gps: GPSPoint
current_grid_size: int # Starts at 1
max_grid_size: int # 25
found: bool
```
**Processing Flow**:
1. Create search session
2. Set center from estimated_gps
3. Set current_grid_size = 1
4. Return session
**Test Cases**:
1. Start search → session created with grid_size=1
---
### `expand_search_radius(session: SearchSession) -> List[TileCoords]`
**Description**: Expands search grid to next size (1→4→9→16→25).
**Called By**: Internal (after try_current_grid fails)
**Input**: `SearchSession`
**Output**: `List[TileCoords]` - Tiles for next grid size
**Processing Flow**:
1. Increment current_grid_size (1→4→9→16→25)
2. Call G04.expand_search_grid() to get new tiles only
3. Return new tile coordinates
**Test Cases**:
1. Expand 1→4 → returns 3 new tiles
2. Expand 4→9 → returns 5 new tiles
3. At grid_size=25 → no more expansion
---
### `try_current_grid(session: SearchSession, tiles: Dict[str, np.ndarray]) -> Optional[AlignmentResult]`
**Description**: Tries LiteSAM matching on current tile grid.
**Called By**: Internal (progressive search loop)
**Input**:
```python
session: SearchSession
tiles: Dict[str, np.ndarray] # From G04
```
**Output**: `Optional[AlignmentResult]` - Match result or None
**Processing Flow**:
1. Get UAV image for frame_id
2. For each tile in grid:
- Call G09.align_to_satellite(uav_image, tile)
- If match found with confidence > threshold:
- mark_found(session, result)
- Return result
3. Return None if no match
**Test Cases**:
1. Match on 3rd tile → returns result
2. No match in grid → returns None
---
### `mark_found(session: SearchSession, result: AlignmentResult) -> bool`
**Description**: Marks search session as successful.
**Called By**: Internal
**Input**:
```python
session: SearchSession
result: AlignmentResult
```
**Output**: `bool` - True
**Processing Flow**:
1. Set session.found = True
2. Log success (grid_size where found)
3. Resume processing
---
### `get_search_status(session: SearchSession) -> SearchStatus`
**Description**: Gets current search status.
**Called By**: G01 REST API (for status endpoint)
**Output**:
```python
SearchStatus:
current_grid_size: int
found: bool
exhausted: bool # Reached grid_size=25 without match
```
---
### `create_user_input_request(flight_id: str, frame_id: int, candidate_tiles: List[TileCandidate]) -> UserInputRequest`
**Description**: Creates user input request when all search strategies exhausted.
**Called By**: Internal (when grid_size=25 and no match)
**Input**:
```python
flight_id: str
frame_id: int
candidate_tiles: List[TileCandidate] # Top-5 from G08
```
**Output**:
```python
UserInputRequest:
request_id: str
flight_id: str
frame_id: int
uav_image: np.ndarray
candidate_tiles: List[TileCandidate]
message: str
```
**Processing Flow**:
1. Get UAV image for frame_id
2. Get top-5 candidates from G08
3. Create request
4. Send via G14 SSE → "user_input_needed" event
5. Update G02 flight_status("BLOCKED")
**Test Cases**:
1. All search failed → creates request
2. Request sent to client via SSE
---
### `apply_user_anchor(flight_id: str, frame_id: int, anchor: UserAnchor) -> bool`
**Description**: Applies user-provided GPS anchor.
**Called By**: G01 REST API (user-fix endpoint)
**Input**:
```python
flight_id: str
frame_id: int
anchor: UserAnchor:
uav_pixel: Tuple[float, float]
satellite_gps: GPSPoint
```
**Output**: `bool` - True if applied
**Processing Flow**:
1. Validate anchor data
2. Call G10.add_absolute_factor(frame_id, gps, is_user_anchor=True)
3. G10.optimize() → refines trajectory
4. Update G02 flight_status("PROCESSING")
5. Resume processing from next frame
**Test Cases**:
1. Valid anchor → applied, processing resumes
2. Invalid anchor → rejected
## Integration Tests
### Test 1: Progressive Search Flow
1. Tracking lost detected
2. start_search() → grid_size=1
3. try_current_grid(1 tile) → no match
4. expand_search_radius() → grid_size=4
5. try_current_grid(4 tiles) → match found
6. mark_found() → success
### Test 2: Full Search Exhaustion
1. start_search()
2. try grids: 1→4→9→16→25, all fail
3. create_user_input_request()
4. User provides anchor
5. apply_user_anchor() → processing resumes
### Test 3: Confidence Monitoring
1. Normal frames → confidence good
2. Low overlap frame → confidence degraded
3. Sharp turn → tracking lost, trigger search
## Non-Functional Requirements
### Performance
- **check_confidence**: < 10ms
- **Progressive search (25 tiles)**: < 1.5s total
- **User input latency**: < 500ms from creation to SSE event
### Reliability
- Always exhausts all search strategies before requesting user input
- Guarantees processing block when awaiting user input
- Graceful recovery from all failure modes
## Dependencies
### Internal Components
- G04 Satellite Data Manager (tile grids)
- G06 Image Rotation Manager (rotation sweep)
- G08 Global Place Recognition (candidates)
- G09 Metric Refinement (LiteSAM)
- G10 Factor Graph Optimizer (anchor application)
- G02 Flight Manager (status updates)
- G14 SSE Event Streamer (user input events)
### External Dependencies
- None
## Data Models
### ConfidenceAssessment
```python
class ConfidenceAssessment(BaseModel):
overall_confidence: float
vo_confidence: float
litesam_confidence: float
inlier_count: int
tracking_status: str
```
### SearchSession
```python
class SearchSession(BaseModel):
session_id: str
flight_id: str
frame_id: int
center_gps: GPSPoint
current_grid_size: int
max_grid_size: int
found: bool
exhausted: bool
```
### UserInputRequest
```python
class UserInputRequest(BaseModel):
request_id: str
flight_id: str
frame_id: int
uav_image: np.ndarray
candidate_tiles: List[TileCandidate]
message: str
created_at: datetime
```
### UserAnchor
```python
class UserAnchor(BaseModel):
uav_pixel: Tuple[float, float]
satellite_gps: GPSPoint
confidence: float = 1.0
```
@@ -0,0 +1,333 @@
# Coordinate Transformer
## Interface Definition
**Interface Name**: `ICoordinateTransformer`
### Interface Methods
```python
class ICoordinateTransformer(ABC):
@abstractmethod
def pixel_to_gps(self, pixel: Tuple[float, float], frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> GPSPoint:
pass
@abstractmethod
def gps_to_pixel(self, gps: GPSPoint, frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> Tuple[float, float]:
pass
@abstractmethod
def image_object_to_gps(self, object_pixel: Tuple[float, float], frame_id: int) -> GPSPoint:
pass
@abstractmethod
def compute_gsd(self, altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float:
pass
@abstractmethod
def transform_points(self, points: List[Tuple[float, float]], transformation: np.ndarray) -> List[Tuple[float, float]]:
pass
```
## Component Description
### Responsibilities
- Pixel-to-GPS coordinate conversions
- GPS-to-pixel inverse projections
- **Critical**: Convert object pixel coordinates (from external detection system) to GPS
- Ground Sampling Distance (GSD) calculations
- Handle multiple coordinate systems: WGS84, Web Mercator, ENU, image pixels, rotated coordinates
- Camera model integration for projection operations
### Scope
- Coordinate system transformations
- Camera projection mathematics
- Integration with Factor Graph poses
- Object localization (pixel → GPS)
- Support for external object detection system
## API Methods
### `pixel_to_gps(pixel: Tuple[float, float], frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> GPSPoint`
**Description**: Converts pixel coordinates to GPS using camera pose and ground plane assumption.
**Called By**:
- G13 Result Manager (for frame center GPS)
- Internal (for image_object_to_gps)
**Input**:
```python
pixel: Tuple[float, float] # (x, y) in image coordinates
frame_pose: Pose # From Factor Graph (ENU coordinates)
camera_params: CameraParameters
altitude: float # Ground altitude
```
**Output**:
```python
GPSPoint:
lat: float
lon: float
```
**Algorithm**:
1. Unproject pixel to 3D ray using H01 Camera Model
2. Intersect ray with ground plane at altitude
3. Transform 3D point from camera frame to ENU using frame_pose
4. Convert ENU to WGS84 GPS using H06 Web Mercator Utils
**Assumptions**:
- Ground plane assumption (terrain height negligible)
- Downward-pointing camera
- Known altitude
**Error Conditions**:
- None (always returns GPS, may be inaccurate if assumptions violated)
**Test Cases**:
1. **Image center**: Returns frame center GPS
2. **Image corner**: Returns GPS at corner
3. **Object pixel**: Returns object GPS
4. **Altitude variation**: Correct GPS at different altitudes
---
### `gps_to_pixel(gps: GPSPoint, frame_pose: Pose, camera_params: CameraParameters, altitude: float) -> Tuple[float, float]`
**Description**: Inverse projection from GPS to image pixel coordinates.
**Called By**:
- Visualization tools (overlay GPS annotations)
- Testing/validation
**Input**:
```python
gps: GPSPoint
frame_pose: Pose
camera_params: CameraParameters
altitude: float
```
**Output**:
```python
Tuple[float, float]: (x, y) pixel coordinates
```
**Algorithm**:
1. Convert GPS to ENU using H06
2. Transform ENU point to camera frame using frame_pose
3. Project 3D point to image plane using H01 Camera Model
4. Return pixel coordinates
**Test Cases**:
1. **Frame center GPS**: Returns image center pixel
2. **Out of view GPS**: Returns pixel outside image bounds
---
### `image_object_to_gps(object_pixel: Tuple[float, float], frame_id: int) -> GPSPoint`
**Description**: **Critical method** - Converts object pixel coordinates to GPS. Used by external object detection system.
**Called By**:
- External object detection system (provides pixel coordinates)
- G13 Result Manager (converts objects to GPS for output)
**Input**:
```python
object_pixel: Tuple[float, float] # Pixel coordinates from object detector
frame_id: int # Frame containing object
```
**Output**:
```python
GPSPoint: GPS coordinates of object center
```
**Processing Flow**:
1. Get frame_pose from G10 Factor Graph
2. Get camera_params from G16 Configuration Manager
3. Get altitude from configuration
4. Call pixel_to_gps(object_pixel, frame_pose, camera_params, altitude)
5. Return GPS
**User Story**:
- External system detects object in UAV image at pixel (1024, 768)
- Calls image_object_to_gps(frame_id=237, object_pixel=(1024, 768))
- Returns GPSPoint(lat=48.123, lon=37.456)
- Object GPS can be used for navigation, targeting, etc.
**Test Cases**:
1. **Object at image center**: Returns frame center GPS
2. **Object at corner**: Returns GPS with offset
3. **Multiple objects**: Each gets correct GPS
4. **Refined trajectory**: Object GPS updates after refinement
---
### `compute_gsd(altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float`
**Description**: Computes Ground Sampling Distance (meters per pixel).
**Called By**:
- Internal (for pixel_to_gps)
- G09 Metric Refinement (for scale calculations)
- H02 GSD Calculator (may delegate to)
**Input**:
```python
altitude: float # meters
focal_length: float # mm
sensor_width: float # mm
image_width: int # pixels
```
**Output**:
```python
float: GSD in meters/pixel
```
**Formula**:
```
GSD = (altitude * sensor_width) / (focal_length * image_width)
```
**Example**:
- altitude = 800m
- focal_length = 24mm
- sensor_width = 36mm
- image_width = 6000px
- GSD = (800 * 36) / (24 * 6000) = 0.2 m/pixel
**Test Cases**:
1. **Standard parameters**: Returns reasonable GSD (~0.1-0.3 m/pixel)
2. **Higher altitude**: GSD increases
3. **Longer focal length**: GSD decreases
---
### `transform_points(points: List[Tuple[float, float]], transformation: np.ndarray) -> List[Tuple[float, float]]`
**Description**: Applies homography or affine transformation to list of points.
**Called By**:
- G06 Image Rotation Manager (for rotation transforms)
- G09 Metric Refinement (homography application)
**Input**:
```python
points: List[Tuple[float, float]]
transformation: np.ndarray # 3×3 homography or 2×3 affine
```
**Output**:
```python
List[Tuple[float, float]]: Transformed points
```
**Processing Flow**:
1. Convert points to homogeneous coordinates
2. Apply transformation matrix
3. Normalize and return
**Test Cases**:
1. **Identity transform**: Points unchanged
2. **Rotation**: Points rotated correctly
3. **Homography**: Points transformed with perspective
## Integration Tests
### Test 1: Frame Center GPS Calculation
1. Get frame_pose from Factor Graph
2. pixel_to_gps(image_center) → GPS
3. Verify GPS matches expected location
4. Verify accuracy < 50m
### Test 2: Object Localization
1. External detector finds object at pixel (1500, 2000)
2. image_object_to_gps(frame_id, pixel) → GPS
3. Verify GPS correct
4. Multiple objects → all get correct GPS
### Test 3: Round-Trip Conversion
1. Start with GPS point
2. gps_to_pixel() → pixel
3. pixel_to_gps() → GPS
4. Verify GPS matches original (within tolerance)
### Test 4: GSD Calculation
1. compute_gsd() with known parameters
2. Verify matches expected value
3. Test at different altitudes
## Non-Functional Requirements
### Performance
- **pixel_to_gps**: < 5ms
- **gps_to_pixel**: < 5ms
- **image_object_to_gps**: < 10ms
- **compute_gsd**: < 1ms
### Accuracy
- **GPS accuracy**: Inherits from Factor Graph accuracy (~20m)
- **GSD calculation**: ±1% precision
- **Projection accuracy**: < 1 pixel error
### Reliability
- Handle edge cases (points outside image)
- Graceful handling of degenerate configurations
- Numerical stability
## Dependencies
### Internal Components
- **G10 Factor Graph Optimizer**: For frame poses
- **G16 Configuration Manager**: For camera parameters
- **H01 Camera Model**: For projection operations
- **H02 GSD Calculator**: For GSD calculations
- **H06 Web Mercator Utils**: For coordinate conversions
### External Dependencies
- **numpy**: Matrix operations
- **opencv-python**: Homography operations (optional)
## Data Models
### Pose (from Factor Graph)
```python
class Pose(BaseModel):
position: np.ndarray # (3,) - [x, y, z] in ENU
orientation: np.ndarray # (4,) quaternion or (3,3) rotation
timestamp: datetime
```
### CameraParameters
```python
class CameraParameters(BaseModel):
focal_length: float # mm
sensor_width: float # mm
sensor_height: float # mm
resolution_width: int # pixels
resolution_height: int # pixels
principal_point: Tuple[float, float] # (cx, cy)
distortion_coefficients: Optional[List[float]]
```
### GPSPoint
```python
class GPSPoint(BaseModel):
lat: float
lon: float
```
### CoordinateFrame
```python
class CoordinateFrame(Enum):
WGS84 = "wgs84" # GPS coordinates
ENU = "enu" # East-North-Up local frame
ECEF = "ecef" # Earth-Centered Earth-Fixed
IMAGE = "image" # Image pixel coordinates
CAMERA = "camera" # Camera frame
```
@@ -0,0 +1,267 @@
# Result Manager
## Interface Definition
**Interface Name**: `IResultManager`
### Interface Methods
```python
class IResultManager(ABC):
@abstractmethod
def update_frame_result(self, flight_id: str, frame_id: int, result: FrameResult) -> bool:
pass
@abstractmethod
def publish_to_route_api(self, flight_id: str, frame_id: int) -> bool:
pass
@abstractmethod
def get_flight_results(self, flight_id: str) -> FlightResults:
pass
@abstractmethod
def mark_refined(self, flight_id: str, frame_ids: List[int]) -> bool:
pass
@abstractmethod
def get_changed_frames(self, flight_id: str, since: datetime) -> List[int]:
pass
```
## Component Description
### Responsibilities
- Manage trajectory results per flight
- Track frame refinements and changes
- Trigger per-frame Route API updates via G03
- Send incremental updates via G14 SSE
- Maintain result versioning for audit trail
- Convert optimized poses to GPS coordinates
### Scope
- Result state management
- Route API integration
- SSE event triggering
- Incremental update detection
- Result persistence
## API Methods
### `update_frame_result(flight_id: str, frame_id: int, result: FrameResult) -> bool`
**Description**: Updates result for a processed frame.
**Called By**:
- Main processing loop (after each frame)
- G10 Factor Graph (after refinement)
**Input**:
```python
flight_id: str
frame_id: int
result: FrameResult:
gps_center: GPSPoint
altitude: float
heading: float
confidence: float
timestamp: datetime
refined: bool
objects: List[ObjectLocation] # From external detector
```
**Output**: `bool` - True if updated
**Processing Flow**:
1. Store result in memory/database
2. Call publish_to_route_api()
3. Call G14.send_frame_result()
4. Update flight statistics
**Test Cases**:
1. New frame result → stored and published
2. Refined result → updates existing, marks refined=True
---
### `publish_to_route_api(flight_id: str, frame_id: int) -> bool`
**Description**: Sends frame GPS to Route API via G03 client.
**Called By**:
- Internal (after update_frame_result)
**Input**:
```python
flight_id: str
frame_id: int
```
**Output**: `bool` - True if published successfully
**Processing Flow**:
1. Get result for frame_id
2. Convert to Waypoint format
3. Call G03.update_route_waypoint()
4. Handle errors (retry if transient)
**Test Cases**:
1. Successful publish → Route API updated
2. Route API unavailable → logs error, continues
---
### `get_flight_results(flight_id: str) -> FlightResults`
**Description**: Retrieves all results for a flight.
**Called By**:
- G01 REST API (results endpoint)
- Testing/validation
**Input**: `flight_id: str`
**Output**:
```python
FlightResults:
flight_id: str
frames: List[FrameResult]
statistics: FlightStatistics
```
**Test Cases**:
1. Get all results → returns complete trajectory
---
### `mark_refined(flight_id: str, frame_ids: List[int]) -> bool`
**Description**: Marks frames as refined after batch optimization.
**Called By**:
- G10 Factor Graph (after asynchronous refinement)
**Input**:
```python
flight_id: str
frame_ids: List[int] # Frames with updated poses
```
**Output**: `bool`
**Processing Flow**:
1. For each frame_id:
- Get refined pose from G10
- Convert to GPS via G12
- Update result with refined=True
- publish_to_route_api()
- Call G14.send_refinement()
**Test Cases**:
1. Batch refinement → all frames updated and published
---
### `get_changed_frames(flight_id: str, since: datetime) -> List[int]`
**Description**: Gets frames changed since timestamp (for incremental updates).
**Called By**:
- G14 SSE Event Streamer (for reconnection replay)
**Input**:
```python
flight_id: str
since: datetime
```
**Output**: `List[int]` - Frame IDs changed since timestamp
**Test Cases**:
1. Get changes → returns only modified frames
2. No changes → returns empty list
## Integration Tests
### Test 1: Per-Frame Processing
1. Process frame 237
2. update_frame_result() → stores result
3. Verify publish_to_route_api() called
4. Verify G14 SSE event sent
### Test 2: Batch Refinement
1. Process 100 frames
2. Factor graph refines frames 10-50
3. mark_refined([10-50]) → updates all
4. Verify Route API updated
5. Verify SSE refinement events sent
### Test 3: Incremental Updates
1. Process frames 1-100
2. Client disconnects at frame 50
3. Client reconnects
4. get_changed_frames(since=frame_50_time)
5. Client receives frames 51-100
## Non-Functional Requirements
### Performance
- **update_frame_result**: < 50ms
- **publish_to_route_api**: < 100ms (non-blocking)
- **get_flight_results**: < 200ms for 2000 frames
### Reliability
- Result persistence survives crashes
- Guaranteed at-least-once delivery to Route API
- Idempotent updates
## Dependencies
### Internal Components
- G03 Route API Client
- G10 Factor Graph Optimizer
- G12 Coordinate Transformer
- G14 SSE Event Streamer
- G17 Database Layer
### External Dependencies
- None
## Data Models
### FrameResult
```python
class ObjectLocation(BaseModel):
object_id: str
pixel: Tuple[float, float]
gps: GPSPoint
class_name: str
confidence: float
class FrameResult(BaseModel):
frame_id: int
gps_center: GPSPoint
altitude: float
heading: float
confidence: float
timestamp: datetime
refined: bool
objects: List[ObjectLocation]
updated_at: datetime
```
### FlightResults
```python
class FlightStatistics(BaseModel):
total_frames: int
processed_frames: int
refined_frames: int
mean_confidence: float
processing_time: float
class FlightResults(BaseModel):
flight_id: str
frames: List[FrameResult]
statistics: FlightStatistics
```
@@ -0,0 +1,242 @@
# SSE Event Streamer
## Interface Definition
**Interface Name**: `ISSEEventStreamer`
### Interface Methods
```python
class ISSEEventStreamer(ABC):
@abstractmethod
def create_stream(self, flight_id: str, client_id: str) -> StreamConnection:
pass
@abstractmethod
def send_frame_result(self, flight_id: str, frame_result: FrameResult) -> bool:
pass
@abstractmethod
def send_search_progress(self, flight_id: str, search_status: SearchStatus) -> bool:
pass
@abstractmethod
def send_user_input_request(self, flight_id: str, request: UserInputRequest) -> bool:
pass
@abstractmethod
def send_refinement(self, flight_id: str, frame_id: int, updated_result: FrameResult) -> bool:
pass
@abstractmethod
def close_stream(self, flight_id: str, client_id: str) -> bool:
pass
```
## Component Description
### Responsibilities
- Server-Sent Events broadcaster for real-time results
- Stream per-frame processing results to clients
- Send refinement updates asynchronously
- Request user input when processing blocked
- Handle client connections and reconnections
- Event replay from last received event
### Scope
- SSE protocol implementation
- Event formatting and sending
- Connection management
- Client reconnection handling
- Multiple concurrent streams per flight
## API Methods
### `create_stream(flight_id: str, client_id: str) -> StreamConnection`
**Description**: Creates SSE connection for a client.
**Called By**: G01 REST API (GET /stream endpoint)
**Output**:
```python
StreamConnection:
stream_id: str
flight_id: str
client_id: str
last_event_id: Optional[str]
```
**Event Types**:
- `frame_processed`
- `frame_refined`
- `search_expanded`
- `user_input_needed`
- `processing_blocked`
- `route_api_updated`
- `route_completed`
**Test Cases**:
1. Create stream → client receives keepalive pings
2. Multiple clients → each gets own stream
---
### `send_frame_result(flight_id: str, frame_result: FrameResult) -> bool`
**Description**: Sends frame_processed event.
**Called By**: G13 Result Manager
**Event Format**:
```json
{
"event": "frame_processed",
"id": "frame_237",
"data": {
"frame_id": 237,
"gps": {"lat": 48.123, "lon": 37.456},
"altitude": 800.0,
"confidence": 0.95,
"heading": 87.3,
"timestamp": "2025-11-24T10:30:00Z"
}
}
```
**Test Cases**:
1. Send event → all clients receive
2. Client disconnected → event buffered for replay
---
### `send_search_progress(flight_id: str, search_status: SearchStatus) -> bool`
**Description**: Sends search_expanded event.
**Called By**: G11 Failure Recovery Coordinator
**Event Format**:
```json
{
"event": "search_expanded",
"data": {
"frame_id": 237,
"grid_size": 9,
"status": "searching"
}
}
```
---
### `send_user_input_request(flight_id: str, request: UserInputRequest) -> bool`
**Description**: Sends user_input_needed event.
**Called By**: G11 Failure Recovery Coordinator
**Event Format**:
```json
{
"event": "user_input_needed",
"data": {
"request_id": "uuid",
"frame_id": 237,
"candidate_tiles": [...]
}
}
```
---
### `send_refinement(flight_id: str, frame_id: int, updated_result: FrameResult) -> bool`
**Description**: Sends frame_refined event.
**Called By**: G13 Result Manager
**Event Format**:
```json
{
"event": "frame_refined",
"data": {
"frame_id": 237,
"gps": {"lat": 48.1235, "lon": 37.4562},
"refined": true
}
}
```
---
### `close_stream(flight_id: str, client_id: str) -> bool`
**Description**: Closes SSE connection.
**Called By**: G01 REST API (on client disconnect)
## Integration Tests
### Test 1: Real-Time Streaming
1. Client connects
2. Process 100 frames
3. Client receives 100 frame_processed events
4. Verify order and completeness
### Test 2: Reconnection with Replay
1. Client connects
2. Process 50 frames
3. Client disconnects
4. Process 50 more frames
5. Client reconnects with last_event_id
6. Client receives frames 51-100
### Test 3: User Input Flow
1. Processing blocks
2. send_user_input_request()
3. Client receives event
4. Client responds with fix
5. Processing resumes
## Non-Functional Requirements
### Performance
- **Event latency**: < 500ms from generation to client
- **Throughput**: 100 events/second
- **Concurrent connections**: 1000+ clients
### Reliability
- Event buffering for disconnected clients
- Automatic reconnection support
- Keepalive pings every 30 seconds
## Dependencies
### Internal Components
- None (receives calls from other components)
### External Dependencies
- **FastAPI** or **Flask** SSE support
- **asyncio**: For async event streaming
## Data Models
### StreamConnection
```python
class StreamConnection(BaseModel):
stream_id: str
flight_id: str
client_id: str
created_at: datetime
last_event_id: Optional[str]
```
### SSEEvent
```python
class SSEEvent(BaseModel):
event: str
id: Optional[str]
data: Dict[str, Any]
```
@@ -0,0 +1,224 @@
# Model Manager
## Interface Definition
**Interface Name**: `IModelManager`
### Interface Methods
```python
class IModelManager(ABC):
@abstractmethod
def load_model(self, model_name: str, model_format: str) -> bool:
pass
@abstractmethod
def get_inference_engine(self, model_name: str) -> InferenceEngine:
pass
@abstractmethod
def optimize_to_tensorrt(self, model_name: str, onnx_path: str) -> str:
pass
@abstractmethod
def fallback_to_onnx(self, model_name: str) -> bool:
pass
@abstractmethod
def warmup_model(self, model_name: str) -> bool:
pass
```
## Component Description
### Responsibilities
- Load ML models (TensorRT primary, ONNX fallback)
- Manage model lifecycle (loading, unloading, warmup)
- Provide inference engines for:
- SuperPoint (feature extraction)
- LightGlue (feature matching)
- DINOv2 (global descriptors)
- LiteSAM (cross-view matching)
- Handle TensorRT optimization and ONNX fallback
- Ensure <5s processing requirement through acceleration
### Scope
- Model loading and caching
- TensorRT optimization
- ONNX fallback handling
- Inference engine abstraction
- GPU memory management
## API Methods
### `load_model(model_name: str, model_format: str) -> bool`
**Description**: Loads model in specified format.
**Called By**: G02 Flight Manager (during initialization)
**Input**:
```python
model_name: str # "SuperPoint", "LightGlue", "DINOv2", "LiteSAM"
model_format: str # "tensorrt", "onnx", "pytorch"
```
**Output**: `bool` - True if loaded
**Processing Flow**:
1. Check if model already loaded
2. Load model file
3. Initialize inference engine
4. Warm up model
5. Cache for reuse
**Test Cases**:
1. Load TensorRT model → succeeds
2. TensorRT unavailable → fallback to ONNX
3. Load all 4 models → all succeed
---
### `get_inference_engine(model_name: str) -> InferenceEngine`
**Description**: Gets inference engine for a model.
**Called By**:
- G07 Sequential VO (SuperPoint, LightGlue)
- G08 Global Place Recognition (DINOv2)
- G09 Metric Refinement (LiteSAM)
**Output**:
```python
InferenceEngine:
model_name: str
format: str
infer(input: np.ndarray) -> np.ndarray
```
**Test Cases**:
1. Get SuperPoint engine → returns engine
2. Call infer() → returns features
---
### `optimize_to_tensorrt(model_name: str, onnx_path: str) -> str`
**Description**: Converts ONNX model to TensorRT for acceleration.
**Called By**: System initialization (one-time)
**Input**:
```python
model_name: str
onnx_path: str # Path to ONNX model
```
**Output**: `str` - Path to TensorRT engine
**Processing Details**:
- FP16 precision (2-3x speedup)
- Graph fusion and kernel optimization
- One-time conversion, cached for reuse
**Test Cases**:
1. Convert ONNX to TensorRT → engine created
2. Load TensorRT engine → inference faster than ONNX
---
### `fallback_to_onnx(model_name: str) -> bool`
**Description**: Falls back to ONNX if TensorRT fails.
**Called By**: Internal (during load_model)
**Processing Flow**:
1. Detect TensorRT failure
2. Load ONNX model
3. Log warning
4. Continue with ONNX
**Test Cases**:
1. TensorRT fails → ONNX loaded automatically
2. System continues functioning
---
### `warmup_model(model_name: str) -> bool`
**Description**: Warms up model with dummy input.
**Called By**: Internal (after load_model)
**Purpose**: Initialize CUDA kernels, allocate GPU memory
**Test Cases**:
1. Warmup → first real inference fast
## Integration Tests
### Test 1: Model Loading
1. load_model("SuperPoint", "tensorrt")
2. load_model("LightGlue", "tensorrt")
3. load_model("DINOv2", "tensorrt")
4. load_model("LiteSAM", "tensorrt")
5. Verify all loaded
### Test 2: Inference Performance
1. Get inference engine
2. Run inference 100 times
3. Measure average latency
4. Verify meets performance targets
### Test 3: Fallback Scenario
1. Simulate TensorRT failure
2. Verify fallback to ONNX
3. Verify inference still works
## Non-Functional Requirements
### Performance
- **SuperPoint**: ~15ms (TensorRT), ~50ms (ONNX)
- **LightGlue**: ~50ms (TensorRT), ~150ms (ONNX)
- **DINOv2**: ~150ms (TensorRT), ~500ms (ONNX)
- **LiteSAM**: ~60ms (TensorRT), ~200ms (ONNX)
### Memory
- GPU memory: ~4GB for all 4 models
### Reliability
- Graceful fallback to ONNX
- Automatic retry on transient errors
## Dependencies
### External Dependencies
- **TensorRT**: NVIDIA inference optimization
- **ONNX Runtime**: ONNX inference
- **PyTorch**: Model weights (optional)
- **CUDA**: GPU acceleration
## Data Models
### InferenceEngine
```python
class InferenceEngine(ABC):
model_name: str
format: str
@abstractmethod
def infer(self, input: np.ndarray) -> np.ndarray:
pass
```
### ModelConfig
```python
class ModelConfig(BaseModel):
model_name: str
model_path: str
format: str
precision: str # "fp16", "fp32"
warmup_iterations: int = 3
```
@@ -0,0 +1,172 @@
# Configuration Manager
## Interface Definition
**Interface Name**: `IConfigurationManager`
### Interface Methods
```python
class IConfigurationManager(ABC):
@abstractmethod
def load_config(self, config_path: str) -> SystemConfig:
pass
@abstractmethod
def get_camera_params(self, camera_id: Optional[str] = None) -> CameraParameters:
pass
@abstractmethod
def validate_config(self, config: SystemConfig) -> ValidationResult:
pass
@abstractmethod
def get_flight_config(self, flight_id: str) -> FlightConfig:
pass
@abstractmethod
def update_config(self, section: str, key: str, value: Any) -> bool:
pass
```
## Component Description
### Responsibilities
- Load system configuration from files/environment
- Provide camera parameters (focal length, sensor size, resolution)
- Manage flight-specific configurations
- Validate configuration integrity
- Support configuration updates at runtime
### Scope
- System-wide configuration
- Camera parameter management
- Operational area bounds
- Model paths and settings
- Database connections
- API endpoints
## API Methods
### `load_config(config_path: str) -> SystemConfig`
**Description**: Loads system configuration.
**Called By**: System startup
**Input**: `config_path: str` - Path to config file (YAML/JSON)
**Output**: `SystemConfig` - Complete configuration
**Test Cases**:
1. Load valid config → succeeds
2. Missing file → uses defaults
3. Invalid config → raises error
---
### `get_camera_params(camera_id: Optional[str] = None) -> CameraParameters`
**Description**: Gets camera parameters.
**Called By**: All processing components
**Output**:
```python
CameraParameters:
focal_length: float
sensor_width: float
sensor_height: float
resolution_width: int
resolution_height: int
principal_point: Tuple[float, float]
distortion_coefficients: List[float]
```
**Test Cases**:
1. Get default camera → returns params
2. Get specific camera → returns params
---
### `validate_config(config: SystemConfig) -> ValidationResult`
**Description**: Validates configuration.
**Called By**: After load_config
**Validation Rules**:
- Camera parameters sensible
- Paths exist
- Operational area valid
- Database connection string valid
**Test Cases**:
1. Valid config → passes
2. Invalid focal length → fails
---
### `get_flight_config(flight_id: str) -> FlightConfig`
**Description**: Gets flight-specific configuration.
**Called By**: G02 Flight Manager
**Output**:
```python
FlightConfig:
camera_params: CameraParameters
altitude: float
operational_area: OperationalArea
```
**Test Cases**:
1. Get flight config → returns params
---
### `update_config(section: str, key: str, value: Any) -> bool`
**Description**: Updates config at runtime.
**Called By**: Admin tools
**Test Cases**:
1. Update value → succeeds
2. Invalid key → fails
## Data Models
### SystemConfig
```python
class SystemConfig(BaseModel):
camera: CameraParameters
operational_area: OperationalArea
models: ModelPaths
database: DatabaseConfig
api: APIConfig
```
### CameraParameters
```python
class CameraParameters(BaseModel):
focal_length: float # mm
sensor_width: float # mm
sensor_height: float # mm
resolution_width: int
resolution_height: int
principal_point: Tuple[float, float]
distortion_coefficients: List[float]
```
### OperationalArea
```python
class OperationalArea(BaseModel):
name: str = "Eastern Ukraine"
min_lat: float = 45.0
max_lat: float = 52.0
min_lon: float = 22.0
max_lon: float = 40.0
```
@@ -0,0 +1,193 @@
# GPS-Denied Database Layer
## Interface Definition
**Interface Name**: `IGPSDeniedDatabase`
### Interface Methods
```python
class IGPSDeniedDatabase(ABC):
@abstractmethod
def save_flight_state(self, flight_state: FlightState) -> bool:
pass
@abstractmethod
def load_flight_state(self, flight_id: str) -> Optional[FlightState]:
pass
@abstractmethod
def query_processing_history(self, filters: Dict[str, Any]) -> List[FlightState]:
pass
@abstractmethod
def save_frame_result(self, flight_id: str, frame_result: FrameResult) -> bool:
pass
@abstractmethod
def get_frame_results(self, flight_id: str) -> List[FrameResult]:
pass
```
## Component Description
### Responsibilities
- Database access for GPS-Denied processing state
- Separate schema from Route API database
- Persist flight state, frame results
- Query processing history
- Support crash recovery
### Scope
- Flight state persistence
- Frame result storage
- Processing history queries
- Connection management
- Transaction handling
## API Methods
### `save_flight_state(flight_state: FlightState) -> bool`
**Description**: Saves flight processing state.
**Called By**: G02 Flight Manager
**Input**:
```python
FlightState:
flight_id: str
route_id: str
status: str
frames_processed: int
frames_total: int
current_heading: float
blocked: bool
...
```
**Output**: `bool` - True if saved
**Test Cases**:
1. Save state → persisted
2. Update state → overwrites
---
### `load_flight_state(flight_id: str) -> Optional[FlightState]`
**Description**: Loads flight state.
**Called By**: G02 Flight Manager (crash recovery)
**Output**: `Optional[FlightState]`
**Test Cases**:
1. Load existing → returns state
2. Load non-existent → returns None
---
### `query_processing_history(filters: Dict[str, Any]) -> List[FlightState]`
**Description**: Queries historical processing data.
**Called By**: Analytics, admin tools
**Test Cases**:
1. Query by date range → returns flights
2. Query by status → returns filtered
---
### `save_frame_result(flight_id: str, frame_result: FrameResult) -> bool`
**Description**: Saves frame processing result.
**Called By**: G13 Result Manager
**Test Cases**:
1. Save result → persisted
2. Update result (refinement) → overwrites
---
### `get_frame_results(flight_id: str) -> List[FrameResult]`
**Description**: Gets all frame results for flight.
**Called By**: G13 Result Manager
**Test Cases**:
1. Get results → returns all frames
2. No results → returns empty list
## Database Schema
```sql
-- Flights table
CREATE TABLE gps_denied_flights (
flight_id VARCHAR(36) PRIMARY KEY,
route_id VARCHAR(36) NOT NULL,
status VARCHAR(50),
frames_processed INT,
frames_total INT,
current_heading FLOAT,
blocked BOOLEAN,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- Frame results table
CREATE TABLE frame_results (
id VARCHAR(36) PRIMARY KEY,
flight_id VARCHAR(36) NOT NULL,
frame_id INT NOT NULL,
gps_lat DECIMAL(10, 7),
gps_lon DECIMAL(11, 7),
altitude FLOAT,
heading FLOAT,
confidence FLOAT,
refined BOOLEAN,
timestamp TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (flight_id) REFERENCES gps_denied_flights(flight_id) ON DELETE CASCADE,
UNIQUE KEY (flight_id, frame_id)
);
```
## Dependencies
### External Dependencies
- **PostgreSQL** or **MySQL**
- **SQLAlchemy** or **psycopg2**
## Data Models
### FlightState
```python
class FlightState(BaseModel):
flight_id: str
route_id: str
status: str
frames_processed: int
frames_total: int
current_heading: Optional[float]
blocked: bool
created_at: datetime
updated_at: datetime
```
### FrameResult
```python
class FrameResult(BaseModel):
frame_id: int
gps_center: GPSPoint
altitude: float
heading: float
confidence: float
refined: bool
timestamp: datetime
updated_at: datetime
```
@@ -0,0 +1,101 @@
# Camera Model Helper
## Interface Definition
**Interface Name**: `ICameraModel`
### Interface Methods
```python
class ICameraModel(ABC):
@abstractmethod
def project(self, point_3d: np.ndarray, camera_params: CameraParameters) -> Tuple[float, float]:
pass
@abstractmethod
def unproject(self, pixel: Tuple[float, float], depth: float, camera_params: CameraParameters) -> np.ndarray:
pass
@abstractmethod
def get_focal_length(self, camera_params: CameraParameters) -> Tuple[float, float]:
pass
@abstractmethod
def apply_distortion(self, pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]:
pass
@abstractmethod
def remove_distortion(self, pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]:
pass
```
## Component Description
Pinhole camera projection model with Brown-Conrady distortion handling.
## API Methods
### `project(point_3d: np.ndarray, camera_params: CameraParameters) -> Tuple[float, float]`
**Description**: Projects 3D point to 2D image pixel.
**Formula**:
```
x = fx * X/Z + cx
y = fy * Y/Z + cy
```
---
### `unproject(pixel: Tuple[float, float], depth: float, camera_params: CameraParameters) -> np.ndarray`
**Description**: Unprojects pixel to 3D ray at given depth.
**Formula**:
```
X = (x - cx) * depth / fx
Y = (y - cy) * depth / fy
Z = depth
```
---
### `get_focal_length(camera_params: CameraParameters) -> Tuple[float, float]`
**Description**: Returns (fx, fy) in pixels.
**Formula**:
```
fx = focal_length_mm * image_width / sensor_width_mm
fy = focal_length_mm * image_height / sensor_height_mm
```
---
### `apply_distortion(pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]`
**Description**: Applies radial and tangential distortion (Brown-Conrady model).
---
### `remove_distortion(pixel: Tuple[float, float], distortion_coeffs: List[float]) -> Tuple[float, float]`
**Description**: Removes distortion from observed pixel.
## Dependencies
**External**: opencv-python, numpy
## Data Models
```python
class CameraParameters(BaseModel):
focal_length: float # mm
sensor_width: float # mm
sensor_height: float # mm
resolution_width: int
resolution_height: int
principal_point: Tuple[float, float] # (cx, cy) pixels
distortion_coefficients: List[float] # [k1, k2, p1, p2, k3]
```
@@ -0,0 +1,78 @@
# GSD Calculator Helper
## Interface Definition
**Interface Name**: `IGSDCalculator`
### Interface Methods
```python
class IGSDCalculator(ABC):
@abstractmethod
def compute_gsd(self, altitude: float, camera_params: CameraParameters) -> float:
pass
@abstractmethod
def altitude_to_scale(self, altitude: float, focal_length: float) -> float:
pass
@abstractmethod
def meters_per_pixel(self, lat: float, zoom: int) -> float:
pass
@abstractmethod
def gsd_from_camera(self, altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float:
pass
```
## Component Description
Ground Sampling Distance computations for altitude and coordinate systems.
## API Methods
### `compute_gsd(altitude: float, camera_params: CameraParameters) -> float`
**Description**: Computes GSD from altitude and camera parameters.
**Formula**:
```
GSD = (altitude * sensor_width) / (focal_length * image_width)
```
**Example**: altitude=800m, focal=24mm, sensor=36mm, width=6000px → GSD=0.2 m/pixel
---
### `altitude_to_scale(altitude: float, focal_length: float) -> float`
**Description**: Converts altitude to scale factor for VO.
---
### `meters_per_pixel(lat: float, zoom: int) -> float`
**Description**: Computes GSD for Web Mercator tiles at zoom level.
**Formula**:
```
meters_per_pixel = 156543.03392 * cos(lat * π/180) / 2^zoom
```
**Example**: lat=48°N, zoom=19 → ~0.3 m/pixel
---
### `gsd_from_camera(altitude: float, focal_length: float, sensor_width: float, image_width: int) -> float`
**Description**: Direct GSD calculation from parameters.
## Dependencies
**External**: numpy
## Test Cases
1. Standard camera at 800m → GSD ~0.1-0.3 m/pixel
2. Web Mercator zoom 19 at Ukraine → ~0.3 m/pixel
@@ -0,0 +1,74 @@
# Robust Kernels Helper
## Interface Definition
**Interface Name**: `IRobustKernels`
### Interface Methods
```python
class IRobustKernels(ABC):
@abstractmethod
def huber_loss(self, error: float, threshold: float) -> float:
pass
@abstractmethod
def cauchy_loss(self, error: float, k: float) -> float:
pass
@abstractmethod
def compute_weight(self, error: float, kernel_type: str, params: Dict[str, float]) -> float:
pass
```
## Component Description
Huber/Cauchy loss functions for outlier rejection in optimization.
## API Methods
### `huber_loss(error: float, threshold: float) -> float`
**Description**: Huber robust loss function.
**Formula**:
```
if |error| <= threshold:
loss = 0.5 * error^2
else:
loss = threshold * (|error| - 0.5 * threshold)
```
**Purpose**: Quadratic for small errors, linear for large errors (outliers).
---
### `cauchy_loss(error: float, k: float) -> float`
**Description**: Cauchy robust loss function.
**Formula**:
```
loss = (k^2 / 2) * log(1 + (error/k)^2)
```
**Purpose**: More aggressive outlier rejection than Huber.
---
### `compute_weight(error: float, kernel_type: str, params: Dict[str, float]) -> float`
**Description**: Computes robust weight for error.
**Usage**: Factor Graph applies weights to downweight outliers.
## Dependencies
**External**: numpy
## Test Cases
1. Small error → weight ≈ 1.0
2. Large error (350m outlier) → weight ≈ 0.1 (downweighted)
3. Huber vs Cauchy → Cauchy more aggressive
@@ -0,0 +1,84 @@
# Faiss Index Manager Helper
## Interface Definition
**Interface Name**: `IFaissIndexManager`
### Interface Methods
```python
class IFaissIndexManager(ABC):
@abstractmethod
def build_index(self, descriptors: np.ndarray, index_type: str) -> FaissIndex:
pass
@abstractmethod
def add_descriptors(self, index: FaissIndex, descriptors: np.ndarray) -> bool:
pass
@abstractmethod
def search(self, index: FaissIndex, query: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]:
pass
@abstractmethod
def save_index(self, index: FaissIndex, path: str) -> bool:
pass
@abstractmethod
def load_index(self, path: str) -> FaissIndex:
pass
```
## Component Description
Manages Faiss indices for AnyLoc retrieval (IVF, HNSW options).
## API Methods
### `build_index(descriptors: np.ndarray, index_type: str) -> FaissIndex`
**Description**: Builds Faiss index from descriptors.
**Index Types**:
- **"IVF"**: Inverted File (fast for large databases)
- **"HNSW"**: Hierarchical Navigable Small World (best accuracy/speed trade-off)
- **"Flat"**: Brute force (exact, slow for large datasets)
**Input**: (N, D) descriptors array
---
### `add_descriptors(index: FaissIndex, descriptors: np.ndarray) -> bool`
**Description**: Adds more descriptors to existing index.
---
### `search(index: FaissIndex, query: np.ndarray, k: int) -> Tuple[np.ndarray, np.ndarray]`
**Description**: Searches for k nearest neighbors.
**Output**: (distances, indices) - shape (k,)
---
### `save_index(index: FaissIndex, path: str) -> bool`
**Description**: Saves index to disk for fast startup.
---
### `load_index(path: str) -> FaissIndex`
**Description**: Loads pre-built index from disk.
## Dependencies
**External**: faiss-gpu or faiss-cpu
## Test Cases
1. Build index with 10,000 descriptors → succeeds
2. Search query → returns top-k matches
3. Save/load index → index restored correctly
@@ -0,0 +1,93 @@
# Performance Monitor Helper
## Interface Definition
**Interface Name**: `IPerformanceMonitor`
### Interface Methods
```python
class IPerformanceMonitor(ABC):
@abstractmethod
def start_timer(self, operation: str) -> str:
pass
@abstractmethod
def end_timer(self, timer_id: str) -> float:
pass
@abstractmethod
def get_statistics(self, operation: str) -> PerformanceStats:
pass
@abstractmethod
def check_sla(self, operation: str, threshold: float) -> bool:
pass
@abstractmethod
def get_bottlenecks(self) -> List[Tuple[str, float]]:
pass
```
## Component Description
Tracks processing times, ensures <5s constraint per frame.
## API Methods
### `start_timer(operation: str) -> str`
**Description**: Starts timing an operation.
**Returns**: timer_id (UUID)
---
### `end_timer(timer_id: str) -> float`
**Description**: Ends timer and records duration.
**Returns**: Duration in seconds
---
### `get_statistics(operation: str) -> PerformanceStats`
**Description**: Gets statistics for an operation.
**Output**:
```python
PerformanceStats:
operation: str
count: int
mean: float
p50: float
p95: float
p99: float
max: float
```
---
### `check_sla(operation: str, threshold: float) -> bool`
**Description**: Checks if operation meets SLA threshold.
**Example**: check_sla("frame_processing", 5.0) → True if < 5s
---
### `get_bottlenecks() -> List[Tuple[str, float]]`
**Description**: Returns slowest operations.
## Dependencies
**External**: time, statistics
## Test Cases
1. Start/end timer → records duration
2. Get statistics → returns percentiles
3. Check SLA → returns True if meeting targets
@@ -0,0 +1,94 @@
# Web Mercator Utils Helper
## Interface Definition
**Interface Name**: `IWebMercatorUtils`
### Interface Methods
```python
class IWebMercatorUtils(ABC):
@abstractmethod
def latlon_to_tile(self, lat: float, lon: float, zoom: int) -> Tuple[int, int]:
pass
@abstractmethod
def tile_to_latlon(self, x: int, y: int, zoom: int) -> Tuple[float, float]:
pass
@abstractmethod
def compute_tile_bounds(self, x: int, y: int, zoom: int) -> TileBounds:
pass
@abstractmethod
def get_zoom_gsd(self, lat: float, zoom: int) -> float:
pass
```
## Component Description
Web Mercator projection (EPSG:3857) for tile coordinates. Used for Google Maps tiles.
## API Methods
### `latlon_to_tile(lat: float, lon: float, zoom: int) -> Tuple[int, int]`
**Description**: Converts GPS to tile coordinates.
**Formula**:
```
n = 2^zoom
x = floor((lon + 180) / 360 * n)
lat_rad = lat * π / 180
y = floor((1 - log(tan(lat_rad) + sec(lat_rad)) / π) / 2 * n)
```
**Returns**: (x, y) tile coordinates
---
### `tile_to_latlon(x: int, y: int, zoom: int) -> Tuple[float, float]`
**Description**: Converts tile coordinates to GPS (NW corner).
**Formula** (inverse of above)
---
### `compute_tile_bounds(x: int, y: int, zoom: int) -> TileBounds`
**Description**: Computes GPS bounding box of tile.
**Returns**:
```python
TileBounds:
nw: GPSPoint # North-West
ne: GPSPoint # North-East
sw: GPSPoint # South-West
se: GPSPoint # South-East
center: GPSPoint
gsd: float
```
---
### `get_zoom_gsd(lat: float, zoom: int) -> float`
**Description**: Gets GSD for zoom level at latitude.
**Formula**:
```
gsd = 156543.03392 * cos(lat * π/180) / 2^zoom
```
## Dependencies
**External**: numpy
## Test Cases
1. GPS to tile at zoom 19 → valid tile coords
2. Tile to GPS → inverse correct
3. Compute bounds → 4 corners valid
4. GSD at zoom 19, Ukraine → ~0.3 m/pixel
@@ -0,0 +1,92 @@
# Image Rotation Utils Helper
## Interface Definition
**Interface Name**: `IImageRotationUtils`
### Interface Methods
```python
class IImageRotationUtils(ABC):
@abstractmethod
def rotate_image(self, image: np.ndarray, angle: float, center: Optional[Tuple[int, int]] = None) -> np.ndarray:
pass
@abstractmethod
def calculate_rotation_from_points(self, src_points: np.ndarray, dst_points: np.ndarray) -> float:
pass
@abstractmethod
def normalize_angle(self, angle: float) -> float:
pass
@abstractmethod
def compute_rotation_matrix(self, angle: float, center: Tuple[int, int]) -> np.ndarray:
pass
```
## Component Description
Image rotation operations, angle calculations from point shifts.
## API Methods
### `rotate_image(image: np.ndarray, angle: float, center: Optional[Tuple[int, int]] = None) -> np.ndarray`
**Description**: Rotates image around center.
**Implementation**: Uses cv2.getRotationMatrix2D + cv2.warpAffine
**Parameters**:
- **angle**: Degrees (0-360)
- **center**: Rotation center (default: image center)
**Returns**: Rotated image (same dimensions)
---
### `calculate_rotation_from_points(src_points: np.ndarray, dst_points: np.ndarray) -> float`
**Description**: Calculates rotation angle from point correspondences.
**Input**: (N, 2) arrays of matching points
**Algorithm**:
1. Compute centroids
2. Calculate angle from centroid shifts
3. Return angle in degrees
**Use Case**: Extract precise angle from LiteSAM homography
---
### `normalize_angle(angle: float) -> float`
**Description**: Normalizes angle to 0-360 range.
**Formula**:
```
angle = angle % 360
if angle < 0:
angle += 360
```
---
### `compute_rotation_matrix(angle: float, center: Tuple[int, int]) -> np.ndarray`
**Description**: Computes 2D rotation matrix.
**Returns**: 2×3 affine transformation matrix
## Dependencies
**External**: opencv-python, numpy
## Test Cases
1. Rotate 90° → image rotated correctly
2. Calculate angle from points → accurate angle
3. Normalize 370° → 10°
4. Rotation matrix → correct transformation
@@ -0,0 +1,329 @@
# Batch Validator Helper
## Interface Definition
**Interface Name**: `IBatchValidator`
### Interface Methods
```python
class IBatchValidator(ABC):
@abstractmethod
def validate_batch_size(self, batch: ImageBatch) -> ValidationResult:
pass
@abstractmethod
def check_sequence_continuity(self, batch: ImageBatch, expected_start: int) -> ValidationResult:
pass
@abstractmethod
def validate_naming_convention(self, filenames: List[str]) -> ValidationResult:
pass
@abstractmethod
def validate_format(self, image_data: bytes) -> ValidationResult:
pass
```
## Component Description
### Responsibilities
- Validate image batch integrity
- Check sequence continuity and naming conventions
- Validate image format and dimensions
- Ensure batch size constraints (10-50 images)
- Support strict sequential ordering (ADxxxxxx.jpg)
### Scope
- Batch validation for G05 Image Input Pipeline
- Image format validation
- Filename pattern matching
- Sequence gap detection
## API Methods
### `validate_batch_size(batch: ImageBatch) -> ValidationResult`
**Description**: Validates batch contains 10-50 images.
**Called By**:
- G05 Image Input Pipeline (before queuing)
**Input**:
```python
batch: ImageBatch:
images: List[bytes]
filenames: List[str]
start_sequence: int
end_sequence: int
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
- **Minimum batch size**: 10 images
- **Maximum batch size**: 50 images
- **Reason**: Balance between upload overhead and processing granularity
**Error Conditions**:
- Returns `valid=False` with error message (not an exception)
**Test Cases**:
1. **Valid batch (20 images)**: Returns `valid=True`
2. **Too few images (5)**: Returns `valid=False`, error="Batch size 5 below minimum 10"
3. **Too many images (60)**: Returns `valid=False`, error="Batch size 60 exceeds maximum 50"
4. **Empty batch**: Returns `valid=False`
---
### `check_sequence_continuity(batch: ImageBatch, expected_start: int) -> ValidationResult`
**Description**: Validates images form consecutive sequence with no gaps.
**Called By**:
- G05 Image Input Pipeline (before queuing)
**Input**:
```python
batch: ImageBatch
expected_start: int # Expected starting sequence number
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
1. **Sequence starts at expected_start**: First image sequence == expected_start
2. **Consecutive numbers**: No gaps in sequence (AD000101, AD000102, AD000103, ...)
3. **Filename extraction**: Parse sequence from ADxxxxxx.jpg pattern
4. **Strict ordering**: Images must be in sequential order
**Algorithm**:
```python
sequences = [extract_sequence(filename) for filename in batch.filenames]
if sequences[0] != expected_start:
return invalid("Expected start {expected_start}, got {sequences[0]}")
for i in range(len(sequences) - 1):
if sequences[i+1] != sequences[i] + 1:
return invalid(f"Gap detected: {sequences[i]} -> {sequences[i+1]}")
return valid()
```
**Error Conditions**:
- Returns `valid=False` with specific gap information
**Test Cases**:
1. **Valid sequence (101-150)**: expected_start=101 → valid=True
2. **Wrong start**: expected_start=101, got 102 → valid=False
3. **Gap in sequence**: AD000101, AD000103 (missing 102) → valid=False
4. **Out of order**: AD000102, AD000101 → valid=False
---
### `validate_naming_convention(filenames: List[str]) -> ValidationResult`
**Description**: Validates filenames match ADxxxxxx.jpg pattern.
**Called By**:
- Internal (during check_sequence_continuity)
- G05 Image Input Pipeline
**Input**:
```python
filenames: List[str]
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
1. **Pattern**: `AD\d{6}\.(jpg|JPG|png|PNG)`
2. **Examples**: AD000001.jpg, AD000237.JPG, AD002000.png
3. **Case insensitive**: Accepts .jpg, .JPG, .Jpg
4. **6 digits required**: Zero-padded to 6 digits
**Regex Pattern**: `^AD\d{6}\.(jpg|JPG|png|PNG)$`
**Error Conditions**:
- Returns `valid=False` listing invalid filenames
**Test Cases**:
1. **Valid names**: ["AD000001.jpg", "AD000002.jpg"] → valid=True
2. **Invalid prefix**: "IMG_0001.jpg" → valid=False
3. **Wrong digit count**: "AD001.jpg" (3 digits) → valid=False
4. **Missing extension**: "AD000001" → valid=False
5. **Invalid extension**: "AD000001.bmp" → valid=False
---
### `validate_format(image_data: bytes) -> ValidationResult`
**Description**: Validates image file format and properties.
**Called By**:
- G05 Image Input Pipeline (per-image validation)
**Input**:
```python
image_data: bytes # Raw image file bytes
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
1. **Format**: Valid JPEG or PNG
2. **Dimensions**: 640×480 to 6252×4168 pixels
3. **File size**: < 10MB per image
4. **Image readable**: Not corrupted
5. **Color channels**: RGB (3 channels)
**Algorithm**:
```python
try:
image = PIL.Image.open(BytesIO(image_data))
width, height = image.size
if image.format not in ['JPEG', 'PNG']:
return invalid("Format must be JPEG or PNG")
if width < 640 or height < 480:
return invalid("Dimensions too small")
if width > 6252 or height > 4168:
return invalid("Dimensions too large")
if len(image_data) > 10 * 1024 * 1024:
return invalid("File size exceeds 10MB")
return valid()
except Exception as e:
return invalid(f"Corrupted image: {e}")
```
**Error Conditions**:
- Returns `valid=False` with specific error
**Test Cases**:
1. **Valid JPEG (2048×1536)**: valid=True
2. **Valid PNG (6252×4168)**: valid=True
3. **Too small (320×240)**: valid=False
4. **Too large (8000×6000)**: valid=False
5. **File too big (15MB)**: valid=False
6. **Corrupted file**: valid=False
7. **BMP format**: valid=False
## Integration Tests
### Test 1: Complete Batch Validation
1. Create batch with 20 images, AD000101.jpg - AD000120.jpg
2. validate_batch_size() → valid
3. validate_naming_convention() → valid
4. check_sequence_continuity(expected_start=101) → valid
5. validate_format() for each image → all valid
### Test 2: Invalid Batch Detection
1. Create batch with 60 images → validate_batch_size() fails
2. Create batch with gap (AD000101, AD000103) → check_sequence_continuity() fails
3. Create batch with IMG_0001.jpg → validate_naming_convention() fails
4. Create batch with corrupted image → validate_format() fails
### Test 3: Edge Cases
1. Batch with exactly 10 images → valid
2. Batch with exactly 50 images → valid
3. Batch with 51 images → invalid
4. Batch starting at AD999995.jpg (near max) → valid
## Non-Functional Requirements
### Performance
- **validate_batch_size**: < 1ms
- **check_sequence_continuity**: < 10ms for 50 images
- **validate_naming_convention**: < 5ms for 50 filenames
- **validate_format**: < 20ms per image (with PIL)
- **Total batch validation**: < 100ms for 50 images
### Reliability
- Never raises exceptions (returns ValidationResult with errors)
- Handles edge cases gracefully
- Clear, actionable error messages
### Maintainability
- Configurable validation rules (min/max batch size, dimensions)
- Easy to add new validation rules
- Comprehensive error reporting
## Dependencies
### Internal Components
- None (pure utility, no internal dependencies)
### External Dependencies
- **Pillow (PIL)**: Image format validation and dimension checking
- **re** (regex): Filename pattern matching
## Data Models
### ImageBatch
```python
class ImageBatch(BaseModel):
images: List[bytes] # Raw image data
filenames: List[str] # e.g., ["AD000101.jpg", ...]
start_sequence: int # 101
end_sequence: int # 150
batch_number: int # Sequential batch number
```
### ValidationResult
```python
class ValidationResult(BaseModel):
valid: bool
errors: List[str] = [] # Empty if valid
warnings: List[str] = [] # Optional warnings
```
### ValidationRules (Configuration)
```python
class ValidationRules(BaseModel):
min_batch_size: int = 10
max_batch_size: int = 50
min_width: int = 640
min_height: int = 480
max_width: int = 6252
max_height: int = 4168
max_file_size_mb: int = 10
allowed_formats: List[str] = ["JPEG", "PNG"]
filename_pattern: str = r"^AD\d{6}\.(jpg|JPG|png|PNG)$"
```
### Sequence Extraction
```python
def extract_sequence(filename: str) -> int:
"""
Extracts sequence number from filename.
Example: "AD000237.jpg" -> 237
"""
match = re.match(r"AD(\d{6})\.", filename)
if match:
return int(match.group(1))
raise ValueError(f"Invalid filename format: {filename}")
```
@@ -0,0 +1,289 @@
# Route REST API
## Interface Definition
**Interface Name**: `IRouteRestAPI`
### Interface Methods
```python
class IRouteRestAPI(ABC):
@abstractmethod
def create_route(self, route_data: RouteCreateRequest) -> RouteResponse:
pass
@abstractmethod
def get_route(self, route_id: str) -> RouteResponse:
pass
@abstractmethod
def update_waypoints(self, route_id: str, waypoints: List[Waypoint]) -> UpdateResponse:
pass
@abstractmethod
def delete_route(self, route_id: str) -> DeleteResponse:
pass
```
## Component Description
### Responsibilities
- Expose REST API endpoints for route lifecycle management
- Handle HTTP request validation and routing
- Coordinate with Route Data Manager for persistence operations
- Validate incoming requests through Waypoint Validator
- Return appropriate HTTP responses with proper status codes
### Scope
- CRUD operations for routes
- Waypoint management within routes
- Geofence management
- Route metadata retrieval
- Used by both GPS-Denied system and Mission Planner
## API Methods
### `create_route(route_data: RouteCreateRequest) -> RouteResponse`
**Description**: Creates a new route with initial waypoints and geofences.
**Called By**:
- Client applications (GPS-Denied UI, Mission Planner UI)
**Input**:
```python
RouteCreateRequest:
id: Optional[str] # UUID, generated if not provided
name: str
description: str
points: List[GPSPoint] # Initial rough waypoints
geofences: Geofences
```
**Output**:
```python
RouteResponse:
route_id: str
created: bool
timestamp: datetime
```
**Error Conditions**:
- `400 Bad Request`: Invalid input data (missing required fields, invalid GPS coordinates)
- `409 Conflict`: Route with same ID already exists
- `500 Internal Server Error`: Database or internal error
**Test Cases**:
1. **Valid route creation**: Provide valid route data → returns 201 with routeId
2. **Missing required field**: Omit name → returns 400 with error message
3. **Invalid GPS coordinates**: Provide lat > 90 → returns 400
4. **Duplicate route ID**: Create route with existing ID → returns 409
---
### `get_route(route_id: str) -> RouteResponse`
**Description**: Retrieves complete route information including all waypoints and geofences.
**Called By**:
- Client applications
- G03 Route API Client (from GPS-Denied system)
**Input**:
```python
route_id: str # UUID
```
**Output**:
```python
RouteResponse:
route_id: str
name: str
description: str
points: List[Waypoint] # All waypoints with metadata
geofences: Geofences
created_at: datetime
updated_at: datetime
```
**Error Conditions**:
- `404 Not Found`: Route ID does not exist
- `500 Internal Server Error`: Database error
**Test Cases**:
1. **Existing route**: Valid routeId → returns 200 with complete route data
2. **Non-existent route**: Invalid routeId → returns 404
3. **Route with many waypoints**: Route with 2000+ waypoints → returns 200 with all data
---
### `update_waypoints(route_id: str, waypoint_id: str, waypoint_data: Waypoint) -> UpdateResponse`
**Description**: Updates a specific waypoint within a route. Used for per-frame GPS refinement from GPS-Denied system.
**Called By**:
- G03 Route API Client (per-frame updates)
- Client applications (manual corrections)
**Input**:
```python
route_id: str
waypoint_id: str # Frame sequence number or waypoint ID
waypoint_data: Waypoint:
lat: float
lon: float
altitude: Optional[float]
confidence: float
timestamp: datetime
refined: bool # True if updated by GPS-Denied refinement
```
**Output**:
```python
UpdateResponse:
updated: bool
waypoint_id: str
```
**Error Conditions**:
- `404 Not Found`: Route or waypoint not found
- `400 Bad Request`: Invalid waypoint data
- `500 Internal Server Error`: Database error
**Test Cases**:
1. **Update existing waypoint**: Valid data → returns 200
2. **Refinement update**: GPS-Denied sends refined coordinates → updates successfully
3. **Invalid coordinates**: lat > 90 → returns 400
4. **Non-existent waypoint**: Invalid waypoint_id → returns 404
---
### `delete_route(route_id: str) -> DeleteResponse`
**Description**: Deletes a route and all associated waypoints.
**Called By**:
- Client applications
**Input**:
```python
route_id: str
```
**Output**:
```python
DeleteResponse:
deleted: bool
route_id: str
```
**Error Conditions**:
- `404 Not Found`: Route does not exist
- `500 Internal Server Error`: Database error
**Test Cases**:
1. **Delete existing route**: Valid routeId → returns 200
2. **Delete non-existent route**: Invalid routeId → returns 404
3. **Delete route with active flight**: Route linked to processing flight → returns 200 (cascade handling in DB)
## Integration Tests
### Test 1: Route Creation and Retrieval Flow
1. POST `/routes` with valid data
2. Verify 201 response with routeId
3. GET `/routes/{routeId}`
4. Verify returned data matches created data
### Test 2: GPS-Denied Integration Flow
1. Create route via POST
2. Simulate GPS-Denied per-frame updates via PUT `/routes/{routeId}/waypoints/{waypointId}` × 100
3. GET route and verify all waypoints updated
4. Verify `refined: true` flag set
### Test 3: Concurrent Waypoint Updates
1. Create route
2. Send 50 concurrent PUT requests to different waypoints
3. Verify all updates succeed
4. Verify data consistency
## Non-Functional Requirements
### Performance
- **Create route**: < 500ms response time
- **Get route**: < 200ms for routes with < 2000 waypoints
- **Update waypoint**: < 100ms (critical for GPS-Denied real-time updates)
- **Delete route**: < 300ms
- **Throughput**: Handle 100 concurrent waypoint updates per second
### Scalability
- Support 1000+ concurrent route processing sessions
- Handle routes with up to 3000 waypoints
### Availability
- 99.9% uptime SLA
- Graceful degradation under load
### Security
- Input validation on all endpoints
- SQL injection prevention
- Rate limiting: 1000 requests/minute per client
## Dependencies
### Internal Components
- **R02 Route Data Manager**: For all data persistence operations
- **R03 Waypoint Validator**: For input validation
- **R04 Route Database Layer**: Indirectly through Data Manager
### External Dependencies
- **FastAPI/Flask**: Web framework
- **Pydantic**: Request/response validation
- **Uvicorn**: ASGI server
## Data Models
### RouteCreateRequest
```python
class GPSPoint(BaseModel):
lat: float # Latitude -90 to 90
lon: float # Longitude -180 to 180
class Polygon(BaseModel):
north_west: GPSPoint
south_east: GPSPoint
class Geofences(BaseModel):
polygons: List[Polygon]
class RouteCreateRequest(BaseModel):
id: Optional[str] = None # UUID
name: str
description: str
points: List[GPSPoint] # Initial rough waypoints
geofences: Geofences
```
### Waypoint
```python
class Waypoint(BaseModel):
id: str # Sequence number or UUID
lat: float
lon: float
altitude: Optional[float] = None
confidence: float # 0.0 to 1.0
timestamp: datetime
refined: bool = False
```
### RouteResponse
```python
class RouteResponse(BaseModel):
route_id: str
name: str
description: str
points: List[Waypoint]
geofences: Geofences
created_at: datetime
updated_at: datetime
```
@@ -0,0 +1,338 @@
# Route Data Manager
## Interface Definition
**Interface Name**: `IRouteDataManager`
### Interface Methods
```python
class IRouteDataManager(ABC):
@abstractmethod
def save_route(self, route: Route) -> str:
pass
@abstractmethod
def load_route(self, route_id: str) -> Optional[Route]:
pass
@abstractmethod
def update_waypoint(self, route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool:
pass
@abstractmethod
def delete_waypoint(self, route_id: str, waypoint_id: str) -> bool:
pass
@abstractmethod
def get_route_metadata(self, route_id: str) -> Optional[RouteMetadata]:
pass
@abstractmethod
def delete_route(self, route_id: str) -> bool:
pass
```
## Component Description
### Responsibilities
- Manage route persistence and retrieval
- Coordinate with Route Database Layer for data operations
- Handle waypoint CRUD operations within routes
- Manage route metadata (timestamps, statistics)
- Ensure data consistency and transaction management
### Scope
- Business logic layer between REST API and Database Layer
- Route lifecycle management
- Waypoint batch operations
- Query optimization for large route datasets
- Caching layer for frequently accessed routes (optional)
## API Methods
### `save_route(route: Route) -> str`
**Description**: Persists a new route with initial waypoints and geofences.
**Called By**:
- R01 Route REST API
**Input**:
```python
Route:
id: Optional[str] # Generated if not provided
name: str
description: str
points: List[Waypoint]
geofences: Geofences
```
**Output**:
```python
route_id: str # UUID of saved route
```
**Error Conditions**:
- `DuplicateRouteError`: Route with same ID exists
- `ValidationError`: Invalid route data
- `DatabaseError`: Database connection or constraint violation
**Test Cases**:
1. **New route**: Valid route → returns routeId, verifies in DB
2. **Route with 1000 waypoints**: Large route → saves successfully
3. **Duplicate ID**: Existing route ID → raises DuplicateRouteError
4. **Transaction rollback**: DB error mid-save → no partial data persisted
---
### `load_route(route_id: str) -> Optional[Route]`
**Description**: Retrieves complete route data including all waypoints.
**Called By**:
- R01 Route REST API
- R03 Waypoint Validator (for context validation)
**Input**:
```python
route_id: str
```
**Output**:
```python
Route or None if not found
```
**Error Conditions**:
- `DatabaseError`: Database connection error
- Returns `None`: Route not found (not an error condition)
**Test Cases**:
1. **Existing route**: Valid ID → returns complete Route object
2. **Non-existent route**: Invalid ID → returns None
3. **Large route**: 3000 waypoints → returns all data efficiently
4. **Concurrent reads**: Multiple simultaneous loads → all succeed
---
### `update_waypoint(route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool`
**Description**: Updates a single waypoint within a route. Optimized for high-frequency GPS-Denied updates.
**Called By**:
- R01 Route REST API
**Input**:
```python
route_id: str
waypoint_id: str
waypoint: Waypoint
```
**Output**:
```python
bool: True if updated, False if route/waypoint not found
```
**Error Conditions**:
- `ValidationError`: Invalid waypoint data
- `DatabaseError`: Database error
**Test Cases**:
1. **Update existing waypoint**: Valid data → returns True
2. **Non-existent waypoint**: Invalid waypoint_id → returns False
3. **Concurrent updates**: 100 simultaneous updates to different waypoints → all succeed
4. **Update timestamp**: Automatically updates route.updated_at
---
### `delete_waypoint(route_id: str, waypoint_id: str) -> bool`
**Description**: Deletes a specific waypoint from a route.
**Called By**:
- R01 Route REST API (rare, for manual corrections)
**Input**:
```python
route_id: str
waypoint_id: str
```
**Output**:
```python
bool: True if deleted, False if not found
```
**Error Conditions**:
- `DatabaseError`: Database error
**Test Cases**:
1. **Delete existing waypoint**: Valid IDs → returns True
2. **Delete non-existent waypoint**: Invalid ID → returns False
3. **Delete all waypoints**: Delete all waypoints one by one → succeeds
---
### `get_route_metadata(route_id: str) -> Optional[RouteMetadata]`
**Description**: Retrieves route metadata without loading all waypoints (lightweight operation).
**Called By**:
- R01 Route REST API
- Client applications (route listing)
**Input**:
```python
route_id: str
```
**Output**:
```python
RouteMetadata:
route_id: str
name: str
description: str
waypoint_count: int
created_at: datetime
updated_at: datetime
```
**Error Conditions**:
- Returns `None`: Route not found
**Test Cases**:
1. **Get metadata**: Valid ID → returns metadata without waypoints
2. **Performance**: Metadata retrieval < 50ms even for 3000-waypoint route
---
### `delete_route(route_id: str) -> bool`
**Description**: Deletes a route and all associated waypoints.
**Called By**:
- R01 Route REST API
**Input**:
```python
route_id: str
```
**Output**:
```python
bool: True if deleted, False if not found
```
**Error Conditions**:
- `DatabaseError`: Database error
**Test Cases**:
1. **Delete route**: Valid ID → deletes route and all waypoints
2. **Cascade delete**: Verify all waypoints deleted
3. **Non-existent route**: Invalid ID → returns False
## Integration Tests
### Test 1: Complete Route Lifecycle
1. save_route() with 100 waypoints
2. load_route() and verify all data
3. update_waypoint() for 50 waypoints
4. delete_waypoint() for 10 waypoints
5. get_route_metadata() and verify count
6. delete_route() and verify removal
### Test 2: High-Frequency Update Simulation (GPS-Denied Pattern)
1. save_route() with 2000 waypoints
2. Simulate per-frame updates: update_waypoint() × 2000 in sequence
3. Verify all updates persisted correctly
4. Measure total time < 200 seconds (100ms per update)
### Test 3: Concurrent Route Operations
1. Create 10 routes concurrently
2. Update different waypoints in parallel (100 concurrent updates)
3. Delete 5 routes concurrently while updating others
4. Verify data consistency
## Non-Functional Requirements
### Performance
- **save_route**: < 300ms for routes with 100 waypoints
- **load_route**: < 150ms for routes with 2000 waypoints
- **update_waypoint**: < 50ms (critical path for GPS-Denied)
- **get_route_metadata**: < 30ms
- **delete_route**: < 200ms
### Scalability
- Support 1000+ concurrent route operations
- Handle routes with up to 3000 waypoints efficiently
- Optimize for read-heavy workload (90% reads, 10% writes)
### Reliability
- ACID transaction guarantees
- Automatic retry on transient database errors (3 attempts)
- Data validation before persistence
### Maintainability
- Clear separation from database implementation
- Support for future caching layer integration
- Comprehensive error handling and logging
## Dependencies
### Internal Components
- **R03 Waypoint Validator**: Validates waypoints before persistence
- **R04 Route Database Layer**: For all database operations
### External Dependencies
- None (pure business logic layer)
## Data Models
### Route
```python
class Route(BaseModel):
id: str # UUID
name: str
description: str
points: List[Waypoint]
geofences: Geofences
created_at: datetime
updated_at: datetime
```
### RouteMetadata
```python
class RouteMetadata(BaseModel):
route_id: str
name: str
description: str
waypoint_count: int
geofence_count: int
created_at: datetime
updated_at: datetime
```
### Waypoint
```python
class Waypoint(BaseModel):
id: str
lat: float
lon: float
altitude: Optional[float]
confidence: float
timestamp: datetime
refined: bool
```
### Geofences
```python
class Polygon(BaseModel):
north_west: GPSPoint
south_east: GPSPoint
class Geofences(BaseModel):
polygons: List[Polygon]
```
@@ -0,0 +1,294 @@
# Waypoint Validator
## Interface Definition
**Interface Name**: `IWaypointValidator`
### Interface Methods
```python
class IWaypointValidator(ABC):
@abstractmethod
def validate_waypoint(self, waypoint: Waypoint) -> ValidationResult:
pass
@abstractmethod
def validate_geofence(self, geofence: Geofences) -> ValidationResult:
pass
@abstractmethod
def check_bounds(self, waypoint: Waypoint, geofences: Geofences) -> bool:
pass
@abstractmethod
def validate_route_continuity(self, waypoints: List[Waypoint]) -> ValidationResult:
pass
```
## Component Description
### Responsibilities
- Validate individual waypoint data (GPS coordinates, altitude, confidence)
- Validate geofence definitions (polygon bounds, topology)
- Check waypoints against geofence boundaries
- Validate route continuity (detect large gaps, validate sequencing)
- Provide detailed validation error messages
### Scope
- Input validation for Route API
- Business rule enforcement (operational area restrictions for Ukraine)
- Geospatial boundary checking
- Data quality assurance
## API Methods
### `validate_waypoint(waypoint: Waypoint) -> ValidationResult`
**Description**: Validates a single waypoint's data integrity and constraints.
**Called By**:
- R01 Route REST API (before creating/updating)
- R02 Route Data Manager (pre-persistence validation)
**Input**:
```python
Waypoint:
id: str
lat: float
lon: float
altitude: Optional[float]
confidence: float
timestamp: datetime
refined: bool
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
1. **Latitude**: -90.0 <= lat <= 90.0
2. **Longitude**: -180.0 <= lon <= 180.0
3. **Altitude**: 0 <= altitude <= 1000 meters (if provided)
4. **Confidence**: 0.0 <= confidence <= 1.0
5. **Timestamp**: Not in future, not older than 1 year
6. **Operational area** (Ukraine restriction): Latitude ~45-52N, Longitude ~22-40E
7. **ID**: Non-empty string
**Error Conditions**:
- Returns `ValidationResult` with `valid=False` and error list (not an exception)
**Test Cases**:
1. **Valid waypoint**: All fields correct → returns `valid=True`
2. **Invalid latitude**: lat=100 → returns `valid=False`, error="Latitude out of range"
3. **Invalid longitude**: lon=200 → returns `valid=False`
4. **Invalid confidence**: confidence=1.5 → returns `valid=False`
5. **Future timestamp**: timestamp=tomorrow → returns `valid=False`
6. **Outside operational area**: lat=10 (not Ukraine) → returns `valid=False`
7. **Valid altitude**: altitude=500 → returns `valid=True`
8. **Invalid altitude**: altitude=1500 → returns `valid=False`
---
### `validate_geofence(geofence: Geofences) -> ValidationResult`
**Description**: Validates geofence polygon definitions.
**Called By**:
- R01 Route REST API (during route creation)
**Input**:
```python
Geofences:
polygons: List[Polygon]
Polygon:
north_west: GPSPoint
south_east: GPSPoint
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
```
**Validation Rules**:
1. **North-West corner**: NW.lat > SE.lat
2. **North-West corner**: NW.lon < SE.lon (for Eastern Ukraine)
3. **Polygon size**: Max 500km × 500km
4. **Polygon count**: 1 <= len(polygons) <= 10
5. **No self-intersection**: Polygons should not overlap
6. **Within operational area**: All corners within Ukraine bounds
**Error Conditions**:
- Returns `ValidationResult` with validation errors
**Test Cases**:
1. **Valid geofence**: Single polygon in Ukraine → valid=True
2. **Invalid corners**: NW.lat < SE.lat → valid=False
3. **Too large**: 600km × 600km → valid=False
4. **Too many polygons**: 15 polygons → valid=False
5. **Overlapping polygons**: Two overlapping → valid=False (warning)
---
### `check_bounds(waypoint: Waypoint, geofences: Geofences) -> bool`
**Description**: Checks if a waypoint falls within geofence boundaries.
**Called By**:
- R01 Route REST API (optional check during waypoint updates)
- R02 Route Data Manager (business rule enforcement)
**Input**:
```python
waypoint: Waypoint
geofences: Geofences
```
**Output**:
```python
bool: True if waypoint is within any geofence polygon
```
**Algorithm**:
- Point-in-polygon test for each geofence polygon
- Returns True if point is inside at least one polygon
**Error Conditions**:
- None (returns False if outside all geofences)
**Test Cases**:
1. **Inside geofence**: Waypoint in polygon center → returns True
2. **Outside geofence**: Waypoint 10km outside → returns False
3. **On boundary**: Waypoint on polygon edge → returns True
4. **Multiple geofences**: Waypoint in second polygon → returns True
---
### `validate_route_continuity(waypoints: List[Waypoint]) -> ValidationResult`
**Description**: Validates route continuity, detecting large gaps and sequence issues.
**Called By**:
- R01 Route REST API (during route creation)
- R02 Route Data Manager (route quality check)
**Input**:
```python
waypoints: List[Waypoint] # Should be ordered by sequence/timestamp
```
**Output**:
```python
ValidationResult:
valid: bool
errors: List[str]
warnings: List[str]
```
**Validation Rules**:
1. **Minimum waypoints**: len(waypoints) >= 2
2. **Maximum waypoints**: len(waypoints) <= 3000
3. **Timestamp ordering**: waypoints[i].timestamp < waypoints[i+1].timestamp
4. **Distance gaps**: Consecutive waypoints < 500 meters apart (warning if violated)
5. **Large gap detection**: Flag gaps > 1km (warning for potential data loss)
6. **No duplicate timestamps**: All timestamps unique
**Error Conditions**:
- Returns `ValidationResult` with errors and warnings
**Test Cases**:
1. **Valid route**: 100 waypoints, 100m spacing → valid=True
2. **Too few waypoints**: 1 waypoint → valid=False
3. **Too many waypoints**: 3500 waypoints → valid=False
4. **Unordered timestamps**: waypoints out of order → valid=False
5. **Large gap**: 2km gap between waypoints → valid=True with warning
6. **Duplicate timestamps**: Two waypoints same time → valid=False
## Integration Tests
### Test 1: Complete Validation Pipeline
1. Create waypoint with all valid data
2. validate_waypoint() → passes
3. Create geofence for Eastern Ukraine
4. validate_geofence() → passes
5. check_bounds() → waypoint inside geofence
### Test 2: Route Validation Flow
1. Create 500 waypoints with 100m spacing
2. validate_route_continuity() → passes
3. Add waypoint 2km away
4. validate_route_continuity() → passes with warning
5. Add waypoint with past timestamp
6. validate_route_continuity() → fails
### Test 3: Edge Cases
1. Waypoint on geofence boundary
2. Waypoint at North Pole (lat=90)
3. Waypoint at dateline (lon=180)
4. Route with exactly 3000 waypoints
## Non-Functional Requirements
### Performance
- **validate_waypoint**: < 1ms per waypoint
- **validate_geofence**: < 10ms per geofence
- **check_bounds**: < 2ms per check
- **validate_route_continuity**: < 100ms for 2000 waypoints
### Accuracy
- GPS coordinate validation: 6 decimal places precision (0.1m)
- Geofence boundary check: 1-meter precision
### Maintainability
- Validation rules configurable via configuration file
- Easy to add new validation rules
- Clear error messages for debugging
## Dependencies
### Internal Components
- **R04 Route Database Layer**: For loading existing route data (optional context)
- **H06 Web Mercator Utils**: For distance calculations (optional)
### External Dependencies
- **Shapely** (optional): For advanced polygon operations
- **Geopy**: For geodesic distance calculations
## Data Models
### ValidationResult
```python
class ValidationResult(BaseModel):
valid: bool
errors: List[str] = []
warnings: List[str] = []
```
### OperationalArea (Configuration)
```python
class OperationalArea(BaseModel):
name: str = "Eastern Ukraine"
min_lat: float = 45.0
max_lat: float = 52.0
min_lon: float = 22.0
max_lon: float = 40.0
```
### ValidationRules (Configuration)
```python
class ValidationRules(BaseModel):
max_altitude: float = 1000.0 # meters
max_waypoint_gap: float = 500.0 # meters
max_route_waypoints: int = 3000
min_route_waypoints: int = 2
max_geofence_size: float = 500000.0 # meters (500km)
max_geofences: int = 10
```
@@ -0,0 +1,475 @@
# Route Database Layer
## Interface Definition
**Interface Name**: `IRouteDatabase`
### Interface Methods
```python
class IRouteDatabase(ABC):
@abstractmethod
def insert_route(self, route: Route) -> str:
pass
@abstractmethod
def update_route(self, route: Route) -> bool:
pass
@abstractmethod
def query_routes(self, filters: Dict[str, Any], limit: int, offset: int) -> List[Route]:
pass
@abstractmethod
def get_route_by_id(self, route_id: str) -> Optional[Route]:
pass
@abstractmethod
def get_waypoints(self, route_id: str, limit: Optional[int] = None) -> List[Waypoint]:
pass
@abstractmethod
def insert_waypoint(self, route_id: str, waypoint: Waypoint) -> str:
pass
@abstractmethod
def update_waypoint(self, route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool:
pass
@abstractmethod
def delete_route(self, route_id: str) -> bool:
pass
```
## Component Description
### Responsibilities
- Direct database access layer for route data
- Execute SQL queries and commands
- Manage database connections and transactions
- Handle connection pooling and retry logic
- Provide database abstraction for potential migration (PostgreSQL, MySQL, etc.)
### Scope
- CRUD operations on routes table
- CRUD operations on waypoints table
- CRUD operations on geofences table
- Query optimization for large datasets
- Database schema management
- Separate schema from GPS-Denied API database
## API Methods
### `insert_route(route: Route) -> str`
**Description**: Inserts a new route with initial waypoints and geofences into the database.
**Called By**:
- R02 Route Data Manager
**Input**:
```python
Route:
id: str
name: str
description: str
points: List[Waypoint]
geofences: Geofences
created_at: datetime
updated_at: datetime
```
**Output**:
```python
route_id: str # Inserted route ID
```
**Database Operations**:
1. Begin transaction
2. INSERT INTO routes (id, name, description, created_at, updated_at)
3. INSERT INTO waypoints (route_id, ...) for each waypoint
4. INSERT INTO geofences (route_id, ...) for each polygon
5. Commit transaction
**Error Conditions**:
- `IntegrityError`: Duplicate route_id (unique constraint violation)
- `DatabaseError`: Connection error, transaction failure
- Automatic rollback on any error
**Test Cases**:
1. **Insert route with 100 waypoints**: Successful insertion, all waypoints persisted
2. **Duplicate route_id**: Raises IntegrityError
3. **Transaction rollback**: Error on waypoint insertion → route also rolled back
4. **Connection loss**: Mid-transaction error → graceful rollback
---
### `update_route(route: Route) -> bool`
**Description**: Updates route metadata (name, description, updated_at).
**Called By**:
- R02 Route Data Manager
**Input**:
```python
Route with updated fields
```
**Output**:
```python
bool: True if updated, False if route not found
```
**Database Operations**:
```sql
UPDATE routes
SET name = ?, description = ?, updated_at = ?
WHERE id = ?
```
**Error Conditions**:
- `DatabaseError`: Connection or query error
**Test Cases**:
1. **Update existing route**: Returns True
2. **Update non-existent route**: Returns False
3. **Update with same data**: Succeeds, updates timestamp
---
### `query_routes(filters: Dict[str, Any], limit: int, offset: int) -> List[Route]`
**Description**: Queries routes with filtering, pagination for route listing.
**Called By**:
- R02 Route Data Manager
- R01 Route REST API (list endpoints)
**Input**:
```python
filters: Dict[str, Any] # e.g., {"name": "Mission%", "created_after": datetime}
limit: int # Max results
offset: int # For pagination
```
**Output**:
```python
List[Route] # Routes without full waypoint data (metadata only)
```
**Database Operations**:
```sql
SELECT * FROM routes
WHERE name LIKE ? AND created_at > ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
```
**Error Conditions**:
- `DatabaseError`: Query error
**Test Cases**:
1. **Filter by name**: Returns matching routes
2. **Pagination**: offset=100, limit=50 → returns routes 100-149
3. **Empty result**: No matches → returns []
4. **No filters**: Returns all routes (with limit)
---
### `get_route_by_id(route_id: str) -> Optional[Route]`
**Description**: Retrieves complete route with all waypoints by ID.
**Called By**:
- R02 Route Data Manager
**Input**:
```python
route_id: str
```
**Output**:
```python
Optional[Route] # Complete route with all waypoints, or None
```
**Database Operations**:
1. SELECT FROM routes WHERE id = ?
2. SELECT FROM waypoints WHERE route_id = ? ORDER BY timestamp
3. SELECT FROM geofences WHERE route_id = ?
4. Assemble Route object
**Error Conditions**:
- `DatabaseError`: Query error
- Returns None if route not found
**Test Cases**:
1. **Existing route**: Returns complete Route object
2. **Non-existent route**: Returns None
3. **Large route (3000 waypoints)**: Returns all data within 150ms
4. **Route with no waypoints**: Returns route with empty points list
---
### `get_waypoints(route_id: str, limit: Optional[int] = None) -> List[Waypoint]`
**Description**: Retrieves waypoints for a route, optionally limited.
**Called By**:
- R02 Route Data Manager
**Input**:
```python
route_id: str
limit: Optional[int] # For pagination or preview
```
**Output**:
```python
List[Waypoint]
```
**Database Operations**:
```sql
SELECT * FROM waypoints
WHERE route_id = ?
ORDER BY timestamp ASC
LIMIT ? -- if limit provided
```
**Error Conditions**:
- `DatabaseError`: Query error
**Test Cases**:
1. **All waypoints**: limit=None → returns all
2. **Limited waypoints**: limit=100 → returns first 100
3. **No waypoints**: Empty list
---
### `insert_waypoint(route_id: str, waypoint: Waypoint) -> str`
**Description**: Inserts a new waypoint into a route.
**Called By**:
- R02 Route Data Manager
**Input**:
```python
route_id: str
waypoint: Waypoint
```
**Output**:
```python
waypoint_id: str
```
**Database Operations**:
```sql
INSERT INTO waypoints (id, route_id, lat, lon, altitude, confidence, timestamp, refined)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
```
**Error Conditions**:
- `ForeignKeyError`: route_id doesn't exist
- `IntegrityError`: Duplicate waypoint_id
**Test Cases**:
1. **Valid insertion**: Returns waypoint_id
2. **Non-existent route**: Raises ForeignKeyError
---
### `update_waypoint(route_id: str, waypoint_id: str, waypoint: Waypoint) -> bool`
**Description**: Updates a waypoint. Critical path for GPS-Denied per-frame updates.
**Called By**:
- R02 Route Data Manager
**Input**:
```python
route_id: str
waypoint_id: str
waypoint: Waypoint
```
**Output**:
```python
bool: True if updated, False if not found
```
**Database Operations**:
```sql
UPDATE waypoints
SET lat = ?, lon = ?, altitude = ?, confidence = ?, refined = ?
WHERE id = ? AND route_id = ?
```
**Optimization**:
- Prepared statement caching
- Connection pooling for high throughput
- Indexed on (route_id, id) for fast lookups
**Error Conditions**:
- `DatabaseError`: Query error
**Test Cases**:
1. **Update existing waypoint**: Returns True, updates data
2. **Non-existent waypoint**: Returns False
3. **High-frequency updates**: 100 updates/sec sustained for 20 seconds
---
### `delete_route(route_id: str) -> bool`
**Description**: Deletes a route and cascades to waypoints and geofences.
**Called By**:
- R02 Route Data Manager
**Input**:
```python
route_id: str
```
**Output**:
```python
bool: True if deleted, False if not found
```
**Database Operations**:
```sql
DELETE FROM routes WHERE id = ?
-- Cascade deletes from waypoints and geofences via FK constraints
```
**Error Conditions**:
- `DatabaseError`: Query error
**Test Cases**:
1. **Delete route with waypoints**: Deletes route and all waypoints
2. **Verify cascade**: Check waypoints table empty for route_id
3. **Non-existent route**: Returns False
## Integration Tests
### Test 1: Complete Database Lifecycle
1. insert_route() with 500 waypoints
2. get_route_by_id() and verify all data
3. update_waypoint() × 100
4. query_routes() with filters
5. delete_route() and verify removal
### Test 2: High-Frequency Update Pattern (GPS-Denied Simulation)
1. insert_route() with 2000 waypoints
2. update_waypoint() × 2000 sequentially
3. Measure total time and throughput
4. Verify all updates persisted correctly
### Test 3: Concurrent Access
1. Insert 10 routes concurrently
2. Update waypoints in parallel (100 concurrent connections)
3. Query routes while updates occurring
4. Verify no deadlocks or data corruption
### Test 4: Transaction Integrity
1. Begin insert_route() transaction
2. Simulate error mid-waypoint insertion
3. Verify complete rollback (no partial data)
## Non-Functional Requirements
### Performance
- **insert_route**: < 200ms for 100 waypoints
- **update_waypoint**: < 30ms (critical path)
- **get_route_by_id**: < 100ms for 2000 waypoints
- **query_routes**: < 150ms for pagination queries
- **Throughput**: 200+ waypoint updates per second
### Scalability
- Connection pool: 50-100 connections
- Support 1000+ concurrent operations
- Handle tables with millions of waypoints
### Reliability
- ACID transaction guarantees
- Automatic retry on transient errors (3 attempts with exponential backoff)
- Connection health checks
- Graceful degradation on connection pool exhaustion
### Security
- SQL injection prevention (parameterized queries only)
- Principle of least privilege (database user permissions)
- Connection string encryption
## Dependencies
### Internal Components
- None (lowest layer)
### External Dependencies
- **PostgreSQL** or **MySQL**: Relational database
- **SQLAlchemy** or **psycopg2**: Database driver
- **Alembic**: Schema migration tool
## Data Models
### Database Schema
```sql
-- Routes table
CREATE TABLE routes (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at),
INDEX idx_name (name)
);
-- Waypoints table
CREATE TABLE waypoints (
id VARCHAR(36) PRIMARY KEY,
route_id VARCHAR(36) NOT NULL,
lat DECIMAL(10, 7) NOT NULL,
lon DECIMAL(11, 7) NOT NULL,
altitude DECIMAL(7, 2),
confidence DECIMAL(3, 2) NOT NULL,
timestamp TIMESTAMP NOT NULL,
refined BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
INDEX idx_route_timestamp (route_id, timestamp),
INDEX idx_route_id (route_id, id)
);
-- Geofences table
CREATE TABLE geofences (
id VARCHAR(36) PRIMARY KEY,
route_id VARCHAR(36) NOT NULL,
nw_lat DECIMAL(10, 7) NOT NULL,
nw_lon DECIMAL(11, 7) NOT NULL,
se_lat DECIMAL(10, 7) NOT NULL,
se_lon DECIMAL(11, 7) NOT NULL,
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
INDEX idx_route_id (route_id)
);
```
### Connection Configuration
```python
class DatabaseConfig(BaseModel):
host: str
port: int
database: str
username: str
password: str
pool_size: int = 50
max_overflow: int = 50
pool_timeout: int = 30
pool_recycle: int = 3600
```
+7
View File
@@ -92,6 +92,13 @@
### Revise ### Revise
- Revise the plan, answer questions, put detailed descriptions - Revise the plan, answer questions, put detailed descriptions
- Make sure stored components are coherent and make sense - Make sure stored components are coherent and make sense
- Ask AI
```
analyze carefully interaction between components. All covered? also, state that each component should implement interface, so that they could be interchangeable with different implementations
```
### Store plan
save plan to `docs/02_components/00_decomposition_plan.md`
## 2.2 **🤖📋AI plan**: Generate tests ## 2.2 **🤖📋AI plan**: Generate tests