Skip to content

Calibration#

Calibration#

hapi.calibration.Calibration #

Bases: Catchment

Calibration class for distributed hydrological model parameter optimization.

The Calibration class connects the parameter spatial distribution function with both components of the spatial representation of the hydrological process (conceptual model and spatial routing) to calculate the performance of predicted runoff at known locations based on a given performance function.

The Calibration class is a subclass of the Catchment superclass, so you need to create the Catchment object first to be able to run the calibration.

Source code in src/hapi/calibration.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
class Calibration(Catchment):
    """Calibration class for distributed hydrological model parameter optimization.

    The Calibration class connects the parameter spatial distribution function
    with both components of the spatial representation of the hydrological
    process (conceptual model and spatial routing) to calculate the
    performance of predicted runoff at known locations based on a given
    performance function.

    The Calibration class is a subclass of the Catchment superclass, so you
    need to create the Catchment object first to be able to run the
    calibration.
    """

    def __init__(
        self,
        name: Any,
        start: str,
        end: str,
        fmt: str = "%Y-%m-%d",
        spatial_resolution: str | None = "Lumped",
        temporal_resolution: str | None = "Daily",
        routing_method: str | None = "Muskingum",
    ):
        """Initialize the Calibration object.

        Args:
            name (Any): Name of the Catchment.
            start (str): Starting date as a string.
            end (str): End date as a string.
            fmt (str, optional): Format of the given date.
                Default is "%Y-%m-%d".
            spatial_resolution (str, optional): Spatial resolution mode,
                either "Lumped" or "Distributed". Default is "Lumped".
            temporal_resolution (str, optional): Temporal resolution mode,
                either "Hourly" or "Daily". Default is "Daily".
            routing_method (str, optional): Routing method name.
                Default is "Muskingum".
        """
        super().__init__(
            name,
            start,
            end,
            fmt,
            spatial_resolution,
            temporal_resolution,
            routing_method,
        )
        self.objective_function: Callable[..., Any] | None = None
        self.OFArgs: list | None = None
        self.OFvalue: float | None = None

    def read_objective_function(
        self, objective_function: Callable[..., Any], args: list | None
    ):
        """Read and store the objective function and its arguments.

        Takes the objective function and any additional arguments that
        need to be passed to the objective function during calibration.

        Args:
            objective_function (callable): A callable function to calculate
                any kind of metric to be used in the calibration.
            args: Any positional or keyword arguments to pass to the
                objective function. If None, defaults to an empty list.

        Raises:
            AssertionError: If objective_function is not callable.
        """
        # check objective_function
        assert callable(objective_function), (
            "The Objective function should be a function"
        )
        self.objective_function = objective_function

        if args is None:
            args = []

        self.OFArgs = args

        print("Objective function is read successfully")

    def extract_discharge(
        self,
        calculate_metrics: bool = True,
        frame_work_1: bool = False,
        factor: list | None = None,
        only_outlet: bool = False,
    ):
        """Extract the simulated discharge hydrograph at gauge locations.

        Extracts discharge values from the total routed discharge array
        (``self.Qtot``) at each gauge location and stores them in
        ``self.Qsim``. Optionally applies a multiplication factor per
        gauge.

        Args:
            calculate_metrics (bool, optional): Whether to calculate
                performance metrics. Not used in this override but
                kept for signature compatibility. Default is True.
            frame_work_1 (bool, optional): True if the routing
                function is Maxbas. Not used in this override but
                kept for signature compatibility. Default is False.
            factor (list, optional): List of multiplication factors for
                the simulated discharge, one per gauge. If None, no
                scaling is applied. Default is None.
            only_outlet (bool, optional): True to extract discharge
                only at the outlet cell. Not used in this override but
                kept for signature compatibility. Default is False.
        """
        self.Qsim = np.zeros((self.TS - 1, len(self.GaugesTable)))
        # error = 0
        for i in range(len(self.GaugesTable)):
            Xind = int(self.GaugesTable.loc[self.GaugesTable.index[i], "cell_row"])
            Yind = int(self.GaugesTable.loc[self.GaugesTable.index[i], "cell_col"])
            # gaugeid = self.GaugesTable.loc[self.GaugesTable.index[i],"id"]

            # Quz = self.quz_routed[Xind,Yind,:-1]
            # Qlz = self.qlz_translated[Xind,Yind,:-1]
            # self.Qsim[:,i] = Quz + Qlz

            Qsim = np.reshape(self.Qtot[Xind, Yind, :-1], self.TS - 1)

            if factor is not None:
                self.Qsim[:, i] = Qsim * factor[i]
            else:
                self.Qsim[:, i] = Qsim

            # Qobs = Coello.QGauges.loc[:,gaugeid]
            # error = error + objective_function(Qobs, Qsim)

        # return error

    def run_calibration(
        self,
        spatial_var_fun: Callable[..., Any],
        optimization_args: list,
        print_error: int | None = None,
    ):
        """Run the calibration algorithm for the distributed hydrological model.

        Executes the Harmony Search optimization algorithm to calibrate
        parameters for the conceptual distributed hydrological model.
        The method distributes parameters spatially using ``spatial_var_fun``,
        runs the RRM model via ``Wrapper.RRMModel``, and evaluates
        performance using the stored objective function.

        The following attributes must be set on the instance before calling
        this method:

            - ``Prec``, ``ET``, ``Temp``: Meteorological input arrays.
            - ``flow_dir_arr``: Flow direction array.
            - ``rows``, ``cols``: Grid dimensions.
            - ``LB``, ``UB``: Lower and upper parameter bounds.
            - ``objective_function``: Objective function for evaluation.
            - ``QGauges``, ``GaugesTable``: Observed discharge data and
              gauge metadata.

        Args:
            spatial_var_fun: Spatial variable function object with a
                ``Function`` method that distributes parameters and a
                ``Par3d`` attribute holding the 3D parameter array, plus
                ``no_parameters`` and ``no_elem`` attributes.
            optimization_args: A list of three elements:
                - ``optimization_args[0]`` (dict): Harmony Search API
                  objective arguments (e.g., HMS, HMCR, PAR).
                - ``optimization_args[1]``: Parallel type for the
                  optimizer.
                - ``optimization_args[2]`` (dict): Solver arguments with
                  keys ``"store_sol"``, ``"display_opts"``,
                  ``"store_hst"``, and ``"hot_start"``.
            print_error: If not 0, prints the error value and parameters
                at each iteration. Default is None.

        Returns:
            tuple: Optimization result tuple containing:
                - res[0]: The optimal objective function value.
                - res[1]: The optimal parameter set.

        Raises:
            AssertionError: If input dimensions are inconsistent or if
                optimization arguments are not dictionaries.
        """
        # input dimensions
        # [rows,cols] = self.FlowAcc.ReadAsArray().shape
        [fd_rows, fd_cols] = self.flow_dir_arr.shape
        assert fd_rows == self.rows and fd_cols == self.cols, ROWS_MISMATCH_ERROR

        # input dimensions
        assert (
            np.shape(self.Prec)[0] == self.rows
            and np.shape(self.ET)[0] == self.rows
            and np.shape(self.Temp)[0] == self.rows
        ), ROWS_MISMATCH_ERROR
        assert (
            np.shape(self.Prec)[1] == self.cols
            and np.shape(self.ET)[1] == self.cols
            and np.shape(self.Temp)[1] == self.cols
        ), COLUMNS_MISMATCH_ERROR
        assert (
            np.shape(self.Prec)[2] == np.shape(self.ET)[2] and np.shape(self.Temp)[2]
        ), "all meteorological input data should have the same length"

        # basic inputs
        # check if all inputs are included
        # assert all(["p2","init_st","UB","LB","snow "][i] in basic_inputs.keys()
        #     for i in range(4)), "basic_inputs should contain ['p2','init_st','UB','LB']"

        ### optimization

        # get arguments
        api_obj_args = optimization_args[0]
        pll_type = optimization_args[1]
        api_solve_args = optimization_args[2]
        # check optimization arguement
        assert type(api_obj_args) is dict, "store_history should be 0 or 1"
        assert type(api_solve_args) is dict, "history_fname should be of type string "

        print("Calibration starts")

        ### calculate the objective function
        def opt_fun(par):
            try:
                # distribute the parameters
                spatial_var_fun.Function(
                    par
                )  # , kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb
                self.Parameters = spatial_var_fun.Par3d
                # run the model
                Wrapper.RRMModel(self)
                # calculate performance of the model
                try:
                    error = self.objective_function(
                        self.QGauges, *[self.GaugesTable]
                    )  # self.qout, self.quz_routed, self.qlz_translated,
                    f = list(range(9, len(par), spatial_var_fun.no_parameters))
                    g = list()
                    for i in range(len(f)):
                        k = par[f[i]]
                        x = par[f[i] + 1]
                        g.append(2 * k * x / self.dt)
                        g.append((2 * k * (1 - x)) / self.dt)

                except TypeError as e:
                    # the objective function received fewer inputs than it needs
                    raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e

                # print error
                if print_error != 0:
                    print(round(error, 3))
                    print(par)

                fail = 0
            except:
                error = np.nan
                g = []
                fail = 1

            return error, g, fail

        ### define the optimization components
        opt_prob = Optimization("HBV Calibration", opt_fun)
        for i in range(len(self.LB)):
            opt_prob.addVar(f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i])

        opt_prob.addObj("f")

        for i in range(spatial_var_fun.no_elem):
            opt_prob.addCon("g" + str(i) + "-1", "i")
            opt_prob.addCon("g" + str(i) + "-2", "i")

        print(opt_prob)

        opt_engine = HSapi(pll_type=pll_type, options=api_obj_args)

        store_sol = api_solve_args["store_sol"]
        display_opts = api_solve_args["display_opts"]
        store_hst = api_solve_args["store_hst"]
        hot_start = api_solve_args["hot_start"]

        res = opt_engine(
            opt_prob,
            store_sol=store_sol,
            display_opts=display_opts,
            store_hst=store_hst,
            hot_start=hot_start,
        )

        self.Parameters = res[1]
        self.OFvalue = res[0]

        return res

    def FW1Calibration(
        self,
        spatial_var_fun: Callable[..., Any],
        optimization_args: list,
        print_error: int | None = None,
    ):
        """Run calibration using the FW1 (Focussed Width-1) routing scheme.

        Executes the Harmony Search optimization algorithm to calibrate
        parameters for the conceptual distributed hydrological model using
        the FW1 routing approach via ``Wrapper.FW1``.

        The following attributes must be set on the instance before calling
        this method:

            - ``Prec``, ``ET``, ``Temp``: Meteorological input arrays.
            - ``rows``, ``cols``: Grid dimensions.
            - ``LB``, ``UB``: Lower and upper parameter bounds.
            - ``objective_function``: Objective function for evaluation.
            - ``QGauges``, ``GaugesTable``: Observed discharge data and
              gauge metadata.

        Args:
            spatial_var_fun: Spatial variable function object with a
                ``Function`` method that distributes parameters and a
                ``Par3d`` attribute holding the 3D parameter array.
            optimization_args: A list of three elements:
                - ``optimization_args[0]`` (dict): Harmony Search API
                  objective arguments (e.g., HMS, HMCR, PAR).
                - ``optimization_args[1]``: Parallel type for the
                  optimizer.
                - ``optimization_args[2]`` (dict): Solver arguments with
                  keys ``"store_sol"``, ``"display_opts"``,
                  ``"store_hst"``, and ``"hot_start"``.
            print_error: If not 0, prints the error value and parameters
                at each iteration. Default is None.

        Returns:
            tuple: Optimization result tuple containing:
                - res[0]: The optimal objective function value.
                - res[1]: The optimal parameter set.

        Raises:
            AssertionError: If input dimensions are inconsistent or if
                optimization arguments are not dictionaries.
        """
        # input dimensions
        # [rows,cols] = self.FlowAcc.ReadAsArray().shape
        # [fd_rows,fd_cols] = self.flow_dir_arr.shape
        # assert fd_rows == self.rows and fd_cols == self.cols, ROWS_MISMATCH_ERROR

        # input dimensions
        assert (
            np.shape(self.Prec)[0] == self.rows
            and np.shape(self.ET)[0] == self.rows
            and np.shape(self.Temp)[0] == self.rows
        ), ROWS_MISMATCH_ERROR
        assert (
            np.shape(self.Prec)[1] == self.cols
            and np.shape(self.ET)[1] == self.cols
            and np.shape(self.Temp)[1] == self.cols
        ), COLUMNS_MISMATCH_ERROR
        assert (
            np.shape(self.Prec)[2] == np.shape(self.ET)[2] and np.shape(self.Temp)[2]
        ), "all meteorological input data should have the same length"

        # basic inputs
        # check if all inputs are included
        # assert all(["p2","init_st","UB","LB","snow "][i] in basic_inputs.keys()
        #     for i in range(4)), "basic_inputs should contain ['p2','init_st','UB','LB']"

        ### optimization

        # get arguments
        api_obj_args = optimization_args[0]
        pll_type = optimization_args[1]
        api_solve_args = optimization_args[2]
        # check optimization arguement
        assert type(api_obj_args) is dict, "store_history should be 0 or 1"
        assert type(api_solve_args) is dict, "history_fname should be of type string "

        print("Calibration starts")

        # calculate the objective function
        def opt_fun(par):
            try:
                # distribute the parameters
                spatial_var_fun.Function(
                    par
                )  # , kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb, Maskingum=spatial_var_fun.Maskingum
                self.Parameters = spatial_var_fun.Par3d
                # run the model
                Wrapper.FW1(self)
                # calculate performance of the model
                try:
                    error = self.objective_function(
                        self.QGauges, self.qout, *[self.GaugesTable]
                    )
                except TypeError as e:
                    # the objective function received fewer inputs than it needs
                    raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e

                # print error
                if print_error != 0:
                    print(round(error, 3))
                    print(par)

                fail = 0
            except:
                error = np.nan
                fail = 1

            return error, [], fail

        # define the optimization components
        opt_prob = Optimization("HBV Calibration", opt_fun)
        for i in range(len(self.LB)):
            opt_prob.addVar(f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i])

        print(opt_prob)

        opt_engine = HSapi(pll_type=pll_type, options=api_obj_args)

        store_sol = api_solve_args["store_sol"]
        display_opts = api_solve_args["display_opts"]
        store_hst = api_solve_args["store_hst"]
        hot_start = api_solve_args["hot_start"]

        res = opt_engine(
            opt_prob,
            store_sol=store_sol,
            display_opts=display_opts,
            store_hst=store_hst,
            hot_start=hot_start,
        )

        self.Parameters = res[1]
        self.OFvalue = res[0]

        return res

    def lumpedCalibration(
        self,
        basic_inputs: dict,
        optimization_args: list,
        print_error: int | None = None,
    ):
        """Run the calibration algorithm for the lumped hydrological model.

        Executes the Harmony Search optimization algorithm to calibrate
        parameters for the lumped conceptual hydrological model. The
        method runs the model via ``Wrapper.Lumped`` and evaluates
        performance using the stored objective function. Muskingum
        routing constraints are enforced as inequality constraints.

        The following attributes must be set on the instance before calling
        this method:

            - ``LB``, ``UB``: Lower and upper parameter bounds.
            - ``objective_function``: Objective function for evaluation.
            - ``OFArgs``: Arguments for the objective function.
            - ``QGauges``: Observed discharge DataFrame.
            - ``dt``: Time step duration.

        Args:
            basic_inputs (dict): Dictionary containing:
                - ``"Route"`` (int): Routing flag (1 to enable routing).
                - ``"RoutingFn"`` (callable): Routing function to use.
                - ``"InitialValues"`` (list, optional): Initial parameter
                  values for the optimizer. Defaults to an empty list if
                  not provided.
            optimization_args: A list of three elements:
                - ``optimization_args[0]`` (dict): Harmony Search API
                  objective arguments (e.g., HMS, HMCR, PAR).
                - ``optimization_args[1]``: Parallel type for the
                  optimizer.
                - ``optimization_args[2]`` (dict): Solver arguments with
                  keys ``"store_sol"``, ``"display_opts"``,
                  ``"store_hst"``, and ``"hot_start"``.
            print_error: If not 0, prints the error value and constraint
                values at each iteration. Default is None.

        Returns:
            tuple: Optimization result tuple containing:
                - res[0]: The optimal objective function value.
                - res[1]: The optimal parameter set.

        Raises:
            AssertionError: If ``basic_inputs`` is missing required keys
                ``"Route"`` or ``"RoutingFn"``, or if optimization
                arguments are not dictionaries.
        """
        # basic inputs
        # check if all inputs are included
        assert all(["Route", "RoutingFn"][i] in basic_inputs for i in range(2)), (
            "basic_inputs should contain ['p2','init_st','UB','LB'] "
        )

        route = basic_inputs["Route"]
        routing_fn = basic_inputs["RoutingFn"]
        if "InitialValues" in basic_inputs:
            initial_values = basic_inputs["InitialValues"]
        else:
            initial_values = []

        ### optimization

        # get arguments
        api_obj_args = optimization_args[0]
        pll_type = optimization_args[1]
        api_solve_args = optimization_args[2]
        # check optimization arguement
        assert isinstance(api_obj_args, dict), "store_history should be 0 or 1"
        assert isinstance(api_solve_args, dict), (
            "history_fname should be of type string "
        )

        print("Calibration starts")

        ### calculate the objective function
        def opt_fun(par):
            try:
                # parameters
                self.Parameters = par
                # run the model
                Wrapper.Lumped(self, route, routing_fn)
                # calculate performance of the model
                try:
                    error = self.objective_function(
                        self.QGauges[self.QGauges.columns[-1]], self.Qsim, *self.OFArgs
                    )
                    g = [
                        2 * par[-2] * par[-1] / self.dt,
                        (2 * par[-2] * (1 - par[-1])) / self.dt,
                    ]
                except TypeError as e:
                    # the objective function received fewer inputs than it needs
                    raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e

                if print_error != 0:
                    print(
                        f"Error = {round(error, 3)} Inequality Const = {np.round(g, 2)}"
                    )
                    # print(par)
                fail = 0
            except:
                error = np.nan
                g = []
                fail = 1
            return error, g, fail

        ### define the optimization components
        opt_prob = Optimization("HBV Calibration", opt_fun)

        if initial_values != []:
            for i in range(len(self.LB)):
                opt_prob.addVar(
                    f"x{i}",
                    type="c",
                    lower=self.LB[i],
                    upper=self.UB[i],
                    value=initial_values[i],
                )
        else:
            for i in range(len(self.LB)):
                opt_prob.addVar(f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i])

        opt_prob.addObj("f")

        opt_prob.addCon("g1", "i")
        opt_prob.addCon("g2", "i")
        # print(opt_prob)
        opt_engine = HSapi(pll_type=pll_type, options=api_obj_args)

        # parse the api_solve_args inputs
        # availablekeys = ['store_sol',"display_opts","store_hst","hot_start"]
        store_sol = api_solve_args["store_sol"]
        display_opts = api_solve_args["display_opts"]
        store_hst = api_solve_args["store_hst"]
        hot_start = api_solve_args["hot_start"]

        # for i in range(len(availablekeys)):
        # if availablekeys[i] in api_solve_args.keys():
        # exec(availablekeys[i] + "=" + str(api_solve_args[availablekeys[i]]))
        # print(availablekeys[i] + " = " + str(api_solve_args[availablekeys[i]]))

        res = opt_engine(
            opt_prob,
            store_sol=store_sol,
            display_opts=display_opts,
            store_hst=store_hst,
            hot_start=hot_start,
        )

        self.OFvalue = res[0]
        self.Parameters = res[1]

        return res

