Asset Hierarchy Design for Water & Electric Utility Networks

A correct asset hierarchy is what separates a map from a network. When containment and connectivity relationships are missing, mislabelled, or spatially broken, the most expensive failures in a topology-aware utility network are silent: an isolation trace returns the wrong set of valves, a switching order leaves a circuit energized, or a versioned edit orphans hundreds of contained features without raising a single dirty area. This guide solves that specific failure mode for dual-commodity operations — defining containment, structural, and connectivity hierarchies for water and electric assets so that every downstream trace, lifecycle transition, and compliance report rests on a verifiable parent-child model. It sits inside the broader Core Utility GIS Fundamentals & Network Models program and assumes you have already standardized your spatial reference and ingestion layers.

The Failure Mode This Solves

The operational gap is not “we have no hierarchy” — it is “we have a hierarchy that looks right and behaves wrong.” Three signatures recur across water and electric migrations:

  • Phantom containment. A valve sits visually inside a vault but is not bound by a containment association, so an isolation trace walks past it. The map looks correct in the viewer; the trace result is dangerously incomplete.
  • Cross-commodity leakage. Water and electric features share a feature dataset or a misconfigured connectivity rule lets a water fitting “connect” to an electric junction, producing impossible flow paths during a trace.
  • Orphaned children on edit. A structure feature is retired or moved, but its contained assets retain stale association rows, leaving features that belong to nothing and break topology validation only on the next full rebuild.

Each of these stems from the same root cause: hierarchy intent encoded in geometry or convention rather than in enforced rules. Water distribution leans on spatial containment (valves inside vaults, pumps within stations) and structural attachment to model physical enclosures, while electric distribution leans on connectivity through junctions and edges with strict phase tracking and switching-device placement. The design must express both paradigms explicitly so the network — not the technician’s memory — guarantees correctness.

Prerequisite Checklist

Confirm every item below before authoring rules. The most common cause of a failed hierarchy build is a prerequisite skipped here, not a logic error later.

Core Data Model: How Hierarchy Is Actually Stored

A utility network hierarchy is three distinct association types layered over the feature classes, not a single parent column. Understanding their separation — which is the central insight behind why a utility network differs from a traditional GIS network — is what prevents the failure modes above.

  • Containment associations answer “what is inside what.” A vault contains valves; a substation contains transformers and switches. Containment is spatial in intent but stored as an explicit relationship, so a contained feature can be displayed in or out of its container without changing the truth of the hierarchy.
  • Structural attachment associations answer “what is mounted on what.” A streetlight attaches to a pole; a regulator attaches to a skid. The attached feature has no independent network role without its support structure.
  • Connectivity associations answer “what flows to what.” These are terminal-to-terminal links that carry hydraulic or electrical behavior and are the only association type a trace traverses for flow.

Each commodity binds these to enforced geodatabase domains. Asset groups and asset types must map to recognized classifications — NESC and IEEE device classes for electric, AWWA material and fitting classes for water — and every code must resolve to a coded-value domain so an invalid type can never enter the network. Lifecycle states (Proposed, Active, Abandoned, Retired) are modelled as a constrained status field that gates association validity: a Retired structure must not retain Active containment rows.

A compact way to think about the rule set is a permission matrix — for each container asset type, the list of asset types it may legally contain, and for each terminal, the terminals it may connect to:

# Declarative hierarchy intent, parsed by the build automation.
# Keys are (domain_network, container_asset_type); values are permitted child asset types.
CONTAINMENT_RULES = {
    ("WaterDistribution", "Vault"): ["Gate Valve", "Butterfly Valve", "Air Release Valve"],
    ("WaterDistribution", "Pump Station"): ["Centrifugal Pump", "Check Valve", "Pressure Sensor"],
    ("ElectricDistribution", "Substation"): ["Power Transformer", "Circuit Breaker", "Disconnect Switch"],
    ("ElectricDistribution", "Padmount Enclosure"): ["Distribution Transformer", "Fused Cutout"],
}

