document the analysis contracts and correct the job-queue claim
The README's architecture diagram and stack list promised Redis + RQ. There is no such thing in this codebase: redis_url is a settings field nothing reads, rq is not a dependency, and the only worker is an asyncio poller over a Postgres job table. Describe what exists. API_CONTRACTS and KNOWN_LIMITATIONS gain the rules a reader needs to interpret a result: deterministic confidence-ranked matching, the precision/recall curve, the three flood-hazard cell populations, the sub-cell selection fallback, the whole-feature object count next to clipped area metrics, and the required manifest CRS. The new limitations are stated as limits rather than buried: raster analysis has no sub-cell precision, and the object count is not fractional. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+110
-7
@@ -617,13 +617,39 @@ is not a temporal observation and receives no fabricated `observed_at` value.
|
||||
|
||||
### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/raster/flood-hazard/select`
|
||||
|
||||
Returns mapped positive-depth area in hectares, share of the selection, mean,
|
||||
P90 and maximum modeled local depth and `modelled_max_depth_area_integral_m3`.
|
||||
Every result identifies mechanism, climate context, probability class and
|
||||
return period. The integral sums local modeled maximum depth times cell area;
|
||||
it is explicitly not concurrent flood storage, permanent waterbody content,
|
||||
current water level or bathymetry. These unsupported metrics remain listed in
|
||||
the response.
|
||||
Returns mapped positive-depth area in hectares, share of the *modelled* area,
|
||||
mean, P90 and maximum modeled local depth and
|
||||
`modelled_max_depth_area_integral_m3`. Every result identifies mechanism,
|
||||
climate context, probability class and return period. The integral sums local
|
||||
modeled maximum depth times cell area; it is explicitly not concurrent flood
|
||||
storage, permanent waterbody content, current water level or bathymetry. These
|
||||
unsupported metrics remain listed in the response.
|
||||
|
||||
Three cell populations are reported separately, because conflating them turns
|
||||
missing data into a claim of safety:
|
||||
|
||||
- `selected_cell_count` — cells inside the drawn selection;
|
||||
- `valid_cell_count` / `no_data_cell_count` — the split between cells the VMM
|
||||
raster models and cells it does not;
|
||||
- `inundated_cell_count` — modelled cells with a positive depth.
|
||||
|
||||
`inundated_fraction` and `modelled_inundated_share_pct` are shares of the
|
||||
modelled cells, not of the drawn selection. `inundated_fraction` is `null` when
|
||||
nothing was modelled at all. `data_coverage_ratio`, `model_coverage_pct`,
|
||||
`modelled_area_ha` and `selection_area_ha` make the difference between the
|
||||
drawn area and the analysed area explicit, and `coverage_warning` states it in
|
||||
words. A selection reaching past the modelled extent previously reported a
|
||||
diluted risk share for the whole rectangle.
|
||||
|
||||
**Selections finer than one source cell.** `geometry_mask` selects a cell when
|
||||
its centre falls inside the geometry, so a rectangle smaller than a cell — or
|
||||
one landing between four centres — selected nothing and the analysis returned
|
||||
zeros indistinguishable from "nothing here". Every raster selection now falls
|
||||
back to the cells the geometry touches and reports that in
|
||||
`cell_selection_warning` (`coverage_warning` for flood hazard), because the
|
||||
result then covers more ground than was drawn. This applies to terrain,
|
||||
bathymetry, thematic raster and flood hazard, in both the single-dataset and
|
||||
the partitioned paths.
|
||||
|
||||
### POST `/api/v1/projects/{project_id}/datasets/raster/flood-hazard/select`
|
||||
|
||||
@@ -1056,6 +1082,21 @@ Return vector bounds and feature count.
|
||||
|
||||
### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/vector/select`
|
||||
|
||||
Selection metrics describe two different populations and now say so.
|
||||
`intersection_area` and `intersection_length` clip each feature to the
|
||||
selection, while the object count treats any feature touching the selection as
|
||||
whole — which is what an operator expects from "objecten", but overstates the
|
||||
count along every edge. The response therefore adds
|
||||
`fully_covered_feature_count`, `partially_covered_feature_count` and
|
||||
`selection_edge_warning`, and marks the count metric as an estimate whenever
|
||||
the selection cuts features. Area and length metrics stay exact and do not
|
||||
inherit that caveat.
|
||||
|
||||
Partitioned selection (`/datasets/vector/partitions/select`) de-duplicates on
|
||||
`source_feature_id` across municipal partitions for the returned geometry as
|
||||
well as for the count. A feature on a shared boundary was previously counted
|
||||
once but drawn once per partition.
|
||||
|
||||
Read-only spatial selection over persisted `vector_features`.
|
||||
|
||||
Request:
|
||||
@@ -1619,6 +1660,44 @@ reproducibility. Clients must not submit arbitrary model paths.
|
||||
|
||||
GeoIntel does not download model weights automatically. Configured YOLO runs read existing tile files from the manifest, convert YOLO pixel-space boxes to EPSG:4326 detection polygons and persist detections as first-class records.
|
||||
|
||||
The manifest must carry explicit CRS metadata (`crs`, `source_crs` or
|
||||
`dataset_crs`). A manifest without it fails with
|
||||
`DETECTION_TILE_MANIFEST_INVALID` rather than being georeferenced against an
|
||||
assumed EPSG:4326, which would place detections plausibly but wrongly.
|
||||
|
||||
Tiles are read with rasterio: the visible RGB bands are selected explicitly and
|
||||
percentile-stretched to 8-bit, so 16-bit and 4-band (RGB + NIR) orthophotos
|
||||
reach the model as the kind of image it was trained on. Tiles are predicted in
|
||||
batches of `YOLO_BATCH_SIZE`.
|
||||
|
||||
Post-processing removes two artefacts of tiled inference:
|
||||
|
||||
- boxes truncated by an *interior* tile edge are dropped, because the
|
||||
overlapping neighbouring tile observed the same object completely
|
||||
(`YOLO_SUPPRESS_TILE_EDGE_DETECTIONS`, default on). Boxes against the outer
|
||||
raster edge are kept;
|
||||
- duplicates are suppressed on IoU *and* on intersection-over-smaller-area, so
|
||||
an object wider than the tile overlap does not survive as two partial boxes.
|
||||
|
||||
`result_json` reports `raw_detection_count`, `suppressed_detection_count`,
|
||||
`tile_edge_truncated_count`, `duplicate_iou_threshold` and
|
||||
`containment_suppression_threshold`.
|
||||
|
||||
### POST `/api/v1/detection/run-async`
|
||||
|
||||
Same request body as `POST /api/v1/detection/run`, but queues the run instead of
|
||||
executing it inside the request, and returns a `JobRead`. Tiled GPU inference
|
||||
over up to `YOLO_MAX_TILES` tiles takes minutes; performing it in the request
|
||||
holds a worker thread and times the client out. Cheap validation (project,
|
||||
dataset, dataset type) still happens synchronously, so an invalid request is
|
||||
rejected immediately rather than by a job that fails minutes later.
|
||||
|
||||
Queued jobs are executed by the background analysis worker
|
||||
(`GEOINTEL_ANALYSIS_WORKER_ENABLED`, poll interval
|
||||
`GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS`), which claims a job before dispatching
|
||||
it so the same run is never started twice. Poll `GET /api/v1/jobs/{id}` for
|
||||
progress. `POST /api/v1/segmentation/run-async` behaves identically.
|
||||
|
||||
Unavailable model response:
|
||||
|
||||
```json
|
||||
@@ -1759,6 +1838,30 @@ candidate geometry versus the persisted reference footprint. Envelope results
|
||||
are explicitly `diagnostic_only` and are persisted in
|
||||
`quality_checks.findings_json`; they never replace or inflate canonical metrics.
|
||||
|
||||
`box_to_footprint_diagnostics.candidate_geometry_mode` reports whether the
|
||||
candidates are `axis_aligned_boxes` or `footprint_polygons`. For a box detector
|
||||
the strict footprint IoU has a ceiling below 1 on rotated or non-rectangular
|
||||
buildings, and the response says so in `warnings` and in `interpretation`.
|
||||
|
||||
Matching is deterministic. Candidates are ranked by confidence, highest first,
|
||||
with feature identity as tiebreaker, before the greedy IoU assignment. Database
|
||||
row order is not usable for this: every detection in a run shares one
|
||||
transaction timestamp, so ordering by `created_at` left the assignment — and
|
||||
therefore the score and the false-positive evidence shown to a reviewer —
|
||||
undefined between identical runs.
|
||||
|
||||
The response also returns `precision_recall_curve`: precision, recall and F1 at
|
||||
every confidence value present in the run, plus `average_precision`, `best_f1`
|
||||
and `best_f1_threshold`. A single F1 describes one operating point and cannot
|
||||
compare two models whose calibration differs; the curve can. `average_precision`,
|
||||
`best_f1` and `best_f1_threshold` are persisted as `metrics` rows alongside the
|
||||
existing ones.
|
||||
|
||||
Segmentation QA (`POST /api/v1/segmentation/runs/{analysis_run_id}/qa/reference`)
|
||||
applies the same tile-coverage clipping and returns the same `coverage` block.
|
||||
Without it, every reference feature outside the inferred tiles counted as a
|
||||
false negative and recall was understated by an arbitrary amount.
|
||||
|
||||
Configured-YOLO QA fails closed with `DETECTION_QA_COVERAGE_UNAVAILABLE` when
|
||||
manifest provenance is absent, `DETECTION_QA_COVERAGE_MISMATCH` when it belongs
|
||||
to another raster, `DETECTION_QA_COVERAGE_INVALID` when bounds/CRS are invalid,
|
||||
|
||||
@@ -43,6 +43,16 @@ runtime source of truth.
|
||||
underlying named Area itself is wholly valid.
|
||||
- Cross-region and land/sea selections stay split by legal/source zone.
|
||||
Semantically incompatible metrics are not merged.
|
||||
- Raster analysis is cell-based. A selection smaller than one source cell is
|
||||
answered over the cells it touches, so the analysed area is larger than the
|
||||
drawn area; the response says so rather than returning zeros. Sub-cell
|
||||
precision is not available from a raster source.
|
||||
- The object count in a vector selection counts whole features that touch the
|
||||
selection, while area and length metrics clip to it. The response reports how
|
||||
many features the edge cuts; it does not attempt a fractional object count.
|
||||
- Flood-hazard percentages are shares of the modelled area. Where the VMM model
|
||||
does not cover the selection, that is reported as missing coverage and never
|
||||
as an absence of risk.
|
||||
|
||||
## Historical analysis
|
||||
|
||||
@@ -68,12 +78,28 @@ runtime source of truth.
|
||||
`YOLO_SEG_MODEL_PATH`/`SAM_MODEL_PATH` to existing local weights and enables
|
||||
them explicitly. GeoIntel never downloads segmentation weights automatically;
|
||||
fixture segmentation remains explicit-only.
|
||||
- Detection QA reports both a strict footprint IoU and an envelope diagnostic.
|
||||
For an axis-aligned box detector the strict figure has a ceiling below 1 on
|
||||
rotated or non-rectangular buildings; the response states which geometry mode
|
||||
applies rather than silently attributing that gap to detection quality.
|
||||
- Segmentation inference still predicts one tile per call. Batching is
|
||||
implemented for detection only, because the SAM adapter is prompt-driven and
|
||||
does not share the detector's batch semantics.
|
||||
- Tile-edge truncated boxes are dropped in favour of the overlapping
|
||||
neighbour's complete view. On a manifest generated with `overlap=0` there is
|
||||
no such neighbour, so an object on a seam is lost rather than duplicated.
|
||||
Generate tiles with an overlap larger than the biggest expected object.
|
||||
|
||||
## Operations
|
||||
|
||||
- Long AI/GIS work still uses the existing synchronous job abstraction rather
|
||||
than a distributed durable queue. Interrupted synchronous work is marked
|
||||
failed on restart and must be retried explicitly.
|
||||
- Detection and segmentation runs can be queued through
|
||||
`POST /detection/run-async` and executed by the in-process analysis worker, so
|
||||
tiled GPU inference no longer blocks an HTTP request. This is a job table in
|
||||
PostgreSQL polled by the API process, not a distributed durable queue: there
|
||||
is no cross-host distribution and no automatic retry. The synchronous
|
||||
`POST /detection/run` remains available for small runs. Interrupted work is
|
||||
marked failed on restart and must be retried explicitly.
|
||||
- Other long GIS work still uses the synchronous job abstraction.
|
||||
- GeoIntel remains a controlled single-operator product. The optional guest
|
||||
demo adds bounded presentation access only; multi-user authorization and
|
||||
tenant isolation remain outside the product scope.
|
||||
|
||||
Reference in New Issue
Block a user