surface the new analysis caveats in the workbench
The backend now says when a result covers a different area than was drawn, or when a score belongs to one confidence cut only. None of that helps an operator while it stays in the response body. - the raster selection adapters carry the model-coverage and widened-cell warnings into the map panel and mark the result an estimate when either applies, so an existing warning slot renders them; - the map workspace shows the selection-edge disclosure next to the object count; - the detection panel shows average precision and the F1-optimal threshold beside the single-threshold figures, and the box-versus-footprint interpretation when candidates are detector boxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -949,6 +949,26 @@ export function DetectionLab({
|
||||
<p>
|
||||
{detectionQaResult.box_to_footprint_diagnostics.possible_box_to_footprint_mismatch_count} mogelijke vormafwijkingen. Precisie, herkenningsgraad en F1 hierboven blijven gebaseerd op de strikte geometrische overlap.
|
||||
</p>
|
||||
{detectionQaResult.box_to_footprint_diagnostics.interpretation ? (
|
||||
<p>{detectionQaResult.box_to_footprint_diagnostics.interpretation}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{detectionQaResult.precision_recall_curve ? (
|
||||
<div className="detection-qa-diagnostic">
|
||||
<span>Drempelonafhankelijke kwaliteit</span>
|
||||
<strong>
|
||||
AP {formatNullableNumber(detectionQaResult.precision_recall_curve.average_precision, 3)} · beste F1{' '}
|
||||
{formatNullableNumber(detectionQaResult.precision_recall_curve.best_f1, 3)}
|
||||
</strong>
|
||||
<p>
|
||||
{detectionQaResult.precision_recall_curve.best_f1_threshold === null
|
||||
? 'Geen kandidaten om een werkpunt uit af te leiden.'
|
||||
: `De beste F1 ligt bij drempel ${formatNullableNumber(
|
||||
detectionQaResult.precision_recall_curve.best_f1_threshold,
|
||||
2,
|
||||
)}. Precisie en herkenningsgraad hierboven gelden alleen voor de gekozen drempel; AP vat de volledige curve samen en maakt vergelijking tussen modellen mogelijk.`}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2957,6 +2957,9 @@ export function MapWorkspace({
|
||||
{analysisMode === 'current' && activeSelectionResult?.truncated ? (
|
||||
<p className="geo-data-notice">De telling is volledig; op de kaart en in de tabel worden maximaal {activeSelectionResult.limit.toLocaleString('nl-BE')} objecten getoond.</p>
|
||||
) : null}
|
||||
{analysisMode === 'current' && activeSelectionResult?.summary?.selection_edge_warning ? (
|
||||
<p className="geo-data-notice">{activeSelectionResult.summary.selection_edge_warning}</p>
|
||||
) : null}
|
||||
{analysisMode === 'current' && activeSelectionResult?.summary?.warning ? (
|
||||
<p className="geo-data-notice">{activeSelectionResult.summary.warning}</p>
|
||||
) : null}
|
||||
|
||||
@@ -19,7 +19,10 @@ export function bathymetryRasterSelectionToMapSelection(
|
||||
...result.summary,
|
||||
feature_count: result.valid_cell_count,
|
||||
is_estimate: false,
|
||||
warning: result.limitation_message,
|
||||
// A selection finer than one source cell was widened to the cells it
|
||||
// touches; the result then covers more ground than was drawn.
|
||||
warning: [result.cell_selection_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.cell_selection_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: false,
|
||||
|
||||
@@ -12,9 +12,15 @@ export function floodHazardSelectionToMapSelection(result: FloodHazardSelectionR
|
||||
summary: {
|
||||
...result.summary,
|
||||
feature_count: result.inundated_cell_count,
|
||||
is_estimate: false,
|
||||
warning: result.limitation_message,
|
||||
metrics: result.summary.metrics.map((metric) => ({ ...metric, is_estimate: false })),
|
||||
// Coverage is not certain when the model does not span the selection, or
|
||||
// when the selection was widened to whole cells.
|
||||
is_estimate: Boolean(result.coverage_warning),
|
||||
warning: [result.coverage_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.coverage_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: Boolean(result.coverage_warning),
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { floodHazardSelectionToMapSelection } from './floodHazardSelection'
|
||||
import { thematicRasterSelectionToMapSelection } from './thematicRaster'
|
||||
import type { FloodHazardSelectionResponse, ThematicRasterSelectionResponse } from '../types'
|
||||
|
||||
/**
|
||||
* Raster analyses answer over whole cells. When a selection is finer than one
|
||||
* source cell, or when the flood model does not span it, the value describes a
|
||||
* different area than the operator drew. The adapters must carry that statement
|
||||
* into the map panel; without it the number reads as an exact answer.
|
||||
*/
|
||||
|
||||
const BBOX = { min_x: 5.0, min_y: 51.1, max_x: 5.2, max_y: 51.3, crs: 'EPSG:4326' }
|
||||
|
||||
function floodResponse(overrides: Partial<FloodHazardSelectionResponse> = {}): FloodHazardSelectionResponse {
|
||||
return {
|
||||
dataset_id: 'dataset-1',
|
||||
dataset_ids: ['dataset-1'],
|
||||
partition_count: 1,
|
||||
product_key: 'fluviaal_current_t100',
|
||||
mechanism: 'fluviaal',
|
||||
climate_context: 'current',
|
||||
probability_class: 'grote kans',
|
||||
return_period_years: 100,
|
||||
selection_bbox: BBOX,
|
||||
selected_cell_count: 400,
|
||||
inundated_cell_count: 200,
|
||||
inundated_fraction: 1,
|
||||
resolution_m: 5,
|
||||
summary: {
|
||||
metric_label: 'Gemodelleerd overstroomd oppervlak',
|
||||
metric_value: 0.5,
|
||||
metric_unit: 'ha',
|
||||
aggregation_method: 'positive_depth_cells_times_cell_area',
|
||||
primary_metric_key: 'modelled_inundated_area_ha',
|
||||
metrics: [],
|
||||
},
|
||||
unsupported_metrics: [],
|
||||
limitation_message: 'Scenariodiepte is geen bathymetrie.',
|
||||
generated_at: '2026-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
} as FloodHazardSelectionResponse
|
||||
}
|
||||
|
||||
function thematicResponse(
|
||||
overrides: Partial<ThematicRasterSelectionResponse> = {},
|
||||
): ThematicRasterSelectionResponse {
|
||||
return {
|
||||
dataset_id: 'dataset-2',
|
||||
product_key: 'population_density',
|
||||
selection_bbox: BBOX,
|
||||
selected_cell_count: 1,
|
||||
valid_cell_count: 1,
|
||||
coverage_ratio: 1,
|
||||
resolution_m: 100,
|
||||
summary: {
|
||||
metric_label: 'Inwoners',
|
||||
metric_value: 42,
|
||||
metric_unit: 'inwoners',
|
||||
aggregation_method: 'sum_valid_source_cells',
|
||||
primary_metric_key: 'population',
|
||||
metrics: [],
|
||||
},
|
||||
limitation_message: 'Rasterbron op 100 m.',
|
||||
generated_at: '2026-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
} as ThematicRasterSelectionResponse
|
||||
}
|
||||
|
||||
describe('flood hazard selection adapter', () => {
|
||||
it('surfaces a partial model coverage warning and marks the result an estimate', () => {
|
||||
const mapped = floodHazardSelectionToMapSelection(
|
||||
floodResponse({ coverage_warning: 'Het VMM-model dekt 50.0% van deze selectie.' }),
|
||||
)
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toContain('50.0%')
|
||||
expect(mapped.summary?.warning).toContain('50.0%')
|
||||
expect(mapped.summary?.warning).toContain('Scenariodiepte is geen bathymetrie.')
|
||||
expect(mapped.summary?.is_estimate).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves a fully modelled selection exact', () => {
|
||||
const mapped = floodHazardSelectionToMapSelection(floodResponse())
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toBeNull()
|
||||
expect(mapped.summary?.is_estimate).toBe(false)
|
||||
expect(mapped.summary?.warning).toBe('Scenariodiepte is geen bathymetrie.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('thematic raster selection adapter', () => {
|
||||
it('reports that a sub-cell selection was widened to whole cells', () => {
|
||||
const mapped = thematicRasterSelectionToMapSelection(
|
||||
thematicResponse({
|
||||
cell_selection_warning: 'De selectie is kleiner dan één rastercel van deze bron.',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toContain('rastercel')
|
||||
expect(mapped.summary?.warning).toContain('rastercel')
|
||||
expect(mapped.summary?.warning).toContain('Rasterbron op 100 m.')
|
||||
})
|
||||
|
||||
it('keeps a normal selection free of an invented caveat', () => {
|
||||
const mapped = thematicRasterSelectionToMapSelection(thematicResponse())
|
||||
|
||||
expect(mapped.summary?.selection_edge_warning).toBeNull()
|
||||
expect(mapped.summary?.warning).toBe('Rasterbron op 100 m.')
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,10 @@ export function terrainSelectionToMapSelection(result: TerrainSelectionResponse)
|
||||
...result.summary,
|
||||
feature_count: result.sample_count,
|
||||
is_estimate: false,
|
||||
warning: result.limitation_message,
|
||||
// A selection finer than one source cell was widened to the cells it
|
||||
// touches; the result then covers more ground than was drawn.
|
||||
warning: [result.cell_selection_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.cell_selection_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: false,
|
||||
|
||||
@@ -21,7 +21,10 @@ export function thematicRasterSelectionToMapSelection(result: ThematicRasterSele
|
||||
...result.summary,
|
||||
feature_count: result.valid_cell_count,
|
||||
is_estimate: true,
|
||||
warning: result.limitation_message,
|
||||
// A selection finer than one source cell was widened to the cells it
|
||||
// touches; the result then covers more ground than was drawn.
|
||||
warning: [result.cell_selection_warning, result.limitation_message].filter(Boolean).join(' '),
|
||||
selection_edge_warning: result.cell_selection_warning ?? null,
|
||||
metrics: result.summary.metrics.map((metric) => ({
|
||||
...metric,
|
||||
is_estimate: metric.is_estimate ?? true,
|
||||
|
||||
+53
-1
@@ -505,6 +505,8 @@ export interface TerrainSelectionResponse {
|
||||
sample_count: number
|
||||
slope_sample_count: number
|
||||
coverage_ratio: number
|
||||
/** Set when the selection was finer than one source cell and widened. */
|
||||
cell_selection_warning?: string | null
|
||||
resolution_m: number
|
||||
vertical_reference: string
|
||||
summary: {
|
||||
@@ -558,8 +560,14 @@ export interface FloodHazardSelectionResponse {
|
||||
selection_bbox: VectorSelectionBBox
|
||||
selection_area_id?: string | null
|
||||
selected_cell_count: number
|
||||
/** Cells the flood model actually covers inside the selection. */
|
||||
valid_cell_count?: number
|
||||
no_data_cell_count?: number
|
||||
data_coverage_ratio?: number
|
||||
inundated_cell_count: number
|
||||
inundated_fraction: number
|
||||
/** Share of the *modelled* cells; null when nothing was modelled. */
|
||||
inundated_fraction: number | null
|
||||
coverage_warning?: string | null
|
||||
resolution_m: number
|
||||
summary: {
|
||||
metric_label: string
|
||||
@@ -633,6 +641,8 @@ export interface BathymetryRasterSelectionResponse {
|
||||
selected_cell_count: number
|
||||
valid_cell_count: number
|
||||
coverage_ratio: number
|
||||
/** Set when the selection was finer than one source cell and widened. */
|
||||
cell_selection_warning?: string | null
|
||||
resolution_m: number
|
||||
vertical_reference: 'mDNG'
|
||||
survey_period: string
|
||||
@@ -690,6 +700,8 @@ export interface ThematicRasterSelectionResponse {
|
||||
selected_cell_count: number
|
||||
valid_cell_count: number
|
||||
coverage_ratio: number
|
||||
/** Set when the selection was finer than one source cell and widened. */
|
||||
cell_selection_warning?: string | null
|
||||
resolution_m: number
|
||||
observation_year: number
|
||||
summary: {
|
||||
@@ -749,7 +761,14 @@ export interface VectorSelectionSummary {
|
||||
metric_unit: string
|
||||
aggregation_method: string
|
||||
primary_metric_key?: string | null
|
||||
/**
|
||||
* Whole features touching the selection. Area and length metrics clip to the
|
||||
* selection, so these two fields say how far the populations diverge.
|
||||
*/
|
||||
feature_count: number
|
||||
fully_covered_feature_count?: number | null
|
||||
partially_covered_feature_count?: number | null
|
||||
selection_edge_warning?: string | null
|
||||
is_estimate: boolean
|
||||
warning?: string | null
|
||||
metrics?: VectorSelectionMetric[]
|
||||
@@ -1435,6 +1454,9 @@ export interface DetectionQaResult {
|
||||
canonical_method: string
|
||||
diagnostic_method: string
|
||||
iou_threshold: number
|
||||
/** Whether the candidates are detector boxes or true footprint polygons. */
|
||||
candidate_geometry_mode?: string
|
||||
interpretation?: string
|
||||
strict_matches: number
|
||||
envelope_matches: number
|
||||
possible_box_to_footprint_mismatch_count: number
|
||||
@@ -1444,7 +1466,37 @@ export interface DetectionQaResult {
|
||||
envelope_recall?: number | null
|
||||
envelope_f1_score?: number | null
|
||||
envelope_mean_iou?: number | null
|
||||
envelope_precision_recall_curve?: PrecisionRecallCurve
|
||||
}
|
||||
precision_recall_curve?: PrecisionRecallCurve
|
||||
}
|
||||
|
||||
/**
|
||||
* Threshold-independent view of a detection run: precision and recall at every
|
||||
* confidence value that occurs, so two models can be compared without both
|
||||
* being read at one arbitrary cut.
|
||||
*/
|
||||
export interface PrecisionRecallCurve {
|
||||
iou_threshold: number
|
||||
reference_count: number
|
||||
candidate_count: number
|
||||
average_precision: number
|
||||
best_f1: number
|
||||
best_f1_threshold: number | null
|
||||
best_f1_precision?: number | null
|
||||
best_f1_recall?: number | null
|
||||
points: PrecisionRecallPoint[]
|
||||
}
|
||||
|
||||
export interface PrecisionRecallPoint {
|
||||
confidence_threshold: number
|
||||
candidate_count: number
|
||||
true_positives: number
|
||||
false_positives: number
|
||||
false_negatives: number
|
||||
precision: number
|
||||
recall: number
|
||||
f1_score: number
|
||||
}
|
||||
|
||||
export type SegmentationModelCapability = DetectionModelCapability
|
||||
|
||||
Reference in New Issue
Block a user