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,184 @@
1
+ """
2
+ Operations for handling geometry files in HEC-RAS projects.
3
+ """
4
+ from pathlib import Path
5
+ from .plan_operations import PlanOperations
6
+ from .file_operations import FileOperations
7
+ from .project_config import ProjectConfig
8
+ import re
9
+
10
+ class GeometryOperations:
11
+ """
12
+ A class for operations on HEC-RAS geometry files.
13
+ """
14
+ @staticmethod
15
+ def clear_geometry_preprocessor_files(plan_file):
16
+ """
17
+ Clear HEC-RAS geometry preprocessor files for a given plan file.
18
+
19
+ Parameters:
20
+ plan_file (str): Full path to the HEC-RAS plan file (.p*)
21
+
22
+ Returns:
23
+ None
24
+ """
25
+ config = ProjectConfig()
26
+ config.check_initialized()
27
+
28
+ plan_path = Path(plan_file)
29
+ geom_preprocessor_file = plan_path.with_suffix('.c' + plan_path.suffix[2:])
30
+ if geom_preprocessor_file.exists():
31
+ print(f"Deleting geometry preprocessor file: {geom_preprocessor_file}")
32
+ geom_preprocessor_file.unlink()
33
+ print("File deletion completed successfully.")
34
+ else:
35
+ print(f"No geometry preprocessor file found for: {plan_file}")
36
+
37
+ @staticmethod
38
+ def clear_geometry_preprocessor_files_for_all_plans():
39
+ """
40
+ Clear HEC-RAS geometry preprocessor files for all plan files in the project directory.
41
+
42
+ Returns:
43
+ None
44
+ """
45
+ config = ProjectConfig()
46
+ config.check_initialized()
47
+
48
+ for plan_file in config.project_folder.glob("*.p[0-9][0-9]"):
49
+ GeometryOperations.clear_geometry_preprocessor_files(plan_file)
50
+
51
+ @staticmethod
52
+ def copy_geometry_files(dst_folder, template_geom):
53
+ """
54
+ Copy geometry files from a template to the next available geometry number
55
+ and update the destination folder accordingly.
56
+
57
+ Parameters:
58
+ dst_folder (str): Destination folder path
59
+ template_geom (str): Template geometry number (e.g., 'g01')
60
+
61
+ Returns:
62
+ str: New geometry number (e.g., 'g03')
63
+ """
64
+ config = ProjectConfig()
65
+ config.check_initialized()
66
+
67
+ src_folder = config.project_folder
68
+ dst_folder = Path(dst_folder)
69
+ dst_folder.mkdir(parents=True, exist_ok=True)
70
+
71
+ # Determine the next available geometry number
72
+ existing_numbers = []
73
+ geom_file_pattern = re.compile(r'^Geom File=g(\d+)', re.IGNORECASE)
74
+
75
+ for plan_file in src_folder.glob("*.p[0-9][0-9]"):
76
+ with open(plan_file, 'r') as file:
77
+ lines = file.readlines()
78
+ for line in lines:
79
+ match = geom_file_pattern.match(line.strip())
80
+ if match:
81
+ existing_numbers.append(int(match.group(1)))
82
+
83
+ if existing_numbers:
84
+ existing_numbers.sort()
85
+ next_number = 1
86
+ for num in existing_numbers:
87
+ if num == next_number:
88
+ next_number += 1
89
+ else:
90
+ break
91
+ else:
92
+ next_number = 1
93
+
94
+ next_geom_number = f"{next_number:02d}"
95
+
96
+ # Copy the template geometry file to the new geometry file
97
+ template_geom_filename = f"{config.project_name}.g{template_geom}"
98
+ src_g_file = src_folder / template_geom_filename
99
+ if src_g_file.exists():
100
+ new_geom_filename = f"{config.project_name}.g{next_geom_number}"
101
+ dst_g_file = dst_folder / new_geom_filename
102
+ dst_g_file.write_bytes(src_g_file.read_bytes())
103
+ print(f"Copied {src_g_file} to {dst_g_file}")
104
+ else:
105
+ raise FileNotFoundError(f"Template geometry file '{src_g_file}' does not exist.")
106
+
107
+ # Copy the corresponding .hdf file
108
+ src_hdf_file = src_folder / f"{template_geom_filename}.hdf"
109
+ if src_hdf_file.exists():
110
+ new_hdf_filename = f"{new_geom_filename}.hdf"
111
+ dst_hdf_file = dst_folder / new_hdf_filename
112
+ dst_hdf_file.write_bytes(src_hdf_file.read_bytes())
113
+ print(f"Copied {src_hdf_file} to {dst_hdf_file}")
114
+ else:
115
+ raise FileNotFoundError(f"Template geometry .hdf file '{src_hdf_file}' does not exist.")
116
+ config = ProjectConfig()
117
+ return f"g{next_geom_number}"
118
+
119
+ @staticmethod
120
+ def rename_geometry_files(old_number, new_number):
121
+ """
122
+ Rename geometry files in the project folder.
123
+
124
+ Parameters:
125
+ old_number (str): Old geometry number (e.g., 'g01')
126
+ new_number (str): New geometry number (e.g., 'g02')
127
+
128
+ Returns:
129
+ None
130
+ """
131
+ config = ProjectConfig()
132
+ config.check_initialized()
133
+
134
+ folder = config.project_folder
135
+ old_g_file = next(folder.glob(f"*.{old_number}"), None)
136
+ if old_g_file:
137
+ new_g_file = folder / old_g_file.name.replace(old_number, new_number)
138
+ old_g_file.rename(new_g_file)
139
+ print(f"Renamed {old_g_file} to {new_g_file}")
140
+ else:
141
+ print(f"No .{old_number} file found in {folder}")
142
+
143
+ old_hdf_file = next(folder.glob(f"*.{old_number}.hdf"), None)
144
+ if old_hdf_file:
145
+ new_hdf_file = folder / old_hdf_file.name.replace(old_number, new_number)
146
+ old_hdf_file.rename(new_hdf_file)
147
+ print(f"Renamed {old_hdf_file} to {new_hdf_file}")
148
+ else:
149
+ print(f"No .{old_number}.hdf file found in {folder}")
150
+
151
+ @staticmethod
152
+ def update_geometry_reference_in_plan(plan_file, new_geometry_number):
153
+ """
154
+ Update the geometry reference in a plan file.
155
+
156
+ Parameters:
157
+ plan_file (str): Full path to the plan file
158
+ new_geometry_number (str): New geometry number (e.g., 'g02')
159
+
160
+ Returns:
161
+ None
162
+ """
163
+ config = ProjectConfig()
164
+ config.check_initialized()
165
+
166
+ geometry_full_path = next(config.project_folder.glob(f"*.g{new_geometry_number}"), None)
167
+ if not geometry_full_path:
168
+ raise FileNotFoundError(f"Geometry file '*.g{new_geometry_number}' not found in the project folder.")
169
+
170
+ plan_path = Path(plan_file)
171
+ with open(plan_path, 'r') as file:
172
+ lines = file.readlines()
173
+ updated = False
174
+ for index, line in enumerate(lines):
175
+ if line.startswith("Geom File="):
176
+ lines[index] = f"Geom File=g{new_geometry_number}\n"
177
+ updated = True
178
+ break
179
+ if updated:
180
+ with open(plan_path, 'w') as file:
181
+ file.writelines(lines)
182
+ print(f"Updated geometry reference in {plan_file} to {new_geometry_number}")
183
+ else:
184
+ print(f"No geometry reference found in {plan_file}")
@@ -0,0 +1,307 @@
1
+ """
2
+ Operations for modifying and updating HEC-RAS plan files.
3
+ """
4
+ import re
5
+ from pathlib import Path
6
+
7
+
8
+ from .project_config import ProjectConfig
9
+
10
+ class PlanOperations:
11
+ """
12
+ A class for operations on HEC-RAS plan files.
13
+ """
14
+ @staticmethod
15
+ def apply_geometry_to_plan(plan_file, geometry_number):
16
+ """
17
+ Apply a geometry file to a plan file.
18
+
19
+ Parameters:
20
+ plan_file (str): Full path to the HEC-RAS plan file (.pXX)
21
+ geometry_number (str): Geometry number to apply (e.g., '01')
22
+
23
+ Returns:
24
+ None
25
+
26
+ Raises:
27
+ ValueError: If the specified geometry number is not found in the project file
28
+ """
29
+ config = ProjectConfig()
30
+ config.check_initialized()
31
+
32
+ if f"g{geometry_number}" not in config.ras_geom_entries['geom_number'].values:
33
+ raise ValueError(f"Geometry number g{geometry_number} not found in project file.")
34
+ with open(plan_file, 'r') as f:
35
+ lines = f.readlines()
36
+ with open(plan_file, 'w') as f:
37
+ for line in lines:
38
+ if line.startswith("Geom File="):
39
+ f.write(f"Geom File=g{geometry_number}\n")
40
+ print(f"Updated Geom File in {plan_file} to g{geometry_number}")
41
+ else:
42
+ f.write(line)
43
+
44
+ @staticmethod
45
+ def apply_flow_to_plan(plan_file, flow_number):
46
+ """
47
+ Apply a steady flow file to a plan file.
48
+
49
+ Parameters:
50
+ plan_file (str): Full path to the HEC-RAS plan file (.pXX)
51
+ flow_number (str): Flow number to apply (e.g., '02')
52
+
53
+ Returns:
54
+ None
55
+
56
+ Raises:
57
+ ValueError: If the specified flow number is not found in the project file
58
+ """
59
+ config = ProjectConfig()
60
+ config.check_initialized()
61
+
62
+ if f"{flow_number}" not in config.ras_flow_entries['flow_number'].values:
63
+ raise ValueError(f"Flow number f{flow_number} not found in project file.")
64
+ with open(plan_file, 'r') as f:
65
+ lines = f.readlines()
66
+ with open(plan_file, 'w') as f:
67
+ for line in lines:
68
+ if line.startswith("Flow File="):
69
+ f.write(f"Flow File=f{flow_number}\n")
70
+ print(f"Updated Flow File in {plan_file} to f{flow_number}")
71
+ else:
72
+ f.write(line)
73
+
74
+ @staticmethod
75
+ def copy_plan_from_template(template_plan):
76
+ """
77
+ Create a new plan file based on a template and update the project file.
78
+
79
+ Parameters:
80
+
81
+ template_plan (str): Plan file to use as template (e.g., '01')
82
+ project_folder, project_name,
83
+ Returns:
84
+ str: New plan number
85
+ """
86
+ config = ProjectConfig()
87
+ config.check_initialized()
88
+
89
+ # Read existing plan numbers
90
+ ras_plan_entries = config.ras_plan_entries
91
+
92
+ # Get next available plan number
93
+ new_plan_num = PlanOperations.get_next_available_number(ras_plan_entries['plan_number'])
94
+
95
+ # Copy template plan file to new plan file
96
+ template_plan_path = Path(config.project_folder) / f"{config.project_name}.p{template_plan}"
97
+ new_plan_path = Path(config.project_folder) / f"{config.project_name}.p{new_plan_num}"
98
+ new_plan_path.write_bytes(template_plan_path.read_bytes())
99
+ print(f"Copied {template_plan_path} to {new_plan_path}")
100
+
101
+ # Update project file with new plan
102
+ project_file = config.project_file
103
+ with open(project_file, 'a') as f:
104
+ f.write(f"\nPlan File=p{new_plan_num}")
105
+ print(f"Updated {project_file} with new plan p{new_plan_num}")
106
+ config = ProjectConfig()
107
+ return f"{new_plan_num}"
108
+
109
+ @staticmethod
110
+ def get_next_available_number(existing_numbers):
111
+ """
112
+ Determine the next available number from a list of existing numbers.
113
+
114
+ Parameters:
115
+ existing_numbers (list): List of existing numbers as strings
116
+
117
+ Returns:
118
+ str: Next available number as a zero-padded string
119
+ """
120
+ existing_numbers = sorted(int(num) for num in existing_numbers)
121
+ next_number = 1
122
+ for num in existing_numbers:
123
+ if num == next_number:
124
+ next_number += 1
125
+ else:
126
+ break
127
+ return f"{next_number:02d}"
128
+
129
+
130
+
131
+ @staticmethod
132
+ def apply_unsteady_to_plan(plan_file, unsteady_number):
133
+ """
134
+ Apply an unsteady flow file to a plan file.
135
+
136
+ Parameters:
137
+ plan_file (str): Full path to the HEC-RAS plan file (.pXX)
138
+ unsteady_number (str): Unsteady flow number to apply (e.g., '01')
139
+
140
+ Returns:
141
+ None
142
+
143
+ Raises:
144
+ ValueError: If the specified unsteady number is not found in the project file
145
+ """
146
+ config = ProjectConfig()
147
+ config.check_initialized()
148
+
149
+ if f"u{unsteady_number}" not in config.ras_unsteady_entries['unsteady_number'].values:
150
+ raise ValueError(f"Unsteady number u{unsteady_number} not found in project file.")
151
+ with open(plan_file, 'r') as f:
152
+ lines = f.readlines()
153
+ with open(plan_file, 'w') as f:
154
+ for line in lines:
155
+ if line.startswith("Unsteady File=") or line.startswith("Flow File=u"):
156
+ f.write(f"Unsteady File=u{unsteady_number}\n")
157
+ print(f"Updated Unsteady File in {plan_file} to u{unsteady_number}")
158
+ else:
159
+ f.write(line)
160
+
161
+ @staticmethod
162
+ def set_num_cores(plan_file, num_cores):
163
+ """
164
+ Update the maximum number of cores to use in the HEC-RAS plan file.
165
+
166
+ Parameters:
167
+ plan_file (str): Full path to the plan file
168
+ num_cores (int): Maximum number of cores to use
169
+
170
+ Returns:
171
+ None
172
+ """
173
+ config = ProjectConfig()
174
+ config.check_initialized()
175
+
176
+ cores_pattern = re.compile(r"(UNET D1 Cores= )\d+")
177
+ with open(plan_file, 'r') as file:
178
+ content = file.read()
179
+ new_content = cores_pattern.sub(rf"\g<1>{num_cores}", content)
180
+ with open(plan_file, 'w') as file:
181
+ file.write(new_content)
182
+ print(f"Updated {plan_file} with {num_cores} cores.")
183
+
184
+ @staticmethod
185
+ def update_geompre_flags(file_path, run_htab_value, use_ib_tables_value):
186
+ """
187
+ Update the simulation plan file to modify the `Run HTab` and `UNET Use Existing IB Tables` settings.
188
+
189
+ Parameters:
190
+ file_path (str): Path to the simulation plan file (.p06 or similar) that you want to modify.
191
+ run_htab_value (int): Value for the `Run HTab` setting:
192
+ - `0` : Do not run the geometry preprocessor, use existing geometry tables.
193
+ - `-1` : Run the geometry preprocessor, forcing a recomputation of the geometry tables.
194
+ use_ib_tables_value (int): Value for the `UNET Use Existing IB Tables` setting:
195
+ - `0` : Use existing interpolation/boundary (IB) tables without recomputing them.
196
+ - `-1` : Do not use existing IB tables, force a recomputation.
197
+
198
+ Returns:
199
+ None
200
+
201
+ Raises:
202
+ ValueError: If `run_htab_value` or `use_ib_tables_value` are not integers or not within the accepted values (`0` or `-1`).
203
+ FileNotFoundError: If the specified file does not exist.
204
+ IOError: If there is an error reading or writing the file.
205
+ """
206
+ config = ProjectConfig()
207
+ config.check_initialized()
208
+
209
+ if run_htab_value not in [-1, 0]:
210
+ raise ValueError("Invalid value for `Run HTab`. Expected `0` or `-1`.")
211
+ if use_ib_tables_value not in [-1, 0]:
212
+ raise ValueError("Invalid value for `UNET Use Existing IB Tables`. Expected `0` or `-1`.")
213
+ try:
214
+ print(f"Reading the file: {file_path}")
215
+ with open(file_path, 'r') as file:
216
+ lines = file.readlines()
217
+ print("Updating the file with new settings...")
218
+ updated_lines = []
219
+ for line in lines:
220
+ if line.strip().startswith("Run HTab="):
221
+ updated_line = f"Run HTab= {run_htab_value} \n"
222
+ updated_lines.append(updated_line)
223
+ print(f"Updated 'Run HTab' to {run_htab_value}")
224
+ elif line.strip().startswith("UNET Use Existing IB Tables="):
225
+ updated_line = f"UNET Use Existing IB Tables= {use_ib_tables_value} \n"
226
+ updated_lines.append(updated_line)
227
+ print(f"Updated 'UNET Use Existing IB Tables' to {use_ib_tables_value}")
228
+ else:
229
+ updated_lines.append(line)
230
+ print(f"Writing the updated settings back to the file: {file_path}")
231
+ with open(file_path, 'w') as file:
232
+ file.writelines(updated_lines)
233
+ print("File update completed successfully.")
234
+ except FileNotFoundError:
235
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
236
+ except IOError as e:
237
+ raise IOError(f"An error occurred while reading or writing the file: {e}")
238
+
239
+ @staticmethod
240
+ def get_plan_full_path(plan_number: str) -> str:
241
+ """Return the full path for a given plan number."""
242
+ config = ProjectConfig()
243
+ config.check_initialized()
244
+
245
+ plan_path = config.ras_plan_entries[config.ras_plan_entries['plan_number'] == plan_number]
246
+ if not plan_path.empty:
247
+ return plan_path['full_path'].iloc[0]
248
+ else:
249
+ raise ValueError(f"Plan number {plan_number} not found.")
250
+
251
+ @staticmethod
252
+ def get_results_full_path(plan_number: str) -> str:
253
+ """Return the results path for a given plan number."""
254
+ config = ProjectConfig()
255
+ config.check_initialized()
256
+
257
+ results_path = config.ras_plan_entries[config.ras_plan_entries['plan_number'] == plan_number]
258
+ if not results_path.empty:
259
+ results_file_path = Path(results_path['results_path'].iloc[0])
260
+ if results_file_path.exists():
261
+ print(f"Results file for Plan number {plan_number} exists at: {results_file_path}")
262
+ return str(results_file_path)
263
+ else:
264
+ print(f"Error: Results file for Plan number {plan_number} does not exist at the expected location: {results_file_path}")
265
+ return None
266
+ else:
267
+ print(f"Error: Results file for Plan number {plan_number} not found in the entries.")
268
+ return None
269
+
270
+
271
+
272
+
273
+ @staticmethod
274
+ def get_flow_full_path(flow_number: str) -> str:
275
+ """Return the full path for a given flow number."""
276
+ config = ProjectConfig()
277
+ config.check_initialized()
278
+
279
+ flow_path = config.ras_flow_entries[config.ras_flow_entries['flow_number'] == flow_number]
280
+ if not flow_path.empty:
281
+ return flow_path['full_path'].iloc[0]
282
+ else:
283
+ raise ValueError(f"Flow number {flow_number} not found.")
284
+
285
+ @staticmethod
286
+ def get_unsteady_full_path(unsteady_number: str) -> str:
287
+ """Return the full path for a given unsteady number."""
288
+ config = ProjectConfig()
289
+ config.check_initialized()
290
+
291
+ unsteady_path = config.ras_unsteady_entries[config.ras_unsteady_entries['unsteady_number'] == unsteady_number]
292
+ if not unsteady_path.empty:
293
+ return unsteady_path['full_path'].iloc[0]
294
+ else:
295
+ raise ValueError(f"Unsteady number {unsteady_number} not found.")
296
+
297
+ @staticmethod
298
+ def get_geom_full_path(geometry_number: str) -> str:
299
+ """Return the full path for a given geometry number."""
300
+ config = ProjectConfig()
301
+ config.check_initialized()
302
+
303
+ geom_path = config.ras_geom_entries[config.ras_geom_entries['geom_number'] == geometry_number]
304
+ if not geom_path.empty:
305
+ return geom_path['full_path'].iloc[0]
306
+ else:
307
+ raise ValueError(f"Geometry number {geometry_number} not found.")
@@ -0,0 +1,64 @@
1
+ from pathlib import Path
2
+ from .project_setup import find_hecras_project_file, load_project_data
3
+
4
+ # Notes (do note delete):
5
+ # This ProjectConfig class is a singleton, so it will have the same values for all instances.
6
+ # The class is very important for the project setup, as it will be used to store all the project's basic information.
7
+ # Instead of having to pass around the project folder, project file, etc., we can just pass around the ProjectConfig class instance.
8
+ # The class will need to be initialized with the project folder, which will then be used to set all the other project information.
9
+ # The project information will be stored in the instance as class variables.
10
+
11
+ # Documentation Notes:
12
+ # print each variable name and value after it is defined, so the user can easily reference the project's basic information using the self. class variables
13
+ # example: print(f"self.project_file: {self.project_file}")
14
+
15
+ class ProjectConfig:
16
+ _instance = None
17
+
18
+ def __new__(cls):
19
+ if cls._instance is None:
20
+ cls._instance = super(ProjectConfig, cls).__new__(cls)
21
+ cls._instance.initialized = False
22
+ return cls._instance
23
+
24
+ def initialize(self, project_folder, hecras_exe_path):
25
+ self.project_folder = Path(project_folder)
26
+ print(f"self.project_folder: {self.project_folder}")
27
+
28
+ self.project_file = find_hecras_project_file(self.project_folder)
29
+ print(f"self.project_file: {self.project_file}")
30
+
31
+ if self.project_file is None:
32
+ raise ValueError(f"No HEC-RAS project file found in {self.project_folder}")
33
+
34
+ self.project_name = self.project_file.stem
35
+ print(f"self.project_name: {self.project_name}")
36
+
37
+ self.hecras_exe_path = hecras_exe_path
38
+ print(f"self.hecras_exe_path: {self.hecras_exe_path}")
39
+
40
+ self._load_project_data()
41
+ self.initialized = True
42
+ print(f"self.initialized: {self.initialized}")
43
+
44
+ def _load_project_data(self):
45
+ data = load_project_data(self.project_file)
46
+ self.ras_plan_entries = data['ras_plan_entries']
47
+ print(f"self.ras_plan_entries: {self.ras_plan_entries}")
48
+
49
+ self.ras_flow_entries = data['ras_flow_entries']
50
+ print(f"self.ras_flow_entries: {self.ras_flow_entries}")
51
+
52
+ self.ras_unsteady_entries = data['ras_unsteady_entries']
53
+ print(f"self.ras_unsteady_entries: {self.ras_unsteady_entries}")
54
+
55
+ self.ras_geom_entries = data['ras_geom_entries']
56
+ print(f"self.ras_geom_entries: {self.ras_geom_entries}")
57
+
58
+ @property
59
+ def is_initialized(self):
60
+ return self.initialized
61
+
62
+ def check_initialized(self):
63
+ if not self.initialized:
64
+ raise RuntimeError("Project not initialized. Call init_ras_project() first.")