ras-commander 0.44.0__py3-none-any.whl → 0.46.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.
@@ -8,20 +8,21 @@ released under MIT license and Copyright (c) 2024 fema-ffrd
8
8
  The file has been forked and modified for use in RAS Commander.
9
9
  """
10
10
 
11
+ from pathlib import Path
12
+ from typing import Union, Optional, List, Dict, Tuple
13
+
11
14
  import h5py
12
15
  import numpy as np
13
16
  import pandas as pd
14
- from pathlib import Path
15
- from typing import Union, Optional, List
17
+ import xarray as xr
18
+
16
19
  from .HdfBase import HdfBase
17
20
  from .HdfUtils import HdfUtils
18
21
  from .Decorators import standardize_input, log_call
19
- from .LoggingConfig import setup_logging, get_logger
20
- import xarray as xr
22
+ from .LoggingConfig import get_logger
21
23
 
22
24
  logger = get_logger(__name__)
23
25
 
24
-
25
26
  class HdfResultsXsec:
26
27
  """
27
28
  A class for handling cross-section results from HEC-RAS HDF files.
@@ -36,202 +37,236 @@ class HdfResultsXsec:
36
37
  Attributes:
37
38
  None
38
39
 
39
- Methods:
40
- steady_profile_xs_output: Extract steady profile cross-section output for a specified variable.
41
- cross_sections_wsel: Get water surface elevation data for cross-sections.
42
- cross_sections_flow: Get flow data for cross-sections.
43
- cross_sections_energy_grade: Get energy grade data for cross-sections.
44
- cross_sections_additional_enc_station_left: Get left encroachment station data for cross-sections.
45
- cross_sections_additional_enc_station_right: Get right encroachment station data for cross-sections.
46
- cross_sections_additional_area_total: Get total ineffective area data for cross-sections.
47
- cross_sections_additional_velocity_total: Get total velocity data for cross-sections.
40
+
48
41
  """
49
42
 
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
50
54
  @staticmethod
55
+ @log_call
51
56
  @standardize_input(file_type='plan_hdf')
52
- def steady_profile_xs_output(hdf_path: Path, var: str, round_to: int = 2) -> pd.DataFrame:
57
+ def get_pump_station_profile_output(hdf_path: Path) -> pd.DataFrame:
53
58
  """
54
- Create a DataFrame from steady cross section results based on the specified variable.
59
+ Extract pump station profile output data from the HDF file.
55
60
 
56
- Parameters:
57
- ----------
58
- hdf_path : Path
59
- Path to the HEC-RAS plan HDF file.
60
- var : str
61
- The variable to extract from the steady cross section results.
62
- round_to : int, optional
63
- Number of decimal places to round the results to (default is 2).
61
+ Args:
62
+ hdf_path (Path): Path to the HDF file.
64
63
 
65
64
  Returns:
