Skip to content

Commit

Permalink
Merge pull request #252 from joemoorhouse/expose-vuln
Browse files Browse the repository at this point in the history
Add plug-and-playness of vulnerability models to dependency injection.
  • Loading branch information
joemoorhouse authored Mar 18, 2024
2 parents af2bc68 + 2046f52 commit 707f156
Show file tree
Hide file tree
Showing 16 changed files with 141 additions and 63 deletions.
14 changes: 14 additions & 0 deletions src/physrisk/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
from physrisk.data.inventory_reader import InventoryReader
from physrisk.data.pregenerated_hazard_model import ZarrHazardModel
from physrisk.data.zarr_reader import ZarrReader
from physrisk.kernel import calculation as calc
from physrisk.kernel.hazard_model import HazardModelFactory
from physrisk.kernel.vulnerability_model import (
DictBasedVulnerabilityModels,
VulnerabilityModels,
VulnerabilityModelsFactory,
)
from physrisk.requests import Requester, _create_inventory, create_source_paths


Expand All @@ -30,6 +36,11 @@ def hazard_model(self, interpolation: str = "floor"):
)


class DictBasedVulnerabilityModelsFactory(VulnerabilityModelsFactory):
def vulnerability_models(self) -> VulnerabilityModels:
return DictBasedVulnerabilityModels(calc.get_default_vulnerability_models())


class Container(containers.DeclarativeContainer):
config = providers.Configuration(default={"zarr_sources": ["embedded", "hazard"]})

Expand All @@ -47,9 +58,12 @@ class Container(containers.DeclarativeContainer):

hazard_model_factory = providers.Factory(ZarrHazardModelFactory, reader=zarr_reader, source_paths=source_paths)

vulnerability_models_factory = providers.Factory(DictBasedVulnerabilityModelsFactory)

requester = providers.Singleton(
Requester,
hazard_model_factory=hazard_model_factory,
vulnerability_models_factory=vulnerability_models_factory,
inventory=inventory,
inventory_reader=inventory_reader,
reader=zarr_reader,
Expand Down
13 changes: 9 additions & 4 deletions src/physrisk/kernel/impact.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Union
from typing import Dict, Iterable, List, NamedTuple, Optional, Tuple, Union

from physrisk.kernel.assets import Asset
from physrisk.kernel.hazard_event_distrib import HazardEventDistrib
from physrisk.kernel.hazard_model import HazardDataFailedResponse, HazardDataRequest, HazardDataResponse, HazardModel
from physrisk.kernel.impact_distrib import EmptyImpactDistrib, ImpactDistrib
from physrisk.kernel.vulnerability_distrib import VulnerabilityDistrib
from physrisk.kernel.vulnerability_model import DataRequester, VulnerabilityModelAcuteBase, VulnerabilityModelBase
from physrisk.kernel.vulnerability_model import (
DataRequester,
VulnerabilityModelAcuteBase,
VulnerabilityModelBase,
VulnerabilityModels,
)
from physrisk.utils.helpers import get_iterable

logger = logging.getLogger(__name__)
Expand All @@ -35,7 +40,7 @@ class AssetImpactResult:
def calculate_impacts( # noqa: C901
assets: Iterable[Asset],
hazard_model: HazardModel,
vulnerability_models: Dict[type, Sequence[VulnerabilityModelBase]],
vulnerability_models: VulnerabilityModels,
*,
scenario: str,
year: int,
Expand All @@ -48,7 +53,7 @@ def calculate_impacts( # noqa: C901

for asset in assets:
asset_type = type(asset)
mappings = vulnerability_models[asset_type]
mappings = vulnerability_models.vuln_model_for_asset_of_type(asset_type)
for mapping in mappings:
model_assets[mapping].append(asset)
results = {}
Expand Down
6 changes: 3 additions & 3 deletions src/physrisk/kernel/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from physrisk.kernel.hazards import Hazard, all_hazards
from physrisk.kernel.impact import AssetImpactResult, ImpactKey, calculate_impacts
from physrisk.kernel.impact_distrib import EmptyImpactDistrib, ImpactDistrib
from physrisk.kernel.vulnerability_model import VulnerabilityModelBase
from physrisk.kernel.vulnerability_model import VulnerabilityModels

# from asyncio import ALL_COMPLETED
# import concurrent.futures
Expand All @@ -26,7 +26,7 @@ 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]]):
def __init__(self, hazard_model: HazardModel, vulnerability_models: VulnerabilityModels):
self._hazard_model = hazard_model
self._vulnerability_models = vulnerability_models

