fix: make regional time series discoverable
This commit is contained in:
@@ -7,6 +7,17 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## Sprint 200 Operational time-series handoff (2026-07-15)
|
||||
|
||||
- Removed the dead-end Evolution state that appeared when the current building
|
||||
theme had only one regional snapshot while real historical series existed.
|
||||
- Evolution now opens the first available persisted series automatically and
|
||||
loads its latest snapshot on the map.
|
||||
- Theme cards distinguish genuine multi-snapshot series from current-only
|
||||
sources, including observation counts and year ranges.
|
||||
- Confirmed the deployed PostGIS temporal path with a real Statbel 2021-2025
|
||||
comparison; no synthetic values or parallel frontend calculations were added.
|
||||
|
||||
## Sprint 199 Reviewed accuracy expansion (2026-07-15)
|
||||
|
||||
- Added six leakage-free training AOIs in Arendonk, Dessel, Meerhout, Laakdal,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def read(path: str) -> str:
|
||||
return (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_evolution_mode_falls_back_to_an_available_series() -> None:
|
||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
||||
|
||||
assert "themeTemporalSeriesMap" in workspace
|
||||
assert "availableEvolutionThemes[0]" in workspace
|
||||
assert "activeTemporalSeriesGroups.length > 0" in workspace
|
||||
assert "setActiveThemeId(fallbackTheme.id)" in workspace
|
||||
assert "onOpenDatasetInMap(fallbackDataset)" in workspace
|
||||
|
||||
|
||||
def test_evolution_theme_catalog_distinguishes_history_from_current_only_data() -> None:
|
||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
||||
|
||||
assert "analysisMode === 'current' || evolutionAvailable" in workspace
|
||||
assert "meetmomenten" in workspace
|
||||
assert "Tijdreeks" in workspace
|
||||
assert "Alleen huidige toestand" in workspace
|
||||
assert "Alleen huidig" in workspace
|
||||
assert "Geen tijdreeks beschikbaar" not in workspace
|
||||
|
||||
|
||||
def test_regional_time_series_remain_real_persisted_sources() -> None:
|
||||
regional_operator = read("scripts/provision_regional_timeseries.py")
|
||||
temporal_service = read("backend/app/services/temporal_analysis_service.py")
|
||||
|
||||
assert "provision_mol_population_history.py" in regional_operator
|
||||
assert "provision_official_landuse_timeseries.py" in regional_operator
|
||||
assert "TemporalAnalysisService._get_temporal_dataset" in temporal_service
|
||||
assert "VectorFeatureService.summarize_features_by_bbox" in temporal_service
|
||||
assert "object_changes=object_changes" in temporal_service
|
||||
@@ -8293,3 +8293,43 @@ Next:
|
||||
- Convert the two confirmed Postel-bos challenger errors into a complete,
|
||||
leakage-free hard-negative training sample, add independent empty controls,
|
||||
then train a new inactive candidate through the same fail-closed gate.
|
||||
|
||||
## Sprint 200 - Operational time-series handoff (2026-07-15)
|
||||
|
||||
Implemented:
|
||||
- Reproduced the `Geen tijdreeks beschikbaar` dead end in the deployed regional
|
||||
map: Evolution retained the current-only GRB building snapshot even though
|
||||
persisted population and forest series were available.
|
||||
- Built one temporal-series catalog for every end-user theme from ready
|
||||
persisted datasets. Entering Evolution now retains a comparable theme or
|
||||
automatically opens the first available series and its latest snapshot.
|
||||
- Theme cards now expose observation count and year range for real series.
|
||||
Single-snapshot sources remain visible as `Alleen huidige toestand` but are
|
||||
disabled for comparison instead of opening empty controls.
|
||||
- Kept every comparison behind the existing temporal API and PostGIS
|
||||
`vector_features` aggregation. No browser-side metrics, inferred snapshots
|
||||
or synthetic historical values were introduced.
|
||||
- Updated the top-level Detection documentation values while touching the
|
||||
frontend handoff documentation so the active profile no longer shows an
|
||||
older superseded benchmark.
|
||||
|
||||
Validation evidence:
|
||||
- A live regional Statbel comparison for a Mol bbox returned 87,868.45
|
||||
area-weighted inhabitants in 2021 and 91,497.15 in 2025: +3,628.70 or
|
||||
+4.13%. The API correctly marked the result as an estimate and suppressed
|
||||
unsupported object lineage.
|
||||
- Focused temporal/regional/frontend contract tests passed: 21 tests.
|
||||
- Full readiness passed 601 backend tests, backend compilation, 88 documented
|
||||
API routes, one Alembic head, frontend typecheck/build and shell gates.
|
||||
|
||||
Known limitation:
|
||||
- The complete regional project currently has real 2021-2025 population and
|
||||
2013-2025 modern forest series. Buildings, water, roads and parcels each
|
||||
have one current GRB snapshot at regional scope. The official 1778/1873/1969
|
||||
historical building, forest, water and road series is persisted for Mol but
|
||||
has not yet been partitioned and imported for all 28 municipalities.
|
||||
|
||||
Next:
|
||||
- Generalize the audited historical-land-use operator to the approved regional
|
||||
scope, partition source retrieval by municipality and provision the three
|
||||
official editions without merging their methodology into current GRB.
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
- [x] Reduce end-user noise by moving technical projects, source metadata, QA evidence, provider internals and model diagnostics behind explicit advanced disclosures.
|
||||
- [x] Default Detection Lab to the configured local YOLO asset and present measured model quality and control requirements honestly.
|
||||
- [x] Extend official population and land-use time series from Mol to the approved 28-municipality regional scope.
|
||||
- [x] Make Evolution automatically open an available regional series and distinguish historical themes from current-only snapshots.
|
||||
- [ ] Extend the official 1778/1873/1969 historical buildings, water and roads series from Mol to the approved regional scope with partitioned source audits.
|
||||
- [x] Connect a drawn rectangle to bounded official orthophoto acquisition, local configured-YOLO detection and persisted GRB QA.
|
||||
|
||||
This file now starts with the current implementation status. Older preparation/backlog sections are preserved below as historical planning context and should not be treated as the live sprint board without checking `docs/CODEX_EXECUTION_LOG.md`.
|
||||
|
||||
+5
-1
@@ -6,7 +6,7 @@ The persisted `Kempen Regional Workbench` is the automatic operational data cont
|
||||
|
||||
The user-facing shell is task based: `Kaart`, `Bronnen`, `Kwaliteit`, `Beeldanalyse`, `Downloads`, `Status` and `Beheer`. Internal benchmark projects, raw dataset metadata, provider capabilities, model registry details and QA evidence remain accessible through labelled advanced disclosures instead of competing with the normal workflow.
|
||||
|
||||
Detection defaults to the configured local YOLO asset and automatically selects an available raster and active model asset where possible. The model registry and preflight remain honest when PyTorch, Ultralytics, a local model file or a tile manifest is unavailable. The active building profile is operational but remains review-required: its recorded coverage-aware benchmark is approximately precision 0.590, recall 0.577 and F1 0.582 over seven positive AOIs, with zero detections in the empty-background control. Another training pass is intentionally blocked until the generated false-positive and false-negative review decisions are completed.
|
||||
Detection defaults to the configured local YOLO asset and automatically selects an available raster and active model asset where possible. The model registry and preflight remain honest when PyTorch, Ultralytics, a local model file or a tile manifest is unavailable. The active building profile is operational but remains review-required: its current coverage-aligned benchmark is approximately precision 0.614, recall 0.606 and F1 0.607 over seven positive AOIs, with zero detections in all three pure-empty controls. A reviewed challenger remains inactive because it produced two detections in empty Postel forest.
|
||||
|
||||
Map-driven building analysis uses the documented footprint-IoU `0.25` and
|
||||
distinguishes model candidates from verified buildings. It shows persisted
|
||||
@@ -20,6 +20,10 @@ current, while `Evolution` lets the operator compare an earlier and later
|
||||
snapshot from the same series over a drawn rectangle. Results show units,
|
||||
absolute/percentage change, estimate status and source limitations.
|
||||
Added/removed/modified overlays only appear for stable source identities.
|
||||
When Evolution is opened from a current-only theme, the explorer automatically
|
||||
switches to the first available persisted series. Theme cards then show either
|
||||
the number and range of historical observations or `Alleen huidige toestand`,
|
||||
so a single GRB snapshot can no longer strand the workflow in an empty state.
|
||||
When a theme has multiple valid methodologies, a compact series selector keeps
|
||||
them explicit. Forest therefore defaults to the official modern 2013-2025
|
||||
10 m series, while the separate 1778-1969 historical map series remains
|
||||
|
||||
@@ -613,10 +613,15 @@ export function MapWorkspace({
|
||||
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
|
||||
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
|
||||
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
|
||||
const activeTemporalSeriesGroups = useMemo(
|
||||
() => listThemeTemporalSeries(availableMapDatasets, activeTheme),
|
||||
[activeTheme, availableMapDatasets],
|
||||
const themeTemporalSeriesMap = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
DATA_THEMES.map((theme) => [theme.id, listThemeTemporalSeries(availableMapDatasets, theme)]),
|
||||
) as Record<DataThemeId, TemporalSeriesGroup[]>,
|
||||
[availableMapDatasets],
|
||||
)
|
||||
const activeTemporalSeriesGroups = themeTemporalSeriesMap[activeTheme.id]
|
||||
const availableEvolutionThemes = DATA_THEMES.filter((theme) => themeTemporalSeriesMap[theme.id].length > 0)
|
||||
const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey)
|
||||
?? activeTemporalSeriesGroups[0]
|
||||
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
|
||||
@@ -808,7 +813,10 @@ export function MapWorkspace({
|
||||
}
|
||||
|
||||
const selectDataTheme = (theme: DataTheme) => {
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
const temporalGroup = themeTemporalSeriesMap[theme.id][0]
|
||||
const dataset = analysisMode === 'evolution'
|
||||
? temporalGroup?.items[temporalGroup.items.length - 1] ?? null
|
||||
: themeDatasetMap[theme.id]
|
||||
if (!dataset) {
|
||||
return
|
||||
}
|
||||
@@ -820,6 +828,16 @@ export function MapWorkspace({
|
||||
const setExplorerMode = (mode: 'current' | 'evolution') => {
|
||||
setAnalysisMode(mode)
|
||||
clearTemporalComparison()
|
||||
if (mode !== 'evolution' || activeTemporalSeriesGroups.length > 0) {
|
||||
return
|
||||
}
|
||||
const fallbackTheme = availableEvolutionThemes[0]
|
||||
const fallbackGroup = fallbackTheme ? themeTemporalSeriesMap[fallbackTheme.id][0] : null
|
||||
const fallbackDataset = fallbackGroup?.items[fallbackGroup.items.length - 1]
|
||||
if (fallbackTheme && fallbackDataset) {
|
||||
setActiveThemeId(fallbackTheme.id)
|
||||
onOpenDatasetInMap(fallbackDataset)
|
||||
}
|
||||
}
|
||||
|
||||
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
|
||||
@@ -985,11 +1003,19 @@ export function MapWorkspace({
|
||||
<div className="geo-theme-list">
|
||||
{DATA_THEMES.map((theme) => {
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
const temporalGroups = themeTemporalSeriesMap[theme.id]
|
||||
const temporalGroup = temporalGroups[0]
|
||||
const evolutionAvailable = temporalGroups.length > 0
|
||||
const available = Boolean(dataset) && (analysisMode === 'current' || evolutionAvailable)
|
||||
const active = activeThemeId === theme.id
|
||||
const firstObservation = temporalGroup?.items[0]?.observed_at
|
||||
const lastObservation = temporalGroup?.items[temporalGroup.items.length - 1]?.observed_at
|
||||
const firstYear = firstObservation ? new Date(firstObservation).getUTCFullYear() : null
|
||||
const lastYear = lastObservation ? new Date(lastObservation).getUTCFullYear() : null
|
||||
return (
|
||||
<button
|
||||
className={active ? 'geo-theme-option geo-theme-option-active' : 'geo-theme-option'}
|
||||
disabled={!dataset}
|
||||
disabled={!available}
|
||||
key={theme.id}
|
||||
type="button"
|
||||
onClick={() => selectDataTheme(theme)}
|
||||
@@ -998,9 +1024,23 @@ export function MapWorkspace({
|
||||
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
||||
<span>
|
||||
<strong>{theme.label}</strong>
|
||||
<small>{dataset ? `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar` : 'Bron nog niet ingeladen'}</small>
|
||||
<small>
|
||||
{analysisMode === 'evolution'
|
||||
? evolutionAvailable
|
||||
? `${temporalGroup.items.length} meetmomenten${firstYear && lastYear ? ` · ${firstYear}-${lastYear}` : ''}`
|
||||
: dataset
|
||||
? 'Alleen huidige toestand'
|
||||
: 'Bron nog niet ingeladen'
|
||||
: dataset
|
||||
? `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar`
|
||||
: 'Bron nog niet ingeladen'}
|
||||
</small>
|
||||
</span>
|
||||
<i>{dataset ? 'Beschikbaar' : 'Ontbreekt'}</i>
|
||||
<i>
|
||||
{analysisMode === 'evolution'
|
||||
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
|
||||
: dataset ? 'Beschikbaar' : 'Ontbreekt'}
|
||||
</i>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -1012,7 +1052,7 @@ export function MapWorkspace({
|
||||
{analysisOverlayActive
|
||||
? mapLayerLabel
|
||||
: analysisMode === 'evolution'
|
||||
? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar'
|
||||
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
|
||||
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
|
||||
</strong>
|
||||
<small>
|
||||
@@ -1021,7 +1061,7 @@ export function MapWorkspace({
|
||||
: analysisMode === 'evolution'
|
||||
? activeTemporalSeries.length >= 2
|
||||
? `${activeTemporalSeries.length} officiële meetmomenten · ${formatObservationDate(activeTemporalSeries[0].observed_at)} tot ${formatObservationDate(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at)}`
|
||||
: 'Minstens twee expliciet gedateerde snapshots zijn vereist.'
|
||||
: 'Voor dit thema is nog geen tweede officieel meetmoment beschikbaar.'
|
||||
: activeThemeDataset
|
||||
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatObservationDate(activeThemeDataset.observed_at)}`
|
||||
: activeTheme.description}
|
||||
|
||||
Reference in New Issue
Block a user