ras-commander 0.80.3__py3-none-any.whl → 0.82.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/RasPlan.py CHANGED
@@ -652,29 +652,44 @@ class RasPlan:
652
652
 
653
653
  @staticmethod
654
654
  @log_call
655
- def clone_plan(template_plan, new_plan_shortid=None, ras_object=None):
655
+ def clone_plan(template_plan, new_shortid=None, new_title=None, ras_object=None):
656
656
  """
657
657
  Create a new plan file based on a template and update the project file.
658
-
658
+
659
659
  Parameters:
660
660
  template_plan (str): Plan number to use as template (e.g., '01')
661
- new_plan_shortid (str, optional): New short identifier for the plan file
661
+ new_shortid (str, optional): New short identifier for the plan file (max 24 chars).
662
+ If not provided, appends '_copy' to original.
663
+ new_title (str, optional): New plan title (max 32 chars, updates "Plan Title=" line).
664
+ If not provided, keeps original title.
662
665
  ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
663
-
666
+
664
667
  Returns:
665
668
  str: New plan number
666
-
669
+
667
670
  Example:
668
- >>> ras_plan = RasPlan()
669
- >>> new_plan_number = ras_plan.clone_plan('01', new_plan_shortid='New Plan')
670
- >>> print(f"New plan created with number: {new_plan_number}")
671
+ >>> # Clone with default shortid and title
672
+ >>> new_plan = RasPlan.clone_plan('01')
673
+ >>>
674
+ >>> # Clone with custom shortid and title
675
+ >>> new_plan = RasPlan.clone_plan('01',
676
+ ... new_shortid='Steady_v41',
677
+ ... new_title='Steady Flow - HEC-RAS 4.1')
671
678
 
672
679
  Note:
680
+ Both new_shortid and new_title are optional.
673
681
  This function updates the ras object's dataframes after modifying the project structure.
674
682
  """
675
683
  ras_obj = ras_object or ras
676
684
  ras_obj.check_initialized()
677
685
 
686
+ # Validate new_title length if provided
687
+ if new_title is not None and len(new_title) > 32:
688
+ raise ValueError(
689
+ f"Plan title must be 32 characters or less. "
690
+ f"Got {len(new_title)} characters: '{new_title}'"
691
+ )
692
+
678
693
  # Update plan entries without reinitializing the entire project
679
694
  ras_obj.plan_df = ras_obj.get_prj_entries('Plan')
680
695
 
@@ -682,22 +697,32 @@ class RasPlan:
682
697
  template_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{template_plan}"
683
698
  new_plan_path = ras_obj.project_folder / f"{ras_obj.project_name}.p{new_plan_num}"
684
699
 
685
- def update_shortid(lines):
700
+ def update_plan_metadata(lines):
701
+ """Update both Plan Title and Short Identifier"""
702
+ title_pattern = re.compile(r'^Plan Title=(.*)$', re.IGNORECASE)
686
703
  shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
704
+
687
705
  for i, line in enumerate(lines):
688
- match = shortid_pattern.match(line.strip())
689
- if match:
690
- current_shortid = match.group(1)
691
- if new_plan_shortid is None:
692
- new_shortid = (current_shortid + "_copy")[:24]
706
+ # Update Plan Title if new_title provided
707
+ title_match = title_pattern.match(line.strip())
708
+ if title_match and new_title is not None:
709
+ lines[i] = f"Plan Title={new_title[:32]}\n"
710
+ continue
711
+
712
+ # Update Short Identifier
713
+ shortid_match = shortid_pattern.match(line.strip())
714
+ if shortid_match:
715
+ current_shortid = shortid_match.group(1)
716
+ if new_shortid is None:
717
+ new_shortid_value = (current_shortid + "_copy")[:24]
693
718
  else:
694
- new_shortid = new_plan_shortid[:24]
695
- lines[i] = f"Short Identifier={new_shortid}\n"
696
- break
719
+ new_shortid_value = new_shortid[:24]
720
+ lines[i] = f"Short Identifier={new_shortid_value}\n"
721
+
697
722
  return lines
698
723
 
699
- # Use RasUtils to clone the file and update the short identifier
700
- RasUtils.clone_file(template_plan_path, new_plan_path, update_shortid)
724
+ # Use RasUtils to clone the file and update metadata
725
+ RasUtils.clone_file(template_plan_path, new_plan_path, update_plan_metadata)
701
726
 
702
727
  # Use RasUtils to update the project file
