fix: make regional time series discoverable
This commit is contained in:
@@ -7,6 +7,17 @@
|
|||||||
|
|
||||||
# Changelog
|
# 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)
|
## Sprint 199 Reviewed accuracy expansion (2026-07-15)
|
||||||
|
|
||||||
- Added six leakage-free training AOIs in Arendonk, Dessel, Meerhout, Laakdal,
|
- 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,
|
- Convert the two confirmed Postel-bos challenger errors into a complete,
|
||||||
leakage-free hard-negative training sample, add independent empty controls,
|
leakage-free hard-negative training sample, add independent empty controls,
|
||||||
then train a new inactive candidate through the same fail-closed gate.
|
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] 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] 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] 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.
|
- [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`.
|
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.
|
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
|
Map-driven building analysis uses the documented footprint-IoU `0.25` and
|
||||||
distinguishes model candidates from verified buildings. It shows persisted
|
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,
|
snapshot from the same series over a drawn rectangle. Results show units,
|
||||||
absolute/percentage change, estimate status and source limitations.
|
absolute/percentage change, estimate status and source limitations.
|
||||||
Added/removed/modified overlays only appear for stable source identities.
|
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
|
When a theme has multiple valid methodologies, a compact series selector keeps
|
||||||
them explicit. Forest therefore defaults to the official modern 2013-2025
|
them explicit. Forest therefore defaults to the official modern 2013-2025
|
||||||
10 m series, while the separate 1778-1969 historical map series remains
|
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 activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
|
||||||
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
|
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
|
||||||
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
|
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
|
||||||
const activeTemporalSeriesGroups = useMemo(
|
const themeTemporalSeriesMap = useMemo(
|
||||||
() => listThemeTemporalSeries(availableMapDatasets, activeTheme),
|
() =>
|
||||||
[activeTheme, availableMapDatasets],
|
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)
|
const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey)
|
||||||
?? activeTemporalSeriesGroups[0]
|
?? activeTemporalSeriesGroups[0]
|
||||||
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
|
const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES
|
||||||
@@ -808,7 +813,10 @@ export function MapWorkspace({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectDataTheme = (theme: DataTheme) => {
|
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) {
|
if (!dataset) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -820,6 +828,16 @@ export function MapWorkspace({
|
|||||||
const setExplorerMode = (mode: 'current' | 'evolution') => {
|
const setExplorerMode = (mode: 'current' | 'evolution') => {
|
||||||
setAnalysisMode(mode)
|
setAnalysisMode(mode)
|
||||||
clearTemporalComparison()
|
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) => {
|
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
|
||||||
@@ -985,11 +1003,19 @@ export function MapWorkspace({
|
|||||||
<div className="geo-theme-list">
|
<div className="geo-theme-list">
|
||||||
{DATA_THEMES.map((theme) => {
|
{DATA_THEMES.map((theme) => {
|
||||||
const dataset = themeDatasetMap[theme.id]
|
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 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 (
|
return (
|
||||||
<button
|
<button
|
||||||
className={active ? 'geo-theme-option geo-theme-option-active' : 'geo-theme-option'}
|
className={active ? 'geo-theme-option geo-theme-option-active' : 'geo-theme-option'}
|
||||||
disabled={!dataset}
|
disabled={!available}
|
||||||
key={theme.id}
|
key={theme.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => selectDataTheme(theme)}
|
onClick={() => selectDataTheme(theme)}
|
||||||
@@ -998,9 +1024,23 @@ export function MapWorkspace({
|
|||||||
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
||||||
<span>
|
<span>
|
||||||
<strong>{theme.label}</strong>
|
<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>
|
</span>
|
||||||
<i>{dataset ? 'Beschikbaar' : 'Ontbreekt'}</i>
|
<i>
|
||||||
|
{analysisMode === 'evolution'
|
||||||
|
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
|
||||||
|
: dataset ? 'Beschikbaar' : 'Ontbreekt'}
|
||||||
|
</i>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -1012,7 +1052,7 @@ export function MapWorkspace({
|
|||||||
{analysisOverlayActive
|
{analysisOverlayActive
|
||||||
? mapLayerLabel
|
? mapLayerLabel
|
||||||
: analysisMode === 'evolution'
|
: analysisMode === 'evolution'
|
||||||
? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar'
|
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
|
||||||
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
|
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
|
||||||
</strong>
|
</strong>
|
||||||
<small>
|
<small>
|
||||||
@@ -1021,7 +1061,7 @@ export function MapWorkspace({
|
|||||||
: analysisMode === 'evolution'
|
: analysisMode === 'evolution'
|
||||||
? activeTemporalSeries.length >= 2
|
? activeTemporalSeries.length >= 2
|
||||||
? `${activeTemporalSeries.length} officiële meetmomenten · ${formatObservationDate(activeTemporalSeries[0].observed_at)} tot ${formatObservationDate(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at)}`
|
? `${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
|
: activeThemeDataset
|
||||||
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatObservationDate(activeThemeDataset.observed_at)}`
|
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatObservationDate(activeThemeDataset.observed_at)}`
|
||||||
: activeTheme.description}
|
: activeTheme.description}
|
||||||
|
|||||||
Reference in New Issue
Block a user