Claude Skill

swmm-params

Deterministic mapping from land use and soil texture to SWMM runoff/subarea and Green-Ampt infiltration parameters. Use when generating first-pass subcatchment parameter tables for swmm-builder.

LLM Mart · 0 points · 5 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download zhonghao1995-agentic-swmm-workflow-skills_swmm-params-2d743b9.zip · 8 KB
Part of zhonghao1995/agentic-swmm-workflow — 18 skills

Install

skills CLI npx skills add https://github.com/Zhonghao1995/agentic-swmm-workflow/tree/main/skills/swmm-params
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install zhonghao1995-agentic-swmm-workflow@llmmart
Git git clone https://github.com/Zhonghao1995/agentic-swmm-workflow.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole zhonghao1995/agentic-swmm-workflow collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SWMM Params (MVP mapping layer)

Part of Agentic SWMM — install the project first for the executable toolchain (aiswmm CLI, SWMM solver, MCP servers).

What this skill provides

  • Transparent CSV-to-JSON mapping for:
    • land use class -> SWMM [SUBCATCHMENTS] + [SUBAREAS] defaults
    • soil texture/type -> SWMM [INFILTRATION] (Green-Ampt) defaults
  • Deterministic, auditable outputs with explicit fallback usage and unmatched-key reporting.
  • Optional merge step that emits one builder-ready JSON artifact.

Scripts

  • scripts/landuse_to_swmm_params.py
    • maps subcatchment_id + landuse_class to runoff/subarea parameters
  • scripts/soil_to_greenampt.py
    • maps subcatchment_id + soil_texture to Green-Ampt infiltration parameters
  • scripts/merge_swmm_params.py
    • merges outputs from the two mapping scripts into one JSON package for future swmm-builder

Default lookup tables

By default, scripts read bundled lookup CSVs:

  • skills/swmm-params/references/landuse_class_to_subcatch_params.csv
  • skills/swmm-params/references/soil_texture_to_greenampt.csv

You can override lookup paths with CLI flags.

Minimal input format

Land use input CSV:

  • required columns: subcatchment_id, landuse_class

Soil input CSV:

  • required columns: subcatchment_id, soil_texture

Example files are provided under examples/.

Outputs

Each mapper writes explicit JSON containing:

  • records (row-level audit trail)
  • sections (SWMM-oriented lists keyed by subcatchment)
  • unmatched_* lists (rows that used fallback)
  • counts summary

The merge script writes:

  • sections (subcatchments, subareas, infiltration)
  • by_subcatchment (combined record per subcatchment ID)
  • incomplete_ids (IDs missing one or more sections)

CLI flags

All three scripts share these optional flags:

  • --strict — fail instead of using the DEFAULT fallback row when an input key is missing from the lookup table. Useful for auditable production runs where silent fallback would mask a data gap.

landuse_to_swmm_params.py also accepts:

  • --subcatchment-column <col> — override the CSV column used as the subcatchment ID (default: subcatchment_id).
  • --landuse-column <col> — override the CSV column used as the land use class (default: landuse_class).

soil_to_greenampt.py also accepts:

  • --subcatchment-column <col> — override the CSV column used as the subcatchment ID (default: subcatchment_id).
  • --soil-column <col> — override the CSV column used as the soil texture/type (default: soil_texture).

MCP

MCP wrapper location:

  • mcp/swmm-params/server.js

Exposed tools:

  • map_landuse (inputCsvPath, optional lookupCsvPath, outputPath)
  • map_soil (inputCsvPath, optional lookupCsvPath, outputPath)
  • merge_params (landuseJsonPath, soilJsonPath, outputPath)

Quick start:

npm --prefix mcp/swmm-params install
npm --prefix mcp/swmm-params run start

Known limitations

  • Lookup mapping is key-based only (no spatial interpolation or fuzzy matching).
  • A fallback row is expected in lookup tables (DEFAULT for land use, - or DEFAULT for soil).
  • No unit conversion or calibration logic is included here.
  • This skill only maps parameters; it does not write a full SWMM .inp.

Example commands

python3 skills/swmm-params/scripts/landuse_to_swmm_params.py \
  --input skills/swmm-params/examples/landuse_input.csv \
  --output runs/swmm-params/example_landuse.json