FW1Calibration(spatial_var_fun: Callable[..., Any], optimization_args: list, print_error: int | None = None) #

Run calibration using the FW1 (Focussed Width-1) routing scheme.

Executes the Harmony Search optimization algorithm to calibrate parameters for the conceptual distributed hydrological model using the FW1 routing approach via Wrapper.FW1.

The following attributes must be set on the instance before calling this method:

- ``Prec``, ``ET``, ``Temp``: Meteorological input arrays.
- ``rows``, ``cols``: Grid dimensions.
- ``LB``, ``UB``: Lower and upper parameter bounds.
- ``objective_function``: Objective function for evaluation.
- ``QGauges``, ``GaugesTable``: Observed discharge data and
  gauge metadata.

Parameters:

Name Type Description Default
spatial_var_fun Callable[..., Any]

Spatial variable function object with a Function method that distributes parameters and a Par3d attribute holding the 3D parameter array.

required
optimization_args list

A list of three elements: - optimization_args[0] (dict): Harmony Search API objective arguments (e.g., HMS, HMCR, PAR). - optimization_args[1]: Parallel type for the optimizer. - optimization_args[2] (dict): Solver arguments with keys "store_sol", "display_opts", "store_hst", and "hot_start".

required
print_error int | None

If not 0, prints the error value and parameters at each iteration. Default is None.

