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,428 @@
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
+ UNPLOTTABLE_SOURCES = [ForcingSource.TRACK, ForcingSource.METEO, ForcingSource.MODEL]
43
+ logger = FloodAdaptLogging.getLogger("Plotting")
44
+
45
+
46
+ def plot_forcing(
47
+ event: Event,
48
+ site: Site,
49
+ forcing_type: ForcingType,
50
+ ) -> tuple[str, Optional[List[Exception]]]:
51
+ """Plot the forcing data for the event."""
52
+ if event.forcings.get(forcing_type) is None:
53
+ return "", None
54
+
55
+ match forcing_type:
56
+ case ForcingType.RAINFALL:
57
+ return plot_rainfall(event, site)
58
+ case ForcingType.WIND:
59
+ return plot_wind(event, site)
60
+ case ForcingType.WATERLEVEL:
61
+ return plot_waterlevel(event, site)
62
+ case ForcingType.DISCHARGE:
63
+ return plot_discharge(event, site)
64
+ case _:
65
+ raise NotImplementedError(
66
+ "Plotting only available for rainfall, wind, waterlevel, and discharge forcings."
67
+ )
68
+
69
+
70
+ def plot_discharge(
71
+ event: Event,
72
+ site: Site,
73
+ ) -> tuple[str, Optional[List[Exception]]]:
74
+ rivers: List[IDischarge] = event.forcings.get(ForcingType.DISCHARGE)
75
+ if site.sfincs.river is None:
76
+ raise ValueError("No rivers defined for this site.")
77
+ elif not rivers:
78
+ return "", None
79
+ logger.debug("Plotting discharge data")
80
+
81
+ units = site.gui.units.default_discharge_units
82
+
83
+ data = pd.DataFrame()
84
+ errors = []
85
+
86
+ for discharge in rivers:
87
+ try:
88
+ if discharge.source in UNPLOTTABLE_SOURCES:
89
+ logger.debug(
90
+ f"Plotting not supported for discharge data from `{discharge.source}`"
91
+ )
92
+ continue
93
+ elif isinstance(
94
+ discharge, (DischargeConstant, DischargeSynthetic, DischargeCSV)
95
+ ):
96
+ river_data = discharge.to_dataframe(event.time)
97
+ else:
98
+ raise ValueError(f"Unknown discharge source: `{discharge.source}`")
99
+
100
+ # Rename columns to avoid conflicts
101
+ river_data.columns = [discharge.river.name]
102
+ if data.empty:
103
+ data = river_data
104
+ else:
105
+ # add river_data as a column to the dataframe. keep the same index
106
+ data = data.join(river_data, how="outer")
107
+ except Exception as e:
108
+ errors.append((discharge.river.name, e))
109
+
110
+ if errors:
111
+ logger.error(
112
+ f"Could not retrieve discharge data for {', '.join([entry[0] for entry in errors])}: {errors}"
113
+ )
114
+ return "", errors
115
+
116
+ river_names, river_descriptions = [], []
117
+ for river in site.sfincs.river:
118
+ river_names.append(river.name)
119
+ river_descriptions.append(river.description or river.name)
120
+
121
+ # Plot actual thing
122
+ fig = go.Figure()
123
+ for ii, col in enumerate(data.columns):
124
+ fig.add_trace(
125
+ go.Scatter(
126
+ x=data.index,
127
+ y=data[col],
128
+ name=river_descriptions[ii],
129
+ mode="lines",
130
+ )
131
+ )
132
+
133
+ fig.update_layout(
134
+ autosize=False,
135
+ height=100 * 2,
136
+ width=280 * 2,
137
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
138
+ font={"size": 10, "color": "black", "family": "Arial"},
139
+ title_font={"size": 10, "color": "black", "family": "Arial"},
140
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
141
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
142
+ xaxis_title={"text": "Time"},
143
+ yaxis_title={"text": f"River discharge [{units.value}]"},
144
+ xaxis={"range": [event.time.start_time, event.time.end_time]},
145
+ )
146
+
147
+ # Only save to the the event folder if that has been created already.
148
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
149
+ output_dir = db_path(object_dir="events", obj_name=event.name)
150
+ if not output_dir.exists():
151
+ output_dir = gettempdir()
152
+ output_loc = Path(output_dir) / "discharge_timeseries.html"
153
+ if output_loc.exists():
154
+ output_loc.unlink()
155
+ fig.write_html(output_loc)
156
+ return str(output_loc), None
157
+
158
+
159
+ def plot_waterlevel(
160
+ event: Event,
161
+ site: Site,
162
+ ) -> tuple[str, Optional[List[Exception]]]:
163
+ forcing_list = event.forcings.get(ForcingType.WATERLEVEL)
164
+ if not forcing_list:
165
+ return "", None
166
+ elif site.sfincs.water_level is None:
167
+ raise ValueError("No water levels defined for this site.")
168
+
169
+ waterlevel = forcing_list[0]
170
+ if waterlevel.source in UNPLOTTABLE_SOURCES:
171
+ logger.debug(
172
+ f"Plotting not supported for waterlevel data from {waterlevel.source}"
173
+ )
174
+ return "", None
175
+
176
+ logger.debug("Plotting water level data")
177
+ units = site.gui.units.default_length_units
178
+ data = None
179
+ try:
180
+ if isinstance(waterlevel, WaterlevelGauged):
181
+ if site.sfincs.tide_gauge is None:
182
+ raise ValueError("No tide gauge defined for this site.")
183
+ data = site.sfincs.tide_gauge.get_waterlevels_in_time_frame(
184
+ event.time, units=units
185
+ )
186
+
187
+ # Convert to main reference
188
+ datum_correction = site.sfincs.water_level.get_datum(
189
+ site.sfincs.tide_gauge.reference
190
+ ).height.convert(units)
191
+ data += datum_correction
192
+
193
+ elif isinstance(waterlevel, WaterlevelCSV):
194
+ data = waterlevel.to_dataframe(event.time)
195
+ elif isinstance(waterlevel, WaterlevelSynthetic):
196
+ data = waterlevel.to_dataframe(time_frame=event.time)
197
+ datum_correction = site.sfincs.water_level.get_datum(
198
+ site.gui.plotting.synthetic_tide.datum
199
+ ).height.convert(units)
200
+ data += datum_correction
201
+ else:
202
+ raise ValueError(f"Unknown waterlevel type: {waterlevel}")
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error getting water level data: {e}")
206
+ return "", [e]
207
+
208
+ if data is not None and data.empty:
209
+ logger.error(f"Could not retrieve waterlevel data: {waterlevel} {data}")
210
+ return "", None
211
+
212
+ if event.template == Template.Synthetic:
213
+ data.index = (
214
+ data.index - data.index[0]
215
+ ).total_seconds() / 3600 # Convert to hours
216
+ x_title = "Hours from start"
217
+ else:
218
+ x_title = "Time"
219
+
220
+ # Plot actual thing
221
+ fig = px.line(data)
222
+
223
+ # plot main reference
224
+ fig.add_hline(
225
+ y=0,
226
+ line_dash="dash",
227
+ line_color="#000000",
228
+ annotation_text=site.sfincs.water_level.reference,
229
+ annotation_position="bottom right",
230
+ )
231
+
232
+ # plot other references
233
+ for wl_ref in site.sfincs.water_level.datums:
234
+ if (
235
+ wl_ref.name == site.sfincs.config.overland_model.reference
236
+ or wl_ref.name in site.gui.plotting.excluded_datums
237
+ ):
238
+ continue
239
+
240
+ fig.add_hline(
241
+ y=wl_ref.height.convert(units),
242
+ line_dash="dash",
243
+ line_color="#3ec97c",
244
+ annotation_text=wl_ref.name,
245
+ annotation_position="bottom right",
246
+ )
247
+
248
+ fig.update_layout(
249
+ autosize=False,
250
+ height=100 * 2,
251
+ width=280 * 2,
252
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
253
+ font={"size": 10, "color": "black", "family": "Arial"},
254
+ title_font={"size": 10, "color": "black", "family": "Arial"},
255
+ legend=None,
256
+ xaxis_title=x_title,
257
+ yaxis_title=f"Water level [{units.value}]",
258
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
259
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
260
+ showlegend=False,
261
+ xaxis={"range": [data.index.min(), data.index.max()]},
262
+ )
263
+
264
+ # Only save to the the event folder if that has been created already.
265
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
266
+ output_dir = db_path(object_dir="events", obj_name=event.name)
267
+ if not output_dir.exists():
268
+ output_dir = gettempdir()
269
+ output_loc = Path(output_dir) / "waterlevel_timeseries.html"
270
+ if output_loc.exists():
271
+ output_loc.unlink()
272
+ fig.write_html(output_loc)
273
+ return str(output_loc), None
274
+
275
+
276
+ def plot_rainfall(
277
+ event: Event,
278
+ site: Site,
279
+ ) -> tuple[str, Optional[List[Exception]]]:
280
+ forcing_list = event.forcings.get(ForcingType.RAINFALL)
281
+ if not forcing_list:
282
+ return "", None
283
+ elif forcing_list[0].source in UNPLOTTABLE_SOURCES:
284
+ logger.warning(
285
+ f"Plotting not supported for rainfall datafrom sources {', '.join(UNPLOTTABLE_SOURCES)}"
286
+ )
287
+ return "", None
288
+
289
+ rainfall = forcing_list[0]
290
+ logger.debug("Plotting rainfall data")
291
+
292
+ data = None
293
+ try:
294
+ if isinstance(rainfall, (RainfallConstant, RainfallCSV, RainfallSynthetic)):
295
+ data = rainfall.to_dataframe(event.time)
296
+ else:
297
+ raise ValueError(f"Unknown rainfall type: {rainfall}")
298
+ except Exception as e:
299
+ logger.error(f"Error getting rainfall data: {e}")
300
+ return "", [e]
301
+
302
+ if data is None or data.empty:
303
+ logger.error(f"Could not retrieve rainfall data: {rainfall} {data}")
304
+ return "", None
305
+
306
+ # Add multiplier
307
+ data *= event.rainfall_multiplier
308
+
309
+ # Plot actual thing
310
+ fig = px.line(data_frame=data)
311
+
312
+ fig.update_layout(
313
+ autosize=False,
314
+ height=100 * 2,
315
+ width=280 * 2,
316
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
317
+ font={"size": 10, "color": "black", "family": "Arial"},
318
+ title_font={"size": 10, "color": "black", "family": "Arial"},
319
+ legend=None,
320
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
321
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
322
+ xaxis_title={"text": "Time"},
323
+ yaxis_title={
324
+ "text": f"Rainfall intensity [{site.gui.units.default_intensity_units.value}]"
325
+ },
326
+ showlegend=False,
327
+ xaxis={"range": [event.time.start_time, event.time.end_time]},
328
+ )
329
+ # Only save to the the event folder if that has been created already.
330
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
331
+ output_dir = db_path(object_dir="events", obj_name=event.name)
332
+ if not output_dir.exists():
333
+ output_dir = gettempdir()
334
+ output_loc = Path(output_dir) / "rainfall_timeseries.html"
335
+ if output_loc.exists():
336
+ output_loc.unlink()
337
+ fig.write_html(output_loc)
338
+ return str(output_loc), None
339
+
340
+
341
+ def plot_wind(
342
+ event: Event,
343
+ site: Site,
344
+ ) -> tuple[str, Optional[List[Exception]]]:
345
+ logger.debug("Plotting wind data")
346
+ forcing_list = event.forcings.get(ForcingType.WIND)
347
+ if not forcing_list:
348
+ return "", None
349
+ elif forcing_list[0].source in UNPLOTTABLE_SOURCES:
350
+ logger.warning(
351
+ f"Plotting not supported for wind data from sources {', '.join(UNPLOTTABLE_SOURCES)}"
352
+ )
353
+ return "", None
354
+
355
+ wind = forcing_list[0]
356
+ data = None
357
+ try:
358
+ if isinstance(wind, (WindConstant, WindCSV, WindSynthetic)):
359
+ data = wind.to_dataframe(event.time)
360
+ else:
361
+ raise ValueError(f"Unknown wind type: {wind}")
362
+ except Exception as e:
363
+ logger.error(f"Error getting wind data: {e}")
364
+ return "", [e]
365
+
366
+ if data is None or data.empty:
367
+ logger.error(
368
+ f"Could not retrieve wind data: {event.forcings.get(ForcingType.WIND)} {data}"
369
+ )
370
+ return "", None
371
+
372
+ # Plot actual thing
373
+ # Create figure with secondary y-axis
374
+
375
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
376
+
377
+ # Add traces
378
+ fig.add_trace(
379
+ go.Scatter(
380
+ x=data.index,
381
+ y=data.iloc[:, 0],
382
+ name="Wind speed",
383
+ mode="lines",
384
+ ),
385
+ secondary_y=False,
386
+ )
387
+ fig.add_trace(
388
+ go.Scatter(
389
+ x=data.index, y=data.iloc[:, 1], name="Wind direction", mode="markers"
390
+ ),
391
+ secondary_y=True,
392
+ )
393
+
394
+ # Set y-axes titles
395
+ fig.update_yaxes(
396
+ title_text=f"Wind speed [{site.gui.units.default_velocity_units.value}]",
397
+ secondary_y=False,
398
+ )
399
+ fig.update_yaxes(
400
+ title_text=f"Wind direction {site.gui.units.default_direction_units.value}",
401
+ secondary_y=True,
402
+ )
403
+
404
+ fig.update_layout(
405
+ autosize=False,
406
+ height=100 * 2,
407
+ width=280 * 2,
408
+ margin={"r": 0, "l": 0, "b": 0, "t": 0},
409
+ font={"size": 10, "color": "black", "family": "Arial"},
410
+ title_font={"size": 10, "color": "black", "family": "Arial"},
411
+ legend=None,
412
+ yaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
413
+ xaxis_title_font={"size": 10, "color": "black", "family": "Arial"},
414
+ xaxis={"range": [event.time.start_time, event.time.end_time]},
415
+ xaxis_title={"text": "Time"},
416
+ showlegend=False,
417
+ )
418
+
419
+ # Only save to the the event folder if that has been created already.
420
+ # Otherwise this will create the folder and break the db since there is no event.toml yet
421
+ output_dir = db_path(object_dir="events", obj_name=event.name)
422
+ if not output_dir.exists():
423
+ output_dir = gettempdir()
424
+ output_loc = Path(output_dir) / "wind_timeseries.html"
425
+ if output_loc.exists():
426
+ output_loc.unlink()
427
+ fig.write_html(output_loc)
428
+ return str(output_loc), None
@@ -0,0 +1,98 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import pandas as pd
6
+ import xarray as xr
7
+
8
+ from flood_adapt.misc.utils import (
9
+ copy_file_to_output_dir,
10
+ validate_file_extension,
11
+ )
12
+ from flood_adapt.objects.forcing import unit_system as us
13
+ from flood_adapt.objects.forcing.forcing import (
14
+ ForcingSource,
15
+ IRainfall,
16
+ )
17
+ from flood_adapt.objects.forcing.netcdf import validate_netcdf_forcing
18
+ from flood_adapt.objects.forcing.time_frame import TimeFrame
19
+ from flood_adapt.objects.forcing.timeseries import (
20
+ CSVTimeseries,
21
+ SyntheticTimeseries,
22
+ TimeseriesFactory,
23
+ )
24
+
25
+
26
+ class RainfallConstant(IRainfall):
27
+ source: ForcingSource = ForcingSource.CONSTANT
28
+
29
+ intensity: us.UnitfulIntensity
30
+
31
+ def to_dataframe(self, time_frame: TimeFrame) -> pd.DataFrame:
32
+ time = pd.date_range(
33
+ start=time_frame.start_time,
34
+ end=time_frame.end_time,
35
+ freq=time_frame.time_step,
36
+ name="time",
37
+ )
38
+ values = [self.intensity.value for _ in range(len(time))]
39
+ return pd.DataFrame(data=values, index=time)
40
+
41
+
42
+ class RainfallSynthetic(IRainfall):
43
+ source: ForcingSource = ForcingSource.SYNTHETIC
44
+ timeseries: SyntheticTimeseries
45
+
46
+ def to_dataframe(self, time_frame: TimeFrame) -> pd.DataFrame:
47
+ return TimeseriesFactory.from_object(self.timeseries).to_dataframe(
48
+ time_frame=time_frame
49
+ )
50
+
51
+
52
+ class RainfallMeteo(IRainfall):
53
+ source: ForcingSource = ForcingSource.METEO
54
+ precip_units: us.UnitTypesIntensity = us.UnitTypesIntensity.mm_hr
55
+ wind_units: us.UnitTypesVelocity = us.UnitTypesVelocity.mps
56
+
57
+
58
+ class RainfallTrack(IRainfall):
59
+ source: ForcingSource = ForcingSource.TRACK
60
+
61
+ path: Annotated[Path, validate_file_extension([".cyc", ".spw"])]
62
+
63
+ def save_additional(self, output_dir: Path | str | os.PathLike) -> None:
64
+ if self.path:
65
+ self.path = copy_file_to_output_dir(self.path, Path(output_dir))
66
+
67
+
68
+ class RainfallCSV(IRainfall):
69
+ source: ForcingSource = ForcingSource.CSV
70
+
71
+ path: Annotated[Path, validate_file_extension([".csv"])]
72
+
73
+ units: us.UnitTypesIntensity = us.UnitTypesIntensity.mm_hr
74
+
75
+ def to_dataframe(self, time_frame: TimeFrame) -> pd.DataFrame:
76
+ return CSVTimeseries.load_file(
77
+ path=self.path, units=us.UnitfulIntensity(value=0, units=self.units)
78
+ ).to_dataframe(time_frame=time_frame)
79
+
80
+ def save_additional(self, output_dir: Path | str | os.PathLike) -> None:
81
+ self.path = copy_file_to_output_dir(self.path, Path(output_dir))
82
+
83
+
84
+ class RainfallNetCDF(IRainfall):
85
+ source: ForcingSource = ForcingSource.NETCDF
86
+ units: us.UnitTypesIntensity = us.UnitTypesIntensity.mm_hr
87
+
88
+ path: Annotated[Path, validate_file_extension([".nc"])]
89
+
90
+ def read(self) -> xr.Dataset:
91
+ required_vars = ("precip",)
92
+ required_coords = ("time", "lat", "lon")
93
+ with xr.open_dataset(self.path) as ds:
94
+ validated_ds = validate_netcdf_forcing(ds, required_vars, required_coords)
95
+ return validated_ds
96
+
97
+ def save_additional(self, output_dir: Path | str | os.PathLike) -> None:
98
+ self.path = copy_file_to_output_dir(self.path, Path(output_dir))