• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hydrologie / xhydro / 13865314259

14 Mar 2025 09:03PM UTC coverage: 93.467%. First build
13865314259

Pull #268

github

web-flow
Merge 641951adc into 57e40d79b
Pull Request #268: Add a Use Case example and fix kernel crashes

4 of 16 new or added lines in 2 files covered. (25.0%)

2418 of 2587 relevant lines covered (93.47%)

3.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.17
/src/xhydro/modelling/obj_funcs.py
1
# Created on Tue Dec 12 21:55:25 2023
2
# @author: Richard Arsenault
3
"""Objective function package for xhydro, for calibration and model evaluation.
4

5
This package provides a flexible suite of popular objective function metrics in
6
hydrological modelling and hydrological model calibration. The main function
7
'get_objective_function' returns the value of the desired objective function
8
while allowing users to customize many aspects:
9

10
    1-  Select the objective function to run;
11
    2-  Allow providing a mask to remove certain elements from the objective
12
    function calculation (e.g. for odd/even year calibration, or calibration
13
    on high or low flows only, or any custom setup).
14
    3-  Apply a transformation on the flows to modify the behaviour of the
15
    objective function calculation (e.g taking the log, inverse or square
16
    root transform of the flows before computing the objective function).
17

18
This function also contains some tools and inputs reserved for the calibration
19
toolbox, such as the ability to take the negative of the objective function to
20
maximize instead of minimize a metric according to the needs of the optimizing
21
algorithm.
22
"""
23
# Import packages
24
import numpy as np
4✔
25
import xarray as xr
4✔
26

27
__all__ = ["get_objective_function", "transform_flows"]
4✔
28

29

30
def get_objective_function(
4✔
31
    qobs: np.ndarray | xr.Dataset,
32
    qsim: np.ndarray | xr.Dataset,
33
    obj_func: str = "rmse",
34
    take_negative: bool = False,
35
    mask: np.ndarray | xr.Dataset | None = None,
36
    transform: str | None = None,
37
    epsilon: float | None = None,
38
):
39
    """Entrypoint function for the objective function calculation.
40

41
    More can be added by adding the function to this file and adding the option
42
    in this function.
43

44
    Notes
45
    -----
46
    All data corresponding to NaN values in the observation set are removed from the calculation.
47
    If a mask is passed, it must be the same size as the qsim and qobs vectors.
48
    If any NaNs are present in the qobs dataset, all corresponding data in the qobs, qsim and mask
49
    will be removed prior to passing to the processing function.
50

51
    Parameters
52
    ----------
53
    qobs : array_like
54
        Vector containing the Observed streamflow to be used in the objective
55
        function calculation. It is the target to attain.
56
    qsim : array_like
57
        Vector containing the Simulated streamflow as generated by the hydrological model.
58
        It is modified by changing parameters and resumulating the hydrological model.
59
    obj_func : str
60
        String representing the objective function to use in the calibration.
61
        Options must be one of the accepted objective functions:
62
        - "abs_bias" : Absolute value of the "bias" metric
63
        - "abs_pbias": Absolute value of the "pbias" metric
64
        - "abs_volume_error" : Absolute value of the volume_error metric
65
        - "agreement_index": Index of agreement
66
        - "bias" : Bias metric
67
        - "correlation_coeff": Correlation coefficient
68
        - "kge" : Kling Gupta Efficiency metric (2009 version)
69
        - "kge_mod" : Kling Gupta Efficiency metric (2012 version)
70
        - "mae": Mean Absolute Error metric
71
        - "mare": Mean Absolute Relative Error metric
72
        - "mse" : Mean Square Error metric
73
        - "nse": Nash-Sutcliffe Efficiency metric
74
        - "pbias" : Percent bias (relative bias)
75
        - "r2" : r-squared, i.e. square of correlation_coeff.
76
        - "rmse" : Root Mean Square Error
77
        - "rrmse" : Relative Root Mean Square Error (RMSE-to-mean ratio)
78
        - "rsr" : Ratio of RMSE to standard deviation.
79
        - "volume_error": Total volume error over the period.
80
        The default is 'rmse'.
81
    take_negative : bool
82
        Used to force the objective function to be multiplied by minus one (-1)
83
        such that it is possible to maximize it if the optimizer is a minimizer
84
        and vice versa. Should always be set to False unless required by an
85
        optimization setup, which is handled internally and transparently to
86
        the user. The default is False.
87
    mask : array_like
88
        Array of 0 or 1 on which the objective function should be applied.
89
        Values of 1 indicate that the value is included in the calculation, and
90
        values of 0 indicate that the value is excluded and will have no impact
91
        on the objective function calculation. This can be useful for specific
92
        optimization strategies such as odd/even year calibration, seasonal
93
        calibration or calibration based on high/low flows. The default is None
94
        and all data are preserved.
95
    transform : str
96
        Indicates the type of transformation required. Can be one of the
97
        following values:
98
        - "sqrt" : Square root transformation of the flows [sqrt(Q)]
99
        - "log" : Logarithmic transformation of the flows [log(Q)]
100
        - "inv" : Inverse transformation of the flows [1/Q]
101
        The default value is "None", by which no transformation is performed.
102
    epsilon : float
103
        Indicates the perturbation to add to the flow time series during a
104
        transformation to avoid division by zero and logarithmic transformation.
105
        The perturbation is equal to: `perturbation = epsilon * mean(qobs)`.
106
        The default value is 0.01.
107

108
    Returns
109
    -------
110
    float
111
        Value of the selected objective function (obj_fun).
112
    """