None

Returns:

Type Description
tuple

Optimization result tuple containing: - res[0]: The optimal objective function value. - res[1]: The optimal parameter set.

Raises:

Type Description
AssertionError

If input dimensions are inconsistent or if optimization arguments are not dictionaries.

Source code in src/hapi/calibration.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def FW1Calibration(
    self,
    spatial_var_fun: Callable[..., Any],
    optimization_args: list,
    print_error: int | None = None,
):
    """Run calibration using the FW1 (Focussed Width-1) routing scheme.

    Executes the Harmony Search optimization algorithm to calibrate
    parameters for the conceptual distributed hydrological model using
    the FW1 routing approach via ``Wrapper.FW1``.

    The following attributes must be set on the instance before calling
    this method:

        - ``Prec``, ``ET``, ``Temp``: Meteorological input arrays.
        - ``rows``, ``cols``: Grid dimensions.
        - ``LB``, ``UB``: Lower and upper parameter bounds.
        - ``objective_function``: Objective function for evaluation.
        - ``QGauges``, ``GaugesTable``: Observed discharge data and
          gauge metadata.

    Args:
        spatial_var_fun: Spatial variable function object with a
            ``Function`` method that distributes parameters and a
            ``Par3d`` attribute holding the 3D parameter array.
        optimization_args: A list of three elements:
            - ``optimization_args[0]`` (dict): Harmony Search API
              objective arguments (e.g., HMS, HMCR, PAR).
            - ``optimization_args[1]``: Parallel type for the
              optimizer.
            - ``optimization_args[2]`` (dict): Solver arguments with
              keys ``"store_sol"``, ``"display_opts"``,
              ``"store_hst"``, and ``"hot_start"``.
        print_error: If not 0, prints the error value and parameters
            at each iteration. Default is None.

    Returns:
        tuple: Optimization result tuple containing:
            - res[0]: The optimal objective function value.
            - res[1]: The optimal parameter set.

    Raises:
        AssertionError: If input dimensions are inconsistent or if
            optimization arguments are not dictionaries.
    """
    # input dimensions
    # [rows,cols] = self.FlowAcc.ReadAsArray().shape
    # [fd_rows,fd_cols] = self.flow_dir_arr.shape
    # assert fd_rows == self.rows and fd_cols == self.cols, ROWS_MISMATCH_ERROR

    # input dimensions
    assert (
        np.shape(self.Prec)[0] == self.rows
        and np.shape(self.ET)[0] == self.rows
        and np.shape(self.Temp)[0] == self.rows
    ), ROWS_MISMATCH_ERROR
    assert (
        np.shape(self.Prec)[1] == self.cols
        and np.shape(self.ET)[1] == self.cols
        and np.shape(self.Temp)[1] == self.cols
    ), COLUMNS_MISMATCH_ERROR
    assert (
        np.shape(self.Prec)[2] == np.shape(self.ET)[2] and np.shape(self.Temp)[2]
    ), "all meteorological input data should have the same length"

    # basic inputs
    # check if all inputs are included
    # assert all(["p2","init_st","UB","LB","snow "][i] in basic_inputs.keys()
    #     for i in range(4)), "basic_inputs should contain ['p2','init_st','UB','LB']"

    ### optimization

    # get arguments
    api_obj_args = optimization_args[0]
    pll_type = optimization_args[1]
    api_solve_args = optimization_args[2]
    # check optimization arguement
    assert type(api_obj_args) is dict, "store_history should be 0 or 1"
    assert type(api_solve_args) is dict, "history_fname should be of type string "

    print("Calibration starts")

    # calculate the objective function
    def opt_fun(par):
        try:
            # distribute the parameters
            spatial_var_fun.Function(
                par
            )  # , kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb, Maskingum=spatial_var_fun.Maskingum
            self.Parameters = spatial_var_fun.Par3d
            # run the model
            Wrapper.FW1(self)
            # calculate performance of the model
            try:
                error = self.objective_function(
                    self.QGauges, self.qout, *[self.GaugesTable]
                )
            except TypeError as e:
                # the objective function received fewer inputs than it needs
                raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e

            # print error
            if print_error != 0:
                print(round(error, 3))
                print(par)

            fail = 0
        except:
            error = np.nan
            fail = 1

        return error, [], fail

    # define the optimization components
    opt_prob = Optimization("HBV Calibration", opt_fun)
    for i in range(len(self.LB)):
        opt_prob.addVar(f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i])

    print(opt_prob)

    opt_engine = HSapi(pll_type=pll_type, options=api_obj_args)

    store_sol = api_solve_args["store_sol"]
    display_opts = api_solve_args["display_opts"]
    store_hst = api_solve_args["store_hst"]
    hot_start = api_solve_args["hot_start"]

    res = opt_engine(
        opt_prob,
        store_sol=store_sol,
        display_opts=display_opts,
        store_hst=store_hst,
        hot_start=hot_start,
    )

    self.Parameters = res[1]
    self.OFvalue = res[0]

    return res