Expand Down Expand Up @@ -98,7 +98,7 @@ class AssetLevelRiskModel(RiskModel):
def __init__(
self,
hazard_model: HazardModel,
vulnerability_models: Dict[type, Sequence[VulnerabilityModelBase]],
vulnerability_models: VulnerabilityModels,
measure_calculators: Dict[type, RiskMeasureCalculator],
):
"""Risk model that calculates risk measures at the asset level for a sequence
Expand Down
32 changes: 31 additions & 1 deletion src/physrisk/kernel/vulnerability_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import importlib.resources
import json
from abc import ABC, abstractmethod
from typing import Iterable, List, Optional, Protocol, Sequence, Tuple, Union
from typing import Dict, Iterable, List, Optional, Protocol, Sequence, Tuple, Type, Union

import numpy as np
from scipy import stats
Expand Down Expand Up @@ -98,6 +98,36 @@ def get_data_requests(
def get_impact(self, asset: Asset, event_data: List[HazardDataResponse]) -> ImpactDistrib: ...


class VulnerabilityModels(Protocol):
def vuln_model_for_asset_of_type(self, type: Type[Asset]) -> Sequence[VulnerabilityModelBase]:
"""Returns for a given asset type the vulnerability models for each hazard required.
Returns:
Dict[type, Sequence[VulnerabilityModelBase]]: Vulnerability models.s
"""
...


class VulnerabilityModelsFactory(Protocol):
def vulnerability_models(self) -> VulnerabilityModels:
"""Create a VulnerabilityModels instance, that can based on a number of options.
Although no options used at present, implemented this way in order to add in future
(e.g. to allow a request to specify preferred methodology).
Returns:
VulnerabilityModels: Instance that provides vulnerability models for asset types.
"""
...


class DictBasedVulnerabilityModels(VulnerabilityModels):
def __init__(self, models: Dict[Type[Asset], Sequence[VulnerabilityModelBase]]):
self.models = models

def vuln_model_for_asset_of_type(self, type: Type[Asset]):
return self.models[type]


class VulnerabilityModelAcuteBase(VulnerabilityModelBase):
"""Models generate the VulnerabilityDistrib and HazardEventDistrib of an
Asset.
Expand Down
17 changes: 13 additions & 4 deletions src/physrisk/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
from physrisk.kernel.hazards import all_hazards
from physrisk.kernel.impact_distrib import EmptyImpactDistrib
from physrisk.kernel.risk import AssetLevelRiskModel, Measure, MeasureKey
from physrisk.kernel.vulnerability_model import VulnerabilityModelBase
from physrisk.kernel.vulnerability_model import (
DictBasedVulnerabilityModels,
VulnerabilityModels,
VulnerabilityModelsFactory,
)