703
728
  RasUtils.update_project_file(ras_obj.prj_file, 'Plan', new_plan_num, ras_object=ras_obj)
@@ -714,21 +739,22 @@ class RasPlan:
714
739
 
715
740
  @staticmethod
716
741
  @log_call
717
- def clone_unsteady(template_unsteady, ras_object=None):
742
+ def clone_unsteady(template_unsteady, new_title=None, ras_object=None):
718
743
  """
719
744
  Copy unsteady flow files from a template, find the next unsteady number,
720
745
  and update the project file accordingly.
721
746
 
722
747
  Parameters:
723
748
  template_unsteady (str): Unsteady flow number to be used as a template (e.g., '01')
749
+ new_title (str, optional): New flow title (max 32 chars, updates "Flow Title=" line)
724
750
  ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
725
751
 
726
752
  Returns:
727
753
  str: New unsteady flow number (e.g., '03')
728
754
 
729
755
  Example:
730
- >>> ras_plan = RasPlan()
731
- >>> new_unsteady_num = ras_plan.clone_unsteady('01')
756
+ >>> new_unsteady_num = RasPlan.clone_unsteady('01',
757
+ ... new_title='Unsteady - HEC-RAS 4.1')
732
758
  >>> print(f"New unsteady flow file created: u{new_unsteady_num}")
733
759
 
734
760
  Note:
@@ -737,6 +763,13 @@ class RasPlan:
737
763
  ras_obj = ras_object or ras
738
764
  ras_obj.check_initialized()
739
765
 
766
+ # Validate new_title length if provided
767
+ if new_title is not None and len(new_title) > 32:
768
+ raise ValueError(
769
+ f"Flow title must be 32 characters or less. "
770
+ f"Got {len(new_title)} characters: '{new_title}'"
771
+ )
772
+
740
773
  # Update unsteady entries without reinitializing the entire project
741
774
  ras_obj.unsteady_df = ras_obj.get_prj_entries('Unsteady')
742
775
 
@@ -744,8 +777,21 @@ class RasPlan:
744
777
  template_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}"
745
778
  new_unsteady_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{new_unsteady_num}"
746
779
 
747
- # Use RasUtils to clone the file
748
- RasUtils.clone_file(template_unsteady_path, new_unsteady_path)
780
+ def update_flow_title(lines):
781
+ """Update Flow Title if new_title provided"""
782
+ if new_title is None:
783
+ return lines
784
+
785
+ title_pattern = re.compile(r'^Flow Title=(.*)$', re.IGNORECASE)
786
+ for i, line in enumerate(lines):
787
+ title_match = title_pattern.match(line.strip())
788
+ if title_match:
789
+ lines[i] = f"Flow Title={new_title[:32]}\n"
790
+ break
791
+ return lines
792
+
793
+ # Use RasUtils to clone the file and update flow title
794
+ RasUtils.clone_file(template_unsteady_path, new_unsteady_path, update_flow_title)
749
795
 
750
796
  # Copy the corresponding .hdf file if it exists
751
797
  template_hdf_path = ras_obj.project_folder / f"{ras_obj.project_name}.u{template_unsteady}.hdf"
@@ -769,21 +815,22 @@ class RasPlan:
769
815
 
770
816
  @staticmethod
771
817
  @log_call
772
- def clone_steady(template_flow, ras_object=None):
818
+ def clone_steady(template_flow, new_title=None, ras_object=None):
773
819
  """
774
820
  Copy steady flow files from a template, find the next flow number,
775
821
  and update the project file accordingly.
776
-
822
+
777
823
  Parameters:
778
824
  template_flow (str): Flow number to be used as a template (e.g., '01')
825
+ new_title (str, optional): New flow title (max 32 chars, updates "Flow Title=" line)
779
826
  ras_object (RasPrj, optional): Specific RAS object to use. If None, uses the global ras instance.
780
-
827
+
781
828
  Returns:
782
829
  str: New flow number (e.g., '03')
783
830
 
784
831
  Example:
785
- >>> ras_plan = RasPlan()
786
- >>> new_flow_num = ras_plan.clone_steady('01')
832
+ >>> new_flow_num = RasPlan.clone_steady('01',
833
+ ... new_title='Steady Flow - HEC-RAS 4.1')
787
834
  >>> print(f"New steady flow file created: f{new_flow_num}")
788
835
 
789
836
  Note:
@@ -792,6 +839,13 @@ class RasPlan:
792
839
  ras_obj = ras_object or ras
