Walk-through: modelling impact of climate hazards on cement plants

This is a worked example of how to specify the vulnerability of an arbitrary asset to different climate hazards via configuration. In this walk-through, assets of type ‘CementPlant’ which belong to the ‘ManufacturingAsset’ class are modelled. A custom vulnerability configuration is defined in csv format, both for acute and chronic hazards.

[1]:
from IPython.display import display
import json
from typing import NamedTuple
from dependency_injector import providers
from physrisk.container import Container
from physrisk.vulnerability_models.vulnerability import VulnerabilityModelsFactory
from physrisk.vulnerability_models.config_based_impact_curves import (
    config_items_from_csv,
    config_items_to_df,
)
import plotly.io

plotly.io.renderers.default = "notebook"

Define a portfolio. The asset_class must match one of the pre-defined physrisk classes, but type is free text that links to the vulnerability functions. In this example we use ‘CementPlant’ for the type. The field can be hierarchical using a ‘\’ separator, but here for simplicity there is just a single level.

[2]:
portfolio = {
    "items": [
        {
            "asset_class": "ManufacturingAsset",
            "type": "CementPlant",
            "latitude": 46.73753446,
            "longitude": 15.57335874,
        },
        {
            "asset_class": "ManufacturingAsset",
            "type": "CementPlant",
            "latitude": 251.06066169,
            "longitude": -115.1701174,
        },
        {
            "asset_class": "ManufacturingAsset",
            "type": "CementPlant",
            "latitude": 49.1581039901669,
            "longitude": -123.00551933666,
        },
    ]
}
request = {
    "assets": portfolio,
    "include_asset_level": True,
    "include_calc_details": True,
    "include_measures": False,
    "years": [2050],
    "scenarios": ["historical", "ssp585"],
    "calc_settings": {
        "hazard_interp": "linear",
        # "hazard_scope": "RiverineInundation" # a useful way to select a subset of hazards
    },
}

The configuration for the CementPlants is contained in the file cement_plants_example_config.csv. We will load into a Pandas DataFrame for ease of viewing.

