swmm-calibration — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited swmm-calibration (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Part of Agentic SWMM — install the project first for the executable toolchain (aiswmm CLI, SWMM solver, MCP servers).
swmm-uncertainty for prior uncertainty propagation instead of calling the run calibrated..csv, .tsv, .dat, whitespace-separated text).sensitivitycalibratevalidatesearch --strategy random — uniform random sampling (fast prototyping).search --strategy lhs — Latin Hypercube Sampling (fast prototyping).search --strategy adaptive — multi-round LHS refinement around elite trials (fast prototyping).search --strategy sceua — Shuffled Complex Evolution (SCE-UA); recommended for publication-grade point-estimate calibration. Minimises (1 - KGE) via spotpy.algorithms.sceua and emits a calibration_summary.json with KGE decomposition + secondary metrics.search --strategy dream-zs — DREAM-ZS Bayesian calibration with a KGE-based likelihood exp(-0.5 * (1 - KGE) / sigma^2). Produces a posterior over parameters via spotpy.algorithms.dream, writes 5 audit artefacts (posterior_samples.csv, best_params.json, chain_convergence.json, posterior_<param>.png, posterior_correlation.png) plus a Slice 1 -compatible calibration_summary.json with a posterior_summary block (Gelman-Rubin Rhat per parameter + per-parameter quantiles).skills/swmm-uncertainty/scripts/sensitivity.py and the swmm_sensitivity_oat / swmm_sensitivity_morris / swmm_sensitivity_sobol MCP tools.| Strategy | When to use | Cost | Reports |
|---|---|---|---|
random | First-pass prototyping, smoke-testing the patch map | Very low | Ranking table |
lhs | Quick coverage of a small search space | Very low | Ranking table |
adaptive | LHS with multi-round refinement around elite trials | Low | Ranking table per round |
| `sceua` | Publication-grade point-estimate calibration on a fixed search space | Medium | `calibration_summary.json` with KGE primary + decomposition + secondary metrics + `convergence.csv` |
| `dream-zs` | Bayesian posterior calibration with Gelman-Rubin convergence checks | High | `calibration_summary.json` + `posterior_samples.csv` + `chain_convergence.json` + per-parameter marginal PNGs + correlation PNG |
mcp/swmm-calibration/server.js exposes six tools, all thin wrappers around scripts/swmm_calibrate.py.
swmm_sensitivity_* tools on the swmm-uncertainty MCP server.)best_params.json. Use when you already have a curated candidate list.random, lhs, adaptive (multi-round LHS refinement around elite trials). Use when you have a search-space JSON instead of an explicit candidate list.calibration_summary.json containing primary_objective, primary_value, kge_decomposition (r / alpha / beta), secondary_metrics (NSE, PBIAS%, RMSE, peak-flow error, peak-timing error), and a convergence.csv trace. Use for publication-grade point-estimate calibration. Requires the optional spotpy dependency.exp(-0.5 * (1 - KGE) / sigma^2). Writes 5 posterior artefacts to the chosen audit directory (defaults to the parent of summaryJson): posterior_samples.csv (post-burn-in MCMC samples), best_params.json (MAP estimate), chain_convergence.json (Gelman-Rubin Rhat per parameter), posterior_<param>.png (marginal histogram per parameter), posterior_correlation.png (parameter correlation matrix). The calibration_summary.json keeps the Slice 1 shape (primary_objective=kge, primary_value, kge_decomposition, secondary_metrics) plus a posterior_summary block with chain count, Rhat values, and per-parameter quantiles. Use for Bayesian uncertainty quantification on top of (or instead of) the SCE-UA point estimate. Requires the optional spotpy dependency.Sensitivity analysis (OAT / Morris / Sobol') is owned byswmm-uncertainty. Seemcp/swmm-uncertainty/server.jsforswmm_sensitivity_oat,swmm_sensitivity_morris, andswmm_sensitivity_sobol.
scripts/swmm_calibrate.py — backs swmm_sensitivity_scan, swmm_calibrate, swmm_calibrate_search, swmm_validate. Subcommands: sensitivity, calibrate, search, validate.scripts/obs_reader.py — heuristically reads timestamp + flow series from text tables.scripts/metrics.py — computes hydrograph comparison metrics after time alignment.scripts/inp_patch.py — patches selected numeric tokens in an .inp file using a simple JSON mapping.min/max/type/precision) for internal bounded search.sensitivitycalibratevalidateswmm-calibration and swmm-uncertainty share parameter patching but answer different questions.
Calibration asks:
Given observed data, which parameter set best reproduces the observed hydrograph?Uncertainty / sensitivity analysis asks:
Given uncertain parameters, how much does the SWMM output ensemble spread,
and which parameters drive that spread?Use this skill only when observed data are available and the workflow can compute metrics such as NSE, RMSE, bias, peak-flow error, or peak-timing error. If no observed data are available, use swmm-uncertainty for prior Monte Carlo, fuzzy, entropy, or sensitivity analysis.
Per issue #49 the OAT / Morris / Sobol' sensitivity-analysis path lives on swmm-uncertainty (skills/swmm-uncertainty/scripts/sensitivity.py). The calibration scaffold consumes its output via:
runs/<case>/09_audit/sensitivity_indices.json — per-parameter ranking with mu_star/sigma (Morris) or S_i/S_T_i (Sobol'). Use this to pre-screen which parameters to feed into SCE-UA or LHS search.A calibration run can feed uncertainty / sensitivity analysis back by exporting:
best_params.json for a baseline parameter setranking.json for candidate performance.out (preferred, via swmmtoolbox), orswmm_sensitivity_scan tool here scores explicit candidate sets against an observed series; it is not parameter screening. Use swmm-uncertainty's swmm_sensitivity_oat / swmm_sensitivity_morris / swmm_sensitivity_sobol tools for OAT / Morris / Sobol' screening.A patch-map JSON connects friendly parameter names to concrete INP edits.
Example:
{
"pct_imperv_s1": {
"section": "[SUBCATCHMENTS]",
"object": "S1",
"field_index": 4
},
"n_imperv_s1": {
"section": "[SUBAREAS]",
"object": "S1",
"field_index": 1
}
}Interpretation:
section = INP section header to search withinobject = first token on the target rowfield_index = zero-based token index within the data row[
{"name": "trial_001", "params": {"pct_imperv_s1": 42.0, "n_imperv_s1": 0.015}},
{"name": "trial_002", "params": {"pct_imperv_s1": 47.0, "n_imperv_s1": 0.018}}
]{
"pct_imperv_s1": {"min": 15.0, "max": 40.0, "type": "float", "precision": 3},
"n_imperv_s1": {"min": 0.01, "max": 0.03, "type": "float", "precision": 4}
}python3 skills/swmm-calibration/scripts/swmm_calibrate.py sensitivity \
--base-inp <your case>/event.inp \
--patch-map path/to/patch_map.json \
--parameter-sets path/to/parameter_sets.json \
--observed path/to/observed_flow.csv \
--run-root runs/calibration \
--swmm-node O1 \
--objective nsepython3 skills/swmm-calibration/scripts/swmm_calibrate.py calibrate \
--base-inp <your case>/event.inp \
--patch-map path/to/patch_map.json \
--parameter-sets path/to/parameter_sets.json \
--observed path/to/observed_flow.csv \
--run-root runs/calibration \
--swmm-node O1 \
--objective nsepython3 skills/swmm-calibration/scripts/swmm_calibrate.py validate \
--base-inp path/to/validation_event.inp \
--patch-map path/to/patch_map.json \
--best-params path/to/best_params.json \
--observed path/to/validation_observed.csv \
--run-root runs/validation \
--swmm-node O1python3 skills/swmm-calibration/scripts/swmm_calibrate.py search \
--base-inp <your case>/event.inp \
--patch-map <your case>/calibration/patch_map.json \
--search-space <your case>/calibration/search_space.json \
--observed <your case>/calibration/observed_flow.csv \
--run-root runs/calibration-search \
--summary-json runs/calibration-search/summary.json \
--ranking-json runs/calibration-search/ranking.json \
--strategy lhs \
--iterations 12 \
--seed 42python3 skills/swmm-calibration/scripts/swmm_calibrate.py search \
--base-inp <your case>/event.inp \
--patch-map <your case>/calibration/patch_map.json \
--search-space <your case>/calibration/search_space.json \
--observed <your case>/calibration/observed_flow.csv \
--run-root runs/calibration-search-adaptive \
--summary-json runs/calibration-search-adaptive/summary.json \
--strategy adaptive \
--iterations 8 \
--rounds 3 \
--seed 42Requires spotpy to be installed (it ships as a runtime dependency in pyproject.toml).
python3 skills/swmm-calibration/scripts/swmm_calibrate.py search \
--base-inp <your case>/event.inp \
--patch-map <your case>/calibration/patch_map.json \
--search-space <your case>/calibration/search_space.json \
--observed <your case>/calibration/observed_flow.csv \
--run-root runs/calibration-sceua \
--summary-json runs/calibration-sceua/calibration_summary.json \
--best-params-out runs/calibration-sceua/best_params.json \
--convergence-csv runs/calibration-sceua/convergence.csv \
--strategy sceua \
--objective kge \
--iterations 200 \
--seed 42calibration_summary.json shape:
{
"primary_objective": "kge",
"primary_value": 0.78,
"kge_decomposition": {"r": 0.92, "alpha": 1.05, "beta": 0.97},
"secondary_metrics": {
"nse": 0.74, "pbias_pct": -3.2, "rmse": 0.043,
"peak_error_rel": 0.08, "peak_timing_min": 12
},
"strategy": "sceua",
"iterations": 200,
"convergence_trace_ref": "convergence.csv"
}Requires spotpy (already a runtime dependency). Likelihood is exp(-0.5 * (1 - KGE) / sigma^2).
python3 skills/swmm-calibration/scripts/swmm_calibrate.py search \
--base-inp <your case>/event.inp \
--patch-map <your case>/calibration/patch_map.json \
--search-space <your case>/calibration/search_space.json \
--observed <your case>/calibration/observed_flow.csv \
--run-root runs/calibration-dream-zs/trials \
--summary-json runs/calibration-dream-zs/09_audit/calibration_summary.json \
--dream-output-dir runs/calibration-dream-zs/09_audit \
--best-params-out runs/calibration-dream-zs/09_audit/best_params.json \
--strategy dream-zs \
--objective kge \
--iterations 2000 \
--dream-chains 4 \
--dream-sigma 0.1 \
--dream-rhat-threshold 1.2 \
--seed 42The 09_audit/ folder will contain five DREAM-ZS artefacts plus calibration_summary.json:
posterior_samples.csv — post-burn-in MCMC samples (chain, iteration_in_chain, likelihood, one column per parameter).best_params.json — MAP estimate (highest-likelihood row from the chains).chain_convergence.json — Gelman-Rubin Rhat per parameter, threshold, and a converged flag.posterior_<param>.png — marginal histogram per parameter.posterior_correlation.png — posterior parameter correlation matrix.calibration_summary.json keeps the same shape as SCE-UA (so downstream tooling stays compatible) and adds a posterior_summary block:
{
"primary_objective": "kge",
"primary_value": 0.83,
"kge_decomposition": {"r": 0.94, "alpha": 1.02, "beta": 0.99},
"secondary_metrics": {"nse": 0.79, "pbias_pct": -1.4, "rmse": 0.038, "peak_error_rel": 0.05, "peak_timing_min": 8},
"strategy": "dream-zs",
"iterations": 2000,
"convergence_trace_ref": "chain_convergence.json",
"posterior_summary": {
"n_chains": 4,
"n_chains_requested": 4,
"n_samples_post_burnin": 1996,
"converged": true,
"rhat_threshold": 1.2,
"rhat": {"pct_imperv_s1": 1.07, "n_imperv_s1": 1.04, "...": "..."},
"per_parameter": {
"pct_imperv_s1": {"mean": 29.7, "median": 29.8, "std": 1.2, "q05": 27.6, "q95": 31.5}
}
}
}Calibration runs never patch the canonical INP. Every strategy (random / lhs / adaptive / SCE-UA / DREAM-ZS) emits three artefacts to <run_dir>/09_audit/ when invoked with --candidate-run-dir <run_dir>:
| Artefact | Purpose |
|---|---|
candidate_calibration.json | Best params + KGE + decomposition + secondary metrics + evidence_boundary: "candidate_not_accepted_yet" + SHA256 of the patch file + (DREAM only) posterior_samples_ref. |
candidate_inp_patch.json | One row per parameter (section, object, field_index, old_value, new_value) — the diff to apply when the human accepts. |
calibration_report.md | Human-readable summary: KGE decomposition table, secondary metrics, best parameters, convergence trace reference (SCE-UA) and posterior block (DREAM-ZS). |
The canonical INP file SHA256 is unchanged before and after calibration — the scaffold only reads it, to extract the old_value for each diff row.
Promotion is gated behind the expert-only CLI:
aiswmm calibration accept <run_dir>aiswmm calibration accept:
candidate_calibration.json; refuses if missing.candidate_inp_patch.json; refuses if missing.SHA recorded inside the candidate; refuses on mismatch (tamper detection).
inp_patchmachinery the agent uses.
human_decisions row on the run's09_audit/experiment_provenance.json with action == "calibration_accept", by == $USER, evidence_ref == "09_audit/candidate_calibration.json", and decision_text containing the applied patch SHA.
The agent has no path to step 4. Only the human can promote the candidate.
python3 skills/swmm-calibration/scripts/swmm_calibrate.py search \
--base-inp runs/<case>/model.inp \
--patch-map runs/<case>/calibration/patch_map.json \
--search-space runs/<case>/calibration/search_space.json \
--observed runs/<case>/calibration/observed_flow.csv \
--run-root runs/<case>/calibration-sceua/trials \
--summary-json runs/<case>/09_audit/calibration_summary.json \
--strategy sceua --objective kge --iterations 200 --seed 42 \
--candidate-run-dir runs/<case>
# After review:
aiswmm calibration accept runs/<case>~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.