66
- -------
67
- pd.DataFrame
68
- DataFrame containing the steady cross section results for the specified variable.
65
+ pd.DataFrame: DataFrame containing pump station profile output data.
66
+
67
+ Raises:
68
+ KeyError: If the required datasets are not found in the HDF file.
69
69
  """
70
- XS_STEADY_OUTPUT_ADDITIONAL = [
71
- "Additional Encroachment Station Left",
72
- "Additional Encroachment Station Right",
73
- "Additional Area Ineffective Total",
74
- "Additional Velocity Total",
75
- ]
76
-
77
70
  try:
78
- with h5py.File(hdf_path, 'r') as hdf_file:
79
- # Determine the correct path based on the variable
80
- if var in XS_STEADY_OUTPUT_ADDITIONAL:
81
- path = f"/Results/Steady/Cross Sections/Additional Output/{var}"
82
- else:
83
- path = f"/Results/Steady/Cross Sections/{var}"
84
-
85
- # Check if the path exists in the HDF file
86
- if path not in hdf_file:
71
+ with h5py.File(hdf_path, 'r') as hdf:
72
+ # Extract profile output data
73
+ profile_path = "/Results/Unsteady/Output/Output Blocks/DSS Profile Output/Unsteady Time Series/Pumping Stations"
74
+ if profile_path not in hdf:
75
+ logger.warning("Pump Station profile output data not found in HDF file")
87
76
  return pd.DataFrame()
88
77
 
89
- # Get the profile names
90
- profiles = HdfBase.steady_flow_names(hdf_path)
91
-
92
- # Extract the steady data
93
- steady_data = hdf_file[path]
94
-
95
- # Create a DataFrame with profiles as index
96
- df = pd.DataFrame(steady_data, index=profiles)
97
-
98
- # Transpose the DataFrame and round values
99
- df_t = df.T.copy()
100
- for p in profiles:
101
- df_t[p] = df_t[p].apply(lambda x: round(x, round_to))
102
-
103
- return df_t
78
+ # Initialize an empty list to store data from all pump stations
79
+ all_data = []
80
+
81
+ # Iterate through all pump stations
82
+ for station in hdf[profile_path].keys():
83
+ station_path = f"{profile_path}/{station}/Structure Variables"
84
+
85
+ data = hdf[station_path][()]
86
+
87
+ # Create a DataFrame for this pump station
88
+ df = pd.DataFrame(data, columns=['Flow', 'Stage HW', 'Stage TW', 'Pump Station', 'Pumps on'])
89
+ df['Station'] = station
90
+
91
+ all_data.append(df)
92
+
93
+ # Concatenate all DataFrames
94
+ result_df = pd.concat(all_data, ignore_index=True)
95
+
96
+ # Add time information
97
+ time = HdfBase._get_unsteady_datetimes(hdf)
98
+ result_df['Time'] = [time[i] for i in result_df.index]
99
+
100
+ return result_df
101
+
102
+ except KeyError as e:
103
+ logger.error(f"Required dataset not found in HDF file: {e}")
104
+ raise
104
105
  except Exception as e:
105
- HdfUtils.logger.error(f"Failed to get steady profile cross section output: {str(e)}")
106
- return pd.DataFrame()
106
+ logger.error(f"Error extracting pump station profile output data: {e}")
107
+ raise
108
+
107
109
 
108
- @staticmethod
109
- @standardize_input(file_type='plan_hdf')
110
- def cross_sections_wsel(hdf_path: Path) -> pd.DataFrame:
111
- """
112
- Return the water surface elevation information for each 1D Cross Section.
113
110
 
114
- Parameters:
115
- ----------
116
- hdf_path : Path
117
- Path to the HEC-RAS plan HDF file.
118
111
 
119
- Returns:
120
- -------
121
- pd.DataFrame
122
- A DataFrame containing the water surface elevations for each cross section and event.
123
- """
124
- return HdfResultsXsec.steady_profile_xs_output(hdf_path, "Water Surface")
125
112
 
126
- @staticmethod
127
- @standardize_input(file_type='plan_hdf')
128
- def cross_sections_flow(hdf_path: Path) -> pd.DataFrame:
129
- """
130
- Return the Flow information for each 1D Cross Section.
131
113
 
132
- Parameters:
133
- ----------
134
- hdf_path : Path
135
- Path to the HEC-RAS plan HDF file.
136
114
 
137
- Returns:
138
- -------
139
- pd.DataFrame
140
- A DataFrame containing the flow for each cross section and event.
141
- """
142
- return HdfResultsXsec.steady_profile_xs_output(hdf_path, "Flow")
143
115
 
144
- @staticmethod
145
- @standardize_input(file_type='plan_hdf')
146
- def cross_sections_energy_grade(hdf_path: Path) -> pd.DataFrame:
147
- """
148
- Return the energy grade information for each 1D Cross Section.
149
116
 
150
- Parameters:
151
- ----------
152
- hdf_path : Path
153
- Path to the HEC-RAS plan HDF file.
154
117
 
155
- Returns:
156
- -------
157
- pd.DataFrame
158
- A DataFrame containing the energy grade for each cross section and event.
159
- """
160
- return HdfResultsXsec.steady_profile_xs_output(hdf_path, "Energy Grade")
161
118
 
162
- @staticmethod
163
- @standardize_input(file_type='plan_hdf')
164
- def cross_sections_additional_enc_station_left(hdf_path: Path) -> pd.DataFrame:
165
- """
166
- Return the left side encroachment information for a floodway plan hdf.
167
119
 