793
840
  ras_obj.check_initialized()
794
841
 
842
+ # Validate new_title length if provided
843
+ if new_title is not None and len(new_title) > 32:
844
+ raise ValueError(
845
+ f"Flow title must be 32 characters or less. "
846
+ f"Got {len(new_title)} characters: '{new_title}'"
847
+ )
848
+
795
849
  # Update flow entries without reinitializing the entire project
796
850
  ras_obj.flow_df = ras_obj.get_prj_entries('Flow')
797
851
 
@@ -799,8 +853,21 @@ class RasPlan:
799
853
  template_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{template_flow}"
800
854
  new_flow_path = ras_obj.project_folder / f"{ras_obj.project_name}.f{new_flow_num}"
801
855
 
802
- # Use RasUtils to clone the file
803
- RasUtils.clone_file(template_flow_path, new_flow_path)
856
+ def update_flow_title(lines):
857
+ """Update Flow Title if new_title provided"""
858
+ if new_title is None:
859
+ return lines
860
+
861
+ title_pattern = re.compile(r'^Flow Title=(.*)$', re.IGNORECASE)
862
+ for i, line in enumerate(lines):
863
+ title_match = title_pattern.match(line.strip())
864
+ if title_match:
865
+ lines[i] = f"Flow Title={new_title[:32]}\n"
866
+ break
867
+ return lines
868
+
869
+ # Use RasUtils to clone the file and update flow title
870
+ RasUtils.clone_file(template_flow_path, new_flow_path, update_flow_title)
804
871
 
805
872
  # Use RasUtils to update the project file
806
873
  RasUtils.update_project_file(ras_obj.prj_file, 'Flow', new_flow_num, ras_object=ras_obj)
ras_commander/RasPrj.py CHANGED
@@ -116,7 +116,7 @@ class RasPrj:
116
116
  self.suppress_logging = False # Add suppress_logging as instance variable
117
117
 
118
118
  @log_call
119
- def initialize(self, project_folder, ras_exe_path, suppress_logging=True):
119
+ def initialize(self, project_folder, ras_exe_path, suppress_logging=True, prj_file=None):
120
120
  """
121
121
  Initialize a RasPrj instance with project folder and RAS executable path.
122
122
 
@@ -127,13 +127,16 @@ class RasPrj:
127
127
  project_folder (str or Path): Path to the HEC-RAS project folder.
128
128
  ras_exe_path (str or Path): Path to the HEC-RAS executable.
129
129
  suppress_logging (bool, default=True): If True, suppresses initialization logging messages.
130
+ prj_file (str or Path, optional): If provided, use this specific .prj file instead of searching.
131
+ This is used when user specifies a .prj file directly.
130
132
 
131
133
  Raises:
132
- ValueError: If no HEC-RAS project file is found in the specified folder.
134
+ ValueError: If no HEC-RAS project file is found in the specified folder,
135
+ or if the specified prj_file doesn't exist or is invalid.
133
136
 
134
137
  Note:
135
138
  This method sets up the RasPrj instance by:
136
- 1. Finding the project file (.prj)
139
+ 1. Finding the project file (.prj) or using the provided prj_file
137
140
  2. Loading project data (plans, geometries, flows)
138
141
  3. Extracting boundary conditions
139
142
  4. Setting the initialization flag
@@ -141,10 +144,21 @@ class RasPrj:
141
144
  """
142
145
  self.suppress_logging = suppress_logging # Store suppress_logging state
143
146
  self.project_folder = Path(project_folder)
144
- self.prj_file = self.find_ras_prj(self.project_folder)
145
- if self.prj_file is None:
146
- logger.error(f"No HEC-RAS project file found in {self.project_folder}")
147
- raise ValueError(f"No HEC-RAS project file found in {self.project_folder}. Please check the path and try again.")
147
+
148
+ # If user specified a .prj file directly, use it (Phase 2 optimization)
149
+ if prj_file is not None:
150
+ self.prj_file = Path(prj_file).resolve()
151
+ if not self.prj_file.exists():
152
+ logger.error(f"Specified .prj file does not exist: {self.prj_file}")
153
+ raise ValueError(f"Specified .prj file does not exist: {self.prj_file}. Please check the path and try again.")
154
+ logger.debug(f"Using specified .prj file: {self.prj_file}")
155
+ else:
156
+ # Search for .prj file (existing behavior)
157
+ self.prj_file = self.find_ras_prj(self.project_folder)
158
+ if self.prj_file is None:
159
+ logger.error(f"No HEC-RAS project file found in {self.project_folder}")
160
+ raise ValueError(f"No HEC-RAS project file found in {self.project_folder}. Please check the path and try again.")
161
+
148
162
  self.project_name = Path(self.prj_file).stem