113
    # List of available objective functions
114
    obj_func_dict = {
4✔
115
        "abs_bias": _abs_bias,
116
        "abs_pbias": _abs_pbias,
117
        "abs_volume_error": _abs_volume_error,
118
        "agreement_index": _agreement_index,
119
        "bias": _bias,
120
        "correlation_coeff": _correlation_coeff,
121
        "kge": _kge,
122
        "kge_mod": _kge_mod,
123
        "mae": _mae,
124
        "mare": _mare,
125
        "mse": _mse,
126
        "nse": _nse,
127
        "pbias": _pbias,
128
        "r2": _r2,
129
        "rmse": _rmse,
130
        "rrmse": _rrmse,
131
        "rsr": _rsr,
132
        "volume_error": _volume_error,
133
    }
134

135
    # If we got a dataset, change to np.array
136
    # FIXME: Implement a more flexible method
137
    if isinstance(qsim, xr.Dataset):
4✔
138
        qsim = qsim["streamflow"]
4✔
139

140
    if isinstance(qobs, xr.Dataset):
4✔
NEW
141
        qobs = qobs["streamflow"]
×
142

143
    # Basic error checking
144
    if qobs.shape[0] != qsim.shape[0]:
4✔
145
        raise ValueError("Observed and Simulated flows are not of the same size.")
4✔
146

147
    # Check mask size and contents
148
    if mask is not None:
4✔
149
        # Size
150
        if qobs.shape[0] != mask.shape[0]:
4✔
151
            raise ValueError("Mask is not of the same size as the streamflow data.")
4✔
152

153
        # All zero or one?
154
        if not np.setdiff1d(np.unique(mask), np.array([0, 1])).size == 0:
4✔
155
            raise ValueError("Mask contains values other than 0 or 1. Please modify.")
4✔
156

157
    # Check that the objective function is in the list of available methods
158
    if obj_func not in obj_func_dict:
4✔
159
        raise ValueError(
4✔
160
            "Selected objective function is currently unavailable. "
161
            + "Consider contributing to our project at: "
162
            + "github.com/hydrologie/xhydro"
163
        )
164

165
    # Ensure there are no NaNs in the dataset
166
    if mask is not None:
4✔
167
        mask = mask[~np.isnan(qobs)]
4✔
168
    qsim = qsim[~np.isnan(qobs)]
4✔
169
    qobs = qobs[~np.isnan(qobs)]
4✔
170

171
    # Apply mask before transform
172
    if mask is not None:
4✔
173
        qsim = qsim[mask == 1]
4✔
174
        qobs = qobs[mask == 1]
4✔
175
        mask = mask[mask == 1]
4✔
176