[3]:
config_items = config_items_from_csv("cement_plants_example_config.csv")
config_df = config_items_to_df(config_items)
display(config_df.head())
hazard_class asset_class asset_identifier indicator_id indicator_units impact_id impact_units curve_type points_x points_y points_z points_kind cap_of_points_x cap_of_points_y activation_of_points_x
0 Wind Asset type=Generic,location=Asia max_speed m/s damage None indicator/piecewise_linear [20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 60.... [0.0, 0.0, 0.001, 0.007, 0.024, 0.058, 0.109, ... None None None None None
1 Wind Asset type=Generic,location=China Mainland max_speed m/s damage None indicator/piecewise_linear [20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 60.... [0.0, 0.0, 0.0, 0.002, 0.008, 0.02, 0.039, 0.1... None None None None None
2 Wind Asset type=Generic,location=Europe max_speed m/s damage None indicator/piecewise_linear [20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 60.... [0.0, 0.0, 0.0, 0.003, 0.012, 0.029, 0.057, 0.... None None None None None
3 Wind Asset type=Generic,location=Generic max_speed m/s damage None indicator/piecewise_linear [20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 60.... [0.0, 0.0, 0.0, 0.003, 0.012, 0.029, 0.057, 0.... None None None None None
4 Wind Asset type=Generic,location=North America max_speed m/s damage None indicator/piecewise_linear [20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 60.... [0.0, 0.0, 0.0, 0.003, 0.012, 0.029, 0.057, 0.... None None None None None

Having defined the configuration the calculation is run. Dependency injection is used to generate vulnerability models from the configuration.

[4]:
# we make use of dependency injection to inject a vulnerability model that accepts the configuration.
container = Container()
container.override_providers(
    vulnerability_models_factory=providers.Factory(
        VulnerabilityModelsFactory, config=config_items
    )
)
requester = container.requester()
response = json.loads(
    requester.get(request_id="get_asset_impact", request_dict=request)
)

The impacts of chronic and acute hazards are generally treated differently in financial calculations. We assume that chronic hazards such as heat and drought already affect the revenue generation (attributable to a given asset). For example:

  • A construction company that requires high-intensity outdoor labour from some employees is already affected by lower productivity (than some ideal) for a certain portion of the year as a result of conditions of high temperature and humidity. Typically of interest is the incremental impact on productivity.

  • A power generating company may already factor into their operations a lower contribution from power generating assets under conditions of drought, but it may be relevant to calculate how that contribution could decrease over time.

For chronic hazards, the metric of interest is therefore likely to be the change in revenue generation (attributable to an asset) between current climate and a potential future climate.

In contrast we assume that acute hazards do not already affect the revenue generation, in particular infrequent but severe events. Whereas insurance premiums may affect company financials, a 1-in-100 year event does not impact revenue generation in the same way as an annual occurrence.

[5]:
asset_index = 0
asset_impacts = response["asset_impacts"][asset_index]["impacts"]


# for convenience unpack using a key
class Key(NamedTuple):
    hazard_type: str
    scenario_id: str
    year: str


asset_impact_dict = {}
for i in asset_impacts:
    key = i["key"]
    asset_impact_dict[Key(key["hazard_type"], key["scenario_id"], key["year"])] = i

hazard_types = set(k.hazard_type for k in asset_impact_dict.keys())

for hazard_type in hazard_types:
    impact_histo = asset_impact_dict[Key(hazard_type, "historical", "None")]
    impact_ssp585 = asset_impact_dict[Key(hazard_type, "ssp585", "2050")]
    mean_histo = impact_histo["impact_mean"]
    mean_ssp585 = impact_ssp585["impact_mean"]
    if hazard_type in ["ChronicHeat", "Drought"]:
        print(
            f"For hazard {hazard_type}, modelled reduction to revenue generation is {(mean_ssp585 - mean_histo) * 100:.2g}%."
        )
    else:
        print(
            f"For hazard {hazard_type}, modelled Annual Average Loss is {mean_histo * 100:.2g}% (current) and {mean_ssp585 * 100:.2g}% (future climate)."
        )
For hazard Drought, modelled reduction to revenue generation is 1.4%.
For hazard RiverineInundation, modelled Annual Average Loss is 0.054% (current) and 0.14% (future climate).
For hazard Wind, modelled Annual Average Loss is 0% (current) and 0% (future climate).
For hazard ChronicHeat, modelled reduction to revenue generation is 0.81%.
For hazard CoastalInundation, modelled Annual Average Loss is 0% (current) and 0% (future climate).
[6]:
asset_impact_dict[Key("RiverineInundation", "ssp585", "2050")]
[6]:
{'key': {'hazard_type': 'RiverineInundation',
  'scenario_id': 'ssp585',
  'year': '2050'},
 'impact_type': 'damage',
 'impact_distribution': {'bin_edges': [0.044445752650287114,
   0.04626312087278834,
   0.054144813488467274,
   0.06443316370379783,
   0.07216670265527929,
   0.07987659637727229,
   0.07987659637727229],
  'probabilities': [0.004555753918420825, 0.01, 0.006, 0.002, 0.001, 0.001]},
 'impact_exceedance': {'values': [0.044445752650287114,
   0.04626312087278834,
   0.054144813488467274,
   0.06443316370379783,
   0.07216670265527929,
   0.07987659637727229,
   0.07987659637727229],
  'exceed_probabilities': [0.024555753918420825,
   0.02,
   0.01,
   0.004,
   0.002,
   0.001,
   0.0]},
 'impact_mean': 0.0013568953686298436,
 'impact_std_deviation': 0.008685379334934497,
 'impact_semi_std_deviation': 0.008581367044124037,
 'calc_details': {'hazard_exceedance': {'values': [0.07482449941125777,
    0.07788404187338105,
    0.09115288466071932,
    0.10847333956868321,
    0.12149276541292811,
    0.1344723844735224,
    0.1344723844735224],
   'exceed_probabilities': [0.024555753918420825,
    0.02,
    0.01,
    0.004,
    0.002,
    0.001,
    0.0]},
  'hazard_distribution': {'bin_edges': [0.07482449941125777,
    0.07788404187338105,
    0.09115288466071932,
    0.10847333956868321,
    0.12149276541292811,
    0.1344723844735224,
    0.1344723844735224],
   'probabilities': [0.004555753918420825, 0.01, 0.006, 0.002, 0.001, 0.001]},
  'vulnerability_distribution': {'intensity_bin_edges': [0.07482449941125777,
    0.07788404187338105,
    0.09115288466071932,
    0.10847333956868321,
    0.12149276541292811,
    0.1344723844735224,
    0.1344723844735224],
   'impact_bin_edges': [0.044445752650287114,
    0.04626312087278834,
    0.054144813488467274,
    0.06443316370379783,
    0.07216670265527929,
    0.07987659637727229,
    0.07987659637727229],
   'prob_matrix': [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
    [0.0, 0.0, 0.0, 1.0, 0.0, 0.0],
    [0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0, 0.0, 0.0, 1.0]]},
  'hazard_path': ['inundation/wri/v2/inunriver_{scenario}_MIROC-ESM-CHEM_{year}']}}

The User Guide describes how the vulnerability config is defined, in particular vulnerability curves with type indicator/piecewise_linear and threshold/piecewise_linear.

An example vulnerability config is put together from various physrisk sources. Inundation vulnerability curves are taken from the EU JRC global flood depth-damage functions, although vulnerability curves derived from Hazus might equally have been used. One example is:

"CoastalInundation,PluvialInundation,RiverineInundation",ManufacturingAsset,"type=CementPlant,location=Europe",flood_depth,metres,damage,,indicator/piecewise_linear,"[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0]","[0.0, 0.15, 0.27, 0.4, 0.52, 0.7, 0.85, 1.0, 1.0]",,,,,

This is a deterministic curve; the interpretion is that all types of inundation are modelled by the flood_depth indicator. A depth of 0 metres gives rise to a damage of 0% of the total insurable value of the asset, 0.5 metres gives rise to 15% damage and so on. Wind curves follow a similar pattern.

Chronic hazards can be modelled in a similar way. This line of config,

Drought,ManufacturingAsset,"type=CementPlant,location=Generic",months/spei3m/below/-2,months/year,disruption,,indicator/piecewise_linear,"[0.0, 12.0]","[0.0, 0.3]",,,,,

is of similar form but with indicator months/spei3m/below/-2,months: mean months per years where the 3 month SPEI ([VSBegueriaLL+12]) is below -2. It specifies a sliding scale of 0% to 30% loss in case of 12 months where the index is < -2. This model is limited however because it considers just a single SPEI level (of -2). Frameworks such as that of [LCF+23] adopt a more sophisticated approach which was developed specifically for power generating assets, but which we adapt to the case of Cement Plants. The generalization is:

Drought,ManufacturingAsset,"type=CementPlant,location=Generic",months/spei12m/below/threshold,months/year,disruption,,threshold/piecewise_linear,"[-3.6, -3, -2.5, -2]","[1.0, 0.2, 0.1, 0.0]",,,,,

Note here that the indicator is now months/spei12m/below/threshold which gives, for each latitude and longitude, the mean months per year below a set of thresholds (e.g. -1, -2, -2.5, -3, -3.6). It is thereby possible to isolate periods of extreme drought from drought. The curve gives the disruption to revenue generation for a set of SPEI thresholds. For periods with SPEI of -3 there is a revenue loss of 20% (i.e. the asset is operating at 80% capacity). For periods with SPEI of -3.6 revenue generation stops.

These threshold-based curves are approximate, meant for identification of where where the impact of climate change may be significant. For more accurate results the curves should be refined, ideally based on local empirical data (similar to the analysis [ZMC+21] for thermal power plants, for example).