NetworkX vs ArcPy UN Trace — When to Use Each

Every upstream or downstream trace in a utility network eventually runs on one of two engines: the ArcGIS Utility Network trace API exposed through arcpy.un.Trace, or an in-memory directed graph traversed with NetworkX. They are not interchangeable, and choosing wrongly is expensive in both directions — a NetworkX prototype quietly promoted into an operational shutoff tool can miss a terminal-level barrier and de-energize the wrong span, while forcing every nightly analytic through the licensed trace API turns a two-second graph walk into a lock-contending, seat-consuming batch job. The two engines answer subtly different questions: arcpy.un.Trace answers what does the authoritative Utility Network, with its published connectivity rules and terminal configurations, say is connected right now?, and NetworkX answers what does this exported directed graph say is reachable under the barrier attributes I chose to carry? This comparison, which sits under the upstream and downstream tracing algorithms guide and the broader topology and tracing workflows discipline, gives engineers a decision matrix, benchmarks, and two runnable reference traces so the choice is made on evidence rather than habit. Both engines depend on the barrier semantics established in valve and isolator mapping strategies, and both ultimately feed the same downstream consumers — including impact analysis and affected-customer tracing.

Environment Prerequisites

The comparison only holds if both engines run against the same validated topology. Lock the following before benchmarking or trusting either result:

  • ArcGIS Pro 3.2+ with a Standard or Advanced license and the arcpy module importable from the target Python 3.11 interpreter — arcpy.un.Trace requires an active utility network and a validated topology, not merely a network dataset.
  • A validated Utility Network with configured terminal configurations, published connectivity rules, and defined subnetworks. Never benchmark a trace against a dirty-area backlog; an unvalidated network produces results that neither engine can defend.
  • networkx 3.x and geopandas 1.x pinned in an isolated conda environment, with the graph exported from a reconciled, unversioned snapshot so the NetworkX side reflects committed geometry rather than an open edit session.
  • A shared export contract — the same from-node, to-node, subtype, terminal, and barrier attributes carried out of the geodatabase into the graph, so a NetworkX traversal can reproduce the barrier logic the trace engine applies natively.
  • A certification network — a small, hand-verified feeder or pressure zone whose correct upstream and downstream sets are known, used to prove parity between the two engines before either is trusted at scale.

Selection Protocol — Score the Task Before You Commit

Most engine-choice mistakes come from picking on familiarity, not on task shape. Work this ordered protocol; the first question decides the majority of cases.

  1. Is the output an operational decision governed by Utility Network rules? If the trace drives a shutoff, a switching order, or a regulator-facing affected-customer count, rule and terminal fidelity is non-negotiable and arcpy.un.Trace is the default. A graph you assembled yourself is only as correct as the barrier attributes you remembered to export.
  2. Does correctness depend on terminal-level connectivity? Multi-terminal devices — a three-phase sectionalizing switch, a valve with distinct inlet and outlet terminals — connect selectively across terminals. The trace API models this natively; a plain NetworkX node collapses the device to a single vertex and can report continuity the real device does not permit unless you model terminals explicitly as split nodes.
  3. Must the trace run offline, cross-platform, or inside CI? If the answer runs on a build agent with no ArcGIS license, in a container, or on a developer laptop, NetworkX is the only option that ships. This is the decisive factor for validation tooling and analytics.
  4. What is the call volume and latency budget? A single interactive trace favors the API’s native optimization; tens of thousands of repeated traversals over a static snapshot favor an in-memory graph that never touches a geodatabase lock.
  5. Can you prove parity? Whichever engine you lean toward, run both against the certification network and quantify divergence before production. An unexplained mismatch is a defect in your export contract, not a rounding difference.

Decision Matrix and Two Reference Implementations

The matrix below scores the two engines across the dimensions that actually move a production decision. Read it as per task, not per organization — most teams run both, using the trace API as the authoritative operational engine and NetworkX as the portable analytic and validation engine.

