fdup
fdup is a Python toolkit for upscaling D8 flow direction grids, computing flow accumulation and watersheds, and evaluating the accuracy of upscaled results.
Implemented upscaling algorithms
- DMM (Double Maximum Method) — Olivera et al., 2002
- NSA (Network Scaling Algorithm) — Fekete et al., 2001
- COTAT / COTAT+ (Cell Outlet Tracing with an Area Threshold) — Reed, 2003
Install
From PyPI:
pip install fdup
For development (editable install with test dependencies):
git clone https://github.com/sasjabs/fdup
cd fdup
pip install -e .[dev]
Requirements: Python >=3.10, numpy, numba, rasterio, pandas, affine, geopandas>=0.14, shapely>=2.0.
Quickstart
import fdup
# Pre-compile all numba kernels (~30 s on first run; cached afterwards).
fdup.warmup()
fa = fdup.io.read("flowacc.tif", grid_type=fdup.GridType.FlowAcc)
fd_coarse = fdup.upscalers.DMM(fa, k=4) # isotropic; same as k=(4, 4)
fd_rect = fdup.upscalers.DMM(fa, k=(4, 2)) # anisotropic (kx, ky)
fdup.io.write(fd_coarse, "flowdir_coarse.tif", overwrite=True)
k is a positive integer (isotropic, equivalent to (k, k)) or a (kx, ky) tuple:
kx is columns / transform.a; ky is rows / transform.e.
A coarse cell covers ky fine rows by kx fine columns, and the output
transform is Affine(t.a * kx, t.b, t.c, t.d, t.e * ky, t.f).
DMM requires both kx and ky to be even.
See examples/api_demo.py for a fully self-contained pipeline that runs on a synthetic DEM.
Submodule reference
fdup.io
| Function | Description |
|---|---|
read(path, grid_type) |
Read a GeoTIFF into a Grid. Validates dtype against the requested GridType. |
write(grid, path, *, overwrite, compress) |
Write a Grid to a GeoTIFF. |
fdup.upscalers
| Function | Description |
|---|---|
DMM(flowacc, k) |
Double Maximum Method. k is an even int or (kx, ky) with both even. Returns GridType.FlowDir. |
NSA(flowacc, k) |
Network Scaling Algorithm. k is an int or (kx, ky). Returns GridType.FlowDir. |
COTAT(flowdir, flowacc, k, *, area_threshold, mufp) |
COTAT / COTAT+. k is an int or (kx, ky). Returns GridType.FlowDir. |
fdup.utils
| Function | Description |
|---|---|
d8(dem, spherical=True) |
Compute ESRI D8 flow directions from a DEM. |
flow_accumulation(flowdir, *, area=True) |
Compute upstream flow accumulation (area in km² for geographic CRS, CRS units² for projected CRS, or raw cell count). |
strahler_order(flowdir) |
Compute Strahler stream orders via BFS. Returns GridType.Strahler (uint8; 0 = nodata/sink). |
snap_pour_cell(flowacc, x, y, radius) |
Snap a pour point to the highest flow-accumulation cell within radius. Returns (row, col). |
delineate_watershed(flowdir, pour_row, pour_col) |
BFS upstream delineation from a pour cell. Returns GridType.Mask. |
disaggregate_mask(mask, k) |
Expand a coarse mask by k (int or (kx, ky); nearest-neighbour). |
match_grids(reference, other) |
Crop/pad other to the same extent as reference. |
mask_area(mask) |
Total area of True-valued cells: km² for geographic CRS, CRS units² for projected CRS. |
threshold_mask(grid, cutoff) |
Boolean mask where value >= cutoff; nodata/NaN cells → False. Accepts FlowAcc or Strahler. |
mask_grid(grid, mask) |
Set nodata on grid wherever mask is False. Returns a new Grid of the same type. |
crop_grid(grid) |
Trim grid to the minimal bounding box of data cells; raises ValueError for all-nodata grids. |
river_tree(flowdir, flowacc, *, mask, min_upstream_area) |
Extract the river network as a GridType.Tree grid plus an array of seed coordinates. |
mask_seeds(seeds, flowdir, flowacc, mask) |
Prune a seeds array to mask-intersecting sub-segments; returns a new structured array with recomputed lengths. |
vectorize_network(flowdir, flowacc=None) |
Decompose D8 grid into river-segment LineStrings (GeoDataFrame). Requires geopandas and shapely. |
vectorize_tree(seeds, flowdir, *, cutoff=None, rank=None, accuracy=None) |
Trace each seed from headwater to mouth and return one LineString per seed (GeoDataFrame). |
vectorize_watershed(mask) |
Convert a boolean Mask grid to a MultiPolygon covering True cells exactly (cell corners). |
fdup.evals
| Function | Description |
|---|---|
compare_watersheds(mask1, mask2) |
Squared Ochiai overlap index + intersection mask. |
compare_flowdir(flowdir_fine, flowdir_coarse, seeds, *, alpha, shuffle, strict_upstream, cascade) |
Per-seed flow-direction accuracy scores. |
huac(flowacc_fine, flowdir_coarse, flowacc_coarse, x, y, radius, *, upstream_area_threshold) |
Hiearchical upstream-area comparison: returns an error raster and a DataFrame. |
flowdir_windrose(flowdir, mask) |
Direction-frequency windrose for a flow direction grid. |
windrose_emd(wr1, wr2) |
Earth Mover's Distance between two windroses. |
Top-level names
import fdup
fdup.warmup() # pre-compile all numba kernels
fdup.Grid # Grid value object
fdup.GridMeta # GridMeta value object
fdup.GridType # GridType enum (DEM, FlowDir, FlowAcc, Mask, Tree, Strahler)
CLI reference
After installation the fdup command is available on the PATH.
fdup --help
Upscaling
-k takes one integer (isotropic) or two integers kx ky (anisotropic:
kx = columns / transform.a, ky = rows / transform.e). DMM requires
even values on both axes.
# DMM (isotropic)
fdup dmm --flowacc flowacc.tif -o flowdir_coarse.tif -k 4
# DMM anisotropic: kx=4 columns, ky=2 rows
fdup dmm --flowacc flowacc.tif -o flowdir_coarse.tif -k 4 2
# NSA
fdup nsa --flowacc flowacc.tif -o flowdir_coarse.tif -k 4
fdup nsa --flowacc flowacc.tif -o flowdir_coarse.tif -k 4 2
# COTAT
fdup cotat --flowdir flowdir.tif --flowacc flowacc.tif -o flowdir_coarse.tif -k 4 \
--area-threshold 10
# COTAT anisotropic
fdup cotat --flowdir flowdir.tif --flowacc flowacc.tif -o flowdir_coarse.tif -k 4 2 \
--area-threshold 10
# COTAT+ (enable MUFP outlet selection; threshold in metres)
fdup cotat --flowdir flowdir.tif --flowacc flowacc.tif -o flowdir_coarse.tif -k 4 \
--area-threshold 10 --mufp 5000
Derivation utilities
# D8 flow directions from a DEM
fdup d8 --dem dem.tif -o flowdir.tif
# Flow accumulation
fdup flowacc --flowdir flowdir.tif -o flowacc.tif # area in km² / CRS units²
fdup flowacc --flowdir flowdir.tif -o flowacc.tif --cells # raw cell count
# Strahler stream order
fdup strahler --flowdir flowdir.tif -o strahler.tif
# Watershed delineation (snap to flowacc if --flowacc is provided)
fdup watershed --flowdir flowdir.tif --x -80.05 --y 35.05 --radius 5000 \
-o watershed.tif --flowacc flowacc.tif
Masking
# Boolean mask: cells with flow accumulation >= 500 km² (or Strahler order >= 4)
fdup threshold-mask --flowacc flowacc.tif --cutoff 500 -o streams.tif
fdup threshold-mask --strahler strahler.tif --cutoff 4 -o streams.tif
# Apply mask to another grid (sets nodata outside the mask)
fdup mask-grid --grid flowacc.tif --mask streams.tif -o flowacc_masked.tif
# Trim grid to the minimal bounding box of data cells
fdup crop-grid --grid flowacc.tif -o flowacc_cropped.tif
River tree and vectorization
# River tree + seed export
fdup river-tree --flowdir flowdir.tif --flowacc flowacc.tif \
--o-tree tree.tif --o-seeds seeds.npz
# Prune seeds to a spatial mask
fdup mask-seeds --seeds seeds.npz --flowdir flowdir.tif --flowacc flowacc.tif \
--mask streams.tif -o seeds_masked.npz
# Vectorize full D8 network to GeoPackage
fdup vectorize-network --flowdir flowdir.tif -o network.gpkg
fdup vectorize-network --flowdir flowdir.tif --flowacc flowacc.tif -o network.gpkg
# Vectorize river-tree seeds to GeoPackage
fdup vectorize-tree --flowdir flowdir.tif --seeds seeds.npz -o tree.gpkg
fdup vectorize-tree --flowdir flowdir.tif --seeds seeds.npz --cutoff 1000 -o tree.gpkg
fdup vectorize-tree --flowdir flowdir.tif --seeds seeds.npz --rank 10 -o tree.gpkg
Evaluation
# Ochiai overlap between two watershed masks
fdup compare-watersheds --mask1 ws1.tif --mask2 ws2.tif
# Per-seed flow-direction accuracy (seeds file: .npz with 'seeds' array)
fdup compare-flowdir --flowdir-fine fd_fine.tif --flowdir-coarse fd_coarse.tif \
--seeds seeds.npz -o scores.npy
# HUAC upstream-area error
fdup huac --flowacc-fine fa_fine.tif --flowdir-coarse fd_coarse.tif \
--flowacc-coarse fa_coarse.tif --x -80.05 --y 35.05 --radius 5000 \
--o-raster huac_errors.tif --o-csv huac_errors.csv
Caveats
snap_pour_cell: radius is in CRS units
radius is deliberately a Euclidean distance in the grid's CRS units, not necessarily metres.
For geographic grids (e.g. EPSG:4326) the unit is degrees.
A radius of 0.5 on an EPSG:4326 grid means 0.5 degrees, not 0.5 km.
To use metric radii, reproject the grid to a projected CRS first.
Area units depend on CRS type
mask_area and flow_accumulation(..., area=True) return areas in different
units depending on the CRS:
- Geographic CRS (lat/lon, e.g. EPSG:4326): areas are in km², computed via the spherical-trapezoid formula, so cell area varies by latitude.
- Projected CRS (e.g. UTM): areas are in CRS units². For a metre-based CRS this is m²; for a foot-based CRS this is ft². Callers are responsible for interpreting the unit based on their CRS. Reproject to a metre-based CRS (e.g. a UTM zone) if m² output is required.
COTAT+ MUFP is in metres
The mufp parameter of COTAT() (and --mufp on the CLI) is always in metres,
regardless of the grid's CRS. The internal distance accumulation uses the
spherical-distance formula for geographic grids and the Euclidean metric
(scaled by the CRS unit) for projected grids.
Float64 flow accumulation precision above 2⁵³
When flow accumulation is computed in float64, the internal numba kernels
accumulate values as 64-bit floats. Values above 2**53 (~9 × 10¹⁵) cannot be
represented exactly, and cell-count precision is lost. For realistic grid sizes
this is not a concern, but very large global grids at fine resolution may be
affected. Use uint64 accumulation (cell counts only) if exact integer results
are required.
Migration guide from v0.1
The object-oriented BaseUpscaler surface has been removed. Replace the old
call pattern:
# old (v0.1)
from fdup.upscalers import DMM
dmm = DMM()
dmm.load_flowacc("flowacc.tif")
dmm.upscale(k=4)
dmm.save("flowdir_coarse.tif")
with the new functional API:
# new (v0.2+)
import fdup
fd = fdup.upscalers.DMM(fdup.io.read("flowacc.tif", grid_type=fdup.GridType.FlowAcc), k=4)
fdup.io.write(fd, "flowdir_coarse.tif", overwrite=True)
The same pattern applies to NSA and COTAT.
Input data conventions
Flow direction grids use the ESRI D8 encoding:
1=E, 2=SE, 4=S, 8=SW, 16=W, 32=NW, 64=N, 128=NE, 255=nodata.
Supported flow accumulation dtypes: int32, uint32, int64, uint64, float32, float64.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fdup-0.3.0.tar.gz.
File metadata
- Download URL: fdup-0.3.0.tar.gz
- Upload date:
- Size: 105.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bc03ebe5d8d516ddf8234fd3bdb94d751fe6c7f52b96f8a7958e79d02a0e758
|
|
| MD5 |
35b52e520b96290161222c0c162ab214
|
|
| BLAKE2b-256 |
150c3c510ed9e040702c4f92250a929b891f2741cf9753ef1e99f769658136d4
|
File details
Details for the file fdup-0.3.0-py3-none-any.whl.
File metadata
- Download URL: fdup-0.3.0-py3-none-any.whl
- Upload date:
- Size: 91.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a068d961d5b704be2d6c03367053dbdf7a57e134167b71a9b2597f16cb3d908a
|
|
| MD5 |
5c424cfcd6d0c6b254b155e8c46d7454
|
|
| BLAKE2b-256 |
2ddb1c887a04a42d48069e1d212d237b3046a134ad3dee2b7a92e36a1b1847ff
|