Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine risk model tests #157

Merged
merged 2 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading