Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 595 adaptation run and combine losses and damages #616

Open
wants to merge 89 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
48326b5
chore: extend config and reader
ArdtK Nov 27, 2024
84f599d
chore: add/correct attributes of config data
ArdtK Nov 27, 2024
2954acf
chore: extend reader with options
ArdtK Nov 27, 2024
69015a8
chore: add adaptation property and extend test
ArdtK Nov 27, 2024
88af277
chore: first changes
ArdtK Nov 27, 2024
2b70a23
chore: add adaptation to enum
ArdtK Nov 27, 2024
bdbc35a
Merge branch 'feat/604-adaptation-extend-analysis-config-dataclass-an…
ArdtK Nov 27, 2024
ed85cca
chore: extend classes
ArdtK Nov 28, 2024
e84e8b2
chore: put adaptation in separate enum
ArdtK Nov 28, 2024
825d2b1
Merge branch 'feat/604-adaptation-extend-analysis-config-dataclass-an…
ArdtK Nov 28, 2024
befdfa8
chore: add get_analysis
ArdtK Nov 28, 2024
4bab16c
chore: remove no_intervention from config
ArdtK Nov 28, 2024
a4c6d24
chore: rename no_intervention to no_adaptation
ArdtK Nov 28, 2024
47c94fc
Delete ra2ce/analysis/adaptation/adaptation_option_collection.py
ArdtK Nov 28, 2024
b00ba88
chore: restore no_adaptation_option
ArdtK Nov 28, 2024
214582a
Merge branch 'feat/604-adaptation-extend-analysis-config-dataclass-an…
ArdtK Nov 28, 2024
d23492b
Merge branch 'feat/604-adaptation-extend-analysis-config-dataclass-an…
ArdtK Nov 28, 2024
c1dee8b
chore: expand logic and add tests
ArdtK Nov 28, 2024
d11c58c
chore: add losses analysis name to config
ArdtK Nov 28, 2024
e7826df
Merge branch 'feat/604-adaptation-extend-analysis-config-dataclass-an…
ArdtK Nov 28, 2024
61e09a1
chore: take losses_analysis from config
ArdtK Nov 28, 2024
60a349f
chore: fix test
ArdtK Nov 28, 2024
af07a89
test: add tests
ArdtK Nov 28, 2024
3f49320
chore: fix logic for paths
ArdtK Nov 28, 2024
75f3117
chore: small changes
ArdtK Nov 28, 2024
1fc6161
Merge branch 'master' into feat/592-adaptation-create-class-adaptatio…
ArdtK Nov 28, 2024
03be931
test: add test data
ArdtK Nov 28, 2024
4cbdd1c
chore: small change of folders
ArdtK Nov 28, 2024
cda391a
chore: small changes to paths
ArdtK Nov 28, 2024
9c06029
chore: small ini change
ArdtK Nov 28, 2024
7485c06
chore: typo
ArdtK Nov 28, 2024
5c9e0fb
chore: create class and extend factory
ArdtK Nov 28, 2024
7a0223f
test: fix failing tests
ArdtK Nov 28, 2024
26863c4
Merge branch 'feat/592-adaptation-create-class-adaptationoptioncollec…
ArdtK Nov 28, 2024
6254a5d
Merge branch 'master' into feat/584-adaptation-run-a-cost-analysis
ArdtK Nov 28, 2024
a76027f
chore: remove VAT
ArdtK Nov 29, 2024
50be217
chore: add cost calculation
ArdtK Nov 29, 2024
4c0c20b
chore: process review comments
ArdtK Nov 29, 2024
16edc1d
chore: process review comments
ArdtK Nov 29, 2024
1cd3433
merge master
ArdtK Nov 29, 2024
72c9b22
Merge branch 'feat/592-adaptation-create-class-adaptationoptioncollec…
ArdtK Nov 29, 2024
0368840
chore: fix issues
ArdtK Nov 29, 2024
1e1fb5b
chore: fix test
ArdtK Nov 29, 2024
3f42bce
test: add output_graph files
ArdtK Nov 29, 2024
4bcdc28
test: remove test
ArdtK Nov 29, 2024
f13eb06
empty commit
ArdtK Nov 29, 2024
a8c4b56
chore: process review comments
ArdtK Nov 29, 2024
de0e7ed
chore: add base_network in the factory
ArdtK Nov 29, 2024
f831e28
chore: add cost calculation
ArdtK Nov 29, 2024
8465254
chore: rename collection attribute
ArdtK Nov 29, 2024
c72fce6
empty commit
ArdtK Nov 29, 2024
4bb2c6d
chore: small changes
ArdtK Nov 29, 2024
690ff85
chore: add calculate to collectino
ArdtK Nov 30, 2024
98480a6
chore: add root_path
ArdtK Dec 2, 2024
9b34fc5
test: extend conftest
ArdtK Dec 2, 2024
84def9d
test: move test input
ArdtK Dec 2, 2024
5d0bb7e
chore: add path properties to adaptation_option
ArdtK Dec 2, 2024
76f761f
chore: first damages setup
ArdtK Dec 2, 2024
6235bb3
test: fix/extend tests
ArdtK Dec 2, 2024
7d95030
Merge branch '594-adaptation-create-and-save-result-cost-options' int…
ArdtK Dec 2, 2024
7a8ec23
chore: add aggregate_wl to config
ArdtK Dec 2, 2024
a618cf6
chore: add losses (start)
ArdtK Dec 2, 2024
9f64234
chore: big overhaul of the creation of adaptation options
ArdtK Dec 2, 2024
7aa4602
chore: add docstring
ArdtK Dec 2, 2024
5d3001c
chore: remove unused imports
ArdtK Dec 2, 2024
743cb47
empty commit to conclude merge
ArdtK Dec 2, 2024
19d749e
test: fix losses input paths
ArdtK Dec 2, 2024
20da45c
chore: fix losses run
ArdtK Dec 2, 2024
a450bd3
chore: small cleanup
ArdtK Dec 2, 2024
5dc4cd4
chore: small changes, add docstring
ArdtK Dec 3, 2024
3f86093
chore: fix tests
ArdtK Dec 3, 2024
9cc580d
chore: small changes
ArdtK Dec 3, 2024
d07a98d
test: adapt losses configs
ArdtK Dec 3, 2024
78bc2a6
chore: restore graph_file_hazard
ArdtK Dec 3, 2024
fc2bbf0
chore: copy static folder as well for avg_speed
ArdtK Dec 3, 2024
20801ce
chore: add hazard section with wl_aggregate
ArdtK Dec 3, 2024
3303e1f
test: correct name of resilience_curve csv
ArdtK Dec 3, 2024
d1ac1dc
updated network in test data
Cham8920 Dec 3, 2024
1c46891
Merge branch 'feat/595-adaptation-run-and-combine-losses-and-damages'…
ArdtK Dec 3, 2024
f6baae1
chore: calculate benefit for options
ArdtK Dec 3, 2024
c8c5eb6
chore: final changes
ArdtK Dec 3, 2024
c05cdb6
chore: add TODOs
ArdtK Dec 3, 2024
716ec3d
chore: change readers into dataclasses
ArdtK Dec 4, 2024
861fb0c
chore: add expected total cost (for now based on unit cost only)
ArdtK Dec 4, 2024
dfd550d
chore: process rework
ArdtK Dec 5, 2024
f9e629e
chore: add/update tests
ArdtK Dec 5, 2024
ba83bf5
chore: rework rerooting config
ArdtK Dec 5, 2024
55a743d
chore: last changes for handling paths
ArdtK Dec 5, 2024
4bad422
chore: remove analysis.ini output file
ArdtK Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 38 additions & 13 deletions ra2ce/analysis/adaptation/adaptation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,65 +27,90 @@
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
)

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:
ArdtK marked this conversation as resolved.
Show resolved Hide resolved
"""
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())
ArdtK marked this conversation as resolved.
Show resolved Hide resolved

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
113 changes: 61 additions & 52 deletions ra2ce/analysis/adaptation/adaptation_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,71 +44,57 @@ 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 = []
for _analysis in [
AnalysisDamagesEnum.DAMAGES,
analysis_config.config_data.adaptation.losses_analysis,
]:
_analyses.append(
AdaptationOptionAnalysis.from_config(
analysis_config=analysis_config,
analysis_type=_analysis,
option_id=adaptation_option.id,
)
)
Comment on lines +81 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_analyses = []
for _analysis in [
AnalysisDamagesEnum.DAMAGES,
analysis_config.config_data.adaptation.losses_analysis,
]:
_analyses.append(
AdaptationOptionAnalysis.from_config(
analysis_config=analysis_config,
analysis_type=_analysis,
option_id=adaptation_option.id,
)
)
_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:
Expand Down Expand Up @@ -148,3 +139,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
137 changes: 137 additions & 0 deletions ra2ce/analysis/adaptation/adaptation_option_analysis.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
"""
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the executo of the AnalysisProtocol? If so then you are missing the protocol declaration at the dataclass level.

"""
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()
Loading