flood-adapt 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. flood_adapt/__init__.py +22 -0
  2. flood_adapt/adapter/__init__.py +9 -0
  3. flood_adapt/adapter/fiat_adapter.py +1502 -0
  4. flood_adapt/adapter/interface/__init__.py +0 -0
  5. flood_adapt/adapter/interface/hazard_adapter.py +70 -0
  6. flood_adapt/adapter/interface/impact_adapter.py +36 -0
  7. flood_adapt/adapter/interface/model_adapter.py +89 -0
  8. flood_adapt/adapter/interface/offshore.py +19 -0
  9. flood_adapt/adapter/sfincs_adapter.py +1857 -0
  10. flood_adapt/adapter/sfincs_offshore.py +193 -0
  11. flood_adapt/config/__init__.py +0 -0
  12. flood_adapt/config/config.py +245 -0
  13. flood_adapt/config/fiat.py +219 -0
  14. flood_adapt/config/gui.py +224 -0
  15. flood_adapt/config/sfincs.py +336 -0
  16. flood_adapt/config/site.py +124 -0
  17. flood_adapt/database_builder/__init__.py +0 -0
  18. flood_adapt/database_builder/database_builder.py +2175 -0
  19. flood_adapt/database_builder/templates/default_units/imperial.toml +9 -0
  20. flood_adapt/database_builder/templates/default_units/metric.toml +9 -0
  21. flood_adapt/database_builder/templates/green_infra_table/green_infra_lookup_table.csv +10 -0
  22. flood_adapt/database_builder/templates/icons/black_down_48x48.png +0 -0
  23. flood_adapt/database_builder/templates/icons/black_left_48x48.png +0 -0
  24. flood_adapt/database_builder/templates/icons/black_right_48x48.png +0 -0
  25. flood_adapt/database_builder/templates/icons/black_up_48x48.png +0 -0
  26. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_down.png +0 -0
  27. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_left.png +0 -0
  28. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_right.png +0 -0
  29. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-16_white_up.png +0 -0
  30. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_down.png +0 -0
  31. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_left.png +0 -0
  32. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_right.png +0 -0
  33. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_black_up.png +0 -0
  34. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_white_left.png +0 -0
  35. flood_adapt/database_builder/templates/icons/icons8-triangle-arrow-24_white_right.png +0 -0
  36. flood_adapt/database_builder/templates/icons/white_down_48x48.png +0 -0
  37. flood_adapt/database_builder/templates/icons/white_left_48x48.png +0 -0
  38. flood_adapt/database_builder/templates/icons/white_right_48x48.png +0 -0
  39. flood_adapt/database_builder/templates/icons/white_up_48x48.png +0 -0
  40. flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +90 -0
  41. flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +57 -0
  42. flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +121 -0
  43. flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +65 -0
  44. flood_adapt/database_builder/templates/infographics/OSM/styles.css +45 -0
  45. flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +126 -0
  46. flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +60 -0
  47. flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +121 -0
  48. flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +65 -0
  49. flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +45 -0
  50. flood_adapt/database_builder/templates/infographics/images/ambulance.png +0 -0
  51. flood_adapt/database_builder/templates/infographics/images/car.png +0 -0
  52. flood_adapt/database_builder/templates/infographics/images/cart.png +0 -0
  53. flood_adapt/database_builder/templates/infographics/images/firetruck.png +0 -0
  54. flood_adapt/database_builder/templates/infographics/images/hospital.png +0 -0
  55. flood_adapt/database_builder/templates/infographics/images/house.png +0 -0
  56. flood_adapt/database_builder/templates/infographics/images/info.png +0 -0
  57. flood_adapt/database_builder/templates/infographics/images/money.png +0 -0
  58. flood_adapt/database_builder/templates/infographics/images/person.png +0 -0
  59. flood_adapt/database_builder/templates/infographics/images/school.png +0 -0
  60. flood_adapt/database_builder/templates/infographics/images/truck.png +0 -0
  61. flood_adapt/database_builder/templates/infographics/images/walking_person.png +0 -0
  62. flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +4 -0
  63. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +143 -0
  64. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +153 -0
  65. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +127 -0
  66. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +57 -0
  67. flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +4 -0
  68. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +191 -0
  69. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +153 -0
  70. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +178 -0
  71. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +57 -0
  72. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +9 -0
  73. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +65 -0
  74. flood_adapt/database_builder/templates/mapbox_layers/bin_colors.toml +5 -0
  75. flood_adapt/database_builder.py +16 -0
  76. flood_adapt/dbs_classes/__init__.py +21 -0
  77. flood_adapt/dbs_classes/database.py +716 -0
  78. flood_adapt/dbs_classes/dbs_benefit.py +97 -0
  79. flood_adapt/dbs_classes/dbs_event.py +91 -0
  80. flood_adapt/dbs_classes/dbs_measure.py +103 -0
  81. flood_adapt/dbs_classes/dbs_projection.py +52 -0
  82. flood_adapt/dbs_classes/dbs_scenario.py +150 -0
  83. flood_adapt/dbs_classes/dbs_static.py +261 -0
  84. flood_adapt/dbs_classes/dbs_strategy.py +147 -0
  85. flood_adapt/dbs_classes/dbs_template.py +302 -0
  86. flood_adapt/dbs_classes/interface/database.py +147 -0
  87. flood_adapt/dbs_classes/interface/element.py +137 -0
  88. flood_adapt/dbs_classes/interface/static.py +47 -0
  89. flood_adapt/flood_adapt.py +1371 -0
  90. flood_adapt/misc/__init__.py +0 -0
  91. flood_adapt/misc/database_user.py +16 -0
  92. flood_adapt/misc/log.py +183 -0
  93. flood_adapt/misc/path_builder.py +54 -0
  94. flood_adapt/misc/utils.py +185 -0
  95. flood_adapt/objects/__init__.py +59 -0
  96. flood_adapt/objects/benefits/__init__.py +0 -0
  97. flood_adapt/objects/benefits/benefits.py +61 -0
  98. flood_adapt/objects/events/__init__.py +0 -0
  99. flood_adapt/objects/events/event_factory.py +135 -0
  100. flood_adapt/objects/events/event_set.py +84 -0
  101. flood_adapt/objects/events/events.py +221 -0
  102. flood_adapt/objects/events/historical.py +55 -0
  103. flood_adapt/objects/events/hurricane.py +64 -0
  104. flood_adapt/objects/events/synthetic.py +48 -0
  105. flood_adapt/objects/forcing/__init__.py +0 -0
  106. flood_adapt/objects/forcing/csv.py +68 -0
  107. flood_adapt/objects/forcing/discharge.py +66 -0
  108. flood_adapt/objects/forcing/forcing.py +142 -0
  109. flood_adapt/objects/forcing/forcing_factory.py +182 -0
  110. flood_adapt/objects/forcing/meteo_handler.py +93 -0
  111. flood_adapt/objects/forcing/netcdf.py +40 -0
  112. flood_adapt/objects/forcing/plotting.py +428 -0
  113. flood_adapt/objects/forcing/rainfall.py +98 -0
  114. flood_adapt/objects/forcing/tide_gauge.py +191 -0
  115. flood_adapt/objects/forcing/time_frame.py +77 -0
  116. flood_adapt/objects/forcing/timeseries.py +552 -0
  117. flood_adapt/objects/forcing/unit_system.py +580 -0
  118. flood_adapt/objects/forcing/waterlevels.py +108 -0
  119. flood_adapt/objects/forcing/wind.py +124 -0
  120. flood_adapt/objects/measures/__init__.py +0 -0
  121. flood_adapt/objects/measures/measure_factory.py +92 -0
  122. flood_adapt/objects/measures/measures.py +506 -0
  123. flood_adapt/objects/object_model.py +68 -0
  124. flood_adapt/objects/projections/__init__.py +0 -0
  125. flood_adapt/objects/projections/projections.py +89 -0
  126. flood_adapt/objects/scenarios/__init__.py +0 -0
  127. flood_adapt/objects/scenarios/scenarios.py +22 -0
  128. flood_adapt/objects/strategies/__init__.py +0 -0
  129. flood_adapt/objects/strategies/strategies.py +68 -0
  130. flood_adapt/workflows/__init__.py +0 -0
  131. flood_adapt/workflows/benefit_runner.py +541 -0
  132. flood_adapt/workflows/floodmap.py +85 -0
  133. flood_adapt/workflows/impacts_integrator.py +82 -0
  134. flood_adapt/workflows/scenario_runner.py +69 -0
  135. flood_adapt-0.3.0.dist-info/LICENSE +21 -0
  136. flood_adapt-0.3.0.dist-info/METADATA +183 -0
  137. flood_adapt-0.3.0.dist-info/RECORD +139 -0
  138. flood_adapt-0.3.0.dist-info/WHEEL +5 -0
  139. flood_adapt-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,68 @@
