Manual Damage Curves Tutorial#

This tutorial demonstrates how to perform a damage analysis using custom/manual damage curves in RA2CE.
Manual curves allow you to define site-specific or locally calibrated hazard–damage relationships rather than relying on generic reference curves.

This is particularly useful when:

  • You have local historical damage data.

  • You want to account for specific infrastructure types.

  • You need to test alternative vulnerability scenarios.


Step 1: Define project paths#

As usual, define your project folder and subdirectories:

[1]:
from pathlib import Path

root_dir = Path("data", "damage_manual")
static_path = root_dir.joinpath("static")
hazard_path = static_path.joinpath("hazard")
network_path = static_path.joinpath("network")
output_path = root_dir.joinpath("output")

Step 2: Configure the road network and hazard#

The network is downloaded from OpenStreetMap (OSM), clipped to a region polygon (polygon.geojson).
We specify which road types should be included in the analysis.
[2]:
from ra2ce.network.network_config_data.enums.road_type_enum import RoadTypeEnum
from ra2ce.network.network_config_data.enums.source_enum import SourceEnum
from ra2ce.network.network_config_data.enums.network_type_enum import NetworkTypeEnum
from ra2ce.network.network_config_data.network_config_data import NetworkSection
from ra2ce.ra2ce_handler import Ra2ceHandler


network_section = NetworkSection(
    network_type=NetworkTypeEnum.DRIVE,
    source=SourceEnum.OSM_DOWNLOAD,
    polygon=static_path.joinpath("polygon.geojson"),
    save_gpkg=True,
    road_types=[
        RoadTypeEnum.SECONDARY,
        RoadTypeEnum.SECONDARY_LINK,
        RoadTypeEnum.PRIMARY,
        RoadTypeEnum.PRIMARY_LINK,
        RoadTypeEnum.TRUNK,
        RoadTypeEnum.MOTORWAY,
        RoadTypeEnum.MOTORWAY_LINK,
    ],
)
c:\Users\hauth\miniforge3\envs\ra2ce_env\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
We provide hazard input in the form of GeoTIFF raster files (e.g., flood depth maps).
RA2CE will overlay these rasters with the road network to compute hazard intensities for each asset.
[3]:
from ra2ce.network.network_config_data.enums.aggregate_wl_enum import AggregateWlEnum
from ra2ce.network.network_config_data.network_config_data import HazardSection

hazard_section = HazardSection(
    hazard_map=[Path(file) for file in hazard_path.glob("*.tif")],
    aggregate_wl=AggregateWlEnum.MEAN,  # mean water depth used in analysis
    hazard_crs="EPSG:4326",  # ensure hazard map is in EPSG:4326 projection
)

We combine the network and hazard information into a single configuration object.

[4]:
from ra2ce.network.network_config_data.network_config_data import NetworkConfigData

network_config_data = NetworkConfigData(
    root_path=root_dir,
    static_path=static_path,
    network=network_section,
    hazard=hazard_section
)
network_config_data.network.save_gpkg = True

Step 3: Define the damage analysis#

Here, we specify that RA2CE should perform a damage analysis using manual damage curves (MAN) with the class AnalysisSectionDamages and the attribute damage_curve set to DamageCurveEnum.MAN For manual damage curves, it is important to also specify the input data in the folder path in the config input_path. This is the location where the custom manual curves will be defined and placed (see next step).

The event type can be set to EVENT if damages are to be calculated for the hazard maps only (example below), or to RETURN_PERIOD if the analysis should estimate risk over a specified return period (see tutorial ).

[ ]:

[5]:
from ra2ce.analysis.damages.damages import AnalysisSectionDamages
from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import AnalysisDamagesEnum
from ra2ce.analysis.analysis_config_data.enums.event_type_enum import EventTypeEnum
from ra2ce.analysis.analysis_config_data.enums.damage_curve_enum import DamageCurveEnum
from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData

damages_analysis = [AnalysisSectionDamages(
    name='damages_reference_curve_manual',
    analysis=AnalysisDamagesEnum.DAMAGES,
    event_type=EventTypeEnum.EVENT,
    damage_curve=DamageCurveEnum.MAN,  # use manual damage curve
    save_csv=True,
    save_gpkg=True
)]

