Skip to content

Commit

Permalink
Refine risk model tests (#157)
Browse files Browse the repository at this point in the history
* Test tidy

Signed-off-by: Joe Moorhouse <[email protected]>

* Refine score-based risk measures. Wind-related tweaks.

Signed-off-by: Joe Moorhouse <[email protected]>

---------

Signed-off-by: Joe Moorhouse <[email protected]>
  • Loading branch information
joemoorhouse authored Oct 29, 2023
1 parent d331a94 commit 8484bc5
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 70 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ zarr = "==2.10.3"
pillow = "==9.4.0"
dependency-injector = "==4.41.0"
numba = "==0.56.4"
pint = "*"

[dev-packages]
isort = "*"
Expand Down
11 changes: 10 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions methodology/PhysicalRiskMethodologyBibliography.bib
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ @article{DunneEtAl:2013
publisher={Nature Publishing Group}
}

@Article{EberenzEtAl:2013,
title={Regional tropical cyclone impact functions for globally consistent risk assessments},
author={Eberenz, Samuel and L{\"u}thi, Samuel and Bresch, David N},
journal={Natural Hazards and Earth System Sciences},
volume={21},
number={1},
pages={393--415},
year={2021},
publisher={Copernicus GmbH}
}

@techreport{HuizingaEtAl:2017,
title={Global flood depth-damage functions: Methodology and the database with guidelines},
author={Huizinga, Jan and De Moel, Hans and Szewczyk, Wojciech and others},
Expand Down
13 changes: 10 additions & 3 deletions src/physrisk/api/v1/impact_req_resp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic import BaseModel, Field

from physrisk.api.v1.common import Assets, Distribution, ExceedanceCurve, VulnerabilityDistrib
from physrisk.api.v1.hazard_data import Scenario


class CalcSettings(BaseModel):
Expand Down Expand Up @@ -59,9 +60,13 @@ class RiskScoreValue(BaseModel):
class ScoreBasedRiskMeasureDefinition(BaseModel, frozen=True):
hazard_types: List[str] = Field([], description="Defines the hazards that the measure is used for.")
values: List[RiskScoreValue] = Field([], description="Defines the set of values that the score can take.")
child_measure_ids: List[str] = Field(
[], description="The identifiers of the risk measures used to calculate the score."
underlying_measures: List[RiskMeasureDefinition] = Field(
[], description="Defines the underlying risk measures from which the scores are inferred."
)
# for now underlying measures defined directly rather than by referencing an ID via:
# underlying_measure_ids: List[str] = Field(
# [], description="The identifiers of the underlying risk measures from which the scores are inferred."
# )

# should be sufficient to pass frozen=True, but does not seem to work (pydantic docs says feature in beta)
def __hash__(self):
Expand All @@ -85,7 +90,7 @@ class RiskMeasuresForAssets(BaseModel):
class ScoreBasedRiskMeasureSetDefinition(BaseModel):
measure_set_id: str
asset_measure_ids_for_hazard: Dict[str, List[str]]
score_definitions: Optional[Dict[str, ScoreBasedRiskMeasureDefinition]]
score_definitions: Dict[str, ScoreBasedRiskMeasureDefinition]


class RiskMeasures(BaseModel):
Expand All @@ -94,6 +99,8 @@ class RiskMeasures(BaseModel):
measures_for_assets: List[RiskMeasuresForAssets]
score_based_measure_set_defn: ScoreBasedRiskMeasureSetDefinition
measures_definitions: Optional[List[RiskMeasureDefinition]]
scenarios: List[Scenario]
asset_ids: List[str]


class AcuteHazardCalculationDetails(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion src/physrisk/data/hazard_data_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class HazardDataHint:
A hazard resource path can be specified which uniquely defines the hazard resource; otherwise the resource
is inferred from the indicator_id."""

path: Optional[str]
path: Optional[str] = None
# consider adding: indicator_model_gcm: str

def group_key(self):
Expand Down
16 changes: 14 additions & 2 deletions src/physrisk/hazard_models/core_hazards.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@
from physrisk.data.hazard_data_provider import HazardDataHint, SourcePath
from physrisk.data.inventory import EmbeddedInventory, Inventory
from physrisk.kernel import hazards
from physrisk.kernel.hazards import ChronicHeat, CoastalInundation, RiverineInundation
from physrisk.kernel.hazards import ChronicHeat, CoastalInundation, RiverineInundation, Wind


class ResourceSubset:
def __init__(self, resources: Iterable[HazardResource]):
self.resources = resources
self.resources = list(resources)

def any(self):
return any(self.resources)

def first(self):
return next(r for r in self.resources)

def match(self, hint: HazardDataHint):
return next(r for r in self.resources if r.path == hint.path)

def prefer_group_id(self, group_id: str):
with_condition = self.with_group_id(group_id)
return with_condition if with_condition.any() else self

def with_group_id(self, group_id: str):
return ResourceSubset(r for r in self.resources if r.group_id == group_id)

Expand Down Expand Up @@ -97,6 +104,7 @@ def __init__(self, inventory: Inventory):
self.add_selector(ChronicHeat, "mean/degree/days/above/32c", self._select_chronic_heat)
self.add_selector(RiverineInundation, "flood_depth", self._select_riverine_inundation)
self.add_selector(CoastalInundation, "flood_depth", self._select_coastal_inundation)
self.add_selector(Wind, "max_speed", self._select_wind)

def resources_with(self, *, hazard_type: type, indicator_id: str):
return ResourceSubset(self._inventory.resources_by_type_id[(hazard_type.__name__, indicator_id)])
Expand Down Expand Up @@ -127,6 +135,10 @@ def _select_riverine_inundation(
else candidates.with_model_gcm("MIROC-ESM-CHEM").first()
)

@staticmethod
def _select_wind(candidates: ResourceSubset, scenario: str, year: int, hint: Optional[HazardDataHint] = None):
return candidates.prefer_group_id("iris_osc").first()


def cmip6_scenario_to_rcp(scenario: str):
"""Convention is that CMIP6 scenarios are expressed by identifiers:
Expand Down
2 changes: 2 additions & 0 deletions src/physrisk/kernel/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from physrisk.vulnerability_models import power_generating_asset_models as pgam
from physrisk.vulnerability_models.chronic_heat_models import ChronicHeatGZNModel
from physrisk.vulnerability_models.real_estate_models import (
GenericTropicalCycloneModel,
RealEstateCoastalInundationModel,
RealEstateRiverineInundationModel,
)
Expand All @@ -28,6 +29,7 @@ def get_default_vulnerability_models() -> Dict[type, Sequence[VulnerabilityModel
RealEstateAsset: [
RealEstateCoastalInundationModel(),
RealEstateRiverineInundationModel(),
GenericTropicalCycloneModel(),
],
IndustrialActivity: [ChronicHeatGZNModel()],
TestAsset: [pgam.TemperatureModel()],
Expand Down
3 changes: 3 additions & 0 deletions src/physrisk/kernel/exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import numpy as np

from physrisk.data.hazard_data_provider import HazardDataHint
from physrisk.kernel.assets import Asset
from physrisk.kernel.hazard_model import (
HazardDataRequest,
Expand Down Expand Up @@ -65,6 +66,8 @@ def get_data_requests(self, asset: Asset, *, scenario: str, year: int) -> Iterab
scenario=scenario,
year=year,
indicator_id=indicator_id,
# select specific model for wind for consistency with thresholds
hint=HazardDataHint(path="wind/jupiter/v1/max_1min_{scenario}_{year}") if hazard_type == Wind else None,
)
for (hazard_type, indicator_id) in self.exposure_bins.keys()
]
Expand Down
22 changes: 16 additions & 6 deletions src/physrisk/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from physrisk.kernel.exposure import JupterExposureMeasure, calculate_exposures
from physrisk.kernel.hazards import all_hazards
from physrisk.kernel.risk import AssetLevelRiskModel, BatchId, Measure, MeasureKey
from physrisk.kernel.vulnerability_model import VulnerabilityModelBase

from .api.v1.hazard_data import (
HazardAvailabilityRequest,
Expand All @@ -29,6 +30,7 @@
HazardDescriptionResponse,
HazardResource,
IntensityCurve,
Scenario,
)
from .api.v1.impact_req_resp import (
AcuteHazardCalculationDetails,
Expand Down Expand Up @@ -264,14 +266,18 @@ def _get_asset_exposures(request: AssetExposureRequest, hazard_model: HazardMode
)


def _get_asset_impacts(request: AssetImpactRequest, hazard_model: HazardModel):
vulnerability_models = calc.get_default_vulnerability_models()
def _get_asset_impacts(
request: AssetImpactRequest,
hazard_model: HazardModel,
vulnerability_models: Optional[Dict[Type[Asset], Sequence[VulnerabilityModelBase]]] = None,
):
vulnerability_models = (
calc.get_default_vulnerability_models() if vulnerability_models is None else vulnerability_models
)

# we keep API definition of asset separate from internal Asset class; convert by reflection
# based on asset_class:
assets = create_assets(request.assets)

vulnerability_models = calc.get_default_vulnerability_models()
measure_calcs = calc.get_default_risk_measure_calculators()
risk_model = AssetLevelRiskModel(hazard_model, vulnerability_models, measure_calcs)

Expand Down Expand Up @@ -351,8 +357,9 @@ def _create_risk_measures(
Returns:
RiskMeasures: Output for writing to JSON.
"""
nan_value = -9999.0 # Nan not part of JSON spec
hazard_types = all_hazards()
measure_set_id = "measure_set_1"
measure_set_id = "measure_set_0"
measures_for_assets: List[RiskMeasuresForAssets] = []
for hazard_type in hazard_types:
for scenario_id in scenarios:
Expand All @@ -362,7 +369,8 @@ def _create_risk_measures(
hazard_type=hazard_type.__name__, scenario_id=scenario_id, year=str(year), measure_id=measure_set_id
)
scores = [-1] * len(assets)
measures_0 = [float("nan")] * len(assets)
# measures_0 = [float("nan")] * len(assets)
measures_0 = [nan_value] * len(assets)
for i, asset in enumerate(assets):
# look up result using the MeasureKey:
measure_key = MeasureKey(asset=asset, prosp_scen=scenario_id, year=year, hazard_type=hazard_type)
Expand All @@ -382,6 +390,8 @@ def _create_risk_measures(
measures_for_assets=measures_for_assets,
score_based_measure_set_defn=score_based_measure_set_defn,
measures_definitions=None,
scenarios=[Scenario(id=scenario, years=list(years)) for scenario in scenarios],
asset_ids=[f"asset_{i}" for i, _ in enumerate(assets)],
)


Expand Down
19 changes: 15 additions & 4 deletions src/physrisk/risk_models/risk_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from typing import Set

from physrisk.api.v1.impact_req_resp import Category, RiskScoreValue, ScoreBasedRiskMeasureDefinition
from physrisk.api.v1.impact_req_resp import (
Category,
RiskMeasureDefinition,
RiskScoreValue,
ScoreBasedRiskMeasureDefinition,
)
from physrisk.kernel.hazards import CoastalInundation, RiverineInundation, Wind
from physrisk.kernel.impact_distrib import ImpactDistrib
from physrisk.kernel.risk import Measure, RiskMeasureCalculator
Expand All @@ -18,7 +23,7 @@ def __init__(self):
Category.MEDIUM: 0.05,
}
definition = ScoreBasedRiskMeasureDefinition(
hazard_types=[RiverineInundation.__name__, CoastalInundation.__name__],
hazard_types=[RiverineInundation.__name__, CoastalInundation.__name__, Wind.__name__],
values=[
RiskScoreValue(
value=Category.REDFLAG,
Expand All @@ -42,10 +47,16 @@ def __init__(self):
),
RiskScoreValue(value=Category.NODATA, label="No data.", description="No data."),
],
child_measure_ids=["annual_loss_{return_period:0.0f}year"],
underlying_measures=[
RiskMeasureDefinition(
measure_id="measures_0",
label=f"1-in-{self.return_period:0.0f} year annual loss.",
description=f"1-in-{self.return_period:0.0f} year loss as fraction of asset insured value.",
)
],
)
self.measure_definitions = [definition]
self._definition_lookup = {RiverineInundation: definition, CoastalInundation: definition}
self._definition_lookup = {RiverineInundation: definition, CoastalInundation: definition, Wind: definition}

def _description(self, category: Category):
return (
Expand Down
15 changes: 14 additions & 1 deletion src/test/api/test_impact_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
from physrisk.data.pregenerated_hazard_model import ZarrHazardModel
from physrisk.data.zarr_reader import ZarrReader
from physrisk.hazard_models.core_hazards import get_default_source_paths
from physrisk.kernel.assets import PowerGeneratingAsset, RealEstateAsset
from physrisk.vulnerability_models.power_generating_asset_models import InundationModel
from physrisk.vulnerability_models.real_estate_models import (
RealEstateCoastalInundationModel,
RealEstateRiverineInundationModel,
)

# from physrisk.api.v1.impact_req_resp import AssetImpactResponse
# from physrisk.data.static.world import get_countries_and_continents
Expand Down Expand Up @@ -76,8 +82,15 @@ def test_impact_request(self):
store = mock_hazard_model_store_inundation(TestData.longitudes, TestData.latitudes, curve)

source_paths = get_default_source_paths(EmbeddedInventory())
vulnerability_models = {
PowerGeneratingAsset: [InundationModel()],
RealEstateAsset: [RealEstateCoastalInundationModel(), RealEstateRiverineInundationModel()],
}

response = requests._get_asset_impacts(
request, ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store))
request,
ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)),
vulnerability_models=vulnerability_models,
)

self.assertEqual(response.asset_impacts[0].impacts[0].hazard_type, "CoastalInundation")
Expand Down
Loading

0 comments on commit 8484bc5

Please sign in to comment.