ras-commander 0.33.0__py3-none-any.whl → 0.35.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/RasPrj.py CHANGED
@@ -15,23 +15,37 @@ By default, the RasPrj class is initialized with the global 'ras' object.
15
15
  However, you can create multiple RasPrj instances to manage multiple projects.
16
16
  Do not mix and match global 'ras' object instances and custom instances of RasPrj - it will cause errors.
17
17
  """
18
- # Example Terminal Output for RasPrj Functions:
19
- # print(f"\n----- INSERT TEXT HERE -----\n")
20
18
 
19
+ # Example Terminal Output for RasPrj Functions:
20
+ # logging.info("----- INSERT TEXT HERE -----")
21
+ import re
21
22
  from pathlib import Path
22
23
  import pandas as pd
23
- import re
24
+ import logging
25
+ from typing import Union, Any, List, Dict, Tuple
26
+
27
+
28
+ # Configure logging
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format='%(asctime)s - %(levelname)s - %(message)s',
32
+ handlers=[
33
+ logging.StreamHandler()
34
+ ]
35
+ )
24
36
 
25
37
  class RasPrj:
26
38
  def __init__(self):
27
39
  self.initialized = False
40
+ self.boundaries_df = None # New attribute to store boundary conditions
28
41
 
29
42
  def initialize(self, project_folder, ras_exe_path):
30
43
  """
31
44
  Initialize a RasPrj instance.
32
45
 
33
46
  This method sets up the RasPrj instance with the given project folder and RAS executable path.
34
- It finds the project file, loads project data, and sets the initialization flag.
47
+ It finds the project file, loads project data, sets the initialization flag, and now also
48
+ extracts boundary conditions.
35
49
 
36
50
  Args:
37
51
  project_folder (str or Path): Path to the HEC-RAS project folder.
@@ -46,13 +60,17 @@ class RasPrj:
46
60
  self.project_folder = Path(project_folder)
47
61
  self.prj_file = self.find_ras_prj(self.project_folder)
48
62
  if self.prj_file is None:
63
+ logging.error(f"No HEC-RAS project file found in {self.project_folder}")
49
64
  raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
50
65
  self.project_name = Path(self.prj_file).stem
51
66
  self.ras_exe_path = ras_exe_path
52
67
  self._load_project_data()
68
+ self.boundaries_df = self.get_boundary_conditions() # Extract boundary conditions
53
69
  self.initialized = True
54
- print(f"\n-----Initialization complete for project: {self.project_name}-----")
55
- print(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}\n")
70
+ logging.info(f"Initialization complete for project: {self.project_name}")
71
+ logging.info(f"Plan entries: {len(self.plan_df)}, Flow entries: {len(self.flow_df)}, "
72
+ f"Unsteady entries: {len(self.unsteady_df)}, Geometry entries: {len(self.geom_df)}, "
73
+ f"Boundary conditions: {len(self.boundaries_df)}")
56
74
 
57
75
  def _load_project_data(self):
58
76
  """
@@ -67,6 +85,56 @@ class RasPrj:
67
85
  self.unsteady_df = self._get_prj_entries('Unsteady')
68
86
  self.geom_df = self._get_prj_entries('Geom')
69
87
 
88
+ def _parse_plan_file(self, plan_file_path):
89
+ """
90
+ Parse a plan file and extract critical information.
91
+
92
+ Args:
93
+ plan_file_path (Path): Path to the plan file.
94
+
95
+ Returns:
96
+ dict: Dictionary containing extracted plan information.
97
+ """
98
+ plan_info = {}
99
+ with open(plan_file_path, 'r') as file:
100
+ content = file.read()
101
+
102
+ # Extract description
103
+ description_match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
104
+ if description_match:
105
+ plan_info['description'] = description_match.group(1).strip()
106
+
107
+ # Extract other critical information
108
+ patterns = {
109
+ 'computation_interval': r'Computation Interval=(.+)',
110
+ 'dss_file': r'DSS File=(.+)',
111
+ 'flow_file': r'Flow File=(.+)',
112
+ 'friction_slope_method': r'Friction Slope Method=(.+)',
113
+ 'geom_file': r'Geom File=(.+)',
114
+ 'mapping_interval': r'Mapping Interval=(.+)',
115
+ 'plan_title': r'Plan Title=(.+)',
116
+ 'program_version': r'Program Version=(.+)',
117
+ 'run_htab': r'Run HTab=(.+)',
118
+ 'run_post_process': r'Run PostProcess=(.+)',
119
+ 'run_sediment': r'Run Sediment=(.+)',
120
+ 'run_unet': r'Run UNet=(.+)',
121
+ 'run_wqnet': r'Run WQNet=(.+)',
122
+ 'short_identifier': r'Short Identifier=(.+)',
123
+ 'simulation_date': r'Simulation Date=(.+)',
124
+ 'unet_d1_cores': r'UNET D1 Cores=(.+)',
125
+ 'unet_use_existing_ib_tables': r'UNET Use Existing IB Tables=(.+)',
126
+ 'unet_1d_methodology': r'UNET 1D Methodology=(.+)',
127
+ 'unet_d2_solver_type': r'UNET D2 SolverType=(.+)',
128
+ 'unet_d2_name': r'UNET D2 Name=(.+)'
129
+ }
130
+
131
+ for key, pattern in patterns.items():
132
+ match = re.search(pattern, content)
133
+ if match:
134
+ plan_info[key] = match.group(1).strip()
135
+
136
+ return plan_info
137
+
70
138
  def _get_prj_entries(self, entry_type):
71
139
  """