168
- Parameters:
169
- ----------
170
- hdf_path : Path
171
- Path to the HEC-RAS plan HDF file.
172
120
 
173
- Returns:
174
- -------
175
- pd.DataFrame
176
- A DataFrame containing the cross sections left side encroachment stations.
177
- """
178
- return HdfResultsXsec.steady_profile_xs_output(
179
- hdf_path, "Encroachment Station Left"
180
- )
181
121
 
182
- @staticmethod
183
- @standardize_input(file_type='plan_hdf')
184
- def cross_sections_additional_enc_station_right(hdf_path: Path) -> pd.DataFrame:
185
- """
186
- Return the right side encroachment information for a floodway plan hdf.
187
122
 
188
- Parameters:
189
- ----------
190
- hdf_path : Path
191
- Path to the HEC-RAS plan HDF file.
192
123
 
193
- Returns:
194
- -------
195
- pd.DataFrame
196
- A DataFrame containing the cross sections right side encroachment stations.
197
- """
198
- return HdfResultsXsec.steady_profile_xs_output(
199
- hdf_path, "Encroachment Station Right"
200
- )
201
124
 
202
125
  @staticmethod
126
+ @log_call
203
127
  @standardize_input(file_type='plan_hdf')
204
- def cross_sections_additional_area_total(hdf_path: Path) -> pd.DataFrame:
128
+ def get_pump_station_summary(hdf_path: Path) -> pd.DataFrame:
205
129
  """
206
- Return the 1D cross section area for each profile.
130
+ Extract summary data for pump stations from the HDF file.
207
131
 
208
- Parameters:
209
- ----------
210
- hdf_path : Path
211
- Path to the HEC-RAS plan HDF file.
132
+ Args:
133
+ hdf_path (Path): Path to the HDF file.
212
134
 
213
135
  Returns:
214
- -------
215
- pd.DataFrame
216
- A DataFrame containing the wet area inside the cross sections.
136
+ pd.DataFrame: DataFrame containing pump station summary data.
137
+
138
+ Raises:
139
+ KeyError: If the required datasets are not found in the HDF file.
217
140
  """
218
- return HdfResultsXsec.steady_profile_xs_output(hdf_path, "Area Ineffective Total")
141
+ try:
142
+ with h5py.File(hdf_path, 'r') as hdf:
143
+ # Extract summary data
144
+ summary_path = "/Results/Unsteady/Summary/Pump Station"
145
+ if summary_path not in hdf:
146
+ logger.warning("Pump Station summary data not found in HDF file")
147
+ return pd.DataFrame()
148
+
149
+ summary_data = hdf[summary_path][()]
150
+
151
+ # Create DataFrame
152
+ df = pd.DataFrame(summary_data)
153
+
154
+ # Convert column names
155
+ df.columns = [col.decode('utf-8') if isinstance(col, bytes) else col for col in df.columns]
156
+
157
+ # Convert byte string values to regular strings
158
+ for col in df.columns:
159
+ if df[col].dtype == object:
160
+ df[col] = df[col].apply(lambda x: x.decode('utf-8') if isinstance(x, bytes) else x)
161
+
162
+ return df
163
+
164
+ except KeyError as e:
165
+ logger.error(f"Required dataset not found in HDF file: {e}")
166
+ raise
167
+ except Exception as e:
168
+ logger.error(f"Error extracting pump station summary data: {e}")
169
+ raise
170
+
171
+
172
+
173
+
174
+ # Tested functions from AWS webinar where the code was developed
175
+ # Need to add examples
176
+
219
177
 
220
178
  @staticmethod
179
+ @log_call
221
180
  @standardize_input(file_type='plan_hdf')
222
- def cross_sections_additional_velocity_total(hdf_path: Path) -> pd.DataFrame:
181
+ def extract_cross_section_results(hdf_path: Path) -> xr.Dataset:
223
182
  """
224
- Return the 1D cross section velocity for each profile.
183
+ Extract Water Surface, Velocity Total, Velocity Channel, Flow Lateral, and Flow data from HEC-RAS HDF file.
184
+ Includes Cross Section Only and Cross Section Attributes as coordinates in the xarray.Dataset.
185
+ Also calculates maximum values for key parameters.
225
186
 