__init__(name: Any, start: str, end: str, fmt: str = '%Y-%m-%d', spatial_resolution: str | None = 'Lumped', temporal_resolution: str | None = 'Daily', routing_method: str | None = 'Muskingum') #

Initialize the Calibration object.

Parameters:

Name Type Description Default
name Any

Name of the Catchment.

required
start str

Starting date as a string.

required
end str

End date as a string.

required
fmt str

Format of the given date. Default is "%Y-%m-%d".

'%Y-%m-%d'
spatial_resolution str

Spatial resolution mode, either "Lumped" or "Distributed". Default is "Lumped".

'Lumped'
temporal_resolution str

Temporal resolution mode, either "Hourly" or "Daily". Default is "Daily".

'Daily'
routing_method str

Routing method name. Default is "Muskingum".

'Muskingum'
Source code in src/hapi/calibration.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def __init__(
    self,
    name: Any,
    start: str,
    end: str,
    fmt: str = "%Y-%m-%d",
    spatial_resolution: str | None = "Lumped",
    temporal_resolution: str | None = "Daily",
    routing_method: str | None = "Muskingum",
):
    """Initialize the Calibration object.

    Args:
        name (Any): Name of the Catchment.
        start (str): Starting date as a string.
        end (str): End date as a string.
        fmt (str, optional): Format of the given date.
            Default is "%Y-%m-%d".
        spatial_resolution (str, optional): Spatial resolution mode,
            either "Lumped" or "Distributed". Default is "Lumped".
        temporal_resolution (str, optional): Temporal resolution mode,
            either "Hourly" or "Daily". Default is "Daily".
        routing_method (str, optional): Routing method name.
            Default is "Muskingum".
    """
    super().__init__(
        name,
        start,
        end,
        fmt,
        spatial_resolution,
        temporal_resolution,
        routing_method,
    )
    self.objective_function: Callable[..., Any] | None = None
    self.OFArgs: list | None = None
    self.OFvalue: float | None = None