72
140
  Extract entries of a specific type from the HEC-RAS project file.
@@ -79,40 +147,77 @@ class RasPrj:
79
147
 
80
148
  Note:
81
149
  This method reads the project file and extracts entries matching the specified type.
82
- For 'Plan' entries, it also checks for the existence of HDF results files.
150
+ For 'Unsteady' entries, it parses additional information from the unsteady file.
83
151
  """
84
- # Initialize an empty list to store entries
85
152
  entries = []
86
- # Create a regex pattern to match the specific entry type
87
153
  pattern = re.compile(rf"{entry_type} File=(\w+)")
88
154
 
89
- # Open and read the project file
90
- with open(self.prj_file, 'r') as file:
91
- for line in file:
92
- # Check if the line matches the pattern
93
- match = pattern.match(line.strip())
94
- if match:
95
- # Extract the file name from the matched pattern
96
- file_name = match.group(1)
97
- # Create a dictionary for the current entry
98
- entry = {
99
- f'{entry_type.lower()}_number': file_name[1:],
100
- 'full_path': str(self.project_folder / f"{self.project_name}.{file_name}")
101
- }
102
-
103
- # Special handling for Plan entries
104
- if entry_type == 'Plan':
105
- # Construct the path for the HDF results file
106
- hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
107
- # Add the results_path to the entry, if the file exists
108
- entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
109
-
110
- # Add the entry to the list
111
- entries.append(entry)
112
-
113
- # Convert the list of entries to a DataFrame and return it
155
+ try:
156
+ with open(self.prj_file, 'r') as file:
157
+ for line in file:
158
+ match = pattern.match(line.strip())
159
+ if match:
160
+ file_name = match.group(1)
161
+ full_path = str(self.project_folder / f"{self.project_name}.{file_name}")
162
+ entry = {
163
+ f'{entry_type.lower()}_number': file_name[1:],
164
+ 'full_path': full_path
165
+ }
166
+
167
+ if entry_type == 'Plan':
168
+ plan_info = self._parse_plan_file(Path(full_path))
169
+ entry.update(plan_info)
170
+
171
+ # Add HDF results path if it exists
172
+ hdf_results_path = self.project_folder / f"{self.project_name}.p{file_name[1:]}.hdf"
173
+ entry['HDF_Results_Path'] = str(hdf_results_path) if hdf_results_path.exists() else None
174
+
175
+ if entry_type == 'Unsteady':
176
+ unsteady_info = self._parse_unsteady_file(Path(full_path))
177
+ entry.update(unsteady_info)
178
+
179
+ entries.append(entry)
180
+ except Exception as e:
181
+ logging.exception(f"Failed to read project file {self.prj_file}: {e}")
182
+ raise
183
+
114
184
  return pd.DataFrame(entries)
115
185
 
186
+ def _parse_unsteady_file(self, unsteady_file_path):
187
+ """
188
+ Parse an unsteady flow file and extract critical information.
189
+
190
+ Args:
191
+ unsteady_file_path (Path): Path to the unsteady flow file.
192
+
193
+ Returns:
194
+ dict: Dictionary containing extracted unsteady flow information.
195
+ """
196
+ unsteady_info = {}
197
+ with open(unsteady_file_path, 'r') as file:
198
+ content = file.read()
199
+
200
+ # Extract critical information
201
+ patterns = {
202
+ 'flow_title': r'Flow Title=(.+)',
203
+ 'program_version': r'Program Version=(.+)',
204
+ 'use_restart': r'Use Restart=(.+)',
205
+ 'precipitation_mode': r'Precipitation Mode=(.+)',
206
+ 'wind_mode': r'Wind Mode=(.+)',
207
+ 'precipitation_bc_mode': r'Met BC=Precipitation\|Mode=(.+)',
208
+ 'evapotranspiration_bc_mode': r'Met BC=Evapotranspiration\|Mode=(.+)',
209
+ 'precipitation_expanded_view': r'Met BC=Precipitation\|Expanded View=(.+)',
210
+ 'precipitation_constant_units': r'Met BC=Precipitation\|Constant Units=(.+)',
211
+ 'precipitation_gridded_source': r'Met BC=Precipitation\|Gridded Source=(.+)'
212
+ }
213
+
214
+ for key, pattern in patterns.items():
215
+ match = re.search(pattern, content)
216
+ if match:
217
+ unsteady_info[key] = match.group(1).strip()
218
+
219
+ return unsteady_info
220
+
116
221
  @property
117
222
  def is_initialized(self):
118
223
  """