149
163
  self.ras_exe_path = ras_exe_path
150
164
 
@@ -1274,13 +1288,18 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
1274
1288
  Initialize a RAS project for use with the ras-commander library.
1275
1289
 
1276
1290
  This is the primary function for setting up a HEC-RAS project. It:
1277
- 1. Finds the project file (.prj) in the specified folder
1278
- 2. Identifies the appropriate HEC-RAS executable
1279
- 3. Loads project data (plans, geometries, flows)
1280
- 4. Creates dataframes containing project components
1291
+ 1. Finds the project file (.prj) in the specified folder OR uses the provided .prj file
1292
+ 2. Validates .prj files by checking for "Proj Title=" marker
1293
+ 3. Identifies the appropriate HEC-RAS executable
1294
+ 4. Loads project data (plans, geometries, flows)
1295
+ 5. Creates dataframes containing project components
1281
1296
 
1282
1297
  Args:
1283
- ras_project_folder (str or Path): The path to the RAS project folder.
1298
+ ras_project_folder (str or Path): Path to the RAS project folder OR direct path to a .prj file.
1299
+ If a .prj file is provided:
1300
+ - File is validated to have .prj extension
1301
+ - File content is checked for "Proj Title=" marker
1302
+ - Parent folder is used as the project folder
1284
1303
  ras_version (str, optional): The version of RAS to use (e.g., "6.6") OR
1285
1304
  a full path to the Ras.exe file (e.g., "D:/Programs/HEC/HEC-RAS/6.6/Ras.exe").
1286
1305
  If None, will attempt to detect from plan files.
@@ -1290,25 +1309,71 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
1290
1309
 
1291
1310
  Returns:
1292
1311
  RasPrj: An initialized RasPrj instance.
1293
-
1312
+
1294
1313
  Raises:
1295
- FileNotFoundError: If the specified project folder doesn't exist.
1296
- ValueError: If no HEC-RAS project file is found in the folder.
1297
-
1314
+ FileNotFoundError: If the specified project folder or .prj file doesn't exist.
1315
+ ValueError: If the provided file is not a .prj file, does not contain "Proj Title=",
1316
+ or if no HEC-RAS project file is found in the folder.
1317
+
1298
1318
  Example:
1299
- >>> # Initialize using the global 'ras' object (most common)
1319
+ >>> # Initialize using project folder (existing behavior)
1300
1320
  >>> init_ras_project("/path/to/project", "6.6")
1301
1321
  >>> print(f"Initialized project: {ras.project_name}")
1302
1322
  >>>
1303
- >>> # Create a new RasPrj instance
1304
- >>> my_project = init_ras_project("/path/to/project", "6.6", "new")
1323
+ >>> # Initialize using direct .prj file path (new feature)
1324
+ >>> init_ras_project("/path/to/project/MyModel.prj", "6.6")
1325
+ >>> print(f"Initialized project: {ras.project_name}")
1326
+ >>>
1327
+ >>> # Create a new RasPrj instance with .prj file
1328
+ >>> my_project = init_ras_project("/path/to/project/MyModel.prj", "6.6", "new")
1305
1329
  >>> print(f"Created project instance: {my_project.project_name}")
