Skip to content

fl_flood_depth() pools every stream's flood level; max-per-watercourse fix over-floods, needs drainage-based ownership #68

Open

Description

@NewGraphEnvironment

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 one scipy griddata interpolation.
  • 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 BreachDepressionsLeastCost and FillDepressions are not deterministic: 36,000–60,000 of 518,400 cells differ between identical runs. Their fill branch also panics intermittently on an Arc::try_unwrap race. 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_factor ordering 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

  1. changed the title [-]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[/+] on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions