diff --git a/methodology/PhysicalRiskMethodologyBibliography.bib b/methodology/PhysicalRiskMethodologyBibliography.bib index bdec3521..8a7fe9a6 100644 --- a/methodology/PhysicalRiskMethodologyBibliography.bib +++ b/methodology/PhysicalRiskMethodologyBibliography.bib @@ -70,7 +70,7 @@ @inbook{Cooley:2013 @techreport{CFRF:2022, title={Scenario Analysis: Physical Risk}, institution={Climate Financial Risk Forum}, - url={ttps://www.fca.org.uk/publication/corporate/cfrf-guide-2022-scenario-analysis-physical-risk-underwriting-guide.pdf.pdf}, + url={https://www.fca.org.uk/publication/corporate/cfrf-guide-2022-scenario-analysis-physical-risk-underwriting-guide.pdf.pdf}, year={2022} } diff --git a/src/physrisk/__init__.py b/src/physrisk/__init__.py index a6baa10c..e69de29b 100644 --- a/src/physrisk/__init__.py +++ b/src/physrisk/__init__.py @@ -1,9 +0,0 @@ -from .kernel import ( - Asset, - Drought, - ExceedanceCurve, - HazardEventDistrib, - RiverineInundation, - VulnerabilityDistrib, - calculate_impacts, -) diff --git a/src/physrisk/api/v1/hazard_data.py b/src/physrisk/api/v1/hazard_data.py index 740fcaf7..cd2e7210 100644 --- a/src/physrisk/api/v1/hazard_data.py +++ b/src/physrisk/api/v1/hazard_data.py @@ -30,15 +30,21 @@ class MapInfo(BaseModel): """Provides information about map layer.""" colormap: Optional[Colormap] = Field(description="Details of colormap.") - array_name: Optional[str] = Field( - description="Name of array reprojected to Web Mercator for on-the-fly display or to hash to obtain tile ID. If not supplied, convention is to add '_map' to array_name." # noqa + path: str = Field( + description="Name of array reprojected to Web Mercator for on-the-fly display or to hash to obtain tile ID. If not supplied, convention is to add '_map' to path." # noqa ) - bounds: Optional[List[Tuple[float, float]]] = Field( + bounds: List[Tuple[float, float]] = Field( [(-180.0, 85.0), (180.0, 85.0), (180.0, -85.0), (-180.0, -85.0)], description="Bounds (top/left, top/right, bottom/right, bottom/left) as degrees. Note applied to map reprojected into Web Mercator CRS.", # noqa ) # note that the bounds should be consistent with the array attributes - source: Optional[str] = Field(description="Source of map image: 'map_array' or 'tiles'.") + source: Optional[str] = Field( + description="""Source of map image. These are + 'map_array': single Mercator projection array at path above + 'map_array_pyramid': pyramid of Mercator projection arrays + 'mapbox'. + """ + ) class Period(BaseModel): @@ -69,6 +75,11 @@ class HazardResource(BaseModel): indicator_id: str = Field( description="Identifier of the hazard indicator (i.e. the modelled quantity), e.g. 'flood_depth'." ) + indicator_model_id: Optional[str] = Field( + None, + description="Identifier specifying the type of model used in the derivation of the indicator " + "(e.g. whether flood model includes impact of sea-level rise).", + ) indicator_model_gcm: str = Field( description="Identifier of general circulation model(s) used in the derivation of the indicator." ) @@ -119,11 +130,7 @@ def expand_resource( else ( item.map.copy( deep=True, - update={ - "array_name": expand( - item.map.array_name if item.map.array_name is not None else "", key, param - ) - }, + update={"path": expand(item.map.path if item.map.path is not None else "", key, param)}, ) ), }, diff --git a/src/physrisk/api/v1/hazard_image.py b/src/physrisk/api/v1/hazard_image.py index d168d57d..06071b6b 100644 --- a/src/physrisk/api/v1/hazard_image.py +++ b/src/physrisk/api/v1/hazard_image.py @@ -1,9 +1,20 @@ -from typing import Optional +from typing import NamedTuple, Optional from pydantic import BaseModel, Field from physrisk.api.v1.common import BaseHazardRequest +# class Tile(BaseHazardRequest): +# x: int +# y: int +# z: int + + +class Tile(NamedTuple): + x: int + y: int + z: int + class HazardImageRequest(BaseHazardRequest): resource: str = Field(description="Full path to the array; formed by '{path}/{id}'.") @@ -13,6 +24,7 @@ class HazardImageRequest(BaseHazardRequest): format: Optional[str] = Field("PNG") min_value: Optional[float] max_value: Optional[float] + tile: Optional[Tile] class HazardImageResponse(BaseModel): diff --git a/src/physrisk/api/v1/impact_req_resp.py b/src/physrisk/api/v1/impact_req_resp.py index b75eceb5..957420e6 100644 --- a/src/physrisk/api/v1/impact_req_resp.py +++ b/src/physrisk/api/v1/impact_req_resp.py @@ -1,11 +1,10 @@ +from enum import Enum from typing import List, Optional from pydantic import BaseModel, Field from physrisk.api.v1.common import Assets, Distribution, ExceedanceCurve, VulnerabilityDistrib -# region Request - class CalcSettings(BaseModel): hazard_interp: str = Field("floor", description="Method used for interpolation of hazards: 'floor' or 'bilinear'.") @@ -16,7 +15,8 @@ class AssetImpactRequest(BaseModel): assets: Assets calc_settings: CalcSettings = Field(default_factory=CalcSettings, description="Interpolation method.") - include_asset_level: bool = Field(True, description="If true, include ") + include_asset_level: bool = Field(True, description="If true, include asset-level impacts.") + include_measures: bool = Field(True, description="If true, include measures.") include_calc_details: bool = Field(True, description="If true, include impact calculation details.") scenario: str = Field("rcp8p5", description="Name of scenario ('rcp8p5')") year: int = Field( @@ -25,11 +25,34 @@ class AssetImpactRequest(BaseModel): ) -# endregion - # region Response +class Category(str, Enum): + NODATA = "NODATA" + LOW = "LOW" + MEDIUM = "MEDIUM" + HIGH = "HIGH" + REDFLAG = "REDFLAG" + + +class Indicator(BaseModel): + value: float + label: str + + +class RiskMeasureResult(BaseModel): + """Provides a risk category based on one or more risk indicators. + A risk indicator is a quantity derived from one or more vulnerability models, + e.g. the change in 1-in-100 year damage or disruption. + """ + + category: Category = Field(description="Result category.") + cat_defn: str = Field(description="Definition of the category for the particular indicator.") + indicators: List[Indicator] + summary: str = Field(description="Summary of the indicator.") + + class AcuteHazardCalculationDetails(BaseModel): """Details of an acute hazard calculation.""" @@ -48,6 +71,7 @@ class AssetSingleHazardImpact(BaseModel): ('damage') or disruption to the annual economic benefit obtained from the asset ('disruption'), expressed as fractional decrease to an equivalent cash amount.""", ) + risk_measure: Optional[RiskMeasureResult] impact_distribution: Distribution impact_exceedance: ExceedanceCurve impact_mean: float diff --git a/src/physrisk/data/image_creator.py b/src/physrisk/data/image_creator.py index 131e86a2..daa0d19d 100644 --- a/src/physrisk/data/image_creator.py +++ b/src/physrisk/data/image_creator.py @@ -1,5 +1,7 @@ import io -from typing import Callable, List, Optional +from functools import lru_cache +from pathlib import PurePosixPath +from typing import Callable, List, NamedTuple, Optional import numpy as np import PIL.Image as Image @@ -9,6 +11,12 @@ from physrisk.data.zarr_reader import ZarrReader +class Tile(NamedTuple): + x: int + y: int + z: int + + class ImageCreator: """Convert small arrays into images for map display. Intended for arrays <~1500x1500 (otherwise, recommended to use Mapbox tiles - or similar). @@ -22,6 +30,7 @@ def convert( path: str, format="PNG", colormap: str = "heating", + tile: Optional[Tile] = None, min_value: Optional[float] = None, max_value: Optional[float] = None, ) -> bytes: @@ -37,7 +46,7 @@ def convert( Returns: bytes: Image data. """ - image = self._to_image(path, colormap, min_value=min_value, max_value=max_value) + image = self._to_image(path, colormap, tile=tile, min_value=min_value, max_value=max_value) image_bytes = io.BytesIO() image.save(image_bytes, format=format) return image_bytes.getvalue() @@ -65,12 +74,27 @@ def to_file( image.save(filename, format=format) def _to_image( - self, path, colormap: str = "heating", min_value: Optional[float] = None, max_value: Optional[float] = None + self, + path, + colormap: str = "heating", + tile: Optional[Tile] = None, + index: Optional[int] = None, + min_value: Optional[float] = None, + max_value: Optional[float] = None, ) -> Image.Image: """Get image for path specified as array of bytes.""" - data = self.reader.all_data(path) + tile_path = path if tile is None else str(PurePosixPath(path, f"{tile.z}")) + data = get_data(self.reader, tile_path) + # data = self.reader.all_data(tile_path) if len(data.shape) == 3: - data = data[:, :, :].squeeze(axis=0) + index = len(self.reader.get_index_values(data)) - 1 if index is None else index + if tile is None: + # return whole array + data = data[index, :, :] # .squeeze(axis=0) + else: + # (from zarr 2.16.0 we can also use block indexing) + data = data[index, 256 * tile.y : 256 * (tile.y + 1), 256 * tile.x : 256 * (tile.x + 1)] + if any(dim > 1500 for dim in data.shape): raise Exception("dimension too large (over 1500).") map_defn = colormap_provider.colormap(colormap) @@ -169,3 +193,8 @@ def test_store(path: str): ) z[0, :, :] = im return store + + +@lru_cache(maxsize=32) +def get_data(reader, path): + return reader.all_data(path) diff --git a/src/physrisk/data/inventory.py b/src/physrisk/data/inventory.py index 72958856..10637ed3 100644 --- a/src/physrisk/data/inventory.py +++ b/src/physrisk/data/inventory.py @@ -89,14 +89,14 @@ def expand(resources: List[HazardResource]) -> List[HazardResource]: expanded_models = [e for model in resources for e in model.expand()] # we populate map_id hashes programmatically for model in expanded_models: - if model.map and model.map.source == "mapbox" and model.map.array_name: + if model.map and model.map.source == "mapbox" and model.map.path: for scenario in model.scenarios: test_periods = scenario.periods scenario.periods = [] for year in scenario.years: - name_format = model.map.array_name - array_name = name_format.format(scenario=scenario.id, year=year, return_period=1000) - id = alphanumeric(array_name)[0:6] + name_format = model.map.path + path = name_format.format(scenario=scenario.id, year=year, return_period=1000) + id = alphanumeric(path)[0:6] scenario.periods.append(Period(year=year, map_id=id)) # if a period was specified explicitly, we check that hash is the same: a build-in check if test_periods is not None: diff --git a/src/physrisk/data/static/hazard/inventory.json b/src/physrisk/data/static/hazard/inventory.json index adf2674d..3560351f 100644 --- a/src/physrisk/data/static/hazard/inventory.json +++ b/src/physrisk/data/static/hazard/inventory.json @@ -5,9 +5,10 @@ "group_id": "public", "path": "inundation/wri/v2/inunriver_{scenario}_000000000WATCH_{year}", "indicator_id": "flood_depth", + "indicator_model_id": null, "indicator_model_gcm": "historical", "params": {}, - "display_name": "WRI/Baseline", + "display_name": "Flood depth/baseline (WRI)", "display_groups": [], "description": "\nWorld Resources Institute Aqueduct Floods baseline riverine model using historical data.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -20,7 +21,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inunriver_{scenario}_000000000WATCH_{year}_rp{return_period:05d}", + "path": "inunriver_{scenario}_000000000WATCH_{year}_rp{return_period:05d}", "bounds": [ [ -180.0, @@ -56,9 +57,10 @@ "group_id": "public", "path": "inundation/wri/v2/inunriver_{scenario}_00000NorESM1-M_{year}", "indicator_id": "flood_depth", + "indicator_model_id": null, "indicator_model_gcm": "NorESM1-M", "params": {}, - "display_name": "WRI/NorESM1-M", + "display_name": "Flood depth/NorESM1-M (WRI)", "display_groups": [], "description": "\nWorld Resources Institute Aqueduct Floods riverine model using GCM model from\nBjerknes Centre for Climate Research, Norwegian Meteorological Institute.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -71,7 +73,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inunriver_{scenario}_00000NorESM1-M_{year}_rp{return_period:05d}", + "path": "inunriver_{scenario}_00000NorESM1-M_{year}_map", "bounds": [ [ -180.0, @@ -90,7 +92,7 @@ -85.0 ] ], - "source": "mapbox" + "source": "map_array_pyramid" }, "scenarios": [ { @@ -117,9 +119,10 @@ "group_id": "public", "path": "inundation/wri/v2/inunriver_{scenario}_0000GFDL-ESM2M_{year}", "indicator_id": "flood_depth", + "indicator_model_id": null, "indicator_model_gcm": "GFDL-ESM2M", "params": {}, - "display_name": "WRI/GFDL-ESM2M", + "display_name": "Flood depth/GFDL-ESM2M (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods riverine model using GCM model from\nGeophysical Fluid Dynamics Laboratory (NOAA).\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -132,7 +135,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inunriver_{scenario}_0000GFDL-ESM2M_{year}_rp{return_period:05d}", + "path": "inunriver_{scenario}_0000GFDL-ESM2M_{year}_map", "bounds": [ [ -180.0, @@ -151,7 +154,7 @@ -85.0 ] ], - "source": "mapbox" + "source": "map_array_pyramid" }, "scenarios": [ { @@ -178,9 +181,10 @@ "group_id": "public", "path": "inundation/wri/v2/inunriver_{scenario}_0000HadGEM2-ES_{year}", "indicator_id": "flood_depth", + "indicator_model_id": null, "indicator_model_gcm": "HadGEM2-ES", "params": {}, - "display_name": "WRI/HadGEM2-ES", + "display_name": "Flood depth/HadGEM2-ES (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods riverine model using GCM model:\nMet Office Hadley Centre.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -193,7 +197,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inunriver_{scenario}_0000HadGEM2-ES_{year}_rp{return_period:05d}", + "path": "inunriver_{scenario}_0000HadGEM2-ES_{year}_map", "bounds": [ [ -180.0, @@ -212,7 +216,7 @@ -85.0 ] ], - "source": "mapbox" + "source": "map_array_pyramid" }, "scenarios": [ { @@ -239,9 +243,10 @@ "group_id": "public", "path": "inundation/wri/v2/inunriver_{scenario}_00IPSL-CM5A-LR_{year}", "indicator_id": "flood_depth", + "indicator_model_id": null, "indicator_model_gcm": "IPSL-CM5A-LR", "params": {}, - "display_name": "WRI/IPSL-CM5A-LR", + "display_name": "Flood depth/IPSL-CM5A-LR (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods riverine model using GCM model from\nInstitut Pierre Simon Laplace\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -254,7 +259,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inunriver_{scenario}_00IPSL-CM5A-LR_{year}_rp{return_period:05d}", + "path": "inunriver_{scenario}_00IPSL-CM5A-LR_{year}_map", "bounds": [ [ -180.0, @@ -273,7 +278,7 @@ -85.0 ] ], - "source": "mapbox" + "source": "map_array_pyramid" }, "scenarios": [ { @@ -300,9 +305,10 @@ "group_id": "public", "path": "inundation/wri/v2/inunriver_{scenario}_MIROC-ESM-CHEM_{year}", "indicator_id": "flood_depth", + "indicator_model_id": null, "indicator_model_gcm": "MIROC-ESM-CHEM", "params": {}, - "display_name": "WRI/MIROC-ESM-CHEM", + "display_name": "Flood depth/MIROC-ESM-CHEM (WRI)", "display_groups": [], "description": "World Resource Institute Aqueduct Floods riverine model using\n GCM model from Atmosphere and Ocean Research Institute\n (The University of Tokyo), National Institute for Environmental Studies, and Japan Agency\n for Marine-Earth Science and Technology.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -315,7 +321,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inunriver_{scenario}_MIROC-ESM-CHEM_{year}_rp{return_period:05d}", + "path": "inunriver_{scenario}_MIROC-ESM-CHEM_{year}_rp{return_period:05d}", "bounds": [ [ -180.0, @@ -360,10 +366,11 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_historical_nosub_hist_0", - "indicator_id": "flood_depth/nosub", + "indicator_id": "flood_depth", + "indicator_model_id": "nosub", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/Baseline no subsidence", + "display_name": "Flood depth/baseline, no subsidence (WRI)", "display_groups": [], "description": "\nWorld Resources Institute Aqueduct Floods baseline coastal model using historical data. Model excludes subsidence.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -376,7 +383,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_historical_nosub_hist_rp{return_period:04d}_0", + "path": "inuncoast_historical_nosub_hist_rp{return_period:04d}_0", "bounds": [ [ -180.0, @@ -411,12 +418,13 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_{scenario}_nosub_{year}_0", - "indicator_id": "flood_depth/nosub/95", + "indicator_id": "flood_depth", + "indicator_model_id": "nosub/95", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/95% no subsidence", + "display_name": "Flood depth/95%, no subsidence (WRI)", "display_groups": [], - "description": "\nWorld Resource Institute Aqueduct Floods coastal model, exclusing subsidence; 95th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", + "description": "\nWorld Resource Institute Aqueduct Floods coastal model, excluding subsidence; 95th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { "colormap": { "min_index": 1, @@ -427,7 +435,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_{scenario}_nosub_{year}_rp{return_period:04d}_0", + "path": "inuncoast_{scenario}_nosub_{year}_rp{return_period:04d}_0", "bounds": [ [ -180.0, @@ -473,9 +481,10 @@ "group_id": "public", "path": "inundation/wri/v2/inuncoast_{scenario}_nosub_{year}_0_perc_05", "indicator_id": "flood_depth/nosub/5", + "indicator_model_id": "nosub/5", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/5% no subsidence", + "display_name": "Flood depth/5%, no subsidence (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods coastal model, excluding subsidence; 5th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -488,7 +497,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_{scenario}_nosub_{year}_rp{return_period:04d}_0_perc_05", + "path": "inuncoast_{scenario}_nosub_{year}_rp{return_period:04d}_0_perc_05", "bounds": [ [ -180.0, @@ -533,10 +542,11 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_{scenario}_nosub_{year}_0_perc_50", - "indicator_id": "flood_depth/nosub/50", + "indicator_id": "flood_depth", + "indicator_model_id": "nosub/50", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/50% no subsidence", + "display_name": "Flood depth/50%, no subsidence (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods model, excluding subsidence; 50th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -549,7 +559,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_{scenario}_nosub_{year}_rp{return_period:04d}_0_perc_50", + "path": "inuncoast_{scenario}_nosub_{year}_rp{return_period:04d}_0_perc_50", "bounds": [ [ -180.0, @@ -594,10 +604,11 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_historical_wtsub_hist_0", - "indicator_id": "flood_depth/wtsub", + "indicator_id": "flood_depth", + "indicator_model_id": "wtsub", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/Baseline with subsidence", + "display_name": "Flood depth/baseline, with subsidence (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods model, excluding subsidence; baseline (based on historical data).\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -610,7 +621,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_historical_wtsub_hist_rp{return_period:04d}_0", + "path": "inuncoast_historical_wtsub_hist_rp{return_period:04d}_0", "bounds": [ [ -180.0, @@ -645,10 +656,11 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_{scenario}_wtsub_{year}_0", - "indicator_id": "flood_depth/wtsub/95", + "indicator_id": "flood_depth", + "indicator_model_id": "wtsub/95", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/95% with subsidence", + "display_name": "Flood depth/95%, with subsidence (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods model, including subsidence; 95th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -661,7 +673,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_{scenario}_wtsub_{year}_rp{return_period:04d}_0", + "path": "inuncoast_{scenario}_wtsub_{year}_rp{return_period:04d}_0", "bounds": [ [ -180.0, @@ -706,10 +718,11 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_{scenario}_wtsub_{year}_0_perc_05", - "indicator_id": "flood_depth/wtsub/5", + "indicator_id": "flood_depth", + "indicator_model_id": "wtsub/5", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/5% with subsidence", + "display_name": "Flood depth/5%, with subsidence (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods model, including subsidence; 5th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -722,7 +735,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_{scenario}_wtsub_{year}_rp{return_period:04d}_0_perc_05", + "path": "inuncoast_{scenario}_wtsub_{year}_rp{return_period:04d}_0_perc_05", "bounds": [ [ -180.0, @@ -767,10 +780,11 @@ "hazard_type": "CoastalInundation", "group_id": "public", "path": "inundation/wri/v2/inuncoast_{scenario}_wtsub_{year}_0_perc_50", - "indicator_id": "flood_depth/wtsub/50", + "indicator_id": "flood_depth", + "indicator_model_id": "wtsub/50", "indicator_model_gcm": "unknown", "params": {}, - "display_name": "WRI/50% with subsidence", + "display_name": "Flood depth/50%, with subsidence (WRI)", "display_groups": [], "description": "\nWorld Resource Institute Aqueduct Floods model, including subsidence; 50th percentile sea level rise.\n\n \nThe World Resources Institute (WRI) [Aqueduct Floods model](https://www.wri.org/aqueduct) is an acute riverine and coastal flood hazard model with a spatial resolution of 30 \u00d7 30 arc seconds (approx. 1 km at the equator). Flood intensity is provided as a _return period_ map: each point comprises a curve of inundation depths for 9 different return periods (also known as reoccurrence periods). If a flood event has depth $d_i$ with return period of $r_i$ this implies that the probability of a flood event with depth greater than $d_i$ occurring in any one year is $1 / r_i$; this is the _exceedance probability_. Aqueduct Floods is based on Global Flood Risk with IMAGE Scenarios (GLOFRIS); see [here](https://www.wri.org/aqueduct/publications) for more details.\n\nFor more details and relevant citations see the\n[OS-Climate Physical Climate Risk Methodology document](https://github.com/os-climate/physrisk/blob/main/methodology/PhysicalRiskMethodology.pdf).", "map": { @@ -783,7 +797,7 @@ "nodata_index": 0, "units": "m" }, - "array_name": "inuncoast_{scenario}_wtsub_{year}_rp{return_period:04d}_0_perc_50", + "path": "inuncoast_{scenario}_wtsub_{year}_rp{return_period:04d}_0_perc_50", "bounds": [ [ -180.0, @@ -829,6 +843,7 @@ "group_id": "", "path": "chronic_heat/osc/v2/mean_degree_days_v2_above_32c_{gcm}_{scenario}_{year}", "indicator_id": "mean_degree_days/above/32c", + "indicator_model_id": null, "indicator_model_gcm": "{gcm}", "params": { "gcm": [ @@ -844,7 +859,7 @@ "display_groups": [ "Mean degree days" ], - "description": "Degree days indicators are calculated by integrating over time the absolute difference in temperature\nof the medium over a reference temperature. The exact method of calculation may vary;\nhere the daily maximum near-surface temperature 'tasmax' is used to calculate an annual indicator:\n$$\nI^\\text{dd} = \\sum_{i = 1}^{n_y} | T^\\text{max}_i - T^\\text{ref} | \\nonumber\n$$\n$I^\\text{dd}$ is the indicator, $T^\\text{max}$ is the daily maximum near-surface temperature, $n_y$ is the number of days in the year and $i$ is the day index.\nand $T^\\text{ref}$ is the reference temperature of 32\u00b0C. The OS-Climate-generated indicators are inferred\nfrom CMIP6 data, averaged over 6 models: ACCESS-CM2, CMCC-ESM2, CNRM-CM6-1, MPI-ESM1-2-LR, MIROC6 and NorESM2-MM.\nThe indicators are generated for periods: 'historical' (averaged over 1995-2014), 2030 (2021-2040), 2040 (2031-2050)\nand 2050 (2041-2060).", + "description": "Degree days indicators are calculated by integrating over time the absolute difference in temperature\nof the medium over a reference temperature. The exact method of calculation may vary;\nhere the daily maximum near-surface temperature 'tasmax' is used to calculate an annual indicator:\n$$\nI^\\text{dd} = \\frac{365}{n_y} \\sum_{i = 1}^{n_y} | T^\\text{max}_i - T^\\text{ref} | \\nonumber\n$$\n$I^\\text{dd}$ is the indicator, $T^\\text{max}$ is the daily maximum near-surface temperature, $n_y$ is the number of days in the year and $i$ is the day index.\nand $T^\\text{ref}$ is the reference temperature of 32\u00b0C. The OS-Climate-generated indicators are inferred\nfrom downscaled CMIP6 data, averaged over 6 models: ACCESS-CM2, CMCC-ESM2, CNRM-CM6-1, MPI-ESM1-2-LR, MIROC6 and NorESM2-MM.\nThe downscaled data is sourced from the [NASA Earth Exchange Global Daily Downscaled Projections](https://www.nccs.nasa.gov/services/data-collections/land-based-products/nex-gddp-cmip6).\nThe indicators are generated for periods: 'historical' (averaged over 1995-2014), 2030 (2021-2040), 2040 (2031-2050)\nand 2050 (2041-2060).", "map": { "colormap": { "min_index": 1, @@ -855,7 +870,7 @@ "nodata_index": 0, "units": "degree days" }, - "array_name": "mean_degree_days_v2_above_32c_{gcm}_{scenario}_{year}_map", + "path": "mean_degree_days_v2_above_32c_{gcm}_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -915,11 +930,12 @@ "group_id": "jupiter_osc", "path": "fire/jupiter/v1/fire_probability_{scenario}_{year}", "indicator_id": "fire_probability", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Fire probability", + "display_name": "Fire probability (Jupiter)", "display_groups": [], - "description": "\nThe maximum value, found across all months, of the probability of a wildfire occurring\nat some point in an individual month within 100km of the location. For example, if the probability\nof occurrence of a wildfire is 5% for July, 20% in August, 10% in September and 0% for\nother months, the hazard indicator value is 20%.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nThis fire model computes the maximum monthly probability per annum of a wildfire within 100 km of a given location based on several parameters from multiple bias corrected \nand downscaled Global Climate Models (GCMs).\nFor example, if the probability of occurrence of a wildfire is 5% in July, 20% in August, 10% in September and 0% for other months, the hazard indicator value is 20%.\n ", "map": { "colormap": { "min_index": 1, @@ -928,9 +944,9 @@ "max_value": 0.7, "name": "heating", "nodata_index": 0, - "units": "none" + "units": "" }, - "array_name": "fire_probability_{scenario}_{year}_map", + "path": "fire_probability_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -975,18 +991,19 @@ ] } ], - "units": "none" + "units": "" }, { "hazard_type": "Drought", "group_id": "jupiter_osc", "path": "drought/jupiter/v1/months_spei3m_below_-2_{scenario}_{year}", "indicator_id": "months/spei3m/below/-2", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Drought", + "display_name": "Drought (Jupiter)", "display_groups": [], - "description": "\nMonths per year where the rolling 3-month averaged Standardized Precipitation Evapotranspiration Index \nis below -2.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nThis drought model is based on the Standardized Precipitation-Evapotranspiration Index (SPEI). \nThe SPEl is an extension of the Standardized Precipitation Index which also considers Potential Evapotranspiration (PET) in determining drought events. \nThe SPEl is calculated from a log-logistic probability distribution function of climatic water balance (precipitation minus evapotranspiration) over a given time scale. \nThe SPEI itself is a standardized variable with a mean value 0 and standard deviation 1. \nThis drought model computes the number of months per annum where the 3-month rolling average of SPEI is below -2 based on the mean values of several parameters from \nbias-corrected and downscaled multiple Global Climate Models (GCMs).\n ", "map": { "colormap": { "min_index": 1, @@ -997,7 +1014,7 @@ "nodata_index": 0, "units": "months/year" }, - "array_name": "months_spei3m_below_-2_{scenario}_{year}_map", + "path": "months_spei3m_below_-2_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1049,11 +1066,12 @@ "group_id": "jupiter_osc", "path": "precipitation/jupiter/v1/max_daily_water_equivalent_{scenario}_{year}", "indicator_id": "max/daily/water_equivalent", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Precipitation", + "display_name": "Precipitation (Jupiter)", "display_groups": [], - "description": "\nMaximum daily total water equivalent precipitation experienced at a return period of 100 years.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nThis model computes the maximum daily water equivalent precipitation (in mm) measured at the 100 year return period based on the mean of the precipitation distribution \nfrom multiple bias corrected and downscaled Global Climate Models (GCMs).\n ", "map": { "colormap": { "min_index": 1, @@ -1064,7 +1082,7 @@ "nodata_index": 0, "units": "mm" }, - "array_name": "max_daily_water_equivalent_{scenario}_{year}_map", + "path": "max_daily_water_equivalent_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1116,11 +1134,12 @@ "group_id": "jupiter_osc", "path": "hail/jupiter/v1/days_above_5cm_{scenario}_{year}", "indicator_id": "days/above/5cm", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Large hail days per year", + "display_name": "Large hail days per year (Jupiter)", "display_groups": [], - "description": "\nNumber of days per year where large hail (> 5cm diameter) is possible.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nThis hail model computes the number of days per annum where hail exceeding 5 cm diameter is possible based on the mean distribution of several parameters \nacross multiple bias-corrected and downscaled Global Climate Models (GCMs).\n ", "map": { "colormap": { "min_index": 1, @@ -1131,7 +1150,7 @@ "nodata_index": 0, "units": "days/year" }, - "array_name": "days_above_5cm_{scenario}_{year}_map", + "path": "days_above_5cm_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1183,11 +1202,12 @@ "group_id": "jupiter_osc", "path": "chronic_heat/jupiter/v1/days_above_35c_{scenario}_{year}", "indicator_id": "days/above/35c", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Days per year above 35\u00b0C", + "display_name": "Days per year above 35\u00b0C (Jupiter)", "display_groups": [], - "description": "\nMaximum daily total water equivalent precipitation experienced at a return period of 200 years.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nThis heat model computes the number of days exceeding 35\u00b0C per annum based on the mean of distribution fits to the bias-corrected and downscaled high temperature distribution \nacross multiple Global Climate Models (GCMs).\n ", "map": { "colormap": { "min_index": 1, @@ -1196,9 +1216,9 @@ "max_value": 365.0, "name": "heating", "nodata_index": 0, - "units": "mm" + "units": "days/year" }, - "array_name": "days_above_35c_{scenario}_{year}_map", + "path": "days_above_35c_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1250,11 +1270,12 @@ "group_id": "jupiter_osc", "path": "wind/jupiter/v1/max_1min_{scenario}_{year}", "indicator_id": "max/1min", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Max 1 minute sustained wind speed", + "display_name": "Max 1 minute sustained wind speed (Jupiter)", "display_groups": [], - "description": "\nMaximum 1-minute sustained wind speed in km/hour experienced at different return periods.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nThis wind speed model computes the maximum 1-minute sustained wind speed (in km/hr) experienced over a 100 year return period based on mean wind speed distributions \nfrom multiple Global Climate Models (GCMs).\n ", "map": { "colormap": { "min_index": 1, @@ -1265,7 +1286,7 @@ "nodata_index": 0, "units": "km/hour" }, - "array_name": "max_1min_{scenario}_{year}_map", + "path": "max_1min_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1317,11 +1338,12 @@ "group_id": "jupiter_osc", "path": "combined_flood/jupiter/v1/fraction_{scenario}_{year}", "indicator_id": "flooded_fraction", + "indicator_model_id": null, "indicator_model_gcm": "unknown", "params": {}, - "display_name": "Flooded fraction", + "display_name": "Flooded fraction (Jupiter)", "display_groups": [], - "description": "\nThe fraction of land within a 30-km grid cell that experiences flooding at different return periods.\n ", + "description": "\nThese data should not be used in any manner relating to emergency management or planning, public safety, physical safety or property endangerment. \nFor higher-resolution data based on up-to-date methods, subject to greater validation, and suitable for bottom-up risk analysis please contact \n[Jupiter Intelligence](https://www.jupiterintel.com).\n\nFlooded fraction provides the spatial fraction of land flooded in a defined grid. \nIt is derived from higher-resolution flood hazards, and computed directly as the fraction of cells within the 30-km cell that have non-zero flooding at that return period. \nThis model uses a 30-km grid that experiences flooding at the 200-year return period.\nOpen oceans are excluded.\n ", "map": { "colormap": { "min_index": 1, @@ -1330,9 +1352,9 @@ "max_value": 1.0, "name": "heating", "nodata_index": 0, - "units": "none" + "units": "" }, - "array_name": "fraction_{scenario}_{year}_map", + "path": "fraction_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1384,6 +1406,7 @@ "group_id": "", "path": "chronic_heat/osc/v2/mean_work_loss_{intensity}_{gcm}_{scenario}_{year}", "indicator_id": "mean_work_loss/{intensity}", + "indicator_model_id": null, "indicator_model_gcm": "{gcm}", "params": { "intensity": [ @@ -1415,7 +1438,7 @@ "nodata_index": 0, "units": "fractional loss" }, - "array_name": "mean_work_loss_{intensity}_{gcm}_{scenario}_{year}_map", + "path": "mean_work_loss_{intensity}_{gcm}_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1475,6 +1498,7 @@ "group_id": "", "path": "chronic_heat/osc/v2/days_tas_above_{temp_c}c_{gcm}_{scenario}_{year}", "indicator_id": "days_tas/above/{temp_c}c", + "indicator_model_id": null, "indicator_model_gcm": "{gcm}", "params": { "temp_c": [ @@ -1495,11 +1519,11 @@ "NorESM2-MM" ] }, - "display_name": "Days with average temperature above {temp_c}C/{gcm}", + "display_name": "Days with average temperature above {temp_c}\u00b0C/{gcm}", "display_groups": [ "Days with average temperature above" ], - "description": "Add description here.", + "description": "Days per year for which the average near-surface temperature 'tas' is above a threshold specified in \u00b0C.\n\n$$\nI = \\frac{365}{n_y} \\sum_{i = 1}^{n_y} \\boldsymbol{\\mathbb{1}}_{\\; \\, T^{avg}_i > T^\\text{ref}} \\nonumber\n$$\n$I$ is the indicator, $T^\\text{avg}_i$ is the daily average near-surface temperature for day index $i$ in \u00b0C, $n_y$ is the number of days in the year\nand $T^\\text{ref}$ is the reference temperature.\nThe OS-Climate-generated indicators are inferred from downscaled CMIP6 data. This is done for 6 Global Circulation Models: ACCESS-CM2, CMCC-ESM2, CNRM-CM6-1, MPI-ESM1-2-LR, MIROC6 and NorESM2-MM.\nThe downscaled data is sourced from the [NASA Earth Exchange Global Daily Downscaled Projections](https://www.nccs.nasa.gov/services/data-collections/land-based-products/nex-gddp-cmip6).\nIndicators are generated for periods: 'historical' (averaged over 1995-2014), 2030 (2021-2040), 2040 (2031-2050)\nand 2050 (2041-2060).", "map": { "colormap": { "min_index": 1, @@ -1508,9 +1532,9 @@ "max_value": 100.0, "name": "heating", "nodata_index": 0, - "units": "days" + "units": "days/year" }, - "array_name": "days_tas_above_{temp_c}c_{gcm}_{scenario}_{year}_map", + "path": "days_tas_above_{temp_c}c_{gcm}_{scenario}_{year}_map", "bounds": [ [ -180.0, @@ -1563,7 +1587,7 @@ ] } ], - "units": "days" + "units": "days/year" } ] } \ No newline at end of file diff --git a/src/physrisk/data/zarr_reader.py b/src/physrisk/data/zarr_reader.py index 101fec56..b8125a02 100644 --- a/src/physrisk/data/zarr_reader.py +++ b/src/physrisk/data/zarr_reader.py @@ -97,10 +97,7 @@ def get_curves(self, set_id, longitudes, latitudes, interpolation="floor"): transform = Affine(t[0], t[1], t[2], t[3], t[4], t[5]) # in the case of acute risks, index_values will contain the return periods - index_values = z.attrs.get("index_values", [0]) - if index_values is None: - index_values = [0] - + index_values = self.get_index_values(z) image_coords = self._get_coordinates(longitudes, latitudes, transform) if interpolation == "floor": @@ -119,6 +116,12 @@ def get_curves(self, set_id, longitudes, latitudes, interpolation="floor"): else: raise ValueError("interpolation must have value 'floor', 'linear', 'max' or 'min") + def get_index_values(self, z: zarr.Array): + index_values = z.attrs.get("index_values", [0]) + if index_values is None: + index_values = [0] + return index_values + def get_max_curves(self, set_id, longitudes, latitudes, interpolation="floor", delta_km=1.0, n_grid=5): """Get maximal intensity curve for a grid around a given latitude and longitude coordinate pair. diff --git a/src/physrisk/hazard_models/core_hazards.py b/src/physrisk/hazard_models/core_hazards.py new file mode 100644 index 00000000..55a26ef1 --- /dev/null +++ b/src/physrisk/hazard_models/core_hazards.py @@ -0,0 +1,137 @@ +from typing import Dict, Iterable, NamedTuple, Optional, Protocol + +from physrisk.api.v1.hazard_data import HazardResource +from physrisk.data.hazard_data_provider import HazardDataHint, SourcePath +from physrisk.data.inventory import EmbeddedInventory, Inventory +from physrisk.kernel import hazards +from physrisk.kernel.hazards import ChronicHeat, CoastalInundation, RiverineInundation + + +class ResourceSubset: + def __init__(self, resources: Iterable[HazardResource]): + self.resources = resources + + def first(self): + return next(r for r in self.resources) + + def with_model_gcm(self, gcm): + return ResourceSubset(r for r in self.resources if r.indicator_model_gcm == gcm) + + def with_model_id(self, gcm): + return ResourceSubset(r for r in self.resources if r.indicator_model_id == gcm) + + +class ResourceSelector(Protocol): + """For a particular hazard type and indicator_id (specifying the type of indicator), + defines the rule for selecting a resource from + all matches. The selection rule depends on scenario and year.""" + + def __call__(self, *, candidates: ResourceSubset, scenario: str, year: int) -> HazardResource: + ... + + +class ResourceSelectorKey(NamedTuple): + hazard_type: type + indicator_id: str + + +class InventorySourcePaths: + """Class used to generate SourcePaths by selecting the appropriate HazardResource from the + Inventory of HazardResources. + """ + + def __init__(self, inventory: Inventory): + self._inventory = inventory + self._selectors: Dict[ResourceSelectorKey, ResourceSelector] = {} + + def source_paths(self) -> Dict[type, SourcePath]: + all_hazard_types = list(set(htype for ((htype, _), _) in self._inventory.resources_by_type_id.items())) + source_paths: Dict[type, SourcePath] = {} + for hazard_type in all_hazard_types: + source_paths[hazards.hazard_class(hazard_type)] = self._get_resource_source_path( + hazard_type, + ) + return source_paths + + def _add_selector(self, hazard_type: type, indicator_id: str, selector: ResourceSelector): + self._selectors[ResourceSelectorKey(hazard_type, indicator_id)] = selector + + def _get_resource_source_path(self, hazard_type: str): + def _get_source_path(*, indicator_id: str, scenario: str, year: int, hint: Optional[HazardDataHint] = None): + # all matching resources in the inventory + selector = self._selectors.get( + ResourceSelectorKey(hazard_type=hazards.hazard_class(hazard_type), indicator_id=indicator_id), + self._no_selector, + ) + resources = self._inventory.resources_by_type_id[(hazard_type, indicator_id)] + resource = selector(candidates=ResourceSubset(resources), scenario=scenario, year=year) + if hint is not None: + return hint.path + proxy_scenario = cmip6_scenario_to_rcp(scenario) if resource.scenarios[0].id.startswith("rcp") else scenario + if scenario == "historical": + year = next(y for y in next(s for s in resource.scenarios if s.id == "historical").years) + return resource.path.format(id=indicator_id, scenario=proxy_scenario, year=year) + + return _get_source_path + + @staticmethod + def _no_selector(candidates: ResourceSubset, scenario: str, year: int): + return candidates.first() + + +class CoreInventorySourcePaths(InventorySourcePaths): + def __init__(self, inventory: Inventory): + super().__init__(inventory) + for indicator_id in ["mean_work_loss/low", "mean_work_loss/medium", "mean_work_loss/high"]: + self._add_selector(ChronicHeat, indicator_id, self._select_chronic_heat) + self._add_selector(ChronicHeat, "mean/degree/days/above/32c", self._select_chronic_heat) + self._add_selector(RiverineInundation, "flood_depth", self._select_riverine_inundation) + self._add_selector(CoastalInundation, "flood_depth", self._select_coastal_inundation) + + def resources_with(self, *, hazard_type: type, indicator_id: str): + return ResourceSubset(self._inventory.resources_by_type_id[(hazard_type.__name__, indicator_id)]) + + @staticmethod + def _select_chronic_heat(candidates: ResourceSubset, scenario: str, year: int): + return candidates.with_model_gcm("ACCESS-CM2").first() + + @staticmethod + def _select_coastal_inundation(candidates: ResourceSubset, scenario: str, year: int): + return ( + candidates.with_model_id("nosub").first() + if scenario == "historical" + else candidates.with_model_id("wtsub/95").first() + ) + + @staticmethod + def _select_riverine_inundation(candidates: ResourceSubset, scenario: str, year: int): + return ( + candidates.with_model_gcm("historical").first() + if scenario == "historical" + else candidates.with_model_gcm("MIROC-ESM-CHEM").first() + ) + + +def cmip6_scenario_to_rcp(scenario: str): + """Convention is that CMIP6 scenarios are expressed by identifiers: + SSP1-2.6: 'ssp126' + SSP2-4.5: 'ssp245' + SSP5-8.5: 'ssp585' etc. + Here we translate to form + RCP-4.5: 'rcp4p5' + RCP-8.5: 'rcp8p5' etc. + """ + if scenario == "ssp126": + return "rcp2p6" + elif scenario == "ssp245": + return "rcp4p5" + elif scenario == "ssp585": + return "rcp8p5" + else: + if scenario not in ["rcp2p6", "rcp4p5", "rcp8p5", "historical"]: + raise ValueError(f"unexpected scenario {scenario}") + return scenario + + +def get_default_source_paths(inventory: Inventory = EmbeddedInventory()): + return CoreInventorySourcePaths(inventory).source_paths() diff --git a/src/physrisk/hazard_models/embedded.py b/src/physrisk/hazard_models/embedded.py deleted file mode 100644 index 3a1ca0ac..00000000 --- a/src/physrisk/hazard_models/embedded.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import Callable, Dict, List, Optional, Tuple - -from physrisk.api.v1.hazard_data import HazardResource -from physrisk.data.hazard_data_provider import HazardDataHint, SourcePath -from physrisk.data.inventory import EmbeddedInventory, Inventory -from physrisk.kernel import hazards -from physrisk.kernel.hazards import ChronicHeat, CoastalInundation, RiverineInundation - -Selector = Callable[[List[HazardResource], str, int], HazardResource] - - -class ResourceSelector: - def __init__(self): - self._selectors: Dict[Tuple[type, str], Selector] = {} - - def select_resource( - self, hazard_type: type, indicator_id: str, resources: List[HazardResource], scenario: str, year: int - ): - if (hazard_type, indicator_id) not in self._selectors: - return resources[0] - return self._selectors[(hazard_type, indicator_id)](resources, scenario, year) - - def _add_selector(self, hazard_type: type, indicator_id: str, selector: Selector): - self._selectors[(hazard_type, indicator_id)] = selector - - def _use_gcm(self, gcm: str) -> Selector: - selector: Selector = lambda resources, scenario, year: next( - r for r in resources if r.indicator_model_gcm == gcm - ) - return selector - - -class EmbeddedResourceSelector(ResourceSelector): - def __init__(self): - super().__init__() - - # whether for retrospective or prospective scenarios use GCM: - self._add_selector(ChronicHeat, "mean_degree_days/above/32c", self._use_gcm("ACCESS-CM2")) - - for intensity in ["low", "medium", "high"]: - self._add_selector(ChronicHeat, f"mean_work_loss/{intensity}", self._use_gcm("ACCESS-CM2")) - - # for retrospective scenarios use historical data (as opposed to bias-corrected retrospective run of GCM): - selector: Selector = ( - lambda resources, scenario, year: next(r for r in resources if r.indicator_model_gcm == "historical") - if scenario == "historical" - else next(r for r in resources if r.indicator_model_gcm == "MIROC-ESM-CHEM") - ) - self._add_selector(RiverineInundation, "flood_depth", selector) - self._add_selector(CoastalInundation, "flood_depth", selector) - - -def cmip6_scenario_to_rcp(scenario: str): - """Convention is that CMIP6 scenarios are expressed by identifiers: - SSP1-2.6: 'ssp126' - SSP2-4.5: 'ssp245' - SSP5-8.5: 'ssp585' etc. - Here we translate to form - RCP-4.5: 'rcp4p5' - RCP-8.5: 'rcp8p5' etc. - """ - if scenario == "ssp126": - return "rcp2p6" - elif scenario == "ssp245": - return "rcp4p5" - elif scenario == "ssp585": - return "rcp8p5" - else: - if scenario not in ["rcp2p6", "rcp4p5", "rcp8p5", "historical"]: - raise ValueError(f"unexpected scenario {scenario}") - return scenario - - -def get_default_source_paths(inventory: Inventory = EmbeddedInventory()): - return get_source_paths(inventory, EmbeddedResourceSelector()) - - -def get_source_paths(inventory: Inventory, selector: ResourceSelector): - all_hazard_types = list(set(htype for ((htype, _), _) in inventory.resources_by_type_id.items())) - source_paths: Dict[type, SourcePath] = {} - for hazard_type in all_hazard_types: - source_paths[hazards.hazard_class(hazard_type)] = get_resource_source_path(hazard_type, inventory, selector) - return source_paths - - -def get_resource_source_path(hazard_type: str, inventory: Inventory, selector: ResourceSelector): - def get_source_path(*, indicator_id: str, scenario: str, year: int, hint: Optional[HazardDataHint] = None): - # all matching resources in the inventory - if hint is not None: - return hint.path - resources = inventory.resources_by_type_id[hazard_type, indicator_id] - selected_resource = selector.select_resource( - hazards.hazard_class(hazard_type), indicator_id, resources, scenario, year - ) - proxy_scenario = ( - cmip6_scenario_to_rcp(scenario) if selected_resource.scenarios[0].id.startswith("rcp") else scenario - ) - if scenario == "historical": - year = next(y for y in next(s for s in selected_resource.scenarios if s.id == "historical").years) - return selected_resource.path.format(id=indicator_id, scenario=proxy_scenario, year=year) - - return get_source_path diff --git a/src/physrisk/kernel/__init__.py b/src/physrisk/kernel/__init__.py index a0379eff..41f35bb3 100644 --- a/src/physrisk/kernel/__init__.py +++ b/src/physrisk/kernel/__init__.py @@ -1,5 +1,4 @@ from .assets import Asset, PowerGeneratingAsset -from .calculation import calculate_impacts from .curve import ExceedanceCurve from .hazard_event_distrib import HazardEventDistrib from .hazards import Drought, Hazard, RiverineInundation diff --git a/src/physrisk/kernel/asset_impact.py b/src/physrisk/kernel/asset_impact.py deleted file mode 100644 index bba8ed0c..00000000 --- a/src/physrisk/kernel/asset_impact.py +++ /dev/null @@ -1,34 +0,0 @@ -# import numpy.typing as npt -from abc import ABC, abstractmethod - - -class AssetImpact: - """Calculates the impacts associated with a portfolio of assets.""" - - def __init__(self, assets, vulnerabilities): - pass - - -class AssetEventProvider(ABC): - @abstractmethod - def get_asset_events(self, assets, event_types): - """Source event distributions in the locale of each asset for events of certain types""" - - -class ModelsBuilder(ABC): - """Provides VulnerabilityModels and EventProviders for a type of aset.""" - - @abstractmethod - def get_vulnerability_model(self, asset_type): - pass - - @abstractmethod - def get_event_data_provider(self, asset_type): - """Return a list of backends matching the specified filtering. - Args: - asset_type (AssetType): type of asset. - Returns: - dict[EventType, AssetEvents]: a list of Backends that match the filtering - criteria. - """ - pass diff --git a/src/physrisk/kernel/calculation.py b/src/physrisk/kernel/calculation.py index ff11840e..5d34c8e3 100644 --- a/src/physrisk/kernel/calculation.py +++ b/src/physrisk/kernel/calculation.py @@ -1,53 +1,27 @@ -import logging -from collections import defaultdict -from dataclasses import dataclass -from typing import Any, Dict, Iterable, List, Optional, Tuple, Union - -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel.exposure import Category, DataRequester, ExposureMeasure - -from ..data.pregenerated_hazard_model import ZarrHazardModel -from ..utils.helpers import get_iterable -from ..vulnerability_models import power_generating_asset_models as pgam -from ..vulnerability_models.chronic_heat_models import ChronicHeatGZNModel -from ..vulnerability_models.real_estate_models import ( +from typing import Dict, Sequence + +from physrisk.data.pregenerated_hazard_model import ZarrHazardModel +from physrisk.hazard_models.core_hazards import get_default_source_paths +from physrisk.kernel.risk import RiskMeasureCalculator +from physrisk.risk_models.risk_models import RealEstateToyRiskMeasures +from physrisk.vulnerability_models import power_generating_asset_models as pgam +from physrisk.vulnerability_models.chronic_heat_models import ChronicHeatGZNModel +from physrisk.vulnerability_models.real_estate_models import ( RealEstateCoastalInundationModel, RealEstateRiverineInundationModel, ) -from .assets import Asset, IndustrialActivity, PowerGeneratingAsset, RealEstateAsset, TestAsset -from .hazard_event_distrib import HazardEventDistrib -from .hazard_model import HazardDataRequest, HazardDataResponse, HazardModel -from .impact_distrib import ImpactDistrib -from .vulnerability_distrib import VulnerabilityDistrib -from .vulnerability_model import VulnerabilityModelAcuteBase, VulnerabilityModelBase - - -@dataclass -class AssetExposureResult: - hazard_categories: Dict[type, Tuple[Category, float]] - -class AssetImpactResult: - def __init__( - self, - impact: ImpactDistrib, - vulnerability: Optional[VulnerabilityDistrib] = None, - event: Optional[HazardEventDistrib] = None, - hazard_data: Optional[Iterable[HazardDataResponse]] = None, - ): - self.impact = impact - # optional detailed results for drill-down - self.hazard_data = hazard_data - self.vulnerability = vulnerability - self.event = event +from .assets import IndustrialActivity, PowerGeneratingAsset, RealEstateAsset, TestAsset +from .hazard_model import HazardModel +from .vulnerability_model import VulnerabilityModelBase -def get_default_hazard_model(): +def get_default_hazard_model() -> HazardModel: # Model that gets hazard event data from Zarr storage return ZarrHazardModel(source_paths=get_default_source_paths()) -def get_default_vulnerability_models(): +def get_default_vulnerability_models() -> Dict[type, Sequence[VulnerabilityModelBase]]: """Get default exposure/vulnerability models for different asset types.""" return { PowerGeneratingAsset: [pgam.InundationModel()], @@ -57,91 +31,6 @@ def get_default_vulnerability_models(): } -def calculate_exposures( - assets: List[Asset], hazard_model: HazardModel, exposure_measure: ExposureMeasure, scenario: str, year: int -) -> Dict[Asset, AssetExposureResult]: - requester_assets: Dict[DataRequester, List[Asset]] = {exposure_measure: assets} - assetRequests, responses = _request_consolidated(hazard_model, requester_assets, scenario, year) - - logging.info( - "Applying exposure measure {0} to {1} assets of type {2}".format( - type(exposure_measure).__name__, len(assets), type(assets[0]).__name__ - ) - ) - result: Dict[Asset, AssetExposureResult] = {} - - for asset in assets: - requests = assetRequests[(exposure_measure, asset)] # (ordered) requests for a given asset - hazard_data = [responses[req] for req in get_iterable(requests)] - result[asset] = AssetExposureResult(hazard_categories=exposure_measure.get_exposures(asset, hazard_data)) - - return result - - -def calculate_impacts( # noqa: C901 - assets: Iterable[Asset], - hazard_model: Optional[HazardModel] = None, - vulnerability_models: Optional[Any] = None, #: Optional[Dict[type, Sequence[VulnerabilityModelBase]]] = None, - *, - scenario: str, - year: int, -) -> Dict[Tuple[Asset, type], AssetImpactResult]: - """Calculate asset level impacts.""" - - if hazard_model is None: - hazard_model = get_default_hazard_model() - # the models that apply to asset of a particular type - if vulnerability_models is None: - vulnerability_models = get_default_vulnerability_models() - model_assets: Dict[DataRequester, List[Asset]] = defaultdict( - list - ) # list of assets to be modelled using vulnerability model - - for asset in assets: - asset_type = type(asset) - mappings = vulnerability_models[asset_type] - for m in mappings: - model_assets[m].append(asset) - results = {} - asset_requests, responses = _request_consolidated(hazard_model, model_assets, scenario, year) - - logging.info("Calculating impacts") - for model, assets in model_assets.items(): - logging.info( - "Applying model {0} to {1} assets of type {2}".format( - type(model).__name__, len(assets), type(assets[0]).__name__ - ) - ) - for asset in assets: - requests = asset_requests[(model, asset)] - hazard_data = [responses[req] for req in get_iterable(requests)] - if isinstance(model, VulnerabilityModelAcuteBase): - impact, vul, event = model.get_impact_details(asset, hazard_data) - results[(asset, model.hazard_type)] = AssetImpactResult( - impact, vulnerability=vul, event=event, hazard_data=hazard_data - ) - elif isinstance(model, VulnerabilityModelBase): - impact = model.get_impact(asset, hazard_data) - results[(asset, model.hazard_type)] = AssetImpactResult(impact, hazard_data=hazard_data) - return results - - -def _request_consolidated( - hazard_model: HazardModel, requester_assets: Dict[DataRequester, List[Asset]], scenario: str, year: int -): - """As an important performance optimization, data requests are consolidated for all requesters - (e.g. vulnerability mode) because different requesters may query the same hazard data sets - note that key for a single request is (requester, asset). - """ - # the list of requests for each requester and asset - asset_requests: Dict[Tuple[DataRequester, Asset], Union[HazardDataRequest, Iterable[HazardDataRequest]]] = {} - - logging.info("Generating hazard data requests for requesters") - for requester, assets in requester_assets.items(): - for asset in assets: - asset_requests[(requester, asset)] = requester.get_data_requests(asset, scenario=scenario, year=year) - - logging.info("Retrieving hazard data") - flattened_requests = [req for requests in asset_requests.values() for req in get_iterable(requests)] - responses = hazard_model.get_hazard_events(flattened_requests) - return asset_requests, responses +def get_default_risk_measure_calculators() -> Dict[type, RiskMeasureCalculator]: + """For asset-level risk measure, define the measure calculators to use.""" + return {RealEstateAsset: RealEstateToyRiskMeasures()} diff --git a/src/physrisk/kernel/exposure.py b/src/physrisk/kernel/exposure.py index 5c6b3f8a..90e2af81 100644 --- a/src/physrisk/kernel/exposure.py +++ b/src/physrisk/kernel/exposure.py @@ -1,15 +1,18 @@ +import logging import math from abc import abstractmethod from dataclasses import dataclass from enum import Enum -from typing import Dict, Iterable, Tuple +from typing import Dict, Iterable, List, Tuple import numpy as np from physrisk.kernel.assets import Asset -from physrisk.kernel.hazard_model import HazardDataRequest, HazardDataResponse, HazardParameterDataResponse +from physrisk.kernel.hazard_model import HazardDataRequest, HazardDataResponse, HazardModel, HazardParameterDataResponse from physrisk.kernel.hazards import ChronicHeat, CombinedInundation, Drought, Fire, Hail, Wind +from physrisk.kernel.impact import _request_consolidated from physrisk.kernel.vulnerability_model import DataRequester +from physrisk.utils.helpers import get_iterable class Category(Enum): @@ -30,6 +33,11 @@ class Bounds: upper: float +@dataclass +class AssetExposureResult: + hazard_categories: Dict[type, Tuple[Category, float]] + + class ExposureMeasure(DataRequester): @abstractmethod def get_exposures( @@ -131,3 +139,24 @@ def bounds_to_lookup(self, bounds: Iterable[Bounds]): lower_bounds = np.array([b.lower for b in bounds]) categories = np.array([b.category for b in bounds]) return (lower_bounds, categories) + + +def calculate_exposures( + assets: List[Asset], hazard_model: HazardModel, exposure_measure: ExposureMeasure, scenario: str, year: int +) -> Dict[Asset, AssetExposureResult]: + requester_assets: Dict[DataRequester, List[Asset]] = {exposure_measure: assets} + assetRequests, responses = _request_consolidated(hazard_model, requester_assets, scenario, year) + + logging.info( + "Applying exposure measure {0} to {1} assets of type {2}".format( + type(exposure_measure).__name__, len(assets), type(assets[0]).__name__ + ) + ) + result: Dict[Asset, AssetExposureResult] = {} + + for asset in assets: + requests = assetRequests[(exposure_measure, asset)] # (ordered) requests for a given asset + hazard_data = [responses[req] for req in get_iterable(requests)] + result[asset] = AssetExposureResult(hazard_categories=exposure_measure.get_exposures(asset, hazard_data)) + + return result diff --git a/src/physrisk/kernel/impact.py b/src/physrisk/kernel/impact.py new file mode 100644 index 00000000..3149b983 --- /dev/null +++ b/src/physrisk/kernel/impact.py @@ -0,0 +1,91 @@ +import logging +from collections import defaultdict +from dataclasses import dataclass +from typing import Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Union + +from physrisk.kernel.assets import Asset +from physrisk.kernel.hazard_event_distrib import HazardEventDistrib +from physrisk.kernel.hazard_model import HazardDataRequest, HazardDataResponse, HazardModel +from physrisk.kernel.impact_distrib import ImpactDistrib +from physrisk.kernel.vulnerability_distrib import VulnerabilityDistrib +from physrisk.kernel.vulnerability_model import DataRequester, VulnerabilityModelAcuteBase, VulnerabilityModelBase +from physrisk.utils.helpers import get_iterable + + +class ImpactKey(NamedTuple): + asset: Asset + hazard_type: type + # consider adding type: whether damage or disruption + + +@dataclass +class AssetImpactResult: + impact: ImpactDistrib + vulnerability: Optional[VulnerabilityDistrib] = None + event: Optional[HazardEventDistrib] = None + hazard_data: Optional[Iterable[HazardDataResponse]] = None # optional detailed results for drill-down + + +def calculate_impacts( # noqa: C901 + assets: Iterable[Asset], + hazard_model: HazardModel, + vulnerability_models: Dict[type, Sequence[VulnerabilityModelBase]], + *, + scenario: str, + year: int, +) -> Dict[ImpactKey, AssetImpactResult]: + """Calculate asset level impacts.""" + + model_assets: Dict[DataRequester, List[Asset]] = defaultdict( + list + ) # list of assets to be modelled using vulnerability model + + for asset in assets: + asset_type = type(asset) + mappings = vulnerability_models[asset_type] + for m in mappings: + model_assets[m].append(asset) + results = {} + + asset_requests, responses = _request_consolidated(hazard_model, model_assets, scenario, year) + + logging.info("Calculating impacts") + for model, assets in model_assets.items(): + logging.info( + "Applying vulnerability model {0} to {1} assets of type {2}".format( + type(model).__name__, len(assets), type(assets[0]).__name__ + ) + ) + for asset in assets: + requests = asset_requests[(model, asset)] + hazard_data = [responses[req] for req in get_iterable(requests)] + if isinstance(model, VulnerabilityModelAcuteBase): + impact, vul, event = model.get_impact_details(asset, hazard_data) + results[ImpactKey(asset, model.hazard_type)] = AssetImpactResult( + impact, vulnerability=vul, event=event, hazard_data=hazard_data + ) + elif isinstance(model, VulnerabilityModelBase): + impact = model.get_impact(asset, hazard_data) + results[ImpactKey(asset, model.hazard_type)] = AssetImpactResult(impact, hazard_data=hazard_data) + return results + + +def _request_consolidated( + hazard_model: HazardModel, requester_assets: Dict[DataRequester, List[Asset]], scenario: str, year: int +): + """As an important performance optimization, data requests are consolidated for all requesters + (e.g. vulnerability mode) because different requesters may query the same hazard data sets + note that key for a single request is (requester, asset). + """ + # the list of requests for each requester and asset + asset_requests: Dict[Tuple[DataRequester, Asset], Union[HazardDataRequest, Iterable[HazardDataRequest]]] = {} + + logging.info("Generating hazard data requests for requesters") + for requester, assets in requester_assets.items(): + for asset in assets: + asset_requests[(requester, asset)] = requester.get_data_requests(asset, scenario=scenario, year=year) + + logging.info("Retrieving hazard data") + flattened_requests = [req for requests in asset_requests.values() for req in get_iterable(requests)] + responses = hazard_model.get_hazard_events(flattened_requests) + return asset_requests, responses diff --git a/src/physrisk/kernel/impact_distrib.py b/src/physrisk/kernel/impact_distrib.py index 2c6f0998..8fd5d375 100644 --- a/src/physrisk/kernel/impact_distrib.py +++ b/src/physrisk/kernel/impact_distrib.py @@ -3,7 +3,7 @@ import numpy as np -from .curve import to_exceedance_curve +from physrisk.kernel.curve import to_exceedance_curve class ImpactType(Enum): diff --git a/src/physrisk/kernel/risk.py b/src/physrisk/kernel/risk.py new file mode 100644 index 00000000..27e1ef78 --- /dev/null +++ b/src/physrisk/kernel/risk.py @@ -0,0 +1,106 @@ +from typing import Dict, List, NamedTuple, Optional, Protocol, Sequence, Tuple + +from physrisk.api.v1.impact_req_resp import RiskMeasureResult +from physrisk.kernel.assets import Asset +from physrisk.kernel.hazard_model import HazardModel +from physrisk.kernel.impact import AssetImpactResult, calculate_impacts +from physrisk.kernel.impact_distrib import ImpactDistrib +from physrisk.kernel.vulnerability_model import VulnerabilityModelBase + +# from asyncio import ALL_COMPLETED +# import concurrent.futures + + +Impact = Dict[Tuple[Asset, type], AssetImpactResult] # the key is (Asset, Hazard type) + + +class BatchId(NamedTuple): + scenario: str + key_year: Optional[int] + + +class RiskModel: + """Base class for a risk model (i.e. a calculation of risk that makes use of hazard and vulnerability + models).""" + + def __init__(self, hazard_model: HazardModel, vulnerability_models: Dict[type, Sequence[VulnerabilityModelBase]]): + self._hazard_model = hazard_model + self._vulnerability_models = vulnerability_models + + def calculate_risk_measures(self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int]): + ... + + def _calculate_all_impacts(self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int]): + scenarios = set(["historical"] + list(prosp_scens)) + impact_results: Dict[BatchId, Impact] = {} + items = [(scenario, year) for scenario in scenarios for year in years] + for scenario, year in items: + key_year = None if scenario == "historical" else year + impact_results[BatchId(scenario, key_year)] = self._calculate_single_impact(assets, scenario, year) + return impact_results + # consider parallelizing using approach similar to: + # with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: + # future_to_url = {executor.submit(self._calculate_single_impact, assets, scenario, year): \ + # (scenario, year) for scenario in scenarios for year in years} + # for future in concurrent.futures.as_completed(future_to_url): + # tag = future_to_url[future] + # try: + # data = future.result() + # except Exception as exc: + # print('%r generated an exception: %s' % (tag, exc)) + # else: + # ... + + def _calculate_single_impact(self, assets: Sequence[Asset], scenario: str, year: int): + """Calculate impacts for a single scenario and year.""" + return calculate_impacts(assets, self._hazard_model, self._vulnerability_models, scenario=scenario, year=year) + + +class MeasureKey(NamedTuple): + asset: Asset + prosp_scen: str # prospective scenario + year: int + hazard_type: type + + +class RiskMeasureCalculator(Protocol): + def calc_measure(self, hazard_type: type, base_impact: ImpactDistrib, impact: ImpactDistrib) -> RiskMeasureResult: + ... + + def supported_hazards(self) -> List[type]: + ... + + +class AssetLevelRiskModel(RiskModel): + def __init__( + self, + hazard_model: HazardModel, + vulnerability_models: Dict[type, Sequence[VulnerabilityModelBase]], + measure_calculators: Dict[type, RiskMeasureCalculator], + ): + super().__init__(hazard_model, vulnerability_models) + self._measure_calculators = measure_calculators + + def calculate_impacts(self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int]): + impacts = self._calculate_all_impacts(assets, prosp_scens, years) + return impacts + + def calculate_risk_measures(self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int]): + impacts = self._calculate_all_impacts(assets, prosp_scens, years) + measures: Dict[MeasureKey, RiskMeasureResult] = {} + for asset in assets: + if type(asset) not in self._measure_calculators: + continue + measure_calc = self._measure_calculators[type(asset)] + for prosp_scen in prosp_scens: + for year in years: + scenario_impacts = impacts[(prosp_scen, year)] + for hazard_type in measure_calc.supported_hazards(): + key = (asset, hazard_type) + if key in scenario_impacts: + base_impact = impacts[("historical", None)][key].impact + impact = scenario_impacts[key].impact + risk_ind = measure_calc.calc_measure(hazard_type, base_impact, impact) + measures[MeasureKey(asset, prosp_scen, year, hazard_type)] = risk_ind + # if the fractional loss is material and materially increases + return impacts, measures diff --git a/src/physrisk/kernel/vulnerability_model.py b/src/physrisk/kernel/vulnerability_model.py index 14198e8c..e524f8a7 100644 --- a/src/physrisk/kernel/vulnerability_model.py +++ b/src/physrisk/kernel/vulnerability_model.py @@ -6,13 +6,13 @@ import numpy as np import physrisk.data.static.vulnerability +from physrisk.kernel.impact_distrib import ImpactDistrib from ..api.v1.common import VulnerabilityCurves from .assets import Asset from .curve import ExceedanceCurve from .hazard_event_distrib import HazardEventDistrib from .hazard_model import HazardDataRequest, HazardDataResponse, HazardEventDataResponse -from .impact_distrib import ImpactDistrib from .vulnerability_distrib import VulnerabilityDistrib from .vulnerability_matrix_provider import VulnMatrixProvider @@ -92,7 +92,7 @@ def get_distributions( Args: asset: the asset. - event_data_responses: the responses to the requests made by get_event_data_requests, in the same order. + event_data_responses: the responses to the requests made by get_data_requests, in the same order. """ ... @@ -107,7 +107,7 @@ def get_impact_details( Args: asset: the asset. - event_data_responses: the responses to the requests made by get_event_data_requests, in the same order. + 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, event_data_responses) impact_prob = vulnerability_dist.prob_matrix.T @ event_dist.prob diff --git a/src/physrisk/requests.py b/src/physrisk/requests.py index 913c2946..c846c687 100644 --- a/src/physrisk/requests.py +++ b/src/physrisk/requests.py @@ -14,8 +14,9 @@ from physrisk.data.inventory import expand from physrisk.data.inventory_reader import InventoryReader from physrisk.data.zarr_reader import ZarrReader -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel.exposure import JupterExposureMeasure +from physrisk.hazard_models.core_hazards import get_default_source_paths +from physrisk.kernel.exposure import JupterExposureMeasure, calculate_exposures +from physrisk.kernel.risk import AssetLevelRiskModel, BatchId, MeasureKey from .api.v1.hazard_data import ( HazardAvailabilityRequest, @@ -90,12 +91,13 @@ def get_image(self, *, request_dict): if not _read_permitted(request.group_ids, inventory.resources[request.resource]): raise PermissionError() model = inventory.resources[request.resource] - path = str(PosixPath(model.path).with_name(model.map.array_name)).format( + path = str(PosixPath(model.path).with_name(model.map.path)).format( scenario=request.scenarioId, year=request.year ) + colormap = request.colormap if request.colormap is not None else model.map.colormap.name creator = ImageCreator(zarr_reader) # store=ImageCreator.test_store(path)) return creator.convert( - path, colormap=request.colormap, min_value=request.min_value, max_value=request.max_value + path, colormap=colormap, tile=request.tile, min_value=request.min_value, max_value=request.max_value ) @@ -237,7 +239,7 @@ def create_assets(assets: Assets): def _get_asset_exposures(request: AssetExposureRequest, hazard_model: HazardModel): assets = create_assets(request.assets) measure = JupterExposureMeasure() - results = calc.calculate_exposures(assets, hazard_model, measure, scenario="ssp585", year=2030) + results = calculate_exposures(assets, hazard_model, measure, scenario="ssp585", year=2030) return AssetExposureResponse( items=[ AssetExposure( @@ -258,9 +260,22 @@ def _get_asset_impacts(request: AssetImpactRequest, hazard_model: HazardModel): # based on asset_class: assets = create_assets(request.assets) - results = calc.calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=request.scenario, year=request.year - ) + vulnerability_models = calc.get_default_vulnerability_models() + measure_calcs = calc.get_default_risk_measure_calculators() + risk_model = AssetLevelRiskModel(hazard_model, vulnerability_models, measure_calcs) + + scenarios = [request.scenario] + years = [request.year] + if request.include_measures: + batch_impacts, measures = risk_model.calculate_risk_measures(assets, scenarios, years) + else: + batch_impacts = risk_model.calculate_impacts(assets, scenarios, years) + measures = None + + # results = calculate_impacts( + # assets, hazard_model, vulnerability_models, scenario=request.scenario, year=request.year + # ) + results = batch_impacts[BatchId(scenarios[0], years[0])] # note that this does rely on ordering of dictionary (post 3.6) impacts: Dict[Asset, List[AssetSingleHazardImpact]] = {} @@ -284,9 +299,11 @@ def _get_asset_impacts(request: AssetImpactRequest, hazard_model: HazardModel): ) impact_exceedance = v.impact.to_exceedance_curve() + measure_key = MeasureKey(asset, scenarios[0], years[0], v.impact.hazard_type) hazard_impacts = AssetSingleHazardImpact( hazard_type=v.impact.hazard_type.__name__, impact_type=v.impact.impact_type.name, + risk_measure=None if measures is None or measure_key not in measures else measures[measure_key], impact_exceedance=ExceedanceCurve( values=impact_exceedance.values, exceed_probabilities=impact_exceedance.probs ), diff --git a/src/physrisk/kernel/loss_model.py b/src/physrisk/risk_models/loss_model.py similarity index 88% rename from src/physrisk/kernel/loss_model.py rename to src/physrisk/risk_models/loss_model.py index 19288c82..84a41a1d 100644 --- a/src/physrisk/kernel/loss_model.py +++ b/src/physrisk/risk_models/loss_model.py @@ -3,12 +3,14 @@ import numpy as np -from .assets import Asset -from .calculation import calculate_impacts, get_default_hazard_model, get_default_vulnerability_models -from .financial_model import FinancialModelBase -from .hazard_model import HazardModel -from .impact_distrib import ImpactDistrib, ImpactType -from .vulnerability_model import VulnerabilityModelAcuteBase +from physrisk.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.financial_model import FinancialModelBase +from ..kernel.hazard_model import HazardModel +from ..kernel.impact import calculate_impacts +from ..kernel.vulnerability_model import VulnerabilityModelBase class Aggregator(ABC): @@ -26,7 +28,7 @@ class LossModel: def __init__( self, hazard_model: Optional[HazardModel] = None, - vulnerability_models: Optional[Dict[type, Sequence[VulnerabilityModelAcuteBase]]] = None, + vulnerability_models: Optional[Dict[type, Sequence[VulnerabilityModelBase]]] = None, ): self.hazard_model = get_default_hazard_model() if hazard_model is None else hazard_model self.vulnerability_models = ( diff --git a/src/physrisk/risk_models/risk_models.py b/src/physrisk/risk_models/risk_models.py new file mode 100644 index 00000000..ee56da87 --- /dev/null +++ b/src/physrisk/risk_models/risk_models.py @@ -0,0 +1,51 @@ +from typing import List + +from physrisk.api.v1.impact_req_resp import Category, Indicator, RiskMeasureResult +from physrisk.kernel.hazards import CoastalInundation, Hazard, HazardKind, RiverineInundation +from physrisk.kernel.impact_distrib import ImpactDistrib +from physrisk.kernel.risk import RiskMeasureCalculator + + +class RealEstateToyRiskMeasures(RiskMeasureCalculator): + """Toy model for calculating risk measures for real estate assets.""" + + def __init__(self): + self.model_summary = {"*Toy* model for real estate risk assessment."} + self._category_defns = { + Category.NODATA: "No information.", + Category.LOW: "Marginal impact on real estate valuation very unlikely under RCP 8.5.", + Category.MEDIUM: "Material marginal impact on real estate valuation unlikely under RCP 8.5.", + Category.HIGH: "Material marginal impact on real estate valuation possible under RCP 8.5.", + Category.REDFLAG: "Material marginal impact on real estate valuation likely under RCP 8.5 " + "with possible impact on availability of insurance.", + } + + def calc_measure(self, hazard_type: type, base_impact: ImpactDistrib, impact: ImpactDistrib) -> RiskMeasureResult: + if Hazard.kind(base_impact.hazard_type) == HazardKind.acute: + return_period = 100.0 # criterion based on 1 in 100-year flood events + histo_loss = base_impact.to_exceedance_curve().get_value(1.0 / return_period) + future_loss = impact.to_exceedance_curve().get_value(1.0 / return_period) + loss_increase = future_loss - histo_loss + + if loss_increase > 0.3: + category = Category.REDFLAG + elif loss_increase > 0.1: + category = Category.HIGH + elif loss_increase > 0.05: + category = Category.MEDIUM + else: + category = Category.LOW + + summary = ( + f"Projected 1-in-{return_period:0.0f} year annual loss " + f"increases by {loss_increase*100:0.0f}% of asset value over historical baseline. " + ) + cat_defn = self._category_defns[category] + indicator = Indicator(value=loss_increase, label=f"{loss_increase * 100:0.0f}%") + + return RiskMeasureResult(category=category, cat_defn=cat_defn, indicators=[indicator], summary=summary) + else: + raise NotImplementedError() + + def supported_hazards(self) -> List[type]: + return [RiverineInundation, CoastalInundation] diff --git a/src/physrisk/vulnerability_models/real_estate_models.py b/src/physrisk/vulnerability_models/real_estate_models.py index c8213a8b..ee61c48e 100644 --- a/src/physrisk/vulnerability_models/real_estate_models.py +++ b/src/physrisk/vulnerability_models/real_estate_models.py @@ -102,7 +102,7 @@ class RealEstateCoastalInundationModel(RealEstateInundationModel): def __init__( self, *, - indicator_id: str = "flood_depth/wtsub/95", + indicator_id: str = "flood_depth", resource: str = RealEstateInundationModel._default_resource, impact_bin_edges=RealEstateInundationModel._default_impact_bin_edges ): diff --git a/src/test/api/test_data_requests.py b/src/test/api/test_data_requests.py index d9b4b01c..67f96378 100644 --- a/src/test/api/test_data_requests.py +++ b/src/test/api/test_data_requests.py @@ -11,13 +11,13 @@ import numpy as np import numpy.testing -from physrisk import RiverineInundation, requests +from physrisk import requests 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 -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel.hazards import ChronicHeat +from physrisk.hazard_models.core_hazards import get_default_source_paths +from physrisk.kernel.hazards import ChronicHeat, RiverineInundation class TestDataRequests(TestWithCredentials): @@ -78,13 +78,11 @@ def test_zarr_reading(self): ZarrHazardModel(source_paths=get_default_source_paths(EmbeddedInventory()), reader=ZarrReader(store=store)), ) - result.items[0].intensity_curve_set[0].intensities - - numpy.testing.assert_array_almost_equal_nulp(result.items[0].intensity_curve_set[0].intensities, np.zeros((8))) + 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, 8, dtype="f4") + 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((8))) + 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 = { diff --git a/src/test/api/test_impact_requests.py b/src/test/api/test_impact_requests.py index b09f4bf8..49489243 100644 --- a/src/test/api/test_impact_requests.py +++ b/src/test/api/test_impact_requests.py @@ -6,10 +6,11 @@ from physrisk import requests from physrisk.api.v1.common import Assets +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 -from physrisk.hazard_models.embedded import get_default_source_paths +from physrisk.hazard_models.core_hazards import get_default_source_paths # from physrisk.api.v1.impact_req_resp import AssetImpactResponse # from physrisk.data.static.world import get_countries_and_continents @@ -63,6 +64,7 @@ def test_impact_request(self): request_dict = { "assets": assets, "include_asset_level": True, + "include_measures": False, "include_calc_details": True, "year": 2080, "scenario": "rcp8p5", @@ -91,7 +93,7 @@ def test_example_portfolios(self): "year": 2050, "scenario": "ssp585", } - - request = requests.AssetImpactRequest(**request_dict) # type: ignore - response = requests._get_asset_impacts(request) + container = Container() + requester = container.requester() + response = requester.get(request_id="get_asset_impact", request_dict=request_dict) assert response is not None diff --git a/src/test/data/hazard_model_store.py b/src/test/data/hazard_model_store.py index c980f8ba..0e253fc6 100644 --- a/src/test/data/hazard_model_store.py +++ b/src/test/data/hazard_model_store.py @@ -1,12 +1,12 @@ import os -from typing import Dict +from typing import Dict, List, Tuple import numpy as np import zarr import zarr.storage from affine import Affine -from physrisk.hazard_models.embedded import cmip6_scenario_to_rcp +from physrisk.hazard_models.core_hazards import cmip6_scenario_to_rcp class TestData: @@ -53,7 +53,7 @@ def get_mock_hazard_model_store_single_curve(): """Create a test MemoryStore for creation of Zarr hazard model for unit testing. A single curve is applied at all locations.""" - return_periods = [5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] + return_periods = inundation_return_periods() t = [0.008333333333333333, 0.0, -180.0, 0.0, -0.008333333333333333, 90.0, 0.0, 0.0, 1.0] shape = (len(return_periods), 21600, 43200) store = zarr.storage.MemoryStore(root="hazard.zarr") @@ -104,7 +104,7 @@ def mock_hazard_model_store_for_parameter_sets(longitudes, latitudes, path_param root = zarr.open(store=store, mode="w") for path, parameter in path_parameters.items(): - _add_curves(root, longitudes, latitudes, path, shape, parameter, return_periods, t) + add_curves(root, longitudes, latitudes, path, shape, parameter, return_periods, t) return store @@ -112,25 +112,34 @@ def mock_hazard_model_store_for_parameter_sets(longitudes, latitudes, path_param def mock_hazard_model_store_single_curve_for_paths(longitudes, latitudes, curve, paths): """Create a MemoryStore for creation of Zarr hazard model to be used with unit tests, with the specified longitudes and latitudes set to the curve supplied.""" - if len(curve) == 1: - return_periods = None - shape = (1, 21600, 43200) - else: - return_periods = [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] - if len(curve) != len(return_periods): - raise ValueError(f"curve must be single value or of length {len(return_periods)}") - shape = (len(return_periods), 21600, 43200) - t = [0.008333333333333333, 0.0, -180.0, 0.0, -0.008333333333333333, 90.0, 0.0, 0.0, 1.0] - store = zarr.storage.MemoryStore(root="hazard.zarr") - root = zarr.open(store=store, mode="w") + return_periods = [0.0] if len(curve) == 1 else [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] + if len(curve) != len(return_periods): + raise ValueError(f"curve must be single value or of length {len(return_periods)}") + + shape, t = shape_transform_21600_43200(return_periods=return_periods) + store, root = zarr_memory_store() for path in paths(): - _add_curves(root, longitudes, latitudes, path, shape, curve, return_periods, t) + add_curves(root, longitudes, latitudes, path, shape, curve, return_periods, t) return store +def shape_transform_21600_43200(return_periods: List[float] = [0.0]): + t = [360.0 / 43200, 0.0, -180.0, 0.0, -180.0 / 21600, 90.0, 0.0, 0.0, 1.0] + return (len(return_periods), 21600, 43200), t + + +def inundation_return_periods(): + return [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] + + +def zarr_memory_store(path="hazard.zarr"): + store = zarr.storage.MemoryStore(root=path) + return store, zarr.open(store=store, mode="w") + + def mock_hazard_model_store_path_curves(longitudes, latitudes, path_curves: Dict[str, np.ndarray]): """Create a MemoryStore for creation of Zarr hazard model to be used with unit tests, with the specified longitudes and latitudes set to the curve supplied.""" @@ -141,7 +150,7 @@ def mock_hazard_model_store_path_curves(longitudes, latitudes, path_curves: Dict for path, curve in path_curves.items(): if len(curve) == 1: - return_periods = None + return_periods = [0.0] shape = (1, 21600, 43200) else: return_periods = [2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0] @@ -149,7 +158,7 @@ def mock_hazard_model_store_path_curves(longitudes, latitudes, path_curves: Dict raise ValueError(f"curve must be single value or of length {len(return_periods)}") shape = (len(return_periods), 21600, 43200) - _add_curves(root, longitudes, latitudes, path, shape, curve, return_periods, t) + add_curves(root, longitudes, latitudes, path, shape, curve, return_periods, t) return store @@ -193,20 +202,33 @@ def inundation_paths(): paths = [] for model, scenario, year in [("MIROC-ESM-CHEM", "rcp8p5", 2080), ("000000000WATCH", "historical", 1980)]: paths.append(get_source_path_wri_riverine_inundation(model=model, scenario=scenario, year=year)) - for model, scenario, year in [("wtsub/95", "rcp8p5", 2080), ("wtsub", "historical", 1980)]: + for model, scenario, year in [ + ("wtsub/95", "rcp8p5", "2080"), + ("wtsub", "historical", "hist"), + ("nosub", "historical", "hist"), + ]: paths.append(get_source_path_wri_coastal_inundation(model=model, scenario=scenario, year=year)) return paths -def _add_curves(root, longitudes, latitudes, array_path, shape, curve, return_periods, t): +def add_curves( + root: zarr.Group, + longitudes, + latitudes, + array_path: str, + shape: Tuple[int, int, int], + curve: np.ndarray, + return_periods: List[float], + trans: List[float], +): z = root.create_dataset( # type: ignore array_path, shape=(shape[0], shape[1], shape[2]), chunks=(shape[0], 1000, 1000), dtype="f4" ) - z.attrs["transform_mat3x3"] = t + z.attrs["transform_mat3x3"] = trans z.attrs["index_values"] = return_periods - t = z.attrs["transform_mat3x3"] - transform = Affine(t[0], t[1], t[2], t[3], t[4], t[5]) + trans = z.attrs["transform_mat3x3"] + transform = Affine(trans[0], trans[1], trans[2], trans[3], trans[4], trans[5]) coords = np.vstack((longitudes, latitudes, np.ones(len(longitudes)))) inv_trans = ~transform @@ -225,7 +247,7 @@ def _wri_inundation_prefix(): _subsidence_set = {"wtsub", "nosub"} -def get_source_path_wri_coastal_inundation(*, model: str, scenario: str, year: int): +def get_source_path_wri_coastal_inundation(*, model: str, scenario: str, year: str): type = "coast" # model is expected to be of the form subsidence/percentile, e.g. wtsub/95 # if percentile is omitted then 95th percentile is used diff --git a/src/test/kernel/test_asset_impact.py b/src/test/kernel/test_asset_impact.py index 9b4e08aa..98f5c76d 100644 --- a/src/test/kernel/test_asset_impact.py +++ b/src/test/kernel/test_asset_impact.py @@ -4,10 +4,13 @@ import numpy as np -from physrisk import ExceedanceCurve, HazardEventDistrib, RiverineInundation, VulnerabilityDistrib from physrisk.kernel.assets import Asset, RealEstateAsset +from physrisk.kernel.curve import ExceedanceCurve +from physrisk.kernel.hazard_event_distrib import HazardEventDistrib from physrisk.kernel.hazard_model import HazardDataRequest -from physrisk.kernel.impact_distrib import ImpactDistrib +from physrisk.kernel.hazards import RiverineInundation +from physrisk.kernel.impact import ImpactDistrib +from physrisk.kernel.vulnerability_distrib import VulnerabilityDistrib from physrisk.kernel.vulnerability_model import VulnerabilityModelBase from physrisk.vulnerability_models.real_estate_models import ( RealEstateCoastalInundationModel, diff --git a/src/test/kernel/test_chronic_asset_impact.py b/src/test/kernel/test_chronic_asset_impact.py index e4786fbe..a6d01ca2 100644 --- a/src/test/kernel/test_chronic_asset_impact.py +++ b/src/test/kernel/test_chronic_asset_impact.py @@ -6,11 +6,11 @@ from scipy.stats import norm from physrisk.data.pregenerated_hazard_model import ZarrHazardModel -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel import calculation +from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import Asset, IndustrialActivity from physrisk.kernel.hazard_model import HazardDataRequest, HazardDataResponse, HazardParameterDataResponse from physrisk.kernel.hazards import ChronicHeat +from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType from physrisk.kernel.vulnerability_model import VulnerabilityModelBase from physrisk.vulnerability_models.chronic_heat_models import ChronicHeatGZNModel @@ -162,9 +162,7 @@ def test_chronic_vulnerability_model(self): for lon, lat in zip(TestData.longitudes, TestData.latitudes) ][:1] - results = calculation.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].impact.mean_impact() value_test = list(results.values())[0].impact.prob diff --git a/src/test/kernel/test_curves.py b/src/test/kernel/test_curves.py index 520c1851..99e9d092 100644 --- a/src/test/kernel/test_curves.py +++ b/src/test/kernel/test_curves.py @@ -3,7 +3,7 @@ import numpy as np -from physrisk import ExceedanceCurve +from physrisk.kernel.curve import ExceedanceCurve class TestAssetImpact(unittest.TestCase): diff --git a/src/test/kernel/test_exposure.py b/src/test/kernel/test_exposure.py index fdd5880e..d832ba2e 100644 --- a/src/test/kernel/test_exposure.py +++ b/src/test/kernel/test_exposure.py @@ -12,10 +12,9 @@ from physrisk.data.inventory_reader import InventoryReader from physrisk.data.pregenerated_hazard_model import ZarrHazardModel from physrisk.data.zarr_reader import ZarrReader -from physrisk.hazard_models.embedded import get_default_source_paths +from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import Asset -from physrisk.kernel.calculation import calculate_exposures -from physrisk.kernel.exposure import Category, JupterExposureMeasure +from physrisk.kernel.exposure import Category, JupterExposureMeasure, calculate_exposures from physrisk.kernel.hazards import ChronicHeat, CombinedInundation, Drought, Fire, Hail, Wind from physrisk.requests import Requester diff --git a/src/test/kernel/test_financial_model.py b/src/test/kernel/test_financial_model.py index bb0a8bfd..80e70f2c 100644 --- a/src/test/kernel/test_financial_model.py +++ b/src/test/kernel/test_financial_model.py @@ -4,10 +4,10 @@ import numpy as np from physrisk.data.pregenerated_hazard_model import ZarrHazardModel -from physrisk.hazard_models.embedded import get_default_source_paths +from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import Asset, PowerGeneratingAsset from physrisk.kernel.financial_model import FinancialDataProvider, FinancialModel -from physrisk.kernel.loss_model import LossModel +from physrisk.risk_models.loss_model import LossModel from ..data.hazard_model_store import TestData, mock_hazard_model_store_inundation diff --git a/src/test/models/test_WBGT_Model.py b/src/test/models/test_WBGT_Model.py index af7d2298..b5730d77 100644 --- a/src/test/models/test_WBGT_Model.py +++ b/src/test/models/test_WBGT_Model.py @@ -5,11 +5,11 @@ import numpy as np from physrisk.data.pregenerated_hazard_model import ZarrHazardModel -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel import calculation +from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import Asset, IndustrialActivity from physrisk.kernel.hazard_model import HazardDataRequest, HazardDataResponse, HazardParameterDataResponse from physrisk.kernel.hazards import ChronicHeat +from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.impact_distrib import ImpactDistrib, ImpactType from physrisk.vulnerability_models.chronic_heat_models import ChronicHeatGZNModel, get_impact_distrib @@ -189,9 +189,7 @@ def test_wbgt_vulnerability(self): IndustrialActivity(lat, lon, type="high") for lon, lat in zip(TestData.longitudes, TestData.latitudes) ][:1] - results = calculation.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].impact.prob diff --git a/src/test/models/test_example_models.py b/src/test/models/test_example_models.py index 0c82a11b..9661442a 100644 --- a/src/test/models/test_example_models.py +++ b/src/test/models/test_example_models.py @@ -5,11 +5,11 @@ from scipy import stats from physrisk.data.pregenerated_hazard_model import ZarrHazardModel -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel import calculation +from physrisk.hazard_models.core_hazards import get_default_source_paths from physrisk.kernel.assets import Asset, RealEstateAsset from physrisk.kernel.hazard_model import HazardEventDataResponse from physrisk.kernel.hazards import Inundation, RiverineInundation +from physrisk.kernel.impact import calculate_impacts from physrisk.kernel.vulnerability_matrix_provider import VulnMatrixProvider from physrisk.kernel.vulnerability_model import VulnerabilityModel from physrisk.vulnerability_models.example_models import ExampleCdfBasedVulnerabilityModel @@ -85,8 +85,6 @@ def test_user_supplied_model(self): for lon, lat in zip(TestData.longitudes, TestData.latitudes) ] - results = calculation.calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) + results = calculate_impacts(assets, hazard_model, vulnerability_models, scenario=scenario, year=year) self.assertAlmostEqual(results[assets[0], RiverineInundation].impact.to_exceedance_curve().probs[0], 0.499) diff --git a/src/test/models/test_power_generating_asset_models.py b/src/test/models/test_power_generating_asset_models.py index a20f338e..c2bd2ff6 100644 --- a/src/test/models/test_power_generating_asset_models.py +++ b/src/test/models/test_power_generating_asset_models.py @@ -8,10 +8,10 @@ import physrisk.api.v1.common import physrisk.data.static.world as wd -from physrisk import calculate_impacts from physrisk.kernel import Asset, PowerGeneratingAsset from physrisk.kernel.assets import IndustrialActivity, RealEstateAsset from physrisk.kernel.hazard_model import HazardEventDataResponse +from physrisk.kernel.impact import calculate_impacts from physrisk.utils.lazy import lazy_import from physrisk.vulnerability_models.power_generating_asset_models import InundationModel diff --git a/src/test/models/test_real_estate_models.py b/src/test/models/test_real_estate_models.py index 18a11208..d1fb00a6 100644 --- a/src/test/models/test_real_estate_models.py +++ b/src/test/models/test_real_estate_models.py @@ -5,10 +5,10 @@ import numpy as np from physrisk.data.pregenerated_hazard_model import ZarrHazardModel -from physrisk.hazard_models.embedded import get_default_source_paths -from physrisk.kernel import calculation +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.impact import calculate_impacts from physrisk.vulnerability_models.real_estate_models import ( RealEstateCoastalInundationModel, RealEstateRiverineInundationModel, @@ -34,9 +34,7 @@ def test_real_estate_model_details(self): vulnerability_models = {RealEstateAsset: [RealEstateRiverineInundationModel()]} - results = calculation.calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) + results = calculate_impacts(assets, hazard_model, vulnerability_models, scenario=scenario, year=year) hazard_bin_edges = results[(assets[0], RiverineInundation)].event.intensity_bin_edges hazard_bin_probs = results[(assets[0], RiverineInundation)].event.prob @@ -101,9 +99,7 @@ def test_coastal_real_estate_model(self): vulnerability_models = {RealEstateAsset: [RealEstateCoastalInundationModel()]} - results = calculation.calculate_impacts( - assets, hazard_model, vulnerability_models, scenario=scenario, year=year - ) + results = calculate_impacts(assets, hazard_model, vulnerability_models, scenario=scenario, year=year) np.testing.assert_allclose( results[(assets[0], CoastalInundation)].impact.prob, diff --git a/src/test/risk_models/__init__.py b/src/test/risk_models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/test/risk_models/test_risk_models.py b/src/test/risk_models/test_risk_models.py new file mode 100644 index 00000000..75068321 --- /dev/null +++ b/src/test/risk_models/test_risk_models.py @@ -0,0 +1,57 @@ +""" Test asset impact calculations.""" +import test.data.hazard_model_store as hms +import unittest +from test.data.hazard_model_store import TestData + +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.calculation import get_default_vulnerability_models +from physrisk.kernel.hazards import CoastalInundation, RiverineInundation +from physrisk.kernel.risk import AssetLevelRiskModel, MeasureKey +from physrisk.risk_models.risk_models import RealEstateToyRiskMeasures + + +class TestRiskModels(unittest.TestCase): + """Tests RealEstateInundationModel.""" + + def test_risk_indicator_model(self): + source_paths = get_default_source_paths() + scenarios = ["rcp8p5"] + years = [2050] + + 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) + + store, root = hms.zarr_memory_store() + return_periods = hms.inundation_return_periods() + shape, transform = hms.shape_transform_21600_43200(return_periods=return_periods) + + histo_curve = np.array([0.0596, 0.333, 0.505, 0.715, 0.864, 1.003, 1.149, 1.163, 1.163]) + projected_curve = np.array([0.0596, 0.333, 0.605, 0.915, 1.164, 1.503, 1.649, 1.763, 1.963]) + for path in [sp_riverine("historical", 1980), sp_coastal("historical", 1980)]: + hms.add_curves( + root, TestData.longitudes, TestData.latitudes, path, shape, histo_curve, return_periods, transform + ) + for path in [sp_riverine("rcp8p5", 2050), sp_coastal("rcp8p5", 2050)]: + hms.add_curves( + root, TestData.longitudes, TestData.latitudes, path, shape, projected_curve, return_periods, transform + ) + + 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]) + ] + + model = AssetLevelRiskModel( + hazard_model, get_default_vulnerability_models(), {RealEstateAsset: RealEstateToyRiskMeasures()} + ) + impacts, measures = model.calculate_risk_measures(assets, prosp_scens=scenarios, years=years) + print(measures[MeasureKey(assets[0], scenarios[0], years[0], RiverineInundation)])