diff --git a/pyproject.toml b/pyproject.toml index ecad3021..994b7fe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,10 +73,13 @@ include = ["physrisk*"] [tool.pdm.dev-dependencies] test = [ - "pdm[pytest]", - "pytest", - "pytest-cov", - "sphinx-pyproject" + "pdm[pytest]", + "pytest", + "pytest-cov", + "sphinx-pyproject", + "pandas>=2.0.3", + "dependency-injector>=4.41.0", + "geopandas<1.0,>=0.13.2", ] dev = [ "mypy", diff --git a/src/physrisk/api/v1/common.py b/src/physrisk/api/v1/common.py index c79387b8..fba5571a 100644 --- a/src/physrisk/api/v1/common.py +++ b/src/physrisk/api/v1/common.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Dict, List, Optional, Union import numpy as np diff --git a/src/physrisk/api/v1/impact_req_resp.py b/src/physrisk/api/v1/impact_req_resp.py index 790168f4..011a7d80 100644 --- a/src/physrisk/api/v1/impact_req_resp.py +++ b/src/physrisk/api/v1/impact_req_resp.py @@ -37,8 +37,10 @@ class AssetImpactRequest(BaseModel): True, description="If true, include impact calculation details." ) use_case_id: str = Field( - "", - description="Identifier for 'use case' used in the risk measures calculation.", + "DEFAULT", + description=( + "Identifier for 'use case' used in vulnerability models and risk measures calculations." + ), ) provider_max_requests: Dict[str, int] = Field( {}, @@ -70,6 +72,8 @@ class Category(int, Enum): HIGH = 3 REDFLAG = 4 + NORISK = -1 + class RiskMeasureDefinition(BaseModel): measure_id: str = Field(None, description="Identifier for the risk measure.") diff --git a/src/physrisk/container.py b/src/physrisk/container.py index 49c92311..85e20512 100644 --- a/src/physrisk/container.py +++ b/src/physrisk/container.py @@ -2,18 +2,14 @@ from dependency_injector import containers, providers +from physrisk.kernel import calculation as calc from physrisk.data.hazard_data_provider import SourcePath from physrisk.data.inventory import EmbeddedInventory from physrisk.data.inventory_reader import InventoryReader from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.zarr_reader import ZarrReader -from physrisk.kernel import calculation as calc from physrisk.kernel.hazard_model import HazardModelFactory -from physrisk.kernel.vulnerability_model import ( - DictBasedVulnerabilityModels, - VulnerabilityModels, - VulnerabilityModelsFactory, -) +from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModelsFactory from physrisk.requests import Requester, _create_inventory, create_source_paths @@ -41,11 +37,6 @@ def hazard_model( ) -class DictBasedVulnerabilityModelsFactory(VulnerabilityModelsFactory): - def vulnerability_models(self) -> VulnerabilityModels: - return DictBasedVulnerabilityModels(calc.get_default_vulnerability_models()) - - class Container(containers.DeclarativeContainer): config = providers.Configuration(default={"zarr_sources": ["embedded", "hazard"]}) diff --git a/src/physrisk/data/inventory.py b/src/physrisk/data/inventory.py index d766d0fb..9a29937c 100644 --- a/src/physrisk/data/inventory.py +++ b/src/physrisk/data/inventory.py @@ -9,10 +9,9 @@ import physrisk.data.colormap_provider as colormap_provider import physrisk.data.static.hazard +from physrisk.api.v1.hazard_data import HazardResource, Period from physrisk.data.inventory_reader import HazardModels -from ..api.v1.hazard_data import HazardResource, Period - # from physrisk.kernel.hazards import ChronicHeat diff --git a/src/physrisk/data/static/hazard/inventory.json b/src/physrisk/data/static/hazard/inventory.json index d6164a6b..a2dde57b 100644 --- a/src/physrisk/data/static/hazard/inventory.json +++ b/src/physrisk/data/static/hazard/inventory.json @@ -2991,7 +2991,7 @@ { "hazard_type": "RiverineInundation", "group_id": "", - "path": "inundation/river_tudelft/v2/flood_depth_unprot_{scenario}_{year}", + "path": "inundation/river_tudelft/v1/flood_depth_{scenario}_{year}", "indicator_id": "flood_depth", "indicator_model_id": "tudelft", "indicator_model_gcm": "CLMcom-CCLM4-8-17-EC-EARTH", @@ -3009,7 +3009,7 @@ "nodata_index": 0, "units": "metres" }, - "path": "maps/inundation/river_tudelft/v2/flood_depth_unprot_{scenario}_{year}_map", + "path": "maps/inundation/river_tudelft/v1/flood_depth_{scenario}_{year}_map", "bounds": [], "bbox": [], "index_values": null, @@ -3019,21 +3019,21 @@ { "id": "historical", "years": [ - 1985 + 1971 ] }, { - "id": "rcp4p5", + "id": "rcp45", "years": [ - 2035, - 2085 + 2050, + 2070 ] }, { - "id": "rcp8p5", + "id": "rcp85", "years": [ - 2035, - 2085 + 2050, + 2070 ] } ], @@ -3070,65 +3070,525 @@ { "id": "historical", "years": [ - 1985 + 1971 ] }, { "id": "rcp4p5", "years": [ - 2035, - 2085 + 2050, + 2070 ] }, { "id": "rcp8p5", "years": [ - 2035, - 2085 + 2050, + 2070 ] } ], "units": "years" }, { - "hazard_type": "Subsidence", - "group_id": "", - "path": "subsidence/csm/v1/land_subsidence_rate_{scenario}_{year}", - "indicator_id": "land_subsidence_rate", - "indicator_model_id": null, - "indicator_model_gcm": "historical", - "params": {}, - "display_name": "Land subsidence rate (Davydzenka et Al (2024))", - "display_groups": [], - "description": "Land subsidence refers to gradual settlement or rapid sinking of the ground that can occur as a result of natural factors (e.g., volcanic or seismic activity, collapse of subsurface cavities, compaction of loose fine-grained deposits) or anthropogenic activities (e.g., excessive groundwater (GW) abstraction, mining, subsurface energy extraction). It is a destructive phenomenon occurring around the globe causing damage to infrastructure, increased flood risks, and reduction of aquifer storage. However, prediction and quantification of land subsidence rates globally using physics-based methods presents a major challenge. Capitalizing on the robustness of modern deep learning methods and taking advantage of the increasingly available data sets of environmental parameters, the study [here](https://doi.org/10.1029/2023GL104497) introduces the first map of land subsidence rates on a global scale with a spatial resolution of 30 \u00c3\u2014 30 arc seconds (approx. 1 km at the equator).\n\nDavydzenka, T., Tahmasebi, P., & Shokri, N. (2023). [Data](https://doi.org/10.5281/zenodo.10223637) for \"Unveiling the Global Extent of Land Subsidence: The sinking crisis\".", - "map": { - "colormap": { - "min_index": 1, - "min_value": 0.0, - "max_index": 255, - "max_value": 100.0, - "name": "heating", - "nodata_index": 0, - "units": "millimetres/year" + "hazard_type": "Subsidence", + "group_id": "", + "path": "subsidence/csm/v1/land_subsidence_rate_{scenario}_{year}", + "indicator_id": "land_subsidence_rate", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "Land subsidence rate (Davydzenka et Al (2024))", + "display_groups": [], + "description": "Land subsidence refers to gradual settlement or rapid sinking of the ground that can occur as a result of natural factors (e.g., volcanic or seismic activity, collapse of subsurface cavities, compaction of loose fine-grained deposits) or anthropogenic activities (e.g., excessive groundwater (GW) abstraction, mining, subsurface energy extraction). It is a destructive phenomenon occurring around the globe causing damage to infrastructure, increased flood risks, and reduction of aquifer storage. However, prediction and quantification of land subsidence rates globally using physics-based methods presents a major challenge. Capitalizing on the robustness of modern deep learning methods and taking advantage of the increasingly available data sets of environmental parameters, the study [here](https://doi.org/10.1029/2023GL104497) introduces the first map of land subsidence rates on a global scale with a spatial resolution of 30 \u00c3\u2014 30 arc seconds (approx. 1 km at the equator).\n\nDavydzenka, T., Tahmasebi, P., & Shokri, N. (2023). [Data](https://doi.org/10.5281/zenodo.10223637) for \"Unveiling the Global Extent of Land Subsidence: The sinking crisis\".", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 100.0, + "name": "heating", + "nodata_index": 0, + "units": "millimetres/year" + }, + "path": "maps/subsidence/csm/v1/land_subsidence_rate_{scenario}_{year}_map", + "bounds": [ + [ + -180.0, + 85.0 + ], + [ + 180.0, + 85.0 + ], + [ + 180.0, + -60.0 + ], + [ + -180.0, + -60.0 + ] + ], + "bbox": [ + -180.0, + -60.0, + 180.0, + 85.0 + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 2021 + ] + } + ], + "units": "millimetres/year" + }, + { + "hazard_type": "CoastalInundation", + "group_id": "coastal_tudelft", + "path": "inundation/coastal_tudelft/v1/flood_depth_{scenario}_{year}", + "indicator_id": "flood_depth", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "Coastal Flood Depth (tudelft)", + "display_groups": [], + "description": "\n GIS-compatible files containing data related to the probability of \n river floods occurring in Europe under present and future climate. \n Includes gridded (GeoTIFF) datasets of river flood extents (in two \n variants, with or without flood protection) and water depths.\n Additionally includes extreme river discharge estimates in ESRI \n Shapefile format. Based upon CLMcom-CCLM4-8-17-EC-EARTH regional \n climate simulation (EURO-CORDEX).\n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 5.0, + "name": "flare", + "nodata_index": 0, + "units": "meters" + }, + "path": "flood_depth_{scenario}_{year}_map", + "bounds": [ + [ + -180.0, + 85.0 + ], + [ + 180.0, + 85.0 + ], + [ + 180.0, + -85.0 + ], + [ + -180.0, + -85.0 + ] + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 1971 + ] + }, + { + "id": "rcp45", + "years": [ + 2050, + 2070 + ] + }, + { + "id": "rcp85", + "years": [ + 2050, + 2070 + ] + } + ], + "units": "none" + }, + { + "hazard_type": "ChronicWind", + "group_id": "wind_tudelft", + "path": "wind/wind_tudelft/v1/wind25_{scenario}_{year}", + "indicator_id": "wind25", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "Wind gust speed greater than 25 m/s", + "display_groups": [], + "description": "\n NetCDF files containing gridded annual probability of severe convective\n windstorms (wind gusts > 25 m/s) and of extremely severe convective\n windstorms (wind gusts > 32 m/s) for present day and the future climate.\n The fields are multi model means of 15 regional climate model simulations (CORDEX).\n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 1.0, + "name": "flare", + "nodata_index": 0, + "units": "prob" + }, + "path": "wind25_{scenario}_{year}_map", + "bounds": [ + [ + -45.0, + 73.0 + ], + [ + 65.5, + 73.0 + ], + [ + 65.5, + 21.5 + ], + [ + -45.0, + -21.5 + ] + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 1971 + ] + }, + { + "id": "rcp45", + "years": [ + 2050, + 2100 + ] + }, + { + "id": "rcp85", + "years": [ + 2050, + 2100 + ] + } + ], + "units": "none" + }, + { + "hazard_type": "ChronicWind", + "group_id": "wind_tudelft", + "path": "wind/wind_tudelft/v1/wind32_{scenario}_{year}", + "indicator_id": "wind32", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "Wind gust speed greater than 32 m/s", + "display_groups": [], + "description": "\n NetCDF files containing gridded annual probability of severe convective\n windstorms (wind gusts > 25 m/s) and of extremely severe convective\n windstorms (wind gusts > 32 m/s) for present day and the future climate.\n The fields are multi model means of 15 regional climate model simulations (CORDEX).\n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 1.0, + "name": "flare", + "nodata_index": 0, + "units": "prob" + }, + "path": "wind32_{scenario}_{year}_map", + "bounds": [ + [ + -45.0, + 73.0 + ], + [ + 65.5, + 73.0 + ], + [ + 65.5, + 21.5 + ], + [ + -45.0, + -21.5 + ] + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 1971 + ] + }, + { + "id": "rcp45", + "years": [ + 2050, + 2100 + ] + }, + { + "id": "rcp85", + "years": [ + 2050, + 2100 + ] + } + ], + "units": "none" + }, + { + "hazard_type": "Fire", + "group_id": "fire_tudelft", + "path": "fire/fire_tudelft/v1/fwi20_{scenario}_{year}", + "indicator_id": "fwi20", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "FWI under 20", + "display_groups": [], + "description": "\n NetCDF files containing daily probabilities of high forest fire danger in \n Europe under present and projected future climates. Includes gridded (NetCDF) \n datasets of high forest fire danger probabilities for the present climate \n (1981-2010) based on the ERA-Interim reanalysis and for the projected \n climates under the RCP4.5 and RCP8.5 scenarios (periods 1971-2000, 2021-2050 \n and 2071-2100). \n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 100.0, + "name": "flare", + "nodata_index": 0, + "units": "prob" + }, + "path": "fwi20_{scenario}_{year}_map", + "bounds": [ + [ + -45.0, + 73.0 + ], + [ + 65.5, + 73.0 + ], + [ + 65.5, + 21.5 + ], + [ + -45.0, + -21.5 + ] + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 1971 + ] + }, + { + "id": "rcp45", + "years": [ + 2050, + 2100 + ] + }, + { + "id": "rcp85", + "years": [ + 2050, + 2100 + ] + } + ], + "units": "none" + }, + { + "hazard_type": "Fire", + "group_id": "fire_tudelft", + "path": "fire/fire_tudelft/v1/fwi45_{scenario}_{year}", + "indicator_id": "fwi45", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "FWI under 45", + "display_groups": [], + "description": "\n NetCDF files containing daily probabilities of high forest fire danger in \n Europe under present and projected future climates. Includes gridded (NetCDF) \n datasets of high forest fire danger probabilities for the present climate \n (1981-2010) based on the ERA-Interim reanalysis and for the projected \n climates under the RCP4.5 and RCP8.5 scenarios (periods 1971-2000, 2021-2050 \n and 2071-2100). \n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 100.0, + "name": "flare", + "nodata_index": 0, + "units": "prob" + }, + "path": "fwi45_{scenario}_{year}_map", + "bounds": [ + [ + -45.0, + 73.0 + ], + [ + 65.5, + 73.0 + ], + [ + 65.5, + 21.5 + ], + [ + -45.0, + -21.5 + ] + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 1971 + ] + }, + { + "id": "rcp45", + "years": [ + 2050, + 2100 + ] + }, + { + "id": "rcp85", + "years": [ + 2050, + 2100 + ] + } + ], + "units": "none" + }, + { + "hazard_type": "Landslide", + "group_id": "landslide_jrc", + "path": "drought/landslide_jrc/v1/susceptability_{scenario}_{year}", + "indicator_id": "susceptability", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "Landslide Susceptability", + "display_groups": [], + "description": "\n The spatial dataset (GIS map) shows landslide susceptibility levels at European scale, \n derived from heuristic-statistical modelling of main landslide conditioning factors \n using also landslide location data. It covers all EU member states except Malta, in \n addition to Albania, Andorra, Bosnia and Herzegovina, Croatia, FYR Macedonia, Iceland, \n Kosovo, Liechtenstein, Montenegro, Norway, San Marino, Serbia, and Switzerland.\n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 5.0, + "name": "flare", + "nodata_index": 0, + "units": "meters" + }, + "path": "susceptability_{scenario}_{year}_map", + "bounds": [ + [ + -180.0, + 85.0 + ], + [ + 180.0, + 85.0 + ], + [ + 180.0, + -85.0 + ], + [ + -180.0, + -85.0 + ] + ], + "index_values": null, + "source": "map_array_pyramid" + }, + "scenarios": [ + { + "id": "historical", + "years": [ + 1980 + ] + } + ], + "units": "none" + }, + { + "hazard_type": "Subsidence", + "group_id": "subsidence_jrc", + "path": "drought/subsidence_jrc/v1/susceptability_{scenario}_{year}", + "indicator_id": "susceptability", + "indicator_model_id": null, + "indicator_model_gcm": "historical", + "params": {}, + "display_name": "Subsidence Susceptability", + "display_groups": [], + "description": "\n A number of layers for soil properties have been created based on data from the European\n Soil Database in combination with data from the Harmonized World Soil Database (HWSD) \n and Soil-Terrain Database (SOTER). The available layers include: Total available water \n content, Depth available to roots, Clay content, Silt content, Sand content, Organic \n carbon, Bulk Density, Coarse fragments.\n ", + "map": { + "colormap": { + "min_index": 1, + "min_value": 0.0, + "max_index": 255, + "max_value": 5.0, + "name": "flare", + "nodata_index": 0, + "units": "index" + }, + "path": "susceptability_{scenario}_{year}_map", + "bounds": [ + [ + -180.0, + 85.0 + ], + [ + 180.0, + 85.0 + ], + [ + 180.0, + -85.0 + ], + [ + -180.0, + -85.0 + ] + ], + "index_values": null, + "source": "map_array_pyramid" }, - "path": "maps/subsidence/csm/v1/land_subsidence_rate_{scenario}_{year}_map", - "bounds": [ - [-180.0, 85.0], - [180.0, 85.0], - [180.0, -60.0], - [-180.0, -60.0] + "scenarios": [ + { + "id": "historical", + "years": [ + 1980 + ] + } ], - "bbox": [-180.0, -60.0, 180.0, 85.0], - "index_values": null, - "source": "map_array_pyramid" - }, - "scenarios": [ - { - "id": "historical", - "years": [2021] - } - ], - "units": "millimetres/year" + "units": "none" } ] } diff --git a/src/physrisk/hazard_models/core_hazards.py b/src/physrisk/hazard_models/core_hazards.py index 93568d16..6a779afa 100644 --- a/src/physrisk/hazard_models/core_hazards.py +++ b/src/physrisk/hazard_models/core_hazards.py @@ -10,6 +10,11 @@ CoastalInundation, RiverineInundation, Wind, + ChronicWind, + Fire, + WaterRisk, + Landslide, + Subsidence, ) @@ -194,10 +199,20 @@ def __init__( else self._select_riverine_inundation_tudelft, ) self.add_selector( - CoastalInundation, "flood_depth", self._select_coastal_inundation + CoastalInundation, + "flood_depth", + self._select_coastal_inundation + if flood_model == CoreFloodModels.WRI + else self._select_coastal_inundation_tudelft, ) self.add_selector(Wind, "max_speed", self._select_wind) + self.add_selector(ChronicWind, "wind25", self._select_chronicwind) + self.add_selector(Fire, "fwi20", self._select_fire) + self.add_selector(WaterRisk, "water_stress", self._select_water_stress) + self.add_selector(Landslide, "susceptability", self._select_landslide) + self.add_selector(Subsidence, "susceptability", self._select_subsidence) + def resources_with(self, *, hazard_type: type, indicator_id: str): return ResourceSubset( self._inventory.resources_by_type_id[(hazard_type.__name__, indicator_id)] @@ -241,6 +256,15 @@ def _select_riverine_inundation_tudelft( ): return candidates.with_model_id("tudelft").first() + @staticmethod + def _select_coastal_inundation_tudelft( + candidates: ResourceSubset, + scenario: str, + year: int, + hint: Optional[HazardDataHint] = None, + ): + return candidates.with_group_id("coastal_tudelft").first() + @staticmethod def _select_wind( candidates: ResourceSubset, @@ -250,6 +274,51 @@ def _select_wind( ): return candidates.prefer_group_id("iris_osc").first() + @staticmethod + def _select_chronicwind( + candidates: ResourceSubset, + scenario: str, + year: int, + hint: Optional[HazardDataHint] = None, + ): + return candidates.with_group_id("wind_tudelft").first() + + @staticmethod + def _select_fire( + candidates: ResourceSubset, + scenario: str, + year: int, + hint: Optional[HazardDataHint] = None, + ): + return candidates.with_group_id("fire_tudelft").first() + + @staticmethod + def _select_water_stress( + candidates: ResourceSubset, + scenario: str, + year: int, + hint: Optional[HazardDataHint] = None, + ): + return candidates.first() + + @staticmethod + def _select_landslide( + candidates: ResourceSubset, + scenario: str, + year: int, + hint: Optional[HazardDataHint] = None, + ): + return candidates.with_group_id("landslide_jrc").first() + + @staticmethod + def _select_subsidence( + candidates: ResourceSubset, + scenario: str, + year: int, + hint: Optional[HazardDataHint] = None, + ): + return candidates.with_group_id("subsidence_jrc").first() + def cmip6_scenario_to_rcp(scenario: str): """Convention is that CMIP6 scenarios are expressed by identifiers: @@ -267,7 +336,18 @@ def cmip6_scenario_to_rcp(scenario: str): elif scenario == "ssp585": return "rcp8p5" else: - if scenario not in ["rcp2p6", "rcp4p5", "rcp6p0", "rcp8p5", "historical"]: + if scenario not in [ + "rcp2p6", + "rcp4p5", + "rcp6p0", + "rcp8p5", + "historical", + # Added while the data is in the dev bucket with the old naming. + "rcp26", + "rcp45", + "rcp60", + "rcp85", + ]: raise ValueError(f"unexpected scenario {scenario}") return scenario diff --git a/src/physrisk/kernel/__init__.py b/src/physrisk/kernel/__init__.py index 41f35bb3..8bf0c355 100644 --- a/src/physrisk/kernel/__init__.py +++ b/src/physrisk/kernel/__init__.py @@ -1,6 +1 @@ -from .assets import Asset, PowerGeneratingAsset -from .curve import ExceedanceCurve -from .hazard_event_distrib import HazardEventDistrib -from .hazards import Drought, Hazard, RiverineInundation -from .vulnerability_distrib import VulnerabilityDistrib -from .vulnerability_model import VulnerabilityModelAcuteBase +"""Kernel init.""" diff --git a/src/physrisk/kernel/assets.py b/src/physrisk/kernel/assets.py index 6d5bb30c..2e33588b 100644 --- a/src/physrisk/kernel/assets.py +++ b/src/physrisk/kernel/assets.py @@ -4,7 +4,7 @@ # 'primary_fuel' entries in Global Power Plant Database v1.3.0 (World Resources Institute) -# https://wri-dataportal-prod.s3.amazonaws.com/manual/global_power_plant_database_v_1_3 +# https://datasets.wri.org/dataset/globalpowerplantdatabase class FuelKind(Enum): Biomass = 1 Coal = 2 diff --git a/src/physrisk/kernel/calculation.py b/src/physrisk/kernel/calculation.py index 4ba2df9f..6016de6c 100644 --- a/src/physrisk/kernel/calculation.py +++ b/src/physrisk/kernel/calculation.py @@ -1,4 +1,5 @@ -from typing import Dict, Sequence, Type +from typing import Dict, Optional, Sequence, Type + from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.hazard_models.core_hazards import get_default_source_paths @@ -6,7 +7,10 @@ from physrisk.kernel.impact_distrib import ImpactType from physrisk.kernel.risk import RiskMeasureCalculator, RiskMeasuresFactory from physrisk.risk_models.generic_risk_model import GenericScoreBasedRiskMeasures -from physrisk.risk_models.risk_models import RealEstateToyRiskMeasures +from physrisk.risk_models.risk_models import ( + RealEstateToyRiskMeasures, + ThermalPowerPlantsRiskMeasures, +) from physrisk.vulnerability_models import power_generating_asset_models as pgam from physrisk.vulnerability_models.chronic_heat_models import ChronicHeatGZNModel from physrisk.vulnerability_models.example_models import PlaceholderVulnerabilityModel @@ -16,6 +20,7 @@ RealEstateCoastalInundationModel, RealEstateRiverineInundationModel, ) + from physrisk.vulnerability_models.thermal_power_generation_models import ( Asset, ThermalPowerGenerationAirTemperatureModel, @@ -24,17 +29,22 @@ ThermalPowerGenerationRiverineInundationModel, ThermalPowerGenerationWaterStressModel, ThermalPowerGenerationWaterTemperatureModel, + ThermalPowerGenerationSevereConvectiveWindstormModel, + ThermalPowerGenerationHighFireModel, + ThermalPowerGenerationAqueductWaterRiskModel, + ThermalPowerGenerationLandslideModel, + ThermalPowerGenerationSubsidenceModel, ) -from .assets import ( +from physrisk.kernel.assets import ( IndustrialActivity, PowerGeneratingAsset, RealEstateAsset, TestAsset, ThermalPowerGeneratingAsset, ) -from .hazard_model import HazardModel -from .vulnerability_model import VulnerabilityModelBase +from physrisk.kernel.hazard_model import HazardModel +from physrisk.kernel.vulnerability_model import VulnerabilityModelBase def get_default_hazard_model() -> HazardModel: @@ -83,13 +93,60 @@ def get_default_vulnerability_models() -> Dict[type, Sequence[VulnerabilityModel } -def get_default_risk_measure_calculators() -> Dict[Type[Asset], RiskMeasureCalculator]: +def get_stress_test_vulnerability_models() -> ( + Dict[type, Sequence[VulnerabilityModelBase]] +): + """Get exposure/vulnerability models for different asset types. + + This set uses the data used in the stress test article from the ECB. + """ + return { + PowerGeneratingAsset: [pgam.InundationModel()], + RealEstateAsset: [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + GenericTropicalCycloneModel(), + CoolingModel(), + ], + IndustrialActivity: [ChronicHeatGZNModel()], + ThermalPowerGeneratingAsset: [ + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ThermalPowerGenerationSevereConvectiveWindstormModel(), + ThermalPowerGenerationHighFireModel(), + ThermalPowerGenerationAqueductWaterRiskModel(), + ThermalPowerGenerationLandslideModel(), + ThermalPowerGenerationSubsidenceModel(), + ], + TestAsset: [pgam.TemperatureModel()], + } + + +def get_default_risk_measure_calculators() -> Dict[type, RiskMeasureCalculator]: """For asset-level risk measure, define the measure calculators to use.""" return {RealEstateAsset: RealEstateToyRiskMeasures()} +def get_stress_test_risk_measure_calculators() -> Dict[type, RiskMeasureCalculator]: + """For asset-level stress test risk measure, define the measure calculators to use.""" + return {ThermalPowerGeneratingAsset: ThermalPowerPlantsRiskMeasures()} + + +def get_generic_risk_measure_calculators() -> Dict[type, RiskMeasureCalculator]: + """For asset-level generic risk measure, define the measure calculators to use.""" + return {Asset: GenericScoreBasedRiskMeasures()} + + class DefaultMeasuresFactory(RiskMeasuresFactory): - def calculators(self, use_case_id: str) -> Dict[Type[Asset], RiskMeasureCalculator]: - if use_case_id == "generic": - return {Asset: GenericScoreBasedRiskMeasures()} - return get_default_risk_measure_calculators() + """Factory class for selecting appropriate risk measure calculators based on the use case.""" + + def calculators( + self, use_case_id: str = "" + ) -> Dict[Type[Asset], RiskMeasureCalculator]: + """Get the appropriate risk measure calculators based on the use case identifier.""" + if use_case_id.upper() == "DEFAULT": + return get_default_risk_measure_calculators() + elif use_case_id.upper() == "STRESS_TEST": + return get_stress_test_risk_measure_calculators() + else: + return get_generic_risk_measure_calculators() diff --git a/src/physrisk/kernel/hazards.py b/src/physrisk/kernel/hazards.py index 4c8ed8ae..b325688b 100644 --- a/src/physrisk/kernel/hazards.py +++ b/src/physrisk/kernel/hazards.py @@ -112,6 +112,11 @@ class Subsidence(Hazard): pass +class Landslide(Hazard): + kind = HazardKind.CHRONIC + pass + + def all_hazards(): return [ obj diff --git a/src/physrisk/kernel/impact_distrib.py b/src/physrisk/kernel/impact_distrib.py index d6627124..1d6cf3c6 100644 --- a/src/physrisk/kernel/impact_distrib.py +++ b/src/physrisk/kernel/impact_distrib.py @@ -25,11 +25,14 @@ def __init__( impact_type: ImpactType = ImpactType.damage, ): """Create a new asset event distribution. + Args: + ---- event_type: type of event impact_bins: non-decreasing impact bin bounds prob: probabilities with size [len(intensity_bins) - 1] path: path to the hazard indicator data source + """ self.__hazard_type = hazard_type self.__impact_bins = np.array(impact_bins) diff --git a/src/physrisk/kernel/risk.py b/src/physrisk/kernel/risk.py index c4712d97..33821081 100644 --- a/src/physrisk/kernel/risk.py +++ b/src/physrisk/kernel/risk.py @@ -14,12 +14,16 @@ ) from physrisk.api.v1.impact_req_resp import Category, ScoreBasedRiskMeasureDefinition +from physrisk.kernel import calculation from physrisk.kernel.assets import Asset from physrisk.kernel.hazard_model import HazardModel from physrisk.kernel.hazards import Hazard, all_hazards from physrisk.kernel.impact import AssetImpactResult, ImpactKey, calculate_impacts -from physrisk.kernel.vulnerability_model import VulnerabilityModels - +from physrisk.kernel.impact_distrib import EmptyImpactDistrib +from physrisk.kernel.vulnerability_model import ( + DictBasedVulnerabilityModelsFactory, + VulnerabilityModels, +) # from asyncio import ALL_COMPLETED # import concurrent.futures @@ -33,18 +37,54 @@ class BatchId(NamedTuple): class RiskModel: - """Base class for a risk model (i.e. a calculation of risk that makes use of hazard and vulnerability - models).""" + """Base class for a risk model. + + That is, a calculation of risk that makes use of hazard and vulnerability models). + """ def __init__( - self, hazard_model: HazardModel, vulnerability_models: VulnerabilityModels + self, + hazard_model: HazardModel, + vulnerability_models: Optional[VulnerabilityModels] = None, + use_case_id: Optional[str] = "DEFAULT", ): + """Initialize a RiskModel instance. + + Parameter: + --------- + hazard_model (HazardModel): The hazard model to be used for risk calculations. + vulnerability_models (Optional[VulnerabilityModels]): Optional vulnerability models; if not provided, will use default. + use_case_id (Optional[str]): Use case identifier to determine vulnerability models if not provided. + + Raise: + ----- + ValueError: If neither vulnerability_models nor use_case_id is provided. + + """ + super().__init__() + if vulnerability_models is None and use_case_id is None: + raise ValueError( + "Either vulnerability_models or use_case_id must be provided." + ) + self._hazard_model = hazard_model - self._vulnerability_models = vulnerability_models + + if use_case_id is None: + self.use_case_id = "DEFAULT" + else: + self.use_case_id = use_case_id + + if vulnerability_models is None: + factory = DictBasedVulnerabilityModelsFactory(self.use_case_id) + self._vulnerability_models = factory.vulnerability_models() + else: + self._vulnerability_models = vulnerability_models def calculate_risk_measures( self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int] - ): ... + ): + """Calculate risk measures for a set of assets, scenarios, and years.""" + ... def _calculate_all_impacts( self, @@ -163,26 +203,68 @@ def aggregate_risk_measures( class RiskMeasuresFactory(Protocol): - def calculators(self, use_case_id: str) -> Dict[Type[Asset], RiskMeasureCalculator]: + """Protocol for selecting risk measure calculators.""" + + def calculators( + self, use_case_id: str = "" + ) -> Dict[Type[Asset], RiskMeasureCalculator]: + """Get risk measure calculators for asset types. + + Args: + ---- + use_case_id (Optional[str]): Optional use case ID to filter calculators. + + """ pass class AssetLevelRiskModel(RiskModel): + """Risk model that calculates risk measures at the asset level for various assets.""" + def __init__( self, hazard_model: HazardModel, - vulnerability_models: VulnerabilityModels, - measure_calculators: Dict[type, RiskMeasureCalculator], + vulnerability_models: Optional[VulnerabilityModels] = None, + measure_calculators: Optional[Dict[type, RiskMeasureCalculator]] = None, + use_case_id: Optional[str] = None, ): - """Risk model that calculates risk measures at the asset level for a sequence - of assets. + """Risk model that calculates risk measures at the asset level for a sequence of assets. Args: + ---- hazard_model (HazardModel): The hazard model. - vulnerability_models (Dict[type, Sequence[VulnerabilityModelBase]]): Vulnerability models for asset types. + vulnerability_models (VulnerabilityModels): Vulnerability models for asset types. measure_calculators (Dict[type, RiskMeasureCalculator]): Risk measure calculators for asset types. + use_case_id (str): 'use case' identifier used to get the measure calculators and/or vulnerability + models if they are not provided. + """ - super().__init__(hazard_model, vulnerability_models) + if vulnerability_models is None and use_case_id is None: + raise ValueError( + "Either vulnerability_models or use_case_id must be provided." + ) + + if measure_calculators is None and use_case_id is None: + raise ValueError( + "Either measure_calculators or use_case_id must be provided." + ) + + if use_case_id is None: + self.use_case_id = "DEFAULT" + else: + self.use_case_id = use_case_id + + if measure_calculators is None: + measure_factory = calculation.DefaultMeasuresFactory() + measure_calculators = measure_factory.calculators(self.use_case_id) + + if vulnerability_models is None: + vulnerability_factory = DictBasedVulnerabilityModelsFactory( + self.use_case_id + ) + vulnerability_models = vulnerability_factory.vulnerability_models() + + super().__init__(hazard_model, vulnerability_models, use_case_id) self._measure_calculators = measure_calculators def calculate_impacts( @@ -229,7 +311,9 @@ def get_measure_id( ): if measure_calc is None: return "na" + measure = measure_calc.get_definition(hazard_type=hazard_type) + return measure_id_lookup[measure] if measure is not None else "na" for hazard_type in hazards: @@ -240,6 +324,39 @@ def get_measure_id( def calculate_risk_measures( self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int] ): + """Calculate risk measures for a set of assets, scenarios, and years, according to the selected method calculation. + + For the Default Method: + The impact of the historical scenario is chosen as the base impact, and risk measures are + calculated using the calc_measure function defined in the RealEstateToyRiskMeasures class. This method performs + calculations differently depending on whether the hazard is chronic heat or another type. The difference between + the two methods is that calc_measure_cooling uses mean impacts for calculations, while calc_measure_acute uses + exceedance curves. In both cases, a Measure object is returned, which contains a score (REDFLAG, HIGH, MEDIUM, LOW), + measures_0 (future_loss), and a definition. + + For the stress_test method: + An StressTestImpact object is chosen as the base impact, and risk measures are calculated using the calc_measure + defined in the ThermalPowerPlantsRiskMeasures class. In this method the base impact is first used to obtain the + percentiles (norisk, p50, p75, p90), which are used to evaluate the impact via its mean_intensity. + This method also returns a Measure object with a score (HIGH, MEDIUM, LOW, NORISK, NODATA), + measures_0 (mean_intensity), and a definition. + + Args: + ---- + assets (Sequence[Asset]): List of assets. + prosp_scens (Sequence[str]): List of prospective scenarios. + years (Sequence[int]): List of years for the calculations. + + Return: + ------ + Tuple[ + Dict[Tuple[Asset, type], AssetImpactResult], + Dict[MeasureKey, Measure] + ]: A tuple containing: + - A dictionary mapping asset and hazard type tuples to impact results. + - A dictionary mapping MeasureKeys to calculated measures. + + """ impacts = self._calculate_all_impacts( assets, prosp_scens, years, include_histo=True ) @@ -249,16 +366,18 @@ def calculate_risk_measures( if type(asset) not in self._measure_calculators: continue measure_calc = self._measure_calculators[type(asset)] + hazards = measure_calc.supported_hazards() for prosp_scen in prosp_scens: for year in years: - for hazard_type in measure_calc.supported_hazards(): + for hazard_type in hazards: base_impacts = impacts.get( ImpactKey( asset=asset, hazard_type=hazard_type, scenario="historical", key_year=None, - ) + ), + [EmptyImpactDistrib()], ) prosp_impacts = impacts.get( ImpactKey( @@ -266,7 +385,8 @@ def calculate_risk_measures( hazard_type=hazard_type, scenario=prosp_scen, key_year=year, - ) + ), + [EmptyImpactDistrib()], ) risk_inds = [ measure_calc.calc_measure( diff --git a/src/physrisk/kernel/vulnerability_model.py b/src/physrisk/kernel/vulnerability_model.py index 6886d076..96436aa1 100644 --- a/src/physrisk/kernel/vulnerability_model.py +++ b/src/physrisk/kernel/vulnerability_model.py @@ -19,13 +19,18 @@ import physrisk.data.static.vulnerability from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType -from ..api.v1.common import VulnerabilityCurve, VulnerabilityCurves -from .assets import Asset -from .curve import ExceedanceCurve -from .hazard_event_distrib import HazardEventDistrib -from .hazard_model import HazardDataRequest, HazardDataResponse, HazardEventDataResponse -from .vulnerability_distrib import VulnerabilityDistrib -from .vulnerability_matrix_provider import VulnMatrixProvider +from physrisk.api.v1.common import VulnerabilityCurve, VulnerabilityCurves +from physrisk.kernel.assets import Asset +from physrisk.kernel import calculation as calc +from physrisk.kernel.curve import ExceedanceCurve +from physrisk.kernel.hazard_event_distrib import HazardEventDistrib +from physrisk.kernel.hazard_model import ( + HazardDataRequest, + HazardDataResponse, + HazardEventDataResponse, +) +from physrisk.kernel.vulnerability_distrib import VulnerabilityDistrib +from physrisk.kernel.vulnerability_matrix_provider import VulnMatrixProvider PLUGINS = dict() # type:ignore @@ -104,8 +109,7 @@ def __init__(self, indicator_id: str, hazard_type: type, impact_type: ImpactType def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: - """Provide the one or more hazard event data requests required in order to calculate - the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + """Provide the one or more hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" ... @abstractmethod @@ -118,10 +122,12 @@ class VulnerabilityModels(Protocol): def vuln_model_for_asset_of_type( self, type: Type[Asset] ) -> Sequence[VulnerabilityModelBase]: - """Returns for a given asset type the vulnerability models for each hazard required. + """Return for a given asset type the vulnerability models for each hazard required. - Returns: + Returns + ------- Dict[type, Sequence[VulnerabilityModelBase]]: Vulnerability models.s + """ ... @@ -129,15 +135,44 @@ def vuln_model_for_asset_of_type( class VulnerabilityModelsFactory(Protocol): def vulnerability_models(self) -> VulnerabilityModels: """Create a VulnerabilityModels instance, that can based on a number of options. + Although no options used at present, implemented this way in order to add in future (e.g. to allow a request to specify preferred methodology). - Returns: + Returns + ------- VulnerabilityModels: Instance that provides vulnerability models for asset types. + """ ... +class DictBasedVulnerabilityModelsFactory(VulnerabilityModelsFactory): + """Factory class for selecting appropriate vulnerability models based on the use case.""" + + def __init__(self, use_case_id: str = "DEFAULT"): + """Initialize the factory with a specific use case identifier. + + Parameters + ---------- + use_case_id : str, optional + An identifier for the use case to determine the appropriate vulnerability models. + Defaults to DEFAULT. + + """ + self.use_case_id = use_case_id + + def vulnerability_models(self) -> VulnerabilityModels: + """Get the appropriate vulnerability model based on the use case identifier.""" + if self.use_case_id.upper() == "DEFAULT": + models = calc.get_default_vulnerability_models() + elif self.use_case_id.upper() == "STRESS_TEST": + models = calc.get_stress_test_vulnerability_models() + else: + raise ValueError("Unsupported use_case_id") + return DictBasedVulnerabilityModels(models) + + class DictBasedVulnerabilityModels(VulnerabilityModels): def __init__(self, models: Dict[Type[Asset], Sequence[VulnerabilityModelBase]]): self.models = models @@ -147,9 +182,7 @@ def vuln_model_for_asset_of_type(self, type: Type[Asset]): class VulnerabilityModelAcuteBase(VulnerabilityModelBase): - """Models generate the VulnerabilityDistrib and HazardEventDistrib of an - Asset. - """ + """Models generate the VulnerabilityDistrib and HazardEventDistrib of an Asset.""" def __init__(self, indicator_id: str, hazard_type: type, impact_type: ImpactType): super().__init__( @@ -161,11 +194,14 @@ def get_distributions( self, asset: Asset, event_data_responses: Iterable[HazardDataResponse] ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: """Return distributions for asset: VulnerabilityDistrib and HazardEventDistrib. + The hazard event data is used to do this. Args: + ---- asset: the asset. event_data_responses: the responses to the requests made by get_data_requests, in the same order. + """ ... @@ -179,8 +215,10 @@ def get_impact_details( """Return impact distribution along with vulnerability and hazard event distributions used to infer this. Args: + ---- asset: the asset. event_data_responses: the responses to the requests made by get_data_requests, in the same order. + """ vulnerability_dist, event_dist = self.get_distributions(asset, data_responses) impact_prob = vulnerability_dist.prob_matrix.T @ event_dist.prob @@ -204,8 +242,7 @@ def _check_event_type(self): class VulnerabilityModel(VulnerabilityModelAcuteBase): - """A vulnerability model that requires only specification of distributions of impacts for given intensities, - by implementing get_impact_curve.""" + """A vulnerability model that requires only specification of distributions of impacts for given intensities, by implementing get_impact_curve.""" def __init__( self, @@ -264,8 +301,10 @@ def get_distributions( def get_impact_curve( self, intensity_bin_centres: np.ndarray, asset: Asset ) -> VulnMatrixProvider: - """Defines a VulnMatrixProvider. The VulnMatrixProvider returns probabilities of specified impact bins - for the intensity bin centres.""" + """Define a VulnMatrixProvider. + + The VulnMatrixProvider returns probabilities of specified impact bins for the intensity bin centres. + """ ... @@ -302,20 +341,23 @@ def __init__( impact_type: ImpactType, buffer: Optional[int] = None, ): - """A vulnerability model that requires only specification of a damage/disruption curve. + """Define a vulnerability model that requires only specification of a damage/disruption curve. + This simple model contains no uncertainty around damage/disruption. The damage curve is passed via the constructor. The edges of the (hazard) intensity bins are determined by the granularity of the hazard data itself. The impact bin edges are inferred from the intensity bin edges, by looking up the impact corresponding to the hazard indicator intensity from the damage curve. - Args: - event_type (type): _description_ - damage_curve_intensities (Sequence[float]): Intensities - (i.e. hazard indicator values) of the damage/disruption (aka impact) curve. - damage_curve_impacts (Sequence[float]): Fractional damage to asset/disruption - to operation resulting from a hazard of the corresponding intensity. - indicator_id (str): ID of the hazard indicator to which this applies. Defaults to "". - buffer (Optional[int]): Delimitation of the area for the hazard data in metres (within [0,1000]). + Args: + ---- + event_type (type): _description_ + damage_curve_intensities (Sequence[float]): Intensities + (i.e. hazard indicator values) of the damage/disruption (aka impact) curve. + damage_curve_impacts (Sequence[float]): Fractional damage to asset/disruption + to operation resulting from a hazard of the corresponding intensity. + indicator_id (str): ID of the hazard indicator to which this applies. Defaults to "". + buffer (Optional[int]): Delimitation of the area for the hazard data in metres (within [0,1000]). + """ super().__init__( indicator_id=indicator_id, hazard_type=hazard_type, impact_type=impact_type diff --git a/src/physrisk/requests.py b/src/physrisk/requests.py index d3b007b0..2f16a7e0 100644 --- a/src/physrisk/requests.py +++ b/src/physrisk/requests.py @@ -32,12 +32,13 @@ RiskMeasuresFactory, ) from physrisk.kernel.vulnerability_model import ( - DictBasedVulnerabilityModels, VulnerabilityModels, VulnerabilityModelsFactory, + DictBasedVulnerabilityModelsFactory, ) -from .api.v1.hazard_data import ( + +from physrisk.api.v1.hazard_data import ( HazardAvailabilityRequest, HazardAvailabilityResponse, HazardDataRequest, @@ -49,7 +50,7 @@ IntensityCurve, Scenario, ) -from .api.v1.impact_req_resp import ( +from physrisk.api.v1.impact_req_resp import ( AcuteHazardCalculationDetails, AssetImpactRequest, AssetImpactResponse, @@ -57,21 +58,24 @@ Assets, AssetSingleImpact, ) -from .api.v1.impact_req_resp import ImpactKey as APIImpactKey -from .api.v1.impact_req_resp import ( +from physrisk.api.v1.impact_req_resp import ImpactKey as APIImpactKey +from physrisk.api.v1.impact_req_resp import ( RiskMeasureKey, RiskMeasures, RiskMeasuresForAssets, ScoreBasedRiskMeasureDefinition, ScoreBasedRiskMeasureSetDefinition, ) -from .data.image_creator import ImageCreator -from .data.inventory import EmbeddedInventory, Inventory -from .kernel import Asset, Hazard -from .kernel import calculation as calc -from .kernel.hazard_model import HazardDataRequest as hmHazardDataRequest -from .kernel.hazard_model import HazardEventDataResponse as hmHazardEventDataResponse -from .kernel.hazard_model import ( +from physrisk.data.image_creator import ImageCreator +from physrisk.data.inventory import EmbeddedInventory, Inventory +from physrisk.kernel.assets import Asset +from physrisk.kernel.hazards import Hazard +from physrisk.kernel import calculation as calc +from physrisk.kernel.hazard_model import HazardDataRequest as hmHazardDataRequest +from physrisk.kernel.hazard_model import ( + HazardEventDataResponse as hmHazardEventDataResponse, +) +from physrisk.kernel.hazard_model import ( HazardModel, HazardModelFactory, HazardParameterDataResponse, @@ -399,21 +403,23 @@ def _get_asset_impacts( measure_calculators: Optional[Dict[Type[Asset], RiskMeasureCalculator]] = None, assets: Optional[List[Asset]] = None, ): - vulnerability_models = ( - DictBasedVulnerabilityModels(calc.get_default_vulnerability_models()) - if vulnerability_models is None - else vulnerability_models - ) + if vulnerability_models is None: + factory = DictBasedVulnerabilityModelsFactory(request.use_case_id) + vulnerability_models = factory.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, assets) - measure_calculators = ( - calc.get_default_risk_measure_calculators() - if measure_calculators is None - else measure_calculators - ) + if measure_calculators is None: + factory_mc = calc.DefaultMeasuresFactory() + measure_calcs = factory_mc.calculators(request.use_case_id) + else: + measure_calcs = measure_calculators + risk_model = AssetLevelRiskModel( - hazard_model, vulnerability_models, measure_calculators + hazard_model, + vulnerability_models=vulnerability_models, + measure_calculators=measure_calcs, + use_case_id=request.use_case_id, ) scenarios = ( diff --git a/src/physrisk/risk_models/generic_risk_model.py b/src/physrisk/risk_models/generic_risk_model.py index 78ff0465..234f78b9 100644 --- a/src/physrisk/risk_models/generic_risk_model.py +++ b/src/physrisk/risk_models/generic_risk_model.py @@ -63,7 +63,9 @@ class HazardIndicatorBounds: class GenericScoreBasedRiskMeasures(RiskMeasureCalculator): - """A generic score based risk measure. 'Generic' indicates that the user of the score is unknown. + """A generic score based risk measure. + + 'Generic' indicates that the user of the score is unknown. i.e. it is unknown whether the user owns the assets in question, or interested in the assets from the point of view of loan origination or project financing. """ diff --git a/src/physrisk/risk_models/hazard_stress_test_percentiles.py b/src/physrisk/risk_models/hazard_stress_test_percentiles.py new file mode 100644 index 00000000..61189d80 --- /dev/null +++ b/src/physrisk/risk_models/hazard_stress_test_percentiles.py @@ -0,0 +1,226 @@ +"""Module containing the HazardPercentilesStressTest class.""" + + +class HazardPercentilesStressTest: + """Class for storing and managing hazard percentile from the stress test data. + + Attributes + ---------- + data (dict): Dictionary containing hazard percentile data. + + """ + + def __init__(self): + """Class for storing and managing hazard percentile from the stress test data. + + Attributes + ---------- + data (dict): Dictionary containing hazard percentile data. + + """ + self.data = { + "RiverineInundation": { + "historical_1985": [ + 0, + 2.0926772356033325, + 4.517875671386719, + 8.146644496917732, + ], + "rcp4p5_2035": [ + 0, + 2.123079776763916, + 4.727473139762878, + 8.397174167633054, + ], + "rcp4p5_2085": [ + 0, + 2.1849400997161865, + 4.746373891830444, + 8.531092262268068, + ], + "rcp8p5_2035": [ + 0, + 2.1317601203918457, + 4.699397325515747, + 8.462242317199708, + ], + "rcp8p5_2085": [ + 0, + 2.161303997039795, + 4.721877813339233, + 8.560105323791506, + ], + }, + "CoastalInundation": { + "historical_1985": [ + 0, + 2.3894078731536865, + 4.403600215911865, + 5.984654474258423, + ], + "rcp45_2050": [ + 0, + 2.3527116775512695, + 4.318411707878113, + 5.894825696945191, + ], + "rcp45_2070": [ + 0, + 2.5329999923706055, + 4.625603795051575, + 6.286437940597538, + ], + "rcp85_2050": [ + 0, + 2.318581223487854, + 4.2181302309036255, + 5.782120084762571, + ], + "rcp85_2070": [ + 0, + 2.5999999046325684, + 4.615525245666504, + 6.324570655822754, + ], + }, + "ChronicWind": { + "historical_1971": [ + 0, + 2.5999999046325684, + 4.615525245666504, + 6.324570655822754, + ], + "rcp45_2050": [ + 0, + 2.5999999046325684, + 4.615525245666504, + 6.324570655822754, + ], + "rcp45_2100": [ + 0, + 2.5999999046325684, + 4.615525245666504, + 6.324570655822754, + ], + "rcp85_2050": [ + 0, + 2.5999999046325684, + 4.615525245666504, + 6.324570655822754, + ], + "rcp85_2100": [ + 0, + 2.5999999046325684, + 4.615525245666504, + 6.324570655822754, + ], + }, + "Fire": { + "historical_1971": [ + 0, + 0.3631645739078522, + 6.0311747789382935, + 15.562520790100104, + ], + "rcp45_2050": [ + 0, + 0.5496047139167786, + 7.531521677970886, + 17.935127639770517, + ], + "rcp45_2100": [ + 0, + 0.5829123258590698, + 8.233976364135742, + 19.678568649292, + ], + "rcp85_2050": [ + 0, + 0.5778544545173645, + 7.795466423034668, + 18.597507476806644, + ], + "rcp85_2100": [ + 0, + 1.2540100812911987, + 10.670519351959229, + 23.3424015045166, + ], + }, + "WaterRisk": { + "ssp126_2030": [0, 1.0, 5.0, 6.0], + "ssp126_2050": [0, 1.0, 4.0, 6.0], + "ssp126_2080": [0, 1.0, 4.0, 6.0], + "ssp370_2030": [0, 1.0, 5.0, 6.0], + "ssp370_2050": [0, 1.0, 5.0, 6.0], + "ssp370_2080": [0, 1.0, 5.0, 6.0], + "ssp585_2030": [0, 1.0, 5.0, 6.0], + "ssp585_2050": [0, 1.0, 5.0, 6.0], + "ssp585_2080": [0, 1.0, 5.0, 6.0], + }, + "Landslide": {"historical_1980": [0, 2.0, 3.0, 4.0]}, + "Subsidence": {"historical_1980": [0, 2, 3, 4]}, + } + + def get_data(self, hazard_type: str, scenario: str, year: int): + """Retrieve data for a given hazard type, scenario, and year. This is used in StressTestImpact. + + Args: + ---- + hazard_type (str): The type of hazard (e.g., 'RiverineInundation'). + scenario (str): The scenario (e.g., 'historical', 'rcp45'). + year (int): The year (e.g., 1971, 2050). + + Returns: + ------- + list: The data for the given hazard type, scenario, and year. + + """ + key = f"{scenario}_{year}" + if hazard_type not in self.data: + return ["Error: Hazard type not found"] + + hazard_data = self.data[hazard_type] + + if key not in hazard_data: + return ["Error: Data for the given scenario and year not found"] + + return hazard_data[key] + + +class StressTestImpact: + """Class for calculating and retrieving stress test impacts for a specific hazard type, scenario, and year.""" + + def __init__( + self, + hazard_type: type, + scenario: str, + year: int, + ): + """Initialize a StressTestImpact instance. + + Args: + ---- + hazard_type (type): The type of hazard associated with the stress test. + scenario (str): The stress test scenario being considered. + year (int): The year for which the stress test is performed. + + """ + self.hazard_type = hazard_type + self.scenario = scenario + self.year = year + self.hazard_percentiles = HazardPercentilesStressTest() + + def impact(self): + """Get the impact data (stress test percentiles) for the given hazard type, scenario, and year. + + Return: + ------ + Union[ImpactDistrib, EmptyImpactDistrib]: The impact distribution data for the stress test scenario. + + """ + return self.hazard_percentiles.get_data( + hazard_type=self.hazard_type.__name__, + scenario=self.scenario, + year=self.year, + ) diff --git a/src/physrisk/risk_models/loss_model.py b/src/physrisk/risk_models/loss_model.py index 337d596b..4133dc42 100644 --- a/src/physrisk/risk_models/loss_model.py +++ b/src/physrisk/risk_models/loss_model.py @@ -3,18 +3,15 @@ import numpy as np -from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType +from ..kernel.impact_distrib import ImpactDistrib, ImpactType from ..kernel.assets import Asset -from ..kernel.calculation import ( - get_default_hazard_model, - get_default_vulnerability_models, -) +from ..kernel.calculation import get_default_hazard_model from ..kernel.financial_model import FinancialModelBase from ..kernel.hazard_model import HazardModel from ..kernel.impact import calculate_impacts from ..kernel.vulnerability_model import ( - DictBasedVulnerabilityModels, + DictBasedVulnerabilityModelsFactory, VulnerabilityModels, ) @@ -34,15 +31,16 @@ def __init__( self, hazard_model: Optional[HazardModel] = None, vulnerability_models: Optional[VulnerabilityModels] = None, + use_case_id: str = "DEFAULT", ): self.hazard_model = ( get_default_hazard_model() if hazard_model is None else hazard_model ) - self.vulnerability_models = ( - DictBasedVulnerabilityModels(get_default_vulnerability_models()) - if vulnerability_models is None - else vulnerability_models - ) + if vulnerability_models is None: + factory = DictBasedVulnerabilityModelsFactory(use_case_id) + self.vulnerability_models = factory.vulnerability_models() + else: + self.vulnerability_models = vulnerability_models """Calculates the financial impact on a list of assets.""" diff --git a/src/physrisk/risk_models/risk_models.py b/src/physrisk/risk_models/risk_models.py index edb1531d..45d645f4 100644 --- a/src/physrisk/risk_models/risk_models.py +++ b/src/physrisk/risk_models/risk_models.py @@ -9,14 +9,20 @@ ) from physrisk.kernel.hazards import ( ChronicHeat, + ChronicWind, CoastalInundation, + Fire, Hazard, + Landslide, RiverineInundation, + Subsidence, + WaterRisk, Wind, ) from physrisk.kernel.impact import AssetImpactResult from physrisk.kernel.impact_distrib import EmptyImpactDistrib, ImpactDistrib from physrisk.kernel.risk import Measure, RiskMeasureCalculator +from physrisk.risk_models.hazard_stress_test_percentiles import StressTestImpact class Threshold(int, Enum): @@ -184,6 +190,9 @@ def calc_measure( impact_res.impact, EmptyImpactDistrib ): return None + + assert isinstance(base_impact_res.impact, ImpactDistrib) + if hazard_type == ChronicHeat: return self.calc_measure_cooling( hazard_type, base_impact_res.impact, impact_res.impact @@ -259,3 +268,136 @@ def get_definition(self, hazard_type: type): def supported_hazards(self) -> Set[type]: return set([RiverineInundation, CoastalInundation, Wind, ChronicHeat]) + + +class ThermalPowerPlantsRiskMeasures(RiskMeasureCalculator): + """Toy model for calculating risk measures for real estate assets.""" + + # https://www.ecb.europa.eu/stats/ecb_statistics/sustainability-indicators/data/shared/files/technical_annex202311.fr.pdf + + def __init__(self): + """Toy model for calculating risk measures for real estate assets.""" + self.model_summary = {"Stress test risk measures for real assets."} + + definition_stress_test = self._definition_stress_test() + + self._definition_lookup = { + RiverineInundation: definition_stress_test, + CoastalInundation: definition_stress_test, + ChronicWind: definition_stress_test, + Fire: definition_stress_test, + WaterRisk: definition_stress_test, + Landslide: definition_stress_test, + Subsidence: definition_stress_test, + } + + def _definition_stress_test(self): + definition = ScoreBasedRiskMeasureDefinition( + hazard_types=[ + RiverineInundation.__name__, + CoastalInundation.__name__, + ChronicWind.__name__, + Fire.__name__, + WaterRisk.__name__, + Landslide.__name__, + Subsidence.__name__, + ], + values=self._definition_values(self._stress_test_description), + underlying_measures=[ + RiskMeasureDefinition( + measure_id="measures_0", + label="Exposure to hazard.", + description="Score", + ) + ], + ) + return definition + + def _definition_values(self, description: Callable[[Category], str]): + return [ + RiskScoreValue( + value=Category.HIGH, + label=("Exposure to hazard is high"), + description=description(Category.HIGH), + ), + RiskScoreValue( + value=Category.MEDIUM, + label="Exposure to hazard is medium", + description=description(Category.MEDIUM), + ), + RiskScoreValue( + value=Category.LOW, + label=("Exposure to hazard is low"), + description=description(Category.LOW), + ), + RiskScoreValue( + value=Category.NORISK, + label="No risk", + description=description(Category.NORISK), + ), + RiskScoreValue( + value=Category.NODATA, label="No data.", description="No data." + ), + ] + + def _stress_test_description(self, category: Category): + if category == Category.LOW: + description = "Exposure to hazard is LOW (score 1) " + elif category == Category.MEDIUM: + description = "Exposure to hazard is MEDIUM (score 2) " + elif category == Category.HIGH: + description = "Exposure to hazard is HIGH (score 3) " + elif category == Category.NORISK: + description = "No exposure to hazard (no risk) " + else: + description = "No Data" + return description + + def calc_measure( + self, + hazard_type: type, + base_impact: AssetImpactResult, + impact_res: AssetImpactResult, + ) -> Optional[Measure]: + """Calculate the stress test risk measure based on the type of hazard and impact results.""" + if isinstance(impact_res, EmptyImpactDistrib) or impact_res.hazard_data is None: + return None + + scen, year = impact_res.hazard_data[0].path.split("_")[-2:] # type: ignore + norisk, p50, p75, p90 = StressTestImpact(hazard_type, scen, year).impact() + + mean_intensity = impact_res.impact.impact_bins.mean() + + if mean_intensity >= p90: + score = Category.HIGH + elif mean_intensity >= p75: + score = Category.MEDIUM + elif mean_intensity >= p50: + score = Category.LOW + elif mean_intensity == norisk: + score = Category.NORISK + else: + score = Category.NODATA + return Measure( + score=score, + measure_0=mean_intensity, + definition=self.get_definition(hazard_type), + ) + + def get_definition(self, hazard_type: type): + """Get the risk measure definition for a given hazard type.""" + return self._definition_lookup.get(hazard_type, None) + + def supported_hazards(self) -> Set[type]: + """Return the set of supported hazard types for the thermal power plants risk measure calculator.""" + return set( + [ + RiverineInundation, + CoastalInundation, + ChronicWind, + Fire, + WaterRisk, + Landslide, + Subsidence, + ] + ) diff --git a/src/physrisk/vulnerability_models/real_estate_models.py b/src/physrisk/vulnerability_models/real_estate_models.py index d601f76c..948b93f7 100644 --- a/src/physrisk/vulnerability_models/real_estate_models.py +++ b/src/physrisk/vulnerability_models/real_estate_models.py @@ -1,3 +1,5 @@ +"""Define vulnerability models for assessing the impact of various hazards on real estate assets.""" + from collections import defaultdict from typing import Dict, List, Tuple @@ -12,16 +14,22 @@ ) from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType from physrisk.kernel.vulnerability_matrix_provider import VulnMatrixProvider -from physrisk.kernel.vulnerability_model import VulnerabilityModel -from ..kernel.hazards import ( + +from physrisk.kernel.hazards import ( ChronicHeat, CoastalInundation, PluvialInundation, RiverineInundation, Wind, + ChronicWind, + Fire, + WaterRisk, + Landslide, + Subsidence, ) -from ..kernel.vulnerability_model import ( +from physrisk.kernel.vulnerability_model import ( + VulnerabilityModel, DeterministicVulnerabilityModel, VulnerabilityModelBase, applies_to_events, @@ -31,6 +39,8 @@ class RealEstateInundationModel(VulnerabilityModel): + """Inundation vulnerability model for real estates assets.""" + _default_impact_bin_edges = np.array( [0, 0.01, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] ) @@ -44,16 +54,18 @@ def __init__( resource: str = _default_resource, impact_bin_edges=_default_impact_bin_edges, ): - """ - Inundation vulnerability model for real estates assets. Applies to both riverine and coastal inundation. + """Initialize the inundation vulnerability model for real estates assets. + + Applies to both riverine and coastal inundation. Args: - event_type: Event type. - model: optional identifier for hazard event model, passed to HazardModel. + ---- + hazard_type: hazard type. + indicator_id: identifier for hazard event model, passed to HazardModel. resource: embedded resource identifier used to infer vulnerability matrix. impact_bin_edges: specifies the impact (fractional damage/disruption bins). - """ + """ curve_set: VulnerabilityCurves = get_vulnerability_curves_from_resource( resource ) @@ -84,6 +96,7 @@ def __init__( ) def get_impact_curve(self, intensity_bin_centres: np.ndarray, asset: Asset): + """Calculate the impact curve based on flood intensity and asset characteristics.""" # we interpolate the mean and standard deviation and use this to construct distributions if isinstance(asset, RealEstateAsset): key = (asset.location, asset.type) @@ -113,6 +126,7 @@ def get_impact_curve(self, intensity_bin_centres: np.ndarray, asset: Asset): ) def closest_curve_of_type(self, curve: VulnerabilityCurve, asset_type: str): + """Find the closest matching vulnerability curve of the same type for interpolation.""" # we return the standard deviations of the damage curve most similar to the asset location candidate_set = list( cand @@ -124,6 +138,7 @@ def closest_curve_of_type(self, curve: VulnerabilityCurve, asset_type: str): return candidate_set[lowest] def sum_square_diff(self, curve1: VulnerabilityCurve, curve2: VulnerabilityCurve): + """Compute the sum of squared differences between two vulnerability curves.""" return np.sum( ( curve1.impact_mean @@ -135,6 +150,8 @@ def sum_square_diff(self, curve1: VulnerabilityCurve, curve2: VulnerabilityCurve @applies_to_events([CoastalInundation]) class RealEstateCoastalInundationModel(RealEstateInundationModel): + """Vulnerability model for assessing the impact of coastal inundation on real estate assets.""" + def __init__( self, *, @@ -142,6 +159,7 @@ def __init__( resource: str = RealEstateInundationModel._default_resource, impact_bin_edges=RealEstateInundationModel._default_impact_bin_edges, ): + """Initialize the coastal inundation vulnerability model.""" # by default include subsidence and 95% sea-level rise super().__init__( hazard_type=CoastalInundation, @@ -152,6 +170,8 @@ def __init__( class RealEstatePluvialInundationModel(RealEstateInundationModel): + """Pluvial inundation vulnerability model for real estate assets.""" + def __init__( self, *, @@ -159,6 +179,7 @@ def __init__( resource: str = RealEstateInundationModel._default_resource, impact_bin_edges=RealEstateInundationModel._default_impact_bin_edges, ): + """Initialize the pluvial inundation vulnerability model.""" # by default include subsidence and 95% sea-level rise super().__init__( hazard_type=PluvialInundation, @@ -170,6 +191,8 @@ def __init__( @applies_to_events([RiverineInundation]) class RealEstateRiverineInundationModel(RealEstateInundationModel): + """Riverine inundation vulnerability model for real estate assets.""" + def __init__( self, *, @@ -177,6 +200,7 @@ def __init__( resource: str = RealEstateInundationModel._default_resource, impact_bin_edges=RealEstateInundationModel._default_impact_bin_edges, ): + """Initialize the riverine inundation vulnerability model.""" super().__init__( hazard_type=RiverineInundation, indicator_id=indicator_id, @@ -186,8 +210,10 @@ def __init__( class GenericTropicalCycloneModel(DeterministicVulnerabilityModel): + """Simple generic tropical cyclone vulnerability model for real estate assets.""" + def __init__(self): - """A very simple generic tropical cyclone vulnerability model.""" + """Initialize a very simple generic tropical cyclone vulnerability model.""" v_half = 74.7 # m/s intensities = np.arange(0, 100, 10) impacts = self.wind_damage(intensities, v_half) @@ -200,20 +226,25 @@ def __init__(self): ) def wind_damage(self, v: np.ndarray, v_half: float): - """Calculates damage based on functional form of - Emanuel K. Global warming effects on US hurricane damage. Weather, Climate, and Society. 2011 Oct 1;3(4):261-8. + """Calculate damage based on functional form of Emanuel K. Global warming effects on US hurricane damage. + + Weather, Climate, and Society. 2011 Oct 1;3(4):261-8. Using a threshold speed of 25.7 m/s. A review of the origin of parameters is available in Eberenz S, Lüthi S, Bresch DN. Regional tropical cyclone impact functions for globally consistent risk assessments. Natural Hazards and Earth System Sciences. 2021 Jan 29;21(1):393-415. which also provides suggested region-specific variations. + Args: + ---- v (np.ndarray[float]): Wind speeds at which to calculate the fractional damage. v_half (float): The 'v_half' function parameter. Returns: + ------- np.ndarray[float]: Fractional damage. + """ v_thresh = 25.7 # m/s vn = np.where(v > v_thresh, v - v_thresh, 0) / (v_half - v_thresh) @@ -221,6 +252,8 @@ def wind_damage(self, v: np.ndarray, v_half: float): class CoolingModel(VulnerabilityModelBase): + """Simple degree-days-based vulnerability model for real estate assets.""" + _default_transfer_coeff = 200 # W/K _default_cooling_cop = 3 # W/K @@ -232,8 +265,9 @@ class CoolingModel(VulnerabilityModelBase): # degree days for home cooling/heating. def __init__(self, threshold_temp_c: float = 23): - """Simple degree-days-based model for calculating cooling requirements as annual kWh of - electricity equivalent. The main limitation of the approach is that solar radiation and + """Initialize the simple degree-days-based model for calculating cooling requirements as annual kWh of electricity equivalent. + + The main limitation of the approach is that solar radiation and humidity are not taken into account. Limitations of similar approaches and ways to address are default with, for example in: @@ -250,6 +284,7 @@ def __init__(self, threshold_temp_c: float = 23): self.threshold_temp_c = threshold_temp_c def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" return HazardDataRequest( self.hazard_type, asset.longitude, @@ -262,6 +297,7 @@ def get_data_requests(self, asset: Asset, *, scenario: str, year: int): def get_impact( self, asset: Asset, data_responses: List[HazardDataResponse] ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" (data,) = data_responses assert isinstance(data, HazardParameterDataResponse) # we interpolate the specific threshold from the different values @@ -278,3 +314,151 @@ def get_impact( return ImpactDistrib( ChronicHeat, [annual_electricity, annual_electricity], [1], [data.path] ) + + +class HighFireModel(VulnerabilityModelBase): + """High fire vulnerability model for real estate assets.""" + + def __init__(self): + """Initialize the high fire vulnerability model.""" + self.indicator_id = "fwi20" + self.hazard_type = Fire + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + return ImpactDistrib(Fire, [data.parameter, data.parameter], [1], [data.path]) + + +class WaterstressModel(VulnerabilityModelBase): + """Water stress vulnerability model for real estate assets.""" + + def __init__(self): + """Initialize the water stress vulnerability model.""" + self.indicator_id = "water_stress" + self.hazard_type = WaterRisk + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + return ImpactDistrib( + WaterRisk, [data.parameter, data.parameter], [1], [data.path] + ) + + +class LandslideModel(VulnerabilityModelBase): + """Landslide vulnerability model for real estate assets.""" + + def __init__(self): + """Initialize the landslide vulnerability model.""" + self.indicator_id = "susceptability" + self.hazard_type = Landslide + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + return ImpactDistrib( + Landslide, [data.parameter, data.parameter], [1], [data.path] + ) + + +class SubsidenceModel(VulnerabilityModelBase): + """Subsidence vulnerability model for real estate assets.""" + + def __init__(self): + """Initialize the subsidence vulnerability model.""" + self.indicator_id = "susceptability" + self.hazard_type = Subsidence + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + return ImpactDistrib( + Subsidence, [data.parameter, data.parameter], [1], [data.path] + ) + + +class ChronicWindModel(VulnerabilityModelBase): + """Chronic wind vulnerability model for real estate assets.""" + + def __init__(self): + """Initialize the chronic wind vulnerability model.""" + self.indicator_id = "wind" + self.hazard_type = ChronicWind + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + return ImpactDistrib( + ChronicWind, [data.parameter, data.parameter], [1], [data.path] + ) diff --git a/src/physrisk/vulnerability_models/thermal_power_generation_models.py b/src/physrisk/vulnerability_models/thermal_power_generation_models.py index eb1dba65..573d87b0 100644 --- a/src/physrisk/vulnerability_models/thermal_power_generation_models.py +++ b/src/physrisk/vulnerability_models/thermal_power_generation_models.py @@ -1,9 +1,12 @@ +"""Define vulnerability models for assessing the impact of various hazards on thermal power generation assets.""" + from collections import defaultdict from typing import Iterable, List, Tuple, Union, cast import numpy as np from scipy.stats import multivariate_normal, norm +from physrisk.data.hazard_data_provider import HazardDataHint from physrisk.api.v1.common import VulnerabilityCurve, VulnerabilityCurves from physrisk.kernel.assets import Asset, ThermalPowerGeneratingAsset, TurbineKind from physrisk.kernel.impact_distrib import EmptyImpactDistrib, ImpactDistrib, ImpactType @@ -12,15 +15,15 @@ VulnerabilityModelBase, ) -from ..kernel.curve import ExceedanceCurve -from ..kernel.hazard_event_distrib import HazardEventDistrib -from ..kernel.hazard_model import ( +from physrisk.kernel.curve import ExceedanceCurve +from physrisk.kernel.hazard_event_distrib import HazardEventDistrib +from physrisk.kernel.hazard_model import ( HazardDataRequest, HazardDataResponse, HazardEventDataResponse, HazardParameterDataResponse, ) -from ..kernel.hazards import ( +from physrisk.kernel.hazards import ( AirTemperature, ChronicHeat, CoastalInundation, @@ -28,9 +31,14 @@ RiverineInundation, WaterRisk, WaterTemperature, + ChronicWind, + Fire, + Landslide, + Subsidence, + Wind, ) -from ..kernel.vulnerability_distrib import VulnerabilityDistrib -from ..kernel.vulnerability_model import ( +from physrisk.kernel.vulnerability_distrib import VulnerabilityDistrib +from physrisk.kernel.vulnerability_model import ( applies_to_assets, applies_to_events, get_vulnerability_curves_from_resource, @@ -38,6 +46,12 @@ class ThermalPowerGenerationInundationModel(DeterministicVulnerabilityModel): + """Inundation vulnerability model for thermal power generation. + + This model applies to both riverine and coastal inundation and uses vulnerability curves + to assess the impact on thermal power generating assets. + """ + # Number of disrupted days per year _default_resource = "WRI thermal power plant physical climate vulnerability factors" @@ -52,17 +66,18 @@ def __init__( resource: str = _default_resource, buffer: int = _default_buffer, ): - """ - Inundation vulnerability model for thermal power generation. + """Initialize the inundation vulnerability model for thermal power generation. + Applies to both riverine and coastal inundation. Args: - hazard_type (type): _description_ - indicator_id (str): ID of the hazard indicator to which this applies. - resource (str): embedded resource identifier used to infer vulnerability table. - buffer (int): delimitation of the area for the hazard data expressed in metres (within [0,1000]). - """ + ---- + hazard_type (type): _description_ + indicator_id (str): ID of the hazard indicator to which this applies. + resource (str): embedded resource identifier used to infer vulnerability table. + buffer (int): delimitation of the area for the hazard data expressed in metres (within [0,1000]). + """ curve_set: VulnerabilityCurves = get_vulnerability_curves_from_resource( resource ) @@ -101,8 +116,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: - """Provide the list of hazard event data requests required in order to calculate - the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" request_scenario = HazardDataRequest( self.hazard_type, asset.longitude, @@ -126,6 +140,7 @@ def get_data_requests( def get_distributions( self, asset: Asset, event_data_responses: Iterable[HazardDataResponse] ) -> Tuple[VulnerabilityDistrib, HazardEventDistrib]: + """Calculate the vulnerability and hazard event distributions for the asset.""" assert isinstance(asset, ThermalPowerGeneratingAsset) (response_scenario, response_baseline) = event_data_responses @@ -199,15 +214,27 @@ def get_distributions( class ThermalPowerGenerationCoastalInundationModel( ThermalPowerGenerationInundationModel ): + """Coastal inundation vulnerability model for thermal power generation.""" + def __init__( self, *, indicator_id: str = "flood_depth", resource: str = ThermalPowerGenerationInundationModel._default_resource, ): + """Initialize the coastal inundation vulnerability model for thermal power generation. + + Args: + ---- + indicator_id (str): ID of the hazard indicator to which this applies. + resource (str): embedded resource identifier used to infer vulnerability table. + + """ # by default include subsidence and 95% sea-level rise super().__init__( - hazard_type=CoastalInundation, indicator_id=indicator_id, resource=resource + hazard_type=CoastalInundation, + indicator_id=indicator_id, + resource=resource, ) @@ -216,21 +243,35 @@ def __init__( class ThermalPowerGenerationRiverineInundationModel( ThermalPowerGenerationInundationModel ): + """Riverine inundation vulnerability model for thermal power generation.""" + def __init__( self, *, indicator_id: str = "flood_depth", resource: str = ThermalPowerGenerationInundationModel._default_resource, ): + """Initialize the riverine inundation vulnerability model for thermal power generation. + + Args: + ---- + indicator_id (str): ID of the hazard indicator to which this applies. + resource (str): embedded resource identifier used to infer vulnerability table. + + """ # by default request HazardModel to use "MIROC-ESM-CHEM" GCM super().__init__( - hazard_type=RiverineInundation, indicator_id=indicator_id, resource=resource + hazard_type=RiverineInundation, + indicator_id=indicator_id, + resource=resource, ) @applies_to_events([Drought]) @applies_to_assets([ThermalPowerGeneratingAsset]) class ThermalPowerGenerationDroughtModel(VulnerabilityModelBase): + """Drought vulnerability model for thermal power generation.""" + # Number of disrupted days per year _default_resource = "WRI thermal power plant physical climate vulnerability factors" _impact_based_on_a_single_point = False @@ -241,14 +282,14 @@ def __init__( resource: str = _default_resource, impact_based_on_a_single_point: bool = _impact_based_on_a_single_point, ): - """ - Drought vulnerability model for thermal power generation. + """Initialize the drought vulnerability model for thermal power generation. Args: - resource (str): embedded resource identifier used to infer vulnerability table. - impact_based_on_a_single_point (str): calculation based on a single point instead of a curve. - """ + ---- + resource (str): embedded resource identifier used to infer vulnerability table. + impact_based_on_a_single_point (str): calculation based on a single point instead of a curve. + """ hazard_type = Drought curve_set: VulnerabilityCurves = get_vulnerability_curves_from_resource( resource @@ -288,6 +329,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" return HazardDataRequest( self.hazard_type, asset.longitude, @@ -300,6 +342,7 @@ def get_data_requests( def get_impact( self, asset: Asset, data_responses: List[HazardDataResponse] ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" assert isinstance(asset, ThermalPowerGeneratingAsset) hazard_paths = [ @@ -390,6 +433,8 @@ def get_impact( @applies_to_events([AirTemperature]) @applies_to_assets([ThermalPowerGeneratingAsset]) class ThermalPowerGenerationAirTemperatureModel(VulnerabilityModelBase): + """Air temperature vulnerability model for thermal power generation.""" + # Number of disrupted days per year _default_resource = "WRI thermal power plant physical climate vulnerability factors" _default_temperatures = [25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0] @@ -400,12 +445,13 @@ def __init__( resource: str = _default_resource, temperatures: List[float] = _default_temperatures, ): - """ - Air temperature vulnerability model for thermal power generation. + """Initialize the air temperature vulnerability model for thermal power generation. Args: + ---- resource (str): embedded resource identifier used to infer vulnerability table. temperatures (list[Float]): thresholds of the "days with average temperature above". + """ curve_set: VulnerabilityCurves = get_vulnerability_curves_from_resource( resource @@ -444,6 +490,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" data_request = [] for temperature in self.temperatures: data_request.append( @@ -472,6 +519,7 @@ def get_data_requests( def get_impact( self, asset: Asset, data_responses: List[HazardDataResponse] ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" assert isinstance(asset, ThermalPowerGeneratingAsset) assert 2 * len(self.temperatures) == len(data_responses) @@ -555,6 +603,8 @@ def get_impact( @applies_to_events([WaterTemperature]) @applies_to_assets([ThermalPowerGeneratingAsset]) class ThermalPowerGenerationWaterTemperatureModel(VulnerabilityModelBase): + """Water temperature vulnerability model for thermal power generation.""" + # Number of disrupted days per year _default_resource = "WRI thermal power plant physical climate vulnerability factors" _default_correlation = 0.5 @@ -565,13 +615,14 @@ def __init__( resource: str = _default_resource, correlation: float = _default_correlation, ): - """ - Water temperature vulnerability model for thermal power generation. + """Initialize the water temperature vulnerability model for thermal power generation. Args: + ---- resource (str): embedded resource identifier used to infer vulnerability table. correlation (float): correlation specifying the Gaussian copula which joins the marginal distributions of water temperature and WBGT. + """ curve_set: VulnerabilityCurves = get_vulnerability_curves_from_resource( resource @@ -618,6 +669,7 @@ def __init__( def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" data_request = [] data_request.append( HazardDataRequest( @@ -664,6 +716,7 @@ def get_data_requests( def get_impact( self, asset: Asset, data_responses: List[HazardDataResponse] ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" assert isinstance(asset, ThermalPowerGeneratingAsset) assert len(data_responses) == 4 @@ -870,15 +923,18 @@ def get_impact( @applies_to_events([WaterRisk]) @applies_to_assets([ThermalPowerGeneratingAsset]) class ThermalPowerGenerationWaterStressModel(VulnerabilityModelBase): + """Water stress vulnerability model for thermal power generation.""" + # Number of disrupted days per year _default_resource = "WRI thermal power plant physical climate vulnerability factors" def __init__(self, *, resource: str = _default_resource): - """ - Water stress vulnerability model for thermal power generation. + """Initialize the water stress vulnerability model for thermal power generation. Args: - resource (str): embedded resource identifier used to infer vulnerability table. + ---- + resource (str): embedded resource identifier used to infer vulnerability table. + """ curve_set: VulnerabilityCurves = get_vulnerability_curves_from_resource( resource @@ -911,6 +967,7 @@ def __init__(self, *, resource: str = _default_resource): def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" data_request = [] data_request.append( HazardDataRequest( @@ -947,6 +1004,7 @@ def get_data_requests( def get_impact( self, asset: Asset, data_responses: List[HazardDataResponse] ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" assert isinstance(asset, ThermalPowerGeneratingAsset) assert len(data_responses) == 3 @@ -1016,3 +1074,193 @@ def get_impact( self.impact_type, ) return impact_distrib + + +@applies_to_events([Wind]) +@applies_to_assets([ThermalPowerGeneratingAsset]) +class ThermalPowerGenerationSevereConvectiveWindstormModel(VulnerabilityModelBase): + """Severe conective windstorm vulnerability model for thermal power generation.""" + + def __init__(self): + """Initialize the severe conective windstorm vulnerability model.""" + self.indicator_id = "wind25" + self.hazard_type = ChronicWind + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + hazard_paths = [ + cast(HazardParameterDataResponse, r).path for r in data_responses + ] + return ImpactDistrib( + ThermalPowerGenerationSevereConvectiveWindstormModel, + [data.parameter, data.parameter], + [1], + hazard_paths, + ) + + +@applies_to_events([Fire]) +@applies_to_assets([ThermalPowerGeneratingAsset]) +class ThermalPowerGenerationHighFireModel(VulnerabilityModelBase): + """High fire vulnerability model for thermal power generation.""" + + def __init__(self): + """Initialize the high fire vulnerability model for thermal power generation.""" + self.indicator_id = "fwi20" + self.hazard_type = Fire + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + hazard_paths = [ + cast(HazardParameterDataResponse, r).path for r in data_responses + ] + return ImpactDistrib( + ThermalPowerGenerationHighFireModel, + [data.parameter, data.parameter], + [1], + hazard_paths, + ) + + +@applies_to_events([WaterRisk]) +@applies_to_assets([ThermalPowerGeneratingAsset]) +class ThermalPowerGenerationAqueductWaterRiskModel(VulnerabilityModelBase): + """Aqueduct Water risk vulnerability model for thermal power generation.""" + + def __init__(self): + """Initialize the aqueduct Water risk vulnerability model for thermal power generation.""" + self.indicator_id = "water_stress" + self.hazard_type = WaterRisk + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + hazard_paths = [ + cast(HazardParameterDataResponse, r).path for r in data_responses + ] + return ImpactDistrib( + ThermalPowerGenerationAqueductWaterRiskModel, + [data.parameter, data.parameter], + [1], + hazard_paths, + ) + + +@applies_to_events([Landslide]) +@applies_to_assets([ThermalPowerGeneratingAsset]) +class ThermalPowerGenerationLandslideModel(VulnerabilityModelBase): + """Landslide vulnerability model for thermal power generation.""" + + def __init__(self): + """Initialize the landslide vulnerability model for thermal power generation.""" + self.indicator_id = "susceptability" + self.hazard_type = Landslide + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + hazard_paths = [ + cast(HazardParameterDataResponse, r).path for r in data_responses + ] + return ImpactDistrib( + ThermalPowerGenerationLandslideModel, + [data.parameter, data.parameter], + [1], + hazard_paths, + ) + + +@applies_to_events([Subsidence]) +@applies_to_assets([ThermalPowerGeneratingAsset]) +class ThermalPowerGenerationSubsidenceModel(VulnerabilityModelBase): + """Subsidence vulnerability model for thermal power generation.""" + + def __init__(self): + """Initialize the subsidence vulnerability model for thermal power generation.""" + self.indicator_id = "susceptability" + self.hazard_type = Subsidence + + def get_data_requests(self, asset: Asset, *, scenario: str, year: int): + """Provide the list of hazard event data requests required in order to calculate the VulnerabilityDistrib and HazardEventDistrib for the asset.""" + return HazardDataRequest( + self.hazard_type, + asset.longitude, + asset.latitude, + scenario=scenario, + year=year, + indicator_id=self.indicator_id, + ) + + def get_impact( + self, asset: Asset, data_responses: List[HazardDataResponse] + ) -> ImpactDistrib: + """Calculate the impact distribution based on hazard data responses.""" + (data,) = data_responses + assert isinstance(data, HazardParameterDataResponse) + hazard_paths = [ + cast(HazardParameterDataResponse, r).path for r in data_responses + ] + return ImpactDistrib( + ThermalPowerGenerationSubsidenceModel, + [data.parameter, data.parameter], + [1], + hazard_paths, + ) diff --git a/tests/api/container_test.py b/tests/api/container_test.py index c0875190..de4fa34d 100644 --- a/tests/api/container_test.py +++ b/tests/api/container_test.py @@ -2,7 +2,6 @@ from dependency_injector import containers, providers from physrisk.data.inventory_reader import InventoryReader - from ..data.hazard_model_store_test import mock_hazard_model_store_heat diff --git a/tests/api/data_requests_test.py b/tests/api/data_requests_test.py index a7783d98..043ecf29 100644 --- a/tests/api/data_requests_test.py +++ b/tests/api/data_requests_test.py @@ -1,19 +1,18 @@ -import unittest +import pytest import numpy as np -import numpy.testing -from physrisk import requests -from physrisk.container import Container +from physrisk.hazard_models import core_hazards from physrisk.data.hazard_data_provider import HazardDataHint from physrisk.data.inventory import EmbeddedInventory 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.hazards import ChronicHeat, RiverineInundation +from physrisk.container import Container +from physrisk import requests from ..api.container_test import TestContainer -from ..base_test import TestWithCredentials from ..data.hazard_model_store_test import ( TestData, get_mock_hazard_model_store_single_curve, @@ -21,215 +20,183 @@ ) -class TestDataRequests(TestWithCredentials): - def setUp(self): - super().setUp() - - def tearDown(self): - super().tearDown() - - def test_hazard_data_availability(self): - # test that validation passes: - container = Container() - container.override(TestContainer()) - requester = container.requester() - _ = requester.get(request_id="get_hazard_data_availability", request_dict={}) - - @unittest.skip("requires mocking.") - def test_hazard_data_description(self): - # test that validation passes: - container = Container() - requester = container.requester - _ = requester.get( - request_id="get_hazard_data_description", - request_dict={"paths": ["test_path.md"]}, - ) - - def test_generic_source_path(self): - inventory = EmbeddedInventory() - source_paths = get_default_source_paths(inventory) - result_heat = source_paths[ChronicHeat]( - indicator_id="mean_degree_days/above/32c", scenario="rcp8p5", year=2050 - ) - result_flood = source_paths[RiverineInundation]( - indicator_id="flood_depth", scenario="rcp8p5", year=2050 - ) - result_flood_hist = source_paths[RiverineInundation]( - indicator_id="flood_depth", scenario="historical", year=2080 - ) - result_heat_hint = source_paths[ChronicHeat]( - indicator_id="mean_degree_days/above/32c", - scenario="rcp8p5", - year=2050, - hint=HazardDataHint( - path="chronic_heat/osc/v2/mean_degree_days_v2_above_32c_CMCC-ESM2_{scenario}_{year}" - ), - ) - - assert ( - result_heat - == "chronic_heat/osc/v2/mean_degree_days_v2_above_32c_ACCESS-CM2_rcp8p5_2050" - ) - assert result_flood == "inundation/wri/v2/inunriver_rcp8p5_MIROC-ESM-CHEM_2050" - assert ( - result_flood_hist - == "inundation/wri/v2/inunriver_rcp4p5_MIROC-ESM-CHEM_2030" - ) - assert ( - result_heat_hint - == "chronic_heat/osc/v2/mean_degree_days_v2_above_32c_CMCC-ESM2_rcp8p5_2050" - ) - - def test_zarr_reading(self): - request_dict = { - "items": [ - { - "request_item_id": "test_inundation", - "event_type": "RiverineInundation", - "longitudes": TestData.longitudes[ - 0:3 - ], # coords['longitudes'][0:100], - "latitudes": TestData.latitudes[0:3], # coords['latitudes'][0:100], - "year": 2080, - "scenario": "rcp8p5", - "indicator_id": "flood_depth", - "indicator_model_gcm": "MIROC-ESM-CHEM", - } - ], - } - # validate request - request = requests.HazardDataRequest(**request_dict) # type: ignore - - store = get_mock_hazard_model_store_single_curve() - - result = requests._get_hazard_data( - request, - ZarrHazardModel( - source_paths=get_default_source_paths(EmbeddedInventory()), - reader=ZarrReader(store=store), - ), - ) - - numpy.testing.assert_array_almost_equal_nulp( - result.items[0].intensity_curve_set[0].intensities, np.zeros((9)) - ) - numpy.testing.assert_array_almost_equal_nulp( - result.items[0].intensity_curve_set[1].intensities, - np.linspace(0.1, 1.0, 9, dtype="f4"), - ) - numpy.testing.assert_array_almost_equal_nulp( - result.items[0].intensity_curve_set[2].intensities, np.zeros((9)) - ) - - def test_zarr_reading_chronic(self): - request_dict = { - "group_ids": ["osc"], - "items": [ - { - "request_item_id": "test_inundation", - "event_type": "ChronicHeat", - "longitudes": TestData.longitudes[ - 0:3 - ], # coords['longitudes'][0:100], - "latitudes": TestData.latitudes[0:3], # coords['latitudes'][0:100], - "year": 2050, - "scenario": "ssp585", - "indicator_id": "mean_degree_days/above/32c", - } - ], - } - # validate request - request = requests.HazardDataRequest(**request_dict) # type: ignore - - store = mock_hazard_model_store_heat(TestData.longitudes, TestData.latitudes) - - source_paths = get_default_source_paths(EmbeddedInventory()) - result = requests._get_hazard_data( - request, - ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), - ) - numpy.testing.assert_array_almost_equal_nulp( - result.items[0].intensity_curve_set[0].intensities[0], 600.0 - ) - - # request_with_hint = request.copy() - # request_with_hint.items[0].path = "chronic_heat/osc/v2/mean_degree_days_v2_above_32c_CMCC-ESM2_rcp8p5_2050" - # result = requests._get_hazard_data( - # request_with_hint, ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)) - # ) - - @unittest.skip("requires OSC environment variables set") - def test_zarr_reading_live(self): - # needs valid OSC_S3_BUCKET, OSC_S3_ACCESS_KEY, OSC_S3_SECRET_KEY - container = Container() - requester = container.requester() - - import json - from zipfile import ZipFile - - with ZipFile("src/test/api/test_lat_lons.json.zip") as z: - with z.open("test_lat_lons.json") as f: - data = json.loads(f.read()) - - request1 = { - "items": [ - { - "request_item_id": "test_inundation", - "event_type": "ChronicHeat", - "longitudes": TestData.longitudes, - "latitudes": TestData.latitudes, - "year": 2030, - "scenario": "ssp585", - "indicator_id": "mean_work_loss/high", - } - ], - } - - request1 = { - "items": [ - { - "request_item_id": "test_inundation", - "event_type": "ChronicHeat", - "longitudes": data["longitudes"], - "latitudes": data["latitudes"], - "year": 2030, - "scenario": "ssp585", - "indicator_id": "mean_work_loss/high", - } - ], - } - - response_floor = requester.get( - request_id="get_hazard_data", request_dict=request1 - ) - request1["interpolation"] = "linear" # type: ignore - response_linear = requester.get( - request_id="get_hazard_data", request_dict=request1 - ) - print(response_linear) - - floor = json.loads(response_floor)["items"][0]["intensity_curve_set"][5][ - "intensities" - ] - linear = json.loads(response_linear)["items"][0]["intensity_curve_set"][5][ - "intensities" - ] - - print(floor) - print(linear) - - request2 = { - "items": [ - { - "request_item_id": "test_inundation", - "event_type": "CoastalInundation", - "longitudes": TestData.coastal_longitudes, - "latitudes": TestData.coastal_latitudes, - "year": 2080, - "scenario": "rcp8p5", - "model": "wtsub/95", - } - ], - } - response = requester.get(request_type="get_hazard_data", request_dict=request2) - print(response) +def test_hazard_data_availability(): + # test that validation passes: + container = Container() + container.override(TestContainer()) + requester = container.requester() + _ = requester.get(request_id="get_hazard_data_availability", request_dict={}) + + +@pytest.mark.skip(reason="requires mocking.") +def test_hazard_data_description(): + # test that validation passes: + container = Container() + requester = container.requester + _ = requester.get( + request_id="get_hazard_data_description", + request_dict={"paths": ["test_path.md"]}, + ) + + +def test_generic_source_path(): + inventory = EmbeddedInventory() + source_paths = core_hazards.get_default_source_paths(inventory) + result_heat = source_paths[ChronicHeat]( + indicator_id="mean_degree_days/above/32c", scenario="rcp8p5", year=2050 + ) + result_flood = source_paths[RiverineInundation]( + indicator_id="flood_depth", scenario="rcp8p5", year=2050 + ) + result_flood_hist = source_paths[RiverineInundation]( + indicator_id="flood_depth", scenario="historical", year=2080 + ) + result_heat_hint = source_paths[ChronicHeat]( + indicator_id="mean_degree_days/above/32c", + scenario="rcp8p5", + year=2050, + hint=HazardDataHint( + path="chronic_heat/osc/v2/mean_degree_days_v2_above_32c_CMCC-ESM2_{scenario}_{year}" + ), + ) + + assert ( + result_heat + == "chronic_heat/osc/v2/mean_degree_days_v2_above_32c_ACCESS-CM2_rcp8p5_2050" + ) + assert result_flood == "inundation/wri/v2/inunriver_rcp8p5_MIROC-ESM-CHEM_2050" + assert result_flood_hist == "inundation/wri/v2/inunriver_rcp4p5_MIROC-ESM-CHEM_2030" + assert ( + result_heat_hint + == "chronic_heat/osc/v2/mean_degree_days_v2_above_32c_CMCC-ESM2_rcp8p5_2050" + ) + + +def test_zarr_reading(): + request_dict = { + "items": [ + { + "request_item_id": "test_inundation", + "event_type": "RiverineInundation", + "longitudes": TestData.longitudes[0:3], # coords['longitudes'][0:100], + "latitudes": TestData.latitudes[0:3], # coords['latitudes'][0:100], + "year": 2080, + "scenario": "rcp8p5", + "indicator_id": "flood_depth", + "indicator_model_gcm": "MIROC-ESM-CHEM", + } + ], + } + # validate request + request = requests.HazardDataRequest(**request_dict) # type: ignore + + store = get_mock_hazard_model_store_single_curve() + + result = requests._get_hazard_data( + request, + ZarrHazardModel( + source_paths=get_default_source_paths(EmbeddedInventory()), + reader=ZarrReader(store=store), + ), + ) + + np.testing.assert_array_almost_equal_nulp( + result.items[0].intensity_curve_set[0].intensities, np.zeros((9)) + ) + np.testing.assert_array_almost_equal_nulp( + result.items[0].intensity_curve_set[1].intensities, + np.linspace(0.1, 1.0, 9, dtype="f4"), + ) + np.testing.assert_array_almost_equal_nulp( + result.items[0].intensity_curve_set[2].intensities, np.zeros((9)) + ) + + +def test_zarr_reading_chronic(): + request_dict = { + "group_ids": ["osc"], + "items": [ + { + "request_item_id": "test_inundation", + "event_type": "ChronicHeat", + "longitudes": TestData.longitudes[0:3], # coords['longitudes'][0:100], + "latitudes": TestData.latitudes[0:3], # coords['latitudes'][0:100], + "year": 2050, + "scenario": "ssp585", + "indicator_id": "mean_degree_days/above/32c", + } + ], + } + # validate request + request = requests.HazardDataRequest(**request_dict) # type: ignore + + store = mock_hazard_model_store_heat(TestData.longitudes, TestData.latitudes) + + source_paths = get_default_source_paths(EmbeddedInventory()) + result = requests._get_hazard_data( + request, + ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), + ) + np.testing.assert_array_almost_equal_nulp( + result.items[0].intensity_curve_set[0].intensities[0], 600.0 + ) + + +@pytest.mark.skip(reason="Requires credentials.") +def test_zarr_reading_live(load_credentials): + # needs valid OSC_S3_BUCKET, OSC_S3_ACCESS_KEY, OSC_S3_SECRET_KEY + container = Container() + requester = container.requester() + + import json + from zipfile import ZipFile + + with ZipFile("./tests/api/test_lat_lons.json.zip") as z: + with z.open("test_lat_lons.json") as f: + data = json.loads(f.read()) + + request1 = { + "items": [ + { + "request_item_id": "test_inundation", + "event_type": "ChronicHeat", + "longitudes": data["longitudes"], + "latitudes": data["latitudes"], + "year": 2030, + "scenario": "ssp585", + "indicator_id": "mean_work_loss/high", + } + ], + } + + response_floor = requester.get(request_id="get_hazard_data", request_dict=request1) + request1["interpolation"] = "linear" # type: ignore + response_linear = requester.get(request_id="get_hazard_data", request_dict=request1) + print(response_linear) + + floor = json.loads(response_floor)["items"][0]["intensity_curve_set"][5][ + "intensities" + ] + linear = json.loads(response_linear)["items"][0]["intensity_curve_set"][5][ + "intensities" + ] + + print(floor) + print(linear) + + request2 = { + "items": [ + { + "request_item_id": "test_inundation", + "event_type": "CoastalInundation", + "longitudes": TestData.coastal_longitudes, + "latitudes": TestData.coastal_latitudes, + "year": 2080, + "scenario": "rcp8p5", + "indicator_id": "flood_depth", + "model_id": "wtsub/95", + } + ], + } + response = requester.get(request_id="get_hazard_data", request_dict=request2) + print(response) diff --git a/tests/api/housing_kaggle_spain.json.bz2 b/tests/api/housing_kaggle_spain.json.bz2 new file mode 100644 index 00000000..65d3cf75 Binary files /dev/null and b/tests/api/housing_kaggle_spain.json.bz2 differ diff --git a/tests/api/impact_requests_test.py b/tests/api/impact_requests_test.py index 16576d10..c3e2b1ed 100644 --- a/tests/api/impact_requests_test.py +++ b/tests/api/impact_requests_test.py @@ -1,13 +1,11 @@ import json -import unittest - import numpy as np +import os from pydantic import TypeAdapter +import pytest -from physrisk import requests -from physrisk.api.v1.common import Asset, Assets +from physrisk.api.v1.common import Assets, Asset from physrisk.api.v1.impact_req_resp import RiskMeasures, RiskMeasuresHelper -from physrisk.container import Container from physrisk.data.inventory import EmbeddedInventory from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.zarr_reader import ZarrReader @@ -17,6 +15,8 @@ RealEstateAsset, ThermalPowerGeneratingAsset, ) + +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels from physrisk.vulnerability_models.power_generating_asset_models import InundationModel from physrisk.vulnerability_models.real_estate_models import ( @@ -30,8 +30,9 @@ ThermalPowerGenerationWaterStressModel, ThermalPowerGenerationWaterTemperatureModel, ) +from physrisk.container import Container +from physrisk import requests -from ..base_test import TestWithCredentials from ..data.hazard_model_store_test import ( TestData, add_curves, @@ -40,814 +41,803 @@ zarr_memory_store, ) -# from physrisk.api.v1.impact_req_resp import AssetImpactResponse -# from physrisk.data.static.world import get_countries_and_continents - - -class TestImpactRequests(TestWithCredentials): - def test_asset_list_json(self): - assets = { - "items": [ - { - "asset_class": "RealEstateAsset", - "type": "Buildings/Industrial", - "location": "Asia", - "longitude": 69.4787, - "latitude": 34.556, - }, - { - "asset_class": "PowerGeneratingAsset", - "type": "Nuclear", - "location": "Asia", - "longitude": -70.9157, - "latitude": -39.2145, - }, - ], - } - assets_obj = Assets(**assets) - self.assertIsNotNone(assets_obj) - - def test_extra_fields(self): - assets = { - "items": [ - { - "asset_class": "RealEstateAsset", - "type": "Buildings/Industrial", - "location": "Asia", - "longitude": 69.4787, - "latitude": 34.556, - "extra_field": 2.0, - "capacity": 1000.0, - } - ], - } - assets = requests.create_assets(Assets(**assets)) - # in the case of RealEstateAsset, extra fields are allowed, including those not in the Pydantic Asset object - assert assets[0].capacity == 1000.0 - assert assets[0].extra_field == 2.0 - - def test_impact_request(self): - """Runs short asset-level impact request.""" - - assets = { - "items": [ - { - "asset_class": "RealEstateAsset", - "type": "Buildings/Industrial", - "location": "Asia", - "longitude": TestData.longitudes[0], - "latitude": TestData.latitudes[0], - }, - { - "asset_class": "PowerGeneratingAsset", - "type": "Nuclear", - "location": "Asia", - "longitude": TestData.longitudes[1], - "latitude": TestData.latitudes[1], - }, - ], - } - - request_dict = { - "assets": assets, - "include_asset_level": True, - "include_measures": False, - "include_calc_details": True, - "years": [2080], - "scenarios": ["rcp8p5"], - } - - request = requests.AssetImpactRequest(**request_dict) # type: ignore - - curve = np.array( - [0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163] - ) - store = mock_hazard_model_store_inundation( - TestData.longitudes, TestData.latitudes, curve - ) - source_paths = get_default_source_paths(EmbeddedInventory()) - vulnerability_models = DictBasedVulnerabilityModels( +def test_asset_list_json(): + assets = { + "items": [ { - PowerGeneratingAsset: [InundationModel()], - RealEstateAsset: [ - RealEstateCoastalInundationModel(), - RealEstateRiverineInundationModel(), - ], + "asset_class": "RealEstateAsset", + "type": "Buildings/Industrial", + "location": "Asia", + "longitude": 69.4787, + "latitude": 34.556, + }, + { + "asset_class": "PowerGeneratingAsset", + "type": "Nuclear", + "location": "Asia", + "longitude": -70.9157, + "latitude": -39.2145, + }, + ], + } + assets_obj = Assets(**assets) + assert assets_obj is not None + + +def test_extra_fields(): + assets = { + "items": [ + { + "asset_class": "RealEstateAsset", + "type": "Buildings/Industrial", + "location": "Asia", + "longitude": 69.4787, + "latitude": 34.556, + "extra_field": 2.0, + "capacity": 1000.0, } - ) - - response = requests._get_asset_impacts( - request, - ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), - vulnerability_models=vulnerability_models, - ) - - self.assertEqual( - response.asset_impacts[0].impacts[0].hazard_type, "CoastalInundation" - ) - - def test_risk_model_impact_request(self): - """Tests the risk model functionality of the impact request.""" - - assets = { - "items": [ - { - "asset_class": "RealEstateAsset", - "type": "Buildings/Industrial", - "location": "Asia", - "longitude": TestData.longitudes[0], - "latitude": TestData.latitudes[0], - }, - { - "asset_class": "PowerGeneratingAsset", - "type": "Nuclear", - "location": "Asia", - "longitude": TestData.longitudes[1], - "latitude": TestData.latitudes[1], - }, + ], + } + assets = requests.create_assets(Assets(**assets)) + # in the case of RealEstateAsset, extra fields are allowed, including those not in the Pydantic Asset object + assert assets[0].capacity == 1000.0 + assert assets[0].extra_field == 2.0 + + +def test_impact_request(): + """Runs short asset-level impact request.""" + assets = { + "items": [ + { + "asset_class": "RealEstateAsset", + "type": "Buildings/Industrial", + "location": "Asia", + "longitude": TestData.longitudes[0], + "latitude": TestData.latitudes[0], + }, + { + "asset_class": "PowerGeneratingAsset", + "type": "Nuclear", + "location": "Asia", + "longitude": TestData.longitudes[1], + "latitude": TestData.latitudes[1], + }, + ], + } + + request_dict = { + "assets": assets, + "include_asset_level": True, + "include_measures": False, + "include_calc_details": True, + "years": [2080], + "scenarios": ["rcp8p5"], + } + + request = requests.AssetImpactRequest(**request_dict) # type: ignore + + curve = np.array([0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163]) + store = mock_hazard_model_store_inundation( + TestData.longitudes, TestData.latitudes, curve + ) + + source_paths = get_default_source_paths(EmbeddedInventory()) + vulnerability_models = DictBasedVulnerabilityModels( + { + PowerGeneratingAsset: [InundationModel()], + RealEstateAsset: [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), ], } + ) - request_dict = { - "assets": assets, - "include_asset_level": True, - "include_measures": False, - "include_calc_details": True, - "years": [2080], - "scenarios": ["rcp8p5"], - } - - request = requests.AssetImpactRequest(**request_dict) # type: ignore - - curve = np.array( - [0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163] - ) - store = mock_hazard_model_store_inundation( - TestData.longitudes, TestData.latitudes, curve - ) - - source_paths = get_default_source_paths(EmbeddedInventory()) - vulnerability_models = DictBasedVulnerabilityModels( - { - PowerGeneratingAsset: [InundationModel()], - RealEstateAsset: [ - RealEstateCoastalInundationModel(), - RealEstateRiverineInundationModel(), - ], - } - ) - - response = requests._get_asset_impacts( - request, - ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), - vulnerability_models=vulnerability_models, - ) + response = requests._get_asset_impacts( + request, + ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), + vulnerability_models=vulnerability_models, + ) - self.assertEqual( - response.asset_impacts[0].impacts[0].hazard_type, "CoastalInundation" - ) + assert response.asset_impacts[0].impacts[0].hazard_type == "CoastalInundation" - def test_thermal_power_generation(self): - latitudes = np.array([32.6017]) - longitudes = np.array([-87.7811]) - - assets = [ - ThermalPowerGeneratingAsset( - latitude=latitudes[0], - longitude=longitudes[0], - location="North America", - capacity=1288.4, - type=archetype, - ) - for archetype in [ - "Gas", - "Gas/Gas", - "Gas/Steam", - "Gas/Steam/Dry", - "Gas/Steam/OnceThrough", - "Gas/Steam/Recirculating", - ] - ] - assets_provided_in_the_request = False +def test_risk_model_impact_request(): + """Tests the risk model functionality of the impact request.""" - request_dict = { - "assets": Assets( - items=( - [ - Asset( - asset_class=asset.__class__.__name__, - latitude=asset.latitude, - longitude=asset.longitude, - type=asset.type, - capacity=asset.capacity, - location=asset.location, - ) - for asset in assets - ] - if assets_provided_in_the_request - else [] - ) - ), - "include_asset_level": True, - "include_calc_details": True, - "years": [2050], - "scenarios": ["ssp585"], + assets = { + "items": [ + { + "asset_class": "RealEstateAsset", + "type": "Buildings/Industrial", + "location": "Asia", + "longitude": TestData.longitudes[0], + "latitude": TestData.latitudes[0], + }, + { + "asset_class": "PowerGeneratingAsset", + "type": "Nuclear", + "location": "Asia", + "longitude": TestData.longitudes[1], + "latitude": TestData.latitudes[1], + }, + ], + } + + request_dict = { + "assets": assets, + "include_asset_level": True, + "include_measures": False, + "include_calc_details": True, + "years": [2080], + "scenarios": ["rcp8p5"], + } + + request = requests.AssetImpactRequest(**request_dict) # type: ignore + + curve = np.array([0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163]) + store = mock_hazard_model_store_inundation( + TestData.longitudes, TestData.latitudes, curve + ) + + source_paths = get_default_source_paths(EmbeddedInventory()) + vulnerability_models = DictBasedVulnerabilityModels( + { + PowerGeneratingAsset: [InundationModel()], + RealEstateAsset: [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], } - - request = requests.AssetImpactRequest(**request_dict) # type: ignore - - store, root = zarr_memory_store() - - # Add mock riverine inundation data: - return_periods = [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - shape, t = shape_transform_21600_43200(return_periods=return_periods) - add_curves( - root, - longitudes, - latitudes, - "inundation/wri/v2/inunriver_rcp4p5_MIROC-ESM-CHEM_2030", - shape, - np.array( - [ - 8.378922939300537e-05, - 0.3319014310836792, - 0.7859689593315125, - 1.30947744846344, - 1.6689927577972412, - 2.002290964126587, - 2.416414737701416, - 2.7177860736846924, - 3.008821725845337, - ] - ), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "inundation/wri/v2/inunriver_rcp8p5_MIROC-ESM-CHEM_2050", - shape, - np.array( - [ - 0.001158079132437706, - 0.3938717246055603, - 0.8549619913101196, - 1.3880255222320557, - 1.7519289255142212, - 2.0910017490386963, - 2.5129663944244385, - 2.8202412128448486, - 3.115604877471924, - ] - ), - return_periods, - t, - ) - - # Add mock drought data: - return_periods = [0.0, -1.0, -1.5, -2.0, -2.5, -3.0, -3.6] - shape, t = shape_transform_21600_43200(return_periods=return_periods) - add_curves( - root, - longitudes, - latitudes, - "drought/osc/v1/months_spei12m_below_index_MIROC6_ssp585_2050", - shape, - np.array( - [ - 6.900000095367432, - 1.7999999523162842, - 0.44999998807907104, - 0.06584064255906408, - 0.06584064255906408, - 0.0, - 0.0, - ] - ), - return_periods, - t, - ) - - return_periods = [0.0] - shape, t = shape_transform_21600_43200(return_periods=return_periods) - - # Add mock drought (Jupiter) data: - add_curves( - root, - longitudes, - latitudes, - "drought/jupiter/v1/months_spei3m_below_-2_ssp585_2050", - shape, - np.array([0.06584064255906408]), - return_periods, - t, - ) - - # Add mock water-related risk data: - add_curves( - root, - longitudes, - latitudes, - "water_risk/wri/v2/water_stress_ssp585_2050", - shape, - np.array([0.14204320311546326]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "water_risk/wri/v2/water_supply_ssp585_2050", - shape, - np.array([76.09415435791016]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "water_risk/wri/v2/water_supply_historical_1999", - shape, - np.array([88.62285614013672]), - return_periods, - t, - ) - - # Add mock chronic heat data: - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_25c_ACCESS-CM2_ssp585_2050", - shape, - np.array([148.55369567871094]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_30c_ACCESS-CM2_ssp585_2050", - shape, - np.array([65.30751037597656]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_35c_ACCESS-CM2_ssp585_2050", - shape, - np.array([0.6000000238418579]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_40c_ACCESS-CM2_ssp585_2050", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_45c_ACCESS-CM2_ssp585_2050", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_50c_ACCESS-CM2_ssp585_2050", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_55c_ACCESS-CM2_ssp585_2050", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_25c_ACCESS-CM2_historical_2005", - shape, - np.array([120.51940155029297]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_30c_ACCESS-CM2_historical_2005", - shape, - np.array([14.839207649230957]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_35c_ACCESS-CM2_historical_2005", - shape, - np.array([0.049863386899232864]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_40c_ACCESS-CM2_historical_2005", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_45c_ACCESS-CM2_historical_2005", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_50c_ACCESS-CM2_historical_2005", - shape, - np.array([0.0]), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_tas_above_55c_ACCESS-CM2_historical_2005", - shape, - np.array([0.0]), - return_periods, - t, - ) - - # Add mock water temperature data: - return_periods = [ - 5, - 7.5, - 10, - 12.5, - 15, - 17.5, - 20, - 22.5, - 25, - 27.5, - 30, - 32.5, - 35, - 37.5, - 40, + ) + + response = requests._get_asset_impacts( + request, + ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), + vulnerability_models=vulnerability_models, + ) + + assert response.asset_impacts[0].impacts[0].hazard_type == "CoastalInundation" + + +def test_thermal_power_generation(): + latitudes = np.array([32.6017]) + longitudes = np.array([-87.7811]) + + assets = [ + ThermalPowerGeneratingAsset( + latitude=latitudes[0], + longitude=longitudes[0], + location="North America", + capacity=1288.4, + type=archetype, + ) + for archetype in [ + "Gas", + "Gas/Gas", + "Gas/Steam", + "Gas/Steam/Dry", + "Gas/Steam/OnceThrough", + "Gas/Steam/Recirculating", ] - shape, t = shape_transform_21600_43200(return_periods=return_periods) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/nluu/v2/weeks_water_temp_above_GFDL_historical_1991", - shape, - np.array( - [ - 52.0, - 51.9, - 49.666668, - 45.066666, - 38.0, - 31.1, - 26.0, - 21.066668, - 14.233334, - 8.0333338, - 5.0999999, - 2.3666666, - 6.6666669, - 3.3333335, - 0.0, - ] - ), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/nluu/v2/weeks_water_temp_above_GFDL_rcp8p5_2050", - shape, - np.array( - [ - 51.85, - 51.5, - 50.25, - 46.75, - 41.95, - 35.35, - 29.4, - 24.55, - 20.15, - 13.85, - 6.75, - 3.5, - 1.3, - 0.25, - 0.1, - ] - ), - return_periods, - t, - ) - - # Add mock WBGT data: - return_periods = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60] - shape, t = shape_transform_21600_43200(return_periods=return_periods) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_wbgt_above_ACCESS-CM2_ssp585_2050", - shape, - np.array( - [ - 363.65054, - 350.21094, - 303.6388, - 240.48442, - 181.82924, - 128.46844, - 74.400276, - 1.3997267, - 0.0, - 0.0, - 0.0, - 0.0, - ] - ), - return_periods, - t, - ) - add_curves( - root, - longitudes, - latitudes, - "chronic_heat/osc/v2/days_wbgt_above_ACCESS-CM2_historical_2005", - shape, - np.array( - [ - 361.95273, - 342.51804, - 278.8146, - 213.5123, - 157.4511, - 101.78238, - 12.6897545, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ] - ), - return_periods, - t, - ) - - source_paths = get_default_source_paths(EmbeddedInventory()) - vulnerability_models = DictBasedVulnerabilityModels( - { - ThermalPowerGeneratingAsset: [ - ThermalPowerGenerationAirTemperatureModel(), - ThermalPowerGenerationDroughtModel(), - ThermalPowerGenerationRiverineInundationModel(), - ThermalPowerGenerationWaterStressModel(), - ThermalPowerGenerationWaterTemperatureModel(), - ] - } - ) + ] - response = requests._get_asset_impacts( - request, - ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), - vulnerability_models=vulnerability_models, - assets=None if assets_provided_in_the_request else assets, - ) - - # Air Temperature - self.assertAlmostEqual( - response.asset_impacts[0].impacts[0].impact_mean, 0.0075618606988512764 - ) - self.assertAlmostEqual( - response.asset_impacts[1].impacts[0].impact_mean, 0.0075618606988512764 - ) - self.assertAlmostEqual( - response.asset_impacts[2].impacts[0].impact_mean, 0.0025192163596997963 - ) - self.assertAlmostEqual( - response.asset_impacts[3].impacts[0].impact_mean, 0.0025192163596997963 - ) - self.assertAlmostEqual(response.asset_impacts[4].impacts[0].impact_mean, 0.0) - self.assertAlmostEqual(response.asset_impacts[5].impacts[0].impact_mean, 0.0) - - # Drought - self.assertAlmostEqual( - response.asset_impacts[0].impacts[1].impact_mean, 0.0008230079663917424 - ) - self.assertAlmostEqual(response.asset_impacts[1].impacts[1].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[2].impacts[1].impact_mean, 0.0008230079663917424 - ) - self.assertAlmostEqual(response.asset_impacts[3].impacts[1].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[4].impacts[1].impact_mean, 0.0008230079663917424 - ) - self.assertAlmostEqual( - response.asset_impacts[5].impacts[1].impact_mean, 0.0008230079663917424 - ) - - # Riverine Inundation - self.assertAlmostEqual( - response.asset_impacts[0].impacts[2].impact_mean, 0.0046864436945997625 - ) - self.assertAlmostEqual( - response.asset_impacts[1].impacts[2].impact_mean, 0.0046864436945997625 - ) - self.assertAlmostEqual( - response.asset_impacts[2].impacts[2].impact_mean, 0.0046864436945997625 - ) - self.assertAlmostEqual( - response.asset_impacts[3].impacts[2].impact_mean, 0.0046864436945997625 - ) - self.assertAlmostEqual( - response.asset_impacts[4].impacts[2].impact_mean, 0.0046864436945997625 - ) - self.assertAlmostEqual( - response.asset_impacts[5].impacts[2].impact_mean, 0.0046864436945997625 - ) + assets_provided_in_the_request = False - # Water Stress - self.assertAlmostEqual( - response.asset_impacts[0].impacts[3].impact_mean, 0.010181435900296947 - ) - self.assertAlmostEqual(response.asset_impacts[1].impacts[3].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[2].impacts[3].impact_mean, 0.010181435900296947 - ) - self.assertAlmostEqual(response.asset_impacts[3].impacts[3].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[4].impacts[3].impact_mean, 0.010181435900296947 - ) - self.assertAlmostEqual( - response.asset_impacts[5].impacts[3].impact_mean, 0.010181435900296947 - ) - - # Water Temperature - self.assertAlmostEqual( - response.asset_impacts[0].impacts[4].impact_mean, 0.1448076958069578 - ) - self.assertAlmostEqual(response.asset_impacts[1].impacts[4].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[2].impacts[4].impact_mean, 0.1448076958069578 - ) - self.assertAlmostEqual(response.asset_impacts[3].impacts[4].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[4].impacts[4].impact_mean, 0.1448076958069578 - ) - self.assertAlmostEqual( - response.asset_impacts[5].impacts[4].impact_mean, 0.005896707722257193 - ) - - vulnerability_models = DictBasedVulnerabilityModels( - { - ThermalPowerGeneratingAsset: [ - ThermalPowerGenerationDroughtModel( - impact_based_on_a_single_point=True - ), + request_dict = { + "assets": Assets( + items=( + [ + Asset( + asset_class=asset.__class__.__name__, + latitude=asset.latitude, + longitude=asset.longitude, + type=asset.type, + capacity=asset.capacity, + location=asset.location, + ) + for asset in assets ] - } - ) - - response = requests._get_asset_impacts( - request, - ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), - vulnerability_models=vulnerability_models, - assets=None if assets_provided_in_the_request else assets, - ) - - # Drought (Jupiter) - self.assertAlmostEqual( - response.asset_impacts[0].impacts[0].impact_mean, 0.0005859470850072303 - ) - self.assertAlmostEqual(response.asset_impacts[1].impacts[0].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[2].impacts[0].impact_mean, 0.0005859470850072303 - ) - self.assertAlmostEqual(response.asset_impacts[3].impacts[0].impact_mean, 0.0) - self.assertAlmostEqual( - response.asset_impacts[4].impacts[0].impact_mean, 0.0005859470850072303 - ) - self.assertAlmostEqual( - response.asset_impacts[5].impacts[0].impact_mean, 0.0005859470850072303 - ) - - @unittest.skip("example, not test") - def test_example_portfolios(self): - example_portfolios = requests._get_example_portfolios() - for assets in example_portfolios: - request_dict = { - "assets": assets, - "include_asset_level": True, - "include_calc_details": False, - "years": [2030, 2040, 2050], - "scenarios": ["ssp585"], - } - container = Container() - requester = container.requester() - response = requester.get( - request_id="get_asset_impact", request_dict=request_dict + if assets_provided_in_the_request + else [] ) - with open("out.json", "w") as f: - f.write(response) - assert response is not None - - @unittest.skip("example, not test") - def test_example_portfolios_risk_measures(self): - assets = { - "items": [ - { - "asset_class": "RealEstateAsset", - "type": "Buildings/Commercial", - "location": "Europe", - "longitude": 11.5391, - "latitude": 48.1485, - } - ], + ), + "include_asset_level": True, + "include_calc_details": True, + "years": [2050], + "scenarios": ["ssp585"], + } + + request = requests.AssetImpactRequest(**request_dict) # type: ignore + + store, root = zarr_memory_store() + + # Add mock riverine inundation data: + return_periods = [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] + shape, t = shape_transform_21600_43200(return_periods=return_periods) + add_curves( + root, + longitudes, + latitudes, + "inundation/wri/v2/inunriver_rcp4p5_MIROC-ESM-CHEM_2030", + shape, + np.array( + [ + 8.378922939300537e-05, + 0.3319014310836792, + 0.7859689593315125, + 1.30947744846344, + 1.6689927577972412, + 2.002290964126587, + 2.416414737701416, + 2.7177860736846924, + 3.008821725845337, + ] + ), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "inundation/wri/v2/inunriver_rcp8p5_MIROC-ESM-CHEM_2050", + shape, + np.array( + [ + 0.001158079132437706, + 0.3938717246055603, + 0.8549619913101196, + 1.3880255222320557, + 1.7519289255142212, + 2.0910017490386963, + 2.5129663944244385, + 2.8202412128448486, + 3.115604877471924, + ] + ), + return_periods, + t, + ) + + # Add mock drought data: + return_periods = [0.0, -1.0, -1.5, -2.0, -2.5, -3.0, -3.6] + shape, t = shape_transform_21600_43200(return_periods=return_periods) + add_curves( + root, + longitudes, + latitudes, + "drought/osc/v1/months_spei12m_below_index_MIROC6_ssp585_2050", + shape, + np.array( + [ + 6.900000095367432, + 1.7999999523162842, + 0.44999998807907104, + 0.06584064255906408, + 0.06584064255906408, + 0.0, + 0.0, + ] + ), + return_periods, + t, + ) + + return_periods = [0.0] + shape, t = shape_transform_21600_43200(return_periods=return_periods) + + # Add mock drought (Jupiter) data: + add_curves( + root, + longitudes, + latitudes, + "drought/jupiter/v1/months_spei3m_below_-2_ssp585_2050", + shape, + np.array([0.06584064255906408]), + return_periods, + t, + ) + + # Add mock water-related risk data: + add_curves( + root, + longitudes, + latitudes, + "water_risk/wri/v2/water_stress_ssp585_2050", + shape, + np.array([0.14204320311546326]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "water_risk/wri/v2/water_supply_ssp585_2050", + shape, + np.array([76.09415435791016]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "water_risk/wri/v2/water_supply_historical_1999", + shape, + np.array([88.62285614013672]), + return_periods, + t, + ) + + # Add mock chronic heat data: + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_25c_ACCESS-CM2_ssp585_2050", + shape, + np.array([148.55369567871094]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_30c_ACCESS-CM2_ssp585_2050", + shape, + np.array([65.30751037597656]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_35c_ACCESS-CM2_ssp585_2050", + shape, + np.array([0.6000000238418579]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_40c_ACCESS-CM2_ssp585_2050", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_45c_ACCESS-CM2_ssp585_2050", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_50c_ACCESS-CM2_ssp585_2050", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_55c_ACCESS-CM2_ssp585_2050", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_25c_ACCESS-CM2_historical_2005", + shape, + np.array([120.51940155029297]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_30c_ACCESS-CM2_historical_2005", + shape, + np.array([14.839207649230957]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_35c_ACCESS-CM2_historical_2005", + shape, + np.array([0.049863386899232864]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_40c_ACCESS-CM2_historical_2005", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_45c_ACCESS-CM2_historical_2005", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_50c_ACCESS-CM2_historical_2005", + shape, + np.array([0.0]), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_tas_above_55c_ACCESS-CM2_historical_2005", + shape, + np.array([0.0]), + return_periods, + t, + ) + + # Add mock water temperature data: + return_periods = [ + 5, + 7.5, + 10, + 12.5, + 15, + 17.5, + 20, + 22.5, + 25, + 27.5, + 30, + 32.5, + 35, + 37.5, + 40, + ] + shape, t = shape_transform_21600_43200(return_periods=return_periods) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/nluu/v2/weeks_water_temp_above_GFDL_historical_1991", + shape, + np.array( + [ + 52.0, + 51.9, + 49.666668, + 45.066666, + 38.0, + 31.1, + 26.0, + 21.066668, + 14.233334, + 8.0333338, + 5.0999999, + 2.3666666, + 6.6666669, + 3.3333335, + 0.0, + ] + ), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/nluu/v2/weeks_water_temp_above_GFDL_rcp8p5_2050", + shape, + np.array( + [ + 51.85, + 51.5, + 50.25, + 46.75, + 41.95, + 35.35, + 29.4, + 24.55, + 20.15, + 13.85, + 6.75, + 3.5, + 1.3, + 0.25, + 0.1, + ] + ), + return_periods, + t, + ) + + # Add mock WBGT data: + return_periods = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60] + shape, t = shape_transform_21600_43200(return_periods=return_periods) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_wbgt_above_ACCESS-CM2_ssp585_2050", + shape, + np.array( + [ + 363.65054, + 350.21094, + 303.6388, + 240.48442, + 181.82924, + 128.46844, + 74.400276, + 1.3997267, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ), + return_periods, + t, + ) + add_curves( + root, + longitudes, + latitudes, + "chronic_heat/osc/v2/days_wbgt_above_ACCESS-CM2_historical_2005", + shape, + np.array( + [ + 361.95273, + 342.51804, + 278.8146, + 213.5123, + 157.4511, + 101.78238, + 12.6897545, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ), + return_periods, + t, + ) + + source_paths = get_default_source_paths(EmbeddedInventory()) + vulnerability_models = DictBasedVulnerabilityModels( + { + ThermalPowerGeneratingAsset: [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationDroughtModel(), + ThermalPowerGenerationRiverineInundationModel(), + ThermalPowerGenerationWaterStressModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ] + } + ) + + response = requests._get_asset_impacts( + request, + ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), + vulnerability_models=vulnerability_models, + assets=None if assets_provided_in_the_request else assets, + ) + + # Air Temperature + assert response.asset_impacts[0].impacts[0].impact_mean == pytest.approx( + 0.0075618606988512764 + ) + assert response.asset_impacts[1].impacts[0].impact_mean == pytest.approx( + 0.0075618606988512764 + ) + assert response.asset_impacts[2].impacts[0].impact_mean == pytest.approx( + 0.0025192163596997963 + ) + assert response.asset_impacts[3].impacts[0].impact_mean == pytest.approx( + 0.0025192163596997963 + ) + assert response.asset_impacts[4].impacts[0].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[5].impacts[0].impact_mean == pytest.approx(0.0) + + # Drought + assert response.asset_impacts[0].impacts[1].impact_mean == pytest.approx( + 0.0008230079663917424 + ) + assert response.asset_impacts[1].impacts[1].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[2].impacts[1].impact_mean == pytest.approx( + 0.0008230079663917424 + ) + assert response.asset_impacts[3].impacts[1].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[4].impacts[1].impact_mean == pytest.approx( + 0.0008230079663917424 + ) + assert response.asset_impacts[5].impacts[1].impact_mean == pytest.approx( + 0.0008230079663917424 + ) + + # Riverine Inundation + assert response.asset_impacts[0].impacts[2].impact_mean == pytest.approx( + 0.0046864436945997625 + ) + assert response.asset_impacts[1].impacts[2].impact_mean == pytest.approx( + 0.0046864436945997625 + ) + assert response.asset_impacts[2].impacts[2].impact_mean == pytest.approx( + 0.0046864436945997625 + ) + assert response.asset_impacts[3].impacts[2].impact_mean == pytest.approx( + 0.0046864436945997625 + ) + assert response.asset_impacts[4].impacts[2].impact_mean == pytest.approx( + 0.0046864436945997625 + ) + assert response.asset_impacts[5].impacts[2].impact_mean == pytest.approx( + 0.0046864436945997625 + ) + + # Water Stress + assert response.asset_impacts[0].impacts[3].impact_mean == pytest.approx( + 0.010181435900296947 + ) + assert response.asset_impacts[1].impacts[3].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[2].impacts[3].impact_mean == pytest.approx( + 0.010181435900296947 + ) + assert response.asset_impacts[3].impacts[3].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[4].impacts[3].impact_mean == pytest.approx( + 0.010181435900296947 + ) + assert response.asset_impacts[5].impacts[3].impact_mean == pytest.approx( + 0.010181435900296947 + ) + + # Water Temperature + assert response.asset_impacts[0].impacts[4].impact_mean == pytest.approx( + 0.1448076958069578 + ) + assert response.asset_impacts[1].impacts[4].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[2].impacts[4].impact_mean == pytest.approx( + 0.1448076958069578 + ) + assert response.asset_impacts[3].impacts[4].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[4].impacts[4].impact_mean == pytest.approx( + 0.1448076958069578 + ) + assert response.asset_impacts[5].impacts[4].impact_mean == pytest.approx( + 0.005896707722257193 + ) + + vulnerability_models = DictBasedVulnerabilityModels( + { + ThermalPowerGeneratingAsset: [ + ThermalPowerGenerationDroughtModel(impact_based_on_a_single_point=True), + ] } - # 48.1485°, 11.5391° - # 48.1537°, 11.5852° + ) + + response = requests._get_asset_impacts( + request, + ZarrHazardModel(source_paths=source_paths, reader=ZarrReader(store)), + vulnerability_models=vulnerability_models, + assets=None if assets_provided_in_the_request else assets, + ) + + # Drought (Jupiter) + assert response.asset_impacts[0].impacts[0].impact_mean == pytest.approx( + 0.0005859470850072303 + ) + assert response.asset_impacts[1].impacts[0].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[2].impacts[0].impact_mean == pytest.approx( + 0.0005859470850072303 + ) + assert response.asset_impacts[3].impacts[0].impact_mean == pytest.approx(0.0) + assert response.asset_impacts[4].impacts[0].impact_mean == pytest.approx( + 0.0005859470850072303 + ) + assert response.asset_impacts[5].impacts[0].impact_mean == pytest.approx( + 0.0005859470850072303 + ) + + +@pytest.mark.skip(reason="Requires credentials.") +def test_example_portfolios(test_dir, load_credentials): + example_portfolios = requests._get_example_portfolios() + for assets in example_portfolios: request_dict = { "assets": assets, "include_asset_level": True, - "include_calc_details": True, - "include_measures": True, + "include_calc_details": False, "years": [2030, 2040, 2050], - "scenarios": ["ssp245", "ssp585"], # ["ssp126", "ssp245", "ssp585"], + "scenarios": ["ssp585"], } container = Container() requester = container.requester() response = requester.get( request_id="get_asset_impact", request_dict=request_dict ) - risk_measures_dict = json.loads(response)["risk_measures"] - helper = RiskMeasuresHelper( - TypeAdapter(RiskMeasures).validate_python(risk_measures_dict) - ) - for hazard_type in [ - "RiverineInundation", - "CoastalInundation", - "ChronicHeat", - "Wind", - ]: - scores, measure_values, measure_defns = helper.get_measure( - hazard_type, "ssp585", 2050 - ) - label, description = helper.get_score_details(scores[0], measure_defns[0]) - print(label) + with open(os.path.join(test_dir, "out.json"), "w") as f: + f.write(response) + assert response is not None + + +@pytest.mark.skip(reason="Example, not test. And requires credentials") +def test_example_portfolios_risk_measures(load_credentials): + assets = { + "items": [ + { + "asset_class": "RealEstateAsset", + "type": "Buildings/Commercial", + "location": "Europe", + "longitude": 11.5391, + "latitude": 48.1485, + } + ], + } + # 48.1485°, 11.5391° + # 48.1537°, 11.5852° + request_dict = { + "assets": assets, + "include_asset_level": True, + "include_calc_details": True, + "include_measures": True, + "years": [2030, 2040, 2050], + "scenarios": ["ssp245", "ssp585"], # ["ssp126", "ssp245", "ssp585"], + } + container = Container() + requester = container.requester() + response = requester.get(request_id="get_asset_impact", request_dict=request_dict) + risk_measures_dict = json.loads(response)["risk_measures"] + helper = RiskMeasuresHelper( + TypeAdapter(RiskMeasures).validate_python(risk_measures_dict) + ) + for hazard_type in [ + "RiverineInundation", + "CoastalInundation", + "ChronicHeat", + "Wind", + ]: + scores, measure_values, measure_defns = helper.get_measure( + hazard_type, "ssp585", 2050 + ) + label, description = helper.get_score_details(scores[0], measure_defns[0]) + print(label) diff --git a/tests/api/wri_global_power_plant_database.tbz2 b/tests/api/wri_global_power_plant_database.tbz2 new file mode 100644 index 00000000..6846bf92 Binary files /dev/null and b/tests/api/wri_global_power_plant_database.tbz2 differ diff --git a/tests/base_test.py b/tests/base_test.py deleted file mode 100644 index 5e72175a..00000000 --- a/tests/base_test.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import pathlib -import shutil -import tempfile -import unittest - -from dotenv import load_dotenv - - -class TestWithCredentials(unittest.TestCase): - """Test that attempts to load contents of credentials.env into environment variables (if present)""" - - def setUp(self): - self.test_dir = tempfile.mkdtemp() - dotenv_dir = os.environ.get("CREDENTIAL_DOTENV_DIR", os.getcwd()) - dotenv_path = pathlib.Path(dotenv_dir) / "credentials.env" - if os.path.exists(dotenv_path): - load_dotenv(dotenv_path=dotenv_path, override=True) - - def tearDown(self): - shutil.rmtree(self.test_dir) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..92b87ad6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,69 @@ +import os +import pathlib +import shutil +import tarfile +import tempfile + +import pandas as pd +import pytest +from dotenv import load_dotenv + + +@pytest.fixture(scope="function") +def test_dir(): + # Setup + test_dir = tempfile.mkdtemp() + yield test_dir + # Teardown + shutil.rmtree(test_dir) + + +@pytest.fixture( + scope="function", +) +def load_credentials(): + dotenv_dir = os.environ.get("CREDENTIAL_DOTENV_DIR", os.getcwd()) + dotenv_path = pathlib.Path(dotenv_dir) / "credentials.env" + if os.path.exists(dotenv_path): + load_dotenv(dotenv_path=dotenv_path, override=True) + + +@pytest.fixture( + scope="function", +) +def wri_power_plant_assets(): + """ + Load the WRI Global Power Plant Database dataset. + + This function extracts the `global_power_plant_database.csv` file from the + `wri_global_power_plant_database.tbz2` archive and loads it into a pandas DataFrame. + + The dataset is sourced from the World Resources Institute's (WRI) Global Power Plant Database. + The original dataset and more information can be found at: + https://datasets.wri.org/dataset/globalpowerplantdatabase + + License: + The dataset is provided under the Creative Commons Attribution 4.0 International (CC BY 4.0) license. + This means you are free to: + - Share: copy and redistribute the material in any medium or format + - Adapt: remix, transform, and build upon the material for any purpose, even commercially. + + Under the following terms: + - Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. + You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + + More details about the license can be found at: https://creativecommons.org/licenses/by/4.0/ + + Note: + The original README file and a copy of the CC BY 4.0 license are included in the tar.bz2 archive. + + Returns: + pandas.DataFrame: A DataFrame containing the Global Power Plant Database. + """ + with tarfile.open( + "./tests/api/wri_global_power_plant_database.tbz2", "r:bz2" + ) as tf: + with tf.extractfile("global_power_plant_database.csv") as f: + asset_list = pd.read_csv(f, low_memory=False) + + return asset_list diff --git a/tests/data/events_retrieval_test.py b/tests/data/events_retrieval_test.py index 9f5f02dd..867bb81b 100644 --- a/tests/data/events_retrieval_test.py +++ b/tests/data/events_retrieval_test.py @@ -1,9 +1,7 @@ import os -import unittest -# import fsspec.implementations.local as local # type: ignore import numpy as np -import numpy.testing +import pytest import scipy.interpolate import zarr from fsspec.implementations.memory import MemoryFileSystem @@ -20,285 +18,275 @@ from physrisk.data.zarr_reader import ZarrReader from physrisk.kernel.hazard_model import HazardDataRequest from physrisk.kernel.hazards import RiverineInundation +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.requests import _get_hazard_data_availability -# from pathlib import PurePosixPath -from ..base_test import TestWithCredentials from ..data.hazard_model_store_test import ( ZarrStoreMocker, mock_hazard_model_store_inundation, ) -class TestEventRetrieval(TestWithCredentials): - @unittest.skip("S3 access needed") - def test_inventory_change(self): - # check validation passes calling in service-like way - embedded = EmbeddedInventory() - resources1 = embedded.to_resources() - inventory = Inventory(resources1).json_ordered() - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "inventory.json"), - "w", - ) as f: - f.write(inventory) - - def test_hazard_data_availability_summary(self): - # check validation passes calling in service-like way - inventory = EmbeddedInventory() - response = _get_hazard_data_availability( - HazardAvailabilityRequest(sources=["embedded"]), - inventory, - inventory.colormaps(), - ) # , "hazard_test"]) - assert len(response.models) > 0 # rely on Pydantic validation for test - - def test_set_get_inventory(self): - fs = MemoryFileSystem() - reader = InventoryReader(fs=fs) - reader.append("hazard_test", [self._test_hazard_model()]) - assert reader.read("hazard_test")[0].indicator_id == "test_indicator_id" - - @unittest.skip("S3 access needed") - def test_set_get_inventory_s3(self): - reader = InventoryReader() - reader.append("hazard_test", [self._test_hazard_model()]) - assert reader.read("hazard_test")[0].id == "test_indicator_id" - - def _test_hazard_model(self): - return HazardResource( - hazard_type="TestHazardType", - indicator_id="test_indicator_id", - indicator_model_gcm="test_gcm", - path="test_array_path", - display_name="Test hazard indicator", - description="Description of test hazard indicator", - scenarios=[Scenario(id="historical", years=[2010])], - units="K", - ) +def test_inventory_change(test_dir): + embedded = EmbeddedInventory() + resources1 = embedded.resources.values() + inventory = Inventory(resources1).json_ordered() + with open( + os.path.join(test_dir, "inventory.json"), + "w", + ) as f: + f.write(inventory) - def test_zarr_bilinear(self): - # create suitable asymmetric data set and compare with scipy - xt, yt = np.meshgrid(np.linspace(-5, 5, 10), np.linspace(-5, 5, 10)) - data0 = np.exp(-(xt**2 / 25.0 + yt**2 / 16.0)) - data1 = np.exp(-(xt**2 / 36.0 + yt**2 / 25.0)) +def test_hazard_data_availability_summary(): + inventory = EmbeddedInventory() + response = _get_hazard_data_availability( + HazardAvailabilityRequest(sources=["embedded"]), + inventory, + inventory.colormaps(), + ) + assert len(response.models) > 0 - data = np.stack([data0, data1], axis=0) - # note that zarr array has index [z, y, x], e.g. 9, 21600, 43200 or [index, lat, lon] - y = np.array([1.4, 2.8, 3.4]) # row indices - x = np.array([3.2, 6.7, 7.9]) # column indices - image_coords = np.stack([x, y]) - data_zarr = zarr.array(data) - candidate_lin = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords, np.array([0, 1]), interpolation="linear" - ) - candidate_max = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords, np.array([0, 1]), interpolation="max" - ) - candidate_min = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords, np.array([0, 1]), interpolation="min" - ) +def test_set_get_inventory(): + fs = MemoryFileSystem() + reader = InventoryReader(fs=fs) + reader.append("hazard_test", [_test_hazard_model()]) + assert reader.read("hazard_test")[0].indicator_id == "test_indicator_id" - image_coords_surr = np.stack( - [ - np.concatenate( - [np.floor(x), np.floor(x) + 1, np.floor(x), np.floor(x) + 1] - ), - np.concatenate( - [np.floor(y), np.floor(y), np.floor(y) + 1, np.floor(y) + 1] - ), - ] - ) - values_surr = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords_surr, np.array([0, 1]), interpolation="linear" - ).reshape((4, 3, 2)) - interp_scipy0 = scipy.interpolate.RectBivariateSpline( - np.linspace(0, 9, 10), np.linspace(0, 9, 10), data0.T, kx=1, ky=1 - ) - interp_scipy1 = scipy.interpolate.RectBivariateSpline( - np.linspace(0, 9, 10), np.linspace(0, 9, 10), data1.T, kx=1, ky=1 - ) - expected0_lin = interp_scipy0(x, y).diagonal().reshape(len(y)) - expected1_lin = interp_scipy1(x, y).diagonal().reshape(len(y)) - expected0_max = np.max(values_surr[:, :, 0], axis=0) - expected1_max = np.max(values_surr[:, :, 1], axis=0) - expected0_min = np.min(values_surr[:, :, 0], axis=0) - expected1_min = np.min(values_surr[:, :, 1], axis=0) - - numpy.testing.assert_allclose(candidate_lin[:, 0], expected0_lin, rtol=1e-6) - numpy.testing.assert_allclose(candidate_lin[:, 1], expected1_lin, rtol=1e-6) - numpy.testing.assert_allclose(candidate_max[:, 0], expected0_max, rtol=1e-6) - numpy.testing.assert_allclose(candidate_max[:, 1], expected1_max, rtol=1e-6) - numpy.testing.assert_allclose(candidate_min[:, 0], expected0_min, rtol=1e-6) - numpy.testing.assert_allclose(candidate_min[:, 1], expected1_min, rtol=1e-6) - - def test_zarr_bilinear_with_bad_data(self): - # create suitable asymmetric data set and compare with scipy - - xt, yt = np.meshgrid(np.linspace(0, 1, 2), np.linspace(0, 1, 2)) - data = np.array([[[1.0, -9999.0], [2.0, 0.0]]]) - - # note that zarr array has index [z, y, x], e.g. 9, 21600, 43200 or [index, lat, lon] - y = np.array([0.4, 0.5, 0.8]) # row indices - x = np.array([0.1, 0.6, 0.7]) # column indices - image_coords = np.stack([x, y]) - data_zarr = zarr.array(data) - - candidate_lin = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords, np.array([0]), interpolation="linear" - ).flatten() - candidate_max = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords, np.array([0]), interpolation="max" - ).flatten() - candidate_min = ZarrReader._linear_interp_frac_coordinates( - data_zarr, image_coords, np.array([0]), interpolation="min" - ).flatten() - - expected_lin = np.array([1.34042553, 0.85714286, 0.62790698]) - expected_max = np.array([2.0, 2.0, 2.0]) - expected_min = np.array([0.0, 0.0, 0.0]) - - numpy.testing.assert_allclose(candidate_lin, expected_lin, rtol=1e-6) - numpy.testing.assert_allclose(candidate_max, expected_max, rtol=1e-6) - numpy.testing.assert_allclose(candidate_min, expected_min, rtol=1e-6) - - def test_zarr_geomax_on_grid(self): - lons_ = np.array([3.92783]) - lats_ = np.array([50.882394]) - curve = np.array( - [ - 0.00, - 0.06997928, - 0.2679602, - 0.51508933, - 0.69842442, - 0.88040525, - 1.11911115, - 1.29562478, - 1.47200677, - ] - ) - set_id = r"inundation/wri/v2\\inunriver_rcp8p5_MIROC-ESM-CHEM_2080" - interpolation = "linear" - delta_km = 0.100 - n_grid = 11 - store_ = mock_hazard_model_store_inundation(lons_, lats_, curve) - zarrreader_ = ZarrReader(store_) - - lons_ = np.array([3.92916667, 3.925] + list(lons_)) - lats_ = np.array([50.87916667, 50.88333333] + list(lats_)) - curves_max_candidate, _ = zarrreader_.get_max_curves_on_grid( - set_id, - lons_, - lats_, - interpolation=interpolation, - delta_km=delta_km, - n_grid=n_grid, - ) - curves_max_expected = np.array( - [ - curve, - [ - 0.0, - 0.02272942, - 0.08703404, - 0.16730212, - 0.22684974, - 0.28595751, - 0.3634897, - 0.42082168, - 0.47811095, - ], - [ - 0.0, - 0.0432026, - 0.16542863, - 0.31799695, - 0.43118118, - 0.54352937, - 0.69089751, - 0.7998704, - 0.90876211, - ], - ] - ) - numpy.testing.assert_allclose( - curves_max_candidate, curves_max_expected, rtol=1e-6 - ) +@pytest.mark.skip(reason="Requires credentials.") +def test_set_get_inventory_s3(load_credentials): + reader = InventoryReader() + reader.append("hazard_test", [_test_hazard_model()]) + assert reader.read("hazard_test")[0].indicator_id == "test_indicator_id" - def test_zarr_geomax(self): - longitudes = np.array([3.926]) - latitudes = np.array([50.878]) - curve = np.array( - [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4], - ) - set_id = r"inundation/wri/v2\\inunriver_rcp8p5_MIROC-ESM-CHEM_2080" - delta_deg = 0.1 - shapes = [ - Polygon( - ( - (x - 0.5 * delta_deg, y - 0.5 * delta_deg), - (x - 0.5 * delta_deg, y + 0.5 * delta_deg), - (x + 0.5 * delta_deg, y + 0.5 * delta_deg), - (x + 0.5 * delta_deg, y - 0.5 * delta_deg), - ) - ) - for x, y in zip(longitudes, latitudes) + +def _test_hazard_model(): + return HazardResource( + hazard_type="TestHazardType", + indicator_id="test_indicator_id", + indicator_model_gcm="test_gcm", + path="test_array_path", + display_name="Test hazard indicator", + description="Description of test hazard indicator", + scenarios=[Scenario(id="historical", years=[2010])], + units="K", + ) + + +def test_zarr_bilinear(): + xt, yt = np.meshgrid(np.linspace(-5, 5, 10), np.linspace(-5, 5, 10)) + data0 = np.exp(-(xt**2 / 25.0 + yt**2 / 16.0)) + data1 = np.exp(-(xt**2 / 36.0 + yt**2 / 25.0)) + + data = np.stack([data0, data1], axis=0) + + y = np.array([1.4, 2.8, 3.4]) + x = np.array([3.2, 6.7, 7.9]) + image_coords = np.stack([x, y]) + data_zarr = zarr.array(data) + candidate_lin = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords, np.array([0, 1]), interpolation="linear" + ) + candidate_max = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords, np.array([0, 1]), interpolation="max" + ) + candidate_min = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords, np.array([0, 1]), interpolation="min" + ) + + image_coords_surr = np.stack( + [ + np.concatenate( + [np.floor(x), np.floor(x) + 1, np.floor(x), np.floor(x) + 1] + ), + np.concatenate( + [np.floor(y), np.floor(y), np.floor(y) + 1, np.floor(y) + 1] + ), ] - store = mock_hazard_model_store_inundation(longitudes, latitudes, curve) - zarr_reader = ZarrReader(store) - curves_max_expected = np.array([[0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]]) + ) + values_surr = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords_surr, np.array([0, 1]), interpolation="linear" + ).reshape((4, 3, 2)) + interp_scipy0 = scipy.interpolate.RectBivariateSpline( + np.linspace(0, 9, 10), np.linspace(0, 9, 10), data0.T, kx=1, ky=1 + ) + interp_scipy1 = scipy.interpolate.RectBivariateSpline( + np.linspace(0, 9, 10), np.linspace(0, 9, 10), data1.T, kx=1, ky=1 + ) + expected0_lin = interp_scipy0(x, y).diagonal().reshape(len(y)) + expected1_lin = interp_scipy1(x, y).diagonal().reshape(len(y)) + expected0_max = np.max(values_surr[:, :, 0], axis=0) + expected1_max = np.max(values_surr[:, :, 1], axis=0) + expected0_min = np.min(values_surr[:, :, 0], axis=0) + expected1_min = np.min(values_surr[:, :, 1], axis=0) - curves_max_candidate, _, _ = zarr_reader.get_max_curves( - set_id, shapes, interpolation="floor" - ) - numpy.testing.assert_allclose( - curves_max_candidate, curves_max_expected, rtol=1e-6 - ) + np.testing.assert_allclose(candidate_lin[:, 0], expected0_lin, rtol=1e-6) + np.testing.assert_allclose(candidate_lin[:, 1], expected1_lin, rtol=1e-6) + np.testing.assert_allclose(candidate_max[:, 0], expected0_max, rtol=1e-6) + np.testing.assert_allclose(candidate_max[:, 1], expected1_max, rtol=1e-6) + np.testing.assert_allclose(candidate_min[:, 0], expected0_min, rtol=1e-6) + np.testing.assert_allclose(candidate_min[:, 1], expected1_min, rtol=1e-6) - curves_max_candidate, _, _ = zarr_reader.get_max_curves( - set_id, shapes, interpolation="linear" - ) - numpy.testing.assert_allclose( - curves_max_candidate, curves_max_expected / 4, rtol=1e-6 - ) - def test_reproject(self): - """Test adding data in a non-ESPG-4326 coordinate reference system. Check that attribute - end in the correct convertion.""" - mocker = ZarrStoreMocker() - lons = [1.1, -0.31] - lats = [47.0, 52.0] - mocker._add_curves( - "test", - lons, - lats, - "epsg:3035", - [3, 39420, 38371], - [100.0, 0.0, 2648100.0, 0.0, -100.0, 5404500], - [10.0, 100.0, 1000.0], - [1.0, 2.0, 3.0], - ) +def test_zarr_bilinear_with_bad_data(): + xt, yt = np.meshgrid(np.linspace(0, 1, 2), np.linspace(0, 1, 2)) + data = np.array([[[1.0, -9999.0], [2.0, 0.0]]]) + + y = np.array([0.4, 0.5, 0.8]) + x = np.array([0.1, 0.6, 0.7]) + image_coords = np.stack([x, y]) + data_zarr = zarr.array(data) + + candidate_lin = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords, np.array([0]), interpolation="linear" + ).flatten() + candidate_max = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords, np.array([0]), interpolation="max" + ).flatten() + candidate_min = ZarrReader._linear_interp_frac_coordinates( + data_zarr, image_coords, np.array([0]), interpolation="min" + ).flatten() + + expected_lin = np.array([1.34042553, 0.85714286, 0.62790698]) + expected_max = np.array([2.0, 2.0, 2.0]) + expected_min = np.array([0.0, 0.0, 0.0]) - source_paths = { - RiverineInundation: lambda indicator_id, scenario, year, hint: "test" - } - hazard_model = ZarrHazardModel(source_paths=source_paths, store=mocker.store) - response = hazard_model.get_hazard_events( + np.testing.assert_allclose(candidate_lin, expected_lin, rtol=1e-6) + np.testing.assert_allclose(candidate_max, expected_max, rtol=1e-6) + np.testing.assert_allclose(candidate_min, expected_min, rtol=1e-6) + + +def test_zarr_geomax_on_grid(): + lons_ = np.array([3.92783]) + lats_ = np.array([50.882394]) + curve = np.array( + [ + 0.00, + 0.06997928, + 0.2679602, + 0.51508933, + 0.69842442, + 0.88040525, + 1.11911115, + 1.29562478, + 1.47200677, + ] + ) + set_id = r"inundation/wri/v2\\inunriver_rcp8p5_MIROC-ESM-CHEM_2080" + interpolation = "linear" + delta_km = 0.100 + n_grid = 11 + store_ = mock_hazard_model_store_inundation(lons_, lats_, curve) + zarrreader_ = ZarrReader(store_) + + lons_ = np.array([3.92916667, 3.925] + list(lons_)) + lats_ = np.array([50.87916667, 50.88333333] + list(lats_)) + curves_max_candidate, _ = zarrreader_.get_max_curves_on_grid( + set_id, + lons_, + lats_, + interpolation=interpolation, + delta_km=delta_km, + n_grid=n_grid, + ) + + curves_max_expected = np.array( + [ + curve, [ - HazardDataRequest( - RiverineInundation, - lons[0], - lats[0], - indicator_id="", - scenario="", - year=2050, - ) - ] - ) - numpy.testing.assert_equal( - next(iter(response.values())).intensities, [1.0, 2.0, 3.0] + 0.0, + 0.02272942, + 0.08703404, + 0.16730212, + 0.22684974, + 0.28595751, + 0.3634897, + 0.42082168, + 0.47811095, + ], + [ + 0.0, + 0.0432026, + 0.16542863, + 0.31799695, + 0.43118118, + 0.54352937, + 0.69089751, + 0.7998704, + 0.90876211, + ], + ] + ) + np.testing.assert_allclose(curves_max_candidate, curves_max_expected, rtol=1e-6) + + +def test_zarr_geomax(): + longitudes = np.array([3.926]) + latitudes = np.array([50.878]) + curve = np.array( + [0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4], + ) + set_id = r"inundation/wri/v2\\inunriver_rcp8p5_MIROC-ESM-CHEM_2080" + delta_deg = 0.1 + shapes = [ + Polygon( + ( + (x - 0.5 * delta_deg, y - 0.5 * delta_deg), + (x - 0.5 * delta_deg, y + 0.5 * delta_deg), + (x + 0.5 * delta_deg, y + 0.5 * delta_deg), + (x + 0.5 * delta_deg, y - 0.5 * delta_deg), + ) ) + for x, y in zip(longitudes, latitudes) + ] + store = mock_hazard_model_store_inundation(longitudes, latitudes, curve) + zarr_reader = ZarrReader(store) + curves_max_expected = np.array([[0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]]) + + curves_max_candidate, _, _ = zarr_reader.get_max_curves( + set_id, shapes, interpolation="floor" + ) + np.testing.assert_allclose(curves_max_candidate, curves_max_expected, rtol=1e-6) + + curves_max_candidate, _, _ = zarr_reader.get_max_curves( + set_id, shapes, interpolation="linear" + ) + np.testing.assert_allclose(curves_max_candidate, curves_max_expected / 4, rtol=1e-6) + + +def test_reproject(): + """Test adding data in a non-ESPG-4326 coordinate reference system. Check that attribute + end in the correct convertion.""" + mocker = ZarrStoreMocker() + lons = [1.1, -0.31] + lats = [47.0, 52.0] + mocker._add_curves( + "test", + lons, + lats, + "epsg:3035", + [3, 39420, 38371], + [100.0, 0.0, 2648100.0, 0.0, -100.0, 5404500], + [10.0, 100.0, 1000.0], + [1.0, 2.0, 3.0], + ) + + source_paths = { + RiverineInundation: lambda indicator_id, scenario, year, hint: "test" + } + hazard_model = ZarrHazardModel(source_paths=source_paths, store=mocker.store) + response = hazard_model.get_hazard_events( + [ + HazardDataRequest( + RiverineInundation, + lons[0], + lats[0], + indicator_id="", + scenario="", + year=2050, + ) + ] + ) + np.testing.assert_equal(next(iter(response.values())).intensities, [1.0, 2.0, 3.0]) diff --git a/tests/data/static_data_test.py b/tests/data/static_data_test.py index cc9f57c7..f121b8f9 100644 --- a/tests/data/static_data_test.py +++ b/tests/data/static_data_test.py @@ -1,29 +1,25 @@ -import unittest +import os from physrisk.data.static.world import ( World, get_countries_and_continents, get_countries_json, ) - from ..data.hazard_model_store_test import TestData -class TestStaticDate(unittest.TestCase): - @unittest.skip( - "example that requires geopandas (consider adding for running tests only)" +def test_get_countries_and_continents(): + countries, continents = get_countries_and_continents( + TestData.longitudes, TestData.latitudes ) - def test_get_countries_and_continents(self): - countries, continents = get_countries_and_continents( - TestData.longitudes, TestData.latitudes - ) - self.assertEqual(countries[0:3], ["Afghanistan", "Afghanistan", "Albania"]) + assert countries[0:3] == ["Afghanistan", "Afghanistan", "Albania"] + + +def test_get_countries_json(test_dir): + with open(os.path.join(test_dir, "world.json"), "w") as f: + world_json = get_countries_json() + f.write(world_json) - @unittest.skip("not really a test; just showing how world.json was generated") - def test_get_countries_json(self): - with open("world.json", "w") as f: - world_json = get_countries_json() - f.write(world_json) - def test_get_load_world(self): - self.assertEqual(World.countries["United Kingdom"].continent, "Europe") +def test_get_load_world(): + assert World.countries["United Kingdom"].continent == "Europe" diff --git a/tests/kernel/asset_impact_test.py b/tests/kernel/asset_impact_test.py index 5726f12a..5d1eb5f6 100644 --- a/tests/kernel/asset_impact_test.py +++ b/tests/kernel/asset_impact_test.py @@ -1,6 +1,6 @@ """Test asset impact calculations.""" -import unittest +import pytest import numpy as np @@ -9,6 +9,7 @@ from physrisk.kernel.hazard_event_distrib import HazardEventDistrib from physrisk.kernel.hazard_model import HazardDataRequest from physrisk.kernel.hazards import RiverineInundation +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.impact import ImpactDistrib from physrisk.kernel.vulnerability_distrib import VulnerabilityDistrib from physrisk.vulnerability_models.real_estate_models import ( @@ -17,176 +18,156 @@ ) -class TestAssetImpact(unittest.TestCase): - """Tests asset impact calculations.""" - - def test_impact_curve(self): - """Testing the generation of an asset when only an impact curve (e.g. damage curve is available)""" - - # exceedance curve - return_periods = np.array( - [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - ) - exceed_probs = 1.0 / return_periods - depths = np.array( - [ - 0.059601218, - 0.33267087, - 0.50511575, - 0.71471703, - 0.8641244, - 1.0032823, - 1.1491022, - 1.1634114, - 1.1634114, - ] - ) - curve = ExceedanceCurve(exceed_probs, depths) - - # impact curve - vul_depths = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 1]) - vul_impacts = np.array([0, 1, 2, 7, 14, 30, 60, 180, 365]) - - # say we need to add an extra depth point because the damage below that inundation depth is zero - cutoff_depth = 0.9406518 # 0.75 - curve = curve.add_value_point(cutoff_depth) - # we could also choose ensure that all impact curve depth points are - # represented in exceedance curve; we do not here - - depth_bins, probs = curve.get_probability_bins() - - impact_bins = np.interp(depth_bins, vul_depths, vul_impacts) - - include_bin = depth_bins < cutoff_depth - probs[include_bin[:-1]] = 0 # type: ignore - - mean = np.sum((impact_bins[1:] + impact_bins[:-1]) * probs / 2) # type: ignore - self.assertAlmostEqual(mean, 4.8453897) - - def test_protection_level(self): - return_periods = np.array( - [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - ) - base_depth = np.array( - [ - 0.0, - 0.22372675, - 0.3654859, - 0.5393629, - 0.6642473, - 0.78564394, - 0.9406518, - 1.0539534, - 1.1634114, - ] - ) - # future_depth = np.array( - # [0.059601218, 0.33267087, 0.50511575, 0.71471703, 0.8641244, 1.0032823, 1.1491022, 1.1634114, 1.1634114] - # ) - - exceed_probs = 1.0 / return_periods - - protection_return_period = 250.0 # protection level of 250 years - protection_depth = np.interp( - 1.0 / protection_return_period, exceed_probs[::-1], base_depth[::-1] - ) - - self.assertAlmostEqual(protection_depth, 0.9406518) # type: ignore - - def test_single_asset_impact(self): - # exceedance curve - return_periods = np.array( - [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - ) - exceed_probs = 1.0 / return_periods - depths = np.array( - [ - 0.059601218, - 0.33267087, - 0.50511575, - 0.71471703, - 0.8641244, - 1.0032823, - 1.1491022, - 1.1634114, - 1.1634114, - ] - ) - curve = ExceedanceCurve(exceed_probs, depths) - - cutoff_depth = 0.9406518 - curve = curve.add_value_point(cutoff_depth) - - # impact curve - vul_depths = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 1]) - vul_impacts = np.array([0, 1, 2, 7, 14, 30, 60, 180, 365]) - - depth_bins, probs = curve.get_probability_bins() +def test_impact_curve(): + """Testing the generation of an asset when only an impact curve (e.g. damage curve is available)""" + + # exceedance curve + return_periods = np.array([2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + exceed_probs = 1.0 / return_periods + depths = np.array( + [ + 0.059601218, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, + ] + ) + curve = ExceedanceCurve(exceed_probs, depths) - impact_bins = np.interp(depth_bins, vul_depths, vul_impacts) + vul_depths = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 1]) + vul_impacts = np.array([0, 1, 2, 7, 14, 30, 60, 180, 365]) - # if upper end of bin less then cutoff then exclude - probs_w_cutoff = np.where(depth_bins[1:] <= cutoff_depth, 0.0, 1.0) - # n_bins = len(probs) # type: ignore - vul = VulnerabilityDistrib( - type(RiverineInundation), depth_bins, impact_bins, np.diag(probs_w_cutoff) - ) # np.eye(n_bins, n_bins)) - hazard_paths = ["unknown"] - event = HazardEventDistrib( - type(RiverineInundation), depth_bins, probs, hazard_paths - ) # type: ignore + cutoff_depth = 0.9406518 + curve = curve.add_value_point(cutoff_depth) - impact_prob = vul.prob_matrix.T @ event.prob - impact = ImpactDistrib(vul.event_type, vul.impact_bins, impact_prob, event.path) + depth_bins, probs = curve.get_probability_bins() - mean = impact.mean_impact() + impact_bins = np.interp(depth_bins, vul_depths, vul_impacts) - self.assertAlmostEqual(mean, 4.8453897) + include_bin = depth_bins < cutoff_depth + probs[include_bin[:-1]] = 0 - def test_performance_hazardlookup(self): - """Just for reference: not true test""" - asset_requests = {} - import time + mean = np.sum((impact_bins[1:] + impact_bins[:-1]) * probs / 2) + assert mean == pytest.approx(4.8453897) - start = time.time() - assets = [ - RealEstateAsset(latitude=0, longitude=0, location="", type="") - for _ in range(10000) +def test_protection_level(): + return_periods = np.array([2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + base_depth = np.array( + [ + 0.0, + 0.22372675, + 0.3654859, + 0.5393629, + 0.6642473, + 0.78564394, + 0.9406518, + 1.0539534, + 1.1634114, ] - - vulnerability_models = [ - RealEstateCoastalInundationModel(), - RealEstateRiverineInundationModel(), + ) + + exceed_probs = 1.0 / return_periods + + protection_return_period = 250.0 # protection level of 250 years + protection_depth = np.interp( + 1.0 / protection_return_period, exceed_probs[::-1], base_depth[::-1] + ) + + assert protection_depth == pytest.approx(0.9406518) + + +def test_single_asset_impact(): + # exceedance curve + return_periods = np.array([2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + exceed_probs = 1.0 / return_periods + depths = np.array( + [ + 0.059601218, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, ] + ) + curve = ExceedanceCurve(exceed_probs, depths) + + cutoff_depth = 0.9406518 + curve = curve.add_value_point(cutoff_depth) + + vul_depths = np.array([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 1]) + vul_impacts = np.array([0, 1, 2, 7, 14, 30, 60, 180, 365]) + + depth_bins, probs = curve.get_probability_bins() + + impact_bins = np.interp(depth_bins, vul_depths, vul_impacts) + + probs_w_cutoff = np.where(depth_bins[1:] <= cutoff_depth, 0.0, 1.0) + vul = VulnerabilityDistrib( + type(RiverineInundation), depth_bins, impact_bins, np.diag(probs_w_cutoff) + ) + hazard_paths = ["unknown"] + event = HazardEventDistrib( + type(RiverineInundation), depth_bins, probs, hazard_paths + ) + + impact_prob = vul.prob_matrix.T @ event.prob + impact = ImpactDistrib(vul.event_type, vul.impact_bins, impact_prob, event.path) + + mean = impact.mean_impact() + + assert mean == pytest.approx(4.8453897) + + +def test_performance_hazardlookup(): + asset_requests = {} + import time + + start = time.time() + + assets = [ + RealEstateAsset(latitude=0, longitude=0, location="", type="") + for _ in range(10000) + ] + vulnerability_models = [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ] + + time_assets = time.time() - start + print(f"Time for asset generation {time_assets}s ") + start = time.time() + + # create requests: + for v in vulnerability_models: + for a in assets: + asset_requests[(v, a)] = [ + HazardDataRequest( + RiverineInundation, + 0, + 0, + indicator_id="", + scenario="", + year=2030, + ) + ] + + time_requests = time.time() - start + print(f"Time for requests dictionary creation {time_requests}s ") + start = time.time() + + for key in asset_requests: + if asset_requests[key][0].longitude != 0: + raise Exception() - time_assets = time.time() - start - print(f"Time for asset generation {time_assets}s ") - start = time.time() - # we key requests via model and assets; let's check dictionary look-up is fast enough - # (there are less simple alternatives) - - # create requests: - for v in vulnerability_models: - for a in assets: - asset_requests[(v, a)] = [ - HazardDataRequest( - RiverineInundation, - 0, - 0, - indicator_id="", - scenario="", - year=2030, - ) - ] - - time_requests = time.time() - start - print(f"Time for requests dictionary creation {time_requests}s ") - start = time.time() - # read requests: - for key in asset_requests: - if asset_requests[key][0].longitude != 0: - raise Exception() - - time_responses = time.time() - start - print(f"Time for response dictionary creation {time_responses}s ") + time_responses = time.time() - start + print(f"Time for response dictionary creation {time_responses}s ") diff --git a/tests/kernel/chronic_asset_impact_test.py b/tests/kernel/chronic_asset_impact_test.py index 1f5e74b6..299b602c 100644 --- a/tests/kernel/chronic_asset_impact_test.py +++ b/tests/kernel/chronic_asset_impact_test.py @@ -1,7 +1,7 @@ -import unittest from typing import Iterable, List, Union import numpy as np +import pytest from scipy.stats import norm from physrisk.data.pregenerated_hazard_model import ZarrHazardModel @@ -13,6 +13,7 @@ HazardParameterDataResponse, ) from physrisk.kernel.hazards import ChronicHeat +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType from physrisk.kernel.vulnerability_model import ( @@ -100,13 +101,10 @@ def get_impact( Returns: Probability distribution of impacts. """ - assert isinstance(asset, IndustrialActivity) baseline_dd_above_mean, scenario_dd_above_mean = data_responses - - # check expected type; can maybe do this more nicely + assert isinstance(asset, IndustrialActivity) assert isinstance(baseline_dd_above_mean, HazardParameterDataResponse) assert isinstance(scenario_dd_above_mean, HazardParameterDataResponse) - # Ensuring that the values are greater than zero. Should be by defition. assert scenario_dd_above_mean.parameter >= 0 assert baseline_dd_above_mean.parameter >= 0 @@ -119,11 +117,11 @@ def get_impact( ) hours_worked = self.total_labour_hours fraction_loss_mean = ( - delta_dd_above_mean * self.time_lost_per_degree_day - ) / hours_worked + delta_dd_above_mean * self.time_lost_per_degree_day / hours_worked + ) fraction_loss_std = ( - delta_dd_above_mean * self.time_lost_per_degree_day_se - ) / hours_worked + delta_dd_above_mean * self.time_lost_per_degree_day_se / hours_worked + ) return get_impact_distrib( fraction_loss_mean, @@ -184,70 +182,69 @@ def get_impact_distrib( return ImpactDistrib(hazard_type, impact_bins, probs, hazard_paths, impact_type) -class TestChronicAssetImpact(unittest.TestCase): - """Tests the impact on an asset of a chronic hazard model.""" +@pytest.fixture +def setup_hazard_model(): + store = mock_hazard_model_store_heat(TestData.longitudes, TestData.latitudes) + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + return hazard_model - def test_chronic_vulnerability_model(self): - """Testing the generation of an asset when only an impact curve (e.g. damage curve is available)""" - store = mock_hazard_model_store_heat(TestData.longitudes, TestData.latitudes) - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store - ) - # to run a live calculation, we omit the store parameter +def test_chronic_vulnerability_model(setup_hazard_model): + """Testing the generation of an asset when only an impact curve (e.g. damage curve is available)""" - scenario = "ssp585" - year = 2050 + hazard_model = setup_hazard_model + scenario = "ssp585" + year = 2050 - vulnerability_models = DictBasedVulnerabilityModels( - {IndustrialActivity: [ChronicHeatGZNModel()]} - ) + vulnerability_models = DictBasedVulnerabilityModels( + {IndustrialActivity: [ChronicHeatGZNModel()]} + ) - assets = [ - IndustrialActivity(lat, lon, type="Construction") - for lon, lat in zip(TestData.longitudes, TestData.latitudes) - ][:1] + assets = [ + IndustrialActivity(lat, lon, type="Construction") + for lon, lat in zip(TestData.longitudes, TestData.latitudes) + ][:1] - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) - value_test = list(results.values())[0][0].impact.mean_impact() - value_test = list(results.values())[0][0].impact.prob - value_exp = np.array( - [ - 0.02656777935, - 0.01152965908, - 0.01531928095, - 0.01983722513, - 0.02503479879, - 0.03079129430, - 0.03690901485, - 0.04311790414, - 0.04909118572, - 0.05447159590, - 0.51810304973, - 0.16109092806, - 0.00807680527, - 0.00005941883, - 0.00000005990, - 0.00000000001, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - ] - ) - value_diff = np.sum(np.abs(value_test - value_exp)) - self.assertAlmostEqual(value_diff, 0.0, places=8) + value_test = list(results.values())[0][0].impact.mean_impact() + value_test = list(results.values())[0][0].impact.prob + value_exp = np.array( + [ + 0.02656777935, + 0.01152965908, + 0.01531928095, + 0.01983722513, + 0.02503479879, + 0.03079129430, + 0.03690901485, + 0.04311790414, + 0.04909118572, + 0.05447159590, + 0.51810304973, + 0.16109092806, + 0.00807680527, + 0.00005941883, + 0.00000005990, + 0.00000000001, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + ] + ) + value_diff = np.sum(np.abs(value_test - value_exp)) + assert np.isclose(value_diff, 0.0, atol=1.0e-7) diff --git a/tests/kernel/curves_test.py b/tests/kernel/curves_test.py index bf488772..ee6d01d7 100644 --- a/tests/kernel/curves_test.py +++ b/tests/kernel/curves_test.py @@ -1,26 +1,19 @@ """Test asset impact calculations.""" -import unittest - import numpy as np +import pytest from physrisk.kernel.curve import ExceedanceCurve -class TestAssetImpact(unittest.TestCase): - """Tests asset impact calculations.""" - - def test_return_period_data(self): - return_periods = np.array( - [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - ) - depths = np.array([0.059, 0.33, 0.51, 0.71, 0.86, 1.00, 1.15, 1.16, 1.16]) - - # say we need to add an extra depth point because the damage below that point is zero - # extra_depth = 0.75 +@pytest.fixture +def curve_data(): + return_periods = np.array([2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + depths = np.array([0.059, 0.33, 0.51, 0.71, 0.86, 1.00, 1.15, 1.16, 1.16]) + exceed_probs = 1.0 / return_periods + return ExceedanceCurve(exceed_probs, depths) - exceed_probs = 1.0 / return_periods - curve = ExceedanceCurve(exceed_probs, depths) - curve = curve.add_value_point(0.75) - self.assertAlmostEqual(curve.probs[4], 0.03466667) +def test_return_period_data(curve_data): + curve = curve_data.add_value_point(0.75) + assert pytest.approx(curve.probs[4], rel=1e-6) == 0.03466667 diff --git a/tests/kernel/exposure_test.py b/tests/kernel/exposure_test.py index 72dad183..4a817018 100644 --- a/tests/kernel/exposure_test.py +++ b/tests/kernel/exposure_test.py @@ -1,13 +1,14 @@ -import json - import fsspec.implementations.local as local +import json import numpy as np +import pytest import physrisk.api.v1.common from physrisk.api.v1.exposure_req_resp import ( AssetExposureRequest, AssetExposureResponse, ) +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.container import ZarrHazardModelFactory from physrisk.data.inventory import EmbeddedInventory from physrisk.data.inventory_reader import InventoryReader @@ -30,98 +31,98 @@ ) from physrisk.requests import Requester -from ..base_test import TestWithCredentials from ..data.hazard_model_store_test import TestData, mock_hazard_model_store_path_curves -class TestExposureMeasures(TestWithCredentials): - def test_jupiter_exposure_service(self): - assets, store, hazard_model_factory, expected = self._get_components() - inventory = EmbeddedInventory() - requester = Requester( - hazard_model_factory=hazard_model_factory, - vulnerability_models_factory=None, - inventory=inventory, - inventory_reader=InventoryReader(fs=local.LocalFileSystem(), base_path=""), - reader=ZarrReader(store=store), - colormaps=inventory.colormaps(), - measures_factory=DefaultMeasuresFactory, - ) - assets_api = physrisk.api.v1.common.Assets( - items=[ - physrisk.api.v1.common.Asset( - asset_class="Asset", latitude=a.latitude, longitude=a.longitude - ) - for a in assets[0:1] - ] +@pytest.fixture +def get_components(): + # "precipitation/jupiter/v1/max_daily_water_equivalent_{scenario}_{year}" + paths = [ + "combined_flood/jupiter/v1/fraction_{scenario}_{year}", + "chronic_heat/jupiter/v1/days_above_35c_{scenario}_{year}", + "wind/jupiter/v1/max_1min_{scenario}_{year}", + "drought/jupiter/v1/months_spei3m_below_-2_{scenario}_{year}", + "hail/jupiter/v1/days_above_5cm_{scenario}_{year}", + "fire/jupiter/v1/fire_probability_{scenario}_{year}", + ] + + all_resources = EmbeddedInventory().resources + resources = [all_resources[p] for p in paths] + + values = [np.array([v]) for v in [0.02, 15, 100, 0.7, 0.1, 0.9]] + + expected = { + CombinedInundation: Category.LOW, + ChronicHeat: Category.MEDIUM, + Wind: Category.MEDIUM, + Drought: Category.HIGH, + Hail: Category.LOWEST, + Fire: Category.HIGHEST, + } + + def path_curves(): + return dict( + (r.path.format(scenario="ssp585", year=2030), v) + for (r, v) in zip(resources, values) ) - request = AssetExposureRequest(assets=assets_api, scenario="ssp585", year=2050) - response = requester.get( - request_id="get_asset_exposure", request_dict=request.model_dump() - ) - result = AssetExposureResponse(**json.loads(response)).items[0] - expected = dict((k.__name__, v) for (k, v) in expected.items()) - for key in result.exposures.keys(): - assert result.exposures[key].category == expected[key].name - - def test_jupiter_exposure(self): - assets, _, hazard_model_factory, expected = self._get_components() - asset = assets[0] - measure = JupterExposureMeasure() - - results = calculate_exposures( - [asset], - hazard_model_factory.hazard_model(), - measure, - scenario="ssp585", - year=2030, - ) - categories = results[asset].hazard_categories - for k, v in expected.items(): - assert categories[k][0] == v - - def _get_components(self): - # "precipitation/jupiter/v1/max_daily_water_equivalent_{scenario}_{year}" - paths = [ - "combined_flood/jupiter/v1/fraction_{scenario}_{year}", - "chronic_heat/jupiter/v1/days_above_35c_{scenario}_{year}", - "wind/jupiter/v1/max_1min_{scenario}_{year}", - "drought/jupiter/v1/months_spei3m_below_-2_{scenario}_{year}", - "hail/jupiter/v1/days_above_5cm_{scenario}_{year}", - "fire/jupiter/v1/fire_probability_{scenario}_{year}", - ] - - all_resources = EmbeddedInventory().resources - resources = [all_resources[p] for p in paths] - values = [np.array([v]) for v in [0.02, 15, 100, 0.7, 0.1, 0.9]] - - expected = { - CombinedInundation: Category.LOW, - ChronicHeat: Category.MEDIUM, - Wind: Category.MEDIUM, - Drought: Category.HIGH, - Hail: Category.LOWEST, - Fire: Category.HIGHEST, - } - - def path_curves(): - return dict( - (r.path.format(scenario="ssp585", year=2030), v) - for (r, v) in zip(resources, values) + assets = [ + Asset(lat, lon) for (lat, lon) in zip(TestData.latitudes, TestData.longitudes) + ] + + store = mock_hazard_model_store_path_curves( + TestData.longitudes, TestData.latitudes, path_curves() + ) + + hazard_model_factory = ZarrHazardModelFactory( + source_paths=get_default_source_paths(EmbeddedInventory()), store=store + ) + + return assets, store, hazard_model_factory, expected + + +def test_jupiter_exposure_service(get_components): + assets, store, hazard_model_factory, expected = get_components + inventory = EmbeddedInventory() + requester = Requester( + hazard_model_factory=hazard_model_factory, + vulnerability_models_factory=None, + inventory=inventory, + inventory_reader=InventoryReader(fs=local.LocalFileSystem(), base_path=""), + reader=ZarrReader(store=store), + colormaps=inventory.colormaps(), + measures_factory=DefaultMeasuresFactory, + ) + assets_api = physrisk.api.v1.common.Assets( + items=[ + physrisk.api.v1.common.Asset( + asset_class="Asset", latitude=a.latitude, longitude=a.longitude ) - - assets = [ - Asset(lat, lon) - for (lat, lon) in zip(TestData.latitudes, TestData.longitudes) + for a in assets[0:1] ] - - store = mock_hazard_model_store_path_curves( - TestData.longitudes, TestData.latitudes, path_curves() - ) - - hazard_model_factory = ZarrHazardModelFactory( - source_paths=get_default_source_paths(EmbeddedInventory()), store=store - ) - - return assets, store, hazard_model_factory, expected + ) + request = AssetExposureRequest(assets=assets_api, scenario="ssp585", year=2050) + response = requester.get( + request_id="get_asset_exposure", request_dict=request.model_dump() + ) + result = AssetExposureResponse(**json.loads(response)).items[0] + expected = dict((k.__name__, v) for (k, v) in expected.items()) + for key in result.exposures.keys(): + assert result.exposures[key].category == expected[key].name + + +def test_jupiter_exposure(get_components): + assets, _, hazard_model_factory, expected = get_components + asset = assets[0] + measure = JupterExposureMeasure() + + results = calculate_exposures( + [asset], + hazard_model_factory.hazard_model(), + measure, + scenario="ssp585", + year=2030, + ) + categories = results[asset].hazard_categories + for k, v in expected.items(): + assert categories[k][0] == v diff --git a/tests/kernel/financial_model_test.py b/tests/kernel/financial_model_test.py index e06a3a49..601af25c 100644 --- a/tests/kernel/financial_model_test.py +++ b/tests/kernel/financial_model_test.py @@ -1,7 +1,7 @@ -import unittest from datetime import datetime import numpy as np +import pytest from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.hazard_models.core_hazards import get_default_source_paths @@ -22,56 +22,53 @@ def get_asset_aggregate_cashflows( return 1000 -class TestAssetImpact(unittest.TestCase): - """Tests asset impact calculations.""" - - def test_financial_model(self): - curve = np.array( - [ - 0.059601218, - 0.33267087, - 0.50511575, - 0.71471703, - 0.8641244, - 1.0032823, - 1.1491022, - 1.1634114, - 1.1634114, - ] - ) - store = mock_hazard_model_store_inundation( - TestData.longitudes, TestData.latitudes, curve - ) - - # we need to define - # 1) The hazard models - # 2) The vulnerability models - # 3) The financial models - - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store - ) +@pytest.fixture +def financial_model_fixture(): + curve = np.array( + [ + 0.059601218, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, + ] + ) + store = mock_hazard_model_store_inundation( + TestData.longitudes, TestData.latitudes, curve + ) - model = LossModel(hazard_model=hazard_model) + # we need to define + # 1) The hazard models + # 2) The vulnerability models + # 3) The financial models + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + model = LossModel(hazard_model=hazard_model) + data_provider = MockFinancialDataProvider() + financial_model = FinancialModel(data_provider) + return model, financial_model - data_provider = MockFinancialDataProvider() - financial_model = FinancialModel(data_provider) - assets = [ - PowerGeneratingAsset(lat, lon) - for lon, lat in zip(TestData.longitudes, TestData.latitudes) - ] - measures = model.get_financial_impacts( - assets, - financial_model=financial_model, - scenario="ssp585", - year=2080, - sims=100000, - ) +def test_financial_model(financial_model_fixture): + model, financial_model = financial_model_fixture + assets = [ + PowerGeneratingAsset(lat, lon) + for lon, lat in zip(TestData.longitudes, TestData.latitudes) + ] + measures = model.get_financial_impacts( + assets, + financial_model=financial_model, + scenario="ssp585", + year=2080, + sims=100000, + ) - np.testing.assert_array_almost_equal_nulp( - measures["RiverineInundation"]["percentile_values"], - np.array( - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0, 2000.0] - ), - ) + np.testing.assert_array_almost_equal_nulp( + measures["RiverineInundation"]["percentile_values"], + np.array( + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0, 2000.0] + ), + ) diff --git a/tests/kernel/hazard_models_test.py b/tests/kernel/hazard_models_test.py index 7950b9a1..69abe725 100644 --- a/tests/kernel/hazard_models_test.py +++ b/tests/kernel/hazard_models_test.py @@ -3,7 +3,6 @@ import numpy as np -import tests.data.hazard_model_store_test as hms from physrisk.kernel.assets import RealEstateAsset from physrisk.kernel.hazard_model import ( HazardDataRequest, @@ -13,10 +12,13 @@ HazardParameterDataResponse, ) from physrisk.kernel.hazards import ChronicHeat, Wind +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels from physrisk.vulnerability_models.real_estate_models import GenericTropicalCycloneModel +from ..data.hazard_model_store_test import TestData + @dataclass class SinglePointData: @@ -90,15 +92,15 @@ def test_using_point_based_hazard_model(): year = 2080 assets = [ RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") - for lon, lat in zip(hms.TestData.longitudes[0:1], hms.TestData.latitudes[0:1]) + for lon, lat in zip(TestData.longitudes[0:1], TestData.latitudes[0:1]) ] # fmt: off wind_return_periods = np.array([10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0]) # noqa wind_intensities = np.array([37.279999, 44.756248, 48.712502, 51.685001, 53.520000, 55.230000, 56.302502, 57.336250, 58.452499, 59.283749, 63.312500, 65.482498, 66.352501, 67.220001, 67.767502, 68.117500, 68.372498, 69.127502, 70.897499 ]) # noqa # fmt: on point = SinglePointData( - hms.TestData.latitudes[0], - hms.TestData.longitudes[0], + TestData.latitudes[0], + TestData.longitudes[0], scenario=scenario, year=year, wind_return_periods=wind_return_periods, diff --git a/tests/kernel/image_creation_test.py b/tests/kernel/image_creation_test.py index 975bea9c..481ffedb 100644 --- a/tests/kernel/image_creation_test.py +++ b/tests/kernel/image_creation_test.py @@ -1,7 +1,6 @@ import os -import unittest - import numpy as np +import pytest import zarr import zarr.storage @@ -9,50 +8,45 @@ from physrisk.data.image_creator import ImageCreator from physrisk.data.zarr_reader import ZarrReader -from ..base_test import TestWithCredentials - - -class TestImageCreation(TestWithCredentials): - def test_image_creation(self): - path = "test_array" - store = zarr.storage.MemoryStore(root="hazard.zarr") - root = zarr.open(store=store, mode="w") - - im = np.array([[1.2, 0.8], [0.5, 0.4]]) - z = root.create_dataset( # type: ignore - path, - shape=(1, im.shape[0], im.shape[1]), - chunks=(1, im.shape[0], im.shape[1]), - dtype="f4", - ) - z[0, :, :] = im - converter = ImageCreator(reader=ZarrReader(store)) - colormap = colormap_provider.colormap("test") - - def get_colors(index: int): - return colormap[str(index)] - - result = converter._to_rgba(im, get_colors) - # Max should be 255, min should be 1. Other values span the 253 elements from 2 to 254. - expected = np.array( - [ - [255, 2 + (0.8 - 0.4) * 253 / (1.2 - 0.4)], - [2 + (0.5 - 0.4) * 253 / (1.2 - 0.4), 1], - ] - ) - converter.convert( - path, colormap="test" - ) # check no error running through mocked example. - np.testing.assert_equal(result, expected.astype(np.uint8)) - - @unittest.skip("just example") - def test_write_file(self): - # show how to create image from zarr array - # useful for testing image generation - test_output_dir = "{set me}" - test_path = "wildfire/jupiter/v1/wildfire_probability_ssp585_2050_map" - store = zarr.DirectoryStore( - os.path.join(test_output_dir, "hazard_test", "hazard.zarr") - ) - creator = ImageCreator(ZarrReader(store)) - creator.to_file(os.path.join(test_output_dir, "test.png"), test_path) + +def test_image_creation(): + path = "test_array" + store = zarr.storage.MemoryStore(root="hazard.zarr") + root = zarr.open(store=store, mode="w") + + im = np.array([[1.2, 0.8], [0.5, 0.4]]) + z = root.create_dataset( # type: ignore + path, + shape=(1, im.shape[0], im.shape[1]), + chunks=(1, im.shape[0], im.shape[1]), + dtype="f4", + ) + z[0, :, :] = im + converter = ImageCreator(reader=ZarrReader(store)) + colormap = colormap_provider.colormap("test") + + def get_colors(index: int): + return colormap[str(index)] + + result = converter._to_rgba(im, get_colors) + # Max should be 255, min should be 1. Other values span the 253 elements from 2 to 254. + expected = np.array( + [ + [255, 2 + (0.8 - 0.4) * 253 / (1.2 - 0.4)], + [2 + (0.5 - 0.4) * 253 / (1.2 - 0.4), 1], + ] + ) + converter.convert( + path, colormap="test" + ) # check no error running through mocked example. + np.testing.assert_equal(result, expected.astype(np.uint8)) + + +@pytest.mark.skip(reason="just example") +def test_write_file(test_dir): + # show how to create image from zarr array + # useful for testing image generation + test_path = "wildfire/jupiter/v1/wildfire_probability_ssp585_2050_map" + store = zarr.DirectoryStore(os.path.join(test_dir, "hazard_test", "hazard.zarr")) + creator = ImageCreator(ZarrReader(store)) + creator.to_file(os.path.join(test_dir, "test.png"), test_path) diff --git a/tests/models/example_models_test.py b/tests/models/example_models_test.py index 3026865c..eb492b3e 100644 --- a/tests/models/example_models_test.py +++ b/tests/models/example_models_test.py @@ -1,5 +1,3 @@ -import unittest - import numpy as np from scipy import stats @@ -8,6 +6,7 @@ from physrisk.kernel.assets import Asset, RealEstateAsset from physrisk.kernel.hazard_model import HazardEventDataResponse from physrisk.kernel.hazards import Inundation, RiverineInundation +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.impact_distrib import ImpactType from physrisk.kernel.vulnerability_matrix_provider import VulnMatrixProvider @@ -18,10 +17,7 @@ from physrisk.vulnerability_models.example_models import ( ExampleCdfBasedVulnerabilityModel, ) -from tests.data.hazard_model_store_test import ( - TestData, - mock_hazard_model_store_inundation, -) +from ..data.hazard_model_store_test import TestData, mock_hazard_model_store_inundation class ExampleRealEstateInundationModel(VulnerabilityModel): @@ -75,76 +71,87 @@ def beta_distrib(mean, std): return lambda x, a=a, b=b: stats.beta.cdf(x, a, b) -class TestExampleModels(unittest.TestCase): - def test_pdf_based_vulnerability_model(self): - model = ExampleCdfBasedVulnerabilityModel( - indicator_id="", hazard_type=Inundation - ) - - latitude, longitude = 45.268405, 19.885738 - asset = Asset(latitude, longitude) - - return_periods = np.array( - [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - ) - intensities = np.array( - [ - 0.059601218, - 0.33267087, - 0.50511575, - 0.71471703, - 0.8641244, - 1.0032823, - 1.1491022, - 1.1634114, - 1.1634114, - ] - ) - - mock_response = HazardEventDataResponse(return_periods, intensities) - - vul, event = model.get_distributions(asset, [mock_response]) - - def test_user_supplied_model(self): - curve = np.array( - [ - 0.059601218, - 0.33267087, - 0.50511575, - 0.71471703, - 0.8641244, - 1.0032823, - 1.1491022, - 1.1634114, - 1.1634114, - ] - ) - store = mock_hazard_model_store_inundation( - TestData.longitudes, TestData.latitudes, curve - ) - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store - ) - - scenario = "rcp8p5" - year = 2080 - - vulnerability_models = DictBasedVulnerabilityModels( - {RealEstateAsset: [ExampleRealEstateInundationModel()]} - ) - - assets = [ - RealEstateAsset(lat, lon, location="Asia", type="Building/Industrial") - for lon, lat in zip(TestData.longitudes, TestData.latitudes) +def test_cdf_based_vulnerability_model(): + model = ExampleCdfBasedVulnerabilityModel(indicator_id="", hazard_type=Inundation) + + latitude, longitude = 45.268405, 19.885738 + asset = Asset(latitude, longitude) + + return_periods = np.array([2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + intensities = np.array( + [ + 0.059601218, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, ] - - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) - - self.assertAlmostEqual( - results[assets[0], RiverineInundation, scenario, year][0] - .impact.to_exceedance_curve() - .probs[0], - 0.499, - ) + ) + + mock_response = HazardEventDataResponse(return_periods, intensities) + + vul, event = model.get_distributions(asset, [mock_response]) + + value_exp = np.array( + [ + 0.05960122, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, + ] + ) + + value_diff = np.sum(np.abs(vul.intensity_bins - value_exp)) + assert np.isclose(value_diff, 0.0, atol=1.0e-7) + + +def test_user_supplied_model(): + curve = np.array( + [ + 0.059601218, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, + ] + ) + store = mock_hazard_model_store_inundation( + TestData.longitudes, TestData.latitudes, curve + ) + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + + scenario = "rcp8p5" + year = 2080 + + vulnerability_models = DictBasedVulnerabilityModels( + {RealEstateAsset: [ExampleRealEstateInundationModel()]} + ) + + assets = [ + RealEstateAsset(lat, lon, location="Asia", type="Building/Industrial") + for lon, lat in zip(TestData.longitudes, TestData.latitudes) + ] + + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) + + assert np.isclose( + results[assets[0], RiverineInundation, scenario, year][0] + .impact.to_exceedance_curve() + .probs[0], + 0.499, + ) diff --git a/tests/models/hazard_selection_test.py b/tests/models/hazard_selection_test.py index 3d984947..a07d03ec 100644 --- a/tests/models/hazard_selection_test.py +++ b/tests/models/hazard_selection_test.py @@ -28,13 +28,13 @@ def test_tudelft_selection(): source_paths[RiverineInundation]( indicator_id="flood_depth", scenario="rcp8p5", year=2050 ) - == "inundation/river_tudelft/v2/flood_depth_unprot_rcp8p5_2050" + == "inundation/river_tudelft/v1/flood_depth_rcp8p5_2050" ) assert ( source_paths[RiverineInundation]( indicator_id="flood_depth", scenario="historical", year=-1 ) - == "inundation/river_tudelft/v2/flood_depth_unprot_historical_1985" + == "inundation/river_tudelft/v1/flood_depth_historical_1971" ) diff --git a/tests/models/power_generating_asset_models_test.py b/tests/models/power_generating_asset_models_test.py index 5478686c..a059eb86 100644 --- a/tests/models/power_generating_asset_models_test.py +++ b/tests/models/power_generating_asset_models_test.py @@ -1,16 +1,26 @@ """Test asset impact calculations.""" import os -import unittest from typing import List import numpy as np +import pandas as pd +import pytest import physrisk.api.v1.common import physrisk.data.static.world as wd -from physrisk.kernel import Asset, PowerGeneratingAsset, calculation +from physrisk.data.inventory import EmbeddedInventory +from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.data.zarr_reader import ZarrReader +from physrisk.hazard_models.core_hazards import ( + CoreFloodModels, + CoreInventorySourcePaths, +) +from physrisk.kernel import calculation from physrisk.kernel.assets import ( + Asset, IndustrialActivity, + PowerGeneratingAsset, RealEstateAsset, ThermalPowerGeneratingAsset, ) @@ -18,244 +28,819 @@ from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.impact_distrib import EmptyImpactDistrib from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels -from physrisk.utils.lazy import lazy_import from physrisk.vulnerability_models.power_generating_asset_models import InundationModel -from tests.base_test import TestWithCredentials +from physrisk.vulnerability_models.thermal_power_generation_models import ( + ThermalPowerGenerationAirTemperatureModel, + ThermalPowerGenerationAqueductWaterRiskModel, + ThermalPowerGenerationCoastalInundationModel, + ThermalPowerGenerationDroughtModel, + ThermalPowerGenerationHighFireModel, + ThermalPowerGenerationLandslideModel, + ThermalPowerGenerationRiverineInundationModel, + ThermalPowerGenerationSevereConvectiveWindstormModel, + ThermalPowerGenerationSubsidenceModel, + ThermalPowerGenerationWaterStressModel, + ThermalPowerGenerationWaterTemperatureModel, +) -pd = lazy_import("pandas") +@pytest.fixture +def setup_data(wri_power_plant_assets): + """Fixture to set up the test data.""" + asset_list = wri_power_plant_assets -class TestPowerGeneratingAssetModels(TestWithCredentials): - """Tests World Resource Institute (WRI) models for power generating assets.""" + filtered = asset_list.loc[ + asset_list["primary_fuel"].isin(["Coal", "Gas", "Nuclear", "Oil"]) + ] + filtered = filtered[filtered["latitude"] > -60] - def test_inundation(self): - # exceedance curve - return_periods = np.array( - [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - ) - base_depth = np.array( - [ - 0.0, - 0.22372675, - 0.3654859, - 0.5393629, - 0.6642473, - 0.78564394, - 0.9406518, - 1.0539534, - 1.1634114, - ] + longitudes = np.array(filtered["longitude"]) + latitudes = np.array(filtered["latitude"]) + primary_fuels = np.array( + [ + primary_fuel.replace(" and ", "And").replace(" ", "") + for primary_fuel in filtered["primary_fuel"] + ] + ) + capacities = np.array(filtered["capacity_mw"]) + asset_names = np.array(filtered["name"]) + + countries, continents = wd.get_countries_and_continents( + latitudes=latitudes, longitudes=longitudes + ) + + assets = [ + ThermalPowerGeneratingAsset( + latitude, + longitude, + type=primary_fuel, + location=continent, + capacity=capacity, ) - future_depth = np.array( - [ - 0.059601218, - 0.33267087, - 0.50511575, - 0.71471703, - 0.8641244, - 1.0032823, - 1.1491022, - 1.1634114, - 1.1634114, - ] + for latitude, longitude, capacity, primary_fuel, continent in zip( + latitudes, + longitudes, + capacities, + primary_fuels, + continents, ) + ] - # we mock the response of the data request - responses_mock = [ - HazardEventDataResponse(return_periods, base_depth), - HazardEventDataResponse(return_periods, future_depth), + for i, asset_name in enumerate(asset_names): + assets[i].__dict__.update({"asset_name": asset_name}) + + return assets + + +@pytest.fixture +def setup_assets_extra(wri_power_plant_assets): + asset_list = wri_power_plant_assets + filtered = asset_list.loc[ + asset_list["primary_fuel"].isin(["Coal", "Gas", "Nuclear", "Oil"]) + ] + filtered = filtered[-60 < filtered["latitude"]] + + longitudes = np.array(filtered["longitude"]) + latitudes = np.array(filtered["latitude"]) + primary_fuels = np.array( + [ + primary_fuel.replace(" and ", "And").replace(" ", "") + for primary_fuel in filtered["primary_fuel"] ] + ) + capacities = np.array(filtered["capacity_mw"]) + + countries, continents = wd.get_countries_and_continents( + latitudes=latitudes, longitudes=longitudes + ) - latitude, longitude = 45.268405, 19.885738 - assets = [Asset(latitude, longitude)] - model = InundationModel(assets) + assets = [ + ThermalPowerGeneratingAsset( + latitude, + longitude, + type=primary_fuel, + location=country, + capacity=capacity, + ) + for latitude, longitude, capacity, primary_fuel, country in zip( + latitudes, + longitudes, + capacities, + primary_fuels, + countries, + ) + if country in ["Spain"] + ] + + return assets - impact, _, _ = model.get_impact_details(assets[0], responses_mock) - mean = impact.mean_impact() - self.assertAlmostEqual(mean, 4.8453897 / 365.0) +@pytest.fixture +def hazard_indicator_dict(): + """Fixture for hazard indicator dictionary.""" + return { + "AirTemperature": "days_tas_above", + "CoastalInundation": "flood_depth", + "RiverineInundation": "flood_depth", + "Drought": "months_spei3m_below_minus2", + "WaterStress": "water_stress_and_water_supply", + "WaterTemperature": "weeks_water_temp_above", + } - @unittest.skip("example, not test") - def test_create_synthetic_portfolios_and_test(self): - # cache_folder = r"" - cache_folder = r"/users/joemoorhouse/code/data" +@pytest.fixture +def vulnerability_models_dict(): + """Fixture for vulnerability models dictionary.""" + return { + "historical_1980": [ + ThermalPowerGenerationRiverineInundationModel(), + ThermalPowerGenerationCoastalInundationModel(), + ], + "historical_2005": [ThermalPowerGenerationAirTemperatureModel()], + "ssp126_2030": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ], + "ssp126_2040": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ], + "ssp126_2050": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ], + "ssp126_2060": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp126_2070": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp126_2080": [ + ThermalPowerGenerationWaterTemperatureModel(), + ], + "ssp126_2090": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp245_2030": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "ssp245_2040": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ], + "ssp245_2050": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ThermalPowerGenerationRiverineInundationModel(), + ThermalPowerGenerationCoastalInundationModel(), + ], + "ssp245_2060": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp245_2070": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp245_2080": [ + ThermalPowerGenerationWaterTemperatureModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "ssp245_2090": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp585_2005": [ThermalPowerGenerationDroughtModel()], + "ssp585_2030": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationDroughtModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "ssp585_2040": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationDroughtModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ], + "ssp585_2050": [ + ThermalPowerGenerationAirTemperatureModel(), + ThermalPowerGenerationDroughtModel(), + ThermalPowerGenerationWaterTemperatureModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "ssp585_2060": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp585_2070": [ThermalPowerGenerationWaterTemperatureModel()], + "ssp585_2080": [ + ThermalPowerGenerationWaterTemperatureModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ThermalPowerGenerationDroughtModel(), + ], + "ssp585_2090": [ThermalPowerGenerationWaterTemperatureModel()], + } - asset_list = pd.read_csv(os.path.join(cache_folder, "wri-all.csv")) - # types = asset_list["primary_fuel"].unique() - # interesting = [3, 8, 13, 14, 22, 25, 27, 28, 33, 40, 51, 64, 65, 66, 71, 72, 80, 88, 92, 109] - filtered = asset_list[0:1000] +@pytest.fixture +def vulnerability_models_dict_water_stress(): + """Fixture for vulnerability models dictionary.""" + return { + "historical_1999": [ThermalPowerGenerationWaterStressModel()], + "ssp126_2030": [ + ThermalPowerGenerationWaterStressModel(), + ], + "ssp126_2050": [ + ThermalPowerGenerationWaterStressModel(), + ], + "ssp126_2080": [ + ThermalPowerGenerationWaterStressModel(), + ], + "ssp370_2030": [ThermalPowerGenerationWaterStressModel()], + "ssp370_2050": [ThermalPowerGenerationWaterStressModel()], + "ssp370_2080": [ThermalPowerGenerationWaterStressModel()], + "ssp585_2030": [ + ThermalPowerGenerationWaterStressModel(), + ], + "ssp585_2050": [ + ThermalPowerGenerationWaterStressModel(), + ], + "ssp585_2080": [ + ThermalPowerGenerationWaterStressModel(), + ], + } - longitudes = np.array(filtered["longitude"]) - latitudes = np.array(filtered["latitude"]) - primary_fuel = np.array(filtered["primary_fuel"]) - generation = np.array(filtered["estimated_generation_gwh"]) - _, continents = wd.get_countries_and_continents( - latitudes=latitudes, longitudes=longitudes +@pytest.fixture +def vul_models_dict_extra(): + return { + "historical_1971": [ + ThermalPowerGenerationHighFireModel(), + ThermalPowerGenerationSevereConvectiveWindstormModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "historical_1980": [ + ThermalPowerGenerationLandslideModel(), + ThermalPowerGenerationSubsidenceModel(), + ], + "ssp126_2030": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp126_2050": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp126_2080": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp370_2030": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp370_2050": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp370_2080": [ThermalPowerGenerationAqueductWaterRiskModel()], + "rcp45_2050": [ + ThermalPowerGenerationHighFireModel(), + ThermalPowerGenerationSevereConvectiveWindstormModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "rcp45_2070": [ + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "rcp45_2100": [ + ThermalPowerGenerationHighFireModel(), + ThermalPowerGenerationSevereConvectiveWindstormModel(), + ], + "ssp585_2030": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp585_2050": [ThermalPowerGenerationAqueductWaterRiskModel()], + "ssp585_2080": [ThermalPowerGenerationAqueductWaterRiskModel()], + "rcp85_2050": [ + ThermalPowerGenerationHighFireModel(), + ThermalPowerGenerationSevereConvectiveWindstormModel(), + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "rcp85_2070": [ + ThermalPowerGenerationCoastalInundationModel(), + ThermalPowerGenerationRiverineInundationModel(), + ], + "rcp85_2100": [ + ThermalPowerGenerationHighFireModel(), + ThermalPowerGenerationSevereConvectiveWindstormModel(), + ], + } + + +def api_assets(assets: List[Asset]): + items = [ + physrisk.api.v1.common.Asset( + asset_class=type(a).__name__, + type=getattr(a, "type", None), + location=getattr(a, "location", None), + latitude=a.latitude, + longitude=a.longitude, ) + for a in assets + ] + return physrisk.api.v1.common.Assets(items=items) - # Power generating assets that are of interest - assets = [ - PowerGeneratingAsset( - lat, lon, generation=gen, location=continent, type=prim_fuel - ) - for lon, lat, gen, prim_fuel, continent in zip( - longitudes, latitudes, generation, primary_fuel, continents - ) + +def test_inundation(): + # exceedance curve + return_periods = np.array([2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]) + base_depth = np.array( + [ + 0.0, + 0.22372675, + 0.3654859, + 0.5393629, + 0.6642473, + 0.78564394, + 0.9406518, + 1.0539534, + 1.1634114, ] - detailed_results = calculate_impacts(assets, scenario="ssp585", year=2030) - keys = list(detailed_results.keys()) - # detailed_results[keys[0]].impact.to_exceedance_curve() - means = np.array([detailed_results[key].impact.mean_impact() for key in keys]) - interesting = [k for (k, m) in zip(keys, means) if m > 0] - assets_out = self.api_assets(item[0] for item in interesting[0:10]) - with open( - os.path.join(cache_folder, "assets_example_power_generating_small.json"), - "w", - ) as f: - f.write(assets_out.model_dump_json(indent=4)) - - # Synthetic portfolio; industrial activity at different locations - assets = [ - IndustrialActivity(lat, lon, type="Construction", location=continent) - for lon, lat, continent in zip(longitudes, latitudes, continents) + ) + future_depth = np.array( + [ + 0.059601218, + 0.33267087, + 0.50511575, + 0.71471703, + 0.8641244, + 1.0032823, + 1.1491022, + 1.1634114, + 1.1634114, ] - assets = [assets[i] for i in [0, 100, 200, 300, 400, 500, 600, 700, 800, 900]] - detailed_results = calculate_impacts(assets, scenario="ssp585", year=2030) - keys = list(detailed_results.keys()) - means = np.array( - [ - detailed_results[key][0].impact.mean_impact() - for key in detailed_results.keys() - ] + ) + + # Mock the response of the data request + responses_mock = [ + HazardEventDataResponse(return_periods, base_depth), + HazardEventDataResponse(return_periods, future_depth), + ] + + latitude, longitude = 45.268405, 19.885738 + assets = [Asset(latitude, longitude)] + model = InundationModel(assets) + + impact, _, _ = model.get_impact_details(assets[0], responses_mock) + mean = impact.mean_impact() + + assert np.isclose(mean, 4.8453897 / 365.0) + + +@pytest.mark.skip(reason="Requires credentials.") +def test_create_synthetic_portfolios_and_test( + wri_power_plant_assets, test_dir, load_credentials +): + asset_list = wri_power_plant_assets + filtered = asset_list[0:1000] + + longitudes = np.array(filtered["longitude"]) + latitudes = np.array(filtered["latitude"]) + primary_fuel = np.array(filtered["primary_fuel"]) + generation = np.array(filtered["estimated_generation_gwh_2017"]) + + _, continents = wd.get_countries_and_continents( + latitudes=latitudes, longitudes=longitudes + ) + + hazard_model = calculation.get_default_hazard_model() + vulnerability_models = DictBasedVulnerabilityModels( + calculation.get_default_vulnerability_models() + ) + + # Power generating assets that are of interest + assets = [ + PowerGeneratingAsset( + lat, lon, generation=gen, location=continent, type=prim_fuel ) - interesting = [k for (k, m) in zip(keys, means) if m > 0] - assets_out = self.api_assets(item[0] for item in interesting[0:10]) - with open( - os.path.join(cache_folder, "assets_example_industrial_activity_small.json"), - "w", - ) as f: - f.write(assets_out.model_dump_json(indent=4)) - - # Synthetic portfolio; real estate assets at different locations - assets = [ - RealEstateAsset(lat, lon, location=continent, type="Buildings/Industrial") - for lon, lat, continent in zip(longitudes, latitudes, continents) - if isinstance(continent, str) and continent != "Oceania" - ] - detailed_results = calculate_impacts(assets, scenario="ssp585", year=2030) - keys = list(detailed_results.keys()) - means = np.array( - [ - detailed_results[key][0].impact.mean_impact() - for key in detailed_results.keys() - ] + for lon, lat, gen, prim_fuel, continent in zip( + longitudes, latitudes, generation, primary_fuel, continents ) - interesting = [k for (k, m) in zip(keys, means) if m > 0] - assets_out = self.api_assets(item[0] for item in interesting[0:10]) - with open( - os.path.join(cache_folder, "assets_example_real_estate_small.json"), "w" - ) as f: - f.write(assets_out.model_dump_json(indent=4)) - self.assertAlmostEqual(1, 1) - - @unittest.skip("example, not test") - def test_thermal_power_generation_portfolio(self): - cache_folder = os.environ.get("CREDENTIAL_DOTENV_DIR", os.getcwd()) - - asset_list = pd.read_csv(os.path.join(cache_folder, "wri-all.csv")) - filtered = asset_list.loc[ - asset_list["primary_fuel"].isin(["Coal", "Gas", "Nuclear", "Oil"]) + ] + detailed_results = calculate_impacts( + assets, + hazard_model=hazard_model, + vulnerability_models=vulnerability_models, + scenario="ssp585", + year=2030, + ) + keys = list(detailed_results.keys()) + means = np.array([detailed_results[key][0].impact.mean_impact() for key in keys]) + interesting = [k for (k, m) in zip(keys, means) if m > 0] + assets_out = api_assets(item[0] for item in interesting[0:10]) + with open( + os.path.join(test_dir, "assets_example_power_generating_small.json"), "w" + ) as f: + f.write(assets_out.model_dump_json(indent=4)) + + # Synthetic portfolio; industrial activity at different locations + assets = [ + IndustrialActivity(lat, lon, type="Construction", location=continent) + for lon, lat, continent in zip(longitudes, latitudes, continents) + ] + assets = [assets[i] for i in [0, 100, 200, 300, 400, 500, 600, 700, 800, 900]] + detailed_results = calculate_impacts( + assets, + hazard_model=hazard_model, + vulnerability_models=vulnerability_models, + scenario="ssp585", + year=2030, + ) + keys = list(detailed_results.keys()) + means = np.array( + [ + detailed_results[key][0].impact.mean_impact() + for key in detailed_results.keys() + ] + ) + interesting = [k for (k, m) in zip(keys, means) if m > 0] + assets_out = api_assets(item[0] for item in interesting[0:10]) + with open( + os.path.join(test_dir, "assets_example_industrial_activity_small.json"), "w" + ) as f: + f.write(assets_out.model_dump_json(indent=4)) + + # Synthetic portfolio; real estate assets at different locations + assets = [ + RealEstateAsset(lat, lon, location=continent, type="Buildings/Industrial") + for lon, lat, continent in zip(longitudes, latitudes, continents) + if isinstance(continent, str) and continent != "Oceania" + ] + detailed_results = calculate_impacts( + assets, + hazard_model=hazard_model, + vulnerability_models=vulnerability_models, + scenario="ssp585", + year=2030, + ) + keys = list(detailed_results.keys()) + means = np.array( + [ + detailed_results[key][0].impact.mean_impact() + if detailed_results[key] + else None + for key in detailed_results.keys() ] - filtered = filtered[-60 < filtered["latitude"]] + ) + interesting = [k for (k, m) in zip(keys, means) if (m and m > 0)] + assets_out = api_assets(item[0] for item in interesting[0:10]) + with open( + os.path.join(test_dir, "assets_example_real_estate_small.json"), "w" + ) as f: + f.write(assets_out.model_dump_json(indent=4)) + assert len(os.listdir(test_dir)) == 3 + + +@pytest.mark.skip(reason="Requires credentials.") +def test_thermal_power_generation_portfolio( + wri_power_plant_assets, test_dir, load_credentials +): + asset_list = wri_power_plant_assets + + filtered = asset_list.loc[ + asset_list["primary_fuel"].isin(["Coal", "Gas", "Nuclear", "Oil"]) + ] + filtered = filtered[-60 < filtered["latitude"]] + + longitudes = np.array(filtered["longitude"]) + latitudes = np.array(filtered["latitude"]) + + primary_fuels = np.array( + [ + primary_fuel.replace(" and ", "And").replace(" ", "") + for primary_fuel in filtered["primary_fuel"] + ] + ) + + # Capacity describes a maximum electric power rate. + # Generation describes the actual electricity output of the plant over a period of time. + capacities = np.array(filtered["capacity_mw"]) + + _, continents = wd.get_countries_and_continents( + latitudes=latitudes, longitudes=longitudes + ) + + # Power generating assets that are of interest + assets = [ + ThermalPowerGeneratingAsset( + latitude, + longitude, + type=primary_fuel, + location=continent, + capacity=capacity, + ) + for latitude, longitude, capacity, primary_fuel, continent in zip( + latitudes, + longitudes, + capacities, + primary_fuels, + continents, + ) + ] + + scenario = "ssp585" + year = 2030 - longitudes = np.array(filtered["longitude"]) - latitudes = np.array(filtered["latitude"]) + hazard_model = calculation.get_default_hazard_model() + vulnerability_models = DictBasedVulnerabilityModels( + calculation.get_default_vulnerability_models() + ) - primary_fuels = np.array( - [ - primary_fuel.replace(" and ", "And").replace(" ", "") - for primary_fuel in filtered["primary_fuel"] - ] + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) + out = [ + { + "asset": type(result.asset).__name__, + "type": getattr(result.asset, "type", None), + "capacity": getattr(result.asset, "capacity", None), + "location": getattr(result.asset, "location", None), + "latitude": result.asset.latitude, + "longitude": result.asset.longitude, + "impact_mean": ( + None + if isinstance(results[key][0].impact, EmptyImpactDistrib) + else results[key][0].impact.mean_impact() + ), + "hazard_type": key.hazard_type.__name__, + } + for result, key in zip(results, results.keys()) + ] + pd.DataFrame.from_dict(out).to_csv( + os.path.join( + test_dir, f"thermal_power_generation_example_{scenario}_{year}.csv" ) + ) + assert len(out) == 53052 + assert len(os.listdir(test_dir)) == 1 + - # Capacity describes a maximum electric power rate. - # Generation describes the actual electricity output of the plant over a period of time. - capacities = np.array(filtered["capacity_mw"]) +@pytest.mark.skip(reason="Requires credentials.") +def test_thermal_power_generation_impacts( + setup_data, hazard_indicator_dict, vulnerability_models_dict, load_credentials +): + assets = setup_data + hazard_model = calculation.get_default_hazard_model() + out = [] + empty_impact_count = 0 + asset_subtype_none_count = 0 + empty_impact_scenarios = [] + asset_subtype_none_assets = [] + exception_scenarios = [] + for scenario_year, vulnerability_models in vulnerability_models_dict.items(): + scenario, year = scenario_year.split("_") - _, continents = wd.get_countries_and_continents( - latitudes=latitudes, longitudes=longitudes + vulnerability_models = DictBasedVulnerabilityModels( + {ThermalPowerGeneratingAsset: vulnerability_models} ) - # Power generating assets that are of interest - assets = [ - ThermalPowerGeneratingAsset( - latitude, - longitude, - type=primary_fuel, - location=continent, - capacity=capacity, + try: + results = calculate_impacts( + assets, + hazard_model, + vulnerability_models, + scenario=scenario, + year=int(year), ) - for latitude, longitude, capacity, primary_fuel, continent in zip( - latitudes, - longitudes, - capacities, - primary_fuels, - continents, + except Exception as e: + exception_scenarios.append((scenario, year, str(e))) + continue + + for result, key in zip(results, results.keys()): + impact = results[key][0].impact + if isinstance(impact, EmptyImpactDistrib): + impact_mean = None + hazard_type = None + impact_distr_bin_edges = "0;0" + impact_distr_p = "0;0" + empty_impact_count += 1 + empty_impact_scenarios.append((scenario, year, result.asset.asset_name)) + else: + impact_mean = impact.mean_impact() + impact_distr_bin_edges = ";".join(impact.impact_bins.astype(str)) + impact_distr_p = ";".join(impact.prob.astype(str)) + hazard_type = ( + impact.hazard_type.__name__ + if impact.hazard_type.__name__ != "type" + else "Wind" + ) + + indicator_id = ( + hazard_indicator_dict.get(hazard_type) if hazard_type else None ) - ] + asset_subtype = result.asset.type if hasattr(result.asset, "type") else None + if asset_subtype is None: + asset_subtype_none_count += 1 + asset_subtype_none_assets.append(result.asset.asset_name) + + out.append( + { + "asset_name": result.asset.asset_name, + "latitude": result.asset.latitude, + "longitude": result.asset.longitude, + "hazard_type": hazard_type, + "indicator_id": indicator_id, + "scenario": scenario, + "year": int(year), + "impact_mean": impact_mean, + "impact_distr_bin_edges": impact_distr_bin_edges, + "impact_distr_p": impact_distr_p, + } + ) + + # Out can be used when dealing with expected values. + + # Assert the counts and details for empty impacts, None asset_subtype, and exceptions - scenario = "ssp585" - year = 2030 + assert ( + empty_impact_count == 0 + ), f"Found {empty_impact_count} EmptyImpactDistrib instances in scenarios: {empty_impact_scenarios}" + assert ( + asset_subtype_none_count == 0 + ), f"Found {asset_subtype_none_count} assets with None asset_subtype: {asset_subtype_none_assets}" + assert ( + not exception_scenarios + ), f"Exceptions occurred in scenarios: {exception_scenarios}" + + +@pytest.mark.skip(reason="Requires credentials.") +def test_thermal_power_generation_impacts_water_stress( + setup_data, + hazard_indicator_dict, + vulnerability_models_dict_water_stress, + load_credentials, +): + assets = setup_data + hazard_model = calculation.get_default_hazard_model() + out = [] + empty_impact_count = 0 + asset_subtype_none_count = 0 + empty_impact_scenarios = [] + asset_subtype_none_assets = [] + exception_scenarios = [] + for ( + scenario_year, + vulnerability_models, + ) in vulnerability_models_dict_water_stress.items(): + scenario, year = scenario_year.split("_") - hazard_model = calculation.get_default_hazard_model() vulnerability_models = DictBasedVulnerabilityModels( - calculation.get_default_vulnerability_models() + {ThermalPowerGeneratingAsset: vulnerability_models} ) - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) - out = [ - { - "asset": type(result.asset).__name__, - "type": getattr(result.asset, "type", None), - "capacity": getattr(result.asset, "capacity", None), - "location": getattr(result.asset, "location", None), - "latitude": result.asset.latitude, - "longitude": result.asset.longitude, - "impact_mean": ( - None - if isinstance(results[key][0].impact, EmptyImpactDistrib) - else results[key].impact.mean_impact() - ), - "hazard_type": key.hazard_type.__name__, - } - for result, key in zip(results, results.keys()) - ] - pd.DataFrame.from_dict(out).to_csv( - os.path.join( - cache_folder, - "thermal_power_generation_example_" - + scenario - + "_" - + str(year) - + ".csv", + try: + results = calculate_impacts( + assets, + hazard_model, + vulnerability_models, + scenario=scenario, + year=int(year), ) + except Exception as e: + exception_scenarios.append((scenario, year, str(e))) + continue + + for result, key in zip(results, results.keys()): + impact = results[key][0].impact + if isinstance(impact, EmptyImpactDistrib): + impact_mean = None + hazard_type = None + impact_distr_bin_edges = "0;0" + impact_distr_p = "0;0" + empty_impact_count += 1 + empty_impact_scenarios.append((scenario, year, result.asset.asset_name)) + else: + impact_mean = impact.mean_impact() + impact_distr_bin_edges = ";".join(impact.impact_bins.astype(str)) + impact_distr_p = ";".join(impact.prob.astype(str)) + hazard_type = ( + impact.hazard_type.__name__ + if impact.hazard_type.__name__ != "type" + else "Wind" + ) + + indicator_id = ( + hazard_indicator_dict.get(hazard_type) if hazard_type else None + ) + asset_subtype = result.asset.type if hasattr(result.asset, "type") else None + if asset_subtype is None: + asset_subtype_none_count += 1 + asset_subtype_none_assets.append(result.asset.asset_name) + + out.append( + { + "asset_name": result.asset.asset_name, + "latitude": result.asset.latitude, + "longitude": result.asset.longitude, + "hazard_type": hazard_type, + "indicator_id": indicator_id, + "scenario": scenario, + "year": int(year), + "impact_mean": impact_mean, + "impact_distr_bin_edges": impact_distr_bin_edges, + "impact_distr_p": impact_distr_p, + } + ) + + # Out can be used when dealing with expected values. + + # Assert the counts and details for empty impacts, None asset_subtype, and exceptions + + # There are 57 assets that are EmptyImpactDistrib objects, all for ThermalPowerGenerationWaterStressModel, which is used 10 times + assert ( + empty_impact_count == 570 + ), f"Found {empty_impact_count} EmptyImpactDistrib instances in scenarios: {empty_impact_scenarios}" + assert ( + asset_subtype_none_count == 0 + ), f"Found {asset_subtype_none_count} assets with None asset_subtype: {asset_subtype_none_assets}" + assert ( + not exception_scenarios + ), f"Exceptions occurred in scenarios: {exception_scenarios}" + + +@pytest.mark.skip(reason="Requires credentials.") +def test_thermal_power_generation_impacts_extra( + load_credentials, setup_assets_extra, vul_models_dict_extra +): + """Calculate impacts for the vulnerability models from use case id STRESSTEST.""" + assets = setup_assets_extra + + out = [] + empty_impact_count = 0 + asset_subtype_none_count = 0 + empty_impact_scenarios = [] + asset_subtype_none_assets = [] + exception_scenarios = [] + + for scenario_year, vulnerability_models in vul_models_dict_extra.items(): + scenario, year = scenario_year.split("_") + + print(scenario_year) + + if vulnerability_models is ThermalPowerGenerationAqueductWaterRiskModel(): + reader = ZarrReader() + else: + devaccess = { + "OSC_S3_ACCESS_KEY": os.environ.get("OSC_S3_ACCESS_KEY_DEV", None), + "OSC_S3_SECRET_KEY": os.environ.get("OSC_S3_SECRET_KEY_DEV", None), + "OSC_S3_BUCKET": os.environ.get("OSC_S3_BUCKET_DEV", None), + } + get_env = devaccess.get + reader = ZarrReader(get_env=get_env) + + vulnerability_models = DictBasedVulnerabilityModels( + {ThermalPowerGeneratingAsset: vulnerability_models} ) - self.assertAlmostEqual(1, 1) - - def api_assets(self, assets: List[Asset]): - items = [ - physrisk.api.v1.common.Asset( - asset_class=type(a).__name__, - type=getattr(a, "type", None), - location=getattr(a, "location", None), - latitude=a.latitude, - longitude=a.longitude, + + # Use TUDelft flood models. + hazard_model = ZarrHazardModel( + source_paths=CoreInventorySourcePaths( + EmbeddedInventory(), flood_model=CoreFloodModels.TUDelft + ).source_paths(), + reader=reader, + ) + + try: + results = calculate_impacts( + assets, + hazard_model, + vulnerability_models, + scenario=scenario, + year=int(year), ) - for a in assets - ] - return physrisk.api.v1.common.Assets(items=items) + except Exception as e: + exception_scenarios.append((scenario, year, str(e))) + continue + + for result, key in zip(results, results.keys()): + impact = results[key][0].impact + if isinstance(impact, EmptyImpactDistrib): + impact_mean = None + hazard_type = None + empty_impact_count += 1 + empty_impact_scenarios.append((scenario, year, result.asset.location)) + else: + impact_mean = impact.mean_impact() + hazard_type = ( + impact.hazard_type.__name__ + if impact.hazard_type.__name__ != "type" + else "Wind" + ) + + asset_subtype = result.asset.type if hasattr(result.asset, "type") else None + if asset_subtype is None: + asset_subtype_none_count += 1 + asset_subtype_none_assets.append(result.asset.location) + + out.append( + { + "asset": type(result.asset).__name__, + "type": getattr(result.asset, "type", None), + "location": getattr(result.asset, "location", None), + "latitude": result.asset.latitude, + "longitude": result.asset.longitude, + "impact_mean": impact_mean, + "hazard_type": hazard_type if hazard_type else "Wind", + "scenario": scenario, + "year": int(year), + } + ) + + # out can be used when dealing with expected values. + + # Assert the counts and details for empty impacts, None asset_subtype, and exceptions + assert ( + empty_impact_count == 0 + ), f"Found {empty_impact_count} EmptyImpactDistrib instances in scenarios: {empty_impact_scenarios}" + assert ( + asset_subtype_none_count == 0 + ), f"Found {asset_subtype_none_count} assets with None asset_subtype: {asset_subtype_none_assets}" + assert ( + not exception_scenarios + ), f"Exceptions occurred in scenarios: {exception_scenarios}" diff --git a/tests/models/real_estate_models_stress_test_test.py b/tests/models/real_estate_models_stress_test_test.py new file mode 100644 index 00000000..84f7925f --- /dev/null +++ b/tests/models/real_estate_models_stress_test_test.py @@ -0,0 +1,305 @@ +"""Test asset impact calculations using pytest.""" + +import bz2 +import json + +import numpy as np +import pandas as pd +import pytest + +import physrisk.data.static.world as wd +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 import calculation # noqa: F401 ## Avoid circular imports +from physrisk.kernel.assets import RealEstateAsset +from physrisk.kernel.impact import calculate_impacts +from physrisk.kernel.impact_distrib import EmptyImpactDistrib +from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels +from physrisk.vulnerability_models.real_estate_models import ( + CoolingModel, + GenericTropicalCycloneModel, + RealEstateCoastalInundationModel, + RealEstateRiverineInundationModel, +) + + +@pytest.fixture +def load_assets(): + with bz2.open("./tests/api/housing_kaggle_spain.json.bz2") as f: + houses = json.load(f) + + asset_df = pd.DataFrame(houses["items"]) + longitudes = asset_df.longitude + latitudes = asset_df.latitude + types = asset_df.type + asset_names = asset_df.address + asset_prices = asset_df.price + + countries, continents = wd.get_countries_and_continents( + latitudes=latitudes, longitudes=longitudes + ) + + assets = [ + RealEstateAsset(latitude, longitude, type=type_, location="Europe") + for latitude, longitude, type_ in zip(latitudes, longitudes, types) + ] + + for i, asset_name in enumerate(asset_names): + assets[i].__dict__.update({"asset_name": asset_name}) + + for i, asset_price in enumerate(asset_prices): + assets[i].__dict__.update({"asset_price": asset_price}) + + entrada = np.random.choice( + a=[10, 20, 30], size=len(asset_prices), p=[0.1, 0.2, 0.7] + ) + loan_amounts = (1 - entrada / 100) * asset_prices.to_numpy().astype(float) + + for i, loan_amount in enumerate(loan_amounts): + assets[i].__dict__.update({"asset_loan_amount": loan_amount}) + + return assets + + +@pytest.fixture +def hazard_indicator_dict(): + return { + "Wind": "max_speed", + "CoastalInundation": "flood_depth", + "RiverineInundation": "flood_depth", + "ChronicHeat": '"mean_degree_days_above', + } + + +@pytest.fixture +def vulnerability_models_dict(): + return { + "ssp119_2050": [GenericTropicalCycloneModel()], + "ssp126_2030": [CoolingModel()], + "ssp126_2040": [CoolingModel()], + "ssp126_2050": [CoolingModel()], + "ssp245_2030": [CoolingModel()], + "ssp245_2040": [CoolingModel()], + "ssp245_2050": [CoolingModel(), GenericTropicalCycloneModel()], + "ssp585_2030": [CoolingModel()], + "ssp585_2040": [CoolingModel()], + "ssp585_2050": [CoolingModel(), GenericTropicalCycloneModel()], + } + + +@pytest.fixture +def rcp_vulnerability_models_dict(): + return { + "rcp4p5_2030": [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], + "rcp4p5_2050": [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], + "rcp4p5_2080": [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], + "rcp8p5_2030": [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], + "rcp8p5_2050": [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], + "rcp8p5_2080": [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + ], + } + + +@pytest.mark.skip(reason="Requires credentials.") +def test_calculate_impacts( + load_assets, + hazard_indicator_dict, + vulnerability_models_dict, + rcp_vulnerability_models_dict, + load_credentials, +): + assets = load_assets + + reader = ZarrReader() + + out = [] + empty_impact_count = 0 + asset_subtype_none_count = 0 + empty_impact_scenarios = [] + asset_subtype_none_assets = [] + exception_scenarios = [] + + for scenario_year, vulnerability_models in vulnerability_models_dict.items(): + scenario, year = scenario_year.split("_") + + vulnerability_models = DictBasedVulnerabilityModels( + {RealEstateAsset: vulnerability_models} + ) + + hazard_model = ZarrHazardModel( + source_paths=get_default_source_paths(), reader=reader + ) + + try: + results = calculate_impacts( + assets, + hazard_model, + vulnerability_models, + scenario=scenario, + year=int(year), + ) + except Exception as e: + exception_scenarios.append((scenario, year, str(e))) + continue + + for result, key in zip(results, results.keys()): + impact = results[key][0].impact + if isinstance(impact, EmptyImpactDistrib): + impact_mean = None + hazard_type = None + impact_distr_bin_edges = "0;0" + impact_distr_p = "0;0" + empty_impact_count += 1 + empty_impact_scenarios.append((scenario, year, result.asset.asset_name)) + else: + impact_mean = impact.mean_impact() + impact_distr_bin_edges = ";".join(impact.impact_bins.astype(str)) + impact_distr_p = ";".join(impact.prob.astype(str)) + hazard_type = ( + impact.hazard_type.__name__ + if impact.hazard_type.__name__ != "type" + else "Wind" + ) + + if hazard_type is None: + indicator_id = None + else: + indicator_id = hazard_indicator_dict.get(hazard_type) + + asset_subtype = result.asset.type if hasattr(result.asset, "type") else None + if asset_subtype is None: + asset_subtype_none_count += 1 + asset_subtype_none_assets.append(result.asset.asset_name) + + out.append( + { + "asset_name": result.asset.asset_name, + "asset_price": result.asset.asset_price, + "asset_loan_amount": result.asset.asset_loan_amount, + "asset_subtype": asset_subtype, + "latitude": result.asset.latitude, + "longitude": result.asset.longitude, + "hazard_type": hazard_type, + "indicator_id": indicator_id, + "display_name": "display_name_vacio", + "model": "model_vacio", + "scenario": scenario, + "year": int(year), + "return_periods": {"0": "0"}, + "parameter": 0, + "impact_mean": impact_mean, + "impact_distr_bin_edges": impact_distr_bin_edges, + "impact_distr_p": impact_distr_p, + "impact_exc_exceed_p": "0;0", + "impact_exc_values": "0;0", + "vuln_model_designer": "OS-C-RealEstate-LTV", + } + ) + + for scenario_year, vulnerability_models in rcp_vulnerability_models_dict.items(): + scenario, year = scenario_year.split("_") + + vulnerability_models = DictBasedVulnerabilityModels( + {RealEstateAsset: vulnerability_models} + ) + + hazard_model = ZarrHazardModel( + source_paths=get_default_source_paths(), reader=reader + ) + + try: + results = calculate_impacts( + assets, + hazard_model, + vulnerability_models, + scenario=scenario, + year=int(year), + ) + except Exception as e: + exception_scenarios.append((scenario, year, str(e))) + continue + + for result, key in zip(results, results.keys()): + impact = results[key][0].impact + if isinstance(impact, EmptyImpactDistrib): + impact_mean = None + hazard_type = None + impact_distr_bin_edges = "0;0" + impact_distr_p = "0;0" + empty_impact_count += 1 + empty_impact_scenarios.append((scenario, year, result.asset.asset_name)) + else: + impact_mean = impact.mean_impact() + impact_distr_bin_edges = ";".join(impact.impact_bins.astype(str)) + impact_distr_p = ";".join(impact.prob.astype(str)) + hazard_type = ( + impact.hazard_type.__name__ + if impact.hazard_type.__name__ != "type" + else "Wind" + ) + + if hazard_type is None: + indicator_id = None + else: + indicator_id = hazard_indicator_dict.get(hazard_type) + + asset_subtype = result.asset.type if hasattr(result.asset, "type") else None + if asset_subtype is None: + asset_subtype_none_count += 1 + asset_subtype_none_assets.append(result.asset.asset_name) + + out.append( + { + "asset_name": result.asset.asset_name, + "asset_price": result.asset.asset_price, + "asset_loan_amount": result.asset.asset_loan_amount, + "asset_subtype": asset_subtype, + "latitude": result.asset.latitude, + "longitude": result.asset.longitude, + "hazard_type": hazard_type, + "indicator_id": indicator_id, + "display_name": "display_name_vacio", + "model": "model_vacio", + "scenario": scenario, + "year": int(year), + "return_periods": {"0": "0"}, + "parameter": 0, + "impact_mean": impact_mean, + "impact_distr_bin_edges": impact_distr_bin_edges, + "impact_distr_p": impact_distr_p, + "impact_exc_exceed_p": "0;0", + "impact_exc_values": "0;0", + "vuln_model_designer": "OS-C-RealEstate-LTV", + } + ) + + # out can be used when dealing with expected values. + + # Assert the counts and details for empty impacts, None asset_subtype, and exceptions + assert ( + empty_impact_count == 0 + ), f"Found {empty_impact_count} EmptyImpactDistrib instances in scenarios: {empty_impact_scenarios}" + assert ( + asset_subtype_none_count == 0 + ), f"Found {asset_subtype_none_count} assets with None asset_subtype: {asset_subtype_none_assets}" + assert ( + not exception_scenarios + ), f"Exceptions occurred in scenarios: {exception_scenarios}" diff --git a/tests/models/real_estate_models_test.py b/tests/models/real_estate_models_test.py index 1d1df679..df1a73b9 100644 --- a/tests/models/real_estate_models_test.py +++ b/tests/models/real_estate_models_test.py @@ -1,290 +1,266 @@ -"""Test asset impact calculations.""" - -import unittest - import numpy as np 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.hazards import CoastalInundation, RiverineInundation +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular importsfrom physrisk.kernel.impact import ImpactKey, calculate_impacts from physrisk.kernel.impact import ImpactKey, calculate_impacts from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels from physrisk.vulnerability_models.real_estate_models import ( RealEstateCoastalInundationModel, RealEstateRiverineInundationModel, ) - from ..data.hazard_model_store_test import TestData, mock_hazard_model_store_inundation -class TestRealEstateModels(unittest.TestCase): - """Tests RealEstateInundationModel.""" - - def test_real_estate_model_details(self): - curve = np.array( - [0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163] - ) - store = mock_hazard_model_store_inundation( - TestData.longitudes, TestData.latitudes, curve - ) - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store - ) - - # location="Europe", type="Buildings/Residential" - assets = [ - RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") - for lon, lat in zip(TestData.longitudes[0:1], TestData.latitudes[0:1]) - ] - - scenario = "rcp8p5" - year = 2080 - - vulnerability_models = DictBasedVulnerabilityModels( - {RealEstateAsset: [RealEstateRiverineInundationModel()]} - ) - - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) - - hazard_bin_edges = results[ - ImpactKey(assets[0], RiverineInundation, scenario, year) - ][0].event.intensity_bin_edges - hazard_bin_probs = results[ - ImpactKey(assets[0], RiverineInundation, scenario, year) - ][0].event.prob - - # check one: - # the probability of inundation greater than 0.505m in a year is 1/10.0 - # the probability of inundation greater than 0.333m in a year is 1/5.0 - # therefore the probability of an inundation between 0.333 and 0.505 in a year is 1/5.0 - 1/10.0 - np.testing.assert_almost_equal(hazard_bin_edges[1:3], np.array([0.333, 0.505])) - np.testing.assert_almost_equal(hazard_bin_probs[1], 0.1) - - # check that intensity bin edges for vulnerability matrix are same as for hazard - vulnerability_intensity_bin_edges = results[ - ImpactKey(assets[0], RiverineInundation, scenario, year) - ][0].vulnerability.intensity_bins - np.testing.assert_almost_equal( - vulnerability_intensity_bin_edges, hazard_bin_edges - ) +def test_real_estate_model_details(): + curve = np.array([0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163]) + store = mock_hazard_model_store_inundation( + TestData.longitudes, TestData.latitudes, curve + ) + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + + assets = [ + RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") + for lon, lat in zip(TestData.longitudes[0:1], TestData.latitudes[0:1]) + ] + + scenario = "rcp8p5" + year = 2080 + + vulnerability_models = DictBasedVulnerabilityModels( + {RealEstateAsset: [RealEstateRiverineInundationModel()]} + ) + + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) + + hazard_bin_edges = results[ + ImpactKey(assets[0], RiverineInundation, scenario, year) + ][0].event.intensity_bin_edges + hazard_bin_probs = results[ + ImpactKey(assets[0], RiverineInundation, scenario, year) + ][0].event.prob + + # check one: + # the probability of inundation greater than 0.505m in a year is 1/10.0 + # the probability of inundation greater than 0.333m in a year is 1/5.0 + # therefore the probability of an inundation between 0.333 and 0.505 in a year is 1/5.0 - 1/10.0 + np.testing.assert_almost_equal(hazard_bin_edges[1:3], np.array([0.333, 0.505])) + np.testing.assert_almost_equal(hazard_bin_probs[1], 0.1) + + # check that intensity bin edges for vulnerability matrix are same as for hazard + vulnerability_intensity_bin_edges = results[ + ImpactKey(assets[0], RiverineInundation, scenario, year) + ][0].vulnerability.intensity_bins + np.testing.assert_almost_equal(vulnerability_intensity_bin_edges, hazard_bin_edges) + + # check the impact distribution the matrix is size [len(intensity_bins) - 1, len(impact_bins) - 1] + + cond_probs = results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ + 0 + ].vulnerability.prob_matrix[1, :] + # check conditional prob for inundation intensity 0.333..0.505 + mean, std = np.mean(cond_probs), np.std(cond_probs) + np.testing.assert_almost_equal(cond_probs.sum(), 1) + np.testing.assert_allclose([mean, std], [0.09090909, 0.08184968], rtol=1e-6) + + # probability that impact occurs between impact bin edge 1 and impact bin edge 2 + prob_impact = np.dot( + hazard_bin_probs, + results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ + 0 + ].vulnerability.prob_matrix[:, 1], + ) + np.testing.assert_almost_equal(prob_impact, 0.19350789547968042) - # check the impact distribution the matrix is size [len(intensity_bins) - 1, len(impact_bins) - 1] - cond_probs = results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ + # no check with pre-calculated values for others: + np.testing.assert_allclose( + results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ 0 - ].vulnerability.prob_matrix[1, :] - # check conditional prob for inundation intensity 0.333..0.505 - mean, std = np.mean(cond_probs), np.std(cond_probs) - np.testing.assert_almost_equal(cond_probs.sum(), 1) - np.testing.assert_allclose([mean, std], [0.09090909, 0.08184968], rtol=1e-6) - - # probability that impact occurs between impact bin edge 1 and impact bin edge 2 - prob_impact = np.dot( - hazard_bin_probs, - results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ - 0 - ].vulnerability.prob_matrix[:, 1], - ) - np.testing.assert_almost_equal(prob_impact, 0.19350789547968042) - - # no check with pre-calculated values for others: - np.testing.assert_allclose( - results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ - 0 - ].impact.prob, - np.array( - [ - 0.02815762, - 0.1935079, - 0.11701139, - 0.06043065, - 0.03347816, - 0.02111368, - 0.01504522, - 0.01139892, - 0.00864469, - 0.00626535, - 0.00394643, - ] - ), - rtol=2e-6, - ) + ].impact.prob, + np.array( + [ + 0.02815762, + 0.1935079, + 0.11701139, + 0.06043065, + 0.03347816, + 0.02111368, + 0.01504522, + 0.01139892, + 0.00864469, + 0.00626535, + 0.00394643, + ] + ), + rtol=2e-6, + ) - def test_coastal_real_estate_model(self): - curve = np.array([0.223, 0.267, 0.29, 0.332, 0.359, 0.386, 0.422, 0.449, 0.476]) - store = mock_hazard_model_store_inundation( - TestData.coastal_longitudes, TestData.coastal_latitudes, curve - ) - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store - ) +def test_coastal_real_estate_model(): + curve = np.array([0.223, 0.267, 0.29, 0.332, 0.359, 0.386, 0.422, 0.449, 0.476]) - # location="Europe", type="Buildings/Residential" - assets = [ - RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") - for lon, lat in zip( - TestData.coastal_longitudes[0:1], TestData.coastal_latitudes[0:1] - ) - ] - - scenario = "rcp8p5" - year = 2080 + store = mock_hazard_model_store_inundation( + TestData.coastal_longitudes, TestData.coastal_latitudes, curve + ) + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) - vulnerability_models = DictBasedVulnerabilityModels( - {RealEstateAsset: [RealEstateCoastalInundationModel()]} + assets = [ + RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") + for lon, lat in zip( + TestData.coastal_longitudes[0:1], TestData.coastal_latitudes[0:1] ) + ] - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) + scenario = "rcp8p5" + year = 2080 - np.testing.assert_allclose( - results[ImpactKey(assets[0], CoastalInundation, scenario, year)][ - 0 - ].impact.prob, - np.array( - [ - 2.78081230e-02, - 1.96296619e-01, - 1.32234770e-01, - 7.36581177e-02, - 3.83434609e-02, - 1.83916914e-02, - 7.97401009e-03, - 3.04271878e-03, - 9.79400125e-04, - 2.41250436e-04, - 2.98387241e-05, - ] - ), - rtol=2e-6, - ) + vulnerability_models = DictBasedVulnerabilityModels( + {RealEstateAsset: [RealEstateCoastalInundationModel()]} + ) + + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) - def test_commercial_real_estate_model_details(self): - curve = np.array( + np.testing.assert_allclose( + results[ImpactKey(assets[0], CoastalInundation, scenario, year)][0].impact.prob, + np.array( [ - 2.8302893e-06, - 0.09990284, - 0.21215445, - 0.531271, - 0.7655724, - 0.99438345, - 1.2871761, - 1.502281, - 1.7134278, + 2.78081230e-02, + 1.96296619e-01, + 1.32234770e-01, + 7.36581177e-02, + 3.83434609e-02, + 1.83916914e-02, + 7.97401009e-03, + 3.04271878e-03, + 9.79400125e-04, + 2.41250436e-04, + 2.98387241e-05, ] - ) - store = mock_hazard_model_store_inundation( - TestData.longitudes, TestData.latitudes, curve - ) - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store - ) - - # location="South America", type="Buildings/Commercial" - assets = [ - RealEstateAsset( - lat, lon, location="South America", type="Buildings/Commercial" - ) - for lon, lat in zip(TestData.longitudes[-4:-3], TestData.latitudes[-4:-3]) + ), + rtol=2e-6, + ) + + +def test_commercial_real_estate_model_details(): + curve = np.array( + [ + 2.8302893e-06, + 0.09990284, + 0.21215445, + 0.531271, + 0.7655724, + 0.99438345, + 1.2871761, + 1.502281, + 1.7134278, ] - - scenario = "rcp8p5" - year = 2080 - - # impact bin edges are calibrated so that hazard_bin_probs == impact_bin_probs - # when the impact standard deviation is negligible: - vulnerability_models = DictBasedVulnerabilityModels( - { - RealEstateAsset: [ - RealEstateRiverineInundationModel( - impact_bin_edges=np.array( - [ - 0, - 0.030545039098059, - 0.125953058445539, - 0.322702019487674, - 0.566880882840096, - 0.731980974578735, - 0.823993215529066, - 0.884544511664047, - 0.922115133960502, - 0.969169745946688, - 1.0, - ] - ) + ) + store = mock_hazard_model_store_inundation( + TestData.longitudes, TestData.latitudes, curve + ) + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + + assets = [ + RealEstateAsset(lat, lon, location="South America", type="Buildings/Commercial") + for lon, lat in zip(TestData.longitudes[-4:-3], TestData.latitudes[-4:-3]) + ] + + scenario = "rcp8p5" + year = 2080 + + # impact bin edges are calibrated so that hazard_bin_probs == impact_bin_probs + # when the impact standard deviation is negligible: + vulnerability_models = DictBasedVulnerabilityModels( + { + RealEstateAsset: [ + RealEstateRiverineInundationModel( + impact_bin_edges=np.array( + [ + 0, + 0.030545039098059, + 0.125953058445539, + 0.322702019487674, + 0.566880882840096, + 0.731980974578735, + 0.823993215529066, + 0.884544511664047, + 0.922115133960502, + 0.969169745946688, + 1.0, + ] ) - ] - } - ) - - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) - - hazard_bin_edges = results[ - ImpactKey(assets[0], RiverineInundation, scenario, year) - ][0].event.intensity_bin_edges - hazard_bin_probs = results[ - ImpactKey(assets[0], RiverineInundation, scenario, year) - ][0].event.prob - - # check one: - # the probability of inundation greater than 0.531271m in a year is 1/25 - # the probability of inundation greater than 0.21215445m in a year is 1/10 - # therefore the probability of an inundation between 0.21215445 and 0.531271 in a year is 1/10 - 1/25 - np.testing.assert_almost_equal( - hazard_bin_edges[2:4], np.array([0.21215445, 0.531271]) - ) - np.testing.assert_almost_equal(hazard_bin_probs[2], 0.06) - - # check that intensity bin edges for vulnerability matrix are same as for hazard - vulnerability_intensity_bin_edges = results[ - ImpactKey(assets[0], RiverineInundation, scenario, year) - ][0].vulnerability.intensity_bins - np.testing.assert_almost_equal( - vulnerability_intensity_bin_edges, hazard_bin_edges - ) + ) + ] + } + ) + + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) + + hazard_bin_edges = results[ + ImpactKey(assets[0], RiverineInundation, scenario, year) + ][0].event.intensity_bin_edges + hazard_bin_probs = results[ + ImpactKey(assets[0], RiverineInundation, scenario, year) + ][0].event.prob + + # check one: + # the probability of inundation greater than 0.531271m in a year is 1/25 + # the probability of inundation greater than 0.21215445m in a year is 1/10 + # therefore the probability of an inundation between 0.21215445 and 0.531271 in a year is 1/10 - 1/25 + np.testing.assert_almost_equal( + hazard_bin_edges[2:4], np.array([0.21215445, 0.531271]) + ) + np.testing.assert_almost_equal(hazard_bin_probs[2], 0.06) + + # check that intensity bin edges for vulnerability matrix are same as for hazard + vulnerability_intensity_bin_edges = results[ + ImpactKey(assets[0], RiverineInundation, scenario, year) + ][0].vulnerability.intensity_bins + np.testing.assert_almost_equal(vulnerability_intensity_bin_edges, hazard_bin_edges) + + # check the impact distribution the matrix is size [len(intensity_bins) - 1, len(impact_bins) - 1] + cond_probs = results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ + 0 + ].vulnerability.prob_matrix[2, :] + # check conditional prob for inundation intensity at 0.371712725m + mean, std = np.mean(cond_probs), np.std(cond_probs) + np.testing.assert_almost_equal(cond_probs.sum(), 1) + np.testing.assert_allclose([mean, std], [0.1, 0.2884275164878624], rtol=1e-6) + + # probability that impact occurs between impact bin edge 2 and impact bin edge 3 + prob_impact = np.dot( + hazard_bin_probs, + results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ + 0 + ].vulnerability.prob_matrix[:, 2], + ) + np.testing.assert_almost_equal(prob_impact, 0.10040196672295522) - # check the impact distribution the matrix is size [len(intensity_bins) - 1, len(impact_bins) - 1] - cond_probs = results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ + np.testing.assert_allclose( + results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ 0 - ].vulnerability.prob_matrix[2, :] - # check conditional prob for inundation intensity at 0.371712725m - mean, std = np.mean(cond_probs), np.std(cond_probs) - np.testing.assert_almost_equal(cond_probs.sum(), 1) - np.testing.assert_allclose([mean, std], [0.1, 0.2884275164878624], rtol=1e-6) - - # probability that impact occurs between impact bin edge 2 and impact bin edge 3 - prob_impact = np.dot( - hazard_bin_probs, - results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ - 0 - ].vulnerability.prob_matrix[:, 2], - ) - np.testing.assert_almost_equal(prob_impact, 0.10040196672295522) - - # no check with pre-calculated values for others: - np.testing.assert_allclose( - results[ImpactKey(assets[0], RiverineInundation, scenario, year)][ - 0 - ].impact.prob, - np.array( - [ - 2.009085e-07, - 3.001528e-01, - 1.004020e-01, - 5.885136e-02, - 1.760415e-02, - 1.159864e-02, - 6.130639e-03, - 2.729225e-03, - 1.446537e-03, - 8.450993e-05, - ] - ), - rtol=2e-6, - ) + ].impact.prob, + np.array( + [ + 2.009085e-07, + 3.001528e-01, + 1.004020e-01, + 5.885136e-02, + 1.760415e-02, + 1.159864e-02, + 6.130639e-03, + 2.729225e-03, + 1.446537e-03, + 8.450993e-05, + ] + ), + rtol=2e-6, + ) diff --git a/tests/models/wbgt_model_test.py b/tests/models/wbgt_model_test.py index ea4f5195..e2b46344 100644 --- a/tests/models/wbgt_model_test.py +++ b/tests/models/wbgt_model_test.py @@ -1,7 +1,7 @@ -import unittest from typing import Iterable, List, Union, cast import numpy as np +import pytest from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.hazard_models.core_hazards import get_default_source_paths @@ -12,6 +12,7 @@ HazardParameterDataResponse, ) from physrisk.kernel.hazards import ChronicHeat +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels @@ -19,7 +20,6 @@ ChronicHeatGZNModel, get_impact_distrib, ) - from ..data.hazard_model_store_test import TestData, mock_hazard_model_store_heat_wbgt @@ -44,7 +44,6 @@ def get_data_requests( self, asset: Asset, *, scenario: str, year: int ) -> Union[HazardDataRequest, Iterable[HazardDataRequest]]: """Request the hazard data needed by the vulnerability model for a specific asset - (this is a Google-style doc string) Args: asset: Asset for which data is requested. @@ -221,68 +220,70 @@ def two_variable_joint_variance(ex, varx, ey, vary): return varx * vary + varx * (ey**2) + vary * (ex**2) -class TestChronicAssetImpact(unittest.TestCase): - """Tests the impact on an asset of a chronic hazard model.""" - - def test_wbgt_vulnerability(self): - store = mock_hazard_model_store_heat_wbgt( - TestData.longitudes, TestData.latitudes - ) - hazard_model = ZarrHazardModel( - source_paths=get_default_source_paths(), store=store +@pytest.mark.parametrize( + "asset_type, expected_value", + [ + ( + "high", + np.array( + [ + 0.00000119194, + 0.00000046573, + 0.00000063758, + 0.00000086889, + 0.00000117871, + 0.00000159172, + 0.00000213966, + 0.00000286314, + 0.00000381379, + 0.00000505696, + 0.00021143251, + 0.00167372506, + 0.00924050344, + 0.03560011430, + 0.09575512509, + 0.17988407024, + 0.23607703667, + 0.21646814108, + 0.13867487025, + 0.06205630207, + 0.02433887116, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + 0.00000000000, + ] + ), ) - # 'chronic_heat/osc/v2/mean_work_loss_high_ACCESS-CM2_historical_2005' - scenario = "ssp585" - year = 2050 + # Add other test cases here + ], +) +def test_wbgt_vulnerability(asset_type, expected_value): + store = mock_hazard_model_store_heat_wbgt(TestData.longitudes, TestData.latitudes) + hazard_model = ZarrHazardModel(source_paths=get_default_source_paths(), store=store) + # 'chronic_heat/osc/v2/mean_work_loss_high_ACCESS-CM2_historical_2005' - vulnerability_models = DictBasedVulnerabilityModels( - {IndustrialActivity: [ExampleWBGTGZNJointModel()]} - ) + scenario = "ssp585" + year = 2050 - assets = [ - IndustrialActivity(lat, lon, type="high") - for lon, lat in zip(TestData.longitudes, TestData.latitudes) - ][:1] + vulnerability_models = DictBasedVulnerabilityModels( + {IndustrialActivity: [ExampleWBGTGZNJointModel()]} + ) - results = calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) + assets = [ + IndustrialActivity(lat, lon, type=asset_type) + for lon, lat in zip(TestData.longitudes, TestData.latitudes) + ][:1] - value_test = list(results.values())[0][0].impact.prob - - value_exp = np.array( - [ - 0.00000119194, - 0.00000046573, - 0.00000063758, - 0.00000086889, - 0.00000117871, - 0.00000159172, - 0.00000213966, - 0.00000286314, - 0.00000381379, - 0.00000505696, - 0.00021143251, - 0.00167372506, - 0.00924050344, - 0.03560011430, - 0.09575512509, - 0.17988407024, - 0.23607703667, - 0.21646814108, - 0.13867487025, - 0.06205630207, - 0.02433887116, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - 0.00000000000, - ] - ) - np.testing.assert_almost_equal(value_test, value_exp, decimal=8) + results = calculate_impacts( + assets, hazard_model, vulnerability_models, scenario=scenario, year=year + ) + + value_test = list(results.values())[0][0].impact.prob + np.allclose(value_test, expected_value, 1e-4) diff --git a/tests/models/wind_models_test.py b/tests/models/wind_models_test.py index 7b7e64b9..3aa1a0d5 100644 --- a/tests/models/wind_models_test.py +++ b/tests/models/wind_models_test.py @@ -1,6 +1,6 @@ import numpy as np +import pytest -import tests.data.hazard_model_store_test as hms from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.hazard_models.core_hazards import ( ResourceSubset, @@ -8,28 +8,77 @@ ) from physrisk.kernel.assets import RealEstateAsset from physrisk.kernel.hazards import Wind +from physrisk.kernel import calculation # noqa: F401 ## Avoid circular imports from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels from physrisk.vulnerability_models.real_estate_models import GenericTropicalCycloneModel +from ..data.hazard_model_store_test import ( + zarr_memory_store, + add_curves, + TestData, + shape_transform_21600_43200, +) -def test_wind_real_estate_model(): +@pytest.fixture +def hazard_model_setup(): scenario = "rcp8p5" year = 2080 - # mock some IRIS data for the calculation: - store, root = hms.zarr_memory_store() - # fmt: off - return_periods = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0] # noqa - intensity = np.array([37.279999, 44.756248, 48.712502, 51.685001, 53.520000, 55.230000, 56.302502, 57.336250, 58.452499, 59.283749, 63.312500, 65.482498, 66.352501, 67.220001, 67.767502, 68.117500, 68.372498, 69.127502, 70.897499 ]) # noqa - # fmt: on - shape, transform = hms.shape_transform_21600_43200(return_periods=return_periods) - path = f"wind/iris/v1/max_speed_{scenario}_{year}".format( - scenario=scenario, year=year + + # Mock some IRIS data for the calculation: + store, root = zarr_memory_store() + return_periods = [ + 10.0, + 20.0, + 30.0, + 40.0, + 50.0, + 60.0, + 70.0, + 80.0, + 90.0, + 100.0, + 200.0, + 300.0, + 400.0, + 500.0, + 600.0, + 700.0, + 800.0, + 900.0, + 1000.0, + ] + intensity = np.array( + [ + 37.279999, + 44.756248, + 48.712502, + 51.685001, + 53.520000, + 55.230000, + 56.302502, + 57.336250, + 58.452499, + 59.283749, + 63.312500, + 65.482498, + 66.352501, + 67.220001, + 67.767502, + 68.117500, + 68.372498, + 69.127502, + 70.897499, + ] ) - hms.add_curves( + + shape, transform = shape_transform_21600_43200(return_periods=return_periods) + path = f"wind/iris/v1/max_speed_{scenario}_{year}" + + add_curves( root, - hms.TestData.longitudes, - hms.TestData.latitudes, + TestData.longitudes, + TestData.latitudes, path, shape, intensity, @@ -44,21 +93,31 @@ def select_iris_osc( ): return candidates.with_group_id("iris_osc").first() - # specify use of IRIS (OSC contribution) + # Specify use of IRIS (OSC contribution) provider.add_selector(Wind, "max_speed", select_iris_osc) hazard_model = ZarrHazardModel(source_paths=provider.source_paths(), store=store) + + return hazard_model, scenario, year, return_periods, intensity + + +def test_wind_real_estate_model(hazard_model_setup): + hazard_model, scenario, year, return_periods, intensity = hazard_model_setup + assets = [ RealEstateAsset(lat, lon, location="Asia", type="Buildings/Industrial") - for lon, lat in zip(hms.TestData.longitudes[0:1], hms.TestData.latitudes[0:1]) + for lon, lat in zip(TestData.longitudes[0:1], TestData.latitudes[0:1]) ] + vulnerability_models = DictBasedVulnerabilityModels( {RealEstateAsset: [GenericTropicalCycloneModel()]} ) + results = calculate_impacts( assets, hazard_model, vulnerability_models, scenario=scenario, year=year ) - # check calculation + + # Check calculation cum_probs = 1.0 / np.array(return_periods) probs = cum_probs[:-1] - cum_probs[1:] model = GenericTropicalCycloneModel() @@ -70,4 +129,5 @@ def select_iris_osc( impact_distrib = results[(assets[0], Wind, scenario, year)][0].impact mean_impact = impact_distrib.mean_impact() + np.testing.assert_allclose(mean_impact, mean_check) diff --git a/tests/models/wind_turbine_models_test.py b/tests/models/wind_turbine_models_test.py index 6c01e371..3f63cc3d 100644 --- a/tests/models/wind_turbine_models_test.py +++ b/tests/models/wind_turbine_models_test.py @@ -1,7 +1,7 @@ import typing -import unittest import numpy as np +import pytest from physrisk.kernel.assets import WindTurbine from physrisk.kernel.events import ( @@ -24,8 +24,10 @@ def get_impact( class WindTurbineModel(SupportsEventImpact[WindTurbine]): """Placeholder wind turbine model to be populated.""" - def prob_collapse(self, turbine: WindTurbine, wind_speed_hub: np.ndarray): - """Calculates probability of turbine collapse for a num ber of events given the wind speed + def prob_collapse( + self, turbine: WindTurbine, wind_speed_hub: np.ndarray + ) -> np.ndarray: + """Calculates probability of turbine collapse for a number of events given the wind speed at the hub per event and characteristics of the turbine. Args: @@ -38,25 +40,22 @@ def prob_collapse(self, turbine: WindTurbine, wind_speed_hub: np.ndarray): # just a placeholder model that returns a probability of 0.3 for all events, regardless of wind speed! return np.ones_like(wind_speed_hub) * 0.3 - def get_impact(self, asset: WindTurbine, event_data: HazardEventDataResponse): + def get_impact( + self, asset: WindTurbine, event_data: HazardEventDataResponse + ) -> MultivariateDistribution: """Returns the probability distributions of fractional damage to the turbine for each event. Args: asset (WindTurbine): Wind turbine asset. - event_data (HazardEventDataResponse): Provides wind speeds for different events with different probabilities. # noqa: E501 - - Raises: - NotImplementedError: Supports only the case of single probability bin: i.e. for each event the wind speed is deterministic. # noqa: E501 + event_data (HazardEventDataResponse): Provides wind speeds for different events with different probabilities. Returns: MultivariateDistribution: Probability distribution of impacts associated with events. """ intens = event_data.intensities - # shape is (nb_events, nb_prob_bins) if intens.ndim > 1 and intens.shape[0] != 1: - # only the single probability bin case implemented raise NotImplementedError() - wind_speed = intens.squeeze(axis=0) # vector of wind speeds + wind_speed = intens.squeeze(axis=0) pc = self.prob_collapse(asset, wind_speed) bins_lower = np.array([0.0, 1.0]) bins_upper = np.array([0.0, 1.0]) @@ -64,117 +63,119 @@ def get_impact(self, asset: WindTurbine, event_data: HazardEventDataResponse): return EmpiricalMultivariateDistribution(bins_lower, bins_upper, probs) -class TestWindTurbineModels: - def test_cumulative_probs(self): - """Test calculation of cumulative probability from a combination of lumped probability and uniform probability - density bins. - """ - bins_lower = np.array([2.0, 3.0]) - bins_upper = np.array([2.5, 3.5]) - probs = np.array([[0.5, 0.5], [0.2, 0.8]]) - values, cum_probs = calculate_cumulative_probs(bins_lower, bins_upper, probs) - np.testing.assert_almost_equal(values, [2.0, 2.5, 3.0, 3.5]) - np.testing.assert_almost_equal(cum_probs[0, :], [0, 0.5, 0.5, 1.0]) - np.testing.assert_almost_equal(cum_probs[1, :], [0, 0.2, 0.2, 1.0]) - - bins_lower = np.array([2.0]) - bins_upper = np.array([2.0]) - probs = np.array([[1.0], [1.0]]) - values, cum_probs = calculate_cumulative_probs(bins_lower, bins_upper, probs) - np.testing.assert_almost_equal(values, [2.0, 2.0]) - np.testing.assert_almost_equal(cum_probs[0, :], [0, 1.0]) - np.testing.assert_almost_equal(cum_probs[1, :], [0, 1.0]) - - bins_lower = np.array([2.0, 2.0]) - bins_upper = np.array([2.0, 3.0]) - probs = np.array([[0.5, 0.5], [0.1, 0.9]]) - values, cum_probs = calculate_cumulative_probs(bins_lower, bins_upper, probs) - np.testing.assert_almost_equal(values, [2.0, 2.0, 3.0]) - np.testing.assert_almost_equal(cum_probs[0, :], [0.0, 0.5, 1.0]) - np.testing.assert_almost_equal(cum_probs[1, :], [0, 0.1, 1.0]) - - bins_lower = np.array([2.0, 3.0, 4.0, 5.0, 6.0]) - bins_upper = np.array([2.0, 3.0, 5.0, 5.5, 6.0]) - probs = np.array([[0.1, 0.2, 0.3, 0.2, 0.2], [0.1, 0.1, 0.1, 0.1, 0.6]]) - values, cum_probs = calculate_cumulative_probs(bins_lower, bins_upper, probs) - np.testing.assert_almost_equal( - values, [2.0, 2.0, 3.0, 3.0, 4.0, 5.0, 5.5, 6.0, 6.0] - ) - np.testing.assert_almost_equal( - cum_probs[0, :], [0, 0.1, 0.1, 0.3, 0.3, 0.6, 0.8, 0.8, 1.0] - ) - np.testing.assert_almost_equal( - cum_probs[1, :], [0, 0.1, 0.1, 0.2, 0.2, 0.3, 0.4, 0.4, 1.0] - ) - - def test_sampling(self): - """Test sampling from probability distributions comprising two lumped probabilities.""" - bins_lower = np.array([2.0, 3.0]) - bins_upper = np.array([2.0, 3.0]) - probs = np.array([[0.3, 0.7], [0.4, 0.6]]) - pdf = EmpiricalMultivariateDistribution(bins_lower, bins_upper, probs) - gen = np.random.Generator(np.random.MT19937(111)) - samples = pdf.inv_cumulative_marginal_probs(gen.random(size=(2, 10000))) - check_2 = np.count_nonzero(samples[0, :] == 2.0) - check_3 = np.count_nonzero(samples[0, :] == 3.0) - assert check_2 == 3062 - assert check_3 == 6938 - check_2 = np.count_nonzero(samples[1, :] == 2.0) - check_3 = np.count_nonzero(samples[1, :] == 3.0) - assert check_2 == 3924 - assert check_3 == 6076 - - @unittest.skip("Performance test: slow.") - def test_performance(self): - nb_events = 10000 - nb_samples = 10 - bins_lower = np.array([0.0, 1.0]) - bins_upper = np.array([0.0, 1.0]) - probs = np.tile(np.array([[0.3, 0.7]]), reps=(nb_events, 1)) - pdf = EmpiricalMultivariateDistribution(bins_lower, bins_upper, probs) - gen = np.random.Generator(np.random.MT19937(111)) +@pytest.mark.parametrize( + "bins_lower, bins_upper, probs, expected_values, expected_cum_probs", + [ + ( + np.array([2.0, 3.0]), + np.array([2.5, 3.5]), + np.array([[0.5, 0.5], [0.2, 0.8]]), + [2.0, 2.5, 3.0, 3.5], + [[0, 0.5, 0.5, 1.0], [0, 0.2, 0.2, 1.0]], + ), + ( + np.array([2.0]), + np.array([2.0]), + np.array([[1.0], [1.0]]), + [2.0, 2.0], + [[0, 1.0], [0, 1.0]], + ), + ( + np.array([2.0, 2.0]), + np.array([2.0, 3.0]), + np.array([[0.5, 0.5], [0.1, 0.9]]), + [2.0, 2.0, 3.0], + [[0.0, 0.5, 1.0], [0, 0.1, 1.0]], + ), + ( + np.array([2.0, 3.0, 4.0, 5.0, 6.0]), + np.array([2.0, 3.0, 5.0, 5.5, 6.0]), + np.array([[0.1, 0.2, 0.3, 0.2, 0.2], [0.1, 0.1, 0.1, 0.1, 0.6]]), + [2.0, 2.0, 3.0, 3.0, 4.0, 5.0, 5.5, 6.0, 6.0], + [ + [0, 0.1, 0.1, 0.3, 0.3, 0.6, 0.8, 0.8, 1.0], + [0, 0.1, 0.1, 0.2, 0.2, 0.3, 0.4, 0.4, 1.0], + ], + ), + ], +) +def test_cumulative_probs( + bins_lower, bins_upper, probs, expected_values, expected_cum_probs +): + values, cum_probs = calculate_cumulative_probs(bins_lower, bins_upper, probs) + np.testing.assert_almost_equal(values, expected_values) + np.testing.assert_almost_equal(cum_probs[0, :], expected_cum_probs[0]) + np.testing.assert_almost_equal(cum_probs[1, :], expected_cum_probs[1]) + + +def test_sampling(): + """Test sampling from probability distributions comprising two lumped probabilities.""" + bins_lower = np.array([2.0, 3.0]) + bins_upper = np.array([2.0, 3.0]) + probs = np.array([[0.3, 0.7], [0.4, 0.6]]) + pdf = EmpiricalMultivariateDistribution(bins_lower, bins_upper, probs) + gen = np.random.Generator(np.random.MT19937(111)) + samples = pdf.inv_cumulative_marginal_probs(gen.random(size=(2, 10000))) + check_2 = np.count_nonzero(samples[0, :] == 2.0) + check_3 = np.count_nonzero(samples[0, :] == 3.0) + assert check_2 == 3062 + assert check_3 == 6938 + check_2 = np.count_nonzero(samples[1, :] == 2.0) + check_3 = np.count_nonzero(samples[1, :] == 3.0) + assert check_2 == 3924 + assert check_3 == 6076 + + +def test_performance(): + """Performance test: slow.""" + nb_events = 10000 + nb_samples = 10 + bins_lower = np.array([0.0, 1.0]) + bins_upper = np.array([0.0, 1.0]) + probs = np.tile(np.array([[0.3, 0.7]]), reps=(nb_events, 1)) + pdf = EmpiricalMultivariateDistribution(bins_lower, bins_upper, probs) + gen = np.random.Generator(np.random.MT19937(111)) + for _ in range(1000): uniforms = gen.random(size=(nb_events, nb_samples)) samples = pdf.inv_cumulative_marginal_probs(uniforms) - for _i in range(1000): - uniforms = gen.random(size=(nb_events, nb_samples)) - samples = pdf.inv_cumulative_marginal_probs(uniforms) - print(samples) - - def test_rotor_damage_event_based(self): - """Test demonstrating how WindTurbineModel can be used in an event-based calculation.""" - # The data response for a single asset will be a HazardEventDataResponse. - # The format supports a distribution of hazard intensities *per event* - # but here we simply have a single realisation per event. - - # The hazard model is responsible for sourcing the events. Here we provide output from - # that model directly. - - rng = np.random.Generator(np.random.MT19937(111)) - asset1, asset2 = WindTurbine(), WindTurbine() - nb_events = 20 - response_asset1 = HazardEventDataResponse( - np.array([1.0]), np.array(rng.weibull(a=4, size=[1, nb_events])) - ) - response_asset2 = HazardEventDataResponse( - np.array([1.0]), np.array(rng.weibull(a=4, size=[1, nb_events])) - ) - - turbine_model = WindTurbineModel() - - # provides the impact distributions for each asset - impacts_asset1 = turbine_model.get_impact(asset1, response_asset1) - impacts_asset2 = turbine_model.get_impact(asset2, response_asset2) - - # we sample 10 times for each event for each asset - uniforms = rng.random(size=(nb_events, 10)) - samples_asset1 = impacts_asset1.inv_cumulative_marginal_probs(uniforms) - samples_asset2 = impacts_asset2.inv_cumulative_marginal_probs(uniforms) - - # we can then combine samples and calculate measures... - # for now just sanity-check that we get the approx. 0.3 of total loss in events from placeholder. - np.testing.assert_almost_equal( - np.count_nonzero(samples_asset1 == 1.0) / samples_asset1.size, 0.31 - ) - np.testing.assert_almost_equal( - np.count_nonzero(samples_asset2 == 1.0) / samples_asset2.size, 0.31 - ) + print(samples) + + +def test_rotor_damage_event_based(): + """Test demonstrating how WindTurbineModel can be used in an event-based calculation.""" + # The data response for a single asset will be a HazardEventDataResponse. + # The format supports a distribution of hazard intensities *per event* + # but here we simply have a single realisation per event. + + # The hazard model is responsible for sourcing the events. Here we provide output from + # that model directly. + + rng = np.random.Generator(np.random.MT19937(111)) + asset1, asset2 = WindTurbine(), WindTurbine() + nb_events = 20 + response_asset1 = HazardEventDataResponse( + np.array([1.0]), np.array(rng.weibull(a=4, size=[1, nb_events])) + ) + response_asset2 = HazardEventDataResponse( + np.array([1.0]), np.array(rng.weibull(a=4, size=[1, nb_events])) + ) + + turbine_model = WindTurbineModel() + + # provides the impact distributions for each asset + impacts_asset1 = turbine_model.get_impact(asset1, response_asset1) + impacts_asset2 = turbine_model.get_impact(asset2, response_asset2) + + # we sample 10 times for each event for each asset + uniforms = rng.random(size=(nb_events, 10)) + samples_asset1 = impacts_asset1.inv_cumulative_marginal_probs(uniforms) + samples_asset2 = impacts_asset2.inv_cumulative_marginal_probs(uniforms) + + # we can then combine samples and calculate measures... + # for now just sanity-check that we get the approx. 0.3 of total loss in events from placeholder. + np.testing.assert_almost_equal( + np.count_nonzero(samples_asset1 == 1.0) / samples_asset1.size, 0.31 + ) + np.testing.assert_almost_equal( + np.count_nonzero(samples_asset2 == 1.0) / samples_asset2.size, 0.31 + ) diff --git a/tests/risk_models/risk_models_stress_test_test.py b/tests/risk_models/risk_models_stress_test_test.py new file mode 100644 index 00000000..900bdf1c --- /dev/null +++ b/tests/risk_models/risk_models_stress_test_test.py @@ -0,0 +1,215 @@ +from typing import Dict, Sequence + +import numpy as np +import pytest +from dependency_injector import providers + +import physrisk.data.static.world as wd +from physrisk.api.v1.impact_req_resp import ( + AssetImpactResponse, + RiskMeasureKey, + RiskMeasuresHelper, +) +from physrisk.container import Container +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 import calculation # noqa: F401 ## Avoid circular imports +from physrisk.kernel.assets import ThermalPowerGeneratingAsset +from physrisk.kernel.hazard_model import HazardModelFactory +from physrisk.kernel.hazards import ( + WaterRisk, +) +from physrisk.kernel.risk import AssetLevelRiskModel, MeasureKey +from physrisk.kernel.vulnerability_model import ( + DictBasedVulnerabilityModels, + VulnerabilityModelsFactory, +) +from physrisk.requests import _create_risk_measures +from physrisk.vulnerability_models.thermal_power_generation_models import ( + ThermalPowerGenerationAqueductWaterRiskModel, +) + + +@pytest.fixture +def create_assets(wri_power_plant_assets): + asset_list = wri_power_plant_assets + filtered = asset_list.loc[ + asset_list["primary_fuel"].isin(["Coal", "Gas", "Nuclear", "Oil"]) + ] + filtered = filtered[-60 < filtered["latitude"]] + + longitudes = np.array(filtered["longitude"]) + latitudes = np.array(filtered["latitude"]) + + primary_fuels = np.array( + [ + primary_fuel.replace(" and ", "And").replace(" ", "") + for primary_fuel in filtered["primary_fuel"] + ] + ) + + # Capacity describes a maximum electric power rate. + # Generation describes the actual electricity output of the plant over a period of time. + capacities = np.array(filtered["capacity_mw"]) + + countries, continents = wd.get_countries_and_continents( + latitudes=latitudes, longitudes=longitudes + ) + + assets = [] + for latitude, longitude, capacity, primary_fuel, country in zip( + latitudes, + longitudes, + capacities, + primary_fuels, + countries, + ): + if country in ["Spain"]: + assets.append( + ThermalPowerGeneratingAsset( + latitude, + longitude, + type=primary_fuel, + location=country, + capacity=capacity, + ) + ) + + return assets + + +def create_assets_json(assets: Sequence[ThermalPowerGeneratingAsset]): + assets_dict = { + "items": [ + { + "asset_class": type(asset).__name__, + "type": asset.type, + "location": asset.location, + "longitude": asset.longitude, + "latitude": asset.latitude, + } + for asset in assets + ], + } + return assets_dict + + +@pytest.mark.skip(reason="Requires credentials.") +def test_risk_indicator_model(load_credentials, create_assets): + scenarios = ["ssp585"] + years = [2030] + reader = ZarrReader() + hazard_model = ZarrHazardModel( + source_paths=get_default_source_paths(), reader=reader + ) + assets = create_assets + + vulnerability_models = DictBasedVulnerabilityModels( + {ThermalPowerGeneratingAsset: [ThermalPowerGenerationAqueductWaterRiskModel()]} + ) + + model = AssetLevelRiskModel( + hazard_model=hazard_model, + vulnerability_models=vulnerability_models, + use_case_id="STRESS_TEST", + ) + + measure_ids_for_asset, definitions = model.populate_measure_definitions(assets) + _, measures = model.calculate_risk_measures( + assets, prosp_scens=scenarios, years=years + ) + + measure = measures[MeasureKey(assets[0], scenarios[0], years[0], WaterRisk)] + score = measure.score + measure_0 = measure.measure_0 + np.testing.assert_allclose([measure_0], [0.30079105]) + + risk_measures = _create_risk_measures( + measures, measure_ids_for_asset, definitions, assets, scenarios, years + ) + key = RiskMeasureKey( + hazard_type="WaterRisk", + scenario_id=scenarios[0], + year=str(years[0]), + measure_id=risk_measures.score_based_measure_set_defn.measure_set_id, + ) + item = next(m for m in risk_measures.measures_for_assets if m.key == key) + score2 = item.scores[0] + measure_0_2 = item.measures_0[0] + assert score == score2 + assert measure_0 == measure_0_2 + + helper = RiskMeasuresHelper(risk_measures) + asset_scores, measures, definitions = helper.get_measure( + "WaterRisk", scenarios[0], years[0] + ) + label, description = helper.get_score_details(asset_scores[0], definitions[0]) + assert asset_scores[0] == pytest.approx(0) # TODO check if asserts are correct + + +@pytest.mark.skip(reason="Requires credentials.") +def test_via_requests(load_credentials, create_assets): + scenarios = ["ssp585"] + years = [2030] + reader = ZarrReader() + hazard_model = ZarrHazardModel( + source_paths=get_default_source_paths(), reader=reader + ) + + assets = create_assets + request_dict = { + "assets": create_assets_json(assets=assets), + "include_asset_level": False, + "include_measures": True, + "include_calc_details": False, + "use_case_id": "STRESS_TEST", + "years": years, + "scenarios": scenarios, + } + + container = Container() + + class TestHazardModelFactory(HazardModelFactory): + def hazard_model( + self, + interpolation: str = "floor", + provider_max_requests: Dict[str, int] = {}, + ): + return hazard_model + + class TestVulnerabilityModelFactory(VulnerabilityModelsFactory): + def vulnerability_models(self): + vulnerability_models = DictBasedVulnerabilityModels( + { + ThermalPowerGeneratingAsset: [ + ThermalPowerGenerationAqueductWaterRiskModel() + ] + } + ) + return vulnerability_models + + container.override_providers( + hazard_model_factory=providers.Factory(TestHazardModelFactory) + ) + + container.override_providers( + config=providers.Configuration(default={"zarr_sources": ["embedded"]}) + ) + container.override_providers(inventory_reader=reader) + container.override_providers(zarr_reader=reader) + + container.override_providers( + vulnerability_models_factory=providers.Factory(TestVulnerabilityModelFactory) + ) + + requester = container.requester() + res = requester.get(request_id="get_asset_impact", request_dict=request_dict) + response = AssetImpactResponse.model_validate_json(res) + + res = next( + ma + for ma in response.risk_measures.measures_for_assets + if ma.key.hazard_type == "WaterRisk" + ) + np.testing.assert_allclose(res.measures_0[1], 1.067627) diff --git a/tests/risk_models/risk_models_test.py b/tests/risk_models/risk_models_test.py index ca10a2d6..c03155ba 100644 --- a/tests/risk_models/risk_models_test.py +++ b/tests/risk_models/risk_models_test.py @@ -1,11 +1,9 @@ -"""Test asset impact calculations.""" - from typing import Dict, Sequence import numpy as np +import pytest from dependency_injector import providers -from physrisk import requests from physrisk.api.v1.impact_req_resp import ( AssetImpactResponse, Category, @@ -16,7 +14,6 @@ from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import Asset, RealEstateAsset -from physrisk.kernel.calculation import get_default_vulnerability_models from physrisk.kernel.hazard_model import HazardModelFactory from physrisk.kernel.hazards import ( ChronicHeat, @@ -30,7 +27,9 @@ ) from physrisk.kernel.impact_distrib import ImpactType from physrisk.kernel.risk import AssetLevelRiskModel, MeasureKey -from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels +from physrisk.kernel.vulnerability_model import ( + DictBasedVulnerabilityModels, +) from physrisk.requests import _create_risk_measures from physrisk.risk_models.generic_risk_model import GenericScoreBasedRiskMeasures from physrisk.risk_models.risk_models import RealEstateToyRiskMeasures @@ -41,9 +40,7 @@ RealEstatePluvialInundationModel, RealEstateRiverineInundationModel, ) -from tests.api.container_test import TestContainer -from ..base_test import TestWithCredentials from ..data.hazard_model_store_test import ( TestData, ZarrStoreMocker, @@ -51,350 +48,365 @@ ) -class TestRiskModels(TestWithCredentials): - def test_risk_indicator_model(self): - scenarios = ["rcp8p5"] - years = [2050] - - assets = self._create_assets() - hazard_model = self._create_hazard_model(scenarios, years) - - model = AssetLevelRiskModel( - hazard_model, - DictBasedVulnerabilityModels(get_default_vulnerability_models()), - {RealEstateAsset: RealEstateToyRiskMeasures()}, +@pytest.fixture +def create_assets(): + assets = [ + RealEstateAsset( + TestData.latitudes[0], + TestData.longitudes[0], + location="Asia", + type="Buildings/Industrial", + id=f"unique_asset_{i}", ) - measure_ids_for_asset, definitions = model.populate_measure_definitions(assets) - _, measures = model.calculate_risk_measures( - assets, prosp_scens=scenarios, years=years + for i in range(2) + ] + return assets + + +def create_assets_json(assets: Sequence[RealEstateAsset]): + assets_dict = { + "items": [ + { + "asset_class": type(asset).__name__, + "type": asset.type, + "location": asset.location, + "longitude": asset.longitude, + "latitude": asset.latitude, + "attributes": { + "number_of_storeys": "2", + "structure_type": "concrete", + }, + } + for asset in assets + ], + } + return assets_dict + + +def create_hazard_model(scenarios, years): + source_paths = get_default_source_paths() + + def sp_riverine(scenario, year): + return source_paths[RiverineInundation]( + indicator_id="flood_depth", scenario=scenario, year=year ) - # how to get a score using the MeasureKey - measure = measures[ - MeasureKey(assets[0], scenarios[0], years[0], RiverineInundation) - ] - score = measure.score - measure_0 = measure.measure_0 - np.testing.assert_allclose([measure_0], [0.89306593179]) - - # packing up the risk measures, e.g. for JSON transmission: - risk_measures = _create_risk_measures( - measures, measure_ids_for_asset, definitions, assets, scenarios, years - ) - # we still have a key, but no asset: - key = RiskMeasureKey( - hazard_type="RiverineInundation", - scenario_id=scenarios[0], - year=str(years[0]), - measure_id=risk_measures.score_based_measure_set_defn.measure_set_id, - ) - item = next(m for m in risk_measures.measures_for_assets if m.key == key) - score2 = item.scores[0] - measure_0_2 = item.measures_0[0] - assert score == score2 - assert measure_0 == measure_0_2 - - helper = RiskMeasuresHelper(risk_measures) - asset_scores, measures, definitions = helper.get_measure( - "CoastalInundation", scenarios[0], years[0] - ) - label, description = helper.get_score_details(asset_scores[0], definitions[0]) - assert asset_scores[0] == 4 - - def _create_assets(self): - assets = [ - RealEstateAsset( - TestData.latitudes[0], - TestData.longitudes[0], - location="Asia", - type="Buildings/Industrial", - id=f"unique_asset_{i}", - ) - for i in range(2) - ] - return assets - - def _create_assets_json(self, assets: Sequence[RealEstateAsset]): - assets_dict = { - "items": [ - { - "asset_class": type(asset).__name__, - "type": asset.type, - "location": asset.location, - "longitude": asset.longitude, - "latitude": asset.latitude, - "attributes": { - "number_of_storeys": "2", - "structure_type": "concrete", - }, - } - for asset in assets - ], - } - return assets_dict - - def _create_hazard_model(self, scenarios, years): - source_paths = get_default_source_paths() - - def sp_riverine(scenario, year): - return source_paths[RiverineInundation]( - indicator_id="flood_depth", scenario=scenario, year=year - ) - - def sp_coastal(scenario, year): - return source_paths[CoastalInundation]( - indicator_id="flood_depth", scenario=scenario, year=year - ) - - def sp_wind(scenario, year): - return source_paths[Wind]( - indicator_id="max_speed", scenario=scenario, year=year - ) - - def sp_heat(scenario, year): - return source_paths[ChronicHeat]( - indicator_id="days/above/35c", scenario=scenario, year=year - ) - - def sp_fire(scenario, year): - return source_paths[Fire]( - indicator_id="fire_probability", scenario=scenario, year=year - ) - - def sp_hail(scenario, year): - return source_paths[Hail]( - indicator_id="days/above/5cm", scenario=scenario, year=year - ) - - def sp_drought(scenario, year): - return source_paths[Drought]( - indicator_id="months/spei3m/below/-2", scenario=scenario, year=year - ) - - def sp_precipitation(scenario, year): - return source_paths[Precipitation]( - indicator_id="max/daily/water_equivalent", scenario=scenario, year=year - ) - - mocker = ZarrStoreMocker() - return_periods = inundation_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] + def sp_coastal(scenario, year): + return source_paths[CoastalInundation]( + indicator_id="flood_depth", scenario=scenario, year=year ) - for path in [sp_riverine("historical", 1980), sp_coastal("historical", 1980)]: - 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)]: - mocker.add_curves_global( - path, - TestData.longitudes, - TestData.latitudes, - return_periods, - flood_projected_curve, - ) - - mocker.add_curves_global( - sp_wind("historical", -1), - TestData.longitudes, - TestData.latitudes, - TestData.wind_return_periods, - TestData.wind_intensities_1, - units="m/s", - ) - mocker.add_curves_global( - sp_wind("rcp8p5", 2050), - TestData.longitudes, - TestData.latitudes, - TestData.wind_return_periods, - TestData.wind_intensities_2, - units="m/s", - ) - mocker.add_curves_global( - sp_heat("historical", -1), - TestData.longitudes, - TestData.latitudes, - TestData.temperature_thresholds, - TestData.degree_days_above_index_1, + def sp_wind(scenario, year): + return source_paths[Wind]( + indicator_id="max_speed", scenario=scenario, year=year ) - mocker.add_curves_global( - sp_heat("rcp8p5", 2050), - TestData.longitudes, - TestData.latitudes, - TestData.temperature_thresholds, - TestData.degree_days_above_index_2, - ) - mocker.add_curves_global( - sp_fire("historical", -1), - TestData.longitudes, - TestData.latitudes, - [0], - [0.15], + + def sp_heat(scenario, year): + return source_paths[ChronicHeat]( + indicator_id="days/above/35c", scenario=scenario, year=year ) - mocker.add_curves_global( - sp_fire("rcp8p5", 2050), - TestData.longitudes, - TestData.latitudes, - [0], - [0.2], + + def sp_heat_mean_degrees(scenario, year): + return source_paths[ChronicHeat]( + indicator_id="mean_degree_days/above/index", scenario=scenario, year=year ) - mocker.add_curves_global( - sp_hail("historical", -1), - TestData.longitudes, - TestData.latitudes, - [0], - [2.15], + + def sp_fire(scenario, year): + return source_paths[Fire]( + indicator_id="fire_probability", scenario=scenario, year=year ) - mocker.add_curves_global( - sp_hail("rcp8p5", 2050), - TestData.longitudes, - TestData.latitudes, - [0], - [4], + + def sp_hail(scenario, year): + return source_paths[Hail]( + indicator_id="days/above/5cm", scenario=scenario, year=year ) - mocker.add_curves_global( - sp_drought("historical", -1), - TestData.longitudes, - TestData.latitudes, - [0], - [3], + + def sp_drought(scenario, year): + return source_paths[Drought]( + indicator_id="months/spei3m/below/-2", scenario=scenario, year=year ) - mocker.add_curves_global( - sp_drought("rcp8p5", 2050), - TestData.longitudes, - TestData.latitudes, - [0], - [0.8], + + def sp_precipitation(scenario, year): + return source_paths[Precipitation]( + indicator_id="max/daily/water_equivalent", scenario=scenario, year=year ) + + mocker = ZarrStoreMocker() + return_periods = inundation_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] + ) + + for path in [sp_riverine("historical", 1980), sp_coastal("historical", 1980)]: mocker.add_curves_global( - sp_precipitation("historical", -1), + path, TestData.longitudes, TestData.latitudes, - [0], - [10], + return_periods, + flood_histo_curve, ) + + for path in [sp_riverine("rcp8p5", 2050), sp_coastal("rcp8p5", 2050)]: mocker.add_curves_global( - sp_precipitation("rcp8p5", 2050), + path, TestData.longitudes, TestData.latitudes, - [0], - [70], - ) - - return ZarrHazardModel( - source_paths=get_default_source_paths(), store=mocker.store + return_periods, + flood_projected_curve, ) - def test_via_requests(self): - scenarios = ["ssp585"] - years = [2050] - - assets = self._create_assets() - # hazard_model = ZarrHazardModel(source_paths=get_default_source_paths()) - hazard_model = self._create_hazard_model(scenarios, years) - - request_dict = { - "assets": self._create_assets_json(assets), - "include_asset_level": False, - "include_measures": True, - "include_calc_details": False, - "years": years, - "scenarios": scenarios, - } - - # request = requests.AssetImpactRequest(**request_dict) - - container = Container() - - class TestHazardModelFactory(HazardModelFactory): - def hazard_model( - self, - interpolation: str = "floor", - provider_max_requests: Dict[str, int] = {}, - ): - return hazard_model - - container.override_providers( - hazard_model_factory=providers.Factory(TestHazardModelFactory) - ) - container.override_providers( - config=providers.Configuration(default={"zarr_sources": ["embedded"]}) - ) - container.override_providers(inventory_reader=None) - container.override_providers(zarr_reader=None) - - requester = container.requester() - res = requester.get(request_id="get_asset_impact", request_dict=request_dict) - response = AssetImpactResponse.model_validate_json(res) - - # response = requests._get_asset_impacts( - # request, - # hazard_model, - # vulnerability_models=DictBasedVulnerabilityModels(get_default_vulnerability_models()), - # ) - res = next( - ma - for ma in response.risk_measures.measures_for_assets - if ma.key.hazard_type == "RiverineInundation" - ) - np.testing.assert_allclose(res.measures_0, [0.89306593179, 0.89306593179]) - # json_str = json.dumps(response.model_dump(), cls=NumpyArrayEncoder) - - def test_generic_model(self): - scenarios = ["rcp8p5"] - years = [2050] - - assets = [ - Asset(TestData.latitudes[0], TestData.longitudes[0], id=f"unique_id_{i}") - for i in range(2) - ] - # assets = [RealEstateAsset(TestData.latitudes[0], TestData.longitudes[0], location="Asia", type="Buildings/Industrial") for i in range(2)] - hazard_model = self._create_hazard_model(scenarios, years) - - model_set = [ - RealEstateCoastalInundationModel(), - RealEstateRiverineInundationModel(), - RealEstatePluvialInundationModel(), - GenericTropicalCycloneModel(), - PlaceholderVulnerabilityModel("fire_probability", Fire, ImpactType.damage), - PlaceholderVulnerabilityModel( - "days/above/35c", ChronicHeat, ImpactType.damage - ), - PlaceholderVulnerabilityModel("days/above/5cm", Hail, ImpactType.damage), - PlaceholderVulnerabilityModel( - "months/spei3m/below/-2", Drought, ImpactType.damage - ), - PlaceholderVulnerabilityModel( - "max/daily/water_equivalent", Precipitation, ImpactType.damage - ), - ] - - vulnerability_models = {Asset: model_set, RealEstateAsset: model_set} - - generic_measures = GenericScoreBasedRiskMeasures() - model = AssetLevelRiskModel( - hazard_model, - DictBasedVulnerabilityModels(vulnerability_models), - {Asset: generic_measures, RealEstateAsset: generic_measures}, - ) - measure_ids_for_asset, definitions = model.populate_measure_definitions(assets) - _, measures = model.calculate_risk_measures( - assets, prosp_scens=scenarios, years=years - ) - np.testing.assert_approx_equal( - measures[MeasureKey(assets[0], scenarios[0], years[0], Wind)].measure_0, - 214.01549835205077, - ) - np.testing.assert_equal( - measures[MeasureKey(assets[0], scenarios[0], years[0], Drought)].score, - Category.HIGH, - ) + mocker.add_curves_global( + sp_wind("historical", -1), + TestData.longitudes, + TestData.latitudes, + TestData.wind_return_periods, + TestData.wind_intensities_1, + units="m/s", + ) + mocker.add_curves_global( + sp_wind("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + TestData.wind_return_periods, + TestData.wind_intensities_2, + units="m/s", + ) + mocker.add_curves_global( + sp_heat("historical", -1), + TestData.longitudes, + TestData.latitudes, + TestData.temperature_thresholds, + TestData.degree_days_above_index_1, + ) + mocker.add_curves_global( + sp_heat("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + TestData.temperature_thresholds, + TestData.degree_days_above_index_2, + ) + mocker.add_curves_global( + sp_heat_mean_degrees("historical", -1), + TestData.longitudes, + TestData.latitudes, + TestData.temperature_thresholds, + TestData.degree_days_above_index_1, + ) + mocker.add_curves_global( + sp_heat_mean_degrees("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + TestData.temperature_thresholds, + TestData.degree_days_above_index_2, + ) + mocker.add_curves_global( + sp_fire("historical", -1), + TestData.longitudes, + TestData.latitudes, + [0], + [0.15], + ) + mocker.add_curves_global( + sp_fire("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + [0], + [0.2], + ) + mocker.add_curves_global( + sp_hail("historical", -1), + TestData.longitudes, + TestData.latitudes, + [0], + [2.15], + ) + mocker.add_curves_global( + sp_hail("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + [0], + [4], + ) + mocker.add_curves_global( + sp_drought("historical", -1), + TestData.longitudes, + TestData.latitudes, + [0], + [3], + ) + mocker.add_curves_global( + sp_drought("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + [0], + [0.8], + ) + mocker.add_curves_global( + sp_precipitation("historical", -1), + TestData.longitudes, + TestData.latitudes, + [0], + [10], + ) + mocker.add_curves_global( + sp_precipitation("rcp8p5", 2050), + TestData.longitudes, + TestData.latitudes, + [0], + [70], + ) + + return ZarrHazardModel(source_paths=get_default_source_paths(), store=mocker.store) + + +@pytest.fixture +def hazard_model(): + scenarios = ["rcp8p5"] + years = [2050] + return create_hazard_model(scenarios, years) + + +def test_risk_indicator_model(hazard_model, create_assets): + scenarios = ["rcp8p5"] + years = [2050] + + assets = create_assets + + model = AssetLevelRiskModel( + hazard_model, + vulnerability_models=None, + measure_calculators={RealEstateAsset: RealEstateToyRiskMeasures()}, + use_case_id="DEFAULT", + ) + measure_ids_for_asset, definitions = model.populate_measure_definitions(assets) + _, measures = model.calculate_risk_measures( + assets, prosp_scens=scenarios, years=years + ) + + measure = measures[ + MeasureKey(assets[0], scenarios[0], years[0], RiverineInundation) + ] + score = measure.score + measure_0 = measure.measure_0 + np.testing.assert_allclose([measure_0], [0.89306593179]) + + risk_measures = _create_risk_measures( + measures, measure_ids_for_asset, definitions, assets, scenarios, years + ) + key = RiskMeasureKey( + hazard_type="RiverineInundation", + scenario_id=scenarios[0], + year=str(years[0]), + measure_id=risk_measures.score_based_measure_set_defn.measure_set_id, + ) + item = next(m for m in risk_measures.measures_for_assets if m.key == key) + score2 = item.scores[0] + measure_0_2 = item.measures_0[0] + assert score == score2 + assert measure_0 == measure_0_2 + + helper = RiskMeasuresHelper(risk_measures) + asset_scores, measures, definitions = helper.get_measure( + "ChronicHeat", scenarios[0], years[0] + ) + label, description = helper.get_score_details(asset_scores[0], definitions[0]) + assert asset_scores[0] == 4 + + +def test_via_requests(hazard_model, create_assets): + scenarios = ["rcp8p5"] + years = [2050] + + assets = create_assets + request_dict = { + "assets": create_assets_json(assets), + "include_asset_level": False, + "include_measures": True, + "include_calc_details": False, + "years": years, + "scenarios": scenarios, + } + + container = Container() + + class TestHazardModelFactory(HazardModelFactory): + def hazard_model( + self, + interpolation: str = "floor", + provider_max_requests: Dict[str, int] = {}, + ): + return hazard_model + + container.override_providers( + hazard_model_factory=providers.Factory(TestHazardModelFactory) + ) + container.override_providers( + config=providers.Configuration(default={"zarr_sources": ["embedded"]}) + ) + container.override_providers(inventory_reader=None) + container.override_providers(zarr_reader=None) + + requester = container.requester() + res = requester.get(request_id="get_asset_impact", request_dict=request_dict) + response = AssetImpactResponse.model_validate_json(res) + + res = next( + ma + for ma in response.risk_measures.measures_for_assets + if ma.key.hazard_type == "RiverineInundation" + ) + np.testing.assert_allclose(res.measures_0, [0.89306593179, 0.89306593179]) + + +def test_generic_model(hazard_model): + scenarios = ["rcp8p5"] + years = [2050] + + assets = [ + Asset(TestData.latitudes[0], TestData.longitudes[0], id=f"unique_id_{i}") + for i in range(2) + ] + + model_set = [ + RealEstateCoastalInundationModel(), + RealEstateRiverineInundationModel(), + RealEstatePluvialInundationModel(), + GenericTropicalCycloneModel(), + PlaceholderVulnerabilityModel("fire_probability", Fire, ImpactType.damage), + PlaceholderVulnerabilityModel("days/above/35c", ChronicHeat, ImpactType.damage), + PlaceholderVulnerabilityModel("days/above/5cm", Hail, ImpactType.damage), + PlaceholderVulnerabilityModel( + "months/spei3m/below/-2", Drought, ImpactType.damage + ), + PlaceholderVulnerabilityModel( + "max/daily/water_equivalent", Precipitation, ImpactType.damage + ), + ] + + vulnerability_models = {Asset: model_set, RealEstateAsset: model_set} + + generic_measures = GenericScoreBasedRiskMeasures() + model = AssetLevelRiskModel( + hazard_model=hazard_model, + vulnerability_models=DictBasedVulnerabilityModels(vulnerability_models), + measure_calculators={ + Asset: generic_measures, + RealEstateAsset: generic_measures, + }, + use_case_id="GENERIC", + ) + measure_ids_for_asset, definitions = model.populate_measure_definitions(assets) + _, measures = model.calculate_risk_measures( + assets, prosp_scens=scenarios, years=years + ) + np.testing.assert_approx_equal( + measures[MeasureKey(assets[0], scenarios[0], years[0], Wind)].measure_0, + 214.01549835205077, + ) + np.testing.assert_equal( + measures[MeasureKey(assets[0], scenarios[0], years[0], Drought)].score, + Category.HIGH, + )