analysis_config_data = AnalysisConfigData(
    analyses=damages_analysis,
    output_path=output_path,
    input_path=root_dir.joinpath("input_data")
)

Step 4: Create damage curve files#

In the manual situation, two input files are expected:

  1. A file that specifies the shape of the vulnerability curve:

    • x-axis = hazard intensity (e.g. water depth in cm)

    • y-axis = damage fraction (0–1, representing the % of total construction cost)

  2. A file that specifies the construction costs per road type and number of lanes.

Both files should be placed in the folder input_data/damage_functions/all_road_types/.

Vulnerability curve (hazard severity vs. damage fraction)#

The file hazard_severity_damage_fraction.csv looks like:

depth;damage
cm;% of total construction costs
0;0
100;0.1
200;0.2
400;0.4
800;0.8
12000;1

Where:

  • depth = water depth in cm

  • damage = damage fraction (0–1, relative to construction cost)

[6]:
import matplotlib.pyplot as plt
import pandas as pd

input_data_path = root_dir.joinpath("input_data")
vuln_curves = pd.read_csv(input_data_path.joinpath("damage_functions", "all_road_types","hazard_severity_damage_fraction.csv"), delimiter=";")

plt.plot(vuln_curves["depth"], vuln_curves["damage"], marker="o")
plt.xlabel("Depth [cm]")
plt.ylabel("Damage fraction")
plt.title("Manual vulnerability curve")
plt.grid(True)
plt.show()
../_images/_examples_damages_manual_curves_14_0.png

Manual vulnerability curve

Maximum construction costs per road type and lanes#

The file max_damage_road_types.csv looks like:

Road_type \ lanes;1;2;3;4;5;6;7;8
unit;euro/m;euro/m;euro/m;euro/m;euro/m;euro/m;euro/m;euro/m
tertiary_link;110;120;130;140;150;130;140;150
tertiary;110;120;130;140;150;130;140;150
trunk;110;120;130;140;150;130;140;150
trunk_link;110;120;130;140;150;130;140;150
secondary_link;11;12;13;14;15;13;14;15
secondary;11;12;13;14;15;13;14;15
primary_link;11;12;13;14;15;13;14;15
primary;11;12;13;14;15;13;14;15
residential;1100;1200;1300;1400;1500;1300;1400;1500
['tertiary', 'residential'];510;520;530;540;550;530;540;550
motorway;1100;1200;1300;1400;1500;1300;1400;1500
motorway_link;510;520;530;540;550;530;540;550

Note

  • The first file defines the shape of the damage curve (hazard → damage fraction).

  • The second file defines the maximum construction costs per road type and lane count.

  • Together, they allow RA2CE to estimate damages in absolute currency units.


Step 5: Run the analysis#

[7]:
Ra2ceHandler.run_with_config_data(network_config_data, analysis_config_data)
c:\Users\hauth\miniforge3\envs\ra2ce_env\Lib\site-packages\osmnx\simplification.py:513: UserWarning: Geometry is in a geographic CRS. Results from 'buffer' are likely incorrect. Use 'GeoSeries.to_crs()' to re-project geometries to a projected CRS before this operation.

  merged = convert.graph_to_gdfs(G, edges=False)["geometry"].buffer(tolerance).unary_union
c:\Users\hauth\miniforge3\envs\ra2ce_env\Lib\site-packages\osmnx\simplification.py:560: UserWarning: Geometry is in a geographic CRS. Results from 'centroid' are likely incorrect. Use 'GeoSeries.to_crs()' to re-project geometries to a projected CRS before this operation.

  centroids = node_clusters.centroid
