Skip to content

Commit

Permalink
Support historical scenario for score-based risk measures. (#372)
Browse files Browse the repository at this point in the history
* Support historical scenario for score-based risk measures.

Signed-off-by: Joe Moorhouse <[email protected]>

* Type fixes

Signed-off-by: Joe Moorhouse <[email protected]>

---------

Signed-off-by: Joe Moorhouse <[email protected]>
  • Loading branch information
joemoorhouse authored Dec 18, 2024
1 parent 7be265b commit aac8619
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 36 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "physrisk-lib"
# Could test changing the below to be sourced "dynamically"
# dynamic = ['version']
version = "0.41.0"
version = "0.42.0"
description = "OS-Climate Physical Risk Library"
authors = [
{name = "Joe Moorhouse",email = "[email protected]"},
Expand Down
14 changes: 7 additions & 7 deletions src/physrisk/api/v1/impact_req_resp.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class AssetImpactRequest(BaseModel):
# to be deprecated
scenario: str = Field("rcp8p5", description="Name of scenario ('rcp8p5')")
year: int = Field(
[2050],
2050,
description="""Projection years (e.g. 2030, 2050, 2080). Any year before 2030,
e.g. 1980, is treated as historical.""",
)
Expand All @@ -72,7 +72,7 @@ class Category(int, Enum):


class RiskMeasureDefinition(BaseModel):
measure_id: str = Field(None, description="Identifier for the risk measure.")
measure_id: str = Field("", description="Identifier for the risk measure.")
label: str = Field(
"<short description of the measure, e.g. fractional loss for 1-in-100 year event>",
description="Short label for the measure quantity.",
Expand All @@ -85,7 +85,7 @@ class RiskMeasureDefinition(BaseModel):

class RiskScoreValue(BaseModel):
value: Category = Field(
"", description="Value of the score: red, amber, green, nodata."
Category.NODATA, description="Value of the score: red, amber, green, nodata."
)
label: str = Field(
"",
Expand Down Expand Up @@ -128,7 +128,7 @@ class RiskMeasureKey(BaseModel):

class RiskMeasuresForAssets(BaseModel):
key: RiskMeasureKey
scores: List[int] = Field(None, description="Identifier for the risk measure.")
scores: List[int] = Field([0], description="Identifier for the risk measure.")
measures_0: List[float]
measures_1: Optional[List[float]]

Expand Down Expand Up @@ -156,14 +156,14 @@ class AcuteHazardCalculationDetails(BaseModel):
hazard_distribution: Distribution
vulnerability_distribution: VulnerabilityDistrib
hazard_path: List[str] = Field(
"unknown", description="Path to the hazard indicator data source."
["unknown"], description="Path to the hazard indicator data source."
)


class ImpactKey(BaseModel):
hazard_type: str = Field("", description="Type of the hazard.")
scenario_id: str = Field(None, description="Identifier of the scenario.")
year: str = Field(None, description="Year of impact.")
scenario_id: str = Field("", description="Identifier of the scenario.")
year: str = Field("", description="Year of impact.")


class AssetSingleImpact(BaseModel):
Expand Down
12 changes: 7 additions & 5 deletions src/physrisk/data/pregenerated_hazard_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,21 @@ def _get_hazard_data_batch(
)
except Exception as err:
# e.g. the requested data is unavailable
for _i, req in enumerate(batch):
for _, req in enumerate(batch):
failed_response = HazardDataFailedResponse(err)
responses[req] = failed_response
failures.append(failed_response)

if any(failures):
logger.error(
f"{len(failures)} errors in batch (hazard_type={hazard_type.__name__}, indicator_id={indicator_id}, "
# only a warning: perhaps the caller does not expect data to be present for all
# year/scenario combinations.
logger.warning(
f"{len(failures)} requests failed in batch (hazard_type={hazard_type.__name__}, indicator_id={indicator_id}, "
f"scenario={scenario}, year={year}): (logs limited to first 3)"
)
errors = (str(i.error) for i in failures)
for _ in range(3):
logger.error(next(errors))
for _ in range(min(len(failures), 3)):
logger.warning(next(errors))
return


Expand Down
34 changes: 18 additions & 16 deletions src/physrisk/kernel/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _calculate_single_impact(
class MeasureKey(NamedTuple):
asset: Asset
prosp_scen: str # prospective scenario
year: int
year: Optional[int]
hazard_type: type


Expand Down Expand Up @@ -238,19 +238,19 @@ def get_measure_id(
return measure_ids_for_hazard, measure_id_lookup

def calculate_risk_measures(
self, assets: Sequence[Asset], prosp_scens: Sequence[str], years: Sequence[int]
self, assets: Sequence[Asset], scenarios: Sequence[str], years: Sequence[int]
):
impacts = self._calculate_all_impacts(
assets, prosp_scens, years, include_histo=True
assets, scenarios, years, include_histo=True
)
measures: Dict[MeasureKey, Measure] = {}
aggregated_measures: Dict[MeasureKey, Measure] = {}
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:
for scenario in scenarios:
for year in [None] if scenario == "historical" else years:
for hazard_type in measure_calc.supported_hazards():
base_impacts = impacts.get(
ImpactKey(
Expand All @@ -260,33 +260,35 @@ def calculate_risk_measures(
key_year=None,
)
)
prosp_impacts = impacts.get(
# the future impact might also be the historical if that is also specified
fut_impacts = impacts.get(
ImpactKey(
asset=asset,
hazard_type=hazard_type,
scenario=prosp_scen,
scenario=scenario,
key_year=year,
)
)
if base_impacts is None or fut_impacts is None:
# should only happen if we are working with limited hazard scope
continue
risk_inds = [
measure_calc.calc_measure(
hazard_type, base_impact, prosp_impact
)
for base_impact, prosp_impact in zip(
base_impacts, prosp_impacts
base_impacts, fut_impacts
)
]
risk_ind = [
risk_ind for risk_ind in risk_inds if risk_ind is not None
]
if len(risk_ind) > 0:
# TODO: Aggregate measures instead of picking the first value.
measures[
MeasureKey(asset, prosp_scen, year, hazard_type)
] = risk_ind[0]
aggregated_measures.update(
measure_calc.aggregate_risk_measures(
measures, assets, prosp_scens, years
)
)
measures[MeasureKey(asset, scenario, year, hazard_type)] = (
risk_ind[0]
)
aggregated_measures.update(
measure_calc.aggregate_risk_measures(measures, assets, scenarios, years)
)
return impacts, aggregated_measures
2 changes: 1 addition & 1 deletion src/physrisk/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ def _create_risk_measures(
measures_for_assets: List[RiskMeasuresForAssets] = []
for hazard_type in hazard_types:
for scenario_id in scenarios:
for year in years:
for year in [None] if scenario_id == "historical" else years:
# we calculate and tag results for each scenario, year and hazard
score_key = RiskMeasureKey(
hazard_type=hazard_type.__name__,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ class VulnerabilityConfigItem(BaseModel):
"curve/surface. 1 or 2 dimensional array.",
)
cap_of_points_x: Optional[float] = Field(
"", description="Cap of x (indicator/threshold)."
None, description="Cap of x (indicator/threshold)."
)
cap_of_points_y: Optional[float] = Field("", description="Cap of y (impact).")
cap_of_points_y: Optional[float] = Field(None, description="Cap of y (impact).")
activation_of_points_x: Optional[float] = Field(
"", description="Activation threshold of x (indicator/threshold)."
None, description="Activation threshold of x (indicator/threshold)."
)


Expand Down
6 changes: 3 additions & 3 deletions tests/risk_models/risk_models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_risk_indicator_model(self):
)
measure_ids_for_asset, definitions = model.populate_measure_definitions(assets)
_, measures = model.calculate_risk_measures(
assets, prosp_scens=scenarios, years=years
assets, scenarios=scenarios, years=years
)

# how to get a score using the MeasureKey
Expand Down Expand Up @@ -293,7 +293,7 @@ def sp_precipitation(scenario, year):
)

def test_via_requests(self):
scenarios = ["ssp585"]
scenarios = ["ssp585", "historical"]
years = [2050]

assets = self._create_assets()
Expand Down Expand Up @@ -386,7 +386,7 @@ def test_generic_model(self):
)
measure_ids_for_asset, definitions = model.populate_measure_definitions(assets)
_, measures = model.calculate_risk_measures(
assets, prosp_scens=scenarios, years=years
assets, scenarios=scenarios, years=years
)
np.testing.assert_approx_equal(
measures[MeasureKey(assets[0], scenarios[0], years[0], Wind)].measure_0,
Expand Down

0 comments on commit aac8619

Please sign in to comment.