226
187
  Parameters:
227
- ----------
188
+ -----------
228
189
  hdf_path : Path
229
- Path to the HEC-RAS plan HDF file.
190
+ Path to the HEC-RAS results HDF file
230
191
 
231
192
  Returns:
232
- -------
233
- pd.DataFrame
234
- A DataFrame containing the velocity inside the cross sections.
193
+ --------
194
+ xr.Dataset
195
+ Xarray Dataset containing the extracted cross-section results with appropriate coordinates and attributes.
196
+ Includes maximum values for Water Surface, Flow, Channel Velocity, Total Velocity, and Lateral Flow.
235
197
  """
236
- return HdfResultsXsec.steady_profile_xs_output(hdf_path, "Velocity Total")
198
+ try:
199
+ with h5py.File(hdf_path, 'r') as hdf_file:
200
+ # Define base paths
201
+ base_output_path = "/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Cross Sections/"
202
+ time_stamp_path = "/Results/Unsteady/Output/Output Blocks/Base Output/Unsteady Time Series/Time Date Stamp (ms)"
203
+
204
+ # Extract Cross Section Attributes
205
+ attrs_dataset = hdf_file[f"{base_output_path}Cross Section Attributes"][:]
206
+ rivers = [attr['River'].decode('utf-8').strip() for attr in attrs_dataset]
207
+ reaches = [attr['Reach'].decode('utf-8').strip() for attr in attrs_dataset]
208
+ stations = [attr['Station'].decode('utf-8').strip() for attr in attrs_dataset]
209
+ names = [attr['Name'].decode('utf-8').strip() for attr in attrs_dataset]
210
+
211
+ # Extract Cross Section Only (Unique Names)
212
+ cross_section_only_dataset = hdf_file[f"{base_output_path}Cross Section Only"][:]
213
+ cross_section_names = [cs.decode('utf-8').strip() for cs in cross_section_only_dataset]
214
+
215
+ # Extract Time Stamps and convert to datetime
216
+ time_stamps = hdf_file[time_stamp_path][:]
217
+ if any(isinstance(ts, bytes) for ts in time_stamps):
218
+ time_stamps = [ts.decode('utf-8') for ts in time_stamps]
219
+ # Convert RAS format timestamps to datetime
220
+ times = pd.to_datetime(time_stamps, format='%d%b%Y %H:%M:%S:%f')
221
+
222
+ # Extract Required Datasets
223
+ water_surface = hdf_file[f"{base_output_path}Water Surface"][:]
224
+ velocity_total = hdf_file[f"{base_output_path}Velocity Total"][:]
225
+ velocity_channel = hdf_file[f"{base_output_path}Velocity Channel"][:]
226
+ flow_lateral = hdf_file[f"{base_output_path}Flow Lateral"][:]
227
+ flow = hdf_file[f"{base_output_path}Flow"][:]
228
+
229
+ # Calculate maximum values along time axis
230
+ max_water_surface = np.max(water_surface, axis=0)
231
+ max_flow = np.max(flow, axis=0)
232
+ max_velocity_channel = np.max(velocity_channel, axis=0)
233
+ max_velocity_total = np.max(velocity_total, axis=0)
234
+ max_flow_lateral = np.max(flow_lateral, axis=0)
235
+
236
+ # Create Xarray Dataset
237
+ ds = xr.Dataset(
238
+ {
239
+ 'Water_Surface': (['time', 'cross_section'], water_surface),
240
+ 'Velocity_Total': (['time', 'cross_section'], velocity_total),
241
+ 'Velocity_Channel': (['time', 'cross_section'], velocity_channel),
242
+ 'Flow_Lateral': (['time', 'cross_section'], flow_lateral),
243
+ 'Flow': (['time', 'cross_section'], flow),
244
+ },
245
+ coords={
246
+ 'time': times,
247
+ 'cross_section': cross_section_names,
248
+ 'River': ('cross_section', rivers),
249
+ 'Reach': ('cross_section', reaches),
250
+ 'Station': ('cross_section', stations),
251
+ 'Name': ('cross_section', names),
252
+ 'Maximum_Water_Surface': ('cross_section', max_water_surface),
253
+ 'Maximum_Flow': ('cross_section', max_flow),
254
+ 'Maximum_Channel_Velocity': ('cross_section', max_velocity_channel),
255
+ 'Maximum_Velocity_Total': ('cross_section', max_velocity_total),
256
+ 'Maximum_Flow_Lateral': ('cross_section', max_flow_lateral)
257
+ },
258
+ attrs={
259
+ 'description': 'Cross-section results extracted from HEC-RAS HDF file',
260
+ 'source_file': str(hdf_path)
261
+ }
262
+ )
263
+
264
+ return ds
265
+
266
+ except KeyError as e:
267
+ logger.error(f"Required dataset not found in HDF file: {e}")
268
+ raise
269
+ except Exception as e:
270
+ logger.error(f"Error extracting cross section results: {e}")
271
+ raise
237
272
 
ras_commander/HdfStruc.py CHANGED
@@ -38,79 +38,177 @@ class HdfStruc:
38
38
 
39
39
  Note: This class contains static methods and does not require instantiation.
40
40
  """