100%|██████████| 127/127 [00:00<00:00, 126827.76it/s]
2025-10-01 05:05:17 PM - [avg_speed_calculator.py:175] - root - WARNING - No valid file found with average speeds data\damage_manual\static\output_graph\avg_speed.csv, calculating and saving them instead.
Graph hazard overlay with flood100: 100%|██████████| 109/109 [00:00<00:00, 184.69it/s]
Graph fraction with hazard overlay with flood100: 100%|██████████| 109/109 [01:00<00:00,  1.81it/s]
Graph hazard overlay with flood1000: 100%|██████████| 109/109 [00:00<00:00, 229.58it/s]
Graph fraction with hazard overlay with flood1000: 100%|██████████| 109/109 [01:16<00:00,  1.42it/s]
2025-10-01 05:07:37 PM - [hazard_intersect_builder_for_tif.py:225] - root - WARNING - Some geometries have NoneType objects (no coordinate information), namely: Empty GeoDataFrame
Columns: [osmid, oneway, lanes, ref, highway, maxspeed, reversed, length, rfid_c, rfid, avgspeed, time, bridge, name, junction, geometry]
Index: [].This could be due to segmentation, and might cause an exception in hazard overlay
Network hazard overlay with flood100: 100%|██████████| 2727/2727 [00:02<00:00, 910.21it/s]
Network fraction with hazard overlay with flood100: 100%|██████████| 2727/2727 [00:12<00:00, 209.88it/s]
Network hazard overlay with flood1000: 100%|██████████| 2727/2727 [00:03<00:00, 880.10it/s]
Network fraction with hazard overlay with flood1000: 100%|██████████| 2727/2727 [00:17<00:00, 156.20it/s]
2025-10-01 05:08:14 PM - [damage_network_base.py:121] - root - WARNING - Of the 2727 road segments, only 1568 had lane data, so for 1159 the '
                                    lane data will be interpolated from the existing data
