Repository navigation
Conversation
|
Added one commit after the initial push, which also touches on #85.
Both now go through The weights grid had a related problem: it used bare After the change, on a 🤖 Generated with Claude Code |
654ea7a to
943c56a
Compare
943c56a to
ca78295
Compare
| acell, bcell, afraction, bfraction = flowneighbors | ||
|
|
||
| # A receiver the partition names but that is not lower takes nothing, so the shares of | ||
| # those that remain are renormalised before being recorded. |
There was a problem hiding this comment.
Rewritten as one loop over the two receivers, instead of two parallel sets of takesa/abit flags. Same logic, and I benchmarked it to be sure the loop cost nothing: DInf over 1500x1500 is 0.437 s against 0.431 s before, inside noise.
| ### Filling and flow directions | ||
|
|
||
| [`filldepressions`](@ref) takes a `method`: the default [`PriorityFlood`](@ref) of | ||
| [barnesPriorityFloodOptimalDepressionFilling2014](@citet), or [`Zhou`](@ref) after | ||
| [zhouEfficientVariantPriorityFlood2016](@citet), which reaches the same surface 1.1-1.4x | ||
| faster by region-growing slope cells on a plain queue instead of a priority queue. | ||
|
|
||
| Filling leaves flats, over which no flow direction is defined, so by default it also gives | ||
| them a drainage gradient with [`resolveflats`](@ref), after | ||
| [barnesEfficientAssignmentDrainage2014](@citet). Cells are raised by single `nextfloat` | ||
| steps, so the surface is unchanged at any meaningful precision. Pass `flats=false` for the | ||
| filled surface alone. | ||
|
|
||
| [`flowdirection`](@ref) then reads directions off that surface, looking only at each cell's | ||
| neighbours, and returns them with the share each receiver takes: | ||
|
|
||
| ```@example plots | ||
| z = filldepressions(cdtm) # fill depressions, resolve flats | ||
| directions, weights = flowdirection(z; method=D8()) # directions off the filled surface | ||
| nothing # hide | ||
| ``` | ||
|
|
||
| [`D8`](@ref) has a single receiver that takes everything, so its weights are a `Fill` and | ||
| cost nothing whatever the size of the grid. [`DInf`](@ref) and [`FD8`](@ref) store a share | ||
| per receiver, two and eight respectively, which `weighttype=Float16` halves. | ||
|
|
There was a problem hiding this comment.
This is way too much text and the example doesn't plot anything.
There was a problem hiding this comment.
Section is 22 lines now from 48, and the example shows the arrow encoding (parent(directions[400:800, 300:700])) rather than assigning to nothing. My first attempt plotted sum(weights; dims=3), which throws on a Raster with a Band lookup, so it is the arrows instead.
| _convention(::D8) = LDD | ||
| _convention(::DInf) = D8D | ||
| _convention(::FD8) = D8D |
There was a problem hiding this comment.
Why do these have underscores, and the rest not? Stuff is only public if we export it explicitely, so feel free not to use _.
There was a problem hiding this comment.
Dropped. direction.jl and flats.jl are new files that mixed both styles, so their helpers are plain now: convention, directionstate, setshare!, steepestdescent, celldirection!, flatmask, growflat!. _weights became allocweights because weights is a local name throughout that file. I left hydrology.jl alone deliberately: it is underscored throughout, so renaming only my additions there would have made it less consistent, not more. Say the word if you want that file swept too.
| @inline _setshare!(weights::AbstractArray{T}, cell, slot, fraction) where {T} = | ||
| @inbounds weights[cell, slot] = T(fraction) | ||
|
|
||
| # Per-method state cheaper to derive once for the whole DEM than per cell. |
There was a problem hiding this comment.
What does that even mean? Is this a cache?
There was a problem hiding this comment.
Fair. It is not a cache, it is per-method setup done once for the whole grid rather than per cell: a grid of aspects for DInf off a raster, and the reused scratch buffers for FD8. Methods needing neither get nothing. That is now the docstring.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical defects allow flat resolution to create new pits, produce inconsistent FD8 weights, and cause DInf to misroute flow on negative-spacing rasters.
5 open issues
What changed in this PR
Extends the generic hydrology APIs introduced in #83 with depression-filling options, flat resolution, and local flow directions with receiver weights.
Changes:
- Adds Zhou filling and enables flat resolution by default, with
flats=falsepreserving fill-only behavior. - Adds D8, DInf, and FD8 direction stencils and receiver weights, plus shared routing helpers.
- Adds hydrology tests, raster allocation support, usage examples, and algorithm references.
| File | Description |
|---|---|
test/runtests.jl |
Includes hydrology tests and adjusts filling assertions. |
test/hydrology.jl |
Tests filling, flat drainage, directions, and weights. |
src/hydrology.jl |
Adds filling methods and refactors routing helpers. |
src/Geomorphometry.jl |
Includes and exports the new APIs. |
src/flats.jl |
Implements drainage gradients across flats. |
src/direction.jl |
Implements local flow directions and receiver weights. |
Project.toml |
Adds Random to test dependencies. |
ext/GeomorphometryRastersExt.jl |
Allocates raster direction grids without a missing-value sentinel. |
docs/src/usage.md |
Documents filling options and direction derivation. |
docs/src/refs.bib |
Adds Barnes flat-resolution and Zhou filling references. |
Review effort: Balanced
|
|
||
| # Per-method state cheaper to derive once for the whole DEM than per cell. | ||
| _directionstate(::FlowDirectionMethod, dem, cellsize) = nothing | ||
| _directionstate(::DInf, dem, cellsize) = _dinf_aspects(dem, cellsize) |
| function _flowdirection!(weights, fd8::FD8, scratch, dem, cell, cellsize) | ||
| z = dem[cell] | ||
| isfinite(z) || return zero(UInt8) | ||
| total = _fd8weights!(scratch, fd8, dem, cell, _Unvisited(), cellsize) |
| increments = _flatmask(dem) | ||
| @inbounds for cell in eachcell(dem) | ||
| increment = increments[cell] | ||
| iszero(increment) || (dem[cell] = nextfloat(dem[cell], increment)) |
| flats::Bool = true, | ||
| ) | ||
| _filldepressions!(method, dem, queued) | ||
| flats && resolveflats!(dem) |
| flats::Bool = true, | ||
| ) | ||
| _filldepressions!(method, dem, queued) | ||
| flats && resolveflats!(dem) |
Three serial pieces the package was missing, each as a method alongside what is already there rather than a replacement. `filldepressions` takes a `method`: the existing `PriorityFlood` or `Zhou`, after Zhou et al. (2016), which reaches the same surface 1.1-1.4x faster by region-growing slope cells on a plain queue instead of a priority queue. `resolveflats` gives flats a drainage gradient, after Barnes et al. (2014b): two breadth-first gradients, away from higher terrain and towards lower, the latter at twice the weight so no iteration is needed. `filldepressions` calls it by default, since a filled surface is otherwise full of flats that have no flow direction; `flats=false` gives the filled surface alone. `flowdirection` derives directions from the local neighbourhood alone, for D8, DInf and FD8, returning the share each receiver takes beside them. D8 has one receiver that takes everything, so its weights are a `Fill` and cost nothing. The three share one stencil and the partitioning rules are shared with `flowaccumulation` through `_dinfreceivers` and `_fd8weights!`, so the carved and derived routes cannot drift apart. `_dinfreceivers` also guards a non-finite aspect, which is undefined over a flat and would otherwise bracket to the cell itself as both receivers. FD8 caches the neighbourhood geometry per cell rather than recomputing every bearing per neighbour pair, which is 5.6x faster, and clamps angular gaps to 90 degrees: at a border a cell is missing neighbours, and the contour length otherwise runs to tand(90) = Inf and takes the cell's whole flow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4DNRaTBnCQ2KF2aCbq6Nf
…heir type
`flowdirection` allocated its direction grid with `similar(dem, FlowDirection{...})`,
which throws on a `Raster` carrying a `missingval`: Rasters converts the fill
through `typemax`, and a direction eltype has no such method. `flowaccumulation`
has the same bug on this base. Both now go through `_directiongrid`, which the
Rasters extension overrides to allocate without a missingval.
The weights grid used bare `zeros`, which drops the dimensions of a wrapper
type. It now reuses `_alloc_directions`, the hook `horizon_angle` already uses
for exactly this (rows, cols, directions) shape and which the Rasters and
GeoArrays extensions already implement.
`_directiongrid` is taken from #85 unchanged, name and signature, so the two
converge rather than collide.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4DNRaTBnCQ2KF2aCbq6Nf
Four defects, all from making flat resolution the default: - An integer DEM reached `nextfloat(::Int, ::Int32)` and threw. `flats` now defaults to true only for a floating point DEM, since integer elevations cannot express a gradient of single float steps, so integer input behaves as it did before. - `resolveflats!` ignored the caller's exclusion mask, so cells documented as excluded were modified. The flood overwrites `queued`, so the mask is kept and handed to `resolveflats!`, which treats those cells as already draining. - The increments assumed the surrounding relief dwarfs a few float steps. Where it does not, a raised cell could overtake a neighbour that was strictly higher and take away that neighbour's drainage. Each increment is now capped below its lowest higher neighbour. It costs 0.08 s on a quantized 1000x1000 DEM, since ample headroom is one comparison. - `_dinf_aspects` dropped the signs of `cellsize` while `_cellbearing` keeps them, leaving the aspect and the receiver bearings in different frames. On a negative-spacing raster DInf picked a neighbour of gradient 0.707 where 2.121 was available. FD8 contour lengths now come from one place, `_rastercontour`, for both `flowdirection` and `flowaccumulation`. They had separate formulas, so the two disagreed on the same raster: a symmetric peak split eight equal ways through the stencil and 2:1 cardinal-to-diagonal through accumulation. The Moore neighbourhood is not a Voronoi one, so the angular construction degenerates on it and a square grid needs the fixed table. Also, on review comments: `direction.jl` and `flats.jl` are new files that mixed underscore and plain names, so the helpers they add lose the underscore (`hydrology.jl` keeps its own convention, which is underscored throughout). `directionstate` says what it holds rather than that it is cheaper. The DInf branch reads as one loop over the two receivers instead of two parallel sets of flags, at no cost in speed. The manual section is a third shorter and its example now shows something. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4DNRaTBnCQ2KF2aCbq6Nf
ca78295 to
0dd6635
Compare
|
Rebased onto
Two were wrong in their details while right in substance, which is worth knowing when reading the rest:
On the FD8 split — it directly contradicted this PR's own claim that the rules are shared, so there is now one
On your comments: 🤖 Generated with Claude Code |


First of four, stacked on #83. Three serial pieces the package was missing, each added as a method alongside what is there rather than a replacement.
filldepressionstakes amethod. The existingPriorityFlood, orZhou(Zhou et al. 2016), which reaches the same surface 1.1–1.4x faster by region-growing slope cells on a plain queue instead of a priority queue. There is a test that the two agree exactly, including on quantized DEMs where large flats make divergence most likely.resolveflatsgives flats a drainage gradient (Barnes et al. 2014b): two breadth-first gradients, away from higher terrain and towards lower, the latter at twice the weight so no iteration is needed. Filling leaves flats everywhere, and no flow direction is defined over one, sofilldepressionsnow calls this by default;flats=falsegives the filled surface alone. Cells are raised by singlenextfloatsteps, so the surface is unchanged at any meaningful precision — one existing assertion inruntests.jlmoves from==(10)to≈(10)for that reason, with the exact form kept underflats=false.flowdirectionderives directions from the local neighbourhood alone, for D8, DInf and FD8, returning the share each receiver takes beside them. D8 has a single receiver that takes everything, so its weights are aFilland cost nothing whatever the size of the grid. The trailing weights axis is only as long as the method can need: 1, 2 or 8.The three methods share one stencil, and the partitioning rules are shared with
flowaccumulationthrough_dinfreceiversand_fd8weights!, so the carved and derived routes cannot drift apart.Two things worth a reviewer's eye:
_dinfreceiversguards a non-finite aspect. It is undefined over a flat, and bracketing a NaN picks the cell itself as both of its receivers.tand(90) = Infand that neighbour takes the cell's whole flow.Weights live here rather than in the accumulation PR on purpose: they are what
flowdirectionreturns, and splitting them would have meant writing a weightless version and then rewriting it.🤖 Generated with Claude Code
https://claude.ai/code/session_01C4DNRaTBnCQ2KF2aCbq6Nf