rashdf 0.2.2__py3-none-any.whl → 0.4.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.
rashdf/plan.py CHANGED
@@ -1,8 +1,152 @@
1
1
  """HEC-RAS Plan HDF class."""
2
2
 
3
3
  from .geom import RasGeomHdf
4
- from typing import Dict
4
+ from .utils import (
5
+ df_datetimes_to_str,
6
+ ras_timesteps_to_datetimes,
7
+ parse_ras_datetime_ms,
8
+ )
9
+
5
10
  from geopandas import GeoDataFrame
11
+ import h5py
12
+ import numpy as np
13
+ from pandas import DataFrame
14
+ import pandas as pd
15
+ import xarray as xr
16
+
17
+ from datetime import datetime
18
+ from enum import Enum
19
+ from typing import Dict, List, Optional, Tuple, Union
20
+
21
+
22
+ class RasPlanHdfError(Exception):
23
+ """HEC-RAS Plan HDF error class."""
24
+
25
+ pass
26
+
27
+
28
+ class XsSteadyOutputVar(Enum):
29
+ """Summary of steady cross section output variables."""
30
+
31
+ ENERGY_GRADE = "Energy Grade"
32
+ FLOW = "Flow"
33
+ WATER_SURFACE = "Water Surface"
34
+ ENCROACHMENT_STATION_LEFT = "Encroachment Station Left"
35
+ ENCROACHMENT_STATION_RIGHT = "Encroachment Station Right"
36
+ AREA_INEFFECTIVE_TOTAL = "Area including Ineffective Total"
37
+ VELOCITY_TOTAL = "Velocity Total"
38
+
39
+
40
+ XS_STEADY_OUTPUT_ADDITIONAL = [
41
+ XsSteadyOutputVar.ENCROACHMENT_STATION_LEFT,
42
+ XsSteadyOutputVar.ENCROACHMENT_STATION_RIGHT,
43
+ XsSteadyOutputVar.AREA_INEFFECTIVE_TOTAL,
44
+ XsSteadyOutputVar.VELOCITY_TOTAL,
45
+ ]
46
+
47
+
48
+ class SummaryOutputVar(Enum):
49
+ """Summary output variables."""
50
+
51
+ MAXIMUM_WATER_SURFACE = "Maximum Water Surface"
52
+ MINIMUM_WATER_SURFACE = "Minimum Water Surface"
53
+ MAXIMUM_FACE_VELOCITY = "Maximum Face Velocity"
54
+ MINIMUM_FACE_VELOCITY = "Minimum Face Velocity"
55
+ CELL_MAXIMUM_WATER_SURFACE_ERROR = "Cell Maximum Water Surface Error"
56
+ CELL_CUMULATIVE_ITERATION = "Cell Cumulative Iteration"
57
+ CELL_LAST_ITERATION = "Cell Last Iteration"
58
+
59
+
60
+ SUMMARY_OUTPUT_VARS_CELLS = [
61
+ SummaryOutputVar.MAXIMUM_WATER_SURFACE,
62
+ SummaryOutputVar.MINIMUM_WATER_SURFACE,
63
+ SummaryOutputVar.CELL_MAXIMUM_WATER_SURFACE_ERROR,
64
+ SummaryOutputVar.CELL_CUMULATIVE_ITERATION,
65
+ SummaryOutputVar.CELL_LAST_ITERATION,
66
+ ]
67
+
68
+ SUMMARY_OUTPUT_VARS_FACES = [
69
+ SummaryOutputVar.MAXIMUM_FACE_VELOCITY,
70
+ SummaryOutputVar.MINIMUM_FACE_VELOCITY,
71
+ ]
72
+
73
+
74
+ class TimeSeriesOutputVar(Enum):
75
+ """Time series output variables."""
76
+
77
+ # Default Outputs
78
+ WATER_SURFACE = "Water Surface"
79
+ FACE_VELOCITY = "Face Velocity"
80
+
81
+ # Optional Outputs
82
+ CELL_COURANT = "Cell Courant"
83
+ CELL_CUMULATIVE_PRECIPITATION_DEPTH = "Cell Cumulative Precipitation Depth"
84
+ CELL_DIVERGENCE_TERM = "Cell Divergence Term"
85
+ CELL_EDDY_VISCOSITY_X = "Cell Eddy Viscosity - Eddy Viscosity X"
86
+ CELL_EDDY_VISCOSITY_Y = "Cell Eddy Viscosity - Eddy Viscosity Y"
87
+ CELL_FLOW_BALANCE = "Cell Flow Balance"
88
+ CELL_HYDRAULIC_DEPTH = "Cell Hydraulic Depth"
89
+ CELL_INVERT_DEPTH = "Cell Invert Depth"
90
+ CELL_STORAGE_TERM = "Cell Storage Term"
91
+ CELL_VELOCITY_X = "Cell Velocity - Velocity X"
92
+ CELL_VELOCITY_Y = "Cell Velocity - Velocity Y"
93
+ CELL_VOLUME = "Cell Volume"
94
+ CELL_VOLUME_ERROR = "Cell Volume Error"
95
+ CELL_WATER_SOURCE_TERM = "Cell Water Source Term"
96
+ CELL_WATER_SURFACE_ERROR = "Cell Water Surface Error"
97
+
98
+ FACE_COURANT = "Face Courant"
99
+ FACE_CUMULATIVE_VOLUME = "Face Cumulative Volume"
100
+ FACE_EDDY_VISCOSITY = "Face Eddy Viscosity"
101
+ FACE_FLOW = "Face Flow"
102
+ FACE_FLOW_PERIOD_AVERAGE = "Face Flow Period Average"
103
+ FACE_FRICTION_TERM = "Face Friction Term"
104
+ FACE_PRESSURE_GRADIENT_TERM = "Face Pressure Gradient Term"
105
+ FACE_SHEAR_STRESS = "Face Shear Stress"
106
+ FACE_TANGENTIAL_VELOCITY = "Face Tangential Velocity"
107
+ FACE_WATER_SURFACE = "Face Water Surface"
108
+ FACE_WIND_TERM = "Face Wind Term"
109
+
110
+
111
+ TIME_SERIES_OUTPUT_VARS_CELLS = [
112
+ TimeSeriesOutputVar.WATER_SURFACE,
113
+ TimeSeriesOutputVar.CELL_COURANT,
114
+ TimeSeriesOutputVar.CELL_CUMULATIVE_PRECIPITATION_DEPTH,
115
+ TimeSeriesOutputVar.CELL_DIVERGENCE_TERM,
116
+ TimeSeriesOutputVar.CELL_EDDY_VISCOSITY_X,
117
+ TimeSeriesOutputVar.CELL_EDDY_VISCOSITY_Y,
118
+ TimeSeriesOutputVar.CELL_FLOW_BALANCE,
119
+ TimeSeriesOutputVar.CELL_HYDRAULIC_DEPTH,
120
+ TimeSeriesOutputVar.CELL_INVERT_DEPTH,
121
+ TimeSeriesOutputVar.CELL_STORAGE_TERM,
122
+ TimeSeriesOutputVar.CELL_VELOCITY_X,
123
+ TimeSeriesOutputVar.CELL_VELOCITY_Y,
124
+ TimeSeriesOutputVar.CELL_VOLUME,
125
+ TimeSeriesOutputVar.CELL_VOLUME_ERROR,
126
+ TimeSeriesOutputVar.CELL_WATER_SOURCE_TERM,
127
+ TimeSeriesOutputVar.CELL_WATER_SURFACE_ERROR,
128
+ ]
129
+
130
+ TIME_SERIES_OUTPUT_VARS_FACES = [
131
+ TimeSeriesOutputVar.FACE_COURANT,
132
+ TimeSeriesOutputVar.FACE_CUMULATIVE_VOLUME,
133
+ TimeSeriesOutputVar.FACE_EDDY_VISCOSITY,
134
+ TimeSeriesOutputVar.FACE_FLOW,
135
+ TimeSeriesOutputVar.FACE_FLOW_PERIOD_AVERAGE,
136
+ TimeSeriesOutputVar.FACE_FRICTION_TERM,
137
+ TimeSeriesOutputVar.FACE_PRESSURE_GRADIENT_TERM,
138
+ TimeSeriesOutputVar.FACE_SHEAR_STRESS,
139
+ TimeSeriesOutputVar.FACE_TANGENTIAL_VELOCITY,
140
+ TimeSeriesOutputVar.FACE_VELOCITY,
141
+ TimeSeriesOutputVar.FACE_WATER_SURFACE,
142
+ # TODO: investigate why "Face Wind Term" data gets written as a 1D array
143
+ # TimeSeriesOutputVar.FACE_WIND_TERM,
144
+ ]
145
+
146
+ TIME_SERIES_OUTPUT_VARS_DEFAULT = [
147
+ TimeSeriesOutputVar.WATER_SURFACE,
148
+ TimeSeriesOutputVar.FACE_VELOCITY,
149
+ ]
6
150
 