41
-
42
- GEOM_STRUCTURES_PATH = "Geometry/Structures"
43
-
41
+
44
42
  @staticmethod
45
43
  @log_call
46
44
  @standardize_input(file_type='geom_hdf')
47
45
  def structures(hdf_path: Path, datetime_to_str: bool = False) -> GeoDataFrame:
48
46
  """
49
- Return the model structures.
47
+ Extracts structure data from a HEC-RAS geometry HDF5 file and returns it as a GeoDataFrame.
50
48
 
51
- This method extracts structure data from the HDF file, including geometry
52
- and attributes, and returns it as a GeoDataFrame.
49
+ This function excludes Property Tables, Pier and Abutment Data/Attributes, and Gate Groups.
50
+ It includes Table Info, Centerlines as LineStrings, Structures Attributes, Bridge Coefficient Attributes,
51
+ and Profile Data (as a list of station and elevation values for each structure).
53
52
 
54
53
  Parameters
55
54
  ----------
56
55
  hdf_path : Path
57
- Path to the HEC-RAS geometry HDF file.
56
+ Path to the HEC-RAS geometry HDF5 file.
58
57
  datetime_to_str : bool, optional
59
- If True, convert datetime objects to strings. Default is False.
58
+ Convert datetime objects to strings, by default False.
60
59
 
61
60
  Returns
62
61
  -------
63
62
  GeoDataFrame
64
- A GeoDataFrame containing the structures, with columns for attributes
65
- and geometry.
66
-
67
- Raises
68
- ------
69
- Exception
70
- If there's an error reading the structures data from the HDF file.
63
+ A GeoDataFrame containing all relevant structure data with geometries and attributes.
71
64
  """
72
65
  try:
73
- with h5py.File(hdf_path, 'r') as hdf_file:
74
- # Check if the structures path exists in the HDF file
75
- if HdfStruc.GEOM_STRUCTURES_PATH not in hdf_file:
76
- logger.info(f"No structures found in the geometry file: {hdf_path}")
66
+ with h5py.File(hdf_path, 'r') as hdf:
67
+ if "Geometry/Structures" not in hdf:
68
+ logger.info(f"No structures found in: {hdf_path}")
77
69
  return GeoDataFrame()
