Ecohydraulics

What the design is worth ecologically. Three independent analyses; all of them need a prepared condition, and habitat area additionally needs a flow duration curve from Get started, terminology and signposts.

Habitat Area (SHArC)

riverarchitect.sharc - the Ecohydraulics ▸ Habitat Area (SHArC) tab.

Turns 2D hydrodynamic results into a map of how good the habitat is for a given fish species and lifestage, and then into a single number a project can be judged on.

  1. Habitat suitability curves come from Fish.xlsx: for each species and lifestage, a piecewise-linear curve mapping water depth, velocity, substrate size or cover radius onto a suitability index between 0 and 1.

  2. Hydraulic HSI rasters apply those curves to the depth and velocity rasters of a discharge, giving dsi and vsi.

  3. Composite HSI (cHSI) combines them by geometric mean or product, optionally with a cover HSI, and is masked to the wetted area.

  4. Usable area at a discharge is the area where cHSI exceeds a threshold (0.4 by default), optionally weighted by the mean cHSI there.

  5. SHArea integrates usable area over the flow duration curve, so habitat that only exists at a rare discharge counts for little.

\[ \text{SHArea} = \sum_i \frac{E_i - E_{i-1}}{100}\,A_i \]

with \(E_i\) the cumulative per-cent exceedance of discharge \(i\) and \(A_i\) its usable area. That reproduces the =(E5-E4)/100*F5 column of the original CONDITION_sharea_template.xlsx exactly.

Read the mean cHSI, not only the area

Usable area is not monotonic in discharge. A high flow can inundate a large area of channel margin shallowly enough to clear the threshold even though the reach as a whole is less suitable. Area and quality are different questions; SHArea is the one that weighs them together.

Cover

Depth and velocity say whether a fish can be somewhere. Cover says whether it is safe to be there - a fry in open water at the right depth and velocity is still a meal. Pass cover=True to riverarchitect.sharc.SHArC.run(), or call riverarchitect.sharc.cover_hsi() directly.

Cover type

Source

How it is applied

substrate

the grain size raster

mapped through its suitability curve, like depth or velocity

cobbles

grain size 0.064 - 0.256 m

presence, spread over the curve’s radius

boulders

grain size above 0.256 m

presence, spread over the curve’s radius

plants

plants.tif beside the condition

presence, spread over the curve’s radius

wood

wood.tif beside the condition

presence, spread over the curve’s radius

A cover element shelters everything within its radius, taken from the first x value of its curve in Fish.xlsx, and those cells take the curve’s suitability. The result is the cell-wise maximum across the types present: the best shelter available at a cell is what counts, not the sum of every kind of it. Cover is cropped to water the lifestage can reach - shelter a fish cannot get to shelters nothing.

With cover, the composite is the cube root of dsi * vsi * cover rather than the square root of dsi * vsi.

Mineral cover can also be read as an areal fraction

Fish.xlsx heads both cover blocks Rad., which is why cobbles and boulders are applied by radius like plants and streamwood. The working-principles page describes them differently - “areas where the boulder presence covers more than 30 % of the surface get assigned an HSI value of 0.5” - and reads the same two cells as areal fractions. That reading is available:

SHArC("2100_sample", unit="us", mineral_rule="fraction")   # or cover_window=2

The fraction is measured over a 3x3 window by default (riverarchitect.sharc.COVER_WINDOW); the original’s window size is not recorded anywhere that survived, so it is a parameter rather than a constant. See riverarchitect.sharc.MINERAL_COVER_RULES.

Worth knowing before choosing. The mineral values the workbook holds are 0.1 for cobbles and 1.0 for boulders, which as radii are smaller than a cell on any real grid - so under the default a mineral cover element shelters only itself, with no spreading at all. As fractions the same numbers ask for 10 % and 100 % of the neighbourhood. On the sample reach, Chinook juvenile SHArea with cover is 55 331 sqft by radius against 53 723 sqft by fraction, and boulders drop out of the fraction result entirely: their value asks for a neighbourhood that is wholly boulder, and a gravel-cobble reach has none.


