Emergency Isolation Scripting for Water Main Breaks
When a distribution main ruptures, the clock that matters is measured in minutes: every minute of uncontrolled discharge scours the trench, undermines pavement, drops system pressure toward the backflow-contamination threshold, and floods property. The operator’s decision — which valves close, and in what order, to stop the flow while stranding the fewest customers — cannot be reconstructed from a paper valve book under that pressure. Doing it by eye also tends to over-isolate, shutting a whole zone when four valves would have bounded the break, needlessly de-watering hospitals and dialysis clinics that a tighter isolation would have spared. The correct isolation set is a graph computation: from the broken edge, walk outward to the nearest operable valves on every branch, and the span they enclose is the minimal cut. This page turns that computation into a routine that returns an answer in seconds.
The barrier model this routine consumes is the one established in Valve & Isolator Mapping Strategies: each valve is a node carrying a constrained OPERATIONAL_STATUS, and the “nearest operable valve” search is a bounded application of the upstream and downstream tracing algorithms that underpin the whole Topology & Tracing Workflows framework. The affected-customer enumeration it produces feeds directly into impact analysis and affected-customer tracing, which turns the isolated span into notifications and dispatch priority. The routine is only as good as the valve states it reads, which is why an accurate, live barrier model is a hard prerequisite rather than a nicety.
Environment Prerequisites
Confirm every item before running an isolation script against production data. A stale valve status or an unvalidated topology will produce a confident, wrong isolation set — the most dangerous failure mode in this workflow.
- ArcGIS Pro 3.2+ with a Standard or Advanced license, so the utility network trace and terminal configurations behind the pressure-zone model are available.
- Python 3.11 in a cloned ArcGIS conda environment, with
networkx>=3.0for the isolation search andarcpyimportable for extracting the graph snapshot. - A validated water-distribution topology with no outstanding dirty areas. An isolation set computed over an invalid graph can miss a valve or a whole branch; validate before you trust a cut.
- Operable-valve attribution. Each valve node must carry both
OPERATIONAL_STATUSin{OPEN, CLOSED, PARTIALLY_OPEN, UNKNOWN}and anis_operableflag reflecting the latest field-inspection record. A valve that is buried, seized, or unlocatable is not a boundary and must be excluded from the search, forcing the walk outward to the next operable device. - Main diameter and directionality attributes on every edge, so the shutoff sequencer can close larger and downstream mains first to limit surge.
- A critical-customer register joined to service points — hospitals, dialysis centers, schools, and other facilities with statutory water-continuity obligations — so the routine can flag them the moment they fall inside an isolated span.
- A current barrier snapshot synchronized with field and SCADA state, per Valve & Isolator Mapping Strategies; an isolation computed on yesterday’s valve positions is not an emergency tool.
Schema-Aware Validation Protocol — Run Before the Isolation
An emergency script that fails at the console during a break is worse than useless. Work this ordered checklist during commissioning and as a fast pre-flight, so the routine degrades safely rather than returning a wrong cut. The first item is the most frequent culprit.
- Confirm the break resolves to exactly one edge. The reported location — a coordinate, an asset ID, or a nearest-address geocode — must snap to a single main edge whose endpoints both exist in the snapshot. A break that resolves to a node or to zero edges means the search has no seed; fail loudly rather than guessing.
- Verify operability, not just status. A valve can read
OPENand still be unusable because it is seized or paved over. The search must key onis_operable, and any valve withUNKNOWNstatus or a failed last inspection must be treated as not a boundary, so the walk continues to the next dependable device. Skipping this check is what produces an isolation that cannot actually be executed. - Check for disconnected branches around the break. A sub-tolerance gap near the rupture can hide a branch from the traversal, under-stating both the valve set and the affected customers. Confirm every edge incident to the break’s endpoints is connected in the graph before trusting the cut.
- Validate diameter and direction attributes on candidate edges. The shutoff order depends on them; a null diameter or a missing flow direction collapses the sequencer to arbitrary order, which risks a surge transient. Assert these fields are populated on the mains inside the search radius.
- Reconcile the critical-customer join. Confirm every service point carries a resolvable critical-facility flag. A silent join failure drops a hospital from the affected list — a notification and compliance failure, not merely a data gap.
Minimal Reproducible Implementation
The routine below is the centerpiece. Given a directed network graph and a broken main edge, it walks outward from both endpoints to the nearest operable valves, forming the minimal isolation set; derives a shutoff order that closes larger and downstream mains first; and enumerates the affected service points, flagging critical facilities. It raises on an unresolvable break and returns a structured dispatch packet the crew and outage systems can consume directly.
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Iterable
import networkx as nx
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
TRAVERSABLE = {"OPEN", "PARTIALLY_OPEN"} # a closed valve already bounds the flow
@dataclass
class IsolationPlan:
"""Structured, dispatchable result of an emergency isolation computation."""
broken_main: tuple[str, str]
isolation_valves: list[str] = field(default_factory=list)
shutoff_order: list[str] = field(default_factory=list)
isolated_nodes: list[str] = field(default_factory=list)
affected_service_points: list[str] = field(default_factory=list)
affected_critical: list[str] = field(default_factory=list)
def as_dict(self) -> dict:
return {
"broken_main": self.broken_main,
"isolation_valves": self.isolation_valves,
"shutoff_order": self.shutoff_order,
"customers_out": len(self.affected_service_points),
"affected_critical": self.affected_critical,
"ok": bool(self.isolation_valves),
}
def _nearest_operable_valves(graph: nx.Graph, seed: str) -> set[str]:
"""Walk outward from a seed node, stopping at the first operable valve
on each branch. Those valves form the boundary of the isolation cut."""
boundary: set[str] = set()
visited: set[str] = {seed}
queue: list[str] = [seed]
while queue:
node = queue.pop(0)
for nbr in graph.neighbors(node):
if nbr in visited:
continue
data = graph.nodes.get(nbr, {})
is_valve = data.get("kind") == "valve"
if is_valve and data.get("is_operable") and data.get("status") in TRAVERSABLE:
boundary.add(nbr) # this valve can bound the break
continue # do not traverse past a usable valve
visited.add(nbr)
queue.append(nbr)
return boundary
def compute_isolation(
graph: nx.Graph,
broken_main: tuple[str, str],
critical_ids: Iterable[str] = (),
) -> IsolationPlan:
"""Compute the minimal valve-isolation set for a broken main.
Args:
graph: Undirected view of the water network; valve nodes carry
'kind'='valve', 'status', 'is_operable', and service points carry
'kind'='service'. Edges carry 'diameter_mm' and 'downstream_rank'.
broken_main: The (u, v) edge that has ruptured.
critical_ids: Service-point ids with statutory water-continuity needs.
Returns:
An IsolationPlan with the valve set, shutoff order, isolated span,
and affected (including critical) customers.
"""
u, v = broken_main
if not graph.has_edge(u, v):
raise KeyError(f"broken main {broken_main!r} not found in network snapshot")
plan = IsolationPlan(broken_main=broken_main)
critical = set(critical_ids)
# 1. Nearest operable valves outward from BOTH endpoints form the cut.
valves = _nearest_operable_valves(graph, u) | _nearest_operable_valves(graph, v)
if not valves:
raise RuntimeError(
f"no operable valve bounds {broken_main!r}; escalate to zone shutdown"
)
plan.isolation_valves = sorted(valves)
# 2. Shutoff order: close larger-diameter, more-downstream mains first to
# limit surge and backflow. Sort by (-diameter, -downstream_rank).
def valve_key(valve: str) -> tuple[float, float]:
widths, ranks = [], []
for nbr in graph.neighbors(valve):
edge = graph.edges.get((valve, nbr), {})
widths.append(edge.get("diameter_mm", 0) or 0)
ranks.append(edge.get("downstream_rank", 0) or 0)
return (-max(widths, default=0), -max(ranks, default=0))
plan.shutoff_order = sorted(valves, key=valve_key)
# 3. Isolated span: nodes reachable from the break without crossing a
# boundary valve. Copy the graph and remove the cut valves, then take
# the component that still contains the broken main.
cut = graph.copy()
cut.remove_nodes_from(valves)
isolated: set[str] = set()
for endpoint in (u, v):
if endpoint in cut:
isolated |= nx.node_connected_component(cut, endpoint)
plan.isolated_nodes = sorted(isolated)
# 4. Affected customers, with critical facilities flagged.
service_points = [
n for n in isolated if graph.nodes.get(n, {}).get("kind") == "service"
]
plan.affected_service_points = sorted(service_points)
plan.affected_critical = sorted(sp for sp in service_points if sp in critical)
logging.info(
"Break %s: %d isolation valves, %d customers out (%d critical)",
broken_main, len(valves), len(service_points), len(plan.affected_critical),
)
return plan
if __name__ == "__main__":
G = nx.Graph()
G.add_node("V1", kind="valve", status="OPEN", is_operable=True)
G.add_node("V2", kind="valve", status="OPEN", is_operable=True)
G.add_node("J1", kind="junction")
G.add_node("SVC1", kind="service")
G.add_node("HOSP", kind="service")
G.add_edges_from([
("V1", "J1", {"diameter_mm": 300, "downstream_rank": 2}),
("J1", "J2", {"diameter_mm": 200, "downstream_rank": 3}), # broken main
("J2", "V2", {"diameter_mm": 200, "downstream_rank": 4}),
("J1", "SVC1", {"diameter_mm": 150, "downstream_rank": 5}),
("J2", "HOSP", {"diameter_mm": 150, "downstream_rank": 6}),
])
result = compute_isolation(G, ("J1", "J2"), critical_ids={"HOSP"})
print(result.as_dict())
Running the example resolves the break on J1–J2, walks out to valves V1 and V2 as the minimal cut, orders V1 (300 mm) before V2 (200 mm), and reports two customers out with HOSP flagged as critical. The key modeling decision is that the search stops at the first operable valve on each branch — it never keeps walking to a farther valve — which is exactly what makes the cut minimal and spares every customer outside the enclosed span.
The shutoff order is not cosmetic. Closing a large downstream valve before its upstream feed lets the enclosed volume drain toward the break rather than slamming shut against full system pressure, which limits the water-hammer transient that can rupture a second, weaker joint. Ordering by descending diameter and downstream rank encodes that hydraulic rule so the crew receives a sequence, not just a set. When compute_isolation raises because no operable valve bounds the break, that is itself the answer: the routine has proven the isolation cannot be made at valve granularity, and the event must escalate to a zone shutdown — a decision far better surfaced in milliseconds than discovered at the third seized valve.
Production Deployment Pattern
An isolation script that lives in one engineer’s notebook helps no one at 2 a.m. Promote it into an operational control as follows.
- Extract the graph from a validated snapshot on a schedule. Build the
networkxview from the utility network witharcpy.dacursors against a reconciled, unversioned snapshot, refreshed frequently and always after topology validation, so the emergency call reads a graph that is both current and correct. - Overlay live valve state at call time. Merge the latest field and SCADA
OPERATIONAL_STATUSandis_operablevalues onto the cached graph immediately before computing, using the synchronization pattern from Valve & Isolator Mapping Strategies. A break-time isolation must never run on stale valve positions. - Expose the routine behind a thin service endpoint. Wrap
compute_isolationin a REST or internal API that accepts a break location and returns theIsolationPlanas JSON, so a dispatcher’s console or a mobile app renders the valve list, shutoff order, and critical-customer flags in seconds. - Hand the affected set to impact automation. Post
affected_service_pointsandaffected_criticalto impact analysis and affected-customer tracing so notification and dispatch draw from the same enclosed span the crew is isolating, keeping the map, the hotline, and the field consistent. - Apply retry with backoff and a safe fallback. Wrap the snapshot read in a bounded exponential-backoff retry; if the live overlay is unavailable, fail toward the conservative wider cut and label it explicitly, never toward a narrower, unverified one.
- Persist an audit record per invocation. Log the break edge, the computed valve set and order, the affected and critical customers, and the snapshot timestamp to an append-only store. That record supports the after-action review and the reliability reporting a water main break invariably triggers.
Conclusion
Emergency isolation is a graph problem wearing a stopwatch, and treating it as one replaces a frantic search through a valve book with a computed, minimal cut delivered in seconds. The routine here finds the nearest operable valves bounding a break, orders their closure to blunt the surge transient, and names every customer — critical facilities first — who loses water, all as structured output a dispatcher and the outage systems can act on immediately. Wired to a validated snapshot with live valve state overlaid at call time, it turns the network model into an operational instrument that shrinks both restoration time and the count of needlessly de-watered customers. The next refinement is to fold in real-time hydraulic pressure so the shutoff order adapts to the actual gradient at the moment of the break.
Related
- Up to the parent topic: Valve & Isolator Mapping Strategies
- Up to the section: Topology & Tracing Workflows
- Syncing SCADA Barriers to Valve State in Real Time
- Mapping Underground Cable Isolators with Spatial Joins
- Upstream & Downstream Tracing Algorithms
For authoritative reference, consult the NetworkX connectivity documentation and the AWWA standards program.