70
+
71
+ def get_dataset_df(path: str) -> pd.DataFrame:
72
+ """
73
+ Helper function to convert an HDF5 dataset to a pandas DataFrame.
74
+
75
+ Parameters
76
+ ----------
77
+ path : str
78
+ The path to the dataset within the HDF5 file.
79
+
80
+ Returns
81
+ -------
82
+ pd.DataFrame
83
+ DataFrame representation of the dataset.
84
+ """
85
+ if path not in hdf:
86
+ logger.warning(f"Dataset not found: {path}")
87
+ return pd.DataFrame()
88
+
89
+ data = hdf[path][()]
90
+
91
+ if data.dtype.names:
92
+ df = pd.DataFrame(data)
93
+ # Decode byte strings to UTF-8
94
+ for col in df.columns:
95
+ if df[col].dtype.kind in {'S', 'a'}: # Byte strings
96
+ df[col] = df[col].str.decode('utf-8', errors='ignore')
97
+ return df
98
+ else:
99
+ # If no named fields, assign generic column names
100
+ return pd.DataFrame(data, columns=[f'Value_{i}' for i in range(data.shape[1])])
101
+
102
+ # Extract relevant datasets
103
+ struct_attrs = get_dataset_df("Geometry/Structures/Attributes")
104
+ bridge_coef = get_dataset_df("Geometry/Structures/Bridge Coefficient Attributes")
105
+ table_info = get_dataset_df("Geometry/Structures/Table Info")
106
+ profile_data = get_dataset_df("Geometry/Structures/Profile Data")
107
+
108
+ # Assign 'Structure ID' based on index (starting from 1)
109
+ struct_attrs.reset_index(drop=True, inplace=True)
110
+ struct_attrs['Structure ID'] = range(1, len(struct_attrs) + 1)
111
+ logger.debug(f"Assigned Structure IDs: {struct_attrs['Structure ID'].tolist()}")
112
+
113
+ # Check if 'Structure ID' was successfully assigned
114
+ if 'Structure ID' not in struct_attrs.columns:
115
+ logger.error("'Structure ID' column could not be assigned to Structures/Attributes.")
116
+ return GeoDataFrame()
117
+
118
+ # Get centerline geometry
119
+ centerline_info = hdf["Geometry/Structures/Centerline Info"][()]
120
+ centerline_points = hdf["Geometry/Structures/Centerline Points"][()]
78
121
 
79
- struct_data = hdf_file[HdfStruc.GEOM_STRUCTURES_PATH]
80
- v_conv_val = np.vectorize(HdfUtils._convert_ras_hdf_value)
81
- sd_attrs = struct_data["Attributes"][()]
82
-
83
- # Create a dictionary to store structure data
84
- struct_dict = {"struct_id": range(sd_attrs.shape[0])}
85
- struct_dict.update(
86
- {name: v_conv_val(sd_attrs[name]) for name in sd_attrs.dtype.names}
87
- )
88
-
89
- # Get structure geometries
90
- geoms = HdfXsec._get_polylines(
91
- hdf_path,
92
- HdfStruc.GEOM_STRUCTURES_PATH,
93
- info_name="Centerline Info",
94
- parts_name="Centerline Parts",
95
- points_name="Centerline Points"
96
- )
97
-
98
- # Create GeoDataFrame
122
+ # Create LineString geometries for each structure
123
+ geoms = []
124
+ for i in range(len(centerline_info)):
125
+ start_idx = centerline_info[i][0] # Point Starting Index
126
+ point_count = centerline_info[i][1] # Point Count
127
+ points = centerline_points[start_idx:start_idx + point_count]
128
+ if len(points) >= 2:
129
+ geoms.append(LineString(points))
130
+ else:
131
+ logger.warning(f"Insufficient points for LineString in structure index {i}.")
132
+ geoms.append(None)
133
+
134
+ # Create base GeoDataFrame with Structures Attributes and geometries
99
135
  struct_gdf = GeoDataFrame(
100
- struct_dict,
136
+ struct_attrs,
101
137
  geometry=geoms,
102
- crs=HdfUtils.projection(hdf_path),
138
+ crs=HdfUtils.projection(hdf_path)
103
139
  )
