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,
|
||||
|
||||
Reference in New Issue
Block a user