Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactoring to support risk models; resync inventory #142

Merged
merged 4 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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