7
151
 
8
152
  class RasPlanHdf(RasGeomHdf):
@@ -14,6 +158,17 @@ class RasPlanHdf(RasGeomHdf):
14
158
  RESULTS_UNSTEADY_PATH = "Results/Unsteady"
15
159
  RESULTS_UNSTEADY_SUMMARY_PATH = f"{RESULTS_UNSTEADY_PATH}/Summary"
16
160
  VOLUME_ACCOUNTING_PATH = f"{RESULTS_UNSTEADY_PATH}/Volume Accounting"
161
+ BASE_OUTPUT_PATH = f"{RESULTS_UNSTEADY_PATH}/Output/Output Blocks/Base Output"
162
+ SUMMARY_OUTPUT_2D_FLOW_AREAS_PATH = (
163
+ f"{BASE_OUTPUT_PATH}/Summary Output/2D Flow Areas"
164
+ )
165
+ UNSTEADY_TIME_SERIES_PATH = f"{BASE_OUTPUT_PATH}/Unsteady Time Series"
166
+
167
+ RESULTS_STEADY_PATH = "Results/Steady"
168
+ BASE_STEADY_PATH = f"{RESULTS_STEADY_PATH}/Output/Output Blocks/Base Output"
169
+ STEADY_PROFILES_PATH = f"{BASE_STEADY_PATH}/Steady Profiles"
170
+ STEADY_XS_PATH = f"{STEADY_PROFILES_PATH}/Cross Sections"
171
+ STEADY_XS_ADDITIONAL_PATH = f"{STEADY_XS_PATH}/Additional Variables"
17
172
 
18
173
  def __init__(self, name: str, **kwargs):
19
174
  """Open a HEC-RAS Plan HDF file.
@@ -27,6 +182,745 @@ class RasPlanHdf(RasGeomHdf):
27
182
  """
28
183
  super().__init__(name, **kwargs)
29
184
 
