riverarchitect.flows

Flow analysis: seasonal flow duration curves and annual peak series.

The open-source replacement for the Analyze Flows part of the ArcGIS GetStarted module (riverpy/cFlows.py) and for the Tools/make_annual_*.py scripts.

Why it matters

riverarchitect.sharc integrates usable habitat area over a flow duration curve to produce SHArea, the single number a habitat project is judged on. That curve is not something the hydrodynamic model produces: it comes from a gauged daily flow record, and it is seasonal - Chinook fry occupy the reach between 1 February and 15 June, so the flows of the rest of the year say nothing about their habitat. Without this module a project has 2D results, habitat suitability curves and no way to weigh one discharge against another.

The chain

  1. Read a daily record of dates and mean daily discharge (FlowSeries).

  2. Take every flow that falls inside a lifestage’s season, in every year of the record, and sort them descending. Rank i of n is exceeded i / n * 100 per cent of that season - the seasonal flow duration curve (FlowSeries.duration_curve()).

  3. Interpolate that curve at each discharge the 2D model was run for (FlowSeries.exceedance_of()), because those are the only discharges habitat can be evaluated at.

  4. Write 00_Flows/<condition>/flow_duration_<code>.xlsx in the layout riverarchitect.sharc.read_flow_duration() expects (FlowSeries.write_flow_duration()).

Also here: FlowSeries.annual_peaks(), the annual maximum series that a flood frequency analysis needs to attach a return period to each modelled discharge - the input_definitions.inp “Return periods” line that lifespan mapping reads. The original exported it for HEC-SSP; return_periods() additionally fits a Gumbel distribution so that a first estimate needs no second program.

Relation to the original

cFlows.SeasonalFlowProcessor built a n_days x n_years matrix by walking the record day by day, with a hand-rolled leap-day skip and an index that had to stay in step with the file. The result is a plain “collect the flows inside the season, sort, rank”, which is what this does. Leap days are kept rather than dropped: excluding 29 February biased the curve by one day in four years for no reason the original documented.

class riverarchitect.flows.FlowSeries(series, unit='us', fish=None)[source]

Bases: object

A daily flow record, and the duration curves derived from it.

Parameters:
  • series (dict or str) – {date: discharge}, or a path to read one from.

  • unit (str) – "us" or "si"; only used for labels.

  • fish (riverarchitect.sharc.FishDatabase) – the database seasons are read from. Built on demand.

Variables:

series (dict) – the record, ascending by date.

property fish

The habitat database, loaded on first use.

property years

Calendar years the record spans.

season_flows(start=None, end=None)[source]

Every discharge inside a season, across every year of the record.

Parameters:
  • start (tuple) – (month, day) bounds. Omit both for the whole year.

  • end (tuple) – (month, day) bounds. Omit both for the whole year.

Returns:

the flows, unsorted.

Return type:

numpy.ndarray

duration_curve(start=None, end=None)[source]

Seasonal flow duration curve: discharge against per-cent exceedance.

Rank i of n sorted descending is exceeded i / n * 100 per cent of the season, which is the plotting position the original used. The highest flow therefore carries a small non-zero exceedance rather than zero, and the lowest carries exactly 100.

Returns:

(discharge, exceedance), discharge descending.

Return type:

tuple

exceedance_of(discharges, start=None, end=None)[source]

Per-cent of the season each discharge is exceeded.

Interpolated on the duration curve. A discharge above everything on record is exceeded 0 per cent of the time and one below everything 100 per cent, which is what cFlows.interpolate_flow_exceedance returned at those ends.

Parameters:
  • discharges (iterable) – the discharges to evaluate, normally the ones the 2D model was run for.

  • start (tuple) – season bounds, as (month, day).

  • end (tuple) – season bounds, as (month, day).

Returns:

{discharge: per-cent exceedance}.

Return type:

dict

annual_peaks(water_year_start=(10, 1))[source]

Largest discharge of each water year.

Parameters:

water_year_start (tuple) – (month, day) the water year begins on. The default is 1 October, the U.S. convention. Pass (1, 1) for calendar years.

Returns:

{water year: peak discharge}. The year label is the one the water year ends in, so 1 October 2019 to 30 September 2020 is 2020.

Return type:

dict

write_flow_duration(path, discharges=None, start=None, end=None, code='', condition='', limit=20000)[source]

Write a flow_duration_<code>.xlsx in the layout SHArC reads.

Two sheets, matching the workbooks the original produced:

Q data

the full duration curve, discharge in column A and per-cent exceedance in column B from row 3, with the species code and the season in columns D and E.

2D data

the modelled discharges in column B and their interpolated exceedance in column C from row 3 - the sheet riverarchitect.sharc.read_flow_duration() reads.

Parameters:
  • path (str) – where to write.

  • discharges (iterable) – the modelled discharges for the 2D data sheet.

  • start (tuple) – season bounds as (month, day).

  • end (tuple) – season bounds as (month, day).

  • code (str) – four-letter species/lifestage code, for the header.

  • condition (str) – condition name, for the header.

  • limit (int) – cap on the number of Q data rows written. A 50-year record has 18 000 of them; beyond a point they are only file size.

Returns:

the path written.

Return type:

str

riverarchitect.flows.read_flow_series(path, date_column=0, discharge_column=1, sheet=None)[source]

Read a daily flow record from a spreadsheet or CSV.

Parameters:
  • path (str) – .xlsx, .xls or .csv with a date column and a mean daily discharge column.

  • date_column (int) – zero-based index of the date column.

  • discharge_column (int) – zero-based index of the discharge column.

  • sheet (str or int) – worksheet, for a workbook with more than one.

Returns:

{datetime.date: discharge}, ascending by date.

Return type:

dict

riverarchitect.flows.seasonal_flow_duration(series, condition, discharges=None, pairs=None, unit='us', output_dir=None, fish=None)[source]

Build a flow duration workbook for every species and lifestage.

This is Analyze Flows: the step between a gauge record and a SHArea calculation. One workbook per species and lifestage lands in 00_Flows/<condition>/, named with the four-letter code SHArC looks for, so riverarchitect.sharc.SHArC.run() finds it without being told where it is.

Parameters:
  • series (FlowSeries or dict or str) – the daily record.

  • condition (str) – condition name; sets the output folder and the discharges.

  • discharges (iterable) – the modelled discharges. Defaults to every h<Q>.tif in the condition.

  • pairs (iterable) – (species, lifestage) tuples. Defaults to every pair the fish database defines that has a season.

  • unit (str) – "us" or "si".

  • output_dir (str) – where the workbooks go. Defaults to 00_Flows/<condition>/.

  • fish (riverarchitect.sharc.FishDatabase) – the database to read seasons from.

Returns:

one summary dict per workbook written.

Return type:

list

riverarchitect.flows.return_periods(peaks, discharges=None)[source]

Flood return periods from an annual maximum series, by a Gumbel fit.

The Return periods line of input_definitions.inp is what turns a set of modelled discharges into a lifespan axis, and it is the input most often missing. The original exported the annual peaks for HEC-SSP and left the fitting to it; this gives a defensible first estimate in one step, by the method of moments on a Gumbel (EV1) distribution:

\[\alpha = \frac{s\sqrt{6}}{\pi}, \quad u = \bar{x} - \gamma\alpha, \quad T = \frac{1}{1 - \exp(-\exp(-(x - u) / \alpha))}\]
Parameters:
  • peaks (dict or iterable) – annual peak discharges, or {year: peak}.

  • discharges (iterable) – discharges to evaluate. Defaults to the peaks themselves.

Returns:

{discharge: return period in years}, never below 1.0.

Return type:

dict

Raises:

ValueError – with fewer than ten peaks, where a Gumbel fit is not worth quoting.

riverarchitect.flows.GUMBEL_EULER = 0.5772156649015329

Euler-Mascheroni constant, for the Gumbel moment estimator.