@@ -131,6 +236,7 @@ class RasPrj:
131
236
  RuntimeError: If the project has not been initialized.
132
237
  """
133
238
  if not self.initialized:
239
+ logging.error("Project not initialized. Call init_ras_project() first.")
134
240
  raise RuntimeError("Project not initialized. Call init_ras_project() first.")
135
241
 
136
242
  @staticmethod
@@ -148,17 +254,26 @@ class RasPrj:
148
254
  prj_files = list(folder_path.glob("*.prj"))
149
255
  rasmap_files = list(folder_path.glob("*.rasmap"))
150
256
  if len(prj_files) == 1:
257
+ logging.info(f"Single .prj file found: {prj_files[0]}")
151
258
  return prj_files[0].resolve()
152
259
  if len(prj_files) > 1:
153
260
  if len(rasmap_files) == 1:
154
261
  base_filename = rasmap_files[0].stem
155
262
  prj_file = folder_path / f"{base_filename}.prj"
156
- return prj_file.resolve()
263
+ if prj_file.exists():
264
+ logging.info(f"Matched .prj file based on .rasmap: {prj_file}")
265
+ return prj_file.resolve()
157
266
  for prj_file in prj_files:
158
- with open(prj_file, 'r') as file:
159
- if "Proj Title=" in file.read():
160
- return prj_file.resolve()
161
- print("No suitable .prj file found after all checks.")
267
+ try:
268
+ with open(prj_file, 'r') as file:
269
+ content = file.read()
270
+ if "Proj Title=" in content:
271
+ logging.info(f".prj file with 'Proj Title=' found: {prj_file}")
272
+ return prj_file.resolve()
273
+ except Exception as e:
274
+ logging.warning(f"Failed to read .prj file {prj_file}: {e}")
275
+ continue
276
+ logging.warning("No suitable .prj file found after all checks.")
162
277
  return None
163
278
 
164
279
  def get_project_name(self):
@@ -255,35 +370,238 @@ class RasPrj:
255
370
  # Filter the plan_df to include only entries with existing HDF results
256
371
  hdf_entries = self.plan_df[self.plan_df['HDF_Results_Path'].notna()].copy()
257
372
 
258
- # If no HDF entries are found, return an empty DataFrame with the correct columns
373
+ # If no HDF entries are found, log the information
259
374
  if hdf_entries.empty:
375
+ logging.info("No HDF entries found.")
260
376
  return pd.DataFrame(columns=self.plan_df.columns)
261
377
 
378
+ logging.info(f"Found {len(hdf_entries)} HDF entries.")
262
379
  return hdf_entries
263
380
 
264
381
  def print_data(self):
265
- """Print all RAS Object data for this instance.
266
- If any objects are added, add them to the print statements below."""
267
- print(f"\n--- Data for {self.project_name} ---")
268
- print(f"Project folder: {self.project_folder}")
269
- print(f"PRJ file: {self.prj_file}")
270
- print(f"HEC-RAS executable: {self.ras_exe_path}")
271
- print("\nPlan files:")
272
- print(self.plan_df)
273
- print("\nFlow files:")
274
- print(self.flow_df)
275
- print("\nUnsteady flow files:")
276
- print(self.unsteady_df)
277
- print("\nGeometry files:")
278
- print(self.geom_df)
279
- print("\nHDF entries:")
280
- print(self.get_hdf_entries())
281
- print("----------------------------\n")
382
+ """Print all RAS Object data for this instance."""
383
+ self.check_initialized()
384
+ logging.info(f"--- Data for {self.project_name} ---")
385
+ logging.info(f"Project folder: {self.project_folder}")
386
+ logging.info(f"PRJ file: {self.prj_file}")
387
+ logging.info(f"HEC-RAS executable: {self.ras_exe_path}")
388
+ logging.info("Plan files:")
389
+ logging.info(f"\n{self.plan_df}")
390
+ logging.info("Flow files:")
391
+ logging.info(f"\n{self.flow_df}")
392
+ logging.info("Unsteady flow files:")
393
+ logging.info(f"\n{self.unsteady_df}")
394
+ logging.info("Geometry files:")
395
+ logging.info(f"\n{self.geom_df}")
396
+ logging.info("HDF entries:")
397
+ logging.info(f"\n{self.get_hdf_entries()}")
398
+ logging.info("Boundary conditions:")
399
+ logging.info(f"\n{self.boundaries_df}")
400
+ logging.info("----------------------------")
401
+
402
+
403
+ @staticmethod
404
+ def get_plan_value(
405
+ plan_number_or_path: Union[str, Path],
406
+ key: str,
407
+ ras_object=None
408
+ ) -> Any:
409
+ """
410
+ Retrieve a specific value from a HEC-RAS plan file.
411
+
412
+ Parameters:
413
+ plan_number_or_path (Union[str, Path]): The plan number (1 to 99) or full path to the plan file
414
+ key (str): The key to retrieve from the plan file
415
+ ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
416
+
417
+ Returns:
418
+ Any: The value associated with the specified key
419
+
420
+ Raises:
421
+ ValueError: If an invalid key is provided or if the plan file is not found
422
+ IOError: If there's an error reading the plan file
423
+
424
+ Note: See the docstring of update_plan_file for a full list of available keys and their types.
425
+
426
+ Example:
427
+ >>> computation_interval = RasUtils.get_plan_value("01", "computation_interval")
428
+ >>> print(f"Computation interval: {computation_interval}")
429
+ """
430
+ ras_obj = ras_object or ras
431
+ ras_obj.check_initialized()
432
+
433
+ valid_keys = {
434
+ 'description', 'computation_interval', 'dss_file', 'flow_file', 'friction_slope_method',
435
+ 'geom_file', 'mapping_interval', 'plan_file', 'plan_title', 'program_version',
436
+ 'run_htab', 'run_post_process', 'run_sediment', 'run_unet', 'run_wqnet',
437
+ 'short_identifier', 'simulation_date', 'unet_d1_cores', 'unet_use_existing_ib_tables',
438
+ 'unet_1d_methodology', 'unet_d2_solver_type', 'unet_d2_name'
439
+ }
440
+
441
+ if key not in valid_keys:
442
+ raise ValueError(f"Invalid key: {key}. Valid keys are: {', '.join(valid_keys)}")
443
+
444
+ plan_file_path = Path(plan_number_or_path)
445
+ if not plan_file_path.is_file():
446
+ plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
447
+ if not plan_file_path.exists():
448
+ raise ValueError(f"Plan file not found: {plan_file_path}")
449
+
450
+ try:
451
+ with open(plan_file_path, 'r') as file:
452
+ content = file.read()
453
+ except IOError as e:
454
+ logging.error(f"Error reading plan file {plan_file_path}: {e}")
455
+ raise
456
+
457
+ if key == 'description':
458
+ import re
459
+ match = re.search(r'Begin DESCRIPTION(.*?)END DESCRIPTION', content, re.DOTALL)
460
+ return match.group(1).strip() if match else None
461
+ else:
462
+ pattern = f"{key.replace('_', ' ').title()}=(.*)"
463
+ import re
464
+ match = re.search(pattern, content)
465
+ return match.group(1).strip() if match else None
466
+
467
+ def get_boundary_conditions(self) -> pd.DataFrame:
468
+ """
469
+ Extract boundary conditions from unsteady flow files and create a DataFrame.
470
+
471
+ This method parses unsteady flow files to extract boundary condition information.
472
+ It creates a DataFrame with structured data for known boundary condition types
473
+ and parameters, and associates this information with the corresponding unsteady flow file.
474
+
475
+ Note:
476
+ Any lines in the boundary condition blocks that are not explicitly parsed and
477
+ incorporated into the DataFrame are captured in a multi-line string. This string
478
+ is logged at the DEBUG level for each boundary condition. This feature is crucial
479
+ for developers incorporating new boundary condition types or parameters, as it
480
+ allows them to see what information might be missing from the current parsing logic.
481
+
482
+ Returns:
483
+ pd.DataFrame: A DataFrame containing detailed boundary condition information,
484
+ linked to the unsteady flow files.
485
+
486
+ Usage:
487
+ To see the unparsed lines, set the logging level to DEBUG before calling this method:
488
+
489
+ import logging
490
+ logging.getLogger().setLevel(logging.DEBUG)
491
+
492
+ boundaries_df = ras_project.get_boundary_conditions()
493
+ """
494
+ boundary_data = []
495
+
496
+ for _, row in self.unsteady_df.iterrows():
497
+ unsteady_file_path = row['full_path']
498
+ unsteady_number = row['unsteady_number']
499
+
500
+ with open(unsteady_file_path, 'r') as file:
501
+ content = file.read()
502
+
503
+ bc_blocks = re.split(r'(?=Boundary Location=)', content)[1:]
504
+
505
+ for i, block in enumerate(bc_blocks, 1):
506
+ bc_info, unparsed_lines = self._parse_boundary_condition(block, unsteady_number, i)
507
+ boundary_data.append(bc_info)
508
+
509
+ if unparsed_lines:
510
+ logging.debug(f"Unparsed lines for boundary condition {i} in unsteady file {unsteady_number}:\n{unparsed_lines}")
511
+
512
+ boundaries_df = pd.DataFrame(boundary_data)
513
+
514
+ # Merge with unsteady_df to get relevant unsteady flow file information
515
+ merged_df = pd.merge(boundaries_df, self.unsteady_df,
516
+ left_on='unsteady_number', right_on='unsteady_number', how='left')
517
+
518
+ return merged_df
519
+
520
+ def _parse_boundary_condition(self, block: str, unsteady_number: str, bc_number: int) -> Tuple[Dict, str]:
521
+ lines = block.split('\n')
522
+ bc_info = {
523
+ 'unsteady_number': unsteady_number,
524
+ 'boundary_condition_number': bc_number
525
+ }
526
+
527
+ parsed_lines = set()
528
+
529
+ # Parse Boundary Location
530
+ boundary_location = lines[0].split('=')[1].strip()
531
+ fields = [field.strip() for field in boundary_location.split(',')]
532
+ bc_info.update({
533
+ 'river_reach_name': fields[0] if len(fields) > 0 else '',
534
+ 'river_station': fields[1] if len(fields) > 1 else '',
535
+ 'storage_area_name': fields[2] if len(fields) > 2 else '',
536
+ 'pump_station_name': fields[3] if len(fields) > 3 else ''
537
+ })
538
+ parsed_lines.add(0)
539
+
540
+ # Determine BC Type
541
+ bc_types = {
542
+ 'Flow Hydrograph=': 'Flow Hydrograph',
543
+ 'Lateral Inflow Hydrograph=': 'Lateral Inflow Hydrograph',
544
+ 'Uniform Lateral Inflow Hydrograph=': 'Uniform Lateral Inflow Hydrograph',
545
+ 'Stage Hydrograph=': 'Stage Hydrograph',
546
+ 'Friction Slope=': 'Normal Depth',
547
+ 'Gate Name=': 'Gate Opening'
548
+ }
549
+
550
+ bc_info['bc_type'] = 'Unknown'
551
+ bc_info['hydrograph_type'] = None
552
+ for i, line in enumerate(lines[1:], 1):
553
+ for key, bc_type in bc_types.items():
554
+ if line.startswith(key):
555
+ bc_info['bc_type'] = bc_type
556
+ if 'Hydrograph' in bc_type:
557
+ bc_info['hydrograph_type'] = bc_type
558
+ parsed_lines.add(i)
559
+ break
560
+ if bc_info['bc_type'] != 'Unknown':
561
+ break
562
+
563
+ # Parse other fields
564
+ known_fields = ['Interval', 'DSS Path', 'Use DSS', 'Use Fixed Start Time', 'Fixed Start Date/Time',
565
+ 'Is Critical Boundary', 'Critical Boundary Flow', 'DSS File']
566
+ for i, line in enumerate(lines):
567
+ if '=' in line:
568
+ key, value = line.split('=', 1)
569
+ key = key.strip()
570
+ if key in known_fields:
571
+ bc_info[key] = value.strip()
572
+ parsed_lines.add(i)
573
+
574
+ # Handle hydrograph values
575
+ bc_info['hydrograph_num_values'] = 0
576
+ if bc_info['hydrograph_type']:
577
+ hydrograph_key = f"{bc_info['hydrograph_type']}="
578
+ hydrograph_line = next((line for i, line in enumerate(lines) if line.startswith(hydrograph_key)), None)
579
+ if hydrograph_line:
580
+ hydrograph_index = lines.index(hydrograph_line)
581
+ values_count = int(hydrograph_line.split('=')[1].strip())
582
+ bc_info['hydrograph_num_values'] = values_count
583
+ if values_count > 0:
584
+ values = ' '.join(lines[hydrograph_index + 1:]).split()[:values_count]
585
+ bc_info['hydrograph_values'] = values
586
+ parsed_lines.update(range(hydrograph_index, hydrograph_index + (values_count // 5) + 2))
587
+
588
+ # Collect unparsed lines
589
+ unparsed_lines = '\n'.join(line for i, line in enumerate(lines) if i not in parsed_lines and line.strip())
590
+
591
+ return bc_info, unparsed_lines
282
592
 
283
593
 
284
594
  # Create a global instance named 'ras'
285
595
  ras = RasPrj()
286
596
 
597
+ # END OF CLASS DEFINITION
598
+
599
+
600
+
601
+
602
+ # START OF FUNCTION DEFINITIONS
603
+
604
+
287
605
  def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
288
606
  """