185
+ def _simulation_start_time(self) -> datetime:
186
+ """Return the simulation start time from the plan file.
187
+
188
+ Returns
189
+ -------
190
+ datetime
191
+ The simulation start time.
192
+ """
193
+ plan_info = self.get_plan_info_attrs()
194
+ return plan_info["Simulation Start Time"]
195
+
196
+ def _2d_flow_area_names_and_counts(self) -> List[Tuple[str, int]]:
197
+ """
198
+ Return a list of 2D flow area names and cell counts.
199
+
200
+ Returns
201
+ -------
202
+ List[Tuple[str, int]]
203
+ A list of tuples, where each tuple contains a 2D flow area name
204
+ and the number of cells in that area.
205
+ """
206
+ d2_flow_areas = self[f"{self.FLOW_AREA_2D_PATH}/Attributes"][:]
207
+ return [
208
+ (d2_flow_area[0].decode("utf-8"), d2_flow_area[-1])
209
+ for d2_flow_area in d2_flow_areas
210
+ ]
211
+
212
+ def _mesh_summary_output_group(
213
+ self, mesh_name: str, output_var: SummaryOutputVar
214
+ ) -> h5py.Group:
215
+ """Return the HDF group for a 2D flow area summary output variable.
216
+
217
+ Parameters
218
+ ----------
219
+ mesh_name : str
220
+ The name of the 2D flow area mesh.
221
+ output_var : str
222
+ The name of the output variable.
223
+
224
+ Returns
225
+ -------
226
+ h5py.Group
227
+ The HDF group for the output variable.
228
+ """
229
+ output_path = (
230
+ f"{self.SUMMARY_OUTPUT_2D_FLOW_AREAS_PATH}/{mesh_name}/{output_var.value}"
231
+ )
232
+ output_group = self.get(output_path)
233
+ if output_group is None:
234
+ raise RasPlanHdfError(
235
+ f"Could not find HDF group at path '{output_path}'."
236
+ " Does the Plan HDF file contain 2D output data?"
237
+ )
238
+ return output_group
239
+
240
+ def _mesh_summary_output_min_max_values(
241
+ self, mesh_name: str, var: SummaryOutputVar
242
+ ) -> np.ndarray:
243
+ """Return values for a "Maximum"/"Minimum" summary output variable.
244
+
245
+ Parameters
246
+ ----------
247
+ mesh_name : str
248
+ The name of the 2D flow area mesh.
249
+ var : SummaryOutputVar
250
+ The summary output variable to retrieve.
251
+
252
+ Returns
253
+ -------
254
+ np.ndarray
255
+ An array of maximum water surface elevation values.
256
+ """
257
+ max_ws_group = self._mesh_summary_output_group(mesh_name, var)
258
+ max_ws_raw = max_ws_group[:]
259
+ max_ws_values = max_ws_raw[0]
260
+ return max_ws_values
261
+
262
+ def _summary_output_min_max_time_unit(self, dataset: h5py.Dataset) -> str:
263
+ """Return the time unit for "Maximum"/"Minimum" summary output datasets.
264
+
265
+ I.e., for summary output such as "Maximum Water Surface", "Minimum Water Surface", etc.
266
+
267
+ Should normally return the string: "days".
268
+
269
+ Parameters
270
+ ----------
271
+ mesh_name : str
272
+ The name of the 2D flow area mesh.
273
+
274
+ Returns
275
+ -------
276
+ str
277
+ The time unit for the maximum water surface elevation data.
278
+ """
279
+ if "Units per row" in dataset.attrs:
280
+ units = dataset.attrs["Units per row"]
281
+ else:
282
+ units = dataset.attrs["Units"]
283
+ # expect an array of size 2, with the first element being length or velocity units
284
+ # and the second element being time units (e.g., ["ft", "days"])
285
+ time_unit = units[1]
286
+ return time_unit.decode("utf-8")
287
+
288
+ def _mesh_summary_output_min_max_times(
289
+ self,
290
+ mesh_name: str,
291
+ var: SummaryOutputVar,
292
+ time_unit: str = "days",
293
+ round_to: str = "0.1 s",
294
+ ) -> np.ndarray[np.datetime64]:
295
+ """Return an array of times for min/max summary output data.
296
+
297
+ Parameters
298
+ ----------
299
+ mesh_name : str
300
+ The name of the 2D flow area mesh.
301
+ var : SummaryOutputVar
302
+ The summary output variable to retrieve.
303
+ time_unit : str, optional
304
+ The time unit for the maximum water surface elevation data.
305
+ Default: "days".
306
+ round_to : str, optional
307
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
308
+
309
+ Returns
310
+ -------
311
+ np.ndarray[np.datetime64]
312
+ An array of times for the maximum water surface elevation data.
313
+ """
314
+ start_time = self._simulation_start_time()
315
+ max_ws_group = self._mesh_summary_output_group(mesh_name, var)
316
+ time_unit = self._summary_output_min_max_time_unit(max_ws_group)
317
+ max_ws_raw = max_ws_group[:]
318
+ max_ws_times_raw = max_ws_raw[1]
319
+ # we get weirdly specific datetime values if we don't round to e.g., 0.1 seconds;
320
+ # otherwise datetimes don't align with the actual timestep values in the plan file
321
+ max_ws_times = ras_timesteps_to_datetimes(
322
+ max_ws_times_raw, start_time, time_unit=time_unit, round_to=round_to
323
+ )
324
+ return max_ws_times
325
+
326
+ def _mesh_summary_output_min_max(
327
+ self,
328
+ var: SummaryOutputVar,
329
+ value_col: str = "value",
330
+ time_col: str = "time",
331
+ round_to: str = "0.1 s",
332
+ ) -> DataFrame:
333
+ """Return the min/max values and times for a summary output variable.
334
+
335
+ Valid for:
336
+ - Maximum Water Surface
337
+ - Minimum Water Surface
338
+ - Maximum Face Velocity
339
+ - Minimum Face Velocity
340
+ - Cell Maximum Water Surface Error
341
+
342
+ Parameters
343
+ ----------
344
+ var : SummaryOutputVar
345
+ The summary output variable to retrieve.
346
+ round_to : str, optional
347
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
348
+
349
+ Returns
350
+ -------
351
+ DataFrame
352
+ A DataFrame with columns 'mesh_name', 'cell_id' or 'face_id', 'value', and 'time'.
353
+ """
354
+ dfs = []
355
+ for mesh_name, cell_count in self._2d_flow_area_names_and_counts():
356
+ values = self._mesh_summary_output_min_max_values(mesh_name, var)
357
+ times = self._mesh_summary_output_min_max_times(
358
+ mesh_name, var, round_to=round_to
359
+ )
360
+ if var in [
361
+ SummaryOutputVar.MAXIMUM_FACE_VELOCITY,
362
+ SummaryOutputVar.MINIMUM_FACE_VELOCITY,
363
+ ]:
364
+ geom_id_col = "face_id"
365
+ else:
366
+ geom_id_col = "cell_id"
367
+ # The 2D mesh output data contains values for more cells than are actually
368
+ # in the mesh. The the true number of cells for a mesh is found in the table:
369
+ # "/Geometry/2D Flow Areas/Attributes". The number of cells in the 2D output
370
+ # data instead matches the number of cells in the "Cells Center Coordinate"
371
+ # array, which contains extra points along the perimeter of the mesh. These
372
+ # extra points are appended to the end of the mesh data and contain bogus
373
+ # output values (e.g., 0.0, NaN). We need to filter out these bogus values.
374
+ values = values[:cell_count]
375
+ times = times[:cell_count]
376
+ df = DataFrame(
377
+ {
378
+ "mesh_name": [mesh_name] * len(values),
379
+ geom_id_col: range(len(values)),
380
+ value_col: values,
381
+ time_col: times,
382
+ }
383
+ )
384
+ dfs.append(df)
385
+ df = pd.concat(dfs, ignore_index=True)
386
+ return df
387
+
388
+ def _mesh_summary_output_basic(
389
+ self, var: SummaryOutputVar, value_col: str = "value"
390
+ ) -> DataFrame:
391
+ """Return values and times for a summary output variable.
392
+
393
+ Valid for:
394
+ - Cell Cumulative Iteration (i.e. Cumulative Max Iterations)
395
+ - Cell Last Iteration
396
+
397
+ Parameters
398
+ ----------
399
+ var : SummaryOutputVar
400
+ The summary output variable to retrieve.
401
+
402
+ Returns
403
+ -------
404
+ DataFrame
405
+ A DataFrame with columns 'mesh_name', 'cell_id' or 'face_id', 'value', and 'time'.
406
+ """
407
+ dfs = []
408
+ for mesh_name, cell_count in self._2d_flow_area_names_and_counts():
409
+ group = self._mesh_summary_output_group(mesh_name, var)
410
+ values = group[:][:cell_count]
411
+ df = DataFrame(
412
+ {
413
+ "mesh_name": [mesh_name] * len(values),
414
+ "cell_id": range(len(values)),
415
+ value_col: values,
416
+ }
417
+ )
418
+ dfs.append(df)
419
+ df = pd.concat(dfs, ignore_index=True)
420
+ return df
421
+
422
+ def mesh_max_iter(self) -> DataFrame:
423
+ """Return the number of times each cell in the mesh reached the max number of iterations.
424
+
425
+ Returns
426
+ -------
427
+ DataFrame
428
+ A DataFrame with columns 'mesh_name', 'cell_id', and 'max_iterations'.
429
+ """
430
+ df = self._mesh_summary_output_basic(
431
+ SummaryOutputVar.CELL_CUMULATIVE_ITERATION, value_col="max_iter"
432
+ )
433
+ return df
434
+
435
+ def mesh_last_iter(self) -> DataFrame:
436
+ """Return the number of times each cell in the mesh was the last cell to converge.
437
+
438
+ Returns
439
+ -------
440
+ DataFrame
441
+ A DataFrame with columns 'mesh_name', 'cell_id', and 'last_iter'.
442
+ """
443
+ df = self._mesh_summary_output_basic(
444
+ SummaryOutputVar.CELL_LAST_ITERATION, value_col="last_iter"
445
+ )
446
+ return df
447
+
448
+ def mesh_max_ws(self, round_to: str = "0.1 s") -> DataFrame:
449
+ """Return the max water surface elevation for each cell in the mesh.
450
+
451
+ Includes the corresponding time of max water surface elevation.
452
+
453
+ Parameters
454
+ ----------
455
+ round_to : str, optional
456
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
457
+ See Pandas documentation for valid time units:
458
+ https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
459
+
460
+ Returns
461
+ -------
462
+ DataFrame
463
+ A DataFrame with columns 'mesh_name', 'cell_id', 'max_ws', and 'max_ws_time'.
464
+ """
465
+ df = self._mesh_summary_output_min_max(
466
+ SummaryOutputVar.MAXIMUM_WATER_SURFACE,
467
+ value_col="max_ws",
468
+ time_col="max_ws_time",
469
+ round_to=round_to,
470
+ )
471
+ return df
472
+
473
+ def mesh_min_ws(self, round_to: str = "0.1 s") -> DataFrame:
474
+ """Return the min water surface elevation for each cell in the mesh.
475
+
476
+ Includes the corresponding time of min water surface elevation.
477
+
478
+ Parameters
479
+ ----------
480
+ round_to : str, optional
481
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
482
+ See Pandas documentation for valid time units:
483
+ https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
484
+
485
+ Returns
486
+ -------
487
+ DataFrame
488
+ A DataFrame with columns 'mesh_name', 'cell_id', 'min_ws', and 'min_ws_time'.
489
+ """
490
+ df = self._mesh_summary_output_min_max(
491
+ SummaryOutputVar.MINIMUM_WATER_SURFACE,
492
+ value_col="min_ws",
493
+ time_col="min_ws_time",
494
+ round_to=round_to,
495
+ )
496
+ return df
497
+
498
+ def mesh_max_face_v(self, round_to: str = "0.1 s") -> DataFrame:
499
+ """Return the max face velocity for each face in the mesh.
500
+
501
+ Includes the corresponding time of max face velocity.
502
+
503
+ Parameters
504
+ ----------
505
+ round_to : str, optional
506
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
507
+ See Pandas documentation for valid time units:
508
+ https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
509
+
510
+ Returns
511
+ -------
512
+ DataFrame
513
+ A DataFrame with columns 'mesh_name', 'face_id', 'max_v', and 'max_v_time'.
514
+ """
515
+ df = self._mesh_summary_output_min_max(
516
+ SummaryOutputVar.MAXIMUM_FACE_VELOCITY,
517
+ value_col="max_v",
518
+ time_col="max_v_time",
519
+ round_to=round_to,
520
+ )
521
+ return df
522
+
523
+ def mesh_min_face_v(self, round_to: str = "0.1 s") -> DataFrame:
524
+ """Return the min face velocity for each face in the mesh.
525
+
526
+ Includes the corresponding time of min face velocity.
527
+
528
+ Parameters
529
+ ----------
530
+ round_to : str, optional
531
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
532
+ See Pandas documentation for valid time units:
533
+ https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
534
+
535
+ Returns
536
+ -------
537
+ DataFrame
538
+ A DataFrame with columns 'mesh_name', 'face_id', 'min_v', and 'min_v_time'.
539
+ """
540
+ df = self._mesh_summary_output_min_max(
541
+ SummaryOutputVar.MINIMUM_FACE_VELOCITY,
542
+ value_col="min_v",
543
+ time_col="min_v_time",
544
+ round_to=round_to,
545
+ )
546
+ return df
547
+
548
+ def mesh_max_ws_err(self, round_to: str = "0.1 s") -> DataFrame:
549
+ """Return the max water surface error for each cell in the mesh.
550
+
551
+ Includes the corresponding time of max water surface error.
552
+
553
+ Parameters
554
+ ----------
555
+ round_to : str, optional
556
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
557
+ See Pandas documentation for valid time units:
558
+ https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html
559
+
560
+ Returns
561
+ -------
562
+ DataFrame
563
+ A DataFrame with columns 'mesh_name', 'cell_id', 'max_ws_err', and 'max_ws_err_time'.
564
+ """
565
+ df = self._mesh_summary_output_min_max(
566
+ SummaryOutputVar.CELL_MAXIMUM_WATER_SURFACE_ERROR,
567
+ value_col="max_ws_err",
568
+ time_col="max_ws_err_time",
569
+ round_to=round_to,
570
+ )
571
+ return df
572
+
573
+ def mesh_summary_output(
574
+ self, var: SummaryOutputVar, round_to: str = "0.1 s"
575
+ ) -> DataFrame:
576
+ """Return the summary output data for a given variable.
577
+
578
+ Parameters
579
+ ----------
580
+ var : SummaryOutputVar
581
+ The summary output variable to retrieve.
582
+
583
+ Returns
584
+ -------
585
+ DataFrame
586
+ A DataFrame with columns 'mesh_name', 'cell_id' or 'face_id', a value column, and a time column.
587
+ """
588
+ methods_with_times = {
589
+ SummaryOutputVar.MAXIMUM_WATER_SURFACE: self.mesh_max_ws,
590
+ SummaryOutputVar.MINIMUM_WATER_SURFACE: self.mesh_min_ws,
591
+ SummaryOutputVar.MAXIMUM_FACE_VELOCITY: self.mesh_max_face_v,
592
+ SummaryOutputVar.MINIMUM_FACE_VELOCITY: self.mesh_min_face_v,
593
+ SummaryOutputVar.CELL_MAXIMUM_WATER_SURFACE_ERROR: self.mesh_max_ws_err,
594
+ }
595
+ other_methods = {
596
+ SummaryOutputVar.CELL_CUMULATIVE_ITERATION: self.mesh_max_iter,
597
+ SummaryOutputVar.CELL_LAST_ITERATION: self.mesh_last_iter,
598
+ }
599
+ if var in methods_with_times:
600
+ df = methods_with_times[var](round_to=round_to)
601
+ else:
602
+ df = other_methods[var]()
603
+ return df
604
+
605
+ def _summary_output_vars(
606
+ self, cells_or_faces: Optional[str] = None
607
+ ) -> List[SummaryOutputVar]:
608
+ """Return a list of available summary output variables from the Plan HDF file.
609
+
610
+ Returns
611
+ -------
612
+ List[SummaryOutputVar]
613
+ A list of summary output variables.
614
+ """
615
+ mesh_names_counts = self._2d_flow_area_names_and_counts()
616
+ mesh_names = [mesh_name for mesh_name, _ in mesh_names_counts]
617
+ vars = set()
618
+ for mesh_name in mesh_names:
619
+ path = f"{self.SUMMARY_OUTPUT_2D_FLOW_AREAS_PATH}/{mesh_name}"
620
+ datasets = self[path].keys()
621
+ for dataset in datasets:
622
+ try:
623
+ var = SummaryOutputVar(dataset)
624
+ except ValueError:
625
+ continue
626
+ vars.add(var)
627
+ if cells_or_faces == "cells":
628
+ vars = vars.intersection(SUMMARY_OUTPUT_VARS_CELLS)
629
+ elif cells_or_faces == "faces":
630
+ vars = vars.intersection(SUMMARY_OUTPUT_VARS_FACES)
631
+ return sorted(list(vars), key=lambda x: x.value)
632
+
633
+ def _mesh_summary_outputs_gdf(
634
+ self,
635
+ geom_func: str,
636
+ cells_or_faces: str = "cells",
637
+ include_output: Union[bool, List[SummaryOutputVar]] = True,
638
+ round_to: str = "0.1 s",
639
+ datetime_to_str: bool = False,
640
+ ) -> GeoDataFrame:
641
+ """Return a GeoDataFrame with mesh geometry and summary output data.
642
+
643
+ Parameters
644
+ ----------
645
+ geom_func : str
646
+ The method name to call to get the mesh geometry.
647
+ cells_or_faces : str, optional
648
+ The type of geometry to include in the GeoDataFrame.
649
+ Must be either "cells" or "faces". (default: "cells")
650
+ include_output : Union[bool, List[SummaryOutputVar]], optional
651
+ If True, include all available summary output data in the GeoDataFrame.
652
+ If a list of SummaryOutputVar values, include only the specified summary output data.
653
+ If False, do not include any summary output data.
654
+ (default: True)
655
+ round_to : str, optional
656
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
657
+ datetime_to_str : bool, optional
658
+ If True, convert datetime columns to strings. (default: False)
659
+ """
660
+ gdf = getattr(super(), geom_func)()
661
+ if include_output is False:
662
+ return gdf
663
+ if include_output is True:
664
+ summary_output_vars = self._summary_output_vars(
665
+ cells_or_faces=cells_or_faces
666
+ )
667
+ elif isinstance(include_output, list):
668
+ summary_output_vars = []
669
+ for var in include_output:
670
+ if not isinstance(var, SummaryOutputVar):
671
+ var = SummaryOutputVar(var)
672
+ summary_output_vars.append(var)
673
+ else:
674
+ raise ValueError(
675
+ "include_output must be a boolean or a list of SummaryOutputVar values."
676
+ )
677
+ if cells_or_faces == "cells":
678
+ geom_id_col = "cell_id"
679
+ elif cells_or_faces == "faces":
680
+ geom_id_col = "face_id"
681
+ else:
682
+ raise ValueError('cells_or_faces must be either "cells" or "faces".')
683
+ for var in summary_output_vars:
684
+ df = self.mesh_summary_output(var, round_to=round_to)
685
+ gdf = gdf.merge(df, on=["mesh_name", geom_id_col], how="left")
686
+ if datetime_to_str:
687
+ gdf = df_datetimes_to_str(gdf)
688
+ return gdf
689
+
690
+ def mesh_cell_points(
691
+ self,
692
+ include_output: Union[bool, List[SummaryOutputVar], List[str]] = True,
693
+ round_to: str = "0.1 s",
694
+ datetime_to_str: bool = False,
695
+ ) -> GeoDataFrame:
696
+ """Return the cell points for each cell in the mesh, including summary output.
697
+
698
+ Parameters
699
+ ----------
700
+ include_output : Union[bool, List[SummaryOutputVar], List[str]], optional
701
+ If True, include all available summary output data in the GeoDataFrame.
702
+ If a list of SummaryOutputVar values, include only the specified summary output data.
703
+ If a list of strings, include only the specified summary output data by name.
704
+ If False, do not include any summary output data.
705
+ (default: True)
706
+ round_to : str, optional
707
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
708
+ datetime_to_str : bool, optional
709
+ If True, convert datetime columns to strings. (default: False)
710
+
711
+ Returns
712
+ -------
713
+ GeoDataFrame
714
+ A GeoDataFrame with columns 'mesh_name', 'cell_id', 'geometry', and columns for each
715
+ summary output variable.
716
+ """
717
+ return self._mesh_summary_outputs_gdf(
718
+ "mesh_cell_points",
719
+ "cells",
720
+ include_output=include_output,
721
+ round_to=round_to,
722
+ datetime_to_str=datetime_to_str,
723
+ )
724
+
725
+ def mesh_cell_polygons(
726
+ self,
727
+ include_output: Union[bool, List[SummaryOutputVar], List[str]] = True,
728
+ round_to: str = "0.1 s",
729
+ datetime_to_str: bool = False,
730
+ ) -> GeoDataFrame:
731
+ """Return the cell polygons for each cell in the mesh, including output.
732
+
733
+ Parameters
734
+ ----------
735
+ include_output : Union[bool, List[SummaryOutputVar], List[str]], optional
736
+ If True, include all available summary output data in the GeoDataFrame.
737
+ If a list of SummaryOutputVar values, include only the specified summary output data.
738
+ If a list of strings, include only the specified summary output data by name.
739
+ If False, do not include any summary output data.
740
+ (default: True)
741
+ round_to : str, optional
742
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
743
+ datetime_to_str : bool, optional
744
+ If True, convert datetime columns to strings. (default: False)
745
+
746
+ Returns
747
+ -------
748
+ GeoDataFrame
749
+ A GeoDataFrame with columns 'mesh_name', 'cell_id', 'geometry', and columns for each
750
+ summary output variable.
751
+ """
752
+ return self._mesh_summary_outputs_gdf(
753
+ "mesh_cell_polygons",
754
+ "cells",
755
+ include_output=include_output,
756
+ round_to=round_to,
757
+ datetime_to_str=datetime_to_str,
758
+ )
759
+
760
+ def mesh_cell_faces(
761
+ self,
762
+ include_output: Union[bool, List[SummaryOutputVar], List[str]] = True,
763
+ round_to: str = "0.1 s",
764
+ datetime_to_str: bool = False,
765
+ ) -> GeoDataFrame:
766
+ """Return the cell faces for each cell in the mesh, including output.
767
+
768
+ Parameters
769
+ ----------
770
+ include_output : Union[bool, List[SummaryOutputVar], List[str]], optional
771
+ If True, include all available summary output data in the GeoDataFrame.
772
+ If a list of SummaryOutputVar values, include only the specified summary output data.
773
+ If a list of strings, include only the specified summary output data by name.
774
+ If False, do not include any summary output data.
775
+ (default: True)
776
+ round_to : str, optional
777
+ The time unit to round the datetimes to. Default: "0.1 s" (seconds).
778
+ datetime_to_str : bool, optional
779
+ If True, convert datetime columns to strings. (default: False)
780
+
781
+ Returns
782
+ -------
783
+ GeoDataFrame
784
+ A GeoDataFrame with columns 'mesh_name', 'cell_id', 'geometry', and columns for each
785
+ summary output variable.
786
+ """
787
+ return self._mesh_summary_outputs_gdf(
788
+ "mesh_cell_faces",
789
+ "faces",
790
+ include_output=include_output,
791
+ round_to=round_to,
792
+ datetime_to_str=datetime_to_str,
793
+ )
794
+
795
+ def unsteady_datetimes(self) -> List[datetime]:
796
+ """Return the unsteady timeseries datetimes from the plan file.
797
+
798
+ Returns
799
+ -------
800
+ List[datetime]
801
+ A list of datetimes for the unsteady timeseries data.
802
+ """
803
+ group_path = f"{self.UNSTEADY_TIME_SERIES_PATH}/Time Date Stamp (ms)"
804
+ raw_datetimes = self[group_path][:]
805
+ dt = [parse_ras_datetime_ms(x.decode("utf-8")) for x in raw_datetimes]
806
+ return dt
807
+
808
+ def _mesh_timeseries_output_values_units(
809
+ self,
810
+ mesh_name: str,
811
+ var: TimeSeriesOutputVar,
812
+ ) -> Tuple[np.ndarray, str]:
813
+ path = f"{self.UNSTEADY_TIME_SERIES_PATH}/2D Flow Areas/{mesh_name}/{var.value}"
814
+ group = self.get(path)
815
+ try:
816
+ import dask.array as da
817
+
818
+ # TODO: user-specified chunks?
819
+ values = da.from_array(group, chunks=group.chunks)
820
+ except ImportError:
821
+ values = group[:]
822
+ units = group.attrs.get("Units")
823
+ if units is not None:
824
+ units = units.decode("utf-8")
825
+ return values, units
826
+
827
+ def mesh_timeseries_output(
828
+ self,
829
+ mesh_name: str,
830
+ var: Union[str, TimeSeriesOutputVar],
831
+ ) -> xr.DataArray:
832
+ """Return the time series output data for a given variable.
833
+
834
+ Parameters
835
+ ----------
836
+ mesh_name : str
837
+ The name of the 2D flow area mesh.
838
+ var : TimeSeriesOutputVar
839
+ The time series output variable to retrieve.
840
+
841
+ Returns
842
+ -------
843
+ xr.DataArray
844
+ An xarray DataArray with dimensions 'time' and 'cell_id'.
845
+ """
846
+ times = self.unsteady_datetimes()
847
+ mesh_names_counts = {
848
+ name: count for name, count in self._2d_flow_area_names_and_counts()
849
+ }
850
+ if mesh_name not in mesh_names_counts:
851
+ raise ValueError(f"Mesh '{mesh_name}' not found in the Plan HDF file.")
852
+ if isinstance(var, str):
853
+ var = TimeSeriesOutputVar(var)
854
+ values, units = self._mesh_timeseries_output_values_units(mesh_name, var)
855
+ if var in TIME_SERIES_OUTPUT_VARS_CELLS:
856
+ cell_count = mesh_names_counts[mesh_name]
857
+ values = values[:, :cell_count]
858
+ id_coord = "cell_id"
859
+ elif var in TIME_SERIES_OUTPUT_VARS_FACES:
860
+ id_coord = "face_id"
861
+ else:
862
+ raise ValueError(f"Invalid time series output variable: {var.value}")
863
+ da = xr.DataArray(
864
+ values,
865
+ name=var.value,
866
+ dims=["time", id_coord],
867
+ coords={
868
+ "time": times,
869
+ id_coord: range(values.shape[1]),
870
+ },
871
+ attrs={
872
+ "mesh_name": mesh_name,
873
+ "variable": var.value,
874
+ "units": units,
875
+ },
876
+ )
877
+ return da
878
+
879
+ def _mesh_timeseries_outputs(
880
+ self, mesh_name: str, vars: List[TimeSeriesOutputVar]
881
+ ) -> xr.Dataset:
882
+ datasets = {}
883
+ for var in vars:
884
+ var_path = f"{self.UNSTEADY_TIME_SERIES_PATH}/2D Flow Areas/{mesh_name}/{var.value}"
885
+ if self.get(var_path) is None:
886
+ continue
887
+ da = self.mesh_timeseries_output(mesh_name, var)
888
+ datasets[var.value] = da
889
+ ds = xr.Dataset(datasets, attrs={"mesh_name": mesh_name})
890
+ return ds
891
+
892
+ def mesh_timeseries_output_cells(self, mesh_name: str) -> xr.Dataset:
893
+ """Return the time series output data for cells in a 2D flow area mesh.
894
+
895
+ Parameters
896
+ ----------
897
+ mesh_name : str
898
+ The name of the 2D flow area mesh.
899
+
900
+ Returns
901
+ -------
902
+ xr.Dataset
903
+ An xarray Dataset with DataArrays for each time series output variable.
904
+ """
905
+ ds = self._mesh_timeseries_outputs(mesh_name, TIME_SERIES_OUTPUT_VARS_CELLS)
906
+ return ds
907
+
908
+ def mesh_timeseries_output_faces(self, mesh_name: str) -> xr.Dataset:
909
+ """Return the time series output data for faces in a 2D flow area mesh.
910
+
911
+ Parameters
912
+ ----------
913
+ mesh_name : str
914
+ The name of the 2D flow area mesh.
915
+
916
+ Returns
917
+ -------
918
+ xr.Dataset
919
+ An xarray Dataset with DataArrays for each time series output variable.
920
+ """
921
+ ds = self._mesh_timeseries_outputs(mesh_name, TIME_SERIES_OUTPUT_VARS_FACES)
922
+ return ds
923
+
30
924
  def get_plan_info_attrs(self) -> Dict:
31
925
  """Return plan information attributes from a HEC-RAS HDF plan file.
32
926
 
@@ -89,3 +983,127 @@ class RasPlanHdf(RasGeomHdf):
89
983
 
90
984
  def enroachment_points(self) -> GeoDataFrame: # noqa: D102
91
985
  raise NotImplementedError
986
+
987
+ def steady_flow_names(self) -> list:
988
+ """Return the profile information for each steady flow event.
989
+
990
+ Returns
991
+ -------
992
+ DataFrame
993
+ A Dataframe containing the profile names for each event
994
+ """
995
+ if self.STEADY_PROFILES_PATH not in self:
996
+ return pd.DataFrame()
997
+
998
+ profile_data = self[self.STEADY_PROFILES_PATH]
999
+ profile_attrs = profile_data["Profile Names"][()]
1000
+
1001
+ return [x.decode("utf-8") for x in profile_attrs]
1002
+
1003
+ def steady_profile_xs_output(
1004
+ self, var: XsSteadyOutputVar, round_to: int = 2
1005
+ ) -> DataFrame:
1006
+ """Create a Dataframe from steady cross section results based on path.
1007
+
1008
+ Parameters
1009
+ ----------
1010
+ var : XsSteadyOutputVar
1011
+ The name of the table in the steady results that information is to be retireved from.
1012
+
1013
+ round_to : int, optional
1014
+ Number of decimal places to round output data to.
1015
+
1016
+ Returns
1017
+ -------
1018
+ Dataframe with desired hdf data.
1019
+ """
1020
+ if var in XS_STEADY_OUTPUT_ADDITIONAL:
1021
+ path = f"{self.STEADY_XS_ADDITIONAL_PATH}/{var.value}"
1022
+ else:
1023
+ path = f"{self.STEADY_XS_PATH}/{var.value}"
1024
+ if path not in self:
1025
+ return DataFrame()
1026
+
1027
+ profiles = self.steady_flow_names()
1028
+
1029
+ steady_data = self[path]
1030
+ df = DataFrame(steady_data, index=profiles)
1031
+ df_t = df.T.copy()
1032
+ for p in profiles:
1033
+ df_t[p] = df_t[p].apply(lambda x: round(x, round_to))
1034
+
1035
+ return df_t
1036
+
1037
+ def cross_sections_wsel(self) -> DataFrame:
1038
+ """Return the water surface elevation information for each 1D Cross Section.
1039
+
1040
+ Returns
1041
+ -------
1042
+ DataFrame
1043
+ A Dataframe containing the water surface elevations for each cross section and event
1044
+ """
1045
+ return self.steady_profile_xs_output(XsSteadyOutputVar.WATER_SURFACE)
1046
+
1047
+ def cross_sections_flow(self) -> DataFrame:
1048
+ """Return the Flow information for each 1D Cross Section.
1049
+
1050
+ Returns
1051
+ -------
1052
+ DataFrame
1053
+ A Dataframe containing the flow for each cross section and event
1054
+ """
1055
+ return self.steady_profile_xs_output(XsSteadyOutputVar.FLOW)
1056
+
1057
+ def cross_sections_energy_grade(self) -> DataFrame:
1058
+ """Return the energy grade information for each 1D Cross Section.
1059
+
1060
+ Returns
1061
+ -------
1062
+ DataFrame
1063
+ A Dataframe containing the water surface elevations for each cross section and event
1064
+ """
1065
+ return self.steady_profile_xs_output(XsSteadyOutputVar.ENERGY_GRADE)
1066
+
1067
+ def cross_sections_additional_enc_station_left(self) -> DataFrame:
1068
+ """Return the left side encroachment information for a floodway plan hdf.
1069
+
1070
+ Returns
1071
+ -------
1072
+ DataFrame
1073
+ A DataFrame containing the cross sections left side encroachment stations
1074
+ """
1075
+ return self.steady_profile_xs_output(
1076
+ XsSteadyOutputVar.ENCROACHMENT_STATION_LEFT
1077
+ )
1078
+
1079
+ def cross_sections_additional_enc_station_right(self) -> DataFrame:
1080
+ """Return the right side encroachment information for a floodway plan hdf.
1081
+
1082
+ Returns
1083
+ -------
1084
+ DataFrame
1085
+ A DataFrame containing the cross sections right side encroachment stations
1086
+ """
1087
+ return self.steady_profile_xs_output(
1088
+ XsSteadyOutputVar.ENCROACHMENT_STATION_RIGHT
1089
+ )
1090
+
1091
+ def cross_sections_additional_area_total(self) -> DataFrame:
1092
+ """Return the 1D cross section area for each profile.
1093
+
1094
+ Returns
1095
+ -------
1096
+ DataFrame
1097
+ A DataFrame containing the wet area inside the cross sections
1098
+ """
1099
+ return self.steady_profile_xs_output(XsSteadyOutputVar.AREA_INEFFECTIVE_TOTAL)
1100
+
1101
+ def cross_sections_additional_velocity_total(self) -> DataFrame:
1102
+ """Return the 1D cross section velocity for each profile.
1103
+
1104
+ Returns
1105
+ -------
1106
+ DataFrame
1107
+ A DataFrame containing the velocity inside the cross sections
1108
+ """
1109
+ return self.steady_profile_xs_output(XsSteadyOutputVar.VELOCITY_TOTAL)