Dimension NetworkX (in-memory DiGraph) ArcPy UN Trace (arcpy.un.Trace) Reach for
Fidelity to UN connectivity rules None inherent — the graph knows only the edges and attributes you exported; rule compliance is your responsibility Native — evaluates published connectivity rules, associations, and subnetwork definitions directly against the validated network ArcPy for any authoritative result
Barriers & terminal support Barriers are node/edge attribute filters you implement; multi-terminal devices need explicit split-node modeling First-class category, network-attribute, and function barriers; terminal-aware traversal across configured terminals ArcPy when terminals decide connectivity
Portability / offline / cross-platform Pure Python — runs in any container, CI agent, or laptop with no license or ArcGIS install Requires ArcGIS Pro, an active license, and a validated utility network in reach NetworkX for offline and CI
Performance at repeat volume Trace is an in-memory descendants/ancestors walk in microseconds once the graph is loaded; load cost amortizes over many traces Optimized single-trace execution, but each call touches the geodatabase and can contend for version locks under batch load NetworkX for high-volume static analytics
Licensing & cost Free, open-source (BSD); no per-seat or per-core cost Consumes an ArcGIS Pro seat and Standard/Advanced entitlement per running process NetworkX where licenses are scarce
CI / testability / determinism Trivially unit-testable with synthetic graphs; fully deterministic and mockable Testable only where a licensed network is reachable; harder to fixture in a pipeline NetworkX for gated validation
Data currency / system of record Only as current as the last export; risks staleness against live device states Reads the live, validated network — the authoritative current configuration ArcPy when currency is safety-critical
Custom graph algorithms Full algorithm library — shortest path, centrality, connected components, flow Fixed trace types (upstream, downstream, connected, isolation, subnetwork) NetworkX for analytics beyond tracing
Decision flow for choosing between ArcPy UN Trace and NetworkX A trace request over a validated snapshot enters the first decision: does the task depend on Utility Network rules and terminals? If yes, route to the ArcPy UN Trace engine. If no, a second decision asks whether the task must be portable, CI-gated, or offline. If yes, route to the NetworkX directed-graph engine. If no, the recommended path is a hybrid: prototype in NetworkX and certify the result in ArcPy UN Trace. Trace request over validated snapshot UN rules + terminals? ArcPy UN Trace authoritative, rule-aware Portable, CI, or offline? NetworkX DiGraph portable, license-free Hybrid prototype in NetworkX, certify in ArcPy UN Trace YES NO YES NO

The authoritative engine is arcpy.un.Trace. It evaluates the same connectivity rules, terminals, and barriers that the network enforces natively, so its downstream trace is the defensible answer for an operational decision. The snippet below runs an upstream trace from a starting point, applying a category barrier so traversal halts at operable devices flagged as open:

import arcpy
import logging

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")


def un_upstream_trace(un_layer: str, starting_point_fc: str,
                      barrier_category: str = "Open") -> dict:
    """Run an authoritative upstream trace over the Utility Network.

    Applies a category barrier so traversal halts at open devices. Returns a
    structured, pipeline-gateable summary of the traced result.
    """
    try:
        result = arcpy.un.Trace(
            in_utility_network=un_layer,
            trace_type="UPSTREAM",
            starting_points=starting_point_fc,
            barriers=None,
            include_barriers="INCLUDE_BARRIERS",
            condition_barriers=[["Device Status", "IS_EQUAL_TO", barrier_category, "#"]],
        )
    except arcpy.ExecuteError as exc:
        logging.error("UN trace failed: %s", arcpy.GetMessages(2))
        raise RuntimeError("arcpy.un.Trace did not complete") from exc

    aggregated = result.getOutput(0)  # aggregated geometry / selection
    logging.info("Upstream trace completed against %s", un_layer)
    return {
        "engine": "arcpy.un.Trace",
        "trace_type": "UPSTREAM",
        "barrier_category": barrier_category,
        "result_layer": aggregated,
        "ok": True,
    }

