flood-adapt 0.3.9__py3-none-any.whl → 0.3.11__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 (100) hide show
  1. flood_adapt/__init__.py +26 -22
  2. flood_adapt/adapter/__init__.py +9 -9
  3. flood_adapt/adapter/fiat_adapter.py +1541 -1541
  4. flood_adapt/adapter/interface/hazard_adapter.py +70 -70
  5. flood_adapt/adapter/interface/impact_adapter.py +36 -36
  6. flood_adapt/adapter/interface/model_adapter.py +89 -89
  7. flood_adapt/adapter/interface/offshore.py +19 -19
  8. flood_adapt/adapter/sfincs_adapter.py +1853 -1848
  9. flood_adapt/adapter/sfincs_offshore.py +187 -193
  10. flood_adapt/config/config.py +248 -248
  11. flood_adapt/config/fiat.py +219 -219
  12. flood_adapt/config/gui.py +331 -331
  13. flood_adapt/config/sfincs.py +481 -336
  14. flood_adapt/config/site.py +129 -129
  15. flood_adapt/database_builder/database_builder.py +2210 -2210
  16. flood_adapt/database_builder/templates/default_units/imperial.toml +9 -9
  17. flood_adapt/database_builder/templates/default_units/metric.toml +9 -9
  18. flood_adapt/database_builder/templates/green_infra_table/green_infra_lookup_table.csv +10 -10
  19. flood_adapt/database_builder/templates/infographics/OSM/config_charts.toml +90 -90
  20. flood_adapt/database_builder/templates/infographics/OSM/config_people.toml +57 -57
  21. flood_adapt/database_builder/templates/infographics/OSM/config_risk_charts.toml +121 -121
  22. flood_adapt/database_builder/templates/infographics/OSM/config_roads.toml +65 -65
  23. flood_adapt/database_builder/templates/infographics/OSM/styles.css +45 -45
  24. flood_adapt/database_builder/templates/infographics/US_NSI/config_charts.toml +126 -126
  25. flood_adapt/database_builder/templates/infographics/US_NSI/config_people.toml +60 -60
  26. flood_adapt/database_builder/templates/infographics/US_NSI/config_risk_charts.toml +121 -121
  27. flood_adapt/database_builder/templates/infographics/US_NSI/config_roads.toml +65 -65
  28. flood_adapt/database_builder/templates/infographics/US_NSI/styles.css +45 -45
  29. flood_adapt/database_builder/templates/infometrics/OSM/metrics_additional_risk_configs.toml +4 -4
  30. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config.toml +143 -143
  31. flood_adapt/database_builder/templates/infometrics/OSM/with_SVI/infographic_metrics_config_risk.toml +153 -153
  32. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config.toml +127 -127
  33. flood_adapt/database_builder/templates/infometrics/OSM/without_SVI/infographic_metrics_config_risk.toml +57 -57
  34. flood_adapt/database_builder/templates/infometrics/US_NSI/metrics_additional_risk_configs.toml +4 -4
  35. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config.toml +191 -191
  36. flood_adapt/database_builder/templates/infometrics/US_NSI/with_SVI/infographic_metrics_config_risk.toml +153 -153
  37. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config.toml +178 -178
  38. flood_adapt/database_builder/templates/infometrics/US_NSI/without_SVI/infographic_metrics_config_risk.toml +57 -57
  39. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config.toml +9 -9
  40. flood_adapt/database_builder/templates/infometrics/mandatory_metrics_config_risk.toml +65 -65
  41. flood_adapt/database_builder/templates/output_layers/bin_colors.toml +5 -5
  42. flood_adapt/database_builder.py +16 -16
  43. flood_adapt/dbs_classes/__init__.py +21 -21
  44. flood_adapt/dbs_classes/database.py +533 -684
  45. flood_adapt/dbs_classes/dbs_benefit.py +77 -76
  46. flood_adapt/dbs_classes/dbs_event.py +61 -59
  47. flood_adapt/dbs_classes/dbs_measure.py +112 -111
  48. flood_adapt/dbs_classes/dbs_projection.py +34 -34
  49. flood_adapt/dbs_classes/dbs_scenario.py +137 -137
  50. flood_adapt/dbs_classes/dbs_static.py +274 -273
  51. flood_adapt/dbs_classes/dbs_strategy.py +130 -129
  52. flood_adapt/dbs_classes/dbs_template.py +279 -278
  53. flood_adapt/dbs_classes/interface/database.py +107 -139
  54. flood_adapt/dbs_classes/interface/element.py +121 -121
  55. flood_adapt/dbs_classes/interface/static.py +47 -47
  56. flood_adapt/flood_adapt.py +1229 -1178
  57. flood_adapt/misc/database_user.py +16 -16
  58. flood_adapt/misc/exceptions.py +22 -0
  59. flood_adapt/misc/log.py +183 -183
  60. flood_adapt/misc/path_builder.py +54 -54
  61. flood_adapt/misc/utils.py +185 -185
  62. flood_adapt/objects/__init__.py +82 -82
  63. flood_adapt/objects/benefits/benefits.py +61 -61
  64. flood_adapt/objects/events/event_factory.py +135 -135
  65. flood_adapt/objects/events/event_set.py +88 -84
  66. flood_adapt/objects/events/events.py +236 -234
  67. flood_adapt/objects/events/historical.py +58 -58
  68. flood_adapt/objects/events/hurricane.py +68 -67
  69. flood_adapt/objects/events/synthetic.py +46 -50
  70. flood_adapt/objects/forcing/__init__.py +92 -92
  71. flood_adapt/objects/forcing/csv.py +68 -68
  72. flood_adapt/objects/forcing/discharge.py +66 -66
  73. flood_adapt/objects/forcing/forcing.py +150 -150
  74. flood_adapt/objects/forcing/forcing_factory.py +182 -182
  75. flood_adapt/objects/forcing/meteo_handler.py +93 -93
  76. flood_adapt/objects/forcing/netcdf.py +40 -40
  77. flood_adapt/objects/forcing/plotting.py +453 -429
  78. flood_adapt/objects/forcing/rainfall.py +98 -98
  79. flood_adapt/objects/forcing/tide_gauge.py +191 -191
  80. flood_adapt/objects/forcing/time_frame.py +90 -90
  81. flood_adapt/objects/forcing/timeseries.py +564 -564
  82. flood_adapt/objects/forcing/unit_system.py +580 -580
  83. flood_adapt/objects/forcing/waterlevels.py +108 -108
  84. flood_adapt/objects/forcing/wind.py +124 -124
  85. flood_adapt/objects/measures/measure_factory.py +92 -92
  86. flood_adapt/objects/measures/measures.py +551 -529
  87. flood_adapt/objects/object_model.py +74 -68
  88. flood_adapt/objects/projections/projections.py +103 -103
  89. flood_adapt/objects/scenarios/scenarios.py +22 -22
  90. flood_adapt/objects/strategies/strategies.py +89 -89
  91. flood_adapt/workflows/benefit_runner.py +579 -554
  92. flood_adapt/workflows/floodmap.py +85 -85
  93. flood_adapt/workflows/impacts_integrator.py +85 -85
  94. flood_adapt/workflows/scenario_runner.py +70 -70
  95. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/LICENSE +674 -674
  96. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/METADATA +867 -865
  97. flood_adapt-0.3.11.dist-info/RECORD +140 -0
  98. flood_adapt-0.3.9.dist-info/RECORD +0 -139
  99. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/WHEEL +0 -0
  100. {flood_adapt-0.3.9.dist-info → flood_adapt-0.3.11.dist-info}/top_level.txt +0 -0
