Source code for ra2ce.analysis.adaptation.adaptation_option

"""
                    GNU GENERAL PUBLIC LICENSE
                      Version 3, 29 June 2007

    Risk Assessment and Adaptation for Critical Infrastructure (RA2CE).
    Copyright (C) 2023 Stichting Deltares

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
from __future__ import annotations

import math
from dataclasses import asdict, dataclass

from geopandas import GeoDataFrame

from ra2ce.analysis.adaptation.adaptation_option_analysis import (
    AdaptationOptionAnalysis,
)
from ra2ce.analysis.adaptation.adaptation_option_partial_result import (
    AdaptationOptionPartialResult,
)
from ra2ce.analysis.adaptation.adaptation_result_enum import AdaptationResultEnum
from ra2ce.analysis.adaptation.adaptation_settings import AdaptationSettings
from ra2ce.analysis.analysis_config_data.analysis_config_data import (
    AnalysisSectionAdaptationOption,
)
from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import (
    AnalysisDamagesEnum,
)
from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper


[docs] @dataclass class AdaptationOption: id: str name: str construction_cost: float construction_interval: float maintenance_cost: float maintenance_interval: float analyses: list[AdaptationOptionAnalysis] analysis_config: AnalysisConfigWrapper adaptation_settings: AdaptationSettings
[docs] @classmethod def from_config( cls, analysis_config: AnalysisConfigWrapper, adaptation_option: AnalysisSectionAdaptationOption, ) -> AdaptationOption: """ Classmethod to create an AdaptationOption from an analysis configuration and an adaptation option. Args: analysis_config (AnalysisConfigWrapper): Analysis config input adaptation_option (AnalysisSectionAdaptationOption): Adaptation option input Raises: ValueError: If damages and losses sections are not present in the analysis config data. Returns: AdaptationOption: The created adaptation option. """ if ( not analysis_config.config_data.damages_list and not analysis_config.config_data.losses_list ): raise ValueError( "Damages and/or losses sections are required to create an adaptation option." ) # Create input for the damages and losses analyses (if present in config) _config_analyses = [x.analysis for x in analysis_config.config_data.analyses] _analyses = [ AdaptationOptionAnalysis.from_config( analysis_config, _analysis_type, adaptation_option.id, ) for _analysis_type in [ AnalysisDamagesEnum.DAMAGES, analysis_config.config_data.adaptation.losses_analysis, ] if _analysis_type in _config_analyses ] _adaptation_settings = AdaptationSettings( discount_rate=analysis_config.config_data.adaptation.discount_rate, time_horizon=analysis_config.config_data.adaptation.time_horizon, initial_frequency=analysis_config.config_data.adaptation.initial_frequency, climate_factor=analysis_config.config_data.adaptation.climate_factor, ) return cls( **asdict(adaptation_option), analyses=_analyses, analysis_config=analysis_config, adaptation_settings=_adaptation_settings, )
[docs] def get_bc_ratio( self, reference_impact: AdaptationOptionPartialResult, gdf_in: GeoDataFrame, hazard_fraction_cost: bool, ) -> AdaptationOptionPartialResult: """ Calculate the benefit-cost ratio of the adaptation option. Args: reference_impact (AdaptationOptionPartialResult): The impact of the reference option. Returns: AdaptationOptionPartialResult: The benefit-cost ratio of the adaptation option. """ # Calculate cost _result = self._get_cost(gdf_in, hazard_fraction_cost) # Calculate impact/benefit _result += self._get_benefit(reference_impact) # Calculate BC-ratio _benefit = _result.get_column(AdaptationResultEnum.BENEFIT) _cost = _result.get_column(AdaptationResultEnum.COST) _result.add_column( AdaptationResultEnum.BC_RATIO, _benefit / _cost.replace(0, math.nan), ) return _result
def _get_benefit( self, reference_impact: AdaptationOptionPartialResult ) -> AdaptationOptionPartialResult: _result = self.get_impact() # Benefit = reference impact - adaptation impact _benefit = reference_impact.get_column( AdaptationResultEnum.NET_IMPACT ) - _result.get_column(AdaptationResultEnum.NET_IMPACT) _result.add_column(AdaptationResultEnum.BENEFIT, _benefit) return _result def _get_cost( self, gdf_in: GeoDataFrame, hazard_fraction_cost: bool ) -> AdaptationOptionPartialResult: """ Calculate the cost of the adaptation option. Args: input_gdf (GeoDataFrame): The input GeoDataFrame. Returns: AdaptationOptionPartialResult: The cost of the adaptation option. """ _result = AdaptationOptionPartialResult.from_input_gdf(self.id, gdf_in) _cost_col = gdf_in.apply( lambda x, cost=self._get_unit_cost(): x["length"] * cost, axis=1 ) # Only calculate the cost for the impacted fraction of the links. if hazard_fraction_cost: _fraction_col = gdf_in.filter(regex="EV.*_fr").columns[0] _cost_col *= gdf_in[_fraction_col] _result.add_column(AdaptationResultEnum.COST, _cost_col) return _result def _get_unit_cost(self) -> float: """ Calculate the net present value unit cost (per meter) of the adaptation option. Args: None Returns: float: The net present value unit cost of the adaptation option. """ def is_constr_year(year: float) -> bool: if self.construction_interval == 0: return False return (round(year) % round(self.construction_interval)) == 0 def is_maint_year(year: float) -> bool: if self.maintenance_interval == 0: return False if self.construction_interval > 0: # Take year relative to last construction year year = round(year) % round(self.construction_interval) return (year % round(self.maintenance_interval)) == 0 def calculate_cost(year) -> float: if is_constr_year(year): _cost = self.construction_cost elif is_maint_year(year): _cost = self.maintenance_cost else: return 0.0 return _cost / (1 + self.adaptation_settings.discount_rate) ** year return sum( calculate_cost(_year) for _year in range(0, round(self.adaptation_settings.time_horizon), 1) )
[docs] def get_impact(self) -> AdaptationOptionPartialResult: """ Calculate the impact of the adaptation option. Args: net_present_value_factor (float): The net present value factor to apply to the event impact. Returns: AdaptationOptionPartialResult: The impact (event and net) of the adaptation option per link. """ # Get all results from the analyses _result = AdaptationOptionPartialResult(option_id=self.id) for _analysis in self.analyses: _result += _analysis.execute(self.analysis_config) # Calculate the impact (summing the results of the analysis results per link) _impact = _result.data_frame.filter(regex=self.id).sum(axis=1) _result.add_column(AdaptationResultEnum.EVENT_IMPACT, _impact) _result.add_column( AdaptationResultEnum.NET_IMPACT, _impact * self.adaptation_settings.net_present_value_factor, ) return _result