Skip to content

[BUG] 2D results render in the wrong location for models in feet-based CRS (HDF5 node coordinates are written in meters) #155

Description

@cbuahin

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

  1. Open a 2D model whose CRS is in feet (EPSG:2249) with a .2dm mesh.
  2. Run (or load) the 2D results (.2d.h5).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions