From 24046faaf2509e9a6339aa7523cb5297218203e8 Mon Sep 17 00:00:00 2001 From: Ardt Klapwijk <59741981+ArdtK@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:30:39 +0100 Subject: [PATCH] feat: 595 adaptation run and combine losses and damages (#616) --- ra2ce/analysis/adaptation/adaptation.py | 51 +++++-- .../analysis/adaptation/adaptation_option.py | 112 +++++++------- .../adaptation/adaptation_option_analysis.py | 137 ++++++++++++++++++ .../adaptation_option_collection.py | 70 ++++++--- ra2ce/analysis/analysis_collection.py | 2 +- .../analysis_config_data.py | 52 ++++++- .../analysis_config_data_reader.py | 9 +- .../enums/analysis_losses_enum.py | 6 + ra2ce/analysis/analysis_factory.py | 39 ++++- ra2ce/analysis/analysis_input_wrapper.py | 10 +- ra2ce/analysis/analysis_protocol.py | 4 +- ra2ce/analysis/losses/losses_base.py | 2 +- .../resilience_curves_reader.py | 16 +- .../losses/time_values/time_values_reader.py | 8 +- .../traffic_intensities_reader.py | 11 +- ra2ce/configuration/ra2ce_enum_base.py | 6 +- tests/analysis/adaptation/conftest.py | 85 +++++++++-- tests/analysis/adaptation/test_adaptation.py | 71 ++++----- .../adaptation/test_adaptation_option.py | 71 +++++---- .../test_adaptation_option_analysis.py | 97 +++++++++++++ .../test_adaptation_option_collection.py | 12 +- .../test_analysis_config_data.py | 47 +++++- .../test_traffic_intensities_reader.py | 4 +- .../test_analysis_result_wrapper_exporter.py | 4 +- .../input/resilience_curves.csv} | 0 .../input/traffic_intensities.csv | 0 .../input/values_of_time.csv | 0 .../output/hazard_names.xlsx | Bin 0 -> 5141 bytes .../adaptation/output/hazard_names.xlsx | Bin 0 -> 5053 bytes .../static/output_graph/avg_speed.csv | 12 +- .../static/output_graph/base_graph.p | Bin 74735 -> 74735 bytes .../static/output_graph/base_graph_edges.gpkg | Bin 159744 -> 159744 bytes .../static/output_graph/base_graph_hazard.p | Bin 83015 -> 83015 bytes .../output_graph/base_graph_hazard_edges.gpkg | Bin 159744 -> 159744 bytes .../output_graph/base_graph_hazard_nodes.gpkg | Bin 118784 -> 118784 bytes .../static/output_graph/base_graph_nodes.gpkg | Bin 118784 -> 118784 bytes .../static/output_graph/base_network.gpkg | Bin 159744 -> 159744 bytes .../output_graph/base_network_hazard.gpkg | Bin 159744 -> 159744 bytes 38 files changed, 694 insertions(+), 244 deletions(-) create mode 100644 ra2ce/analysis/adaptation/adaptation_option_analysis.py create mode 100644 tests/analysis/adaptation/test_adaptation_option_analysis.py rename tests/test_data/adaptation/input/{losses/input/resilience_curve.csv => multi_link_losses/input/resilience_curves.csv} (100%) rename tests/test_data/adaptation/input/{losses => multi_link_losses}/input/traffic_intensities.csv (100%) rename tests/test_data/adaptation/input/{losses => multi_link_losses}/input/values_of_time.csv (100%) create mode 100644 tests/test_data/adaptation/input/multi_link_losses/output/hazard_names.xlsx create mode 100644 tests/test_data/adaptation/output/hazard_names.xlsx diff --git a/ra2ce/analysis/adaptation/adaptation.py b/ra2ce/analysis/adaptation/adaptation.py index 716ed72fe..33045cb09 100644 --- a/ra2ce/analysis/adaptation/adaptation.py +++ b/ra2ce/analysis/adaptation/adaptation.py @@ -27,31 +27,35 @@ AdaptationOptionCollection, ) from ra2ce.analysis.analysis_config_data.analysis_config_data import ( - AnalysisConfigData, AnalysisSectionAdaptation, ) +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper from ra2ce.analysis.damages.analysis_damages_protocol import AnalysisDamagesProtocol from ra2ce.network.graph_files.network_file import NetworkFile class Adaptation(AnalysisDamagesProtocol): + """ + Execute the adaptation analysis. + For each adaptation option a damages and losses analysis is executed. + """ + analysis: AnalysisSectionAdaptation graph_file: NetworkFile - graph_file_hazard: NetworkFile input_path: Path output_path: Path adaptation_collection: AdaptationOptionCollection # TODO: add the proper protocol for the adaptation analysis. def __init__( - self, analysis_input: AnalysisInputWrapper, analysis_config: AnalysisConfigData + self, + analysis_input: AnalysisInputWrapper, + analysis_config: AnalysisConfigWrapper, ): self.analysis = analysis_input.analysis self.graph_file = analysis_input.graph_file 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 ) @@ -59,33 +63,54 @@ def __init__( def execute(self) -> GeoDataFrame: """ Run the adaptation analysis. + + Returns: + GeoDataFrame: The result of the adaptation analysis. """ return self.calculate_bc_ratio() def run_cost(self) -> GeoDataFrame: """ - Calculate the cost for all adaptation options. + Calculate the unit cost for all adaptation options. + + Returns: + GeoDataFrame: The result of the cost calculation. """ - # Open the network without hazard data _cost_gdf = deepcopy(self.graph_file.get_graph()) + for ( _option, _cost, - ) in self.adaptation_collection.calculate_option_cost().items(): - _cost_gdf[f"costs_{_option.id}"] = _cost + ) in self.adaptation_collection.calculate_options_cost().items(): + _cost_gdf[f"{_option.id}_cost"] = _cost + + # TODO: calculate link cost instead of unit cost return _cost_gdf def run_benefit(self) -> GeoDataFrame: """ - Calculate the benefit for all adaptation options + Calculate the benefit for all adaptation options. + + Returns: + GeoDataFrame: The result of the benefit calculation. """ - return None + _benefit_gdf = deepcopy(self.graph_file.get_graph()) + + return self.adaptation_collection.calculation_options_impact(_benefit_gdf) def calculate_bc_ratio(self) -> GeoDataFrame: """ - Calculate the benefit-cost ratio for all adaptation options + Calculate the benefit-cost ratio for all adaptation options. + + Returns: + GeoDataFrame: The result of the benefit-cost ratio calculation. """ _cost_gdf = self.run_cost() _benefit_gdf = self.run_benefit() - return None + + # TODO: apply economic discounting + # TODO: calculate B/C ratio + # TODO: apply overlay + + return _cost_gdf diff --git a/ra2ce/analysis/adaptation/adaptation_option.py b/ra2ce/analysis/adaptation/adaptation_option.py index 2a8c34b96..095b0a905 100644 --- a/ra2ce/analysis/adaptation/adaptation_option.py +++ b/ra2ce/analysis/adaptation/adaptation_option.py @@ -20,15 +20,20 @@ """ from __future__ import annotations -from copy import deepcopy from dataclasses import asdict, dataclass -from pathlib import Path +from geopandas import GeoDataFrame + +from ra2ce.analysis.adaptation.adaptation_option_analysis import ( + AdaptationOptionAnalysis, +) from ra2ce.analysis.analysis_config_data.analysis_config_data import ( AnalysisSectionAdaptationOption, - AnalysisSectionDamages, - AnalysisSectionLosses, ) +from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( + AnalysisDamagesEnum, +) +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper @dataclass @@ -39,71 +44,56 @@ class AdaptationOption: construction_interval: float maintenance_cost: float maintenance_interval: float - damages_root: Path - damages_config: AnalysisSectionDamages - losses_root: Path - losses_config: AnalysisSectionLosses + analyses: list[AdaptationOptionAnalysis] + analysis_config: AnalysisConfigWrapper def __hash__(self) -> int: return hash(self.id) - @property - def input_path(self) -> Path: - return self.damages_root.joinpath("input") - - @property - def static_path(self) -> Path: - return self.damages_root.joinpath("static") - - @property - def output_path(self) -> Path: - return self.damages_root.joinpath("output") - @classmethod def from_config( cls, - root_path: Path, + analysis_config: AnalysisConfigWrapper, adaptation_option: AnalysisSectionAdaptationOption, - damages_section: AnalysisSectionDamages, - losses_section: AnalysisSectionLosses, ) -> AdaptationOption: - # Adjust path to the input files - def extend_path(analysis: str, input_path: Path) -> Path: - if not input_path: - return None - # Input is directory: add stuff at the end - if not (input_path.suffix): - return input_path.joinpath("input", adaptation_option.id, analysis) - return input_path.parent.joinpath( - "input", adaptation_option.id, analysis, input_path.name - ) + """ + 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 - if not damages_section or not losses_section: + 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 + or not analysis_config.config_data.losses_list + ): raise ValueError( "Damages and losses sections are required to create an adaptation option." ) - _damages_root = extend_path("damages", root_path) - _damages_section = deepcopy(damages_section) - - _losses_root = extend_path("losses", root_path) - _losses_section = deepcopy(losses_section) - _losses_section.resilience_curves_file = extend_path( - "losses", losses_section.resilience_curves_file - ) - _losses_section.traffic_intensities_file = extend_path( - "losses", losses_section.traffic_intensities_file - ) - _losses_section.values_of_time_file = extend_path( - "losses", losses_section.values_of_time_file - ) + # Create input for the analyses + _analyses = [ + AdaptationOptionAnalysis.from_config( + analysis_config=analysis_config, + analysis_type=_analysis, + option_id=adaptation_option.id, + ) + for _analysis in [ + AnalysisDamagesEnum.DAMAGES, + analysis_config.config_data.adaptation.losses_analysis, + ] + ] return cls( **asdict(adaptation_option), - damages_root=_damages_root, - damages_config=_damages_section, - losses_root=_losses_root, - losses_config=_losses_section, + analyses=_analyses, + analysis_config=analysis_config, ) def calculate_cost(self, time_horizon: float, discount_rate: float) -> float: @@ -148,3 +138,21 @@ def calc_cost(cost: float, year: float) -> float: _lifetime_cost += calc_cost(self.maintenance_cost, _maint_year) return _lifetime_cost + + def calculate_impact(self, benefit_graph: GeoDataFrame) -> GeoDataFrame: + """ + Calculate the impact of the adaptation option. + + Returns: + float: The impact of the adaptation option. + """ + for _analysis in self.analyses: + _result = _analysis.execute(self.analysis_config) + _col = _result.filter(regex=_analysis.result_col).columns[0] + benefit_graph[f"{self.id}_{_col}"] = _result[_col] + + # Calculate the impact (summing the damages and losses values) + _option_cols = benefit_graph.filter(regex=f"{self.id}_").columns + benefit_graph[f"{self.id}_impact"] = benefit_graph[_option_cols].sum(axis=1) + + return benefit_graph diff --git a/ra2ce/analysis/adaptation/adaptation_option_analysis.py b/ra2ce/analysis/adaptation/adaptation_option_analysis.py new file mode 100644 index 000000000..5bb00e4aa --- /dev/null +++ b/ra2ce/analysis/adaptation/adaptation_option_analysis.py @@ -0,0 +1,137 @@ +""" + 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 . +""" +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass + +from geopandas import GeoDataFrame + +from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( + AnalysisDamagesEnum, +) +from ra2ce.analysis.analysis_config_data.enums.analysis_losses_enum import ( + AnalysisLossesEnum, +) +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper +from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper +from ra2ce.analysis.damages.damages import Damages +from ra2ce.analysis.losses.losses_base import LossesBase +from ra2ce.analysis.losses.multi_link_losses import MultiLinkLosses +from ra2ce.analysis.losses.single_link_losses import SingleLinkLosses + + +@dataclass +class AdaptationOptionAnalysis: + analysis_class: type[Damages | LossesBase] + analysis_input: AnalysisInputWrapper + result_col: str + + @staticmethod + def get_analysis( + analysis_type: AnalysisDamagesEnum | AnalysisLossesEnum, + ) -> tuple[type[Damages | LossesBase], str]: + """ + Get the analysis class and the result column for the given analysis. + + Args: + analysis (AnalysisDamagesEnum | AnalysisLossesEnum): The type of analysis. + + Raises: + NotImplementedError: The analysis type is not implemented. + + Returns: + tuple[type[Damages | LossesBase], str]: The analysis class and the result column. + """ + if analysis_type == AnalysisDamagesEnum.DAMAGES: + return (Damages, "dam_") + elif analysis_type == AnalysisLossesEnum.SINGLE_LINK_LOSSES: + return (SingleLinkLosses, "vlh_.*_total") + elif analysis_type == AnalysisLossesEnum.MULTI_LINK_LOSSES: + return (MultiLinkLosses, "vlh_.*_total") + raise NotImplementedError(f"Analysis {analysis_type} not implemented") + + @classmethod + def from_config( + cls, + analysis_config: AnalysisConfigWrapper, + analysis_type: AnalysisLossesEnum | AnalysisDamagesEnum, + option_id: str, + ) -> AdaptationOptionAnalysis: + """ + Classmethod to create an AdaptationOptionAnalysis from an analysis configuration. + + Args: + analysis_config (AnalysisConfigWrapper): The analysis configuration. + analysis_type (AnalysisLossesEnum | AnalysisDamagesEnum): The type of analysis. + option_id (str): The ID of the adaptation option. + + Returns: + AdaptationOptionAnalysis: The created AdaptationOptionAnalysis. + """ + + # Need a deepcopy to avoid mixing up configs across analyses. + _analysis_config = deepcopy(analysis_config) + _analysis_config.config_data = ( + _analysis_config.config_data.reroot_analysis_config( + analysis_type, + analysis_config.config_data.root_path.joinpath("input", option_id), + ) + ) + + # Create analysis input + _analysis = _analysis_config.config_data.get_analysis(analysis_type) + if analysis_type == AnalysisDamagesEnum.DAMAGES: + _graph_file = None + _graph_file_hazard = analysis_config.graph_files.base_network_hazard + else: + _graph_file = analysis_config.graph_files.base_graph + _graph_file_hazard = analysis_config.graph_files.base_graph_hazard + + _analysis_input = AnalysisInputWrapper.from_input( + analysis=_analysis, + analysis_config=_analysis_config, + graph_file=_graph_file, + graph_file_hazard=_graph_file_hazard, + ) + + # Create output object + _analysis_class, _result_col = cls.get_analysis(analysis_type) + + return cls( + analysis_class=_analysis_class, + analysis_input=_analysis_input, + result_col=_result_col, + ) + + def execute(self, analysis_config: AnalysisConfigWrapper) -> GeoDataFrame: + """ + Execute the analysis. + + Args: + analysis_config (AnalysisConfigWrapper): The config for the analysis. + + Returns: + DataFrame: The results of the analysis. + """ + if self.analysis_class == Damages: + return self.analysis_class(self.analysis_input).execute() + return self.analysis_class(self.analysis_input, analysis_config).execute() diff --git a/ra2ce/analysis/adaptation/adaptation_option_collection.py b/ra2ce/analysis/adaptation/adaptation_option_collection.py index d12b313f3..fd6dc4a8b 100644 --- a/ra2ce/analysis/adaptation/adaptation_option_collection.py +++ b/ra2ce/analysis/adaptation/adaptation_option_collection.py @@ -22,11 +22,10 @@ from dataclasses import dataclass, field +from geopandas import GeoDataFrame + from ra2ce.analysis.adaptation.adaptation_option import AdaptationOption -from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData -from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( - AnalysisDamagesEnum, -) +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper @dataclass @@ -56,38 +55,48 @@ def adaptation_options(self) -> list[AdaptationOption]: @classmethod def from_config( cls, - analysis_config_data: AnalysisConfigData, + analysis_config: AnalysisConfigWrapper, ) -> AdaptationOptionCollection: - if not analysis_config_data.adaptation: + """ + Classmethod to create a collection of adaptation options from an analysis configuration. + + Args: + analysis_config (AnalysisConfigWrapper): Analysis config input + + Raises: + ValueError: If no adaptation section is found in the analysis config data. + + Returns: + AdaptationOptionCollection: The created collection of adaptation options. + """ + if ( + analysis_config.config_data is None + or analysis_config.config_data.adaptation is None + ): raise ValueError("No adaptation section found in the analysis config data.") - _collection = cls( - discount_rate=analysis_config_data.adaptation.discount_rate, - time_horizon=analysis_config_data.adaptation.time_horizon, - climate_factor=analysis_config_data.adaptation.climate_factor, - initial_frequency=analysis_config_data.adaptation.initial_frequency, - ) - _damages_analysis = analysis_config_data.get_analysis( - AnalysisDamagesEnum.DAMAGES - ) - _losses_analysis = analysis_config_data.get_analysis( - analysis_config_data.adaptation.losses_analysis + _collection = cls( + discount_rate=analysis_config.config_data.adaptation.discount_rate, + time_horizon=analysis_config.config_data.adaptation.time_horizon, + climate_factor=analysis_config.config_data.adaptation.climate_factor, + initial_frequency=analysis_config.config_data.adaptation.initial_frequency, ) - for _config_option in analysis_config_data.adaptation.adaptation_options: + for _config_option in analysis_config.config_data.adaptation.adaptation_options: _collection.all_options.append( AdaptationOption.from_config( - analysis_config_data.root_path, + analysis_config, _config_option, - _damages_analysis, - _losses_analysis, ) ) return _collection - def calculate_option_cost(self) -> dict[AdaptationOption, float]: + def calculate_options_cost(self) -> dict[AdaptationOption, float]: """ - Calculate the cost for all adaptation options. + Calculate the unit cost for all adaptation options. + + Returns: + dict[AdaptationOption, float]: The calculated cost for all adaptation options. """ return { _option: _option.calculate_cost( @@ -96,3 +105,18 @@ def calculate_option_cost(self) -> dict[AdaptationOption, float]: ) for _option in self.adaptation_options } + + def calculation_options_impact(self, benefit_graph: GeoDataFrame) -> GeoDataFrame: + """ + Calculate the impact of all adaptation options (including the reference option). + + Args: + benefit_graph (GeoDataFrame): The graph to which the impact of the adaptation options will be added. + + Returns: + NetworkFile: The calculated impact of all adaptation options. + """ + for _option in self.all_options: + benefit_graph = _option.calculate_impact(benefit_graph) + + return benefit_graph diff --git a/ra2ce/analysis/analysis_collection.py b/ra2ce/analysis/analysis_collection.py index 9d9011cac..4eae56807 100644 --- a/ra2ce/analysis/analysis_collection.py +++ b/ra2ce/analysis/analysis_collection.py @@ -55,7 +55,7 @@ def from_config(cls, analysis_config: AnalysisConfigWrapper) -> AnalysisCollecti AnalysisFactory.get_losses_analysis(analysis, analysis_config) for analysis in analysis_config.config_data.losses_list ], - adaptation_analysis=AnalysisFactory.get_damages_analysis( + adaptation_analysis=AnalysisFactory.get_adaptation_analysis( analysis_config.config_data.adaptation, analysis_config ), ) diff --git a/ra2ce/analysis/analysis_config_data/analysis_config_data.py b/ra2ce/analysis/analysis_config_data/analysis_config_data.py index e8db14c56..fef3ca62e 100644 --- a/ra2ce/analysis/analysis_config_data/analysis_config_data.py +++ b/ra2ce/analysis/analysis_config_data/analysis_config_data.py @@ -185,12 +185,16 @@ class AnalysisConfigData(ConfigDataProtocol): Additionally, some attributes from the network config are added for completeness (files, origins_destinations, network, hazard_names) """ + ANALYSIS_SECTION = ( + AnalysisSectionDamages | AnalysisSectionLosses | AnalysisSectionAdaptation + ) + root_path: Optional[Path] = None input_path: Optional[Path] = None output_path: Optional[Path] = None static_path: Optional[Path] = None project: ProjectSection = field(default_factory=ProjectSection) - analyses: list[AnalysisSectionBase] = field(default_factory=list) + analyses: list[ANALYSIS_SECTION] = field(default_factory=list) origins_destinations: Optional[OriginsDestinationsSection] = field( default_factory=OriginsDestinationsSection ) @@ -198,6 +202,47 @@ class AnalysisConfigData(ConfigDataProtocol): aggregate_wl: AggregateWlEnum = field(default_factory=lambda: AggregateWlEnum.NONE) hazard_names: list[str] = field(default_factory=list) + def reroot_analysis_config( + self, + analysis_type: AnalysisDamagesEnum | AnalysisLossesEnum, + new_root: Path, + ) -> AnalysisConfigData: + """ + Reroot dependent analysis in config data to the input of another analysis. + + Returns: + AnalysisConfigData: The rerooted config data. + """ + + def reroot_path(orig_path: Optional[Path]) -> Optional[Path]: + # Rewrite the path to the new root + if not orig_path or not self.root_path: + return None + _orig_parts = orig_path.parts + _rel_path = Path(*_orig_parts[len(self.root_path.parts) :]) + return new_root.joinpath(analysis_type.config_value, _rel_path) + + self.input_path = reroot_path(self.input_path) + self.static_path = reroot_path(self.static_path) + self.output_path = reroot_path(self.output_path) + + # Rewrite the paths of the files in the analysis + _analysis = self.get_analysis(analysis_type) + if isinstance(_analysis, AnalysisSectionDamages): + _analysis.file_name = reroot_path(_analysis.file_name) + elif isinstance(_analysis, AnalysisSectionLosses): + _analysis.resilience_curves_file = reroot_path( + _analysis.resilience_curves_file + ) + _analysis.traffic_intensities_file = reroot_path( + _analysis.traffic_intensities_file + ) + _analysis.values_of_time_file = reroot_path(_analysis.values_of_time_file) + + self.root_path = new_root + + return self + @property def damages_list(self) -> list[AnalysisSectionDamages]: """ @@ -237,12 +282,13 @@ def adaptation(self) -> AnalysisSectionAdaptation | None: def get_analysis( self, analysis: AnalysisEnum | AnalysisDamagesEnum | AnalysisLossesEnum - ) -> AnalysisSectionBase | None: + ) -> ANALYSIS_SECTION | None: """ Get a certain analysis from config. Returns: - AnalysisSectionBase: The analysis. + AnalysisSectionLosses | AnalysisSectionDamages | AnalysisSectionAdaptation: + The analysis. """ return next(filter(lambda x: x.analysis == analysis, self.analyses), None) diff --git a/ra2ce/analysis/analysis_config_data/analysis_config_data_reader.py b/ra2ce/analysis/analysis_config_data/analysis_config_data_reader.py index 87ccd1ff8..2f910c9af 100644 --- a/ra2ce/analysis/analysis_config_data/analysis_config_data_reader.py +++ b/ra2ce/analysis/analysis_config_data/analysis_config_data_reader.py @@ -28,7 +28,6 @@ AnalysisConfigData, AnalysisSectionAdaptation, AnalysisSectionAdaptationOption, - AnalysisSectionBase, AnalysisSectionDamages, AnalysisSectionLosses, DamagesAnalysisNameList, @@ -234,7 +233,7 @@ def _get_adaptation_option( _section = AnalysisSectionAdaptation(**self._parser[section_name]) _section.losses_analysis = ( _section.losses_analysis - ) = AnalysisDamagesEnum.get_enum( + ) = AnalysisLossesEnum.get_enum( self._parser.get(section_name, "losses_analysis", fallback=None) ) @@ -250,14 +249,14 @@ def _get_adaptation_option( return _section - def get_analysis_sections(self) -> list[AnalysisSectionBase]: + def get_analysis_sections(self) -> list[AnalysisConfigData.ANALYSIS_SECTION]: """ Extracts info from [analysis] sections Returns: - list[AnalysisSection]: List of analyses (damages, losses and adaptation) + list[ANALYSIS_SECTION]: List of analyses """ - _analysis_sections: list[AnalysisSectionBase] = [] + _analysis_sections: list[AnalysisConfigData.ANALYSIS_SECTION] = [] _section_names = list( section_name diff --git a/ra2ce/analysis/analysis_config_data/enums/analysis_losses_enum.py b/ra2ce/analysis/analysis_config_data/enums/analysis_losses_enum.py index 65493e491..faebb144b 100644 --- a/ra2ce/analysis/analysis_config_data/enums/analysis_losses_enum.py +++ b/ra2ce/analysis/analysis_config_data/enums/analysis_losses_enum.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from ra2ce.configuration.ra2ce_enum_base import Ra2ceEnumBase @@ -12,3 +14,7 @@ class AnalysisLossesEnum(Ra2ceEnumBase): MULTI_LINK_LOSSES = 8 MULTI_LINK_ISOLATED_LOCATIONS = 9 INVALID = 99 + + @classmethod + def get_enum(cls, input_str: str | None) -> AnalysisLossesEnum: + return AnalysisLossesEnum(super().get_enum(input_str)) diff --git a/ra2ce/analysis/analysis_factory.py b/ra2ce/analysis/analysis_factory.py index ebb22f468..d7a92776d 100644 --- a/ra2ce/analysis/analysis_factory.py +++ b/ra2ce/analysis/analysis_factory.py @@ -21,6 +21,7 @@ from ra2ce.analysis.adaptation.adaptation import Adaptation from ra2ce.analysis.analysis_config_data.analysis_config_data import ( + AnalysisSectionAdaptation, AnalysisSectionDamages, AnalysisSectionLosses, ) @@ -82,16 +83,12 @@ def get_damages_analysis( _analysis_input = AnalysisInputWrapper.from_input( analysis=analysis, analysis_config=analysis_config, - graph_file=analysis_config.graph_files.base_network, graph_file_hazard=analysis_config.graph_files.base_network_hazard, ) if analysis.analysis == AnalysisDamagesEnum.DAMAGES: return Damages(_analysis_input) - if analysis.analysis == AnalysisEnum.ADAPTATION: - return Adaptation(_analysis_input, analysis_config.config_data) - raise NotImplementedError(f"Analysis {analysis.analysis} not implemented") @staticmethod @@ -157,7 +154,7 @@ def get_losses_analysis( graph_file=analysis_config.graph_files.origins_destinations_graph, graph_file_hazard=analysis_config.graph_files.origins_destinations_graph_hazard, ) - return OptimalRouteOriginClosestDestination(analysis_input=_analysis_input) + return OptimalRouteOriginClosestDestination(_analysis_input) if ( analysis.analysis @@ -197,3 +194,35 @@ def get_losses_analysis( return MultiLinkIsolatedLocations(_analysis_input) raise NotImplementedError(f"Analysis {analysis.analysis} not implemented") + + @staticmethod + def get_adaptation_analysis( + analysis: AnalysisSectionAdaptation, + analysis_config: AnalysisConfigWrapper, + ) -> AnalysisDamagesProtocol: + """ + Create an analysis based on the given analysis configuration. + + Args: + analysis (AnalysisSectionAdaptation): Analysis section. + analysis_config (AnalysisConfigWrapper): Analysis configuration. + + Raises: + NotImplementedError: The analysis type is not implemented. + + Returns: + AnalysisAdaptationProtocol: The adaptation analysis to be executed. + """ + if not analysis: + return None + + _analysis_input = AnalysisInputWrapper.from_input( + analysis=analysis, + analysis_config=analysis_config, + graph_file=analysis_config.graph_files.base_network, + ) + + if analysis.analysis == AnalysisEnum.ADAPTATION: + return Adaptation(_analysis_input, analysis_config) + + raise NotImplementedError(f"Analysis {analysis.analysis} not implemented") diff --git a/ra2ce/analysis/analysis_input_wrapper.py b/ra2ce/analysis/analysis_input_wrapper.py index 235f55f81..8a343e59e 100644 --- a/ra2ce/analysis/analysis_input_wrapper.py +++ b/ra2ce/analysis/analysis_input_wrapper.py @@ -4,11 +4,7 @@ from pathlib import Path from typing import Optional -from ra2ce.analysis.analysis_config_data.analysis_config_data import ( - AnalysisSectionAdaptation, - AnalysisSectionDamages, - AnalysisSectionLosses, -) +from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper from ra2ce.network.graph_files.graph_files_protocol import GraphFileProtocol from ra2ce.network.hazard.hazard_names import HazardNames @@ -19,7 +15,7 @@ @dataclass class AnalysisInputWrapper: - analysis: AnalysisSectionDamages | AnalysisSectionLosses | AnalysisSectionAdaptation + analysis: AnalysisConfigData.ANALYSIS_SECTION graph_file: Optional[GraphFileProtocol] graph_file_hazard: Optional[GraphFileProtocol] input_path: Path @@ -32,7 +28,7 @@ class AnalysisInputWrapper: @classmethod def from_input( cls, - analysis: AnalysisSectionDamages | AnalysisSectionLosses, + analysis: AnalysisConfigData.ANALYSIS_SECTION, analysis_config: AnalysisConfigWrapper, graph_file: Optional[GraphFileProtocol] = None, graph_file_hazard: Optional[GraphFileProtocol] = None, diff --git a/ra2ce/analysis/analysis_protocol.py b/ra2ce/analysis/analysis_protocol.py index 552ba3482..69be6f658 100644 --- a/ra2ce/analysis/analysis_protocol.py +++ b/ra2ce/analysis/analysis_protocol.py @@ -24,14 +24,14 @@ from geopandas import GeoDataFrame -from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisSectionBase +from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData from ra2ce.network.graph_files.graph_files_protocol import GraphFileProtocol class AnalysisProtocol(Protocol): graph_file: Optional[GraphFileProtocol] graph_file_hazard: Optional[GraphFileProtocol] - analysis: AnalysisSectionBase + analysis: AnalysisConfigData.ANALYSIS_SECTION input_path: Path output_path: Path diff --git a/ra2ce/analysis/losses/losses_base.py b/ra2ce/analysis/losses/losses_base.py index 555792f5d..56b978e3b 100644 --- a/ra2ce/analysis/losses/losses_base.py +++ b/ra2ce/analysis/losses/losses_base.py @@ -100,7 +100,7 @@ def __init__( ) self._check_validity_analysis_files() - self.intensities = TrafficIntensitiesReader(self.link_id).read( + self.intensities = TrafficIntensitiesReader([self.link_id]).read( self.analysis.traffic_intensities_file ) self.resilience_curves = ResilienceCurvesReader().read( diff --git a/ra2ce/analysis/losses/resilience_curves/resilience_curves_reader.py b/ra2ce/analysis/losses/resilience_curves/resilience_curves_reader.py index a343ee49c..56054cba3 100644 --- a/ra2ce/analysis/losses/resilience_curves/resilience_curves_reader.py +++ b/ra2ce/analysis/losses/resilience_curves/resilience_curves_reader.py @@ -15,6 +15,7 @@ along with this program. If not, see . """ from ast import literal_eval +from dataclasses import dataclass, field from pathlib import Path from re import findall @@ -27,17 +28,20 @@ from ra2ce.network.network_config_data.enums.road_type_enum import RoadTypeEnum +@dataclass class ResilienceCurvesReader(LossesInputDataReaderBase): """ Class to read the resilience curves from a csv file. """ - csv_columns = [ - "link_type_hazard_intensity", - "duration_steps", - "functionality_loss_ratio", - ] - data_type = ResilienceCurves + object_type: type = ResilienceCurves + csv_columns: list[str] = field( + default_factory=lambda: [ + "link_type_hazard_intensity", + "duration_steps", + "functionality_loss_ratio", + ] + ) def _parse_df(self, df: pd.DataFrame) -> ResilienceCurves: def parse_link_type_hazard_intensity( diff --git a/ra2ce/analysis/losses/time_values/time_values_reader.py b/ra2ce/analysis/losses/time_values/time_values_reader.py index 22641810c..357b315db 100644 --- a/ra2ce/analysis/losses/time_values/time_values_reader.py +++ b/ra2ce/analysis/losses/time_values/time_values_reader.py @@ -14,7 +14,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from pandas import DataFrame @@ -32,8 +32,10 @@ class TimeValuesReader(LossesInputDataReaderBase): Class to read the time values from a csv file. """ - csv_columns = ["trip_types", "value_of_time", "occupants"] - data_type = TimeValues + object_type: type = TimeValues + csv_columns: list[str] = field( + default_factory=lambda: ["trip_types", "value_of_time", "occupants"] + ) def _parse_df(self, df: DataFrame) -> TimeValues: _time_values = { diff --git a/ra2ce/analysis/losses/traffic_intensities/traffic_intensities_reader.py b/ra2ce/analysis/losses/traffic_intensities/traffic_intensities_reader.py index 7defbb2a3..f607a333b 100644 --- a/ra2ce/analysis/losses/traffic_intensities/traffic_intensities_reader.py +++ b/ra2ce/analysis/losses/traffic_intensities/traffic_intensities_reader.py @@ -15,6 +15,7 @@ along with this program. If not, see . """ import re +from dataclasses import dataclass, field from pathlib import Path import pandas as pd @@ -31,17 +32,15 @@ ) +@dataclass class TrafficIntensitiesReader(LossesInputDataReaderBase): """ Class to read the traffic intensities per traffic period from a csv file. """ - csv_columns = [] - separator = "," - data_type = TrafficIntensities - - def __init__(self, link_id: str) -> None: - self.csv_columns = [link_id] + csv_columns: list[str] = field(default_factory=list) + separator: str = "," + object_type: type = TrafficIntensities def _parse_df(self, df: pd.DataFrame) -> TrafficIntensities: _traffic_intensities = TrafficIntensities() diff --git a/ra2ce/configuration/ra2ce_enum_base.py b/ra2ce/configuration/ra2ce_enum_base.py index dad60a8a8..1fe7344ab 100644 --- a/ra2ce/configuration/ra2ce_enum_base.py +++ b/ra2ce/configuration/ra2ce_enum_base.py @@ -11,7 +11,7 @@ class Ra2ceEnumBase(Enum): """ @classmethod - def get_enum(cls, input: str | None) -> Ra2ceEnumBase: + def get_enum(cls, input_str: str | None) -> Ra2ceEnumBase: """ Create an enum from a given input string. @@ -24,9 +24,9 @@ def get_enum(cls, input: str | None) -> Ra2ceEnumBase: INVALID: This entry is used if the config value is invalid. """ try: - if not input: + if not input_str: return cls.NONE - return cls[input.upper().strip()] + return cls[input_str.upper().strip()] except (AttributeError, KeyError): return cls.INVALID diff --git a/tests/analysis/adaptation/conftest.py b/tests/analysis/adaptation/conftest.py index fd9ee0e4d..c64e1d3eb 100644 --- a/tests/analysis/adaptation/conftest.py +++ b/tests/analysis/adaptation/conftest.py @@ -1,3 +1,4 @@ +from pathlib import Path from shutil import copytree, rmtree from typing import Iterator @@ -24,10 +25,23 @@ ) from ra2ce.analysis.analysis_config_data.enums.trip_purpose_enum import TripPurposeEnum from ra2ce.analysis.analysis_config_data.enums.weighing_enum import WeighingEnum +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper +from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper +from ra2ce.network.network_config_data.enums.aggregate_wl_enum import AggregateWlEnum +from ra2ce.network.network_config_data.network_config_data import ( + HazardSection, + NetworkConfigData, + NetworkSection, +) +from ra2ce.network.network_config_wrapper import NetworkConfigWrapper from tests import test_data, test_results class AdaptationOptionCases: + """ + Test cases for the adaptation options. + """ + config_cases: list[AnalysisSectionAdaptationOption] = [ AnalysisSectionAdaptationOption( id="AO0", @@ -50,21 +64,38 @@ class AdaptationOptionCases: maintenance_interval=3.0, ), ] - cost: list[float] = [0.0, 2693.684211, 5231.908660] - cases = list(zip(config_cases, cost)) + unit_cost: list[float] = [0.0, 2693.684211, 5231.908660] + total_cost: list[float] = [0.0, 633015.789583, 1229498.535112] + cases: list[tuple[AnalysisSectionAdaptationOption, float, float]] = list( + zip(config_cases, unit_cost, total_cost) + ) @pytest.fixture(name="valid_adaptation_config") def _get_valid_adaptation_config_fixture( request: pytest.FixtureRequest, -) -> Iterator[AnalysisConfigData]: + valid_analysis_ini: Path, +) -> Iterator[tuple[AnalysisInputWrapper, AnalysisConfigWrapper]]: + """ + Create valid input and config for the adaptation analysis. + + Args: + request (pytest.FixtureRequest): Pytest fixture request. + valid_analysis_ini (Path): Path to a valid analysis ini file. + + Yields: + Iterator[tuple[AnalysisInputWrapper, AnalysisConfigWrapper]]: + Tuple with the input and config for the adaptation analysis. + """ + def get_losses_section(analysis: AnalysisLossesEnum) -> AnalysisSectionLosses: return AnalysisSectionLosses( analysis=analysis, event_type=EventTypeEnum.EVENT, weighing=WeighingEnum.TIME, - threshold=0.5, + threshold=0, production_loss_per_capita_per_hour=42, + hours_per_traffic_period=8, traffic_period=TrafficPeriodEnum.DAY, trip_purposes=[ TripPurposeEnum.BUSINESS, @@ -72,15 +103,9 @@ def get_losses_section(analysis: AnalysisLossesEnum) -> AnalysisSectionLosses: TripPurposeEnum.FREIGHT, TripPurposeEnum.OTHER, ], - resilience_curves_file=_root_path.joinpath( - "damage_functions", "resilience_curves.csv" - ), - traffic_intensities_file=_root_path.joinpath( - "damage_functions", "traffic_intensities.csv" - ), - values_of_time_file=_root_path.joinpath( - "damage_functions", "values_of_time.csv" - ), + resilience_curves_file=_input_path.joinpath("resilience_curves.csv"), + traffic_intensities_file=_input_path.joinpath("traffic_intensities.csv"), + values_of_time_file=_input_path.joinpath("values_of_time.csv"), save_gpkg=True, save_csv=True, ) @@ -98,9 +123,27 @@ def get_losses_section(analysis: AnalysisLossesEnum) -> AnalysisSectionLosses: for _option in AdaptationOptionCases.config_cases: _ao_path = _input_path.joinpath(_option.id) copytree(test_data.joinpath("adaptation", "input"), _ao_path) + copytree( + test_data.joinpath("adaptation", "static"), + _ao_path.joinpath("multi_link_losses", "static"), + ) copytree(test_data.joinpath("adaptation", "static"), _static_path) # Create the config + + # - network + _hazard_section = HazardSection(aggregate_wl=AggregateWlEnum.MEAN) + _network_section = NetworkSection( + file_id="ID", + link_type_column="highway", + ) + _network_config_data = NetworkConfigData( + static_path=test_results.joinpath(request.node.name, "static"), + hazard=_hazard_section, + network=_network_section, + ) + _network_config = NetworkConfigWrapper.from_data(None, _network_config_data) + # - damages _damages_section = AnalysisSectionDamages( analysis=AnalysisDamagesEnum.DAMAGES, @@ -125,7 +168,7 @@ def get_losses_section(analysis: AnalysisLossesEnum) -> AnalysisSectionLosses: time_horizon=20, ) - yield AnalysisConfigData( + _analysis_data = AnalysisConfigData( root_path=_root_path, input_path=_input_path, static_path=_static_path, @@ -136,4 +179,18 @@ def get_losses_section(analysis: AnalysisLossesEnum) -> AnalysisSectionLosses: _multi_link_losses_section, _adaptation_section, ], + aggregate_wl=AggregateWlEnum.MEAN, + ) + + _analysis_config = AnalysisConfigWrapper.from_data_with_network( + valid_analysis_ini, _analysis_data, _network_config + ) + + _analysis_input = AnalysisInputWrapper.from_input( + analysis=_analysis_config.config_data.adaptation, + analysis_config=_analysis_config, + graph_file=_analysis_config.graph_files.base_network, + graph_file_hazard=_analysis_config.graph_files.base_network_hazard, ) + + yield (_analysis_input, _analysis_config) diff --git a/tests/analysis/adaptation/test_adaptation.py b/tests/analysis/adaptation/test_adaptation.py index a5c6df601..a32ffe66f 100644 --- a/tests/analysis/adaptation/test_adaptation.py +++ b/tests/analysis/adaptation/test_adaptation.py @@ -1,72 +1,59 @@ -from pathlib import Path -from typing import Iterator - import pytest from geopandas import GeoDataFrame from ra2ce.analysis.adaptation.adaptation import Adaptation -from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper -from ra2ce.network.network_config_data.network_config_data import NetworkConfigData -from ra2ce.network.network_config_wrapper import NetworkConfigWrapper -from tests import test_results +from tests.analysis.adaptation.conftest import AdaptationOptionCases class TestAdaptation: - @pytest.fixture(name="valid_adaptation_input") - def _get_valid_adaptation_input_fixture( - self, - request: pytest.FixtureRequest, - valid_analysis_ini: Path, - valid_adaptation_config: AnalysisConfigData, - ) -> Iterator[AnalysisInputWrapper]: - _network_config_data = NetworkConfigData( - static_path=test_results.joinpath(request.node.name, "static") - ) - _network_config = NetworkConfigWrapper.from_data( - valid_analysis_ini, _network_config_data - ) - _config = AnalysisConfigWrapper.from_data_with_network( - valid_analysis_ini, valid_adaptation_config, _network_config - ) - - _analysis_input = AnalysisInputWrapper.from_input( - analysis=valid_adaptation_config.adaptation, - analysis_config=_config, - graph_file=_config.graph_files.base_network, - graph_file_hazard=_config.graph_files.base_network_hazard, - ) - - yield _analysis_input - def test_initialize( self, - valid_adaptation_input: AnalysisInputWrapper, - valid_adaptation_config: AnalysisConfigData, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], ): # 1./2. Define test data./Run test. - _adaptation = Adaptation(valid_adaptation_input, valid_adaptation_config) + _adaptation = Adaptation(valid_adaptation_config[0], valid_adaptation_config[1]) # 3. Verify expectations. assert isinstance(_adaptation, Adaptation) - def test_run_cost( + def test_run_cost_returns_gdf( self, - valid_adaptation_input: AnalysisInputWrapper, - valid_adaptation_config: AnalysisConfigData, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], ): # 1. Define test data. - _adaptation = Adaptation(valid_adaptation_input, valid_adaptation_config) + _adaptation = Adaptation(valid_adaptation_config[0], valid_adaptation_config[1]) # 2. Run test. _cost_gdf = _adaptation.run_cost() # 3. Verify expectations. assert isinstance(_cost_gdf, GeoDataFrame) + assert all( + f"{_option.id}_cost" in _cost_gdf.columns + for _option in _adaptation.adaptation_collection.adaptation_options + ) + for _option, _, _total_cost in AdaptationOptionCases.cases[1:]: + assert _cost_gdf[f"{_option.id}_cost"].sum(axis=0) == pytest.approx( + _total_cost + ) + + def test_run_benefit_returns_gdf( + self, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + ): + # 1. Define test data. + _adaptation = Adaptation(valid_adaptation_config[0], valid_adaptation_config[1]) + + # 2. Run test. + _result = _adaptation.run_benefit() + + # 3. Verify expectations. + assert isinstance(_result, GeoDataFrame) assert all( [ - f"costs_{_option.id}" in _cost_gdf.columns - for _option in _adaptation.adaptation_collection.adaptation_options + f"{_option.id}_impact" in _result.columns + for _option in _adaptation.adaptation_collection.all_options ] ) diff --git a/tests/analysis/adaptation/test_adaptation_option.py b/tests/analysis/adaptation/test_adaptation_option.py index 0ee56be56..127e2c535 100644 --- a/tests/analysis/adaptation/test_adaptation_option.py +++ b/tests/analysis/adaptation/test_adaptation_option.py @@ -2,65 +2,61 @@ from ra2ce.analysis.adaptation.adaptation_option import AdaptationOption from ra2ce.analysis.analysis_config_data.analysis_config_data import ( - AnalysisConfigData, 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, ) +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper +from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper +from ra2ce.analysis.damages.damages import Damages +from ra2ce.analysis.losses.multi_link_losses import MultiLinkLosses +from ra2ce.analysis.losses.single_link_losses import SingleLinkLosses from tests.analysis.adaptation.conftest import AdaptationOptionCases class TestAdaptationOption: @pytest.mark.parametrize( - "losses_analysis", - [AnalysisLossesEnum.SINGLE_LINK_LOSSES, AnalysisLossesEnum.MULTI_LINK_LOSSES], + "losses_analysis_type, losses_analysis", + [ + (AnalysisLossesEnum.SINGLE_LINK_LOSSES, SingleLinkLosses), + (AnalysisLossesEnum.MULTI_LINK_LOSSES, MultiLinkLosses), + ], ) - def test_from_config( + def test_from_config_returns_object_with_2_analyses( self, - valid_adaptation_config: AnalysisConfigData, - losses_analysis: AnalysisLossesEnum, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + losses_analysis_type: AnalysisLossesEnum, + losses_analysis: type[SingleLinkLosses | MultiLinkLosses], ): # 1. Define test data. - _orig_path = valid_adaptation_config.losses_list[0].resilience_curves_file - _expected_path = _orig_path.parent.joinpath( - "input", - valid_adaptation_config.adaptation.adaptation_options[0].id, - "losses", - _orig_path.name, - ) + _config_data = valid_adaptation_config[1].config_data + assert _config_data.adaptation + _config_data.adaptation.losses_analysis = losses_analysis_type + _config_option = _config_data.adaptation.adaptation_options[0] # 2. Run test. _option = AdaptationOption.from_config( - root_path=valid_adaptation_config.root_path, - adaptation_option=valid_adaptation_config.adaptation.adaptation_options[0], - damages_section=valid_adaptation_config.get_analysis( - AnalysisDamagesEnum.DAMAGES - ), - losses_section=valid_adaptation_config.get_analysis(losses_analysis), + analysis_config=valid_adaptation_config[1], + adaptation_option=_config_option, ) # 3. Verify expectations. assert isinstance(_option, AdaptationOption) - assert _option.id == "AO0" - assert _option.damages_config.analysis == AnalysisDamagesEnum.DAMAGES - assert _option.losses_config.analysis == losses_analysis - assert _option.losses_config.resilience_curves_file == _expected_path + assert _option.id == _config_option.id + assert len(_option.analyses) == 2 + assert Damages in [x.analysis_class for x in _option.analyses] + assert losses_analysis in [x.analysis_class for x in _option.analyses] def test_from_config_no_damages_losses_raises(self): # 1. Define test data. - _config = AnalysisConfigData() + _config = AnalysisConfigWrapper() # 2. Run test. with pytest.raises(ValueError) as _exc: AdaptationOption.from_config( - root_path=_config.root_path, + analysis_config=_config, adaptation_option=AnalysisSectionAdaptation(), - damages_section=None, - losses_section=None, ) # 3. Verify expectations. @@ -72,23 +68,22 @@ def test_from_config_no_damages_losses_raises(self): "adaptation_option", AdaptationOptionCases.cases, ) - def test_calculate_option_cost( + def test_calculate_option_cost_returns_float( self, - valid_adaptation_config: AnalysisConfigData, - adaptation_option: tuple[AnalysisSectionAdaptation, float], + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + adaptation_option: tuple[AnalysisSectionAdaptation, float, float], ): # 1. Define test data. _option = AdaptationOption.from_config( - root_path=valid_adaptation_config.root_path, + analysis_config=valid_adaptation_config[1], adaptation_option=adaptation_option[0], - damages_section=valid_adaptation_config.damages_list[0], - losses_section=valid_adaptation_config.losses_list[0], ) - _time_horizon = valid_adaptation_config.adaptation.time_horizon - _discount_rate = valid_adaptation_config.adaptation.discount_rate + _time_horizon = valid_adaptation_config[1].config_data.adaptation.time_horizon + _discount_rate = valid_adaptation_config[1].config_data.adaptation.discount_rate # 2. Run test. _cost = _option.calculate_cost(_time_horizon, _discount_rate) # 3. Verify expectations. + assert isinstance(_cost, float) assert _cost == pytest.approx(adaptation_option[1]) diff --git a/tests/analysis/adaptation/test_adaptation_option_analysis.py b/tests/analysis/adaptation/test_adaptation_option_analysis.py new file mode 100644 index 000000000..eb44677d9 --- /dev/null +++ b/tests/analysis/adaptation/test_adaptation_option_analysis.py @@ -0,0 +1,97 @@ +import pytest + +from ra2ce.analysis.adaptation.adaptation_option_analysis import ( + AdaptationOptionAnalysis, +) +from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( + AnalysisDamagesEnum, +) +from ra2ce.analysis.analysis_config_data.enums.analysis_losses_enum import ( + AnalysisLossesEnum, +) +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper +from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper +from ra2ce.analysis.damages.damages import Damages +from ra2ce.analysis.losses.losses_base import LossesBase +from ra2ce.analysis.losses.multi_link_losses import MultiLinkLosses +from ra2ce.analysis.losses.single_link_losses import SingleLinkLosses + + +class TestAnalysisOptionAnalysis: + @pytest.mark.parametrize( + "analysis_type, expected_analysis", + [ + pytest.param(AnalysisDamagesEnum.DAMAGES, Damages, id="damages"), + pytest.param( + AnalysisLossesEnum.SINGLE_LINK_LOSSES, + SingleLinkLosses, + id="single_link_losses", + ), + pytest.param( + AnalysisLossesEnum.MULTI_LINK_LOSSES, + MultiLinkLosses, + id="multi_link_losses", + ), + ], + ) + def test_get_analysis_returns_tuple( + self, + analysis_type: AnalysisDamagesEnum | AnalysisLossesEnum, + expected_analysis: type[Damages | LossesBase], + ): + # 1./2. Define test data./Run test. + _result = AdaptationOptionAnalysis.get_analysis(analysis_type) + + # 3. Verify expectations. + assert isinstance(_result, tuple) + assert _result[0] == expected_analysis + assert isinstance(_result[1], str) + + def test_get_analysis_raises_not_implemented_error(self): + # 1. Define test data. + _analysis_type = "not implemented" + + # 2. Run test. + with pytest.raises(NotImplementedError) as exc: + AdaptationOptionAnalysis.get_analysis(_analysis_type) + + # 3. Verify expectations. + assert exc.match(f"Analysis {_analysis_type} not implemented") + + @pytest.mark.parametrize( + "analysis_type, expected_analysis", + [ + pytest.param(AnalysisDamagesEnum.DAMAGES, Damages, id="damages"), + pytest.param( + AnalysisLossesEnum.SINGLE_LINK_LOSSES, + SingleLinkLosses, + id="single_link_losses", + ), + pytest.param( + AnalysisLossesEnum.MULTI_LINK_LOSSES, + MultiLinkLosses, + id="multi_link_losses", + ), + ], + ) + def test_from_config_returns_object( + self, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + analysis_type: AnalysisLossesEnum, + expected_analysis: type[Damages | LossesBase], + ): + # 1. Define test data. + _analysis_config = valid_adaptation_config[1] + assert _analysis_config.config_data.adaptation + + _analysis_config.config_data.adaptation.losses_analysis = analysis_type + _id = _analysis_config.config_data.adaptation.adaptation_options[0].id + + # 2. Run test. + _result = AdaptationOptionAnalysis.from_config( + analysis_config=_analysis_config, analysis_type=analysis_type, option_id=_id + ) + + # 3. Verify expectations. + assert isinstance(_result, AdaptationOptionAnalysis) + assert _result.analysis_class == expected_analysis diff --git a/tests/analysis/adaptation/test_adaptation_option_collection.py b/tests/analysis/adaptation/test_adaptation_option_collection.py index d037f0dc2..0dd9a58bf 100644 --- a/tests/analysis/adaptation/test_adaptation_option_collection.py +++ b/tests/analysis/adaptation/test_adaptation_option_collection.py @@ -4,7 +4,8 @@ from ra2ce.analysis.adaptation.adaptation_option_collection import ( AdaptationOptionCollection, ) -from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData +from ra2ce.analysis.analysis_config_wrapper import AnalysisConfigWrapper +from ra2ce.analysis.analysis_input_wrapper import AnalysisInputWrapper class TestAdaptationOptionCollection: @@ -15,9 +16,12 @@ def test_initialize(self): # 3. Verify expectations. assert isinstance(_collection, AdaptationOptionCollection) - def test_from_config(self, valid_adaptation_config: AnalysisConfigData): + def test_from_config( + self, + valid_adaptation_config: tuple[AnalysisInputWrapper, AnalysisConfigWrapper], + ): # 1./2. Define test data./Run test. - _collection = AdaptationOptionCollection.from_config(valid_adaptation_config) + _collection = AdaptationOptionCollection.from_config(valid_adaptation_config[1]) # 3. Verify expectations. assert isinstance(_collection, AdaptationOptionCollection) @@ -34,7 +38,7 @@ def test_from_config(self, valid_adaptation_config: AnalysisConfigData): def test_from_config_no_adaptation_raises(self): # 1. Define test data. - _config = AnalysisConfigData() + _config = AnalysisConfigWrapper() # 2. Run test. with pytest.raises(ValueError) as _exc: diff --git a/tests/analysis/analysis_config_data/test_analysis_config_data.py b/tests/analysis/analysis_config_data/test_analysis_config_data.py index cd5490199..aeff27a74 100644 --- a/tests/analysis/analysis_config_data/test_analysis_config_data.py +++ b/tests/analysis/analysis_config_data/test_analysis_config_data.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from ra2ce.analysis.analysis_config_data.analysis_config_data import ( @@ -13,6 +15,7 @@ from ra2ce.analysis.analysis_config_data.enums.analysis_damages_enum import ( AnalysisDamagesEnum, ) +from ra2ce.analysis.analysis_config_data.enums.analysis_enum import AnalysisEnum from ra2ce.analysis.analysis_config_data.enums.analysis_losses_enum import ( AnalysisLossesEnum, ) @@ -47,9 +50,7 @@ def valid_config(self) -> AnalysisConfigData: yield _config def test_losses(self, valid_config: AnalysisConfigData): - # 1. Define test data - - # 2. Run test + # 1./2. Define test data/Run test _losses = [ _config.analysis.config_value for _config in valid_config.losses_list ] @@ -58,9 +59,7 @@ def test_losses(self, valid_config: AnalysisConfigData): assert all(item in _losses for item in LossesAnalysisNameList) def test_damages(self, valid_config: AnalysisConfigData): - # 1. Define test data - - # 2. Run test + # 1./2. Define test data/Run test _damages = [ _config.analysis.config_value for _config in valid_config.damages_list ] @@ -89,3 +88,39 @@ def test_get_data_output(self): # 3. Verify expectations. assert _return_value == _expected_value + + @pytest.mark.parametrize( + "analysis_type", + ( + *AnalysisLossesEnum.list_valid_options(), + *AnalysisDamagesEnum.list_valid_options(), + AnalysisEnum.ADAPTATION, + ), + ) + def test_get_analysis_returns_analysis_config( + self, + valid_config: AnalysisConfigData, + analysis_type: AnalysisLossesEnum | AnalysisDamagesEnum | AnalysisEnum, + ): + # 1./2. Define test data/Run test + _result = valid_config.get_analysis(analysis_type) + + # 3. Verify expectations + assert _result.analysis == analysis_type + + def test_reroot_analysis_config(self, valid_config: AnalysisConfigData): + # 1. Define test data + _analysis_type = AnalysisLossesEnum.SINGLE_LINK_LOSSES + _analysis = valid_config.get_analysis(_analysis_type) + _file = Path("old_root/a_dir/file.ext") + valid_config.root_path = _file.parent + _analysis.resilience_curves_file = _file + _root_path = Path("new_root/another_dir") + _expected_path = _root_path.joinpath(_analysis_type.config_value, _file.name) + + # 2. Run test + _result = valid_config.reroot_analysis_config(_analysis_type, _root_path) + + # 3. Verify expectations + _result_analysis = _result.get_analysis(_analysis_type) + assert _result_analysis.resilience_curves_file == _expected_path diff --git a/tests/analysis/losses/traffic_intensities/test_traffic_intensities_reader.py b/tests/analysis/losses/traffic_intensities/test_traffic_intensities_reader.py index e87e4b1b3..71dbef045 100644 --- a/tests/analysis/losses/traffic_intensities/test_traffic_intensities_reader.py +++ b/tests/analysis/losses/traffic_intensities/test_traffic_intensities_reader.py @@ -19,7 +19,7 @@ class TestTimeValuesReader: def test_initialize(self): # 1. Run test - _reader = TrafficIntensitiesReader("link_id") + _reader = TrafficIntensitiesReader(["link_id"]) # 2. Verify expections assert isinstance(_reader, TrafficIntensitiesReader) @@ -37,7 +37,7 @@ def test_read_traffic_intensities( assert traffic_intensities_csv.is_file() # 2. Execute test - _traffic_intensities = TrafficIntensitiesReader("link_id").read( + _traffic_intensities = TrafficIntensitiesReader(["link_id"]).read( traffic_intensities_csv ) diff --git a/tests/analysis/test_analysis_result_wrapper_exporter.py b/tests/analysis/test_analysis_result_wrapper_exporter.py index ef7d806b3..3c3305cc2 100644 --- a/tests/analysis/test_analysis_result_wrapper_exporter.py +++ b/tests/analysis/test_analysis_result_wrapper_exporter.py @@ -4,7 +4,7 @@ import pytest from shapely import Point -from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisSectionBase +from ra2ce.analysis.analysis_config_data.analysis_config_data import AnalysisConfigData from ra2ce.analysis.analysis_config_data.enums.analysis_losses_enum import ( AnalysisLossesEnum, ) @@ -36,7 +36,7 @@ def valid_result_wrapper( ) -> AnalysisResultWrapper: class MockedAnalysis(AnalysisProtocol): def __init__(self) -> None: - _analysis = AnalysisSectionBase( + _analysis = AnalysisConfigData.ANALYSIS_SECTION( name="Mocked Analysis", save_csv=False, save_gpkg=False ) _analysis.analysis = AnalysisLossesEnum.SINGLE_LINK_LOSSES diff --git a/tests/test_data/adaptation/input/losses/input/resilience_curve.csv b/tests/test_data/adaptation/input/multi_link_losses/input/resilience_curves.csv similarity index 100% rename from tests/test_data/adaptation/input/losses/input/resilience_curve.csv rename to tests/test_data/adaptation/input/multi_link_losses/input/resilience_curves.csv diff --git a/tests/test_data/adaptation/input/losses/input/traffic_intensities.csv b/tests/test_data/adaptation/input/multi_link_losses/input/traffic_intensities.csv similarity index 100% rename from tests/test_data/adaptation/input/losses/input/traffic_intensities.csv rename to tests/test_data/adaptation/input/multi_link_losses/input/traffic_intensities.csv diff --git a/tests/test_data/adaptation/input/losses/input/values_of_time.csv b/tests/test_data/adaptation/input/multi_link_losses/input/values_of_time.csv similarity index 100% rename from tests/test_data/adaptation/input/losses/input/values_of_time.csv rename to tests/test_data/adaptation/input/multi_link_losses/input/values_of_time.csv diff --git a/tests/test_data/adaptation/input/multi_link_losses/output/hazard_names.xlsx b/tests/test_data/adaptation/input/multi_link_losses/output/hazard_names.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..51b096bcf75cd1827e879194c7ebd9690d23e798 GIT binary patch literal 5141 zcmZ`-2T)Vp)(uU%biq(W>C$@#1?fdl1f&Z@KmtTSItUngF%$u*Ql$r^cLJf8(5nk4IEr*002M$kPYX4tZalJ3BGED7GxgFUhMH!6&Hh9E4l@p`aDFu zo|Tywh^(jSHAJ$wiv}a#`%(0$0IG?V+Z9I(uB)&C?F^aj40=XTP1_(@%jmmsENC=^ z2UAWNx2mMp7#c;FxHgJvf|62Q6Q2ok9w*u#LKMVtK3?fv7U2lmbr$a1!|Ekge<879L)OtYtrp&v;ID zcf-9(CBNtkkRetmoI0}YR=={_1E4H-y9$^iVA!8mYKs$#U(&>|ZG9wz^r29k;s@dJ zZ@$y~9DGj5Pe*&7JdK*mOmB_bSx1Q&t%rvDfIB1Fp4+ndaQBXG!LgH-(wW!kAnk)a zefwxfZ&Y%9Aw2wV<11v#Fxt1Ien-d zIFnAr+QQe2$M(*Up01ITIdkRe4dG*&KD{VVHWrn^UG}=r%KZ~?$LY+x90;``#V8cu zC_rz>{BqFg{ff3x=qJ54Wa~2Ncder5%D>a_$mVX4?%}PoZZ@WAX~49^N1Q=DV<& zVw*}7Wqg0s-hFN~I;^u~$b+90>qKSnmRu4 zyVc=~ZLuRJun81bl@5>~xjW!UCGcj)NBG%MVna`kuveO|)S!L*OS>Yh_uCCAsJK&? z^_7hGag$mKgOElZ!o43X#!o^x9Hpt8r5J1!d{Ag~OfH)cYUhM#Q(MO7hJHesDGIfc z9_h@|Fo!nuMc-F(WSJ~zt++KAugt2H(al@GLLpJ5Mk+)lPmmUz^h}2VJBLs(Av5^6 zva^_Ck;=EGnV@?iD)d{CQ?jf3xRmPh*<~2~13y=J82za6V|Jn;4OSxE zf=Hn#PsfDEo7TOOR2fDp%7J*|D|dqvS=lKIB>l8hsm{4`XUcZT@25Nw-p@+wo1O_^F`(E)E&xZj?{>=_}U;wBaG*j z9gUGcN(E;oMffbL#LE_<*=?e6Y8xN1v-%PH;>o{v=j8`_`nXYm?s1q6UwaNivVZ|C} zjW5;l%~k9Y>WAh15N)PD5@{QmSjKAicZ{Mx(57xHEV%*?6BTQL3e7kU^=TiaB+`>& z>WD@kz?|AoHaa|jOOn&Lb-qak4r^1x!#~y#x@FlrG6{@bq5D}kzh(v}q1qKgJiX16 zY~FP0h)_F#i&=S;Yb4^0(I{m^sE-yP$mRW(yRJ1DSR zm9SafFkHDtE3~5XH5PV-3Tq&~=iV-MpmiRDf6{m2rhSq+E}Fp-ImwqwJg9X3fTAD`{nw53=7bGF61Qq6{TH@$@%5Ajy0xu@It^3=|%ZD3**Oa=JTyXH|Z7jMNrQD#8$hXD0ZzS}c1- zn@Hy4`3`AW8#4OSc0>!--y4YBq|LcFHUMyf2LRCh-ay=)ob6p~K_FKb{$F3ec9C&C zt@|GZ$j`X-zrx7W<@Kjk%ZP7>6o&F1QgS6nafAg~y+=cX&b`3=Jk$>PW5)OQrVi%& z8+$aHhd)oag(AOt7i`IV2@QVCFk9cww3W%EQurdw-i>2j}Lj;xcn(g1r(&; zZz23zdt`c_lR2;}sx`{@#na}bs)?HU+QkFTJ;_XDvJ74=ijhx89kn8Ka&!;agnQII zxn}eIK&M5+iS!z+(K&T$z2~8%B?CPjo9V`YfluMaF4Id$DI{ZpTEC9@xjp-kcK zUllwH^sPS@LH6<`Xer+YG94tD2%QU+X1sg9qj-O2!h$$+o~dBehpcPKH@YYe{9KIY z1&gZdo-+r(KoC{X)l%xv1Qv-Oy;qJdyGAN_(I91IAd}`IvKC zj$PD)D=z-tk9x%M0@69Tz*wT zr|O3%grK*`PMWt2e$EjcVE6tBW?87NiT$m)5UVwPVc^czV35ntIb}|NHaZ2PFd8I&yq1n`zCw=Ji29qEXSs7B&B+#l zZ07lbMb<6N!qrrkIO*0QzPE3X;X_#`syzPU%U-*V<($Cvrb*NZZ40`)j<9~Czj$g7Bd0{eWd9l{+oktsFGWq+rhP|jq2pJ91Y=NcbHGnN?{r3BaH#k${y9rSv zvT2ChX)8O86xp{?4NPU%o=>vAY}YlR9<{q-qac3Tn&MzGVyi0GdeIvV7t;(7HHv4Jw5XZm7C|Fa>v#?tIlK2A zYn(#L+3XFek8hI1QrrqT44OZ$-WSFZ5EgrxMEaqx!b&kuMmWp5SNM%eX#P#--dH-o z2(5E>9*ipJE_pL#j6gr#Dx-&>$4U<84IAMn80DxjwO%GsW0+q|2q&s97L!n0w+m$( zjx90CR>(ti)Kn|8ErrHKdO9zhOyr##gUf6# zxKjXWGOaIK0{CFhWs&|o%kZ|S#vR^F5!let6gki51GaA7cE^btO_x)(L!y|?dY4L& zI}!B})^TZjGK;#R(e!hFVU3G{k0_$C{tfpP~*~E3gYOR zoakvXOARHH&Qe4_i8^rMd&E65aPZN9*Kix2k3;*Ew0xY>Xnt)>#1Ii)I3F12V?vv@k71r4@wdz3qey)RSs(v zU+q6GJQRB;6mgX|J(KTIrqII3MhqQlVS-(CVi|!W#(Lj2DBX!tv}<_zQ*C3;M_KG-nWdYE z*SJGUQt4K7yY=VF(WyY7tByQA?DZ>HArVIuTTI@hc--t_v-oE|TmG+Mu4h)@#xMKC zf2&A%bGg0*L+TF^#0=8rsmn6N-R) zntcDDo#Wp}Nx^@`?sdgR+)J-q`4H2O+Xi)_D3#ihn~R5=OP3az6088F$1HFX z=l4<7SkcMR73An@uIC8>xtRV`QAr%?r;0kwbHBWJCM*HDv&k39z12z#MVe-R&aiZH zb$UJfLH%7h=^KsgPV_gIQ?{ZSE8Ko58(Do%Md}+tcgFqW41)>MSzf4WQ(okyg+8cq z#$WQQ^C7?OfqftE2zcUE6=3w07ldMT$|h{?r84ZVcrRFP^QbOz7|0g3ytH^kroN}d zS?Ao1=)7kFQAiJ~2)%j8{YmfG%RfFc7|c!8q}pYdRe`->i`CZ$w5ca~;Xu9q4jrwb z9!Z$fdBs1KS7o?^Mr056zfc@X`I@@KfX487d9|3K9-gUP?T_+{YbKKKVHeD(h_|EDWn z24Al8KVTT9r($^fA60)j&gD}26XylS&igNgrK5p|F^K>GLd+$H>EMW;E*juJec}Ag literal 0 HcmV?d00001 diff --git a/tests/test_data/adaptation/output/hazard_names.xlsx b/tests/test_data/adaptation/output/hazard_names.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b656b80c03d1fd25156dcb3b4434f3bc53cbc307 GIT binary patch literal 5053 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 KxZO{+3-Et`OT@_l literal 0 HcmV?d00001 diff --git a/tests/test_data/adaptation/static/output_graph/avg_speed.csv b/tests/test_data/adaptation/static/output_graph/avg_speed.csv index 6519c94bc..bacd4f399 100644 --- a/tests/test_data/adaptation/static/output_graph/avg_speed.csv +++ b/tests/test_data/adaptation/static/output_graph/avg_speed.csv @@ -1,7 +1,7 @@ ,road_types,avg_speed -0,tertiary,58.4 -1,secondary_link,50.0 -2,secondary,50.0 -3,trunk_link,50.0 -4,tertiary_link,58.4 -5,trunk,50.0 +0,tertiary_link,58.4 +1,tertiary,58.4 +2,trunk,50.0 +3,secondary_link,50.0 +4,trunk_link,50.0 +5,secondary,50.0 diff --git a/tests/test_data/adaptation/static/output_graph/base_graph.p b/tests/test_data/adaptation/static/output_graph/base_graph.p index a7c917cd0e19fe2b52a471b80b2c88118cab7ebf..cd2f3327efad7cf0c2a8ae2bc057abe7fadbb239 100644 GIT binary patch delta 48 zcmaEVoaOy-mJMoN>}K&6Q+k-qH|u&$iV-o5H#MKq!)j_CZ*B;pfqb*gmzT-Z0sy@s B5oG`X delta 48 zcmaEVoaOy-mJMoN?B?+nQ+k-qHtTv#iV-o7H#eNp!)j_8Z)ygm%{O0OCQ}Oly$=y( diff --git a/tests/test_data/adaptation/static/output_graph/base_graph_edges.gpkg b/tests/test_data/adaptation/static/output_graph/base_graph_edges.gpkg index 9183ea5c33ef1e8b12dcc00539fceb7c0b0022dc..8450198872a75ec12e699c6260b68da8c3d6e2d0 100644 GIT binary patch delta 79 zcmZp8z}fJCbHf>Z0V7=l;}AmwD^oKoBLh8C)8;$++wbTz&f()Qi?>kMQBXJE-eAYL d&O*#I-qak#H#LtpHv}_)B4*oN${F|k007lk7qb8W delta 79 zcmZp8z}fJCbHf>Z0YhCQ%Mb$#D`P_|6C*uy%jP@!+wbTz&f()QkGD|QQBXJA-eAYL b&O*#Q-rNwxH#LnnHG?tCx4V=x?)d=#*-sb5 diff --git a/tests/test_data/adaptation/static/output_graph/base_graph_hazard.p b/tests/test_data/adaptation/static/output_graph/base_graph_hazard.p index 6780e708fe87313263c37f36c7b9ac5816b08cb2..13d5ca7b4f160650cdf61f46221bbe47e2d05fec 100644 GIT binary patch delta 3445 zcmZ8kdst0b6#sU033XB=${>;(GG*LbBwg=nd^Big5P8&i42n@EBR3+~c-!fiC?sia z-doO0j4>Ydp{Xe&B~&AZD0!DxbN0P=-?Ps2Ppz|Fzx7*due~eKDGGFo4SsDd79n2kmPZp%#g-xJHA@Vf1uq2rlWlH3u(nfYC7?Y z8gm>MJZa($$&mz2@&%?OW3m#8k3%h(D8@Zt`xTI|=hy#8fw0$<29RXf)Ec1EOk>6x(Wo8m^(C+K zg@uKGlKMy$)Q_0kiHUSH;^x^ikwVtb2e8Qb1yi7M@It1frsEeeiN=Xo%;tzJiJA_* z8lrDVXmRb5I;k4cuyiyu)yHyp*0P(DIhhjU4Gf4r){%*{-_^^XNx^XQIS#jc553>S z!QzPWhZrL@*ZgbrdVLPbUfG(36DuOc@tG#->@Eel?Fb^Pw6ZxUj^~KwYA5DG5%{gC zF(M$Yi9LXV%wGF~iLQwrI5FvsR2AO6{-Gq1OdTYFwE5AJdD?IM0O}jC2@>hH8H@$# zvt=lAqOjI(tpbjT+ulm)aQb$jBWXKqfFrqzKVu^8@h3YWosKCA3&Us?-QHv1ulKHL z%$jE1{}Y>A%4T-zmrO}Rb=y-0KscrW^rDW#?&YX)#OeFrTGP>P z^uq~wUq(Fzqp@#1OXnQ#I#6c-_bTaO<)YJ-cys1QgWh&*W`y`dQV#YKW01ToWN5O+ zVMa`O+J*{}f9SCBgSG#F+#}ykBk@0PF@CUHHEyw$pFio8V`Kbavq5tsG&B@@9&Ry& zM(cZ0nfM%WMh0Nljz%W%$Q?P6=%f8iX%rQ8;;iAUV^5@(kdw!k!um#?v^82=%}IKm zbT$Kr5w>*cu3UPmzN0E!<&(Tgq4Ej$@ut5;S&hHTg^TlCZWk6*YzJvN?aV~lkAc4^ zfQUn0b2#$X3cz`I9y`MHM!~u>Y)EPQww(n7OtOE2$5K3b_d%SN-$Otm67>hnOOh{` zMVck=VvR%#K643-I5}|{fk3!4|2oK$U$7D?(;_$=eFb2|x~uRC+{L#%HewR&M{HbF1lp;_Q7 z$XUp1zzd%D=AD!dCpO&%lD7FK0KNPzgGrNZ{J>$|J9cAJ;OpN5;G(|VK!;E}6fgJ) zZ+&euhf`WiFCu!%a^H0I3kW0I-p`u;GvHHH(_0*0Zw&7FO9}DvT+S!i2E?2yR(nji)?p)&N=9%uiWZ`d=8| delta 3446 zcmZ8kdtA+D81F4n(l1HUWprFp!eq`VLPu_i(Ok9}mdL`#6ce*)nV2@__QualNkU6Y zdbQsc!x%o;r<6%@=`=BFC5tGR_Iu7b@A*By_0Q=!-{<;$p7(j*b0OLGLb7e%Zmn%p z$~Y@4TB7r0h1yQUB(f5nllfm75bniD&_FJh==^BpkS4PkfGdXHwP`1N33>Wp0-G}; z(MG}YL+unhPv$$SBNA!lSb=^t`njUc636I~0;?RWcSe#x^lL(&t`#68((hv zh6RN8=Bgz+ce;E+QMVLtopMbvWaFm>Ayt+!%@>Qc)2+EE``z#O9+GJP_y1BH(SbASQIHWIR3S`1 zlWRi#p|eU6rk(v7*-icMDZ&;;-a%N*oWBtk8+BKO33SceA_Zr$^8}PEON|~w9H7_N zY8tKNKUOzii}BEyE?k7sShc{Ji;!~mLX;Ley=Vpohb-nGs80HrOW-GZ37;djG(}dqP1RL=$5><0hoFFipFWtBaWDvZz%9H`q zBzYr%&0W{XMNN_)P5SDYQWgDv!vjTPnaP+0X8(;1w{+h43e`7Y6DG3Pw`eR_pUuO# z5u~+lOC>T)+S;OoqiNd^#!|O;K!)rReT$3G<2QC-I$d|#S{p~p(6-|m7_HvBW^!$a zy6-zax4>rB?squ|uDb0hLrJ)%qU>c%pgw7J3eE=a9f^$Dj&wUhqG;N_7CRX2S2@;t znx0VyJ5BF(XBVAZ)0O7zFH^vX8aEo3>qPT1Z^D;)jmD5pBWm1P&H+c+6DvGOh!OC% z*6L-zja3}lWo*9QJ5PHWic9!mvq?o$Yi+cp{SG%7ZCng{PlF8|nNw>_!KEcZPQin{ z{bL(q+_RVOXG2abF4y1W!)v%Vn7GpuM>q|@QGVVsT;}L}M^g^n#+3lUbQa^g7gpx7 zV|ASaBLf3cHrZ;mky@SkGmw5#fcNLg0(b6I?h1SUlLr?e?_JQC>o z;)_Z&8hgb|h8W;U{OI};Ok85A#?2SGa&p{%F8;B2DR)ITm=O#aN;O73VM#N+DZNvm~t_?3)o}`ocQW?4(7P@qx7nx=} zBzmvK@o;B|9zLX2sax{tqi%b+uPQ*8AE!kO=u7zJ0I|aTiG$H|NN$CdG^-!M`&C-o zU+wnb0F~R!H@RE*nIUEkRG|k0R8^fBMCNhEpc!Ka6ZGm8!?f5~l7Apr9#NouGEg-N zr9p&0?;&#O5K>`cLpnN?xZp7Ng2{cve8UKNXqtx+ew~8vhT$q_vWnT6v6ZzU#PY}o Q#)OgNB?=(frMF0Q* diff --git a/tests/test_data/adaptation/static/output_graph/base_graph_hazard_edges.gpkg b/tests/test_data/adaptation/static/output_graph/base_graph_hazard_edges.gpkg index 7b6fe56bfd9899e5eacb8e8d6dd3659cd70eb84d..dc1ed4e46892240fbc48c8d9a07b6e6bd34ac056 100644 GIT binary patch delta 1986 zcmcJQ>r>Qa6vy|=A`68wumv+FNv7GD3WBh-fOrX!%b*#gmVy^Z2SrT?2SkTeLLjdo zJ_z1Yxwu^9qWnOG03k#WP(zWMR%pfxZxdrqnw{tHH?(g)`#sM&=Xsv5e}LDW5_R9Q2GTJ+6`3<$+3q-czPlIz91@JjDYq>^FA{bDuobVX{sn%)&G>8fVGe zN|ZO^rO%_;oV;!hCohbvCvRTY1UW}`xV!uE?AvhQZ2@iwOOo1gK zD3F0mmPjeaFXe(MOEE-?ruQb?uh_X;<)i75nw#!iu zO{>?hMx(J%46J|yC3&-gb2W!jYL&7%48K4#ocyRasClI0nn0FxC|>`yxUn6lH8!E&bstN;t8CkW9ng&SJrho!!-+URr#_g7bA&ktE?&QZ z8zO!u+Q{g;3-<_P%acdyyLC4XlXXlAE)rT&I88(<18E%CI)cm} zxJ+C2;4FEL+lxO5X8TY>Xxt}TPYK%(Q(21e186ZJ^YjdA29;1B;LGB@%wbF>)Sz3R9p~20exEX-pz})M@_tnw>!*sq!;i1ixIy z{aoHXA&;-7&*uZa%x7a}0e+%@kh8p|HOl4`#YQf7d?C*rpCV2@P(&@K5~;=P@?iFTZ7O&3XCM|cAvh%t!zP?pC%E(w>j#9#w3SN8rJWs>2^Q`>T+s&r6$T zT%o+To3RlZ>&1hWy-|t?Xu-WH-mi^c<=l$jD()R9Gb0Xlp+sKi$Q>?? zdpG|~y1F?e`YsQJ4|>>G+rw%@FDr{acF7-sPd~aT%liX-!P)^iX_PPT!Fb?5P+gF? delta 1986 zcmcJQ`%~0a7{>S6C3Y#)U<+mpbiHNc4H1{c#S3AGf`ev|I-??9(m_xY!V$q?l?=%% zhzIcksa#wExu{>zKtKr*NN=7)f@2M_eNOxU3Aah* z_~2(k$`lBK!OW;I<2tDwzPLazPnD&WwyAha?&AJ5?9-XTeMXNnjP@1@(=ncm*6Fgg z5DKpPch&LMNSzKaf%&0GWiW0Q^NrpT(yM4nzehyV+7HU(be% z%(yuRm4rP(Xe2BQMkhHZ&81Dg*U|gCias-PhgM!_7?o<7`L3&K`#?%^a&mDTXfMhAE5COU@IEF<0%}W%K1NRX%-{Ht*4Li@UEVT?V}dI-TOO1hvrKeC{tJ zME6n{sK~3OT&rOjmDVX6!|@%oFALRvG9)+xXOZsq5uyQ(QN|Dy!9PL=j=*ryOeDB(Fle9fP}s-(pbdVhT5T%BNv8h8w;A z`(hHcvGAfYeX&>!?U2BB-bGlP+=UVs2NTs+iZ^2ov~JT3HFAq?TcBtjSXJWI7Myuq z|2&b5D$BSCad!)b3Tr%eYW22k8LF7J(j(Lk`&FGI)w%?PLu)88N}p&?#7V6|ZS<3_ zt=q7T0*BdhNF7STe%kecB%CF@Z{g;3S@=Q3Z$~p319#vaVRSN|_DM2wNTq!)m9k`K ze}6ZBfkR{+n1YLh#uP5IdKUw!oY>Tlj4!!On|9+gMY`?5F9iF&s3Kh5E1#Ycz7KY? z6|456QBCFv>GT+MLUWMsqT(Q4kUF2i#?(W2Nh(F4nh==DlkHI^pXT^sUf-Mr8hYYJ z7GG1r5xGTWkTP8b8sOjjNP&F^8V2P9%JVV-!62 z1jayf`Ol*w@FYf&J^Cd7eCo`IcWq zD`Da#=~G%SA%XhwBua!-Ay3od&?cT%abw)8Wntw?H8qCfH`H+U@LFV(s;lJ*v$T%Y z%Q~K#HCHeTn&M8CaH!`J9`)>Qt>@s#Z+KohH{c4@z14vA^dpJ-j4qAFLnH2u@iUmy zl1R;HZ?X11wz^t8EJ3a+ONd&n)9c-N3hMRm=;ag)z0SMQHgUCSsqST()+ks+voWr-dH9mLvXVP(pQ)0g7p)jnIjn$p3qJCiIaF;{! Z`w`HEJ5*&zHy^ONTTU9~<9o2~{})r-bZr0t diff --git a/tests/test_data/adaptation/static/output_graph/base_graph_hazard_nodes.gpkg b/tests/test_data/adaptation/static/output_graph/base_graph_hazard_nodes.gpkg index b83aedf80e035f05a87e63ce8894c9c84e2f5fea..229beff870b8ea19cfbc127213d2ffb95e3c8778 100644 GIT binary patch delta 36 scmZozz}~QceZv`jK_guQ;}AmwD^oKoV^ci~1GDD4`rGg7Gb%3t0N2n9s{jB1 delta 36 scmZozz}~QceZv`jK|@_5%Mb$#D`P_|Q%gN_OOxii`rGg7Gb%3t0NEQ0z5oCK diff --git a/tests/test_data/adaptation/static/output_graph/base_graph_nodes.gpkg b/tests/test_data/adaptation/static/output_graph/base_graph_nodes.gpkg index 7af1d761f225708b3a229496925d671f89b1bd0f..60e781a50a16558ce4d558e78d82f9adc66ac884 100644 GIT binary patch delta 36 scmZozz}~QceZv`jK_guQ;}AmwD^oKoBLh7XOM~XS`rGg7Gb%3t0M~L0qyPW_ delta 36 scmZozz}~QceZv`jK|@_5%Mb$#D`P_|6C*uyL*wSV`rGg7Gb%3t0N6+itpET3 diff --git a/tests/test_data/adaptation/static/output_graph/base_network.gpkg b/tests/test_data/adaptation/static/output_graph/base_network.gpkg index f35ce6b9ed30564e46434732ee96a06f1f70e397..334bba5db8fce97fb84e7fbd7021e1f083e41e04 100644 GIT binary patch delta 39 vcmZp8z}fJCbHf>Z0V7=l;}AmwD^oKoBLh7%!{$5s?RWGUx8KocQgHwP5^4=3 delta 39 vcmZp8z}fJCbHf>Z0YhCQ%Mb$#D`P_|6C*uKqvkvM?RWGUx8KocQgHwP6S566 diff --git a/tests/test_data/adaptation/static/output_graph/base_network_hazard.gpkg b/tests/test_data/adaptation/static/output_graph/base_network_hazard.gpkg index ad476b67e45cdca89e4c3f6a2d8a19747cf29c5f..e3ab284c90b69ab8d1347862a00b9715cf65e66f 100644 GIT binary patch delta 40 wcmZp8z}fJCbHf>ZK_guQ;}AmwD^oKoQ$sxiGmGZC`t5i18MoinXHs?m03E^&VgLXD delta 40 wcmZp8z}fJCbHf>ZK|@_5%Mb$#D`O)oBNIIn6Z7V~`t5i18MoinXHs?m03NmtYXATM