Skip to content

Experimental graph-based streamflow modelling system in Julialang

License

Notifications You must be signed in to change notification settings

ConnectedSystems/Streamfall.jl

Repository files navigation

Streamfall.jl

Streamfall: An experimental graph-based streamflow modelling system written in Julialang.

DOI

Aims of the project are to leverage the Julia language and ecosystem to support:

  • Quick application and exploratory analysis
  • Use of different rainfall-runoff models and their ensembles in tandem
  • Modelling and assessment of interacting systems
  • Parallel scenario runs

Streamfall now includes tentative implementations of GR4J, HyMod, IHACRES, and SYMHYD. The IHACRES rainfall-runoff model is implemented with ihacres_nim.

Graphs and MetaGraphs are used underneath for network traversal/analysis.

Development version of the documentation can be found here.

[NOTE] Streamfall is currently in its early stages and under active development. Although it is fairly usable for small networks and single node analyses, things may change drastically and unexpectedly.

Development

To build locally:

$ julia --project=.
julia>] build

To run tests:

julia>] test

Quick start (prep)

using YAML, DataFrames, CSV, Plots
using Statistics
using Streamfall, BlackBoxOptim


# Load observations
date_format = "YYYY-mm-dd"

# Load file which holds streamflow, precipitation and PET data
obs_data = CSV.File("example_data.csv",
                    comment="#",
                    dateformat=date_format) |> DataFrame

# Historic observations
Qo = obs_data[:, ["Date", "Gauge_Q"]]

# Create climate data interface (all climate data are expected to have a "Date" column)
climate_data = obs_data[:, ["Date", "Gauge_P", "Gauge_PET"]]
climate = Climate(climate_data, "_P", "_PET")

Quick start (single node)

# Create a node (node type, node_name, sub-catchment area)
hymod_node = create_node(SimpleHyModNode, "Gauge", 129.2)
# gr4j_node = create_node(GR4JNode, "Gauge", 129.2)
# ihacres_node = create_node(BilinearNode, "Gauge", 129.2)

# Calibrate a node for 30 seconds (uses the BlackBoxOptim package)
# Default metric used is RMSE
calibrate!(hymod_node, climate, Qo; MaxTime=30)

# Basic overview plot (shows time series and Q-Q plot)
# Uses a 365 day offset (e.g., 1 year burn-in period)
run_node!(hymod_node, climate)
quickplot(Qo[:, "Gauge_Q"], hymod_node, climate, "HyMod"; burn_in=366, limit=nothing)

# save figure
savefig("quick_example.png")

Quick start (network of nodes)

# Load and generate stream network
network_spec = YAML.load_file("network.yml")
sn = create_network("Example Network", network_spec)

# Show figure of network
plot_network(sn)

# Calibrate network using the BlackBoxOptim package
# keyword arguments will be passed to the `bboptimize()` function
calibrate!(sn, climate, Qo; MaxTime=180.0)

# Run stream network
# There is also `run_catchment!()` which does the same thing
run_basin!(sn, climate)

# Get a specific node in network
node = sn[1]  # get node 1

# Could also get node by name
# which will also return its position in the network:
# nid, node = sn["node1"]

# Compare "goodness-of-fit"
Streamfall.RMSE(obs_streamflow, node.outflow)

# Save calibrated network spec to file
Streamfall.save_network_spec(sn, "calibrated_example.yml")

To display an overview of a node or network:

julia> node
Name: 406219 [BilinearNode]
Area: 1985.73
┌──────────────┬───────────┬─────────────┬─────────────┐
│    Parameter │     Value │ Lower Bound │ Upper Bound │
├──────────────┼───────────┼─────────────┼─────────────┤
│            d │   84.280210.0550.0 │
│           d2 │   2.422410.000110.0 │
│            e │  0.8129590.11.5 │
│            f │   2.579280.013.0 │
│            a │   5.923380.110.0 │
│            b │ 0.09899260.0010.1 │
│ storage_coef │   1.861341.0e-1010.0 │
│        alpha │  0.7279051.0e-51.0 │
└──────────────┴───────────┴─────────────┴─────────────┘

More information

Stream networks are specified as Dictionaries, with an entry for each node.

An example spec from a YAML file is shown here, with connectivity between nodes defined by their names.

# ... Partial snippet of stream definition as an example ...

Node3:
    node_type: IHACRESNode  # node type, typically tied to the attached model
    inlets:  # nodes that contribute incoming streamflow
        - Node1
        - Node2
    outlets: Node4  # node that this node flows to
    area: 150.0  # subcatchment area in km^2
    parameters:
        # model specific parameters defined here
        ...

A full example of the spec is available here. The snippet above defines Node 3 in the diagram below.

Each node defines a subcatchment and holds the relevant parameter values for the associated model. In the future, it will be possible to read in stream network information from other formats (e.g., GeoPackage).

Running a network

using YAML, DataFrames, CSV, Plots
using Streamfall


# Load and generate stream network
# Creates a graph representation of the stream with associated metadata.
network = YAML.load_file("../test/data/campaspe/campaspe_network.yml")

# Name of network/catchment and its specification
sn = create_network("Example Network", network)

# Load climate data - in this case from a CSV file with data for all nodes.
climate_data = CSV.File("../test/data/campaspe/climate/climate_historic.csv",
                        comment="#",
                        dateformat="YYYY-mm-dd") |> DataFrame

# Indicate which columns are precipitation and evaporation data based on partial identifiers
climate = Climate(climate_data, "_rain", "_evap")


# This runs an entire stream network
@info "Running an example stream..."
run_catchment!(sn, climate)

@info "Displaying outflow from node 406219"
node_id, node = sn["406219"]
plot(node.outflow)

Individual nodes can be run for more fine-grain control.

# Run up to a point in the stream for all time steps.
# All nodes upstream will be run as well (but not those downstream)
node_id, node = sn["406219"]
run_node!(sn, node_id, climate)

# Reset a node (clears stored states)
reset!(node)

# Run a specific node, and only a specific node, for all time steps
inflow = ...      # array of inflows for each time step
extractions = ... # extractions from stream for each time step
gw_flux = ...     # forced groundwater interactions for each time step
run_node!(node, climate; inflow=inflow, extraction=extractions, exchange=gw_flux)

Another approach is to identify the outlets for a given network...

inlets, outlets = find_inlets_and_outlets(sn)

... and call run_node! for each outlet (with relevant climate data), which will recurse through all relevant nodes upstream.

@info "Running example stream..."
timesteps = sim_length(climate)
for ts in (1:timesteps)
    for outlet in outlets
        run_node!(sn, outlet, climate, ts)
    end
end

See the docs for an overview and example applications.

Further preliminary usage examples are provided in the examples directory.

About

Experimental graph-based streamflow modelling system in Julialang

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages