pcswmm-engineering-optimizer / pcswmm_tools.py
razaali10's picture
Upload 55 files
7857730 verified
Raw
History Blame Contribute Delete
26.7 kB
"""Public tool surface for the PCSWMM Engineering MCP.
This product retains its PCSWMM-engineering identity while supporting controlled deterministic evidence backends. The public workflow starts from a
structured package exported by the PCSWMM Engineering SDK. The legacy
uploaded-INP functions remain available internally so the service can perform
an independent SWMM verification, but they are not published as MCP tools.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Callable
from sessions import STORE
import tools as core
from pcswmm_bridge import extract_inp_content
from revision_pipeline import revision_summary_text
from deterministic_evidence_adapter import (
build_mcp_package_from_evidence, selected_figure_payloads, validate_evidence_directory,
)
def connect_active_pcswmm_project(
package: dict | str,
submission_type: str = "model_only_review",
submission_number: str = "1",
previous_submission_reference: str = "",
auto_verify: bool = False,
) -> dict:
"""Connect the active PCSWMM project package and optionally verify it independently.
The package must be exported by the PCSWMM Engineering SDK and should include
project identity, active scenario context, engineering findings, and the exact
INP text when independent verification is requested.
"""
ingested = core.ingest_pcswmm_package(
package=package,
session_id="",
create_model_session=True,
)
session_id = ingested["session_id"]
configured = core.configure_submission(
session_id=session_id,
submission_type=submission_type,
submission_number=submission_number,
previous_submission_reference=previous_submission_reference,
)
verification: dict[str, Any] = {
"status": "not_run",
"reason": "auto_verify=False",
}
normalized = STORE.get(session_id).data.get("pcswmm_package", {})
has_inp = bool(extract_inp_content(normalized))
if auto_verify:
if not has_inp:
verification = {
"status": "not_run",
"reason": "The PCSWMM package does not contain INP text.",
}
else:
verification = core.run_simulation(session_id)
return {
"session_id": session_id,
"product": "PCSWMM Engineering MCP",
"active_project": ingested["validation"],
"submission": configured,
"independent_verification": verification,
"revision_review": ingested.get("revision_review", {}),
"next_step": (
"Call review_active_pcswmm_model, inspect findings, then generate_pcswmm_swmr."
),
}
def connect_deterministic_calgary_evidence(
evidence_dir: str,
submission_type: str = "model_only_review",
submission_number: str = "1",
auto_attach_figures: bool = True,
max_figures: int = 12,
) -> dict:
"""Connect an existing deterministic PCSWMM evidence directory to this MCP.
This is the backend-neutral bridge for local workflows used by PCSWMM, PySWMM, Claude Desktop,
ChatGPT Work, and Codex. The engineer runs PCSWMM and the native SDK first;
this tool then ingests the resulting evidence directory without recalculating
model values.
"""
check = validate_evidence_directory(evidence_dir)
if not check["valid"]:
raise ValueError("Invalid evidence directory: " + "; ".join(check["missing"]))
package = build_mcp_package_from_evidence(evidence_dir)
result = connect_active_pcswmm_project(
package=package,
submission_type=submission_type,
submission_number=submission_number,
auto_verify=False,
)
session_id = result["session_id"]
session = STORE.get(session_id)
session.data["deterministic_evidence_dir"] = check["evidence_dir"]
attached = []
if auto_attach_figures:
for payload in selected_figure_payloads(evidence_dir, max_figures=max_figures):
attached.append(core.attach_figure(
session_id=session_id,
image_base64=payload["image_base64"],
caption=payload["caption"],
section=payload["section"],
))
result.update({
"evidence_directory": check,
"attached_primary_figures": len(attached),
"next_step": (
"Call review_active_pcswmm_model, set_pcswmm_report_details, "
"get_pcswmm_submission_readiness, then generate_pcswmm_swmr."
),
})
return result
def generate_calgary_swmr_from_evidence(
evidence_dir: str,
project_name: str = "",
client: str = "",
consultant: str = "",
prepared_by: str = "",
outline_plan_no: str = "",
submission_type: str = "model_only_review",
submission_number: str = "1",
include_model_appendix: bool = True,
max_figures: int = 12,
) -> dict:
"""One-command City of Calgary SWMR draft from a prior deterministic evidence run.
Reuses the existing MCP review and report engines. It does not introduce a
parallel SWMR generator and does not rerun PCSWMM.
"""
connected = connect_deterministic_calgary_evidence(
evidence_dir=evidence_dir,
submission_type=submission_type,
submission_number=submission_number,
auto_attach_figures=True,
max_figures=max_figures,
)
session_id = connected["session_id"]
session = STORE.get(session_id)
package = session.data.get("pcswmm_package", {}) or {}
project = package.get("project", {}) or {}
resolved_project = project_name or str(project.get("project_name") or "PCSWMM Project")
details = {
"client": client or project.get("client") or "Not provided",
"consultant": consultant or project.get("consultant") or "Not provided",
"prepared_by": prepared_by or project.get("prepared_by") or "Not provided",
"outline_plan_no": outline_plan_no or project.get("outline_plan_number") or "Not provided",
"municipality": project.get("municipality") or "City of Calgary",
}
core.set_report_details(session_id, details)
review = review_active_pcswmm_model(session_id)
readiness = core.get_submission_readiness(session_id)
report = generate_pcswmm_swmr(
session_id=session_id,
project_name=resolved_project,
client=str(details["client"]),
consultant=str(details["consultant"]),
prepared_by=str(details["prepared_by"]),
outline_plan_no=str(details["outline_plan_no"]),
include_model_appendix=include_model_appendix,
)
return {
"session_id": session_id,
"project_name": resolved_project,
"review": review,
"submission_readiness": readiness,
"report": report,
"evidence_directory": connected["evidence_directory"],
"attached_primary_figures": connected["attached_primary_figures"],
"note": "Draft generated from deterministic PCSWMM evidence for professional engineering review.",
}
def connect_pyswmm_calgary_evidence(
evidence_dir: str,
submission_type: str = "model_only_review",
submission_number: str = "1",
auto_attach_figures: bool = True,
max_figures: int = 12,
) -> dict:
"""Connect deterministic evidence produced by the PySWMM Calgary backend.
Reuses the existing PCSWMM-engineering MCP review, readiness, figure, response
matrix, and SWMR tools. Hydraulic values are not recalculated during connection.
"""
result = connect_deterministic_calgary_evidence(
evidence_dir=evidence_dir,
submission_type=submission_type,
submission_number=submission_number,
auto_attach_figures=auto_attach_figures,
max_figures=max_figures,
)
session = STORE.get(result["session_id"])
package = session.data.get("pcswmm_package", {}) or {}
backend = (package.get("source") or {}).get("backend")
result["backend"] = backend or "PySWMM / EPA SWMM"
result["workflow"] = "PySWMM deterministic evidence -> existing PCSWMM-engineering MCP"
return result
def generate_calgary_swmr_from_pyswmm_evidence(
evidence_dir: str,
project_name: str = "",
client: str = "",
consultant: str = "",
prepared_by: str = "",
outline_plan_no: str = "",
submission_type: str = "model_only_review",
submission_number: str = "1",
include_model_appendix: bool = True,
max_figures: int = 12,
) -> dict:
"""Generate a City of Calgary SWMR draft from PySWMM deterministic evidence."""
result = generate_calgary_swmr_from_evidence(
evidence_dir=evidence_dir, project_name=project_name, client=client,
consultant=consultant, prepared_by=prepared_by, outline_plan_no=outline_plan_no,
submission_type=submission_type, submission_number=submission_number,
include_model_appendix=include_model_appendix, max_figures=max_figures,
)
result["backend"] = "PySWMM / EPA SWMM"
result["note"] = (
"Draft generated by the existing PCSWMM-engineering MCP from deterministic "
"PySWMM/EPA SWMM evidence for professional engineering review."
)
return result
def validate_active_pcswmm_project(session_id: str) -> dict:
"""Validate the connected PCSWMM package and report available capabilities."""
result = core.validate_pcswmm_package(session_id)
session = STORE.get(session_id)
result["independent_results_available"] = bool(session.data.get("results"))
result["active_project_connected"] = True
return result
def run_independent_pcswmm_verification(session_id: str) -> dict:
"""Independently rerun the active PCSWMM model through the isolated SWMM worker."""
session = STORE.get(session_id)
if not session.data.get("pcswmm_package"):
raise ValueError("This session is not connected to a PCSWMM engineering package.")
if not session.data.get("inp_path"):
raise ValueError(
"The PCSWMM package did not include INP content; independent execution is unavailable."
)
return core.run_simulation(session_id)
def optimize_pcswmm_design(
session_id: str,
objective: dict | str,
variables: dict | str | None = None,
candidates: list | str | None = None,
constraints: dict | str | None = None,
outfall_link_id: str = "",
max_evaluations: int = 24,
) -> dict:
"""Screen candidate designs against FIXED City of Calgary design-standard
constraints and rank the feasible ones by a VARIABLE design objective.
Requires run_independent_pcswmm_verification to have been called first on
this session (optimization re-simulates the model, so it needs the same
INP text and base run that independent verification requires).
objective: {"metric": "maximum_link_velocity" | "maximum_modelled_depth_ratio"
| "maximum_node_flooding" | "peak_link_flow"
| "maximum_storage_volume" | "maximum_storage_depth"
| "peak_subcatchment_runoff" | "total_conduit_volume_m3",
"direction": "minimize" | "maximize"}
variables: grid-search mode -- {"conduit_diameter_overrides":
{"C1": [0.3, 0.375, 0.45]}, "storage_depth_overrides":
{"ST1": [1.0, 1.5]}}; every combination is evaluated, capped
by max_evaluations. Supported categories:
conduit_diameter_overrides, conduit_roughness_overrides,
storage_depth_overrides.
candidates: shortlist mode -- a list of fully-formed override dicts to
evaluate directly instead of a grid. Supply either variables
or candidates, not conceptually both.
constraints: overrides merged onto the fixed COC defaults (max_depth_ratio,
min_velocity_mps, max_velocity_mps, max_node_flooding_cms,
max_continuity_error_pct, max_allowable_outfall_flow_cms).
These defaults are commonly-used stormwater design thresholds,
NOT a transcription of a specific City of Calgary SWMDM or
Industry Bulletin edition -- confirm every value against
current criteria before treating a PASS as compliance.
outfall_link_id: the link ID whose peak flow is checked against
max_allowable_outfall_flow_cms, if that constraint is set.
"""
if not isinstance(objective, dict) and not isinstance(objective, str):
raise ValueError('objective must be an object like {"metric": ..., "direction": ...}')
return core.optimize_design(
session_id=session_id,
objective=objective,
variables=variables,
candidates=candidates,
constraints=constraints,
outfall_link_id=outfall_link_id,
max_evaluations=max_evaluations,
)
def get_pcswmm_overland_flow_assessment(
session_id: str,
major_link_ids: list | str | None = None,
) -> dict:
"""Extract peak Q/v/d for major-system (overland) flow routes from the
connected model and screen them against the Alberta Environment / City
of Calgary depth-velocity criteria (SWMDM 2011, Table 3-20/Figure 3-23),
returning the Table-11-13-style tabulation and a Figure-11-3-style chart
ready to attach to the SWMR via attach_pcswmm_figure -- the extraction
and calculation step consultants have otherwise had to do by hand from
raw model output.
major_link_ids: optional list of link IDs to treat as the major/overland
system, matching your own drawing classification. If omitted, overland
routes are auto-detected from the INP by cross-section shape
(TRAPEZOIDAL, RECT_OPEN, TRIANGULAR, IRREGULAR, STREET).
Requires run_independent_pcswmm_verification to have been called first
on this session (this reads the same simulated node/link results).
Returns: detection_method, the depth-velocity curve and its source
citation, a unit_system flag, the tabulated per-link results (native
model units), and chart_png_base64 -- a ready-to-attach PNG chart.
"""
return core.get_overland_flow_assessment(session_id, major_link_ids=major_link_ids)
def get_pcswmm_discharge_volume_summary(
session_id: str,
locations: list | str | None = None,
outfall_node_ids: list | str | None = None,
) -> dict:
"""Table 11-12-style permissible-discharge/runoff-volume summary per
location/manhole, plus cumulative system volume at one or more outfalls
(City of Calgary SWMDM 2011, Section 11.1.7.2.8 item iv).
Requires run_independent_pcswmm_verification to have been called first
on this session.
From the model: Invert, Obvert, maximum HGL, and Runoff Volume (a
trapezoidal integration of that node's simulated inflow, independently
validated against the SWMM engine's own continuity totals to 9
significant figures) are computed directly. Area is auto-summed from
subcatchments draining to that node unless overridden.
NOT computed, by design -- must be supplied if wanted in the table:
permissible Discharge Rate (L/s/ha, a City-assigned/design value) and
Storage Volume (a preliminary on-site storage result, e.g. from the
wbscc or calgary-storm-retention tools). Omitted fields are left blank
with a note, never fabricated.
locations: [{"node_id": "MH17-3", "location_label": "Site 1",
"manhole_number": "17-3", "area_ha": 1.2 (optional override),
"discharge_rate_lps_ha": 50 (optional), "storage_volume_m3": 120
(optional)}, ...]
outfall_node_ids: outfall node IDs to compute cumulative volume for; if
omitted, every OUTFALLS node in the model is used.
"""
return core.get_discharge_volume_summary(
session_id, locations=locations, outfall_node_ids=outfall_node_ids
)
def get_active_pcswmm_project(session_id: str) -> dict:
"""Return connected PCSWMM project, package, scenario, and execution identity."""
session = STORE.get(session_id)
validation = session.data.get("pcswmm_validation", {})
package = session.data.get("pcswmm_package", {})
project = package.get("project") or package.get("project_summary") or {}
source = package.get("source") or {}
metadata = (session.data.get("results") or {}).get("metadata", {})
return {
"session_id": session_id,
"project": project,
"source": source,
"package_validation": validation,
"active_scenario": package.get("active_scenario") or project.get("active_scenario"),
"pcswmm_sdk_version": source.get("sdk_version") or package.get("sdk_version"),
"independent_execution": {
"available": bool(session.data.get("results")),
"run_id": metadata.get("run_id"),
"model_sha256": metadata.get("model_sha256"),
"results_usable": metadata.get("results_usable"),
"engine": "EPA SWMM / pyswmm isolated local worker",
},
}
def review_active_pcswmm_model(session_id: str) -> dict:
"""Run the consolidated deterministic review of the connected active PCSWMM model."""
session = STORE.get(session_id)
if not session.data.get("pcswmm_package"):
raise ValueError("No active PCSWMM project is connected to this session.")
package_validation = core.validate_pcswmm_package(session_id)
package = session.data.get("pcswmm_package", {}) or {}
revision = session.data.get("revision_review", {}) or {}
submission_type = str(session.data.get("submission_type", "first_submission"))
revision_applicable = submission_type in {"revised_submission", "final_submission"} or bool(revision.get("object_impacts"))
revision_review = (
{"applicable": True, "summary": revision_summary_text(revision), **revision}
if revision.get("object_impacts")
else {
"applicable": False,
"status": "not_applicable" if not revision_applicable else "missing",
"message": (
"Revision review is not applicable to this first/model-only submission."
if not revision_applicable else
"A revised/final submission requires a baseline snapshot and structured revision review."
),
}
)
review: dict[str, Any] = {
"session_id": session_id,
"package_validation": package_validation,
"revision_review": revision_review,
"pcswmm_engineering_review": package.get("engineering_review", {}),
"pcswmm_result_summary": (package.get("results", {}) or {}),
}
if session.data.get("results"):
review.update({
"execution_reconciliation": core.get_reconciliation(session_id),
"calgary_screening": core.calgary_screening(session_id),
"engineering_findings": core.preliminary_design_review(session_id),
"node_results": core.get_node_results(session_id, limit=20),
"link_results": core.get_link_results(session_id, limit=20),
})
else:
review["independent_verification"] = {
"status": "not_available",
"reason": "Run run_independent_pcswmm_verification before relying on hydraulic conclusions.",
}
reasoning = ((package.get("engineering_review") or {}).get("reasoning_narrative"))
review["executive_summary"] = (
revision_summary_text(revision) if revision.get("object_impacts")
else (reasoning or "PCSWMM-native engineering review completed from the active project package.")
)
review["disclaimer"] = (
"Deterministic engineering screening for professional review; not a sealed design determination."
)
return review
def get_pcswmm_node_results(
session_id: str,
node_type: str = "",
sort_by: str = "Depth Ratio",
limit: int = 20,
) -> dict:
"""Return independently verified node results for the active PCSWMM project."""
return core.get_node_results(session_id, node_type=node_type, sort_by=sort_by, limit=limit)
def get_pcswmm_link_results(
session_id: str,
sort_by: str = "Peak Velocity (m/s)",
limit: int = 20,
) -> dict:
"""Return independently verified link results for the active PCSWMM project."""
return core.get_link_results(session_id, sort_by=sort_by, limit=limit)
def get_pcswmm_subcatchment_results(session_id: str, limit: int = 30) -> dict:
"""Return independently verified subcatchment results for the active PCSWMM project."""
return core.get_subcatchment_results(session_id, limit=limit)
def get_pcswmm_timeseries(
session_id: str,
object_type: str,
object_id: str,
variable: str,
) -> dict:
"""Return a bounded time series for an active PCSWMM project object."""
return core.get_timeseries(session_id, object_type, object_id, variable)
def review_pcswmm_revision(session_id: str) -> dict:
"""Return the normalized baseline-to-revised PCSWMM impact assessment."""
return core.get_design_revision_review(session_id)
def configure_pcswmm_submission(
session_id: str,
submission_type: str = "first_submission",
submission_number: str = "1",
previous_submission_reference: str = "",
certified_checklist_attached: bool = False,
no_objections_letter_attached: bool = False,
prior_approved_report_attached: bool = False,
correspondence_appendix_attached: bool = False,
clean_copy: bool = False,
) -> dict:
"""Configure the Calgary submission route for the active PCSWMM project."""
return core.configure_submission(
session_id=session_id,
submission_type=submission_type,
submission_number=submission_number,
previous_submission_reference=previous_submission_reference,
certified_checklist_attached=certified_checklist_attached,
no_objections_letter_attached=no_objections_letter_attached,
prior_approved_report_attached=prior_approved_report_attached,
correspondence_appendix_attached=correspondence_appendix_attached,
clean_copy=clean_copy,
)
def set_pcswmm_city_comments(
session_id: str,
comments: list | str,
report_changes: dict | str = {},
) -> dict:
"""Attach City comments and documented changes to the active PCSWMM project session."""
return core.set_city_comments(session_id, comments, report_changes)
def build_pcswmm_city_response_matrix(session_id: str) -> dict:
"""Build the evidence-linked City comment response matrix for the PCSWMM project."""
return core.build_city_comment_response_matrix(session_id)
def get_pcswmm_submission_readiness(session_id: str) -> dict:
"""Apply staged draft, engineer-review, and submission-readiness gates."""
return core.get_submission_readiness(session_id)
def get_pcswmm_hydraulic_summary(session_id: str) -> dict:
"""Return critical deterministic hydraulic results without rerunning the model."""
return core.get_hydraulic_summary(session_id)
def set_pcswmm_report_details(session_id: str, details: dict | str) -> dict:
"""Set project, client, consultant, author, and report metadata."""
return core.set_report_details(session_id, details)
def set_pcswmm_report_configuration(session_id: str, configuration: dict | str) -> dict:
"""Set confirmed report criteria and presentation options."""
return core.set_report_configuration(session_id, configuration)
def attach_pcswmm_figure(
session_id: str,
image_base64: str,
caption: str,
section: str = "Engineering Review",
) -> dict:
"""Attach a PCSWMM profile, graph, map, or engineering figure to the report."""
return core.attach_figure(session_id, image_base64, caption, section)
def _native_pcswmm_swmr(session, project_name: str, client: str, consultant: str,
prepared_by: str, outline_plan_no: str,
include_model_appendix: bool) -> dict:
"""Generate a structured consultant-demonstration SWMR from PCSWMM-native evidence."""
from pcswmm_native_report import generate_native_swmr
return generate_native_swmr(
session=session, project_name=project_name, client=client, consultant=consultant,
prepared_by=prepared_by, outline_plan_no=outline_plan_no,
include_model_appendix=include_model_appendix,
)
def generate_pcswmm_swmr(
session_id: str,
project_name: str,
client: str = "",
consultant: str = "",
prepared_by: str = "",
outline_plan_no: str = "",
include_model_appendix: bool = True,
) -> dict:
"""Generate an SWMR from PCSWMM evidence; use the full reconciled engine when optional verification exists."""
session = STORE.get(session_id)
if not session.data.get("pcswmm_package"):
raise ValueError("A connected PCSWMM project is required.")
if session.data.get("results"):
return core.generate_report(
session_id=session_id, project_name=project_name, client=client,
consultant=consultant, prepared_by=prepared_by,
outline_plan_no=outline_plan_no,
include_model_appendix=include_model_appendix,
)
return _native_pcswmm_swmr(session, project_name, client, consultant, prepared_by,
outline_plan_no, include_model_appendix)
def list_pcswmm_sessions() -> dict:
"""List local PCSWMM Engineering MCP sessions."""
return core.list_sessions()
def close_pcswmm_session(session_id: str) -> dict:
"""Close a PCSWMM Engineering MCP session and remove temporary working files."""
return core.close_session(session_id)
PCSWMM_TOOL_REGISTRY: dict[str, Callable[..., dict]] = {
fn.__name__: fn
for fn in [
connect_active_pcswmm_project,
connect_deterministic_calgary_evidence,
connect_pyswmm_calgary_evidence,
generate_calgary_swmr_from_evidence,
generate_calgary_swmr_from_pyswmm_evidence,
validate_active_pcswmm_project,
get_active_pcswmm_project,
run_independent_pcswmm_verification,
optimize_pcswmm_design,
get_pcswmm_overland_flow_assessment,
get_pcswmm_discharge_volume_summary,
review_active_pcswmm_model,
get_pcswmm_node_results,
get_pcswmm_link_results,
get_pcswmm_subcatchment_results,
get_pcswmm_timeseries,
review_pcswmm_revision,
configure_pcswmm_submission,
set_pcswmm_city_comments,
build_pcswmm_city_response_matrix,
get_pcswmm_submission_readiness,
get_pcswmm_hydraulic_summary,
set_pcswmm_report_details,
set_pcswmm_report_configuration,
attach_pcswmm_figure,
generate_pcswmm_swmr,
list_pcswmm_sessions,
close_pcswmm_session,
]
}