extract_discharge(calculate_metrics: bool = True, frame_work_1: bool = False, factor: list | None = None, only_outlet: bool = False) #

Extract the simulated discharge hydrograph at gauge locations.

Extracts discharge values from the total routed discharge array (self.Qtot) at each gauge location and stores them in self.Qsim. Optionally applies a multiplication factor per gauge.

Parameters:

Name Type Description Default
calculate_metrics bool

Whether to calculate performance metrics. Not used in this override but kept for signature compatibility. Default is True.

True
frame_work_1 bool

True if the routing function is Maxbas. Not used in this override but kept for signature compatibility. Default is False.

False
factor list

List of multiplication factors for the simulated discharge, one per gauge. If None, no scaling is applied. Default is None.

None
only_outlet bool

True to extract discharge only at the outlet cell. Not used in this override but kept for signature compatibility. Default is False.

False
Source code in src/hapi/calibration.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def extract_discharge(
    self,
    calculate_metrics: bool = True,
    frame_work_1: bool = False,
    factor: list | None = None,
    only_outlet: bool = False,
):
    """Extract the simulated discharge hydrograph at gauge locations.

    Extracts discharge values from the total routed discharge array
    (``self.Qtot``) at each gauge location and stores them in
    ``self.Qsim``. Optionally applies a multiplication factor per
    gauge.

    Args:
        calculate_metrics (bool, optional): Whether to calculate
            performance metrics. Not used in this override but
            kept for signature compatibility. Default is True.
        frame_work_1 (bool, optional): True if the routing
            function is Maxbas. Not used in this override but
            kept for signature compatibility. Default is False.
        factor (list, optional): List of multiplication factors for
            the simulated discharge, one per gauge. If None, no
            scaling is applied. Default is None.
        only_outlet (bool, optional): True to extract discharge
            only at the outlet cell. Not used in this override but
            kept for signature compatibility. Default is False.
    """
    self.Qsim = np.zeros((self.TS - 1, len(self.GaugesTable)))
    # error = 0
    for i in range(len(self.GaugesTable)):
        Xind = int(self.GaugesTable.loc[self.GaugesTable.index[i], "cell_row"])
        Yind = int(self.GaugesTable.loc[self.GaugesTable.index[i], "cell_col"])
        # gaugeid = self.GaugesTable.loc[self.GaugesTable.index[i],"id"]

        # Quz = self.quz_routed[Xind,Yind,:-1]
        # Qlz = self.qlz_translated[Xind,Yind,:-1]
        # self.Qsim[:,i] = Quz + Qlz

        Qsim = np.reshape(self.Qtot[Xind, Yind, :-1], self.TS - 1)

        if factor is not None:
            self.Qsim[:, i] = Qsim * factor[i]
        else:
            self.Qsim[:, i] = Qsim

