ras-commander 0.33.0__py3-none-any.whl → 0.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ras_commander/RasCmdr.py +171 -138
- ras_commander/RasExamples.py +334 -120
- ras_commander/RasGeo.py +27 -6
- ras_commander/RasHdf.py +1702 -0
- ras_commander/RasPlan.py +398 -437
- ras_commander/RasPrj.py +403 -65
- ras_commander/RasUnsteady.py +24 -4
- ras_commander/RasUtils.py +352 -51
- ras_commander/__init__.py +4 -1
- ras_commander-0.35.0.dist-info/METADATA +319 -0
- ras_commander-0.35.0.dist-info/RECORD +15 -0
- ras_commander-0.33.0.dist-info/METADATA +0 -5
- ras_commander-0.33.0.dist-info/RECORD +0 -14
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/LICENSE +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/WHEEL +0 -0
- {ras_commander-0.33.0.dist-info → ras_commander-0.35.0.dist-info}/top_level.txt +0 -0
ras_commander/RasUtils.py
CHANGED
@@ -7,7 +7,15 @@ import logging
|
|
7
7
|
import time
|
8
8
|
from pathlib import Path
|
9
9
|
from .RasPrj import ras
|
10
|
-
from typing import Union
|
10
|
+
from typing import Union, Optional, Dict
|
11
|
+
import pandas as pd
|
12
|
+
import numpy as np
|
13
|
+
|
14
|
+
# Configure logging
|
15
|
+
logging.basicConfig(
|
16
|
+
level=logging.INFO,
|
17
|
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
18
|
+
)
|
11
19
|
|
12
20
|
class RasUtils:
|
13
21
|
"""
|
@@ -37,8 +45,12 @@ class RasUtils:
|
|
37
45
|
|
38
46
|
original_path = Path(file_path)
|
39
47
|
backup_path = original_path.with_name(f"{original_path.stem}{backup_suffix}{original_path.suffix}")
|
40
|
-
|
41
|
-
|
48
|
+
try:
|
49
|
+
shutil.copy2(original_path, backup_path)
|
50
|
+
logging.info(f"Backup created: {backup_path}")
|
51
|
+
except Exception as e:
|
52
|
+
logging.error(f"Failed to create backup for {original_path}: {e}")
|
53
|
+
raise
|
42
54
|
return backup_path
|
43
55
|
|
44
56
|
@staticmethod
|
@@ -62,12 +74,21 @@ class RasUtils:
|
|
62
74
|
ras_obj.check_initialized()
|
63
75
|
|
64
76
|
backup_path = Path(backup_path)
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
77
|
+
if '_backup' not in backup_path.stem:
|
78
|
+
logging.error(f"Backup suffix '_backup' not found in {backup_path.name}")
|
79
|
+
raise ValueError(f"Backup suffix '_backup' not found in {backup_path.name}")
|
80
|
+
|
81
|
+
original_stem = backup_path.stem.rsplit('_backup', 1)[0]
|
82
|
+
original_path = backup_path.with_name(f"{original_stem}{backup_path.suffix}")
|
83
|
+
try:
|
84
|
+
shutil.copy2(backup_path, original_path)
|
85
|
+
logging.info(f"File restored: {original_path}")
|
86
|
+
if remove_backup:
|
87
|
+
backup_path.unlink()
|
88
|
+
logging.info(f"Backup removed: {backup_path}")
|
89
|
+
except Exception as e:
|
90
|
+
logging.error(f"Failed to restore from backup {backup_path}: {e}")
|
91
|
+
raise
|
71
92
|
return original_path
|
72
93
|
|
73
94
|
@staticmethod
|
@@ -90,8 +111,12 @@ class RasUtils:
|
|
90
111
|
ras_obj.check_initialized()
|
91
112
|
|
92
113
|
path = Path(directory_path)
|
93
|
-
|
94
|
-
|
114
|
+
try:
|
115
|
+
path.mkdir(parents=True, exist_ok=True)
|
116
|
+
logging.info(f"Directory ensured: {path}")
|
117
|
+
except Exception as e:
|
118
|
+
logging.error(f"Failed to create directory {path}: {e}")
|
119
|
+
raise
|
95
120
|
return path
|
96
121
|
|
97
122
|
@staticmethod
|
@@ -113,11 +138,17 @@ class RasUtils:
|
|
113
138
|
ras_obj = ras_object or ras
|
114
139
|
ras_obj.check_initialized()
|
115
140
|
|
116
|
-
|
117
|
-
|
141
|
+
try:
|
142
|
+
files = list(ras_obj.project_folder.glob(f"*{extension}"))
|
143
|
+
file_list = [str(file) for file in files]
|
144
|
+
logging.info(f"Found {len(file_list)} files with extension '{extension}' in {ras_obj.project_folder}")
|
145
|
+
return file_list
|
146
|
+
except Exception as e:
|
147
|
+
logging.error(f"Failed to find files with extension '{extension}': {e}")
|
148
|
+
raise
|
118
149
|
|
119
150
|
@staticmethod
|
120
|
-
def get_file_size(file_path: Path, ras_object=None) -> int:
|
151
|
+
def get_file_size(file_path: Path, ras_object=None) -> Optional[int]:
|
121
152
|
"""
|
122
153
|
Get the size of a file in bytes.
|
123
154
|
|
@@ -126,7 +157,7 @@ class RasUtils:
|
|
126
157
|
ras_object (RasPrj, optional): RAS object to use. If None, uses the default ras object.
|
127
158
|
|
128
159
|
Returns:
|
129
|
-
int: Size of the file in bytes
|
160
|
+
Optional[int]: Size of the file in bytes, or None if the file does not exist
|
130
161
|
|
131
162
|
Example:
|
132
163
|
>>> size = RasUtils.get_file_size(Path("project.prj"))
|
@@ -137,13 +168,19 @@ class RasUtils:
|
|
137
168
|
|
138
169
|
path = Path(file_path)
|
139
170
|
if path.exists():
|
140
|
-
|
171
|
+
try:
|
172
|
+
size = path.stat().st_size
|
173
|
+
logging.info(f"Size of {path}: {size} bytes")
|
174
|
+
return size
|
175
|
+
except Exception as e:
|
176
|
+
logging.error(f"Failed to get size for {path}: {e}")
|
177
|
+
raise
|
141
178
|
else:
|
142
179
|
logging.warning(f"File not found: {path}")
|
143
180
|
return None
|
144
181
|
|
145
182
|
@staticmethod
|
146
|
-
def get_file_modification_time(file_path: Path, ras_object=None) -> float:
|
183
|
+
def get_file_modification_time(file_path: Path, ras_object=None) -> Optional[float]:
|
147
184
|
"""
|
148
185
|
Get the last modification time of a file.
|
149
186
|
|
@@ -152,7 +189,7 @@ class RasUtils:
|
|
152
189
|
ras_object (RasPrj, optional): RAS object to use. If None, uses the default ras object.
|
153
190
|
|
154
191
|
Returns:
|
155
|
-
float: Last modification time as a timestamp
|
192
|
+
Optional[float]: Last modification time as a timestamp, or None if the file does not exist
|
156
193
|
|
157
194
|
Example:
|
158
195
|
>>> mtime = RasUtils.get_file_modification_time(Path("project.prj"))
|
@@ -163,7 +200,13 @@ class RasUtils:
|
|
163
200
|
|
164
201
|
path = Path(file_path)
|
165
202
|
if path.exists():
|
166
|
-
|
203
|
+
try:
|
204
|
+
mtime = path.stat().st_mtime
|
205
|
+
logging.info(f"Last modification time of {path}: {mtime}")
|
206
|
+
return mtime
|
207
|
+
except Exception as e:
|
208
|
+
logging.error(f"Failed to get modification time for {path}: {e}")
|
209
|
+
raise
|
167
210
|
else:
|
168
211
|
logging.warning(f"File not found: {path}")
|
169
212
|
return None
|
@@ -191,18 +234,29 @@ class RasUtils:
|
|
191
234
|
|
192
235
|
plan_path = Path(current_plan_number_or_path)
|
193
236
|
if plan_path.is_file():
|
237
|
+
logging.info(f"Using provided plan file path: {plan_path}")
|
194
238
|
return plan_path
|
195
239
|
|
196
240
|
try:
|
197
241
|
current_plan_number = f"{int(current_plan_number_or_path):02d}" # Ensure two-digit format
|
242
|
+
logging.info(f"Converted plan number to two-digit format: {current_plan_number}")
|
198
243
|
except ValueError:
|
244
|
+
logging.error(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
|
199
245
|
raise ValueError(f"Invalid plan number: {current_plan_number_or_path}. Expected a number from 1 to 99.")
|
200
246
|
|
201
247
|
plan_name = f"{ras_obj.project_name}.p{current_plan_number}"
|
202
|
-
|
248
|
+
full_plan_path = ras_obj.project_folder / plan_name
|
249
|
+
logging.info(f"Constructed plan file path: {full_plan_path}")
|
250
|
+
return full_plan_path
|
203
251
|
|
204
252
|
@staticmethod
|
205
|
-
def remove_with_retry(
|
253
|
+
def remove_with_retry(
|
254
|
+
path: Path,
|
255
|
+
max_attempts: int = 5,
|
256
|
+
initial_delay: float = 1.0,
|
257
|
+
is_folder: bool = True,
|
258
|
+
ras_object=None
|
259
|
+
) -> bool:
|
206
260
|
"""
|
207
261
|
Attempts to remove a file or folder with retry logic and exponential backoff.
|
208
262
|
|
@@ -224,26 +278,43 @@ class RasUtils:
|
|
224
278
|
ras_obj.check_initialized()
|
225
279
|
|
226
280
|
path = Path(path)
|
227
|
-
for attempt in range(max_attempts):
|
281
|
+
for attempt in range(1, max_attempts + 1):
|
228
282
|
try:
|
229
283
|
if path.exists():
|
230
284
|
if is_folder:
|
231
285
|
shutil.rmtree(path)
|
286
|
+
logging.info(f"Folder removed: {path}")
|
232
287
|
else:
|
233
288
|
path.unlink()
|
289
|
+
logging.info(f"File removed: {path}")
|
290
|
+
else:
|
291
|
+
logging.info(f"Path does not exist, nothing to remove: {path}")
|
234
292
|
return True
|
235
|
-
except PermissionError:
|
236
|
-
if attempt < max_attempts
|
237
|
-
delay = initial_delay * (2 ** attempt) # Exponential backoff
|
238
|
-
logging.warning(
|
293
|
+
except PermissionError as pe:
|
294
|
+
if attempt < max_attempts:
|
295
|
+
delay = initial_delay * (2 ** (attempt - 1)) # Exponential backoff
|
296
|
+
logging.warning(
|
297
|
+
f"PermissionError on attempt {attempt} to remove {path}: {pe}. "
|
298
|
+
f"Retrying in {delay} seconds..."
|
299
|
+
)
|
239
300
|
time.sleep(delay)
|
240
301
|
else:
|
241
|
-
logging.error(
|
302
|
+
logging.error(
|
303
|
+
f"Failed to remove {path} after {max_attempts} attempts due to PermissionError: {pe}. Skipping."
|
304
|
+
)
|
242
305
|
return False
|
306
|
+
except Exception as e:
|
307
|
+
logging.error(f"Failed to remove {path} on attempt {attempt}: {e}")
|
308
|
+
return False
|
243
309
|
return False
|
244
310
|
|
245
311
|
@staticmethod
|
246
|
-
def update_plan_file(
|
312
|
+
def update_plan_file(
|
313
|
+
plan_number_or_path: Union[str, Path],
|
314
|
+
file_type: str,
|
315
|
+
entry_number: int,
|
316
|
+
ras_object=None
|
317
|
+
) -> None:
|
247
318
|
"""
|
248
319
|
Update a plan file with a new file reference.
|
249
320
|
|
@@ -266,45 +337,275 @@ class RasUtils:
|
|
266
337
|
|
267
338
|
valid_file_types = {'Geom': 'g', 'Flow': 'f', 'Unsteady': 'u'}
|
268
339
|
if file_type not in valid_file_types:
|
269
|
-
|
340
|
+
logging.error(
|
341
|
+
f"Invalid file_type '{file_type}'. Expected one of: {', '.join(valid_file_types.keys())}"
|
342
|
+
)
|
343
|
+
raise ValueError(
|
344
|
+
f"Invalid file_type. Expected one of: {', '.join(valid_file_types.keys())}"
|
345
|
+
)
|
270
346
|
|
271
347
|
plan_file_path = Path(plan_number_or_path)
|
272
348
|
if not plan_file_path.is_file():
|
273
349
|
plan_file_path = RasUtils.get_plan_path(plan_number_or_path, ras_object)
|
350
|
+
if not plan_file_path.exists():
|
351
|
+
logging.error(f"Plan file not found: {plan_file_path}")
|
352
|
+
raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
|
274
353
|
|
275
|
-
if not plan_file_path.exists():
|
276
|
-
raise FileNotFoundError(f"Plan file not found: {plan_file_path}")
|
277
|
-
|
278
354
|
file_prefix = valid_file_types[file_type]
|
279
355
|
search_pattern = f"{file_type} File="
|
280
|
-
|
281
|
-
|
282
|
-
RasUtils.check_file_access(plan_file_path, 'r')
|
283
|
-
with open(plan_file_path, 'r') as file:
|
284
|
-
lines = file.readlines()
|
356
|
+
formatted_entry_number = f"{int(entry_number):02d}" # Ensure two-digit format
|
285
357
|
|
358
|
+
try:
|
359
|
+
RasUtils.check_file_access(plan_file_path, 'r')
|
360
|
+
with plan_file_path.open('r') as file:
|
361
|
+
lines = file.readlines()
|
362
|
+
except Exception as e:
|
363
|
+
logging.error(f"Failed to read plan file {plan_file_path}: {e}")
|
364
|
+
raise
|
365
|
+
|
366
|
+
updated = False
|
286
367
|
for i, line in enumerate(lines):
|
287
368
|
if line.startswith(search_pattern):
|
288
|
-
lines[i] = f"{search_pattern}{file_prefix}{
|
289
|
-
logging.info(
|
369
|
+
lines[i] = f"{search_pattern}{file_prefix}{formatted_entry_number}\n"
|
370
|
+
logging.info(
|
371
|
+
f"Updated {file_type} File in {plan_file_path} to {file_prefix}{formatted_entry_number}"
|
372
|
+
)
|
373
|
+
updated = True
|
290
374
|
break
|
291
375
|
|
292
|
-
|
293
|
-
|
376
|
+
if not updated:
|
377
|
+
logging.warning(
|
378
|
+
f"Search pattern '{search_pattern}' not found in {plan_file_path}. No update performed."
|
379
|
+
)
|
294
380
|
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
381
|
+
try:
|
382
|
+
with plan_file_path.open('w') as file:
|
383
|
+
file.writelines(lines)
|
384
|
+
logging.info(f"Successfully updated plan file: {plan_file_path}")
|
385
|
+
except Exception as e:
|
386
|
+
logging.error(f"Failed to write updates to plan file {plan_file_path}: {e}")
|
387
|
+
raise
|
388
|
+
|
389
|
+
# Refresh RasPrj dataframes
|
390
|
+
try:
|
391
|
+
ras_obj.plan_df = ras_obj.get_plan_entries()
|
392
|
+
ras_obj.geom_df = ras_obj.get_geom_entries()
|
393
|
+
ras_obj.flow_df = ras_obj.get_flow_entries()
|
394
|
+
ras_obj.unsteady_df = ras_obj.get_unsteady_entries()
|
395
|
+
logging.info("RAS object dataframes have been refreshed.")
|
396
|
+
except Exception as e:
|
397
|
+
logging.error(f"Failed to refresh RasPrj dataframes: {e}")
|
398
|
+
raise
|
300
399
|
|
301
400
|
@staticmethod
|
302
|
-
def check_file_access(file_path, mode='r'):
|
401
|
+
def check_file_access(file_path: Path, mode: str = 'r') -> None:
|
402
|
+
"""
|
403
|
+
Check if the file can be accessed with the specified mode.
|
404
|
+
|
405
|
+
Parameters:
|
406
|
+
file_path (Path): Path to the file
|
407
|
+
mode (str): Mode to check ('r' for read, 'w' for write, etc.)
|
408
|
+
|
409
|
+
Raises:
|
410
|
+
FileNotFoundError: If the file does not exist
|
411
|
+
PermissionError: If the required permissions are not met
|
412
|
+
"""
|
303
413
|
path = Path(file_path)
|
304
414
|
if not path.exists():
|
415
|
+
logging.error(f"File not found: {file_path}")
|
305
416
|
raise FileNotFoundError(f"File not found: {file_path}")
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
417
|
+
|
418
|
+
if mode in ('r', 'rb'):
|
419
|
+
if not os.access(path, os.R_OK):
|
420
|
+
logging.error(f"Read permission denied for file: {file_path}")
|
421
|
+
raise PermissionError(f"Read permission denied for file: {file_path}")
|
422
|
+
else:
|
423
|
+
logging.debug(f"Read access granted for file: {file_path}")
|
424
|
+
|
425
|
+
if mode in ('w', 'wb', 'a', 'ab'):
|
426
|
+
parent_dir = path.parent
|
427
|
+
if not os.access(parent_dir, os.W_OK):
|
428
|
+
logging.error(f"Write permission denied for directory: {parent_dir}")
|
429
|
+
raise PermissionError(f"Write permission denied for directory: {parent_dir}")
|
430
|
+
else:
|
431
|
+
logging.debug(f"Write access granted for directory: {parent_dir}")
|
432
|
+
|
433
|
+
|
434
|
+
|
435
|
+
|
436
|
+
# -------------------------- Functions below were imported from funkshuns.py --------------------------
|
437
|
+
# -------------------------- Converted to ras-commander style guide ----------------------------------
|
438
|
+
|
439
|
+
|
440
|
+
|
441
|
+
|
442
|
+
@staticmethod
|
443
|
+
def convert_to_dataframe(data_source: Union[pd.DataFrame, Path], **kwargs) -> pd.DataFrame:
|
444
|
+
"""
|
445
|
+
Converts input to a pandas DataFrame. Supports existing DataFrames or file paths (CSV, Excel, TSV, Parquet).
|
446
|
+
|
447
|
+
Args:
|
448
|
+
data_source (Union[pd.DataFrame, Path]): The input to convert to a DataFrame. Can be a file path or an existing DataFrame.
|
449
|
+
**kwargs: Additional keyword arguments to pass to pandas read functions.
|
450
|
+
|
451
|
+
Returns:
|
452
|
+
pd.DataFrame: The resulting DataFrame.
|
453
|
+
|
454
|
+
Raises:
|
455
|
+
NotImplementedError: If the file type is unsupported or input type is invalid.
|
456
|
+
|
457
|
+
Example:
|
458
|
+
>>> df = RasUtils.convert_to_dataframe(Path("data.csv"))
|
459
|
+
>>> print(type(df))
|
460
|
+
<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
|
+
"""
|
464
|
+
if isinstance(data_source, pd.DataFrame):
|
465
|
+
return data_source.copy()
|
466
|
+
elif isinstance(data_source, Path):
|
467
|
+
ext = data_source.suffix.replace('.', '', 1)
|
468
|
+
if ext == 'csv':
|
469
|
+
return pd.read_csv(data_source, **kwargs)
|
470
|
+
elif ext.startswith('x'):
|
471
|
+
return pd.read_excel(data_source, **kwargs)
|
472
|
+
elif ext == "tsv":
|
473
|
+
return pd.read_csv(data_source, sep="\t", **kwargs)
|
474
|
+
elif ext in ["parquet", "pq", "parq"]:
|
475
|
+
return pd.read_parquet(data_source, **kwargs)
|
476
|
+
else:
|
477
|
+
raise NotImplementedError(f"Unsupported file type {ext}. Should be one of csv, tsv, parquet, or xlsx.")
|
478
|
+
else:
|
479
|
+
raise NotImplementedError(f"Unsupported type {type(data_source)}. Only file path / existing DataFrame supported at this time")
|
480
|
+
|
481
|
+
@staticmethod
|
482
|
+
def save_to_excel(dataframe: pd.DataFrame, excel_path: Path, **kwargs) -> None:
|
483
|
+
"""
|
484
|
+
Saves a pandas DataFrame to an Excel file with retry functionality.
|
485
|
+
|
486
|
+
Args:
|
487
|
+
dataframe (pd.DataFrame): The DataFrame to save.
|
488
|
+
excel_path (Path): The path to the Excel file where the DataFrame will be saved.
|
489
|
+
**kwargs: Additional keyword arguments passed to `DataFrame.to_excel()`.
|
490
|
+
|
491
|
+
Raises:
|
492
|
+
IOError: If the file cannot be saved after multiple attempts.
|
493
|
+
|
494
|
+
Example:
|
495
|
+
>>> df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
|
496
|
+
>>> 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
|
+
"""
|
500
|
+
saved = False
|
501
|
+
max_attempts = 3
|
502
|
+
attempt = 0
|
503
|
+
|
504
|
+
while not saved and attempt < max_attempts:
|
505
|
+
try:
|
506
|
+
dataframe.to_excel(excel_path, **kwargs)
|
507
|
+
print(f'DataFrame successfully saved to \n{excel_path}')
|
508
|
+
saved = True
|
509
|
+
except IOError as e:
|
510
|
+
attempt += 1
|
511
|
+
if attempt < max_attempts:
|
512
|
+
print(f"Error saving file. Attempt {attempt} of {max_attempts}. Please close the Excel document if it's open.")
|
513
|
+
else:
|
514
|
+
raise IOError(f"Failed to save {excel_path} after {max_attempts} attempts. Last error: {str(e)}")
|
515
|
+
|
516
|
+
|
517
|
+
|
518
|
+
|
519
|
+
|
520
|
+
|
521
|
+
|
522
|
+
|
523
|
+
|
524
|
+
|
525
|
+
##### Statistical Metrics #####
|
526
|
+
|
527
|
+
|
528
|
+
@staticmethod
|
529
|
+
def calculate_rmse(observed_values: np.ndarray, predicted_values: np.ndarray, normalized: bool = True) -> float:
|
530
|
+
"""
|
531
|
+
Calculate the Root Mean Squared Error (RMSE) between observed and predicted values.
|
532
|
+
|
533
|
+
Args:
|
534
|
+
observed_values (np.ndarray): Actual observations time series.
|
535
|
+
predicted_values (np.ndarray): Estimated/predicted time series.
|
536
|
+
normalized (bool, optional): Whether to normalize RMSE to a percentage of observed_values. Defaults to True.
|
537
|
+
|
538
|
+
Returns:
|
539
|
+
float: The calculated RMSE value.
|
540
|
+
|
541
|
+
Example:
|
542
|
+
>>> observed = np.array([1, 2, 3])
|
543
|
+
>>> predicted = np.array([1.1, 2.2, 2.9])
|
544
|
+
>>> RasUtils.calculate_rmse(observed, predicted)
|
545
|
+
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
|
+
"""
|
549
|
+
rmse = np.sqrt(np.mean((predicted_values - observed_values) ** 2))
|
550
|
+
|
551
|
+
if normalized:
|
552
|
+
rmse = rmse / np.abs(np.mean(observed_values))
|
553
|
+
|
554
|
+
return rmse
|
555
|
+
|
556
|
+
@staticmethod
|
557
|
+
def calculate_percent_bias(observed_values: np.ndarray, predicted_values: np.ndarray, as_percentage: bool = False) -> float:
|
558
|
+
"""
|
559
|
+
Calculate the Percent Bias between observed and predicted values.
|
560
|
+
|
561
|
+
Args:
|
562
|
+
observed_values (np.ndarray): Actual observations time series.
|
563
|
+
predicted_values (np.ndarray): Estimated/predicted time series.
|
564
|
+
as_percentage (bool, optional): If True, return bias as a percentage. Defaults to False.
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
float: The calculated Percent Bias.
|
568
|
+
|
569
|
+
Example:
|
570
|
+
>>> observed = np.array([1, 2, 3])
|
571
|
+
>>> predicted = np.array([1.1, 2.2, 2.9])
|
572
|
+
>>> RasUtils.calculate_percent_bias(observed, predicted, as_percentage=True)
|
573
|
+
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
|
+
"""
|
577
|
+
multiplier = 100 if as_percentage else 1
|
578
|
+
|
579
|
+
percent_bias = multiplier * (np.mean(predicted_values) - np.mean(observed_values)) / np.mean(observed_values)
|
580
|
+
|
581
|
+
return percent_bias
|
582
|
+
|
583
|
+
@staticmethod
|
584
|
+
def calculate_error_metrics(observed_values: np.ndarray, predicted_values: np.ndarray) -> Dict[str, float]:
|
585
|
+
"""
|
586
|
+
Compute a trio of error metrics: correlation, RMSE, and Percent Bias.
|
587
|
+
|
588
|
+
Args:
|
589
|
+
observed_values (np.ndarray): Actual observations time series.
|
590
|
+
predicted_values (np.ndarray): Estimated/predicted time series.
|
591
|
+
|
592
|
+
Returns:
|
593
|
+
Dict[str, float]: A dictionary containing correlation ('cor'), RMSE ('rmse'), and Percent Bias ('pb').
|
594
|
+
|
595
|
+
Example:
|
596
|
+
>>> observed = np.array([1, 2, 3])
|
597
|
+
>>> predicted = np.array([1.1, 2.2, 2.9])
|
598
|
+
>>> RasUtils.calculate_error_metrics(observed, predicted)
|
599
|
+
{'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
|
+
"""
|
603
|
+
correlation = np.corrcoef(observed_values, predicted_values)[0, 1]
|
604
|
+
rmse = RasUtils.calculate_rmse(observed_values, predicted_values)
|
605
|
+
percent_bias = RasUtils.calculate_percent_bias(observed_values, predicted_values)
|
606
|
+
|
607
|
+
return {'cor': correlation, 'rmse': rmse, 'pb': percent_bias}
|
608
|
+
|
609
|
+
|
610
|
+
|
310
611
|
|
ras_commander/__init__.py
CHANGED
@@ -15,6 +15,7 @@ from .RasUnsteady import RasUnsteady
|
|
15
15
|
from .RasCmdr import RasCmdr
|
16
16
|
from .RasUtils import RasUtils
|
17
17
|
from .RasExamples import RasExamples
|
18
|
+
from .RasHdf import RasHdf # Add this line
|
18
19
|
|
19
20
|
# Import all attributes from these modules
|
20
21
|
from .RasPrj import *
|
@@ -24,6 +25,7 @@ from .RasUnsteady import *
|
|
24
25
|
from .RasCmdr import *
|
25
26
|
from .RasUtils import *
|
26
27
|
from .RasExamples import *
|
28
|
+
from .RasHdf import * # Add this line
|
27
29
|
|
28
30
|
# Define __all__ to specify what should be imported when using "from ras_commander import *"
|
29
31
|
__all__ = [
|
@@ -36,5 +38,6 @@ __all__ = [
|
|
36
38
|
"RasUnsteady",
|
37
39
|
"RasCmdr",
|
38
40
|
"RasUtils",
|
39
|
-
"RasExamples"
|
41
|
+
"RasExamples",
|
42
|
+
"RasHdf" # Add this line
|
40
43
|
]
|