Convert an SDTS DEM with GDAL 3.10

How-to Last tested 24 October 2026

Open an SDTS DEM with a GDAL 3.10 environment and convert it to GeoTIFF before returning to current tools: GDAL removed the SDTS driver in 3.111. The example below converts a real USGS-origin test transfer with GDAL 3.10.3, compares every cell, and shows the error from 3.11.5. Keep the complete transfer and its metadata together; selecting a single elevation-looking file is not enough.

Use a reader that still supports the format

The error is version-dependent, so begin with gdalinfo --version rather than changing file extensions. The tested environments were installed separately through conda-forge using micromamba, with GDAL pinned to 3.10.3 in one prefix and 3.11.5 in the other. Keeping both makes the historical read and the current-version failure reproducible without replacing a system-wide installation.

The script addresses each environment’s executable directly. Set GDAL_310 and GDAL_311 to those environment directories, and set SDTS_WORK to a new working directory. Bash and curl handle the acquisition; Python and the GDAL bindings from the 3.10 environment perform the array comparison. The output records the reader version instead of relying on whichever executable happens to appear first on an unrelated shell’s path.

Once the old reader has produced a verified GeoTIFF, keep that file separate from the source transfer. A conversion is an additional representation, not a reason to discard the original modules. If a later question concerns units, lineage or the source date, the transfer metadata may contain information that a compact output header does not express.

Identify the transfer and its provenance

The example uses STDS_1107834_truncated from the GDAL 3.10.3 test repository. GDAL’s test source identifies its origin as a USGS SDTS DEM, and the accompanying README states that the fixture has been reduced to 25 scanlines for testing2. Its title is ALANSON, MI-24000. This is a small format-verification input, not a complete terrain product for the Alanson area.

The script downloads all eighteen DDF modules listed for that fixture and the README from the same tagged repository location. Each DDF file’s size and SHA-256 is recorded. Those hashes identify the actual input bytes; the tag and source addresses explain where they came from. The original README’s testing-only distribution notice is another reason to keep the sample in local working storage rather than republishing its files.

Open 1107CATD.DDF, the catalogue-directory module. The historical driver documentation identifies the CATD.DDF member as the entry point for the transfer3. Files such as the cell module, identification module and external-reference module remain beside it. Renaming just the cell file to .dem would neither recreate the catalogue nor add a supported reader to GDAL 3.11.

Convert, inspect and compare

The first reader reports its version, opens the catalogue and writes an information report. gdal_translate -of GTiff then creates converted.tif, and another inspection records its header. The second reader attempts the same SDTS catalogue and must fail for the contrast demonstrated here. An unexpected success stops the script so a changed installation cannot silently be reported as the recorded 3.11 result.

The Python section reads the full sample from both representations. It checks cell equality, geotransform equality, equivalent horizontal spatial references and matching NoData values. Five valid sample positions are also printed so the comparison is inspectable without treating a single checksum as a complete explanation. The entire small raster is compared, including its missing cells.

# SPDX-License-Identifier: MIT
set -euo pipefail
: "${SDTS_WORK:?Set SDTS_WORK to a new local working directory}"
: "${GDAL_310:?Set GDAL_310 to the GDAL 3.10 environment}"
: "${GDAL_311:?Set GDAL_311 to the GDAL 3.11 environment}"
mkdir -p "$SDTS_WORK"
cd "$SDTS_WORK"
base='https://raw.githubusercontent.com/OSGeo/gdal/v3.10.3/autotest/gdrivers/data/STDS_1107834_truncated'
modules=(CATD CATS CEL0 DDDF DDOM DDSH DQAA DQCG DQHL DQLC DQPA IDEN IREF LDEF RSDF SPDM STAT XREF)
for module in "${modules[@]}"; do
  curl -fsSL --retry 2 "$base/1107${module}.DDF" -o "1107${module}.DDF"
done
curl -fsSL --retry 2 "$base/README" -o source-README.txt
"$GDAL_310/bin/gdalinfo" --version
"$GDAL_310/bin/gdalinfo" 1107CATD.DDF > sdts-gdalinfo.txt
"$GDAL_310/bin/gdal_translate" -of GTiff 1107CATD.DDF converted.tif
"$GDAL_310/bin/gdalinfo" -json converted.tif > converted-gdalinfo.json
"$GDAL_311/bin/gdalinfo" --version
if "$GDAL_311/bin/gdalinfo" 1107CATD.DDF > gdal311-stdout.txt 2> gdal311-error.txt; then
  printf '%s\n' 'Unexpected success: inspect the installed driver set.' >&2
  exit 1
else
  printf '%s\n' 'GDAL 3.11 failed to open the SDTS source:'
  cat gdal311-error.txt