lumpedCalibration(basic_inputs: dict, optimization_args: list, print_error: int | None = None) #

Run the calibration algorithm for the lumped hydrological model.

Executes the Harmony Search optimization algorithm to calibrate parameters for the lumped conceptual hydrological model. The method runs the model via Wrapper.Lumped and evaluates performance using the stored objective function. Muskingum routing constraints are enforced as inequality constraints.

The following attributes must be set on the instance before calling this method:

- ``LB``, ``UB``: Lower and upper parameter bounds.
- ``objective_function``: Objective function for evaluation.
- ``OFArgs``: Arguments for the objective function.
- ``QGauges``: Observed discharge DataFrame.
- ``dt``: Time step duration.

Parameters:

Name Type Description Default
basic_inputs dict

Dictionary containing: - "Route" (int): Routing flag (1 to enable routing). - "RoutingFn" (callable): Routing function to use. - "InitialValues" (list, optional): Initial parameter values for the optimizer. Defaults to an empty list if not provided.

required
optimization_args list

A list of three elements: - optimization_args[0] (dict): Harmony Search API objective arguments (e.g., HMS, HMCR, PAR). - optimization_args[1]: Parallel type for the optimizer. - optimization_args[2] (dict): Solver arguments with keys "store_sol", "display_opts", "store_hst", and "hot_start".

required
print_error int | None

If not 0, prints the error value and constraint values at each iteration. Default is None.

None

Returns:

Type Description
tuple

Optimization result tuple containing: - res[0]: The optimal objective function value. - res[1]: The optimal parameter set.

Raises:

Type Description
AssertionError

If basic_inputs is missing required keys "Route" or "RoutingFn", or if optimization arguments are not dictionaries.

