From 8484bc5f7f8a603bd559498829a8f0d4cecf5ce7 Mon Sep 17 00:00:00 2001 From: Joe Moorhouse Date: Sun, 29 Oct 2023 20:54:59 +0100 Subject: [PATCH] Refine risk model tests (#157) * Test tidy Signed-off-by: Joe Moorhouse * Refine score-based risk measures. Wind-related tweaks. Signed-off-by: Joe Moorhouse --------- Signed-off-by: Joe Moorhouse --- Pipfile | 1 + Pipfile.lock | 11 +- .../PhysicalRiskMethodologyBibliography.bib | 11 ++ src/physrisk/api/v1/impact_req_resp.py | 13 +- src/physrisk/data/hazard_data_provider.py | 2 +- src/physrisk/hazard_models/core_hazards.py | 16 +- src/physrisk/kernel/calculation.py | 2 + src/physrisk/kernel/exposure.py | 3 + src/physrisk/requests.py | 22 ++- src/physrisk/risk_models/risk_models.py | 19 ++- src/test/api/test_impact_requests.py | 15 +- src/test/data/hazard_model_store.py | 141 +++++++++++++----- src/test/risk_models/test_risk_models.py | 112 ++++++++++++-- 13 files changed, 298 insertions(+), 70 deletions(-) diff --git a/Pipfile b/Pipfile index 21cf41bf..1e8d8ba3 100644 --- a/Pipfile +++ b/Pipfile @@ -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 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 748b08f8..90ee901d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d72f83455a6199ee1742915dd3e3aa7efde6ce5ee9ceaa8a1aca8d9138d2e84e" + "sha256": "cebc538550b583317f7ed6f7041fa0075d88140ce8c88c0c3045a44ef5e606b9" }, "pipfile-spec": 6, "requires": { @@ -667,6 +667,15 @@ "markers": "python_version >= '3.7'", "version": "==9.4.0" }, + "pint": { + "hashes": [ + "sha256:230ebccc312693117ee925c6492b3631c772ae9f7851a4e86080a15e7be692d8", + "sha256:5d5b6b518d0c5a7ab03a776175db500f1ed1523ee75fb7fafe38af8149431c8d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.21.1" + }, "pydantic": { "hashes": [ "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", diff --git a/methodology/PhysicalRiskMethodologyBibliography.bib b/methodology/PhysicalRiskMethodologyBibliography.bib index a47fa06c..7bad0134 100644 --- a/methodology/PhysicalRiskMethodologyBibliography.bib +++ b/methodology/PhysicalRiskMethodologyBibliography.bib @@ -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}, diff --git a/src/physrisk/api/v1/impact_req_resp.py b/src/physrisk/api/v1/impact_req_resp.py index 592444ab..3a483e30 100644 --- a/src/physrisk/api/v1/impact_req_resp.py +++ b/src/physrisk/api/v1/impact_req_resp.py @@ -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): @@ -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): @@ -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): @@ -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): diff --git a/src/physrisk/data/hazard_data_provider.py b/src/physrisk/data/hazard_data_provider.py index 4bcdae96..59358a37 100644 --- a/src/physrisk/data/hazard_data_provider.py +++ b/src/physrisk/data/hazard_data_provider.py @@ -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): diff --git a/src/physrisk/hazard_models/core_hazards.py b/src/physrisk/hazard_models/core_hazards.py index d398bbcc..66129647 100644 --- a/src/physrisk/hazard_models/core_hazards.py +++ b/src/physrisk/hazard_models/core_hazards.py @@ -4,12 +4,15 @@ 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) @@ -17,6 +20,10 @@ def first(self): 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) @@ -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)]) @@ -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: diff --git a/src/physrisk/kernel/calculation.py b/src/physrisk/kernel/calculation.py index adea9fe6..fccd11a6 100644 --- a/src/physrisk/kernel/calculation.py +++ b/src/physrisk/kernel/calculation.py @@ -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, ) @@ -28,6 +29,7 @@ def get_default_vulnerability_models() -> Dict[type, Sequence[VulnerabilityModel RealEstateAsset: [ RealEstateCoastalInundationModel(), RealEstateRiverineInundationModel(), + GenericTropicalCycloneModel(), ], IndustrialActivity: [ChronicHeatGZNModel()], TestAsset: [pgam.TemperatureModel()], diff --git a/src/physrisk/kernel/exposure.py b/src/physrisk/kernel/exposure.py index fc7ae249..6fdff5e7 100644 --- a/src/physrisk/kernel/exposure.py +++ b/src/physrisk/kernel/exposure.py @@ -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, @@ -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() ] diff --git a/src/physrisk/requests.py b/src/physrisk/requests.py index 02766c9a..903e51d4 100644 --- a/src/physrisk/requests.py +++ b/src/physrisk/requests.py @@ -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, @@ -29,6 +30,7 @@ HazardDescriptionResponse, HazardResource, IntensityCurve, + Scenario, ) from .api.v1.impact_req_resp import ( AcuteHazardCalculationDetails, @@ -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) @@ -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: @@ -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) @@ -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)], ) diff --git a/src/physrisk/risk_models/risk_models.py b/src/physrisk/risk_models/risk_models.py index f66b511c..500c2d2f 100644 --- a/src/physrisk/risk_models/risk_models.py +++ b/src/physrisk/risk_models/risk_models.py @@ -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 @@ -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, @@ -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 ( diff --git a/src/test/api/test_impact_requests.py b/src/test/api/test_impact_requests.py index 6f0024d9..4ab770d6 100644 --- a/src/test/api/test_impact_requests.py +++ b/src/test/api/test_impact_requests.py @@ -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 @@ -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") diff --git a/src/test/data/hazard_model_store.py b/src/test/data/hazard_model_store.py index 3bf0f59a..a5e1eb82 100644 --- a/src/test/data/hazard_model_store.py +++ b/src/test/data/hazard_model_store.py @@ -1,7 +1,8 @@ import os -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union import numpy as np +import numpy.typing as npt import zarr import zarr.storage from affine import Affine @@ -18,6 +19,106 @@ class TestData: coastal_latitudes = [-5.55, 26.1981, 23.6473, 23.6783, 23.5699, 23.9904, 23.59, 23.6112] # fmt: on + # fmt: off + wind_return_periods = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000] # noqa + wind_intensities_1 = [34.314999, 40.843750, 44.605000, 46.973751, 48.548752, 49.803749, 51.188751, 52.213749, 52.902500, 53.576248, 57.552502, 59.863750, 60.916248, 61.801250, 62.508751, 63.082500, 63.251251, 63.884998, 64.577499] # noqa + wind_intensities_2 = [37.472500, 44.993752, 49.049999, 51.957500, 53.796249, 55.478748, 56.567501, 57.572498, 58.661251, 59.448750, 63.724998, 65.940002, 66.842499, 67.614998, 68.110001, 68.547501, 68.807503, 69.529999, 70.932503] # noqa + # fmt: on + + +class ZarrStoreMocker: + def __init__(self): + self.store, self._root = zarr_memory_store() + + def add_curves_global( + self, + array_path: str, + longitudes: List[float], + latitudes: List[float], + return_periods: Union[List[float], npt.NDArray], + intensities: Union[List[float], npt.NDArray], + width: int = 43200, + height: int = 21600, + ): + crs = "epsg:4326" + crs, shape, trans = self._crs_shape_transform_global(return_periods=return_periods, width=width, height=height) + self._add_curves(array_path, longitudes, latitudes, crs, shape, trans, return_periods, intensities) + + def _crs_shape_transform_global( + self, width: int = 43200, height: int = 21600, return_periods: Union[List[float], npt.NDArray] = [0.0] + ): + return self._crs_shape_transform(width, height, return_periods) + + def _add_curves( + self, + array_path: str, + longitudes: List[float], + latitudes: List[float], + crs: str, + shape: Tuple[int, int, int], + trans: List[float], + return_periods: Union[List[float], npt.NDArray], + intensities: Union[List[float], npt.NDArray], + ): + z = self._root.create_dataset( # type: ignore + array_path, shape=(shape[0], shape[1], shape[2]), chunks=(shape[0], 1000, 1000), dtype="f4" + ) + z.attrs["transform_mat3x3"] = trans + z.attrs["index_values"] = return_periods + + transform = Affine(trans[0], trans[1], trans[2], trans[3], trans[4], trans[5]) + coords = np.vstack((longitudes, latitudes, np.ones(len(longitudes)))) + inv_trans = ~transform + mat = np.array(inv_trans).reshape(3, 3) + frac_image_coords = mat @ coords + image_coords = np.floor(frac_image_coords).astype(int) + for j in range(len(longitudes)): + z[:, image_coords[1, j], image_coords[0, j]] = intensities + + def _crs_shape_transform(self, width: int, height: int, return_periods: Union[List[float], npt.NDArray] = [0.0]): + t = [360.0 / width, 0.0, -180.0, 0.0, -180.0 / height, 90.0, 0.0, 0.0, 1.0] + return "epsg:4326", (len(return_periods), height, width), t + + +def shape_transform_21600_43200( + width: int = 43200, height: int = 21600, return_periods: Union[List[float], npt.NDArray] = [0.0] +): + t = [360.0 / width, 0.0, -180.0, 0.0, -180.0 / height, 90.0, 0.0, 0.0, 1.0] + return (len(return_periods), height, width), t + + +def zarr_memory_store(path="hazard.zarr"): + store = zarr.storage.MemoryStore(root=path) + return store, zarr.open(store=store, mode="w") + + +def add_curves( + root: zarr.Group, + longitudes, + latitudes, + array_path: str, + shape: Tuple[int, int, int], + curve: np.ndarray, + return_periods: List[float], + trans: List[float], +): + z = root.create_dataset( # type: ignore + array_path, shape=(shape[0], shape[1], shape[2]), chunks=(shape[0], 1000, 1000), dtype="f4" + ) + z.attrs["transform_mat3x3"] = trans + z.attrs["index_values"] = return_periods + + trans = z.attrs["transform_mat3x3"] + transform = Affine(trans[0], trans[1], trans[2], trans[3], trans[4], trans[5]) + + coords = np.vstack((longitudes, latitudes, np.ones(len(longitudes)))) + inv_trans = ~transform + mat = np.array(inv_trans).reshape(3, 3) + frac_image_coords = mat @ coords + image_coords = np.floor(frac_image_coords).astype(int) + for j in range(len(longitudes)): + z[:, image_coords[1, j], image_coords[0, j]] = curve + def get_mock_hazard_model_store_single_curve(): """Create a test MemoryStore for creation of Zarr hazard model for unit testing. A single curve @@ -96,20 +197,10 @@ def mock_hazard_model_store_single_curve_for_paths(longitudes, latitudes, curve, return store -def shape_transform_21600_43200(return_periods: List[float] = [0.0]): - t = [360.0 / 43200, 0.0, -180.0, 0.0, -180.0 / 21600, 90.0, 0.0, 0.0, 1.0] - return (len(return_periods), 21600, 43200), t - - def inundation_return_periods(): return [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] -def zarr_memory_store(path="hazard.zarr"): - store = zarr.storage.MemoryStore(root=path) - return store, zarr.open(store=store, mode="w") - - def mock_hazard_model_store_path_curves(longitudes, latitudes, path_curves: Dict[str, np.ndarray]): """Create a MemoryStore for creation of Zarr hazard model to be used with unit tests, with the specified longitudes and latitudes set to the curve supplied.""" @@ -181,34 +272,6 @@ def inundation_paths(): return paths -def add_curves( - root: zarr.Group, - longitudes, - latitudes, - array_path: str, - shape: Tuple[int, int, int], - curve: np.ndarray, - return_periods: List[float], - trans: List[float], -): - z = root.create_dataset( # type: ignore - array_path, shape=(shape[0], shape[1], shape[2]), chunks=(shape[0], 1000, 1000), dtype="f4" - ) - z.attrs["transform_mat3x3"] = trans - z.attrs["index_values"] = return_periods - - trans = z.attrs["transform_mat3x3"] - transform = Affine(trans[0], trans[1], trans[2], trans[3], trans[4], trans[5]) - - coords = np.vstack((longitudes, latitudes, np.ones(len(longitudes)))) - inv_trans = ~transform - mat = np.array(inv_trans).reshape(3, 3) - frac_image_coords = mat @ coords - image_coords = np.floor(frac_image_coords).astype(int) - for j in range(len(longitudes)): - z[:, image_coords[1, j], image_coords[0, j]] = curve - - def _wri_inundation_prefix(): return "inundation/wri/v2" diff --git a/src/test/risk_models/test_risk_models.py b/src/test/risk_models/test_risk_models.py index be3e1ad5..c2a01ab0 100644 --- a/src/test/risk_models/test_risk_models.py +++ b/src/test/risk_models/test_risk_models.py @@ -1,16 +1,17 @@ """ Test asset impact calculations.""" import test.data.hazard_model_store as hms import unittest -from test.data.hazard_model_store import TestData +from test.data.hazard_model_store import TestData, ZarrStoreMocker +from typing import NamedTuple import numpy as np -from physrisk.api.v1.impact_req_resp import RiskMeasureKey +from physrisk.api.v1.impact_req_resp import RiskMeasureKey, RiskMeasures from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import RealEstateAsset from physrisk.kernel.calculation import get_default_vulnerability_models -from physrisk.kernel.hazards import CoastalInundation, RiverineInundation +from physrisk.kernel.hazards import CoastalInundation, RiverineInundation, Wind from physrisk.kernel.risk import AssetLevelRiskModel, MeasureKey from physrisk.requests import _create_risk_measures from physrisk.risk_models.risk_models import RealEstateToyRiskMeasures @@ -28,22 +29,38 @@ def sp_riverine(scenario, year): def sp_coastal(scenario, year): return source_paths[CoastalInundation](indicator_id="flood_depth", scenario=scenario, year=year) - store, root = hms.zarr_memory_store() + def sp_wind(scenario, year): + return source_paths[Wind](indicator_id="max_speed", scenario=scenario, year=year) + + mocker = ZarrStoreMocker() return_periods = hms.inundation_return_periods() - shape, transform = hms.shape_transform_21600_43200(return_periods=return_periods) + flood_histo_curve = np.array([0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163]) + flood_projected_curve = np.array([0.0596, 0.333, 0.605, 0.915, 1.164, 1.503, 1.649, 1.763, 1.963]) - histo_curve = np.array([0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163]) - projected_curve = np.array([0.0596, 0.333, 0.605, 0.915, 1.164, 1.503, 1.649, 1.763, 1.963]) for path in [sp_riverine("historical", 1980), sp_coastal("historical", 1980)]: - hms.add_curves( - root, TestData.longitudes, TestData.latitudes, path, shape, histo_curve, return_periods, transform - ) + mocker.add_curves_global(path, TestData.longitudes, TestData.latitudes, return_periods, flood_histo_curve) + for path in [sp_riverine("rcp8p5", 2050), sp_coastal("rcp8p5", 2050)]: - hms.add_curves( - root, TestData.longitudes, TestData.latitudes, path, shape, projected_curve, return_periods, transform + mocker.add_curves_global( + path, TestData.longitudes, TestData.latitudes, return_periods, flood_projected_curve ) - hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + mocker.add_curves_global( + sp_wind("historical", -1), + TestData.longitudes, + TestData.latitudes, + TestData.wind_return_periods, + TestData.wind_intensities_1, + ) + mocker.add_curves_global( + sp_wind("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + TestData.wind_return_periods, + TestData.wind_intensities_2, + ) + + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=mocker.store) assets = [ RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") @@ -80,3 +97,72 @@ def sp_coastal(scenario, year): measure_0_2 = item.measures_0[0] assert score == score2 assert measure_0 == measure_0_2 + self.interpret_risk_measures(risk_measures) + # example_json = risk_measures.model_dump_json() + + def interpret_risk_measures(self, risk_measure: RiskMeasures): + class Key(NamedTuple): # hashable key for looking up measures + hazard_type: str + scenario_id: str + year: str + measure_id: str + + def key(key: RiskMeasureKey): + return Key( + hazard_type=key.hazard_type, scenario_id=key.scenario_id, year=key.year, measure_id=key.measure_id + ) + + # this is called a measure set, since each type of asset can have its own measure that defines the score. + measure_set_id = risk_measure.score_based_measure_set_defn.measure_set_id + measures = {key(m.key): m for m in risk_measure.measures_for_assets} + + # interested in asset with index 1 + asset_index = 1 + for hazard_type in [ + "ChronicHeat", + "ChronicWind", + "CoastalInundation", + "CombinedInundation", + "Drought", + "Fire", + "Hail", + "Hazard", + "Inundation", + "Precipitation", + "RiverineInundation", + "Wind", + ]: + # for each type of hazard and each asset, the definition of the score can be different. + # The definition is given by: + measure_id = risk_measure.score_based_measure_set_defn.asset_measure_ids_for_hazard[hazard_type][ + asset_index + ] + # note that one of the aims of the schema is to keep the JSON size small for large numbers of assets; + # hence arrays of length number of assets are used. + # the definition of the measure: + measure_definition = ( + risk_measure.score_based_measure_set_defn.score_definitions[measure_id] if measure_id != "na" else None + ) + for scenario in risk_measure.scenarios: + for year in scenario.years: + measure_key = Key( + hazard_type=hazard_type, scenario_id=scenario.id, year=str(year), measure_id=measure_set_id + ) + if measure_key in measures: + measure = measures[measure_key] + asset_score = measure.scores[asset_index] + if asset_score != -1: + assert measure_definition is not None + asset_measure = measure.measures_0[asset_index] + print(f"For key {measure_key}, asset score is {asset_score}.") + print(f"The measure ID is {measure_id}.") + values = measure_definition.values + description = next(v for v in values if v.value == asset_score).description + print( + f"The description for measure ID {measure_id} of score {asset_score} is: {description}" + ) + print(f"The underlying measure value is {asset_measure}.") + print( + f"The definition of the underlying measure is: \ + {measure_definition.underlying_measures[0].description}" + )