Model Calibration Skill
SkillMonitoring & opsCalculating RMSE metrics and parameter calibration for lake model validation
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Model Calibration Skill skill
What this skill tells your AI
The instructions your AI receives, as published by cxcscmu/skilllearnbench in skills/b1-one-shot-claude-haiku-4-5/temperature-simulation/model-calibration/SKILL.md and read by ahel’s review.
Overview
Model calibration involves adjusting parameters to minimize differences between simulations and observations. RMSE (Root Mean Squared Error) is a standard metric for measuring model performance.
RMSE Calculation
Basic Formula
RMSE = sqrt(mean((simulated - observed)^2))
Implementation
import numpy as np
import pandas as pd
def calculate_rmse(simulated, observed):
"""Calculate RMSE between matched pairs"""
if len(simulated) == 0:
return np.nan
residuals = simulated - observed
rmse = np.sqrt(np.mean(residuals**2))
return rmse
def calculate_metrics(sim_temps, obs_temps, depths=None, dates=None):
"""
Calculate multiple RMSE metrics
Parameters:
- sim_temps: matched simulated temperatures
- obs_temps: matched observed temperatures
- depths: depth of each match (for deep water filtering)
- dates: datetime of each match (for seasonal filtering)
Returns:
- overall_rmse: RMSE of all matched pairs
- annual_deep_rmse: RMSE of pairs at depths >= 13m
- summer_deep_rmse: RMSE of summer (Jun-Sep) pairs at depths >= 13m
"""
overall_rmse = calculate_rmse(sim_temps, obs_temps)
# Annual deep water (depths >= 13m)
if depths is not None:
deep_mask = np.array(depths) >= 13
annual_deep_rmse = calculate_rmse(
sim_temps[deep_mask],
obs_temps[deep_mask]
)
else:
annual_deep_rmse = np.nan
# Summer deep water (Jun-Sep, depths >= 13m)
if dates is not None and depths is not None:
summer_mask = (np.array([d.month for d in dates]) >= 6) & \
(np.array([d.month for d in dates]) <= 9)
deep_mask = np.array(depths) >= 13
combined_mask = summer_mask & deep_mask
summer_deep_rmse = calculate_rmse(
sim_temps[combined_mask],
obs_temps[combined_mask]
)
else:
summer_deep_rmse = np.nan
return {
'overall_rmse': overall_rmse,
'annual_deep_rmse': annual_deep_rmse,
'summer_deep_rmse': summer_deep_rmse
}
Matching Observations to Simulation
Critical: Use exact datetime + rounded-depth matching (no interpolation)
def match_obs_to_sim(obs_df, sim_temps, sim_z, sim_dates, round_depth=1):
"""
Match observations to simulation output
obs_df: DataFrame with columns [datetime, depth, temp]
sim_temps: [time, depth] array
sim_z: depth values
sim_dates: datetime for each time step
round_depth: rounding for depth matching
Returns: matched simulation temps, observed temps, and metadata
"""
import pandas as pd
obs_df['depth_rounded'] = (obs_df['depth'] / round_depth).round() * round_depth
matched_sim = []
matched_obs = []
matched_depths = []
matched_dates = []
for idx, row in obs_df.iterrows():
obs_datetime = pd.Timestamp(row['datetime'])
obs_depth = row['depth_rounded']
obs_temp = row['temp']
# Find exact datetime match
time_matches = [i for i, d in enumerate(sim_dates)
if d == obs_datetime]
# Find exact depth match
depth_matches = [i for i, z in enumerate(sim_z)
if z == obs_depth]
# Need both to match
if time_matches and depth_matches:
t_idx = time_matches[0]
z_idx = depth_matches[0]
sim_temp = sim_temps[t_idx, z_idx]
matched_sim.append(sim_temp)
matched_obs.append(obs_temp)
matched_depths.append(obs_depth)
matched_dates.append(obs_datetime)
return (np.array(matched_sim), np.array(matched_obs),
matched_depths, matched_dates)
Parameter Tuning Strategy
1. Sensitivity Analysis
Run model with one parameter varied at a time to identify most influential parameters.
2. Systematic Search
For 5 parameters with small ranges, can do grid search:
- Test 3-5 values per parameter
- Evaluate all combinations
- Select best performers
3. Iterative Refinement
- Start with literature values
- Adjust parameters that have largest impact on target RMSE
- Focus on worst-performing seasonal/depth bins
4. Constraint Checking
Always verify final parameters are within published ranges:
def validate_parameters(params):
ranges = {
'Kw': (0.1, 0.5),
'coef_mix_hyp': (0.3, 0.7),
'wind_factor': (0.7, 1.3),
'lw_factor': (0.7, 1.3),
'ch': (0.0005, 0.002)
}
for param, (min_val, max_val) in ranges.items():
if not (min_val <= params[param] <= max_val):
raise ValueError(f"{param} out of range: {params[param]}")
return True
Evaluation Thresholds
For Lake Mendota:
- Overall RMSE < 1.60°C
- Annual deep (≥13m) RMSE < 1.55°C
- Summer deep (Jun-Sep, ≥13m) RMSE < 1.70°C
Model is successful when ALL three thresholds are met.
Signals
- GitHub stars
- 83
- Forks
- 5
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
model-calibration- Source
- github.com/cxcscmu/skilllearnbench