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"],
}
Step-by-Step Implementation
The build sequence below is deterministic; running steps out of order is itself a common defect source.
- Create the domain networks and tiers. Establish
WaterDistributionandElectricDistributionas 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. - 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.
- Declare containment and connectivity rules. Translate
CONTAINMENT_RULESandCONNECTIVITY_RULESinto UN rules. This is where cross-commodity leakage is prevented — only declared pairs are legal. - 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.
- Stage associations, review, then apply. Generate a staging table, manually review every
FLAGGEDrow, and only then create the live associations inside a versioned edit so the operation is reversible.
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.
- 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_RULESlist will accept geometry but never form an association. This is the single most common silent failure. - Orphaned-child scan. Run a
SearchCursorfor contained asset types whose OID does not appear as aChildOIDin any association table. These are the features a trace silently skips. - Tolerance overruns. Inspect every
FLAGGEDrow 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. - Cross-commodity contamination. Confirm no connectivity association links a
WaterDistributionterminal to anElectricDistributionterminal. Any such row indicates a connectivity rule authored too permissively. - Lifecycle inconsistency. Flag
Retired/Abandonedstructures that still ownActivecontainment rows; these break the next full topology validation without warning during incremental edits. - 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.
Related
- Up to the parent program: Core Utility GIS Fundamentals & Network Models
- Understanding UN vs. Traditional GIS Networks
- CRS Alignment & Geodetic Transformations
- Data Ingestion Pipelines for Utility Assets
- Precision Standards for Sub-Meter Mapping
- Step-by-Step Guide to Building Asset Hierarchies for Gas Networks