Repository navigation
Description
Status (2026-10-10): parked. Two candidate fixes were built and measured, and neither replaces the pooled blend.
- Round 1, max of per-watercourse surfaces, over-floods: +67% to +143%.
- Round 2, drainage ownership, under-floods by about a quarter: −25.6% on MORR and −28.6% on Parsnip at ff4. It is monotone in added streams.
Without an independent reference, nothing says which is closer to the truth, so the "bounded change against today" criterion can't settle accuracy either. Round 2 was not shipped: flooded#70 merges the write-up only, and the implementation stays at commit f30ad65. The pooled loss is about 1.3% at the margin (MORR) and does not block floodplains#104's attribute-then-join path. Durable verdict: research/flood_surface_interpolation.md.
Problem
fl_flood_depth() builds the water surface away from the streams as an inverse-distance-squared average (terra::interpIDW(power = 2, radius = max_width / 2)). The average runs over every stream cell within 1 km, whatever stream it belongs to. Each stream cell's level comes from fl_flood_surface(): bed elevation + flood_factor × bankfull depth.
So a spot between a big river and a small creek gets a weighted blend of a deep flood and a shallow one. A creek crossing a big river's valley floor pulls the river's waterline down around itself, and adding streams to a run can remove floodplain.
Measured symptom (floodplains#110, MORR co_ff04, one common DEM):
- Delineating from all FWA streams instead of the coho network loses 451.9 ha (1.27%) of the coho network's floodplain. That reproduces exactly on flooded main.
- Order ≥ 2 loses 444 ha.
What was measured
1. The blend is what loses the ground. Slope doesn't depend on the streams, and the distance and cost masks only loosen as streams are added. So a lost cell is dropped either by the flood mask or by cleanup. Recomputing the flood mask for both networks:
- 78.6% of the lost cells are wet with the coho streams and dry with all streams.
- Only 4.8% survive the all-streams run's pre-cleanup masks.
- Over the whole grid, 1,256 ha of flood-mask cells go from wet to dry when streams are added.
- The coho flood depth on the lost cells has a median of 2.2 m, so these cells are not marginal.
The diagnostic first proposed here, "do the lost cells sit next to added streams", cannot separate the cases. The added network is dense enough that the median floodplain cell is already 185 m from an added stream, and the lost cells' median is 213 m.
2. Lineage.
- Nagel et al. (2014) set the flood height "for each stream segment" and do not say how neighbouring segments' heights combine laterally.
- The Python VCA (BlueGeo, carried in bcfishpass
valley_confinement.py) pools every stream cell into onescipy griddatainterpolation. - So the blend is inherited from the Python port, not from the published method.
3. The candidate fix over-floods. It was implemented as per-blue_line_key IDW surfaces merged by cell-wise max, with confluence cells shared by every watercourse that crosses them. It passed every property test, including "adding a blue line never removes floodplain". But the extent (bundled tile, valley cells):
| flood_factor | pooled (today) | max per blue line | max per stream order |
|---|---|---|---|
| ff2 | 18,543 | 45,101 (+143%) | 37,209 (+101%) |
| ff4 | 23,192 | 46,758 (+102%) | 40,610 (+75%) |
| ff6 | 28,727 | 48,027 (+67%) | 43,764 (+52%) |
- Per-blue-line ff2 exceeds today's ff6. The extent is set by where the tributaries sit, not by flood height.
- The steep tributaries win all the gained ground, not the Bulkley. Each tributary's own waterline, carried from its steeper upper reach (17–43 m/km), sits metres above the Bulkley's valley floor beside it. Median depth on the gained cells is 4.9 m.
- IDW's reach is not the main cause. In the round-1 reviewer's prototype at ff6, 72% of the gained flood-mask cells stayed wet with each tributary's nearest-cell level instead of IDW.
- Per-order grouping does not avoid it.
So the pooled blend has two errors that partly cancel:
- it lowers a river's waterline near small streams (the 1.3% loss above);
- it hides steep tributaries carrying their waterline sideways onto the mainstem floor, which is a much larger effect.
Removing the first error exposes the second. Any fix has to handle both.
Round 2: drainage ownership, measured (not shipped)
What was built.
- WhiteboxTools conditions the DEM: least-cost breach, then
BreachDepressions, single thread. - Each cell follows its D8 path.
- Each path cell has a candidate level: the lowest original ground in its 3x3 window, plus the deepest
flood_factor× bankfull depth among the stream cells there. - A cell's waterline is the highest candidate level on its path.
| criterion | result |
|---|---|
| Monotone in added watercourses | Pass. MORR, all FWA vs the coho network at ff4: 0 ha lost (pooled 451.9 ha) and 0 flood-mask cells (pooled 13,569). Exact for every blue line on the bundled tile |
| ff2 < ff4 < ff6, gaps of the same order | Ordering passes; gaps 1.5–2.9× pooled's. MORR 9.9/8.4% vs 6.8/5.2%; Parsnip 13.4/13.2% vs 5.6/4.6% |
| Bounded change on fixed streams | Fails. MORR arm 5 −27.7/−25.6/−23.4% at ff2/4/6; Parsnip −33.5/−28.6/−22.7%; bundled tile −52% at ff4, 43% of that loss draining off the tile edge |
| Audit of gained and lost cells | Done; see below |
Why it under-floods (Parsnip, ff4).
- 98.4% of the lost cells have a path that passes within one cell of a stream; 94.7% come out dry under their own waterline.
- On those dry cells, the drainage waterline is a median 5.9 m below pooled's (pooled had them a median 2.3 m under water).
- It is set by a cell a median 590 m away in a straight line, against 156 m to the nearest stream.
A valley-floor cell takes the level of the reach where its flow path joins the network, not of the reach beside it. Round 1 failed the other way: a max over watercourses lets any of them flood the ground near it, whether or not that ground drains to it, and a steep tributary's own waterline sits metres above the mainstem floor beside it (median depth 4.9 m on the gained cells, round-1 reviewer's prototype at ff6). Drainage fixes which watercourse may flood a cell; it gets where along it wrong.
Found on the way:
- Levels must come from the original DEM. A level read from the conditioned DEM moved the bundled extent from −50% to −75% of pooled with the pit-removal algorithm alone.
- WhiteboxTools 2.4.0 has two traps. Multi-threaded
BreachDepressionsLeastCostandFillDepressionsare not deterministic: 36,000–60,000 of 518,400 cells differ between identical runs. Their fill branch also panics intermittently on anArc::try_unwraprace. An upstream report is drafted, not posted.
Next candidate: drainage picks the watercourse, proximity picks the point
- Which watercourses: drainage ownership decides which watercourses may flood a cell, namely those on its downstream path.
- Which point: lateral proximity decides where along each, namely that watercourse's level at its nearest point to the cell.
- The waterline: the max over the watercourses met. Still monotone, because added streams only add candidates.
- The expected effect:
- a mainstem-floor cell gets the river's level beside it, not at a junction downstream;
- a steep tributary only floods ground that drains through it.
Not measured.
Acceptance for any fix:
- Monotone in added watercourses on the bundled tile. The test from the rejected implementation (d71bbb5) can be reused.
flood_factorordering and sensitivity preserved: ff2 < ff4 < ff6, with gaps of the same order as today's.- Bounded change against today on fixed streams: report the bundled tile, MORR arm 5 and the Parsnip A/B, not just arm 1 vs arm 5.
- An audit of gained cells covering which stream owns each cell, the depth, and the height above the owner's bed.
A rejected mitigation, for the record: "no flooding below the group's own bed at its nearest cell" brings the ff6 gain down to +21%. But it loses 7,067 cells the blend had, so it is not a drop-in fix.
Results change: only once a fix passes the acceptance above. Then add a NEWS entry, and have floodplains re-run, timed with flooded#67.
Relates: floodplains#104, floodplains#110, flooded#67, flooded#40.
Activity
[-]fl_flood_depth() averages every stream's flood level, so small streams lower a big river's floodplain[/-][+]fl_flood_depth() pools every stream's flood level; max-per-watercourse fix over-floods, needs drainage-based ownership[/+]Merge pull request #69 from NewGraphEnvironment/68-fl-flood-depth-ave…
Revise the WhiteboxTools upstream draft after a repro attempt (#68)
Standalone reprex for the WhiteboxTools non-determinism (#68)
Revert the drainage method; keep the write-up (#68)
Merge pull request #70 from NewGraphEnvironment/68-fl-flood-depth-poo…