Impact Analysis & Affected-Customer Tracing for Utility Outages

The Failure Mode: A Confident, Precisely Wrong Customer Count

The most damaging outage-management defect is not a system that crashes — it is one that answers quickly and wrongly. When a feeder breaker trips or a transmission main loses pressure, the control room needs one number within seconds: how many customers are out, and which of them are critical. If that number is derived by eyeballing a map, by counting meters inside a hand-drawn polygon, or by a trace that runs over a stale or invalid graph, the answer arrives with the same confident formatting whether it is right or off by a thousand premises. A count that is too low sends too few crews and understates the event to regulators; a count that is too high triggers mutual-aid callouts and public alerts for customers who never lost service. Both failures are expensive, and both erode the trust that the outage-management system depends on to be believed during the next storm.

The engineering fix is to stop treating affected-customer estimation as a cartographic or reporting exercise and to treat it as a deterministic computation: the outage is a downstream trace from the faulted asset over a validated network graph, evaluated against a pinned snapshot of current device states, and the impact set it returns is the single source of truth that every consumer — dispatch, notification, regulatory reporting — reads from. This discipline sits directly beneath the parent outage routing and impact automation reference and inherits its correctness from the upstream and downstream tracing algorithms and the valve barrier logic documented under topology and tracing workflows. This guide targets utility engineers, GIS technicians, and Python automation builders, and it covers the downstream-trace model, the impact set as a shared record, device-state overlays and snapshots, service-point aggregation, and critical-facility roll-up through the asset hierarchy.

From faulted asset and device-state snapshot to a single shared impact set with two aggregations A faulted asset and a pinned topology snapshot with current device states feed a downstream trace. The trace produces one shared impact set, which fans into two aggregations: service-point roll-up yielding an affected-customer count, and critical-facility roll-up yielding statutory flags. Both aggregations feed a single structured impact report consumed by dispatch, notification, and regulatory reporting. INPUT Faulted asset INPUT Snapshot + device states COMPUTE Downstream trace SINGLE SOURCE Impact set (shared) Service-point aggregation → customer count Critical-facility roll-up → statutory flags Impact report

Prerequisite Checklist

Confirm every item before running impact analysis against production data. The topology-state and hierarchy checks are the ones most often skipped, and they are the ones that let a trace return a clean-looking but wrong count.

Core Data Model: The Downstream Trace and the Impact Set

Impact analysis is a directed-graph computation over the same network that every other trace walks: linear assets — cables, mains, laterals — are edges, and devices, junctions, and service points are nodes. An outage perturbs that graph by changing the state of one or more elements, and the impact set is defined precisely as the set of load-serving nodes that become unreachable from any energised source as a result. Because sources sit at the head of the directed graph and loads at the tail, the impact set is exactly the collection of descendants of the faulted asset that are not restored by an alternate path. Framed that way, affected-customer analysis is not a bespoke algorithm; it is a reachability query, and its correctness is inherited entirely from the graph it runs on. An unsnapped dangling edge splits a pressure zone or a feeder into two silent subnetworks, so the descendants query returns a far-too-small set — which is why rigorous topology validation is the non-negotiable precondition, not an optional cleanup step.

Three structures carry the engineering weight, and keeping them distinct is what separates a defensible impact analysis from a plausible guess:

  • The topology snapshot. The network changes continuously during an event as crews open and close devices, so every impact trace must run against a consistent read of the graph taken at a known moment. Reads for a given evaluation take the snapshot; device operations are applied on top as deltas; the same fault evaluated twice against the same snapshot yields the same impact set. That reproducibility is what makes the result auditable rather than merely fast.
  • The device-state overlay. A snapshot captures geometry and connectivity, but the state of each operable device — open or closed — is a fast-moving overlay layered onto it. A normally closed switch that a crew has opened truncates the downstream trace at that node; a normally open tie that has been closed extends it onto a parallel branch. The overlay is the difference between the network as designed and the network as it stands right now, and applying it correctly is the whole game.
  • The impact set as a single source of truth. The set of affected service points, devices, and critical facilities is computed once and read by every consumer. Dispatch scores its queue from it, notification matches customers from it, and regulatory reporting counts from it. When all three read the same set, the map, the dispatch board, and the customer hotline cannot tell three different stories about the same event.

Expressing this at the algorithm level, vendor-neutral, makes the model explicit. A production run may execute against the ArcGIS Utility Network trace API, but the computation underneath any trace engine is a state-aware descendants query:

import networkx as nx


