Skip to content

Commit af72a1a

Browse files
authored
Merge pull request #53 from fema-ffrd/feature/mesh-timeseries-output
Mesh Timeseries Output
2 parents 8d60825 + 3f4fa66 commit af72a1a

10 files changed

+494
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ classifiers = [
1313
"Programming Language :: Python :: 3.12",
1414
]
1515
version = "0.3.0"
16-
dependencies = ["h5py", "geopandas", "pyarrow"]
16+
dependencies = ["h5py", "geopandas", "pyarrow", "xarray"]
1717

1818
[project.optional-dependencies]
1919
dev = ["pre-commit", "ruff", "pytest", "pytest-cov"]

src/rashdf/plan.py

Lines changed: 218 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
"""HEC-RAS Plan HDF class."""
22

33
from .geom import RasGeomHdf
4-
from .utils import df_datetimes_to_str, ras_timesteps_to_datetimes
4+
from .utils import (
5+
df_datetimes_to_str,
6+
ras_timesteps_to_datetimes,
7+
parse_ras_datetime_ms,
8+
)
59

610
from geopandas import GeoDataFrame
711
import h5py
812
import numpy as np
913
from pandas import DataFrame
1014
import pandas as pd
15+
import xarray as xr
1116

1217
from datetime import datetime
1318
from enum import Enum
@@ -46,6 +51,84 @@ class SummaryOutputVar(Enum):
4651
]
4752

4853

54+
class TimeSeriesOutputVar(Enum):
55+
"""Time series output variables."""
56+
57+
# Default Outputs
58+
WATER_SURFACE = "Water Surface"
59+
FACE_VELOCITY = "Face Velocity"
60+
61+
# Optional Outputs
62+
CELL_COURANT = "Cell Courant"
63+
CELL_CUMULATIVE_PRECIPITATION_DEPTH = "Cell Cumulative Precipitation Depth"
64+
CELL_DIVERGENCE_TERM = "Cell Divergence Term"
65+
CELL_EDDY_VISCOSITY_X = "Cell Eddy Viscosity - Eddy Viscosity X"
66+
CELL_EDDY_VISCOSITY_Y = "Cell Eddy Viscosity - Eddy Viscosity Y"
67+
CELL_FLOW_BALANCE = "Cell Flow Balance"
68+
CELL_HYDRAULIC_DEPTH = "Cell Hydraulic Depth"
69+
CELL_INVERT_DEPTH = "Cell Invert Depth"
70+
CELL_STORAGE_TERM = "Cell Storage Term"
71+
CELL_VELOCITY_X = "Cell Velocity - Velocity X"
72+
CELL_VELOCITY_Y = "Cell Velocity - Velocity Y"
73+
CELL_VOLUME = "Cell Volume"
74+
CELL_VOLUME_ERROR = "Cell Volume Error"
75+
CELL_WATER_SOURCE_TERM = "Cell Water Source Term"
76+
CELL_WATER_SURFACE_ERROR = "Cell Water Surface Error"
77+
78+
FACE_COURANT = "Face Courant"
79+
FACE_CUMULATIVE_VOLUME = "Face Cumulative Volume"
80+
FACE_EDDY_VISCOSITY = "Face Eddy Viscosity"
81+
FACE_FLOW = "Face Flow"
82+
FACE_FLOW_PERIOD_AVERAGE = "Face Flow Period Average"
83+
FACE_FRICTION_TERM = "Face Friction Term"
84+
FACE_PRESSURE_GRADIENT_TERM = "Face Pressure Gradient Term"
85+
FACE_SHEAR_STRESS = "Face Shear Stress"
86+
FACE_TANGENTIAL_VELOCITY = "Face Tangential Velocity"
87+
FACE_WATER_SURFACE = "Face Water Surface"
88+
FACE_WIND_TERM = "Face Wind Term"
89+
90+
91+
TIME_SERIES_OUTPUT_VARS_CELLS = [
92+
TimeSeriesOutputVar.WATER_SURFACE,
93+
TimeSeriesOutputVar.CELL_COURANT,
94+
TimeSeriesOutputVar.CELL_CUMULATIVE_PRECIPITATION_DEPTH,
95+
TimeSeriesOutputVar.CELL_DIVERGENCE_TERM,
96+
TimeSeriesOutputVar.CELL_EDDY_VISCOSITY_X,
97+
TimeSeriesOutputVar.CELL_EDDY_VISCOSITY_Y,
98+
TimeSeriesOutputVar.CELL_FLOW_BALANCE,
99+
TimeSeriesOutputVar.CELL_HYDRAULIC_DEPTH,
100+
TimeSeriesOutputVar.CELL_INVERT_DEPTH,
101+
TimeSeriesOutputVar.CELL_STORAGE_TERM,
102+
TimeSeriesOutputVar.CELL_VELOCITY_X,
103+
TimeSeriesOutputVar.CELL_VELOCITY_Y,
104+
TimeSeriesOutputVar.CELL_VOLUME,
105+
TimeSeriesOutputVar.CELL_VOLUME_ERROR,
106+
TimeSeriesOutputVar.CELL_WATER_SOURCE_TERM,
107+
TimeSeriesOutputVar.CELL_WATER_SURFACE_ERROR,
108+
]
109+
110+
TIME_SERIES_OUTPUT_VARS_FACES = [
111+
TimeSeriesOutputVar.FACE_COURANT,
112+
TimeSeriesOutputVar.FACE_CUMULATIVE_VOLUME,
113+
TimeSeriesOutputVar.FACE_EDDY_VISCOSITY,
114+
TimeSeriesOutputVar.FACE_FLOW,
115+
TimeSeriesOutputVar.FACE_FLOW_PERIOD_AVERAGE,
116+
TimeSeriesOutputVar.FACE_FRICTION_TERM,
117+
TimeSeriesOutputVar.FACE_PRESSURE_GRADIENT_TERM,
118+
TimeSeriesOutputVar.FACE_SHEAR_STRESS,
119+
TimeSeriesOutputVar.FACE_TANGENTIAL_VELOCITY,
120+
TimeSeriesOutputVar.FACE_VELOCITY,
121+
TimeSeriesOutputVar.FACE_WATER_SURFACE,
122+
# TODO: investigate why "Face Wind Term" data gets written as a 1D array
123+
# TimeSeriesOutputVar.FACE_WIND_TERM,
124+
]
125+
126+
TIME_SERIES_OUTPUT_VARS_DEFAULT = [
127+
TimeSeriesOutputVar.WATER_SURFACE,
128+
TimeSeriesOutputVar.FACE_VELOCITY,
129+
]
130+
131+
49132
class RasPlanHdf(RasGeomHdf):
50133
"""HEC-RAS Plan HDF class."""
51134