# Terminal connectivity intent (from_terminal -> permitted to_terminals), per commodity.
CONNECTIVITY_RULES = {
    ("ElectricDistribution", "HV Side"): ["Bus", "Feeder"],
    ("ElectricDistribution", "LV Side"): ["Service Point", "Secondary"],
    ("WaterDistribution", "Inlet"): ["Main", "Branch"],
    ("WaterDistribution", "Outlet"): ["Service Lateral"],
}
The three utility-network association types over water and electric feature classes A diagram contrasting three relationship types. Containment: a water vault binds gate, butterfly and air-release valves. Structural attachment: a streetlight mounts on a pole. Connectivity: an electric transformer HV terminal links to a feeder bus. Each is an explicit stored rule, not implied by geometry. Containment “what is inside what” Water Vault Gate Valve Butterfly Valve Air-Release Valve An isolation trace walks only bound children. Structural attachment “what is mounted on what” Pole Street- light No network rolewithout itssupport structure. Connectivity “what flows to what” Transformer HV terminal Feeder Bus terminal The only link a flow trace traverses. Each is an explicit, enforced association — not implied by geometry Bound to coded-value domains (AWWA · NESC · IEEE) and gated by lifecycle state A feature that merely sits inside a structure is invisible to the network until a rule binds it

Step-by-Step Implementation

The build sequence below is deterministic; running steps out of order is itself a common defect source.

  1. Create the domain networks and tiers. Establish WaterDistribution and ElectricDistribution as separate domain networks, each with its own tier definition (hierarchical for electric distribution/transmission, partitioned for pressure zones in water). Never co-mingle commodities in one domain network.
  2. Author asset groups and asset types. Populate each domain network’s classification from the matrix above, binding every asset type to a coded-value domain so invalid codes are rejected at insert time.
  3. Declare containment and connectivity rules. Translate CONTAINMENT_RULES and CONNECTIVITY_RULES into UN rules. This is where cross-commodity leakage is prevented — only declared pairs are legal.
  4. Normalize and validate geometry. Confirm CRS uniformity and run the spatial containment pass below to catch features that fall outside their intended parent before any association is created.
  5. Stage associations, review, then apply. Generate a staging table, manually review every FLAGGED row, and only then create the live associations inside a versioned edit so the operation is reversible.
The five-stage hierarchy build, with flagged features held out of the live associations Stage one creates the WaterDistribution and ElectricDistribution domain networks and their tiers. Stage two authors asset groups and asset types bound to coded-value domains. Stage three declares the containment and connectivity rules that prevent cross-commodity leakage. Stage four normalises CRS and runs the spatial containment pass. Stage five stages every candidate association in a review table; rows marked FLAGGED branch to manual review and never reach the versioned edit, while valid rows are applied and posted. 1 · DOMAIN NETWORKS water + electric tier definitions 2 · CLASSIFICATION asset group / type coded-value domains 3 · RULES containment connectivity 4 · GEOMETRY CRS uniformity containment pass 5 · STAGE & APPLY review table versioned edit tolerance overrun FLAGGED ROWS held out of the live associations resolved by hand, then re-staged Lineage record written by every run timestamp · operator · version · EPSG · tolerance values · valid and flagged counts Nothing becomes an association until it has survived stage five.

The following arcpy workflow performs step 4 and produces the step 5 staging table. It is spatially aware, tolerance-driven, and transaction-safe, intended for the ArcGIS Pro Python 3.x environment with explicit logging.

import arcpy
import logging
import os

# Configure production-grade logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s | %(levelname)s | %(message)s',
    handlers=[
        logging.FileHandler("hierarchy_validation.log"),
        logging.StreamHandler()
    ]
)

