Skip to content
agent-engineering-studioPublic

About

Mappa AI aperta del pericolo di frane, allagamenti e incendi in Italia: 20 regioni, celle da 1 km², adesso e a 72 ore. Open data, PostGIS, spiegazioni con Claude. AI hazard map for landslides, floods and wildfires in Italy.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

665 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Limen — Landslide Monitoring Framework

Limen

🇬🇧 English · 🇮🇹 Italiano

AI monitoring of landslide, flood and wildfire danger across Italy, cell by cell, now and over the next 72 hours. National coverage — all 20 ISTAT regions on a 1 km² grid (~312k cells), open data only, EUPL-1.2 code.

🗺️ Live map: https://limen.agentengineering.it/ (Italian and English) · ☕ Support Limen on Buy Me a Coffee

Python 3.12+ License: EUPL-1.2 uv ruff mypy --strict

Limen (Latin for "threshold") combines morphology, geology, soil moisture, rainfall, seismicity, wildfires and historical archives into a danger score for three hazards on every 1 km² cell of Italy, every hour, with a 72-hour forecast:

  • Landslides: terrain susceptibility (IFFI, PAI, slope, lithology) times rainfall against Caine's intensity-duration thresholds, plus recent earthquakes and wildfires.
  • Floods: ISPRA hydraulic hazard times the stronger of the pluvial branch (forecast rain plus rain already fallen, from the DPC radar-gauge merge) and the fluvial branch (GloFAS discharge against that watercourse's own ordinary high flow).
  • Wildfires: the Canadian Fire Weather Index (Van Wagner), computed per weather node and modulated by fuel and slope.

The numbers come from a deterministic, readable engine; the AI writes the per-region narratives and never changes a number. Stack: Python 3.12 + FastAPI + PostgreSQL 16 + PostGIS, a Vite + React + MapLibre frontend, and multi-channel notifications. The system is built around a Multi-Agent Framework (MAF) that orchestrates ingestion → scoring → explanation, with a data layer portable across local Docker, Neon (serverless) and any managed PostgreSQL.

Quick start

git clone https://github.com/agent-engineering-studio/limen.git
cd limen
uv sync --all-groups
make up                     # Postgres+PostGIS + GeoServer (mcp-geoserver) + frontend
make init                   # migrate → seed 20 regions → ITALICA → bootstrap → calibrate
uv run limen serve          # FastAPI on http://localhost:8080/docs
uv run limen report build   # static HTML report of the riskiest zones → site/index.html
( cd frontend && npm ci && npm run dev )   # map on http://localhost:5173

make init is idempotent and rebuilds all the data on a fresh machine: it applies the migrations, seeds the 20 ISTAT regions (1 km² grid), downloads the e-ITALICA landslide catalogue from Zenodo (the §2.5 backtest truth set), fills the per-cell static factors (IFFI + PAI from the GeoServer PostGIS, slope from the DTM) and calibrates s_static.

On Neon (serverless dev/test): set DB__CONNECTION_STRING with ?sslmode=require and SCHEDULER__CACHE_CLEANUP=apscheduler, nothing else — pg_cron is skipped and the in-process APScheduler takes over the periodic jobs. In production (self-hosted Docker host, no cloud provider): docker compose -f infra/docker/docker-compose.demo.yml up -d --build. The LLM provider is resolved by precedence LLM__PROVIDER > ANTHROPIC_API_KEY > OPENAI_API_KEY > Foundry credentials > llama.cpp. In production all LLM traffic goes through the LiteLLM gateway (:8091): local models run on llama.cpp / llama-swap, and the regional narratives (LLM__MODELS__BRIEFING=quality-cloud) on Claude Sonnet 5.5. The inference stack can be reproduced in containers: see docs/inference.md.

Not a developer? The plain-language documentation in docs/divulgazione/ (Italian) explains without jargon how each cell's risk is computed, what the ML model and the AI agents do (and do not do), which open sources the data come from, and why the LLM runs on our own server. In the SPA, the How it works page (#/come-funziona) gathers the whole engine on one page — data, formulas, forecast, ML, AI, geography: its source is docs/guida/limen.md.

Further reading: docs/demo.md (local demo on a small AOI), docs/architecture.md, docs/openclaw.md, docs/api.md, docs/deployment.md, docs/scoring-model.md, docs/runbook.md, docs/divulgazione/ (for a non-technical audience).

The V1 engine is a pure, interpretable weighted combination (§2.4 of the project document) that reads every weight, threshold and class cutoff from one YAML file per hazard in src/limen/config/hazards/ — named after the enum value (landslide.yaml, flood.yaml, wildfire.yaml). No constants hard-coded in the scoring code. No LLM. No I/O. The same CellFeatureBundle interface also accepts the V2 ML engine.

The architecture is multi-hazard: hazard_type runs through tables, views, API, MCP and A2A, and each hazard brings its own thresholds file, its own schema and its own engine, registered per (hazard, implementation). All three — landslide, flood (#63) and wildfire (#61) — are enabled in production; adding a fourth is a YAML file plus a registration, without touching workflow, API or persistence.


Components and sources

Source What we ingest Cadence Implementation
Open-Meteo hourly precipitation, soil moisture 0–7 / 7–28 cm, snow (forecast); accumulated precipitation (archive). Served by our own instance (compose profile meteo): the public API's 10,000 calls/day were exhausted by the sweep and left flood and wildfire at zero across Italy (#142) live, 30 min cache integrations/openmeteo/ + CachedOpenMeteoClient
ISPRA (GeoServer PostGIS) IFFI inventory (landslides/areas/DGPV, all regions), PAI landslide mosaic; the hydraulic mosaic feeds the flood susceptibility and the landslide H component at init / weekly integrations/geoserver_source/ reads the mcp-geoserver PostGIS
Copernicus EWDS — GloFAS per-node river discharge: the operational forecast (cems-glofas-forecast, control run) downloaded ahead of the per-cell forecast, and each node's ordinary high flow from the multi-year history (glofas_riferimento), the denominator of the fluvial ratio. Without COPERNICUS__EWDS_KEY it falls back to Open-Meteo's GloFAS forecast 4×/day, references nightly integrations/copernicus/ + glofas_previsione / glofas_riferimento
Copernicus Land — EGMS Ortho L3 ground motion (Sentinel-1, 100 m, vertical and east-west): per-cell p90 velocity and the number of valid pixels. Downloaded with the CLMS service key; a nightly step checks whether a new release is out yearly sync + nightly release check integrations/egms/ + limen sync-egms
DPC radar national SRI (1 km / 5 min) as a nowcast trigger, and the radar-gauge «Merging» accumulation, read at each cell centroid so a storm in progress counts in the flood score (#199) every NOWCAST__INTERVAL_MINUTES / per sweep integrations/dpc/
INGV FDSN events (mag ≥ 3.5, last 7 days, AOI bbox); ShakeMap grid event-driven polling integrations/ingv/ + seismic_repo + ObjectStore
EFFIS burnt-area perimeters (a public MapServer, no credentials); bulk Shapefile fallback weekly batch integrations/effis/
Static bootstrap per cell: iffi_density_500 (within 500 m of the cell), distance_to_iffi_m, pai_class_norm, slope_deg (DTM) — set-based PostGIS SQL. Steps whose source has not changed are skipped (#101) one-shot CLI integrations/static_bootstrap/ + limen bootstrap-static
Scoring engine (V1) Caine I/D threshold (re-fitted on ITALICA), sigmoid API, post-fire window modulated by burn severity (FIRMS FRP density: the sum correlates with burnt area at 0.63 and the density at 0.26, so the sum measures the size of the fire, not its intensity), seismic decay, weighted aggregator + 5 classes pure (no I/O) core/scoring/ + MultiFactorScoringEngine
Hazard dimension hazard_type on every risk row and on both dedup ledgers; engines registered per (hazard, implementation) pair; one thresholds file per hazard. Three enabled: landslide, flood, wildfire cross-cutting core/models/hazard.py, core/scoring/registry.py, config/hazards/
Calibrate per-AOI normalisation stats; precompute s_static one-shot limen calibrate + reports/calibrate_<aoi>.md
Event ingest the e-ITALICA catalogue (rainfall-triggered, dated landslides, all of Italy) — the backtest truth set one-shot, auto-download from Zenodo limen ingest-events
Fire history the FIRMS per-country archive (MODIS since 2000, VIIRS S-NPP since 2012): 391,987 Italian detections → 224,156 cell-day events and fire_density on 65,771 cells. Public, no credentials — the download portal asks for Earthdata and delivers by e-mail, but the per-country archives are plain CSV. Only type = 0: without that filter the most "burnt" cell in Italy was the ILVA steelworks in Taranto with 3,308 fire-days, because a steelworks is a high-confidence detection. Sanity check: 16 EFFIS perimeters out of 16 confirmed by a hotspot within 1 km / ±2 days one-shot, cached integrations/firms/archive.py + limen ingest-fire-history
Wildfire ML challenger a per-hazard feature schema (FWI/ISI/DC from the rebuilt chain, CLC fuel, WUI proximity, fire memory, slope, 30-day rain), case-control pseudo-absences, spatial-block CV, a SHA of the matrix for reproducibility. Measured on one season (30,066 samples, 222 blocks): AUC-PR 0.352 against 0.237 for the FWI-only baseline; Random Forest 0.367, logistic 0.284. V1 stays champion; promotion is manual one-shot ml/fire_features.py + LIMEN_TRAIN_HAZARD=wildfire limen train
Flood truth set observed flooded perimeters from Copernicus EMS Rapid Mapping (13 Italian activations, 9,463 polygons, 480 km², 2,354 cells over 9 regions) plus the observation masks, which say where the satellite looked: inside, the absence of a polygon is an observed "not flooded"; outside, an alert is unverifiable, not false. Public, no credentials — FloodCat needs a DPC account and Polaris publishes PDFs one-shot, vector cache integrations/copernicus_ems/ + limen ingest-events --hazard flood
Flood backtest daily-step replay with the forecast as issued (issued, the operational skill) next to the rain that then fell (observed, the ceiling with a perfect forecast) → hit rate, base rate, FAR on observed cell-days only, lead time one-shot limen backtest-flood + reports/backtest_flood_*.md
Backtest replay of a historical window with antecedent rainfall + the e-ITALICA truth set → hit rate / FAR / lead time against the §2.5 targets one-shot limen backtest + reports/backtest_*.md
MAF workflow (V1) AreaResolver → StaticFactors → MeteoFetch → SeismicCheck → FireCheck → [SensorFetch?] → RiskScoring → EscalationGate → RiskAnalyst → Briefing → PersistResult → AlertDispatch one-shot CLI agents/ + limen monitor-once
LLM providers precedence LLM__PROVIDER > Anthropic > OpenAI > Foundry > llama.cpp; the resolver skips cloud providers without an SDK and falls back to llama.cpp (httpx only). Ollama stays selectable with LLM__PROVIDER=ollama. Narratives in Italian and English; RiskAnalyst returns typed JSON resolved at startup agents/llm_factory/resolve_llm_factory
HTTP API /health + /ready, GET /api/aoi/{id}/risk/latest, GET /api/cell/{id}/breakdown, GET /api/aoi, GET /api/alerts, /api/comuni, /api/regioni, /api/tiles/..., OpenAPI on /docs and /redoc FastAPI / uvicorn api/ + limen serve
Scheduled jobs three cadences (#78), in the separate limen worker process: hourly sweep (V1 scoring, persistence, alerts — no LLM); asynchronous regional narratives; nightly run (shadow ML, drift, retrain, partitions, retention, GloFAS references, EGMS release check). The per-cell 72-hour forecast runs at 02/08/14/20 UTC on the latest weather runs; DPC radar nowcast every 15 min, NASA FIRMS hotspots every 45 min, daily national report, weekly ISPRA sync APScheduler in the worker api/jobs/
DPC radar (nowcast) national SRI 1 km / 5 min (DPC radar platform, CC-BY-SA): rain ≥ NOWCAST__MIN_INTENSITY_MMH over a region ⇒ that AOI's workflow starts at once instead of waiting for the hourly tick (45 min cooldown; alerts follow the normal operational path) poll every NOWCAST__INTERVAL_MINUTES integrations/dpc/ + api/jobs/nowcast_monitoring.py
NASA FIRMS hotspots active VIIRS 375 m (S-NPP / NOAA-20 / NOAA-21) and MODIS 1 km detections with ~3 h NRT latency: FIRMS__MIN_HOTSPOTS hotspots inside an AOI ⇒ immediate workflow (cooldown FIRMS__COOLDOWN_MINUTES), and months_since_fire — hence the post-fire F factor — activates within hours instead of waiting for the EFFIS perimeter. Fail-closed: without FIRMS__MAP_KEY the feed is inert and the job is not registered poll every FIRMS__INTERVAL_MINUTES (1 transaction per source over the national bbox) integrations/firms/ + api/jobs/firms_monitoring.py + limen firms-sync
Predictive forecast scoring at now+H hours with Open-Meteo forecast rain (observed + forecast in the same window); champion + ML challenger on the same cells; on-demand report or a scheduled "FORECAST" alert with (AOI, horizon) dedup limen forecast / job every FORECAST__INTERVAL_HOURS agents/workflows/forecast.py + api/jobs/forecast_monitoring.py
MCP limen-ops tool_risk_summary, tool_top_risk_cells, tool_cell_breakdown, tool_recent_alerts, tool_national_report, tool_comune_risk, tool_top_comuni + admin tools (tool_run_monitor, tool_build_report, tool_forecast_history, fail-closed on MCP_ADMIN_TOKEN) for agent gateways (OpenClaw, Claude Desktop) compose service mcp, HTTP 127.0.0.1:8766/mcp mcp/ + limen mcp-serve
A2A (Agent2Agent) JSON-RPC 2.0 interoperability with other agents: Agent Card at /.well-known/agent-card.json, /a2a endpoint with message/send, message/stream (SSE), tasks/get/cancel, push notifications; skills = the read-only queries of limen-ops; tasks persisted (a2a_tasks) mounted on the API, read-only a2a/ + api/endpoints/a2a.py + UI page #/integrazioni
Daily national report aggregated picture of the 20 regions (per-region summary, top national cells, top shadow-ML cells, 24 h alert counts) rendered in deterministic Italian and sent every morning on the notification channels cron REPORT__HOUR_UTC (default 06 UTC) api/jobs/daily_report.py + tool_national_report
Static HTML report a page of the highest-risk zones: clusters of contiguous cells (PostGIS ST_ClusterDBSCAN), an interactive Leaflet mini-map per zone (real zoom/pan on an OSM/Carto basemap, cells as GeoJSON, geographic coordinates highlighted), a deterministic S/M/E/F/H reason + verdict, YlOrRd palette. No LLM, no raster images. It tries the alert threshold (High) and, if no zone exceeds it, steps down (Moderate→Low) and still shows the relatively riskiest areas with an informative banner (no alarm) — the report is never empty. Immutable versioned archive (site/archive/<ts>/ + manifest.json) for later fact-checking; idempotent (skips the rebuild if the assessment has not changed); publishable via GitHub Pages (REPORT__HTML_PUBLISH) at boot + every REPORT__HTML_INTERVAL_HOURS (default 1 h) or on demand site/ + limen report build + api/jobs/html_report.py
Vector tiles materialised view mv_latest_risk (grid cells × enabled hazards ⨝ latest assessment per cell and hazard), refreshed by refresh_mv_latest_risk(); served by pg_tileserv, with risk_at(...) for any hazard and forecast_at(...) for the 72-hour timeline per monitoring cycle migrations 007_map_views.sql, 060_forecast_tiles.sql
Per-comune rollup administrative aggregation mv_comune_risk, one row per comune and hazard: class of the worst cell + profile (counts per class, exposure) + ranking by exposure. Surfaces: map (comune choropleth + alert-cell badge on High+, drill-down), sidebar, ranking, REST /api/comuni + /api/comune/{istat}, MCP/A2A tools top_comuni/comune_risk, report section, comune in alerts. ISTAT boundaries imported with limen seed-comuni; refresh chained to refresh_mv_latest_risk() one-shot seed-comuni + per cycle migration 026_comuni.sql + data/repos/comune_risk.py
Frontend Vite + TS + React + MapLibre GL JS SPA in Italian and English (?lang=en, remembered choice or browser language): risk map with one five-class ramp for all hazards (checked for colour-blind contrast on the dark basemap), legend with labels and ranges (not colour only), 72-hour timeline, alerts, comuni ranking, regional narratives, «Verifiche» fact-check articles, opt-in PAI/IFFI PMTiles overlays public, read-only frontend/
Notifications NotificationChannel protocol + Telegram / MQTT / Email / Webhook (JSON POST to agent gateways, e.g. OpenClaw /hooks with a bearer token); parallel dispatcher with per-channel exception isolation; dedup on alert_dispatches per workflow tick notifications/

Why these choices

Decision Rationale
Engine-agnostic PostgreSQL 16 + PostGIS (no Supabase, no BaaS, no ORM) Same SQL and same code on local Docker, Neon or self-hosted. Only DB__CONNECTION_STRING changes.
asyncpg + a custom PostGIS codec Geometries travel as Shapely objects: no WKB boilerplate, no ORM session lock-in.
Optional pg_cron Neon does not support it. The in-process APScheduler runs the same periodic jobs when the extension is missing.
Object storage behind a Protocol (filesystem / s3) Raster bytes never go into the DB. PostGIS stores only references (path + bbox + CRS + checksum). The s3 backend targets any S3-compatible endpoint (MinIO, R2, B2) via OBJECT_STORE__ENDPOINT_URL — never a cloud SDK.
Plain SQL migrations No Alembic, no ORM. A runner with a schema_migrations table + checksums. Identical behaviour on every Postgres.
Pydantic v2 + structlog Typed configuration and structured logs without reinventing them.
uv + src/ layout Lockfile-first dependency management; the package cannot accidentally import its own test code.
GeoServer as a generic data source mcp-geoserver publishes the ISPRA open data in its PostGIS; ISPRA semantics live only in the Limen loader, not in the MCP (which stays generic).
Copernicus data directly, not through third-party APIs GloFAS from the EWDS and EGMS from the CLMS archive, with our own keys: Open-Meteo cannot host GloFAS, and its cell is one step east of the one it declares (cella_open_meteo) — on the Toce, the wrong cell is worth a factor of ten.

Calibration and validation (§2.5)

The formal test cycle tuned the engine on real data:

  • Caine I/D threshold re-derived from the e-ITALICA catalogue (5,974 intensity-duration pairs measured by rain gauges), T5 lower envelope per macro-region. The historical threshold left 36 % of real landslides below threshold; now ~95 % are above it.
  • Backtest rainfall source: ERA5 from the self-hosted Open-Meteo archive. CERRA (5.5 km) would resolve convective rain better, but the local instance does not serve it: its values came back null, were read as 0 mm, and the backtest was measuring without rain (#184). Now an archive that rains on no node stops the command with backtest.rainfall.empty.
  • IFFI density counted within 500 m of the cell (not of the centroid).
  • Density saturation in YAML (static.iffi_density_saturation), balanced on ITALICA (recall vs precision).

Validation against ground truth (real rain-gauge rainfall): ~63–77 % of real landslides reach ≥ Moderate.

Where we honestly stand (#122). With ERA5 rainfall and antecedents as in production, the «high» class still catches few landslides: in Liguria, November 2019, 11 % of 131 landslides with 0.17 % of cell-hours on alert; in the Marche, May 2014, none of 172. The score ranks cells well within each region, but moving the threshold is not enough: a national threshold tuned on six regions does not hold on the other six, and a per-region threshold does worse at equal alert rate. The limit lies in the rainfall signal as we read it today, and that is the next piece of work. The detailed numbers are in the issue.


Configuration

Loaded from environment variables (and an optional .env) via limen.config.settings.Settings. Nested fields use __ as the delimiter.

Variable Default Notes
DB__CONNECTION_STRING postgresql://limen:limen@localhost:5432/limen PostgreSQL DSN. Add ?sslmode=require for Neon.
OBJECT_STORE__BACKEND filesystem filesystem or s3.
OBJECT_STORE__ENDPOINT_URL empty S3-compatible endpoint (MinIO, R2, B2).
SCHEDULER__CACHE_CLEANUP apscheduler pg_cron or apscheduler. Use APScheduler on Neon.
SCHEDULER__FORECAST_CELLS_HOURS_UTC [2, 8, 14, 20] When the per-cell 72-hour forecast is recomputed.
OPENMETEO__FORECAST_URL / __ARCHIVE_URL empty Self-hosted Open-Meteo; empty ⇒ public API.
COPERNICUS__EWDS_KEY empty EWDS key for the GloFAS forecast and references; empty ⇒ GloFAS via Open-Meteo.
EGMS__SERVICE_KEY empty CLMS service key (single-line JSON) for EGMS downloads and the release check.
LLM__PROVIDER empty Override: anthropic / openai / foundry / ollama / llamacpp. Empty ⇒ llama.cpp unless a cloud key is set.
LLM__LLAMACPP_BASE_URL http://127.0.0.1:8091 Self-hosted inference gateway (OpenAI shape). Not :8081, which is GeoServer. From a container: host.docker.internal.
LLM__MODELS__<ROLE> Claude id Role → model map. Local engines honour only the roles they declare. Never quality-local/glm52 on a synchronous role: the process refuses to start. See docs/inference.md.
LLM__LLAMACPP_TIMEOUT_SECONDS 120 Ceiling for roles without an override; LLM__LLAMACPP_ROLE_TIMEOUT_SECONDS (JSON) per role.
LLM__OLLAMA_BASE_URL http://localhost:11434 Ollama host (from a container: host.docker.internal).
LLM__OLLAMA_MODEL qwen3.6:latest Single Ollama model for every agent role.
SCORING__MODE champion_only shadow runs the ML challenger in parallel (writes model_runs only).
FORECAST__ENABLED true Scheduled predictive sweep; FORECAST__HORIZON_HOURS (48), __INTERVAL_HOURS (6), __MIN_LEVEL (High).
REPORT__HTML_ENABLED true Static HTML report; __HTML_INTERVAL_HOURS (1), __HTML_RUN_AT_STARTUP (true), __HTML_OUTPUT_DIR (site), __HTML_MIN_LEVEL (High), __HTML_MAX_CLUSTERS (50), __HTML_ARCHIVE_KEEP (240), __HTML_PUBLISH (false).
NOTIFICATIONS__WEBHOOK__URL empty Agent gateway (OpenClaw /hooks); __TOKEN for the bearer.
MCP_ADMIN_TOKEN empty Enables the admin MCP tools; unset = disabled (fail-closed).
GEOSERVER_SOURCE__DB_DSN empty DSN of the mcp-geoserver PostGIS (IFFI + PAI).
LIMEN_DEM_RASTER empty DTM GeoTIFF for slope (opt-in).
LIMEN_ITALICA_CSV empty Local e-ITALICA CSV; if absent, auto-download from Zenodo.
LOG_LEVEL / LOG_JSON INFO / false structlog level + JSON output.

See .env.example for the full list with examples, and docs/inference.md for the LLM engine (gateway models, role → model map, constraints on synchronous paths).

Published ports: Docker bypasses ufw

Docker publishes ports by writing to its own iptables chains, which are consulted before ufw's: the host's default deny incoming does not protect a port published by a container. That is why every ports: entry in the compose files has a bind variable defaulting to 127.0.0.1 (LIMEN_PG_BIND, LIMEN_API_BIND, LIMEN_MQTT_BIND, LIMEN_TILESERV_BIND, LIMEN_FRONTEND_BIND, LIMEN_MCP_BIND, LIMEN_GEOSERVER_*_BIND, LIMEN_GEODATA_*_BIND).

The ports stay published — not removed — because the documented uv run limen migrate/seed/... workflow connects to them from the host. Public exposure goes through a reverse proxy on 443 upstream, not by raising a bind to 0.0.0.0.


Database schema (highlights)

aoi, grid_cells, cell_static_factors, iffi_landslides, pai_hazard, landslide_events (dated event catalogue), risk_assessments and model_runs (partitioned by UTC day), latest_risk, training_samples (ML feature store), norm_stats, raster_refs, app_cache, alert_dispatches, forecast_dispatches (predictive alert dedup), fwi_state (the FWI chain per weather node), glofas_riferimento and glofas_previsione, cell_insar_features (EGMS), spiegazioni_regione (regional narratives), seismic_events, fire_perimeters, sensor tables, schema_migrations. All geometries in EPSG:4326; distances/areas computed in EPSG:3035 (LAEA Europe). Immutable SQL migrations in src/limen/data/migrations/NNN_*.sql.


Testing and quality gates

make test                  # unit + integration (testcontainers)
make test-unit             # fast, no Docker
make check                 # lint + typecheck + test
make lint                  # ruff check
make format                # ruff format
make typecheck             # mypy --strict on src/ (run after `uv sync --all-groups`)

Gate before every commit: ruff check + ruff format clean, mypy --strict clean, pytest green. On Apple Silicon the integration tests automatically use imresamu/postgis-arm64 (override with LIMEN_TEST_POSTGIS_IMAGE); LIMEN_TEST_DATABASE_URL points them at an existing PostGIS instead of starting a container.


Status and roadmap

Recently completed:

  • Three hazards in production: landslides, floods and wildfires, with a 72-hour per-cell forecast recomputed four times a day on the latest weather runs (no longer once a night).
  • Italian and English: the SPA, the regional narratives and the national picture are available in both languages.
  • Copernicus data at the source: the GloFAS forecast and each river node's ordinary high flow from the EWDS, EGMS ground motion from the CLMS archive with a service key and a nightly check for new releases.
  • «Regions to watch» page (#/regioni): regions ordered by estimated danger, now and over the next 72 hours, with the official alert alongside and a narrative per hazard written by Claude, with no invented numbers. The report downloads as PDF (/api/regioni/rapporto.pdf, also for a single region).
  • National picture driving the side column: once a hazard is chosen, the «Most exposed comuni» list (/api/comuni?hazard=) and each comune's chart speak only of that hazard, with its own class cutoffs.
  • The first sweep after a restart of the worker starts after two minutes, not an hour: the map no longer freezes at every deploy.
  • National coverage (20 ISTAT regions) with the Caine threshold re-fitted on e-ITALICA.
  • No user authentication, by design (epic #69). Limen is a public map dashboard: map, national picture, alerts, comuni and shadow-model diagnostics open without a login. The login that existed protected the page, not the data — every API feeding the dashboard was already public, so a curl got everything. Launching a sweep, the only privileged operation, stays on the CLI (limen monitor-once) and on MCP behind MCP_ADMIN_TOKEN, fail-closed. The database auth work (#49) remains in git history and can be recovered if an area for Regional or Civil Protection operators is ever needed.
  • Trained ML challenger on 37k samples (e-ITALICA landslides + per-sample antecedent rainfall): AUC-PR 0.60 vs 0.28 for the deterministic engine on the same spatial partition and the same rainfall (limen train; MLflow registry; manual promotion only). Backup/restore of dataset and model: make dump-training / make restore-training.
  • Shadow mode active: the ML challenger runs alongside the champion every night (SCORING__MODE=shadow) and writes its probabilities to model_runs; the deterministic engine stays the only authoritative one.
  • Shadow-ML diagnostics (issue #26): /api/shadow/summary (agreement / divergence per region + real events), shared with the limen shadow-report CLI; a plain-language panel in the dashboard; and a public #/diagnostica-ml page with recall on real events, agreement per region and a per-cell divergence map (neutral palette, not the risk one). Non-authoritative — V1 always drives the alerts.
  • MCP limen-ops (limen mcp-serve): risk, alerts and workflow runs exposed as tools for agent gateways (OpenClaw, Claude Desktop). In compose it is the mcp service (image limen/mcp:0.1, HTTP on 127.0.0.1:8766; LIMEN_MCP_BIND=0.0.0.0 only behind TLS when the gateway is on another machine). OpenClaw hook-up: openclaw mcp set limen-ops '{"url":"http://127.0.0.1:8766/mcp","transport":"streamable-http"}'.
  • webhook notification channel: alerts POST the JSON payload to the agent gateway (e.g. OpenClaw /hooks, bearer token) — pull via MCP for questions, push via webhook for events. Config: NOTIFICATIONS__ENABLED_CHANNELS=["webhook"] + NOTIFICATIONS__WEBHOOK__URL/TOKEN.
  • «How it works» guide in the frontend (#/come-funziona, one sub-route per section) with a simulator that uses the real production formula.
  • Event-driven pipeline (on by default, opt-out FORECAST__ENABLED=false): a predictive sweep at now+48h with forecast rain; cells at level ≥ FORECAST__MIN_LEVEL trigger a predictive alert on the same channels (webhook/OpenClaw, Telegram…), labelled "FORECAST", with a deterministic summary and (AOI, horizon) dedup separate from the operational alerts.
  • DPC radar nowcast (on by default): the national radar network's 1 km SRI acts as a short-horizon trigger — intense rain seen by the radar ⇒ immediate scoring of the affected region.
  • llm-training/: an (assessment → briefing) dataset in Alpaca format + a step-by-step guide for an eventual fine-tuning with LLaMA-Factory.

Next steps:

  • The rainfall signal for landslides (#122): scale is not the lever (see «Where we honestly stand»). We are looking at the rainfall component in the missed cells — hourly intensity against accumulation, the 48-hour window, Caine thresholds per macro-region — always tuning on one group of regions and checking on the others before touching the production engine.
  • Shadow verdict of the ML challenger against the deterministic engine (uv run limen shadow-report); promotion manual only.
  • Reliability plot for the ML diagnostics, once there are enough real events (#30).

Attribution and open licences

Limen uses the following open datasets — attribution is mandatory when the map / briefings are published:

  • ISPRA IdroGEO (IFFI inventory, PAI landslide and hydraulic mosaics) — © ISPRA / Italian authorities, CC-BY 4.0. https://idrogeo.isprambiente.it
  • e-ITALICA (rainfall-triggered landslide catalogue, CNR-IRPI) — CC-BY 4.0, Zenodo DOI 10.5281/zenodo.14204473.
  • Copernicus (Open-Meteo, ERA5, CERRA regional reanalysis) — Copernicus licence, free use with attribution. https://open-meteo.com
  • Copernicus EMS — GloFAS (river discharge, via Open-Meteo and from the Early Warning Data Store) — CEMS-Floods licence: free use, but whoever publishes derived data must state «Contains modified Copernicus Emergency Management Service information [year]» and must not present it as a flood warning. Limen does so on the map (flood and overview), in the home footer, in the flood articles of «Verifiche» and in the regions PDF (frontend/src/lib/attribuzioni.ts, report/regioni_pdf.py). Data restricted by art. 12 of Reg. (EU) 1159/2013 (real-time EFAS) must not be republished. https://ewds.climate.copernicus.eu
  • Copernicus EMS — CEMS Fire (fire danger indices on ERA5, cems-fire-historical-v1) — CC-BY 4.0. https://ewds.climate.copernicus.eu
  • Copernicus Land — EGMS (European Ground Motion Service, Ortho L3 product: mean ground velocity at 100 m from Sentinel-1, vertical and east-west components) — Copernicus Land open licence (Reg. EU 2021/696): free use citing «Generated using European Union's Copernicus Land Monitoring Service information» and stating the modifications. Download requires a CLMS service key (EGMS__SERVICE_KEY, profile on land.copernicus.eu → «API Tokens»). Today it feeds only the ML features, not the map. https://egms.land.copernicus.eu
  • INGV (FDSN event service, ShakeMap) — CC-BY 4.0. https://terremoti.ingv.it
  • EFFIS (burnt-area perimeters) — Copernicus EFFIS terms.
  • NASA FIRMS (active VIIRS/MODIS hotspots, NRT + per-country archive since 2000) — NASA data, free use with attribution; the per-country archive needs no credentials. https://firms.modaps.eosdis.nasa.gov
  • Copernicus EMS Rapid Mapping (observed flooded perimeters, the flood backtest truth set) — © European Union, Copernicus EMS: free with attribution, no credentials. https://rapidmapping.emergency.copernicus.eu
  • DPC radar platform (SRI and «Merging» accumulations) — CC-BY-SA.
  • CORINE Land Cover 2018 Italy (SINAnet/ISPRA) — CC-BY. https://groupware.sinanet.isprambiente.it
  • Geolithological Map of Italy 1:500k (PCN/MASE National Geoportal, Geological Survey of Italy) — CC-BY 4.0.
  • ISTAT (2023 administrative boundaries) — CC-BY 4.0.
  • OpenStreetMap (basemap) — ODbL.

Support Limen

Limen is developed and hosted by Agent Engineering Studio on its own server, with no advertising and no collection of personal data. Autumn brings the rains, and the rains bring landslides and floods: that is when an open map of the danger is most useful.

  • ⭐ Star the repository: it is the simplest way to help people working on civil protection, geology and open data find it.

  • ☕ Buy us a coffee on Buy Me a Coffee (or with the Sponsor button at the top): it covers the server, the weather archives and the time to tune the engine on real data.

    Buy me a coffee

  • 🏛️ Public bodies, municipalities, universities: for trials on your territory, write to us from agentengineering.it.

  • 🔁 Share the live map and tell us, with an issue, where the estimated danger does not look right to you.

How it was built

Limen has been developed with Claude Code since its first commit, on 3 June 2026: most commits are written together with Claude, as the Co-Authored-By line of each one says. The regional narratives are written with Claude, Anthropic's model, and change no number. Limen is an independent project, not affiliated with Anthropic.

Contributing and governance

Limen is a decision-support tool: its outputs are modelled indicators, not official Civil Protection alerts.

Licence

Copyright © 2026 Giuseppe Zileni. Limen is a personal project, developed outside any employment relationship and with the author's own means: the rights holder is the author, who is also the party to whom the CLA grants the licences.

EUPL-1.2 — European Union Public Licence, copyleft that also covers use over a network: whoever offers Limen as a service must publish their modifications. Versions up to v0.6.0-impl-complete remain Apache-2.0.

A commercial licence is available for uses incompatible with copyleft: write to the maintainer. External contributions require the CLA described in CONTRIBUTING.md.

About

Mappa AI aperta del pericolo di frane, allagamenti e incendi in Italia: 20 regioni, celle da 1 km², adesso e a 72 ore. Open data, PostGIS, spiegazioni con Claude. AI hazard map for landslides, floods and wildfires in Italy.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages