Spaces:
Sleeping
Sleeping
File size: 19,657 Bytes
e545bf5 aad9a8a e545bf5 aad9a8a e545bf5 aad9a8a e545bf5 aad9a8a e545bf5 aad9a8a e545bf5 aad9a8a e545bf5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | """Deterministic Calgary SWMR criteria helpers.
The rules in this module are deliberately transparent and configurable. They are
screening/default values, not a substitute for current City direction, an
approved SMDP/MDP, or professional engineering judgement.
"""
from __future__ import annotations
import json
import math
import re
from dataclasses import asdict, dataclass, field
from typing import Any
import pandas as pd
@dataclass
class CalgaryCriteria:
profile_name: str = "City of Calgary SWMR"
source_manual: str = "City of Calgary Stormwater Management & Design Manual (2011)"
manual_status: str = "Historical baseline - verify current amendments"
minor_release_rate_lps_ha: float | None = None
trap_low_max_depth_m: float = 0.50
entrance_grade_margin_m: float = 0.30
pipe_advisory_velocity_mps: float = 3.0
pipe_critical_velocity_mps: float = 4.0
conduit_capacity_review_ratio: float = 0.80
conduit_capacity_warning_ratio: float = 0.95
continuity_review_pct: float = 0.50
continuity_warning_pct: float = 1.00
depth_velocity_curve: tuple[tuple[float, float], ...] = (
(0.5, 0.80), (1.0, 0.32), (2.0, 0.21), (3.0, 0.09)
)
special_link_limits: dict[str, float] = field(default_factory=dict)
storage_classification: dict[str, str] = field(default_factory=dict)
outfall_classification: dict[str, str] = field(default_factory=dict)
def infer_design_event(rain_gages: list[str], fallback: str = "Model design event") -> tuple[str, str]:
names = [str(x) for x in rain_gages if str(x).strip()]
for name in names:
normalized = name.replace("_", " ").replace("-", " ")
duration = re.search(r"(\d+(?:\.\d+)?)\s*h", normalized, re.I)
return_period = re.search(r"(?:1\s*[:/]\s*)?(\d+)\s*y", normalized, re.I)
if duration and return_period:
return (
f"Calgary {duration.group(1)}-hour, 1:{return_period.group(1)}-year design storm",
f"Inferred from rain-gage name '{name}'; confirm before issue.",
)
return fallback, "Entered by user or retained from model metadata; confirm before issue."
def permissible_overland_depth(velocity_mps: float, curve: tuple[tuple[float, float], ...]) -> float | None:
"""Linearly interpolate the Alberta/Calgary street depth-velocity envelope."""
try:
v = float(velocity_mps)
except (TypeError, ValueError):
return None
pts = sorted((float(x), float(y)) for x, y in curve)
if not pts:
return None
if v <= pts[0][0]:
return pts[0][1]
if v > pts[-1][0]:
return pts[-1][1]
for (v1, d1), (v2, d2) in zip(pts[:-1], pts[1:]):
if v1 <= v <= v2:
f = (v - v1) / (v2 - v1)
return d1 + f * (d2 - d1)
return None
def classify_overland(depth_m: float, velocity_mps: float, curve: tuple[tuple[float, float], ...]) -> tuple[str, float | None]:
allowed = permissible_overland_depth(velocity_mps, curve)
if allowed is None:
return "Review - criterion unavailable", None
d = float(depth_m or 0.0)
ratio = d / allowed if allowed > 0 else math.inf
if ratio <= 0.90:
return "Screens within depth-velocity criterion", allowed
if ratio <= 1.00:
return "Screens within criterion - near limit", allowed
return "Exceeds depth-velocity criterion", allowed
def manning_full_capacity_circular(diameter: float, slope: float, n: float, unit_system: str) -> float | None:
"""Full-flow Manning capacity for a circular conduit in model-native flow units.
SI result: m3/s. US customary result: cfs.
"""
try:
d, s, rough = float(diameter), float(slope), float(n)
except (TypeError, ValueError):
return None
if d <= 0 or s <= 0 or rough <= 0:
return None
area = math.pi * d * d / 4.0
radius = d / 4.0
coefficient = 1.0 if unit_system.upper().startswith("SI") else 1.486
return coefficient / rough * area * radius ** (2.0 / 3.0) * math.sqrt(s)
def build_minor_system_capacity_table(link_table: pd.DataFrame, unit_system: str, flow_unit: str, length_unit: str, criteria: CalgaryCriteria) -> pd.DataFrame:
if link_table is None or link_table.empty:
return pd.DataFrame()
df = link_table.copy()
if "Model Type" in df:
df = df[df["Model Type"].astype(str).str.lower().eq("conduit")]
if "Shape" in df:
df = df[df["Shape"].astype(str).str.upper().eq("CIRCULAR")]
diam_col = next((c for c in df.columns if c.startswith("Diameter / Geom1") or c.startswith("Diameter (")), None)
flow_col = next((c for c in df.columns if c.startswith("Peak Flow (")), None)
length_col = next((c for c in df.columns if c.startswith("Length (")), None)
if not all([diam_col, flow_col]):
return pd.DataFrame()
rows = []
for _, r in df.iterrows():
slope = pd.to_numeric(r.get("Slope (ft/ft)", r.get("Slope", math.nan)), errors="coerce")
# The model table may not include slope. Calculate from offsets/inverts only when explicitly available.
cap = manning_full_capacity_circular(r.get(diam_col), slope, r.get("Manning n"), unit_system) if pd.notna(slope) else None
peak = pd.to_numeric(r.get(flow_col), errors="coerce")
modelled_depth_ratio = pd.to_numeric(r.get("Depth Ratio"), errors="coerce")
capacity_ratio = float(peak / cap) if cap and pd.notna(peak) else math.nan
if pd.notna(capacity_ratio):
if capacity_ratio >= criteria.conduit_capacity_warning_ratio:
status = "Warning - limited calculated capacity"
elif capacity_ratio >= criteria.conduit_capacity_review_ratio:
status = "Review calculated capacity"
else:
status = "Pass calculated capacity screening"
capacity_basis = "Manning full-flow capacity"
elif pd.notna(modelled_depth_ratio):
if modelled_depth_ratio >= criteria.conduit_capacity_warning_ratio:
status = "Warning - high modelled depth ratio"
elif modelled_depth_ratio >= criteria.conduit_capacity_review_ratio:
status = "Review modelled depth ratio"
else:
status = "Below depth-ratio screening threshold"
capacity_basis = "Full-flow capacity not calculated"
else:
status = "Not assessed - missing capacity and depth ratio"
capacity_basis = "Insufficient data"
rows.append({
"Segment": r.get("Link ID"), "From": r.get("From Node"), "To": r.get("To Node"),
f"Diameter ({length_unit})": r.get(diam_col), f"Length ({length_unit})": r.get(length_col) if length_col else None,
"Manning n": r.get("Manning n"), f"Routed Flow ({flow_unit})": peak,
f"Full-Flow Capacity ({flow_unit})": cap,
"Calculated Capacity Ratio": capacity_ratio if pd.notna(capacity_ratio) else None,
"Modelled Depth Ratio": modelled_depth_ratio if pd.notna(modelled_depth_ratio) else None,
f"Spare Capacity ({flow_unit})": (cap - peak) if cap and pd.notna(peak) else None,
"Assessment Basis": capacity_basis,
"Status": status,
})
return pd.DataFrame(rows)
def build_overland_compliance_table(overland: pd.DataFrame, criteria: CalgaryCriteria, flow_unit: str, length_unit: str, velocity_unit: str) -> pd.DataFrame:
if overland is None or overland.empty:
return pd.DataFrame()
flow_col = next((c for c in overland.columns if c.startswith("Peak Flow (")), None)
depth_col = next((c for c in overland.columns if c.startswith("Peak Depth (")), None)
vel_col = next((c for c in overland.columns if c.startswith("Peak Velocity (")), None)
rows = []
for _, r in overland.iterrows():
d = pd.to_numeric(r.get(depth_col), errors="coerce") if depth_col else math.nan
v = pd.to_numeric(r.get(vel_col), errors="coerce") if vel_col else math.nan
status, permitted = classify_overland(d, v, criteria.depth_velocity_curve) if pd.notna(d) and pd.notna(v) else ("Review - missing depth/velocity", None)
link_id = str(r.get("Link ID", ""))
special_limit = criteria.special_link_limits.get(link_id)
special_status = ""
if special_limit is not None and flow_col:
q = pd.to_numeric(r.get(flow_col), errors="coerce")
special_status = "Pass" if pd.notna(q) and q <= special_limit else "Exceeds project-specific flow limit"
rows.append({
"Segment": link_id, "From": r.get("From Node"), "To": r.get("To Node"),
f"Peak Flow ({flow_unit})": r.get(flow_col) if flow_col else None,
f"Peak Depth ({length_unit})": d, f"Peak Velocity ({velocity_unit})": v,
f"Permissible Depth ({length_unit})": permitted, "Depth-Velocity Status": status,
f"Special Flow Limit ({flow_unit})": special_limit, "Special Limit Status": special_status,
"Spill Active": "Yes" if ("spill" in link_id.lower() and pd.to_numeric(r.get(flow_col), errors="coerce") > 0) else "No",
})
return pd.DataFrame(rows)
def apply_storage_classification(storage_table: pd.DataFrame, criteria: CalgaryCriteria, length_unit: str) -> pd.DataFrame:
if storage_table is None or storage_table.empty:
return pd.DataFrame()
df = storage_table.copy()
depth_col = next((c for c in df.columns if c.startswith("Time-Series Peak Depth") or c.startswith("Reported Peak Depth")), None)
max_col = next((c for c in df.columns if c.startswith("Maximum Depth")), None)
classes, statuses, margins = [], [], []
for _, r in df.iterrows():
sid = str(r.get("Storage ID", ""))
cls = criteria.storage_classification.get(sid)
if not cls:
s = sid.lower()
if "storage" in s:
cls = "Street trap low / surface storage"
elif s.startswith("cb"):
cls = "Catchbasin ponding storage"
elif s.startswith("sub"):
cls = "Private-site / routing storage"
else:
cls = "Unclassified storage"
peak = pd.to_numeric(r.get(depth_col), errors="coerce") if depth_col else math.nan
maxd = pd.to_numeric(r.get(max_col), errors="coerce") if max_col else math.nan
margin = maxd - peak if pd.notna(maxd) and pd.notna(peak) else math.nan
ratio = peak / maxd if pd.notna(maxd) and maxd > 0 and pd.notna(peak) else math.nan
if cls.startswith("Street trap"):
# Trap-low criterion (0.5 m ponding, Alberta Environment 1999) is an
# established register entry, so a screening verdict is supportable.
if pd.isna(peak): status = "Not assessed - missing peak depth"
elif peak > criteria.trap_low_max_depth_m: status = "Exceeds trap-low depth criterion"
elif peak >= 0.95 * criteria.trap_low_max_depth_m: status = "Screens below trap-low criterion - limited margin"
else: status = "Screens below trap-low depth criterion"
else:
# No facility classification, design HWL, required freeboard, or
# release/spill criterion is available from the model alone, so a
# Pass verdict is unsupportable here (deterministic rule): report
# the modelled utilisation and return Not assessed for compliance.
if pd.isna(ratio): status = "Not assessed - unclassified or incomplete"
elif ratio >= 1.0: status = "Exceeds modelled depth - flag for review"
elif ratio >= 0.90: status = "Near modelled capacity - review"
else: status = "Within modelled depth - compliance Not assessed (criteria not established)"
classes.append(cls); statuses.append(status); margins.append(margin)
df.insert(1, "Calgary Storage Classification", classes)
df[f"Modelled Depth Margin ({length_unit})"] = margins # model quantity, NOT regulatory freeboard
df["Calgary Status"] = statuses
return df
def criteria_register(criteria: CalgaryCriteria) -> pd.DataFrame:
rows = [
("CAL-GEN-001", "Source manual", criteria.source_manual, criteria.manual_status, "Verify current City amendments"),
("CAL-MIN-001", "Minor-system release rate", criteria.minor_release_rate_lps_ha, "Project-specific", "L/s/ha"),
("CAL-MAJ-001", "Street depth-velocity envelope", json.dumps(criteria.depth_velocity_curve), "Calgary/Alberta reference curve", "m/s vs m"),
("CAL-TRL-001", "Trap-low maximum ponding depth", criteria.trap_low_max_depth_m, "Configurable default", "m"),
("CAL-TRL-002", "Entrance grade margin", criteria.entrance_grade_margin_m, "Configurable default", "m"),
("CAL-MIN-002", "Conduit capacity review ratio", criteria.conduit_capacity_review_ratio, "Screening", "fraction"),
("CAL-MIN-003", "Conduit capacity warning ratio", criteria.conduit_capacity_warning_ratio, "Screening", "fraction"),
("CAL-QA-001", "Continuity review threshold", criteria.continuity_review_pct, "Screening", "%"),
("CAL-QA-002", "Continuity warning threshold", criteria.continuity_warning_pct, "Screening", "%"),
]
return pd.DataFrame(rows, columns=["Rule ID", "Criterion", "Value", "Rule Status", "Units / Note"])
def build_llm_report_context(*, metadata: dict[str, Any], criteria: CalgaryCriteria, findings: list[str], tables: dict[str, pd.DataFrame]) -> dict[str, Any]:
compact_tables = {}
for name, df in tables.items():
if df is not None and not df.empty:
compact_tables[name] = df.head(30).where(pd.notna(df), None).to_dict(orient="records")
return {
"task": "Draft a City of Calgary stormwater management report narrative for professional review.",
"metadata": metadata,
"approved_criteria": asdict(criteria),
"deterministic_findings": findings,
"verified_tables": compact_tables,
"interpretation_controls": {
"minor_system_primary_metric": "modelled depth ratio unless a calculated full-flow capacity is present",
"full_flow_capacity_available": bool("minor_system" in compact_tables and any(r.get("Full-Flow Capacity (m³/s)") is not None or r.get("Full-Flow Capacity (cfs)") is not None for r in compact_tables.get("minor_system", []))),
"generic_storage_freeboard_screening_allowed": False,
"storm_name_verified": False,
"model_input_output_overall_status": next((r.get("Status") for r in compact_tables.get("swmr_checklist", []) if r.get("Item") == "SWMR-17"), "Not assessed"),
"allowed_conclusions": {"adequate": False, "compliant": False, "safe": False, "effective": False, "approved": False},
},
"preferred_wording": {
"minor_system": "Use 'modelled depth ratio' when full-flow capacity is unavailable.",
"storage": "Use 'remaining depth margin' for maximum ponding-depth criteria.",
"event": "State that the design-event name is inferred when not independently verified.",
"outfalls": "Report modelled flows and state that downstream capacity requires confirmation.",
"model_documentation": "Distinguish digital package completeness from report appendix, schematic, drawing reconciliation, and authenticated-file completeness.",
},
"constraints": [
"Do not change or recalculate numerical results.",
"Do not claim City approval or professional certification.",
"Identify inferred, project-specific, historical, and unverified criteria.",
"Use 'not provided' where project facts are missing.",
"Distinguish pass, warning, information gap, and professional judgement.",
"Do not use adequate, compliant, safe, effective, acceptable, or approved unless deterministic verified criteria authorize that wording.",
"Use the Calgary SWMR completeness register and state all missing or partial mandatory items.",
"Never call modelled depth ratio capacity used unless calculated full-flow capacity is present.",
"Do not apply generic junction freeboard screening to storage nodes.",
],
}
CALGARY_LLM_SYSTEM_PROMPT = """You are a Calgary stormwater report drafting assistant.
Use only the verified structured data, deterministic findings, approved Project Criteria Register, and Calgary SWMR Completeness Register supplied by the application.
Never calculate or modify hydraulic values. Never invent project facts. Never describe a screening threshold as a current City requirement unless its rule status is verified.
ENGINEERING INTERPRETATION RULES
1. Do not describe the model, system, infrastructure, design, or results as adequate, compliant, safe, effective, acceptable, or approved unless the deterministic compliance engine explicitly returns that conclusion for the applicable verified criterion.
2. Do not apply generic junction freeboard or depth-ratio criteria to storage nodes. Use the Calgary storage classification, adopted maximum ponding depth, remaining depth margin, utilization, spill elevation, and Calgary status fields.
3. For active spill links, report the modeled peak flow, depth, velocity, and deterministic depth-velocity screening result. Also require confirmation of grading continuity, route containment, public safety, erosion, downstream impacts, and receiving-system capacity.
4. Do not state that outfalls manage flow effectively or have adequate receiving capacity. Report only modeled flow, depth, flooding, boundary type, and verified downstream criteria.
5. When full-flow pipe capacity is unavailable, do not describe conduit depth ratio as full capacity utilization, spare capacity, or hydraulic capacity. Call it modeled depth ratio.
6. Prioritize approved project-specific criteria, applicable MDP/SMDP/pond-report/prior-SWMR criteria, verified current City requirements, historical manual criteria, then user-defined screening thresholds.
7. Clearly distinguish deterministic results, screening results, project-specific requirements, historical reference criteria, inferred information, missing information, and professional judgement.
8. Do not invent project facts, capacities, material properties, downstream conditions, grading information, or compliance conclusions.
9. Where criteria are missing or unverified, use wording such as 'requires confirmation', 'screening only', 'not established from the available data', or 'cannot be concluded from the model results alone'.
10. The generated text is an engineering draft for professional review and must not be presented as final certification or municipal approval.
CALGARY SWMR CHECKLIST RULES
Use the completeness register when drafting. Do not omit a checklist subject merely because data are unavailable; state what is missing and what must be provided. For each material conclusion, identify supporting report tables, figures, model results, approved criteria, or source documents. Do not state that the SWMR is complete when mandatory items are Missing, Partially complete, or Require professional confirmation. Highlight unresolved issues, departures, missing project data, drawing gaps, and unverified downstream conditions in the executive summary and Outstanding Information and Actions section. Distinguish model-derived information from drawing-, survey-, project-, and professionally-confirmed information.
Draft clear consultant-quality narrative for professional review. Do not certify compliance or approval.
"""
|