Source code for openswmm_mcp.models

"""Pydantic response models for all OpenSWMM-MCP tool responses."""

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

# ---------------------------------------------------------------------------
# Session / Model metadata
# ---------------------------------------------------------------------------


[docs] class ModelSummary(BaseModel): """Returned after opening or inspecting a SWMM model.""" model_config = ConfigDict(from_attributes=True) session_id: str state: str engine: str = "openswmm" node_count: int link_count: int subcatchment_count: int gage_count: int pollutant_count: int flow_units: str route_model: str start_time: float end_time: float routing_step: float
[docs] class SessionListItem(BaseModel): """Compact session descriptor used in list-sessions responses.""" model_config = ConfigDict(from_attributes=True) session_id: str state: str engine: str = "openswmm" node_count: int link_count: int subcatchment_count: int
[docs] class SystemSummary(BaseModel): """Full system-level summary including optional runtime state.""" model_config = ConfigDict(from_attributes=True) session_id: str state: str engine: str = "openswmm" node_count: int link_count: int subcatchment_count: int gage_count: int pollutant_count: int flow_units: str route_model: str start_time: float end_time: float routing_step: float current_time: float | None = None # Extended fields (from refactored engine) surcharge_method: str | None = None dps_celerity: float | None = None dps_alpha: float | None = None dps_decay_time: float | None = None event_count: int | None = None steady_state_skip: bool | None = None
# --------------------------------------------------------------------------- # Simulation progress # ---------------------------------------------------------------------------
[docs] class SimulationResult(BaseModel): """Returned when a full simulation run completes.""" model_config = ConfigDict(from_attributes=True) session_id: str elapsed_wall_time: float steps_completed: int runoff_continuity_error: float routing_continuity_error: float quality_continuity_error: float | None = None # ------------------------------------------------------------------ # Phase 4d: backend-conditional discriminator. # # ``engine_kind`` advertises which backend produced the response # ("openswmm" or "legacy"); ``unsupported_fields`` lists fields whose # value is meaningless on the active backend (e.g. # ``quality_continuity_error`` is silently None on legacy when there # are no pollutants, but on the new engine it returns a per-pollutant # error). This lets LLM consumers distinguish "feature not supported # on this backend" from "feature supported and returned None". # ------------------------------------------------------------------ engine_kind: str | None = None unsupported_fields: list[str] | None = None
[docs] class StepResult(BaseModel): """Returned after executing one or more simulation steps.""" model_config = ConfigDict(from_attributes=True) session_id: str elapsed: float current_time: float completed: bool steps_taken: int
# --------------------------------------------------------------------------- # Geometry sub-models # ---------------------------------------------------------------------------
[docs] class CrossSectionInfo(BaseModel): """Cross-section shape and geometry parameters for a conduit or weir.""" model_config = ConfigDict(from_attributes=True) shape: int """Integer shape code (see XSectShape enum).""" shape_name: str """Human-readable shape name, e.g. ``CIRCULAR``.""" geom1: float geom2: float = 0.0 geom3: float = 0.0 geom4: float = 0.0 geom_labels: dict[str, float] = Field(default_factory=dict) """Mapping of semantic parameter name to value, e.g. ``{"diameter": 1.2}``."""
[docs] class ConduitGeometry(BaseModel): """Full geometry descriptor for a CONDUIT link.""" model_config = ConfigDict(from_attributes=True) xsect: CrossSectionInfo | None = None slope: float | None = None offset_up: float = 0.0 offset_dn: float = 0.0 initial_flow: float = 0.0 max_flow: float = 0.0 loss_coeff_inlet: float = 0.0 loss_coeff_outlet: float = 0.0 loss_coeff_avg: float = 0.0 flap_gate: bool = False seep_rate: float = 0.0 culvert_code: int = 0 barrels: int = 1
[docs] class WeirGeometry(BaseModel): """Geometry descriptor for a WEIR link.""" model_config = ConfigDict(from_attributes=True) xsect: CrossSectionInfo | None = None crest_height: float = 0.0 discharge_coeff: float = 0.0 end_contractions: float = 0.0 offset_up: float = 0.0 offset_dn: float = 0.0
[docs] class OrificeGeometry(BaseModel): """Geometry descriptor for an ORIFICE link.""" model_config = ConfigDict(from_attributes=True) xsect: CrossSectionInfo | None = None offset_up: float = 0.0 offset_dn: float = 0.0
[docs] class PumpGeometry(BaseModel): """Geometry descriptor for a PUMP link.""" model_config = ConfigDict(from_attributes=True) pump_curve_idx: int = -1 init_state_on: bool = False offset_up: float = 0.0 offset_dn: float = 0.0
[docs] class StorageGeometry(BaseModel): """Storage-unit geometry for a STORAGE node.""" model_config = ConfigDict(from_attributes=True) storage_type: str = "functional" """``curve`` if a stage-area curve is used, ``functional`` otherwise.""" curve_idx: int | None = None functional_a: float | None = None """Coefficient A in Area = A * Depth^B + C.""" functional_b: float | None = None functional_c: float | None = None seep_rate: float = 0.0 exfil_suction: float | None = None exfil_ksat: float | None = None exfil_imd: float | None = None
[docs] class OutfallGeometry(BaseModel): """Boundary-condition geometry for an OUTFALL node.""" model_config = ConfigDict(from_attributes=True) outfall_type: int = 0 outfall_type_name: str = "FREE" """One of FREE, NORMAL, FIXED, TIDAL, TIMESERIES.""" param: float = 0.0 """Stage value for FIXED outfalls; 0 for others.""" flap_gate: bool = False
# --------------------------------------------------------------------------- # Element info # ---------------------------------------------------------------------------
[docs] class NodeInfo(BaseModel): """Properties and current state of a single node.""" model_config = ConfigDict(from_attributes=True) node_id: str index: int node_type: str invert_elev: float | None = None max_depth: float | None = None # Extended geometry crown_elev: float | None = None full_volume: float | None = None surcharge_depth: float | None = None ponded_area: float | None = None degree: int | None = None initial_depth: float | None = None losses: float | None = None outflow: float | None = None # Type-specific geometry (populated only for the matching node type) storage: StorageGeometry | None = None outfall: OutfallGeometry | None = None # Runtime hydraulic state depth: float | None = None head: float | None = None volume: float | None = None lateral_inflow: float | None = None overflow: float | None = None outfall_route_to: int | None = None ponded_quality: dict[str, float] | None = None
[docs] class LinkInfo(BaseModel): """Properties and current state of a single link.""" model_config = ConfigDict(from_attributes=True) link_id: str index: int link_type: str from_node: str to_node: str length: float | None = None roughness: float | None = None # Extended geometry slope: float | None = None offset_up: float | None = None offset_dn: float | None = None initial_flow: float | None = None max_flow: float | None = None xsect: CrossSectionInfo | None = None # Type-specific geometry (populated only for the matching link type) conduit: ConduitGeometry | None = None weir: WeirGeometry | None = None orifice: OrificeGeometry | None = None pump: PumpGeometry | None = None # Runtime hydraulic state flow: float | None = None depth: float | None = None velocity: float | None = None capacity: float | None = None hydraulic_power: float | None = None pump_cycles: int | None = None pump_on_time: float | None = None pump_volume: float | None = None
[docs] class SubcatchmentInfo(BaseModel): """Properties and current state of a single subcatchment.""" model_config = ConfigDict(from_attributes=True) subcatch_id: str index: int area: float | None = None imperv_pct: float | None = None slope: float | None = None width: float | None = None rainfall: float | None = None runoff: float | None = None depth: float | None = None
[docs] class GageInfo(BaseModel): """Properties and current state of a rain gage.""" model_config = ConfigDict(from_attributes=True) gage_id: str index: int data_source: str rain_type: str rainfall: float | None = None
# --------------------------------------------------------------------------- # Mass balance # ---------------------------------------------------------------------------
[docs] class MassBalanceResult(BaseModel): """Continuity errors and volumetric totals.""" model_config = ConfigDict(from_attributes=True) session_id: str runoff_continuity_error: float routing_continuity_error: float quality_continuity_error: float | None = None quality_continuity_errors: dict[str, float] | None = None runoff_total: dict[str, float] routing_total: dict[str, float] routing_stats: dict[str, float] | None = None max_courant: float | None = None # ------------------------------------------------------------------ # Phase 4d: backend-conditional discriminator. # # ``routing_stats`` and the per-pollutant ``quality_continuity_errors`` # dict are available only on the new engine (the legacy backend lacks # ``get_routing_stats`` and silently ignores the ``pollutant_index`` # argument to ``get_quality_continuity_error``). Without an explicit # discriminator an LLM caller can't tell whether ``routing_stats`` # being ``None`` means "supported but no convergence issues" vs # "not supported on this backend." # ------------------------------------------------------------------ engine_kind: str | None = None unsupported_fields: list[str] | None = None
# --------------------------------------------------------------------------- # Time-series # ---------------------------------------------------------------------------
[docs] class TimeSeries(BaseModel): """Variable time-series for a single element.""" element_type: str element_id: str variable: str timestamps: list[float] values: list[float] units: str
# --------------------------------------------------------------------------- # Post-simulation summaries # ---------------------------------------------------------------------------
[docs] class FloodingSummaryItem(BaseModel): """Per-node flooding statistics.""" node_id: str max_overflow_rate: float total_flood_volume: float time_flooded: float max_depth: float
[docs] class CapacitySummaryItem(BaseModel): """Per-link capacity / flow statistics.""" link_id: str max_filling: float max_flow: float max_velocity: float time_above_threshold: float vol_flow: float = 0.0
# --------------------------------------------------------------------------- # Forcing / scenario-editing # ---------------------------------------------------------------------------
[docs] class ForcingResult(BaseModel): """Acknowledgement after applying a forcing override.""" status: str target_type: str element_id: str variable: str value: float mode: str persist: bool
[docs] class BuildingResult(BaseModel): """Acknowledgement after creating or modifying a model element.""" status: str element_type: str element_id: str index: int message: str
# --------------------------------------------------------------------------- # Hot-start / spatial / export / search # ---------------------------------------------------------------------------
[docs] class HotStartResult(BaseModel): """Result of a hot-start save or load operation.""" status: str path: str message: str
[docs] class SpatialResult(BaseModel): """Coordinates and vertex geometry for a model element.""" element_type: str element_id: str x: float | None = None y: float | None = None vertices: list[tuple[float, float]] | None = None
[docs] class ExportResult(BaseModel): """Acknowledgement after an export operation.""" status: str path: str format: str record_count: int
[docs] class ElementSearchResult(BaseModel): """Single match returned by an element search.""" element_type: str element_id: str index: int
# --------------------------------------------------------------------------- # Editing — deletion and type conversion # ---------------------------------------------------------------------------
[docs] class ImpactEntryModel(BaseModel): """One object that is affected by a deletion (cascade or nullification).""" model_config = ConfigDict(from_attributes=True) obj_type: int """Integer code: 0=node, 1=link, 2=subcatchment, 3=gage, 4=table, 5=transect, 6=inlet_usage.""" obj_type_name: str """Human-readable object type name.""" obj_idx: int """Zero-based index of the affected object.""" field: str """Name of the cross-reference field that was affected.""" cascaded: bool """True if the object was deleted; False if only the reference was nullified."""
[docs] class ImpactReportModel(BaseModel): """Result of a deletion impact analysis or a deletion operation.""" model_config = ConfigDict(from_attributes=True) session_id: str object_type: str """Type of the deleted / analysed object (``node``, ``link``, etc.).""" object_id: str """Identifier of the deleted / analysed object.""" dry_run: bool """True when the analysis was non-destructive (no objects were deleted).""" node_count: int """Number of nodes remaining after deletion (same as before for dry_run).""" link_count: int """Number of links remaining after deletion (same as before for dry_run).""" impacts: list[ImpactEntryModel] """Objects that were (or would be) affected."""
[docs] class ConversionResultModel(BaseModel): """Result of an in-place type conversion.""" model_config = ConfigDict(from_attributes=True) session_id: str object_type: str """``node`` or ``link``.""" object_id: str new_type: str """Human-readable new type name.""" cleared_fields: list[str] """Type-specific fields that were cleared during conversion.""" warnings: list[str] """Non-fatal topology warnings (e.g. "model has no outfall")."""
# --------------------------------------------------------------------------- # Property updates # ---------------------------------------------------------------------------
[docs] class PropertyUpdateResult(BaseModel): """Returned after updating element properties in-place.""" model_config = ConfigDict(from_attributes=True) session_id: str element_type: str """``node``, ``link``, or ``subcatchment``.""" element_id: str updated_fields: dict[str, float | int | str] """Mapping of field name to the new value that was applied."""
[docs] class GageConfigResult(BaseModel): """Returned after configuring a rain gage.""" model_config = ConfigDict(from_attributes=True) session_id: str gage_id: str updated_fields: dict[str, str | float | int] """Mapping of field name to the new value that was applied."""
# --------------------------------------------------------------------------- # Pollutant info # ---------------------------------------------------------------------------
[docs] class PollutantInfo(BaseModel): """Definition and properties of a single pollutant.""" model_config = ConfigDict(from_attributes=True) pollutant_id: str index: int units: int """Concentration units code: 0=MG/L, 1=UG/L, 2=#/L.""" units_name: str kdecay: float rain_conc: float gw_conc: float init_conc: float rdii_conc: float mwt: float snow_only: bool co_pollutant_idx: int co_pollutant_frac: float
# --------------------------------------------------------------------------- # Report snapshot models (mirror engine._report dataclasses) # ---------------------------------------------------------------------------
[docs] class RoutingDiagnosticsModel(BaseModel): """Routing Time Step Summary — convergence and time-step statistics.""" model_config = ConfigDict(from_attributes=True) avg_time_step: float min_time_step: float max_time_step: float n_steps: int pct_not_converged: float n_steps_not_converged: int avg_iterations: float max_courant: float
[docs] class RunoffContinuityModel(BaseModel): """Runoff Quantity Continuity table.""" model_config = ConfigDict(from_attributes=True) continuity_error_pct: float total_rainfall: float total_evaporation: float total_infiltration: float total_runoff: float total_snow_removal: float initial_storage: float final_storage: float
[docs] class RoutingContinuityModel(BaseModel): """Flow Routing Continuity table.""" model_config = ConfigDict(from_attributes=True) continuity_error_pct: float dry_weather_inflow: float wet_weather_inflow: float groundwater_inflow: float rdii_inflow: float external_inflow: float total_flooding: float total_outflow: float evaporation_loss: float seepage_loss: float initial_storage: float final_storage: float
[docs] class QualityContinuityModel(BaseModel): """Quality Routing Continuity entry for a single pollutant.""" model_config = ConfigDict(from_attributes=True) pollutant_id: str continuity_error_pct: float seep_loss: float evap_loss: float
[docs] class NodeFloodingEntryModel(BaseModel): """One row of the Node Flooding Summary table.""" model_config = ConfigDict(from_attributes=True) node_id: str node_type: str max_depth: float max_overflow_rate: float total_flood_volume: float time_flooded: float
[docs] class LinkFlowEntryModel(BaseModel): """One row of the Link Flow Summary table.""" model_config = ConfigDict(from_attributes=True) link_id: str link_type: str max_flow: float max_velocity: float max_filling: float total_volume: float surcharge_time: float
[docs] class PumpEntryModel(BaseModel): """One row of the Pump Summary table.""" model_config = ConfigDict(from_attributes=True) link_id: str pump_curve_idx: int num_startups: int total_on_time: float total_volume: float pct_time_on: float
[docs] class SubcatchmentEntryModel(BaseModel): """One row of the Subcatchment Runoff Summary table.""" model_config = ConfigDict(from_attributes=True) subcatch_id: str total_precip: float total_runoff_vol: float max_runoff_rate: float runoff_coefficient: float
[docs] class ReportSnapshotModel(BaseModel): """Full programmatic equivalent of the SWMM .rpt report file.""" model_config = ConfigDict(from_attributes=True) routing_diagnostics: RoutingDiagnosticsModel runoff_continuity: RunoffContinuityModel routing_continuity: RoutingContinuityModel quality_continuity: list[QualityContinuityModel] node_flooding: list[NodeFloodingEntryModel] storage_summary: list[NodeFloodingEntryModel] link_flow_summary: list[LinkFlowEntryModel] pump_summary: list[PumpEntryModel] subcatchment_summary: list[SubcatchmentEntryModel]