Skip to content

Alluvione: due trigger per nodo, suscettibilità dal mosaico ISPRA (Fase 3a) - #97

Merged
gzileni merged 2 commits into
mainfrom
feat/flood-hazard
Sep 5, 2026
Merged

Alluvione: due trigger per nodo, suscettibilità dal mosaico ISPRA (Fase 3a)#97
gzileni merged 2 commits into
mainfrom
feat/flood-hazard

Conversation

@gzileni

@gzileni gzileni commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Chiude #63. Terzo e ultimo pericolo: tutti e tre i membri dell'enum hanno ora schema, motore, configurazione e workflow, e check_scorable passa per ognuno. Sblocca #58.

score = suscettibilità × max(pluviale, fluviale)

Un massimo, non una somma. I due trigger sono modi diversi perché la stessa cella finisca sott'acqua; una cella non è più allagata perché potrebbero accadere entrambi. Sommarli lascerebbe che due segnali moderati ne inventino uno grave.

La suscettibilità moltiplica, al contrario del termine base dell'incendio: lì la roccia nuda resta sopra zero perché il fuoco arriva da fuori, qui l'acqua va in basso e un crinale non si allaga per quanto piova accanto.

Una cella non mappata non è una cella sicura. Il mosaico ISPRA copre i bacini ufficialmente studiati: fuori mappa la cella prende un valore di ripiego basso e il breakdown dichiara mapped: false — visibile, ma incapace di arrivare in classe alta con la sola pioggia.

Il dato che il progetto aveva già in casa

Il mosaico idraulico ISPRA alimentava il componente H del motore frane da tempo. Qui smette di essere un contributo a un altro pericolo e diventa la suscettibilità di un pericolo suo, senza nuovo ingest.

I segnali erano uno per AOI: la correzione più importante

La prima stesura leggeva pioggia e portata al centroide dell'AOI e li copiava su ogni cella. È l'errore che questo repo documenta già per la pioggia — 13 mm al centroide della Puglia contro 77 mm sulle celle che avevano davvero ceduto — e per l'alluvione è peggio, perché i due segnali sono il motore: un bacino in piena spingeva oltre la soglia di allerta celle di un bacino diverso.

Ora entrambi sono campionati per nodo e ogni cella legge il più vicino. Misurato su 400 celle di Basilicata: 10 valori di pioggia distinti invece di uno, 7 celle con segnale fluviale e 393 senza — che è la risposta giusta, perché lì il fiume non c'è.

Il passo passa da 0.25° a 0.1°: a 25 km solo 2 nodi su 35 cadevano su un corso d'acqua modellato da GloFAS, quindi il ramo fluviale era di fatto spento; a 0.1° sono 20 su 176.

Il rapporto di portata è patologico sui rigagnoli — un corso d'acqua da 0.01 m³/s che arriva a 0.6 vale 62. Misurato: un massimo puro dava 62.0 dove i fiumi veri erano a 1.25. Quindi un nodo conta come fiume solo se la sua portata di base è almeno il 10% della maggiore dell'AOI; gli altri riportano None, che è "nessun corso d'acqua qui", non "un fiume in magra". Verificato: Po in Piemonte 1.04, Basilicata 1.25, fiumare pugliesi 7.5.

EFAS: non implementato, e dichiarato

L'issue chiede le soglie di tempo di ritorno EFAS (2/5/20 anni) via CDS API. Richiedono un account CDS e una dipendenza NetCDF per leggere il download: un client che non si può eseguire non è un client. GloFAS via Open-Meteo dà il ramo fluviale oggi, senza credenziali, ed era già ingerito. Il punto d'innesto è il blocco fluvial in YAML, che oggi legge un rapporto e domani leggerebbe un tempo di ritorno.

Sette difetti corretti in revisione

Oltre ai segnali per AOI, tre di sostanza:

  • La finestra della soglia non era quella che si andava a prendere. window_hours: 24 era validato, pubblicato nella scheda del modello e mai letto: il motore riceveva 72 h di accumulo e lo confrontava con soglie calibrate su 24 h, quindi un autunno normale saturava il trigger. Il campo del segnale è condiviso con l'ML feature store, dove il nome è una chiave persistita e rinominarlo invaliderebbe i modelli — quindi le soglie si calibrano sulla finestra vera (80/250 mm su 72 h) e il workflow legge window_hours per andare a prendere esattamente quella.
  • Umidità del suolo ignota valeva "suolo saturo", cioè la lettura più allarmante: una risposta degradata alzava il trigger di ~2.2× su un'intera AOI, e il mio stesso test ci si era appoggiato sopra. Ora l'ignoto smorza a metà fra i due estremi che la configurazione già definisce.
  • Una richiesta troppo grande faceva morire lo sweep: 677 punti in una query string tornano con un corpo che non è JSON, e resp.json() alzava fuori dalla degradazione. Ora lotti da 100, e un lotto fallito contribuisce dizionari vuoti così la mappatura sui nodi resta allineata.