1306
1330
  """
1307
- # Convert to absolute path immediately to ensure consistent path handling
1308
- project_folder = Path(ras_project_folder).resolve()
1309
- if not project_folder.exists():
1310
- logger.error(f"The specified RAS project folder does not exist: {project_folder}")
1311
- raise FileNotFoundError(f"The specified RAS project folder does not exist: {project_folder}. Please check the path and try again.")
1331
+ # Convert to Path object for consistent handling
1332
+ input_path = Path(ras_project_folder).resolve()
1333
+
1334
+ # Detect if input is a file or folder
1335
+ if input_path.is_file():
1336
+ # User provided a .prj file path
1337
+ if input_path.suffix.lower() != '.prj':
1338
+ error_msg = f"The provided file is not a HEC-RAS project file (.prj): {input_path}"
1339
+ logger.error(error_msg)
1340
+ raise ValueError(f"{error_msg}. Please provide either a project folder or a .prj file.")
1341
+
1342
+ # Enhanced validation: Check if file contains "Proj Title=" to verify it's a HEC-RAS project file
1343
+ try:
1344
+ content, encoding = read_file_with_fallback_encoding(input_path)
1345
+ if content is None or "Proj Title=" not in content:
1346
+ error_msg = f"The file does not appear to be a valid HEC-RAS project file (missing 'Proj Title='): {input_path}"
1347
+ logger.error(error_msg)
1348
+ raise ValueError(f"{error_msg}. Please provide a valid HEC-RAS .prj file.")
1349
+ logger.debug(f"Validated .prj file contains 'Proj Title=' marker")
1350
+ except Exception as e:
1351
+ error_msg = f"Error validating .prj file: {e}"
1352
+ logger.error(error_msg)
1353
+ raise ValueError(f"{error_msg}. Please ensure the file is a valid HEC-RAS project file.")
1354
+
1355
+ # Extract the parent folder to use as project_folder
1356
+ project_folder = input_path.parent
1357
+ specified_prj_file = input_path # Store for optimization
1358
+ logger.debug(f"User provided .prj file: {input_path}")
1359
+ logger.debug(f"Using parent folder as project_folder: {project_folder}")
1360
+
1361
+ elif input_path.is_dir():
1362
+ # User provided a folder path (existing behavior)
1363
+ project_folder = input_path
1364
+ specified_prj_file = None
1365
+ logger.debug(f"User provided folder path: {project_folder}")
1366
+
1367
+ else:
1368
+ # Path doesn't exist
1369
+ if input_path.suffix.lower() == '.prj':
1370
+ error_msg = f"The specified .prj file does not exist: {input_path}"
1371
+ logger.error(error_msg)
1372
+ raise FileNotFoundError(f"{error_msg}. Please check the path and try again.")
1373
+ else:
1374
+ error_msg = f"The specified RAS project folder does not exist: {input_path}"
1375
+ logger.error(error_msg)
1376
+ raise FileNotFoundError(f"{error_msg}. Please check the path and try again.")
1312
1377
 
1313
1378
  # Determine which RasPrj instance to use
1314
1379
  if ras_object is None:
@@ -1381,13 +1446,25 @@ def init_ras_project(ras_project_folder, ras_version=None, ras_object=None):
1381
1446
  logger.warning("No valid HEC-RAS version was detected. Running HEC-RAS will fail.")
1382
1447
 
1383
1448
  # Initialize or re-initialize with the determined executable path
1384
- ras_object.initialize(project_folder, ras_exe_path)
1385
-
1449
+ # Pass specified_prj_file to avoid re-searching when user provided .prj file directly
1450
+ if specified_prj_file is not None:
1451
+ ras_object.initialize(project_folder, ras_exe_path, prj_file=specified_prj_file)
1452
+ else:
1453
+ ras_object.initialize(project_folder, ras_exe_path)
1454
+
1455
+ # Store version for RasControl (legacy COM interface support)
1456
+ ras_object.ras_version = ras_version if ras_version else detected_version
1457
+
1386
1458
  # Always update the global ras object as well
1387
1459
  if ras_object is not ras:
1388
- ras.initialize(project_folder, ras_exe_path)
1460
+ if specified_prj_file is not None:
1461
+ ras.initialize(project_folder, ras_exe_path, prj_file=specified_prj_file)
1462
+ else:
1463
+ ras.initialize(project_folder, ras_exe_path)
1464
+ # Also store version in global ras object
1465
+ ras.ras_version = ras_version if ras_version else detected_version
1389
1466
  logger.debug("Global 'ras' object also updated to match the new project.")
1390
-
1467
+
1391
1468
  logger.debug(f"Project initialized. Project folder: {ras_object.project_folder}")
1392
1469
  logger.debug(f"Using HEC-RAS executable: {ras_exe_path}")
1393
1470
  return ras_object
@@ -1429,60 +1506,92 @@ def get_ras_exe(ras_version=None):
1429
1506
  logger.warning(f"HEC-RAS is not installed or version not specified. Running HEC-RAS will fail unless a valid installed version is specified.")
1430
1507
  return default_path
1431
1508
 
1432
- ras_version_numbers = [
1433
- "6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
1509
+ # ACTUAL folder names in C:/Program Files (x86)/HEC/HEC-RAS/
1510
+ # This list matches the exact folder names on disk (verified 2025-10-30)
1511
+ ras_version_folders = [
1512
+ "6.7 Beta 4", "6.6", "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
1434
1513
  "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
1435
- "4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
1514
+ "4.1.0", "4.0"
1436
1515
  ]
1437
-
1516
+
1517
+ # User-friendly aliases (user_input → actual_folder_name)
1518
+ # Allows users to pass "4.1" and find "4.1.0" folder, or "66" to find "6.6"
1519
+ version_aliases = {
1520
+ # 4.x aliases
1521
+ "4.1": "4.1.0", # User passes "4.1" → finds "4.1.0" folder
1522
+ "41": "4.1.0", # Compact format
1523
+ "410": "4.1.0", # Full compact
1524
+ "40": "4.0", # Compact format for 4.0
1525
+
1526
+ # 5.0.x aliases
1527
+ "50": "5.0", # Compact format
1528
+ "501": "5.0.1", # Compact format for 5.0.1
1529
+ "503": "5.0.3", # Compact format
1530
+ "504": "5.0.4", # Compact format for 5.0.4
1531
+ "505": "5.0.5",
1532
+ "506": "5.0.6",
1533
+ "507": "5.0.7",
1534
+
1535
+ # 6.x aliases
1536
+ "60": "6.0",
1537
+ "61": "6.1",
1538
+ "62": "6.2",
1539
+ "63": "6.3",
1540
+ "631": "6.3.1",
1541
+ "6.4": "6.4.1", # No 6.4 folder, use 6.4.1
1542
+ "64": "6.4.1",
1543
+ "641": "6.4.1",
1544
+ "65": "6.5",
1545
+ "66": "6.6",
1546
+ "6.7": "6.7 Beta 4", # User passes "6.7" → finds "6.7 Beta 4"
1547
+ "67": "6.7 Beta 4",
1548
+ }
1549
+
1438
1550
  # Check if input is a direct path to an executable
1439
1551
  hecras_path = Path(ras_version)
1440
1552
  if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
1441
1553
  logger.debug(f"HEC-RAS executable found at specified path: {hecras_path}")
1442
1554
  return str(hecras_path)
1443
-
1444
- # Check known version numbers
1445
- if str(ras_version) in ras_version_numbers:
1446
- default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{ras_version}/Ras.exe")
1555
+
1556
+ version_str = str(ras_version)
1557
+
1558
+ # Check if there's an alias for this version
1559
+ if version_str in version_aliases:
1560
+ actual_folder = version_aliases[version_str]
1561
+ logger.debug(f"Mapped version '{version_str}' to folder '{actual_folder}'")
1562
+ version_str = actual_folder
1563
+
1564
+ # Check if this is a known folder name
1565
+ if version_str in ras_version_folders:
1566
+ default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
1447
1567
  if default_path.is_file():
1448
1568
  logger.debug(f"HEC-RAS executable found at default path: {default_path}")
1449
1569
  return str(default_path)
1450
1570
  else:
1451
- error_msg = f"HEC-RAS Version {ras_version} is not found at expected path. Running HEC-RAS will fail unless a valid installed version is specified."
1571
+ error_msg = f"HEC-RAS Version {version_str} folder exists but Ras.exe not found at expected path. Running HEC-RAS will fail."
1452
1572
  logger.error(error_msg)
1453
1573
  return "Ras.exe"
1454
1574
 
1455
- # Try to handle other version formats (e.g., just the number without dots)
1575
+ # Final fallback: Try to find a matching version from folder list
1456
1576
  try:
1457
- # First check if it's a direct version number
1458
- version_str = str(ras_version)
1459
-
1460
- # Check for paths like "C:/Path/To/Ras.exe"
1461
- if os.path.sep in version_str and version_str.lower().endswith('.exe'):
1462
- exe_path = Path(version_str)
1463
- if exe_path.is_file():
1464
- logger.debug(f"HEC-RAS executable found at specified path: {exe_path}")
1465
- return str(exe_path)
1466
-
1467
1577
  # Try to find a matching version from our list
1468
- for known_version in ras_version_numbers:
1469
- if version_str in known_version or known_version.replace('.', '') == version_str:
1470
- default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{known_version}/Ras.exe")
1578
+ for known_folder in ras_version_folders:
1579
+ # Check for partial matches or compact formats
1580
+ if version_str in known_folder or known_folder.replace('.', '') == version_str:
1581
+ default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{known_folder}/Ras.exe")
1471
1582
  if default_path.is_file():
1472
- logger.debug(f"HEC-RAS executable found at default path: {default_path}")
1583
+ logger.debug(f"HEC-RAS executable found via fuzzy match: {default_path}")
1473
1584
  return str(default_path)
1474
-
1475
- # Check if it's a newer version
1585
+
1586
+ # Try direct path construction for newer versions
1476
1587
  if '.' in version_str:
1477
- major_version = int(version_str.split('.')[0])
1478
- if major_version >= 6:
1479
- default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
1480
- if default_path.is_file():
1481
- logger.debug(f"HEC-RAS executable found at path for newer version: {default_path}")
1482
- return str(default_path)
1588
+ default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{version_str}/Ras.exe")
1589
+ if default_path.is_file():
1590
+ logger.debug(f"HEC-RAS executable found at path: {default_path}")
1591
+ return str(default_path)
1483
1592
  except Exception as e:
1484
1593
  logger.error(f"Error parsing version or finding path: {e}")
1485
-
1594
+
1486
1595
  error_msg = f"HEC-RAS Version {ras_version} is not recognized or installed. Running HEC-RAS will fail unless a valid installed version is specified."
1487
1596
  logger.error(error_msg)
1488
1597
  return "Ras.exe"
ras_commander/__init__.py CHANGED
@@ -10,7 +10,7 @@ try:
10
10
  __version__ = version("ras-commander")
11
11
  except PackageNotFoundError:
12
12
  # package is not installed
13
- __version__ = "0.80.3"
13
+ __version__ = "0.82.0"
14
14
 
15
15
  # Set up logging
16
16
  setup_logging()
@@ -23,6 +23,7 @@ from .RasUnsteady import RasUnsteady
23
23
  from .RasUtils import RasUtils
24
24
  from .RasExamples import RasExamples
25
25
  from .RasCmdr import RasCmdr
26
+ from .RasControl import RasControl
26
27
  from .RasMap import RasMap
27
28
  from .HdfFluvialPluvial import HdfFluvialPluvial
28
29
 
@@ -50,7 +51,7 @@ __all__ = [
50
51
  # Core functionality
51
52
  'RasPrj', 'init_ras_project', 'get_ras_exe', 'ras',
52
53
  'RasPlan', 'RasGeo', 'RasUnsteady', 'RasUtils',
53
- 'RasExamples', 'RasCmdr', 'RasMap', 'HdfFluvialPluvial',
54
+ 'RasExamples', 'RasCmdr', 'RasControl', 'RasMap', 'HdfFluvialPluvial',
54
55
 
55
56
  # HDF handling
56
57
  'HdfBase', 'HdfBndry', 'HdfMesh', 'HdfPlan',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ras-commander
3
- Version: 0.80.3
3
+ Version: 0.82.0
4
4
  Summary: A Python library for automating HEC-RAS 6.x operations
5
5
  Home-page: https://github.com/gpt-cmdr/ras-commander
6
6
  Author: William M. Katzenmeyer, P.E., C.F.M.
@@ -21,6 +21,8 @@ Requires-Dist: shapely
21
21
  Requires-Dist: pathlib
22
22
  Requires-Dist: rasterstats
23
23
  Requires-Dist: rtree
24
+ Requires-Dist: pywin32>=227
25
+ Requires-Dist: psutil>=5.6.6
24
26
  Dynamic: author
25
27
  Dynamic: author-email
26
28
  Dynamic: description
@@ -81,6 +83,15 @@ HEC-RAS Project Management & Execution
81
83
  - Progress tracking and logging
82
84
  - Execution error handling and recovery
83
85
 
86
+ Legacy Version Support (NEW in v0.81.0)
87
+ - RasControl class for HEC-RAS 3.x-4.x via COM interface
88
+ - ras-commander style API - use plan numbers, not file paths
89
+ - Extract steady state profiles AND unsteady time series
90
+ - Supports versions: 3.1, 4.1, 5.0.x, 6.0, 6.3, 6.6
91
+ - Version migration validation and comparison
92
+ - Open-operate-close pattern prevents conflicts with modern workflows
93
+ - Seamless integration with ras-commander project management
94
+
84
95
  HDF Data Access & Analysis
85
96
  - 2D mesh results processing (depths, velocities, WSE)
86
97
  - Cross-section data extraction
@@ -115,7 +126,9 @@ RAS ASCII File Operations
115
126
  - Unsteady flow file management
116
127
  - Project file updates and validation
117
128
 
118
- Note about support for Pipe Networks: As a relatively new feature, only read access to Pipe Network geometry and results data has been included. Users will need to code their own methods to modify/add pipe network data, and pull requests are always welcome to incorporate this capability. Please note that the library has not been tested with versions prior to HEC-RAS 6.2.
129
+ Note about support for Pipe Networks: As a relatively new feature, only read access to Pipe Network geometry and results data has been included. Users will need to code their own methods to modify/add pipe network data, and pull requests are always welcome to incorporate this capability.
130
+
131
+ Note about version support: The modern HDF-based features target HEC-RAS 6.2+ for optimal compatibility. For legacy versions (3.1, 4.1, 5.0.x), use the RasControl class which provides COM-based access to steady state profile extraction and plan execution (see example notebook 17).
119
132
 
120
133
  ## Installation
121
134
 
@@ -17,16 +17,17 @@ ras_commander/HdfUtils.py,sha256=VkIKAXBrLwTlk2VtXSO-W3RU-NHpfHbE1QcZUZgl-t8,152
17
17
  ras_commander/HdfXsec.py,sha256=4DuJvzTTtn4zGcf1lv_TyWyRnYRnR_SE-iWFKix5QzM,27776
18
18
  ras_commander/LoggingConfig.py,sha256=gWe5K5XTmMQpSczsTysAqpC9my24i_IyM8dvD85fxYg,2704
19
19
  ras_commander/RasCmdr.py,sha256=37GnchoQ0fIAkPnssnCr1mRUXY8gm-hIMTmuHZlnYP8,34591
20
+ ras_commander/RasControl.py,sha256=OGEgY_Zw9J3qjXjIm9P9CkSaGK2Z_N1LrXHvEJ2O1fo,32396
20
21
  ras_commander/RasExamples.py,sha256=QFWnWnxACpQzewzA3QFMp4z4iEkg5PWf9cPDdMay7MA,24556
21
22
  ras_commander/RasGeo.py,sha256=Wy5N1yP7_St3cA3ENJliojQ2sb2w2dL8Fy8L_sZsykc,22208
22
23
  ras_commander/RasMap.py,sha256=20db61KkUz2CgjfCCYY8F-IYy5doHOtdnTKChiK0ENs,20257
23
- ras_commander/RasPlan.py,sha256=ogIpLqawXTsjLnKRZTqzZydn_EFVJZFZZGgHvJ_t_-c,65408
24
- ras_commander/RasPrj.py,sha256=g9J_ewWrT0K1OP4BdEauU6VWq67m_JUqFXEr1AVIy2k,63893
24
+ ras_commander/RasPlan.py,sha256=_aDVD3WmncGKmMGDahTQ_KBIMV0OIpfEUABFt5DcbMs,68630
25
+ ras_commander/RasPrj.py,sha256=OkFClPPwZdFU9nTCP9ytZ1pk0H18pZXT3jEiML9R9Hk,69298
25
26
  ras_commander/RasUnsteady.py,sha256=PdQQMiY7Mz1EsOQk6ygFQtlC2sFEa96Ntg-pznWVpLQ,37187
26
27
  ras_commander/RasUtils.py,sha256=0fm4IIs0LH1dgDj3pGd66mR82DhWLEkRKUvIo2M_5X0,35886
27
- ras_commander/__init__.py,sha256=v_fxJffY8lF74jv5gaihsKcLNE-b37dr5uJ6GvLx-Co,2039
28
- ras_commander-0.80.3.dist-info/licenses/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
29
- ras_commander-0.80.3.dist-info/METADATA,sha256=kiXYlVO31V56GC9n0JmU-tjQucNV5yHnXyKqloi65bY,27941
30
- ras_commander-0.80.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- ras_commander-0.80.3.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
32
- ras_commander-0.80.3.dist-info/RECORD,,
28
+ ras_commander/__init__.py,sha256=cs-SaZXXgyDR0c30577Ay-IlDzi1qAgQmcIWjFnlL6c,2089
29
+ ras_commander-0.82.0.dist-info/licenses/LICENSE,sha256=_pbd6qHnlsz1iQ-ozDW_49r86BZT6CRwO2iBtw0iN6M,457
30
+ ras_commander-0.82.0.dist-info/METADATA,sha256=TuEymRIjnn9mOV80U8thii1GYucRwLt1AbsMkiQjB4c,28653
31
+ ras_commander-0.82.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ ras_commander-0.82.0.dist-info/top_level.txt,sha256=i76S7eKLFC8doKcXDl3aiOr9RwT06G8adI6YuKbQDaA,14
33
+ ras_commander-0.82.0.dist-info/RECORD,,