1
+ from flood_adapt.objects.measures.measures import (
2
+ HazardMeasure,
3
+ ImpactMeasure,
4
+ Measure,
5
+ )
6
+ from flood_adapt.objects.object_model import Object
7
+
8
+
9
+ class Strategy(Object):
10
+ """
11
+ Class representing a strategy in FloodAdapt.
12
+
13
+ A strategy is a collection of measures that can be applied to a model.
14
+
15
+ Attributes
16
+ ----------
17
+ measures : list[str]
18
+ A list of measures associated with the strategy. Should be a list of measure names that are saved in the database.
19
+
20
+ """
21
+
22
+ measures: list[str]
23
+
24
+ _measure_objects: list[Measure] | None = None
25
+
26
+ def initialize_measure_objects(self, measures: list[Measure]) -> None:
27
+ self._measure_objects = measures
28
+
29
+ def get_measures(self) -> list[Measure]:
30
+ """Get the measures paths and types."""
31
+ # Get measure paths using a database structure
32
+ if self._measure_objects is None:
33
+ raise ValueError(
34
+ "Measure objects have not been initialized. Call `initialize_measure_objects()` first."
35
+ )
36
+ return self._measure_objects
37
+
38
+ def get_impact_measures(self) -> list[ImpactMeasure]:
39
+ return [
40
+ measure
41
+ for measure in self.get_measures()
42
+ if isinstance(measure, ImpactMeasure)
43
+ ]
44
+
45
+ def get_impact_strategy(self) -> "Strategy":
46
+ impact_measures = self.get_impact_measures()
47
+ impact_strategy = Strategy(
48
+ name=self.name,
49
+ measures=[m.name for m in impact_measures],
50
+ )
51
+ impact_strategy.initialize_measure_objects(impact_measures)
52
+ return impact_strategy
53
+
54
+ def get_hazard_measures(self) -> list[HazardMeasure]:
55
+ return [
56
+ measure
57
+ for measure in self.get_measures()
58
+ if isinstance(measure, HazardMeasure)
59
+ ]
60
+
61
+ def get_hazard_strategy(self) -> "Strategy":
62
+ hazard_measures = self.get_hazard_measures()
63
+ hazard_strategy = Strategy(
64
+ name=self.name,
65
+ measures=[m.name for m in hazard_measures],
66
+ )
67
+ hazard_strategy.initialize_measure_objects(hazard_measures)
68
+ return hazard_strategy
File without changes
@@ -0,0 +1,541 @@
1
+ import shutil
2
+ from typing import Any
3
+
4
+ import geopandas as gpd
5
+ import numpy as np
6
+ import numpy_financial as npf
7
+ import pandas as pd
8
+ import plotly.graph_objects as go
9
+ import tomli
10
+ import tomli_w
11
+ from fiat_toolbox.metrics_writer.fiat_read_metrics_file import MetricsFileReader
12
+
13
+ from flood_adapt.misc.path_builder import (
14
+ ObjectDir,
15
+ TopLevelDir,
16
+ db_path,
17
+ )
18
+ from flood_adapt.objects.benefits.benefits import Benefit
19
+ from flood_adapt.objects.scenarios.scenarios import Scenario
20
+ from flood_adapt.workflows.scenario_runner import ScenarioRunner
21
+
22
+
23
+ class BenefitRunner:
24
+ """Object holding all attributes and methods related to a benefit analysis."""
25
+
26
+ benefit: Benefit
27
+ _scenarios: pd.DataFrame
28
+
29
+ def __init__(self, database, benefit: Benefit):
30
+ """Initialize function called when object is created through the load_file or load_dict methods."""
31
+ self.database = database
32
+ self.benefit = benefit
33
+
34
+ # Get output path based on database path
35
+ self.check_scenarios()
36
+ self.results_path = self.database.benefits.output_path.joinpath(
37
+ self.benefit.name
38
+ )
39
+ self.site_info = self.database.site
40
+ self.unit = self.site_info.fiat.config.damage_unit
41
+
42
+ @property
43
+ def has_run(self):
44
+ return self.has_run_check()
45
+
46
+ @property
47
+ def results(self):
48
+ if hasattr(self, "_results"):
49
+ return self._results
50
+ self._results = self.get_output()
51
+ return self._results
52
+
53
+ def get_output(self) -> dict[str, Any]:
54
+ if not self.has_run:
55
+ raise RuntimeError(
56
+ f"Cannot read output since benefit analysis '{self.benefit.name}' has not been run yet."
57
+ )
58
+
59
+ results_toml = self.results_path.joinpath("results.toml")
60
+ results_html = self.results_path.joinpath("benefits.html")
61
+ with open(results_toml, mode="rb") as fp:
62
+ results = tomli.load(fp)
63
+ results["html"] = str(results_html)
64
+ self._results = results
65
+ return results
66
+
67
+ def has_run_check(self) -> bool:
68
+ """Check if the benefit analysis has already been run.
69
+
70
+ Returns
71
+ -------
72
+ bool
73
+ True if the analysis has already been run, else False
74
+ """
75
+ # Output files to check
76
+ results_toml = self.results_path.joinpath("results.toml")
77
+ results_csv = self.results_path.joinpath("time_series.csv")
78
+ results_html = self.results_path.joinpath("benefits.html")
79
+
80
+ check = all(
81
+ result.exists() for result in [results_toml, results_csv, results_html]
82
+ )
83
+ return check
84
+
85
+ def check_scenarios(self) -> pd.DataFrame:
86
+ """Check which scenarios are needed for this benefit calculation and if they have already been created.
87
+
88
+ The scenarios attribute of the object is updated accordingly and the table of the scenarios is returned.
89
+
90
+ Returns
91
+ -------
92
+ pd.DataFrame
93
+ a table with the scenarios of the Benefit analysis and their status
94
+ """
95
+ # Define names of scenarios
96
+ scenarios_calc = {
97
+ "current_no_measures": {},
98
+ "current_with_strategy": {},
99
+ "future_no_measures": {},
100
+ "future_with_strategy": {},
101
+ }
102
+
103
+ # Use the predefined names for the current projections and no measures strategy
104
+ for scenario in scenarios_calc.keys():
105
+ scenarios_calc[scenario]["event"] = self.benefit.event_set
106
+
107
+ if "current" in scenario:
108
+ scenarios_calc[scenario]["projection"] = (
109
+ self.benefit.current_situation.projection
110
+ )
111
+ elif "future" in scenario:
112
+ scenarios_calc[scenario]["projection"] = self.benefit.projection
113
+
114
+ if "no_measures" in scenario:
115
+ scenarios_calc[scenario]["strategy"] = self.benefit.baseline_strategy
116
+ else:
117
+ scenarios_calc[scenario]["strategy"] = self.benefit.strategy
118
+
119
+ # Get the available scenarios
120
+ scenarios_avail = self.database.scenarios.list_objects()["objects"]
121
+
122
+ # Check if any of the needed scenarios are already there
123
+ for scenario in scenarios_calc.keys():
124
+ scn_dict = scenarios_calc[scenario].copy()
125
+ scn_dict["name"] = scenario
126
+ scenario_obj = Scenario(**scn_dict)
127
+ created = [
128
+ scn_avl for scn_avl in scenarios_avail if scenario_obj == scn_avl
129
+ ]
130
+ if len(created) > 0:
131
+ runner = ScenarioRunner(self.database, scenario=created[0])
132
+ scenarios_calc[scenario]["scenario created"] = created[0].name
133
+ scenarios_calc[scenario]["scenario run"] = runner.impacts.has_run
134
+ else:
135
+ scenarios_calc[scenario]["scenario created"] = "No"
136
+ scenarios_calc[scenario]["scenario run"] = False
137
+
138
+ df = pd.DataFrame(scenarios_calc).T
139
+ self.scenarios = df.astype(
140
+ dtype={
141
+ "event": "str",
142
+ "projection": "str",
143
+ "strategy": "str",
144
+ "scenario created": "str",
145
+ "scenario run": bool,
146
+ }
147
+ )
148
+ return self.scenarios
149
+
150
+ def ready_to_run(self) -> bool:
151
+ """Check if all the required scenarios have already been run.
152
+
153
+ Returns
154
+ -------
155
+ bool
156
+ True if required scenarios have been already run
157
+ """
158
+ self.check_scenarios()
159
+ check = all(self.scenarios["scenario run"])
160
+
161
+ return check
162
+
163
+ def run_cost_benefit(self):
164
+ """Run the cost-benefit calculation for the total study area and the different aggregation levels."""
165
+ # Throw an error if not all runs are finished
166
+ if not self.ready_to_run():
167
+ # First check is scenarios are there
168
+ if "No" in self.scenarios["scenario created"].to_numpy():
169
+ raise RuntimeError("Necessary scenarios have not been created yet.")
170
+ scens = self.scenarios["scenario created"][~self.scenarios["scenario run"]]
171
+ raise RuntimeError(
172
+ f"Scenarios {', '.join(scens.values)} need to be run before the cost-benefit analysis can be performed"
173
+ )
174
+
175
+ # If path for results does not yet exist, make it, and if it does delete it and recreate it
176
+ if not self.results_path.is_dir():
177
+ self.results_path.mkdir(parents=True)
178
+ else:
179
+ shutil.rmtree(self.results_path)
180
+ self.results_path.mkdir(parents=True)
181
+
182
+ # Run the cost-benefit analysis
183
+ self.cba()
184
+ # Run aggregation benefits
185
+ self.cba_aggregation()
186
+ # Updates results
187
+ self.has_run_check()
188
+
189
+ # Cache results
190
+ self.results
191
+
192
+ def cba(self):
193
+ """Cost-benefit analysis for the whole study area."""
194
+ # Get EAD for each scenario and save to new dataframe
195
+ scenarios = self.scenarios.copy(deep=True)
196
+ scenarios["EAD"] = None
197
+
198
+ scn_output_path = db_path(TopLevelDir.output, ObjectDir.scenario)
199
+
200
+ # Get metrics per scenario
201
+ for index, scenario in scenarios.iterrows():
202
+ scn_name = scenario["scenario created"]
203
+ collective_fn = scn_output_path.joinpath(
204
+ scn_name, f"Infometrics_{scn_name}.csv"
205
+ )
206
+ collective_metrics = MetricsFileReader(
207
+ collective_fn,
208
+ ).read_metrics_from_file()
209
+
210
+ # Fill scenarios EAD column with values from metrics
211
+ scenarios.loc[index, "EAD"] = float(
212
+ collective_metrics["Value"]["ExpectedAnnualDamages"]
213
+ )
214
+
215
+ # Get years of interest
216
+ year_start = self.benefit.current_situation.year
217
+ year_end = self.benefit.future_year
218
+
219
+ # Calculate benefits
220
+ cba = self._calc_benefits(
221
+ years=[year_start, year_end],
222
+ risk_no_measures=[
223
+ scenarios.loc["current_no_measures", "EAD"],
224
+ scenarios.loc["future_no_measures", "EAD"],
225
+ ],
226
+ risk_with_strategy=[
227
+ scenarios.loc["current_with_strategy", "EAD"],
228
+ scenarios.loc["future_with_strategy", "EAD"],
229
+ ],
230
+ discount_rate=self.benefit.discount_rate,
231
+ )
232
+
233
+ # Save indicators in dictionary
234
+ results = {}
235
+ # Get net present value of benefits
236
+ results["benefits"] = cba["benefits_discounted"].sum()
237
+
238
+ # Only if costs are provided do the full cost-benefit analysis
239
+ cost_calc = (self.benefit.implementation_cost is not None) and (
240
+ self.benefit.annual_maint_cost is not None
241
+ )
242
+ if cost_calc:
243
+ cba = self._calc_costs(
244
+ benefits=cba,
245
+ implementation_cost=self.benefit.implementation_cost,
246
+ annual_maint_cost=self.benefit.annual_maint_cost,
247
+ discount_rate=self.benefit.discount_rate,
248
+ )
249
+ # Calculate costs
250
+ results["costs"] = cba["costs_discounted"].sum()
251
+ # Benefit to Cost Ratio
252
+ results["BCR"] = np.round(results["benefits"] / results["costs"], 2)
253
+ # Net present value
254
+ results["NPV"] = cba["profits_discounted"].sum()
255
+ # Internal Rate of Return
256
+ results["IRR"] = np.round(npf.irr(cba["profits"]), 3)
257
+
258
+ # Save results
259
+ if not self.results_path.is_dir():
260
+ self.results_path.mkdir(parents=True)
261
+
262
+ # Save indicators in a toml file
263
+ indicators = self.results_path.joinpath("results.toml")
264
+ with open(indicators, "wb") as f:
265
+ tomli_w.dump(results, f)
266
+
267
+ # Save time-series in a csv
268
+ time_series = self.results_path.joinpath("time_series.csv")
269
+ cba.to_csv(time_series)
270
+
271
+ # Make html
272
+ self._make_html(cba)
273
+
274
+ def cba_aggregation(self):
275
+ """Zonal Benefits analysis for the different aggregation areas."""
276
+ results_path = db_path(TopLevelDir.output, ObjectDir.scenario)
277
+ # Get years of interest
278
+ year_start = self.benefit.current_situation.year
279
+ year_end = self.benefit.future_year
280
+
281
+ # Get EAD for each scenario and save to new dataframe
282
+ scenarios = self.scenarios.copy(deep=True)
283
+
284
+ # Read in the names of the aggregation area types
285
+ aggregations = [aggr.name for aggr in self.site_info.fiat.config.aggregation]
286
+
287
+ # Check if equity information is available to define variables to use
288
+ vars = []
289
+ for i, aggr_name in enumerate(aggregations):
290
+ if self.site_info.fiat.config.aggregation[i].equity is not None:
291
+ vars.append(["EAD", "EWEAD"])
292
+ else:
293
+ vars.append(["EAD"])
294
+
295
+ # Define which names are used in the metric tables
296
+ var_metric = {"EAD": "ExpectedAnnualDamages", "EWEAD": "EWEAD"}
297
+
298
+ # Prepare dictionary to save values
299
+ risk = {}
300
+
301
+ # Fill in the dictionary
302
+ for i, aggr_name in enumerate(aggregations):
303
+ risk[aggr_name] = {}
304
+ values = {}
305
+ for var in vars[i]:
306
+ values[var] = []
307
+ for index, scenario in scenarios.iterrows():
308
+ scn_name = scenario["scenario created"]
309
+ # Get available aggregation levels
310
+ aggregation_fn = results_path.joinpath(
311
+ scn_name, f"Infometrics_{scn_name}_{aggr_name}.csv"
312
+ )
313
+ for var in vars[i]:
314
+ # Get metrics per scenario and per aggregation
315
+ aggregated_metrics = MetricsFileReader(
316
+ aggregation_fn,
317
+ ).read_aggregated_metric_from_file(var_metric[var])[2:]
318
+ aggregated_metrics = aggregated_metrics.loc[
319
+ aggregated_metrics.index.dropna()
320
+ ]
321
+ aggregated_metrics.name = scenario.name
322
+ values[var].append(aggregated_metrics)
323
+
324
+ # Combine values in a single dataframe
325
+ for var in vars[i]:
326
+ risk[aggr_name][var] = pd.DataFrame(values[var]).T.astype(float)
327
+
328
+ var_output = {"EAD": "Benefits", "EWEAD": "Equity Weighted Benefits"}
329
+
330
+ # Calculate benefits
331
+ benefits = {}
332
+ for i, aggr_name in enumerate(aggregations):
333
+ benefits[aggr_name] = pd.DataFrame()
334
+ benefits[aggr_name].index = risk[aggr_name]["EAD"].index
335
+ for var in vars[i]:
336
+ for index, row in risk[aggr_name][var].iterrows():
337
+ cba = self._calc_benefits(
338
+ years=[year_start, year_end],
339
+ risk_no_measures=[
340
+ row["current_no_measures"],
341
+ row["future_no_measures"],
342
+ ],
343
+ risk_with_strategy=[
344
+ row["current_with_strategy"],
345
+ row["future_with_strategy"],
346
+ ],
347
+ discount_rate=self.benefit.discount_rate,
348
+ )
349
+ benefits[aggr_name].loc[row.name, var_output[var]] = cba[
350
+ "benefits_discounted"
351
+ ].sum()
352
+
353
+ # Save results
354
+ if not self.results_path.is_dir():
355
+ self.results_path.mkdir(parents=True)
356
+
357
+ # Save benefits per aggregation area (csv and gpkg)
358
+ for i, aggr_name in enumerate(aggregations):
359
+ csv_filename = self.results_path.joinpath(f"benefits_{aggr_name}.csv")
360
+ benefits[aggr_name].to_csv(csv_filename, index=True)
361
+
362
+ # Load aggregation areas
363
+ ind = [
364
+ i
365
+ for i, n in enumerate(self.site_info.fiat.config.aggregation)
366
+ if n.name == aggr_name
367
+ ][0]
368
+ aggr_areas_path = (
369
+ db_path(TopLevelDir.static)
370
+ / self.site_info.fiat.config.aggregation[ind].file
371
+ )
372
+ aggr_areas = gpd.read_file(aggr_areas_path, engine="pyogrio")
373
+ # Define output path
374
+ outpath = self.results_path.joinpath(f"benefits_{aggr_name}.gpkg")
375
+ # Save file
376
+ aggr_areas = aggr_areas.join(
377
+ benefits[aggr_name],
378
+ on=self.site_info.fiat.config.aggregation[ind].field_name,
379
+ )
380
+ aggr_areas.to_file(outpath, driver="GPKG")
381
+
382
+ @staticmethod
383
+ def _calc_benefits(
384
+ years: list[int, int],
385
+ risk_no_measures: list[float, float],
386
+ risk_with_strategy: list[float, float],
387
+ discount_rate: float,
388
+ ) -> pd.DataFrame:
389
+ """Calculate per year benefits and discounted benefits.
390
+
391
+ Parameters
392
+ ----------
393
+ years : list[int, int]
394
+ the current and future year for the analysis
395
+ risk_no_measures : list[float, float]
396
+ the current and future risk value without any measures
397
+ risk_with_strategy : list[float, float]
398
+ the current and future risk value with the strategy under investigation
399
+ discount_rate : float
400
+ the yearly discount rate used to calculated the total benefit
401
+
402
+ Returns
403
+ -------
404
+ pd.DataFrame
405
+ Dataframe containing the time-series of risks and benefits per year
406
+ """
407
+ benefits = pd.DataFrame(
408
+ data={"risk_no_measures": np.nan, "risk_with_strategy": np.nan},
409
+ index=np.arange(years[0], years[1] + 1),
410
+ )
411
+ benefits.index.names = ["year"]
412
+
413
+ # Fill in dataframe
414
+ for strat, risk in zip(
415
+ ["no_measures", "with_strategy"], [risk_no_measures, risk_with_strategy]
416
+ ):
417
+ benefits.loc[years[0], f"risk_{strat}"] = risk[0]
418
+ benefits.loc[years[1], f"risk_{strat}"] = risk[1]
419
+
420
+ # Assume linear trend between current and future
421
+ benefits = benefits.interpolate(method="linear")
422
+
423
+ # Calculate benefits
424
+ benefits["benefits"] = (
425
+ benefits["risk_no_measures"] - benefits["risk_with_strategy"]
426
+ )
427
+ # Calculate discounted benefits using the provided discount rate
428
+ benefits["benefits_discounted"] = benefits["benefits"] / (
429
+ 1 + discount_rate
430
+ ) ** (benefits.index - benefits.index[0])
431
+
432
+ return benefits
433
+
434
+ @staticmethod
435
+ def _calc_costs(
436
+ benefits: pd.DataFrame,
437
+ implementation_cost: float,
438
+ annual_maint_cost: float,
439
+ discount_rate: float,
440
+ ) -> pd.DataFrame:
441
+ """Calculate per year costs and discounted costs.
442
+
443
+ Parameters
444
+ ----------
445
+ benefits : pd.DataFrame
446
+ a time series of benefits per year (produced with __calc_benefits method)
447
+ implementation_cost : float
448
+ initial costs of implementing the adaptation strategy
449
+ annual_maint_cost : float
450
+ annual maintenance cost of the adaptation strategy
451
+ discount_rate : float
452
+ yearly discount rate
453
+
454
+ Returns
455
+ -------
456
+ pd.DataFrame
457
+ Dataframe containing the time-series of benefits, costs and profits per year
458
+ """
459
+ benefits = benefits.copy()
460
+ benefits["costs"] = np.nan
461
+ # implementations costs at current year and maintenance from year 1
462
+ benefits.loc[benefits.index[0], "costs"] = implementation_cost
463
+ benefits.loc[benefits.index[1:], "costs"] = annual_maint_cost
464
+ benefits["costs_discounted"] = benefits["costs"] / (1 + discount_rate) ** (
465
+ benefits.index - benefits.index[0]
466
+ )
467
+
468
+ # Benefit to Cost Ratio
469
+ benefits["profits"] = benefits["benefits"] - benefits["costs"]
470
+ benefits["profits_discounted"] = benefits["profits"] / (1 + discount_rate) ** (
471
+ benefits.index - benefits.index[0]
472
+ )
473
+
474
+ return benefits
475
+
476
+ def _make_html(self, cba):
477
+ """Make an html with the time-series of the benefits and discounted benefits."""
478
+ # Save a plotly graph in an html
479
+ fig = go.Figure()
480
+
481
+ # Get only endpoints
482
+ cba2 = cba.iloc[[0, -1]]
483
+
484
+ # Add graph with benefits
485
+ fig.add_trace(
486
+ go.Scatter(
487
+ x=cba.index,
488
+ y=cba["benefits"],
489
+ mode="lines",
490
+ line_color="black",
491
+ name="Interpolated benefits",
492
+ )
493
+ )
494
+ fig.add_trace(
495
+ go.Scatter(
496
+ x=cba2.index,
497
+ y=cba2["benefits"],
498
+ mode="markers",
499
+ marker_size=10,
500
+ marker_color="rgba(53,217,44,1)",
501
+ name="Calculated benefits",
502
+ )
503
+ )
504
+
505
+ fig.add_trace(
506
+ go.Scatter(
507
+ x=cba.index,
508
+ y=cba["benefits_discounted"],
509
+ mode="lines",
510
+ line_color="rgba(35,150,29,1)",
511
+ name="Discounted benefits",
512
+ ),
513
+ )
514
+
515
+ fig.add_trace(
516
+ go.Scatter(
517
+ x=cba.index,
518
+ y=cba["benefits_discounted"],
519
+ mode="none",
520
+ fill="tozeroy",
521
+ fillcolor="rgba(35,150,29,0.5)",
522
+ name="Benefits",
523
+ )
524
+ )
525
+
526
+ # Update xaxis properties
527
+ fig.update_xaxes(title_text="Year")
528
+ # Update yaxis properties
529
+ fig.update_yaxes(title_text=f"Annual Benefits ({self.unit})")
530
+
531
+ fig.update_layout(
532
+ autosize=False,
533
+ height=400,
534
+ width=800,
535
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
536
+ font={"size": 12, "color": "black", "family": "Arial"},
537
+ )
538
+
539
+ # write html to results folder
540
+ html = self.results_path.joinpath("benefits.html")
541
+ fig.write_html(html)
@@ -0,0 +1,85 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from flood_adapt.config.sfincs import FloodmapType
5
+ from flood_adapt.misc.database_user import DatabaseUser
6
+ from flood_adapt.misc.log import FloodAdaptLogging
7
+ from flood_adapt.objects.events.event_set import EventSet
8
+ from flood_adapt.objects.events.events import Mode
9
+ from flood_adapt.objects.strategies.strategies import Strategy
10
+
11
+
12
+ class FloodMap(DatabaseUser):
13
+ logger = FloodAdaptLogging.getLogger("FloodMap")
14
+
15
+ type: FloodmapType
16
+
17
+ name: str
18
+ path: Path | os.PathLike | list[Path | os.PathLike]
19
+ event_set: EventSet
20
+
21
+ def __init__(self, scenario_name: str) -> None:
22
+ self.name = scenario_name
23
+ self.type = self.database.site.fiat.config.floodmap_type
24
+ self._get_flood_map_paths()
25
+
26
+ def _get_flood_map_paths(self):
27
+ base_dir = self.database.scenarios.output_path / self.name / "Flooding"
28
+ # TODO check naming of files
29
+ if self.mode == Mode.single_event:
30
+ if self.type == FloodmapType.water_level:
31
+ self.path = base_dir / "max_water_level_map.nc"
32
+ elif self.type == FloodmapType.water_depth:
33
+ self.path = base_dir / f"FloodMap_{self.name}.tif"
34
+ elif self.mode == Mode.risk:
35
+ if self.type == FloodmapType.water_level:
36
+ self.path = list(base_dir.glob("RP_*_maps.nc"))
37
+ elif self.type == FloodmapType.water_depth:
38
+ self.path = list(base_dir.glob("RP_*_maps.tif"))
39
+
40
+ @property
41
+ def has_run(self) -> bool:
42
+ if self.mode == Mode.single_event:
43
+ return self.path.exists()
44
+ elif self.mode == Mode.risk:
45
+ check_files = [RP_map.exists() for RP_map in self.path]
46
+ check_rps = len(self.path) == len(
47
+ self.database.site.fiat.risk.return_periods
48
+ )
49
+ return all(check_files) & check_rps
50
+
51
+ @property
52
+ def scenario(self):
53
+ if hasattr(self, "_scenario"):
54
+ return self._scenario
55
+ self._scenario = self.database.scenarios.get(self.name)
56
+ return self._scenario
57
+
58
+ @property
59
+ def mode(self):
60
+ if hasattr(self, "_mode"):
61
+ return self._mode
62
+ self._mode = self.database.events.get(self.scenario.event).mode
63
+ return self._mode
64
+
65
+ @property
66
+ def crs(self):
67
+ if hasattr(self, "_crs"):
68
+ return self._crs
69
+ self._crs = self.database.site.crs
70
+ return self._crs
71
+
72
+ @property
73
+ def hazard_strategy(self) -> Strategy:
74
+ return self.database.strategies.get(
75
+ self.scenario.strategy
76
+ ).get_hazard_strategy()
77
+
78
+ @property
79
+ def physical_projection(self):
80
+ if hasattr(self, "_physical_projection"):
81
+ return self._physical_projection
82
+ self._physical_projection = self.database.projections.get(
83
+ self.scenario.projection
84
+ ).physical_projection
85
+ return self._physical_projection