Skip to content

Commit 198c588

Browse files
committed
mesh timeseries output
1 parent dc0c6f0 commit 198c588

File tree

6 files changed

+304
-4
lines changed

6 files changed

+304
-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: 216 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,83 @@ 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+
# TimeSeriesOutputVar.FACE_WIND_TERM,
123+
]
124+
125+
TIME_SERIES_OUTPUT_VARS_DEFAULT = [
126+
TimeSeriesOutputVar.WATER_SURFACE,
127+
TimeSeriesOutputVar.FACE_VELOCITY,
128+
]
129+
130+
49131
class RasPlanHdf(RasGeomHdf):
50132
"""HEC-RAS Plan HDF class."""
51133

@@ -55,7 +137,11 @@ class RasPlanHdf(RasGeomHdf):
55137
RESULTS_UNSTEADY_PATH = "Results/Unsteady"
56138
RESULTS_UNSTEADY_SUMMARY_PATH = f"{RESULTS_UNSTEADY_PATH}/Summary"
57139
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"
140+
BASE_OUTPUT_PATH = f"{RESULTS_UNSTEADY_PATH}/Output/Output Blocks/Base Output"
141+
SUMMARY_OUTPUT_2D_FLOW_AREAS_PATH = (
142+
f"{BASE_OUTPUT_PATH}/Summary Output/2D Flow Areas"
143+
)
144+
UNSTEADY_TIME_SERIES_PATH = f"{BASE_OUTPUT_PATH}/Unsteady Time Series"
59145

60146
def __init__(self, name: str, **kwargs):
61147
"""Open a HEC-RAS Plan HDF file.
@@ -674,6 +760,134 @@ def mesh_cell_faces(
674760
datetime_to_str=datetime_to_str,
675761
)
676762

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

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
Binary file not shown.

tests/test_plan.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from src.rashdf.plan import RasPlanHdf, SummaryOutputVar, RasPlanHdfError
1+
from src.rashdf.plan import (
2+
RasPlanHdf,
3+
SummaryOutputVar,
4+
RasPlanHdfError,
5+
TimeSeriesOutputVar,
6+
)
27

38
from pathlib import Path
49

@@ -10,6 +15,7 @@
1015
TEST_JSON = TEST_DATA / "json"
1116
TEST_ATTRS = {"test_attribute1": "test_str1", "test_attribute2": 500}
1217
BALD_EAGLE_P18 = TEST_DATA / "ras/BaldEagleDamBrk.p18.hdf"
18+
BALD_EAGLE_P18_TIMESERIES = TEST_DATA / "ras/BaldEagleDamBrk.p18.timeseries.hdf"
1319
MUNCIE_G05 = TEST_DATA / "ras/Muncie.g05.hdf"
1420

1521

@@ -162,3 +168,55 @@ def test_mesh_cell_faces_with_output(tmp_path):
162168
valid = get_sha1_hash(TEST_JSON / "bald-eagle-mesh-cell-faces.geojson")
163169
test = get_sha1_hash(temp_faces)
164170
assert valid == test
171+
172+
173+
def test_mesh_timeseries_output():
174+
with RasPlanHdf(BALD_EAGLE_P18_TIMESERIES) as plan_hdf:
175+
with pytest.raises(ValueError):
176+
plan_hdf.mesh_timeseries_output(
177+
"Fake Mesh", TimeSeriesOutputVar.WATER_SURFACE
178+
)
179+
with pytest.raises(ValueError):
180+
plan_hdf.mesh_timeseries_output("BaldEagleCr", "Fake Variable")
181+
182+
183+
def test_mesh_timeseries_output_cells():
184+
with RasPlanHdf(BALD_EAGLE_P18_TIMESERIES) as plan_hdf:
185+
ds = plan_hdf.mesh_timeseries_output_cells("BaldEagleCr")
186+
assert "time" in ds.coords
187+
assert "cell_id" in ds.coords
188+
assert "Water Surface" in ds.variables
189+
water_surface = ds["Water Surface"]
190+
assert water_surface.shape == (37, 3359)
191+
assert water_surface.attrs["units"] == "ft"
192+
assert water_surface.attrs["mesh_name"] == "BaldEagleCr"
193+
194+
ds = plan_hdf.mesh_timeseries_output_cells("Upper 2D Area")
195+
assert "time" in ds.coords
196+
assert "cell_id" in ds.coords
197+
assert "Water Surface" in ds.variables
198+
water_surface = ds["Water Surface"]
199+
assert water_surface.shape == (37, 1066)
200+
assert water_surface.attrs["units"] == "ft"
201+
assert water_surface.attrs["mesh_name"] == "Upper 2D Area"
202+
203+
204+
def test_mesh_timeseries_output_faces():
205+
with RasPlanHdf(BALD_EAGLE_P18_TIMESERIES) as plan_hdf:
206+
ds = plan_hdf.mesh_timeseries_output_faces("BaldEagleCr")
207+
assert "time" in ds.coords
208+
assert "face_id" in ds.coords
209+
assert "Face Velocity" in ds.variables
210+
ws = ds["Face Velocity"]
211+
assert ws.shape == (37, 7295)
212+
assert ws.attrs["units"] == "ft/s"
213+
assert ws.attrs["mesh_name"] == "BaldEagleCr"
214+
215+
ds = plan_hdf.mesh_timeseries_output_faces("Upper 2D Area")
216+
assert "time" in ds.coords
217+
assert "face_id" in ds.coords
218+
assert "Face Velocity" in ds.variables
219+
v = ds["Face Velocity"]
220+
assert v.shape == (37, 2286)
221+
assert v.attrs["units"] == "ft/s"
222+
assert v.attrs["mesh_name"] == "Upper 2D Area"

tests/test_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,12 @@ def test_df_datetimes_to_str():
6464
assert df["datetime"].dtype.name == "object"
6565
assert df["datetime"].tolist() == ["2024-03-15T16:39:01", "2024-03-16T16:39:01"]
6666
assert df["asdf"].tolist() == [0.123, 0.456]
67+
68+
69+
def test_parse_ras_datetime_ms():
70+
assert utils.parse_ras_datetime_ms("15Mar2024 16:39:01.123") == datetime(
71+
2024, 3, 15, 16, 39, 1, 123000
72+
)
73+
assert utils.parse_ras_datetime_ms("15Mar2024 24:00:00.000") == datetime(
74+
2024, 3, 16, 0, 0, 0, 0
75+
)

0 commit comments

Comments
 (0)