177
    # Transform data if needed
178
    if transform is not None:
4✔
179
        qsim, qobs = transform_flows(qsim, qobs, transform, epsilon)
4✔
180

181
    # Compute objective function by switching to the correct algorithm. Ensure
182
    # that the function name is the same as the obj_func tag or this will fail.
183
    function_call = obj_func_dict[obj_func]
4✔
184
    obj_fun_val = function_call(qsim, qobs)
4✔
185

186
    # Take the negative value of the Objective Function to return to the
187
    # optimizer.
188
    if take_negative:
4✔
189
        obj_fun_val = obj_fun_val * -1
4✔
190

191
    return obj_fun_val
4✔
192

193

194
def _get_objfun_minimize_or_maximize(obj_func: str) -> bool:
4✔
195
    """
196
    Check whether the objective function needs to be maximized or minimized.
197

198
    Returns a boolean value, where True means it should be maximized, and False means that it should be minimized.
199
    Objective functions other than those programmed here will raise an error.
200

201
    Parameters
202
    ----------
203
    obj_func : str
204
        Label of the desired objective function.
205

206
    Returns
207
    -------
208
    bool
209
        Indicator of if the objective function needs to be maximized.
210
    """
211
    # Define metrics that need to be maximized:
212
    if obj_func in [
4✔
213
        "agreement_index",
214
        "correlation_coeff",
215
        "kge",
216
        "kge_mod",
217
        "nse",
218
        "r2",
219
    ]:
220
        maximize = True
4✔
221

222
    # Define the metrics that need to be minimized:
223
    elif obj_func in [
4✔
224
        "abs_bias",
225
        "abs_pbias",
226
        "abs_volume_error",
227
        "mae",
228
        "mare",
229
        "mse",
230
        "rmse",
231
        "rrmse",
232
        "rsr",
233
    ]:
234
        maximize = False
4✔
235

236
    # Check for the metrics that exist but cannot be used for optimization
237
    elif obj_func in ["bias", "pbias", "volume_error"]:
4✔
238
        raise ValueError(
4✔
239
            "The bias, pbias and volume_error metrics cannot be minimized or maximized. \
240
                 Please use the abs_bias, abs_pbias and abs_volume_error instead."
241
        )
242
    else:
243
        raise NotImplementedError("The objective function is unknown.")
4✔
244

245
    return maximize
4✔
246

247

248
def _get_optimizer_minimize_or_maximize(algorithm: str) -> bool:
4✔
249
    """Find the direction in which the optimizer searches.
250

251
    Some optimizers try to maximize the objective function value, and others
252
    try to minimize it. Since our objective functions include some that need to
253
    be maximized and others minimized, it is imperative to ensure that the
254
    optimizer/objective-function pair work in tandem.
255

256
    Parameters
257
    ----------
258
    algorithm : str
259
        Name of the optimizing algorithm.
260

261
    Returns
262
    -------
263
    bool
264
        Indicator of the direction of the optimizer search, True to maximize.
265
    """
266
    # Define metrics that need to be maximized:
267
    if algorithm in [
4✔
268
        "DDS",
269
    ]:
270
        maximize = True
4✔
271

272
    # Define the metrics that need to be minimized:
273
    elif algorithm in [
4✔
274
        "SCEUA",
275
    ]:
276
        maximize = False
4✔
277

278
    # Any other optimizer at this date
279
    else:
280
        raise NotImplementedError("The optimization algorithm is unknown.")
4✔
281

282
    return maximize
4✔
283

284

