Terrain Modules¶
Classes for terrain creation, terrain modification writing, and terrain modification analysis.
Platform Requirements
RasTerrainMod requires Windows with pythonnet and a HEC-RAS installation (uses RasMapperLib.dll via .NET interop). RasTerrain.export_rasmapper_terrain() runs natively on Windows and through a configured Wine runtime on Linux. Other RasTerrain and RasTerrainModWriter methods retain their documented platform requirements.
RasTerrain¶
Terrain HDF creation from rasters using RasProcess.exe CreateTerrain.
Terrain Creation Methods¶
create_terrain_hdf(input_rasters, output_hdf, projection_prj, units="Feet", stitch=True, hecras_version="7.0", timeout_seconds=600)- Create HEC-RAS terrain HDF from input rasterscreate_terrain_from_rasters(input_rasters, output_folder, terrain_name="Terrain", units="Feet", stitch=True, hecras_version="7.0", generate_prj=True)- Convenience wrapper with automatic PRJ generation
Native Registered-Terrain Export¶
export_rasmapper_terrain(ras_project_path, output_tif, terrain_name=None, extent=None, downsample_factor=1, rasterize_modifications=True, overwrite=False, timeout_seconds=1800, hecras_version=None, ras_object=None, receipt_path=None)- Consolidate a registered RAS Mapper terrain to one semantically validated GeoTIFF
The native export loads the exact terrain entry from the project's .rasmap.
Consequently, source priority/order, stitches, masks, and terrain-modification
surfaces remain HEC-RAS behavior. The base raster always uses nearest-neighbor
and the output cell size is exactly the finest registered source cell size
times 1, 2, 4, or 8. Requested bounds snap outward to the authoritative
source grid. Registered sources may have different, non-integer-related cell
sizes: the API passes the exact selected output cell size to RAS Mapper with
Export to Single Raster enabled, and RAS Mapper performs the consolidation
and downsampling using the loaded source order, stitches, and masks. Preflight
rejects only missing, non-finite, or non-positive level-zero source grids.
The method writes to a unique same-directory partial, validates the GeoTIFF,
then atomically promotes it. overwrite=False is the default. A JSON receipt
is written beside the TIFF by default, and the returned TerrainExportResult
is bool-compatible. The derivative is not registered back into the project.
Windows-drive and UNC project/input paths retain their supported normalization.
Direct output to a real UNC share is not qualified: the host stages its owned
GDAL junction beside the output, and Windows junction creation generally cannot
target a remote volume. This fails before export rather than silently changing
destinations. Write to a short local path, then copy the committed TIFF and
receipt to the share. A qualification path whose fully qualified helper response
name exceeded the legacy .NET 260-character limit also failed cleanly; long-path
output staging remains unqualified.
Supported HEC-RAS versions are deliberately narrow:
| Runtime | Status | Evidence |
|---|---|---|
| 6.3 / 6.3.1 | Unsupported | The installed API lacks the bounded GenerateNewRasTerrain(..., resampleVecMods, ...) contract; its public single-file method always uses the full terrain extent |
| 6.4.0 | Unsupported | Not locally installed or qualified; HEC's 6.4.1 resolved issues report that creating a terrain in 6.4 could add 1.0 to elevations |
| 6.4.1 | Supported | Exact private contract reflected; bounded modification-aware, mixed-source, and two-source stitched exports qualified on native Windows and Wine |
| 6.5 | Supported | Exact private contract reflected; bounded modification-aware, mixed-source, and two-source stitched exports qualified on native Windows and Wine |
| 6.6 | Supported | Native Windows and Wine qualified with bounded modification-aware and stitched exports; HEC's 6.6 terrain manual explicitly documents the unified export options |
| 6.7 beta releases | Unsupported prereleases | HEC's archive lists Beta, Beta 2, Beta 3, Beta 4a, and Beta 5 before 7.0. The locally installed Beta 4-labeled and Beta 5 runtimes passed bounded checks, but the API accepts no beta runtime |
| 7.0.0 | Unsupported | Although the checked windows completed, HEC documents a terrain-modification export defect that can omit the minimum-Y portion of a modification |
| 7.0.1 | Supported | The official installer's signature was validated, then the installed runtime was reflected and qualified on native Windows and Wine after HEC reported the 7.0.0 defect fixed in 7.0.1 resolved issues. Bounded modification-aware, mixed-source, and stitched exports passed |
| 7.1 | Forward-open; not yet qualified | The API and installation discovery accept the exact 7.1 term so the official release can run when installed. The helper still verifies the exact managed method contract at runtime. The official HEC-RAS downloads page currently contains no HEC-RAS Classic 7.1 release |
| Other versions | Unsupported | Their exact mapper contract and semantics have not been qualified |
When ras_object is supplied, it must be an initialized RasPrj. The API
checks ras_object.ras_version and, when identifiable, the release folder in
ras_object.ras_exe_path before creating output directories or starting native
work. It raises ValueError for unsupported versions and for any mismatch
between the explicit version, project version, and executable release. The
existing 6.4 convenience term resolves to the qualified 6.4.1 installation;
an actual executable in a 6.4 folder remains rejected.
The four qualified releases all passed on native Windows and under Wine. The
Wine matrix used task-local runtime/project copies and nine successful receipts:
for each of 6.4.1, 6.5, and 7.0.1 it exported a bounded two-source Muncie terrain,
the mixed-resolution Terrain50 terrain with modifications disabled, and the
same terrain with modifications enabled. Arrays and validity masks were
pixel-identical across those three releases; the modification comparison raised
264 cells by 0.15625 to 9.625 feet while 1,769 control cells remained exactly
unchanged. HEC-RAS 6.6 additionally passed exact-input native-Windows/Wine
pixel-parity checks.
HEC-RAS 7.1 is deliberately forward-open rather than pre-qualified. Until HEC
publishes the binary, no claim is made about its export semantics. When the
release is installed, version discovery accepts 7.1, 7.1.0, 7.10, or
71; helper reflection fails closed if HEC changes the expected native contract.
from pathlib import Path
from ras_commander import RasMap, RasTerrain
project = Path(r"C:\Models\Example\Example.prj")
print(RasMap.list_terrain_layers(project)[["name", "resolved_path"]])
result = RasTerrain.export_rasmapper_terrain(
project,
Path(r"C:\Exports\channel_window_2x.tif"),
terrain_name="Terrain",
extent=(100000.0, 200000.0, 101000.0, 201000.0),
downsample_factor=2,
rasterize_modifications=True,
hecras_version="6.6",
)
if not result:
raise RuntimeError(result.error)
print(result.output_path)
print(result.receipt_path)
print(result.source_inventory)
On Linux, configure RasProcess.configure_wine() first. The host clones the
configured Wine prefix into task-local state by default. An orchestration
system that already supplies a prefix owned exclusively by the current task
may set RAS_COMMANDER_TERRAIN_WINE_PREFIX_IS_TASK_LOCAL=1; do not set it for
a shared prefix.
RasTerrainMod.compute_modified_terrain_raster() is deprecated as of 0.99.2
and is scheduled for removal in 1.1. It samples one horizontal
TerrainProfile per row of a caller-supplied GeoTIFF grid and interpolates the
profile onto cell centers. Existing callers may use it during the compatibility
window, but new code should call export_rasmapper_terrain() with
downsample_factor=1 and rasterize_modifications=True. Native export failures
are reported rather than rerouted through the numerically different sampler.
Utility Methods¶
vrt_to_tiff(vrt_path, output_path, compression="LZW", create_overviews=True, overview_levels=None, nodata_value=None, hecras_version="7.0")- Convert VRT to single optimized TIFF using HEC-RAS bundled GDALget_available_versions()- List installed HEC-RAS versions with terrain creation support
Bank Line Generation¶
compute_bank_lines(geom_path, crs=None, ras_object=None)- Generate bank-line geometry from cross-section bank stations
Usage¶
from ras_commander import RasTerrain
# Create terrain from rasters
terrain_hdf = RasTerrain.create_terrain_from_rasters(
input_rasters=["dem_north.tif", "dem_south.tif"],
output_folder="terrain/",
terrain_name="Project_Terrain",
units="Feet",
hecras_version="7.0",
)
RasTerrainModWriter¶
Line and polygon terrain modification HDF/.rasmap writing. Also available as the alias RasTerrainModification.
Line Modification Methods¶
add_high_ground_modification(terrain_hdf_path, rasmap_path, name, polyline_points, top_width=20.0, side_slope=2.0, ...)- Add levee/road terrain modification with trapezoidal profile along polyline (TakeHigher mode)add_fill_surface_modification(terrain_hdf_path, rasmap_path, name, polyline_points, top_width=20.0, side_slope=2.0, ...)- Add fill-surface (SetValue) modification along polylineadd_channel_modification(terrain_hdf_path, rasmap_path, name, polyline_points, width=50.0, depth=10.0, left_slope=3.0, right_slope=3.0, ...)- Add trapezoidal channel modification (TakeLower mode)
Polygon Modification Methods¶
add_modification_polygon(terrain_hdf_path, name, polygon_coords, elevation_method="boundary_from_terrain", control_points=None, ...)- Add polygon multipoint modification (detention pond/wetland grading)
Group Management¶
add_modification_group(terrain_hdf_path, rasmap_path, group_name="Modifications")- Add empty modification group to terrain HDF and.rasmap
Query Methods¶
list_modifications(terrain_hdf_path)- List all terrain modifications stored in HDF sidecar groupget_modification_profile(terrain_hdf_path, name)- Read modification station/elevation profile
Analysis Methods¶
sample_modification_surface(terrain_hdf_path, name, points, existing_elevations=None, ...)- Evaluate line modification surface at XY pointsapply_modification_to_profile(terrain_hdf_path, name, profile, ...)- Apply line modification to existing terrain profilecompare_before_after_profiles(rasmap_existing, rasmap_modified, geom_hdf_path, x_coords, y_coords, ...)- Compare terrain profiles before/after modification
Usage¶
from ras_commander import RasTerrainModWriter
# Add a levee (high ground)
RasTerrainModWriter.add_high_ground_modification(
terrain_hdf_path="Terrain/Terrain.hdf",
rasmap_path="Project.rasmap",
name="Proposed Levee",
polyline_points=[(x1, y1), (x2, y2), (x3, y3)],
top_width=20.0,
side_slope=3.0,
elevation=25.0,
)
# Add a detention pond (polygon)
RasTerrainModWriter.add_modification_polygon(
terrain_hdf_path="Terrain/Terrain.hdf",
name="Detention Pond",
polygon_coords=[(x1, y1), (x2, y2), (x3, y3), (x4, y4)],
control_points=[(cx, cy, target_elev)],
mode="set_value",
rasmap_path="Project.rasmap",
)
RasTerrainMod¶
Terrain profile and volume comparison with modifications applied. Uses RasMapperLib.dll via pythonnet to sample the actual modified terrain surface.
Windows Only
Requires Windows, pythonnet, and an installed HEC-RAS version. Call setup_gdal_bridge() once before other methods.
Setup¶
setup_gdal_bridge(hecras_version="7.0", python_dir=None, create_junction=True)- Configure HEC-RAS GDAL runtime for pythonnet before loading
Terrain Sampling¶
get_terrain_extent(rasmap_path, geom_hdf_path, ras_object=None)- Get terrain bounding box with modifications appliedget_terrain_profile(rasmap_path, geom_hdf_path, x_coords, y_coords, filter_tolerance=0.01, ras_object=None)- Sample terrain elevation along polylineget_terrain_volume_elevation(rasmap_path, geom_hdf_path, x_coords, y_coords, ...)- Compute elevation-volume curve for polygon
Comparison Methods¶
compare_terrain_profiles(rasmap_existing, rasmap_proposed, geom_hdf_path, x_coords, y_coords, ...)- Compare terrain profiles between existing and proposed (cut/fill analysis)compare_terrain_volumes(rasmap_existing, rasmap_proposed, geom_hdf_path, x_coords, y_coords, ...)- Compare elevation-volume curves for no-net-fill analysis
Raster Export¶
compute_modified_terrain_raster(rasmap_path, geom_hdf_path, terrain_tif_path, output_tif_path=None, ...)- Deprecated: row-sampled compatibility raster; useRasTerrain.export_rasmapper_terrain()
Usage¶
from ras_commander.terrain import RasTerrainMod
# One-time setup
RasTerrainMod.setup_gdal_bridge("7.0")
# Compare existing vs proposed terrain along a profile
comparison = RasTerrainMod.compare_terrain_profiles(
rasmap_existing="Existing.rasmap",
rasmap_proposed="Proposed.rasmap",
geom_hdf_path="Model.g01.hdf",
x_coords=[x1, x2, x3],
y_coords=[y1, y2, y3],
)
print(comparison[['station', 'existing_elevation', 'proposed_elevation', 'difference']])
Usgs3depAws¶
USGS 3DEP elevation acquisition from the public AWS S3 bucket, plus VRT mosaicking with HEC-RAS bundled GDAL. Direct download currently covers the 1m project-based products; 10m and 30m are discovery only.
Discovery Methods¶
download_tile_index(resolution, cache_folder=None)- Download and cache the spatial metadata GeoPackagequery_tiles_api(bbox, resolution, buffer_distance=0.0)- Query the National Map tile index REST APIfind_tiles_for_bbox(bbox, resolution, cache_folder=None, buffer_distance=0.0)- Projects intersecting a WGS84 extentlist_projects_for_bbox(bbox, resolution, cache_folder=None, buffer_distance=0.0)- Same, with_yearparsed from project namesselect_projects_for_coverage(projects, bbox, min_coverage_fraction=0.999, min_project_area_fraction=0.0)- Newest project per sub-area, plus a coverage report
Download Methods¶
download_tiles(bbox, resolution, output_folder, cache_folder=None, overwrite_dest=False, max_workers=3, project_name=None, min_year=None, buffer_distance=0.0, *, project_selection="newest", min_coverage_fraction=0.999, min_project_area_fraction=0.0, return_provenance=False, exclude_tile_ids=None)- Download the intersecting 1m tiles
project_selection controls what happens when several projects intersect the
extent:
| Mode | Behavior |
|---|---|
"newest" (default) |
Keeps only the single most recent project. Any sub-area that project does not cover is silently absent from the mosaic. |
"coverage" |
Greedily covers the extent with the newest project available per sub-area, falls back to older projects only for the residual geometry, logs each project's contributed area, and warns when the extent cannot be fully covered. Tiles are returned oldest project first so the newest data wins on overlap in a gdalbuildvrt mosaic. |
With return_provenance=True the call returns (tile_paths, provenance), where
each provenance record carries tile_id, file_name, file_path,
source_url, project_name, project_year, etag, last_modified,
content_length, and local_size_bytes. That is enough to record exactly which
tiles a terrain was built from without re-hashing the rasters.
HEC-RAS Terrain Raster¶
build_terrain_raster(output_raster, project_crs, geom_path=None, aoi_geometry=None, *, buffer_distance=100.0, buffer_units="US survey foot", vertical_unit=None, minimum_cell_size=5.0, target_resolution=None, backfill_resolutions=(10, 30), exclude_tile_ids=None, download_folder=None, cache_folder=None, resampling_method="bilinear", nodata=-9999.0, src_nodata=None, max_workers=3, hecras_version=None, hec_terrain_hdf=None, receipt_path=None, overwrite=False, timeout_seconds=7200, tile_plan=None, require_cached_tiles=False)- Build one gap-free GeoTIFF terrain in the project CRS
RASMapper creates result rasters that mirror the terrain VRT structure: when a
terrain references several rasters, every result output mirrors that
multi-raster structure. build_terrain_raster() therefore delivers exactly one
raster, already in the project CRS so HEC-RAS never reprojects, and can prove
the HEC-RAS terrain built from it has a single source member.
| Step | Behavior |
|---|---|
| AOI | Buffered model extent in the project CRS. From geom_path it is the model footprint (HdfProject.get_project_extent(..., geometry_type="footprint"), text-only 1D supported) unioned with the full cross-section cut lines, which can protrude past the edge-line footprint. The buffer is an absolute distance (default 100 US survey feet), not a percentage. A caller geometry (aoi_geometry, project CRS) is buffered the same way. The project CRS is always caller-supplied. |
| Priority | 3DEP 1m project-based DEMs via download_tiles(project_selection="coverage") (newest project per sub-area), then seamless 1/3 arc-second (~10m), then 1 arc-second (~30m). A lower tier is downloaded only where higher tiers leave AOI pixels uncovered. |
| Cell size | The dominant resolution is the native resolution of the tier contributing the most AOI area, converted to project units with exact fractions (1m is 3937/1200 = 3.2808333333333333 US survey feet). The cell size is k * dominant_resolution for the smallest integer k >= 1 reaching minimum_cell_size (default 5 project units): 1m in EPSG:2277 gives k=2 and 6.5616666666666667 ftUS; a 10m source (32.8 ft) already meets the minimum, so k=1. target_resolution overrides the rule; a value that is not an integer multiple of the dominant resolution is snapped to the nearest multiple (at least 1x) with a logged warning. The receipt records multiple, dominant_resolution, minimum_cell_size, requested_resolution, snapped, chosen_by, and the resulting value. |
| Composite | One HEC-RAS bundled gdalwarp.exe call, sources lowest priority first and highest last, bilinear resampling for every tier (resampling_method, default "bilinear"), -t_srs, -tr, -tap, -te snapped outward, tiled/compressed GeoTIFF with BIGTIFF=IF_SAFER. |
| Vertical units | 3DEP heights are NAVD88 metres. Reprojection changes horizontal units only, and the bundled GDAL 3.0.2 gdalwarp does not rescale Z even with compound CRSs, so valid pixels are explicitly scaled (x 3.2808333333333333 for US survey feet). |
| Gate | Nodata pixels inside the buffered AOI polygon (not its bounding rectangle; any touching pixel counts) must be zero. Otherwise TerrainBuildError is raised with reason_code="terrain_aoi_nodata_after_backfill", the count, bounds, and sample locations. Terrain is never fabricated. |
| Receipt | <output stem>.terrain_receipt.json: per-tier projects, tiles (URL, ETag, Last-Modified, size), contributed AOI pixels and area; the cell-size rule inputs and result; vertical datum, units, and factor; target CRS; grid origin and shape; nodata-inside-AOI count. |
| HEC-RAS handoff | With hec_terrain_hdf, RasTerrain.create_terrain_hdf builds the terrain from the single raster with stitching disabled, and the resulting Terrain.vrt must have exactly one source member (reason_code="hec_terrain_vrt_not_single_source" otherwise). |
from ras_commander.terrain import TerrainBuildError, Usgs3depAws
try:
receipt = Usgs3depAws.build_terrain_raster(
"Terrain/maha_creek_navd88_ftus_epsg2277.tif",
project_crs="EPSG:2277", # caller-asserted for text-only models
geom_path="MAHA CREEK.g01",
hecras_version="6.6",
hec_terrain_hdf="Terrain/hec-terrain-6.6/Terrain.hdf",
)
except TerrainBuildError as error:
print(error.reason_code, error.details)
else:
print(receipt["resolution"]["value"], receipt["hec_terrain"]["source_member_count"])
Catalog Builds: Plan, Prefetch, Build Offline¶
For many models that share one tile store (for example a catalog built in parallel in containers or on HPC nodes with no internet and a read-only store), split acquisition from building:
plan_terrain_tiles(project_crs, geom_path=None, aoi_geometry=None, *, buffer_distance=100.0, buffer_units="US survey foot", backfill_resolutions=(10, 30), min_coverage_fraction=0.999, min_project_area_fraction=0.0, cache_folder=None, exclude_tile_ids=None)- Network. Computes the same buffered AOIbuild_terrain_raster()uses (one shared routine) and every tile each tier needs, and returns a JSON-serializable plan.prefetch_terrain_tiles(plans, download_folder, *, max_workers=4, overwrite=False)- Network, single writer. Deduplicates tiles by URL across any number of plans, downloads each once intodownload_folder/<product>/<filename>, keeps size-matched existing files, and returns a manifest.build_terrain_raster(..., tile_plan=plan, require_cached_tiles=True)- No network. Uses the plan's AOI and tile lists and reads the store without writing to it.
| Plan field | Content |
|---|---|
schema, version, created_at, project_crs |
Identity (ras-commander/usgs-3dep-terrain-tile-plan, 1.0.0) |
buffer |
Distance, units, and distance in project units |
aoi |
wkt (full precision, project CRS), wkt_sha256, bounds, bounds_wgs84, and the AOI report (cross-section counts) |
selection |
Coverage fractions, backfill resolutions, excluded tile ids |
tiers |
Highest priority first. Each tier has tier, product, resolution, and tiles; each tile has tile_id, url, filename, relative_path, product, tier_resolution, project, project_folder, year, etag, last_modified, and content_length (from a HEAD request). Tier 1 lists coverage-selected 1m tiles in composite order and adds coverage (covered and uncovered fraction, uncovered area, and uncovered geometry WKT). Backfill tiers list EVERY seamless 1-degree tile intersecting the AOI, unconditionally, because interior 1m voids cannot be predicted from the index; tiles missing on S3 are listed under unavailable_tiles. |
The prefetch manifest lists one entry per unique URL with filename,
relative_path, local_path, url, tier_resolutions, products,
project, project_folder, year, etag, last_modified,
content_length, local_size_bytes, status (downloaded, cached, or
failed), error, and referenced_by_plans (plan SHA-256 digests), plus
counts. Failures are reported per tile rather than raised; a partial or
size-mismatched download is removed so it can never pass as cached.
With tile_plan, build_terrain_raster() does not recompute the AOI and does
not query the tile index or S3 listings. geom_path and aoi_geometry must be
omitted, project_crs must match the plan, and backfill_resolutions must be
a subset of the planned tiers. With require_cached_tiles=True it never
downloads, never writes into download_folder, and makes no HEAD or other
network request. Every tile of every planned tier must already be in the
store, otherwise TerrainBuildError is raised with
reason_code="terrain_tile_not_cached" and the missing relative paths.
require_cached_tiles=True without tile_plan is rejected. The receipt
build_mode block records offline, network_access, the store folder, and
the plan's SHA-256. Per-tile provenance comes from the plan, plus the local
size and a size_matches_plan flag computed without network access. The
cell-size rule, bilinear composite, single raster, zero-nodata gate, and
single-member Terrain.vrt check are unchanged.
import json
from concurrent.futures import ProcessPoolExecutor
from ras_commander.terrain import Usgs3depAws
# 1. Plan (network), one plan per model
plans = {name: Usgs3depAws.plan_terrain_tiles("EPSG:2277", geom_path=geom) for name, geom in models.items()}
# 2. Prefetch once, single writer
manifest = Usgs3depAws.prefetch_terrain_tiles(plans.values(), "tile-store", max_workers=8)
assert manifest["counts"]["failed"] == 0
# 3. Build offline in parallel against the read-only store
def build(name):
return Usgs3depAws.build_terrain_raster(
f"terrain/{name}.tif", "EPSG:2277",
download_folder="tile-store", tile_plan=plans[name], require_cached_tiles=True,
hec_terrain_hdf=f"terrain/{name}/Terrain.hdf", hecras_version="6.6",
)
with ProcessPoolExecutor() as pool:
receipts = list(pool.map(build, plans))
Mosaic Methods¶
create_vrt(tile_files, output_vrt, hecras_version=None)- Build a VRT mosaic of downloaded tiles with HEC-RAS bundledgdalbuildvrt.exe
create_vrt() inherits the source SRS and native cell size. Do not use a
multi-raster VRT as a HEC-RAS terrain source; use build_terrain_raster().
Related Examples¶
| Notebook | Description |
|---|---|
316_terrain_modifications.ipynb |
Terrain modification writer: high ground, channel, polygon |
920_terrain_creation.ipynb |
Terrain HDF creation from rasters |
930_terrain_modification_analysis.ipynb |
Cut/fill analysis with RasTerrainMod |
931_native_rasmapper_terrain_export.ipynb |
Bounded native export, typed result, receipt, and visual grid evidence |
Notebooks 316, 920, 930, and 931 include freshly executed review outputs and figures for their bounded terrain workflows. Notebook 612 retains its coherent previously computed hydraulic outputs and four final maps; its four hydraulic simulations were deliberately not rerun for this terrain-export change.