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.
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.Hydraulic HSI rasters apply those curves to the depth and velocity rasters of a discharge, giving
dsiandvsi.Composite HSI (
cHSI) combines them by geometric mean or product, optionally with a cover HSI, and is masked to the wetted area.Usable area at a discharge is the area where
cHSIexceeds a threshold (0.4 by default), optionally weighted by the meancHSIthere.SHArea integrates usable area over the flow duration curve, so habitat that only exists at a rare discharge counts for little.
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 |
|---|---|---|
|
the grain size raster |
mapped through its suitability curve, like depth or velocity |
|
grain size 0.064 - 0.256 m |
presence, spread over the curve’s radius |
|
grain size above 0.256 m |
presence, spread over the curve’s radius |
|
|
presence, spread over the curve’s radius |
|
|
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 |
|
|---|---|
|
one per discharge |
|
the highest discharge at which each cell was disconnected: the flow at which that spot becomes a trap as the hydrograph recedes |
|
route length back to the mainstem, with |
|
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
- Seasonal Habitat Area Calculator (SHArC)
- Introduction to Habitat Suitability evaluation
- Quick GUIde to habitat suitability evaluation
- SHArC - Working Principles
- Working principles
- SHArC - Code modification
- Fish species and lifestages
- Changing the structure of
Fish.xlsx - Stranding Risk Assessment
- Introduction to Stranding Risk Module
- Quick GUIde to Stranding Risk Assessment
- Defining Travel Thresholds
- Methodology
- References
- Riparian Seedling Recruitment (RSR)
- Introduction to Riparian Seedling Recruitment Module
- Quick GUIde to Riparian Recruitment Assessment
- Defining Criteria
- Methodology
- References
See also
riverarchitect.sharc, riverarchitect.stranding and
riverarchitect.recruitment in the API reference.