python3 skills/swmm-params/scripts/soil_to_greenampt.py \
  --input skills/swmm-params/examples/soil_input.csv \
  --output runs/swmm-params/example_soil.json
python3 skills/swmm-params/scripts/merge_swmm_params.py \
  --landuse-json runs/swmm-params/example_landuse.json \
  --soil-json runs/swmm-params/example_soil.json \
  --output runs/swmm-params/example_builder_params.json
Files (agentic-swmm-workflow)
  • examples
    • landuse_input.csv 96 B · in bundle
    • soil_input.csv 74 B · in bundle
  • references
    • landuse_class_to_subcatch_params.csv 1.3 KB · in bundle
    • soil_texture_to_greenampt.csv 363 B · in bundle
  • scripts
    • landuse_to_swmm_params.py 7.4 KB
      #!/usr/bin/env python3
      from __future__ import annotations
      
      import argparse
      import csv
      import json
      from pathlib import Path
      from typing import Any
      
      SKILL_DIR = Path(__file__).resolve().parents[1]
      DEFAULT_LOOKUP = SKILL_DIR / "references/landuse_class_to_subcatch_params.csv"
      
      
      def normalize_key(value: str) -> str:
          return " ".join(str(value).strip().lower().split())
      
      
      def load_csv_rows(path: Path) -> list[dict[str, str]]:
          with path.open(newline="", encoding="utf-8") as f:
              rows = list(csv.DictReader(f))
          if not rows:
              raise ValueError(f"CSV has no data rows: {path}")
          return rows
      
      
      def parse_float(value: Any, *, field: str, csv_path: Path, row_number: int) -> float:
          if value is None or str(value).strip() == "":
              raise ValueError(f"Missing numeric value for '{field}' at {csv_path}:{row_number}")
          try:
              return float(str(value).strip())
          except ValueError as exc:
              raise ValueError(f"Invalid float for '{field}' at {csv_path}:{row_number}: {value}") from exc
      
      
      def save_json(path: Path, obj: Any) -> None:
          path.parent.mkdir(parents=True, exist_ok=True)
          path.write_text(json.dumps(obj, indent=2), encoding="utf-8")
      
      
      def build_lookup(lookup_rows: list[dict[str, str]], lookup_path: Path) -> tuple[dict[str, dict[str, Any]], dict[str, Any] | None]:
          lookup: dict[str, dict[str, Any]] = {}
          default_record: dict[str, Any] | None = None
          for i, row in enumerate(lookup_rows, start=2):
              raw_key = (row.get("landuse_class") or "").strip()
              if not raw_key:
                  raise ValueError(f"Missing 'landuse_class' in lookup at {lookup_path}:{i}")
              key = normalize_key(raw_key)
              if key in lookup:
                  raise ValueError(f"Duplicate lookup key '{raw_key}' in {lookup_path}")
              rec = {
                  "landuse_class": raw_key,
                  "imperv_pct": parse_float(row.get("imperv_pct"), field="imperv_pct", csv_path=lookup_path, row_number=i),
                  "n_imperv": parse_float(row.get("n_imperv"), field="n_imperv", csv_path=lookup_path, row_number=i),
                  "n_perv": parse_float(row.get("n_perv"), field="n_perv", csv_path=lookup_path, row_number=i),
                  "dstore_imperv_in": parse_float(
                      row.get("dstore_imperv_in"), field="dstore_imperv_in", csv_path=lookup_path, row_number=i
                  ),
                  "dstore_perv_in": parse_float(
                      row.get("dstore_perv_in"), field="dstore_perv_in", csv_path=lookup_path, row_number=i
                  ),
                  "zero_imperv_pct": parse_float(
                      row.get("zero_imperv_pct"), field="zero_imperv_pct", csv_path=lookup_path, row_number=i
                  ),
                  "route_to": (row.get("route_to") or "").strip(),
                  "pct_routed": parse_float(row.get("pct_routed"), field="pct_routed", csv_path=lookup_path, row_number=i),
                  "notes": (row.get("notes") or "").strip(),
              }
              lookup[key] = rec
              if raw_key.upper() == "DEFAULT":
                  default_record = rec
          return lookup, default_record
      
      
      def require_column(row: dict[str, str], col: str, *, csv_path: Path, row_number: int) -> str:
          if col not in row:
              raise ValueError(f"Missing required column '{col}' in {csv_path}")
          value = (row.get(col) or "").strip()
          if not value:
              raise ValueError(f"Missing value for '{col}' at {csv_path}:{row_number}")
          return value
      
      
      def main() -> None:
          ap = argparse.ArgumentParser(
              description="Map land use class to SWMM runoff/subarea parameters (deterministic CSV -> JSON)."
          )
          ap.add_argument("--input", type=Path, required=True, help="Input CSV with subcatchment_id and landuse_class columns.")
          ap.add_argument("--lookup", type=Path, default=DEFAULT_LOOKUP, help="Lookup CSV for land use mapping.")
          ap.add_argument("--output", type=Path, required=True, help="Output JSON path.")
          ap.add_argument("--subcatchment-column", default="subcatchment_id")
          ap.add_argument("--landuse-column", default="landuse_class")
          ap.add_argument("--strict", action="store_true", help="Fail if any land use class is missing from lookup.")
          args = ap.parse_args()
      
          input_rows = load_csv_rows(args.input)
          lookup_rows = load_csv_rows(args.lookup)
          lookup_map, default_record = build_lookup(lookup_rows, args.lookup)
      
          seen_ids: set[str] = set()
          unmatched: set[str] = set()
          records: list[dict[str, Any]] = []
          section_subcatchments: list[dict[str, Any]] = []
          section_subareas: list[dict[str, Any]] = []
      
          for i, row in enumerate(input_rows, start=2):
              subcatchment_id = require_column(row, args.subcatchment_column, csv_path=args.input, row_number=i)
              landuse_raw = require_column(row, args.landuse_column, csv_path=args.input, row_number=i)
              if subcatchment_id in seen_ids:
                  raise ValueError(f"Duplicate subcatchment_id '{subcatchment_id}' in {args.input}")
              seen_ids.add(subcatchment_id)
      
              key = normalize_key(landuse_raw)
              lookup_rec = lookup_map.get(key)
              used_default = False
              if lookup_rec is None:
                  if args.strict or default_record is None:
                      raise ValueError(
                          f"Unmapped landuse_class '{landuse_raw}' at {args.input}:{i}. "
                          "Add a lookup row or run without --strict and ensure DEFAULT exists."
                      )
                  lookup_rec = default_record
                  used_default = True
                  unmatched.add(landuse_raw)
      
              subcatchment_entry = {
                  "id": subcatchment_id,
                  "pct_imperv": lookup_rec["imperv_pct"],
              }
              subarea_entry = {
                  "id": subcatchment_id,
                  "n_imperv": lookup_rec["n_imperv"],
                  "n_perv": lookup_rec["n_perv"],
                  "dstore_imperv_in": lookup_rec["dstore_imperv_in"],
                  "dstore_perv_in": lookup_rec["dstore_perv_in"],
                  "zero_imperv_pct": lookup_rec["zero_imperv_pct"],
                  "route_to": lookup_rec["route_to"],
                  "pct_routed": lookup_rec["pct_routed"],
              }
              section_subcatchments.append(subcatchment_entry)
              section_subareas.append(subarea_entry)
              records.append(
                  {
                      "subcatchment_id": subcatchment_id,
                      "input_landuse_class": landuse_raw,
                      "lookup_landuse_class": lookup_rec["landuse_class"],
                      "used_default": used_default,
                      "subcatchment": subcatchment_entry,
                      "subarea": subarea_entry,
                      "notes": lookup_rec["notes"],
                  }
              )
      
          payload = {
              "ok": True,
              "mapping": "landuse_to_runoff_subarea",
              "input_csv": str(args.input),
              "lookup_csv": str(args.lookup),
              "counts": {
                  "input_rows": len(input_rows),
                  "mapped_rows": len(records),
                  "used_default_rows": sum(1 for r in records if r["used_default"]),
              },
              "unmatched_landuse_classes": sorted(unmatched),
              "sections": {
                  "subcatchments": section_subcatchments,
                  "subareas": section_subareas,
              },
              "records": records,
          }
          save_json(args.output, payload)
          print(
              json.dumps(
                  {
                      "ok": True,
                      "output_json": str(args.output),
                      "mapped_rows": payload["counts"]["mapped_rows"],
                      "used_default_rows": payload["counts"]["used_default_rows"],
                      "unmatched_landuse_classes": payload["unmatched_landuse_classes"],
                  },
                  indent=2,
              )
          )
      
      
      if __name__ == "__main__":
          main()
      
    • merge_swmm_params.py 5.2 KB
      #!/usr/bin/env python3
      from __future__ import annotations
      
      import argparse
      import json
      from pathlib import Path
      from typing import Any
      
      
      def load_json(path: Path) -> Any:
          return json.loads(path.read_text(encoding="utf-8"))
      
      
      def save_json(path: Path, obj: Any) -> None:
          path.parent.mkdir(parents=True, exist_ok=True)
          path.write_text(json.dumps(obj, indent=2), encoding="utf-8")
      
      
      def index_by_id(entries: list[dict[str, Any]], *, section: str) -> dict[str, dict[str, Any]]:
          idx: dict[str, dict[str, Any]] = {}
          for entry in entries:
              raw_id = entry.get("id")
              if raw_id is None:
                  raise ValueError(f"Missing 'id' in section '{section}'")
              subcatchment_id = str(raw_id).strip()
              if not subcatchment_id:
                  raise ValueError(f"Blank 'id' in section '{section}'")
              if subcatchment_id in idx:
                  raise ValueError(f"Duplicate id '{subcatchment_id}' in section '{section}'")
              idx[subcatchment_id] = entry
          return idx
      
      
      def main() -> None:
          ap = argparse.ArgumentParser(
              description="Merge land use and soil mapping outputs into one explicit JSON payload for future swmm-builder."
          )
          ap.add_argument("--landuse-json", type=Path, default=None, help="Output JSON from landuse_to_swmm_params.py")
          ap.add_argument("--soil-json", type=Path, default=None, help="Output JSON from soil_to_greenampt.py")
          ap.add_argument("--output", type=Path, required=True, help="Output merged JSON path.")
          ap.add_argument("--strict", action="store_true", help="Fail if any subcatchment is missing one or more sections.")
          args = ap.parse_args()
      
          if args.landuse_json is None and args.soil_json is None:
              raise ValueError("At least one source is required: --landuse-json and/or --soil-json")
      
          landuse_subcatchments: dict[str, dict[str, Any]] = {}
          landuse_subareas: dict[str, dict[str, Any]] = {}
          soil_infiltration: dict[str, dict[str, Any]] = {}
      
          if args.landuse_json is not None:
              landuse_obj = load_json(args.landuse_json)
              sections = landuse_obj.get("sections", {})
              landuse_subcatchments = index_by_id(sections.get("subcatchments", []), section="subcatchments")
              landuse_subareas = index_by_id(sections.get("subareas", []), section="subareas")
      
          if args.soil_json is not None:
              soil_obj = load_json(args.soil_json)
              sections = soil_obj.get("sections", {})
              soil_infiltration = index_by_id(sections.get("infiltration", []), section="infiltration")
      
          all_ids = sorted(set(landuse_subcatchments) | set(landuse_subareas) | set(soil_infiltration))
      
          by_subcatchment: list[dict[str, Any]] = []
          incomplete_ids: list[dict[str, Any]] = []
          for subcatchment_id in all_ids:
              rec: dict[str, Any] = {"id": subcatchment_id}
              missing: list[str] = []
      
              subcatchment = landuse_subcatchments.get(subcatchment_id)
              if subcatchment is not None:
                  rec["subcatchment"] = subcatchment
              else:
                  missing.append("subcatchments")
      
              subarea = landuse_subareas.get(subcatchment_id)
              if subarea is not None:
                  rec["subarea"] = subarea
              else:
                  missing.append("subareas")
      
              infiltration = soil_infiltration.get(subcatchment_id)
              if infiltration is not None:
                  rec["infiltration"] = infiltration
              else:
                  missing.append("infiltration")
      
              if missing:
                  rec["missing_sections"] = missing
                  incomplete_ids.append({"id": subcatchment_id, "missing_sections": missing})
      
              by_subcatchment.append(rec)
      
          if args.strict and incomplete_ids:
              raise ValueError(
                  f"Found incomplete subcatchment mappings under --strict: {json.dumps(incomplete_ids, ensure_ascii=True)}"
              )
      
          payload = {
              "ok": True,
              "mapping": "merged_swmm_params",
              "sources": {
                  "landuse_json": str(args.landuse_json) if args.landuse_json is not None else None,
                  "soil_json": str(args.soil_json) if args.soil_json is not None else None,
              },
              "counts": {
                  "subcatchment_count": len(all_ids),
                  "subcatchments_with_subcatchment_section": len(landuse_subcatchments),
                  "subcatchments_with_subarea_section": len(landuse_subareas),
                  "subcatchments_with_infiltration_section": len(soil_infiltration),
                  "incomplete_subcatchment_count": len(incomplete_ids),
              },
              "incomplete_ids": incomplete_ids,
              "sections": {
                  "subcatchments": [landuse_subcatchments[sid] for sid in sorted(landuse_subcatchments)],
                  "subareas": [landuse_subareas[sid] for sid in sorted(landuse_subareas)],
                  "infiltration": [soil_infiltration[sid] for sid in sorted(soil_infiltration)],
              },
              "by_subcatchment": by_subcatchment,
          }
      
          save_json(args.output, payload)
          print(
              json.dumps(
                  {
                      "ok": True,
                      "output_json": str(args.output),
                      "subcatchment_count": payload["counts"]["subcatchment_count"],
                      "incomplete_subcatchment_count": payload["counts"]["incomplete_subcatchment_count"],
                  },
                  indent=2,
              )
          )
      
      
      if __name__ == "__main__":
          main()
      
    • soil_to_greenampt.py 6.4 KB
      #!/usr/bin/env python3
      from __future__ import annotations
      
      import argparse
      import csv
      import json
      from pathlib import Path
      from typing import Any
      
      SKILL_DIR = Path(__file__).resolve().parents[1]
      DEFAULT_LOOKUP = SKILL_DIR / "references/soil_texture_to_greenampt.csv"
      
      
      def normalize_key(value: str) -> str:
          return " ".join(str(value).strip().lower().split())
      
      
      def load_csv_rows(path: Path) -> list[dict[str, str]]:
          with path.open(newline="", encoding="utf-8") as f:
              rows = list(csv.DictReader(f))
          if not rows:
              raise ValueError(f"CSV has no data rows: {path}")
          return rows
      
      
      def parse_float(value: Any, *, field: str, csv_path: Path, row_number: int) -> float:
          if value is None or str(value).strip() == "":
              raise ValueError(f"Missing numeric value for '{field}' at {csv_path}:{row_number}")
          try:
              return float(str(value).strip())
          except ValueError as exc:
              raise ValueError(f"Invalid float for '{field}' at {csv_path}:{row_number}: {value}") from exc
      
      
      def save_json(path: Path, obj: Any) -> None:
          path.parent.mkdir(parents=True, exist_ok=True)
          path.write_text(json.dumps(obj, indent=2), encoding="utf-8")
      
      
      def build_lookup(lookup_rows: list[dict[str, str]], lookup_path: Path) -> tuple[dict[str, dict[str, Any]], dict[str, Any] | None]:
          lookup: dict[str, dict[str, Any]] = {}
          default_record: dict[str, Any] | None = None
          for i, row in enumerate(lookup_rows, start=2):
              raw_key = (row.get("texture") or "").strip()
              if not raw_key:
                  raise ValueError(f"Missing 'texture' in lookup at {lookup_path}:{i}")
              key = normalize_key(raw_key)
              if key in lookup:
                  raise ValueError(f"Duplicate lookup key '{raw_key}' in {lookup_path}")
              rec = {
                  "texture": raw_key,
                  "suction_mm": parse_float(row.get("suction_mm"), field="suction_mm", csv_path=lookup_path, row_number=i),
                  "ksat_mm_per_hr": parse_float(
                      row.get("ksat_mm_per_hr"), field="ksat_mm_per_hr", csv_path=lookup_path, row_number=i
                  ),
                  "imdmax": parse_float(row.get("imdmax"), field="imdmax", csv_path=lookup_path, row_number=i),
                  "notes": (row.get("notes") or "").strip(),
              }
              lookup[key] = rec
              if raw_key in {"-", "DEFAULT", "default"}:
                  default_record = rec
          if default_record is None:
              default_record = lookup.get(normalize_key("-")) or lookup.get(normalize_key("default"))
          return lookup, default_record
      
      
      def require_column(row: dict[str, str], col: str, *, csv_path: Path, row_number: int) -> str:
          if col not in row:
              raise ValueError(f"Missing required column '{col}' in {csv_path}")
          value = (row.get(col) or "").strip()
          if not value:
              raise ValueError(f"Missing value for '{col}' at {csv_path}:{row_number}")
          return value
      
      
      def main() -> None:
          ap = argparse.ArgumentParser(
              description="Map soil texture/type to SWMM Green-Ampt infiltration parameters (deterministic CSV -> JSON)."
          )
          ap.add_argument("--input", type=Path, required=True, help="Input CSV with subcatchment_id and soil_texture columns.")
          ap.add_argument("--lookup", type=Path, default=DEFAULT_LOOKUP, help="Lookup CSV for soil texture mapping.")
          ap.add_argument("--output", type=Path, required=True, help="Output JSON path.")
          ap.add_argument("--subcatchment-column", default="subcatchment_id")
          ap.add_argument("--soil-column", default="soil_texture")
          ap.add_argument("--strict", action="store_true", help="Fail if any soil texture is missing from lookup.")
          args = ap.parse_args()
      
          input_rows = load_csv_rows(args.input)
          lookup_rows = load_csv_rows(args.lookup)
          lookup_map, default_record = build_lookup(lookup_rows, args.lookup)
      
          seen_ids: set[str] = set()
          unmatched: set[str] = set()
          records: list[dict[str, Any]] = []
          section_infiltration: list[dict[str, Any]] = []
      
          for i, row in enumerate(input_rows, start=2):
              subcatchment_id = require_column(row, args.subcatchment_column, csv_path=args.input, row_number=i)
              soil_raw = require_column(row, args.soil_column, csv_path=args.input, row_number=i)
              if subcatchment_id in seen_ids:
                  raise ValueError(f"Duplicate subcatchment_id '{subcatchment_id}' in {args.input}")
              seen_ids.add(subcatchment_id)
      
              key = normalize_key(soil_raw)
              lookup_rec = lookup_map.get(key)
              used_default = False
              if lookup_rec is None:
                  if args.strict or default_record is None:
                      raise ValueError(
                          f"Unmapped soil texture '{soil_raw}' at {args.input}:{i}. "
                          "Add a lookup row or run without --strict and ensure '-' or DEFAULT exists."
                      )
                  lookup_rec = default_record
                  used_default = True
                  unmatched.add(soil_raw)
      
              infiltration_entry = {
                  "id": subcatchment_id,
                  "suction_mm": lookup_rec["suction_mm"],
                  "ksat_mm_per_hr": lookup_rec["ksat_mm_per_hr"],
                  "imdmax": lookup_rec["imdmax"],
              }
              section_infiltration.append(infiltration_entry)
              records.append(
                  {
                      "subcatchment_id": subcatchment_id,
                      "input_soil_texture": soil_raw,
                      "lookup_texture": lookup_rec["texture"],
                      "used_default": used_default,
                      "infiltration": infiltration_entry,
                      "notes": lookup_rec["notes"],
                  }
              )
      
          payload = {
              "ok": True,
              "mapping": "soil_to_green_ampt",
              "input_csv": str(args.input),
              "lookup_csv": str(args.lookup),
              "counts": {
                  "input_rows": len(input_rows),
                  "mapped_rows": len(records),
                  "used_default_rows": sum(1 for r in records if r["used_default"]),
              },
              "unmatched_soil_textures": sorted(unmatched),
              "sections": {
                  "infiltration": section_infiltration,
              },
              "records": records,
          }
          save_json(args.output, payload)
          print(
              json.dumps(
                  {
                      "ok": True,
                      "output_json": str(args.output),
                      "mapped_rows": payload["counts"]["mapped_rows"],
                      "used_default_rows": payload["counts"]["used_default_rows"],
                      "unmatched_soil_textures": payload["unmatched_soil_textures"],
                  },
                  indent=2,
              )
          )
      
      
      if __name__ == "__main__":
          main()
      
  • SKILL.md 4.1 KB
    ---
    name: swmm-params
    description: Deterministic mapping from land use and soil texture to SWMM runoff/subarea and Green-Ampt infiltration parameters. Use when generating first-pass subcatchment parameter tables for swmm-builder.
    ---
    
    # SWMM Params (MVP mapping layer)
    
    Part of [Agentic SWMM](https://github.com/Zhonghao1995/agentic-swmm-workflow) — install the project first for the executable toolchain (aiswmm CLI, SWMM solver, MCP servers).
    
    ## What this skill provides
    - Transparent CSV-to-JSON mapping for:
      - land use class -> SWMM `[SUBCATCHMENTS]` + `[SUBAREAS]` defaults
      - soil texture/type -> SWMM `[INFILTRATION]` (Green-Ampt) defaults
    - Deterministic, auditable outputs with explicit fallback usage and unmatched-key reporting.
    - Optional merge step that emits one builder-ready JSON artifact.
    
    ## Scripts
    - `scripts/landuse_to_swmm_params.py`
      - maps `subcatchment_id + landuse_class` to runoff/subarea parameters
    - `scripts/soil_to_greenampt.py`
      - maps `subcatchment_id + soil_texture` to Green-Ampt infiltration parameters
    - `scripts/merge_swmm_params.py`
      - merges outputs from the two mapping scripts into one JSON package for future `swmm-builder`
    
    ## Default lookup tables
    By default, scripts read bundled lookup CSVs:
    - `skills/swmm-params/references/landuse_class_to_subcatch_params.csv`
    - `skills/swmm-params/references/soil_texture_to_greenampt.csv`
    
    You can override lookup paths with CLI flags.
    
    ## Minimal input format
    Land use input CSV:
    - required columns: `subcatchment_id`, `landuse_class`
    
    Soil input CSV:
    - required columns: `subcatchment_id`, `soil_texture`
    
    Example files are provided under `examples/`.
    
    ## Outputs
    Each mapper writes explicit JSON containing:
    - `records` (row-level audit trail)
    - `sections` (SWMM-oriented lists keyed by subcatchment)
    - `unmatched_*` lists (rows that used fallback)
    - `counts` summary
    
    The merge script writes:
    - `sections` (`subcatchments`, `subareas`, `infiltration`)
    - `by_subcatchment` (combined record per subcatchment ID)
    - `incomplete_ids` (IDs missing one or more sections)
    
    ## CLI flags
    
    All three scripts share these optional flags:
    
    - `--strict` — fail instead of using the `DEFAULT` fallback row when an input key is missing from the lookup table. Useful for auditable production runs where silent fallback would mask a data gap.
    
    `landuse_to_swmm_params.py` also accepts:
    
    - `--subcatchment-column <col>` — override the CSV column used as the subcatchment ID (default: `subcatchment_id`).
    - `--landuse-column <col>` — override the CSV column used as the land use class (default: `landuse_class`).
    
    `soil_to_greenampt.py` also accepts:
    
    - `--subcatchment-column <col>` — override the CSV column used as the subcatchment ID (default: `subcatchment_id`).
    - `--soil-column <col>` — override the CSV column used as the soil texture/type (default: `soil_texture`).
    
    ## MCP
    MCP wrapper location:
    - `mcp/swmm-params/server.js`
    
    Exposed tools:
    - `map_landuse` (`inputCsvPath`, optional `lookupCsvPath`, `outputPath`)
    - `map_soil` (`inputCsvPath`, optional `lookupCsvPath`, `outputPath`)
    - `merge_params` (`landuseJsonPath`, `soilJsonPath`, `outputPath`)
    
    Quick start:
    ```bash
    npm --prefix mcp/swmm-params install
    npm --prefix mcp/swmm-params run start
    ```
    
    ## Known limitations
    - Lookup mapping is key-based only (no spatial interpolation or fuzzy matching).
    - A fallback row is expected in lookup tables (`DEFAULT` for land use, `-` or `DEFAULT` for soil).
    - No unit conversion or calibration logic is included here.
    - This skill only maps parameters; it does not write a full SWMM `.inp`.
    
    ## Example commands
    ```bash
    python3 skills/swmm-params/scripts/landuse_to_swmm_params.py \
      --input skills/swmm-params/examples/landuse_input.csv \
      --output runs/swmm-params/example_landuse.json
    ```
    
    ```bash
    python3 skills/swmm-params/scripts/soil_to_greenampt.py \
      --input skills/swmm-params/examples/soil_input.csv \
      --output runs/swmm-params/example_soil.json
    ```
    
    ```bash
    python3 skills/swmm-params/scripts/merge_swmm_params.py \
      --landuse-json runs/swmm-params/example_landuse.json \
      --soil-json runs/swmm-params/example_soil.json \
      --output runs/swmm-params/example_builder_params.json
    ```
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related