from .api.v1.hazard_data import (
HazardAvailabilityRequest,
Expand Down Expand Up @@ -62,13 +66,15 @@ class Requester:
def __init__(
self,
hazard_model_factory: HazardModelFactory,
vulnerability_models_factory: VulnerabilityModelsFactory,
inventory: Inventory,
inventory_reader: InventoryReader,
reader: ZarrReader,
colormaps: Colormaps,
):
self.colormaps = colormaps
self.hazard_model_factory = hazard_model_factory
self.vulnerability_models_factory = vulnerability_models_factory
self.inventory = inventory
self.inventory_reader = inventory_reader
self.zarr_reader = reader
Expand All @@ -93,7 +99,8 @@ def get(self, *, request_id, request_dict):
elif request_id == "get_asset_impact":
request = AssetImpactRequest(**request_dict)
hazard_model = self.hazard_model_factory.hazard_model(interpolation=request.calc_settings.hazard_interp)
return dumps(_get_asset_impacts(request, hazard_model).model_dump())
vulnerability_models = self.vulnerability_models_factory.vulnerability_models()
return dumps(_get_asset_impacts(request, hazard_model).model_dump(), vulnerability_models)
elif request_id == "get_example_portfolios":
return dumps(_get_example_portfolios())
else:
Expand Down Expand Up @@ -288,10 +295,12 @@ def _get_asset_exposures(request: AssetExposureRequest, hazard_model: HazardMode
def _get_asset_impacts(
request: AssetImpactRequest,
hazard_model: HazardModel,
vulnerability_models: Optional[Dict[Type[Asset], Sequence[VulnerabilityModelBase]]] = None,
vulnerability_models: Optional[VulnerabilityModels] = None,
):
vulnerability_models = (
calc.get_default_vulnerability_models() if vulnerability_models is None else vulnerability_models
DictBasedVulnerabilityModels(calc.get_default_vulnerability_models())
if vulnerability_models is None
else vulnerability_models
)
# we keep API definition of asset separate from internal Asset class; convert by reflection
# based on asset_class:
Expand Down
8 changes: 5 additions & 3 deletions src/physrisk/risk_models/loss_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ..kernel.financial_model import FinancialModelBase
from ..kernel.hazard_model import HazardModel
from ..kernel.impact import calculate_impacts
from ..kernel.vulnerability_model import VulnerabilityModelBase
from ..kernel.vulnerability_model import DictBasedVulnerabilityModels, VulnerabilityModels


class Aggregator(ABC):
Expand All @@ -27,11 +27,13 @@ class LossModel:
def __init__(
self,
hazard_model: Optional[HazardModel] = None,
vulnerability_models: Optional[Dict[type, Sequence[VulnerabilityModelBase]]] = None,
vulnerability_models: Optional[VulnerabilityModels] = None,
):
self.hazard_model = get_default_hazard_model() if hazard_model is None else hazard_model
self.vulnerability_models = (
get_default_vulnerability_models() if vulnerability_models is None else vulnerability_models
DictBasedVulnerabilityModels(get_default_vulnerability_models())
if vulnerability_models is None
else vulnerability_models
)

"""Calculates the financial impact on a list of assets."""
Expand Down
1 change: 1 addition & 0 deletions tests/api/container_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fsspec.implementations.local as local
from dependency_injector import containers, providers

from physrisk.container import Container
from physrisk.data.inventory_reader import InventoryReader
from tests.data.hazard_model_store_test import mock_hazard_model_store_heat

Expand Down
41 changes: 24 additions & 17 deletions tests/api/impact_requests_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from physrisk.data.zarr_reader import ZarrReader
from physrisk.hazard_models.core_hazards import get_default_source_paths
from physrisk.kernel.assets import PowerGeneratingAsset, RealEstateAsset, ThermalPowerGeneratingAsset
from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels
from physrisk.vulnerability_models.power_generating_asset_models import InundationModel
from physrisk.vulnerability_models.real_estate_models import (
RealEstateCoastalInundationModel,
Expand Down Expand Up @@ -98,10 +99,12 @@ def test_impact_request(self):
store = mock_hazard_model_store_inundation(TestData.longitudes, TestData.latitudes, curve)

source_paths = get_default_source_paths(EmbeddedInventory())
vulnerability_models = {
PowerGeneratingAsset: [InundationModel()],
RealEstateAsset: [RealEstateCoastalInundationModel(), RealEstateRiverineInundationModel()],
}
vulnerability_models = DictBasedVulnerabilityModels(
{
PowerGeneratingAsset: [InundationModel()],
RealEstateAsset: [RealEstateCoastalInundationModel(), RealEstateRiverineInundationModel()],
}
)

response = requests._get_asset_impacts(
request,
Expand Down Expand Up @@ -148,10 +151,12 @@ def test_risk_model_impact_request(self):
store = mock_hazard_model_store_inundation(TestData.longitudes, TestData.latitudes, curve)

source_paths = get_default_source_paths(EmbeddedInventory())
vulnerability_models = {
PowerGeneratingAsset: [InundationModel()],
RealEstateAsset: [RealEstateCoastalInundationModel(), RealEstateRiverineInundationModel()],
}
vulnerability_models = DictBasedVulnerabilityModels(
{
PowerGeneratingAsset: [InundationModel()],
RealEstateAsset: [RealEstateCoastalInundationModel(), RealEstateRiverineInundationModel()],
}
)

response = requests._get_asset_impacts(
request,
Expand Down Expand Up @@ -563,15 +568,17 @@ def test_thermal_power_generation(self):
)

source_paths = get_default_source_paths(EmbeddedInventory())
vulnerability_models = {
ThermalPowerGeneratingAsset: [
ThermalPowerGenerationAirTemperatureModel(),
ThermalPowerGenerationDroughtModel(),
ThermalPowerGenerationRiverineInundationModel(),
ThermalPowerGenerationWaterStressModel(),
ThermalPowerGenerationWaterTemperatureModel(),
]
}
vulnerability_models = DictBasedVulnerabilityModels(
{
ThermalPowerGeneratingAsset: [
ThermalPowerGenerationAirTemperatureModel(),
ThermalPowerGenerationDroughtModel(),
ThermalPowerGenerationRiverineInundationModel(),
ThermalPowerGenerationWaterStressModel(),
ThermalPowerGenerationWaterTemperatureModel(),
]
}
)

response = requests._get_asset_impacts(
request,
Expand Down
4 changes: 2 additions & 2 deletions tests/kernel/chronic_asset_impact_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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.kernel.vulnerability_model import DictBasedVulnerabilityModels, VulnerabilityModelBase
from physrisk.vulnerability_models.chronic_heat_models import ChronicHeatGZNModel
from tests.data.hazard_model_store_test import TestData, mock_hazard_model_store_heat

Expand Down Expand Up @@ -157,7 +157,7 @@ def test_chronic_vulnerability_model(self):
scenario = "ssp585"
year = 2050

vulnerability_models = {IndustrialActivity: [ChronicHeatGZNModel()]}
vulnerability_models = DictBasedVulnerabilityModels({IndustrialActivity: [ChronicHeatGZNModel()]})

assets = [
IndustrialActivity(lat, lon, type="Construction")
Expand Down
1 change: 1 addition & 0 deletions tests/kernel/exposure_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_jupiter_exposure_service(self):
inventory = EmbeddedInventory()
requester = Requester(
hazard_model_factory=hazard_model_factory,
vulnerability_models_factory=None,
inventory=inventory,
inventory_reader=InventoryReader(fs=local.LocalFileSystem(), base_path=""),
reader=ZarrReader(store=store),
Expand Down
3 changes: 2 additions & 1 deletion tests/kernel/hazard_models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from physrisk.kernel.hazards import ChronicHeat, Wind
from physrisk.kernel.impact import calculate_impacts
from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels
from physrisk.vulnerability_models.real_estate_models import GenericTropicalCycloneModel


Expand Down Expand Up @@ -88,7 +89,7 @@ def test_using_point_based_hazard_model():
)

hazard_model = PointBasedHazardModel([point])
vulnerability_models = {RealEstateAsset: [GenericTropicalCycloneModel()]}
vulnerability_models = DictBasedVulnerabilityModels({RealEstateAsset: [GenericTropicalCycloneModel()]})
results = calculate_impacts(assets, hazard_model, vulnerability_models, scenario=scenario, year=year)
impact_distrib = results[(assets[0], Wind, None, None)].impact
mean_impact = impact_distrib.mean_impact()
Expand Down
4 changes: 2 additions & 2 deletions tests/models/example_models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from physrisk.kernel.impact import calculate_impacts
from physrisk.kernel.impact_distrib import ImpactType
from physrisk.kernel.vulnerability_matrix_provider import VulnMatrixProvider
from physrisk.kernel.vulnerability_model import VulnerabilityModel
from physrisk.kernel.vulnerability_model import DictBasedVulnerabilityModels, VulnerabilityModel
from physrisk.vulnerability_models.example_models import ExampleCdfBasedVulnerabilityModel
from tests.data.hazard_model_store_test import TestData, mock_hazard_model_store_inundation

Expand Down Expand Up @@ -84,7 +84,7 @@ def test_user_supplied_model(self):
scenario = "rcp8p5"
year = 2080

vulnerability_models = {RealEstateAsset: [ExampleRealEstateInundationModel()]}
vulnerability_models = DictBasedVulnerabilityModels({RealEstateAsset: [ExampleRealEstateInundationModel()]})

assets = [
RealEstateAsset(lat, lon, location="Asia", type="Building/Industrial")
Expand Down
Loading

0 comments on commit 707f156

Please sign in to comment.