Add demo raster fixture workflow
This commit is contained in:
@@ -10,6 +10,7 @@ class DemoWorkflowResponse(BaseModel):
|
||||
area_id: UUID
|
||||
reference_dataset_id: UUID
|
||||
candidate_dataset_id: UUID
|
||||
raster_dataset_id: UUID | None = None
|
||||
quality_check_id: UUID
|
||||
metric_count: int
|
||||
status: str
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import importlib
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from uuid import UUID, uuid4
|
||||
@@ -14,6 +15,7 @@ from app.schemas.demo import DemoWorkflowResponse
|
||||
from app.services.geojson_service import parse_geojson_payload
|
||||
from app.services.qa_service import QaService
|
||||
from app.services.quality_service import QualityService
|
||||
from app.services.raster_service import extract_raster_metadata
|
||||
from app.services.storage_service import StorageService
|
||||
from app.services.vector_feature_service import VectorFeatureService
|
||||
from app.utils.geometry import area_m2, geometry_bbox_polygon, normalize_to_multipolygon
|
||||
@@ -24,6 +26,7 @@ class DemoWorkflowService:
|
||||
AREA_NAME = "Demo AOI - Geel buildings"
|
||||
REFERENCE_FILENAME = "demo_reference_buildings.geojson"
|
||||
CANDIDATE_FILENAME = "demo_predicted_buildings.geojson"
|
||||
RASTER_FILENAME = "demo_context_raster.tif"
|
||||
EXPECTED_METRICS_FILENAME = "expected_qa_metrics.json"
|
||||
|
||||
@staticmethod
|
||||
@@ -101,15 +104,28 @@ class DemoWorkflowService:
|
||||
.filter(Dataset.project_id == project_id)
|
||||
.filter(Dataset.dataset_role == "source")
|
||||
.filter(Dataset.source_name == "fixture")
|
||||
.filter(Dataset.dataset_type == "vector")
|
||||
.first()
|
||||
)
|
||||
raster = DemoWorkflowService._find_demo_raster_dataset(db, project_id)
|
||||
quality_check = (
|
||||
db.query(QualityCheck)
|
||||
.filter(QualityCheck.project_id == project_id)
|
||||
.filter(QualityCheck.check_type == "demo_candidate_vs_reference")
|
||||
.first()
|
||||
)
|
||||
return bool(area and reference and candidate and quality_check)
|
||||
return bool(area and reference and candidate and raster and quality_check)
|
||||
|
||||
@staticmethod
|
||||
def _find_demo_raster_dataset(db: Session, project_id: UUID) -> Dataset | None:
|
||||
return (
|
||||
db.query(Dataset)
|
||||
.filter(Dataset.project_id == project_id)
|
||||
.filter(Dataset.dataset_type == "raster")
|
||||
.filter(Dataset.source_name == "fixture")
|
||||
.filter(Dataset.name == DemoWorkflowService.RASTER_FILENAME)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_area(db: Session, project_id: UUID) -> Area:
|
||||
@@ -207,6 +223,84 @@ class DemoWorkflowService:
|
||||
)
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
def _create_demo_raster_bytes() -> bytes:
|
||||
numpy = importlib.import_module("numpy")
|
||||
rasterio = importlib.import_module("rasterio")
|
||||
rasterio_io = importlib.import_module("rasterio.io")
|
||||
rasterio_transform = importlib.import_module("rasterio.transform")
|
||||
|
||||
width = 64
|
||||
height = 48
|
||||
data = numpy.linspace(20, 220, num=width * height, dtype=numpy.uint8).reshape((height, width))
|
||||
transform = rasterio_transform.from_bounds(4.9895, 51.1595, 4.9930, 51.1615, width, height)
|
||||
with rasterio_io.MemoryFile() as memfile:
|
||||
with memfile.open(
|
||||
driver="GTiff",
|
||||
width=width,
|
||||
height=height,
|
||||
count=1,
|
||||
dtype="uint8",
|
||||
crs="EPSG:4326",
|
||||
transform=transform,
|
||||
nodata=0,
|
||||
) as dataset:
|
||||
dataset.write(data, 1)
|
||||
return memfile.read()
|
||||
|
||||
@staticmethod
|
||||
def _create_raster_dataset(db: Session, *, project_id: UUID, area_id: UUID) -> Dataset:
|
||||
dataset_id = uuid4()
|
||||
raw = DemoWorkflowService._create_demo_raster_bytes()
|
||||
storage_info = StorageService.persist_dataset_file(
|
||||
project_id=str(project_id),
|
||||
dataset_id=str(dataset_id),
|
||||
dataset_type="raster",
|
||||
original_filename=DemoWorkflowService.RASTER_FILENAME,
|
||||
content=raw,
|
||||
content_type="image/tiff",
|
||||
)
|
||||
metadata = extract_raster_metadata(storage_info["storage_path"])
|
||||
bounds = metadata.get("bounds")
|
||||
bounds_json = None
|
||||
if isinstance(bounds, list) and len(bounds) == 4:
|
||||
bounds_json = {"minx": bounds[0], "miny": bounds[1], "maxx": bounds[2], "maxy": bounds[3]}
|
||||
dataset = Dataset(
|
||||
id=dataset_id,
|
||||
project_id=project_id,
|
||||
area_id=area_id,
|
||||
name=DemoWorkflowService.RASTER_FILENAME,
|
||||
dataset_type="raster",
|
||||
source="fixture",
|
||||
dataset_role="source",
|
||||
source_name="fixture",
|
||||
reference_layer_name=None,
|
||||
source_metadata={
|
||||
"fixture": True,
|
||||
"fixture_name": DemoWorkflowService.RASTER_FILENAME,
|
||||
"usage": "offline demo raster workflow only",
|
||||
},
|
||||
provenance_metadata={
|
||||
"created_by": "demo_workflow",
|
||||
"source_path": "generated:demo_context_raster",
|
||||
},
|
||||
imported_at=datetime.now(timezone.utc),
|
||||
storage_path=storage_info["storage_path"],
|
||||
original_filename=storage_info["original_filename"],
|
||||
stored_filename=storage_info["stored_filename"],
|
||||
content_type=storage_info["content_type"],
|
||||
size_bytes=storage_info["size_bytes"],
|
||||
checksum_sha256=storage_info["checksum_sha256"],
|
||||
crs=metadata.get("crs"),
|
||||
bounds_json=bounds_json,
|
||||
metadata_json=metadata,
|
||||
status="ready",
|
||||
)
|
||||
db.add(dataset)
|
||||
db.commit()
|
||||
db.refresh(dataset)
|
||||
return dataset
|
||||
|
||||
@staticmethod
|
||||
def _persist_qa(
|
||||
db: Session,
|
||||
@@ -306,8 +400,10 @@ class DemoWorkflowService:
|
||||
.filter(Dataset.project_id == existing.id)
|
||||
.filter(Dataset.dataset_role == "source")
|
||||
.filter(Dataset.source_name == "fixture")
|
||||
.filter(Dataset.dataset_type == "vector")
|
||||
.first()
|
||||
)
|
||||
raster = DemoWorkflowService._find_demo_raster_dataset(db, existing.id)
|
||||
quality_check = (
|
||||
db.query(QualityCheck)
|
||||
.filter(QualityCheck.project_id == existing.id)
|
||||
@@ -316,6 +412,8 @@ class DemoWorkflowService:
|
||||
.first()
|
||||
)
|
||||
if area and reference and candidate and quality_check:
|
||||
if not raster:
|
||||
raster = DemoWorkflowService._create_raster_dataset(db=db, project_id=existing.id, area_id=area.id)
|
||||
if not DemoWorkflowService._quality_check_matches_expected(db, quality_check):
|
||||
area = DemoWorkflowService._sync_demo_area(db, area)
|
||||
quality_check = DemoWorkflowService._persist_qa(
|
||||
@@ -330,6 +428,7 @@ class DemoWorkflowService:
|
||||
area_id=area.id,
|
||||
reference_dataset_id=reference.id,
|
||||
candidate_dataset_id=candidate.id,
|
||||
raster_dataset_id=raster.id,
|
||||
quality_check_id=quality_check.id,
|
||||
metric_count=db.query(Metric).filter(Metric.quality_check_id == quality_check.id).count(),
|
||||
status="ready",
|
||||
@@ -352,6 +451,7 @@ class DemoWorkflowService:
|
||||
area = None
|
||||
reference = None
|
||||
candidate = None
|
||||
raster = None
|
||||
quality_check = None
|
||||
created = True
|
||||
|
||||
@@ -381,6 +481,8 @@ class DemoWorkflowService:
|
||||
source_name="fixture",
|
||||
reference_layer_name=None,
|
||||
)
|
||||
if not raster:
|
||||
raster = DemoWorkflowService._create_raster_dataset(db=db, project_id=project.id, area_id=area.id)
|
||||
if not quality_check:
|
||||
quality_check = DemoWorkflowService._persist_qa(
|
||||
db=db,
|
||||
@@ -395,6 +497,7 @@ class DemoWorkflowService:
|
||||
area_id=area.id,
|
||||
reference_dataset_id=reference.id,
|
||||
candidate_dataset_id=candidate.id,
|
||||
raster_dataset_id=raster.id,
|
||||
quality_check_id=quality_check.id,
|
||||
metric_count=6,
|
||||
status="ready",
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_workbench_default_state_script_verifies_populated_demo_start_state() ->
|
||||
assert "/quality-checks" in content
|
||||
assert "data.items" in content
|
||||
assert "Demo AOI - Geel buildings" in content
|
||||
assert "2/2 ready" in content
|
||||
assert "3/3 ready" in content
|
||||
|
||||
|
||||
def test_pass_end_check_excludes_vendor_and_build_outputs() -> None:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_demo_workflow_contract_includes_raster_fixture_dataset() -> None:
|
||||
schema = (ROOT / "backend" / "app" / "schemas" / "demo.py").read_text(encoding="utf-8")
|
||||
service = (ROOT / "backend" / "app" / "services" / "demo_workflow_service.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "raster_dataset_id: UUID | None = None" in schema
|
||||
assert 'RASTER_FILENAME = "demo_context_raster.tif"' in service
|
||||
assert "_create_demo_raster_bytes" in service
|
||||
assert "_create_raster_dataset" in service
|
||||
assert "dataset_type=\"raster\"" in service
|
||||
assert "source_name=\"fixture\"" in service
|
||||
assert "raster_dataset_id=raster.id" in service
|
||||
|
||||
|
||||
def test_demo_workflow_frontend_uses_raster_fixture_for_ai_labs() -> None:
|
||||
types = (ROOT / "frontend" / "src" / "types.ts").read_text(encoding="utf-8")
|
||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||
hook = (ROOT / "frontend" / "src" / "hooks" / "useDemoWorkflow.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "raster_dataset_id?: string | null" in types
|
||||
assert "setSelectedDetectionDatasetId" in app
|
||||
assert "setSelectedSegmentationDatasetId" in app
|
||||
assert "setSelectedDetectionDatasetId(result.raster_dataset_id ?? '')" in hook
|
||||
assert "setSelectedSegmentationDatasetId(result.raster_dataset_id ?? '')" in hook
|
||||
assert "rasterDataset" in hook
|
||||
Reference in New Issue
Block a user