289
607
  Initialize a RAS project.
@@ -332,23 +650,28 @@ def init_ras_project(ras_project_folder, ras_version, ras_instance=None):
332
650
  Avoid mixing use of the global 'ras' object and custom instances to prevent
333
651
  confusion and potential bugs.
334
652
  """
653
+ logging.info(f"Initializing project in folder: {ras_project_folder}")
654
+ logging.info(f"Using ras_instance with id: {id(ras_instance)}")
655
+
656
+
335
657
 
336
658
  if not Path(ras_project_folder).exists():
659
+ logging.error(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
337
660
  raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}. Please check the path and try again.")
338
661
 
339
662
  ras_exe_path = get_ras_exe(ras_version)
340
663
 
341
664
  if ras_instance is None:
342
- print(f"\n-----Initializing global 'ras' object via init_ras_project function-----")
665
+ logging.info("Initializing global 'ras' object via init_ras_project function.")
343
666
  ras_instance = ras
344
667
  elif not isinstance(ras_instance, RasPrj):
345
- print(f"\n-----Initializing custom RasPrj instance via init_ras_project function-----")
668
+ logging.error("Provided ras_instance is not an instance of RasPrj.")
346
669
  raise TypeError("ras_instance must be an instance of RasPrj or None.")
347
670
 
348
671
  # Initialize the RasPrj instance
349
672
  ras_instance.initialize(ras_project_folder, ras_exe_path)
350
-
351
- #print(f"\n-----HEC-RAS project initialized via init_ras_project function: {ras_instance.project_name}-----\n")
673
+
674
+ logging.info(f"Project initialized. ras_instance project folder: {ras_instance.project_folder}")
352
675
  return ras_instance
353
676
 
354
677
 
@@ -375,13 +698,16 @@ def get_ras_exe(ras_version):
375
698
  hecras_path = Path(ras_version)
376
699
 
377
700
  if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
701
+ logging.info(f"HEC-RAS executable found at specified path: {hecras_path}")
378
702
  return str(hecras_path)
379
703
 
380
704
  if ras_version in ras_version_numbers:
381
705
  default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
382
706
  if default_path.is_file():
707
+ logging.info(f"HEC-RAS executable found at default path: {default_path}")
383
708
  return str(default_path)
384
709
  else:
710
+ logging.error(f"HEC-RAS executable not found at the expected path: {default_path}")
385
711
  raise FileNotFoundError(f"HEC-RAS executable not found at the expected path: {default_path}")
386
712
 
387
713
  try:
@@ -389,12 +715,24 @@ def get_ras_exe(ras_version):
389
715
  if version_float > max(float(v) for v in ras_version_numbers):
390
716
  newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
391
717
  if newer_version_path.is_file():
718
+ logging.info(f"Newer version of HEC-RAS executable found at: {newer_version_path}")
392
719
  return str(newer_version_path)
393
720
  else:
394
- raise FileNotFoundError(f"Newer version of HEC-RAS was specified. Check the version number or pass the full Ras.exe path as the function argument instead of the version number. The script looked for the executable at: {newer_version_path}")
721
+ logging.error("Newer version of HEC-RAS was specified, but the executable was not found.")
722
+ raise FileNotFoundError(
723
+ f"Newer version of HEC-RAS was specified. Check the version number or pass the full Ras.exe path as the function argument instead of the version number. The script looked for the executable at: {newer_version_path}"
724
+ )
395
725
  except ValueError:
396
726
  pass
397
727
 
398
- raise ValueError(f"Invalid HEC-RAS version or path: {ras_version}. "
399
- f"Please provide a valid version number from {ras_version_numbers} "
400
- "or a full path to the HEC-RAS executable.")
728
+ logging.error(
729
+ f"Invalid HEC-RAS version or path: {ras_version}. "
730
+ f"Please provide a valid version number from {ras_version_numbers} "
731
+ "or a full path to the HEC-RAS executable."
732
+ )
733
+ raise ValueError(
734
+ f"Invalid HEC-RAS version or path: {ras_version}. "
735
+ f"Please provide a valid version number from {ras_version_numbers} "
736
+ "or a full path to the HEC-RAS executable."
737
+ )
738
+
@@ -3,8 +3,20 @@ Operations for handling unsteady flow files in HEC-RAS projects.
3
3
  """