Source code in src/hapi/calibration.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def lumpedCalibration(
    self,
    basic_inputs: dict,
    optimization_args: list,
    print_error: int | None = None,
):
    """Run the calibration algorithm for the lumped hydrological model.

    Executes the Harmony Search optimization algorithm to calibrate
    parameters for the lumped conceptual hydrological model. The
    method runs the model via ``Wrapper.Lumped`` and evaluates
    performance using the stored objective function. Muskingum
    routing constraints are enforced as inequality constraints.

    The following attributes must be set on the instance before calling
    this method:

        - ``LB``, ``UB``: Lower and upper parameter bounds.
        - ``objective_function``: Objective function for evaluation.
        - ``OFArgs``: Arguments for the objective function.
        - ``QGauges``: Observed discharge DataFrame.
        - ``dt``: Time step duration.

    Args:
        basic_inputs (dict): Dictionary containing:
            - ``"Route"`` (int): Routing flag (1 to enable routing).
            - ``"RoutingFn"`` (callable): Routing function to use.
            - ``"InitialValues"`` (list, optional): Initial parameter
              values for the optimizer. Defaults to an empty list if
              not provided.
        optimization_args: A list of three elements:
            - ``optimization_args[0]`` (dict): Harmony Search API
              objective arguments (e.g., HMS, HMCR, PAR).
            - ``optimization_args[1]``: Parallel type for the
              optimizer.
            - ``optimization_args[2]`` (dict): Solver arguments with
              keys ``"store_sol"``, ``"display_opts"``,
              ``"store_hst"``, and ``"hot_start"``.
        print_error: If not 0, prints the error value and constraint
            values at each iteration. Default is None.

    Returns:
        tuple: Optimization result tuple containing:
            - res[0]: The optimal objective function value.
            - res[1]: The optimal parameter set.

    Raises:
        AssertionError: If ``basic_inputs`` is missing required keys
            ``"Route"`` or ``"RoutingFn"``, or if optimization
            arguments are not dictionaries.
    """
    # basic inputs
    # check if all inputs are included
    assert all(["Route", "RoutingFn"][i] in basic_inputs for i in range(2)), (
        "basic_inputs should contain ['p2','init_st','UB','LB'] "
    )

    route = basic_inputs["Route"]
    routing_fn = basic_inputs["RoutingFn"]
    if "InitialValues" in basic_inputs:
        initial_values = basic_inputs["InitialValues"]
    else:
        initial_values = []

    ### optimization

    # get arguments
    api_obj_args = optimization_args[0]
    pll_type = optimization_args[1]
    api_solve_args = optimization_args[2]
    # check optimization arguement
    assert isinstance(api_obj_args, dict), "store_history should be 0 or 1"
    assert isinstance(api_solve_args, dict), (
        "history_fname should be of type string "
    )

    print("Calibration starts")

    ### calculate the objective function
    def opt_fun(par):
        try:
            # parameters
            self.Parameters = par
            # run the model
            Wrapper.Lumped(self, route, routing_fn)
            # calculate performance of the model
            try:
                error = self.objective_function(
                    self.QGauges[self.QGauges.columns[-1]], self.Qsim, *self.OFArgs
                )
                g = [
                    2 * par[-2] * par[-1] / self.dt,
                    (2 * par[-2] * (1 - par[-1])) / self.dt,
                ]
            except TypeError as e:
                # the objective function received fewer inputs than it needs
                raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e

            if print_error != 0:
                print(
                    f"Error = {round(error, 3)} Inequality Const = {np.round(g, 2)}"
                )
                # print(par)
            fail = 0
        except:
            error = np.nan
            g = []
            fail = 1
        return error, g, fail

    ### define the optimization components
    opt_prob = Optimization("HBV Calibration", opt_fun)

    if initial_values != []:
        for i in range(len(self.LB)):
            opt_prob.addVar(
                f"x{i}",
                type="c",
                lower=self.LB[i],
                upper=self.UB[i],
                value=initial_values[i],
            )
    else:
        for i in range(len(self.LB)):
            opt_prob.addVar(f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i])

    opt_prob.addObj("f")

    opt_prob.addCon("g1", "i")
    opt_prob.addCon("g2", "i")
    # print(opt_prob)
    opt_engine = HSapi(pll_type=pll_type, options=api_obj_args)

    # parse the api_solve_args inputs
    # availablekeys = ['store_sol',"display_opts","store_hst","hot_start"]
    store_sol = api_solve_args["store_sol"]
    display_opts = api_solve_args["display_opts"]
    store_hst = api_solve_args["store_hst"]
    hot_start = api_solve_args["hot_start"]

    # for i in range(len(availablekeys)):
    # if availablekeys[i] in api_solve_args.keys():
    # exec(availablekeys[i] + "=" + str(api_solve_args[availablekeys[i]]))
    # print(availablekeys[i] + " = " + str(api_solve_args[availablekeys[i]]))

    res = opt_engine(
        opt_prob,
        store_sol=store_sol,
        display_opts=display_opts,
        store_hst=store_hst,
        hot_start=hot_start,
    )

    self.OFvalue = res[0]
    self.Parameters = res[1]

    return res

read_objective_function(objective_function: Callable[..., Any], args: list | None) #

Read and store the objective function and its arguments.

Takes the objective function and any additional arguments that need to be passed to the objective function during calibration.

Parameters:

Name Type Description Default
objective_function callable

A callable function to calculate any kind of metric to be used in the calibration.

required
args list | None

Any positional or keyword arguments to pass to the objective function. If None, defaults to an empty list.

required

Raises:

Type Description
AssertionError

If objective_function is not callable.

Source code in src/hapi/calibration.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def read_objective_function(
    self, objective_function: Callable[..., Any], args: list | None
):
    """Read and store the objective function and its arguments.

    Takes the objective function and any additional arguments that
    need to be passed to the objective function during calibration.

    Args:
        objective_function (callable): A callable function to calculate
            any kind of metric to be used in the calibration.
        args: Any positional or keyword arguments to pass to the
            objective function. If None, defaults to an empty list.

    Raises:
        AssertionError: If objective_function is not callable.
    """
    # check objective_function
    assert callable(objective_function), (
        "The Objective function should be a function"
    )
    self.objective_function = objective_function

    if args is None:
        args = []

    self.OFArgs = args

    print("Objective function is read successfully")

run_calibration(spatial_var_fun: Callable[..., Any], optimization_args: list, print_error: int | None = None) #

Run the calibration algorithm for the distributed hydrological model.

Executes the Harmony Search optimization algorithm to calibrate parameters for the conceptual distributed hydrological model. The method distributes parameters spatially using spatial_var_fun, runs the RRM model via Wrapper.RRMModel, and evaluates performance using the stored objective function.

The following attributes must be set on the instance before calling this method:

- ``Prec``, ``ET``, ``Temp``: Meteorological input arrays.
- ``flow_dir_arr``: Flow direction array.
- ``rows``, ``cols``: Grid dimensions.
- ``LB``, ``UB``: Lower and upper parameter bounds.
- ``objective_function``: Objective function for evaluation.
- ``QGauges``, ``GaugesTable``: Observed discharge data and
  gauge metadata.

Parameters:

Name Type Description Default
spatial_var_fun Callable[..., Any]

Spatial variable function object with a Function method that distributes parameters and a Par3d attribute holding the 3D parameter array, plus no_parameters and no_elem attributes.

required
optimization_args list

A list of three elements: - optimization_args[0] (dict): Harmony Search API objective arguments (e.g., HMS, HMCR, PAR). - optimization_args[1]: Parallel type for the optimizer. - optimization_args[2] (dict): Solver arguments with keys "store_sol", "display_opts", "store_hst", and "hot_start".

required
print_error int | None

If not 0, prints the error value and parameters at each iteration. Default is None.

None

Returns:

Type Description
tuple

