From 7aec3d08e30ac5827992cc6fc09369d01e2e7fef Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Tue, 10 Dec 2024 14:51:02 +0100 Subject: [PATCH 1/8] chore: add geometry logic --- ra2ce/analysis/adaptation/adaptation.py | 3 +++ tests/analysis/adaptation/test_adaptation.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/ra2ce/analysis/adaptation/adaptation.py b/ra2ce/analysis/adaptation/adaptation.py index 4cd9ff4e4..263656967 100644 --- a/ra2ce/analysis/adaptation/adaptation.py +++ b/ra2ce/analysis/adaptation/adaptation.py @@ -122,6 +122,9 @@ def calculate_bc_ratio( Returns: GeoDataFrame: Gdf containing the benefit-cost ratio of the adaptation options. """ + _orig_gdf = self.graph_file_hazard.get_graph() + benefit_gdf["geometry"] = _orig_gdf.geometry + for _option in self.adaptation_collection.adaptation_options: benefit_gdf[f"{_option.id}_cost"] = cost_gdf[f"{_option.id}_cost"] benefit_gdf[f"{_option.id}_bc_ratio"] = benefit_gdf[ diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index e0a49aca2..89de2fd66 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -3,6 +3,7 @@ import pytest from geopandas import GeoDataFrame +from shapely import Point from ra2ce.analysis.adaptation.adaptation import Adaptation from ra2ce.analysis.adaptation.adaptation_option_collection import ( @@ -11,6 +12,7 @@ from ra2ce.analysis.analysis_base import AnalysisBase from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper +from ra2ce.network.graph_files.network_file import NetworkFile from tests.analysis.adaptation.conftest import AdaptationOptionCases @@ -78,6 +80,11 @@ class MockAdaptationOption: id: str class MockAdaptation(Adaptation): + graph_file_hazard = NetworkFile( + graph=GeoDataFrame( + geometry=[Point(x, 0) for x in range(10)], crs="EPSG:4326" + ) + ) adaptation_collection: AdaptationOptionCollection = ( AdaptationOptionCollection( all_options=[ @@ -99,6 +106,7 @@ def test_calculate_bc_ratio_returns_gdf( _nof_rows = 10 _benefit_gdf = GeoDataFrame(range(_nof_rows)) _cost_gdf = GeoDataFrame(range(_nof_rows)) + for i, _option in enumerate( mocked_adaptation.adaptation_collection.adaptation_options ): @@ -110,6 +118,7 @@ def test_calculate_bc_ratio_returns_gdf( # 3. Verify expectations. assert isinstance(_result, GeoDataFrame) + assert "geometry" in _result.columns assert all( [ f"{_option.id}_bc_ratio" in _result.columns From 4dceb565f4292c5b7c24bf430b1d9770ba6e30f5 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Tue, 10 Dec 2024 16:49:28 +0100 Subject: [PATCH 2/8] chore: enable exporting gdf --- ra2ce/analysis/adaptation/adaptation.py | 2 + tests/analysis/adaptation/test_adaptation.py | 52 ++++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/ra2ce/analysis/adaptation/adaptation.py b/ra2ce/analysis/adaptation/adaptation.py index 263656967..8b84ae79c 100644 --- a/ra2ce/analysis/adaptation/adaptation.py +++ b/ra2ce/analysis/adaptation/adaptation.py @@ -56,6 +56,8 @@ def __init__( ): self.analysis = analysis_input.analysis self.graph_file_hazard = analysis_input.graph_file_hazard + self.input_path = analysis_input.input_path + self.output_path = analysis_input.output_path self.adaptation_collection = AdaptationOptionCollection.from_config( analysis_config ) diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index 89de2fd66..7c21c1210 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from pathlib import Path from typing import Iterator import pytest @@ -72,8 +73,22 @@ def test_run_benefit_returns_gdf( _expected[1] ) + @pytest.fixture + def valid_gdf(self) -> GeoDataFrame: + return GeoDataFrame( + geometry=[Point(x, 0) for x in range(10)], + crs="EPSG:4326", + ) + # return GeoDataFrame.from_dict( + # {"u": range(10), "v": range(10)}, + # geometry=[Point(x, 0) for x in range(10)], + # crs="EPSG:4326", + # ) + @pytest.fixture(name="mocked_adaptation") - def _get_mocked_adaptation_fixture(self) -> Iterator[Adaptation]: + def _get_mocked_adaptation_fixture( + self, valid_gdf: GeoDataFrame + ) -> Iterator[Adaptation]: # Mock to avoid complex setup. @dataclass class MockAdaptationOption: @@ -81,9 +96,7 @@ class MockAdaptationOption: class MockAdaptation(Adaptation): graph_file_hazard = NetworkFile( - graph=GeoDataFrame( - geometry=[Point(x, 0) for x in range(10)], crs="EPSG:4326" - ) + graph=valid_gdf, ) adaptation_collection: AdaptationOptionCollection = ( AdaptationOptionCollection( @@ -99,13 +112,11 @@ def __init__(self): yield MockAdaptation() def test_calculate_bc_ratio_returns_gdf( - self, - mocked_adaptation: Adaptation, + self, mocked_adaptation: Adaptation, valid_gdf: GeoDataFrame ): # 1. Define test data. - _nof_rows = 10 - _benefit_gdf = GeoDataFrame(range(_nof_rows)) - _cost_gdf = GeoDataFrame(range(_nof_rows)) + _benefit_gdf = valid_gdf + _cost_gdf = valid_gdf for i, _option in enumerate( mocked_adaptation.adaptation_collection.adaptation_options @@ -129,5 +140,26 @@ def test_calculate_bc_ratio_returns_gdf( mocked_adaptation.adaptation_collection.adaptation_options ): assert _result[f"{_option.id}_bc_ratio"].sum(axis=0) == pytest.approx( - _nof_rows * (4.0 + i) / (1.0 + i) + 10 * (4.0 + i) / (1.0 + i) ) + + def test_output_gdf_can_be_exported_to_gpkg( + self, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + test_result_param_case: Path, + ): + # 1. Define test data. + _adaptation = Adaptation(valid_adaptation_config[0], valid_adaptation_config[1]) + + # 2. Run test. + _result = _adaptation.execute().results_collection[0] + + _output_path = _result.output_path + _output_path.mkdir(parents=True, exist_ok=True) + + _result.analysis_result.to_file( + _result.output_path.joinpath("adaptation_output.gpkg"), driver="GPKG" + ) + + # 3. Verify expectations. + assert test_result_param_case.exists() From e8f10cc3527b04b745b07458d0b1447438358a05 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Tue, 10 Dec 2024 16:52:10 +0100 Subject: [PATCH 3/8] test: merge test --- tests/analysis/adaptation/test_adaptation.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index f75e0c4db..57ff058e2 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -89,13 +89,7 @@ def test_run_benefit_returns_gdf( def valid_gdf(self) -> GeoDataFrame: return GeoDataFrame( geometry=[Point(x, 0) for x in range(10)], - crs="EPSG:4326", ) - # return GeoDataFrame.from_dict( - # {"u": range(10), "v": range(10)}, - # geometry=[Point(x, 0) for x in range(10)], - # crs="EPSG:4326", - # ) @pytest.fixture(name="mocked_adaptation") def _get_mocked_adaptation_fixture( @@ -157,11 +151,15 @@ def test_calculate_bc_ratio_returns_gdf( def test_output_gdf_can_be_exported_to_gpkg( self, - valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + valid_adaptation_config_with_input: tuple[ + AnalysisInputWrapper, AnalysisConfigWrapper + ], test_result_param_case: Path, ): # 1. Define test data. - _adaptation = Adaptation(valid_adaptation_config[0], valid_adaptation_config[1]) + _adaptation = Adaptation( + valid_adaptation_config_with_input[0], valid_adaptation_config_with_input[1] + ) # 2. Run test. _result = _adaptation.execute().results_collection[0] From 0e50138edcd5757299773c15517b40a1abfb7104 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Wed, 11 Dec 2024 08:21:39 +0100 Subject: [PATCH 4/8] test: tweak test asserts --- ra2ce/analysis/adaptation/adaptation.py | 4 +++- tests/analysis/adaptation/test_adaptation.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ra2ce/analysis/adaptation/adaptation.py b/ra2ce/analysis/adaptation/adaptation.py index 8a770343f..be94947d3 100644 --- a/ra2ce/analysis/adaptation/adaptation.py +++ b/ra2ce/analysis/adaptation/adaptation.py @@ -121,8 +121,10 @@ def calculate_bc_ratio( cost_gdf (GeoDataFrame): Gdf containing the cost of the adaptation options. Returns: - GeoDataFrame: Gdf containing the benefit-cost ratio of the adaptation options. + GeoDataFrame: Gdf containing the benefit-cost ratio of the adaptation options, + including the relevant attributes from the original graph (geometry). """ + # Copy the relevant attributes from the original graph _orig_gdf = self.graph_file_hazard.get_graph() benefit_gdf["geometry"] = _orig_gdf.geometry diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index 57ff058e2..27da84fb3 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -135,7 +135,7 @@ def test_calculate_bc_ratio_returns_gdf( # 3. Verify expectations. assert isinstance(_result, GeoDataFrame) - assert "geometry" in _result.columns + assert not _result.geometry.empty assert all( [ f"{_option.id}_bc_ratio" in _result.columns @@ -146,7 +146,7 @@ def test_calculate_bc_ratio_returns_gdf( mocked_adaptation.adaptation_collection.adaptation_options ): assert _result[f"{_option.id}_bc_ratio"].sum(axis=0) == pytest.approx( - 10 * (4.0 + i) / (1.0 + i) + len(_result.index) * (4.0 + i) / (1.0 + i) ) def test_output_gdf_can_be_exported_to_gpkg( From 0b9754b07d0eee917443846315b5cb8ce667c050 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Wed, 11 Dec 2024 14:36:22 +0100 Subject: [PATCH 5/8] chore: change logic for calculating impact (include base columns) --- ra2ce/analysis/adaptation/adaptation.py | 23 ++++-- .../analysis/adaptation/adaptation_option.py | 40 ++++++++-- .../adaptation/adaptation_option_analysis.py | 1 + .../adaptation_option_collection.py | 18 ++--- tests/analysis/adaptation/test_adaptation.py | 79 ++++++++----------- .../adaptation/test_adaptation_option.py | 18 +++-- .../test_adaptation_option_collection.py | 16 +++- 7 files changed, 115 insertions(+), 80 deletions(-) diff --git a/ra2ce/analysis/adaptation/adaptation.py b/ra2ce/analysis/adaptation/adaptation.py index be94947d3..8f1caa43f 100644 --- a/ra2ce/analysis/adaptation/adaptation.py +++ b/ra2ce/analysis/adaptation/adaptation.py @@ -92,12 +92,12 @@ def run_cost(self) -> GeoDataFrame: _option, _cost, ) in self.adaptation_collection.calculate_options_unit_cost().items(): - _cost_gdf[f"{_option.id}_cost"] = _orig_gdf.apply( + _cost_gdf[_option.cost_col] = _orig_gdf.apply( lambda x, cost=_cost: x["length"] * cost, axis=1 ) # Only calculate the cost for the impacted fraction of the links. if self.analysis.hazard_fraction_cost: - _cost_gdf[f"{_option.id}_cost"] *= _orig_gdf[_fraction_col] + _cost_gdf[_option.cost_col] *= _orig_gdf[_fraction_col] return _cost_gdf @@ -124,15 +124,22 @@ def calculate_bc_ratio( GeoDataFrame: Gdf containing the benefit-cost ratio of the adaptation options, including the relevant attributes from the original graph (geometry). """ - # Copy the relevant attributes from the original graph + + def copy_column(from_gdf: GeoDataFrame, col_name: str) -> None: + benefit_gdf[col_name] = from_gdf[col_name] + + # Copy relevant columns from the original graph _orig_gdf = self.graph_file_hazard.get_graph() - benefit_gdf["geometry"] = _orig_gdf.geometry + for _col in ["link_id", "geometry", "highway", "length"]: + copy_column(_orig_gdf, _col) for _option in self.adaptation_collection.adaptation_options: - benefit_gdf[f"{_option.id}_cost"] = cost_gdf[f"{_option.id}_cost"] - benefit_gdf[f"{_option.id}_bc_ratio"] = benefit_gdf[ - f"{_option.id}_benefit" - ].replace(float("nan"), 0) / benefit_gdf[f"{_option.id}_cost"].replace( + # Copy cost columns from the cost gdf + copy_column(cost_gdf, _option.cost_col) + + benefit_gdf[_option.bc_ratio_col] = benefit_gdf[ + _option.benefit_col + ].replace(float("nan"), 0) / benefit_gdf[_option.cost_col].replace( 0, float("nan") ) diff --git a/ra2ce/analysis/adaptation/adaptation_option.py b/ra2ce/analysis/adaptation/adaptation_option.py index 9deeb93cc..c00bcc1d4 100644 --- a/ra2ce/analysis/adaptation/adaptation_option.py +++ b/ra2ce/analysis/adaptation/adaptation_option.py @@ -23,7 +23,6 @@ from dataclasses import asdict, dataclass from geopandas import GeoDataFrame -from pandas import Series from ra2ce.analysis.adaptation.adaptation_option_analysis import ( AdaptationOptionAnalysis, @@ -51,6 +50,29 @@ class AdaptationOption: def __hash__(self) -> int: return hash(self.id) + @property + def cost_col(self) -> str: + return self._get_column_name("cost") + + @property + def impact_col(self) -> str: + return self._get_column_name("impact") + + @property + def benefit_col(self) -> str: + return self._get_column_name("benefit") + + @property + def impact_col(self) -> str: + return self._get_column_name("impact") + + @property + def bc_ratio_col(self) -> str: + return self._get_column_name("bc_ratio") + + def _get_column_name(self, col_type: str) -> str: + return f"{self.id}_{col_type}" + @classmethod def from_config( cls, @@ -135,18 +157,22 @@ def calculate_cost(year) -> float: return sum(calculate_cost(_year) for _year in range(0, round(time_horizon), 1)) - def calculate_impact(self, net_present_value_factor: float) -> Series: + def calculate_impact(self, net_present_value_factor: float) -> GeoDataFrame: """ Calculate the impact of the adaptation option. Returns: - Series: The impact of the adaptation option. + GeoDataFrame: The impact of the adaptation option. """ _result_gdf = GeoDataFrame() for _analysis in self.analyses: - _result_gdf[_analysis.analysis_type] = _analysis.execute( - self.analysis_config - ) + _result_gdf[ + f"{self.impact_col}_{_analysis.analysis_type.config_value}" + ] = _analysis.execute(self.analysis_config) # Calculate the impact (summing the results of the analyses) - return _result_gdf.sum(axis=1) * net_present_value_factor + _result_gdf[self.impact_col] = ( + _result_gdf.sum(axis=1) * net_present_value_factor + ) + + return _result_gdf diff --git a/ra2ce/analysis/adaptation/adaptation_option_analysis.py b/ra2ce/analysis/adaptation/adaptation_option_analysis.py index da412f2ac..bb531c51e 100644 --- a/ra2ce/analysis/adaptation/adaptation_option_analysis.py +++ b/ra2ce/analysis/adaptation/adaptation_option_analysis.py @@ -171,4 +171,5 @@ def execute(self, analysis_config: AnalysisConfigWrapper) -> Series: self.analysis_input, analysis_config ).execute() _result = _result_wrapper.get_single_result() + return self.get_result_column(_result) diff --git a/ra2ce/analysis/adaptation/adaptation_option_collection.py b/ra2ce/analysis/adaptation/adaptation_option_collection.py index 50c605751..790478104 100644 --- a/ra2ce/analysis/adaptation/adaptation_option_collection.py +++ b/ra2ce/analysis/adaptation/adaptation_option_collection.py @@ -132,18 +132,18 @@ def calculate_options_benefit(self) -> GeoDataFrame: _benefit_gdf = GeoDataFrame() # Calculate impact of reference option - _benefit_gdf[ - f"{self.reference_option.id}_impact" - ] = self.reference_option.calculate_impact(net_present_value_factor) + _impact_gdf = self.reference_option.calculate_impact(net_present_value_factor) + for _col in _impact_gdf.columns: + _benefit_gdf[_col] = _impact_gdf[_col] # Calculate impact and benefit of adaptation options for _option in self.adaptation_options: - _benefit_gdf[f"{_option.id}_impact"] = _option.calculate_impact( - net_present_value_factor - ) - _benefit_gdf[f"{_option.id}_benefit"] = ( - _benefit_gdf[f"{_option.id}_impact"] - - _benefit_gdf[f"{self.reference_option.id}_impact"] + _impact_gdf = _option.calculate_impact(net_present_value_factor) + for _col in _impact_gdf.columns: + _benefit_gdf[_col] = _impact_gdf[_col] + _benefit_gdf[_option.benefit_col] = ( + _benefit_gdf[_option.impact_col] + - _benefit_gdf[self.reference_option.impact_col] ) return _benefit_gdf diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index 27da84fb3..c508e3b9a 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from pathlib import Path from typing import Iterator import pytest @@ -50,7 +49,7 @@ def test_run_cost_returns_gdf( # 3. Verify expectations. assert isinstance(_result, GeoDataFrame) assert all( - f"{_option.id}_cost" in _result.columns + _option.cost_col in _result.columns for _option in _adaptation.adaptation_collection.adaptation_options ) for _option, _expected in AdaptationOptionCases.cases[1:]: @@ -85,24 +84,36 @@ def test_run_benefit_returns_gdf( _expected[1] ) - @pytest.fixture - def valid_gdf(self) -> GeoDataFrame: - return GeoDataFrame( - geometry=[Point(x, 0) for x in range(10)], - ) - @pytest.fixture(name="mocked_adaptation") - def _get_mocked_adaptation_fixture( - self, valid_gdf: GeoDataFrame - ) -> Iterator[Adaptation]: + def _get_mocked_adaptation_fixture(self) -> Iterator[Adaptation]: # Mock to avoid complex setup. @dataclass class MockAdaptationOption: id: str + @property + def cost_col(self) -> str: + return f"{self.id}_cost" + + @property + def benefit_col(self) -> str: + return f"{self.id}_benefit" + + @property + def bc_ratio_col(self) -> str: + return f"{self.id}_bc_ratio" + class MockAdaptation(Adaptation): graph_file_hazard = NetworkFile( - graph=valid_gdf, + graph=GeoDataFrame.from_dict( + data={ + "geometry": [Point(x, 0) for x in range(10)], + "link_id": range(10), + "highway": "residential", + "length": 1.0, + }, + geometry="geometry", + ) ) adaptation_collection: AdaptationOptionCollection = ( AdaptationOptionCollection( @@ -117,18 +128,17 @@ def __init__(self): yield MockAdaptation() - def test_calculate_bc_ratio_returns_gdf( - self, mocked_adaptation: Adaptation, valid_gdf: GeoDataFrame - ): + def test_calculate_bc_ratio_returns_gdf(self, mocked_adaptation: Adaptation): # 1. Define test data. - _benefit_gdf = valid_gdf - _cost_gdf = valid_gdf + _nof_rows = 10 + _benefit_gdf = GeoDataFrame(index=range(_nof_rows)) + _cost_gdf = GeoDataFrame(index=range(_nof_rows)) for i, _option in enumerate( mocked_adaptation.adaptation_collection.adaptation_options ): - _benefit_gdf[f"{_option.id}_benefit"] = 4.0 + i - _cost_gdf[f"{_option.id}_cost"] = 1.0 + i + _benefit_gdf[_option.benefit_col] = 4.0 + i + _cost_gdf[_option.cost_col] = 1.0 + i # 2. Run test. _result = mocked_adaptation.calculate_bc_ratio(_benefit_gdf, _cost_gdf) @@ -138,38 +148,13 @@ def test_calculate_bc_ratio_returns_gdf( assert not _result.geometry.empty assert all( [ - f"{_option.id}_bc_ratio" in _result.columns + _option.bc_ratio_col in _result.columns for _option in mocked_adaptation.adaptation_collection.adaptation_options ] ) for i, _option in enumerate( mocked_adaptation.adaptation_collection.adaptation_options ): - assert _result[f"{_option.id}_bc_ratio"].sum(axis=0) == pytest.approx( - len(_result.index) * (4.0 + i) / (1.0 + i) + assert _result[_option.bc_ratio_col].sum(axis=0) == pytest.approx( + _nof_rows * (4.0 + i) / (1.0 + i) ) - - def test_output_gdf_can_be_exported_to_gpkg( - self, - valid_adaptation_config_with_input: tuple[ - AnalysisInputWrapper, AnalysisConfigWrapper - ], - test_result_param_case: Path, - ): - # 1. Define test data. - _adaptation = Adaptation( - valid_adaptation_config_with_input[0], valid_adaptation_config_with_input[1] - ) - - # 2. Run test. - _result = _adaptation.execute().results_collection[0] - - _output_path = _result.output_path - _output_path.mkdir(parents=True, exist_ok=True) - - _result.analysis_result.to_file( - _result.output_path.joinpath("adaptation_output.gpkg"), driver="GPKG" - ) - - # 3. Verify expectations. - assert test_result_param_case.exists() diff --git a/tests/analysis/adaptation/test_adaptation_option.py b/tests/analysis/adaptation/test_adaptation_option.py index b5239d723..9f532a956 100644 --- a/tests/analysis/adaptation/test_adaptation_option.py +++ b/tests/analysis/adaptation/test_adaptation_option.py @@ -8,6 +8,9 @@ from ra2ce.analysis.analysis_config_data.analysis_config_data import ( AnalysisSectionAdaptation, ) +from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( + AnalysisDamagesEnum, +) from ra2ce.analysis.analysis_config_data.enums.analysis_losses_enum import ( AnalysisLossesEnum, ) @@ -155,11 +158,11 @@ class MockAdaptationOption(AdaptationOption): assert isinstance(_result, float) assert _result == pytest.approx(net_unit_cost) - def test_calculate_impact_returns_series(self) -> GeoDataFrame: + def test_calculate_impact_returns_gdf(self) -> GeoDataFrame: @dataclass # Mock to avoid the need to run the impact analysis. class MockAdaptationOptionAnalysis: - analysis_type: str + analysis_type: AnalysisDamagesEnum | AnalysisLossesEnum result_col: str result: float @@ -170,11 +173,14 @@ def execute(self, _: AnalysisConfigWrapper) -> Series: _nof_rows = 10 _analyses = [ MockAdaptationOptionAnalysis( - analysis_type=f"Analysis_{i}", + analysis_type=_analysis_type, result_col=f"Result_{i}", result=(i + 1) * 1.0e6, ) - for i in range(2) + for i, _analysis_type in zip( + range(2), + [AnalysisDamagesEnum.DAMAGES, AnalysisLossesEnum.MULTI_LINK_LOSSES], + ) ] _id = "Option1" _option = AdaptationOption( @@ -192,7 +198,7 @@ def execute(self, _: AnalysisConfigWrapper) -> Series: _result = _option.calculate_impact(1.0) # 3. Verify expectations. - assert isinstance(_result, Series) - assert _result.sum() == pytest.approx( + assert isinstance(_result, GeoDataFrame) + assert _result[_option.impact_col].sum() == pytest.approx( _nof_rows * sum(x.result for x in _analyses) ) diff --git a/tests/analysis/adaptation/test_adaptation_option_collection.py b/tests/analysis/adaptation/test_adaptation_option_collection.py index 6b7887a45..e85642a4c 100644 --- a/tests/analysis/adaptation/test_adaptation_option_collection.py +++ b/tests/analysis/adaptation/test_adaptation_option_collection.py @@ -67,15 +67,25 @@ def test_calculate_options_unit_cost_returns_dict( assert isinstance(_result, dict) assert all(_option in _result for _option in _collection.adaptation_options) - def test_calculate_options_benefit_returns_series(self): + def test_calculate_options_benefit_returns_gdf(self): @dataclass class MockOption: # Mock to avoid the need to run the impact analysis. id: str impact: float + @property + def benefit_col(self) -> str: + return f"{self.id}_benefit" + + @property + def impact_col(self) -> str: + return f"{self.id}_impact" + def calculate_impact(self, _) -> Series: - return Series(self.impact, index=range(_nof_rows)) + _impact_gdf = GeoDataFrame(index=range(10)) + _impact_gdf[self.impact_col] = self.impact + return _impact_gdf # 1. Define test data. _nof_rows = 10 @@ -91,7 +101,7 @@ def calculate_impact(self, _) -> Series: # 3. Verify expectations. assert isinstance(_result, GeoDataFrame) assert all( - f"{_option.id}_benefit" in _result.columns + _option.benefit_col in _result.columns for _option in _collection.adaptation_options ) assert all( From 5109a17cfa3d753bbb56db66ad12cfea9514b496 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Wed, 11 Dec 2024 16:37:47 +0100 Subject: [PATCH 6/8] tests: rework the adaptation runner test --- ra2ce/analysis/adaptation/adaptation.py | 7 +- ra2ce/analysis/analysis_collection.py | 2 - tests/runners/conftest.py | 11 ++- .../test_adaptation_analysis_runner.py | 92 +++++++++++++++++++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/ra2ce/analysis/adaptation/adaptation.py b/ra2ce/analysis/adaptation/adaptation.py index 8f1caa43f..d5cecfb53 100644 --- a/ra2ce/analysis/adaptation/adaptation.py +++ b/ra2ce/analysis/adaptation/adaptation.py @@ -126,11 +126,14 @@ def calculate_bc_ratio( """ def copy_column(from_gdf: GeoDataFrame, col_name: str) -> None: - benefit_gdf[col_name] = from_gdf[col_name] + if not col_name in from_gdf.columns: + return + benefit_gdf.insert(loc=0, column=col_name, value=from_gdf[col_name]) # Copy relevant columns from the original graph _orig_gdf = self.graph_file_hazard.get_graph() - for _col in ["link_id", "geometry", "highway", "length"]: + benefit_gdf.set_geometry(_orig_gdf.geometry, inplace=True) + for _col in ["length", "highway", "infra_type", "link_id"]: copy_column(_orig_gdf, _col) for _option in self.adaptation_collection.adaptation_options: diff --git a/ra2ce/analysis/analysis_collection.py b/ra2ce/analysis/analysis_collection.py index e2664ca48..3af1f3c6c 100644 --- a/ra2ce/analysis/analysis_collection.py +++ b/ra2ce/analysis/analysis_collection.py @@ -22,12 +22,10 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Type from ra2ce.analysis.adaptation.adaptation import Adaptation from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper from ra2ce.analysis.analysis_factory import AnalysisFactory -from ra2ce.analysis.analysis_protocol import AnalysisProtocol from ra2ce.analysis.damages.analysis_damages_protocol import AnalysisDamagesProtocol from ra2ce.analysis.losses.analysis_losses_protocol import AnalysisLossesProtocol diff --git a/tests/runners/conftest.py b/tests/runners/conftest.py index ebb0039e3..8aebf322a 100644 --- a/tests/runners/conftest.py +++ b/tests/runners/conftest.py @@ -7,6 +7,8 @@ from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( AnalysisDamagesEnum, ) +from ra2ce.analysis.analysis_config_data.enums.damage_curve_enum import DamageCurveEnum +from ra2ce.analysis.analysis_config_data.enums.event_type_enum import EventTypeEnum from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper from ra2ce.configuration.config_wrapper import ConfigWrapper from ra2ce.network.network_config_wrapper import NetworkConfigWrapper @@ -45,7 +47,14 @@ def _get_dummy_ra2ce_input_with_damages( dummy_ra2ce_input: ConfigWrapper, ) -> ConfigWrapper: dummy_ra2ce_input.analysis_config.config_data.analyses = [ - AnalysisSectionDamages(analysis=AnalysisDamagesEnum.DAMAGES) + AnalysisSectionDamages( + analysis=AnalysisDamagesEnum.DAMAGES, + name="Damages", + event_type=EventTypeEnum.EVENT, + damage_curve=DamageCurveEnum.MAN, + save_csv=True, + save_gpkg=True, + ) ] dummy_ra2ce_input.network_config.config_data.hazard.hazard_map = "A value" return dummy_ra2ce_input diff --git a/tests/runners/test_adaptation_analysis_runner.py b/tests/runners/test_adaptation_analysis_runner.py index c55d8b2dd..414fca6ad 100644 --- a/tests/runners/test_adaptation_analysis_runner.py +++ b/tests/runners/test_adaptation_analysis_runner.py @@ -1,9 +1,18 @@ +from pathlib import Path +from shutil import copytree, rmtree + +from ra2ce.analysis.analysis_collection import AnalysisCollection from ra2ce.analysis.analysis_config_data.analysis_config_data import ( AnalysisSectionAdaptation, AnalysisSectionAdaptationOption, ) +from ra2ce.analysis.analysis_config_data.enums.analysis_enum import AnalysisEnum +from ra2ce.analysis.analysis_factory import AnalysisFactory +from ra2ce.analysis.analysis_result.analysis_result_wrapper import AnalysisResultWrapper from ra2ce.configuration.config_wrapper import ConfigWrapper +from ra2ce.network.graph_files.graph_files_collection import GraphFilesCollection from ra2ce.runners.adaptation_analysis_runner import AdaptationAnalysisRunner +from tests import test_data class TestAdaptationAnalysisRunner: @@ -55,3 +64,86 @@ def test_given_valid_damages_and_adaptation_input_configuration_can_run( # 3. Verify expectation assert _result is True + + def test_adapatation_can_run_and_export_result( + self, + damages_ra2ce_input: ConfigWrapper, + test_result_param_case: Path, + ): + # 1. Define test data. + assert damages_ra2ce_input.analysis_config.config_data.adaptation is None + + _root_path = test_result_param_case + damages_ra2ce_input.analysis_config.config_data.root_path = _root_path + damages_ra2ce_input.analysis_config.config_data.input_path = ( + _root_path.joinpath("input") + ) + damages_ra2ce_input.analysis_config.config_data.static_path = ( + _root_path.joinpath("static") + ) + damages_ra2ce_input.analysis_config.config_data.output_path = ( + _root_path.joinpath("output") + ) + + # Add adaptation analysis to the configuration + _adaptation_config = AnalysisSectionAdaptation( + analysis=AnalysisEnum.ADAPTATION, + name="Adaptation", + adaptation_options=[ + AnalysisSectionAdaptationOption(id="AO0"), + ], + save_csv=True, + save_gpkg=True, + ) + damages_ra2ce_input.analysis_config.config_data.analyses.append( + _adaptation_config + ) + + # Copy input files for the adaptation analysis + if _root_path.exists(): + rmtree(_root_path) + damages_ra2ce_input.analysis_config.config_data.output_path.mkdir(parents=True) + for _option in _adaptation_config.adaptation_options: + _ao_path = ( + damages_ra2ce_input.analysis_config.config_data.input_path.joinpath( + _option.id + ) + ) + copytree(test_data.joinpath("adaptation", "input"), _ao_path) + copytree( + test_data.joinpath("adaptation", "static"), + damages_ra2ce_input.analysis_config.config_data.static_path, + ) + + # Read graph/network files + damages_ra2ce_input.analysis_config.graph_files = ( + GraphFilesCollection.set_files( + damages_ra2ce_input.analysis_config.config_data.static_path.joinpath( + "output_graph" + ), + ) + ) + + _analysis_collection = AnalysisCollection( + damages_analyses=None, + losses_analyses=None, + adaptation_analysis=AnalysisFactory.get_adaptation_analysis( + damages_ra2ce_input.analysis_config.config_data.adaptation, + damages_ra2ce_input.analysis_config, + ), + ) + + # 2. Run test. + _result = AdaptationAnalysisRunner().run(_analysis_collection) + + # 3. Verify expectation + assert isinstance(_result, list) + assert len(_result) == 1 + + _result_wrapper = _result[0] + assert isinstance(_result_wrapper, AnalysisResultWrapper) + assert _result_wrapper.is_valid_result() == True + + _analysis_result = _result_wrapper.results_collection[0] + assert _analysis_result.base_export_path.with_suffix(".gpkg").exists() + assert _analysis_result.base_export_path.with_suffix(".csv").exists() From 5e5e18f9911d43a9cf4c7439082dc8a6e13b8355 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Thu, 12 Dec 2024 16:55:15 +0100 Subject: [PATCH 7/8] chore: process re-review comments --- .../analysis/adaptation/adaptation_option.py | 4 --- tests/analysis/adaptation/test_adaptation.py | 29 ++++++++----------- .../adaptation/test_adaptation_option.py | 16 +++++----- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/ra2ce/analysis/adaptation/adaptation_option.py b/ra2ce/analysis/adaptation/adaptation_option.py index c00bcc1d4..547fd45c8 100644 --- a/ra2ce/analysis/adaptation/adaptation_option.py +++ b/ra2ce/analysis/adaptation/adaptation_option.py @@ -62,10 +62,6 @@ def impact_col(self) -> str: def benefit_col(self) -> str: return self._get_column_name("benefit") - @property - def impact_col(self) -> str: - return self._get_column_name("impact") - @property def bc_ratio_col(self) -> str: return self._get_column_name("bc_ratio") diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index c508e3b9a..407e67344 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -6,6 +6,7 @@ from shapely import Point from ra2ce.analysis.adaptation.adaptation import Adaptation +from ra2ce.analysis.adaptation.adaptation_option import AdaptationOption from ra2ce.analysis.adaptation.adaptation_option_collection import ( AdaptationOptionCollection, ) @@ -87,22 +88,6 @@ def test_run_benefit_returns_gdf( @pytest.fixture(name="mocked_adaptation") def _get_mocked_adaptation_fixture(self) -> Iterator[Adaptation]: # Mock to avoid complex setup. - @dataclass - class MockAdaptationOption: - id: str - - @property - def cost_col(self) -> str: - return f"{self.id}_cost" - - @property - def benefit_col(self) -> str: - return f"{self.id}_benefit" - - @property - def bc_ratio_col(self) -> str: - return f"{self.id}_bc_ratio" - class MockAdaptation(Adaptation): graph_file_hazard = NetworkFile( graph=GeoDataFrame.from_dict( @@ -118,7 +103,17 @@ class MockAdaptation(Adaptation): adaptation_collection: AdaptationOptionCollection = ( AdaptationOptionCollection( all_options=[ - MockAdaptationOption(id=f"Option{x}") for x in range(2) + AdaptationOption( + id=f"Option{x}", + name=None, + construction_cost=None, + construction_interval=None, + maintenance_cost=None, + maintenance_interval=None, + analyses=None, + analysis_config=None, + ) + for x in range(2) ] ) ) diff --git a/tests/analysis/adaptation/test_adaptation_option.py b/tests/analysis/adaptation/test_adaptation_option.py index 9f532a956..df16a2e2b 100644 --- a/tests/analysis/adaptation/test_adaptation_option.py +++ b/tests/analysis/adaptation/test_adaptation_option.py @@ -5,6 +5,9 @@ from pandas import Series from ra2ce.analysis.adaptation.adaptation_option import AdaptationOption +from ra2ce.analysis.adaptation.adaptation_option_analysis import ( + AdaptationOptionAnalysis, +) from ra2ce.analysis.analysis_config_data.analysis_config_data import ( AnalysisSectionAdaptation, ) @@ -132,13 +135,8 @@ def test_calculate_unit_cost_returns_float( maint_interval: float, net_unit_cost: float, ): - # Mock to avoid complex setup. - @dataclass - class MockAdaptationOption(AdaptationOption): - id: str - # 1. Define test data. - _option = MockAdaptationOption( + _option = AdaptationOption( id="AnOption", name=None, construction_cost=constr_cost, @@ -161,9 +159,7 @@ class MockAdaptationOption(AdaptationOption): def test_calculate_impact_returns_gdf(self) -> GeoDataFrame: @dataclass # Mock to avoid the need to run the impact analysis. - class MockAdaptationOptionAnalysis: - analysis_type: AnalysisDamagesEnum | AnalysisLossesEnum - result_col: str + class MockAdaptationOptionAnalysis(AdaptationOptionAnalysis): result: float def execute(self, _: AnalysisConfigWrapper) -> Series: @@ -174,6 +170,8 @@ def execute(self, _: AnalysisConfigWrapper) -> Series: _analyses = [ MockAdaptationOptionAnalysis( analysis_type=_analysis_type, + analysis_class=None, + analysis_input=None, result_col=f"Result_{i}", result=(i + 1) * 1.0e6, ) From af36878791d10be145c72e066025ad26d8125e9f Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk Date: Thu, 12 Dec 2024 17:04:36 +0100 Subject: [PATCH 8/8] tests: add assert on file content --- tests/runners/test_adaptation_analysis_runner.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/runners/test_adaptation_analysis_runner.py b/tests/runners/test_adaptation_analysis_runner.py index 414fca6ad..f8107f87c 100644 --- a/tests/runners/test_adaptation_analysis_runner.py +++ b/tests/runners/test_adaptation_analysis_runner.py @@ -1,6 +1,9 @@ from pathlib import Path from shutil import copytree, rmtree +from geopandas import read_file +from geopandas.testing import assert_geodataframe_equal + from ra2ce.analysis.analysis_collection import AnalysisCollection from ra2ce.analysis.analysis_config_data.analysis_config_data import ( AnalysisSectionAdaptation, @@ -145,5 +148,10 @@ def test_adapatation_can_run_and_export_result( assert _result_wrapper.is_valid_result() == True _analysis_result = _result_wrapper.results_collection[0] - assert _analysis_result.base_export_path.with_suffix(".gpkg").exists() + _output_gdf = _analysis_result.base_export_path.with_suffix(".gpkg") + assert _output_gdf.exists() assert _analysis_result.base_export_path.with_suffix(".csv").exists() + + # Check the output geodataframe content + _gdf = read_file(_output_gdf) + assert_geodataframe_equal(_gdf, _analysis_result.analysis_result)