Più due residui nei test — uno snapshot del registry preso dopo la sostituzione, che rimetteva lo stub come motore flood permanente, e un round-trip che smontava il motore vero senza rimetterlo — e un commento obsoleto in .env.example.

Test ristrutturati

Sette test usavano flood come "pericolo che nessun file conosce". Ora lo scenario si costruisce — togliendo il pericolo vero e rimettendolo, o spostando il risolutore di percorso — invece di sfruttare un buco che non esiste più. È anche più fedele: prova il meccanismo, non la coincidenza di quale pericolo è ancora scoperto.

Un limite da sapere

Senza il mosaico idraulico caricato (serve GEOSERVER_SOURCE__DB_DSN) ogni cella prende susceptibility.unmapped e la mappa alluvione è uniforme: leggibile, ma non discriminante. È un caricamento dati, non codice mancante — e a differenza di CORINE per l'incendio, questo layer il progetto lo ingerisce già.

Gate

  • ruff check + ruff format --check, mypy --strict: puliti
  • pytest tests/unit: 681 passati
  • Frontend: tsc -b, ESLint, build puliti; 62 test Vitest in 20 file
  • limen monitor-once --hazard flood end-to-end su dati reali
  • Integrazione, sottoinsieme mirato: 34 passati (hazard API, API, viste mappa, dispatch alert, rollup comunale) più FWI/backtest/lifespan e il job orario a tre pericoli

La suite completa (140 test) non è stata eseguita: richiede ~4,5 ore su questa macchina per la ragione documentata su #90 — un solo test, test_weekly_idrogeo_sync_handles_unreachable_isp_gracefully, ne prende 1678 s perché mocka ISPRA a 503 e la policy tenacity condivisa ritenta i 5xx con backoff per ogni AOI seminata.

Refs #58

…viousness (Fase 3a)

Il terzo pericolo, e quello che il progetto aveva già in casa: il mosaico
idraulico ISPRA alimentava il componente H del motore frane da tempo, e qui
diventa la suscettibilità di un pericolo suo.

    score = suscettibilità × max(pluviale, fluviale)

**Un massimo, non una somma.** I due trigger sono modi diversi perché la
stessa cella finisca sott'acqua; una cella non è più allagata perché
potrebbero accadere entrambi. Sommarli lascerebbe che due segnali moderati ne
inventino uno grave.

**La suscettibilità moltiplica**, al contrario del termine `base`
dell'incendio: lì la roccia nuda resta sopra zero perché il fuoco arriva da
fuori, qui l'acqua va in basso e un crinale non si allaga per quanto piova
accanto. Sotto `susceptibility.floor` il motore va a zero.

**Una cella non mappata non è una cella sicura.** Il mosaico copre i bacini
ufficialmente studiati: "non studiato" non è "non allagabile", quindi fuori
mappa prende un valore di ripiego basso — visibile ma incapace di arrivare in
classe alta con la sola pioggia — e il breakdown dichiara `mapped: false`.

## La sonda fluviale, che era rotta

Il segnale GloFAS esistente campiona il **centroide** dell'AOI, che non sta
quasi mai su un fiume modellato: misurato, 0.0 per la Basilicata e `None` sul
Basento. Per il componente H delle frane era un bonus marginale; per
l'alluvione è metà del motore.

`basin_max=True` sonda il reticolo e prende il rapporto peggiore. Ma il
rapporto è patologico sui rigagnoli — un corso d'acqua da 0.01 m³/s che
arriva a 0.6 vale 62, e un massimo puro pesca esattamente quelli: misurato,
62.0 dove i fiumi veri erano a 1.25. Quindi contano solo i punti la cui
portata di base è almeno il 10% della maggiore dell'AOI. Verificato: Po in
Piemonte 1.04, Basilicata 1.25, fiumare pugliesi 7.5.

**Spento per le frane di proposito**: il campione V1 è stato valutato con il
segnale del centroide, e cambiarlo sotto lo stesso nome sposterebbe i suoi
numeri senza un backtest.

## EFAS: non implementato, e dichiarato

