perf: optimize trusted full-area analysis
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 22:57:09 +02:00
parent c4392c17fd
commit 0381fdd636
10 changed files with 164 additions and 15 deletions
+1
View File
@@ -15,6 +15,7 @@
- Kept every fetch operator-triggered, idempotent and behind the canonical DatasetService upload path; no startup fetch, migration or API contract change was introduced. - Kept every fetch operator-triggered, idempotent and behind the canonical DatasetService upload path; no startup fetch, migration or API contract change was introduced.
- Preserved separate Mol/regional series keys and honest partial-sector population and 10 m forest-area limitations. - Preserved separate Mol/regional series keys and honest partial-sector population and 10 m forest-area limitations.
- Replaced internal provider identifiers with readable source labels in the primary map. - Replaced internal provider identifiers with readable source labels in the primary map.
- Added a provenance-gated full-Area query path for official pre-clipped datasets, avoiding redundant intersection of every feature against the same detailed municipal/regional boundary while preserving exact rectangle selection behavior.
- Added focused scope filtering, command construction, boundary resolution, multi-NIS provenance, packaging and frontend-label tests. - Added focused scope filtering, command construction, boundary resolution, multi-NIS provenance, packaging and frontend-label tests.
## Sprint 193 End-user regional workbench simplification (2026-07-14) ## Sprint 193 End-user regional workbench simplification (2026-07-14)
+8
View File
@@ -1043,6 +1043,14 @@ stay within upstream response limits, then builds one retained 10 m mosaic and
one normal regional vector Dataset. A failed source request leaves completed one normal regional vector Dataset. A failed source request leaves completed
partition artifacts reusable and never lowers source resolution silently. partition artifacts reusable and never lowers source resolution silently.
Official operator datasets record that their geometries were clipped to the
persisted Area. When that exact Area is selected, vector totals and aggregate
metrics use the already clipped geometries directly rather than intersecting
every row with the same detailed boundary again. This optimization is allowed
only for matching Dataset/Area ids with explicit clipping metadata or a known
clipping operator; drawn rectangles and ordinary uploads keep the normal exact
PostGIS intersection path.
## Helpful repository scripts ## Helpful repository scripts
- `bash scripts/backend_install.sh` - `bash scripts/backend_install.sh`
+4
View File
@@ -248,10 +248,13 @@ def select_vector_features(
"bbox": payload.bbox.model_dump(), "bbox": payload.bbox.model_dump(),
"limit": payload.limit, "limit": payload.limit,
} }
full_dataset_area = False
if selection_area is not None: if selection_area is not None:
full_dataset_area = VectorFeatureService.can_use_full_area_fast_path(dataset, selection_area.id)
selection_kwargs.update( selection_kwargs.update(
selection_geometry=selection_area.geometry, selection_geometry=selection_area.geometry,
selection_area_id=selection_area.id, selection_area_id=selection_area.id,
full_dataset_area=full_dataset_area,
) )
result = VectorFeatureService.select_features_by_bbox(db, **selection_kwargs) result = VectorFeatureService.select_features_by_bbox(db, **selection_kwargs)
if isinstance(dataset.source_metadata, dict) and dataset.source_metadata.get("selection_aggregation"): if isinstance(dataset.source_metadata, dict) and dataset.source_metadata.get("selection_aggregation"):
@@ -262,6 +265,7 @@ def select_vector_features(
} }
if selection_area is not None: if selection_area is not None:
summary_kwargs["selection_geometry"] = selection_area.geometry summary_kwargs["selection_geometry"] = selection_area.geometry
summary_kwargs["full_dataset_area"] = full_dataset_area
result["summary"] = VectorFeatureService.summarize_features_by_bbox(db, **summary_kwargs) result["summary"] = VectorFeatureService.summarize_features_by_bbox(db, **summary_kwargs)
return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True)) return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True))
+45 -15
View File
@@ -18,7 +18,25 @@ from app.core.errors import AppError
from app.models import Dataset, VectorFeature from app.models import Dataset, VectorFeature
FULL_AREA_CLIPPED_OPERATOR_TOOLS = {
"provision_mol_population_history.py",
"provision_official_landuse_timeseries.py",
"provision_regional_grb_buildings.py",
"provision_regional_grb_context.py",
}
class VectorFeatureService: class VectorFeatureService:
@staticmethod
def can_use_full_area_fast_path(dataset: Dataset, selection_area_id: UUID | None) -> bool:
if selection_area_id is None or dataset.area_id != selection_area_id:
return False
source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {}
if source_metadata.get("geometry_clipped_to_area") is True:
return True
provenance = dataset.provenance_metadata if isinstance(dataset.provenance_metadata, dict) else {}
return provenance.get("operator_tool") in FULL_AREA_CLIPPED_OPERATOR_TOOLS
@staticmethod @staticmethod
def _feature_row(dataset_id: UUID, feature: dict[str, Any], index: int, feature_class: str | None) -> VectorFeature | None: def _feature_row(dataset_id: UUID, feature: dict[str, Any], index: int, feature_class: str | None) -> VectorFeature | None:
geometry_payload = feature.get("geometry") geometry_payload = feature.get("geometry")
@@ -126,6 +144,7 @@ class VectorFeatureService:
dataset: Dataset | None = None, dataset: Dataset | None = None,
selection_geometry: Any | None = None, selection_geometry: Any | None = None,
selection_area_id: UUID | None = None, selection_area_id: UUID | None = None,
full_dataset_area: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox) normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
safe_limit = max(1, min(int(limit), 1000)) safe_limit = max(1, min(int(limit), 1000))
@@ -139,11 +158,9 @@ class VectorFeatureService:
4326, 4326,
) )
query = ( query = db.query(VectorFeature).filter(VectorFeature.dataset_id == dataset_id)
db.query(VectorFeature) if not full_dataset_area:
.filter(VectorFeature.dataset_id == dataset_id) query = query.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
.filter(ST_Intersects(VectorFeature.geometry, selection_shape))
)
if hasattr(query, "count"): if hasattr(query, "count"):
total_feature_count = int(query.count()) total_feature_count = int(query.count())
else: # Lightweight unit-test sessions do not always implement Query.count(). else: # Lightweight unit-test sessions do not always implement Query.count().
@@ -165,6 +182,7 @@ class VectorFeatureService:
bbox=normalized_bbox, bbox=normalized_bbox,
total_feature_count=total_feature_count, total_feature_count=total_feature_count,
selection_geometry=selection_geometry, selection_geometry=selection_geometry,
full_dataset_area=full_dataset_area,
) )
result = { result = {
@@ -191,6 +209,7 @@ class VectorFeatureService:
bbox: dict[str, Any], bbox: dict[str, Any],
total_feature_count: int | None = None, total_feature_count: int | None = None,
selection_geometry: Any | None = None, selection_geometry: Any | None = None,
full_dataset_area: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox) normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox)
selection_shape = selection_geometry selection_shape = selection_geometry
@@ -202,10 +221,9 @@ class VectorFeatureService:
normalized_bbox["max_y"], normalized_bbox["max_y"],
4326, 4326,
) )
selection_filter = ( selection_filter = (VectorFeature.dataset_id == dataset.id,)
VectorFeature.dataset_id == dataset.id, if not full_dataset_area:
ST_Intersects(VectorFeature.geometry, selection_shape), selection_filter += (ST_Intersects(VectorFeature.geometry, selection_shape),)
)
feature_count = total_feature_count feature_count = total_feature_count
if feature_count is None: if feature_count is None:
feature_count = int(db.query(func.count(VectorFeature.id)).filter(*selection_filter).scalar() or 0) feature_count = int(db.query(func.count(VectorFeature.id)).filter(*selection_filter).scalar() or 0)
@@ -222,14 +240,22 @@ class VectorFeatureService:
metric_value = float(feature_count) metric_value = float(feature_count)
if method == "intersection_area": if method == "intersection_area":
intersection = func.ST_Intersection(VectorFeature.geometry, selection_shape) measured_geometry = (
area_expression = func.ST_Area(func.ST_Transform(intersection, 31370)) VectorFeature.geometry
if full_dataset_area
else func.ST_Intersection(VectorFeature.geometry, selection_shape)
)
area_expression = func.ST_Area(func.ST_Transform(measured_geometry, 31370))
area_m2 = db.query(func.coalesce(func.sum(area_expression), 0.0)).filter(*selection_filter).scalar() area_m2 = db.query(func.coalesce(func.sum(area_expression), 0.0)).filter(*selection_filter).scalar()
divisor = 10_000.0 if unit == "ha" else 1.0 divisor = 10_000.0 if unit == "ha" else 1.0
metric_value = float(area_m2 or 0.0) / divisor metric_value = float(area_m2 or 0.0) / divisor
elif method == "intersection_length": elif method == "intersection_length":
intersection = func.ST_Intersection(VectorFeature.geometry, selection_shape) measured_geometry = (
length_expression = func.ST_Length(func.ST_Transform(intersection, 31370)) VectorFeature.geometry
if full_dataset_area
else func.ST_Intersection(VectorFeature.geometry, selection_shape)
)
length_expression = func.ST_Length(func.ST_Transform(measured_geometry, 31370))
length_m = db.query(func.coalesce(func.sum(length_expression), 0.0)).filter(*selection_filter).scalar() length_m = db.query(func.coalesce(func.sum(length_expression), 0.0)).filter(*selection_filter).scalar()
divisor = 1_000.0 if unit == "km" else 1.0 divisor = 1_000.0 if unit == "km" else 1.0
metric_value = float(length_m or 0.0) / divisor metric_value = float(length_m or 0.0) / divisor
@@ -244,7 +270,7 @@ class VectorFeatureService:
) )
numeric_value = cast(VectorFeature.properties_json.op("->>")(property_name), Float) numeric_value = cast(VectorFeature.properties_json.op("->>")(property_name), Float)
value_expression = numeric_value value_expression = numeric_value
if method == "area_weighted_sum": if method == "area_weighted_sum" and not full_dataset_area:
source_area = func.ST_Area(func.ST_Transform(VectorFeature.geometry, 31370)) source_area = func.ST_Area(func.ST_Transform(VectorFeature.geometry, 31370))
intersection_area = func.ST_Area( intersection_area = func.ST_Area(
func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, selection_shape), 31370) func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, selection_shape), 31370)
@@ -258,7 +284,7 @@ class VectorFeatureService:
.scalar() .scalar()
) )
metric_value = float(aggregate_value or 0.0) metric_value = float(aggregate_value or 0.0)
if method == "area_weighted_sum": if method == "area_weighted_sum" and not full_dataset_area:
partial_feature_count = ( partial_feature_count = (
db.query(func.count(VectorFeature.id)) db.query(func.count(VectorFeature.id))
.filter(*selection_filter) .filter(*selection_filter)
@@ -268,6 +294,10 @@ class VectorFeatureService:
is_estimate = bool(partial_feature_count) is_estimate = bool(partial_feature_count)
if not is_estimate and config.get("warning_only_when_estimate", True): if not is_estimate and config.get("warning_only_when_estimate", True):
warning = None warning = None
elif method == "area_weighted_sum":
is_estimate = False
if config.get("warning_only_when_estimate", True):
warning = None
elif method != "feature_count": elif method != "feature_count":
raise AppError( raise AppError(
code="INVALID_SELECTION_AGGREGATION", code="INVALID_SELECTION_AGGREGATION",
@@ -7,11 +7,16 @@ import json
from pathlib import Path from pathlib import Path
import sys import sys
import zipfile import zipfile
from types import SimpleNamespace
from uuid import uuid4
import numpy as np import numpy as np
import rasterio import rasterio
from rasterio.transform import from_origin from rasterio.transform import from_origin
from app.models import Dataset
from app.services.vector_feature_service import VectorFeatureService
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts" SCRIPTS = ROOT / "scripts"
@@ -184,3 +189,94 @@ def test_end_user_dataset_sources_are_human_readable() -> None:
assert "statbel: 'Statbel'" in display assert "statbel: 'Statbel'" in display
assert "getDatasetSourceDisplayName(activeThemeDataset)" in workspace assert "getDatasetSourceDisplayName(activeThemeDataset)" in workspace
assert "dataset ? getDatasetSourceDisplayName(dataset)" in workspace assert "dataset ? getDatasetSourceDisplayName(dataset)" in workspace
def test_full_area_fast_path_requires_matching_area_and_clipped_operator_provenance() -> None:
project_id = uuid4()
area_id = uuid4()
trusted = Dataset(
id=uuid4(),
project_id=project_id,
area_id=area_id,
name="Regional forest",
dataset_type="vector",
source="operator_official_import",
provenance_metadata={"operator_tool": "provision_official_landuse_timeseries.py"},
)
explicit = Dataset(
id=uuid4(),
project_id=project_id,
area_id=area_id,
name="Clipped vector",
dataset_type="vector",
source="manual",
source_metadata={"geometry_clipped_to_area": True},
)
untrusted = Dataset(
id=uuid4(),
project_id=project_id,
area_id=area_id,
name="Assigned only",
dataset_type="vector",
source="manual",
)
assert VectorFeatureService.can_use_full_area_fast_path(trusted, area_id) is True
assert VectorFeatureService.can_use_full_area_fast_path(explicit, area_id) is True
assert VectorFeatureService.can_use_full_area_fast_path(untrusted, area_id) is False
assert VectorFeatureService.can_use_full_area_fast_path(trusted, uuid4()) is False
assert VectorFeatureService.can_use_full_area_fast_path(trusted, None) is False
def test_full_area_summary_uses_exact_stored_values_without_partial_intersection() -> None:
class ScalarQuery:
def __init__(self, value: float) -> None:
self.value = value
def filter(self, *_args):
return self
def scalar(self):
return self.value
class ScalarSession:
def __init__(self, value: float) -> None:
self.value = value
self.query_count = 0
def query(self, *_args):
self.query_count += 1
return ScalarQuery(self.value)
dataset = Dataset(
id=uuid4(),
project_id=uuid4(),
name="Population",
dataset_type="vector",
source="operator_official_import",
source_metadata={
"selection_aggregation": {
"method": "area_weighted_sum",
"property": "population_total",
"label": "Inwoners",
"unit": "inwoners",
"warning_only_when_estimate": True,
"warning": "Partial-sector estimate",
}
},
)
session = ScalarSession(506_473.0)
result = VectorFeatureService.summarize_features_by_bbox(
session,
dataset=dataset,
bbox={"min_x": 4.5, "min_y": 51.0, "max_x": 5.3, "max_y": 51.6, "crs": "EPSG:4326"},
total_feature_count=733,
selection_geometry=SimpleNamespace(),
full_dataset_area=True,
)
assert result["metric_value"] == 506_473.0
assert result["is_estimate"] is False
assert result["warning"] is None
assert session.query_count == 1
+1
View File
@@ -7982,6 +7982,7 @@ Live source finding:
- The regional Statbel synchronization imported all five requested snapshots successfully. - The regional Statbel synchronization imported all five requested snapshots successfully.
- The first complete-region forest request was rejected by MercatorNet before download because the estimated 97.68 MB response exceeded its 78.12 MB service limit. - The first complete-region forest request was rejected by MercatorNet before download because the estimated 97.68 MB response exceeded its 78.12 MB service limit.
- The operator now uses the 28 retained official municipality boundaries as resumable WCS partitions, merges them locally at the unchanged 10 m grid and applies the exact region-union clip before polygonization. It does not lower resolution or truncate the source. - The operator now uses the 28 retained official municipality boundaries as resumable WCS partitions, merges them locally at the unchanged 10 m grid and applies the exact region-union clip before polygonization. It does not lower resolution or truncate the source.
- Live full-region analysis returned the correct six-theme result but initially took about 92 seconds because every already-clipped feature was intersected again with a 947 kB regional geometry. Added a provenance-gated full-Area path that skips only this redundant intersection; arbitrary rectangles and untrusted uploads retain the existing exact query.
## Sprint 190 Regional Kempen GRB buildings (2026-07-14) ## Sprint 190 Regional Kempen GRB buildings (2026-07-14)
@@ -255,6 +255,7 @@ def build_snapshot(
"scope_type": scope.scope_type, "scope_type": scope.scope_type,
"member_count": len(scope.members), "member_count": len(scope.members),
"member_nis_codes": list(scope.nis_codes), "member_nis_codes": list(scope.nis_codes),
"geometry_clipped_to_area": True,
"observation_year": year, "observation_year": year,
"missing_population_sector_count": missing_population, "missing_population_sector_count": missing_population,
"attribution": ATTRIBUTION, "attribution": ATTRIBUTION,
@@ -301,6 +302,7 @@ def upload_snapshot(
"scope_display_name": scope.display_name, "scope_display_name": scope.display_name,
"member_count": len(scope.members), "member_count": len(scope.members),
"member_nis_codes": list(scope.nis_codes), "member_nis_codes": list(scope.nis_codes),
"geometry_clipped_to_area": True,
"attribution": ATTRIBUTION, "attribution": ATTRIBUTION,
"license": "CC BY 4.0", "license": "CC BY 4.0",
"temporal_series_label": "Officiële bevolkingscijfers per statistische sector", "temporal_series_label": "Officiële bevolkingscijfers per statistische sector",
@@ -320,6 +322,7 @@ def upload_snapshot(
"operator_tool": "provision_mol_population_history.py", "operator_tool": "provision_mol_population_history.py",
"operator_explicit_fetch": True, "operator_explicit_fetch": True,
"scope_key": scope.key, "scope_key": scope.key,
"geometry_clipped_to_area": True,
"sector_geometry_url": SECTOR_URL.format(year=year), "sector_geometry_url": SECTOR_URL.format(year=year),
"population_url": POPULATION_URLS[year], "population_url": POPULATION_URLS[year],
"generated_at": datetime.now(timezone.utc).isoformat(), "generated_at": datetime.now(timezone.utc).isoformat(),
@@ -788,6 +788,7 @@ def build_source_metadata(args: argparse.Namespace, snapshot: PreparedSnapshot)
"authority_level": "authoritative", "authority_level": "authoritative",
"coverage_scope": args.scope_key, "coverage_scope": args.scope_key,
**identity, **identity,
"geometry_clipped_to_area": True,
"attribution": ATTRIBUTION, "attribution": ATTRIBUTION,
"license_note": "Publieke Vlaamse overheidsdata; raadpleeg de toegangs- en gebruiksvoorwaarden in de bronmetadata.", "license_note": "Publieke Vlaamse overheidsdata; raadpleeg de toegangs- en gebruiksvoorwaarden in de bronmetadata.",
"methodology_version": "3", "methodology_version": "3",
@@ -815,6 +816,7 @@ def build_provenance_metadata(args: argparse.Namespace, snapshot: PreparedSnapsh
return { return {
"operator_tool": "provision_official_landuse_timeseries.py", "operator_tool": "provision_official_landuse_timeseries.py",
"operator_explicit_fetch": True, "operator_explicit_fetch": True,
"geometry_clipped_to_area": True,
"wcs_url": WCS_URL, "wcs_url": WCS_URL,
"wcs_version": WCS_VERSION, "wcs_version": WCS_VERSION,
"coverage_id": coverage_id(snapshot.year), "coverage_id": coverage_id(snapshot.year),
@@ -657,6 +657,7 @@ def provision_dataset(
"scope_limitation": scope.limitation_message, "scope_limitation": scope.limitation_message,
"member_count": len(scope.members), "member_count": len(scope.members),
"member_nis_codes": list(scope.nis_codes), "member_nis_codes": list(scope.nis_codes),
"geometry_clipped_to_area": True,
"feature_count": manifest["feature_count"], "feature_count": manifest["feature_count"],
"partition_count": len(partition_paths), "partition_count": len(partition_paths),
"partition_strategy": manifest["partition_strategy"], "partition_strategy": manifest["partition_strategy"],
@@ -671,6 +672,7 @@ def provision_dataset(
provenance_metadata = { provenance_metadata = {
"operator_tool": "provision_regional_grb_buildings.py", "operator_tool": "provision_regional_grb_buildings.py",
"operator_explicit_fetch": True, "operator_explicit_fetch": True,
"geometry_clipped_to_area": True,
"manifest_path": str(manifest_path), "manifest_path": str(manifest_path),
"source_url": GRB_GBG_ITEMS_URL, "source_url": GRB_GBG_ITEMS_URL,
"artifact_sha256": manifest["artifact_sha256"], "artifact_sha256": manifest["artifact_sha256"],
@@ -716,6 +716,7 @@ def provision_dataset(
"layer_limitation": definition.limitation_message, "layer_limitation": definition.limitation_message,
"member_count": len(scope.members), "member_count": len(scope.members),
"member_nis_codes": list(scope.nis_codes), "member_nis_codes": list(scope.nis_codes),
"geometry_clipped_to_area": True,
"feature_count": manifest["feature_count"], "feature_count": manifest["feature_count"],
"partition_count": len(partition_paths), "partition_count": len(partition_paths),
"partition_strategy": manifest["partition_strategy"], "partition_strategy": manifest["partition_strategy"],
@@ -730,6 +731,7 @@ def provision_dataset(
provenance_metadata = { provenance_metadata = {
"operator_tool": "provision_regional_grb_context.py", "operator_tool": "provision_regional_grb_context.py",
"operator_explicit_fetch": True, "operator_explicit_fetch": True,
"geometry_clipped_to_area": True,
"manifest_path": str(manifest_path), "manifest_path": str(manifest_path),
"source_urls": manifest["grb_source_urls"], "source_urls": manifest["grb_source_urls"],
"artifact_sha256": manifest["artifact_sha256"], "artifact_sha256": manifest["artifact_sha256"],