@@ -55,7 +138,11 @@ class RasPlanHdf(RasGeomHdf):
55138
RESULTS_UNSTEADY_PATH = "Results/Unsteady"
56139
RESULTS_UNSTEADY_SUMMARY_PATH = f"{RESULTS_UNSTEADY_PATH}/Summary"
57140
VOLUME_ACCOUNTING_PATH = f"{RESULTS_UNSTEADY_PATH}/Volume Accounting"
58-
SUMMARY_OUTPUT_2D_FLOW_AREAS_PATH = f"{RESULTS_UNSTEADY_PATH}/Output/Output Blocks/Base Output/Summary Output/2D Flow Areas"
141+
BASE_OUTPUT_PATH = f"{RESULTS_UNSTEADY_PATH}/Output/Output Blocks/Base Output"
142+
SUMMARY_OUTPUT_2D_FLOW_AREAS_PATH = (
143+
f"{BASE_OUTPUT_PATH}/Summary Output/2D Flow Areas"
144+
)
145+
UNSTEADY_TIME_SERIES_PATH = f"{BASE_OUTPUT_PATH}/Unsteady Time Series"
59146

60147
def __init__(self, name: str, **kwargs):
61148
"""Open a HEC-RAS Plan HDF file.
@@ -679,6 +766,135 @@ def mesh_cell_faces(
679766
datetime_to_str=datetime_to_str,
680767
)
681768

769+
def unsteady_datetimes(self) -> List[datetime]:
770+
"""Return the unsteady timeseries datetimes from the plan file.
771+
772+
Returns
773+
-------
774+
List[datetime]
775+
A list of datetimes for the unsteady timeseries data.
776+
"""
777+
group_path = f"{self.UNSTEADY_TIME_SERIES_PATH}/Time Date Stamp (ms)"
778+
raw_datetimes = self[group_path][:]
779+
dt = [parse_ras_datetime_ms(x.decode("utf-8")) for x in raw_datetimes]
780+
return dt
781+
782+
def _mesh_timeseries_output_values_units(
783+
self,
784+
mesh_name: str,
785+
var: TimeSeriesOutputVar,
786+
) -> Tuple[np.ndarray, str]:
787+
path = f"{self.UNSTEADY_TIME_SERIES_PATH}/2D Flow Areas/{mesh_name}/{var.value}"
788+
group = self.get(path)
789+
try:
790+
import dask.array as da
791+
792+
# TODO: user-specified chunks?
793+
values = da.from_array(group, chunks=group.chunks)
794+
except ImportError:
795+
values = group[:]
796+
units = group.attrs.get("Units")
797+
if units is not None:
798+
units = units.decode("utf-8")
799+
return values, units
800+
801+
def mesh_timeseries_output(
802+
self,
803+
mesh_name: str,
804+
var: Union[str, TimeSeriesOutputVar],
805+
) -> xr.DataArray:
806+
"""Return the time series output data for a given variable.
807+
808+
Parameters
809+
----------
810+
mesh_name : str
811+
The name of the 2D flow area mesh.
812+
var : TimeSeriesOutputVar
813+
The time series output variable to retrieve.
814+
815+
Returns
816+
-------
817+
xr.DataArray
818+
An xarray DataArray with dimensions 'time' and 'cell_id'.
819+
"""
820+
times = self.unsteady_datetimes()
821+
mesh_names_counts = {
822+
name: count for name, count in self._2d_flow_area_names_and_counts()
823+
}
824+
if mesh_name not in mesh_names_counts:
825+
raise ValueError(f"Mesh '{mesh_name}' not found in the Plan HDF file.")
826+
if isinstance(var, str):
827+
var = TimeSeriesOutputVar(var)
828+
values, units = self._mesh_timeseries_output_values_units(mesh_name, var)
829+
if var in TIME_SERIES_OUTPUT_VARS_CELLS:
830+
cell_count = mesh_names_counts[mesh_name]
831+
values = values[:, :cell_count]
832+
id_coord = "cell_id"
833+
elif var in TIME_SERIES_OUTPUT_VARS_FACES:
834+
id_coord = "face_id"
835+
else:
836+
raise ValueError(f"Invalid time series output variable: {var.value}")
837+
da = xr.DataArray(
838+
values,
839+
name=var.value,
840+
dims=["time", id_coord],
841+
coords={
842+
"time": times,
843+
id_coord: range(values.shape[1]),
844+
},
845+
attrs={
846+
"mesh_name": mesh_name,
847+
"variable": var.value,
848+
"units": units,
849+
},
850+
)
851+
return da
852+
853+
def _mesh_timeseries_outputs(
854+
self, mesh_name: str, vars: List[TimeSeriesOutputVar]
855+
) -> xr.Dataset:
856+
datasets = {}
857+
for var in vars:
858+
var_path = f"{self.UNSTEADY_TIME_SERIES_PATH}/2D Flow Areas/{mesh_name}/{var.value}"
859+
if self.get(var_path) is None:
860+
continue
861+
da = self.mesh_timeseries_output(mesh_name, var)
862+
datasets[var.value] = da
863+
ds = xr.Dataset(datasets, attrs={"mesh_name": mesh_name})
864+
return ds
865+
866+
def mesh_timeseries_output_cells(self, mesh_name: str) -> xr.Dataset:
867+
"""Return the time series output data for cells in a 2D flow area mesh.
868+
869+
Parameters
870+
----------
871+
mesh_name : str
872+
The name of the 2D flow area mesh.
873+
874+
Returns
875+
-------
876+
xr.Dataset
877+
An xarray Dataset with DataArrays for each time series output variable.
878+
"""
879+
ds = self._mesh_timeseries_outputs(mesh_name, TIME_SERIES_OUTPUT_VARS_CELLS)
880+
return ds
881+
882+
def mesh_timeseries_output_faces(self, mesh_name: str) -> xr.Dataset:
883+
"""Return the time series output data for faces in a 2D flow area mesh.
884+
885+
Parameters
886+
----------
887+
mesh_name : str
888+
The name of the 2D flow area mesh.
889+
890+
Returns
891+
-------
892+
xr.Dataset
893+
An xarray Dataset with DataArrays for each time series output variable.
894+
"""
895+
ds = self._mesh_timeseries_outputs(mesh_name, TIME_SERIES_OUTPUT_VARS_FACES)
896+
return ds
897+
682898
def get_plan_info_attrs(self) -> Dict:
683899
"""Return plan information attributes from a HEC-RAS HDF plan file.
684900