@@ -1,429 +1,453 @@
1
- from pathlib import Path
2
- from tempfile import gettempdir
3
- from typing import List, Optional
4
-
5
- import pandas as pd
6
- import plotly.express as px
7
- import plotly.graph_objects as go
8
- from plotly.subplots import make_subplots
9
-
10
- from flood_adapt.config.site import Site
11
- from flood_adapt.misc.log import FloodAdaptLogging
12
- from flood_adapt.misc.path_builder import (
13
- db_path,
14
- )
15
- from flood_adapt.objects.events.events import Event, Template
16
- from flood_adapt.objects.forcing.discharge import (
17
- DischargeConstant,
18
- DischargeCSV,
19
- DischargeSynthetic,
20
- )
21
- from flood_adapt.objects.forcing.forcing import (
22
- ForcingSource,
23
- ForcingType,
24
- IDischarge,
25
- )
26
- from flood_adapt.objects.forcing.rainfall import (
27
- RainfallConstant,
28
- RainfallCSV,
29
- RainfallSynthetic,
30
- )
31
- from flood_adapt.objects.forcing.waterlevels import (
32
- WaterlevelCSV,
33
- WaterlevelGauged,
34
- WaterlevelSynthetic,
35
- )
36
- from flood_adapt.objects.forcing.wind import (
37
- WindConstant,
38
- WindCSV,
39
- WindSynthetic,
40
- )
41
-
42
- # TODO remove from frontend
43
- UNPLOTTABLE_SOURCES = [ForcingSource.TRACK, ForcingSource.METEO, ForcingSource.MODEL]
44
- logger = FloodAdaptLogging.getLogger("Plotting")
45
-
46
-
47
- def plot_forcing(
48
- event: Event,
49
- site: Site,
50
- forcing_type: ForcingType,
51
- ) -> tuple[str, Optional[List[Exception]]]:
52
- """Plot the forcing data for the event."""
53
- if event.forcings.get(forcing_type) is None:
54
- return "", None
55
-
56
- match forcing_type:
57
- case ForcingType.RAINFALL:
58
- return plot_rainfall(event, site)
59
- case ForcingType.WIND:
60
- return plot_wind(event, site)
61
- case ForcingType.WATERLEVEL:
62
- return plot_waterlevel(event, site)
63
- case ForcingType.DISCHARGE:
64
- return plot_discharge(event, site)
65
- case _:
66
- raise NotImplementedError(
67
- "Plotting only available for rainfall, wind, waterlevel, and discharge forcings."
68
- )
69
-
70
-
71
- def plot_discharge(
72
- event: Event,
73
- site: Site,
74
- ) -> tuple[str, Optional[List[Exception]]]:
75
- rivers: List[IDischarge] = event.forcings.get(ForcingType.DISCHARGE)
76
- if site.sfincs.river is None:
77
- raise ValueError("No rivers defined for this site.")
78
- elif not rivers:
79
- return "", None
80
- logger.debug("Plotting discharge data")
81
-
82
- units = site.gui.units.default_discharge_units
83
-
84
- data = pd.DataFrame()
85
- errors = []
86
-
87
- for discharge in rivers:
88
- try:
89
- if discharge.source in UNPLOTTABLE_SOURCES:
90
- logger.debug(
91
- f"Plotting not supported for discharge data from `{discharge.source}`"
92
- )
93
- continue
94
- elif isinstance(
95
- discharge, (DischargeConstant, DischargeSynthetic, DischargeCSV)
96
- ):
97
- river_data = discharge.to_dataframe(event.time)
98
- else:
99
- raise ValueError(f"Unknown discharge source: `{discharge.source}`")
100
-
101
- # Rename columns to avoid conflicts
102
- river_data.columns = [discharge.river.name]
103
- if data.empty:
104
- data = river_data
105
- else:
106
- # add river_data as a column to the dataframe. keep the same index
107
- data = data.join(river_data, how="outer")
108
- except Exception as e:
109
- errors.append((discharge.river.name, e))
110
-
111
- if errors:
112
- logger.error(
113
- f"Could not retrieve discharge data for {', '.join([entry[0] for entry in errors])}: {errors}"
114
- )
115
- return "", errors
116
-
117
- river_names, river_descriptions = [], []
118
- for river in site.sfincs.river:
119
- river_names.append(river.name)
120
- river_descriptions.append(river.description or river.name)
121
-
122
- # Plot actual thing
123
- fig = go.Figure()
124
- for ii, col in enumerate(data.columns):
125
- fig.add_trace(
126
- go.Scatter(
127
- x=data.index,
128
- y=data[col],
129
- name=river_descriptions[ii],
130
- mode="lines",
131
- )
132
- )
133
-
134
- fig.update_layout(
135
- autosize=False,
136
- height=100 * 2,
137
- width=280 * 2,
138
- margin={"r": 0, "l": 0, "b": 0, "t": 0},
139
- font={"size": 10, "color": "black", "family": "Arial"},
140
- title_font={"size": 10, "color": "black", "family": "Arial"},
141
- yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
142
- xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
143
- xaxis_title={"text": "Time"},
144
- yaxis_title={"text": f"River discharge [{units.value}]"},
145
- xaxis={"range": [event.time.start_time, event.time.end_time]},
146
- )
147
-
148
- # Only save to the the event folder if that has been created already.
149
- # Otherwise this will create the folder and break the db since there is no event.toml yet
150
- output_dir = db_path(object_dir="events", obj_name=event.name)
151
- if not output_dir.exists():
152
- output_dir = gettempdir()
153
- output_loc = Path(output_dir) / "discharge_timeseries.html"
154
- if output_loc.exists():
155
- output_loc.unlink()
156
- fig.write_html(output_loc)
157
- return str(output_loc), None
158
-
159
-
160
- def plot_waterlevel(
161
- event: Event,
162
- site: Site,
163
- ) -> tuple[str, Optional[List[Exception]]]:
164
- forcing_list = event.forcings.get(ForcingType.WATERLEVEL)
165
- if not forcing_list:
166
- return "", None
167
- elif site.sfincs.water_level is None:
168
- raise ValueError("No water levels defined for this site.")
169
-
170
- waterlevel = forcing_list[0]
171
- if waterlevel.source in UNPLOTTABLE_SOURCES:
172
- logger.debug(
173
- f"Plotting not supported for waterlevel data from {waterlevel.source}"
174
- )
175
- return "", None
176
-
177
- logger.debug("Plotting water level data")
178
- units = site.gui.units.default_length_units
179
- data = None
180
- try:
181
- if isinstance(waterlevel, WaterlevelGauged):
182
- if site.sfincs.tide_gauge is None:
183
- raise ValueError("No tide gauge defined for this site.")
184
- data = site.sfincs.tide_gauge.get_waterlevels_in_time_frame(
185
- event.time, units=units
186
- )
187
-
188
- # Convert to main reference
189
- datum_correction = site.sfincs.water_level.get_datum(
190
- site.sfincs.tide_gauge.reference
191
- ).height.convert(units)
192
- data += datum_correction
193
-
194
- elif isinstance(waterlevel, WaterlevelCSV):
195
- data = waterlevel.to_dataframe(event.time)
196
- elif isinstance(waterlevel, WaterlevelSynthetic):
197
- data = waterlevel.to_dataframe(time_frame=event.time)
198
- datum_correction = site.sfincs.water_level.get_datum(
199
- site.gui.plotting.synthetic_tide.datum
200
- ).height.convert(units)
201
- data += datum_correction
202
- else:
203
- raise ValueError(f"Unknown waterlevel type: {waterlevel}")
204
-
205
- except Exception as e:
206
- logger.error(f"Error getting water level data: {e}")
207
- return "", [e]
208
-
209
- if data is not None and data.empty:
210
- logger.error(f"Could not retrieve waterlevel data: {waterlevel} {data}")
211
- return "", None
212
-
213
- if event.template == Template.Synthetic:
214
- data.index = (
215
- data.index - data.index[0]
216
- ).total_seconds() / 3600 # Convert to hours
217
- x_title = "Hours from start"
218
- else:
219
- x_title = "Time"
220
-
221
- # Plot actual thing
222
- fig = px.line(data)
223
-
224
- # plot main reference
225
- fig.add_hline(
226
- y=0,
227
- line_dash="dash",
228
- line_color="#000000",
229
- annotation_text=site.sfincs.water_level.reference,
230
- annotation_position="bottom right",
231
- )
232
-
233
- # plot other references
234
- for wl_ref in site.sfincs.water_level.datums:
235
- if (
236
- wl_ref.name == site.sfincs.config.overland_model.reference
237
- or wl_ref.name in site.gui.plotting.excluded_datums
238
- ):
239
- continue
240
-
241
- fig.add_hline(
242
- y=wl_ref.height.convert(units),
243
- line_dash="dash",
244
- line_color="#3ec97c",
245
- annotation_text=wl_ref.name,
246
- annotation_position="bottom right",
247
- )
248
-
249
- fig.update_layout(
250
- autosize=False,
251
- height=100 * 2,
252
- width=280 * 2,
253
- margin={"r": 0, "l": 0, "b": 0, "t": 0},
254
- font={"size": 10, "color": "black", "family": "Arial"},
255
- title_font={"size": 10, "color": "black", "family": "Arial"},
256
- legend=None,
257
- xaxis_title=x_title,
258
- yaxis_title=f"Water level [{units.value}]",
259
- yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
260
- xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
261
- showlegend=False,
262
- xaxis={"range": [data.index.min(), data.index.max()]},
263
- )
264
-
265
- # Only save to the the event folder if that has been created already.
266
- # Otherwise this will create the folder and break the db since there is no event.toml yet
267
- output_dir = db_path(object_dir="events", obj_name=event.name)
268
- if not output_dir.exists():
269
- output_dir = gettempdir()
270
- output_loc = Path(output_dir) / "waterlevel_timeseries.html"
271
- if output_loc.exists():
272
- output_loc.unlink()
273
- fig.write_html(output_loc)
274
- return str(output_loc), None
275
-
276
-
277
- def plot_rainfall(
278
- event: Event,
279
- site: Site,
280
- ) -> tuple[str, Optional[List[Exception]]]:
281
- forcing_list = event.forcings.get(ForcingType.RAINFALL)
282
- if not forcing_list:
283
- return "", None
284
- elif forcing_list[0].source in UNPLOTTABLE_SOURCES:
285
- logger.warning(
286
- f"Plotting not supported for rainfall datafrom sources {', '.join(UNPLOTTABLE_SOURCES)}"
287
- )
288
- return "", None
289
-
290
- rainfall = forcing_list[0]
291
- logger.debug("Plotting rainfall data")
292
-
293
- data = None
294
- try:
295
- if isinstance(rainfall, (RainfallConstant, RainfallCSV, RainfallSynthetic)):
296
- data = rainfall.to_dataframe(event.time)
297
- else:
298
- raise ValueError(f"Unknown rainfall type: {rainfall}")
299
- except Exception as e:
300
- logger.error(f"Error getting rainfall data: {e}")
301
- return "", [e]
302
-
303
- if data is None or data.empty:
304
- logger.error(f"Could not retrieve rainfall data: {rainfall} {data}")
305
- return "", None
306
-
307
- # Add multiplier
308
- data *= event.rainfall_multiplier
309
-
310
- # Plot actual thing
311
- fig = px.line(data_frame=data)
312
-
313
- fig.update_layout(
314
- autosize=False,
315
- height=100 * 2,
316
- width=280 * 2,
317
- margin={"r": 0, "l": 0, "b": 0, "t": 0},
318
- font={"size": 10, "color": "black", "family": "Arial"},
319
- title_font={"size": 10, "color": "black", "family": "Arial"},
320
- legend=None,
321
- yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
322
- xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
323
- xaxis_title={"text": "Time"},
324
- yaxis_title={
325
- "text": f"Rainfall intensity [{site.gui.units.default_intensity_units.value}]"
326
- },
327
- showlegend=False,
328
- xaxis={"range": [event.time.start_time, event.time.end_time]},
329
- )
330
- # Only save to the the event folder if that has been created already.
331
- # Otherwise this will create the folder and break the db since there is no event.toml yet
332
- output_dir = db_path(object_dir="events", obj_name=event.name)
333
- if not output_dir.exists():
334
- output_dir = gettempdir()
335
- output_loc = Path(output_dir) / "rainfall_timeseries.html"
336
- if output_loc.exists():
337
- output_loc.unlink()
338
- fig.write_html(output_loc)
339
- return str(output_loc), None
340
-
341
-
342
- def plot_wind(
343
- event: Event,
344
- site: Site,
345
- ) -> tuple[str, Optional[List[Exception]]]:
346
- logger.debug("Plotting wind data")
347
- forcing_list = event.forcings.get(ForcingType.WIND)
348
- if not forcing_list:
349
- return "", None
350
- elif forcing_list[0].source in UNPLOTTABLE_SOURCES:
351
- logger.warning(
352
- f"Plotting not supported for wind data from sources {', '.join(UNPLOTTABLE_SOURCES)}"
353
- )
354
- return "", None
355
-
356
- wind = forcing_list[0]
357
- data = None
358
- try:
359
- if isinstance(wind, (WindConstant, WindCSV, WindSynthetic)):
360
- data = wind.to_dataframe(event.time)
361
- else:
362
- raise ValueError(f"Unknown wind type: {wind}")
363
- except Exception as e:
364
- logger.error(f"Error getting wind data: {e}")
365
- return "", [e]
366
-
367
- if data is None or data.empty:
368
- logger.error(
369
- f"Could not retrieve wind data: {event.forcings.get(ForcingType.WIND)} {data}"
370
- )
371
- return "", None
372
-
373
- # Plot actual thing
374
- # Create figure with secondary y-axis
375
-
376
- fig = make_subplots(specs=[[{"secondary_y": True}]])
377
-
378
- # Add traces
379
- fig.add_trace(
380
- go.Scatter(
381
- x=data.index,
382
- y=data.iloc[:, 0],
383
- name="Wind speed",
384
- mode="lines",
385
- ),
386
- secondary_y=False,
387
- )
388
- fig.add_trace(
389
- go.Scatter(
390
- x=data.index, y=data.iloc[:, 1], name="Wind direction", mode="markers"
391
- ),
392
- secondary_y=True,
393
- )
394
-
395
- # Set y-axes titles
396
- fig.update_yaxes(
397
- title_text=f"Wind speed [{site.gui.units.default_velocity_units.value}]",
398
- secondary_y=False,
399
- )
400
- fig.update_yaxes(
401
- title_text=f"Wind direction {site.gui.units.default_direction_units.value}",
402
- secondary_y=True,
403
- )
404
-
405
- fig.update_layout(
406
- autosize=False,
407
- height=100 * 2,
408
- width=280 * 2,
409
- margin={"r": 0, "l": 0, "b": 0, "t": 0},
410
- font={"size": 10, "color": "black", "family": "Arial"},
411
- title_font={"size": 10, "color": "black", "family": "Arial"},
412
- legend=None,
413
- yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
414
- xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
415
- xaxis={"range": [event.time.start_time, event.time.end_time]},
416
- xaxis_title={"text": "Time"},
417
- showlegend=False,
418
- )
419
-
420
- # Only save to the the event folder if that has been created already.
421
- # Otherwise this will create the folder and break the db since there is no event.toml yet
422
- output_dir = db_path(object_dir="events", obj_name=event.name)
423
- if not output_dir.exists():
424
- output_dir = gettempdir()
425
- output_loc = Path(output_dir) / "wind_timeseries.html"
426
- if output_loc.exists():
427
- output_loc.unlink()
428
- fig.write_html(output_loc)
429
- return str(output_loc), None
1
+ from pathlib import Path
2
+ from tempfile import gettempdir
3
+ from typing import List, Optional
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+
10
+ from flood_adapt.config.site import Site
11
+ from flood_adapt.misc.log import FloodAdaptLogging
12
+ from flood_adapt.misc.path_builder import (
13
+ db_path,
14
+ )
15
+ from flood_adapt.objects.events.events import Event, Template
16
+ from flood_adapt.objects.forcing.discharge import (
17
+ DischargeConstant,
18
+ DischargeCSV,
19
+ DischargeSynthetic,
20
+ )
21
+ from flood_adapt.objects.forcing.forcing import (
22
+ ForcingSource,
23
+ ForcingType,
24
+ IDischarge,
25
+ )
26
+ from flood_adapt.objects.forcing.rainfall import (
27
+ RainfallConstant,
28
+ RainfallCSV,
29
+ RainfallSynthetic,
30
+ )
31
+ from flood_adapt.objects.forcing.waterlevels import (
32
+ WaterlevelCSV,
33
+ WaterlevelGauged,
34
+ WaterlevelSynthetic,
35
+ )
36
+ from flood_adapt.objects.forcing.wind import (
37
+ WindConstant,
38
+ WindCSV,
39
+ WindSynthetic,
40
+ )
41
+
42
+ # TODO remove from frontend
43
+ UNPLOTTABLE_SOURCES = [ForcingSource.TRACK, ForcingSource.METEO, ForcingSource.MODEL]
44
+ logger = FloodAdaptLogging.getLogger("Plotting")
45
+
46
+
47
+ def plot_forcing(
48
+ event: Event,
49
+ site: Site,
50
+ forcing_type: ForcingType,
51
+ ) -> tuple[str, Optional[List[Exception]]]:
52
+ """Plot the forcing data for the event."""
53
+ if event.forcings.get(forcing_type) is None:
54
+ return "", None
55
+
56
+ match forcing_type:
57
+ case ForcingType.RAINFALL:
58
+ return plot_rainfall(event, site)
59
+ case ForcingType.WIND:
60
+ return plot_wind(event, site)
61
+ case ForcingType.WATERLEVEL:
62
+ return plot_waterlevel(event, site)
63
+ case ForcingType.DISCHARGE:
64
+ return plot_discharge(event, site)
65
+ case _:
66
+ raise NotImplementedError(
67
+ "Plotting only available for rainfall, wind, waterlevel, and discharge forcings."
68
+ )
69
+
70
+
71
+ def plot_discharge(
72
+ event: Event,
73
+ site: Site,
74
+ ) -> tuple[str, Optional[List[Exception]]]:
75
+ rivers: List[IDischarge] = event.forcings.get(ForcingType.DISCHARGE)
76
+ if site.sfincs.river is None:
77
+ raise ValueError("No rivers defined for this site.")
78
+ elif not rivers:
79
+ return "", None
80
+ logger.debug("Plotting discharge data")
81
+
82
+ units = site.gui.units.default_discharge_units
83
+
84
+ data = pd.DataFrame()
85
+ errors = []
86
+
87
+ for discharge in rivers:
88
+ try:
89
+ if discharge.source in UNPLOTTABLE_SOURCES:
90
+ logger.debug(
91
+ f"Plotting not supported for discharge data from `{discharge.source}`"
92
+ )
93
+ continue
94
+ elif isinstance(
95
+ discharge, (DischargeConstant, DischargeSynthetic, DischargeCSV)
96
+ ):
97
+ river_data = discharge.to_dataframe(event.time)
98
+ else:
99
+ raise ValueError(f"Unknown discharge source: `{discharge.source}`")
100
+
101
+ # Rename columns to avoid conflicts
102
+ river_data.columns = [discharge.river.name]
103
+ if data.empty:
104
+ data = river_data
105
+ else:
106
+ # add river_data as a column to the dataframe. keep the same index
107
+ data = data.join(river_data, how="outer")
108
+ except Exception as e:
109
+ errors.append((discharge.river.name, e))
110
+
111
+ if errors:
112
+ logger.error(
113
+ f"Could not retrieve discharge data for {', '.join([entry[0] for entry in errors])}: {errors}"
114
+ )
115
+ return "", errors
116
+
117
+ river_names, river_descriptions = [], []
118
+ for river in site.sfincs.river:
119
+ river_names.append(river.name)
120
+ river_descriptions.append(river.description or river.name)
121
+
122
+ if event.template == Template.Synthetic:
123
+ data.index = (
124
+ data.index - data.index[0]
125
+ ).total_seconds() / 3600 # Convert to hours
126
+ x_title = "Hours from start"
127
+ else:
128
+ x_title = "Time"
129
+
130
+ # Plot actual thing
131
+ fig = go.Figure()
132
+ for ii, col in enumerate(data.columns):
133
+ fig.add_trace(
134
+ go.Scatter(
135
+ x=data.index,
136
+ y=data[col],
137
+ name=river_descriptions[ii],
138
+ mode="lines",
139
+ )
140
+ )
141
+
142
+ fig.update_layout(
143
+ autosize=False,
144
+ height=100 * 2,
145
+ width=280 * 2,
146
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
147
+ font={"size": 10, "color": "black", "family": "Arial"},
148
+ title_font={"size": 10, "color": "black", "family": "Arial"},
149
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
150
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
151
+ xaxis_title={"text": x_title},
152
+ yaxis_title={"text": f"River discharge [{units.value}]"},
153
+ xaxis={"range": [event.time.start_time, event.time.end_time]},
154
+ )
155
+
156
+ # Only save to the the event folder if that has been created already.
157
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
158
+ output_dir = db_path(object_dir="events", obj_name=event.name)
159
+ if not output_dir.exists():
160
+ output_dir = gettempdir()
161
+ output_loc = Path(output_dir) / "discharge_timeseries.html"
162
+ if output_loc.exists():
163
+ output_loc.unlink()
164
+ fig.write_html(output_loc)
165
+ return str(output_loc), None
166
+
167
+
168
+ def plot_waterlevel(
169
+ event: Event,
170
+ site: Site,
171
+ ) -> tuple[str, Optional[List[Exception]]]:
172
+ forcing_list = event.forcings.get(ForcingType.WATERLEVEL)
173
+ if not forcing_list:
174
+ return "", None
175
+ elif site.sfincs.water_level is None:
176
+ raise ValueError("No water levels defined for this site.")
177
+
178
+ waterlevel = forcing_list[0]
179
+ if waterlevel.source in UNPLOTTABLE_SOURCES:
180
+ logger.debug(
181
+ f"Plotting not supported for waterlevel data from {waterlevel.source}"
182
+ )
183
+ return "", None
184
+
185
+ logger.debug("Plotting water level data")
186
+ units = site.gui.units.default_length_units
187
+ data = None
188
+ try:
189
+ if isinstance(waterlevel, WaterlevelGauged):
190
+ if site.sfincs.tide_gauge is None:
191
+ raise ValueError("No tide gauge defined for this site.")
192
+ data = site.sfincs.tide_gauge.get_waterlevels_in_time_frame(
193
+ event.time, units=units
194
+ )
195
+
196
+ # Convert to main reference
197
+ datum_correction = site.sfincs.water_level.get_datum(
198
+ site.sfincs.tide_gauge.reference
199
+ ).height.convert(units)
200
+ data += datum_correction
201
+
202
+ elif isinstance(waterlevel, WaterlevelCSV):
203
+ data = waterlevel.to_dataframe(event.time)
204
+ elif isinstance(waterlevel, WaterlevelSynthetic):
205
+ data = waterlevel.to_dataframe(time_frame=event.time)
206
+ datum_correction = site.sfincs.water_level.get_datum(
207
+ site.gui.plotting.synthetic_tide.datum
208
+ ).height.convert(units)
209
+ data += datum_correction
210
+ else:
211
+ raise ValueError(f"Unknown waterlevel type: {waterlevel}")
212
+
213
+ except Exception as e:
214
+ logger.error(f"Error getting water level data: {e}")
215
+ return "", [e]
216
+
217
+ if data is not None and data.empty:
218
+ logger.error(f"Could not retrieve waterlevel data: {waterlevel} {data}")
219
+ return "", None
220
+
221
+ if event.template == Template.Synthetic:
222
+ data.index = (
223
+ data.index - data.index[0]
224
+ ).total_seconds() / 3600 # Convert to hours
225
+ x_title = "Hours from start"
226
+ else:
227
+ x_title = "Time"
228
+
229
+ # Plot actual thing
230
+ fig = px.line(data)
231
+
232
+ # plot main reference
233
+ fig.add_hline(
234
+ y=0,
235
+ line_dash="dash",
236
+ line_color="#000000",
237
+ annotation_text=site.sfincs.water_level.reference,
238
+ annotation_position="bottom right",
239
+ )
240
+
241
+ # plot other references
242
+ for wl_ref in site.sfincs.water_level.datums:
243
+ if (
244
+ wl_ref.name == site.sfincs.config.overland_model.reference
245
+ or wl_ref.name in site.gui.plotting.excluded_datums
246
+ ):
247
+ continue
248
+
249
+ fig.add_hline(
250
+ y=wl_ref.height.convert(units),
251
+ line_dash="dash",
252
+ line_color="#3ec97c",
253
+ annotation_text=wl_ref.name,
254
+ annotation_position="bottom right",
255
+ )
256
+
257
+ fig.update_layout(
258
+ autosize=False,
259
+ height=100 * 2,
260
+ width=280 * 2,
261
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
262
+ font={"size": 10, "color": "black", "family": "Arial"},
263
+ title_font={"size": 10, "color": "black", "family": "Arial"},
264
+ legend=None,
265
+ xaxis_title=x_title,
266
+ yaxis_title=f"Water level [{units.value}]",
267
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
268
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
269
+ showlegend=False,
270
+ xaxis={"range": [data.index.min(), data.index.max()]},
271
+ )
272
+
273
+ # Only save to the the event folder if that has been created already.
274
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
275
+ output_dir = db_path(object_dir="events", obj_name=event.name)
276
+ if not output_dir.exists():
277
+ output_dir = gettempdir()
278
+ output_loc = Path(output_dir) / "waterlevel_timeseries.html"
279
+ if output_loc.exists():
280
+ output_loc.unlink()
281
+ fig.write_html(output_loc)
282
+ return str(output_loc), None
283
+
284
+
285
+ def plot_rainfall(
286
+ event: Event,
287
+ site: Site,
288
+ ) -> tuple[str, Optional[List[Exception]]]:
289
+ forcing_list = event.forcings.get(ForcingType.RAINFALL)
290
+ if not forcing_list:
291
+ return "", None
292
+ elif forcing_list[0].source in UNPLOTTABLE_SOURCES:
293
+ logger.warning(
294
+ f"Plotting not supported for rainfall datafrom sources {', '.join(UNPLOTTABLE_SOURCES)}"
295
+ )
296
+ return "", None
297
+
298
+ rainfall = forcing_list[0]
299
+ logger.debug("Plotting rainfall data")
300
+
301
+ data = None
302
+ try:
303
+ if isinstance(rainfall, (RainfallConstant, RainfallCSV, RainfallSynthetic)):
304
+ data = rainfall.to_dataframe(event.time)
305
+ else:
306
+ raise ValueError(f"Unknown rainfall type: {rainfall}")
307
+ except Exception as e:
308
+ logger.error(f"Error getting rainfall data: {e}")
309
+ return "", [e]
310
+
311
+ if data is None or data.empty:
312
+ logger.error(f"Could not retrieve rainfall data: {rainfall} {data}")
313
+ return "", None
314
+
315
+ # Add multiplier
316
+ data *= event.rainfall_multiplier
317
+
318
+ if event.template == Template.Synthetic:
319
+ data.index = (
320
+ data.index - data.index[0]
321
+ ).total_seconds() / 3600 # Convert to hours
322
+ x_title = "Hours from start"
323
+ else:
324
+ x_title = "Time"
325
+
326
+ # Plot actual thing
327
+ fig = px.line(data_frame=data)
328
+
329
+ fig.update_layout(
330
+ autosize=False,
331
+ height=100 * 2,
332
+ width=280 * 2,
333
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
334
+ font={"size": 10, "color": "black", "family": "Arial"},
335
+ title_font={"size": 10, "color": "black", "family": "Arial"},
336
+ legend=None,
337
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
338
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
339
+ xaxis_title={"text": x_title},
340
+ yaxis_title={
341
+ "text": f"Rainfall intensity [{site.gui.units.default_intensity_units.value}]"
342
+ },
343
+ showlegend=False,
344
+ xaxis={"range": [event.time.start_time, event.time.end_time]},
345
+ )
346
+ # Only save to the the event folder if that has been created already.
347
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
348
+ output_dir = db_path(object_dir="events", obj_name=event.name)
349
+ if not output_dir.exists():
350
+ output_dir = gettempdir()
351
+ output_loc = Path(output_dir) / "rainfall_timeseries.html"
352
+ if output_loc.exists():
353
+ output_loc.unlink()
354
+ fig.write_html(output_loc)
355
+ return str(output_loc), None
356
+
357
+
358
+ def plot_wind(
359
+ event: Event,
360
+ site: Site,
361
+ ) -> tuple[str, Optional[List[Exception]]]:
362
+ logger.debug("Plotting wind data")
363
+ forcing_list = event.forcings.get(ForcingType.WIND)
364
+ if not forcing_list:
365
+ return "", None
366
+ elif forcing_list[0].source in UNPLOTTABLE_SOURCES:
367
+ logger.warning(
368
+ f"Plotting not supported for wind data from sources {', '.join(UNPLOTTABLE_SOURCES)}"
369
+ )
370
+ return "", None
371
+
372
+ wind = forcing_list[0]
373
+ data = None
374
+ try:
375
+ if isinstance(wind, (WindConstant, WindCSV, WindSynthetic)):
376
+ data = wind.to_dataframe(event.time)
377
+ else:
378
+ raise ValueError(f"Unknown wind type: {wind}")
379
+ except Exception as e:
380
+ logger.error(f"Error getting wind data: {e}")
381
+ return "", [e]
382
+
383
+ if data is None or data.empty:
384
+ logger.error(
385
+ f"Could not retrieve wind data: {event.forcings.get(ForcingType.WIND)} {data}"
386
+ )
387
+ return "", None
388
+
389
+ if event.template == Template.Synthetic:
390
+ data.index = (
391
+ data.index - data.index[0]
392
+ ).total_seconds() / 3600 # Convert to hours
393
+ x_title = "Hours from start"
394
+ else:
395
+ x_title = "Time"
396
+
397
+ # Plot actual thing
398
+ # Create figure with secondary y-axis
399
+
400
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
401
+
402
+ # Add traces
403
+ fig.add_trace(
404
+ go.Scatter(
405
+ x=data.index,
406
+ y=data.iloc[:, 0],
407
+ name="Wind speed",
408
+ mode="lines",
409
+ ),
410
+ secondary_y=False,
411
+ )
412
+ fig.add_trace(
413
+ go.Scatter(
414
+ x=data.index, y=data.iloc[:, 1], name="Wind direction", mode="markers"
415
+ ),
416
+ secondary_y=True,
417
+ )
418
+
419
+ # Set y-axes titles
420
+ fig.update_yaxes(
421
+ title_text=f"Wind speed [{site.gui.units.default_velocity_units.value}]",
422
+ secondary_y=False,
423
+ )
424
+ fig.update_yaxes(
425
+ title_text=f"Wind direction {site.gui.units.default_direction_units.value}",
426
+ secondary_y=True,
427
+ )
428
+
429
+ fig.update_layout(
430
+ autosize=False,
431
+ height=100 * 2,
432
+ width=280 * 2,
433
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
434
+ font={"size": 10, "color": "black", "family": "Arial"},
435
+ title_font={"size": 10, "color": "black", "family": "Arial"},
436
+ legend=None,
437
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
438
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
439
+ xaxis={"range": [event.time.start_time, event.time.end_time]},
440
+ xaxis_title={"text": x_title},
441
+ showlegend=False,
442
+ )
443
+
444
+ # Only save to the the event folder if that has been created already.
445
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
446
+ output_dir = db_path(object_dir="events", obj_name=event.name)
447
+ if not output_dir.exists():
448
+ output_dir = gettempdir()
449
+ output_loc = Path(output_dir) / "wind_timeseries.html"
450
+ if output_loc.exists():
451
+ output_loc.unlink()
452
+ fig.write_html(output_loc)
453
+ return str(output_loc), None