ras-commander 0.35.0__py3-none-any.whl → 0.37.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/RasCmdr.py +360 -332
- ras_commander/RasExamples.py +113 -80
- ras_commander/RasGeo.py +38 -28
- ras_commander/RasGpt.py +142 -0
- ras_commander/RasHdf.py +170 -253
- ras_commander/RasPlan.py +115 -166
- ras_commander/RasPrj.py +212 -141
- ras_commander/RasUnsteady.py +37 -22
- ras_commander/RasUtils.py +98 -82
- ras_commander/__init__.py +11 -13
- ras_commander/logging_config.py +80 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.37.0.dist-info}/METADATA +22 -19
- ras_commander-0.37.0.dist-info/RECORD +17 -0
- ras_commander-0.35.0.dist-info/RECORD +0 -15
- {ras_commander-0.35.0.dist-info → ras_commander-0.37.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.37.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.35.0.dist-info → ras_commander-0.37.0.dist-info}/top_level.txt +0 -0
ras_commander/RasUnsteady.py
CHANGED
@@ -1,21 +1,35 @@
|
|
1
1
|
"""
|
2
|
-
Operations for handling unsteady flow files in HEC-RAS projects.
|
2
|
+
RasUnsteady - Operations for handling unsteady flow files in HEC-RAS projects.
|
3
|
+
|
4
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
5
|
+
|
6
|
+
Logging Configuration:
|
7
|
+
- The logging is set up in the logging_config.py file.
|
8
|
+
- A @log_call decorator is available to automatically log function calls.
|
9
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
10
|
+
- Logs are written to both console and a rotating file handler.
|
11
|
+
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
12
|
+
- The default log level is INFO.
|
13
|
+
|
14
|
+
To use logging in this module:
|
15
|
+
1. Use the @log_call decorator for automatic function call logging.
|
16
|
+
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
17
|
+
|
18
|
+
|
19
|
+
Example:
|
20
|
+
@log_call
|
21
|
+
def my_function():
|
22
|
+
logger.debug("Additional debug information")
|
23
|
+
# Function logic here
|
3
24
|
"""
|
25
|
+
import os
|
4
26
|
from pathlib import Path
|
5
27
|
from .RasPrj import ras
|
6
|
-
import
|
7
|
-
|
28
|
+
from ras_commander.logging_config import get_logger, log_call
|
29
|
+
|
30
|
+
logger = get_logger(__name__)
|
8
31
|
|
9
|
-
#
|
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
|
-
)
|
32
|
+
# Module code starts here
|
19
33
|
|
20
34
|
class RasUnsteady:
|
21
35
|
"""
|
@@ -23,6 +37,7 @@ class RasUnsteady:
|
|
23
37
|
"""
|
24
38
|
|
25
39
|
@staticmethod
|
40
|
+
@log_call
|
26
41
|
def update_unsteady_parameters(unsteady_file, modifications, ras_object=None):
|
27
42
|
"""
|
28
43
|
Modify parameters in an unsteady flow file.
|
@@ -58,12 +73,12 @@ class RasUnsteady:
|
|
58
73
|
try:
|
59
74
|
with open(unsteady_path, 'r') as f:
|
60
75
|
lines = f.readlines()
|
61
|
-
|
76
|
+
logger.debug(f"Successfully read unsteady flow file: {unsteady_path}")
|
62
77
|
except FileNotFoundError:
|
63
|
-
|
78
|
+
logger.error(f"Unsteady flow file not found: {unsteady_path}")
|
64
79
|
raise FileNotFoundError(f"Unsteady flow file not found: {unsteady_path}")
|
65
80
|
except PermissionError:
|
66
|
-
|
81
|
+
logger.error(f"Permission denied when reading unsteady flow file: {unsteady_path}")
|
67
82
|
raise PermissionError(f"Permission denied when reading unsteady flow file: {unsteady_path}")
|
68
83
|
|
69
84
|
updated = False
|
@@ -73,21 +88,21 @@ class RasUnsteady:
|
|
73
88
|
old_value = line.strip().split('=')[1]
|
74
89
|
lines[i] = f"{param}={new_value}\n"
|
75
90
|
updated = True
|
76
|
-
|
91
|
+
logger.info(f"Updated {param} from {old_value} to {new_value}")
|
77
92
|
|
78
93
|
if updated:
|
79
94
|
try:
|
80
95
|
with open(unsteady_path, 'w') as f:
|
81
96
|
f.writelines(lines)
|
82
|
-
|
97
|
+
logger.debug(f"Successfully wrote modifications to unsteady flow file: {unsteady_path}")
|
83
98
|
except PermissionError:
|
84
|
-
|
99
|
+
logger.error(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
|
85
100
|
raise PermissionError(f"Permission denied when writing to unsteady flow file: {unsteady_path}")
|
86
101
|
except IOError as e:
|
87
|
-
|
102
|
+
logger.error(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
|
88
103
|
raise IOError(f"Error writing to unsteady flow file: {unsteady_path}. {str(e)}")
|
89
|
-
|
104
|
+
logger.info(f"Applied modifications to {unsteady_file}")
|
90
105
|
else:
|
91
|
-
|
106
|
+
logger.warning(f"No matching parameters found in {unsteady_file}")
|
92
107
|
|
93
108
|
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
ras_commander/RasUtils.py
CHANGED
@@ -1,21 +1,38 @@
|
|
1
1
|
"""
|
2
|
-
Utility functions for the ras-commander library
|
2
|
+
RasUtils - Utility functions for the ras-commander library
|
3
|
+
|
4
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
5
|
+
|
6
|
+
Logging Configuration:
|
7
|
+
- The logging is set up in the logging_config.py file.
|
8
|
+
- A @log_call decorator is available to automatically log function calls.
|
9
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
10
|
+
- Logs are written to both console and a rotating file handler.
|
11
|
+
- The default log file is 'ras_commander.log' in the 'logs' directory.
|
12
|
+
- The default log level is INFO.
|
13
|
+
|
14
|
+
To use logging in this module:
|
15
|
+
1. Use the @log_call decorator for automatic function call logging.
|
16
|
+
2. For additional logging, use logger.[level]() calls (e.g., logger.info(), logger.debug()).
|
17
|
+
|
18
|
+
Example:
|
19
|
+
@log_call
|
20
|
+
def my_function():
|
21
|
+
logger.debug("Additional debug information")
|
22
|
+
# Function logic here
|
3
23
|
"""
|
4
24
|
import os
|
5
|
-
import shutil
|
6
|
-
import logging
|
7
|
-
import time
|
8
25
|
from pathlib import Path
|
9
26
|
from .RasPrj import ras
|
10
27
|
from typing import Union, Optional, Dict
|
11
28
|
import pandas as pd
|
12
29
|
import numpy as np
|
30
|
+
import shutil
|
31
|
+
from ras_commander import get_logger
|
32
|
+
from ras_commander.logging_config import get_logger, log_call
|
13
33
|
|
14
|
-
|
15
|
-
|
16
|
-
level=logging.INFO,
|
17
|
-
format='%(asctime)s - %(levelname)s - %(message)s'
|
18
|
-
)
|
34
|
+
logger = get_logger(__name__)
|
35
|
+
# Module code starts here
|
19
36
|
|
20
37
|
class RasUtils:
|
21
38
|
"""
|
@@ -24,6 +41,7 @@ class RasUtils:
|
|
24
41
|
"""
|
25
42
|
|
26
43
|
@staticmethod
|
44
|
+
@log_call
|
27
45
|
def create_backup(file_path: Path, backup_suffix: str = "_backup", ras_object=None) -> Path:
|
28
46
|
"""
|
29
47
|
Create a backup of the specified file.
|
@@ -47,13 +65,14 @@ class RasUtils:
|
|
47
65
|
backup_path = original_path.with_name(f"{original_path.stem}{backup_suffix}{original_path.suffix}")
|
48
66
|
try:
|
49
67
|
shutil.copy2(original_path, backup_path)
|
50
|
-
|
68
|
+
logger.info(f"Backup created: {backup_path}")
|
51
69
|
except Exception as e:
|
52
|
-
|
70
|
+
logger.error(f"Failed to create backup for {original_path}: {e}")
|
53
71
|
raise
|
54
72
|
return backup_path
|
55
73
|
|
56
74
|
@staticmethod
|
75
|
+
@log_call
|
57
76
|
def restore_from_backup(backup_path: Path, remove_backup: bool = True, ras_object=None) -> Path:
|
58
77
|
"""
|
59
78
|
Restore a file from its backup.
|
@@ -75,23 +94,24 @@ class RasUtils:
|
|
75
94
|
|
76
95
|
backup_path = Path(backup_path)
|
77
96
|
if '_backup' not in backup_path.stem:
|
78
|
-
|
97
|
+
logger.error(f"Backup suffix '_backup' not found in {backup_path.name}")
|
79
98
|
raise ValueError(f"Backup suffix '_backup' not found in {backup_path.name}")
|
80
99
|
|
81
100
|
original_stem = backup_path.stem.rsplit('_backup', 1)[0]
|
82
101
|
original_path = backup_path.with_name(f"{original_stem}{backup_path.suffix}")
|
83
102
|
try:
|
84
103
|
shutil.copy2(backup_path, original_path)
|
85
|
-
|
104
|
+
logger.info(f"File restored: {original_path}")
|
86
105
|
if remove_backup:
|
87
106
|
backup_path.unlink()
|
88
|
-
|
107
|
+
logger.info(f"Backup removed: {backup_path}")
|
89
108
|
except Exception as e:
|
90
|
-
|
109
|
+
logger.error(f"Failed to restore from backup {backup_path}: {e}")
|
91
110
|
raise
|
92
111
|
return original_path
|
93
112
|
|
94
113
|
@staticmethod
|
114
|
+
@log_call
|
95
115
|
def create_directory(directory_path: Path, ras_object=None) -> Path:
|
96
116
|
"""
|
97
117
|
Ensure that a directory exists, creating it if necessary.
|
@@ -113,13 +133,14 @@ class RasUtils:
|
|
113
133
|
path = Path(directory_path)
|
114
134
|
try:
|
115
135
|
path.mkdir(parents=True, exist_ok=True)
|
116
|
-
|
136
|
+
logger.info(f"Directory ensured: {path}")
|
117
137
|
except Exception as e:
|
118
|
-
|
138
|
+
logger.error(f"Failed to create directory {path}: {e}")
|
119
139
|
raise
|
120
140
|
return path
|
121
141
|
|
122
142
|
@staticmethod
|
143
|
+
@log_call
|
123
144
|
def find_files_by_extension(extension: str, ras_object=None) -> list:
|
124
145
|
"""
|
125
146
|
List all files in the project directory with a specific extension.
|
@@ -141,13 +162,14 @@ class RasUtils:
|
|
141
162
|
try:
|
142
163
|
files = list(ras_obj.project_folder.glob(f"*{extension}"))
|
143
164
|
file_list = [str(file) for file in files]
|
144
|
-
|
165
|
+
logger.info(f"Found {len(file_list)} files with extension '{extension}' in {ras_obj.project_folder}")
|
145
166
|
return file_list
|
146
167
|
except Exception as e:
|
147
|
-
|
168
|
+
logger.error(f"Failed to find files with extension '{extension}': {e}")
|
148
169
|
raise
|
149
170
|
|
150
171
|
@staticmethod
|
172
|
+
@log_call
|
151
173
|
def get_file_size(file_path: Path, ras_object=None) -> Optional[int]:
|
152
174
|
"""
|
153
175
|
Get the size of a file in bytes.
|
@@ -170,16 +192,17 @@ class RasUtils:
|
|
170
192
|
if path.exists():
|
171
193
|
try:
|
172
194
|
size = path.stat().st_size
|
173
|
-
|
195
|
+
logger.info(f"Size of {path}: {size} bytes")
|
174
196
|
return size
|
175
197
|
except Exception as e:
|
176
|
-
|
198
|
+
logger.error(f"Failed to get size for {path}: {e}")
|
177
199
|
raise
|
178
200
|
else:
|
179
|
-
|
201
|
+
logger.warning(f"File not found: {path}")
|
180
202
|
return None
|
181
203
|
|
182
204
|
@staticmethod
|
205
|
+
@log_call
|
183
206
|
def get_file_modification_time(file_path: Path, ras_object=None) -> Optional[float]:
|
184
207
|
"""
|
185
208
|
Get the last modification time of a file.
|
@@ -195,6 +218,7 @@ class RasUtils:
|
|
195
218
|
>>> mtime = RasUtils.get_file_modification_time(Path("project.prj"))
|
196
219
|
>>> print(f"Last modified: {mtime}")
|
197
220
|
"""
|
221
|
+
|
198
222
|
ras_obj = ras_object or ras
|
199
223
|
ras_obj.check_initialized()
|
200
224
|
|
@@ -202,16 +226,17 @@ class RasUtils:
|
|
202
226
|
if path.exists():
|
203
227
|
try:
|
204
228
|
mtime = path.stat().st_mtime
|
205
|
-
|
229
|
+
logger.info(f"Last modification time of {path}: {mtime}")
|
206
230
|
return mtime
|
207
231
|
except Exception as e:
|
208
|
-
|
232
|
+
logger.exception(f"Failed to get modification time for {path}")
|
209
233
|
raise
|
210
234
|
else:
|
211
|
-
|
235
|
+
logger.warning(f"File not found: {path}")
|
212
236
|
return None
|
213
237
|
|
214
238
|
@staticmethod
|
239
|
+
@log_call
|
215
240
|
def get_plan_path(current_plan_number_or_path: Union[str, Path], ras_object=None) -> Path:
|
216
241
|
"""
|
217
242
|
Get the path for a plan file with a given plan number or path.
|
@@ -229,27 +254,29 @@ class RasUtils:
|
|
229
254
|
>>> plan_path = RasUtils.get_plan_path("path/to/plan.p01")
|
230
255
|
>>> print(f"Plan file path: {plan_path}")
|
231
256
|
"""
|
257
|
+
|
232
258
|
ras_obj = ras_object or ras
|
233
259
|
ras_obj.check_initialized()
|
234
260
|
|
235
261
|
plan_path = Path(current_plan_number_or_path)
|
236
262
|
if plan_path.is_file():
|
237
|
-
|
263
|
+
logger.info(f"Using provided plan file path: {plan_path}")
|
238
264
|
return plan_path
|
239
265
|
|
240
266
|
try:
|
241
267
|
current_plan_number = f"{int(current_plan_number_or_path):02d}" # Ensure two-digit format
|
242
|
-
|
268
|
+
logger.debug(f"Converted plan number to two-digit format: {current_plan_number}")
|
243
269
|
except ValueError:
|
244
|
-
|
270
|
+
logger.error(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
|
245
271
|
raise ValueError(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
|
246
272
|
|
247
273
|
plan_name = f"{ras_obj.project_name}.p{current_plan_number}"
|
248
274
|
full_plan_path = ras_obj.project_folder / plan_name
|
249
|
-
|
275
|
+
logger.info(f"Constructed plan file path: {full_plan_path}")
|
250
276
|
return full_plan_path
|
251
277
|
|
252
278
|
@staticmethod
|
279
|
+
@log_call
|
253
280
|
def remove_with_retry(
|
254
281
|
path: Path,
|
255
282
|
max_attempts: int = 5,
|
@@ -274,6 +301,7 @@ class RasUtils:
|
|
274
301
|
>>> success = RasUtils.remove_with_retry(Path("temp_folder"), is_folder=True)
|
275
302
|
>>> print(f"Removal successful: {success}")
|
276
303
|
"""
|
304
|
+
|
277
305
|
ras_obj = ras_object or ras
|
278
306
|
ras_obj.check_initialized()
|
279
307
|
|
@@ -283,32 +311,33 @@ class RasUtils:
|
|
283
311
|
if path.exists():
|
284
312
|
if is_folder:
|
285
313
|
shutil.rmtree(path)
|
286
|
-
|
314
|
+
logger.info(f"Folder removed: {path}")
|
287
315
|
else:
|
288
316
|
path.unlink()
|
289
|
-
|
317
|
+
logger.info(f"File removed: {path}")
|
290
318
|
else:
|
291
|
-
|
319
|
+
logger.info(f"Path does not exist, nothing to remove: {path}")
|
292
320
|
return True
|
293
321
|
except PermissionError as pe:
|
294
322
|
if attempt < max_attempts:
|
295
323
|
delay = initial_delay * (2 ** (attempt - 1)) # Exponential backoff
|
296
|
-
|
324
|
+
logger.warning(
|
297
325
|
f"PermissionError on attempt {attempt} to remove {path}: {pe}. "
|
298
326
|
f"Retrying in {delay} seconds..."
|
299
327
|
)
|
300
328
|
time.sleep(delay)
|
301
329
|
else:
|
302
|
-
|
330
|
+
logger.error(
|
303
331
|
f"Failed to remove {path} after {max_attempts} attempts due to PermissionError: {pe}. Skipping."
|
304
332
|
)
|
305
333
|
return False
|
306
334
|
except Exception as e:
|
307
|
-
|
335
|
+
logger.exception(f"Failed to remove {path} on attempt {attempt}")
|
308
336
|
return False
|
309
337
|
return False
|
310
338
|
|
311
339
|
@staticmethod
|
340
|
+
@log_call
|
312
341
|
def update_plan_file(
|
313
342
|
plan_number_or_path: Union[str, Path],
|
314
343
|
file_type: str,
|
@@ -332,12 +361,13 @@ class RasUtils:
|
|
332
361
|
>>> RasUtils.update_plan_file(1, "Geom", 2)
|
333
362
|
>>> RasUtils.update_plan_file("path/to/plan.p01", "Geom", 2)
|
334
363
|
"""
|
364
|
+
|
335
365
|
ras_obj = ras_object or ras
|
336
366
|
ras_obj.check_initialized()
|
337
367
|
|
338
368
|
valid_file_types = {'Geom': 'g', 'Flow': 'f', 'Unsteady': 'u'}
|
339
369
|
if file_type not in valid_file_types:
|
340
|
-
|
370
|
+
logger.error(
|
341
371
|
f"Invalid file_type '{file_type}'. Expected one of: {', '.join(valid_file_types.keys())}"
|
342
372
|
)
|
343
373
|
raise ValueError(
|
@@ -348,7 +378,7 @@ class RasUtils:
|
|
348
378
|
if not plan_file_path.is_file():
|
349
379
|
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
350
380
|
if not plan_file_path.exists():
|
351
|
-
|
381
|
+
logger.error(f"Plan file not found: {plan_file_path}")
|
352
382
|
raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
|
353
383
|
|
354
384
|
file_prefix = valid_file_types[file_type]
|
@@ -360,30 +390,30 @@ class RasUtils:
|
|
360
390
|
with plan_file_path.open('r') as file:
|
361
391
|
lines = file.readlines()
|
362
392
|
except Exception as e:
|
363
|
-
|
393
|
+
logger.exception(f"Failed to read plan file {plan_file_path}")
|
364
394
|
raise
|
365
395
|
|
366
396
|
updated = False
|
367
397
|
for i, line in enumerate(lines):
|
368
398
|
if line.startswith(search_pattern):
|
369
399
|
lines[i] = f"{search_pattern}{file_prefix}{formatted_entry_number}\n"
|
370
|
-
|
400
|
+
logger.info(
|
371
401
|
f"Updated {file_type} File in {plan_file_path} to {file_prefix}{formatted_entry_number}"
|
372
402
|
)
|
373
403
|
updated = True
|
374
404
|
break
|
375
405
|
|
376
406
|
if not updated:
|
377
|
-
|
407
|
+
logger.warning(
|
378
408
|
f"Search pattern '{search_pattern}' not found in {plan_file_path}. No update performed."
|
379
409
|
)
|
380
410
|
|
381
411
|
try:
|
382
412
|
with plan_file_path.open('w') as file:
|
383
413
|
file.writelines(lines)
|
384
|
-
|
414
|
+
logger.info(f"Successfully updated plan file: {plan_file_path}")
|
385
415
|
except Exception as e:
|
386
|
-
|
416
|
+
logger.exception(f"Failed to write updates to plan file {plan_file_path}")
|
387
417
|
raise
|
388
418
|
|
389
419
|
# Refresh RasPrj dataframes
|
@@ -392,12 +422,13 @@ class RasUtils:
|
|
392
422
|
ras_obj.geom_df = ras_obj.get_geom_entries()
|
393
423
|
ras_obj.flow_df = ras_obj.get_flow_entries()
|
394
424
|
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
395
|
-
|
425
|
+
logger.info("RAS object dataframes have been refreshed.")
|
396
426
|
except Exception as e:
|
397
|
-
|
427
|
+
logger.exception("Failed to refresh RasPrj dataframes")
|
398
428
|
raise
|
399
429
|
|
400
430
|
@staticmethod
|
431
|
+
@log_call
|
401
432
|
def check_file_access(file_path: Path, mode: str = 'r') -> None:
|
402
433
|
"""
|
403
434
|
Check if the file can be accessed with the specified mode.
|
@@ -410,36 +441,30 @@ class RasUtils:
|
|
410
441
|
FileNotFoundError: If the file does not exist
|
411
442
|
PermissionError: If the required permissions are not met
|
412
443
|
"""
|
444
|
+
|
413
445
|
path = Path(file_path)
|
414
446
|
if not path.exists():
|
415
|
-
|
447
|
+
logger.error(f"File not found: {file_path}")
|
416
448
|
raise FileNotFoundError(f"File not found: {file_path}")
|
417
449
|
|
418
450
|
if mode in ('r', 'rb'):
|
419
451
|
if not os.access(path, os.R_OK):
|
420
|
-
|
452
|
+
logger.error(f"Read permission denied for file: {file_path}")
|
421
453
|
raise PermissionError(f"Read permission denied for file: {file_path}")
|
422
454
|
else:
|
423
|
-
|
455
|
+
logger.debug(f"Read access granted for file: {file_path}")
|
424
456
|
|
425
457
|
if mode in ('w', 'wb', 'a', 'ab'):
|
426
458
|
parent_dir = path.parent
|
427
459
|
if not os.access(parent_dir, os.W_OK):
|
428
|
-
|
460
|
+
logger.error(f"Write permission denied for directory: {parent_dir}")
|
429
461
|
raise PermissionError(f"Write permission denied for directory: {parent_dir}")
|
430
462
|
else:
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
# -------------------------- Functions below were imported from funkshuns.py --------------------------
|
437
|
-
# -------------------------- Converted to ras-commander style guide ----------------------------------
|
438
|
-
|
439
|
-
|
463
|
+
logger.debug(f"Write access granted for directory: {parent_dir}")
|
440
464
|
|
441
465
|
|
442
466
|
@staticmethod
|
467
|
+
@log_call
|
443
468
|
def convert_to_dataframe(data_source: Union[pd.DataFrame, Path], **kwargs) -> pd.DataFrame:
|
444
469
|
"""
|
445
470
|
Converts input to a pandas DataFrame. Supports existing DataFrames or file paths (CSV, Excel, TSV, Parquet).
|
@@ -458,13 +483,13 @@ class RasUtils:
|
|
458
483
|
>>> df = RasUtils.convert_to_dataframe(Path("data.csv"))
|
459
484
|
>>> print(type(df))
|
460
485
|
<class 'pandas.core.frame.DataFrame'>
|
461
|
-
|
462
|
-
Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
|
463
486
|
"""
|
464
487
|
if isinstance(data_source, pd.DataFrame):
|
488
|
+
logger.debug("Input is already a DataFrame, returning a copy.")
|
465
489
|
return data_source.copy()
|
466
490
|
elif isinstance(data_source, Path):
|
467
491
|
ext = data_source.suffix.replace('.', '', 1)
|
492
|
+
logger.info(f"Converting file with extension '{ext}' to DataFrame.")
|
468
493
|
if ext == 'csv':
|
469
494
|
return pd.read_csv(data_source, **kwargs)
|
470
495
|
elif ext.startswith('x'):
|
@@ -474,11 +499,14 @@ class RasUtils:
|
|
474
499
|
elif ext in ["parquet", "pq", "parq"]:
|
475
500
|
return pd.read_parquet(data_source, **kwargs)
|
476
501
|
else:
|
502
|
+
logger.error(f"Unsupported file type: {ext}")
|
477
503
|
raise NotImplementedError(f"Unsupported file type {ext}. Should be one of csv, tsv, parquet, or xlsx.")
|
478
504
|
else:
|
505
|
+
logger.error(f"Unsupported input type: {type(data_source)}")
|
479
506
|
raise NotImplementedError(f"Unsupported type {type(data_source)}. Only file path / existing DataFrame supported at this time")
|
480
507
|
|
481
508
|
@staticmethod
|
509
|
+
@log_call
|
482
510
|
def save_to_excel(dataframe: pd.DataFrame, excel_path: Path, **kwargs) -> None:
|
483
511
|
"""
|
484
512
|
Saves a pandas DataFrame to an Excel file with retry functionality.
|
@@ -494,8 +522,6 @@ class RasUtils:
|
|
494
522
|
Example:
|
495
523
|
>>> df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
|
496
524
|
>>> RasUtils.save_to_excel(df, Path('output.xlsx'))
|
497
|
-
|
498
|
-
Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
|
499
525
|
"""
|
500
526
|
saved = False
|
501
527
|
max_attempts = 3
|
@@ -504,28 +530,18 @@ class RasUtils:
|
|
504
530
|
while not saved and attempt < max_attempts:
|
505
531
|
try:
|
506
532
|
dataframe.to_excel(excel_path, **kwargs)
|
507
|
-
|
533
|
+
logger.info(f'DataFrame successfully saved to {excel_path}')
|
508
534
|
saved = True
|
509
535
|
except IOError as e:
|
510
536
|
attempt += 1
|
511
537
|
if attempt < max_attempts:
|
512
|
-
|
538
|
+
logger.warning(f"Error saving file. Attempt {attempt} of {max_attempts}. Please close the Excel document if it's open.")
|
513
539
|
else:
|
540
|
+
logger.error(f"Failed to save {excel_path} after {max_attempts} attempts.")
|
514
541
|
raise IOError(f"Failed to save {excel_path} after {max_attempts} attempts. Last error: {str(e)}")
|
515
542
|
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
##### Statistical Metrics #####
|
526
|
-
|
527
|
-
|
528
543
|
@staticmethod
|
544
|
+
@log_call
|
529
545
|
def calculate_rmse(observed_values: np.ndarray, predicted_values: np.ndarray, normalized: bool = True) -> float:
|
530
546
|
"""
|
531
547
|
Calculate the Root Mean Squared Error (RMSE) between observed and predicted values.
|
@@ -543,17 +559,17 @@ class RasUtils:
|
|
543
559
|
>>> predicted = np.array([1.1, 2.2, 2.9])
|
544
560
|
>>> RasUtils.calculate_rmse(observed, predicted)
|
545
561
|
0.06396394
|
546
|
-
|
547
|
-
Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
|
548
562
|
"""
|
549
563
|
rmse = np.sqrt(np.mean((predicted_values - observed_values) ** 2))
|
550
564
|
|
551
565
|
if normalized:
|
552
566
|
rmse = rmse / np.abs(np.mean(observed_values))
|
553
567
|
|
568
|
+
logger.debug(f"Calculated RMSE: {rmse}")
|
554
569
|
return rmse
|
555
570
|
|
556
571
|
@staticmethod
|
572
|
+
@log_call
|
557
573
|
def calculate_percent_bias(observed_values: np.ndarray, predicted_values: np.ndarray, as_percentage: bool = False) -> float:
|
558
574
|
"""
|
559
575
|
Calculate the Percent Bias between observed and predicted values.
|
@@ -571,16 +587,16 @@ class RasUtils:
|
|
571
587
|
>>> predicted = np.array([1.1, 2.2, 2.9])
|
572
588
|
>>> RasUtils.calculate_percent_bias(observed, predicted, as_percentage=True)
|
573
589
|
3.33333333
|
574
|
-
|
575
|
-
Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
|
576
590
|
"""
|
577
591
|
multiplier = 100 if as_percentage else 1
|
578
592
|
|
579
593
|
percent_bias = multiplier * (np.mean(predicted_values) - np.mean(observed_values)) / np.mean(observed_values)
|
580
594
|
|
595
|
+
logger.debug(f"Calculated Percent Bias: {percent_bias}")
|
581
596
|
return percent_bias
|
582
597
|
|
583
598
|
@staticmethod
|
599
|
+
@log_call
|
584
600
|
def calculate_error_metrics(observed_values: np.ndarray, predicted_values: np.ndarray) -> Dict[str, float]:
|
585
601
|
"""
|
586
602
|
Compute a trio of error metrics: correlation, RMSE, and Percent Bias.
|
@@ -597,14 +613,14 @@ class RasUtils:
|
|
597
613
|
>>> predicted = np.array([1.1, 2.2, 2.9])
|
598
614
|
>>> RasUtils.calculate_error_metrics(observed, predicted)
|
599
615
|
{'cor': 0.9993, 'rmse': 0.06396, 'pb': 0.03333}
|
600
|
-
|
601
|
-
Attribution Note: This function is sourced from funkshuns.py by Sean Micek, and converted to the ras-commander style guide.
|
602
616
|
"""
|
603
617
|
correlation = np.corrcoef(observed_values, predicted_values)[0, 1]
|
604
618
|
rmse = RasUtils.calculate_rmse(observed_values, predicted_values)
|
605
619
|
percent_bias = RasUtils.calculate_percent_bias(observed_values, predicted_values)
|
606
620
|
|
607
|
-
|
621
|
+
metrics = {'cor': correlation, 'rmse': rmse, 'pb': percent_bias}
|
622
|
+
logger.info(f"Calculated error metrics: {metrics}")
|
623
|
+
return metrics
|
608
624
|
|
609
625
|
|
610
626
|
|
ras_commander/__init__.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from importlib.metadata import version, PackageNotFoundError
|
2
|
+
from .logging_config import setup_logging, get_logger, log_call
|
2
3
|
|
3
4
|
try:
|
4
5
|
__version__ = version("ras-commander")
|
@@ -6,6 +7,9 @@ except PackageNotFoundError:
|
|
6
7
|
# package is not installed
|
7
8
|
__version__ = "unknown"
|
8
9
|
|
10
|
+
# Set up logging
|
11
|
+
setup_logging()
|
12
|
+
|
9
13
|
# Import all necessary functions and classes directly
|
10
14
|
from .RasPrj import ras, init_ras_project, get_ras_exe
|
11
15
|
from .RasPrj import RasPrj
|
@@ -15,17 +19,8 @@ from .RasUnsteady import RasUnsteady
|
|
15
19
|
from .RasCmdr import RasCmdr
|
16
20
|
from .RasUtils import RasUtils
|
17
21
|
from .RasExamples import RasExamples
|
18
|
-
from .RasHdf import RasHdf
|
19
|
-
|
20
|
-
# Import all attributes from these modules
|
21
|
-
from .RasPrj import *
|
22
|
-
from .RasPlan import *
|
23
|
-
from .RasGeo import *
|
24
|
-
from .RasUnsteady import *
|
25
|
-
from .RasCmdr import *
|
26
|
-
from .RasUtils import *
|
27
|
-
from .RasExamples import *
|
28
|
-
from .RasHdf import * # Add this line
|
22
|
+
from .RasHdf import RasHdf
|
23
|
+
from .RasGpt import RasGpt
|
29
24
|
|
30
25
|
# Define __all__ to specify what should be imported when using "from ras_commander import *"
|
31
26
|
__all__ = [
|
@@ -39,5 +34,8 @@ __all__ = [
|
|
39
34
|
"RasCmdr",
|
40
35
|
"RasUtils",
|
41
36
|
"RasExamples",
|
42
|
-
"RasHdf"
|
43
|
-
|
37
|
+
"RasHdf",
|
38
|
+
"RasGpt",
|
39
|
+
"get_logger",
|
40
|
+
"log_call"
|
41
|
+
]
|