def validate_and_stage_hierarchy(
    point_fc: str,
    polygon_fc: str,
    output_table: str,
    tolerance_meters: float = 0.25,
    workspace: str = None
):
    """
    Validates spatial containment between point assets and parent polygons.
    Generates a staging table for UN association creation.
    """
    try:
        if not arcpy.Exists(point_fc) or not arcpy.Exists(polygon_fc):
            raise FileNotFoundError("Input feature classes not found in workspace.")

        if workspace:
            arcpy.env.workspace = workspace
        arcpy.env.overwriteOutput = True

        # Create output staging table
        if not arcpy.Exists(output_table):
            arcpy.management.CreateTable(os.path.dirname(output_table), os.path.basename(output_table))
            arcpy.management.AddField(output_table, "ChildOID", "LONG")
            arcpy.management.AddField(output_table, "ParentOID", "LONG")
            arcpy.management.AddField(output_table, "Deviation_m", "DOUBLE")
            arcpy.management.AddField(output_table, "Status", "TEXT", field_length=20)

        logging.info(f"Starting spatial validation with {tolerance_meters}m tolerance.")

        # Spatial join to identify potential containment
        temp_join = "memory\\temp_spatial_join"
        arcpy.analysis.SpatialJoin(
            target_features=point_fc,
            join_features=polygon_fc,
            out_feature_class=temp_join,
            join_operation="JOIN_ONE_TO_ONE",
            match_option="WITHIN_A_DISTANCE",
            search_radius=f"{tolerance_meters} Meters"
        )

        # Validate and populate staging table
        valid_count = 0
        flagged_count = 0

        with arcpy.da.InsertCursor(output_table, ["ChildOID", "ParentOID", "Deviation_m", "Status"]) as insert_cursor:
            with arcpy.da.SearchCursor(temp_join, ["OID@", "TARGET_FID", "JOIN_FID", "SHAPE@"]) as cursor:
                for row in cursor:
                    oid_orig, child_oid, parent_oid = row[0], row[1], row[2]
                    if parent_oid is None or parent_oid == -1:
                        logging.warning(f"Point OID {child_oid} falls outside all containment polygons.")
                        continue

                    # Calculate exact deviation from polygon centroid
                    point_geom = row[3]
                    # Retrieve parent centroid via a nested cursor
                    with arcpy.da.SearchCursor(
                        polygon_fc, ["SHAPE@"], f"OBJECTID = {parent_oid}"
                    ) as parent_cursor:
                        parent_row = next(parent_cursor, None)
                        if parent_row is None:
                            continue
                        deviation = point_geom.distanceTo(parent_row[0].centroid)

                    status = "VALID" if deviation <= tolerance_meters else "FLAGGED"
                    insert_cursor.insertRow([child_oid, parent_oid, deviation, status])
                    if status == "VALID":
                        valid_count += 1
                    else:
                        flagged_count += 1

        arcpy.management.Delete(temp_join)
        logging.info(f"Validation complete. {valid_count} valid, {flagged_count} flagged.")
        return output_table

    except arcpy.ExecuteError as e:
        logging.error(f"ArcPy execution failed: {arcpy.GetMessages(2)}")
        raise
    except Exception as e:
        logging.error(f"Unexpected error during hierarchy validation: {e}")
        raise
    finally:
        logging.info("Hierarchy validation workflow terminated.")

# Example execution (requires ArcGIS Pro environment)
# validate_and_stage_hierarchy(
#     point_fc=r"C:\GIS\Water\Valves.gdb\Valve_Points",
#     polygon_fc=r"C:\GIS\Water\Structures.gdb\Vault_Polygons",
#     output_table=r"C:\GIS\Staging\Valve_Containment_Staging.gdb\ContainmentStaging",
#     tolerance_meters=0.20
# )

This pattern generalizes across commodities. Teams standardizing a multi-utility portfolio can reuse the same validation skeleton for gas by following the step-by-step guide to building asset hierarchies for gas networks, swapping only the rule matrix and tolerance constants.

Diagnostic Protocol

When a hierarchy “looks right but traces wrong,” work this ordered checklist. The first checks catch the highest-frequency defects.

Triage order for a hierarchy that looks correct on the map but traces wrong The first split asks whether the child asset type appears in the container rule list; if it does not, the feature has a domain-code mismatch and will accept geometry without ever forming an association. If the type is legal, the next question is whether the feature OID appears as a ChildOID in an association table; if it does not, it is an orphaned child that the trace silently skips. Where both hold, the remaining candidates are a tolerance overrun from an upstream CRS defect and a retired structure still owning active containment rows. The trace skips a feature the map shows inside its container type not in the rule list Domain-code mismatch check first — highest frequency Query asset type against its coded-value domain fix Re-author the containment rule, then re-stage type is legal Is the OID a ChildOID anywhere? no Orphaned child — association row never written yes Tolerance overrun or Retired parent holding Active rows Work the branches in order; each one rules out a whole class of silent failure.
  1. Domain-code mismatches first. Query the asset-type field against its coded-value domain. A child whose type is not in the container’s CONTAINMENT_RULES list will accept geometry but never form an association. This is the single most common silent failure.
  2. Orphaned-child scan. Run a SearchCursor for contained asset types whose OID does not appear as a ChildOID in any association table. These are the features a trace silently skips.
  3. Tolerance overruns. Inspect every FLAGGED row from the staging table. A 0.6 m deviation on an electric junction usually means a CRS or snapping defect upstream, not a real placement error — do not “fix” it by widening tolerance.
  4. Cross-commodity contamination. Confirm no connectivity association links a WaterDistribution terminal to an ElectricDistribution terminal. Any such row indicates a connectivity rule authored too permissively.
  5. Lifecycle inconsistency. Flag Retired/Abandoned structures that still own Active containment rows; these break the next full topology validation without warning during incremental edits.
  6. Dirty-area residue. Validate the network topology and confirm zero unresolved dirty areas in the edited extent before promoting the version.