104
-
105
- # Convert datetime to string if requested
106
- if datetime_to_str:
107
- struct_gdf["Last Edited"] = struct_gdf["Last Edited"].apply(
108
- lambda x: pd.Timestamp.isoformat(x) if pd.notnull(x) else None
140
+
141
+ # Drop entries with invalid geometries
142
+ initial_count = len(struct_gdf)
143
+ struct_gdf = struct_gdf.dropna(subset=['geometry']).reset_index(drop=True)
144
+ final_count = len(struct_gdf)
145
+ if final_count < initial_count:
146
+ logger.warning(f"Dropped {initial_count - final_count} structures due to invalid geometries.")
147
+
148
+ # Merge Bridge Coefficient Attributes on 'Structure ID'
149
+ if not bridge_coef.empty and 'Structure ID' in bridge_coef.columns:
150
+ struct_gdf = struct_gdf.merge(
151
+ bridge_coef,
152
+ on='Structure ID',
153
+ how='left',
154
+ suffixes=('', '_bridge_coef')
109
155
  )
110
-
156
+ logger.debug("Merged Bridge Coefficient Attributes successfully.")
157
+ else:
158
+ logger.warning("Bridge Coefficient Attributes missing or 'Structure ID' not present.")
159
+
160
+ # Merge Table Info based on the DataFrame index (one-to-one correspondence)
161
+ if not table_info.empty:
162
+ if len(table_info) != len(struct_gdf):
163
+ logger.warning("Table Info count does not match Structures count. Skipping merge.")
164
+ else:
165
+ struct_gdf = pd.concat([struct_gdf, table_info.reset_index(drop=True)], axis=1)
166
+ logger.debug("Merged Table Info successfully.")
167
+ else:
168
+ logger.warning("Table Info dataset is empty or missing.")
169
+
170
+ # Process Profile Data based on Table Info
171
+ if not profile_data.empty and not table_info.empty:
172
+ # Assuming 'Centerline Profile (Index)' and 'Centerline Profile (Count)' are in 'Table Info'
173
+ if ('Centerline Profile (Index)' in table_info.columns and
174
+ 'Centerline Profile (Count)' in table_info.columns):
175
+ struct_gdf['Profile_Data'] = struct_gdf.apply(
176
+ lambda row: [
177
+ {'Station': float(profile_data.iloc[i, 0]),
178
+ 'Elevation': float(profile_data.iloc[i, 1])}
179
+ for i in range(
180
+ int(row['Centerline Profile (Index)']),
181
+ int(row['Centerline Profile (Index)']) + int(row['Centerline Profile (Count)'])
182
+ )
183
+ ],
184
+ axis=1
185
+ )
186
+ logger.debug("Processed Profile Data successfully.")
187
+ else:
188
+ logger.warning("Required columns for Profile Data not found in Table Info.")
189
+ else:
190
+ logger.warning("Profile Data dataset is empty or Table Info is missing.")
191
+
192
+ # Convert datetime columns to string if requested
193
+ if datetime_to_str:
194
+ datetime_cols = struct_gdf.select_dtypes(include=['datetime64']).columns
195
+ for col in datetime_cols:
196
+ struct_gdf[col] = struct_gdf[col].dt.isoformat()
197
+ logger.debug(f"Converted datetime column '{col}' to string.")
198
+
199
+ # Ensure all byte strings are decoded (if any remain)
200
+ for col in struct_gdf.columns:
201
+ if struct_gdf[col].dtype == object:
202
+ struct_gdf[col] = struct_gdf[col].apply(
203
+ lambda x: x.decode('utf-8', errors='ignore') if isinstance(x, bytes) else x
204
+ )
205
+
206
+ # Final GeoDataFrame
207
+ logger.info("Successfully extracted structures GeoDataFrame.")
111
208
  return struct_gdf
209
+
112
210
  except Exception as e:
113
- logger.error(f"Error reading structures: {str(e)}")
211
+ logger.error(f"Error reading structures from {hdf_path}: {str(e)}")
114
212
  raise
115
213
 
116
214
  @staticmethod
@@ -138,10 +236,10 @@ class HdfStruc:
138
236
  """
139
237
  try:
140
238
  with h5py.File(hdf_path, 'r') as hdf_file:
141
- if HdfStruc.GEOM_STRUCTURES_PATH not in hdf_file:
239
+ if "Geometry/Structures" not in hdf_file:
142
240
  logger.info(f"No structures found in the geometry file: {hdf_path}")
143
241
  return {}
144
- return HdfUtils.get_attrs(hdf_file, HdfStruc.GEOM_STRUCTURES_PATH)
242
+ return HdfUtils.get_attrs(hdf_file, "Geometry/Structures")
145
243
  except Exception as e:
146
244
  logger.error(f"Error reading geometry structures attributes: {str(e)}")
147
245
  return {}