The portable engine expresses the same upstream question as a directed-graph walk. Barriers become a node filter, and terminal-level behavior — if it matters — must be modeled by splitting each multi-terminal device into per-terminal nodes during export. The traversal itself is a few microseconds once the graph is resident:

import networkx as nx


def nx_upstream_trace(graph: nx.DiGraph, start: str,
                      open_devices: set[str]) -> dict:
    """Reproduce an upstream trace as an ancestors walk over an exported graph.

    Barriers are applied as a node filter: traversal never passes through a
    device present in `open_devices`. Returns a structured, comparable summary.
    """
    if start not in graph:
        raise KeyError(f"start node {start!r} not in exported graph")

    # Remove open devices so ancestors cannot traverse through a barrier.
    live = graph.copy()
    live.remove_nodes_from(open_devices & set(live.nodes))

    upstream = nx.ancestors(live, start) if start in live else set()
    return {
        "engine": "networkx",
        "trace_type": "UPSTREAM",
        "start": start,
        "upstream_nodes": sorted(upstream),
        "count": len(upstream),
        "ok": True,
    }


if __name__ == "__main__":
    g = nx.DiGraph()
    g.add_edges_from([("SRC", "V1"), ("V1", "V2"), ("V2", "SVC"), ("V1", "OPEN_SW")])
    print(nx_upstream_trace(g, "SVC", open_devices={"OPEN_SW"}))

Run both against the certification network and diff the node sets. On a hand-verified feeder the two must agree exactly; any divergence is a barrier or terminal attribute the export contract dropped, and it must be reconciled before either result is trusted. In practice the NetworkX walk finishes in well under a millisecond for a graph of a few thousand nodes, while a single arcpy.un.Trace call typically runs in the low hundreds of milliseconds including its geodatabase round trip — a gap that is irrelevant for one interactive trace and decisive across a nightly batch of fifty thousand.

Production Deployment Pattern

The mature pattern is not to pick one engine but to give each its lane, wired into the asset lifecycle:

  1. Make ArcPy UN Trace the authoritative operational path. Any trace that drives a shutoff, a switching order, or a customer-facing count runs arcpy.un.Trace against the live validated network, so the result inherits the published connectivity rules and terminal configurations rather than a snapshot’s memory of them.
  2. Make NetworkX the portable analytic and CI path. Export the directed graph on a reconciled snapshot and run graph analytics, regression traces, and validation suites on unlicensed build agents. This is where determinism and zero licensing cost pay off, and it keeps trace-logic tests green without an ArcGIS seat.
  3. Enforce a shared export contract. Version the from-node, to-node, subtype, terminal, and barrier fields the export carries, and treat a NetworkX-versus-ArcPy divergence on the certification network as a build-breaking defect. Parity is the proof that your graph faithfully represents the network.
  4. Apply retry and backoff to the API path. Enterprise geodatabase and version reads fail intermittently under batch load; wrap arcpy.un.Trace in a bounded retry so a transient lock produces a retry rather than a false empty trace.
  5. Log both engines to one audit schema. Record engine, trace type, starting point, barrier configuration, result count, and snapshot timestamp for every run, so a reliability or rate-case review can reconstruct which engine produced which answer and why.

Conclusion

NetworkX and ArcPy UN Trace are complementary, not competing. arcpy.un.Trace is the authoritative engine whenever connectivity rules, terminals, and live data currency decide correctness — which is every operational and regulator-facing trace. NetworkX is the portable, license-free, endlessly testable engine for offline analytics, high-volume batch traversals, and CI-gated validation, provided its export contract faithfully carries the barriers and terminals the network enforces. Score each task against the matrix, prove parity on a certification network, and let each engine own the lane it is built for. Done that way, the choice stops being a matter of preference and becomes a matter of evidence.

For authoritative reference, consult the NetworkX documentation and the ArcGIS Pro utility network trace documentation.