ras-commander 0.1.0__py2.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,174 @@
1
+ from .project_config import ProjectConfig
2
+ from pathlib import Path
3
+
4
+ def init_ras_project(ras_project_folder, hecras_version_or_path):
5
+ """
6
+ Initialize a HEC-RAS project.
7
+
8
+ This function sets up a HEC-RAS project by validating the project folder,
9
+ determining the HEC-RAS executable path, and initializing the project configuration.
10
+
11
+ Args:
12
+ ras_project_folder (str): Path to the HEC-RAS project folder.
13
+ hecras_version_or_path (str): Either the HEC-RAS version number or the full path to the HEC-RAS executable.
14
+
15
+ Returns:
16
+ ProjectConfig: An initialized ProjectConfig object containing the project configuration.
17
+
18
+ Raises:
19
+ FileNotFoundError: If the specified RAS project folder does not exist.
20
+
21
+ Note:
22
+ This function prints a confirmation message upon successful initialization.
23
+
24
+ Future Development Roadmap:
25
+
26
+ 1. Load critical keys and values from the project files into the project config
27
+ Implemented:
28
+ - Project Name
29
+ - Project Folder
30
+ - Lists of plan, flow, unsteady, and geometry files
31
+ - HEC-RAS Executable Path
32
+
33
+ Not Implemented:
34
+ - Units
35
+ - Coordinate System
36
+ - rasmap file path (replace prj file path extension with ".rasmap" and add it to the project config)
37
+ - Current Plan
38
+ - Description (including checks to see if it is a valid string and within the default max length)
39
+ - DSS Start Date=01JAN1999 (note format is MMDDYYY)
40
+ - DSS Start Time=1200 (note format is HHMM)
41
+ - DSS End Date=04JAN1999 (note format is MMDDYYY)
42
+ - DSS End Time=1200 (note format is HHMM)
43
+ - DSS File=dss
44
+ - DSS File=Bald_Eagle_Creek.dss
45
+
46
+ Other not implemented:
47
+
48
+ 2. Load critical keys and lists of string values from the plan files into the project config
49
+ - Plan Title
50
+ - Plan Shortid
51
+ - Simulation Date
52
+ - Geometry File
53
+ - Flow File (may not be present) - if present, the plan is a 1D steady plan
54
+ - Unsteady File (may not be present) - if present, the plan is a 1D or 2D unsteady plan
55
+ - UNET D2 Name (may not be present) - if present, the plan is a 2D plan
56
+ - Type (1D Steady, 1D Unsteady, 1D/2D, or 2D)
57
+ - UNET 1D Methodology
58
+
59
+ 3. Load critical keys and strings from the unsteady flow files into the project config
60
+ - Flow Title
61
+ - Pandas Dataframe for any Boundary Conditions present and whether they are defined in the file or whether they use a DSS file input
62
+ - One dataframe for all unsteady flow files, with each Boundary Location in each file having its own row
63
+ - For each unsteady flow filereturn an appended dataframe with each boundary condition and it's "Boundary Name", "Interval", "DSS Path", "Flow Hydrograph Slope", and whether Use DSS is True or False
64
+ - Need to incorporate knowledge from the excel methods we used for setting boundary conditions
65
+
66
+ 4. Load critical keys and strings from the steady flow files into the project config
67
+ - Flow Title
68
+ - Since steady models are not as commonly used, this is a low priority integration (external contributions are welcome)
69
+
70
+
71
+ 5. Load critical keys and values from the rasmap file into the project config
72
+ - rasmap_projection_string
73
+ - Version #Ex: <Version>2.0.0</Version>
74
+ - RASProjectionFilename Filename=".\Terrain\Projection.prj"
75
+
76
+ - List of ras terrains as pandas dataframes
77
+ - for each, list of tiff files and order
78
+ - flag whether terrain mods exist
79
+
80
+ - List of Infiltration hdf files as pandas dataframes
81
+ - Mapping of infiltration layers to geometries
82
+
83
+ - List of land cover hdf files as pandas dataframes
84
+ - Mapping of land cover to geometries
85
+
86
+ - List of all Mannings N layers, hdf files and mapping to geometries as pandas dataframes
87
+
88
+ 6. Create a list of all valid hdf plan files are present in the project folder, and flag whether they contain a completed simulation
89
+
90
+ This roadmap for the project_init function will provide the basic information needed to support most basic hec-ras automation workflows.
91
+
92
+ Remember, this project init might be called multiple times. Every time, it should clear any previously created datafrarmes and variables and replace them. It is important that revisions can be made, init be re-run, and information is current and reflects the current state of the project.
93
+
94
+
95
+
96
+ """
97
+ # Check if the provided paths exist
98
+ if not Path(ras_project_folder).exists():
99
+ raise FileNotFoundError(f"The specified RAS project folder does not exist: {ras_project_folder}, Please check the path and try again.")
100
+
101
+ hecras_exe_path = get_hecras_exe_path(hecras_version_or_path)
102
+
103
+ config = ProjectConfig()
104
+ config.initialize(ras_project_folder, hecras_exe_path)
105
+ print(f"HEC-RAS project initialized: {config.project_name}")
106
+ return config
107
+
108
+
109
+ def get_hecras_exe_path(hecras_version_or_path):
110
+ """
111
+ Determine the HEC-RAS executable path based on the input.
112
+
113
+ Args:
114
+ hecras_version_or_path (str): Either a version number or a full path to the HEC-RAS executable.
115
+
116
+ Returns:
117
+ str: The full path to the HEC-RAS executable.
118
+
119
+ Raises:
120
+ ValueError: If the input is neither a valid version number nor a valid file path.
121
+ FileNotFoundError: If the executable file does not exist at the specified or constructed path.
122
+ """
123
+
124
+
125
+ # hecras_exe_path should be changed to hecras_version_or_path
126
+ # Based on whether a full path is provided, or a version number is provided, the path will be set accordingly
127
+ # By default, HEC-RAS is installed in the Program Files (x86) folder
128
+ # For example: hecras_exe_path = r"C:\Program Files (x86)\HEC\HEC-RAS\6.5\Ras.exe" for version 6.5
129
+ # an f string to build the path based on the version number
130
+ # hecras_exe_path = f"C:\Program Files (x86)\HEC\HEC-RAS\{hecras_version}\Ras.exe"
131
+ # where hecras_version is one of the following:
132
+
133
+ # List of HEC-RAS version numbers
134
+ ras_version_numbers = [
135
+ "6.5", "6.4.1", "6.3.1", "6.3", "6.2", "6.1", "6.0",
136
+ "5.0.7", "5.0.6", "5.0.5", "5.0.4", "5.0.3", "5.0.1", "5.0",
137
+ "4.1", "4.0", "3.1.3", "3.1.2", "3.1.1", "3.0", "2.2"
138
+ ]
139
+
140
+ hecras_path = Path(hecras_version_or_path)
141
+
142
+ # Check if the input is a full path
143
+ if hecras_path.is_file() and hecras_path.suffix.lower() == '.exe':
144
+ return str(hecras_path)
145
+
146
+ # Check if the input is a version number
147
+ if hecras_version_or_path in ras_version_numbers:
148
+ default_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{hecras_version_or_path}/Ras.exe")
149
+ if default_path.is_file():
150
+ return str(default_path)
151
+ else:
152
+ raise FileNotFoundError(f"HEC-RAS executable not found at the expected path: {default_path}")
153
+
154
+ # Check if it's a newer version
155
+ try:
156
+ version_float = float(hecras_version_or_path)
157
+ if version_float > max(float(v) for v in ras_version_numbers):
158
+ newer_version_path = Path(f"C:/Program Files (x86)/HEC/HEC-RAS/{hecras_version_or_path}/Ras.exe")
159
+ if newer_version_path.is_file():
160
+ return str(newer_version_path)
161
+ else:
162
+ 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}")
163
+ except ValueError:
164
+ pass # Not a valid float, so not a version number
165
+
166
+ raise ValueError(f"Invalid HEC-RAS version or path: {hecras_version_or_path}. "
167
+ f"Please provide a valid version number from {ras_version_numbers} "
168
+ "or a full path to the HEC-RAS executable.")
169
+ # Return the validated HEC-RAS executable path
170
+ return hecras_exe_path
171
+
172
+
173
+
174
+
@@ -0,0 +1,227 @@
1
+ """
2
+ Project management operations for HEC-RAS projects.
3
+ """
4
+ import os
5
+ import re
6
+ import shutil
7
+ from pathlib import Path
8
+ from .file_operations import FileOperations
9
+ from .project_config import ProjectConfig
10
+
11
+ class ProjectManager:
12
+ """
13
+ A class for functions that interface with the HEC-RAS project's project file.
14
+ """
15
+ @staticmethod
16
+ def get_next_available_number(existing_numbers):
17
+ """
18
+ Determine the first available number for plan, unsteady, steady, or geometry files from 01 to 99.
19
+ Parameters:
20
+ existing_numbers (pandas.Series): Series of existing numbers as strings (e.g., ['01', '02', '03'])
21
+ Returns:
22
+ str: First available number as a two-digit string
23
+ """
24
+ existing_set = set(existing_numbers.astype(int))
25
+ for num in range(1, 100):
26
+ if num not in existing_set:
27
+ return f"{num:02d}"
28
+ return None
29
+
30
+ @staticmethod
31
+ def copy_plan_from_template(template_plan, new_plan_shortid=None):
32
+ """
33
+ Create a new plan file based on a template and update the project file.
34
+ Parameters:
35
+ template_plan (str): Plan file to use as template (e.g., 'p01')
36
+ new_plan_shortid (str, optional): New short identifier for the plan file
37
+ Returns:
38
+ str: New plan number
39
+ """
40
+ config = ProjectConfig()
41
+ config.check_initialized()
42
+
43
+ new_plan_num = ProjectManager.get_next_available_number(config.ras_plan_entries['plan_number'])
44
+ template_plan_path = config.project_folder / f"{config.project_name}.{template_plan}"
45
+ new_plan_path = config.project_folder / f"{config.project_name}.p{new_plan_num}"
46
+ shutil.copy(template_plan_path, new_plan_path)
47
+ print(f"Copied {template_plan_path} to {new_plan_path}")
48
+
49
+ with open(new_plan_path, 'r') as f:
50
+ plan_lines = f.readlines()
51
+
52
+ shortid_pattern = re.compile(r'^Short Identifier=(.*)$', re.IGNORECASE)
53
+ for i, line in enumerate(plan_lines):
54
+ match = shortid_pattern.match(line.strip())
55
+ if match:
56
+ current_shortid = match.group(1)
57
+ if new_plan_shortid is None:
58
+ new_shortid = (current_shortid + "_copy")[:24]
59
+ else:
60
+ new_shortid = new_plan_shortid[:24]
61
+ plan_lines[i] = f"Short Identifier={new_shortid}\n"
62
+ break
63
+
64
+ with open(new_plan_path, 'w') as f:
65
+ f.writelines(plan_lines)
66
+
67
+ print(f"Updated short identifier in {new_plan_path}")
68
+
69
+ with open(config.project_file, 'r') as f:
70
+ lines = f.readlines()
71
+
72
+ new_plan_line = f"Plan File=p{new_plan_num}\n"
73
+ plan_file_pattern = re.compile(r'^Plan File=p(\d+)', re.IGNORECASE)
74
+ insertion_index = None
75
+ for i, line in enumerate(lines):
76
+ match = plan_file_pattern.match(line.strip())
77
+ if match:
78
+ current_number = int(match.group(1))
79
+ if current_number > int(new_plan_num):
80
+ insertion_index = i
81
+ break
82
+
83
+ if insertion_index is not None:
84
+ lines.insert(insertion_index, new_plan_line)
85
+ else:
86
+ last_plan_index = max([i for i, line in enumerate(lines) if plan_file_pattern.match(line.strip())], default=-1)
87
+ if last_plan_index != -1:
88
+ lines.insert(last_plan_index + 1, new_plan_line)
89
+ else:
90
+ lines.append(new_plan_line)
91
+
92
+ with open(config.project_file, 'w') as f:
93
+ f.writelines(lines)
94
+
95
+ print(f"Updated {config.project_file} with new plan p{new_plan_num}")
96
+ return f"p{new_plan_num}"
97
+
98
+ @staticmethod
99
+ def copy_geometry_from_template(template_geom):
100
+ """
101
+ Copy geometry files from a template, find the next geometry number,
102
+ and update the project file accordingly.
103
+ Parameters:
104
+ template_geom (str): Geometry number to be used as a template (e.g., 'g01')
105
+ Returns:
106
+ str: New geometry number (e.g., 'g03')
107
+ """
108
+ config = ProjectConfig()
109
+ config.check_initialized()
110
+
111
+ template_geom_filename = f"{config.project_name}.{template_geom}"
112
+ template_geom_path = config.project_folder / template_geom_filename
113
+
114
+ if not template_geom_path.is_file():
115
+ raise FileNotFoundError(f"Template geometry file '{template_geom_path}' does not exist.")
116
+
117
+ template_hdf_path = template_geom_path.with_suffix('.hdf')
118
+ if not template_hdf_path.is_file():
119
+ raise FileNotFoundError(f"Template geometry .hdf file '{template_hdf_path}' does not exist.")
120
+
121
+ next_geom_number = ProjectManager.get_next_available_number(config.ras_geom_entries['geom_number'])
122
+
123
+ new_geom_filename = f"{config.project_name}.g{next_geom_number}"
124
+ new_geom_path = config.project_folder / new_geom_filename
125
+
126
+ shutil.copyfile(template_geom_path, new_geom_path)
127
+ print(f"Copied '{template_geom_path}' to '{new_geom_path}'.")
128
+
129
+ new_hdf_path = new_geom_path.with_suffix('.hdf')
130
+ shutil.copyfile(template_hdf_path, new_hdf_path)
131
+ print(f"Copied '{template_hdf_path}' to '{new_hdf_path}'.")
132
+
133
+ with open(config.project_file, 'r') as file:
134
+ lines = file.readlines()
135
+
136
+ new_geom_line = f"Geom File=g{next_geom_number}\n"
137
+ geom_file_pattern = re.compile(r'^Geom File=g(\d+)', re.IGNORECASE)
138
+ insertion_index = None
139
+ for i, line in enumerate(lines):
140
+ match = geom_file_pattern.match(line.strip())
141
+ if match:
142
+ current_number = int(match.group(1))
143
+ if current_number > int(next_geom_number):
144
+ insertion_index = i
145
+ break
146
+
147
+ if insertion_index is not None:
148
+ lines.insert(insertion_index, new_geom_line)
149
+ else:
150
+ header_pattern = re.compile(r'^(Proj Title|Current Plan|Default Exp/Contr|English Units)', re.IGNORECASE)
151
+ header_indices = [i for i, line in enumerate(lines) if header_pattern.match(line.strip())]
152
+ if header_indices:
153
+ last_header_index = header_indices[-1]
154
+ lines.insert(last_header_index + 2, new_geom_line)
155
+ else:
156
+ lines.insert(0, new_geom_line)
157
+
158
+ with open(config.project_file, 'w') as file:
159
+ file.writelines(lines)
160
+
161
+ print(f"Inserted 'Geom File=g{next_geom_number}' into project file '{config.project_file}'.")
162
+ return f"g{next_geom_number}"
163
+
164
+ @staticmethod
165
+ def copy_unsteady_from_template(template_unsteady):
166
+ """
167
+ Copy unsteady flow files from a template, find the next unsteady number,
168
+ and update the project file accordingly.
169
+ Parameters:
170
+ template_unsteady (str): Unsteady flow number to be used as a template (e.g., 'u01')
171
+ Returns:
172
+ str: New unsteady flow number (e.g., 'u03')
173
+ """
174
+ config = ProjectConfig()
175
+ config.check_initialized()
176
+
177
+ template_unsteady_filename = f"{config.project_name}.{template_unsteady}"
178
+ template_unsteady_path = config.project_folder / template_unsteady_filename
179
+
180
+ if not template_unsteady_path.is_file():
181
+ raise FileNotFoundError(f"Template unsteady flow file '{template_unsteady_path}' does not exist.")
182
+
183
+ next_unsteady_number = ProjectManager.get_next_available_number(config.ras_unsteady_entries['unsteady_number'])
184
+
185
+ new_unsteady_filename = f"{config.project_name}.u{next_unsteady_number}"
186
+ new_unsteady_path = config.project_folder / new_unsteady_filename
187
+
188
+ shutil.copyfile(template_unsteady_path, new_unsteady_path)
189
+ print(f"Copied '{template_unsteady_path}' to '{new_unsteady_path}'.")
190
+
191
+ template_hdf_path = template_unsteady_path.with_suffix('.hdf')
192
+ new_hdf_path = new_unsteady_path.with_suffix('.hdf')
193
+
194
+ if template_hdf_path.is_file():
195
+ shutil.copyfile(template_hdf_path, new_hdf_path)
196
+ print(f"Copied '{template_hdf_path}' to '{new_hdf_path}'.")
197
+ else:
198
+ print(f"No corresponding '.hdf' file found for '{template_unsteady_filename}'. Skipping '.hdf' copy.")
199
+
200
+ with open(config.project_file, 'r') as file:
201
+ lines = file.readlines()
202
+
203
+ new_unsteady_line = f"Unsteady File=u{next_unsteady_number}\n"
204
+ unsteady_file_pattern = re.compile(r'^Unsteady File=u(\d+)', re.IGNORECASE)
205
+ insertion_index = None
206
+ for i, line in enumerate(lines):
207
+ match = unsteady_file_pattern.match(line.strip())
208
+ if match:
209
+ current_number = int(match.group(1))
210
+ if current_number > int(next_unsteady_number):
211
+ insertion_index = i
212
+ break
213
+
214
+ if insertion_index is not None:
215
+ lines.insert(insertion_index, new_unsteady_line)
216
+ else:
217
+ last_unsteady_index = max([i for i, line in enumerate(lines) if unsteady_file_pattern.match(line.strip())], default=-1)
218
+ if last_unsteady_index != -1:
219
+ lines.insert(last_unsteady_index + 1, new_unsteady_line)
220
+ else:
221
+ lines.append(new_unsteady_line)
222
+
223
+ with open(config.project_file, 'w') as file:
224
+ file.writelines(lines)
225
+
226
+ print(f"Inserted 'Unsteady File=u{next_unsteady_number}' into project file '{config.project_file}'.")
227
+ return f"u{next_unsteady_number}"
@@ -0,0 +1,15 @@
1
+ # REVISION NOTE: We should be able to move these functions into the project_management.py file and delete this file.
2
+
3
+ from pathlib import Path
4
+ from .file_operations import FileOperations
5
+
6
+ def find_hecras_project_file(folder_path):
7
+ return FileOperations.find_hecras_project_file(folder_path)
8
+
9
+ def load_project_data(project_file):
10
+ return {
11
+ 'ras_plan_entries': FileOperations.get_plan_entries(project_file),
12
+ 'ras_flow_entries': FileOperations.get_flow_entries(project_file),
13
+ 'ras_unsteady_entries': FileOperations.get_unsteady_entries(project_file),
14
+ 'ras_geom_entries': FileOperations.get_geom_entries(project_file)
15
+ }
@@ -0,0 +1,172 @@
1
+ """
2
+ Operations for handling unsteady flow files in HEC-RAS projects.
3
+ """
4
+ from pathlib import Path
5
+ from .project_config import ProjectConfig
6
+ import re
7
+
8
+ class UnsteadyOperations:
9
+ """
10
+ Class for all operations related to HEC-RAS unsteady flow files.
11
+ """
12
+ @staticmethod
13
+ def copy_unsteady_files(dst_folder, template_unsteady):
14
+ """
15
+ Copy unsteady flow files from a template to the next available unsteady number
16
+ and update the destination folder accordingly.
17
+
18
+ Parameters:
19
+ dst_folder (str): Destination folder path
20
+ template_unsteady (str): Template unsteady flow number (e.g., 'u01')
21
+
22
+ Returns:
23
+ str: New unsteady flow number (e.g., 'u03')
24
+ """
25
+ config = ProjectConfig()
26
+ config.check_initialized()
27
+
28
+ src_folder = config.project_folder
29
+ dst_folder = Path(dst_folder)
30
+ dst_folder.mkdir(parents=True, exist_ok=True)
31
+
32
+ # Determine the next available unsteady number
33
+ existing_numbers = []
34
+ unsteady_file_pattern = re.compile(r'^Flow File=u(\d+)', re.IGNORECASE)
35
+
36
+ for plan_file in src_folder.glob("*.p[0-9][0-9]"):
37
+ with open(plan_file, 'r') as file:
38
+ lines = file.readlines()
39
+ for line in lines:
40
+ match = unsteady_file_pattern.match(line.strip())
41
+ if match:
42
+ existing_numbers.append(int(match.group(1)))
43
+
44
+ if existing_numbers:
45
+ existing_numbers.sort()
46
+ next_number = 1
47
+ for num in existing_numbers:
48
+ if num == next_number:
49
+ next_number += 1
50
+ else:
51
+ break
52
+ else:
53
+ next_number = 1
54
+
55
+ next_unsteady_number = f"{next_number:02d}"
56
+
57
+ # Copy the template unsteady file to the new unsteady file
58
+ template_unsteady_filename = f"{config.project_name}.u{template_unsteady}"
59
+ src_u_file = src_folder / template_unsteady_filename
60
+ if src_u_file.exists():
61
+ new_unsteady_filename = f"{config.project_name}.u{next_unsteady_number}"
62
+ dst_u_file = dst_folder / new_unsteady_filename
63
+ dst_u_file.write_bytes(src_u_file.read_bytes())
64
+ print(f"Copied {src_u_file} to {dst_u_file}")
65
+ else:
66
+ raise FileNotFoundError(f"Template unsteady file '{src_u_file}' does not exist.")
67
+
68
+ # Copy the corresponding .hdf file
69
+ src_hdf_file = src_folder / f"{template_unsteady_filename}.hdf"
70
+ if src_hdf_file.exists():
71
+ new_hdf_filename = f"{new_unsteady_filename}.hdf"
72
+ dst_hdf_file = dst_folder / new_hdf_filename
73
+ dst_hdf_file.write_bytes(src_hdf_file.read_bytes())
74
+ print(f"Copied {src_hdf_file} to {dst_hdf_file}")
75
+ else:
76
+ print(f"No corresponding .hdf file found for '{template_unsteady_filename}'. Skipping '.hdf' copy.")
77
+ config = ProjectConfig()
78
+ return f"u{next_unsteady_number}"
79
+
80
+ @staticmethod
81
+ def rename_unsteady_files(old_number, new_number):
82
+ """
83
+ Rename unsteady flow files in the project folder.
84
+ Parameters:
85
+ old_number (str): Old unsteady flow number (e.g., 'u01')
86
+ new_number (str): New unsteady flow number (e.g., 'u02')
87
+ Returns:
88
+ None
89
+ """
90
+ config = ProjectConfig()
91
+ config.check_initialized()
92
+
93
+ folder = config.project_folder
94
+ old_u_file = next(folder.glob(f"*.{old_number}"), None)
95
+ if old_u_file:
96
+ new_u_file = folder / old_u_file.name.replace(old_number, new_number)
97
+ old_u_file.rename(new_u_file)
98
+ print(f"Renamed {old_u_file} to {new_u_file}")
99
+ else:
100
+ print(f"No .{old_number} file found in {folder}")
101
+
102
+ old_hdf_file = next(folder.glob(f"*.{old_number}.hdf"), None)
103
+ if old_hdf_file:
104
+ new_hdf_file = folder / old_hdf_file.name.replace(old_number, new_number)
105
+ old_hdf_file.rename(new_hdf_file)
106
+ print(f"Renamed {old_hdf_file} to {new_hdf_file}")
107
+ else:
108
+ print(f"No .{old_number}.hdf file found in {folder}")
109
+
110
+ @staticmethod
111
+ def update_unsteady_reference_in_plan(plan_file, new_unsteady_number):
112
+ """
113
+ Update the unsteady flow reference in a plan file.
114
+
115
+ Parameters:
116
+ plan_file (str): Full path to the plan file
117
+ new_unsteady_number (str): New unsteady flow number (e.g., 'u02')
118
+
119
+ Returns:
120
+ None
121
+ """
122
+ config = ProjectConfig()
123
+ config.check_initialized()
124
+
125
+ plan_path = Path(plan_file)
126
+ if not plan_path.is_file():
127
+ raise FileNotFoundError(f"Plan file '{plan_path}' not found. Check that the file exists.")
128
+
129
+ with open(plan_path, 'r') as f:
130
+ lines = f.readlines()
131
+ updated = False
132
+ for i, line in enumerate(lines):
133
+ if line.startswith("Unsteady File=") or line.startswith("Flow File=u"):
134
+ lines[i] = f"Unsteady File={new_unsteady_number}\n"
135
+ updated = True
136
+ break
137
+ if updated:
138
+ with open(plan_path, 'w') as f:
139
+ f.writelines(lines)
140
+ print(f"Updated unsteady flow reference in {plan_file} to {new_unsteady_number}")
141
+ else:
142
+ print(f"No unsteady flow reference found in {plan_file}")
143
+
144
+ @staticmethod
145
+ def modify_unsteady_flow_parameters(unsteady_file, modifications):
146
+ """
147
+ Modify parameters in an unsteady flow file.
148
+ Parameters:
149
+ unsteady_file (str): Full path to the unsteady flow file
150
+ modifications (dict): Dictionary of modifications to apply, where keys are parameter names and values are new values
151
+ Returns:
152
+ None
153
+ """
154
+ config = ProjectConfig()
155
+ config.check_initialized()
156
+
157
+ unsteady_path = Path(unsteady_file)
158
+ with open(unsteady_path, 'r') as f:
159
+ lines = f.readlines()
160
+ updated = False
161
+ for i, line in enumerate(lines):
162
+ for param, new_value in modifications.items():
163
+ if line.startswith(f"{param}="):
164
+ lines[i] = f"{param}={new_value}\n"
165
+ updated = True
166
+ print(f"Updated {param} to {new_value}")
167
+ if updated:
168
+ with open(unsteady_path, 'w') as f:
169
+ f.writelines(lines)
170
+ print(f"Applied modifications to {unsteady_file}")
171
+ else:
172
+ print(f"No matching parameters found in {unsteady_file}")