src/rashdf/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@
1010
from shapely import LineString, Polygon, polygonize_full
1111

1212

13+
def parse_ras_datetime_ms(datetime_str: str) -> datetime:
14+
"""Parse a datetime string with milliseconds from a RAS file into a datetime object.
15+
16+
If the datetime has a time of 2400, then it is converted to midnight of the next day.
17+
18+
Parameters
19+
----------
20+
datetime_str (str): The datetime string to be parsed. The string should be in the format "ddMMMyyyy HH:mm:ss:fff".
21+
22+
Returns
23+
-------
24+
datetime: A datetime object representing the parsed datetime.
25+
"""
26+
milliseconds = int(datetime_str[-3:])
27+
microseconds = milliseconds * 1000
28+
parsed_dt = parse_ras_datetime(datetime_str[:-4]).replace(microsecond=microseconds)
29+
return parsed_dt
30+
31+
1332
def parse_ras_datetime(datetime_str: str) -> datetime:
1433
"""Parse a datetime string from a RAS file into a datetime object.
1534
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
time,face_id,Face Velocity
2+
1999-01-01 12:00:00,678,0.0
3+
1999-01-01 14:00:00,678,0.0
4+
1999-01-01 16:00:00,678,0.0
5+
1999-01-01 18:00:00,678,0.0
6+
1999-01-01 20:00:00,678,0.0
7+
1999-01-01 22:00:00,678,0.0
8+
1999-01-02 00:00:00,678,0.0
9+
1999-01-02 02:00:00,678,0.0
10+
1999-01-02 04:00:00,678,0.0
11+
1999-01-02 06:00:00,678,0.0
12+
1999-01-02 08:00:00,678,0.0
13+
1999-01-02 10:00:00,678,0.0
14+
1999-01-02 12:00:00,678,0.0
15+
1999-01-02 14:00:00,678,0.0
16+
1999-01-02 16:00:00,678,0.0
17+
1999-01-02 18:00:00,678,0.0
18+
1999-01-02 20:00:00,678,0.0
19+
1999-01-02 22:00:00,678,0.0
20+
1999-01-03 00:00:00,678,0.0
21+
1999-01-03 02:00:00,678,0.0
22+
1999-01-03 04:00:00,678,0.0
23+
1999-01-03 06:00:00,678,-0.28543922
24+
1999-01-03 08:00:00,678,-0.94968337
25+
1999-01-03 10:00:00,678,-1.1934088
26+
1999-01-03 12:00:00,678,-1.3193293
27+
1999-01-03 14:00:00,678,-1.448421
28+
1999-01-03 16:00:00,678,-1.5487186
29+
1999-01-03 18:00:00,678,-1.6022989
30+
1999-01-03 20:00:00,678,-1.6255693
31+
1999-01-03 22:00:00,678,-1.6309046
32+
1999-01-04 00:00:00,678,-1.625484
33+
1999-01-04 02:00:00,678,-1.6135458
34+
1999-01-04 04:00:00,678,-1.5972468
35+
1999-01-04 06:00:00,678,-1.5781946
36+
1999-01-04 08:00:00,678,-1.5570217
37+
1999-01-04 10:00:00,678,-1.5398287
38+
1999-01-04 12:00:00,678,-1.5226055
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
time,cell_id,Water Surface
2+
1999-01-01 12:00:00,123,537.2959
3+
1999-01-01 14:00:00,123,537.2959
4+
1999-01-01 16:00:00,123,537.2959
5+
1999-01-01 18:00:00,123,537.2959
6+
1999-01-01 20:00:00,123,537.2959
7+
1999-01-01 22:00:00,123,537.2959
8+
1999-01-02 00:00:00,123,537.2959
9+
1999-01-02 02:00:00,123,537.2959
10+
1999-01-02 04:00:00,123,537.2959
11+
1999-01-02 06:00:00,123,537.2959
12+
1999-01-02 08:00:00,123,537.2959
13+
1999-01-02 10:00:00,123,537.2959
14+
1999-01-02 12:00:00,123,537.2959
15+
1999-01-02 14:00:00,123,537.2959
16+
1999-01-02 16:00:00,123,537.2959
17+
1999-01-02 18:00:00,123,537.2959
18+
1999-01-02 20:00:00,123,537.2959
19+
1999-01-02 22:00:00,123,537.2959
20+
1999-01-03 00:00:00,123,537.2959
21+
1999-01-03 02:00:00,123,537.2959
22+
1999-01-03 04:00:00,123,537.2959
23+
1999-01-03 06:00:00,123,537.39996
24+
1999-01-03 08:00:00,123,543.78345
25+
1999-01-03 10:00:00,123,546.0193
26+
1999-01-03 12:00:00,123,547.2876
27+
1999-01-03 14:00:00,123,548.71246
28+
1999-01-03 16:00:00,123,549.9208
29+
1999-01-03 18:00:00,123,550.59985
30+
1999-01-03 20:00:00,123,550.90826
31+
1999-01-03 22:00:00,123,550.9861
32+
1999-01-04 00:00:00,123,550.92456
33+
1999-01-04 02:00:00,123,550.7785
34+
1999-01-04 04:00:00,123,550.5764
35+
1999-01-04 06:00:00,123,550.3424
36+
1999-01-04 08:00:00,123,550.0916
37+
1999-01-04 10:00:00,123,549.85077
38+
1999-01-04 12:00:00,123,549.6207

0 commit comments

Comments
 (0)