Summary
When a model's spatial reference is a feet-based projected CRS (e.g. EPSG:2249, NAD83 / Massachusetts Mainland, US survey foot), the 2D surface-routing results load at the wrong map location — shrunk toward the origin by a factor of ~0.3048 relative to the 1D network and the 2D mesh, which render correctly.
Root cause: the engine writes the 2D mesh node coordinates into the .2d.h5 results file in meters (SI), but the GUI's results layer reprojects them assuming they are already in the model CRS units (feet). The mesh layer, which reads the .2dm file in native model units, is unaffected — hence "mesh correct, results offset."
Environment
- OpenSWMM Engine 6.0 (
.2d.h5 root attr source = "OpenSWMM Engine 6.0", Conventions = "CF-1.11 UGRID-1.0")
- SWMMVis GUI (openswmm.gui)
- Model CRS: EPSG:2249 (US survey foot);
.2dm header SOURCE_CRS: EPSG:2249, UNITS: US survey foot
- Repro model:
BWSC_Combined_Model_Aug2026NewGARR_TStorm2070_OpenSWMM (.inp + .2dm + .2d.h5)
Steps to reproduce
- Open a 2D model whose CRS is in feet (EPSG:2249) with a
.2dm mesh.
- Run (or load) the 2D results (
.2d.h5).
- Enable the 2D results layer.
Expected
2D results overlay the 2D mesh and 1D network at the correct geographic location.
Actual
2D results render at ~0.3048× scale, offset toward the CRS origin. The 2D mesh (from .2dm) and 1D network render correctly.
Evidence (measured on the repro model, 986,941 nodes each)
| Source |
X range |
Y range |
Units |
.2dm (mesh layer, correct) |
739,809 – 794,916 |
2,908,322 – 2,970,086 |
US survey feet |
.2d.h5 Mesh2_node_x/y (results, wrong) |
225,494 – 242,290 |
886,456 – 905,282 |
meters (units='m') |
- Unit check: 739,808.7 ft × 0.3048 ≈ 225,493.7 m — i.e. the h5 values are the feet coordinates scaled to meters.
Mesh2_node_x attrs: standard_name = "projection_x_coordinate", units = "m".
- The
.2d.h5 writes no grid_mapping / CRS variable, so a consumer cannot tell the coordinates are metric and (reasonably) assumes the model CRS (feet).
- Note: the engine appears to use the international foot (0.3048) rather than the CRS's US survey foot (0.304800609601), adding a ~2 ppm scale error on top of the unit mismatch.
Root cause (code)
- Writer — engine
Default2DOutputPlugin.cpp writes Mesh2_node_x/y/z in the solver's internal SI meters with units='m'.
- Consumer —
openswmm.gui/src/layers/swmm2dresultslayer.cpp, SWMM2DResultsLayer::rebuildSceneGeometry_() (~L2217). It reprojects the source coordinates model-CRS → canvas-CRS via CRSReproject::transformPointsInPlace(m_transform, ...) (~L2270) and Y-flips, but treats the values as model units. The comment at ~L2256 even asserts "vx_/vy_ stay in MODEL units" — which the HDF5 source violates.
- Reader —
openswmm.gui/src/io/mesh2dh5reader.cpp, Mesh2DH5Reader::readMeshGeometry (~L174) returns raw Mesh2_node_x/y/z (meters), no unit handling.
- Contrast (works) —
SWMM2DMeshLayer reads the .2dm in native model units and reprojects correctly, so the mesh is placed right.
- Both result sources deliver meters:
HDF5Mesh2DSource::readMeshGeometry (~L1050) and the live EngineMesh2DSource::readMeshGeometry (~L959).
Proposed solutions
Option A — GUI-side unit conversion (immediate; no re-simulation).
In rebuildSceneGeometry_(), convert the results source coordinates from meters to the model CRS's linear unit before reprojection, e.g. divide by OGRSpatialReference::GetLinearUnits() of the layer SRS (no-op for metric CRS, ÷0.3048006 for feet). Ideally gate on the HDF5 units attribute so a future engine change doesn't double-convert.
- ✅ Renders existing
.2d.h5 files correctly without re-running (repro file is 204 MB).
- ⚠️ Couples the GUI to the engine's meters convention; ~2 ppm residual if engine used the international foot.
Option B — Engine writes coordinates in the model's native CRS units.
Undo the SI conversion when writing georeferenced output so the .2d.h5 matches the .2dm/.inp.
- ✅ Self-consistent file; fixes all consumers.
- ⚠️ Requires engine rebuild and re-running the simulation to regenerate the
.2d.h5.
Option C — Make the .2d.h5 self-describing (durable fix).
Engine writes a grid_mapping/CRS variable (WKT or the correct EPSG). If it keeps meters, declare the metric-equivalent CRS (e.g. EPSG:26986 for this model); the GUI then reprojects from the file's declared CRS instead of assuming the model CRS.
- ✅ Correct and interoperable per CF/UGRID; also resolves the survey-vs-international-foot ambiguity.
- ⚠️ Work spans both repos.
Recommendation
Ship Option A now to unblock viewing existing results, and track Option C as the durable fix (self-describing CRS in the .2d.h5 + GUI honoring it). Separately, align the engine's foot→meter factor with the CRS's US survey foot.
Summary
When a model's spatial reference is a feet-based projected CRS (e.g. EPSG:2249, NAD83 / Massachusetts Mainland, US survey foot), the 2D surface-routing results load at the wrong map location — shrunk toward the origin by a factor of ~0.3048 relative to the 1D network and the 2D mesh, which render correctly.
Root cause: the engine writes the 2D mesh node coordinates into the
.2d.h5results file in meters (SI), but the GUI's results layer reprojects them assuming they are already in the model CRS units (feet). The mesh layer, which reads the.2dmfile in native model units, is unaffected — hence "mesh correct, results offset."Environment
.2d.h5root attrsource = "OpenSWMM Engine 6.0",Conventions = "CF-1.11 UGRID-1.0").2dmheaderSOURCE_CRS: EPSG:2249,UNITS: US survey footBWSC_Combined_Model_Aug2026NewGARR_TStorm2070_OpenSWMM(.inp+.2dm+.2d.h5)Steps to reproduce
.2dmmesh..2d.h5).Expected
2D results overlay the 2D mesh and 1D network at the correct geographic location.
Actual
2D results render at ~0.3048× scale, offset toward the CRS origin. The 2D mesh (from
.2dm) and 1D network render correctly.Evidence (measured on the repro model, 986,941 nodes each)
.2dm(mesh layer, correct).2d.h5Mesh2_node_x/y(results, wrong)units='m')Mesh2_node_xattrs:standard_name = "projection_x_coordinate",units = "m"..2d.h5writes nogrid_mapping/ CRS variable, so a consumer cannot tell the coordinates are metric and (reasonably) assumes the model CRS (feet).Root cause (code)
Default2DOutputPlugin.cppwritesMesh2_node_x/y/zin the solver's internal SI meters withunits='m'.openswmm.gui/src/layers/swmm2dresultslayer.cpp,SWMM2DResultsLayer::rebuildSceneGeometry_()(~L2217). It reprojects the source coordinates model-CRS → canvas-CRS viaCRSReproject::transformPointsInPlace(m_transform, ...)(~L2270) and Y-flips, but treats the values as model units. The comment at ~L2256 even asserts "vx_/vy_ stay in MODEL units" — which the HDF5 source violates.openswmm.gui/src/io/mesh2dh5reader.cpp,Mesh2DH5Reader::readMeshGeometry(~L174) returns rawMesh2_node_x/y/z(meters), no unit handling.SWMM2DMeshLayerreads the.2dmin native model units and reprojects correctly, so the mesh is placed right.HDF5Mesh2DSource::readMeshGeometry(~L1050) and the liveEngineMesh2DSource::readMeshGeometry(~L959).Proposed solutions
Option A — GUI-side unit conversion (immediate; no re-simulation).
In
rebuildSceneGeometry_(), convert the results source coordinates from meters to the model CRS's linear unit before reprojection, e.g. divide byOGRSpatialReference::GetLinearUnits()of the layer SRS (no-op for metric CRS, ÷0.3048006 for feet). Ideally gate on the HDF5unitsattribute so a future engine change doesn't double-convert..2d.h5files correctly without re-running (repro file is 204 MB).Option B — Engine writes coordinates in the model's native CRS units.
Undo the SI conversion when writing georeferenced output so the
.2d.h5matches the.2dm/.inp..2d.h5.Option C — Make the
.2d.h5self-describing (durable fix).Engine writes a
grid_mapping/CRS variable (WKT or the correct EPSG). If it keeps meters, declare the metric-equivalent CRS (e.g. EPSG:26986 for this model); the GUI then reprojects from the file's declared CRS instead of assuming the model CRS.Recommendation
Ship Option A now to unblock viewing existing results, and track Option C as the durable fix (self-describing CRS in the
.2d.h5+ GUI honoring it). Separately, align the engine's foot→meter factor with the CRS's US survey foot.