ras-commander 0.42.0__py3-none-any.whl → 0.43.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.
@@ -0,0 +1,111 @@
1
+ from functools import wraps
2
+ from pathlib import Path
3
+ from typing import Union
4
+ import logging
5
+ import h5py
6
+ import inspect
7
+
8
+
9
+ def log_call(func):
10
+ @wraps(func)
11
+ def wrapper(*args, **kwargs):
12
+ logger = logging.getLogger(func.__module__)
13
+ logger.info(f"Calling {func.__name__}")
14
+ result = func(*args, **kwargs)
15
+ logger.info(f"Finished {func.__name__}")
16
+ return result
17
+ return wrapper
18
+
19
+
20
+ def standardize_input(file_type: str = 'plan_hdf'):
21
+ """
22
+ Decorator to standardize input for HDF file operations.
23
+
24
+ This decorator processes various input types and converts them to a Path object
25
+ pointing to the correct HDF file. It handles the following input types:
26
+ - h5py.File objects
27
+ - pathlib.Path objects
28
+ - Strings (file paths or plan/geom numbers)
29
+ - Integers (interpreted as plan/geom numbers)
30
+
31
+ The decorator also manages RAS object references and logging.
32
+
33
+ Args:
34
+ file_type (str): Specifies whether to look for 'plan_hdf' or 'geom_hdf' files.
35
+
36
+ Returns:
37
+ A decorator that wraps the function to standardize its input to a Path object.
38
+ """
39
+ def decorator(func):
40
+ @wraps(func)
41
+ def wrapper(*args, **kwargs):
42
+ logger = logging.getLogger(func.__module__)
43
+
44
+ # Handle both static method calls and regular function calls
45
+ if args and isinstance(args[0], type):
46
+ # Static method call, remove the class argument
47
+ args = args[1:]
48
+
49
+ hdf_input = kwargs.pop('hdf_path', None) or kwargs.pop('hdf_input', None) or (args[0] if args else None)
50
+ ras_object = kwargs.pop('ras_object', None) or (args[1] if len(args) > 1 else None)
51
+
52
+ hdf_path = None
53
+
54
+ # If hdf_input is already an h5py.File object, use its filename
55
+ if isinstance(hdf_input, h5py.File):
56
+ hdf_path = Path(hdf_input.filename)
57
+ # Handle Path objects
58
+ elif isinstance(hdf_input, Path):
59
+ if hdf_input.is_file():
60
+ hdf_path = hdf_input
61
+ # Handle string inputs
62
+ elif isinstance(hdf_input, str):
63
+ # Check if it's a file path
64
+ if Path(hdf_input).is_file():
65
+ hdf_path = Path(hdf_input)
66
+ # Check if it's a number (with or without 'p' prefix)
67
+ elif hdf_input.isdigit() or (len(hdf_input) == 3 and hdf_input[0] == 'p' and hdf_input[1:].isdigit()):
68
+ if ras_object is None:
69
+ raise ValueError("RAS object is required when using plan or geom numbers.")
70
+ number = hdf_input if hdf_input.isdigit() else hdf_input[1:]
71
+
72
+ if file_type == 'plan_hdf':
73
+ plan_info = ras_object.plan_df[ras_object.plan_df['plan_number'] == number]
74
+ if not plan_info.empty:
75
+ hdf_path = Path(plan_info.iloc[0]['HDF_Results_Path'])
76
+ elif file_type == 'geom_hdf':
77
+ geom_info = ras_object.geom_df[ras_object.geom_df['geom_number'] == number]
78
+ if not geom_info.empty:
79
+ hdf_path = Path(geom_info.iloc[0]['HDF_Path'])
80
+ else:
81
+ raise ValueError(f"Invalid file type: {file_type}")
82
+ # Handle integer inputs (assuming they're plan or geom numbers)
83
+ elif isinstance(hdf_input, int):
84
+ if ras_object is None:
85
+ raise ValueError("RAS object is required when using plan or geom numbers.")
86
+ number = f"{hdf_input:02d}"
87
+
88
+ if file_type == 'plan_hdf':
89
+ plan_info = ras_object.plan_df[ras_object.plan_df['plan_number'] == number]
90
+ if not plan_info.empty:
91
+ hdf_path = Path(plan_info.iloc[0]['HDF_Results_Path'])
92
+ elif file_type == 'geom_hdf':
93
+ geom_info = ras_object.geom_df[ras_object.geom_df['geom_number'] == number]
94
+ if not geom_info.empty:
95
+ hdf_path = Path(geom_info.iloc[0]['HDF_Path'])
96
+ else:
97
+ raise ValueError(f"Invalid file type: {file_type}")
98
+
99
+ if hdf_path is None or not hdf_path.is_file():
100
+ error_msg = f"HDF file not found: {hdf_input}"
101
+ logger.error(error_msg)
102
+ raise FileNotFoundError(error_msg)
103
+
104
+ logger.info(f"Using HDF file: {hdf_path}")
105
+
106
+ # Pass all original arguments and keywords, replacing hdf_input with standardized hdf_path
107
+ new_args = (hdf_path,) + args[1:]
108
+ return func(*new_args, **kwargs)
109
+
110
+ return wrapper
111
+ return decorator
@@ -0,0 +1,189 @@
1
+ import re
2
+ from datetime import datetime, timedelta
3
+ import h5py
4
+ import numpy as np
5
+ import pandas as pd
6
+ import xarray as xr # Added import for xarray
7
+ from typing import List, Tuple, Union, Optional, Dict
8
+ from pathlib import Path
9
+ import logging
10
+ import dask.array as da
11
+
12
+ from .HdfUtils import HdfUtils
13
+ from .Decorators import standardize_input, log_call
14
+ from .LoggingConfig import setup_logging, get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ class HdfBase:
19
+ """
20
+ Base class for HEC-RAS HDF file operations.
21
+
22
+ This class provides fundamental methods for interacting with HEC-RAS HDF files,
23
+ including time-related operations and mesh data retrieval. It serves as a foundation
24
+ for more specialized HDF classes.
25
+
26
+ The methods in this class are designed to work with both plan and geometry HDF files,
27
+ providing low-level access to file structure and content.
28
+
29
+ Note:
30
+ - All methods in this class are static, allowing for use without instantiation.
31
+ - This class is not meant to be used directly in most cases, but rather as a base
32
+ for more specialized HDF classes.
33
+ """
34
+
35
+ @staticmethod
36
+ def _get_simulation_start_time(hdf_file: h5py.File) -> datetime:
37
+ """
38
+ Get the simulation start time from the HDF file.
39
+
40
+ Args:
41
+ hdf_file (h5py.File): Open HDF file object.
42
+
43
+ Returns:
44
+ datetime: The simulation start time.
45
+
46
+ Raises:
47
+ ValueError: If Plan Information is not found in the HDF file.
48
+ """
49
+ plan_info = hdf_file.get("Plan Data/Plan Information")
50
+ if plan_info is None:
51
+ raise ValueError("Plan Information not found in HDF file")
52
+ time_str = plan_info.attrs.get('Simulation Start Time')
53
+ return datetime.strptime(time_str.decode('utf-8'), "%d%b%Y %H:%M:%S")
54
+
55
+ @staticmethod
56
+ def _get_unsteady_datetimes(hdf_file: h5py.File) -> List[datetime]:
57
+ """
58
+ Get the list of unsteady datetimes from the HDF file.
59
+
60
+ Args:
61
+ hdf_file (h5py.File): Open HDF file object.
62
+
63
+ Returns:
64
+ List[datetime]: A list of datetime objects representing the unsteady timestamps.
65
+ """
66
+ group_path = "Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Time Date Stamp (ms)"
67
+ raw_datetimes = hdf_file[group_path][:]
68
+ return [HdfBase._parse_ras_datetime_ms(x.decode("utf-8")) for x in raw_datetimes]
69
+
70
+
71
+ @staticmethod
72
+ def _get_2d_flow_area_names_and_counts(hdf_file: h5py.File) -> List[Tuple[str, int]]:
73
+ """
74
+ Get the names and cell counts of 2D flow areas from the HDF file.
75
+
76
+ Args:
77
+ hdf_file (h5py.File): Open HDF file object.
78
+
79
+ Returns:
80
+ List[Tuple[str, int]]: A list of tuples containing the name and cell count of each 2D flow area.
81
+ """
82
+ d2_flow_areas = hdf_file.get("Geometry/2D Flow Areas/Attributes")
83
+ if d2_flow_areas is None:
84
+ return []
85
+ return [(HdfBase._convert_ras_hdf_string(d2_flow_area[0]), d2_flow_area[-1]) for d2_flow_area in d2_flow_areas[:]]
86
+
87
+ @staticmethod
88
+ def _parse_ras_datetime(datetime_str: str) -> datetime:
89
+ """
90
+ Parse a datetime string from a RAS file into a datetime object.
91
+
92
+ Args:
93
+ datetime_str (str): The datetime string to parse.
94
+
95
+ Returns:
96
+ datetime: The parsed datetime object.
97
+ """
98
+ return datetime.strptime(datetime_str, "%d%b%Y %H:%M:%S")
99
+
100
+ @staticmethod
101
+ def _parse_ras_simulation_window_datetime(datetime_str: str) -> datetime:
102
+ """
103
+ Parse a datetime string from a RAS simulation window into a datetime object.
104
+
105
+ Args:
106
+ datetime_str (str): The datetime string to parse.
107
+
108
+ Returns:
109
+ datetime: The parsed datetime object.
110
+ """
111
+ return datetime.strptime(datetime_str, "%d%b%Y %H%M")
112
+
113
+ @staticmethod
114
+ def _parse_duration(duration_str: str) -> timedelta:
115
+ """
116
+ Parse a duration string into a timedelta object.
117
+
118
+ Args:
119
+ duration_str (str): The duration string to parse.
120
+
121
+ Returns:
122
+ timedelta: The parsed duration as a timedelta object.
123
+ """
124
+ hours, minutes, seconds = map(int, duration_str.split(':'))
125
+ return timedelta(hours=hours, minutes=minutes, seconds=seconds)
126
+
127
+ @staticmethod
128
+ def _parse_ras_datetime_ms(datetime_str: str) -> datetime:
129
+ """
130
+ Parse a datetime string with milliseconds from a RAS file.
131
+
132
+ Args:
133
+ datetime_str (str): The datetime string to parse.
134
+
135
+ Returns:
136
+ datetime: The parsed datetime object.
137
+ """
138
+ milliseconds = int(datetime_str[-3:])
139
+ microseconds = milliseconds * 1000
140
+ parsed_dt = HdfBase._parse_ras_datetime(datetime_str[:-4]).replace(microsecond=microseconds)
141
+ return parsed_dt
142
+
143
+ @staticmethod
144
+ def _convert_ras_hdf_string(value: Union[str, bytes]) -> Union[bool, datetime, List[datetime], timedelta, str]:
145
+ """
146
+ Convert a string value from an HEC-RAS HDF file into a Python object.
147
+
148
+ Args:
149
+ value (Union[str, bytes]): The value to convert.
150
+
151
+ Returns:
152
+ Union[bool, datetime, List[datetime], timedelta, str]: The converted value.
153
+ """
154
+ if isinstance(value, bytes):
155
+ s = value.decode("utf-8")
156
+ else:
157
+ s = value
158
+
159
+ if s == "True":
160
+ return True
161
+ elif s == "False":
162
+ return False
163
+
164
+ ras_datetime_format1_re = r"\d{2}\w{3}\d{4} \d{2}:\d{2}:\d{2}"
165
+ ras_datetime_format2_re = r"\d{2}\w{3}\d{4} \d{2}\d{2}"
166
+ ras_duration_format_re = r"\d{2}:\d{2}:\d{2}"
167
+
168
+ if re.match(rf"^{ras_datetime_format1_re}", s):
169
+ if re.match(rf"^{ras_datetime_format1_re} to {ras_datetime_format1_re}$", s):
170
+ split = s.split(" to ")
171
+ return [
172
+ HdfBase._parse_ras_datetime(split[0]),
173
+ HdfBase._parse_ras_datetime(split[1]),
174
+ ]
175
+ return HdfBase._parse_ras_datetime(s)
176
+ elif re.match(rf"^{ras_datetime_format2_re}", s):
177
+ if re.match(rf"^{ras_datetime_format2_re} to {ras_datetime_format2_re}$", s):
178
+ split = s.split(" to ")
179
+ return [
180
+ HdfBase._parse_ras_simulation_window_datetime(split[0]),
181
+ HdfBase._parse_ras_simulation_window_datetime(split[1]),
182
+ ]
183
+ return HdfBase._parse_ras_simulation_window_datetime(s)
184
+ elif re.match(rf"^{ras_duration_format_re}$", s):
185
+ return HdfBase._parse_duration(s)
186
+ return s
187
+
188
+
189
+