Performance & Scale Considerations

At municipal and regional scale the validation pass dominates runtime, so treat it as a batch job, not an interactive tool. Process containment in bounded batches — partition by tile, pressure zone, or feeder so each SpatialJoin operates on tens of thousands of features rather than millions, keeping memory predictable and allowing a failed tile to retry independently. Use the memory\ workspace for the intermediate join (as shown) to avoid disk I/O, but materialize the staging table on disk so a crash mid-run loses no progress.

For branch-versioned utility networks, isolate the staging run in a named version to prevent lock contention with field-edit traffic on the default version, then reconcile and post once flagged rows are cleared. Take a topology snapshot (record the validated extent and timestamp) before bulk association creation so a regression can be diffed against a known-good baseline. When the volume of edits is high enough that interactive validation stalls, hand the work to the batch automation patterns in batch topology processing with Python, which are built for exactly this throughput profile.

Compliance Notes

A validated hierarchy is also an auditable one. The outputs of this workflow satisfy several regulatory checkpoints provided the right metadata is captured:

  • ISO 55000 asset lineage requires traceability from field collection to network topology. Persist the staging table, the logging output, and the tolerance constants used for each run as the lineage record.
  • AWWA and NESC/IEEE classification conformance is satisfied by the enforced coded-value domains on asset groups and types — export the domain definitions alongside each schema release as evidence.
  • Isolation-trace reliability for emergency response and rate-case asset verification depends on complete connectivity associations; the orphaned-child scan in the diagnostic protocol is the audit artifact that proves completeness.
  • Required audit metadata per run: run timestamp, operator, version name, CRS/EPSG code, tolerance values, valid/flagged counts, and the resolution disposition of every flagged row.

For authoritative rules on association behavior and topology validation, consult the official Esri Utility Network associations documentation, and standardize the automation’s logging and error handling against the Python logging documentation so every deployment produces a consistent, traceable record.

Multi-Commodity Estates: One Hierarchy, Several Physical Realities

A utility that runs water and electric — or water, gas and electric — is not running one hierarchy with three flavours. It is running three hierarchies that share a schema, a coordinate frame and a set of structures, and the design decisions that follow from that sharing are where multi-commodity estates go wrong.

Structures are shared; networks are not. A single vault can hold a water valve and a communications splice. A single pole can carry a primary conductor, a transformer and a street light. The structure is one feature, and the contained assets belong to different domain networks. This is exactly why containment is modelled separately from connectivity: the vault contains both, and neither flows to the other. An estate that encodes “in the same vault” as a connectivity association creates a path between commodities that no trace should ever walk.

Classification cannot be shared across commodities. Asset group and asset type values are scoped to a domain network, and reusing a code across two of them — a generic VALVE type serving both water and gas — makes every rule written on it ambiguous. Prefix or namespace the codes per commodity even where the physical object looks similar, because the rules, the pressure classes and the regulatory obligations differ.

Tier depth differs by commodity and that is correct. Electric distribution is naturally hierarchical: substation, feeder, lateral, service. Water distribution is often partitioned instead: pressure zones that interconnect rather than nest. Forcing one tier model onto both produces a hierarchy that is either too deep for water or too flat for electric. Model each domain network’s tier definition on its own physics.

Shared crews need one identifier scheme. Where the same field crew maintains assets across commodities, the identifier they read on a work order has to be unambiguous without the commodity attached. A globally unique identifier solves this; a per-commodity sequence does not, because V-104 in water and V-104 in gas will eventually appear on the same day’s work list.

The practical test for a multi-commodity hierarchy is a single query: can you list every asset inside a given structure, grouped by domain network, without a spatial search? If containment is modelled properly the answer is yes and the query is trivial. If it needs geometry, the containment associations are not there and the hierarchy is decorative.