fix(lint): resolve all ruff errors — trailing whitespace, E501, F401

- ruff --fix: removed trailing whitespace (W293), sorted imports (I001)
- Manual: broke long lines (E501) in eskf, rotation, vo, gpr, metric, pipeline, rotation tests
- Removed unused imports (F401) in models.py, schemas/__init__.py
- pyproject.toml: line-length 100→120, E501 ignore for abstract interfaces

ruff check: 0 errors. pytest: 195 passed / 8 skipped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yuzviak
2026-04-02 17:09:47 +03:00
parent 094895b21b
commit dd9835c0cd
53 changed files with 395 additions and 374 deletions
+9 -8
View File
@@ -8,19 +8,19 @@ Scenarios tested:
"""
import time
from unittest.mock import AsyncMock, MagicMock
import numpy as np
import pytest
from unittest.mock import MagicMock, AsyncMock
from gps_denied.core.processor import FlightProcessor, TrackingState
from gps_denied.core.models import ModelManager
from gps_denied.core.vo import SequentialVisualOdometry
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.metric import MetricRefinement
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.core.chunk_manager import RouteChunkManager
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.core.metric import MetricRefinement
from gps_denied.core.models import ModelManager
from gps_denied.core.processor import FlightProcessor, TrackingState
from gps_denied.core.recovery import FailureRecoveryCoordinator
from gps_denied.core.vo import SequentialVisualOdometry
from gps_denied.schemas.graph import FactorGraphConfig
@@ -148,9 +148,10 @@ async def test_ac4_user_anchor_fix(wired_processor):
Verify that add_absolute_factor with is_user_anchor=True is accepted
by the graph and the trajectory incorporates the anchor.
"""
from datetime import datetime
from gps_denied.schemas import GPSPoint
from gps_denied.schemas.graph import Pose
from datetime import datetime
flight = "ac4_anchor"
graph = wired_processor._graph
+2 -5
View File
@@ -27,12 +27,10 @@ from gps_denied.core.benchmark import (
BenchmarkResult,
SyntheticTrajectory,
SyntheticTrajectoryConfig,
TrajectoryFrame,
)
from gps_denied.schemas import GPSPoint
from gps_denied.schemas.eskf import ESKFConfig
ORIGIN = GPSPoint(lat=49.0, lon=32.0)
@@ -255,7 +253,6 @@ def test_vo_drift_under_100m_over_1km():
def test_covariance_shrinks_after_satellite_update():
"""AC-PERF-6: ESKF position covariance trace decreases after satellite correction."""
from gps_denied.core.eskf import ESKF
from gps_denied.schemas.eskf import ESKFConfig
eskf = ESKF(ESKFConfig())
eskf.initialize(np.zeros(3), time.time())
@@ -278,7 +275,7 @@ def test_covariance_shrinks_after_satellite_update():
def test_confidence_high_after_fresh_satellite():
"""AC-PERF-5: HIGH confidence when satellite correction is recent + covariance small."""
from gps_denied.core.eskf import ESKF
from gps_denied.schemas.eskf import ConfidenceTier, ESKFConfig, IMUMeasurement
from gps_denied.schemas.eskf import ConfidenceTier
cfg = ESKFConfig(satellite_max_age=30.0, covariance_high_threshold=400.0)
eskf = ESKF(cfg)
@@ -296,7 +293,7 @@ def test_confidence_high_after_fresh_satellite():
def test_confidence_medium_after_vo_only():
"""AC-PERF-5: MEDIUM confidence when only VO is available (no satellite)."""
from gps_denied.core.eskf import ESKF
from gps_denied.schemas.eskf import ConfidenceTier, ESKFConfig
from gps_denied.schemas.eskf import ConfidenceTier
eskf = ESKF(ESKFConfig())
eskf.initialize(np.zeros(3), time.time())
+2 -2
View File
@@ -18,7 +18,7 @@ async def override_get_session():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def _get_session():
async with async_session() as session:
yield session
@@ -93,7 +93,7 @@ async def test_upload_image_batch(client: AsyncClient):
"batch_number": 1
}
files = [("images", ("test1.jpg", b"dummy", "image/jpeg")) for _ in range(10)]
resp2 = await client.post(
f"/flights/{fid}/images/batch",
data={"metadata": json.dumps(meta)},
+14 -13
View File
@@ -1,14 +1,15 @@
"""Tests for Route Chunk Manager (F12)."""
import pytest
import numpy as np
import pytest
from gps_denied.core.chunk_manager import RouteChunkManager
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.schemas.graph import FactorGraphConfig
from gps_denied.schemas.chunk import ChunkStatus
from gps_denied.schemas.graph import FactorGraphConfig
from gps_denied.schemas.metric import Sim3Transform
@pytest.fixture
def chunk_manager():
opt = FactorGraphOptimizer(FactorGraphConfig())
@@ -17,20 +18,20 @@ def chunk_manager():
def test_create_and_get_chunk(chunk_manager):
flight_id = "flight123"
chunk = chunk_manager.create_new_chunk(flight_id, 0)
assert chunk.is_active is True
assert chunk.start_frame_id == 0
assert chunk.flight_id == flight_id
active = chunk_manager.get_active_chunk(flight_id)
assert active.chunk_id == chunk.chunk_id
def test_add_frame_to_chunk(chunk_manager):
flight = "fl2"
chunk_manager.create_new_chunk(flight, 100)
assert chunk_manager.add_frame_to_chunk(flight, 101) is True
active = chunk_manager.get_active_chunk(flight)
assert 101 in active.frames
assert len(active.frames) == 2
@@ -39,21 +40,21 @@ def test_chunk_merging_logic(chunk_manager):
flight = "fl3"
c1 = chunk_manager.create_new_chunk(flight, 0)
c1_id = c1.chunk_id
c2 = chunk_manager.create_new_chunk(flight, 50)
c2_id = c2.chunk_id
chunk_manager.add_frame_to_chunk(flight, 51)
# c2 is active now, c1 is inactive
assert chunk_manager.get_active_chunk(flight).chunk_id == c2_id
transform = Sim3Transform(translation=np.zeros(3), rotation=np.eye(3), scale=1)
# merge c2 into c1
res = chunk_manager.merge_chunks(flight, c2_id, c1_id, transform)
assert res is True
# c2 frames moved to c1
assert 50 in c1.frames
assert len(c2.frames) == 0
+6 -6
View File
@@ -21,11 +21,11 @@ def transformer():
def test_enu_origin_management(transformer):
fid = "flight_123"
origin = GPSPoint(lat=48.0, lon=37.0)
# Before setting
with pytest.raises(OriginNotSetError):
transformer.get_enu_origin(fid)
# After setting
transformer.set_enu_origin(fid, origin)
assert transformer.get_enu_origin(fid).lat == 48.0
@@ -35,11 +35,11 @@ def test_gps_to_enu(transformer):
fid = "flight_123"
origin = GPSPoint(lat=48.0, lon=37.0)
transformer.set_enu_origin(fid, origin)
# Same point -> 0, 0, 0
enu = transformer.gps_to_enu(fid, origin)
assert enu == (0.0, 0.0, 0.0)
# Point north
target = GPSPoint(lat=48.01, lon=37.0)
enu_n = transformer.gps_to_enu(fid, target)
@@ -52,10 +52,10 @@ def test_enu_roundtrip(transformer):
fid = "flight_123"
origin = GPSPoint(lat=48.0, lon=37.0)
transformer.set_enu_origin(fid, origin)
test_gps = GPSPoint(lat=48.056, lon=37.123)
enu = transformer.gps_to_enu(fid, test_gps)
recovered = transformer.enu_to_gps(fid, enu)
assert pytest.approx(recovered.lat, abs=1e-6) == test_gps.lat
assert pytest.approx(recovered.lon, abs=1e-6) == test_gps.lon
+1 -1
View File
@@ -206,7 +206,7 @@ async def test_chunks(repo: FlightRepository, session: AsyncSession):
flight = await repo.insert_flight(
name="CK", description="", start_lat=0, start_lon=0, altitude=100, camera_params=CAM
)
chunk = await repo.save_chunk(
await repo.save_chunk(
flight.id,
chunk_id="chunk_001",
start_frame_id=1,
+17 -8
View File
@@ -18,7 +18,7 @@ def gpr():
def test_compute_location_descriptor(gpr):
img = np.zeros((200, 200, 3), dtype=np.uint8)
desc = gpr.compute_location_descriptor(img)
assert desc.shape == (4096,)
# Should be L2 normalized
assert np.isclose(np.linalg.norm(desc), 1.0)
@@ -26,7 +26,7 @@ def test_compute_location_descriptor(gpr):
def test_retrieve_candidate_tiles(gpr):
img = np.zeros((200, 200, 3), dtype=np.uint8)
candidates = gpr.retrieve_candidate_tiles(img, top_k=5)
assert len(candidates) == 5
for c in candidates:
assert isinstance(c, TileCandidate)
@@ -47,8 +47,8 @@ def test_retrieve_candidate_tiles_for_chunk(gpr):
def test_load_index_missing_file_falls_back(tmp_path):
"""GPR-01: non-existent index path → numpy fallback, still usable."""
from gps_denied.core.models import ModelManager
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.models import ModelManager
g = GlobalPlaceRecognition(ModelManager())
ok = g.load_index("f1", str(tmp_path / "nonexistent.index"))
@@ -62,8 +62,8 @@ def test_load_index_missing_file_falls_back(tmp_path):
def test_load_index_not_loaded_returns_empty():
"""query_database before load_index → empty list (no crash)."""
from gps_denied.core.models import ModelManager
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.models import ModelManager
g = GlobalPlaceRecognition(ModelManager())
desc = np.random.rand(4096).astype(np.float32)
@@ -77,8 +77,8 @@ def test_load_index_not_loaded_returns_empty():
def test_rank_candidates_sorted(gpr):
"""rank_candidates must return descending similarity order."""
from gps_denied.schemas.gpr import TileCandidate
from gps_denied.schemas import GPSPoint
from gps_denied.schemas.gpr import TileCandidate
from gps_denied.schemas.satellite import TileBounds
dummy_bounds = TileBounds(
@@ -87,9 +87,18 @@ def test_rank_candidates_sorted(gpr):
center=GPSPoint(lat=49.05, lon=32.05), gsd=0.6,
)
cands = [
TileCandidate(tile_id="a", gps_center=GPSPoint(lat=49, lon=32), bounds=dummy_bounds, similarity_score=0.3, rank=3),
TileCandidate(tile_id="b", gps_center=GPSPoint(lat=49, lon=32), bounds=dummy_bounds, similarity_score=0.9, rank=1),
TileCandidate(tile_id="c", gps_center=GPSPoint(lat=49, lon=32), bounds=dummy_bounds, similarity_score=0.6, rank=2),
TileCandidate(
tile_id="a", gps_center=GPSPoint(lat=49, lon=32),
bounds=dummy_bounds, similarity_score=0.3, rank=3,
),
TileCandidate(
tile_id="b", gps_center=GPSPoint(lat=49, lon=32),
bounds=dummy_bounds, similarity_score=0.9, rank=1,
),
TileCandidate(
tile_id="c", gps_center=GPSPoint(lat=49, lon=32),
bounds=dummy_bounds, similarity_score=0.6, rank=2,
),
]
ranked = gpr.rank_candidates(cands)
scores = [c.similarity_score for c in ranked]
+24 -23
View File
@@ -6,8 +6,8 @@ import pytest
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.schemas import GPSPoint
from gps_denied.schemas.graph import FactorGraphConfig
from gps_denied.schemas.vo import RelativePose
from gps_denied.schemas.metric import Sim3Transform
from gps_denied.schemas.vo import RelativePose
@pytest.fixture
@@ -18,13 +18,13 @@ def optimizer():
def test_add_relative_factor(optimizer):
flight_id = "test_f_1"
# Needs initial node, using chunk creation wrapper to mock initial
optimizer.create_chunk_subgraph(flight_id, "ch_1", start_frame_id=0)
# Mock relative pose logic: in mock graph logic it's added via "add_relative_factor_to_chunk"
# Mock relative pose logic: in mock graph logic it's added via "add_relative_factor_to_chunk"
# OR we need an initial root via add_absolute/relative. Our mock for `add_relative` is simple.
rel1 = RelativePose(
translation=np.array([1.0, 0.0, 0.0]),
rotation=np.eye(3),
@@ -34,14 +34,14 @@ def test_add_relative_factor(optimizer):
total_matches=120,
tracking_good=True
)
# Test global logic requires frame exist, let's inject a zero frame
optimizer._init_flight(flight_id)
optimizer._flights_state[flight_id]["poses"][0] = optimizer._chunks_state["ch_1"]["poses"][0]
res = optimizer.add_relative_factor(flight_id, 0, 1, rel1, np.eye(6))
assert res is True
traj = optimizer.get_trajectory(flight_id)
assert 1 in traj
assert np.allclose(traj[1].position, [1.0, 0.0, 0.0])
@@ -49,49 +49,50 @@ def test_add_relative_factor(optimizer):
def test_add_absolute_factor(optimizer):
flight_id = "test_f_2"
optimizer._init_flight(flight_id)
# Inject frame 0 and frame 1
from gps_denied.schemas.graph import Pose
from datetime import datetime
from gps_denied.schemas.graph import Pose
optimizer._flights_state[flight_id]["poses"][0] = Pose(
frame_id=0, position=np.zeros(3), orientation=np.eye(3), timestamp=datetime.now()
)
optimizer._flights_state[flight_id]["poses"][1] = Pose(
frame_id=1, position=np.zeros(3), orientation=np.eye(3), timestamp=datetime.now()
)
# First GPS sets the ENU origin → position becomes [0,0,0]
origin_gps = GPSPoint(lat=49.0, lon=30.0)
optimizer.add_absolute_factor(flight_id, 0, origin_gps, np.eye(2), is_user_anchor=True)
assert np.allclose(optimizer.get_trajectory(flight_id)[0].position, [0, 0, 0])
# Second GPS at different coords → should produce non-zero ENU displacement
gps2 = GPSPoint(lat=49.1, lon=30.1)
res = optimizer.add_absolute_factor(flight_id, 1, gps2, np.eye(2), is_user_anchor=True)
assert res is True
traj = optimizer.get_trajectory(flight_id)
assert traj[1].position[1] > 1000 # ~11 km north
def test_optimize_and_retrieve(optimizer):
flight_id = "test_f_3"
optimizer._init_flight(flight_id)
res = optimizer.optimize(flight_id, 5)
assert res.converged is True
assert res.iterations_used == 5
traj = optimizer.get_trajectory(flight_id)
assert isinstance(traj, dict)
def test_chunk_merging(optimizer):
flight_id = "test_f_4"
c1 = "chunk1"
c2 = "chunk2"
assert optimizer.create_chunk_subgraph(flight_id, c1, 0) is True
assert optimizer.create_chunk_subgraph(flight_id, c2, 10) is True
rel = RelativePose(
translation=np.array([5.0, 0, 0]),
rotation=np.eye(3),
@@ -102,17 +103,17 @@ def test_chunk_merging(optimizer):
tracking_good=True
)
optimizer.add_relative_factor_to_chunk(flight_id, c2, 10, 11, rel, np.eye(6))
sim3 = Sim3Transform(translation=np.array([100.0, 0, 0]), rotation=np.eye(3), scale=1.0)
res = optimizer.merge_chunk_subgraphs(flight_id, c2, c1, sim3)
assert res is True
c1_poses = optimizer.get_chunk_trajectory(flight_id, c1)
# 10 and 11 should now be in c1, transformed!
assert 10 in c1_poses
assert 11 in c1_poses
# 100 translation
assert c1_poses[10].position[0] == 100.0
assert c1_poses[11].position[0] == 105.0
-1
View File
@@ -25,7 +25,6 @@ from gps_denied.schemas import GPSPoint
from gps_denied.schemas.eskf import ConfidenceTier, ESKFState
from gps_denied.schemas.mavlink import GPSInputMessage, RelocalizationRequest
# ---------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------
+9 -5
View File
@@ -33,7 +33,7 @@ def test_extract_gps_from_alignment(metric, bounds):
# The image is 256x256 in our mock
# Center pixel is 128, 128
gps = metric.extract_gps_from_alignment(H, bounds, (128, 128))
# 128 is middle -> should be EXACTLY at 49.5 lat and 32.5 lon
assert np.isclose(gps.lat, 49.5)
assert np.isclose(gps.lon, 32.5)
@@ -41,7 +41,11 @@ def test_extract_gps_from_alignment(metric, bounds):
def test_align_to_satellite(metric, bounds, monkeypatch):
def mock_infer(*args, **kwargs):
H = np.eye(3, dtype=np.float64)
return {"homography": H, "inlier_count": 80, "total_correspondences": 100, "confidence": 0.8, "reprojection_error": 1.0}
return {
"homography": H, "inlier_count": 80,
"total_correspondences": 100, "confidence": 0.8,
"reprojection_error": 1.0,
}
engine = metric.model_manager.get_inference_engine("LiteSAM")
monkeypatch.setattr(engine, "infer", mock_infer)
@@ -107,13 +111,13 @@ def test_align_chunk_to_satellite(metric, bounds, monkeypatch):
def mock_infer(*args, **kwargs):
H = np.eye(3, dtype=np.float64)
return {"homography": H, "inlier_count": 80, "confidence": 0.8}
engine = metric.model_manager.get_inference_engine("LiteSAM")
monkeypatch.setattr(engine, "infer", mock_infer)
uavs = [np.zeros((256, 256, 3)) for _ in range(5)]
sat = np.zeros((256, 256, 3))
res = metric.align_chunk_to_satellite(uavs, sat, bounds)
assert res is not None
assert isinstance(res, ChunkAlignmentResult)
+10 -8
View File
@@ -1,15 +1,17 @@
"""Tests for Model Manager (F16)."""
import numpy as np
from gps_denied.core.models import ModelManager
def test_load_and_get_model():
manager = ModelManager()
# Should auto-load
engine = manager.get_inference_engine("SuperPoint")
assert engine.model_name == "SuperPoint"
# Check fallback/dummy
assert manager.fallback_to_onnx("SuperPoint") is True
assert manager.optimize_to_tensorrt("SuperPoint", "path.onnx") == "path.onnx.trt"
@@ -18,10 +20,10 @@ def test_load_and_get_model():
def test_mock_superpoint():
manager = ModelManager()
engine = manager.get_inference_engine("SuperPoint")
dummy_img = np.zeros((480, 640, 3), dtype=np.uint8)
res = engine.infer(dummy_img)
assert "keypoints" in res
assert "descriptors" in res
assert "scores" in res
@@ -32,17 +34,17 @@ def test_mock_superpoint():
def test_mock_lightglue():
manager = ModelManager()
engine = manager.get_inference_engine("LightGlue")
# Need mock features
class DummyF:
def __init__(self, keypoints):
self.keypoints = keypoints
f1 = DummyF(np.random.rand(120, 2))
f2 = DummyF(np.random.rand(150, 2))
res = engine.infer({"features1": f1, "features2": f2})
assert "matches" in res
assert len(res["matches"]) == 100 # min(100, 120, 150)
assert res["keypoints1"].shape == (100, 2)
+14 -12
View File
@@ -1,12 +1,11 @@
"""Tests for Image Input Pipeline (F05)."""
import asyncio
import cv2
import numpy as np
import pytest
from gps_denied.core.pipeline import ImageInputPipeline, QueueFullError, ValidationError
from gps_denied.core.pipeline import ImageInputPipeline, QueueFullError
from gps_denied.schemas.image import ImageBatch
@@ -30,7 +29,10 @@ def test_batch_validation(pipeline):
assert val1.valid, f"Single-image batch should be valid; errors: {val1.errors}"
# 2-image batch — also valid under new rule
b2 = ImageBatch(images=[b"1", b"2"], filenames=["AD000001.jpg", "AD000002.jpg"], start_sequence=1, end_sequence=2, batch_number=1)
b2 = ImageBatch(
images=[b"1", b"2"], filenames=["AD000001.jpg", "AD000002.jpg"],
start_sequence=1, end_sequence=2, batch_number=1,
)
val2 = pipeline.validate_batch(b2)
assert val2.valid
@@ -45,35 +47,35 @@ def test_batch_validation(pipeline):
@pytest.mark.asyncio
async def test_queue_and_process(pipeline):
flight_id = "test_f1"
# Create valid fake images
fake_img_np = np.zeros((10, 10, 3), dtype=np.uint8)
_, encoded = cv2.imencode(".jpg", fake_img_np)
fake_bytes = encoded.tobytes()
fake_imgs = [fake_bytes] * 10
fake_names = [f"AD{i:06d}.jpg" for i in range(1, 11)]
b = ImageBatch(images=fake_imgs, filenames=fake_names, start_sequence=1, end_sequence=10, batch_number=1)
pipeline.queue_batch(flight_id, b)
# Process
processed = await pipeline.process_next_batch(flight_id)
assert processed is not None
assert len(processed.images) == 10
assert processed.images[0].sequence == 1
assert processed.images[-1].sequence == 10
# Status
st = pipeline.get_processing_status(flight_id)
assert st.total_images == 10
assert st.processed_images == 10
# Sequential get
next_img = pipeline.get_next_image(flight_id)
assert next_img is not None
assert next_img.sequence == 1
# Second get
next_img2 = pipeline.get_next_image(flight_id)
assert next_img2 is not None
@@ -109,9 +111,9 @@ def test_queue_full(pipeline):
fake_imgs = [b"fake"] * 10
fake_names = [f"AD{i:06d}.jpg" for i in range(1, 11)]
b = ImageBatch(images=fake_imgs, filenames=fake_names, start_sequence=1, end_sequence=10, batch_number=1)
pipeline.queue_batch(flight_id, b)
pipeline.queue_batch(flight_id, b)
with pytest.raises(QueueFullError):
pipeline.queue_batch(flight_id, b)
+10 -9
View File
@@ -1,17 +1,18 @@
"""Tests for FlightProcessor full processing pipeline (Stage 10)."""
import pytest
import numpy as np
from unittest.mock import MagicMock, AsyncMock
from unittest.mock import AsyncMock, MagicMock
import numpy as np
import pytest
from gps_denied.core.processor import FlightProcessor, TrackingState
from gps_denied.core.models import ModelManager
from gps_denied.core.vo import SequentialVisualOdometry
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.metric import MetricRefinement
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.core.chunk_manager import RouteChunkManager
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.core.metric import MetricRefinement
from gps_denied.core.models import ModelManager
from gps_denied.core.processor import FlightProcessor, TrackingState
from gps_denied.core.recovery import FailureRecoveryCoordinator
from gps_denied.core.vo import SequentialVisualOdometry
from gps_denied.schemas.graph import FactorGraphConfig
+8 -11
View File
@@ -9,20 +9,17 @@ PIPE-07: ESKF state pushed to MAVLinkBridge on every frame.
PIPE-08: ImageRotationManager accepts optional model_manager arg.
"""
import time
from unittest.mock import AsyncMock, MagicMock
import numpy as np
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from gps_denied.core.processor import FlightProcessor, TrackingState
from gps_denied.core.eskf import ESKF
from gps_denied.core.rotation import ImageRotationManager
from gps_denied.core.coordinates import CoordinateTransformer
from gps_denied.schemas import GPSPoint, CameraParameters
from gps_denied.schemas.eskf import ConfidenceTier, ESKFConfig
from gps_denied.core.processor import FlightProcessor, TrackingState
from gps_denied.core.rotation import ImageRotationManager
from gps_denied.schemas import CameraParameters, GPSPoint
from gps_denied.schemas.vo import RelativePose
# ---------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------
@@ -122,9 +119,9 @@ async def test_eskf_vo_update_called_on_good_tracking():
mock_vo.compute_relative_pose.return_value = good_pose
proc._vo = mock_vo
eskf_before_pos = proc._eskf[flight]._nominal_state["position"].copy()
proc._eskf[flight]._nominal_state["position"].copy()
await proc.process_frame(flight, 1, img1)
eskf_after_pos = proc._eskf[flight]._nominal_state["position"].copy()
proc._eskf[flight]._nominal_state["position"].copy()
# ESKF position should have changed due to VO update
assert mock_vo.compute_relative_pose.called
@@ -287,8 +284,8 @@ async def test_convert_object_to_gps_fallback_without_coord():
@pytest.mark.asyncio
async def test_create_flight_initialises_eskf():
"""create_flight should seed ESKF for the new flight."""
from gps_denied.schemas.flight import FlightCreateRequest
from gps_denied.schemas import Geofences
from gps_denied.schemas.flight import FlightCreateRequest
proc, _, _ = _make_processor()
+14 -13
View File
@@ -1,32 +1,33 @@
"""Tests for Failure Recovery Coordinator (F11)."""
import pytest
import numpy as np
import pytest
from gps_denied.core.recovery import FailureRecoveryCoordinator
from gps_denied.core.chunk_manager import RouteChunkManager
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.core.gpr import GlobalPlaceRecognition
from gps_denied.core.graph import FactorGraphOptimizer
from gps_denied.core.metric import MetricRefinement
from gps_denied.core.models import ModelManager
from gps_denied.core.recovery import FailureRecoveryCoordinator
from gps_denied.schemas.graph import FactorGraphConfig
@pytest.fixture
def recovery():
opt = FactorGraphOptimizer(FactorGraphConfig())
cm = RouteChunkManager(opt)
mm = ModelManager()
gpr = GlobalPlaceRecognition(mm)
gpr.load_index("test", "test")
metric = MetricRefinement(mm)
return FailureRecoveryCoordinator(cm, gpr, metric)
def test_handle_tracking_lost(recovery):
flight = "recovery_fl"
res = recovery.handle_tracking_lost(flight, 404)
assert res is True
# active chunk should be 404
active = recovery.chunk_manager.get_active_chunk(flight)
@@ -35,28 +36,28 @@ def test_handle_tracking_lost(recovery):
def test_process_chunk_recovery_success(recovery, monkeypatch):
# Mock LitSAM to guarantee match
def mock_align(*args, **kwargs):
from gps_denied.schemas.metric import ChunkAlignmentResult, Sim3Transform
from gps_denied.schemas import GPSPoint
from gps_denied.schemas.metric import ChunkAlignmentResult, Sim3Transform
return ChunkAlignmentResult(
matched=True, chunk_id="x", chunk_center_gps=GPSPoint(lat=49, lon=30),
rotation_angle=0, confidence=0.9, inlier_count=50,
transform=Sim3Transform(translation=np.zeros(3), rotation=np.eye(3), scale=1),
reprojection_error=1.0
)
monkeypatch.setattr(recovery.metric_refinement, "align_chunk_to_satellite", mock_align)
flight = "rec2"
recovery.handle_tracking_lost(flight, 10)
chunk_id = recovery.chunk_manager.get_active_chunk(flight).chunk_id
images = [np.zeros((256, 256, 3)) for _ in range(3)]
res = recovery.process_chunk_recovery(flight, chunk_id, images)
assert res is True
from gps_denied.schemas.chunk import ChunkStatus
active = recovery.chunk_manager.get_active_chunk(flight)
assert active.has_anchor is True
assert active.matching_status == ChunkStatus.ANCHORED
+19 -16
View File
@@ -6,9 +6,9 @@ import numpy as np
import pytest
from gps_denied.core.rotation import IImageMatcher, ImageRotationManager
from gps_denied.schemas import GPSPoint
from gps_denied.schemas.rotation import RotationResult
from gps_denied.schemas.satellite import TileBounds
from gps_denied.schemas import GPSPoint
@pytest.fixture
@@ -18,13 +18,13 @@ def rotation_manager():
def test_rotate_image_360(rotation_manager):
img = np.zeros((100, 100, 3), dtype=np.uint8)
# Just draw a white rectangle to test rotation
img[10:40, 10:40] = [255, 255, 255]
r90 = rotation_manager.rotate_image_360(img, 90.0)
assert r90.shape == (100, 100, 3)
# Top left corner should move
assert not np.array_equal(img, r90)
@@ -32,12 +32,12 @@ def test_rotate_image_360(rotation_manager):
def test_heading_management(rotation_manager):
fid = "flight_1"
now = datetime.now(timezone.utc)
assert rotation_manager.get_current_heading(fid) is None
rotation_manager.update_heading(fid, 1, 370.0, now) # should modulo to 10
assert rotation_manager.get_current_heading(fid) == 10.0
rotation_manager.update_heading(fid, 2, 90.0, now)
assert rotation_manager.get_current_heading(fid) == 90.0
@@ -45,23 +45,26 @@ def test_heading_management(rotation_manager):
def test_detect_sharp_turn(rotation_manager):
fid = "flight_2"
now = datetime.now(timezone.utc)
assert rotation_manager.detect_sharp_turn(fid, 90.0) is False # no history
rotation_manager.update_heading(fid, 1, 90.0, now)
assert rotation_manager.detect_sharp_turn(fid, 100.0) is False # delta 10
assert rotation_manager.detect_sharp_turn(fid, 180.0) is True # delta 90
assert rotation_manager.detect_sharp_turn(fid, 350.0) is True # delta 100
assert rotation_manager.detect_sharp_turn(fid, 80.0) is False # delta 10 (wraparound)
# Wraparound test explicitly
rotation_manager.update_heading(fid, 2, 350.0, now)
assert rotation_manager.detect_sharp_turn(fid, 10.0) is False # delta 20
class MockMatcher(IImageMatcher):
def align_to_satellite(self, uav_image: np.ndarray, satellite_tile: np.ndarray, tile_bounds: TileBounds) -> RotationResult:
def align_to_satellite(
self, uav_image: np.ndarray, satellite_tile: np.ndarray,
tile_bounds: TileBounds,
) -> RotationResult:
# Mock that only matches when angle was originally 90
# By checking internal state or just returning generic true for test
return RotationResult(matched=True, initial_angle=90.0, precise_angle=90.0, confidence=0.99)
@@ -71,16 +74,16 @@ def test_try_rotation_steps(rotation_manager):
fid = "flight_3"
img = np.zeros((10, 10, 3), dtype=np.uint8)
sat = np.zeros((10, 10, 3), dtype=np.uint8)
nw = GPSPoint(lat=10.0, lon=10.0)
se = GPSPoint(lat=9.0, lon=11.0)
tb = TileBounds(nw=nw, ne=nw, sw=se, se=se, center=nw, gsd=0.5)
matcher = MockMatcher()
# Should perform sweep and mock matcher says matched=True immediately in the loop
res = rotation_manager.try_rotation_steps(fid, 1, img, sat, tb, datetime.now(timezone.utc), matcher)
assert res is not None
assert res.matched is True
# The first step is 0 degrees, the mock matcher returns matched=True.
+1 -2
View File
@@ -7,7 +7,6 @@ from gps_denied.core.satellite import SatelliteDataManager
from gps_denied.schemas import GPSPoint
from gps_denied.utils import mercator
# ---------------------------------------------------------------
# Mercator utils
# ---------------------------------------------------------------
@@ -123,7 +122,7 @@ def test_assemble_mosaic_single(satellite_manager):
def test_assemble_mosaic_2x2(satellite_manager):
"""2×2 tile grid assembles into a single mosaic."""
base = mercator.TileCoords(x=10, y=10, zoom=15)
mercator.TileCoords(x=10, y=10, zoom=15)
tiles = [
(mercator.TileCoords(x=10, y=10, zoom=15), np.zeros((256, 256, 3), dtype=np.uint8)),
(mercator.TileCoords(x=11, y=10, zoom=15), np.zeros((256, 256, 3), dtype=np.uint8)),
+2 -10
View File
@@ -5,26 +5,18 @@ from datetime import datetime, timezone
import pytest
from pydantic import ValidationError
from gps_denied.config import AppSettings, get_settings
from gps_denied.schemas import CameraParameters, Geofences, GPSPoint, Polygon
from gps_denied.config import get_settings
from gps_denied.schemas import CameraParameters, GPSPoint
from gps_denied.schemas.events import (
FlightCompletedEvent,
FrameProcessedEvent,
SSEEventType,
SSEMessage,
)
from gps_denied.schemas.flight import (
BatchMetadata,
BatchResponse,
FlightCreateRequest,
FlightDetailResponse,
FlightResponse,
FlightStatusResponse,
UserFixRequest,
Waypoint,
)
# ── GPSPoint ──────────────────────────────────────────────────────────────
class TestGPSPoint:
-1
View File
@@ -26,7 +26,6 @@ import asyncio
import os
import socket
import time
from typing import Optional
import numpy as np
import pytest
+7 -7
View File
@@ -36,7 +36,7 @@ def cam_params():
def test_extract_features(vo):
img = np.zeros((480, 640, 3), dtype=np.uint8)
features = vo.extract_features(img)
assert isinstance(features, Features)
assert features.keypoints.shape == (500, 2)
assert features.descriptors.shape == (500, 256)
@@ -53,7 +53,7 @@ def test_match_features(vo):
descriptors=np.random.rand(100, 256),
scores=np.random.rand(100)
)
matches = vo.match_features(f1, f2)
assert isinstance(matches, Matches)
assert matches.matches.shape == (100, 2)
@@ -66,7 +66,7 @@ def test_estimate_motion_insufficient_matches(vo, cam_params):
keypoints1=np.zeros((5, 2)),
keypoints2=np.zeros((5, 2))
)
# Less than 8 points should return None
motion = vo.estimate_motion(matches, cam_params)
assert motion is None
@@ -78,14 +78,14 @@ def test_estimate_motion_synthetic(vo, cam_params):
n_pts = 100
pts1 = np.random.rand(n_pts, 2) * 400 + 100
pts2 = pts1 + np.array([10.0, 0.0]) # moving 10 pixels right
matches = Matches(
matches=np.column_stack([np.arange(n_pts), np.arange(n_pts)]),
scores=np.ones(n_pts),
keypoints1=pts1,
keypoints2=pts2
)
motion = vo.estimate_motion(matches, cam_params)
assert motion is not None
assert motion.inlier_count > 20
@@ -96,11 +96,11 @@ def test_estimate_motion_synthetic(vo, cam_params):
def test_compute_relative_pose(vo, cam_params):
img1 = np.zeros((480, 640, 3), dtype=np.uint8)
img2 = np.zeros((480, 640, 3), dtype=np.uint8)
# Given the random nature of our mock, OpenCV's findEssentialMat will likely find 0 inliers
# or fail. We expect compute_relative_pose to gracefully return None or low confidence.
pose = vo.compute_relative_pose(img1, img2, cam_params)
if pose is not None:
assert pose.translation.shape == (3,)
assert pose.rotation.shape == (3, 3)