rashdf 0.8.4__tar.gz → 0.10.0__tar.gz
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-0.8.4 → rashdf-0.10.0}/PKG-INFO +2 -2
- {rashdf-0.8.4 → rashdf-0.10.0}/pyproject.toml +2 -2
- {rashdf-0.8.4 → rashdf-0.10.0}/src/cli.py +1 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf/geom.py +63 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf/plan.py +200 -1
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf/utils.py +160 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf.egg-info/PKG-INFO +2 -2
- {rashdf-0.8.4 → rashdf-0.10.0}/tests/test_geom.py +14 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/tests/test_plan.py +101 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/tests/test_utils.py +112 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/LICENSE +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/README.md +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/setup.cfg +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf/__init__.py +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf/base.py +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf.egg-info/SOURCES.txt +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf.egg-info/dependency_links.txt +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf.egg-info/entry_points.txt +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf.egg-info/requires.txt +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/src/rashdf.egg-info/top_level.txt +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/tests/test_base.py +0 -0
- {rashdf-0.8.4 → rashdf-0.10.0}/tests/test_cli.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rashdf
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Read data from HEC-RAS HDF files.
|
|
5
5
|
Project-URL: repository, https://github.com/fema-ffrd/rashdf
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
7
7
|
Classifier: Intended Audience :: Developers
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: h5py
|
|
@@ -7,12 +7,12 @@ classifiers = [
|
|
|
7
7
|
"Intended Audience :: Developers",
|
|
8
8
|
"License :: OSI Approved :: MIT License",
|
|
9
9
|
"Programming Language :: Python :: 3",
|
|
10
|
-
"Programming Language :: Python :: 3.9",
|
|
11
10
|
"Programming Language :: Python :: 3.10",
|
|
12
11
|
"Programming Language :: Python :: 3.11",
|
|
13
12
|
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
14
|
]
|
|
15
|
-
version = "0.
|
|
15
|
+
version = "0.10.0"
|
|
16
16
|
dependencies = ["h5py", "geopandas>=1.0,<2.0", "pyarrow", "xarray<=2025.4.0"]
|
|
17
17
|
|
|
18
18
|
[project.optional-dependencies]
|
|
@@ -27,6 +27,8 @@ from .utils import (
|
|
|
27
27
|
convert_ras_hdf_value,
|
|
28
28
|
get_first_hdf_group,
|
|
29
29
|
hdf5_attrs_to_dict,
|
|
30
|
+
copy_lines_parallel,
|
|
31
|
+
experimental,
|
|
30
32
|
)
|
|
31
33
|
|
|
32
34
|
|
|
@@ -474,6 +476,67 @@ class RasGeomHdf(RasHdf):
|
|
|
474
476
|
def connections(self) -> GeoDataFrame: # noqa D102
|
|
475
477
|
raise NotImplementedError
|
|
476
478
|
|
|
479
|
+
@experimental
|
|
480
|
+
def generate_bridge_xs_lines(self, datetime_to_str: bool = False) -> GeoDataFrame:
|
|
481
|
+
"""[EXPERIMENTAL] Attempt to return the 2D bridge cross-section lines.
|
|
482
|
+
|
|
483
|
+
This method attempts to generate the cross-section lines for bridges modeled
|
|
484
|
+
within 2D mesh areas. It should be noted that these lines are not explicitly
|
|
485
|
+
stored within the HEC-RAS Geometry HDF file, and are instead generated based
|
|
486
|
+
on the bridge attributes and centerline geometry. As such, the accuracy of
|
|
487
|
+
these lines may vary depending on the complexity of the bridge geometry and
|
|
488
|
+
output from this method should be reviewed for accuracy.
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
datetime_to_str : bool, optional
|
|
493
|
+
If True, convert datetime values to string format (default: False).
|
|
494
|
+
|
|
495
|
+
Returns
|
|
496
|
+
-------
|
|
497
|
+
GeoDataFrame
|
|
498
|
+
A GeoDataFrame containing the 2D bridge cross-section lines if they exist.
|
|
499
|
+
"""
|
|
500
|
+
profile_info = self.get(self.GEOM_STRUCTURES_PATH + "/Table Info")
|
|
501
|
+
structs = self.structures().merge(
|
|
502
|
+
pd.DataFrame(profile_info[()] if profile_info is not None else None),
|
|
503
|
+
left_index=True,
|
|
504
|
+
right_index=True,
|
|
505
|
+
)
|
|
506
|
+
if structs.empty:
|
|
507
|
+
return GeoDataFrame()
|
|
508
|
+
|
|
509
|
+
bridges = structs[structs["Mode"] == "Bridge Opening"].copy()
|
|
510
|
+
if bridges.empty:
|
|
511
|
+
return GeoDataFrame()
|
|
512
|
+
|
|
513
|
+
inside_buffer_widths = bridges["Weir Width"] / 2
|
|
514
|
+
inside_bridge_xs = copy_lines_parallel(
|
|
515
|
+
bridges, inside_buffer_widths.values, "struct_id"
|
|
516
|
+
)
|
|
517
|
+
inside_bridge_xs["level"] = "inside"
|
|
518
|
+
|
|
519
|
+
outside_buffer_widths = inside_buffer_widths + bridges["Upstream Distance"]
|
|
520
|
+
outside_bridge_xs = copy_lines_parallel(
|
|
521
|
+
bridges, outside_buffer_widths.values, "struct_id"
|
|
522
|
+
)
|
|
523
|
+
outside_bridge_xs["level"] = "outside"
|
|
524
|
+
|
|
525
|
+
br_xs = GeoDataFrame(
|
|
526
|
+
pd.concat([inside_bridge_xs, outside_bridge_xs], ignore_index=True),
|
|
527
|
+
geometry="geometry",
|
|
528
|
+
)
|
|
529
|
+
br_xs.side = br_xs.side.apply(
|
|
530
|
+
lambda x: "upstream" if x == "right" else "downstream"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if datetime_to_str:
|
|
534
|
+
br_xs[self.LAST_EDITED_COLUMN] = br_xs[self.LAST_EDITED_COLUMN].apply(
|
|
535
|
+
lambda x: pd.Timestamp.isoformat(x)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return br_xs
|
|
539
|
+
|
|
477
540
|
def ic_points(self) -> GeoDataFrame: # noqa D102
|
|
478
541
|
"""Return initial conditions points.
|
|
479
542
|
|
|
@@ -1178,7 +1178,7 @@ class RasPlanHdf(RasGeomHdf):
|
|
|
1178
1178
|
xr.Dataset
|
|
1179
1179
|
An xarray Dataset with timeseries output data for boundary conditions lines.
|
|
1180
1180
|
"""
|
|
1181
|
-
df_bc_lines =
|
|
1181
|
+
df_bc_lines = super().bc_lines()
|
|
1182
1182
|
bc_lines_names = df_bc_lines["name"]
|
|
1183
1183
|
datasets = []
|
|
1184
1184
|
for bc_line_name in bc_lines_names:
|
|
@@ -1197,6 +1197,57 @@ class RasPlanHdf(RasGeomHdf):
|
|
|
1197
1197
|
)
|
|
1198
1198
|
return ds
|
|
1199
1199
|
|
|
1200
|
+
def bc_lines(
|
|
1201
|
+
self, include_output: bool = True, datetime_to_str: bool = False
|
|
1202
|
+
) -> GeoDataFrame:
|
|
1203
|
+
"""Return the boundary condition lines from a HEC-RAS HDF plan file.
|
|
1204
|
+
|
|
1205
|
+
Optionally include summary output data for each boundary condition line.
|
|
1206
|
+
|
|
1207
|
+
Parameters
|
|
1208
|
+
----------
|
|
1209
|
+
include_output : bool, optional
|
|
1210
|
+
If True, include summary output data in the GeoDataFrame. (default: True)
|
|
1211
|
+
datetime_to_str : bool, optional
|
|
1212
|
+
If True, convert datetime columns to strings. (default: False)
|
|
1213
|
+
|
|
1214
|
+
Returns
|
|
1215
|
+
-------
|
|
1216
|
+
GeoDataFrame
|
|
1217
|
+
A GeoDataFrame with boundary condition line geometry and summary output data.
|
|
1218
|
+
"""
|
|
1219
|
+
gdf = super().bc_lines()
|
|
1220
|
+
if include_output is False:
|
|
1221
|
+
return gdf
|
|
1222
|
+
|
|
1223
|
+
ds = self.bc_lines_timeseries_output()
|
|
1224
|
+
summary = {
|
|
1225
|
+
"bc_line_id": ds.coords["bc_line_id"].values,
|
|
1226
|
+
"name": ds.coords["bc_line_name"].values,
|
|
1227
|
+
"mesh_name": ds.coords["mesh_name"].values,
|
|
1228
|
+
"type": ds.coords["bc_line_type"].values,
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
for var in ds.data_vars:
|
|
1232
|
+
abbrev = "q" if var.lower() == "flow" else "ws"
|
|
1233
|
+
summary[f"max_{abbrev}"] = ds[var].max(dim="time").values
|
|
1234
|
+
summary[f"max_{abbrev}_time"] = (
|
|
1235
|
+
ds[var].time[ds[var].argmax(dim="time")].values
|
|
1236
|
+
)
|
|
1237
|
+
summary[f"min_{abbrev}"] = ds[var].min(dim="time").values
|
|
1238
|
+
summary[f"min_{abbrev}_time"] = (
|
|
1239
|
+
ds[var].time[ds[var].argmin(dim="time")].values
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
gdf_with_output = gdf.merge(
|
|
1243
|
+
pd.DataFrame(summary),
|
|
1244
|
+
on=["bc_line_id", "name", "mesh_name", "type"],
|
|
1245
|
+
how="left",
|
|
1246
|
+
)
|
|
1247
|
+
return (
|
|
1248
|
+
df_datetimes_to_str(gdf_with_output) if datetime_to_str else gdf_with_output
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1200
1251
|
def observed_timeseries_input(self, vartype: str = "Flow") -> xr.DataArray:
|
|
1201
1252
|
"""Return observed timeseries input data for reference lines and points from a HEC-RAS HDF plan file.
|
|
1202
1253
|
|
|
@@ -1728,3 +1779,151 @@ class RasPlanHdf(RasGeomHdf):
|
|
|
1728
1779
|
"""
|
|
1729
1780
|
ds = self.reference_points_timeseries_output()
|
|
1730
1781
|
return self._zmeta(ds)
|
|
1782
|
+
|
|
1783
|
+
def reference_lines_flow(self, use_names: bool = False) -> DataFrame:
|
|
1784
|
+
"""Return wide-format DataFrame for reference lines timeseries flow data.
|
|
1785
|
+
|
|
1786
|
+
Parameters
|
|
1787
|
+
----------
|
|
1788
|
+
use_names : bool, optional
|
|
1789
|
+
(Default) If False, use reference line IDs as column headers.
|
|
1790
|
+
If True, use reference line names as column headers.
|
|
1791
|
+
|
|
1792
|
+
Returns
|
|
1793
|
+
-------
|
|
1794
|
+
DataFrame
|
|
1795
|
+
Wide-format DataFrame with time as index and reference line IDs (or names) as columns.
|
|
1796
|
+
"""
|
|
1797
|
+
ds = self.reference_lines_timeseries_output()
|
|
1798
|
+
return self._timeseries_to_wide_dataframe(
|
|
1799
|
+
ds=ds,
|
|
1800
|
+
var="Flow",
|
|
1801
|
+
id_column="refln_id",
|
|
1802
|
+
name_column="refln_name",
|
|
1803
|
+
mesh_column="mesh_name",
|
|
1804
|
+
use_names_as_col=use_names,
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
def reference_points_stage(self, use_names: bool = False) -> DataFrame:
|
|
1808
|
+
"""Return Wide-format DataFrame for reference points timeseries stage data.
|
|
1809
|
+
|
|
1810
|
+
Parameters
|
|
1811
|
+
----------
|
|
1812
|
+
use_names : bool, optional
|
|
1813
|
+
(Default) If False, use reference point IDs as column headers.
|
|
1814
|
+
If True, use reference point names as column headers.
|
|
1815
|
+
|
|
1816
|
+
Returns
|
|
1817
|
+
-------
|
|
1818
|
+
DataFrame
|
|
1819
|
+
Wide-format DataFrame with time as index and reference point IDs (or names) as columns.
|
|
1820
|
+
"""
|
|
1821
|
+
ds = self.reference_points_timeseries_output()
|
|
1822
|
+
return self._timeseries_to_wide_dataframe(
|
|
1823
|
+
ds=ds,
|
|
1824
|
+
var=WATER_SURFACE,
|
|
1825
|
+
id_column="refpt_id",
|
|
1826
|
+
name_column="refpt_name",
|
|
1827
|
+
mesh_column="mesh_name",
|
|
1828
|
+
use_names_as_col=use_names,
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
def bc_lines_flow(self, use_names: bool = False) -> DataFrame:
|
|
1832
|
+
"""Return wide-format DataFrame for boundary condition lines timeseries flow data with.
|
|
1833
|
+
|
|
1834
|
+
Parameters
|
|
1835
|
+
----------
|
|
1836
|
+
use_names : bool, optional
|
|
1837
|
+
(Default) If False, use BC line IDs as column headers.
|
|
1838
|
+
If True, use BC line names as column headers.
|
|
1839
|
+
|
|
1840
|
+
Returns
|
|
1841
|
+
-------
|
|
1842
|
+
DataFrame
|
|
1843
|
+
Wide-format DataFrame with time as index and BC line IDs (or names) as columns.
|
|
1844
|
+
"""
|
|
1845
|
+
ds = self.bc_lines_timeseries_output()
|
|
1846
|
+
return self._timeseries_to_wide_dataframe(
|
|
1847
|
+
ds=ds,
|
|
1848
|
+
var="Flow",
|
|
1849
|
+
id_column="bc_line_id",
|
|
1850
|
+
name_column="bc_line_name",
|
|
1851
|
+
mesh_column="mesh_name",
|
|
1852
|
+
use_names_as_col=use_names,
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
def _timeseries_to_wide_dataframe(
|
|
1856
|
+
self,
|
|
1857
|
+
ds: xr.Dataset,
|
|
1858
|
+
var: str,
|
|
1859
|
+
id_column: str,
|
|
1860
|
+
name_column: str,
|
|
1861
|
+
mesh_column: str,
|
|
1862
|
+
use_names_as_col: bool = False,
|
|
1863
|
+
) -> DataFrame:
|
|
1864
|
+
"""Convert xarray timeseries Dataset to wide-format DataFrame with metadata.
|
|
1865
|
+
|
|
1866
|
+
Parameters
|
|
1867
|
+
----------
|
|
1868
|
+
ds : xr.Dataset
|
|
1869
|
+
xarray Dataset containing timeseries data
|
|
1870
|
+
var : str
|
|
1871
|
+
Variable name to extract (e.g. "Flow", "Water Surface")
|
|
1872
|
+
id_column : str
|
|
1873
|
+
ID column name for pivoting (e.g. "refln_id", "refpt_id", "bc_line_id")
|
|
1874
|
+
name_column : str
|
|
1875
|
+
Name column for creating readable column names (e.g. "refln_name", "refpt_name")
|
|
1876
|
+
mesh_column : str
|
|
1877
|
+
Mesh column name (e.g. "mesh_name")
|
|
1878
|
+
use_names_as_col : bool, optional
|
|
1879
|
+
(Default) If False, use IDs.
|
|
1880
|
+
If True, use names as column headers.
|
|
1881
|
+
|
|
1882
|
+
Returns
|
|
1883
|
+
-------
|
|
1884
|
+
DataFrame
|
|
1885
|
+
Wide-format DataFrame with time as index and IDs or names as columns.
|
|
1886
|
+
Metadata stored in DataFrame.attrs including name and mesh mappings.
|
|
1887
|
+
"""
|
|
1888
|
+
if var not in ds:
|
|
1889
|
+
raise ValueError(f"{var} data not found in timeseries output")
|
|
1890
|
+
|
|
1891
|
+
df = ds[var].to_dataframe().dropna().reset_index()
|
|
1892
|
+
|
|
1893
|
+
# check for duplicate names when using names as columns
|
|
1894
|
+
if use_names_as_col:
|
|
1895
|
+
unique_names = df[name_column].nunique()
|
|
1896
|
+
unique_ids = df[id_column].nunique()
|
|
1897
|
+
if unique_names < unique_ids: # should have one name for every one id
|
|
1898
|
+
name_counts = (
|
|
1899
|
+
df[[id_column, name_column]]
|
|
1900
|
+
.drop_duplicates()[name_column]
|
|
1901
|
+
.value_counts()
|
|
1902
|
+
)
|
|
1903
|
+
duplicates = name_counts[name_counts > 1].index.tolist()
|
|
1904
|
+
raise ValueError(
|
|
1905
|
+
f"Cannot use names as columns. The following names are not unique: {duplicates}. "
|
|
1906
|
+
)
|
|
1907
|
+
|
|
1908
|
+
pivot_column = name_column if use_names_as_col else id_column
|
|
1909
|
+
wide_df = df.pivot(index="time", columns=pivot_column, values=var)
|
|
1910
|
+
|
|
1911
|
+
lookup = df[[id_column, name_column, mesh_column]].drop_duplicates()
|
|
1912
|
+
if use_names_as_col:
|
|
1913
|
+
# when using names as columns, key=name -> value=id
|
|
1914
|
+
id_mapping = lookup.set_index(name_column)[id_column].to_dict()
|
|
1915
|
+
mesh_mapping = lookup.set_index(name_column)[mesh_column].to_dict()
|
|
1916
|
+
else:
|
|
1917
|
+
# when using IDs as columns, key=id -> value=name
|
|
1918
|
+
id_mapping = lookup.set_index(id_column)[name_column].to_dict()
|
|
1919
|
+
mesh_mapping = lookup.set_index(id_column)[mesh_column].to_dict()
|
|
1920
|
+
|
|
1921
|
+
wide_df.attrs = {
|
|
1922
|
+
"variable": var,
|
|
1923
|
+
"units": ds[var].attrs.get("units", None),
|
|
1924
|
+
"hdf_path": ds[var].attrs.get("hdf_path", None),
|
|
1925
|
+
"id_mapping": id_mapping,
|
|
1926
|
+
"mesh_mapping": mesh_mapping,
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
return wide_df
|
|
@@ -8,6 +8,38 @@ from datetime import datetime, timedelta
|
|
|
8
8
|
import re
|
|
9
9
|
from typing import Any, Callable, List, Tuple, Union, Optional
|
|
10
10
|
import warnings
|
|
11
|
+
from shapely import LineString, MultiLineString
|
|
12
|
+
import geopandas as gpd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def experimental(func) -> Callable:
|
|
16
|
+
"""
|
|
17
|
+
Declare a function to be experimental.
|
|
18
|
+
|
|
19
|
+
This is a decorator which can be used to mark functions as experimental.
|
|
20
|
+
It will result in a warning being emitted when the function is used.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
func: The function to be declared experimental.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
The decorated function.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def new_func(*args, **kwargs):
|
|
32
|
+
warnings.warn(
|
|
33
|
+
f"{func.__name__} is experimental and could change in the future. Please review output carefully.",
|
|
34
|
+
category=UserWarning,
|
|
35
|
+
stacklevel=2,
|
|
36
|
+
)
|
|
37
|
+
return func(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
new_func.__name__ = func.__name__
|
|
40
|
+
new_func.__doc__ = func.__doc__
|
|
41
|
+
new_func.__dict__.update(func.__dict__)
|
|
42
|
+
return new_func
|
|
11
43
|
|
|
12
44
|
|
|
13
45
|
def deprecated(func) -> Callable:
|
|
@@ -354,3 +386,131 @@ def ras_timesteps_to_datetimes(
|
|
|
354
386
|
start_time + pd.Timedelta(timestep, unit=time_unit).round(round_to)
|
|
355
387
|
for timestep in timesteps.astype(np.float64)
|
|
356
388
|
]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def remove_line_ends(
|
|
392
|
+
geom: Union[LineString, MultiLineString],
|
|
393
|
+
) -> Union[LineString, MultiLineString]:
|
|
394
|
+
"""
|
|
395
|
+
Remove endpoints from a LineString or each LineString in a MultiLineString if longer than 3 points.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
geom : LineString or MultiLineString
|
|
400
|
+
The geometry to trim.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
LineString or MultiLineString
|
|
405
|
+
The trimmed geometry, or original if not enough points to trim.
|
|
406
|
+
"""
|
|
407
|
+
if isinstance(geom, LineString):
|
|
408
|
+
coords = list(geom.coords)
|
|
409
|
+
if len(coords) > 3:
|
|
410
|
+
return LineString(coords[1:-1])
|
|
411
|
+
return geom
|
|
412
|
+
elif isinstance(geom, MultiLineString):
|
|
413
|
+
trimmed = []
|
|
414
|
+
for line in geom.geoms:
|
|
415
|
+
coords = list(line.coords)
|
|
416
|
+
if len(coords) > 3:
|
|
417
|
+
trimmed.append(LineString(coords[1:-1]))
|
|
418
|
+
else:
|
|
419
|
+
trimmed.append(line)
|
|
420
|
+
return MultiLineString(trimmed)
|
|
421
|
+
return geom
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def reverse_line(
|
|
425
|
+
line: Union[LineString, MultiLineString],
|
|
426
|
+
) -> Union[LineString, MultiLineString]:
|
|
427
|
+
"""
|
|
428
|
+
Reverse the order of coordinates in a LineString or each LineString in a MultiLineString.
|
|
429
|
+
|
|
430
|
+
Parameters
|
|
431
|
+
----------
|
|
432
|
+
line : LineString or MultiLineString
|
|
433
|
+
The geometry to reverse.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
LineString or MultiLineString
|
|
438
|
+
The reversed geometry.
|
|
439
|
+
"""
|
|
440
|
+
return (
|
|
441
|
+
MultiLineString([LineString(list(line.coords)[::-1]) for line in line.geoms])
|
|
442
|
+
if isinstance(line, MultiLineString)
|
|
443
|
+
else LineString(list(line.coords)[::-1])
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def copy_lines_parallel(
|
|
448
|
+
lines: gpd.GeoDataFrame,
|
|
449
|
+
offset_ft: Union[np.ndarray, float],
|
|
450
|
+
id_col: str = "id",
|
|
451
|
+
) -> gpd.GeoDataFrame:
|
|
452
|
+
"""
|
|
453
|
+
Create parallel copies of line geometries offset to the left and right, then trim and erase overlaps.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
lines : gpd.GeoDataFrame
|
|
458
|
+
GeoDataFrame containing line geometries.
|
|
459
|
+
offset_ft : float or np.ndarray
|
|
460
|
+
Offset distance (in feet) for parallel lines.
|
|
461
|
+
id_col : str
|
|
462
|
+
Name of the column containing unique structure IDs. Default is "id".
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
gpd.GeoDataFrame
|
|
467
|
+
GeoDataFrame with trimmed, parallel left and right offset lines.
|
|
468
|
+
"""
|
|
469
|
+
# Offset lines to the left
|
|
470
|
+
left = lines.copy()
|
|
471
|
+
offset_ft = offset_ft.astype(float)
|
|
472
|
+
left.geometry = lines.buffer(
|
|
473
|
+
offset_ft, cap_style="flat", single_sided=True, resolution=3
|
|
474
|
+
).boundary
|
|
475
|
+
left["side"] = "left"
|
|
476
|
+
|
|
477
|
+
# Offset lines to the right (reverse direction first)
|
|
478
|
+
reversed_lines = lines.copy()
|
|
479
|
+
reversed_lines.geometry = reversed_lines.geometry.apply(reverse_line)
|
|
480
|
+
right = lines.copy()
|
|
481
|
+
right.geometry = reversed_lines.buffer(
|
|
482
|
+
offset_ft, cap_style="flat", single_sided=True, resolution=3
|
|
483
|
+
).boundary.apply(reverse_line)
|
|
484
|
+
right["side"] = "right"
|
|
485
|
+
|
|
486
|
+
# Combine left and right boundaries
|
|
487
|
+
boundaries = pd.concat([left, right], ignore_index=True)
|
|
488
|
+
boundaries_gdf = gpd.GeoDataFrame(boundaries, crs=lines.crs, geometry="geometry")
|
|
489
|
+
|
|
490
|
+
# Erase buffer caps
|
|
491
|
+
erase_buffer = 0.1
|
|
492
|
+
cleaned_list = []
|
|
493
|
+
eraser = gpd.GeoDataFrame(
|
|
494
|
+
{
|
|
495
|
+
id_col: lines[id_col],
|
|
496
|
+
"geometry": lines.buffer(
|
|
497
|
+
offset_ft - erase_buffer, cap_style="square", resolution=3
|
|
498
|
+
),
|
|
499
|
+
},
|
|
500
|
+
crs=lines.crs,
|
|
501
|
+
)
|
|
502
|
+
for id in lines[id_col].unique():
|
|
503
|
+
cleaned_list.append(
|
|
504
|
+
gpd.overlay(
|
|
505
|
+
boundaries_gdf[boundaries_gdf[id_col] == id],
|
|
506
|
+
eraser[eraser[id_col] == id],
|
|
507
|
+
how="difference",
|
|
508
|
+
)
|
|
509
|
+
)
|
|
510
|
+
cleaned = gpd.GeoDataFrame(
|
|
511
|
+
pd.concat(cleaned_list, ignore_index=True), crs=lines.crs, geometry="geometry"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# trim ends
|
|
515
|
+
cleaned["geometry"] = cleaned["geometry"].apply(remove_line_ends)
|
|
516
|
+
return cleaned
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rashdf
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Read data from HEC-RAS HDF files.
|
|
5
5
|
Project-URL: repository, https://github.com/fema-ffrd/rashdf
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
7
7
|
Classifier: Intended Audience :: Developers
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: h5py
|
|
@@ -17,6 +17,7 @@ TEST_JSON = TEST_DATA / "json"
|
|
|
17
17
|
BALD_EAGLE_P18_REF = TEST_DATA / "ras/BaldEagleDamBrk.reflines-refpts.p18.hdf"
|
|
18
18
|
LOWER_KANAWHA_P01_IC_POINTS = TEST_DATA / "ras/LowerKanawha.p01.icpoints.hdf"
|
|
19
19
|
LOWER_KANAWHA_P01_IC_POINTS_JSON = TEST_JSON / "LowerKanawha.p01.icpoints.geojson"
|
|
20
|
+
ROSEBERRY_G01 = TEST_DATA / "ras/Roseberry_Creek.g01.hdf"
|
|
20
21
|
|
|
21
22
|
TEST_ATTRS = {"test_attribute1": "test_str1", "test_attribute2": 500}
|
|
22
23
|
|
|
@@ -274,3 +275,16 @@ def test_ic_points():
|
|
|
274
275
|
valid_gdf,
|
|
275
276
|
check_dtype=False,
|
|
276
277
|
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_generate_bridge_xs_lines():
|
|
281
|
+
bridge_xs_json = TEST_JSON / "bridge_xs_lines.json"
|
|
282
|
+
with RasGeomHdf(ROSEBERRY_G01) as ghdf:
|
|
283
|
+
assert _gdf_matches_json(
|
|
284
|
+
ghdf.generate_bridge_xs_lines(datetime_to_str=True), bridge_xs_json
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_generate_bridge_xs_lines_not_found():
|
|
289
|
+
with RasGeomHdf(MUNCIE_G05) as ghdf:
|
|
290
|
+
assert ghdf.generate_bridge_xs_lines().empty
|
|
@@ -733,3 +733,104 @@ def test_invalid_group_reference_summary_output():
|
|
|
733
733
|
with RasPlanHdf(BALD_EAGLE_P18) as phdf:
|
|
734
734
|
with pytest.raises(ValueError):
|
|
735
735
|
phdf.reference_summary_output(reftype="Not supported type")
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def test_bc_lines_include_output_false():
|
|
739
|
+
bc_lines_json = TEST_JSON / "bc_lines.json"
|
|
740
|
+
with RasPlanHdf(MUNCIE_G05) as plan_hdf:
|
|
741
|
+
assert _gdf_matches_json(plan_hdf.bc_lines(include_output=False), bc_lines_json)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def test_bc_lines_include_output_true():
|
|
745
|
+
bc_lines_with_output_json = TEST_JSON / "bc_lines_with_output.json"
|
|
746
|
+
with RasPlanHdf(LOWER_KANAWHA_P01_BC_LINES) as plan_hdf:
|
|
747
|
+
assert _gdf_matches_json(
|
|
748
|
+
plan_hdf.bc_lines(include_output=True, datetime_to_str=True),
|
|
749
|
+
bc_lines_with_output_json,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def test_reference_lines_flow(tmp_path: Path):
|
|
754
|
+
plan_hdf = RasPlanHdf(BALD_EAGLE_P18_REF)
|
|
755
|
+
df = plan_hdf.reference_lines_flow()
|
|
756
|
+
|
|
757
|
+
assert df.index.name == "time"
|
|
758
|
+
assert df.shape == (37, 4)
|
|
759
|
+
assert list(df.columns) == [0, 1, 2, 3]
|
|
760
|
+
|
|
761
|
+
# Check metadata
|
|
762
|
+
assert df.attrs["variable"] == "Flow"
|
|
763
|
+
assert df.attrs["units"] == "cfs"
|
|
764
|
+
|
|
765
|
+
# Check mappings
|
|
766
|
+
assert "id_mapping" in df.attrs
|
|
767
|
+
assert len(df.attrs["id_mapping"]) == 4
|
|
768
|
+
assert "mesh_mapping" in df.attrs
|
|
769
|
+
assert len(df.attrs["mesh_mapping"]) == 4
|
|
770
|
+
|
|
771
|
+
df_refln2 = df[2].to_frame(name="Flow")
|
|
772
|
+
valid_df = pd.read_csv(
|
|
773
|
+
TEST_CSV / "BaldEagleDamBrk.reflines.2.csv",
|
|
774
|
+
index_col="time",
|
|
775
|
+
parse_dates=True,
|
|
776
|
+
usecols=["time", "Flow"],
|
|
777
|
+
dtype={"Flow": np.float32},
|
|
778
|
+
)
|
|
779
|
+
assert_frame_equal(df_refln2, valid_df, check_dtype=False)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_reference_points_stage(tmp_path: Path):
|
|
783
|
+
plan_hdf = RasPlanHdf(BALD_EAGLE_P18_REF)
|
|
784
|
+
df = plan_hdf.reference_points_stage()
|
|
785
|
+
|
|
786
|
+
assert df.index.name == "time"
|
|
787
|
+
assert df.shape == (37, 3)
|
|
788
|
+
assert list(df.columns) == [0, 1, 2]
|
|
789
|
+
|
|
790
|
+
# Check metadata
|
|
791
|
+
assert df.attrs["variable"] == "Water Surface"
|
|
792
|
+
assert df.attrs["units"] == "ft"
|
|
793
|
+
|
|
794
|
+
# Check mappings
|
|
795
|
+
assert "id_mapping" in df.attrs
|
|
796
|
+
assert len(df.attrs["id_mapping"]) == 3
|
|
797
|
+
assert "mesh_mapping" in df.attrs
|
|
798
|
+
assert len(df.attrs["mesh_mapping"]) == 3
|
|
799
|
+
|
|
800
|
+
df_refpt1 = df[1].to_frame(name="Water Surface")
|
|
801
|
+
valid_df = pd.read_csv(
|
|
802
|
+
TEST_CSV / "BaldEagleDamBrk.refpoints.1.csv",
|
|
803
|
+
index_col="time",
|
|
804
|
+
parse_dates=True,
|
|
805
|
+
usecols=["time", "Water Surface"],
|
|
806
|
+
dtype={"Water Surface": np.float32},
|
|
807
|
+
)
|
|
808
|
+
assert_frame_equal(df_refpt1, valid_df, check_dtype=False)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def test_bc_lines_flow(tmp_path: Path):
|
|
812
|
+
plan_hdf = RasPlanHdf(LOWER_KANAWHA_P01_BC_LINES)
|
|
813
|
+
df = plan_hdf.bc_lines_flow()
|
|
814
|
+
|
|
815
|
+
assert df.index.name == "time"
|
|
816
|
+
assert df.shape == (577, 10)
|
|
817
|
+
|
|
818
|
+
# Check metadata
|
|
819
|
+
assert df.attrs["variable"] == "Flow"
|
|
820
|
+
assert df.attrs["units"] == "cfs"
|
|
821
|
+
|
|
822
|
+
# Check mappings
|
|
823
|
+
assert "id_mapping" in df.attrs
|
|
824
|
+
assert len(df.attrs["id_mapping"]) == 10
|
|
825
|
+
assert "mesh_mapping" in df.attrs
|
|
826
|
+
assert len(df.attrs["mesh_mapping"]) == 10
|
|
827
|
+
|
|
828
|
+
df_bcline7 = df[7].to_frame(name="Flow")
|
|
829
|
+
valid_df = pd.read_csv(
|
|
830
|
+
TEST_CSV / "LowerKanawha.p01.bclines.7.csv",
|
|
831
|
+
index_col="time",
|
|
832
|
+
parse_dates=True,
|
|
833
|
+
usecols=["time", "Flow"],
|
|
834
|
+
dtype={"Flow": np.float32},
|
|
835
|
+
)
|
|
836
|
+
assert_frame_equal(df_bcline7, valid_df, check_dtype=False)
|
|
@@ -5,6 +5,14 @@ import pandas as pd
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
|
+
from shapely.geometry import LineString, MultiLineString
|
|
9
|
+
import geopandas as gpd
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import _assert_geodataframes_close
|
|
13
|
+
|
|
14
|
+
TEST_DATA = Path("./tests/data")
|
|
15
|
+
TEST_JSON = TEST_DATA / "json"
|
|
8
16
|
|
|
9
17
|
|
|
10
18
|
def test_convert_ras_hdf_value():
|
|
@@ -117,3 +125,107 @@ def test_parse_ras_datetime_ms():
|
|
|
117
125
|
assert utils.parse_ras_datetime_ms("15Mar2024 24:00:00.000") == datetime(
|
|
118
126
|
2024, 3, 16, 0, 0, 0, 0
|
|
119
127
|
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_trim_line():
|
|
131
|
+
gdf = gpd.GeoDataFrame(
|
|
132
|
+
{
|
|
133
|
+
"id": [1, 2, 3],
|
|
134
|
+
"geometry": [
|
|
135
|
+
LineString([(0, 0), (5, 5), (10, 10)]),
|
|
136
|
+
LineString([(0, 0), (5, 5), (10, 10), (15, 15)]),
|
|
137
|
+
MultiLineString(
|
|
138
|
+
[
|
|
139
|
+
[(0, 0), (3, 3)],
|
|
140
|
+
[(3, 3), (6, 6), (9, 9)],
|
|
141
|
+
[(3, 3), (6, 6), (9, 9), (12, 12), (15, 15)],
|
|
142
|
+
]
|
|
143
|
+
),
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
assert gdf.geometry.apply(utils.remove_line_ends).equals(
|
|
148
|
+
gpd.GeoSeries(
|
|
149
|
+
[
|
|
150
|
+
LineString([(0, 0), (5, 5), (10, 10)]),
|
|
151
|
+
LineString(
|
|
152
|
+
[
|
|
153
|
+
(5, 5),
|
|
154
|
+
(10, 10),
|
|
155
|
+
]
|
|
156
|
+
),
|
|
157
|
+
MultiLineString(
|
|
158
|
+
[
|
|
159
|
+
[(0, 0), (3, 3)],
|
|
160
|
+
[(3, 3), (6, 6), (9, 9)],
|
|
161
|
+
[(6, 6), (9, 9), (12, 12)],
|
|
162
|
+
]
|
|
163
|
+
),
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_reverse_line():
|
|
170
|
+
gdf = gpd.GeoDataFrame(
|
|
171
|
+
{
|
|
172
|
+
"id": [1, 2, 3],
|
|
173
|
+
"geometry": [
|
|
174
|
+
LineString([(0, 0), (5, 5), (10, 10)]),
|
|
175
|
+
LineString([(0, 0), (5, 5), (10, 10), (15, 15)]),
|
|
176
|
+
MultiLineString(
|
|
177
|
+
[
|
|
178
|
+
[(0, 0), (3, 3)],
|
|
179
|
+
[(3, 3), (6, 6), (9, 9)],
|
|
180
|
+
[(3, 3), (6, 6), (9, 9), (12, 12), (15, 15)],
|
|
181
|
+
]
|
|
182
|
+
),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
assert gdf.geometry.apply(utils.reverse_line).equals(
|
|
187
|
+
gpd.GeoSeries(
|
|
188
|
+
[
|
|
189
|
+
LineString([(10, 10), (5, 5), (0, 0)]),
|
|
190
|
+
LineString([(15, 15), (10, 10), (5, 5), (0, 0)]),
|
|
191
|
+
MultiLineString(
|
|
192
|
+
[
|
|
193
|
+
[(3, 3), (0, 0)],
|
|
194
|
+
[(9, 9), (6, 6), (3, 3)],
|
|
195
|
+
[(15, 15), (12, 12), (9, 9), (6, 6), (3, 3)],
|
|
196
|
+
]
|
|
197
|
+
),
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_copy_lines_parallel():
|
|
204
|
+
gdf = gpd.GeoDataFrame(
|
|
205
|
+
{
|
|
206
|
+
"id": [1, 2, 3],
|
|
207
|
+
"geometry": [
|
|
208
|
+
LineString([(0, 0), (5, 5), (10, 10)]),
|
|
209
|
+
LineString([(20, 20), (30, 30), (40, 40), (50, 50)]),
|
|
210
|
+
MultiLineString(
|
|
211
|
+
[
|
|
212
|
+
[(100.0, 100.0), (103.0, 103.0)],
|
|
213
|
+
[(103.0, 103.0), (106.0, 106.0), (109.0, 109.0)],
|
|
214
|
+
[
|
|
215
|
+
(103.0, 103.0),
|
|
216
|
+
(106.0, 106.0),
|
|
217
|
+
(109.0, 109.0),
|
|
218
|
+
(112.0, 112.0),
|
|
219
|
+
(115.0, 115.0),
|
|
220
|
+
],
|
|
221
|
+
]
|
|
222
|
+
),
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
offsets = np.array([1, 2, 3])
|
|
227
|
+
copied = utils.copy_lines_parallel(gdf, offsets)
|
|
228
|
+
expected = gpd.read_file(TEST_JSON / "copy_lines_parallel.json").set_crs(
|
|
229
|
+
None, allow_override=True
|
|
230
|
+
)
|
|
231
|
+
_assert_geodataframes_close(copied, expected, tol=1e-3)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|