```{admonition} Where the radius came from
:class: note

The original produced it by converting the cover raster to points, running
`SpatialJoin_analysis(..., match_option="CLOSEST", search_radius=r)` against a point per
cell, and converting back. That is a Euclidean distance transform written the only way
`arcpy` allowed; here it is {func}`riverarchitect.raster.within_radius`, which also handles
anisotropic cells correctly. The mineral fraction is
{func}`riverarchitect.raster.focal_fraction`, the equivalent of `FocalStatistics`.

Species and lifestages

The workbook defines Chinook Salmon, Rainbow / Steelhead Trout, Lamprey, Green Sturgeon and a generic “All Aquatic” block, each with its own lifestages, seasons, curves and swimming thresholds. Lifestage labels are read from the workbook rather than assumed, because they vary by species - offset 3 is “fry” for salmon but “ammocoetes” for lamprey.

Names are matched ignoring case and spacing, so "Chinook salmon" and the workbook’s "Chinook Salmon" both work. In the original, and in early versions of this port, the capitalisation of a caller’s argument decided whether the analysis ran at all.

To add or edit a species, edit the packaged Fish.xlsx - see the Edit Fish template page below.

Relation to the original

cHSI.nested_con_raster_calc built one Con() raster per curve segment and took their cell-wise maximum: a piecewise-linear interpolation written the only way arcpy map algebra allowed. Here it is riverarchitect.sharc.apply_curve(), which is numpy.interp with the same end behaviour - hold the first suitability below the curve, but drop to zero above it. That asymmetry is deliberate: a depth beyond the curve is unsuitable, not maximally suitable, and clamping there would invent habitat at the highest discharges.

Stranding Risk

riverarchitect.stranding - the Ecohydraulics ▸ Stranding Risk tab.

As discharge falls the wetted area shrinks and breaks apart; pools that lose their connection to the main channel trap fish. Threshold each depth raster at the minimum swimming depth of the species and lifestage, then ask of every wetted cell whether a route back to the main channel exists. Where none does, a fish that is there is trapped.

That question is answered by Dijkstra’s algorithm over the travel-permissible cells - riverarchitect.raster.least_cost_distance() - which is how the original built its shortest_paths rasters: cells are vertices, steps between neighbours are edges weighted by the distance between cell centres, and the cheapest route to the mainstem is the escape route. When only the depth criterion applies, a route exists exactly when the wetted region touches the mainstem, so the answer coincides with connected-component labelling (riverarchitect.raster.disconnected_mask()); that is the faster path and the one taken when no velocity field is supplied. The two are checked against each other on the sample reach in the test suite.

The main channel is defined once, as the largest wetted region at the lowest analysed discharge, and every higher discharge is judged against it - the same target the original built its escape routes towards.

Output

disconnected_<Q>.tif

one per discharge

Q_disconnect.tif

the highest discharge at which each cell was disconnected: the flow at which that spot becomes a trap as the hydrograph recedes

escape_<Q>.tif

route length back to the mainstem, with write_escape_routes=True - the original’s shortest_paths

pools_<Q>.gpkg

the individual pools at the worst discharge

table

wetted area, stranded area and pool count per discharge

The minimum swimming depth comes from Fish.xlsx (0.2 ft for Chinook fry). It is the single most influential parameter in the analysis - at h_min = 0 every wet cell counts and much of the pool count is single cells at the wetted edge, real in the raster and meaningless in the river. State the value you used alongside any result.

The velocity criterion

A fish cannot swim upstream against more than u_max, so fast water is a one-way door: it can be drifted down but not climbed back up. That makes the escape-route graph directed, and a directed graph needs the flow direction. Conditions ship u<Q>.tif, a speed, so the criterion applies only when the components are supplied as well:

StrandingRisk.for_fish("2100_sample", "Chinook salmon", "juvenile",
                       velocity_field={7250.0: ("ux007250.tif", "uy007250.tif")})

velocity_field takes a {discharge: (ux, uy)} mapping of arrays or paths, or a callable f(discharge, profile) -> (ux, uy). ux<Q>.tif and uy<Q>.tif beside the condition’s hydraulic rasters are picked up automatically. riverarchitect.stranding.StrandingRisk.velocity_limited reports whether the criterion is being applied, so a result can state it either way.

Do not approximate it with the speed alone

Treating a cell as impassable because it is fast, without knowing which way, is not a conservative simplification - it is a different analysis. On the sample reach at 7250 cfs only 0.4 % of the low-flow mainstem is slower than a juvenile Chinook’s 1.9 fps, and at 16 000 cfs none of it is: the mainstem itself becomes unreachable and almost the whole wetted area reads as stranded. This is why u_max on its own does nothing.

Riparian Seedling Recruitment

riverarchitect.recruitment - the Ecohydraulics ▸ Riparian Seedling Recruitment tab.

Cottonwood and willow seedlings establish only where four things happen in the right order over a single season, and this maps where all four coincide - the Recruitment Box Model of Braatne et al. (2007).

Objective

Question

Bed preparation

did a winter flow mobilise the bed, clearing a seedbed?

Recession rate (desiccation)

did the water table drop slowly enough for roots to follow?

Prolonged inundation

did the seedling stay under water long enough to drown?

Scour

did a later flow uproot it?

Each is scored 1 (good), 0.5 (stressed) or 0 (fatal), and the recruitment potential is their product - a seedling has to survive every one of them, so a zero anywhere is a zero overall.

This is the one module that needs a daily flow record: bed preparation, recession and scour are about when flows happened, not just which flows are possible.

Bed preparation and scour both ask whether the bed was mobile, which is the dimensionless bed shear stress \(\theta_{84}\) of riverarchitect.shear - the same regime-aware calculation Lifespan Design uses, described under dimensionless bed shear stress. Each run writes hks<Q>.tif and regime<Q>.tif per modelled discharge beside its objective rasters, showing the relative submergence and which resistance closure applied where.

Three details decide the result, and all three follow the original:

  • a stressful or lethal recession day is only counted where the cell is dry that day - a submerged seedling is not desiccating, however fast the surface is dropping;

  • a cell that goes dry during seed dispersal starts again from there, because that is when its seed actually landed, so both the counts and their denominator are per cell;

  • the inundation objective uses the longest run of consecutive submerged days, not their total. Fourteen days under water in one stretch drowns a seedling; fourteen days spread over a season does not.

In this section