285
def transform_flows(
4✔
286
    qsim: np.ndarray,
287
    qobs: np.ndarray,
288
    transform: str | None = None,
289
    epsilon: float = 0.01,
290
) -> tuple[np.ndarray, np.ndarray]:
291
    """Transform flows before computing the objective function.
292

293
    It is used to transform flows such that the objective function is computed
294
    on a transformed flow metric rather than on the original units of flow
295
    (ex: inverse, log-transformed, square-root)
296

297
    Parameters
298
    ----------
299
    qsim : array_like
300
        Simulated streamflow vector.
301
    qobs : array_like
302
        Observed streamflow vector.
303
    transform : str, optional
304
        Indicates the type of transformation required. Can be one of the
305
        following values:
306
        - "sqrt" : Square root transformation of the flows [sqrt(Q)]
307
        - "log" : Logarithmic transformation of the flows [log(Q)]
308
        - "inv" : Inverse transformation of the flows [1/Q]
309
        The default value is "None", by which no transformation is performed.
310
    epsilon : float
311
        Indicates the perturbation to add to the flow time series during a
312
        transformation to avoid division by zero and logarithmic transformation.
313
        The perturbation is equal to: `perturbation = epsilon * mean(qobs)`.
314
        The default value is 0.01.
315

316
    Returns
317
    -------
318
    qsim : array_like
319
        Transformed simulated flow according to user request.
320
    qobs : array_like
321
        Transformed observed flow according to user request.
322
    """
323
    # Quick check
324
    if transform is not None:
4✔
325
        if transform not in ["log", "inv", "sqrt"]:
4✔
326
            raise NotImplementedError("Flow transformation method not recognized.")
4✔
327

328
    # Transform the flow series if required
329
    if transform == "log":  # log transformation
4✔
330
        epsilon = epsilon * np.nanmean(qobs)
4✔
331
        qobs, qsim = np.log(qobs + epsilon), np.log(qsim + epsilon)
4✔
332

333
    elif transform == "inv":  # inverse transformation
4✔
334
        epsilon = epsilon * np.nanmean(qobs)
4✔
335
        qobs, qsim = 1.0 / (qobs + epsilon), 1.0 / (qsim + epsilon)
4✔
336

337
    elif transform == "sqrt":  # square root transformation
4✔
338
        qobs, qsim = np.sqrt(qobs), np.sqrt(qsim)
4✔
339

340
    # Return the flows after transformation (or original if no transform)
341
    return qsim, qobs
4✔
342

343

344
"""
4✔
345
BEGIN OBJECTIVE FUNCTIONS DEFINITIONS
346
"""
347

348

