From 9096b12f93eb2b2680669631cecf2fe35a5c822e Mon Sep 17 00:00:00 2001 From: MatthiasHauthDeltares <113418841+MatthiasHauthDeltares@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:05:15 +0100 Subject: [PATCH] 598 adaptation calculate impact adaptation option for time horizon (#617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * run_net_present_impact method * chore/rename variable * chore/remove unrelated uncertainty files * calculate net present impact for all options * chore/docstrings * create net_present_value factor * add test get_net_present_value_factor * run black * run black again * Update tests/analysis/adaptation/test_adaptation_option_collection.py Co-authored-by: Carles S. Soriano Pérez * Update ra2ce/analysis/adaptation/adaptation_option.py Co-authored-by: Carles S. Soriano Pérez * chore/remove unncessary imports * chore/remove parenthesis * chore/fix test --------- Co-authored-by: Carles S. Soriano Pérez --- .../analysis/adaptation/adaptation_option.py | 7 ++++++- .../adaptation_option_collection.py | 20 ++++++++++++++++++- tests/analysis/adaptation/conftest.py | 2 ++ .../test_adaptation_option_collection.py | 19 ++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/ra2ce/analysis/adaptation/adaptation_option.py b/ra2ce/analysis/adaptation/adaptation_option.py index 5f0c68a75..1e775fae5 100644 --- a/ra2ce/analysis/adaptation/adaptation_option.py +++ b/ra2ce/analysis/adaptation/adaptation_option.py @@ -132,7 +132,9 @@ def calculate_cost(year) -> float: return sum(calculate_cost(_year) for _year in range(0, round(time_horizon), 1)) - def calculate_impact(self, benefit_graph: GeoDataFrame) -> GeoDataFrame: + def calculate_impact( + self, benefit_graph: GeoDataFrame, net_present_value_factor: float + ) -> GeoDataFrame: """ Calculate the impact of the adaptation option. @@ -150,4 +152,7 @@ def calculate_impact(self, benefit_graph: GeoDataFrame) -> GeoDataFrame: _option_cols = benefit_graph.filter(regex=f"{self.id}_").columns benefit_graph[f"{self.id}_impact"] = benefit_graph[_option_cols].sum(axis=1) + # convert event impact into time-horizon impact + benefit_graph[f"{self.id}_impact"] *= net_present_value_factor + return benefit_graph diff --git a/ra2ce/analysis/adaptation/adaptation_option_collection.py b/ra2ce/analysis/adaptation/adaptation_option_collection.py index 27b7c09fd..722f15acf 100644 --- a/ra2ce/analysis/adaptation/adaptation_option_collection.py +++ b/ra2ce/analysis/adaptation/adaptation_option_collection.py @@ -22,6 +22,7 @@ from dataclasses import dataclass, field +import numpy as np from geopandas import GeoDataFrame from ra2ce.analysis.adaptation.adaptation_option import AdaptationOption @@ -91,6 +92,19 @@ def from_config( return _collection + def get_net_present_value_factor(self) -> float: + """ + Calculate the net present value factor for the entire time horizon. To be multiplied to the event impact to + obtain the net present value. + """ + _years_array = np.arange(0, self.time_horizon) + _frequency_per_year = ( + self.initial_frequency + _years_array * self.climate_factor + ) + _discount = (1 + self.discount_rate) ** _years_array + _ratio = _frequency_per_year / _discount + return _ratio.sum() + def calculate_options_unit_cost(self) -> dict[AdaptationOption, float]: """ Calculate the unit cost for all adaptation options. @@ -116,7 +130,11 @@ def calculation_options_impact(self, benefit_graph: GeoDataFrame) -> GeoDataFram Returns: NetworkFile: The calculated impact of all adaptation options. """ + net_present_value_factor = self.get_net_present_value_factor() + for _option in self.all_options: - benefit_graph = _option.calculate_impact(benefit_graph) + benefit_graph = _option.calculate_impact( + benefit_graph, net_present_value_factor + ) return benefit_graph diff --git a/tests/analysis/adaptation/conftest.py b/tests/analysis/adaptation/conftest.py index 02c739442..d8de969a3 100644 --- a/tests/analysis/adaptation/conftest.py +++ b/tests/analysis/adaptation/conftest.py @@ -165,6 +165,8 @@ def get_losses_section(analysis: AnalysisLossesEnum) -> AnalysisSectionLosses: adaptation_options=AdaptationOptionCases.config_cases, discount_rate=0.025, time_horizon=20, + climate_factor=0.00036842, + initial_frequency=0.01, ) _analysis_data = AnalysisConfigData( diff --git a/tests/analysis/adaptation/test_adaptation_option_collection.py b/tests/analysis/adaptation/test_adaptation_option_collection.py index 2f1c0f18f..150ce78d6 100644 --- a/tests/analysis/adaptation/test_adaptation_option_collection.py +++ b/tests/analysis/adaptation/test_adaptation_option_collection.py @@ -63,3 +63,22 @@ def test_calculate_options_unit_cost( # 3. Verify expectations. assert isinstance(_result, dict) assert all(_option in _result for _option in _collection.adaptation_options) + + def test_calculate_correct_get_net_present_value_factor( + self, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + ): + # 1. Define test data. + _config_wrapper = valid_adaptation_config[1] + assert isinstance(_config_wrapper, AnalysisConfigWrapper) + _collection = AdaptationOptionCollection.from_config(_config_wrapper) + + # 2. Run test. + _result = _collection.get_net_present_value_factor() + + # 3. Verify expectations. + assert isinstance(_result, float) + assert _result == pytest.approx(0.2109011023, rel=1e-9) + + +