Skip to content

Commit

Permalink
Refactoring to support risk models; resync inventory (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
joemoorhouse authored Aug 24, 2023
1 parent b0ac8a1 commit 733ec15
Show file tree
Hide file tree
Showing 37 changed files with 812 additions and 466 deletions.
2 changes: 1 addition & 1 deletion methodology/PhysicalRiskMethodologyBibliography.bib
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down
9 changes: 0 additions & 9 deletions src/physrisk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +0,0 @@
from .kernel import (
Asset,
Drought,
ExceedanceCurve,
HazardEventDistrib,
RiverineInundation,
VulnerabilityDistrib,
calculate_impacts,
)
25 changes: 16 additions & 9 deletions src/physrisk/api/v1/hazard_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."
)
Expand Down Expand Up @@ -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)},
)
),
},
Expand Down
14 changes: 13 additions & 1 deletion src/physrisk/api/v1/hazard_image.py
Original file line number Diff line number Diff line change
@@ -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}'.")
Expand All @@ -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):
Expand Down
34 changes: 29 additions & 5 deletions src/physrisk/api/v1/impact_req_resp.py
Original file line number Diff line number Diff line change
@@ -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'.")
Expand All @@ -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(
Expand All @@ -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."""

Expand All @@ -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
Expand Down
39 changes: 34 additions & 5 deletions src/physrisk/data/image_creator.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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).
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
8 changes: 4 additions & 4 deletions src/physrisk/data/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 733ec15

Please sign in to comment.