From f901b0c8ffb349589e7866c8f7698ce7cd98849a Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Mon, 3 Jun 2024 15:58:47 +0200 Subject: [PATCH 1/3] First draft of BMI docs page --- docs/contribute/bmi.md | 44 +++++++++++++++++++++++++++++++++++++++ docs/contribute/index.qmd | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/contribute/bmi.md diff --git a/docs/contribute/bmi.md b/docs/contribute/bmi.md new file mode 100644 index 000000000..cfe4a2d9f --- /dev/null +++ b/docs/contribute/bmi.md @@ -0,0 +1,44 @@ +# Basic Model Interface (BMI) + +For runtime data exchange and coupling with other kernels, the Julia kernel is wrapped in a Python API (`ribasim_api`) which implements the Basic Modelling Interface [BMI](https://bmi-spec.readthedocs.io/en/latest/). + +## Functions + +The following functions are available to interact with the Ribasim model" + +signature | description +------------------------- | ------------- +`initialize(config_path)` | Initialize a model from the path to the TOML configuration file +`finalize()` | Write all results to the configured files +`get_current_time()` | Get the current time of the Ribasim simulation +`get_end_time()` | Get the final time of the Ribasim simulation in seconds +`get_start_time()` | Get the start time of the Ribasim simulation (`0.0`) +`get_time_step()` | Get the proposed next internal Ribasim timestep +`get_time_units()` | Get the time unit (`s`) +`get_value_ptr(string)` | Get the pointer to a Ribasim internal array (see below) +`update()` | Set a single Ribasim internal time step +`update_until(time)` | Set Ribasim internal timesteps until the specified time + +Depending on what is specified in the Ribasim TOML configuration file, Ribasim can internally have adaptive (non-constant) timesteps. `update_until` will always try to progress the Ribasim simulation to exactly the time specified. This however can fail for algorithms that only support a fixed timestep if that timestep does not fit into the interval until the specified time an integer amount of times. + +## Data pointers + +The following internal array data is exposed via the BMI using `get_value_ptr(string)`: + +string | meaning | type | unit | sorted by +------------------------------- | -------------------------------------- | ------- | ------------ | ---------- +`basin.storage` | storage per basin | Float64 | $m^3$ | basin node ID +`basin.level` | level per basin | Float64 | $m$ | basin node ID +`basin.infiltration` | infiltration per basin | Float64 | $m^3 s^{-1}$ | basin node ID +`basin.drainage` | drainage per basin | Float64 | $m^3 s^{-1}$ | basin node ID +`basin.infiltration_integrated` | time integrated infiltration per basin | Float64 | $m^3$ | basin node ID +`basin.drainage_integrated` | time integrated drainage per basin | Float64 | $m^3$ | basin node ID +`basin.subgrid_level` | subgrid level | Float64 | $m$ | subgrid ID +`user_demand.demand` | demand per node ID per priority | Float64 | $m^3 s^{-1}$ | user_demand node ID, priority index +`user_demand.realized` | time integrated intake flow per user | Float64 | $m^3$ | user_demand node ID + +Additional notes: + +- `user_demand.demand` yields the only 2D array, the other arrays are 1D. This array is indexed as `(node_idx, priority_idx)` +- The index of e.g. basins and user demand nodes needs to be inferred from the Ribasim input. The same holds for `priority_idx`, which is global over all subnetworks +- The `*_integrated` values are integrated from the start of the simulation onward. It might be beneficial to reset this to $0$ at certain times for long simulations to avoid loss of accuracy. diff --git a/docs/contribute/index.qmd b/docs/contribute/index.qmd index 2f787dee9..dc4976bca 100644 --- a/docs/contribute/index.qmd +++ b/docs/contribute/index.qmd @@ -4,7 +4,7 @@ title: "Contributing" Ribasim welcomes contributions. -There is developer documentation for the [Julia core](core.qmd), [Python tooling](python.qmd), and the [QGIS plugin](qgis.qmd). +There is developer documentation for the [Julia core](core.qmd), the [Basic Model Interface (BMI)](bmi.qmd), [Python tooling](python.qmd), and the [QGIS plugin](qgis.qmd). A guide on how to add a new node type to both is written in [adding node types](addnode.qmd). [Release process](release.qmd) describes the steps to follow when creating a new Ribasim release. From 3a7948b8a972d7ef769157c3f7c70b1785402187 Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Thu, 6 Jun 2024 16:06:35 +0200 Subject: [PATCH 2/3] Comments adressed --- core/src/callback.jl | 18 ++++++++-------- docs/_quarto.yml | 1 + docs/contribute/{bmi.md => bmi.qmd} | 32 +++++++++++++++-------------- 3 files changed, 27 insertions(+), 24 deletions(-) rename docs/contribute/{bmi.md => bmi.qmd} (52%) diff --git a/core/src/callback.jl b/core/src/callback.jl index 831c216db..211ca4956 100644 --- a/core/src/callback.jl +++ b/core/src/callback.jl @@ -113,6 +113,15 @@ function integrate_flows!(u, t, integrator)::Nothing user_demand.realized_bmi[i] += 0.5 * (flow[flow_idx] + flow_prev[flow_idx]) * dt end + # *Demand realized flow for output + for (edge, value) in allocation.mean_realized_flows + if edge[1] !== edge[2] + value += + 0.5 * (get_flow(graph, edge..., 0) + get_flow_prev(graph, edge..., 0)) * dt + allocation.mean_realized_flows[edge] = value + end + end + # Allocation source flows for (edge, value) in allocation.mean_input_flows if edge[1] == edge[2] @@ -131,15 +140,6 @@ function integrate_flows!(u, t, integrator)::Nothing end end - # Realized demand flows - for (edge, value) in allocation.mean_realized_flows - if edge[1] !== edge[2] - value += - 0.5 * (get_flow(graph, edge..., 0) + get_flow_prev(graph, edge..., 0)) * dt - allocation.mean_realized_flows[edge] = value - end - end - copyto!(flow_prev, flow) copyto!(vertical_flux_prev, vertical_flux) return nothing diff --git a/docs/_quarto.yml b/docs/_quarto.yml index b3974b853..4055a671c 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -63,6 +63,7 @@ website: - contribute/python.qmd - contribute/qgis.qmd - contribute/addnode.qmd + - contribute/bmi.qmd - contribute/ci.qmd - contribute/release.qmd diff --git a/docs/contribute/bmi.md b/docs/contribute/bmi.qmd similarity index 52% rename from docs/contribute/bmi.md rename to docs/contribute/bmi.qmd index cfe4a2d9f..91c103e86 100644 --- a/docs/contribute/bmi.md +++ b/docs/contribute/bmi.qmd @@ -16,29 +16,31 @@ signature | description `get_time_step()` | Get the proposed next internal Ribasim timestep `get_time_units()` | Get the time unit (`s`) `get_value_ptr(string)` | Get the pointer to a Ribasim internal array (see below) -`update()` | Set a single Ribasim internal time step +`update()` | Perform a Ribasim internal time step `update_until(time)` | Set Ribasim internal timesteps until the specified time Depending on what is specified in the Ribasim TOML configuration file, Ribasim can internally have adaptive (non-constant) timesteps. `update_until` will always try to progress the Ribasim simulation to exactly the time specified. This however can fail for algorithms that only support a fixed timestep if that timestep does not fit into the interval until the specified time an integer amount of times. -## Data pointers +## Memory pointers -The following internal array data is exposed via the BMI using `get_value_ptr(string)`: +The following pointers to memory containing Ribasim internal arrays are given via the BMI using `get_value_ptr(string)`: -string | meaning | type | unit | sorted by -------------------------------- | -------------------------------------- | ------- | ------------ | ---------- -`basin.storage` | storage per basin | Float64 | $m^3$ | basin node ID -`basin.level` | level per basin | Float64 | $m$ | basin node ID -`basin.infiltration` | infiltration per basin | Float64 | $m^3 s^{-1}$ | basin node ID -`basin.drainage` | drainage per basin | Float64 | $m^3 s^{-1}$ | basin node ID -`basin.infiltration_integrated` | time integrated infiltration per basin | Float64 | $m^3$ | basin node ID -`basin.drainage_integrated` | time integrated drainage per basin | Float64 | $m^3$ | basin node ID -`basin.subgrid_level` | subgrid level | Float64 | $m$ | subgrid ID -`user_demand.demand` | demand per node ID per priority | Float64 | $m^3 s^{-1}$ | user_demand node ID, priority index -`user_demand.realized` | time integrated intake flow per user | Float64 | $m^3$ | user_demand node ID +string | meaning | type | unit | temporal type | writable | sorted by +------------------------------- | -------------------------------------- | ------- | ------------ | --------------------- | -------- |---------- +`basin.storage` | storage per basin | Float64 | $m^3$ | instantaneous | no | basin node ID +`basin.level` | level per basin | Float64 | $m$ | instantaneous | no | basin node ID +`basin.infiltration` | infiltration flux per basin | Float64 | $m^3 s^{-1}$ | forward fill | yes | basin node ID +`basin.drainage` | drainage flux per basin | Float64 | $m^3 s^{-1}$ | forward fill | yes | basin node ID +`basin.infiltration_integrated` | cumulative infiltration per basin | Float64 | $m^3$ | integrated from start | yes | basin node ID +`basin.drainage_integrated` | cumulative drainage per basin | Float64 | $m^3$ | integrated from start | yes | basin node ID +`basin.subgrid_level` | subgrid level | Float64 | $m$ | instantaneous | no | subgrid ID +`user_demand.demand` | demand per node ID per priority | Float64 | $m^3 s^{-1}$ | forward fill | yes | user_demand node ID, priority index +`user_demand.realized` | cumulative intake flow per user | Float64 | $m^3$ | integrated from start | yes | user_demand node ID Additional notes: - `user_demand.demand` yields the only 2D array, the other arrays are 1D. This array is indexed as `(node_idx, priority_idx)` - The index of e.g. basins and user demand nodes needs to be inferred from the Ribasim input. The same holds for `priority_idx`, which is global over all subnetworks -- The `*_integrated` values are integrated from the start of the simulation onward. It might be beneficial to reset this to $0$ at certain times for long simulations to avoid loss of accuracy. +- The data being writable means that Ribasim takes into account the possibility that the data is updated outiside the Ribasim core +- Although the `*_integrated` and `*_realized` data is writable, this doesn't affect the Ribasim simulation. This integrated data is only computed for the BMI, and can be set to $0$ via the BMI to avoid accuracy problems when the values get too large. +- Different from what is exposed via the BMI, the basin forcings and realized user demands are averaged over the allocation timestep and saveat interval respectively. From 3028ea9be459cd7622550b77e6b480f1d301d599 Mon Sep 17 00:00:00 2001 From: Bart de Koning Date: Wed, 12 Jun 2024 11:48:30 +0200 Subject: [PATCH 3/3] Indicate that Julia is column major --- docs/contribute/bmi.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contribute/bmi.qmd b/docs/contribute/bmi.qmd index 91c103e86..7d840d10d 100644 --- a/docs/contribute/bmi.qmd +++ b/docs/contribute/bmi.qmd @@ -39,7 +39,7 @@ string | meaning | type Additional notes: -- `user_demand.demand` yields the only 2D array, the other arrays are 1D. This array is indexed as `(node_idx, priority_idx)` +- `user_demand.demand` yields the only 2D array, the other arrays are 1D. This array is indexed as `(node_idx, priority_idx)` in Julia, which stores arrays column-major - The index of e.g. basins and user demand nodes needs to be inferred from the Ribasim input. The same holds for `priority_idx`, which is global over all subnetworks - The data being writable means that Ribasim takes into account the possibility that the data is updated outiside the Ribasim core - Although the `*_integrated` and `*_realized` data is writable, this doesn't affect the Ribasim simulation. This integrated data is only computed for the BMI, and can be set to $0$ via the BMI to avoid accuracy problems when the values get too large.