349
def _abs_bias(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
350
    """Absolute bias metric.
351

352
    Parameters
353
    ----------
354
    qsim : array_like
355
        Simulated streamflow vector.
356
    qobs : array_like
357
        Observed streamflow vector.
358

359
    Returns
360
    -------
361
    float
362
        Absolute value of the "bias" metric. This metric is useful when calibrating
363
        on the bias, because bias should aim to be 0 but can take large positive
364
        or negative values. Taking the absolute value of the bias will let the
365
        optimizer minimize the value to zero.
366

367
    Notes
368
    -----
369
    The abs_bias should be MINIMIZED.
370
    """
371
    return np.abs(_bias(qsim, qobs))
4✔
372

373

374
def _abs_pbias(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
375
    """Absolute pbias metric.
376

377
    Parameters
378
    ----------
379
    qsim : array_like
380
        Simulated streamflow vector.
381
    qobs : array_like
382
        Observed streamflow vector.
383

384
    Returns
385
    -------
386
    float
387
        The absolute value of the "pbias" metric. This metric is useful when
388
        calibrating on the pbias, because pbias should aim to be 0 but can take
389
        large positive or negative values. Taking the absolute value of the
390
        pbias will let the optimizer minimize the value to zero.
391

392
    Notes
393
    -----
394
    The abs_pbias should be MINIMIZED.
395
    """
396
    return np.abs(_pbias(qsim, qobs))
4✔
397

398

399
def _abs_volume_error(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
400
    """Absolute value of the volume error metric.
401

402
    Parameters
403
    ----------
404
    qsim : array_like
405
        Simulated streamflow vector.
406
    qobs : array_like
407
        Observed streamflow vector.
408

409
    Returns
410
    -------
411
    float
412
        The absolute value of the "volume_error" metric. This metric is useful
413
        when calibrating on the volume_error, because volume_error should aim
414
        to be 0 but can take large positive or negative values. Taking the
415
        absolute value of the volume_error will let the optimizer minimize the
416
        value to zero.
417

418
    Notes
419
    -----
420
    The abs_volume_error should be MINIMIZED.
421
    """
422
    return np.abs(_volume_error(qsim, qobs))
4✔
423

424

425
def _agreement_index(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
426
    """Index of agreement metric.
427

428
    Parameters
429
    ----------
430
    qsim : array_like
431
        Simulated streamflow vector.
432
    qobs : array_like
433
        Observed streamflow vector.
434

435
    Returns
436
    -------
437
    float
438
        The agreement index of Willmott (1981). Varies between 0 and 1.
439

440
    Notes
441
    -----
442
    The Agreement index should be MAXIMIZED.
443
    """
444
    # Decompose into clearer chunks
445
    a = np.sum((qobs - qsim) ** 2)
4✔
446
    b = np.abs(qsim - np.mean(qobs)) + np.abs(qobs - np.mean(qobs))
4✔
447
    c = np.sum(b**2)
4✔
448

449
    return 1 - (a / c)
4✔
450

451

452
def _bias(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
453
    """The bias metric.
454

455
    Parameters
456
    ----------
457
    qsim : array_like
458
        Simulated streamflow vector.
459
    qobs : array_like
460
        Observed streamflow vector.
461

462
    Returns
463
    -------
464
    float
465
        The bias in the simulation. Can be negative or positive and gives the
466
        average error between the observed and simulated flows. This
467
        interpretation uses the definition that a positive bias value means
468
        that the simulation overestimates the true value (as opposed to many
469
        other sources on bias calculations that use the contrary interpretation).
470

471
    Notes
472
    -----
473
    BIAS SHOULD AIM TO BE ZERO AND SHOULD NOT BE USED FOR CALIBRATION. FOR
474
    CALIBRATION, USE "abs_bias" TO TAKE THE ABSOLUTE VALUE.
475
    """
476
    return np.mean(qsim - qobs)
4✔
477

478

479
def _correlation_coeff(qsim: np.ndarray, qobs: np.ndarray) -> np.ndarray:
4✔
480
    """Correlation coefficient metric.
481

482
    Parameters
483
    ----------
484
    qsim : array_like
485
        Simulated streamflow vector.
486
    qobs : array_like
487
        Observed streamflow vector.
488

489
    Returns
490
    -------
491
    array-like
492
        The correlation coefficient.
493

494
    Notes
495
    -----
496
    The correlation_coeff should be MAXIMIZED.
497
    """
498
    return np.corrcoef(qobs, qsim)[0, 1]
4✔
499

500

501
def _kge(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
502
    """Kling-Gupta efficiency metric (2009 version).
503

504
    Parameters
505
    ----------
506
    qsim : array_like
507
        Simulated streamflow vector.
508
    qobs : array_like
509
        Observed streamflow vector.
510

511
    Returns
512
    -------
513
    float
514
        The Kling-Gupta Efficiency (KGE) metric of 2009. It can take values
515
        from -inf to 1 (best case).
516

517
    Notes
518
    -----
519
    The KGE should be MAXIMIZED.
520
    """
521
    # This pops up a lot, precalculate.
522
    qsim_mean = np.mean(qsim)
4✔
523
    qobs_mean = np.mean(qobs)
4✔
524

525
    # Calculate the components of KGE
526
    r_num = np.sum((qsim - qsim_mean) * (qobs - qobs_mean))
4✔
527
    r_den = np.sqrt(np.sum((qsim - qsim_mean) ** 2) * np.sum((qobs - qobs_mean) ** 2))
4✔
528
    r = r_num / r_den
4✔
529
    a = np.std(qsim) / np.std(qobs)
4✔
530
    b = np.sum(qsim) / np.sum(qobs)
4✔
531

532
    # Calculate the KGE
533
    kge = 1 - np.sqrt((r - 1) ** 2 + (a - 1) ** 2 + (b - 1) ** 2)
4✔
534

535
    return kge
4✔
536

537

538
def _kge_mod(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
539
    """Kling-Gupta efficiency metric (2012 version).
540

541
    Parameters
542
    ----------
543
    qsim : array_like
544
        Simulated streamflow vector.
545
    qobs : array_like
546
        Observed streamflow vector.
547

548
    Returns
549
    -------
550
    float
551
        The modified Kling-Gupta Efficiency (KGE) metric of 2012. It can take
552
        values from -inf to 1 (best case).
553

554
    Notes
555
    -----
556
    The kge_mod should be MAXIMIZED.
557
    """
558
    # These pop up a lot, precalculate
559
    qsim_mean = np.mean(qsim)
4✔
560
    qobs_mean = np.mean(qobs)
4✔
561

562
    # Calc KGE components
563
    r_num = np.sum((qsim - qsim_mean) * (qobs - qobs_mean))
4✔
564
    r_den = np.sqrt(np.sum((qsim - qsim_mean) ** 2) * np.sum((qobs - qobs_mean) ** 2))
4✔
565
    r = r_num / r_den
4✔
566
    g = (np.std(qsim) / qsim_mean) / (np.std(qobs) / qobs_mean)
4✔
567
    b = np.mean(qsim) / np.mean(qobs)
4✔
568

569
    # Calc the modified KGE metric
570
    kge_mod = 1 - np.sqrt((r - 1) ** 2 + (g - 1) ** 2 + (b - 1) ** 2)
4✔
571

572
    return kge_mod
4✔
573

574

575
def _mae(qsim: np.ndarray, qobs: np.ndarray):
4✔
576
    """Mean absolute error metric.
577

578
    Parameters
579
    ----------
580
    qsim : array_like
581
        Simulated streamflow vector.
582
    qobs : array_like
583
        Observed streamflow vector.
584

585
    Returns
586
    -------
587
    float
588
        Mean Absolute Error. It can be interpreted as the average error
589
        (absolute) between observations and simulations for any time step.
590

591
    Notes
592
    -----
593
    The mae should be MINIMIZED.
594
    """
595
    return np.mean(np.abs(qsim - qobs))
4✔
596

597

598
def _mare(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
599
    """Mean absolute relative error metric.
600

601
    Parameters
602
    ----------
603
    qsim : array_like
604
        Simulated streamflow vector.
605
    qobs : array_like
606
        Observed streamflow vector.
607

608
    Returns
609
    -------
610
    float
611
        Mean Absolute Relative Error. For streamflow, where qobs is always zero
612
        or positive, the MARE is always positive.
613

614
    Notes
615
    -----
616
    The mare should be MINIMIZED.
617
    """
618
    return np.sum(np.abs(qobs - qsim)) / np.sum(qobs)
4✔
619

620

621
def _mse(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
622
    """Mean square error metric.
623

624
    Parameters
625
    ----------
626
    qsim : array_like
627
        Simulated streamflow vector.
628
    qobs : array_like
629
        Observed streamflow vector.
630

631
    Returns
632
    -------
633
    float
634
        Mean Square Error. It is the sum of squared errors for each day divided
635
        by the total number of days. Units are thus squared units, and the best
636
        possible value is 0.
637

638
    Notes
639
    -----
640
    The mse should be MINIMIZED.
641
    """
642
    return np.mean((qobs - qsim) ** 2)
4✔
643

644

645
def _nse(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
646
    """Nash-Sutcliffe efficiency metric.
647

648
    Parameters
649
    ----------
650
    qsim : array_like
651
        Simulated streamflow vector.
652
    qobs : array_like
653
        Observed streamflow vector.
654

655
    Returns
656
    -------
657
    float
658
        Nash-Sutcliffe Efficiency (NSE) metric. It can take values from -inf to
659
        1, with 0 being as good as using the mean observed flow as the estimator.
660

661
    Notes
662
    -----
663
    The nse should be MAXIMIZED.
664
    """
665
    num = np.sum((qobs - qsim) ** 2)
4✔
666
    den = np.sum((qobs - np.mean(qobs)) ** 2)
4✔
667

668
    return 1 - (num / den)
4✔
669

670

671
def _pbias(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
672
    """Percent bias metric.
673

674
    Parameters
675
    ----------
676
    qsim : array_like
677
        Simulated streamflow vector.
678
    qobs : array_like
679
        Observed streamflow vector.
680

681
    Returns
682
    -------
683
    float
684
        Percent bias. Can be negative or positive and gives the average
685
        relative error between the observed and simulated flows. This
686
        interpretation uses the definition that a positive bias value means
687
        that the simulation overestimates the true value (as opposed to many
688
        other sources on bias calculations that use the contrary interpretation).
689

690
    Notes
691
    -----
692
    PBIAS SHOULD AIM TO BE ZERO AND SHOULD NOT BE USED FOR CALIBRATION. FOR
693
    CALIBRATION, USE "abs_pbias" TO TAKE THE ABSOLUTE VALUE.
694
    """
695
    return (np.sum(qsim - qobs) / np.sum(qobs)) * 100
4✔
696

697

698
def _r2(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
699
    """The r-squred metric.
700

701
    Parameters
702
    ----------
703
    qsim : array_like
704
        Simulated streamflow vector.
705
    qobs : array_like
706
        Observed streamflow vector.
707

708
    Returns
709
    -------
710
    float
711
        The r-squared (R2) metric equal to the square of the correlation
712
        coefficient.
713

714
    Notes
715
    -----
716
    The r2 should be MAXIMIZED.
717
    """
718
    return _correlation_coeff(qsim, qobs) ** 2
4✔
719

720

721
def _rmse(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
722
    """Root-mean-square error metric.
723

724
    Parameters
725
    ----------
726
    qsim : array_like
727
        Simulated streamflow vector.
728
    qobs : array_like
729
        Observed streamflow vector.
730

731
    Returns
732
    -------
733
    float
734
        Root Mean Square Error. Units are the same as the timeseries data
735
        (ex. m3/s). It can take zero or positive values.
736

737
    Notes
738
    -----
739
    The rmse should be MINIMIZED.
740
    """
741
    return np.sqrt(np.mean((qobs - qsim) ** 2))
4✔
742

743

744
def _rrmse(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
745
    """Relative root-mean-square error (ratio of rmse to mean) metric.
746

747
    Parameters
748
    ----------
749
    qsim : array_like
750
        Simulated streamflow vector.
751
    qobs : array_like
752
        Observed streamflow vector.
753

754
    Returns
755
    -------
756
    float
757
        Ratio of the RMSE to the mean of the observations. It allows scaling
758
        RMSE values to compare results between time series of different
759
        magnitudes (ex. flows from small and large watersheds). Also known as
760
        the CVRMSE.
761

762
    Notes
763
    -----
764
    The rrmse should be MINIMIZED.
765
    """
766
    return _rmse(qsim, qobs) / np.mean(qobs)
4✔
767

768

769
def _rsr(qsim: np.ndarray, qobs: np.ndarray):
4✔
770
    """Ratio of root mean square error to standard deviation metric.
771

772
    Parameters
773
    ----------
774
    qsim : array_like
775
        Simulated streamflow vector.
776
    qobs : array_like
777
        Observed streamflow vector.
778

779
    Returns
780
    -------
781
    float
782
        Root Mean Square Error (RMSE) divided by the standard deviation of the
783
        observations. Also known as the "Ratio of the Root Mean Square Error to
784
        the Standard Deviation of Observations".
785

786
    Notes
787
    -----
788
    The rsr should be MINIMIZED.
789
    """
790
    return _rmse(qobs, qsim) / np.std(qobs)
4✔
791

792

793
def _volume_error(qsim: np.ndarray, qobs: np.ndarray) -> float:
4✔
794
    """Volume error metric.
795

796
    Parameters
797
    ----------
798
    qsim : array_like
799
        Simulated streamflow vector.
800
    qobs : array_like
801
        Observed streamflow vector.
802

803
    Returns
804
    -------
805
    float
806
        Total error in terms of volume over the entire period. Expressed in
807
        terms of the same units as input data, so for flow rates it is
808
        important to multiply by the duration of the time-step to obtain actual
809
        volumes.
810

811
    Notes
812
    -----
813
    The volume_error should be MINIMIZED.
814
    """
815
    return np.sum(qsim - qobs) / np.sum(qobs)
4✔
816

817

818
"""
4✔
819
ADD OBJECTIVE FUNCTIONS HERE
820
"""
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc