Open-source Python framework for modelling cascading physical climate risk in spatial supply-chain economies. The model combines geospatial flood hazards with an agent-based economy of firms and households, so it can represent both direct asset losses and indirect disruptions propagated through labor markets, input linkages, prices, and firm finance.
The current codebase supports hazard-conditional firm adaptation through multiple deployment channels, including:
capital_hardeningfor direct-loss attenuationbackup_suppliersfor input-continuity supportstockpilingandreserved_capacityas additional experimental strategies (stockpilingcurrently means a larger finished-goods buffer, not extra input inventories)
The codebase also supports first-class external shock APIs so callers do not need wrapper-owned monkeypatches to model transport disruption:
HazardRasterEventfor GeoTIFF-backed hazard windowsNodeShockfor explicit direct-damage shocks by coordinates or firm idsLaneShockfor explicit supplier -> buyer transport throttlesRouteShockfor disruptions keyed by topologyroute_dependenciesbuild_model(...)andrun_model(...)for direct Python use from the repo root with the same core controls exposed by the CLI, including input recipes, firm replacement, dynamic supplier search, and transport-shock toggling
- Samples flood hazard rasters directly from GeoTIFF files at firm locations
- Simulates firms and households on a spatial grid with supply-chain and labor interactions
- Records direct hazard exposure, supplier disruption, and never-hit cascade burden diagnostics
- Supports matched-seed ensembles for reproducible scenario comparison
- Writes self-describing summary and member CSVs with
Meta_*fields so runs remain interpretable after the fact
Tested with Python 3.11.
Install the core dependencies:
pip install -r requirements.txtOptional extras:
# Required only for reading the JRC Excel damage-function workbook
pip install openpyxl
# Required only for the interactive dashboard
pip install solaraCLIMADA is no longer required. Flood damage is read directly from the JRC
depth-damage workbook in data/.
Run a small headless test:
python run_simulation.py --param-file quick_test_parameters.jsonRun directly from Python:
from api import run_model
from shock_inputs import NodeShock, RouteShock
model, results_df, agents_df = run_model(
steps=12,
num_households=100,
num_firms=30,
node_shocks=[
NodeShock(
label="Port outage",
hazard_type="PORT_OUTAGE",
intensity=0.4,
start_step=3,
end_step=5,
affected_coords=[(32.5, 29.9)],
)
],
route_shocks=[
RouteShock(
label="Red Sea bottleneck",
route_tag="RED_SEA",
intensity=0.5,
start_step=3,
end_step=8,
)
],
)NodeShock.intensity is normalized to [0, 1] and mapped to a synthetic flood
pseudo-depth of intensity × 6 m before the upstream damage curves are applied.
Run the main paper-style 20-seed comparison:
python run_simulation.py --param-file aqueduct_riverine_parameters_rcp8p5.json --no-hazards --no-adaptation --n-seeds 20 --seed-start 41
python run_simulation.py --param-file aqueduct_riverine_parameters_rcp8p5.json --no-adaptation --n-seeds 20 --seed-start 41
python run_simulation.py --param-file aqueduct_riverine_parameters_rcp8p5.json --adaptation-strategy capital_hardening --adaptation-sensitivity-min 0.5 --adaptation-sensitivity-max 1.5 --n-seeds 20 --seed-start 41
python run_simulation.py --param-file aqueduct_riverine_parameters_rcp8p5.json --adaptation-strategy backup_suppliers --adaptation-sensitivity-min 0.8 --adaptation-sensitivity-max 1.4 --n-seeds 20 --seed-start 41Plot the paper-style time-series comparison from saved CSVs:
python plot_from_csv.py \
--csv-files simulation_baseline_noadaptation_...csv simulation_hazard_noadaptation_...csv simulation_hazard_capital_hardening_...csv simulation_hazard_backup_suppliers_...csv \
--show-ensemble-band \
--plot-start-year 2020Plot the cascade-risk diagnostics:
python plot_cascade_risk.py \
--csv-files simulation_hazard_noadaptation_...csv simulation_hazard_capital_hardening_...csv simulation_hazard_backup_suppliers_...csv \
--show-ensemble-band \
--out cascade_risk.pngRun the matched-seed sensitivity analysis:
python sensitivity_analysis.py --param-file aqueduct_riverine_parameters_rcp8p5.json --n-seeds 10 --seed-start 41Launch the interactive dashboard:
python run_simulation.py --param-file aqueduct_riverine_parameters_rcp8p5.json --vizrun_simulation.py: main CLI for single-seed and multi-seed runsplot_from_csv.py: maintained time-series plotterplot_cascade_risk.py: figure for direct-vs-indirect cascade diagnosticssensitivity_analysis.py: matched-seed continuity-sensitivity sweepsmerge_ensemble_members.py: merge multiple*_members.csvbatches into one ensemblecheck_consistency.py: scenario preflight checksvisualization.py: Solara dashboard
Scenarios are usually defined through JSON parameter files such as
quick_test_parameters.json
and
aqueduct_riverine_parameters_rcp8p5.json.
Key configuration blocks:
rp_files: hazard schedule encoded asRP:START_STEP:END_STEP:HAZARD_TYPE:pathraster_hazard_events: structured equivalent ofrp_filesnode_shocks: explicit direct-damage events with timing, normalized intensity (intensity × 6 mpseudo-depth), and coordinates / firm idslane_shocks: explicit supplier -> buyer transport throttlesroute_shocks: route-tag transport disruptions keyed byroute_dependenciessteps,start_year,steps_per_year: simulation horizontopology: firm locations and supply-chain edgesdamage_functions_path,land_boundaries_path: optional explicit resource paths for environments where the data lives outsideupstream/dataconsumption_ratios: final-demand allocation over household-purchased sectorsfinal_consumption_sectors: sectors eligible for household final demand in calibrated runs; omit to use the legacy default (retail,wholesale,services)sector_coefficients: optional override for per-sector Leontief technical coefficients (labor, input, capital shares); generated byprepare_parameters/calibrate_from_io.py— omit to use built-in defaultsinput_recipe_ranges: optional override for inter-sector intermediate-input fractions; generated byprepare_parameters/calibrate_from_io.pyadaptation: hazard-conditional firm adaptation settings
The CLI --no-hazards flag disables direct raster/node hazard impacts and
transport shocks for a true no-shock baseline. In direct Python use, pass
apply_hazard_impacts=False for no direct hazard damage; apply_transport_shocks
defaults to the same value but can be set explicitly for logistics-only shock
experiments.
Using None as the hazard path encodes an explicit no-hazard warm-up window,
for example:
"10:1:80:FL:None"This is how the shared warm-up period is represented in the main flood experiments.
Each firm carries a continuity-capacity state, implemented in the code as
continuity_capital for backward compatibility. Firms update continuity targets
from hazard-conditioned signals and finance maintenance or new continuity
spending only from residual post-operations cash.
Main settings in the adaptation block:
enableddecision_intervalewma_alphaobservation_radiusadaptation_sensitivity_minadaptation_sensitivity_maxmax_adaptation_incrementcontinuity_decaymaintenance_cost_rateadaptation_strategymin_money_survivalreplacement_frequency
The current manuscript focuses on capital_hardening and backup_suppliers,
but the code also includes stockpiling and reserved_capacity. In the
current implementation, stockpiling increases the finished-goods inventory
buffer rather than building a separate input stock.
Single-seed and multi-seed runs write time-series outputs in the repository root by default.
Main output types:
simulation_*.csv: model-level time series for single-seed runs, or ensemble summaries for multi-seed runssimulation_*_members.csv: member-level aggregate trajectories for each seedsimulation_*_agents.csv: optional combined agent panel when--save-agent-ensembleis enabledsimulation_*_ensemble.png: quick ensemble plot from the runner
Summary and member CSVs include Meta_* fields that record the effective
scenario label, parameter file, topology file, hazard schedule, seed range, and
adaptation settings. This makes saved outputs self-describing and easier to
merge, re-plot, or audit later.
Reproducibility is controlled by the model seed. Hazard severity sampling uses a model-owned NumPy generator rather than global NumPy RNG state, so multiple models can run in the same Python process without coupling their hazard draws.
The model also records cascade-risk diagnostics used in the paper, including:
Ever_Directly_Hit_Firm_ShareNever_Hit_Currently_Disrupted_Firm_ShareNever_Hit_Supplier_Disruption_Burden_ShareNever_Hit_Production_ShareNever_Hit_Capital_Share
Stock-flow regression test:
PYTHONPYCACHEPREFIX=/tmp/codex-pycache python -m pytest tests/test_stock_flow_closure.py -qScenario consistency preflight:
PYTHONPYCACHEPREFIX=/tmp/codex-pycache python check_consistency.py --param-file aqueduct_riverine_parameters_rcp8p5.json --no-hazards --no-adaptation --steps 80 --min-tail-production 1200 --min-tail-consumption 700 --min-tail-labor 600 --min-tail-active-firms 30These checks are used to verify accounting closure, tail activity, and scenario sanity before committing to longer ensemble runs.
Firm locations and supply chains are provided through topology JSON files such as:
Hazard rasters live under data/.
Optional preprocessing utilities are in
prepare_hazard/.
Example preprocessing command:
python prepare_hazard/preprocess_geotiff.py \
--input data/raw/*.tif \
--model-grid \
--resolution 0.25 \
--resampling mean \
--output-dir data/model_grid/The model's sector-level technical coefficients, inter-sector supply
fractions, and household consumption ratios can be calibrated from real
national accounts data using the scripts in prepare_parameters/.
Without calibration the model uses internally hand-tuned defaults.
| Parameter | Source | Model target |
|---|---|---|
| Labor/input/capital share per sector | Labor compensation and GOS / gross output | sector_coefficients in param file |
| Inter-sector supply fractions | A matrix (column-normalized IO flows) | input_recipe_ranges in param file |
| Household consumption ratios | Final demand vector (household column) | consumption_ratios in param file |
| Firms per sector in topology | Gross output shares | generate_topology.py input |
WIOD 2016 Nov release — 44 countries × 56 ISIC Rev.4 sectors, years 2000–2014. Free download, no registration required:
Two table types are available; choose based on your use case:
| Table type | File pattern | Best for | Format | Labor/capital data |
|---|---|---|---|---|
| WIOT (World IO Table) | WIOT{YEAR}_Nov16_ROW.xlsb |
Generic or multi-country calibration | Binary Excel; requires pyxlsb |
Derived from WIOT VA row (60/40 split) |
| NIOT (National IO Table) | {COUNTRY}_NIOT_nov16.xlsx |
Country-specific calibration | Standard Excel | From Socio_Economic_Accounts.xlsx |
Place downloaded files in data/io/. The SEA file Socio_Economic_Accounts.xlsx
(all countries in one workbook) is needed for accurate labor/capital coefficients
when using NIOT.
Important units note: NIOT files are in millions of USD at current prices; the SEA file is in millions of national currency. The script automatically converts SEA values to USD using the NIOT value-added row as a scaling reference. For the WIOT (global aggregate), the SEA cannot be used across countries and the VA row is used directly instead.
Available NIOT country codes: AUS AUT BEL BGR BRA CAN CHE CHN CYP CZE DEU DNK ESP EST FIN FRA GBR GRC HRV HUN IDN IND IRL ITA JPN KOR LTU LUX LVA MEX MLT NLD NOR POL PRT ROU RUS SVK SVN SWE TUR TWN USA
prepare_parameters/niot_concordance_default.json
maps the 56 ISIC Rev.4 sector codes (A01, B, C10-C12, …) used by both the
WIOT and NIOT to the model's 7 sectors (commodity, agriculture,
components, manufacturing, retail, wholesale, services). Edit this
file to adjust the mapping for your region or research focus. Each ISIC code
must appear in exactly one model sector.
The default concordance keeps producer sectors economically literal:
consumer-goods manufacturing codes such as C10-C12, C13-C15, and C31_C32 stay
in manufacturing rather than being relabeled as retail. Household final
demand is handled separately by the calibrated consumption_ratios and
final_consumption_sectors keys. This lets households buy directly from
manufacturing, agriculture, or other mapped sectors when the IO final-demand
vector records those purchases, without changing the sector's supply-chain role.
1. Calibrate parameters from IO table
Generic/global calibration using the WIOT (recommended default):
python prepare_parameters/calibrate_from_io.py \
--wiot-file data/io/WIOT2014_Nov16_ROW.xlsb \
--out prepare_parameters/calibrated_parameters.jsonCountry-specific calibration using a NIOT (e.g. India):
python prepare_parameters/calibrate_from_io.py \
--niot-file data/io/IND_NIOT_nov16.xlsx \
--sea-file data/io/Socio_Economic_Accounts.xlsx \
--year 2014 \
--out prepare_parameters/calibrated_parameters_IND.jsonCalibration uses IO shares as production guidance, not as a zero-inventory startup state:
-
Self-supply is removed (diagonal of A matrix zeroed). IO tables contain large intra-sector flows (energy firms buying energy, steel mills buying steel). These are real in national accounts, but at the model-sector level they mostly represent within-sector trade rather than a distinct cascade channel. Pass
--self-supplyto retain them for pure IO-table analysis. -
Recipes are sparsified by cumulative coverage. For each buyer sector, the calibration keeps the largest supplier-sector shares until they explain 90% of intermediate input cost, then renormalizes. This avoids treating every tiny IO flow as a hard Leontief requirement while preserving the dominant cascade channels.
-
Circular economies bootstrap from calibrated inventories. The model initializes firms with demand-consistent output, working capital, capital, and input inventories, so IO cycles do not have to be broken by deleting downstream dependencies. The default
--primary-sectors commodity agricultureis a narrow resource-sector closure: natural-resource sectors can produce from labor, capital, and land/resource endowments, while services and manufacturing remain connected through IO demand.
At runtime, recipe shares guide procurement by sector while aggregate intermediate input availability binds production. This matches the model's reduced-form treatment of intermediate inputs as substitutable within a calibrated input bundle. Procurement is two-pass: a recipe-guided per-sector pass preserves the calibrated cost mix when supply allows, followed by an aggregate top-up that fills any residual demand from the cheapest available technical supplier across recipe sectors. The hazard-related supplier disruption signal is computed from the aggregate residual after substitution, so a sector-level gap that has been filled across the input bundle is not flagged as a supplier disruption. Unit cost used in pricing and working-capital sizing weights supplier prices by the calibrated recipe shares, falling back to an unweighted mean when no recipe is configured.
Year is auto-detected from the WIOT filename (WIOT2014 → 2014) and defaults to
2014 for NIOT. Country is auto-detected from the NIOT filename (IND_NIOT → IND).
This writes an intermediate JSON file containing sector_coefficients,
input_recipe_ranges, consumption_ratios, and sector_output_shares. The
checked-in calibration keeps only the final riverine RCP8.5 run artifacts; this
intermediate file can be regenerated when the topology or run parameters need to
be rebuilt.
Optional flags:
--self-supply— retain intra-sector diagonal flows (not recommended for simulation; see note above)--recipe-coverage FLOAT— keep dominant supplier links until this share of intermediate input cost is represented (default 0.90)--primary-sectors SECTOR [SECTOR ...]— explicitly declare labor/capital-only primary producers (default:commodity agriculture)--min-recipe-share FLOAT— ignore very small supplier shares before cumulative coverage selection (default 0.02)--year INT— override year selection--country ISO3— override country detection for NIOT
2. Generate a calibrated firm topology
python prepare_parameters/generate_topology.py \
--calibrated-params prepare_parameters/calibrated_parameters.json \
--total-firms 100 \
--bbox -180.0 -60.0 180.0 70.0 \
--land-shapefile data/ne_110m_admin_0_countries/ \
--out riverine_firm_topology_100_calibrated.json \
--seed 42--bbox is LON_MIN LAT_MIN LON_MAX LAT_MAX. The example covers South/Southeast
Asia; the checked-in riverine calibration uses the broader -180 -60 180 70
extent shown above. The Natural Earth shapefile
(data/ne_110m_admin_0_countries/) is already in the repository and is used
automatically as the land mask.
Firms are allocated across sectors in proportion to the gross output shares
from the IO table. Supply edges are drawn by inverse-distance-weighted
sampling, consistent with the calibrated input_recipe_ranges. A post-pass
ensures every buyer firm has at least one supplier per required sector.
3. Build a run parameter file
python prepare_parameters/build_run_parameters.py \
--calibrated-params prepare_parameters/calibrated_parameters.json \
--topology riverine_firm_topology_100_calibrated.json \
--rp-from aqueduct_riverine_parameters_rcp8p5.json \
--out calibrated_rcp8p5_parameters.jsonThis merges the calibrated economic parameters with the retained riverine topology and RCP8.5 hazard schedule. Common overrides:
# Change horizon, household count, seed, or strategy:
python prepare_parameters/build_run_parameters.py \
--calibrated-params prepare_parameters/calibrated_parameters.json \
--topology riverine_firm_topology_100_calibrated.json \
--steps 200 --num-households 1000 --seed 7 \
--adaptation-strategy backup_suppliers \
--out calibrated_rcp8p5_parameters.json
# Add a hazard schedule (no-hazard warmup + flood event):
python prepare_parameters/build_run_parameters.py \
--calibrated-params prepare_parameters/calibrated_parameters.json \
--topology riverine_firm_topology_100_calibrated.json \
--rp-files "10:1:80:FL:None" "10:81:200:FL:data/processed/flood.tif" \
--steps 200 \
--out calibrated_hazard_parameters.json
# Inherit adaptation settings from an existing parameter file:
python prepare_parameters/build_run_parameters.py \
--calibrated-params prepare_parameters/calibrated_parameters.json \
--topology riverine_firm_topology_100_calibrated.json \
--adaptation-from aqueduct_riverine_parameters_rcp8p5.json \
--out calibrated_rcp8p5_parameters.json
# Inherit a full hazard schedule from an existing parameter file
# (also inherits steps, start_year, steps_per_year, grid_resolution):
python prepare_parameters/build_run_parameters.py \
--calibrated-params prepare_parameters/calibrated_parameters.json \
--topology riverine_firm_topology_100_calibrated.json \
--rp-from aqueduct_riverine_parameters_rcp8p5.json \
--adaptation-from aqueduct_riverine_parameters_rcp8p5.json \
--out calibrated_rcp8p5_parameters.jsonDefaults: 80 steps (20 years at 4 steps/year), 5× households per firm,
start_year inferred from the calibration metadata, capital_hardening
adaptation. All defaults are documented in build_run_parameters.py --help.
Then run as usual:
python run_simulation.py --param-file calibrated_rcp8p5_parameters.jsonThe sector_coefficients key overrides the model's default hand-tuned values
for that run only; existing parameter files without this key are unaffected.
- The IO table gives value-share coefficients (dollars of input per dollar of output). The model's Leontief coefficients are in physical units. These are consistent when prices are normalised to 1.0 at initialisation, which holds for the model's startup price convention.
- If
LABOR_COEFF + INPUT_COEFF > 1.0for any sector the calibration script emits a warning. This usually indicates an unusual concordance mapping or a sector with negative gross operating surplus in the source data. - Calibrated parameter files include
final_consumption_sectors, so household final demand is not limited to the legacy built-in set (retail,wholesale,services). Direct household purchases from mapped manufacturing, agriculture, commodity, or components sectors are preserved inconsumption_ratios. - Firm prices follow a slow-moving normal unit-cost anchor plus a scarcity markup. The markup rises when finished-goods inventories are below target or prior-period sell-through is high, and falls when inventory is abundant. Cost increases pass through faster than cost decreases; cost decreases pass through more slowly when scarcity is high. This prevents input-constrained downturns from mechanically turning into deflation just because wages and current input costs fall in the same period, while still allowing deflation when weak-demand conditions persist.
- Dividend payouts distribute all firm cash above the operating reserve when the firm is in a dividend-eligible state (no current direct loss and no pending deferred capital repair). Capping dividends at current-period earnings would otherwise strand cash retained from hazard-suppressed periods on firm balance sheets and break the closed-economy circular flow when shocks recur. Distributing the full available cash makes hazards manifest as inflation and real-output effects rather than as a monotone wealth transfer from households to firms.
agents.py Agent behavior
model.py Main EconomyModel
run_simulation.py CLI runner
plot_from_csv.py Maintained time-series plotter
plot_cascade_risk.py Cascade-risk figure
sensitivity_analysis.py Matched-seed sensitivity sweeps
merge_ensemble_members.py Ensemble merge utility
check_consistency.py Scenario preflight checks
visualization.py Solara dashboard
prepare_hazard/ Raster preprocessing utilities
prepare_parameters/ IO-table calibration and topology generation
data/ Hazard rasters and JRC damage workbook
manuscript/ Current paper source and submission assets
presentation/ Slides
tests/ Regression tests
This project is released under the terms of the
LICENSE.