Skip to content

Calibration

Model Calibration

Unified Calibration API

The calibrate() function provides a completely unified calibration interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from hydromodel.trainers.unified_calibrate import calibrate

config = {
    "data_cfgs": {
        "dataset": "camels_us",
        "basin_ids": ["01013500"],
        "train_period": ["1990-10-01", "2000-09-30"],
        "test_period": ["2000-10-01", "2010-09-30"],
        "warmup_length": 365,
    },
    "model_cfgs": {
        "name": "xaj_mz",
        "params": {
            "source_type": "sources",
            "source_book": "HF",
        },
    },
    "training_cfgs": {
        "algorithm": "SCE_UA",
        "SCE_UA": {
            "rep": 10000,
            "ngs": 100,
            "random_seed": 1234,
        },
        "loss_config": {
            "type": "time_series",
            "obj_func": "RMSE",  # User objective: RMSE, NSE, KGE, LOGNSE
        },
        "output_dir": "results",
        "experiment_name": "my_experiment",
    },
    "evaluation_cfgs": {
        "metrics": ["NSE", "KGE", "RMSE"],
    },
}

# Run calibration
results = calibrate(config)

Internal Workflow

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
calibrate()
  ↓
1. Parse configuration
  ↓
2. UnifiedDataLoader.load_data()
  ↓
3. Create UnifiedCalibrator (wraps MODEL_DICT)
  ↓
4. Select algorithm (SCE_UA, GA, scipy)
  ↓
5. For each basin:
     a. Initialize parameters (normalized [0,1])
     b. Run optimization loop
     c. For each iteration:
        - Denormalize parameters
        - Call MODEL_DICT[model_name](inputs, params, ...)
        - Calculate objective function
        - Update parameters
     d. Save best parameters
  ↓
6. Save results to output_dir

Algorithm Implementations

Uses spotpy library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
training_cfgs = {
    "algorithm": "SCE_UA",
    "SCE_UA": {
        "rep": 10000,         # Maximum iterations
        "ngs": 100,           # Number of complexes
        "kstop": 50,          # Stopping criteria
        "peps": 0.1,          # Convergence threshold
        "pcento": 0.1,        # Convergence percentage
        "random_seed": 1234,
    },
    "loss_config": {
        "type": "time_series",
        "obj_func": "RMSE",   # user objective: RMSE, NSE, KGE, LOGNSE
    },
}

Hydromodel optimizers always minimize. User-facing objectives NSE, KGE, and LOGNSE are resolved internally to negated objectives such as neg_nashsutcliffe, neg_kge, and neg_lognashsutcliffe. Evaluation metrics remain positive hydrological metrics such as NSE, KGE, RMSE, and PBIAS.

Output: {basin_id}_sceua.csv with columns: - like1: Objective function value - parK, parB, ...: Parameter values (with par prefix) - simulation1_1, ...: Simulation results for each iteration

Genetic Algorithm

Uses DEAP library:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
training_cfgs = {
    "algorithm": "GA",
    "GA": {
        "run_counts": 2,      # Number of evolutionary runs
        "pop_num": 50,        # Population size
        "cross_prob": 0.5,    # Crossover probability
        "mut_prob": 0.5,      # Mutation probability
        "save_freq": 1,       # Save frequency
        "random_seed": 1234,
    },
}

Output: Pickled checkpoints (epoch{N}.pkl) containing: - population: Current population - halloffame: Best individuals - logbook: Optimization history

Scipy Optimizers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
training_cfgs = {
    "algorithm": "scipy",
    "scipy": {
        "method": "Nelder-Mead",  # or "Powell", "COBYLA"
        "options": {
            "maxiter": 1000,
            "disp": True,
        },
    },
}

Parameter Management

Parameters are always normalized to [0, 1] during optimization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from hydromodel.models.model_config import read_model_param_dict

# Get parameter ranges
param_dict = read_model_param_dict(None)  # Uses default
param_ranges = param_dict["xaj_mz"]

print(param_ranges["param_name"])   # ['K', 'B', 'IM', ...]
print(param_ranges["param_range"])  # [[min, max], [min, max], ...]

# During optimization:
# 1. Optimizer works with normalized params [0, 1]
# 2. Before model call: denormalize to physical range
# 3. Run model with physical parameters
# 4. Calculate objective function

Output Files

1
2
3
4
results/{experiment_name}/
├── {basin_id}_sceua.csv            # SCE-UA calibration history
├── calibration_config.yaml          # Config used (for reproducibility)
└── param_range.yaml                 # Parameter ranges used

calibration_results.json is always written in the same directory. It keeps the legacy best_params field as normalized [0,1] values for evaluation compatibility. New fields make the contract explicit:

  • parameter_format: currently "normalized".
  • best_params_normalized: same values as legacy best_params.
  • best_params_denormalized: physical values from the resolved range.
  • param_range_source: default, explicit, or artifact.
  • loss_config.requested_obj_func: user objective such as KGE.
  • loss_config.resolved_obj_func: minimized internal objective such as neg_kge.