2025-10-01 05:08:15 PM - [damage_network_base.py:134] - root - WARNING - Interpolated the missing lane data as follows: {'motorway': 2.0, 'primary': 2.0, 'secondary': 2.0, 'trunk': 1.0}
[7]:
[DamagesResultWrapper(segment_based_result=AnalysisResult(analysis_result=                                osmid  oneway  lanes  ref     infra_type  \
 u           v          key
 42002848    4336238970 0    435722626    True    3.0   N2          trunk
 42003549    5284280675 0    215573156    True    2.0  A79       motorway
             5284280682 0    254696086    True    2.0   A2       motorway
 42004326    3744093239 0    254696086    True    2.0   A2       motorway
 42005137    2832217846 0    720205318    True    1.0  A79  motorway_link
 ...                               ...     ...    ...  ...            ...
 12680821393 42080697   0      6773022   False    2.0  nan      secondary
             42080429   0      6773022   False    2.0  nan      secondary
 12696408646 2540119540 0      6773079   False    2.0  nan      secondary
             42083916   0      6773079   False    2.0  nan      secondary
 13073531942 4659465278 0    435562863    True    2.0  A79       motorway

                            maxspeed  reversed   length  rfid_c  rfid  ...  \
 u           v          key                                            ...
 42002848    4336238970 0         80     False  121.954       1     1  ...
 42003549    5284280675 0        100     False   31.186       2     2  ...
             5284280682 0        100     False   43.429       3     3  ...
 42004326    3744093239 0        100     False   26.171       4     3  ...
 42005137    2832217846 0         80     False   54.061       5    99  ...
 ...                             ...       ...      ...     ...   ...  ...
 12680821393 42080697   0         50     False   29.033    2723    45  ...
             42080429   0         50      True   22.549    2724    54  ...
 12696408646 2540119540 0         50     False   33.712    2725    78  ...
             42083916   0         50      True    2.818    2726    95  ...
 13073531942 4659465278 0        100     False   23.232    2727   124  ...

                             F_EV1_me  F_EV1_fr F_EV2_mi F_EV2_ma  F_EV2_me  \
 u           v          key
 42002848    4336238970 0         NaN       1.0      NaN      NaN       NaN
 42003549    5284280675 0         NaN       1.0      NaN      NaN       NaN
             5284280682 0         NaN       1.0      NaN      NaN       NaN
 42004326    3744093239 0         NaN       1.0      NaN      NaN       NaN
 42005137    2832217846 0    1.000000       1.0      1.0      2.0  1.166667
 ...                              ...       ...      ...      ...       ...
 12680821393 42080697   0    4.090909       1.0      5.0      5.0  5.000000
             42080429   0    3.750000       1.0      3.0      5.0  4.437500
 12696408646 2540119540 0    1.000000       1.0      1.0      1.0  1.000000
             42083916   0    1.000000       1.0      1.0      2.0  1.666667
 13073531942 4659465278 0    1.416667       1.0      1.0      3.0  2.000000

                            F_EV2_fr  road_type  lanes_copy  dam_EV1_al  \
 u           v          key
 42002848    4336238970 0        1.0      trunk         3.0         NaN
 42003549    5284280675 0        1.0   motorway         2.0         NaN
             5284280682 0        1.0   motorway         2.0         NaN
 42004326    3744093239 0        1.0   motorway         2.0         NaN
 42005137    2832217846 0        1.0   motorway         1.0      2757.0
 ...                             ...        ...         ...         ...
 12680821393 42080697   0        1.0  secondary         2.0       143.0
             42080429   0        1.0  secondary         2.0       101.0
 12696408646 2540119540 0        1.0  secondary         2.0        40.0
             42083916   0        1.0  secondary         2.0         3.0
 13073531942 4659465278 0        1.0   motorway         2.0      3949.0

                             dam_EV2_al
 u           v          key
 42002848    4336238970 0           NaN
 42003549    5284280675 0           NaN
             5284280682 0           NaN
 42004326    3744093239 0           NaN
 42005137    2832217846 0        3217.0
 ...                                ...
 12680821393 42080697   0         174.0
             42080429   0         120.0
 12696408646 2540119540 0          40.0
             42083916   0           6.0
 13073531942 4659465278 0        5576.0

 [2727 rows x 28 columns], analysis_config=AnalysisSectionDamages(name='damages_reference_curve_manual', save_gpkg=True, save_csv=True, analysis=<AnalysisDamagesEnum.DAMAGES: 1>, representative_damage_percentage=100, event_type=<EventTypeEnum.EVENT: 1>, damage_curve=<DamageCurveEnum.MAN: 3>, risk_calculation_mode=<RiskCalculationModeEnum.NONE: 0>, risk_calculation_year=0, create_table=False, file_name=None), output_path=WindowsPath('data/damage_manual/output'), _custom_name='damages_reference_curve_manual_segmented'), link_based_result=AnalysisResult(analysis_result=              u            v  key  \
 0      42002848   3510081992    0
 1      42002848     42005643    0
 2      42002848   2832217848    0
 3      42003549   1912827603    0
 4      42003549   2119022099    0
 ..          ...          ...  ...
 104  2587147852   2832217848    0
 105  3441582565   3441582576    0
 106  3475087790   3475088037    0
 107  9646055861   9646055863    0
 108  9646055861  10036651817    0

                                                  osmid  oneway       lanes  \
 0                                            435722626    True           3
 1                                             72400805    True           2
 2                               [720205318, 284802951]    True           1
 3                               [382923864, 215573156]    True  ['3', '2']
 4    [337060729, 254696086, 7125689, 7125692, 10930...    True           2
 ..                                                 ...     ...         ...
 104                             [369221729, 720205319]    True           1
 105                             [337060737, 587459365]    True           2
 106                  [340294690, 340294691, 340294686]    True  ['1', '2']
 107                                            6779700    True           1
 108                                            6779700    True           1

      ref        highway maxspeed reversed  ...    EV1_me EV1_fr    EV2_me  \
 0     N2          trunk       80    False  ...  1.000000    1.0  1.000000
 1     N2          trunk       80    False  ...       NaN    1.0       NaN
 2    A79  motorway_link       80    False  ...  2.837607    1.0  3.508065
 3    A79       motorway      100    False  ...  4.592040    1.0  4.892019
 4     A2       motorway      100    False  ...  4.510000    1.0  4.539823
 ..   ...            ...      ...      ...  ...       ...    ...       ...
 104  A79       motorway      100    False  ...  4.330798    1.0  4.637363
 105  nan        primary       50    False  ...  4.227273    1.0  3.323529
 106  nan        primary       50    False  ...  4.000000    1.0  4.062500
 107  nan      secondary       50    False  ...  1.000000    1.0  3.400000
 108  nan      secondary       50    False  ...  1.500000    1.0  2.444444

      EV2_fr                                dam_EV1_al_segments  dam_EV1_al  \
 0       1.0                                      [nan, 1364.0]      1364.0
 1       1.0                          [nan, nan, nan, nan, nan]         0.0
 2       1.0  [5581.0, 9553.0, 2757.0, nan, nan, nan, nan, 2...     27270.0
 3       1.0  [13464.0, 15332.0, nan, 8167.0, 10481.0, 8444....    191760.0
 4       1.0  [nan, nan, 12109.0, nan, nan, nan, nan, 2392.0...     84491.0
 ..      ...                                                ...         ...
 104     1.0  [16409.0, 27780.0, 27231.0, 36025.0, 33800.0, ...    220478.0
 105     1.0  [27.0, nan, nan, 55.0, 34.0, 47.0, nan, 42.0, ...       405.0
 106     1.0                           [nan, nan, 123.0, 148.0]       271.0
 107     1.0                               [2.0, 2.0, 2.0, 2.0]         8.0
 108     1.0  [nan, nan, nan, nan, nan, nan, 2.0, nan, nan, ...        12.0

                                    dam_EV2_al_segments  dam_EV2_al  \
 0                                        [nan, 1364.0]      1364.0
 1                            [nan, nan, nan, nan, nan]         0.0
 2    [6725.0, 10747.0, 3217.0, nan, nan, nan, nan, ...     34419.0
 3    [13464.0, 15332.0, nan, 10209.0, 10500.0, 9703...    209378.0
 4    [nan, nan, 12109.0, 7222.0, nan, nan, nan, 340...    109199.0
 ..                                                 ...         ...
 104  [19170.0, 20835.0, 28273.0, 33725.0, 39306.0, ...    226039.0
 105  [27.0, 24.0, nan, 55.0, 43.0, 47.0, 9.0, 42.0,...       480.0
 106                           [nan, nan, 127.0, 151.0]       278.0
 107                               [5.0, 8.0, 8.0, 8.0]        29.0
 108  [4.0, 2.0, nan, 2.0, 6.0, 7.0, 7.0, 7.0, 5.0, ...        84.0

                     name    junction
 0                    nan         nan
 1                    nan         nan
 2                    nan         nan
 3                    nan         nan
 4                    nan         nan
 ..                   ...         ...
 104                  nan         nan
 105  Nieuwe Limmelderweg         nan
 106  Nieuwe Limmelderweg         nan
 107                  nan  roundabout
 108                  nan  roundabout

 [109 rows x 27 columns], analysis_config=AnalysisSectionDamages(name='damages_reference_curve_manual', save_gpkg=True, save_csv=True, analysis=<AnalysisDamagesEnum.DAMAGES: 1>, representative_damage_percentage=100, event_type=<EventTypeEnum.EVENT: 1>, damage_curve=<DamageCurveEnum.MAN: 3>, risk_calculation_mode=<RiskCalculationModeEnum.NONE: 0>, risk_calculation_year=0, create_table=False, file_name=None), output_path=WindowsPath('data/damage_manual/output'), _custom_name='damages_reference_curve_manual_link_based'))]

Output#

The results of the manual damage analysis are provided in two GeoPackage (GPKG) files:

  • damages_reference_curve_manual_link_based.gpkg: damage estimates per network link

  • damages_reference_curve_manual_segment.gpkg: damage estimates per 100m segment

Key attributes of interest (in currency):

  • dam_EV1_al : estimated damage for the first flood map (manual method).

  • dam_EV2_al : estimated damage for the second flood map (manual method).

You can open these files in GIS software (QGIS, ArcGIS) or load them in Python with GeoPandas:

[ ]:
import geopandas as gpd
output_path = root_dir / "output" / 'damages'
link_based = gpd.read_file(output_path / "damages_reference_curve_manual_link_based.gpkg")
segment_based = gpd.read_file(output_path / "damages_reference_curve_manual_segmented.gpkg")

# Inspect the first rows
print(link_based.head())
print(segment_based.head())

You can open the results in GIS software to visualize which road segments are most affected by the hazard.