def impact_set(graph: nx.DiGraph, faulted_node: str, sources: set[str]) -> dict:
    """Compute the downstream impact set for a fault at `faulted_node`.

    Nodes carrying an 'open' device state truncate the trace. Returns the
    reachable-but-unsourced load nodes as a structured, reproducible result.
    """
    if faulted_node not in graph:
        raise KeyError(f"faulted node {faulted_node!r} absent from snapshot")

    # Build the energised subgraph: drop edges into any OPEN device so the
    # trace honours current device states, not the design configuration.
    energised = graph.copy()
    open_devices = [
        n for n, d in graph.nodes(data=True) if d.get("device_state") == "OPEN"
    ]
    energised.remove_nodes_from(open_devices)

    # Descendants of the fault that can no longer be reached from any source.
    downstream = nx.descendants(energised, faulted_node) | {faulted_node}
    still_sourced = set()
    for src in sources & set(energised.nodes):
        still_sourced |= nx.descendants(energised, src) | {src}
    affected = downstream - still_sourced

    service_points = sorted(
        n for n in affected if graph.nodes[n].get("kind") == "service"
    )
    return {
        "faulted_node": faulted_node,
        "affected_nodes": sorted(affected),
        "service_points_out": service_points,
        "customers_out": len(service_points),
    }

Removing open-device nodes before the descendants query is the mechanism that makes the trace state-aware: an opened switch or a closed valve is not a special case handled by branching logic, it is simply absent from the energised graph, so reachability naturally stops there. The still_sourced subtraction is what prevents a load fed by an alternate path — a closed tie onto a healthy feeder — from being counted as out.

Step-by-Step Implementation

Follow this sequence to take a fault event from a raw asset identifier to a committed impact report.

  1. Pin the snapshot. Capture the network as it stood at the fault timestamp — a versioned read or a snapshot geodatabase — so the trace is reproducible and cannot drift as later device operations arrive.
  2. Apply the device-state overlay. Join the latest open/closed states onto the snapshot as deltas. States sourced from SCADA arrive continuously; states from field radio arrive in bursts. Stamp each with an as-of time so a stale state cannot silently override a newer one.
  3. Run the downstream trace. Execute the state-aware descendants query from the faulted asset, subtracting anything still reachable from a source, to produce the raw affected-node set.
  4. Aggregate service points into a customer count. Roll reachable service points up to meters and premises. This aggregation, worked end to end in computing affected-customer counts with NetworkX, is what turns a set of graph nodes into the single number the control room needs.
  5. Roll up critical facilities. Walk the asset hierarchy from each affected service point to detect hospitals, water plants, and telecom hubs, and flag them with their statutory notification windows.
  6. Emit and commit the impact report. Serialise a structured record — snapshot id, device-state as-of time, customer count, critical-facility flags — and persist it immutably so reporting can reconstruct exactly what the system believed at that moment.

The device-state overlay in step 2 is where most miscounts originate, so it deserves an explicit, testable function rather than an inline join. The overlay must be idempotent and time-ordered: applying the same batch twice must not change the result, and a newer state must always win over an older one for the same device.

import networkx as nx
from datetime import datetime


def apply_device_states(graph: nx.DiGraph, states: list[dict]) -> nx.DiGraph:
    """Overlay time-stamped device states onto a topology snapshot.

    `states` items carry 'node', 'state' ('OPEN'/'CLOSED'), and 'as_of' (ISO).
    The newest state per device wins; older or malformed rows are skipped.
    """
    latest: dict[str, dict] = {}
    for row in states:
        node = row.get("node")
        if node is None or node not in graph:
            continue
        try:
            stamp = datetime.fromisoformat(row["as_of"])
        except (KeyError, ValueError):
            continue  # unparseable timestamp -> ignore rather than trust
        if node not in latest or stamp > latest[node]["stamp"]:
            latest[node] = {"state": row["state"], "stamp": stamp}

    overlaid = graph.copy()
    for node, info in latest.items():
        overlaid.nodes[node]["device_state"] = info["state"]
        overlaid.nodes[node]["state_as_of"] = info["stamp"].isoformat()
    return overlaid

The roll-up in steps 4 and 5 depends on a clean asset hierarchy, because a service point does not carry “this is a hospital on circuit 12” in its own geometry — it inherits that from its containment and structural-attachment associations. The hierarchy lets a fault on a feeder answer “how many customers, how many critical facilities” by aggregating along the parent-child edges rather than re-deriving from raw geometry on every trace. The following diagram shows how a single affected service point rolls up through the hierarchy to both a circuit-level count and a critical-facility flag.

Rolling affected service points up the asset hierarchy to counts and critical-facility flags A tree rooted at a substation branches to two feeders, then to transformers, then to service points. One branch carries an ordinary premise that rolls up into the affected-customer count. A parallel branch carries a hospital service point that rolls up both into the count and into a separate critical-facility flag with a statutory notification window. The hierarchy edges are the containment and structural-attachment associations that let the count aggregate without re-deriving from geometry. SUBSTATION circuit roll-up root Feeder A Feeder B Transformer Transformer Premise ordinary customer Hospital critical facility STATUTORY critical-facility flag + window containment & structural-attachment associations

With the hierarchy in place, the critical-facility roll-up is a filter over the impact set followed by a lookup against the facility register. The following consolidates a full impact analysis: overlay device states, trace, count service points, and flag critical facilities into one structured report.

import networkx as nx


CRITICAL_TYPES = {"HOSPITAL", "WATER_PLANT", "TELECOM_HUB", "LIFE_SUPPORT"}


