Source code for wntr.extensions.design_verification.runner

"""Run hydraulic simulations and verify design constraints."""

from __future__ import annotations

from dataclasses import asdict
from pathlib import Path
import tempfile
from typing import Any

from wntr.network import WaterNetworkModel
from wntr.sim import EpanetSimulator, WNTRSimulator

from .constraints import (
    evaluate_maximum_velocity,
    evaluate_minimum_pressure,
)
from .exceptions import IncompleteHydraulicResultsError
from .design import apply_pipe_design
from .models import (
    HydraulicScenario,
    PipeDesign,
    VerificationResult,
)
from .pumps import audit_head_pump_curves
from .scenarios import apply_hydraulic_scenario


def _validate_simulator_name(value: object) -> str:
    """Validate the WNTR hydraulic-simulator name."""
    assert isinstance(value, str)

    simulator_name = value.strip()

    assert simulator_name in (
        "WNTRSimulator",
        "EpanetSimulator",
    )

    return simulator_name


def _simulation_error_code(results: Any) -> int | None:
    """Extract a numeric simulator error code when available."""
    value = getattr(
        results,
        "error_code",
        None,
    )

    if value is None:
        return None

    value = getattr(
        value,
        "value",
        value,
    )

    try:
        return int(value)
    except (TypeError, ValueError):
        return None


[docs] def run_design_verification( wn: WaterNetworkModel, scenario: HydraulicScenario, *, design: PipeDesign | None = None, simulator: str = "WNTRSimulator", minimum_pressure_m: float = 15.0, maximum_velocity_mps: float = 2.5, required_compliance_pct: float = 100.0, pressure_tolerance_m: float = 0.0, velocity_tolerance_mps: float = 0.0, ) -> tuple[ VerificationResult, Any, dict[str, object], ]: """Run one hydraulic design-verification assessment. The original network is protected from modification. Pipe-diameter changes and hydraulic simulation-option changes are applied to independent copies. Parameters ---------- wn : WaterNetworkModel Original WNTR water-distribution network model. scenario : HydraulicScenario WNTR time and hydraulic simulation options to assess. design : PipeDesign or None, optional Optional pipe-diameter design. If omitted, the unchanged baseline network is assessed. simulator : str, optional Hydraulic simulation engine. Accepted values are ``WNTRSimulator`` and ``EpanetSimulator``. minimum_pressure_m : float, optional Minimum acceptable junction pressure in metres. maximum_velocity_mps : float, optional Maximum acceptable absolute pipe velocity in metres per second. required_compliance_pct : float, optional Required percentage of assessed node-time and pipe-time values satisfying each constraint. pressure_tolerance_m : float, optional Non-negative numerical tolerance applied below the minimum pressure limit. The default of zero preserves strict comparison behaviour. velocity_tolerance_mps : float, optional Non-negative numerical tolerance applied above the maximum absolute velocity limit. The default of zero preserves strict comparison behaviour. Returns ------- verification : VerificationResult Structured pressure, velocity, pump and overall feasibility result. hydraulic_results : Any Raw WNTR hydraulic simulation results. audit : dict Record of the applied design, scenario, simulator and assessed components. """ assert isinstance(wn, WaterNetworkModel) assert isinstance(scenario, HydraulicScenario) assert design is None or isinstance(design, PipeDesign) simulator_name = _validate_simulator_name(simulator) if design is None: design_wn = wn design_audit = None design_name = None else: design_wn, design_audit = apply_pipe_design( wn=wn, design=design, ) design_name = design.name.strip() scenario_wn, scenario_audit = apply_hydraulic_scenario( wn=design_wn, scenario=scenario, ) if simulator_name == "WNTRSimulator": hydraulic_simulator = WNTRSimulator(scenario_wn) hydraulic_results = hydraulic_simulator.run_sim( convergence_error=True, ) else: with tempfile.TemporaryDirectory( prefix="wntr_design_verification_" ) as temporary_directory: file_prefix = str( Path(temporary_directory) / "verification" ) hydraulic_simulator = EpanetSimulator(scenario_wn) hydraulic_results = hydraulic_simulator.run_sim( file_prefix=file_prefix, version=2.2, convergence_error=True, ) try: pressure = hydraulic_results.node["pressure"] except (AttributeError, KeyError) as error: raise IncompleteHydraulicResultsError( "The simulation did not return node-pressure results." ) from error try: velocity = hydraulic_results.link["velocity"] except (AttributeError, KeyError) as error: raise IncompleteHydraulicResultsError( "The simulation did not return link-velocity results." ) from error try: flowrate = hydraulic_results.link["flowrate"] except (AttributeError, KeyError) as error: raise IncompleteHydraulicResultsError( "The simulation did not return link-flowrate results." ) from error status = hydraulic_results.link.get("status") setting = hydraulic_results.link.get("setting") junction_pressure = pressure.loc[ :, scenario_wn.junction_name_list, ] pipe_velocity = velocity.loc[ :, scenario_wn.pipe_name_list, ] pressure_result = evaluate_minimum_pressure( pressure=junction_pressure, minimum_pressure_m=minimum_pressure_m, required_compliance_pct=required_compliance_pct, pressure_tolerance_m=pressure_tolerance_m, ) velocity_result = evaluate_maximum_velocity( velocity=pipe_velocity, maximum_velocity_mps=maximum_velocity_mps, required_compliance_pct=required_compliance_pct, velocity_tolerance_mps=velocity_tolerance_mps, ) pump_result = audit_head_pump_curves( network=scenario_wn, flowrate=flowrate, status=status, setting=setting, ) verification = VerificationResult( design_name=design_name, scenario_name=scenario.name.strip(), simulator_name=simulator_name, pressure_result=pressure_result, velocity_result=velocity_result, pump_result=pump_result, feasible=bool( pressure_result.feasible and velocity_result.feasible and pump_result.all_pumps_passed ), simulation_error_code=_simulation_error_code( hydraulic_results ), ) audit: dict[str, object] = { "design": design_audit, "scenario": scenario_audit, "pump_curve_audit": asdict(pump_result), "simulator_name": simulator_name, "minimum_pressure_m": float(minimum_pressure_m), "maximum_velocity_mps": float(maximum_velocity_mps), "required_compliance_pct": float(required_compliance_pct), "pressure_tolerance_m": float(pressure_tolerance_m), "velocity_tolerance_mps": float(velocity_tolerance_mps), "expected_junction_count": len( scenario_wn.junction_name_list ), "assessed_junction_count": len( junction_pressure.columns ), "junctions_assessed": list( junction_pressure.columns ), "expected_pipe_count": len( scenario_wn.pipe_name_list ), "assessed_pipe_count": len( pipe_velocity.columns ), "pipes_assessed": list( pipe_velocity.columns ), } return verification, hydraulic_results, audit