4
4
  from pathlib import Path
5
5
  from .RasPrj import ras
6
+ import logging
6
7
  import re
7
8
 
9
+ # Configure logging at the module level
10
+ logging.basicConfig(
11
+ level=logging.INFO, # Set to DEBUG for more detailed output
12
+ format='%(asctime)s - %(levelname)s - %(message)s',
13
+ handlers=[
14
+ logging.StreamHandler(), # Logs to console
15
+ # Uncomment the next line to enable logging to a file
16
+ # logging.FileHandler('ras_unsteady.log')
17
+ ]
18
+ )
19
+
8
20
  class RasUnsteady:
9
21
  """
10
22
  Class for all operations related to HEC-RAS unsteady flow files.
@@ -46,28 +58,36 @@ class RasUnsteady:
46
58
  try:
47
59
  with open(unsteady_path, 'r') as f:
48
60
  lines = f.readlines()
61
+ logging.debug(f"Successfully read unsteady flow file: {unsteady_path}")
49
62
  except FileNotFoundError:
63
+ logging.error(f"Unsteady flow file not found: {unsteady_path}")
50
64
  raise FileNotFoundError(f"Unsteady flow file not found: {unsteady_path}")
51
65
  except PermissionError:
66
+ logging.error(f"Permission denied when reading unsteady flow file: {unsteady_path}")
52
67
  raise PermissionError(f"Permission denied when reading unsteady flow file: {unsteady_path}")
53
68
 
54
69
  updated = False
55
70
  for i, line in enumerate(lines):
56
71
  for param, new_value in modifications.items():
57
72
  if line.startswith(f"{param}="):
73
+ old_value = line.strip().split('=')[1]
58
74
  lines[i] = f"{param}={new_value}\n"
59
75
  updated = True
60
- print(f"Updated {param} to {new_value}")
76
+ logging.info(f"Updated {param} from {old_value} to {new_value}")
77
+
61
78
  if updated:
62
79
  try:
63
80
  with open(unsteady_path, 'w') as f:
64
81
  f.writelines(lines)
82
+ logging.debug(f"Successfully wrote modifications to unsteady flow file: {unsteady_path}")
65
83
  except PermissionError:
84
+ logging.error(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
66
85
  raise PermissionError(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
67
86
  except IOError as e:
87
+ logging.error(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
68
88
  raise IOError(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
69
- print(f"Applied modifications to {unsteady_file}")
89
+ logging.info(f"Applied modifications to {unsteady_file}")
70
90
  else:
71
- print(f"No matching parameters found in {unsteady_file}")
72
-
91
+ logging.warning(f"No matching parameters found in {unsteady_file}")
92
+
73
93
  ras_obj.unsteady_df = ras_obj.get_unsteady_entries()