L'issue chiede le soglie di tempo di ritorno EFAS (2/5/20 anni) via CDS API.
Richiedono un account CDS e una dipendenza NetCDF per leggere il download: un
client che non si può eseguire non è un client, ed è la stessa trappola che
ho evitato altrove. GloFAS via Open-Meteo dà il ramo fluviale **oggi, senza
credenziali**, ed è già ingerito. Il posto dove EFAS entrerebbe è un blocco
`fluvial` in YAML che oggi legge un rapporto e domani leggerebbe un tempo di
ritorno.

## Il resto

`imperviousness_norm` da CLMS amplifica il **solo** ramo pluviale: il cemento
non fa crescere un fiume, fa scorrere la stessa pioggia. NULL finché il raster
non è configurato, e NULL non è 0.

Palette PuBu sulla mappa — l'acqua è l'unica delle tre che non si legge come
"caldo", ed è giusto che sia l'unica fredda. Il popup distingue il segnale del
fiume da quello della pioggia: sono mitigati da cose diverse.

## Test ristrutturati

Sette test usavano `flood` come "pericolo che nessun file conosce". Ora ogni
membro dell'enum ha schema, motore e configurazione, quindi lo scenario si
**costruisce** — togliendo il pericolo vero e rimettendolo — invece di
sfruttare un buco che non esiste più. È anche più fedele: prova il
meccanismo, non la coincidenza di quale pericolo è ancora scoperto.

Chiude #63. Refs #58
**I due segnali erano uno per AOI, copiati su ogni cella.** È l'errore che
questo repo documenta già per la pioggia — 13 mm al centroide della Puglia
contro 77 mm sulle celle che avevano davvero ceduto — e per l'alluvione è
peggio, perché i due segnali *sono* il motore: un bacino in piena spingeva
oltre la soglia di allerta celle di un bacino diverso.

Ora entrambi sono campionati per nodo e ogni cella legge il più vicino.
Misurato su 400 celle di Basilicata: 10 valori di pioggia distinti invece di
uno, 7 celle con segnale fluviale e 393 senza — che è la risposta giusta,
perché lì il fiume non c'è.

Il passo del reticolo passa da 0.25° a 0.1°: a 25 km solo 2 nodi su 35
cadevano su un corso d'acqua modellato da GloFAS, quindi il ramo fluviale era
di fatto spento; a 0.1° sono 20 su 176. Gli scalari di AOI restano per il
componente H delle frane, che li ha sempre usati.

**La finestra della soglia non era quella che si andava a prendere.**
`pluvial.window_hours: 24` era validato, pubblicato nella scheda del modello e
mai letto: il motore riceveva l'accumulo a 72 h e lo confrontava con soglie
calibrate su 24 h, quindi 75 mm in tre giorni tranquilli davano 0.32 dove la
finestra dichiarata dà 0, e un autunno normale saturava il trigger.

Il campo che porta il segnale si chiama `flood_forecast_rain_72h_mm` ed è
condiviso con l'ML feature store, dove il nome è una chiave persistita:
rinominarlo invaliderebbe i modelli addestrati. Quindi la finestra resta 72 h
e le **soglie si calibrano su quella** — 80 mm e 250 mm — e il workflow legge
`window_hours` per andare a prendere esattamente quella finestra, così le due
cose non possono più divergere. L'accumulo è ora delimitato a ore e non a
giorni interi, che allungavano una finestra di 72 h fino a 96.

**Umidità del suolo ignota valeva "suolo saturo".** Saltare lo smorzamento è
numericamente identico al caso più allarmante: una risposta Open-Meteo
degradata alzava il trigger di ~2.2× su un'intera AOI, e il mio stesso test ci
si era appoggiato sopra. Ora l'ignoto smorza a metà fra i due estremi che la
configurazione già definisce.

**Una richiesta troppo grande faceva morire lo sweep.** 677 punti in una query
string tornano con un corpo che non è JSON, e `resp.json()` alzava un
`ValueError` fuori dalla degradazione. Ora le richieste vanno a lotti di 100 e
un lotto fallito contribuisce dizionari vuoti, così la mappatura posizionale
sui nodi resta allineata.

Più due residui nei test — uno snapshot del registry preso dopo la
sostituzione, che rimetteva lo stub come motore flood permanente, e un
round-trip che smontava il motore vero senza rimetterlo — e un commento
obsoleto in `.env.example`.

Refs #63
@gzileni
gzileni merged commit 17b09f7 into main Sep 5, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant