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

Merged
merged 91 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
91 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
ff952c8
chore: process last review comments
ArdtK Dec 5, 2024
1e0ad50
chore: replace base by union
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
39 changes: 28 additions & 11 deletions ra2ce/analysis/adaptation/adaptation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -64,28 +68,41 @@ def execute(self) -> GeoDataFrame:

def run_cost(self) -> GeoDataFrame:
"""
Calculate the cost for all adaptation options.
Calculate the unit cost for all adaptation options.
"""
# 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
"""
return None
_benefit_gdf = deepcopy(self.graph_file.get_graph())
ArdtK marked this conversation as resolved.
Show resolved Hide resolved

_benefit_gdf = self.adaptation_collection.calculation_options_impact(
_benefit_gdf
)

return _benefit_gdf
ArdtK marked this conversation as resolved.
Show resolved Hide resolved

def calculate_bc_ratio(self) -> GeoDataFrame:
"""
Calculate the benefit-cost ratio for all adaptation options
"""
_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
177 changes: 133 additions & 44 deletions ra2ce/analysis/adaptation/adaptation_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,23 @@
from dataclasses import asdict, dataclass
from pathlib import Path

from geopandas import GeoDataFrame

from ra2ce.analysis.analysis_config_data.analysis_config_data import (
AnalysisConfigData,
AnalysisSectionAdaptationOption,
AnalysisSectionDamages,
AnalysisSectionLosses,
)
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


@dataclass
Expand All @@ -39,71 +51,115 @@ class AdaptationOption:
construction_interval: float
maintenance_cost: float
maintenance_interval: float
damages_root: Path
damages_config: AnalysisSectionDamages
losses_root: Path
losses_config: AnalysisSectionLosses
damages_input: AnalysisInputWrapper
losses_input: AnalysisInputWrapper
ArdtK marked this conversation as resolved.
Show resolved Hide resolved
losses_analysis: AnalysisLossesEnum
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:
"""
Classmethod to create an adaptation option from an analysis configuration and an adaptation option.

Args:
analysis_config (AnalysisConfigWrapper): Analysis config input
adaptation_option (AnalysisSectionAdaptationOption): Adaptation option input

Raises:
ValueError: If damages and losses sections are not present in the analysis config data.

Returns:
AdaptationOption: The created adaptation option.
"""
# Adjust path to the files (should be in Adaptation/input)
def construct_path(
orig_path: Path,
analysis: str,
folder: str,
) -> Path:
if not orig_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
# Orig is directory: add stuff at the end
if not (orig_path.suffix):
return orig_path.parent.joinpath(
"input", adaptation_option.id, analysis, folder
)
return orig_path.parent.parent.joinpath(
"input", adaptation_option.id, analysis, folder, orig_path.name
)

def replace_paths(
config_data: AnalysisConfigData, analysis: str
) -> AnalysisConfigData:
config_data.input_path = construct_path(
config_data.input_path, analysis, "input"
)
config_data.static_path = construct_path(
config_data.static_path, analysis, "static"
)
config_data.output_path = construct_path(
config_data.output_path, analysis, "output"
)
return config_data

if not damages_section or not losses_section:
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)
# Create input for the damages analysis
_damages_config = deepcopy(analysis_config)
_damages_config.config_data = replace_paths(
_damages_config.config_data, "damages"
)
ArdtK marked this conversation as resolved.
Show resolved Hide resolved
_damages_analysis = _damages_config.config_data.get_analysis(
AnalysisDamagesEnum.DAMAGES
)
_damages_input = AnalysisInputWrapper.from_input(
analysis=_damages_analysis,
analysis_config=_damages_config,
graph_file_hazard=analysis_config.graph_files.base_network_hazard,
)

_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
# Create input for the losses analysis
_losses_config = deepcopy(analysis_config)
_losses_config.config_data = replace_paths(_losses_config.config_data, "losses")
_losses_analysis = _losses_config.config_data.get_analysis(
analysis_config.config_data.adaptation.losses_analysis
)
_losses_analysis.traffic_intensities_file = construct_path(
_losses_analysis.traffic_intensities_file, "losses", "input"
)
_losses_section.traffic_intensities_file = extend_path(
"losses", losses_section.traffic_intensities_file
_losses_analysis.resilience_curves_file = construct_path(
_losses_analysis.resilience_curves_file, "losses", "input"
)
_losses_section.values_of_time_file = extend_path(
"losses", losses_section.values_of_time_file
_losses_analysis.values_of_time_file = construct_path(
_losses_analysis.values_of_time_file, "losses", "input"
)
_losses_analysis
_losses_input = AnalysisInputWrapper.from_input(
analysis=_losses_analysis,
analysis_config=_losses_config,
graph_file=analysis_config.graph_files.base_graph,
graph_file_hazard=analysis_config.graph_files.base_graph_hazard,
)

return cls(
**asdict(adaptation_option),
damages_root=_damages_root,
damages_config=_damages_section,
losses_root=_losses_root,
losses_config=_losses_section,
damages_input=_damages_input,
ArdtK marked this conversation as resolved.
Show resolved Hide resolved
losses_input=_losses_input,
losses_analysis=analysis_config.config_data.adaptation.losses_analysis,
analysis_config=analysis_config,
)

def calculate_cost(self, time_horizon: float, discount_rate: float) -> float:
Expand Down Expand Up @@ -148,3 +204,36 @@ 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.
"""
# Damages analysis
_damages = Damages(self.damages_input)
_damages_gdf = _damages.execute()
_dam_col = _damages_gdf.filter(regex="dam_").columns[0]
benefit_graph[f"{self.id}_{_dam_col}"] = _damages_gdf[_dam_col]

# Losses analysis
if self.losses_analysis is AnalysisLossesEnum.SINGLE_LINK_LOSSES:
_losses = SingleLinkLosses(self.losses_input, self.analysis_config)
elif self.losses_analysis is AnalysisLossesEnum.MULTI_LINK_LOSSES:
_losses = MultiLinkLosses(self.losses_input, self.analysis_config)
else:
raise NotImplementedError(
f"Losses analysis {self.losses_analysis} not implemented"
)
ArdtK marked this conversation as resolved.
Show resolved Hide resolved
_losses_gdf = _losses.execute()
_los_col = _losses_gdf.filter(regex="vlh_.*_total").columns[0]
benefit_graph[f"{self.id}_{_los_col}"] = _losses_gdf[_los_col]

# Calculate the impact (summing the damages and losses values)
benefit_graph[f"{self.id}_impact"] = (
benefit_graph[[f"{self.id}_{_dam_col}", f"{self.id}_{_los_col}"]]
).sum(axis=1)

return benefit_graph
Loading