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/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