Optimization result tuple containing: - res[0]: The optimal objective function value. - res[1]: The optimal parameter set.

Raises:

Type Description
AssertionError

If input dimensions are inconsistent or if optimization arguments are not dictionaries.

Source code in src/hapi/calibration.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def run_calibration(
    self,
    spatial_var_fun: Callable[..., Any],
    optimization_args: list,
    print_error: int | None = None,
):
    """Run the calibration algorithm for the distributed hydrological model.

    Executes the Harmony Search optimization algorithm to calibrate
    parameters for the conceptual distributed hydrological model.
    The method distributes parameters spatially using ``spatial_var_fun``,
    runs the RRM model via ``Wrapper.RRMModel``, and evaluates
    performance using the stored objective function.

    The following attributes must be set on the instance before calling
    this method:

        - ``Prec``, ``ET``, ``Temp``: Meteorological input arrays.
        - ``flow_dir_arr``: Flow direction array.
        - ``rows``, ``cols``: Grid dimensions.
        - ``LB``, ``UB``: Lower and upper parameter bounds.
        - ``objective_function``: Objective function for evaluation.
        - ``QGauges``, ``GaugesTable``: Observed discharge data and
          gauge metadata.

    Args:
        spatial_var_fun: Spatial variable function object with a
            ``Function`` method that distributes parameters and a
            ``Par3d`` attribute holding the 3D parameter array, plus
            ``no_parameters`` and ``no_elem`` attributes.
        optimization_args: A list of three elements:
            - ``optimization_args[0]`` (dict): Harmony Search API
              objective arguments (e.g., HMS, HMCR, PAR).
            - ``optimization_args[1]``: Parallel type for the
              optimizer.
            - ``optimization_args[2]`` (dict): Solver arguments with
              keys ``"store_sol"``, ``"display_opts"``,
              ``"store_hst"``, and ``"hot_start"``.
        print_error: If not 0, prints the error value and parameters
            at each iteration. Default is None.

    Returns:
        tuple: Optimization result tuple containing:
            - res[0]: The optimal objective function value.
            - res[1]: The optimal parameter set.

    Raises:
        AssertionError: If input dimensions are inconsistent or if
            optimization arguments are not dictionaries.
    """
    # input dimensions
    # [rows,cols] = self.FlowAcc.ReadAsArray().shape
    [fd_rows, fd_cols] = self.flow_dir_arr.shape
    assert fd_rows == self.rows and fd_cols == self.cols, ROWS_MISMATCH_ERROR

    # input dimensions
    assert (
        np.shape(self.Prec)[0] == self.rows
        and np.shape(self.ET)[0] == self.rows
        and np.shape(self.Temp)[0] == self.rows
    ), ROWS_MISMATCH_ERROR
    assert (
        np.shape(self.Prec)[1] == self.cols
        and np.shape(self.ET)[1] == self.cols
        and np.shape(self.Temp)[1] == self.cols
    ), COLUMNS_MISMATCH_ERROR
    assert (
        np.shape(self.Prec)[2] == np.shape(self.ET)[2] and np.shape(self.Temp)[2]
    ), "all meteorological input data should have the same length"

    # basic inputs
    # check if all inputs are included
    # assert all(["p2","init_st","UB","LB","snow "][i] in basic_inputs.keys()
    #     for i in range(4)), "basic_inputs should contain ['p2','init_st','UB','LB']"

    ### optimization

    # get arguments
    api_obj_args = optimization_args[0]
    pll_type = optimization_args[1]
    api_solve_args = optimization_args[2]
    # check optimization arguement
    assert type(api_obj_args) is dict, "store_history should be 0 or 1"
    assert type(api_solve_args) is dict, "history_fname should be of type string "

    print("Calibration starts")

    ### calculate the objective function
    def opt_fun(par):
        try:
            # distribute the parameters
            spatial_var_fun.Function(
                par
            )  # , kub=spatial_var_fun.Kub, klb=spatial_var_fun.Klb
            self.Parameters = spatial_var_fun.Par3d
            # run the model
            Wrapper.RRMModel(self)
            # calculate performance of the model
            try:
                error = self.objective_function(
                    self.QGauges, *[self.GaugesTable]
                )  # self.qout, self.quz_routed, self.qlz_translated,
                f = list(range(9, len(par), spatial_var_fun.no_parameters))
                g = list()
                for i in range(len(f)):
                    k = par[f[i]]
                    x = par[f[i] + 1]
                    g.append(2 * k * x / self.dt)
                    g.append((2 * k * (1 - x)) / self.dt)

            except TypeError as e:
                # the objective function received fewer inputs than it needs
                raise ValueError(OBJECTIVE_FN_ARGS_ERROR) from e

            # print error
            if print_error != 0:
                print(round(error, 3))
                print(par)

            fail = 0
        except:
            error = np.nan
            g = []
            fail = 1

        return error, g, fail

    ### define the optimization components
    opt_prob = Optimization("HBV Calibration", opt_fun)
    for i in range(len(self.LB)):
        opt_prob.addVar(f"x{i}", type="c", lower=self.LB[i], upper=self.UB[i])

    opt_prob.addObj("f")

    for i in range(spatial_var_fun.no_elem):
        opt_prob.addCon("g" + str(i) + "-1", "i")
        opt_prob.addCon("g" + str(i) + "-2", "i")

    print(opt_prob)

    opt_engine = HSapi(pll_type=pll_type, options=api_obj_args)

    store_sol = api_solve_args["store_sol"]
    display_opts = api_solve_args["display_opts"]
    store_hst = api_solve_args["store_hst"]
    hot_start = api_solve_args["hot_start"]

    res = opt_engine(
        opt_prob,
        store_sol=store_sol,
        display_opts=display_opts,
        store_hst=store_hst,
        hot_start=hot_start,
    )

    self.Parameters = res[1]
    self.OFvalue = res[0]

    return res