def analyze_impact(graph: nx.DiGraph, faulted_node: str,
                   sources: set[str]) -> dict:
    """Produce a full impact report: customer count plus critical flags.

    Assumes device states are already overlaid on `graph`. Service nodes carry
    'kind' == 'service' and an optional 'facility_type' from the register.
    """
    result = impact_set(graph, faulted_node, sources)
    critical = []
    for node in result["service_points_out"]:
        facility = graph.nodes[node].get("facility_type", "NONE")
        if facility in CRITICAL_TYPES:
            critical.append({
                "service_point": node,
                "facility_type": facility,
                "jurisdiction": graph.nodes[node].get("jurisdiction"),
            })
    return {
        **result,
        "critical_facilities": critical,
        "critical_count": len(critical),
        "ok": bool(result["service_points_out"]),
    }

The critical-facility branch is where statutory obligations attach, and automating its notification path — SLA windows, confirmation logging, and audit trail — is worked in full in automating critical-facility outage notifications.

Diagnostic Protocol

When an impact count looks wrong — too small, too large, or inconsistent between the map and the dispatch board — work this checklist in order. The first item is the most common root cause.

  1. Stale or missing device states first. Confirm the overlay’s state_as_of timestamps are current. A trace that runs on the design configuration, or on states from an hour ago, will trace through a switch a crew has already opened and dramatically overstate or understate the count. This is the single most frequent cause of a wrong number.
  2. Fragmented topology. Re-run topology validation against the snapshot. A sub-tolerance gap silently splits a feeder, so descendants returns only the near half and the count is confidently too low. A count that is implausibly small on a large circuit points here.
  3. Missing service-point associations. Check that reachable network nodes carry kind == "service" and resolve to meters. Junctions that should aggregate to premises but lack the association drop out of the count even though the trace reached them.
  4. Alternate-path double counting. Verify the still_sourced subtraction is running. If a load fed by a closed tie onto a healthy feeder is still counted as out, the count is inflated — inspect for tie switches whose state overlay never arrived.
  5. Hierarchy roll-up gaps. Confirm every affected service point resolves to a parent transformer, feeder, and substation. An orphaned service point cannot roll up to a circuit total, so the per-circuit breakdown disagrees with the network-wide sum.
  6. Critical-facility register drift. Compare the facility_type domain values against the register. A hospital coded as NONE, or a decommissioned facility still flagged, produces a statutory-notification error that is far more costly than an ordinary miscount.

Performance & Scale Considerations

During a major storm the impact engine does not run once; it runs thousands of times an hour as faults arrive, corroborate, and clear, all while the device-state overlay churns. Naive full-graph rebuilds and repeated descendants calls over the entire network dominate runtime. Partition the network by feeder, pressure zone, or substation and hold one cached DiGraph per partition; an impact trace rarely crosses a partition boundary, so most evaluations stay local and touch a few thousand nodes rather than the whole system. Apply device-state changes as node-attribute deltas against the cached subgraph rather than reloading edges, because a status flip is a cheap attribute write while an edge reload is not.

Run every trace against a pinned snapshot — a versioned read or a snapshot file geodatabase — so a long batch of storm evaluations does not contend for locks on the default version of the production enterprise geodatabase, and so two evaluations of the same fault are byte-for-byte reproducible. Keep the SCADA-driven state refresh on its own cadence: stage telemetry into a table and apply it to node attributes in time-ordered batches rather than querying live feeds inside the traversal loop. When many simultaneous faults must be re-evaluated together, reuse the batch machinery documented for large topology runs so the storm’s worth of traces is scheduled, not fired ad hoc, and cap traversal depth to the largest expected circuit so a malformed loop cannot run unbounded.

Compliance Notes

The affected-customer count and critical-facility flags produced here are not internal conveniences; they are the raw material of publicly reported reliability metrics and statutory obligations, which makes their provenance a compliance requirement. The reliability indices regulators scrutinise — SAIDI, SAIFI, and CAIDI — are computed from customer counts multiplied by outage durations, so a miscount propagates directly into a reported metric and, through it, into penalties and rate cases. Deterministic impact traces over a validated topology, evaluated against a pinned snapshot, produce reproducible counts that a regulator can re-derive from the same inputs — the definition of a defensible record. For water systems, AWWA G400 asset-management practice and EPA Safe Drinking Water Act reporting assume traceable isolation and accurate service-interruption accounting, both of which the impact set supplies. NERC CIP expects an accurate, access-controlled system of record behind every operational decision, which the versioned snapshot and immutable impact report provide.

The required audit metadata for each impact evaluation is therefore explicit: the snapshot identifier and its timestamp, the device-state overlay as-of time, the faulted asset identifier, the resulting customer count, the enumerated critical-facility flags with their jurisdictions, and the software versions that produced the result. Persist these to an append-only store so a count cannot be edited after the fact, and route the record to the asset-management system or SIEM that holds the reliability audit trail. Authority for the reliability and water-management practices themselves should be drawn only from primary sources such as the NERC reliability standards and the AWWA standards program, and Python’s standard logging facility is sufficient to capture the per-evaluation event before it is forwarded to the store of record.