Reprojecting a Live Utility Network Without Downtime
Changing the coordinate frame of a production utility network is one of those tasks that looks like a single geoprocessing operation and behaves like a migration. The features are the least of it: the connectivity tolerance is expressed in the frame’s units, every published service and offline replica carries the old frame until it is rebuilt, and every hard-coded coordinate system in a decade of automation becomes a defect at cutover. Done as a big-bang reprojection under an edit freeze, the freeze lasts as long as the slowest step and the rollback plan is aspirational. Done as a parallel build with a rehearsal, the freeze covers only the final delta and the switch. This guide sets out the second approach, and it assumes the frame decision itself has already been made following CRS alignment and geodetic transformations.
Environment Prerequisites
- ArcGIS Pro 3.2+ with a Standard or Advanced licence,
arcpyimportable from Python 3.11, and enough storage for a parallel copy of the network. - The target frame decided and published as configuration, including the vertical reference, not chosen during the migration.
- The transformation path pinned per source frame, with the grid files verified present, as the CRS validation script checks.
- Surveyed control in both frames, so the reprojected copy can be verified against something external rather than against itself.
- An inventory of consumers: published services, offline replicas, scheduled jobs, downstream extracts, and any automation with an EPSG code in it.
- A trace regression set — origins and expected result sets — captured on the live network before anything changes.
Schema-Aware Validation Protocol — Run Before the Parallel Build
- Confirm every feature class declares a frame. A class with an undefined spatial reference cannot be reprojected, only redefined, and redefining it during a migration buries the error permanently.
- Restate the tolerance in the target units. A tolerance of 0.01 in feet and 0.01 in metres are different networks. Compute the equivalent and record the decision.
- Capture the trace regression set first. Once the live network changes, there is nothing to compare against. Origins, parameters and result counts, stored.
- Enumerate the consumers exhaustively. Search the automation estate for EPSG codes and well-known text strings; each hit is either parameterised before cutover or a failure after it.
- Verify the grid files on the machine that will run the reprojection, not on a workstation. A missing grid on the server silently downgrades the transformation for the whole estate.
Minimal Reproducible Implementation
from __future__ import annotations
import logging
from dataclasses import dataclass, field
import arcpy
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("reproject")
@dataclass
class ReprojectReport:
target_epsg: int
transformation: str
copied: list[str] = field(default_factory=list)
control_residuals_m: dict[str, float] = field(default_factory=dict)
problems: list[str] = field(default_factory=list)
@property
def ok(self) -> bool:
return not self.problems and all(
r <= 0.05 for r in self.control_residuals_m.values())
def reproject_parallel(
source_gdb: str,
target_gdb: str,
classes: list[str],
target_epsg: int,
transformation: str,
control: dict[str, tuple[float, float]],
) -> ReprojectReport:
"""Build a reprojected parallel copy and verify it against known control.
``transformation`` is named explicitly rather than left to the default, because a
default path silently substitutes a parameter-based operation where a grid-based one
was intended. ``control`` maps a control point id to its known coordinates in the
TARGET frame; residuals above five centimetres fail the run.
"""
report = ReprojectReport(target_epsg=target_epsg, transformation=transformation)
sr = arcpy.SpatialReference(target_epsg)
for name in classes:
try:
arcpy.management.Project(
in_dataset=f"{source_gdb}/{name}",
out_dataset=f"{target_gdb}/{name}",
out_coor_system=sr,
transform_method=transformation,
)
report.copied.append(name)
LOG.info("projected %s", name)
except arcpy.ExecuteError as exc:
report.problems.append(f"{name}: {exc}")
for point_id, (want_x, want_y) in control.items():
try:
with arcpy.da.SearchCursor(f"{target_gdb}/ControlPoints",
["SHAPE@X", "SHAPE@Y"],
where_clause=f"POINT_ID = '{point_id}'") as cur:
got_x, got_y = next(iter(cur))
residual = ((got_x - want_x) ** 2 + (got_y - want_y) ** 2) ** 0.5
report.control_residuals_m[point_id] = residual
if residual > 0.05:
LOG.warning("control %s residual %.3f m", point_id, residual)
except (arcpy.ExecuteError, StopIteration) as exc:
report.problems.append(f"control {point_id}: {exc}")
return report
The control check is what separates a reprojection from a hope. Reprojected data always looks right against itself; only an external observation reveals that the transformation ran without its grid file.
What Makes the Freeze Short
The instinct is to plan the freeze around the reprojection, because that is the visible step. In practice the reprojection is the one part that can happen entirely outside it. What must happen inside the freeze is small: reproject the delta accumulated since the last parallel build, switch the connection strings and service definitions, and verify.
Two things decide how small that delta is. The first is how recently the parallel build ran — a build refreshed nightly leaves a day of edits, one refreshed a month ago leaves a month. The second is whether the estate can hold edits briefly rather than needing them applied continuously; most can, for an hour, if it is scheduled.
Everything else that people put in the freeze belongs outside it. Republishing services can be staged and swapped. Replicas can be recreated in advance against the parallel copy and activated at the switch. Automation can be parameterised weeks earlier and tested against the copy. A freeze that contains only the delta and the switch is measured in tens of minutes; one that contains the whole migration is measured in whatever the slowest step turns out to be, which is never known in advance.
Production Deployment Pattern
- Build the parallel copy while the network is live, and repeat it as a delta until the final run. The last delta is small enough to complete inside a short freeze.
- Rehearse with real consumers. Point a service, a replica and a scheduled job at the copy and let them run for a week. Every defect found here is one not found during the freeze.
- Parameterise every EPSG code before cutover. A configuration value that can be changed once is the difference between a switch and a week of individual fixes.
- Keep the old frame readable until confidence is earned. A read-only copy of the pre-cutover network costs storage and buys the ability to answer “was it always like this?”
- Re-run the trace regression set after the switch. Identical result sets are the evidence that connectivity survived the frame change.
- Record the transformation and grid versions in the audit trail, because every coordinate in the estate now depends on them.
Conclusion
A frame change is a migration with a short cutover, not a geoprocessing task with a long one. Building in parallel, verifying against external control, rehearsing with real consumers and parameterising the frame everywhere reduces the freeze to the final delta and the switch. The trace regression set is what proves the network is the same network afterwards — and keeping the old frame readable for a while is what lets you answer the questions that arrive in the weeks after.
Related
- Up to the parent topic: CRS Alignment & Geodetic Transformations
- Up to the section: Core Utility GIS Fundamentals & Network Models
- Python Script for Validating CRS Alignment Across Utility Layers
- Choosing a CRS for Multi-Jurisdiction Water Networks
- Precision Standards for Sub-Meter Mapping
For authoritative reference, consult the ArcGIS Pro Project tool documentation and the PROJ transformation documentation.