fi
"$GDAL_310/bin/python" - <<'PY'
from osgeo import gdal
import json, hashlib, datetime, numpy as np
from pathlib import Path
gdal.UseExceptions()
source = gdal.Open('1107CATD.DDF'); target = gdal.Open('converted.tif')
a = source.ReadAsArray(); b = target.ReadAsArray()
assert np.array_equal(a,b), 'Converted sample cell values changed'
assert source.GetGeoTransform() == target.GetGeoTransform()
assert source.GetSpatialRef().IsSame(target.GetSpatialRef())
sb = source.GetRasterBand(1); tb = target.GetRasterBand(1)
assert sb.GetNoDataValue() == tb.GetNoDataValue()
valid = a != sb.GetNoDataValue(); cells = []
for y,x in np.argwhere(valid)[::max(1,int(valid.sum())//5)][:5]:
    cells.append(dict(column=int(x), row=int(y), source=int(a[y,x]), converted=int(b[y,x])))
xref = Path('1107XREF.DDF').read_bytes().decode('ascii',errors='replace')
report = dict(geographic_crs=source.GetSpatialRef().GetAttrValue('GEOGCS'),
              utm_zone=source.GetSpatialRef().GetUTMZone(),
              source_vertical_datum='NGVD 1929' if 'National Geodetic Vertical Datum 1929' in xref else None,
              vertical_datum_source='1107XREF.DDF; no vertical transformation applied',
              measured_utc=datetime.datetime.now(datetime.timezone.utc).isoformat(),
              source_title=source.GetMetadataItem('TITLE'),
              size=[source.RasterXSize,source.RasterYSize],
              crs=source.GetSpatialRef().GetName(), geotransform=source.GetGeoTransform(),
              band_type=gdal.GetDataTypeName(sb.DataType), nodata=sb.GetNoDataValue(),
              band_unit=sb.GetUnitType(), exact_cells=int((a==b).sum()), total_cells=int(a.size),
              sample_cells=cells, gdal310_version=gdal.VersionInfo('--version'),
              source_files=[dict(name=p.name,bytes=p.stat().st_size,sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted(Path('.').glob('*.DDF'))])
Path('sdts-conversion-check.json').write_text(json.dumps(report,indent=2))
print(json.dumps(report,indent=2))
PY

Read the result in its original reference systems

The relevant output was:

GDAL 3.10.3, released 2025/04/01
Source: SDTS Raster; ALANSON, MI-24000
Output: GeoTIFF; 339 × 25; Int16; NoData -32766; band unit m
Horizontal reference: NAD27, UTM zone 16N
Geotransform: (666015, 30, 0, 5040735, 0, -30)
Source vertical reference: NGVD 1929, from 1107XREF.DDF
Exact cell values: 8475 / 8475
Samples (column,row): (300,0) 204→204; (121,8) 282→282;
                     (170,12) 262→262; (220,16) 239→239; (273,20) 205→205
GDAL 3.11.5 "Eganville", released 2025/10/29
ERROR 4: `1107CATD.DDF' not recognized as being in a supported file format.
gdalinfo failed - unable to open '1107CATD.DDF'.

The horizontal WKT calls the projected CRS “unnamed” but includes NAD27 and the UTM zone 16N conversion. That is why the description above reports the actual datum and projection information rather than inventing a fully named EPSG projected CRS. The grid’s 30-metre steps and top-left coordinates are retained exactly through this conversion.

The band reports metres. The XREF module separately identifies the vertical datum as NGVD 1929. These two facts must travel together: a metre-valued height is not automatically a NAVD88 height. The script preserves the numbers and reads their source reference; it does not apply the datum-shift comment present in the transfer or carry out a modern vertical transformation.

Use the check as a conversion boundary

An exact comparison establishes what happened to this input under these commands. It does not establish that every SDTS transfer is complete, internally consistent or recoverable by the same build. For your own files, retain the complete module inventory, inspect the reader’s reported dimensions and references, then compare values against the source representation before using the converted raster in an analysis.

If your purpose is a new map rather than historical reproduction, compare the old transfer’s coverage and dates with a current source. The DEM download guide provides an acquisition record for modern USGS products. A newer product may be appropriate for that new task, while still being unsuitable as a replacement for the exact surface used in an earlier study.

Preserve the version records, source hashes, information reports and comparison output with the converted file. That small evidence bundle lets another analyst distinguish a reader-compatibility fix from a change in terrain values. It also makes the expected current-version failure explicit, instead of leaving someone to diagnose the same unsupported-format message again.

Not covered on this page

Sources

  1. GDAL RFC 108: driver removals for GDAL 3.11. ↩

  2. GDAL’s SDTS test and USGS provenance and the test sample’s README. ↩

  3. GDAL 3.10.3 SDTS driver documentation. ↩

Code on this page is original and MIT licensed.

How we test: methodology. Found a mistake? Corrections.