ras-commander 0.50.0__py3-none-any.whl → 0.52.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.
- ras_commander/HdfBase.py +344 -307
- ras_commander/HdfBndry.py +8 -8
- ras_commander/HdfFluvialPluvial.py +554 -309
- ras_commander/HdfMesh.py +461 -461
- ras_commander/HdfUtils.py +434 -434
- ras_commander/HdfXsec.py +56 -38
- ras_commander/RasPrj.py +93 -55
- {ras_commander-0.50.0.dist-info → ras_commander-0.52.0.dist-info}/METADATA +12 -9
- {ras_commander-0.50.0.dist-info → ras_commander-0.52.0.dist-info}/RECORD +12 -12
- {ras_commander-0.50.0.dist-info → ras_commander-0.52.0.dist-info}/WHEEL +1 -1
- {ras_commander-0.50.0.dist-info → ras_commander-0.52.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.50.0.dist-info → ras_commander-0.52.0.dist-info}/top_level.txt +0 -0
ras_commander/HdfUtils.py
CHANGED
@@ -1,435 +1,435 @@
|
|
1
|
-
"""
|
2
|
-
HdfUtils Class
|
3
|
-
-------------
|
4
|
-
|
5
|
-
A utility class providing static methods for working with HEC-RAS HDF files.
|
6
|
-
|
7
|
-
Attribution:
|
8
|
-
A substantial amount of code in this file is sourced or derived from the
|
9
|
-
https://github.com/fema-ffrd/rashdf library, released under MIT license
|
10
|
-
and Copyright (c) 2024 fema-ffrd. The file has been forked and modified
|
11
|
-
for use in RAS Commander.
|
12
|
-
|
13
|
-
Key Features:
|
14
|
-
- HDF file data conversion and parsing
|
15
|
-
- DateTime handling for RAS-specific formats
|
16
|
-
- Spatial operations using KDTree
|
17
|
-
- HDF attribute management
|
18
|
-
|
19
|
-
Main Method Categories:
|
20
|
-
|
21
|
-
1. Data Conversion
|
22
|
-
- convert_ras_string: Convert RAS HDF strings to Python objects
|
23
|
-
- convert_ras_hdf_value: Convert general HDF values to Python objects
|
24
|
-
- convert_df_datetimes_to_str: Convert DataFrame datetime columns to strings
|
25
|
-
- convert_hdf5_attrs_to_dict: Convert HDF5 attributes to dictionary
|
26
|
-
- convert_timesteps_to_datetimes: Convert timesteps to datetime objects
|
27
|
-
|
28
|
-
2. Spatial Operations
|
29
|
-
- perform_kdtree_query: KDTree search between datasets
|
30
|
-
- find_nearest_neighbors: Find nearest neighbors within dataset
|
31
|
-
|
32
|
-
3. DateTime Parsing
|
33
|
-
- parse_ras_datetime: Parse standard RAS datetime format (ddMMMYYYY HH:MM:SS)
|
34
|
-
- parse_ras_window_datetime: Parse simulation window datetime (ddMMMYYYY HHMM)
|
35
|
-
- parse_duration: Parse duration strings (HH:MM:SS)
|
36
|
-
- parse_ras_datetime_ms: Parse datetime with milliseconds
|
37
|
-
- parse_run_time_window: Parse time window strings
|
38
|
-
|
39
|
-
Usage Notes:
|
40
|
-
- All methods are static and can be called without class instantiation
|
41
|
-
- Methods handle both raw HDF data and converted Python objects
|
42
|
-
- Includes comprehensive error handling for RAS-specific data formats
|
43
|
-
- Supports various RAS datetime formats and conversions
|
44
|
-
"""
|
45
|
-
import logging
|
46
|
-
from pathlib import Path
|
47
|
-
import h5py
|
48
|
-
import numpy as np
|
49
|
-
import pandas as pd
|
50
|
-
from datetime import datetime, timedelta
|
51
|
-
from typing import Union, Optional, Dict, List, Tuple, Any
|
52
|
-
from scipy.spatial import KDTree
|
53
|
-
import re
|
54
|
-
from shapely.geometry import LineString # Import LineString to avoid NameError
|
55
|
-
|
56
|
-
from .Decorators import standardize_input, log_call
|
57
|
-
from .LoggingConfig import setup_logging, get_logger
|
58
|
-
|
59
|
-
logger = get_logger(__name__)
|
60
|
-
|
61
|
-
class HdfUtils:
|
62
|
-
"""
|
63
|
-
Utility class for working with HEC-RAS HDF files.
|
64
|
-
|
65
|
-
This class provides general utility functions for HDF file operations,
|
66
|
-
including attribute extraction, data conversion, and common HDF queries.
|
67
|
-
It also includes spatial operations and helper methods for working with
|
68
|
-
HEC-RAS specific data structures.
|
69
|
-
|
70
|
-
Note:
|
71
|
-
- Use this class for general HDF utility functions that are not specific to plan or geometry files.
|
72
|
-
- All methods in this class are static and can be called without instantiating the class.
|
73
|
-
"""
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
# RENAME TO convert_ras_string and make public
|
79
|
-
|
80
|
-
@staticmethod
|
81
|
-
def convert_ras_string(value: Union[str, bytes]) -> Union[bool, datetime, List[datetime], timedelta, str]:
|
82
|
-
"""
|
83
|
-
Convert a string value from an HEC-RAS HDF file into a Python object.
|
84
|
-
|
85
|
-
Args:
|
86
|
-
value (Union[str, bytes]): The value to convert.
|
87
|
-
|
88
|
-
Returns:
|
89
|
-
Union[bool, datetime, List[datetime], timedelta, str]: The converted value.
|
90
|
-
"""
|
91
|
-
if isinstance(value, bytes):
|
92
|
-
s = value.decode("utf-8")
|
93
|
-
else:
|
94
|
-
s = value
|
95
|
-
|
96
|
-
if s == "True":
|
97
|
-
return True
|
98
|
-
elif s == "False":
|
99
|
-
return False
|
100
|
-
|
101
|
-
ras_datetime_format1_re = r"\d{2}\w{3}\d{4} \d{2}:\d{2}:\d{2}"
|
102
|
-
ras_datetime_format2_re = r"\d{2}\w{3}\d{4} \d{2}\d{2}"
|
103
|
-
ras_duration_format_re = r"\d{2}:\d{2}:\d{2}"
|
104
|
-
|
105
|
-
if re.match(rf"^{ras_datetime_format1_re}", s):
|
106
|
-
if re.match(rf"^{ras_datetime_format1_re} to {ras_datetime_format1_re}$", s):
|
107
|
-
split = s.split(" to ")
|
108
|
-
return [
|
109
|
-
HdfUtils.parse_ras_datetime(split[0]),
|
110
|
-
HdfUtils.parse_ras_datetime(split[1]),
|
111
|
-
]
|
112
|
-
return HdfUtils.parse_ras_datetime(s)
|
113
|
-
elif re.match(rf"^{ras_datetime_format2_re}", s):
|
114
|
-
if re.match(rf"^{ras_datetime_format2_re} to {ras_datetime_format2_re}$", s):
|
115
|
-
split = s.split(" to ")
|
116
|
-
return [
|
117
|
-
HdfUtils.parse_ras_window_datetime(split[0]),
|
118
|
-
HdfUtils.parse_ras_window_datetime(split[1]),
|
119
|
-
]
|
120
|
-
return HdfUtils.parse_ras_window_datetime(s)
|
121
|
-
elif re.match(rf"^{ras_duration_format_re}$", s):
|
122
|
-
return HdfUtils.parse_ras_duration(s)
|
123
|
-
return s
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
@staticmethod
|
130
|
-
def convert_ras_hdf_value(value: Any) -> Union[None, bool, str, List[str], int, float, List[int], List[float]]:
|
131
|
-
"""
|
132
|
-
Convert a value from a HEC-RAS HDF file into a Python object.
|
133
|
-
|
134
|
-
Args:
|
135
|
-
value (Any): The value to convert.
|
136
|
-
|
137
|
-
Returns:
|
138
|
-
Union[None, bool, str, List[str], int, float, List[int], List[float]]: The converted value.
|
139
|
-
"""
|
140
|
-
if isinstance(value, np.floating) and np.isnan(value):
|
141
|
-
return None
|
142
|
-
elif isinstance(value, (bytes, np.bytes_)):
|
143
|
-
return value.decode('utf-8')
|
144
|
-
elif isinstance(value, np.integer):
|
145
|
-
return int(value)
|
146
|
-
elif isinstance(value, np.floating):
|
147
|
-
return float(value)
|
148
|
-
elif isinstance(value, (int, float)):
|
149
|
-
return value
|
150
|
-
elif isinstance(value, (list, tuple, np.ndarray)):
|
151
|
-
if len(value) > 1:
|
152
|
-
return [HdfUtils.convert_ras_hdf_value(v) for v in value]
|
153
|
-
else:
|
154
|
-
return HdfUtils.convert_ras_hdf_value(value[0])
|
155
|
-
else:
|
156
|
-
return str(value)
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
# RENAME TO convert_df_datetimes_to_str
|
168
|
-
|
169
|
-
@staticmethod
|
170
|
-
def convert_df_datetimes_to_str(df: pd.DataFrame) -> pd.DataFrame:
|
171
|
-
"""
|
172
|
-
Convert any datetime64 columns in a DataFrame to strings.
|
173
|
-
|
174
|
-
Args:
|
175
|
-
df (pd.DataFrame): The DataFrame to convert.
|
176
|
-
|
177
|
-
Returns:
|
178
|
-
pd.DataFrame: The DataFrame with datetime columns converted to strings.
|
179
|
-
"""
|
180
|
-
for col in df.select_dtypes(include=['datetime64']).columns:
|
181
|
-
df[col] = df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
|
182
|
-
return df
|
183
|
-
|
184
|
-
|
185
|
-
# KDTree Methods:
|
186
|
-
|
187
|
-
|
188
|
-
@staticmethod
|
189
|
-
def perform_kdtree_query(
|
190
|
-
reference_points: np.ndarray,
|
191
|
-
query_points: np.ndarray,
|
192
|
-
max_distance: float = 2.0
|
193
|
-
) -> np.ndarray:
|
194
|
-
"""
|
195
|
-
Performs a KDTree query between two datasets and returns indices with distances exceeding max_distance set to -1.
|
196
|
-
|
197
|
-
Args:
|
198
|
-
reference_points (np.ndarray): The reference dataset for KDTree.
|
199
|
-
query_points (np.ndarray): The query dataset to search against KDTree of reference_points.
|
200
|
-
max_distance (float, optional): The maximum distance threshold. Indices with distances greater than this are set to -1. Defaults to 2.0.
|
201
|
-
|
202
|
-
Returns:
|
203
|
-
np.ndarray: Array of indices from reference_points that are nearest to each point in query_points.
|
204
|
-
Indices with distances > max_distance are set to -1.
|
205
|
-
|
206
|
-
Example:
|
207
|
-
>>> ref_points = np.array([[0, 0], [1, 1], [2, 2]])
|
208
|
-
>>> query_points = np.array([[0.5, 0.5], [3, 3]])
|
209
|
-
>>> result = HdfUtils.perform_kdtree_query(ref_points, query_points)
|
210
|
-
>>> print(result)
|
211
|
-
array([ 0, -1])
|
212
|
-
"""
|
213
|
-
dist, snap = KDTree(reference_points).query(query_points, distance_upper_bound=max_distance)
|
214
|
-
snap[dist > max_distance] = -1
|
215
|
-
return snap
|
216
|
-
|
217
|
-
@staticmethod
|
218
|
-
def find_nearest_neighbors(points: np.ndarray, max_distance: float = 2.0) -> np.ndarray:
|
219
|
-
"""
|
220
|
-
Creates a self KDTree for dataset points and finds nearest neighbors excluding self,
|
221
|
-
with distances above max_distance set to -1.
|
222
|
-
|
223
|
-
Args:
|
224
|
-
points (np.ndarray): The dataset to build the KDTree from and query against itself.
|
225
|
-
max_distance (float, optional): The maximum distance threshold. Indices with distances
|
226
|
-
greater than max_distance are set to -1. Defaults to 2.0.
|
227
|
-
|
228
|
-
Returns:
|
229
|
-
np.ndarray: Array of indices representing the nearest neighbor in points for each point in points.
|
230
|
-
Indices with distances > max_distance or self-matches are set to -1.
|
231
|
-
|
232
|
-
Example:
|
233
|
-
>>> points = np.array([[0, 0], [1, 1], [2, 2], [10, 10]])
|
234
|
-
>>> result = HdfUtils.find_nearest_neighbors(points)
|
235
|
-
>>> print(result)
|
236
|
-
array([1, 0, 1, -1])
|
237
|
-
"""
|
238
|
-
dist, snap = KDTree(points).query(points, k=2, distance_upper_bound=max_distance)
|
239
|
-
snap[dist > max_distance] = -1
|
240
|
-
|
241
|
-
snp = pd.DataFrame(snap, index=np.arange(len(snap)))
|
242
|
-
snp = snp.replace(-1, np.nan)
|
243
|
-
snp.loc[snp[0] == snp.index, 0] = np.nan
|
244
|
-
snp.loc[snp[1] == snp.index, 1] = np.nan
|
245
|
-
filled = snp[0].fillna(snp[1])
|
246
|
-
snapped = filled.fillna(-1).astype(np.int64).to_numpy()
|
247
|
-
return snapped
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
# Datetime Parsing Methods:
|
253
|
-
|
254
|
-
@staticmethod
|
255
|
-
@log_call
|
256
|
-
def parse_ras_datetime_ms(datetime_str: str) -> datetime:
|
257
|
-
"""
|
258
|
-
Public method to parse a datetime string with milliseconds from a RAS file.
|
259
|
-
|
260
|
-
Args:
|
261
|
-
datetime_str (str): The datetime string to parse.
|
262
|
-
|
263
|
-
Returns:
|
264
|
-
datetime: The parsed datetime object.
|
265
|
-
"""
|
266
|
-
milliseconds = int(datetime_str[-3:])
|
267
|
-
microseconds = milliseconds * 1000
|
268
|
-
parsed_dt = HdfUtils.parse_ras_datetime(datetime_str[:-4]).replace(microsecond=microseconds)
|
269
|
-
return parsed_dt
|
270
|
-
|
271
|
-
# Rename to convert_timesteps_to_datetimes and make public
|
272
|
-
@staticmethod
|
273
|
-
def convert_timesteps_to_datetimes(timesteps: np.ndarray, start_time: datetime, time_unit: str = "days", round_to: str = "100ms") -> pd.DatetimeIndex:
|
274
|
-
"""
|
275
|
-
Convert RAS timesteps to datetime objects.
|
276
|
-
|
277
|
-
Args:
|
278
|
-
timesteps (np.ndarray): Array of timesteps.
|
279
|
-
start_time (datetime): Start time of the simulation.
|
280
|
-
time_unit (str): Unit of the timesteps. Default is "days".
|
281
|
-
round_to (str): Frequency string to round the times to. Default is "100ms" (100 milliseconds).
|
282
|
-
|
283
|
-
Returns:
|
284
|
-
pd.DatetimeIndex: DatetimeIndex of converted and rounded datetimes.
|
285
|
-
"""
|
286
|
-
if time_unit == "days":
|
287
|
-
datetimes = start_time + pd.to_timedelta(timesteps, unit='D')
|
288
|
-
elif time_unit == "hours":
|
289
|
-
datetimes = start_time + pd.to_timedelta(timesteps, unit='H')
|
290
|
-
else:
|
291
|
-
raise ValueError(f"Unsupported time unit: {time_unit}")
|
292
|
-
|
293
|
-
return pd.DatetimeIndex(datetimes).round(round_to)
|
294
|
-
|
295
|
-
# rename to convert_hdf5_attrs_to_dict and make public
|
296
|
-
|
297
|
-
@staticmethod
|
298
|
-
def convert_hdf5_attrs_to_dict(attrs: Union[h5py.AttributeManager, Dict], prefix: Optional[str] = None) -> Dict:
|
299
|
-
"""
|
300
|
-
Convert HDF5 attributes to a Python dictionary.
|
301
|
-
|
302
|
-
Args:
|
303
|
-
attrs (Union[h5py.AttributeManager, Dict]): The attributes to convert.
|
304
|
-
prefix (Optional[str]): A prefix to add to the attribute keys.
|
305
|
-
|
306
|
-
Returns:
|
307
|
-
Dict: A dictionary of converted attributes.
|
308
|
-
"""
|
309
|
-
result = {}
|
310
|
-
for key, value in attrs.items():
|
311
|
-
if prefix:
|
312
|
-
key = f"{prefix}/{key}"
|
313
|
-
if isinstance(value, (np.ndarray, list)):
|
314
|
-
result[key] = [HdfUtils.convert_ras_hdf_value(v) for v in value]
|
315
|
-
else:
|
316
|
-
result[key] = HdfUtils.convert_ras_hdf_value(value)
|
317
|
-
return result
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
@staticmethod
|
322
|
-
def parse_run_time_window(window: str) -> Tuple[datetime, datetime]:
|
323
|
-
"""
|
324
|
-
Parse a run time window string into a tuple of datetime objects.
|
325
|
-
|
326
|
-
Args:
|
327
|
-
window (str): The run time window string to be parsed.
|
328
|
-
|
329
|
-
Returns:
|
330
|
-
Tuple[datetime, datetime]: A tuple containing two datetime objects representing the start and end of the run
|
331
|
-
time window.
|
332
|
-
"""
|
333
|
-
split = window.split(" to ")
|
334
|
-
begin = HdfUtils._parse_ras_datetime(split[0])
|
335
|
-
end = HdfUtils._parse_ras_datetime(split[1])
|
336
|
-
return begin, end
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
## MOVED FROM HdfBase to HdfUtils:
|
354
|
-
# _parse_ras_datetime
|
355
|
-
# _parse_ras_simulation_window_datetime
|
356
|
-
# _parse_duration
|
357
|
-
# _parse_ras_datetime_ms
|
358
|
-
# _convert_ras_hdf_string
|
359
|
-
|
360
|
-
# Which were renamed and made public as:
|
361
|
-
# parse_ras_datetime
|
362
|
-
# parse_ras_window_datetime
|
363
|
-
# parse_ras_datetime_ms
|
364
|
-
# parse_ras_duration
|
365
|
-
# parse_ras_time_window
|
366
|
-
|
367
|
-
|
368
|
-
# Rename to parse_ras_datetime and make public
|
369
|
-
|
370
|
-
@staticmethod
|
371
|
-
def parse_ras_datetime(datetime_str: str) -> datetime:
|
372
|
-
"""
|
373
|
-
Parse a RAS datetime string into a datetime object.
|
374
|
-
|
375
|
-
Args:
|
376
|
-
datetime_str (str): The datetime string in format "ddMMMYYYY HH:MM:SS"
|
377
|
-
|
378
|
-
Returns:
|
379
|
-
datetime: The parsed datetime object.
|
380
|
-
"""
|
381
|
-
return datetime.strptime(datetime_str, "%d%b%Y %H:%M:%S")
|
382
|
-
|
383
|
-
# Rename to parse_ras_window_datetime and make public
|
384
|
-
|
385
|
-
@staticmethod
|
386
|
-
def parse_ras_window_datetime(datetime_str: str) -> datetime:
|
387
|
-
"""
|
388
|
-
Parse a datetime string from a RAS simulation window into a datetime object.
|
389
|
-
|
390
|
-
Args:
|
391
|
-
datetime_str (str): The datetime string to parse.
|
392
|
-
|
393
|
-
Returns:
|
394
|
-
datetime: The parsed datetime object.
|
395
|
-
"""
|
396
|
-
return datetime.strptime(datetime_str, "%d%b%Y %H%M")
|
397
|
-
|
398
|
-
|
399
|
-
# Rename to parse_duration and make public
|
400
|
-
|
401
|
-
|
402
|
-
@staticmethod
|
403
|
-
def parse_duration(duration_str: str) -> timedelta:
|
404
|
-
"""
|
405
|
-
Parse a duration string into a timedelta object.
|
406
|
-
|
407
|
-
Args:
|
408
|
-
duration_str (str): The duration string to parse.
|
409
|
-
|
410
|
-
Returns:
|
411
|
-
timedelta: The parsed duration as a timedelta object.
|
412
|
-
"""
|
413
|
-
hours, minutes, seconds = map(int, duration_str.split(':'))
|
414
|
-
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
415
|
-
|
416
|
-
|
417
|
-
# Rename to parse_ras_datetime_ms and make public
|
418
|
-
|
419
|
-
@staticmethod
|
420
|
-
def parse_ras_datetime_ms(datetime_str: str) -> datetime:
|
421
|
-
"""
|
422
|
-
Parse a datetime string with milliseconds from a RAS file.
|
423
|
-
|
424
|
-
Args:
|
425
|
-
datetime_str (str): The datetime string to parse.
|
426
|
-
|
427
|
-
Returns:
|
428
|
-
datetime: The parsed datetime object.
|
429
|
-
"""
|
430
|
-
milliseconds = int(datetime_str[-3:])
|
431
|
-
microseconds = milliseconds * 1000
|
432
|
-
parsed_dt = HdfUtils.parse_ras_datetime(datetime_str[:-4]).replace(microsecond=microseconds)
|
433
|
-
return parsed_dt
|
434
|
-
|
1
|
+
"""
|
2
|
+
HdfUtils Class
|
3
|
+
-------------
|
4
|
+
|
5
|
+
A utility class providing static methods for working with HEC-RAS HDF files.
|
6
|
+
|
7
|
+
Attribution:
|
8
|
+
A substantial amount of code in this file is sourced or derived from the
|
9
|
+
https://github.com/fema-ffrd/rashdf library, released under MIT license
|
10
|
+
and Copyright (c) 2024 fema-ffrd. The file has been forked and modified
|
11
|
+
for use in RAS Commander.
|
12
|
+
|
13
|
+
Key Features:
|
14
|
+
- HDF file data conversion and parsing
|
15
|
+
- DateTime handling for RAS-specific formats
|
16
|
+
- Spatial operations using KDTree
|
17
|
+
- HDF attribute management
|
18
|
+
|
19
|
+
Main Method Categories:
|
20
|
+
|
21
|
+
1. Data Conversion
|
22
|
+
- convert_ras_string: Convert RAS HDF strings to Python objects
|
23
|
+
- convert_ras_hdf_value: Convert general HDF values to Python objects
|
24
|
+
- convert_df_datetimes_to_str: Convert DataFrame datetime columns to strings
|
25
|
+
- convert_hdf5_attrs_to_dict: Convert HDF5 attributes to dictionary
|
26
|
+
- convert_timesteps_to_datetimes: Convert timesteps to datetime objects
|
27
|
+
|
28
|
+
2. Spatial Operations
|
29
|
+
- perform_kdtree_query: KDTree search between datasets
|
30
|
+
- find_nearest_neighbors: Find nearest neighbors within dataset
|
31
|
+
|
32
|
+
3. DateTime Parsing
|
33
|
+
- parse_ras_datetime: Parse standard RAS datetime format (ddMMMYYYY HH:MM:SS)
|
34
|
+
- parse_ras_window_datetime: Parse simulation window datetime (ddMMMYYYY HHMM)
|
35
|
+
- parse_duration: Parse duration strings (HH:MM:SS)
|
36
|
+
- parse_ras_datetime_ms: Parse datetime with milliseconds
|
37
|
+
- parse_run_time_window: Parse time window strings
|
38
|
+
|
39
|
+
Usage Notes:
|
40
|
+
- All methods are static and can be called without class instantiation
|
41
|
+
- Methods handle both raw HDF data and converted Python objects
|
42
|
+
- Includes comprehensive error handling for RAS-specific data formats
|
43
|
+
- Supports various RAS datetime formats and conversions
|
44
|
+
"""
|
45
|
+
import logging
|
46
|
+
from pathlib import Path
|
47
|
+
import h5py
|
48
|
+
import numpy as np
|
49
|
+
import pandas as pd
|
50
|
+
from datetime import datetime, timedelta
|
51
|
+
from typing import Union, Optional, Dict, List, Tuple, Any
|
52
|
+
from scipy.spatial import KDTree
|
53
|
+
import re
|
54
|
+
from shapely.geometry import LineString # Import LineString to avoid NameError
|
55
|
+
|
56
|
+
from .Decorators import standardize_input, log_call
|
57
|
+
from .LoggingConfig import setup_logging, get_logger
|
58
|
+
|
59
|
+
logger = get_logger(__name__)
|
60
|
+
|
61
|
+
class HdfUtils:
|
62
|
+
"""
|
63
|
+
Utility class for working with HEC-RAS HDF files.
|
64
|
+
|
65
|
+
This class provides general utility functions for HDF file operations,
|
66
|
+
including attribute extraction, data conversion, and common HDF queries.
|
67
|
+
It also includes spatial operations and helper methods for working with
|
68
|
+
HEC-RAS specific data structures.
|
69
|
+
|
70
|
+
Note:
|
71
|
+
- Use this class for general HDF utility functions that are not specific to plan or geometry files.
|
72
|
+
- All methods in this class are static and can be called without instantiating the class.
|
73
|
+
"""
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
# RENAME TO convert_ras_string and make public
|
79
|
+
|
80
|
+
@staticmethod
|
81
|
+
def convert_ras_string(value: Union[str, bytes]) -> Union[bool, datetime, List[datetime], timedelta, str]:
|
82
|
+
"""
|
83
|
+
Convert a string value from an HEC-RAS HDF file into a Python object.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
value (Union[str, bytes]): The value to convert.
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
Union[bool, datetime, List[datetime], timedelta, str]: The converted value.
|
90
|
+
"""
|
91
|
+
if isinstance(value, bytes):
|
92
|
+
s = value.decode("utf-8")
|
93
|
+
else:
|
94
|
+
s = value
|
95
|
+
|
96
|
+
if s == "True":
|
97
|
+
return True
|
98
|
+
elif s == "False":
|
99
|
+
return False
|
100
|
+
|
101
|
+
ras_datetime_format1_re = r"\d{2}\w{3}\d{4} \d{2}:\d{2}:\d{2}"
|
102
|
+
ras_datetime_format2_re = r"\d{2}\w{3}\d{4} \d{2}\d{2}"
|
103
|
+
ras_duration_format_re = r"\d{2}:\d{2}:\d{2}"
|
104
|
+
|
105
|
+
if re.match(rf"^{ras_datetime_format1_re}", s):
|
106
|
+
if re.match(rf"^{ras_datetime_format1_re} to {ras_datetime_format1_re}$", s):
|
107
|
+
split = s.split(" to ")
|
108
|
+
return [
|
109
|
+
HdfUtils.parse_ras_datetime(split[0]),
|
110
|
+
HdfUtils.parse_ras_datetime(split[1]),
|
111
|
+
]
|
112
|
+
return HdfUtils.parse_ras_datetime(s)
|
113
|
+
elif re.match(rf"^{ras_datetime_format2_re}", s):
|
114
|
+
if re.match(rf"^{ras_datetime_format2_re} to {ras_datetime_format2_re}$", s):
|
115
|
+
split = s.split(" to ")
|
116
|
+
return [
|
117
|
+
HdfUtils.parse_ras_window_datetime(split[0]),
|
118
|
+
HdfUtils.parse_ras_window_datetime(split[1]),
|
119
|
+
]
|
120
|
+
return HdfUtils.parse_ras_window_datetime(s)
|
121
|
+
elif re.match(rf"^{ras_duration_format_re}$", s):
|
122
|
+
return HdfUtils.parse_ras_duration(s)
|
123
|
+
return s
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
|
129
|
+
@staticmethod
|
130
|
+
def convert_ras_hdf_value(value: Any) -> Union[None, bool, str, List[str], int, float, List[int], List[float]]:
|
131
|
+
"""
|
132
|
+
Convert a value from a HEC-RAS HDF file into a Python object.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
value (Any): The value to convert.
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Union[None, bool, str, List[str], int, float, List[int], List[float]]: The converted value.
|
139
|
+
"""
|
140
|
+
if isinstance(value, np.floating) and np.isnan(value):
|
141
|
+
return None
|
142
|
+
elif isinstance(value, (bytes, np.bytes_)):
|
143
|
+
return value.decode('utf-8')
|
144
|
+
elif isinstance(value, np.integer):
|
145
|
+
return int(value)
|
146
|
+
elif isinstance(value, np.floating):
|
147
|
+
return float(value)
|
148
|
+
elif isinstance(value, (int, float)):
|
149
|
+
return value
|
150
|
+
elif isinstance(value, (list, tuple, np.ndarray)):
|
151
|
+
if len(value) > 1:
|
152
|
+
return [HdfUtils.convert_ras_hdf_value(v) for v in value]
|
153
|
+
else:
|
154
|
+
return HdfUtils.convert_ras_hdf_value(value[0])
|
155
|
+
else:
|
156
|
+
return str(value)
|
157
|
+
|
158
|
+
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
# RENAME TO convert_df_datetimes_to_str
|
168
|
+
|
169
|
+
@staticmethod
|
170
|
+
def convert_df_datetimes_to_str(df: pd.DataFrame) -> pd.DataFrame:
|
171
|
+
"""
|
172
|
+
Convert any datetime64 columns in a DataFrame to strings.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
df (pd.DataFrame): The DataFrame to convert.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
pd.DataFrame: The DataFrame with datetime columns converted to strings.
|
179
|
+
"""
|
180
|
+
for col in df.select_dtypes(include=['datetime64']).columns:
|
181
|
+
df[col] = df[col].dt.strftime('%Y-%m-%d %H:%M:%S')
|
182
|
+
return df
|
183
|
+
|
184
|
+
|
185
|
+
# KDTree Methods:
|
186
|
+
|
187
|
+
|
188
|
+
@staticmethod
|
189
|
+
def perform_kdtree_query(
|
190
|
+
reference_points: np.ndarray,
|
191
|
+
query_points: np.ndarray,
|
192
|
+
max_distance: float = 2.0
|
193
|
+
) -> np.ndarray:
|
194
|
+
"""
|
195
|
+
Performs a KDTree query between two datasets and returns indices with distances exceeding max_distance set to -1.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
reference_points (np.ndarray): The reference dataset for KDTree.
|
199
|
+
query_points (np.ndarray): The query dataset to search against KDTree of reference_points.
|
200
|
+
max_distance (float, optional): The maximum distance threshold. Indices with distances greater than this are set to -1. Defaults to 2.0.
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
np.ndarray: Array of indices from reference_points that are nearest to each point in query_points.
|
204
|
+
Indices with distances > max_distance are set to -1.
|
205
|
+
|
206
|
+
Example:
|
207
|
+
>>> ref_points = np.array([[0, 0], [1, 1], [2, 2]])
|
208
|
+
>>> query_points = np.array([[0.5, 0.5], [3, 3]])
|
209
|
+
>>> result = HdfUtils.perform_kdtree_query(ref_points, query_points)
|
210
|
+
>>> print(result)
|
211
|
+
array([ 0, -1])
|
212
|
+
"""
|
213
|
+
dist, snap = KDTree(reference_points).query(query_points, distance_upper_bound=max_distance)
|
214
|
+
snap[dist > max_distance] = -1
|
215
|
+
return snap
|
216
|
+
|
217
|
+
@staticmethod
|
218
|
+
def find_nearest_neighbors(points: np.ndarray, max_distance: float = 2.0) -> np.ndarray:
|
219
|
+
"""
|
220
|
+
Creates a self KDTree for dataset points and finds nearest neighbors excluding self,
|
221
|
+
with distances above max_distance set to -1.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
points (np.ndarray): The dataset to build the KDTree from and query against itself.
|
225
|
+
max_distance (float, optional): The maximum distance threshold. Indices with distances
|
226
|
+
greater than max_distance are set to -1. Defaults to 2.0.
|
227
|
+
|
228
|
+
Returns:
|
229
|
+
np.ndarray: Array of indices representing the nearest neighbor in points for each point in points.
|
230
|
+
Indices with distances > max_distance or self-matches are set to -1.
|
231
|
+
|
232
|
+
Example:
|
233
|
+
>>> points = np.array([[0, 0], [1, 1], [2, 2], [10, 10]])
|
234
|
+
>>> result = HdfUtils.find_nearest_neighbors(points)
|
235
|
+
>>> print(result)
|
236
|
+
array([1, 0, 1, -1])
|
237
|
+
"""
|
238
|
+
dist, snap = KDTree(points).query(points, k=2, distance_upper_bound=max_distance)
|
239
|
+
snap[dist > max_distance] = -1
|
240
|
+
|
241
|
+
snp = pd.DataFrame(snap, index=np.arange(len(snap)))
|
242
|
+
snp = snp.replace(-1, np.nan)
|
243
|
+
snp.loc[snp[0] == snp.index, 0] = np.nan
|
244
|
+
snp.loc[snp[1] == snp.index, 1] = np.nan
|
245
|
+
filled = snp[0].fillna(snp[1])
|
246
|
+
snapped = filled.fillna(-1).astype(np.int64).to_numpy()
|
247
|
+
return snapped
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
|
252
|
+
# Datetime Parsing Methods:
|
253
|
+
|
254
|
+
@staticmethod
|
255
|
+
@log_call
|
256
|
+
def parse_ras_datetime_ms(datetime_str: str) -> datetime:
|
257
|
+
"""
|
258
|
+
Public method to parse a datetime string with milliseconds from a RAS file.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
datetime_str (str): The datetime string to parse.
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
datetime: The parsed datetime object.
|
265
|
+
"""
|
266
|
+
milliseconds = int(datetime_str[-3:])
|
267
|
+
microseconds = milliseconds * 1000
|
268
|
+
parsed_dt = HdfUtils.parse_ras_datetime(datetime_str[:-4]).replace(microsecond=microseconds)
|
269
|
+
return parsed_dt
|
270
|
+
|
271
|
+
# Rename to convert_timesteps_to_datetimes and make public
|
272
|
+
@staticmethod
|
273
|
+
def convert_timesteps_to_datetimes(timesteps: np.ndarray, start_time: datetime, time_unit: str = "days", round_to: str = "100ms") -> pd.DatetimeIndex:
|
274
|
+
"""
|
275
|
+
Convert RAS timesteps to datetime objects.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
timesteps (np.ndarray): Array of timesteps.
|
279
|
+
start_time (datetime): Start time of the simulation.
|
280
|
+
time_unit (str): Unit of the timesteps. Default is "days".
|
281
|
+
round_to (str): Frequency string to round the times to. Default is "100ms" (100 milliseconds).
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
pd.DatetimeIndex: DatetimeIndex of converted and rounded datetimes.
|
285
|
+
"""
|
286
|
+
if time_unit == "days":
|
287
|
+
datetimes = start_time + pd.to_timedelta(timesteps, unit='D')
|
288
|
+
elif time_unit == "hours":
|
289
|
+
datetimes = start_time + pd.to_timedelta(timesteps, unit='H')
|
290
|
+
else:
|
291
|
+
raise ValueError(f"Unsupported time unit: {time_unit}")
|
292
|
+
|
293
|
+
return pd.DatetimeIndex(datetimes).round(round_to)
|
294
|
+
|
295
|
+
# rename to convert_hdf5_attrs_to_dict and make public
|
296
|
+
|
297
|
+
@staticmethod
|
298
|
+
def convert_hdf5_attrs_to_dict(attrs: Union[h5py.AttributeManager, Dict], prefix: Optional[str] = None) -> Dict:
|
299
|
+
"""
|
300
|
+
Convert HDF5 attributes to a Python dictionary.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
attrs (Union[h5py.AttributeManager, Dict]): The attributes to convert.
|
304
|
+
prefix (Optional[str]): A prefix to add to the attribute keys.
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
Dict: A dictionary of converted attributes.
|
308
|
+
"""
|
309
|
+
result = {}
|
310
|
+
for key, value in attrs.items():
|
311
|
+
if prefix:
|
312
|
+
key = f"{prefix}/{key}"
|
313
|
+
if isinstance(value, (np.ndarray, list)):
|
314
|
+
result[key] = [HdfUtils.convert_ras_hdf_value(v) for v in value]
|
315
|
+
else:
|
316
|
+
result[key] = HdfUtils.convert_ras_hdf_value(value)
|
317
|
+
return result
|
318
|
+
|
319
|
+
|
320
|
+
|
321
|
+
@staticmethod
|
322
|
+
def parse_run_time_window(window: str) -> Tuple[datetime, datetime]:
|
323
|
+
"""
|
324
|
+
Parse a run time window string into a tuple of datetime objects.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
window (str): The run time window string to be parsed.
|
328
|
+
|
329
|
+
Returns:
|
330
|
+
Tuple[datetime, datetime]: A tuple containing two datetime objects representing the start and end of the run
|
331
|
+
time window.
|
332
|
+
"""
|
333
|
+
split = window.split(" to ")
|
334
|
+
begin = HdfUtils._parse_ras_datetime(split[0])
|
335
|
+
end = HdfUtils._parse_ras_datetime(split[1])
|
336
|
+
return begin, end
|
337
|
+
|
338
|
+
|
339
|
+
|
340
|
+
|
341
|
+
|
342
|
+
|
343
|
+
|
344
|
+
|
345
|
+
|
346
|
+
|
347
|
+
|
348
|
+
|
349
|
+
|
350
|
+
|
351
|
+
|
352
|
+
|
353
|
+
## MOVED FROM HdfBase to HdfUtils:
|
354
|
+
# _parse_ras_datetime
|
355
|
+
# _parse_ras_simulation_window_datetime
|
356
|
+
# _parse_duration
|
357
|
+
# _parse_ras_datetime_ms
|
358
|
+
# _convert_ras_hdf_string
|
359
|
+
|
360
|
+
# Which were renamed and made public as:
|
361
|
+
# parse_ras_datetime
|
362
|
+
# parse_ras_window_datetime
|
363
|
+
# parse_ras_datetime_ms
|
364
|
+
# parse_ras_duration
|
365
|
+
# parse_ras_time_window
|
366
|
+
|
367
|
+
|
368
|
+
# Rename to parse_ras_datetime and make public
|
369
|
+
|
370
|
+
@staticmethod
|
371
|
+
def parse_ras_datetime(datetime_str: str) -> datetime:
|
372
|
+
"""
|
373
|
+
Parse a RAS datetime string into a datetime object.
|
374
|
+
|
375
|
+
Args:
|
376
|
+
datetime_str (str): The datetime string in format "ddMMMYYYY HH:MM:SS"
|
377
|
+
|
378
|
+
Returns:
|
379
|
+
datetime: The parsed datetime object.
|
380
|
+
"""
|
381
|
+
return datetime.strptime(datetime_str, "%d%b%Y %H:%M:%S")
|
382
|
+
|
383
|
+
# Rename to parse_ras_window_datetime and make public
|
384
|
+
|
385
|
+
@staticmethod
|
386
|
+
def parse_ras_window_datetime(datetime_str: str) -> datetime:
|
387
|
+
"""
|
388
|
+
Parse a datetime string from a RAS simulation window into a datetime object.
|
389
|
+
|
390
|
+
Args:
|
391
|
+
datetime_str (str): The datetime string to parse.
|
392
|
+
|
393
|
+
Returns:
|
394
|
+
datetime: The parsed datetime object.
|
395
|
+
"""
|
396
|
+
return datetime.strptime(datetime_str, "%d%b%Y %H%M")
|
397
|
+
|
398
|
+
|
399
|
+
# Rename to parse_duration and make public
|
400
|
+
|
401
|
+
|
402
|
+
@staticmethod
|
403
|
+
def parse_duration(duration_str: str) -> timedelta:
|
404
|
+
"""
|
405
|
+
Parse a duration string into a timedelta object.
|
406
|
+
|
407
|
+
Args:
|
408
|
+
duration_str (str): The duration string to parse.
|
409
|
+
|
410
|
+
Returns:
|
411
|
+
timedelta: The parsed duration as a timedelta object.
|
412
|
+
"""
|
413
|
+
hours, minutes, seconds = map(int, duration_str.split(':'))
|
414
|
+
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
415
|
+
|
416
|
+
|
417
|
+
# Rename to parse_ras_datetime_ms and make public
|
418
|
+
|
419
|
+
@staticmethod
|
420
|
+
def parse_ras_datetime_ms(datetime_str: str) -> datetime:
|
421
|
+
"""
|
422
|
+
Parse a datetime string with milliseconds from a RAS file.
|
423
|
+
|
424
|
+
Args:
|
425
|
+
datetime_str (str): The datetime string to parse.
|
426
|
+
|
427
|
+
Returns:
|
428
|
+
datetime: The parsed datetime object.
|
429
|
+
"""
|
430
|
+
milliseconds = int(datetime_str[-3:])
|
431
|
+
microseconds = milliseconds * 1000
|
432
|
+
parsed_dt = HdfUtils.parse_ras_datetime(datetime_str[:-4]).replace(microsecond=microseconds)
|
433
|
+
return parsed_dt
|
434
|
+
|
435
435
|
|