Repository: Deltares/Wflow.jl (language: python)

Issue:
Refactor vertical `SBM` concept
### Feature type

Changing existing functionality

### Improvement Description

Subdivide long struct `SBM` into different model components (e.g. snow, glacier, interception, soil) to improve code maintenance and it allows to add other model components more easily to the code (flexibility).   

### Implementation Description

_No response_

### Additional Context

_No response_
Refactor vertical `SBM` concept
### Feature type

Changing existing functionality

### Improvement Description

Subdivide long struct `SBM` into different model components (e.g. snow, glacier, interception, soil) to improve code maintenance and it allows to add other model components more easily to the code (flexibility).   

### Implementation Description

_No response_

### Additional Context

_No response_
Fix initialization of LateralSSF variables `ssf` and `ssfmax` with `ksat_profile` `exponential_constant`
## Issue addressed
Fixes #473 

## Explanation
See issue #473

## Checklist
- [x] Updated tests or added new tests
- [x] Branch is up to date with `master`
- [x] Tests & pre-commit hooks pass
- [x] Updated documentation if needed
- [x] Updated changelog.md if needed


Discussion / hints:

Fix patch:
diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml
new file mode 100644
index 000000000..4b8f03a0c
--- /dev/null
+++ b/.JuliaFormatter.toml
@@ -0,0 +1,12 @@
+# Options for the JuliaFormatter auto syntax formatting tool.
+# https://domluna.github.io/JuliaFormatter.jl/stable/
+# https://docs.sciml.ai/SciMLStyle/stable/
+
+# Based on the default style we do pick these non-default options from SciML style:
+whitespace_ops_in_indices = true
+remove_extra_newlines = true
+always_for_in = true
+whitespace_typedefs = true
+
+# And add other options we like:
+separate_kwargs_with_semicolon = true
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 868c5ee46..60504c78b 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - master
+      - v1
     tags: '*'
 jobs:
   test:
diff --git a/.github/workflows/CIWflowServer.yml b/.github/workflows/CIWflowServer.yml
index a051bcf52..126361940 100644
--- a/.github/workflows/CIWflowServer.yml
+++ b/.github/workflows/CIWflowServer.yml
@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - master
+      - v1
     tags: '*'
 jobs:
   test:
diff --git a/.gitignore b/.gitignore
index 604242d59..aff6d881c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,6 @@
 # General
 .DS_Store
 
-# VS Code stuff
-.vscode
-
 # Packaging stuff
 *.jl.*.cov
 *.jl.cov
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..c24c4ee94
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+    "[julia]": {
+        "editor.formatOnSave": true,
+    },
+    "julia.lint.disabledDirs": [
+        ".pixi"
+    ],
+    "julia.lint.run": true,
+}
diff --git a/docs/make.jl b/docs/make.jl
index a710b5fb3..22dd23a5d 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -23,8 +23,6 @@ pages = [
         "model_docs/model_configurations.md",
         "Vertical concepts" => [
             "model_docs/vertical/sbm.md",
-            "model_docs/vertical/hbv.md",
-            "model_docs/vertical/flextopo.md",
             "model_docs/vertical/sediment.md",
             "model_docs/shared_concepts.md",
         ],
diff --git a/docs/src/changelog.md b/docs/src/changelog.md
index de9f4cffb..72e5ae536 100644
--- a/docs/src/changelog.md
+++ b/docs/src/changelog.md
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## Unreleased
+
+### Fixed
+- Initialization of `LateralSSF` variables `ssf` and `ssfmax` with vertical hydraulic
+  conductivity profile `exponential_constant`. Removed parameter `khfrac` from the
+  computation, as it is already part of parameter `kh_0`.
+
+### Changed
+- Removed vertical concepts `HBV` and `FLEXTopo`.
+- Removed metadata macros `exchange` and `grid_type`. The macro `grid_type` is not required
+  because this functionality is already part of `BMI`. The macro `exchange` is replaced by a
+  function used by `BMI`. Remaining metadata macros `get_units` and `grid_loc` are only used
+  by `BMI`.
+- Refactor the vertical `SBM` concept: divide the long struct `SBM` into different model
+  components for interception, snow, glacier, (open water) runoff, soil, water demand and
+  allocation stored in the struct `LandHydrologySBM`. Additionally, the atmospheric forcing
+  and a shared vegetation parameterset are stored as separate fields in struct
+  `LandHydrologySBM` (with soil model `SbmSoilModel`). The model component structs have
+  model `variables`, `parameters` and `boundary_conditions` (if applicable), including
+  associated functions for initializing and updating these model components. The original
+  long update function of the `SBM` soil part has been split into separate functions. 
+
+### Added
+- Support direct output of snow and glacier melt, and add computation of snow water
+  equivalent (SWE).
+
 ## v0.8.1 - 2024-08-27
 
 ### Fixed
diff --git a/docs/src/images/flextopo_julia_1class.png b/docs/src/images/flextopo_julia_1class.png
deleted file mode 100644
index cb662a324..000000000
Binary files a/docs/src/images/flextopo_julia_1class.png and /dev/null differ
diff --git a/docs/src/images/flextopo_julia_3class.png b/docs/src/images/flextopo_julia_3class.png
deleted file mode 100644
index dd8175800..000000000
Binary files a/docs/src/images/flextopo_julia_3class.png and /dev/null differ
diff --git a/docs/src/images/hbv-soilmoist.png b/docs/src/images/hbv-soilmoist.png
deleted file mode 100644
index 1e6e3ed7d..000000000
Binary files a/docs/src/images/hbv-soilmoist.png and /dev/null differ
diff --git a/docs/src/images/hbv-upper.png b/docs/src/images/hbv-upper.png
deleted file mode 100644
index 4aae2eb12..000000000
Binary files a/docs/src/images/hbv-upper.png and /dev/null differ
diff --git a/docs/src/images/hbv96.png b/docs/src/images/hbv96.png
deleted file mode 100644
index aa95d9f6e..000000000
Binary files a/docs/src/images/hbv96.png and /dev/null differ
diff --git a/docs/src/model_docs/model_configurations.md b/docs/src/model_docs/model_configurations.md
index 814ddbfd2..074082107 100644
--- a/docs/src/model_docs/model_configurations.md
+++ b/docs/src/model_docs/model_configurations.md
@@ -131,128 +131,6 @@ lateral.land => struct ShallowWaterLand{T}
 The local inertial approach is described in more detail in the section [Local inertial
 model](@ref local_inertial).
 
-## [wflow\_hbv](@id config_hbv)
-The Hydrologiska Byrans Vattenbalansavdelning (HBV) model was introduced back in 1972 by the
-Swedish Meteological and Hydrological Institute (SMHI).  The HBV model is mainly used for
-runoff simulation and hydrological forecasting. The model is particularly useful for
-catchments where snow fall and snow melt are dominant factors, but application of the model
-is by no means restricted to these type of catchments.
-
-The model is based on the HBV-96 model. However, the hydrological routing represented in HBV
-by a triangular function controlled by the MAXBAS parameter has been removed. Instead, the
-kinematic wave function is used to route the water downstream. All runoff that is generated
-in a cell in one of the HBV reservoirs is added to the kinematic wave reservoir at the end
-of a timestep. There is no connection between the different HBV cells within the model.
-
-A catchment is divided into a number of grid cells. For each of the cells individually,
-daily runoff is computed through application of the HBV-96 of the HBV model. The use of the
-grid cells offers the possibility to turn the HBV modelling concept, which is originally
-lumped, into a distributed model.
-
-![wflow_hbv model](../images/hbv96.png)
-
-The figure above shows a schematic view of hydrological response simulation with the
-HBV-modelling concept. The land-phase of the hydrological cycle is represented by three
-different components: a snow routine, a soil routine and a runoff response routine. Each
-component is discussed in more detail below.
-
-The vertical HBV concept is described in section [HBV vertical concept](@ref vert_hbv). The
-routing for river and overland flow is described in the section [Kinematic wave](@ref
-kin_wave).
-
-Below the mapping for wflow\_hbv (type `hbv`) to the vertical HBV concept (instance of
-`struct HBV`) and the different lateral concepts. For an explanation about the type
-parameters between curly braces after the `struct` name see the section on model parameters.
-
-```julia
-vertical => struct HBV{T}
-lateral.subsurface => struct LateralSSF{T}
-lateral.land => struct SurfaceFlow{T,R,L}
-lateral.river => struct SurfaceFlow{T,R,L}
-lateral.river.lake => struct NaturalLake{T} # optional
-lateral.river.reservoir => struct SimpleReservoir{T} # optional
-```
-
-## [wflow\_flextopo](@id config_flextopo)
-The FLEXTopo model is a process-based model, which consists of different parallel classes
-connected through their groundwater storage. These classes are usually delineated from
-topographical data to represent the variability in hydrological processes across
-user-defined Hydrological Response Units (HRU). The main assumption underlying the concept,
-which was first introduced by Savenije (2010), is that different parts of the landscape
-fulfill different tasks in runoff generation and, hence, can be represented by different
-model structures. The strength of the concept is that the definition of classes and
-associated model structures is modular and flexible and not fixed to a predefined model
-structure. The flexible approach allows to develop process-based models for different
-topographic, climatic, geologic and land use conditions, making use of the available data
-and expert knowledge.
-
-The kinematic wave function is used to route the water downstream. In a similar way as for
-HBV, all runoff that is generated in a cell in one of the FLEXTopo storages is added to the
-kinematic wave reservoir at the end of a timestep. There is no connection between the
-different vertical FLEXTopo cells within the model. The FLEXTopo model is implemented in a
-fully distributed way in the wflow Julia framework.
-
-In wflow\_flextopo, the user is free to determine the number of classes and which model
-components to include or exclude for each class, this is done in the TOML file. Currently,
-for each storage, it is possible to bypass the storage and pass on the fluxes to the next
-model component. Interested users can contribute to the code by adding other
-conceptualizations for each storage components.
-
-```julia
-[model]
-type = "flextopo"
-classes = ["h", "p", "w"]     #user can set the number and name of each class.
-
-# for each component which is class specific, the user can select which conceptualization
-# to apply for each class as defined above in classes = ["h", "p", "w"]
-select_snow = ["common_snow_hbv"]
-# available options are ["common_snow_hbv", "common_snow_no_storage"]
-select_interception = ["interception_overflow", "interception_overflow", "interception_overflow"]
-# available options are ["interception_overflow", "interception_no_storage"]
-select_hortonponding = ["hortonponding_no_storage", "hortonponding_no_storage", "hortonponding_no_storage"]
-# available options are ["hortonponding", "hortonponding_no_storage"]
-select_hortonrunoff = ["hortonrunoff_no_storage", "hortonrunoff_no_storage", "hortonrunoff_no_storage"]
-# available options are ["hortonrunoff", "hortonrunoff_no_storage"]
-select_rootzone = ["rootzone_storage", "rootzone_storage", "rootzone_storage"]
-# available options are ["rootzone_storage", "rootzone_no_storage"]
-select_fast = ["fast_storage", "fast_storage", "fast_storage"]
-# available options are ["fast_storage", "fast_no_storage"]
-select_slow = ["common_slow_storage"]
-# available options are ["common_slow_storage", "slow_no_storage"]
-```
-
-A schematic representation of the most complete model structure including all storage
-components, as currently implemented in the code, is shown in the Figure below. When setting
-up the model with multiple classes, model structures can be adapted by bypassing storages or
-turning parameter values on or off (e.g.: percolation or capillary rise, non-linear versus
-linear outflow of the fast runoff etc.), an example of a three class model is shown in
-[FLEXTopo vertical concept](@ref vert_flextopo).
-
-![flextopo_julia_1class.png](../images/flextopo_julia_1class.png)
-*Schematic representation of the FLEXTopo model for a single class model including all
-storages and fluxes. Main parameters are denoted in red.*
-
-In the staticmaps, the user needs to provide maps of the fraction of each class within each
-cell, as shown below with `hrufrac`. For each model parameter which is class specific, an
-extra dimension `classes` is required in the staticmaps netcdf. For an example model, see
-[FLEXTopo example model](@ref wflow_flextopo_data).
-
-```julia
-[input.vertical]
-hrufrac = "hrufrac_lu"
-```
-
-Parameter multiplication of model parameters which are defined for several classes is
-possible through the TOML file:
-
-```julia
-[input.vertical.kf]
-netcdf.variable.name = "kf"
-scale = [1.0, 3.0, 4.0]
-offset = [0.0, 0.0, 0.0]
-class = ["h", "p", "w"]
-```
-
 ## [wflow\_sediment](@id config_sediment)
 The processes and fate of many particles and pollutants impacting water quality at the
 catchment level are intricately linked to the processes governing sediment dynamics. Both
@@ -292,8 +170,7 @@ required dynamic inputs to run wflow\_sediment are:
 -  River water level in the kinematic wave,
 -  Rainfall interception by the vegetation.
 
-These inputs can be obtained from other wflow models such as wflow\_sbm, wflow\_hbv or from
-other sources.
+These inputs can be obtained from wflow\_sbm or from other sources.
 
 Model outputs can be saved for both the inland and the instream part of the model. Some
 examples are listed below.
@@ -331,7 +208,4 @@ Bedconc = "Bedconc"
 ## References
 + Köhler, L., Mulligan, M., Schellekens, J., Schmid, S., Tobón, C., 2006, Hydrological
   impacts of converting tropical montane cloud forest to pasture, with initial reference to
-  northern Costa Rica. Final Technical Report DFID‐FRP Project No. R799.
-
-+ Savenije, H. H. G. (2010). HESS opinions “topography driven conceptual modelling (FLEX-Topo).”
-  Hydrology and Earth System Sciences, 14(12), 2681–2692. https://doi.org/10.5194/hess-14-2681-2010
+  northern Costa Rica. Final Technical Report DFID‐FRP Project No. R799.
\ No newline at end of file
diff --git a/docs/src/model_docs/params_vertical.md b/docs/src/model_docs/params_vertical.md
index 12e52bcd6..0202719cd 100644
--- a/docs/src/model_docs/params_vertical.md
+++ b/docs/src/model_docs/params_vertical.md
@@ -148,163 +148,6 @@ profile `kv` is used and `z_layered` is required as input.
 | `industry` | optional industry water demand of type `NonIrrigationDemand` | - | - |
 | `allocation` | optional water allocation of type `AllocationLand` | - | - |
 
-## [HBV](@id params_hbv)
-The Table below shows the parameters (fields) of struct `HBV`, including a description of
-these parameters, the unit, and default value if applicable. The parameters in bold
-represent model parameters that can be set through static and forcing input data (netCDF),
-and can be listed in the TOML configuration file under `[input.vertical]`, to map the
-internal model parameter to the external netCDF variable.
-
-|  parameter | description    | unit | default |
-|:---------------| --------------- | ---------------------- | ----- |
-| **`cfmax`**  | degree-day factor | mm ᵒC``^{-1}`` Δt``^{-1}`` | 3.75653 mm ᵒC``^{-1}`` day``^{-1}``  |
-| **`tt`**  | threshold temperature for snowfall| ᵒC | -1.41934  |
-| **`tti`**  | threshold temperature interval length | ᵒC | 1.0  |
-| **`ttm`**  | threshold temperature for snowmelt  | ᵒC | -1.41934  |
-| **`whc`**  | water holding capacity as fraction of current snow pack  | - | 0.1  |
-| **`g_tt`**  | threshold temperature for snowfall above glacier  | ᵒC| 0.0  |
-| **`g_cfmax`**  | Degree-day factor for glacier  | mm ᵒC``^{-1}`` Δt``^{-1}``| 3.0 mm ᵒC``^{-1}`` day``^{-1}`` |
-| **`g_sifrac`**  | fraction of the snowpack on top of the glacier converted into ice  | Δt``^{-1}``| 0.001 day``^{-1}`` |
-| **`glacierfrac`**  | fraction covered by a glacier | - | 0.0  |
-| **`glacierstore`**  | water within the glacier  | mm | 5500.0  |
-| **`fc`**  | field capacity  | mm | 260.0  |
-| **`betaseepage`**  | exponent in soil runoff generation equation  | - | 1.8  |
-| **`lp`**  | fraction of field capacity below which actual evaporation=potential evaporation | - | 0.53 |
-| **`k4`**  | recession constant baseflow | Δt``^-1`` | 0.02307 day``^{-1}`` |
-| **`kquickflow`**  | recession constant upper reservoir | Δt``^-1`` |  0.09880 day``^{-1}`` |
-| **`suz`**  | Level over which `k0` is used | mm |  100.0 |
-| **`k0`**  | recession constant upper reservoir | Δt``^-1`` | 0.30 day``^{-1}`` |
-| **`khq`**  | recession rate at flow `hq`| Δt``^-1`` | 0.09880 day``^{-1}`` |
-| **`hq`**  | high flow rate hq for which recession rate of upper reservoir is known | mm Δt``^-1`` | 3.27 mm day``^{-1}`` |
-| **`alphanl`**  | measure of non-linearity of upper reservoir | - | 1.1 |
-| **`perc`**  | percolation from upper to lower zone |  mm Δt``^-1`` | 0.4 mm day``^{-1}`` |
-| **`cfr`**  | refreezing efficiency constant in refreezing of freewater in snow | - | 0.05  |
-| **`pcorr`**  | correction factor for precipitation  | - | 1.0  |
-| **`rfcf`**  | correction factor for rainfall   | - | 1.0  |
-| **`sfcf`**  | correction factor for snowfall | - | 1.0  |
-| **`cflux`**  | maximum capillary rise from runoff response routine to soil moisture routine | mm Δt``^-1`` | 2.0 mm day``^{-1}``  |
-| **`icf`**  | maximum interception storage (in forested and non-forested areas) | mm | 2.0  |
-| **`cevpf`**  | correction factor for potential evaporation | - | 1.0  |
-| **`epf`**  | exponent of correction factor for evaporation on days with precipitation | mm``^{-1}`` | 1.0  |
-| **`ecorr`**  | evaporation correction | - | 1.0  |
-| **`precipitation`**           | precipitation     | mm Δt``^-1`` | - |
-| **`temperature`**             | temperature    | ᵒC | - |
-| **`potential_evaporation`**            | potential evapotranspiration     | mm Δt``^-1`` | - |
-| `potsoilevap`             | potential soil evaporation      | mm Δt``^-1`` | - |
-| `soilevap`             | soil evaporation | mm Δt``^-1`` | - |
-| `intevap`             | evaporation from interception storage     | mm Δt``^-1`` | - |
-| `actevap`             | actual evapotranspiration (intevap + soilevap) | mm Δt``^-1`` | - |
-| `interceptionstorage`             | actual interception storage     | mm | - |
-| `snowwater`             | available free water in snow    | mm | - |
-| `snow`             | snow pack    | mm | - |
-| `rainfallplusmelt`    | snow melt + precipitation as rainfall    | mm Δt``^-1`` | - |
-| `soilmoisture`        | actual soil moisture | mm | - |
-| `directrunoff`        | direct runoff to upper zone    | mm Δt``^-1`` | - |
-| `hbv_seepage`         | recharge to upper zone  | mm Δt``^-1``  | - |
-| `in_upperzone`         | water inflow into upper zone  | mm Δt``^-1``  | - |
-| `upperzonestorage`     | water content of the upper zone  | mm  | - |
-| `quickflow`         |  specific runoff (quickflow part)  | mm Δt``^-1``  | - |
-| `real_quickflow`         | specific runoff (quickflow), if K upper zone is precalculated  | mm Δt``^-1``  | - |
-| `percolation`         | actual percolation to the lower zone | mm Δt``^-1``  | - |
-| `capflux`         | capillary rise | mm Δt``^-1``  | - |
-| `lowerzonestorage`         | water content of the lower zone | mm  | - |
-| `baseflow`         | specific runoff (baseflow part) per cell  | mm Δt``^-1``  | - |
-| `runoff`         | total specific runoff per cell  | mm Δt``^-1``  | - |
-
-
-## [FLEXtopo](@id params_flextopo)
-The Table below shows the parameters (fields) of struct `FLEXTOPO`, including a description
-of these parameters, the unit, and default value if applicable. The parameters in bold
-represent model parameters that can be set through static and forcing input data (netCDF),
-and can be listed in the TOML configuration file under `[input.vertical]`, to map the
-internal model parameter to the external netCDF variable.
-
-|  parameter | description    | unit | default |
-|:---------------| --------------- | ---------------------- | ----- |
-| **`cfmax`**  | degree-day factor | mm ᵒC``^{-1}`` Δt``^{-1}`` | 3.75653 mm ᵒC``^{-1}`` day``^{-1}``  |
-| **`tt`**  | threshold temperature for snowfall| ᵒC | -1.41934  |
-| **`tti`**  | threshold temperature interval length | ᵒC | 1.0  |
-| **`ttm`**  | threshold temperature for snowmelt  | ᵒC | -1.41934  |
-| **`whc`**  | water holding capacity as fraction of current snow pack  | - | 0.1  |
-| **`cfr`**  | refreezing efficiency constant in refreezing of freewater in snow | - | 0.05  |
-| **`g_tt`**  | threshold temperature for snowfall above glacier  | ᵒC| 0.0  |
-| **`g_cfmax`**  | Degree-day factor for glacier  | mm ᵒC``^{-1}`` Δt``^{-1}``| 3.0 mm ᵒC``^{-1}`` day``^{-1}`` |
-| **`g_sifrac`**  | fraction of the snowpack on top of the glacier converted into ice  | Δt``^{-1}`` | 0.001 day``^{-1}`` |
-| **`glacierfrac`**  | fraction covered by a glacier | - | 0.0  |
-| **`glacierstore`**  | water within the glacier  | mm | 5500.0  |
-| **`ecorr`**  | evaporation correction | - | 1.0  |
-| **`pcorr`**  | correction factor for precipitation  | - | 1.0  |
-| **`rfcf`**  | correction factor for rainfall   | - | 1.0  |
-| **`sfcf`**  | correction factor for snowfall | - | 1.0  |
-| **`imax`**  | maximum interception storage (``I_\mathrm{max}``) | mm | 3.0  |
-| **`shmax`**  | maximum horton ponding storage capacity (``S_\mathrm{Hmax}``)  | mm | 30.0  |
-| **`srmax`**  | maximum root zone storage capacity (``S_\mathrm{Rmax}``)  | mm | 260.0  |
-| **`beta`**  | exponent in soil runoff generation equation  | - | 0.3  |
-| **`lp`**  | fraction of root zone capacity below which actual evaporation=potential evaporation (``L_\mathrm{P}``) | - | 0.3 |
-| **`ks`**  | recession constant slow groundwater storage (``K_\mathrm{S}``) | Δt``^-1`` | 0.006 day``^{-1}`` |
-| **`kf`**  | recession constant fast storage (``K_\mathrm{F}``) | Δt``^-1`` |  0.1 day``^{-1}`` |
-| **`khf`**  | recession constant horton runoff storage (``K_\mathrm{Hf}``) | Δt``^-1`` |  0.5 day``^{-1}`` |
-| **`alfa`**  | measure of non-linearity of upper reservoir (``\alpha``) | - | 1.0 |
-| **`perc`**  | maximum percolation flux from root zone to slow storage (``Q_\mathrm{perc,max}``) |  mm Δt``^-1``| 0.30 mm day``^{-1}`` |
-| **`cap`**  | maximum capillary rise from slow storage to root zone (``Q_\mathrm{cap,max}``)| mm Δt``^-1`` | 0.20 mm day``^{-1}``  |
-| **`ds`**  | splitter parameter determining fraction of root zone outflow to slow storage (``d_\mathrm{s}``) | - | 0.2 |
-| **`shmin`**  | minimum storage capacity in horton ponding (relative to ``S_\mathrm{Hmax}``) (``S_\mathrm{Hmin}``) | [-] |  0.2 |
-| **`facc0`**  | maximum modelled accumulated frost resulting in `shmin` (``F_\mathrm{acc,fr0}``) | [ᵒC Δt] |  -3.0 |
-| **`facc1`**  | minimum modelled accumulated frost resulting in `shmin` (``F_\mathrm{acc,fr1}``) | [ᵒC Δt] |  0.0 |
-| **`fdec`**  | exponent for the decline of infiltration capacity (``F_\mathrm{dec}``) | [-] |  0.2 |
-| **`fmax`**  | maximum infiltration capacity from horton ponding (``F_\mathrm{max}``) | [mm Δt``^-1``] |  2.0 |
-| **`kmf`**  | melt coefficient of frozen topsoil (``K_\mathrm{mf}``) | [-] |  1.0 |
-| **`hrufrac`**  | fraction of class within cell (``F_\mathrm{hrufrac}``)| - | 1/length(classes) |
-| **`precipitation`**           | precipitation     | mm Δt``^-1`` | - |
-| **`temperature`**             | temperature    | ᵒC | - |
-| **`potential_evaporation`**            | potential evapotranspiration     | mm Δt``^-1`` | - |
-| `precipcorr`           | corrected precipitation     | mm Δt``^-1`` | - |
-| `epotcorr`           | corrected potential evaporation     | mm Δt``^-1`` | - |
-| `snow`             | snow water (``S_\mathrm{W}``)   | mm | - |
-| `snowwater`             | available free water in snow    | mm | - |
-| `interceptionstorage`             | interception storage (``S_\mathrm{I}``)    | mm | - |
-| `interceptionstorage_m`             | average interception storage over classes (``S_\mathrm{I}``)    | mm | - |
-| `hortonpondingstorage`             | horton ponding storage (``S_\mathrm{Hp}``)    | mm | - |
-| `hortonpondingstorage_m`             | average horton ponding storage over classes (``S_\mathrm{Hp}``)    | mm | - |
-| `hortonrunoffstorage`             | horton runoff storage (``S_\mathrm{Hf}``)    | mm | - |
-| `hortonrunoffstorage_m`             | average horton runoff storage over classes (``S_\mathrm{Hf}``)    | mm | - |
-| `rootzonestorage`             | root zone storage (``S_\mathrm{R}``)  | mm | - |
-| `rootzonestorage_m`             | average root zone storage over classes (``S_\mathrm{R}``)  | mm | - |
-| `faststorage`             | fast storage (``S_\mathrm{F}``)  | mm | - |
-| `faststorage_m`             | average fast storage over classes (``S_\mathrm{F}``)  | mm | - |
-| `slowstorage`             | slow storage (``S_\mathrm{S}``)  | mm | - |
-| `potsoilevap`             | potential soil evaporation (``E_\mathrm{P}``)     | mm Δt``^-1`` | - |
-| `soilevap`             | soil evaporation | mm Δt``^-1`` | - |
-| `intevap`             | evaporation from interception storage (``E_\mathrm{I}``)    | mm Δt``^-1`` | - |
-| `intevap_m`             | average evaporation from interception storage over classes  (``E_\mathrm{I}``)   | mm Δt``^-1`` | - |
-| `hortonevap`             | evaporation from horton ponding storage (``E_\mathrm{H}``)    | mm Δt``^-1`` | - |
-| `hortonevap_m`             | average evaporation from horton ponding storage over classes  (``E_\mathrm{H}``)   | mm Δt``^-1`` | - |
-| `rootevap`             | evaporation from root zone storage (``E_\mathrm{R}``)    | mm Δt``^-1`` | - |
-| `rootevap_m`             | average evaporation from root zone storage over classes  (``E_\mathrm{R}``)   | mm Δt``^-1`` | - |
-| `actevap`             | actual evapotranspiration (intevap + hortonevap + rootevap) (``E_\mathrm{A}``) | mm Δt``^-1`` | - |
-| `actevap_m`             | average actual evapotranspiration (intevap + hortonevap + rootevap) over classes (``E_\mathrm{A}``) | mm Δt``^-1`` | - |
-| `precipeffective`    | Effective precipitation (``P_\mathrm{E}``)   | mm Δt``^-1`` | - |
-| `rainfallplusmelt`    | snow melt + precipitation as rainfall (``P_\mathrm{M} + P_\mathrm{R}``)   | mm Δt``^-1`` | - |
-| `snowmelt`    | snowfall   | mm Δt``^-1`` | - |
-| `snowfall`    | snowfall   | mm Δt``^-1`` | - |
-| `facc`    | modeled accumulated frost   | ᵒC Δt | - |
-| `qhortonpond`        | Flux from the hortonian ponding storage to the hortonian runoff storage (``Q_\mathrm{H}``)   | mm Δt``^-1`` | - |
-| `qhortonrootzone`        | Flux from the hortonian ponding storage to the root zone storage (``Q_\mathrm{HR}``)   | mm Δt``^-1`` | - |
-| `qhortonrun`        | Flux from the hortonian runoff storage (``Q_\mathrm{Hf}``)   | mm Δt``^-1`` | - |
-| `qrootzone`        | Flux from the root zone storage (``Q_\mathrm{R}``)  | mm Δt``^-1`` | - |
-| `qrootzonefast`        | Pref. recharge to fast storage (``Q_\mathrm{RF}``)  | mm Δt``^-1`` | - |
-| `qrootzoneslow_m`        | Pref. recharge to slow storage sum classes (``Q_\mathrm{RS}``)   | mm Δt``^-1`` | - |
-| `qcapillary`        | Capillary flux from the slow to the root-zone storage (``Q_\mathrm{cap}``)  | mm Δt``^-1`` | - |
-| `qcapillary_m`        | Capillary flux from the slow to the root-zone storage sum classes (``Q_\mathrm{cap}``)   | mm Δt``^-1`` | - |
-| `qpercolation`        | Percolation flux from the root-zone to the slow storage (``Q_\mathrm{perc}``)  | mm Δt``^-1`` | - |
-| `qpercolation_m`        | Percolation flux from the root-zone to the slow storage sum classes (``Q_\mathrm{perc}``)   | mm Δt``^-1`` | - |
-| `qfast`        | runoff from fast storage (``Q_\mathrm{F}``)  | mm Δt``^-1`` | - |
-| `qfast_tot`        | sum of fast runoff (from fast and horton runoff storages) over classes  | mm Δt``^-1`` | - |
-| `qslow`        | runoff from slow storage (``Q_\mathrm{S}``)  | mm Δt``^-1`` | - |
-| `runoff`         | total specific runoff per cell (qslow + qfast_tot) (``Q``) | mm Δt``^-1``  | - |
-| `wb_tot`             | total water balance | mm Δt``^-1`` | - |
-
-
 ## [Sediment](@id params_sediment)
 
 The Table below shows external parameters that can be set through static input data
diff --git a/docs/src/model_docs/shared_concepts.md b/docs/src/model_docs/shared_concepts.md
index 5596e1f78..2d8e514c7 100644
--- a/docs/src/model_docs/shared_concepts.md
+++ b/docs/src/model_docs/shared_concepts.md
@@ -48,10 +48,10 @@ exceeds `whc`, either through snow melt or incoming rainfall, the surplus water
 
 ### Glacier modelling
 
-Glacier processes can be modelled if the snow model is enabled. For the vertical HBV concept
-snow modelling is not optional. Glacier modelling is very close to snow modelling and
-considers two main processes: glacier build-up from snow turning into firn/ice (using the
-HBV-light model) and glacier melt (using a temperature degree-day model).
+Glacier processes can be modelled if the snow model is enabled. Glacier modelling is very
+similar to snow modelling and considers two main processes: glacier build-up from snow turning
+into firn/ice (using the HBV-light model) and glacier melt (using a temperature degree-day
+model).
 
 The definition of glacier boundaries and initial volume is defined by two parameters. The
 parameter `glacierfrac` gives the fraction of each grid cell covered by a glacier as a
diff --git a/docs/src/model_docs/structures.md b/docs/src/model_docs/structures.md
index 78ca20a85..5f25e6f3f 100644
--- a/docs/src/model_docs/structures.md
+++ b/docs/src/model_docs/structures.md
@@ -21,7 +21,7 @@ end
 The `lateral` field of the `struct Model` can contain different lateral concepts. For each
 wflow model these different lateral concepts are mapped through the use of a `NamedTuple`.
 The `vertical` field of the `struct Model` always contains one vertical concept, for example
-the SBM or HBV vertical concept.
+the SBM vertical concept.
 
 Below an example how lateral concepts are mapped for the SBM model through a `NamedTuple`:
 
diff --git a/docs/src/model_docs/vertical/flextopo.md b/docs/src/model_docs/vertical/flextopo.md
deleted file mode 100644
index 114b54920..000000000
--- a/docs/src/model_docs/vertical/flextopo.md
+++ /dev/null
@@ -1,357 +0,0 @@
-# [FLEXTopo](@id vert_flextopo)
-
-## Introduction
-This section describes the different vertical processes available as part of the vertical
-FLEXTopo concept. This concept is part of the wflow\_flextopo model. The FLEXTopo model is a
-process-based model, which consists of different parallel classes connected through their
-groundwater storage. These classes are usually delineated from topographical data to
-represent the variability in hydrological processes across user-defined Hydrological
-Response Units (HRU). The main assumption underlying the concept, which was first introduced
-by Savenije (2010), is that different parts of the landscape fulfill different tasks in
-runoff generation and, hence, can be represented by different model structures. Commonly
-used classes include hillslopes, plateau and wetlands. Hillslopes are steep areas in a
-catchment and are generally forested. The dominant runoff process in hillslopes is assumed
-to be characterized by subsurface flow. Plateaus are defined as relatively flat and are
-relatively high above the stream, with deep groundwater levels. Depending on the specific
-conditions, the dominant runoff processes are groundwater recharge, quick subsurface flow
-and hortonian overland flow, which is especially important in agricultural areas. Saturation
-overland flow and capillary rise are the dominant processes on the riparian wetland class,
-where groundwater levels are shallow and assumed to rise quickly during an event. The
-strength of the concept is that the definition of classes and associated model structures is
-modular and flexible and not constrained by a predefined fixed model structure. The flexible
-approach allows to develop process-based models for different topographic, climatic,
-geologic and land use conditions, making use of the available data and expert knowledge. The
-FLEXTopo modeling approach has been applied in a lumped and distributed way in various
-applications across the globe (Gao et al., 2014; Euser et al., 2015; Hanus et al., 2021;
-Hrachowitz et al. 2021; Hulsman et al. 2021; Bouaziz et al., 2022).
-
-The wflow\_flextopo model is set-up in a modular way, implying that the user is free to
-determine the number of classes and which processes and/or parameters to include or exclude
-for each class. For each cell in the model domain, the percentage of each class in the cell
-is provided by the user in the staticmaps. The most complete model structure implemented in
-wflow\_flextopo is shown in the figure below. However, it is also possible to bypass each
-bucket or to deactivate processes through parameter values (see [FLEXTopo
-configuration](@ref config_flextopo)). It is also possible for users to contribute to the
-code by adding different conceptualizations for the different storages. When defining
-several classes, the model structures for each class are implemented in parallel, except for
-the common glacier, snow and groundwater processes, which are not class specific. An example
-of a three classes model is also shown in the Figure below.
-
-![flextopo_julia_1class.png](../../images/flextopo_julia_1class.png)
-*Schematic representation of the FLEXTopo model for a single class model including all
-storages and fluxes. Main parameters are denoted in red.*
-
-![flextopo_julia_3class.png](../../images/flextopo_julia_3class.png)
-*Example of a three class model with different model structure configurations per class*
-
-The descriptions below of each of the FLEXTopo model components are given for the most
-complete model structure with the symbols as shown in the schematic representation of the
-one class model in the Figure above. By bypassing storages and/or setting parameters to
-specific values, the model structure can be adapted to a user-defined model structure.
-
-## Snow
-The snow model is described in [Snow and glaciers](@ref snow_and_glac).
-
-## Glaciers
-Glacier processes are described in [Snow and glaciers](@ref snow_and_glac). Glacier
-modelling is enabled by specifying the following in the TOML file:
-
-```toml
-[model]
-glacier = true
-```
-
-## Correction factors for forcing data
-The ``e_\mathrm{corr}`` and ``p_\mathrm{corr}`` model parameters can be used to adjust the
-potential evaporation and precipitation, respectively.
-
-## Interception
-After the snow module, rainfall ``P_\mathrm{R}`` [mm t``^{-1}``] and snowmelt
-``P_\mathrm{M}`` [mm t``^{-1}``] enter the interception storage ``S_\mathrm{I}`` \[mm\]. The
-maximum interception storage is defined by the ``I_\mathrm{max}`` \[mm\] parameter for each
-class. Interception evaporation ``E_\mathrm{I}`` [mm t``^{-1}``] occurs at potential rate
-``E_\mathrm{P}`` [mm t``^{-1}``] as long as there is enough water in the interception
-storage. Effective precipitation ``P_\mathrm{E}`` [mm t``^{-1}``] flows out of the
-interception store when the storage capacity is exceeded.  Interception evaporation is
-subtracted from potential evaporation to ensure total evaporation does not exceed potential
-evaporation.
-
-The following equations apply:
-
-```math
-    \mathrm{d}S_\mathrm{I}/\mathrm{d}t = (P_\mathrm{R} + P_\mathrm{M}) - E_\mathrm{I} - P_\mathrm{E}
-```
-
-```math
-    P_\mathrm{E} = \mathrm{max}(0, (S_\mathrm{I} - I_\mathrm{max})/\mathrm{d}t)
-```
-
-```math
-    E_\mathrm{I} = \mathrm{min}(E_\mathrm{P}, S_\mathrm{I}/\mathrm{d}t)
-```
-
-## Hortonion ponding and runoff
-Hortonian overland flow processes are represented by a combination of two storages: a horton
-ponding storage ``S_\mathrm{Hp}`` [mm] and a runoff generating storage ``S_\mathrm{Hf}``
-[mm]. This conceptualization was introduced by de Boer-Euser (2017) and included in the
-plateau class to represent hortonian overland flow (infiltration excess overland flow) in
-agricultural fields. When the storage capacity of the ponding storage is exceeded, runoff
-``Q_\mathrm{H}`` [mm t``^{-1}``] is generated that enters the horton runoff storage. The
-horton runoff generating storage is included to slightly smooth the precipitation signal.
-However, the response time of the runoff generation storage ``K_\mathrm{Hf}`` [t``^{-1}``]
-is very short to generate fast runoff ``Q_\mathrm{Hf}`` [mm t``^{-1}``].
-
-Effective precipitation ``P_\mathrm{E}`` [mm t``^{-1}``] from the interception module enters
-the horton ponding storage, which has a maximum storage capacity ``S_\mathrm{Hmax}`` [mm].
-When the inflow exceeds the storage capacity, direct runoff is generated
-``Q_\mathrm{H,direct}`` [mm t``^{-1}``] and net infiltration in the horton ponding storage
-is denoted as ``Q_\mathrm{H,in,net}`` [mm t``^{-1}``].
-
-Evaporation from the horton ponding storage ``E_\mathrm{H}`` [mm t``^{-1}``]  is based on a
-simple formulation to express water stress. The equation describes how actual evaporation is
-linearly reduced when the relative horton ponding storage ``\overline{S_\mathrm{Hp}}`` [-]
-is below a certain threshold ``L_\mathrm{P}`` [-] parameter.
-
-A beta function with parameter ``\beta`` [-] is used to split the net infiltrating water to
-storage and to runoff ``Q_\mathrm{H}`` [mm t``^{-1}``], to which is added the direct runoff
-``Q_\mathrm{H,direct}`` [mm t``^{-1}``].
-
-The shape of the beta function for various values of ``\beta`` [-] is shown below:
-
-```@setup plot
-    using Printf
-    using CairoMakie
-```
-
-```@example plot
-    let                                                                                     # hide
-        fig = Figure(resolution = (800, 400))                                               # hide
-        ax = Axis(fig[1, 1], xlabel = "S/Smax [-]", ylabel = "Fraction of runoff [-]")      # hide
-        x = 0:0.01:1                                                                        # hide
-        betas = [0.3, 1.0, 3.0]                                                             # hide
-        for beta in betas                                                                   # hide
-            lines!(ax, x, (1 .- (1 .- x).^beta), label = @sprintf("beta = %.1f", beta))     # hide
-        end                                                                                 # hide
-        Legend(fig[1, 2], ax, "beta")                                                       # hide
-        fig                                                                                 # hide
-    end                                                                                     # hide
-```
-
-Additionally, water infiltrates from the horton ponding storage to the root zone storage
-(``Q_\mathrm{HR}`` [mm t``^{-1}``]), based on a formulation using a maximum infiltration
-capacity ``F_\mathrm{max}`` [mm t``^{-1}``] and a decay coefficient ``F_\mathrm{dec}`` [-].
-
-The maximum storage capacity of the horton ponding storage is reduced when soils are likely
-to be frozen ``S_\mathrm{Hmax,frost}`` [mm], i.e. during periods when the temperature is
-below zero for several consecutive days.
-
-The following equations apply for the Horton ponding storage ``S_\mathrm{Hp}`` [mm]:
-
-```math
-    \mathrm{d}S_\mathrm{Hp}/\mathrm{d}t = P_\mathrm{E} - E_\mathrm{H}  - Q_\mathrm{H} - Q_\mathrm{HR}
-```
-
-The constitutive equations are:
-
-```math
-    Q_\mathrm{H,direct}=\mathrm{max}((S_\mathrm{Hp}+P_\mathrm{E}−S_\mathrm{Hmax});0.0)
-```
-
-```math
-    Q_\mathrm{H,in,net} = P_\mathrm{E} − Q_\mathrm{H,direct}
-```
-
-```math
-    \overline{S_\mathrm{Hp}} = S_\mathrm{Hp}/S_\mathrm{H,max}
-```
-
-```math
-    E_\mathrm{H} = \mathrm{min} ( (E_\mathrm{P} - E_\mathrm{I}) \cdot \mathrm{min}(\overline{S_\mathrm{Hp}}/L_\mathrm{P},1), S_\mathrm{Hp}/\mathrm{d}t  )
-```
-
-```math
-    Q_\mathrm{H} = Q_\mathrm{H,in,net} \cdot (1-(1-\overline{S_\mathrm{Hp}})^\beta)
-```
-
-```math
-    Q_\mathrm{HR} = F_\mathrm{max} \cdot \mathrm{exp}(-F_\mathrm{dec} \cdot \overline{S_\mathrm{Hp}})
-```
-
-The reduction of the storage capacity of the horton ponding storage during frozen soil
-conditions is calculated following the equations provided by de Boer-Euser (2017):
-
-```math
-    S_\mathrm{Hmax,frost} = F_\mathrm{T} \cdot S_\mathrm{Hmax}
-```
-
-with:
-
-```math
-F_\mathrm{T} =
-    \begin{cases}
-      S_\mathrm{Hmin} & \text{if $F_\mathrm{acc,fr} < F_\mathrm{acc,fr0}$}\\
-      \frac{F_\mathrm{acc}}{F_\mathrm{acc,fr1} - F_\mathrm{acc,fr0}} - \frac{F_\mathrm{acc,fr0}}{F_\mathrm{acc,fr1} - F_\mathrm{acc,fr0}}  & \text{if $ F_\mathrm{acc,fr0} \le F_\mathrm{acc,fr} \le F_\mathrm{acc,fr1}$}\\
-      1 & \text{if $F_\mathrm{acc,fr} > F_\mathrm{acc,fr,1}$}
-    \end{cases}
-```
-
-where ``S_\mathrm{Hmin}`` [-], ``F_\mathrm{acc,fr0}`` [degree t], ``F_\mathrm{acc,fr1}``
-[degree t] and ``K_\mathrm{mf}`` [-] are all model parameters to describe: a coefficient to
-reduce ``S_\mathrm{Hmax}`` to a minimum storage capacity, the minimum and maximum modelled
-accumulated frost and a melt coefficient for the frozen topsoil, respectively.
-
-The following equations apply for the Horton fast runoff storage ``S_\mathrm{Hf}`` [mm]:
-
-```math
-    \mathrm{d}S_\mathrm{Hf}/\mathrm{d}t = Q_\mathrm{H} - Q_\mathrm{Hf}
-```
-
-```math
-    Q_\mathrm{Hf} = K_\mathrm{Hf}^{-1} \cdot S_\mathrm{Hf}
-```
-
-## Root zone soil moisture
-The incoming water from the interception and hortonian routines ``Q_\mathrm{HR}`` [mm
-t``^{-1}``] enters the root zone storage ``S_\mathrm{R}`` [mm]. The root zone storage has a
-maximum capacity ``S_\mathrm{Rmax}`` [mm], which represents the volume of water in the
-unsaturated root zone, which is available to the roots of vegetation for transpiration.
-Abundant water which exceeds the capacity of the root zone storage cannot infiltrate and
-becomes directly available for runoff ``Q_\mathrm{R,direct}`` [mm t``^{-1}``]. The net
-infiltration in the root zone storage is denoted as ``Q_\mathrm{R,in,net}`` [mm t``^{-1}``].
-
-A simple formulation to express water stress is used to calculate evaporation
-``E_\mathrm{R}`` [mm t``^{-1}``] from the root zone storage. The equation describes how
-actual evaporation is linearly reduced when the relative root zone storage
-``\overline{S_\mathrm{R}}`` [-] is below a certain threshold ``L_\mathrm{P}`` [-] parameter.
-
-Next, a beta function with parameter ``\beta`` [-] describes the partitioning of incoming
-water to the root zone storage and to runoff ``Q_\mathrm{R}`` [mm t``^{-1}``]. The water
-that leaves the root zone storage is partitioned into the fast storage and through
-preferential recharge to the slow storage, based on a splitter parameter ``d_\mathrm{s}``
-[-].
-
-Water may also leave the root zone storage through percolation to the slow groundwater
-``Q_\mathrm{perc}`` [mm t``^{-1}``] or enter the root zone storage from the slow groundwater
-through capillary rise ``Q_\mathrm{cap}`` [mm t``^{-1}``], based on a maximum percolation
-parameter ``Q_\mathrm{perc,max}`` [mm t``^{-1}``] and a maximum capillary rise flux
-parameter ``Q_\mathrm{cap,max}`` [mm t``^{-1}``].
-
-
-The water balance equation for the root zone storage is:
-
-```math
-    \mathrm{d}S_\mathrm{R}/\mathrm{d}t = Q_\mathrm{HR} - E_\mathrm{R}  - Q_\mathrm{R} - Q_\mathrm{perc} + Q_\mathrm{cap}
-```
-
-The constitutive equations are:
-
-```math
-    Q_\mathrm{R,direct}=\mathrm{max}((S_\mathrm{R}+Q_\mathrm{HR}−S_\mathrm{Rmax});0.0)
-```
-
-```math
-    Q_\mathrm{R,in,net} = Q_\mathrm{HR} − Q_\mathrm{R,direct}
-```
-
-```math
-    \overline{S_\mathrm{R}} = S_\mathrm{R}/S_\mathrm{R,max}
-```
-
-```math
-    E_\mathrm{R} = \mathrm{min} ( (E_\mathrm{P} - E_\mathrm{I} - E_\mathrm{H}) \cdot \mathrm{min}(\overline{S_\mathrm{R}}/L_\mathrm{P},1), S_\mathrm{R}/\mathrm{d}t  )
-```
-
-```math
-    Q_\mathrm{R} = Q_\mathrm{R,in,net} \cdot (1-(1-\overline{S_\mathrm{R}})^\beta)
-```
-
-```math
-    Q_\mathrm{perc} = Q_\mathrm{perc,max} \cdot \overline{S_\mathrm{R}}
-```
-
-```math
-    Q_\mathrm{cap} = Q_\mathrm{cap,max} \cdot (1 - \overline{S_\mathrm{R}})
-```
-
-## Fast storage and runoff
-The outflow from the root zone storage ``Q_\mathrm{R}`` [mm t``^{-1}``] is split with the
-splitter parameter ``d_\mathrm{s}`` [-] into inflow in the fast storage ``Q_\mathrm{RF}``
-[mm t``^{-1}``] and inflow in the slow storage ``Q_\mathrm{RS}`` [mm t``^{-1}``] to
-represent preferential recharge. The fast runoff storage ``S_\mathrm{F}`` [mm] generates
-fast runoff ``Q_\mathrm{F}`` [mm t``^{-1}``] through a simple non-linear equation with a
-recession constant ``K_\mathrm{F}`` [t``^{-1}``] and an exponent ``\alpha`` [-].
-
-The following equations apply:
-
-```math
-    \mathrm{d}S_\mathrm{F}/\mathrm{d}t = Q_\mathrm{RF} - Q_\mathrm{F}
-```
-
-```math
-    Q_\mathrm{RF} = Q_\mathrm{R} \cdot (1-d_\mathrm{s})
-```
-
-```math
-    Q_\mathrm{F} = K_\mathrm{F}^{-1} \cdot S_\mathrm{F}^{\alpha}
-```
-
-
-## Common slow groundwater storage and runoff
-The slow groundwater storage ``S_\mathrm{S}`` [mm] is a shared storage for all the different
-classes. It is filled through preferential recharge from the outflow of the root zone
-storage ``Q_\mathrm{RS}`` [mm t``^{-1}``] and through percolation ``Q_\mathrm{perc}`` [mm
-t``^{-1}``]. It empties through capillary rise ``Q_\mathrm{cap}`` [mm t``^{-1}``] and
-through a linear outflow ``Q_\mathrm{S}`` [mm t``^{-1}``] with recession timescale
-coefficient ``K_\mathrm{S}`` [t``^{-1}``].
-
-Total streamflow ``Q_\mathrm{TOT}`` [mm t``^{-1}``] is the weighted sum of the horton fast
-runoff and the fast runoff from the different classes based on the fraction of each class in
-a cell ``F_\mathrm{hrufrac}`` [-] and the slow runoff, which is then routed downstream along
-the river network through the kinematic wave.
-
-```math
-    \mathrm{d}S_\mathrm{S}/\mathrm{d}t = Q_\mathrm{RS} + Q_\mathrm{perc} - Q_\mathrm{S} - Q_\mathrm{cap}
-```
-
-```math
-    Q_\mathrm{RS} = Q_\mathrm{R} \cdot d_\mathrm{s}
-```
-
-```math
-    Q_\mathrm{S} = K_\mathrm{S}^{-1} \cdot S_\mathrm{S}
-```
-
-```math
-Q_\mathrm{TOT} =  Q_\mathrm{S} + \sum_{class=1}^{n} (Q_\mathrm{F,class} + Q_\mathrm{Hf,class}) \cdot F_\mathrm{hrufrac,class}
-```
-
-## References
-+ de Boer-Euser, T. (2017). Added value of distribution in rainfall-runoff models for the Meuse basin
-   PhD thesis, Delft University of Technology. https://doi.org/10.4233/uuid:89a78ae9-7ffb-4260-b25d-698854210fa8
-
-+ Bouaziz, L. J. E., Aalbers, E. E., Weerts, A. H., Hegnauer, M., Buiteveld, H., Lammersen, R., Stam, J., Sprokkereef, E., Savenije, H. H. G., and Hrachowitz, M. (2022)
-    Ecosystem adaptation to climate change: the sensitivity of hydrological predictions to time-dynamic model parameters,
-    Hydrol. Earth Syst. Sci., 26, 1295–1318, https://doi.org/10.5194/hess-26-1295-2022
-
-+ Euser, T., Hrachowitz, M., Winsemius, H. C., & Savenije, H. H. G. (2015).
-    The effect of forcing and landscape distribution on performance and consistency of model structures.
-    Hydrological Processes, 29(17), 3727–3743. https://doi.org/10.1002/hyp.10445
-
-+ Gao, H., Hrachowitz, M., Fenicia, F., Gharari, S., & Savenije, H. H. G. (2014).
-    Testing the realism of a topography-driven model (FLEX-Topo) in the nested catchments of the Upper Heihe, China.
-    Hydrology and Earth System Sciences, 18(5), 1895–1915. https://doi.org/10.5194/hess-18-1895-2014
-
-+ Hanus, S., Hrachowitz, M., Zekollari, H., Schoups, G., Vizcaino, M., and Kaitna, R. (2021)
-    Future changes in annual, seasonal and monthly runoff signatures in contrasting Alpine catchments in Austria,
-    Hydrol. Earth Syst. Sci., 25, 3429–3453, https://doi.org/10.5194/hess-25-3429-2021
-
-+ Hrachowitz, M., Stockinger, M., Coenders-Gerrits, M., van der Ent, R., Bogena, H., Lücke, A., and Stumpp, C. (2021)
-    Reduction of vegetation-accessible water storage capacity after deforestation affects catchment travel time distributions
-    and increases young water fractions in a headwater catchment, Hydrol. Earth Syst. Sci., 25, 4887–4915, https://doi.org/10.5194/hess-25-4887-2021
-
-+ Hulsman, P., Savenije, H. H. G., & Hrachowitz, M. (2021). Learning from satellite observations:
-    Increased understanding of catchment processes through stepwise model improvement.
-    Hydrology and Earth System Sciences, 25(2), 957–982. https://doi.org/10.5194/hess-25-957-2021
-
-+ Savenije, H. H. G. (2010). HESS opinions “topography driven conceptual modelling (FLEX-Topo).”
-    Hydrology and Earth System Sciences, 14(12), 2681–2692. https://doi.org/10.5194/hess-14-2681-2010
diff --git a/docs/src/model_docs/vertical/hbv.md b/docs/src/model_docs/vertical/hbv.md
deleted file mode 100644
index a0c2ae92f..000000000
--- a/docs/src/model_docs/vertical/hbv.md
+++ /dev/null
@@ -1,195 +0,0 @@
-# [HBV](@id vert_hbv)
-
-## Introduction
-This section describes the different vertical processes available as part of the vertical
-HBV concept. This concept is part of the wflow\_hbv model.
-
-## Snow
-The snow model is described in [Snow and glaciers](@ref snow_and_glac).
-
-## Glaciers
-Glacier processes are described in [Snow and glaciers](@ref snow_and_glac). Glacier modelling is enabled
-by specifying the following in the TOML file:
-
-```toml
-[model]
-glacier = true
-```
-## Potential Evaporation
-The `cevpf` model parameter is used to adjust the potential evaporation based on land use.
-In the original HBV version `cevpfo` is used, a factor for forest land use only.
-
-## Interception
-For interception storage a single `icf` parameter is used according to the land use. In this
-implementation interception evaporation is subtracted to ensure total evaporation does not
-exceed potential evaporation. From this storage evaporation equal to the potential
-evaporation rate will occur as long as water is available, even if it is stored as snow. All
-water enters this store first, there is no concept of free throughfall (e.g. through gaps in
-the canopy). In the model a running water budget is kept of the interception store:
-
-+ The available storage (`icf`- actual storage) is filled with the water coming from the
-  snow routine (``Q_{in}``)
-+ Any surplus water now becomes the new ``Q_{in}``
-+ Interception evaporation is determined as the minimum of the current interception storage
-  and the potential evaporation
-
-## The soil routine
-The incoming water from the snow and interception routines, ``Q_{in}``, is available for
-infiltration in the soil routine. The soil layer has a limited capacity, `fc`, to hold soil
-water, which means if `fc` is exceeded the abundant water cannot infiltrate and,
-consequently, becomes directly available for runoff.
-
-```math
-    Q_{dr}=max((SM+Q_{in}−fc);0.0)
-```
-
-where ``Q_{dr}`` is the abundant soil water (also referred to as direct runoff) and ``SM``
-is the soil moisture content. Consequently, the net amount of water that infiltrates into
-the soil, ``I_{net}``, equals:
-
-```math
-I_{net} = Q_{in} − Q_{dr}
-```
-
-Part of the infiltrating water, ``I_{net}``, will runoff through the soil layer (seepage).
-This runoff volume, ``SP``, is related to the soil moisture content, ``SM``, through the
-following power relation:
-
-```math
-SP = \left(\frac{SM}{fc}\right)^\beta I_{net}
-```
-
-where ``\beta`` is an empirically based parameter. Application of this equation implies that
-the amount of seepage water increases with increasing soil moisture content. The fraction of
-the infiltrating water which does not runoff, ``I_{net}−SP``, is added to the available
-amount of soil moisture, ``SM``. The ``\beta`` parameter affects the amount of supply to the
-soil moisture reservoir that is transferred to the quick response reservoir. Values of
-``\beta`` vary generally between 1 and 3. Larger values of ``\beta`` reduce runoff and
-indicate a higher absorption capacity of the soil.
-
-![hbv-soilmoist.png](../../images/hbv-soilmoist.png)
-
-*Schematic view of the soil moisture routine*
-
-A percentage of the soil moisture will evaporate. This percentage is related to the
-potential evaporation and the available amount of soil moisture:
-
-```math
-    E_a = \frac{SM}{T_m} E_p \, ; \, SM<T_m \\~\\
-    E_a = E_p \, ; \, SM \geq T_m
-```
-
-where ``E_a`` is the actual evaporation, ``E_p`` is the potential evaporation and ``T_m``
-(``\leq fc``) is a user defined threshold, above which the actual evaporation equals the
-potential evaporation. ``T_m`` is defined as ``LP * fc`` in which ``LP`` is a soil dependent
-evaporation factor (``LP\leq 1``).
-
-In the original model (Bergström, 1992), a correction to ``E_a`` is applied in case of
-interception. If ``E_a`` from the soil moisture storage plus ``E_i`` exceeds ``ET_p−E_i``
-(``E_i`` = interception evaporation) then the exceeding part is multiplied by a factor
-(``1-e_{red}``), where the parameter ``e_{red}`` varies between 0 and 1. This correction is
-not present in the wflow\_hbv model.
-
-## The runoff response routine
-
-The volume of water which becomes available for runoff, ``S_{dr}+SP``, is transferred to the
-runoff response routine. In this routine the runoff delay is simulated through the use of a
-number of linear reservoirs.
-
-Two linear reservoirs are defined to simulate the different runoff processes: the upper zone
-(generating quick runoff and interflow) and the lower zone (generating slow runoff). The
-available runoff water from the soil routine (i.e. direct runoff, ``S_{dr}``, and seepage,
-``SP``) in principle ends up in the lower zone, unless the percolation threshold, ``PERC``,
-is exceeded, in which case the redundant water ends up in the upper zone:
-
-```math
-\Delta V_{LZ}=min(PERC;(S_{dr}+SP)) \\~\\
-\Delta V_{UZ}=max(0.0;(S_{dr}+SP−PERC))
-```
-
-where ``V_{UZ}`` is the content of the upper zone, ``V_{LZ}`` is the content of the lower
-zone.
-
-Capillary flow from the upper zone to the soil moisture reservoir is modeled according to:
-
-```math
-Q_{cf}=cflux \; (fc−SM)/fc
-```
-
-where ``cflux`` is the maximum capillary flux [mm day``^{-1}``].
-
-The upper zone generates quick runoff (``Q_q``) using:
-```math
-Q_q=K \, UZ^{(1+\alpha)}
-```
-where ``K`` is the upper zone recession coefficient, and ``\alpha`` determines the amount of
-non-linearity. Within HBV-96, the value of ``K`` is determined from three other parameters:
-``\alpha``, ``KHQ``, and ``HQ`` [mm day``^{-1}``]. The value of ``HQ`` represents an outflow
-rate of the upper zone for which the recession rate is equal to ``KHQ``. If we define
-``UZ_{HQ}`` to be the content of the upper zone at outflow rate ``HQ`` we can write the
-following equation:
-
-```math
-    HQ=K \cdot UZ^{(1+\alpha)} = KHQ \cdot UZ_{HQ}
-```
-
-If we eliminate ``UZ_{HQ}`` we obtain:
-
-```math
-    HQ = K \left(\frac{HQ}{KHQ}\right)^{(1+\alpha)}
-```
-
-Rewriting for ``K`` results in:
-```math
-    K = KQH^{(1−\alpha)} HQ^{−\alpha}
-```
-
-The lower zone is a linear reservoir, which means the rate of slow runoff, ``Q_{LZ}``, which
-leaves this zone during one time step equals:
-
-```math
-Q_{LZ} = K_{LZ} \, V_{LZ}
-```
-
-where ``K_{LZ}`` is the reservoir constant.
-
-The upper zone is also a linear reservoir, but it is slightly more complicated than the
-lower zone because it is divided into two zones: a lower part in which interflow is
-generated and an upper part in which quick flow is generated.
-
-![hbv-upper](../../images/hbv-upper.png)
-
-*Schematic view of the upper zone*
-
-If the total water content of the upper zone, ``V_{UZ}`` is lower than a threshold ``UZ1``,
-the upper zone only generates interflow. On the other hand, if ``V_{UZ}`` exceeds ``UZ1``,
-part of the upper zone water will runoff as quick flow:
-
-```math
-    Q_i = K_i min(UZ1;V_{uz})\\
-    Q_q = K_q max((V_{UZ}−UZ1);0.0)
-```
-
-Where ``Q_i`` is the amount of generated interflow in one time step, ``Q_q`` is the amount
-of generated quick flow in one time step and ``K_i`` and ``K_q`` are reservoir constants for
-interflow and quick flow respectively.
-
-The total runoff rate, ``Q``, is equal to the sum of the three different runoff components:
-
-```math
-    Q = Q_{LZ}+Q_i+Q_q
-```
-
-The runoff behavior in the runoff response routine is controlled by two threshold values
-``P_m`` and ``UZ1`` in combination with three reservoir parameters, ``K_{LZ}``, ``K_i`` and
-``K_q``.
-
-In order to represent the differences in delay times between the three runoff components,
-the reservoir constants have to meet the following requirement:
-```math
-    K_{LZ}<K_i<K_q
-```
-
-## References
- + Bergström, S., 1992, The HBV model–its structure and applications, SMHI Reports RH 4,
-   Norrköping.
\ No newline at end of file
diff --git a/docs/src/user_guide/additional_options.md b/docs/src/user_guide/additional_options.md
index 6b5baa03b..b0a01f8ef 100644
--- a/docs/src/user_guide/additional_options.md
+++ b/docs/src/user_guide/additional_options.md
@@ -193,13 +193,12 @@ and the kinematic wave components of this model can run on multiple threads. The
 [local inertial model for river flow](@ref config_sbm_gwf_lie_river_land) and the optional
 [local inertial model for river (1D) and land (2D)](@ref config_sbm_gwf_lie_river_land),
 both part of wflow\_sbm, can also run on multiple threads. The threading functionality for
-the kinematic wave may also be useful for models that make (partly) use of this routing
-approach as the [wflow_hbv](@ref config_hbv) model and the wflow\_sbm model [SBM +
-Groundwater flow](@ref config_sbm_gwf). The multi-threading functionality in wflow is
-considered experimental, see also the following
-[issue](https://github.com/Deltares/Wflow.jl/issues/139), where an error was not thrown
-running code multi-threaded. Because of this we advise to start with running a wflow model
-single-threaded (for example during the testing phase of setting up an new wflow model).
+the kinematic wave may also be useful for the wflow\_sbm model [SBM + Groundwater flow](@ref
+config_sbm_gwf). The multi-threading functionality in wflow is considered experimental, see
+also the following [issue](https://github.com/Deltares/Wflow.jl/issues/139), where an error
+was not thrown running code multi-threaded. Because of this we advise to start with running
+a wflow model single-threaded (for example during the testing phase of setting up an new
+wflow model).
 
 For information on how to start Julia with multiple threads we refer to [How to start Julia
 with multiple
diff --git a/docs/src/user_guide/intro.md b/docs/src/user_guide/intro.md
index 6a778cfaf..4c815f0a8 100644
--- a/docs/src/user_guide/intro.md
+++ b/docs/src/user_guide/intro.md
@@ -1,13 +1,13 @@
 # About the user guide
 
-The purpose of this user guide is to describe the steps for installing the wflow Julia software
-and setting up a simple model. The guide also includes a step-by-step process for 
+The purpose of this user guide is to describe the steps for installing the wflow Julia
+software and setting up a simple model. The guide also includes a step-by-step process for
 configuring your model using the TOML file (model settings) and the netCDF gridded datasets.
 
 Sample data and model setup are also provided for the Moselle River Basin (a major tributary
-of the Rhine River), which can be used to explore the model software. The guide covers model setups 
-for the `wflow\_sbm`, `wflow\_hbv` and `wflow\_sediment` model concepts.
+of the Rhine River), which can be used to explore the model software. The guide covers model
+setups for the `wflow\_sbm` and `wflow\_sediment` model concepts.
 
-Finally, the guide offers information on setting up your own model, including building a model
-from scratch or alternatively using Deltares [HydroMT](https://github.com/Deltares/hydromt)
-model building tools, which are open source.
+Finally, the guide offers information on setting up your own model, including building a
+model from scratch or alternatively using Deltares
+[HydroMT](https://github.com/Deltares/hydromt) model building tools, which are open source.
diff --git a/docs/src/user_guide/model-setup.md b/docs/src/user_guide/model-setup.md
index 6b8598e62..fccb10638 100644
--- a/docs/src/user_guide/model-setup.md
+++ b/docs/src/user_guide/model-setup.md
@@ -77,17 +77,6 @@ Reservoirs or lakes can be part of the kinematic wave or local inertial model fo
 and input parameters are described in [Reservoirs](@ref reservoir_params) and [Lakes](@ref
 lake_params).
 
-The [wflow\_hbv](@ref config_hbv) model configuration consists besides the river and
-overland flow components of the [HBV](@ref vert_hbv) vertical concept. Input parameters for
-this component are described in the [HBV](@ref params_hbv) section of Model parameters. For
-the river and overland flow components the kinematic wave approach is used.
-
-The [wflow\_flextopo](@ref config_flextopo) model configuration consists besides the river
-and overland flow components of the [FLEXTopo](@ref vert_flextopo) vertical concept. Input
-parameters for this component are described in the [FLEXTopo](@ref params_flextopo) section
-of Model parameters. For the river and overland flow components the kinematic wave approach
-is used.
-
 The [wflow\_sediment](@ref config_sediment) model configuration consists of the vertical
 [Soil Erosion](@ref) concept and the input parameters for this concept are described in the
 [Sediment](@ref params_sediment) section of the Model parameters. The parameters of the
diff --git a/docs/src/user_guide/sample_data.md b/docs/src/user_guide/sample_data.md
index dff6f10f5..7431ea7f3 100644
--- a/docs/src/user_guide/sample_data.md
+++ b/docs/src/user_guide/sample_data.md
@@ -8,8 +8,6 @@ are listed in the Table below:
 |:--------------- | ------------------|
 | wflow\_sbm + kinematic wave | [sbm_config.toml](https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/sbm_config.toml) |
 | wflow\_sbm + groundwater flow | [sbm\_gwf\_config.toml](https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/sbm_gwf_config.toml) |
-| wflow\_hbv | [hbv_config.toml](https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/hbv_config.toml) |
-| wflow\_flextopo | [flextopo_config.toml](https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/flextopo_config.toml) |
 | wflow_sediment | [sediment_config.toml](https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/sediment_config.toml) |
 
 The associated Model files (input static, forcing and state files) can easily be downloaded
@@ -55,42 +53,6 @@ download(forcing, joinpath(datadir, "forcing-sbm-groundwater.nc"))
 download(toml_url, toml_path)
 ```
 
-## wflow\_hbv
-```julia
-# urls to TOML and netCDF of the Moselle example model
-toml_url = "https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/hbv_config.toml"
-staticmaps = "https://github.com/visr/wflow-artifacts/releases/download/v0.2.1/staticmaps-lahn.nc"
-forcing = "https://github.com/visr/wflow-artifacts/releases/download/v0.2.0/forcing-lahn.nc"
-
-# create a "data" directory in the current directory
-datadir = joinpath(@__DIR__, "data")
-mkpath(datadir)
-toml_path = joinpath(@__DIR__, "hbv_config.toml")
-
-# download resources to current and data dirs
-download(staticmaps, joinpath(datadir, "staticmaps-lahn.nc"))
-download(forcing, joinpath(datadir, "forcing-lahn.nc"))
-download(toml_url, toml_path)
-```
-
-## [wflow\_flextopo](@id wflow_flextopo_data)
-```julia
-# urls to TOML and netCDF of the Meuse example model
-toml_url = "https://raw.githubusercontent.com/Deltares/Wflow.jl/master/test/flextopo_config.toml"
-staticmaps = "https://github.com/visr/wflow-artifacts/releases/download/v0.2.8/staticmaps_flex_meuse.nc"
-forcing = "https://github.com/visr/wflow-artifacts/releases/download/v0.2.8/forcing_meuse.nc"
-
-# create a "data" directory in the current directory
-datadir = joinpath(@__DIR__, "data")
-mkpath(datadir)
-toml_path = joinpath(@__DIR__, "flextopo_config.toml")
-
-# download resources to current and data dirs
-download(staticmaps, joinpath(datadir, "staticmaps_flex_meuse.nc"))
-download(forcing, joinpath(datadir, "forcing_meuse.nc"))
-download(toml_url, toml_path)
-```
-
 ## wflow\_sediment
 ```julia
 # urls to TOML and netCDF of the Moselle example model
diff --git a/docs/src/user_guide/step1_requirements.md b/docs/src/user_guide/step1_requirements.md
index 4e6427013..9f4cc5535 100644
--- a/docs/src/user_guide/step1_requirements.md
+++ b/docs/src/user_guide/step1_requirements.md
@@ -21,15 +21,13 @@ combination with `lateral` concepts that control how water is routed for example
 land or river domain. For the wflow\_sbm model, different model configurations are possible.
 The following configurations are supported in wflow:
 
-- wflow\_sbm:
-  - SBM + kinematic wave for subsurface and surface flow
-  - SBM + kinematic wave for subsurface and overland flow + local inertial river (+ optional
-      floodplain)
-  - SBM + kinematic wave for subsurface flow + local inertial river (1D) and land (2D)
-  - SBM + groundwater flow + kinematic wave for surface flow
-- wflow\_hbv: HBV + kinematic wave for surface routing
-- wflow\_flextopo: FLEXTOPO + kinematic wave for surface routing
-- wflow\_sediment as post processing of wflow\_sbm or wflow\_hbv output
+ - wflow\_sbm:
+    - SBM + kinematic wave for subsurface and surface flow
+    - SBM + kinematic wave for subsurface and overland flow + local inertial river (+
+      optional floodplain)
+    - SBM + kinematic wave for subsurface flow + local inertial river (1D) and land (2D)
+    - SBM + groundwater flow + kinematic wave for surface flow
+ - wflow\_sediment as post processing of wflow\_sbm output
 
 In the following pages, some examples will be given on how to prepare a basic wflow\_sbm
 model. Sample data for other model configurations is provided in the [sample data](@ref
diff --git a/docs/src/user_guide/step2_settings_file.md b/docs/src/user_guide/step2_settings_file.md
index 158f3ada1..f2e5095ef 100644
--- a/docs/src/user_guide/step2_settings_file.md
+++ b/docs/src/user_guide/step2_settings_file.md
@@ -57,7 +57,7 @@ Model-specific settings can be included in the model section of the TOML file.
 
 ```toml
 [model]
-type = "sbm"                        # one of ("sbm", "sbm_gwf, "hbv")
+type = "sbm"                        # one of ("sbm" or "sbm_gwf)
 masswasting = false                 # include lateral snow transport in the model, default is false
 snow = false                        # include snow modelling, default is false
 reinit = true                       # cold (reinit = true) or warm state (reinit = false), default is true
@@ -242,16 +242,14 @@ for specific locations (e.g. `gauges`) or areas (e.g. `subcatchment`), the netCD
 names are extracted from these maps. For a specific location (grid cell) a `location` is
 required. For layered model parameters and variables that have an extra `layer` dimension
 and are part of the vertical `sbm` concept, an internal layer index can be specified (an
-example is provided below). Similarly, for model parameters and variables that have an extra
-dimension `classes` and are part of the vertical `FLEXTopo` concept, the class name can be
-specified. If multiple layers or classes are desired, this can be specified in separate
-`[[netcdf.variable]]` entries. Note that the additional dimension should be specified when
-wflow is integrated with Delft-FEWS, for netCDF scalar data an extra dimension is not
-allowed by the `importNetcdfActivity` of the Delft-FEWS General Adapter. In the section
-[Output CSV section](@ref), similar functionality is available for CSV. For integration with
-Delft-FEWS, it is recommended to write scalar data to netCDF format, as the General Adapter
-of Delft-FEWS can directly ingest data from netCDF files. For more information, see [Run
-from Delft-FEWS](@ref run_fews). 
+example is provided below). If multiple layers are desired, this can be specified in
+separate `[[netcdf.variable]]` entries. Note that the additional dimension should be
+specified when wflow is integrated with Delft-FEWS, for netCDF scalar data an extra
+dimension is not allowed by the `importNetcdfActivity` of the Delft-FEWS General Adapter. In
+the section [Output CSV section](@ref), similar functionality is available for CSV. For
+integration with Delft-FEWS, it is recommended to write scalar data to netCDF format, as the
+General Adapter of Delft-FEWS can directly ingest data from netCDF files. For more
+information, see [Run from Delft-FEWS](@ref run_fews). 
 
 ```toml
 [netcdf]
@@ -301,10 +299,8 @@ provided to extract data for certain locations (e.g. `gauges`) or areas (e.g.
 which will be of the form `header_id`, e.g. `Q_20`, for a gauge with integer ID 20. For
 layered model parameters and variables that have an extra dimension `layer` and are part of
 the vertical `sbm` concept an internal layer index (see also example below) should be
-specified. For model parameters and variables that have an extra dimension `classes` and are
-part of the vertical `FLEXTopo` concept, it is possible to specify the class name. If
-multiple layers or classes are desired, this can be specified in separate `[[csv.column]]`
-entries.
+specified. If multiple layers or classes are desired, this can be specified in separate
+`[[csv.column]]` entries.
 
 The double brackets in `[[csv.column]]` follow TOML syntax, indicating that it is part of a
 list. You can specify as many entries as you want.
diff --git a/server/src/WflowServer.jl b/server/src/WflowServer.jl
index c96c7731e..65b05ea30 100644
--- a/server/src/WflowServer.jl
+++ b/server/src/WflowServer.jl
@@ -1,8 +1,8 @@
 module WflowServer
-import ZMQ
-import JSON3
-import StructTypes
-import Wflow
+using ZMQ: ZMQ
+using JSON3: JSON3
+using StructTypes: StructTypes
+using Wflow: Wflow
 
 include("bmi_service.jl")
 include("server.jl")
diff --git a/server/src/bmi_service.jl b/server/src/bmi_service.jl
index 3a344179a..4a87d8f1e 100644
--- a/server/src/bmi_service.jl
+++ b/server/src/bmi_service.jl
@@ -176,7 +176,7 @@ struct SaveState
     fn::String
 end
 
-function wflow_bmi(m::Initialize, model::Union{Wflow.Model,Nothing})
+function wflow_bmi(m::Initialize, model::Union{Wflow.Model, Nothing})
     model = getfield(Wflow.BMI, Symbol(m.fn))(Wflow.Model, m.config_file)
     return model
 end
diff --git a/server/src/server.jl b/server/src/server.jl
index 4e183af90..0ab9f35db 100644
--- a/server/src/server.jl
+++ b/server/src/server.jl
@@ -39,28 +39,28 @@ const map_structs = Dict(
 )
 
 mutable struct ModelHandler
-    model::Union{Wflow.Model,Nothing}
+    model::Union{Wflow.Model, Nothing}
 end
 
 "Shutdown ZMQ server"
 function shutdown(s::ZMQ.Socket, ctx::ZMQ.Context)
     @info "Shutting down Wflow ZMQ server on request..."
     ZMQ.close(s)
-    ZMQ.close(ctx)
+    return ZMQ.close(ctx)
 end
 
 "Error response ZMQ server"
 function response(err::AbstractString, s::ZMQ.Socket)
     @info "Send error response"
-    resp = Dict{String,String}("status" => "ERROR", "error" => err)
-    ZMQ.send(s, JSON3.write(resp))
+    resp = Dict{String, String}("status" => "ERROR", "error" => err)
+    return ZMQ.send(s, JSON3.write(resp))
 end
 
 "Status response ZMQ server"
 function response(s::ZMQ.Socket)
     @info "Send status response"
-    resp = Dict{String,String}("status" => "OK")
-    ZMQ.send(s, JSON3.write(resp))
+    resp = Dict{String, String}("status" => "OK")
+    return ZMQ.send(s, JSON3.write(resp))
 end
 
 "Validate JSON request against mapped Struct"
@@ -128,7 +128,7 @@ function main(ARGS::Vector{String})
             ),
         )
     end
-    start(port)
+    return start(port)
 end
 
 """
diff --git a/src/Wflow.jl b/src/Wflow.jl
index f4f898e0d..233a96fe0 100644
--- a/src/Wflow.jl
+++ b/src/Wflow.jl
@@ -4,11 +4,43 @@ import BasicModelInterface as BMI
 
 using Base.Threads: nthreads
 using CFTime: CFTime, monthday, dayofyear
-using Dates: Dates, Second, Minute, Hour, Day, Month, year, TimeType, DatePeriod, TimePeriod, Date, DateTime, now, isleapyear, datetime2unix
+using Dates:
+    Dates,
+    Second,
+    Minute,
+    Hour,
+    Day,
+    Month,
+    year,
+    TimeType,
+    DatePeriod,
+    TimePeriod,
+    Date,
+    DateTime,
+    now,
+    isleapyear,
+    datetime2unix,
+    canonicalize
 using DelimitedFiles: readdlm
 using FieldMetadata: @metadata
 using Glob: glob
-using Graphs: Graphs, Graph, DiGraph, add_edge!, is_cyclic, inneighbors, outneighbors, edges, topological_sort_by_dfs, src, dst, vertices, nv, ne, induced_subgraph, add_vertex!
+using Graphs:
+    Graphs,
+    Graph,
+    DiGraph,
+    add_edge!,
+    is_cyclic,
+    inneighbors,
+    outneighbors,
+    edges,
+    topological_sort_by_dfs,
+    src,
+    dst,
+    vertices,
+    nv,
+    ne,
+    induced_subgraph,
+    add_vertex!
 using IfElse: IfElse
 using LoggingExtras
 using LoopVectorization: @tturbo
@@ -21,15 +53,13 @@ using Statistics: mean, median, quantile!
 using TerminalLoggers
 using TOML: TOML
 
+# metadata is used in combination with BMI functions `get_var_units` and `get_var_location`
 @metadata get_units "mm dt-1" String
-# metadata for BMI grid
-@metadata exchange 1 Integer
-@metadata grid_type "unstructured" String
-@metadata grid_location "node" String
+@metadata grid_loc "node" String # BMI grid location
 
 const Float = Float64
-const CFDataset = Union{NCDataset,NCDatasets.MFDataset}
-const CFVariable_MF = Union{NCDatasets.CFVariable,NCDatasets.MFCFVariable}
+const CFDataset = Union{NCDataset, NCDatasets.MFDataset}
+const CFVariable_MF = Union{NCDatasets.CFVariable, NCDatasets.MFCFVariable}
 const version =
     VersionNumber(TOML.parsefile(joinpath(@__DIR__, "..", "Project.toml"))["version"])
 
@@ -45,7 +75,7 @@ function Clock(config)
     calendar = get(config, "calendar", "standard")::String
     starttime = cftime(config.starttime, calendar)
     dt = Second(config.timestepsecs)
-    Clock(starttime, 0, dt)
+    return Clock(starttime, 0, dt)
 end
 
 function Clock(config, reader)
@@ -79,7 +109,7 @@ function Clock(config, reader)
     end
     starttime = cftime(config.starttime, calendar)
 
-    Clock(starttime, 0, dt)
+    return Clock(starttime, 0, dt)
 end
 
 include("io.jl")
@@ -90,7 +120,7 @@ include("io.jl")
 Composite type that represents all different aspects of a Wflow Model, such as the
 network, parameters, clock, configuration and input and output.
 """
-struct Model{N,L,V,R,W,T}
+struct Model{N, L, V, R, W, T}
     config::Config  # all configuration options
     network::N  # connectivity information, directed graph
     lateral::L  # lateral model that holds lateral state, moves along network
@@ -104,26 +134,30 @@ end
 # different model types (used for dispatch)
 struct SbmModel end         # "sbm" type / sbm_model.jl
 struct SbmGwfModel end      # "sbm_gwf" type / sbm_gwf_model.jl
-struct HbvModel end         # "hbv" type / hbv_model.jl
-struct FlextopoModel end    # "flextopo" type / flextopo_model.jl
 struct SedimentModel end    # "sediment" type / sediment_model.jl
 
 # prevent a large printout of model components and arrays
 Base.show(io::IO, m::Model) = print(io, "model of type ", typeof(m))
 
-include("horizontal_process.jl")
+include("forcing.jl")
+include("parameters.jl")
 include("flow.jl")
-include("hbv.jl")
-include("water_demand.jl")
+include("horizontal_process.jl")
+include("vegetation/rainfall_interception.jl")
+include("vegetation/canopy.jl")
+include("snow/snow_process.jl")
+include("snow/snow.jl")
+include("glacier/glacier_process.jl")
+include("glacier/glacier.jl")
+include("surfacewater/runoff.jl")
+include("soil/soil.jl")
+include("soil/soil_process.jl")
 include("sbm.jl")
-include("flextopo.jl")
+include("demand/water_demand.jl")
 include("sediment.jl")
 include("reservoir_lake.jl")
-include("hbv_model.jl")
 include("sbm_model.jl")
-include("flextopo_model.jl")
 include("sediment_model.jl")
-include("vertical_process.jl")
 include("groundwater/connectivity.jl")
 include("groundwater/aquifer.jl")
 include("groundwater/boundary_conditions.jl")
@@ -137,7 +171,7 @@ include("states.jl")
 """
     run(tomlpath::AbstractString; silent=false)
     run(config::Config)
-    run(model::Model)
+    run!(model::Model)
     run()
 
 Run an entire simulation starting either from a path to a TOML settings file,
@@ -178,6 +212,7 @@ function run(tomlpath::AbstractString; silent = nothing)
             close(logfile)
         end
     end
+    return nothing
 end
 
 function run(config::Config)
@@ -187,30 +222,27 @@ function run(config::Config)
         initialize_sbm_model(config)
     elseif modeltype == "sbm_gwf"
         initialize_sbm_gwf_model(config)
-    elseif modeltype == "hbv"
-        initialize_hbv_model(config)
     elseif modeltype == "sediment"
         initialize_sediment_model(config)
-    elseif modeltype == "flextopo"
-        initialize_flextopo_model(config)
     else
         error("unknown model type")
     end
-    load_fixed_forcing(model)
-    run(model)
+    load_fixed_forcing!(model)
+    run!(model)
+    return model
 end
 
-function run_timestep(model::Model; update_func = update, write_model_output = true)
+function run_timestep!(model::Model; update_func = update!, write_model_output = true)
     advance!(model.clock)
     load_dynamic_input!(model)
-    model = update_func(model)
+    update_func(model)
     if write_model_output
         write_output(model)
     end
-    return model
+    return nothing
 end
 
-function run(model::Model; close_files = true)
+function run!(model::Model; close_files = true)
     (; config, writer, clock) = model
 
     model_type = config.model.type::String
@@ -225,13 +257,13 @@ function run(model::Model; close_files = true)
     starttime = clock.time
     dt = clock.dt
     endtime = cftime(config.endtime, calendar)
-    times = range(starttime + dt, endtime, step = dt)
+    times = range(starttime + dt, endtime; step = dt)
 
     @info "Run information" model_type starttime dt endtime nthreads()
     runstart_time = now()
     @progress for (i, time) in enumerate(times)
         @debug "Starting timestep." time i now()
-        model = run_timestep(model)
+        run_timestep!(model)
     end
     @info "Simulation duration: $(canonicalize(now() - runstart_time))"
 
@@ -246,7 +278,7 @@ function run(model::Model; close_files = true)
     # option to support running function twice without re-initializing
     # and thus opening the netCDF files
     if close_files
-        Wflow.close_files(model, delete_output = false)
+        Wflow.close_files(model; delete_output = false)
     end
 
     # copy TOML to dir_output, to archive what settings were used
@@ -255,10 +287,10 @@ function run(model::Model; close_files = true)
         dst = output_path(config, basename(src))
         if src != dst
             @debug "Copying TOML file." src dst
-            cp(src, dst, force = true)
+            cp(src, dst; force = true)
         end
     end
-    return model
+    return nothing
 end
 
 function run()
@@ -271,7 +303,7 @@ function run()
     if !isfile(toml_path)
         throw(ArgumentError("File not found: $(toml_path)\n" * usage))
     end
-    run(toml_path)
+    return run(toml_path)
 end
 
 end # module
diff --git a/src/bmi.jl b/src/bmi.jl
index 68093f03a..5d543cac9 100644
--- a/src/bmi.jl
+++ b/src/bmi.jl
@@ -3,7 +3,7 @@
 
 # Mapping of grid identifier to a key, to get the active indices of the model domain.
 # See also function active_indices(network, key::Tuple).
-const grids = Dict{Int,Tuple{Symbol}}(
+const grids = Dict{Int, Tuple{Symbol}}(
     0 => (:reservoir,),
     1 => (:lake,),
     2 => (:drain,),
@@ -27,16 +27,12 @@ function BMI.initialize(::Type{<:Model}, config_file)
         initialize_sbm_model(config)
     elseif modeltype == "sbm_gwf"
         initialize_sbm_gwf_model(config)
-    elseif modeltype == "hbv"
-        initialize_hbv_model(config)
     elseif modeltype == "sediment"
         initialize_sediment_model(config)
-    elseif modeltype == "flextopo"
-        initialize_flextopo_model(config)
     else
         error("unknown model type")
     end
-    load_fixed_forcing(model)
+    load_fixed_forcing!(model)
     return model
 end
 
@@ -49,15 +45,15 @@ Update the model for a single timestep.
 """
 function BMI.update(model::Model; run = nothing)
     if isnothing(run)
-        model = run_timestep(model)
+        run_timestep!(model)
     elseif run == "sbm_until_recharge"
-        model = run_timestep(
-            model,
-            update_func = update_until_recharge,
+        run_timestep!(
+            model;
+            update_func = update_until_recharge!,
             write_model_output = false,
         )
     elseif run == "sbm_after_subsurfaceflow"
-        model = run_timestep(model, update_func = update_after_subsurfaceflow)
+        run_timestep!(model; update_func = update_after_subsurfaceflow!)
     end
     return model
 end
@@ -75,8 +71,8 @@ function BMI.update_until(model::Model, time::Float64)
         )
         error(error_message)
     end
-    for _ = 1:steps
-        model = run_timestep(model)
+    for _ in 1:steps
+        run_timestep!(model)
     end
     return model
 end
@@ -89,7 +85,7 @@ function BMI.finalize(model::Model)
         write_netcdf_timestep(model, writer.state_dataset, writer.state_parameters)
     end
     reset_clock!(model.clock, config)
-    close_files(model, delete_output = false)
+    return close_files(model; delete_output = false)
 end
 
 function BMI.get_component_name(model::Model)
@@ -97,11 +93,11 @@ function BMI.get_component_name(model::Model)
 end
 
 function BMI.get_input_item_count(model::Model)
-    length(BMI.get_input_var_names(model))
+    return length(BMI.get_input_var_names(model))
 end
 
 function BMI.get_output_item_count(model::Model)
-    length(BMI.get_output_var_names(model))
+    return length(BMI.get_output_var_names(model))
 end
 
 """
@@ -117,22 +113,25 @@ function BMI.get_input_var_names(model::Model)
         var_names = Vector{String}()
         for c in config.API.components
             type = typeof(param(model, c))
-            inds = findall(x -> x != 0, exchange(type))
-            field_names = fieldnames(type)[inds]
+            field_names = fieldnames(type)
 
             for name in field_names
                 var = string(c, ".", name)
-                model_var = param(model, var)
-                if eltype(model_var) <: SVector
-                    for i = 1:length(first(model_var))
-                        push!(var_names, string(var, "[", i, "]"))
-                    end
-                elseif ndims(model_var) > 1
-                    for i = 1:length(first(model_var))
-                        push!(var_names, string(var, "[", i, "]"))
+                if exchange(param(model, var))
+                    model_var = param(model, var)
+                    if eltype(model_var) <: SVector
+                        for i in 1:length(first(model_var))
+                            push!(var_names, string(var, "[", i, "]"))
+                        end
+                    elseif ndims(model_var) > 1
+                        for i in 1:length(first(model_var))
+                            push!(var_names, string(var, "[", i, "]"))
+                        end
+                    else
+                        push!(var_names, var)
                     end
                 else
-                    push!(var_names, var)
+                    @warn("$var is not listed as variable for BMI exchange")
                 end
             end
         end
@@ -146,15 +145,14 @@ end
 "Returns input variables from `BMI.get_input_var_names(model::Model)`, there is no
 distinction between input - and output variables."
 function BMI.get_output_var_names(model::Model)
-    BMI.get_input_var_names(model)
+    return BMI.get_input_var_names(model)
 end
 
 function BMI.get_var_grid(model::Model, name::String)
     s = split(name, "[")
     key = symbols(first(s))
-    if exchange(param(model, key[1:end-1]), key[end]) == 1
-        gridtype = grid_type(param(model, key))
-        type = typeof(param(model, key[1:end-1]))
+    if exchange(param(model, key))
+        type = typeof(param(model, key[1:(end - 1)]))
         return if :reservoir in key
             0
         elseif :lake in key
@@ -177,13 +175,13 @@ end
 
 function BMI.get_var_type(model::Model, name::String)
     value = BMI.get_value_ptr(model, name)
-    repr(eltype(first(value)))
+    return repr(eltype(first(value)))
 end
 
 function BMI.get_var_units(model::Model, name::String)
     key = symbols(first(split(name, "[")))
-    if exchange(param(model, key[1:end-1]), key[end]) == 1
-        get_units(param(model, key[1:end-1]), key[end])
+    if exchange(param(model, key))
+        get_units(param(model, key[1:(end - 1)]), key[end])
     else
         error("$name not listed as variable for BMI exchange")
     end
@@ -191,17 +189,17 @@ end
 
 function BMI.get_var_itemsize(model::Model, name::String)
     value = BMI.get_value_ptr(model, name)
-    sizeof(eltype(first(value)))
+    return sizeof(eltype(first(value)))
 end
 
 function BMI.get_var_nbytes(model::Model, name::String)
-    sizeof(BMI.get_value_ptr(model, name))
+    return sizeof(BMI.get_value_ptr(model, name))
 end
 
 function BMI.get_var_location(model::Model, name::String)
     key = symbols(first(split(name, "[")))
-    if exchange(param(model, key[1:end-1]), key[end]) == 1
-        return grid_location(model, key)
+    if exchange(param(model, key))
+        return grid_loc(param(model, key[1:(end - 1)]), key[end])
     else
         error("$name not listed as variable for BMI exchange")
     end
@@ -215,7 +213,7 @@ function BMI.get_current_time(model::Model)
 end
 
 function BMI.get_start_time(model::Model)
-    0.0
+    return 0.0
 end
 
 function BMI.get_end_time(model::Model)
@@ -227,14 +225,18 @@ function BMI.get_end_time(model::Model)
 end
 
 function BMI.get_time_units(model::Model)
-    "s"
+    return "s"
 end
 
 function BMI.get_time_step(model::Model)
-    Float64(model.config.timestepsecs)
+    return Float64(model.config.timestepsecs)
 end
 
-function BMI.get_value(model::Model, name::String, dest::Vector{T}) where {T<:AbstractFloat}
+function BMI.get_value(
+    model::Model,
+    name::String,
+    dest::Vector{T},
+) where {T <: AbstractFloat}
     dest .= copy(BMI.get_value_ptr(model, name))
     return dest
 end
@@ -243,7 +245,7 @@ function BMI.get_value_ptr(model::Model, name::String)
     (; network) = model
     s = split(name, "[")
     key = symbols(first(s))
-    if exchange(param(model, key[1:end-1]), key[end]) == 1
+    if exchange(param(model, key))
         n = length(active_indices(network, key))
         if occursin("[", name)
             ind = tryparse(Int, split(s[end], "]")[1])
@@ -270,7 +272,7 @@ function BMI.get_value_at_indices(
     name::String,
     dest::Vector{T},
     inds::Vector{Int},
-) where {T<:AbstractFloat}
+) where {T <: AbstractFloat}
     dest .= BMI.get_value_ptr(model, name)[inds]
     return dest
 end
@@ -281,8 +283,12 @@ end
 Set a model variable `name` to the values in vector `src`, overwriting the current contents.
 The type and size of `src` must match the model's internal array.
 """
-function BMI.set_value(model::Model, name::String, src::Vector{T}) where {T<:AbstractFloat}
-    BMI.get_value_ptr(model, name) .= src
+function BMI.set_value(
+    model::Model,
+    name::String,
+    src::Vector{T},
+) where {T <: AbstractFloat}
+    return BMI.get_value_ptr(model, name) .= src
 end
 
 """
@@ -296,8 +302,8 @@ function BMI.set_value_at_indices(
     name::String,
     inds::Vector{Int},
     src::Vector{T},
-) where {T<:AbstractFloat}
-    BMI.get_value_ptr(model, name)[inds] .= src
+) where {T <: AbstractFloat}
+    return BMI.get_value_ptr(model, name)[inds] .= src
 end
 
 function BMI.get_grid_type(model::Model, grid::Int)
@@ -318,7 +324,7 @@ function BMI.get_grid_rank(model::Model, grid::Int)
     end
 end
 
-function BMI.get_grid_x(model::Model, grid::Int, x::Vector{T}) where {T<:AbstractFloat}
+function BMI.get_grid_x(model::Model, grid::Int, x::Vector{T}) where {T <: AbstractFloat}
     (; reader, network) = model
     (; dataset) = reader
     sel = active_indices(network, grids[grid])
@@ -328,7 +334,7 @@ function BMI.get_grid_x(model::Model, grid::Int, x::Vector{T}) where {T<:Abstrac
     return x
 end
 
-function BMI.get_grid_y(model::Model, grid::Int, y::Vector{T}) where {T<:AbstractFloat}
+function BMI.get_grid_y(model::Model, grid::Int, y::Vector{T}) where {T <: AbstractFloat}
     (; reader, network) = model
     (; dataset) = reader
     sel = active_indices(network, grids[grid])
@@ -368,21 +374,21 @@ function BMI.get_grid_edge_nodes(model::Model, grid::Int, edge_nodes::Vector{Int
     # inactive nodes (boundary/ghost points) are set at -999
     if grid == 3
         nodes_at_edge = adjacent_nodes_at_link(network.river.graph)
-        nodes_at_edge.dst[nodes_at_edge.dst.==m+1] .= -999
-        edge_nodes[range(1, n, step = 2)] = nodes_at_edge.src
-        edge_nodes[range(2, n, step = 2)] = nodes_at_edge.dst
+        nodes_at_edge.dst[nodes_at_edge.dst .== m + 1] .= -999
+        edge_nodes[range(1, n; step = 2)] = nodes_at_edge.src
+        edge_nodes[range(2, n; step = 2)] = nodes_at_edge.dst
         return edge_nodes
     elseif grid == 4
         xu = network.land.staggered_indices.xu
-        edge_nodes[range(1, n, step = 2)] = 1:m
-        xu[xu.==m+1] .= -999
-        edge_nodes[range(2, n, step = 2)] = xu
+        edge_nodes[range(1, n; step = 2)] = 1:m
+        xu[xu .== m + 1] .= -999
+        edge_nodes[range(2, n; step = 2)] = xu
         return edge_nodes
     elseif grid == 5
         yu = network.land.staggered_indices.yu
-        edge_nodes[range(1, n, step = 2)] = 1:m
-        yu[yu.==m+1] .= -999
-        edge_nodes[range(2, n, step = 2)] = yu
+        edge_nodes[range(1, n; step = 2)] = 1:m
+        yu[yu .== m + 1] .= -999
+        edge_nodes[range(2, n; step = 2)] = yu
         return edge_nodes
     elseif grid in 0:2 || grid == 6
         @warn("edges are not provided for grid type $grid (variables are located at nodes)")
@@ -394,7 +400,7 @@ end
 # Extension of BMI functions (state handling and start time), required for OpenDA coupling.
 # May also be useful for other external software packages.
 function load_state(model::Model)
-    model = set_states(model)
+    set_states!(model)
     return model
 end
 
@@ -404,9 +410,12 @@ function save_state(model::Model)
         @info "Write output states to netCDF file `$(model.writer.state_nc_path)`."
     end
     write_netcdf_timestep(model, writer.state_dataset, writer.state_parameters)
-    close(writer.state_dataset)
+    return close(writer.state_dataset)
 end
 
 function get_start_unix_time(model::Model)
-    datetime2unix(DateTime(model.config.starttime))
+    return datetime2unix(DateTime(model.config.starttime))
 end
+
+exchange(t::Vector) = true
+exchange(t) = false
\ No newline at end of file
diff --git a/src/demand/water_demand.jl b/src/demand/water_demand.jl
new file mode 100644
index 000000000..ed3e7c77c
--- /dev/null
+++ b/src/demand/water_demand.jl
@@ -0,0 +1,934 @@
+abstract type AbstractIrrigationModel{T} end
+abstract type AbstractAllocationModel{T} end
+abstract type AbstractDemandModel end
+
+struct NoIrrigationPaddy{T} <: AbstractIrrigationModel{T} end
+struct NoIrrigationNonPaddy{T} <: AbstractIrrigationModel{T} end
+struct NoNonIrrigationDemand <: AbstractDemandModel end
+struct NoAllocationLand{T} <: AbstractAllocationModel{T} end
+
+"Struct to store non-irrigation water demand variables"
+@get_units @grid_loc @with_kw struct NonIrrigationDemandVariables{T}
+    returnflow::Vector{T}                   # return flow [mm Δt⁻¹]
+    returnflow_fraction::Vector{T} | "-"    # return flow fraction [-]
+end
+
+"Struct to store prescribed water demand variables"
+@get_units @grid_loc @with_kw struct PrescibedDemand{T}
+    demand_gross::Vector{T}     # gross water demand [mm Δt⁻¹]
+    demand_net::Vector{T}       # net water demand [mm Δt⁻¹]
+end
+
+"Non-irrigation water demand model"
+@with_kw struct NonIrrigationDemand{T} <: AbstractDemandModel
+    demand::PrescibedDemand{T}
+    variables::NonIrrigationDemandVariables{T}
+end
+
+# wrapper methods
+get_demand_gross(model::NonIrrigationDemand) = model.demand.demand_gross
+get_demand_gross(model::NoNonIrrigationDemand) = 0.0
+
+"Initialize non-irrigation water demand model for a water use `sector`"
+function NonIrrigationDemand(nc, config, inds, dt, sector)
+    demand_gross =
+        ncread(
+            nc,
+            config,
+            "vertical.demand.$(sector).demand_gross";
+            sel = inds,
+            defaults = 0.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+    demand_net =
+        ncread(
+            nc,
+            config,
+            "vertical.demand.$(sector).demand_net";
+            sel = inds,
+            defaults = 0.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+    n = length(inds)
+    returnflow_f = return_flow_fraction.(demand_gross, demand_net)
+
+    demand = PrescibedDemand{Float}(; demand_gross, demand_net)
+    vars = NonIrrigationDemandVariables{Float}(;
+        returnflow_fraction = returnflow_f,
+        returnflow = fill(Float(0), n),
+    )
+    non_irrigation_demand = NonIrrigationDemand{Float}(; demand, variables = vars)
+
+    return non_irrigation_demand
+end
+
+"Struct to store non-paddy irrigation model variables"
+@get_units @grid_loc @with_kw struct NonPaddyVariables{T}
+    demand_gross::Vector{T}     # irrigation gross demand [mm Δt⁻¹] 
+end
+
+"Struct to store non-paddy irrigation model parameters"
+@get_units @grid_loc @with_kw struct NonPaddyParameters{T}
+    irrigation_efficiency::Vector{T} | "-"      # irrigation efficiency [-]
+    maximum_irrigation_rate::Vector{T}          # maximum irrigation rate [mm Δt⁻¹]
+    irrigation_areas::Vector{Bool} | "-"        # irrigation areas [-]
+    irrigation_trigger::Vector{Bool} | "-"      # irrigation on or off [-]end
+end
+
+"Non-paddy (other crops than flooded rice) irrigation model"
+@with_kw struct NonPaddy{T} <: AbstractIrrigationModel{T}
+    parameters::NonPaddyParameters{T}
+    variables::NonPaddyVariables{T}
+end
+
+"Initialize non-paddy irrigation model"
+function NonPaddy(nc, config, inds, dt)
+    efficiency = ncread(
+        nc,
+        config,
+        "vertical.demand.nonpaddy.parameters.irrigation_efficiency";
+        sel = inds,
+        defaults = 1.0,
+        type = Float,
+    )
+    areas = ncread(
+        nc,
+        config,
+        "vertical.demand.nonpaddy.parameters.irrigation_areas";
+        sel = inds,
+        defaults = 1,
+        optional = false,
+        type = Int,
+    )
+    irrigation_trigger = ncread(
+        nc,
+        config,
+        "vertical.demand.nonpaddy.parameters.irrigation_trigger";
+        sel = inds,
+        defaults = 1,
+        optional = false,
+        type = Bool,
+    )
+    max_irri_rate =
+        ncread(
+            nc,
+            config,
+            "vertical.demand.nonpaddy.parameters.maximum_irrigation_rate";
+            sel = inds,
+            defaults = 25.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+
+    params = NonPaddyParameters{Float}(;
+        maximum_irrigation_rate = max_irri_rate,
+        irrigation_efficiency = efficiency,
+        irrigation_areas = areas,
+        irrigation_trigger,
+    )
+    vars = NonPaddyVariables{Float}(; demand_gross = fill(mv, length(inds)))
+
+    nonpaddy = NonPaddy{Float}(; variables = vars, parameters = params)
+
+    return nonpaddy
+end
+
+# wrapper methods
+get_demand_gross(model::NonPaddy) = model.variables.demand_gross
+get_demand_gross(model::NoIrrigationNonPaddy) = 0.0
+
+"""
+    update_demand_gross!(nonpaddy::NonPaddy, soil::SbmSoilModel)
+
+Update gross water demand `demand_gross` of the non-paddy irrigation model for a single
+timestep.
+
+The gross water demand is based on irrigation that is applied when the `irrigation_trigger`
+is `true` (`on`) and when water depletion exceeds the readily available water in the root
+zone of the SBM soil model. Irrigation brings the root zone back to field capacity, limited
+by the infiltration capacity, taking into account limited irrigation efficiency and limited
+by a maximum irrigation rate.
+"""
+function update_demand_gross!(nonpaddy::NonPaddy, soil::SbmSoilModel)
+    (; hb, theta_s, theta_r, c, sumlayers, act_thickl, pathfrac, infiltcapsoil) =
+        soil.parameters
+    (; h3, n_unsatlayers, zi, ustorelayerdepth, f_infiltration_reduction) = soil.variables
+    (;
+        irrigation_areas,
+        irrigation_trigger,
+        maximum_irrigation_rate,
+        irrigation_efficiency,
+    ) = nonpaddy.parameters
+    rootingdepth = get_rootingdepth(soil)
+
+    for i in eachindex(irrigation_areas)
+        if irrigation_areas[i] && irrigation_trigger[i]
+            usl = set_layerthickness(zi[i], sumlayers[i], act_thickl[i])
+            irri_dem_gross = 0.0
+            for k in 1:n_unsatlayers[i]
+                # compute water demand only for root zone through root fraction per layer
+                rootfrac = min(1.0, (max(0.0, rootingdepth[i] - sumlayers[i][k]) / usl[k]))
+                # vwc_f and vwc_h3 can be precalculated.
+                vwc_fc = vwc_brooks_corey(-100.0, hb[i], theta_s[i], theta_r[i], c[i][k])
+                vwc_h3 = vwc_brooks_corey(h3[i], hb[i], theta_s[i], theta_r[i], c[i][k])
+                depletion =
+                    (vwc_fc * usl[k]) - (ustorelayerdepth[i][k] + theta_r[i] * usl[k])
+                depletion *= rootfrac
+                raw = (vwc_fc - vwc_h3) * usl[k] # readily available water
+                raw *= rootfrac
+
+                # check if maximum irrigation rate has been applied at the previous time step.
+                max_irri_rate_applied =
+                    nonpaddy.variables.demand_gross[i] == maximum_irrigation_rate[i]
+                if depletion >= raw # start irrigation
+                    irri_dem_gross += depletion
+                    # add depletion to irrigation gross demand when the maximum irrigation rate has been 
+                    # applied at the previous time step (to get volumetric water content at field capacity)
+                elseif depletion > 0.0 && max_irri_rate_applied # continue irrigation
+                    irri_dem_gross += depletion
+                end
+            end
+            # limit irrigation demand to infiltration capacity 
+            infiltration_capacity =
+                f_infiltration_reduction[i] * (1.0 - pathfrac[i]) * infiltcapsoil[i]
+            irri_dem_gross = min(irri_dem_gross, infiltration_capacity)
+            irri_dem_gross /= irrigation_efficiency[i]
+            # limit irrigation demand to the maximum irrigation rate
+            irri_dem_gross = min(irri_dem_gross, maximum_irrigation_rate[i])
+        else
+            irri_dem_gross = 0.0
+        end
+        nonpaddy.variables.demand_gross[i] = irri_dem_gross
+    end
+    return nothing
+end
+update_demand_gross!(nonpaddy::NoIrrigationNonPaddy, soil::SbmSoilModel) = nothing
+
+"Struct to store paddy irrigation model variables"
+@get_units @grid_loc @with_kw struct PaddyVariables{T}
+    demand_gross::Vector{T}     # irrigation gross demand [mm Δt⁻¹]
+    h::Vector{T} | "mm"         # actual water depth in rice field [mm]
+    evaporation::Vector{T}      # evaporation rate [mm Δt⁻¹] 
+end
+
+"Struct to store paddy irrigation model parameters"
+@get_units @grid_loc @with_kw struct PaddyParameters{T}
+    irrigation_efficiency::Vector{T} | "-"      # irrigation efficiency [-]
+    maximum_irrigation_rate::Vector{T}          # maximum irrigation rate [mm Δt⁻¹]
+    irrigation_areas::Vector{Bool} | "-"        # irrigation areas [-]
+    irrigation_trigger::Vector{Bool} | "-"      # irrigation on or off [-]
+    h_min::Vector{T} | "mm"                     # minimum required water depth in the irrigated rice field [mm]
+    h_opt::Vector{T} | "mm"                     # optimal water depth in the irrigated rice fields [mm]
+    h_max::Vector{T} | "mm"                     # water depth when rice field starts spilling water (overflow) [mm]
+end
+
+"Paddy (flooded rice) irrigation model"
+@with_kw struct Paddy{T} <: AbstractIrrigationModel{T}
+    parameters::PaddyParameters{T}
+    variables::PaddyVariables{T}
+end
+
+"Initialize paddy irrigation model"
+function Paddy(nc, config, inds, dt)
+    h_min = ncread(
+        nc,
+        config,
+        "vertical.demand.paddy.parameters.h_min";
+        sel = inds,
+        defaults = 20.0,
+        type = Float,
+    )
+    h_opt = ncread(
+        nc,
+        config,
+        "vertical.demand.paddy.parameters.h_opt";
+        sel = inds,
+        defaults = 50.0,
+        type = Float,
+    )
+    h_max = ncread(
+        nc,
+        config,
+        "vertical.demand.paddy.parameters.h_max";
+        sel = inds,
+        defaults = 80.0,
+        type = Float,
+    )
+    efficiency = ncread(
+        nc,
+        config,
+        "vertical.demand.paddy.parameters.irrigation_efficiency";
+        sel = inds,
+        defaults = 1.0,
+        type = Float,
+    )
+    areas = ncread(
+        nc,
+        config,
+        "vertical.demand.paddy.parameters.irrigation_areas";
+        sel = inds,
+        optional = false,
+        type = Bool,
+    )
+    irrigation_trigger = ncread(
+        nc,
+        config,
+        "vertical.demand.paddy.parameters.irrigation_trigger";
+        sel = inds,
+        optional = false,
+        type = Bool,
+    )
+    max_irri_rate =
+        ncread(
+            nc,
+            config,
+            "vertical.demand.paddy.parameters.maximum_irrigation_rate";
+            sel = inds,
+            defaults = 25.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+    n = length(inds)
+    params = PaddyParameters{Float}(;
+        irrigation_efficiency = efficiency,
+        maximum_irrigation_rate = max_irri_rate,
+        irrigation_trigger,
+        h_min,
+        h_max,
+        h_opt,
+        irrigation_areas = areas,
+    )
+    vars = PaddyVariables{Float}(;
+        demand_gross = fill(mv, n),
+        h = fill(0.0, n),
+        evaporation = fill(0.0, n),
+    )
+    paddy = Paddy{Float}(; parameters = params, variables = vars)
+    return paddy
+end
+
+# wrapper methods
+get_water_depth(model::Paddy) = model.variables.h
+get_water_depth(model::NoIrrigationPaddy) = 0.0
+get_demand_gross(model::Paddy) = model.variables.demand_gross
+get_demand_gross(model::NoIrrigationPaddy) = 0.0
+
+"""
+    evaporation!(model::Paddy, potential_evaporation)
+
+Update `evaporation` and the water depth `h` of the paddy irrigation model for a single
+timestep.
+"""
+function evaporation!(model::Paddy, potential_evaporation)
+    for i in eachindex(potential_evaporation)
+        if model.parameters.irrigation_areas[i]
+            evaporation = min(model.variables.h[i], potential_evaporation[i])
+            model.variables.h[i] -= evaporation
+            model.variables.evaporation[i] = evaporation
+        end
+    end
+    return nothing
+end
+evaporation!(model::NoIrrigationPaddy, potential_evaporation) = nothing
+
+# wrapper methods
+get_evaporation(model::NoIrrigationPaddy) = 0.0
+get_evaporation(model::Paddy) = model.variables.evaporation
+
+"""
+    update_runoff!(model::Paddy, runoff)
+
+Update `runoff` based on the water depth `h_max` (paddy field starts spilling), and update
+the water depth `h` of the paddy irrigation model for a single timestep.
+"""
+function update_runoff!(model::Paddy, runoff)
+    for i in eachindex(model.parameters.irrigation_areas)
+        if model.parameters.irrigation_areas[i]
+            paddy_runoff = max(runoff[i] - model.parameters.h_max[i], 0.0)
+            model.variables.h[i] = runoff[i] - paddy_runoff
+            runoff[i] = paddy_runoff
+        end
+    end
+    return nothing
+end
+update_runoff!(model::NoIrrigationPaddy, runoff) = nothing
+
+"""
+    update_demand_gross!(model::Paddy)
+
+Update gross water demand `demand_gross` of the paddy irrigation model for a single
+timestep.
+
+The gross water demand is based on irrigation that is applied when the `irrigation_trigger`
+is `true` (`on`) and when the paddy water depth `h` reaches below the minimum water depth
+`h_min`. Irrigation is the amount required to reach the optimal paddy water depth `h_opt`,
+taking into account limited irrigation efficiency and limited by a maximum irrigation rate.
+"""
+function update_demand_gross!(model::Paddy)
+    (;
+        irrigation_areas,
+        irrigation_trigger,
+        irrigation_efficiency,
+        maximum_irrigation_rate,
+        h_opt,
+        h_min,
+    ) = model.parameters
+    (; h, demand_gross) = model.variables
+    for i in eachindex(irrigation_areas)
+        if irrigation_areas[i] && irrigation_trigger[i]
+            # check if maximum irrigation rate has been applied at the previous time step.
+            max_irri_rate_applied = demand_gross[i] == maximum_irrigation_rate[i]
+            # start irrigation
+            irr_depth_paddy = if h[i] < h_min[i]
+                h_opt[i] - h[i]
+            elseif h[i] < h_opt[i] && max_irri_rate_applied # continue irrigation
+                h_opt[i] - h[i]
+            else
+                0.0
+            end
+            irri_dem_gross = irr_depth_paddy / irrigation_efficiency[i]
+            # limit irrigation demand to the maximum irrigation rate
+            irri_dem_gross = min(irri_dem_gross, maximum_irrigation_rate[i])
+        else
+            irri_dem_gross = 0.0
+        end
+        demand_gross[i] = irri_dem_gross
+    end
+    return nothing
+end
+
+update_demand_gross!(paddy::NoIrrigationPaddy) = nothing
+
+"Struct to store water demand model variables"
+@get_units @grid_loc @with_kw struct DemandVariables{T}
+    irri_demand_gross::Vector{T}        # irrigation gross demand [mm Δt⁻¹]
+    nonirri_demand_gross::Vector{T}     # non-irrigation gross demand [mm Δt⁻¹]
+    total_gross_demand::Vector{T}       # total gross demand [mm Δt⁻¹]
+    surfacewater_demand::Vector{T}      # demand from surface water [mm Δt⁻¹]
+    groundwater_demand::Vector{T}       # demand from groundwater [mm Δt⁻¹]
+end
+
+"Initialize water demand variables"
+function DemandVariables(T::Type{<:AbstractFloat}, n::Int)
+    return DemandVariables{T}(;
+        irri_demand_gross = zeros(T, n),
+        nonirri_demand_gross = zeros(T, n),
+        total_gross_demand = zeros(T, n),
+        surfacewater_demand = zeros(T, n),
+        groundwater_demand = zeros(T, n),
+    )
+end
+
+"Water demand model"
+@with_kw struct Demand{D, I, L, P, NP, V} <: AbstractDemandModel
+    domestic::D
+    industry::I
+    livestock::L
+    paddy::P
+    nonpaddy::NP
+    variables::V
+end
+
+@with_kw struct NoDemand{T} <: AbstractDemandModel
+    domestic::NoNonIrrigationDemand = NoNonIrrigationDemand()
+    industry::NoNonIrrigationDemand = NoNonIrrigationDemand()
+    livestock::NoNonIrrigationDemand = NoNonIrrigationDemand()
+    paddy::NoIrrigationPaddy{T} = NoIrrigationPaddy{T}()
+    nonpaddy::NoIrrigationNonPaddy{T} = NoIrrigationNonPaddy{T}()
+end
+
+"Initialize water demand model"
+function Demand(nc, config, inds, dt)
+    domestic = if get(config.model.water_demand, "domestic", false)
+        NonIrrigationDemand(nc, config, inds, dt, "domestic")
+    else
+        NoNonIrrigationDemand()
+    end
+    industry = if get(config.model.water_demand, "industry", false)
+        NonIrrigationDemand(nc, config, inds, dt, "industry")
+    else
+        NoNonIrrigationDemand()
+    end
+    livestock = if get(config.model.water_demand, "livestock", false)
+        NonIrrigationDemand(nc, config, inds, dt, "livestock")
+    else
+        NoNonIrrigationDemand()
+    end
+    paddy = if get(config.model.water_demand, "paddy", false)
+        Paddy(nc, config, inds, dt)
+    else
+        NoIrrigationPaddy{Float}()
+    end
+    nonpaddy = if get(config.model.water_demand, "nonpaddy", false)
+        NonPaddy(nc, config, inds, dt)
+    else
+        NoIrrigationNonPaddy{Float}()
+    end
+
+    n = length(inds)
+    vars = DemandVariables(Float, n)
+    demand = Demand(; domestic, industry, livestock, paddy, nonpaddy, variables = vars)
+    return demand
+end
+
+"Struct to store river allocation model variables"
+@get_units @grid_loc @with_kw struct AllocationRiverVariables{T}
+    act_surfacewater_abst::Vector{T}                        # actual surface water abstraction [mm Δt⁻¹]
+    act_surfacewater_abst_vol::Vector{T} | "m3 dt-1"        # actual surface water abstraction [m³ Δt⁻¹]
+    available_surfacewater::Vector{T} | "m3"                # available surface water [m³]
+    nonirri_returnflow::Vector{T}                           # return flow from non irrigation [mm Δt⁻¹] 
+end
+
+"Initialize river allocation model variables"
+function AllocationRiverVariables(T::Type{<:AbstractFloat}, n::Int)
+    return AllocationRiverVariables{T}(;
+        act_surfacewater_abst = zeros(T, n),
+        act_surfacewater_abst_vol = zeros(T, n),
+        available_surfacewater = zeros(T, n),
+        nonirri_returnflow = zeros(T, n),
+    )
+end
+
+"River allocation model"
+@with_kw struct AllocationRiver{T} <: AbstractAllocationModel{T}
+    variables::AllocationRiverVariables{T}
+end
+
+"Initialize water allocation for the river domain"
+function AllocationRiver(n)
+    vars = AllocationRiverVariables(Float, n)
+    allocation = AllocationRiver{Float}(; variables = vars)
+    return allocation
+end
+
+"Struct to store land allocation allocation model parameters"
+@get_units @grid_loc @with_kw struct AllocationLandParameters{T}
+    frac_sw_used::Vector{T} | "-"       # fraction surface water used [-]
+    areas::Vector{Int} | "-"            # allocation areas [-]
+end
+
+"Struct to store land allocation model variables"
+@get_units @grid_loc @with_kw struct AllocationLandVariables{T}
+    surfacewater_alloc::Vector{T}                       # allocation from surface water [mm Δt⁻¹]
+    act_groundwater_abst::Vector{T}                     # actual groundwater abstraction [mm Δt⁻¹]
+    act_groundwater_abst_vol::Vector{T} | "m3 dt-1"     # actual groundwater abstraction [m³ Δt⁻¹]
+    available_groundwater::Vector{T} | "m3"             # available groundwater [m³]
+    groundwater_alloc::Vector{T}                        # allocation from groundwater [mm Δt⁻¹]
+    irri_alloc::Vector{T}                               # allocated water for irrigation [mm Δt⁻¹]
+    nonirri_alloc::Vector{T}                            # allocated water for non-irrigation [mm Δt⁻¹]
+    total_alloc::Vector{T}                              # total allocated water [mm Δt⁻¹]
+    nonirri_returnflow::Vector{T}                       # return flow from non irrigation [mm Δt⁻¹]
+end
+
+"Initialize land allocation model variables"
+function AllocationLandVariables(T::Type{<:AbstractFloat}, n::Int)
+    return AllocationLandVariables{T}(;
+        surfacewater_alloc = zeros(T, n),
+        act_groundwater_abst = zeros(T, n),
+        act_groundwater_abst_vol = zeros(T, n),
+        available_groundwater = zeros(T, n),
+        groundwater_alloc = zeros(T, n),
+        irri_alloc = zeros(T, n),
+        nonirri_alloc = zeros(T, n),
+        total_alloc = zeros(T, n),
+        nonirri_returnflow = zeros(T, n),
+    )
+end
+
+"Land allocation model"
+@with_kw struct AllocationLand{T} <: AbstractAllocationModel{T}
+    parameters::AllocationLandParameters{T}
+    variables::AllocationLandVariables{T}
+end
+
+"Initialize water allocation for the land domain"
+function AllocationLand(nc, config, inds)
+    frac_sw_used = ncread(
+        nc,
+        config,
+        "vertical.allocation.parameters.frac_sw_used";
+        sel = inds,
+        defaults = 1,
+        type = Float,
+    )
+    areas = ncread(
+        nc,
+        config,
+        "vertical.allocation.parameters.areas";
+        sel = inds,
+        defaults = 1,
+        type = Int,
+    )
+
+    n = length(inds)
+
+    params = AllocationLandParameters(; areas = areas, frac_sw_used = frac_sw_used)
+    vars = AllocationLandVariables(Float, n)
+    allocation = AllocationLand(; parameters = params, variables = vars)
+    return allocation
+end
+
+# wrapper methods
+get_irrigation_allocated(model::AllocationLand) = model.variables.irri_alloc
+get_irrigation_allocated(model::NoAllocationLand) = 0.0
+
+"Return return flow fraction based on gross water demand `demand_gross` and net water demand `demand_net`"
+function return_flow_fraction(demand_gross, demand_net)
+    fraction = bounded_divide(demand_net, demand_gross)
+    returnflow_fraction = 1.0 - fraction
+    return returnflow_fraction
+end
+
+"Update returnflow fraction for a non-irrigation water demand model"
+function return_flow_fraction!(model::NonIrrigationDemand)
+    (; returnflow_fraction) = model.variables
+    (; demand_gross, demand_net) = model.demand
+    @. returnflow_fraction = return_flow_fraction(demand_gross, demand_net)
+    return nothing
+end
+
+# return zero (gross water demand) if non-irrigation water demand sector is not defined
+return_flow_fraction!(model::NoNonIrrigationDemand) = nothing
+
+"Update water allocation for river and land domains based on local surface water (river) availability."
+function surface_water_allocation_local!(land_allocation, demand, river, network, dt)
+    (; surfacewater_alloc) = land_allocation.variables
+    (; surfacewater_demand) = demand.variables
+    (; act_surfacewater_abst_vol, act_surfacewater_abst, available_surfacewater) =
+        river.allocation.variables
+    # maps from the land domain to the internal river domain (linear index), excluding water bodies
+    index_river = network.land.index_river_wb
+    for i in eachindex(surfacewater_demand)
+        if index_river[i] > 0.0
+            # the available volume is limited by a fixed scaling factor of 0.8 to prevent
+            # rivers completely drying out. check for abstraction through inflow (external
+            # negative inflow) and adjust available volume.
+            if river.inflow[index_river[i]] < 0.0
+                inflow = river.inflow[index_river[i]] * dt
+                available_volume = max(river.volume[index_river[i]] * 0.80 + inflow, 0.0)
+            else
+                available_volume = river.volume[index_river[i]] * 0.80
+            end
+            # satisfy surface water demand with available local river volume
+            surfacewater_demand_vol = surfacewater_demand[i] * 0.001 * network.land.area[i]
+            abstraction_vol = min(surfacewater_demand_vol, available_volume)
+            act_surfacewater_abst_vol[index_river[i]] = abstraction_vol
+            # remaining available surface water and demand 
+            available_surfacewater[index_river[i]] =
+                max(available_volume - abstraction_vol, 0.0)
+            abstraction = (abstraction_vol / network.land.area[i]) * 1000.0
+            surfacewater_demand[i] = max(surfacewater_demand[i] - abstraction, 0.0)
+            # update actual abstraction from river and surface water allocation (land cell)
+            act_surfacewater_abst[index_river[i]] = abstraction
+            surfacewater_alloc[i] = abstraction
+        end
+    end
+    return nothing
+end
+
+"Update water allocation for river and land domains based on surface water (river) availability for allocation areas."
+function surface_water_allocation_area!(land_allocation, demand, river, network)
+    inds_river = network.river.indices_allocation_areas
+    inds_land = network.land.indices_allocation_areas
+    res_index = network.river.reservoir_index
+    lake_index = network.river.lake_index
+
+    (; available_surfacewater, act_surfacewater_abst_vol, act_surfacewater_abst) =
+        river.allocation.variables
+    (; surfacewater_alloc) = land_allocation.variables
+    (; surfacewater_demand) = demand.variables
+
+    # loop over allocation areas
+    for i in eachindex(inds_river)
+        # surface water demand (allocation area)
+        sw_demand_vol = 0.0
+        for j in inds_land[i]
+            sw_demand_vol += surfacewater_demand[j] * 0.001 * network.land.area[j]
+        end
+        # surface water availability (allocation area)
+        sw_available = 0.0
+        for j in inds_river[i]
+            if res_index[j] > 0
+                # for reservoir locations use reservoir volume
+                k = res_index[j]
+                available_surfacewater[j] = river.reservoir.volume[k] * 0.98 # limit available reservoir volume
+                sw_available += available_surfacewater[j]
+            elseif lake_index[j] > 0
+                # for lake locations use lake volume
+                k = lake_index[j]
+                available_surfacewater[j] = river.lake.storage[k] * 0.98 # limit available lake volume
+                sw_available += available_surfacewater[j]
+
+            else
+                # river volume
+                sw_available += available_surfacewater[j]
+            end
+        end
+        # total actual surface water abstraction [m3] in an allocation area, minimum of
+        # available surface water and demand in an allocation area.
+        sw_abstraction = min(sw_available, sw_demand_vol)
+
+        # fraction of available surface water that can be abstracted at allocation area
+        # level
+        frac_abstract_sw = bounded_divide(sw_abstraction, sw_available)
+        # fraction of water demand that can be satisfied by available surface water at
+        # allocation area level. 
+        frac_allocate_sw = bounded_divide(sw_abstraction, sw_demand_vol)
+
+        # water abstracted from surface water at each river cell (including reservoir and
+        # lake locations).
+        for j in inds_river[i]
+            act_surfacewater_abst_vol[j] += frac_abstract_sw * available_surfacewater[j]
+            act_surfacewater_abst[j] =
+                (act_surfacewater_abst_vol[j] / network.river.area[j]) * 1000.0
+        end
+
+        # water allocated to each land cell.
+        for j in inds_land[i]
+            surfacewater_alloc[j] += frac_allocate_sw * surfacewater_demand[j]
+        end
+    end
+    return nothing
+end
+
+"Update water allocation for land domain based on local groundwater availability."
+function groundwater_allocation_local!(land_allocation, demand, groundwater_volume, network)
+    (;
+        surfacewater_alloc,
+        act_groundwater_abst_vol,
+        available_groundwater,
+        act_groundwater_abst,
+        groundwater_alloc,
+    ) = land_allocation.variables
+    (; groundwater_demand, total_gross_demand) = demand.variables
+
+    for i in eachindex(groundwater_demand)
+        # groundwater demand based on allocation from surface water.
+        groundwater_demand[i] = max(total_gross_demand[i] - surfacewater_alloc[i], 0.0)
+        # land index excluding water bodies
+        if network.index_wb[i]
+            # satisfy groundwater demand with available local groundwater volume
+            groundwater_demand_vol = groundwater_demand[i] * 0.001 * network.area[i]
+            available_volume = groundwater_volume[i] * 0.75 # limit available groundwater volume
+            abstraction_vol = min(groundwater_demand_vol, available_volume)
+            act_groundwater_abst_vol[i] = abstraction_vol
+            # remaining available groundwater and demand 
+            available_groundwater[i] = max(available_volume - abstraction_vol, 0.0)
+            abstraction = (abstraction_vol / network.area[i]) * 1000.0
+            groundwater_demand[i] = max(groundwater_demand[i] - abstraction, 0.0)
+            # update actual abstraction from groundwater and groundwater allocation (land cell)
+            act_groundwater_abst[i] = abstraction
+            groundwater_alloc[i] = abstraction
+        end
+    end
+    return nothing
+end
+
+"Update water allocation for land domain based on groundwater availability for allocation areas."
+function groundwater_allocation_area!(land_allocation, demand, network)
+    inds_river = network.river.indices_allocation_areas
+    inds_land = network.land.indices_allocation_areas
+    (;
+        act_groundwater_abst_vol,
+        available_groundwater,
+        act_groundwater_abst,
+        groundwater_alloc,
+    ) = land_allocation.variables
+
+    (; groundwater_demand) = demand.variables
+
+    # loop over allocation areas
+    for i in eachindex(inds_river)
+        # groundwater demand and availability (allocation area)
+        gw_demand_vol = 0.0
+        gw_available = 0.0
+        for j in inds_land[i]
+            gw_demand_vol += groundwater_demand[j] * 0.001 * network.land.area[j]
+            gw_available += available_groundwater[j]
+        end
+        # total actual groundwater abstraction [m3] in an allocation area, minimum of
+        # available  groundwater and demand in an allocation area.
+        gw_abstraction = min(gw_available, gw_demand_vol)
+
+        # fraction of available groundwater that can be abstracted at allocation area level
+        frac_abstract_gw = bounded_divide(gw_abstraction, gw_available)
+        # fraction of water demand that can be satisfied by available groundwater at
+        # allocation area level.
+        frac_allocate_gw = bounded_divide(gw_abstraction, gw_demand_vol)
+
+        # water abstracted from groundwater and allocated.
+        for j in inds_land[i]
+            act_groundwater_abst_vol[j] += frac_abstract_gw * available_groundwater[j]
+            act_groundwater_abst[j] =
+                1000.0 * (act_groundwater_abst_vol[j] / network.land.area[j])
+            groundwater_alloc[j] += frac_allocate_gw * groundwater_demand[j]
+        end
+    end
+    return nothing
+end
+
+"Return and update non-irrigation sector (domestic, livestock, industry) return flow"
+function return_flow(non_irri::NonIrrigationDemand, nonirri_demand_gross, nonirri_alloc)
+    for i in eachindex(non_irri.variables.returnflow)
+        frac = bounded_divide(non_irri.demand.demand_gross[i], nonirri_demand_gross[i])
+        allocate = frac * nonirri_alloc[i]
+        non_irri.variables.returnflow[i] =
+            non_irri.variables.returnflow_fraction[i] * allocate
+    end
+    return non_irri.variables.returnflow
+end
+
+# return zero (return flow) if non-irrigation sector is not defined
+return_flow(non_irri::NoNonIrrigationDemand, nonirri_demand_gross, nonirri_alloc) = 0.0
+
+# wrapper methods
+groundwater_volume(model::LateralSSF) = model.volume
+groundwater_volume(model) = model.flow.aquifer.volume
+
+"""
+    update_water_allocation!(land_allocation, demand::Demand, lateral, network, dt)
+
+Update water allocation for the land domain `land_allocation` and water allocation for the
+river domain (part of `lateral`) based on the water `demand` model for a single timestep.
+First, surface water abstraction is computed to satisfy local water demand (non-irrigation
+and irrigation), and then updated (including lakes and reservoirs) to satisfy the remaining
+water demand for allocation areas. Then groundwater abstraction is computed to satisfy the
+remaining local water demand, and then updated to satisfy the remaining water demand for
+allocation areas. Finally, non-irrigation return flows are updated.
+"""
+function update_water_allocation!(land_allocation, demand::Demand, lateral, network, dt)
+    river = lateral.river
+    index_river = network.land.index_river_wb
+    res_index_f = network.river.reservoir_index_f
+    lake_index_f = network.river.lake_index_f
+    (;
+        groundwater_alloc,
+        surfacewater_alloc,
+        act_groundwater_abst,
+        act_groundwater_abst_vol,
+        total_alloc,
+        irri_alloc,
+        nonirri_alloc,
+        nonirri_returnflow,
+    ) = land_allocation.variables
+
+    (; surfacewater_demand, nonirri_demand_gross, irri_demand_gross, total_gross_demand) =
+        demand.variables
+
+    (; frac_sw_used) = land_allocation.parameters
+    (; act_surfacewater_abst, act_surfacewater_abst_vol) = river.allocation.variables
+
+    surfacewater_alloc .= 0.0
+    act_surfacewater_abst .= 0.0
+    act_surfacewater_abst_vol .= 0.0
+    # total surface water demand for each land cell
+    @. surfacewater_demand =
+        frac_sw_used * nonirri_demand_gross + frac_sw_used * irri_demand_gross
+
+    # local surface water demand and allocation (river, excluding reservoirs and lakes)
+    surface_water_allocation_local!(land_allocation, demand, river, network, dt)
+    # surface water demand and allocation for areas
+    surface_water_allocation_area!(land_allocation, demand, river, network)
+
+    @. river.abstraction = act_surfacewater_abst_vol / dt
+
+    # for reservoir and lake locations set river abstraction at zero and abstract volume
+    # from reservoir and lake, including an update of lake waterlevel
+    if !isnothing(river.reservoir)
+        @. river.abstraction[res_index_f] = 0.0
+        @. river.reservoir.volume -= act_surfacewater_abst_vol[res_index_f]
+    elseif !isnothing(river.lake)
+        @. river.abstraction[lake_index_f] = 0.0
+        lakes = river.lake
+        @. lakes.storage -= act_surfacewater_abst_vol[lake_index_f]
+        @. lakes.waterlevel =
+            waterlevel(lakes.storfunc, lakes.area, lakes.storage, lakes.sh)
+    end
+
+    groundwater_alloc .= 0.0
+    act_groundwater_abst_vol .= 0.0
+    act_groundwater_abst .= 0.0
+    # local groundwater demand and allocation
+    groundwater_allocation_local!(
+        land_allocation,
+        demand,
+        groundwater_volume(lateral.subsurface),
+        network.land,
+    )
+    # groundwater demand and allocation for areas
+    groundwater_allocation_area!(land_allocation, demand, network)
+
+    # irrigation allocation
+    for i in eachindex(total_alloc)
+        total_alloc[i] = groundwater_alloc[i] + surfacewater_alloc[i]
+        frac_irri = bounded_divide(irri_demand_gross[i], total_gross_demand[i])
+        irri_alloc[i] = frac_irri * total_alloc[i]
+        nonirri_alloc[i] = total_alloc[i] - irri_alloc[i]
+    end
+
+    # non-irrigation return flows
+    returnflow_livestock =
+        return_flow(demand.livestock, nonirri_demand_gross, nonirri_alloc)
+    returnflow_domestic = return_flow(demand.domestic, nonirri_demand_gross, nonirri_alloc)
+    returnflow_industry = return_flow(demand.industry, nonirri_demand_gross, nonirri_alloc)
+
+    @. nonirri_returnflow = returnflow_livestock + returnflow_domestic + returnflow_industry
+
+    for i in eachindex(nonirri_returnflow)
+        if index_river[i] > 0.0
+            k = index_river[i]
+            river.allocation.variables.nonirri_returnflow[k] = nonirri_returnflow[i]
+            nonirri_returnflow[i] = 0.0
+        else
+            nonirri_returnflow[i] = nonirri_returnflow[i]
+        end
+    end
+end
+update_water_allocation!(allocation, demand::NoDemand, lateral, network, dt) = nothing
+
+"""
+    update_demand_gross!(demand::Demand)
+
+Update total irrigation gross water demand `irri_demand_gross`, total non-irrigation gross
+water demand `nonirri_demand_gross` and total gross water demand `total_gross_demand`.
+"""
+function update_demand_gross!(demand::Demand)
+    (; nonpaddy, paddy, domestic, industry, livestock) = demand
+    (; irri_demand_gross, nonirri_demand_gross, total_gross_demand) = demand.variables
+    # get gross water demands
+    industry_dem = get_demand_gross(industry)
+    domestic_dem = get_demand_gross(domestic)
+    livestock_dem = get_demand_gross(livestock)
+    nonpaddy_dem_gross = get_demand_gross(nonpaddy)
+    paddy_dem_gross = get_demand_gross(paddy)
+    # update gross water demands
+    @. irri_demand_gross = nonpaddy_dem_gross + paddy_dem_gross
+    @. nonirri_demand_gross = industry_dem + domestic_dem + livestock_dem
+    @. total_gross_demand =
+        nonpaddy_dem_gross + paddy_dem_gross + industry_dem + domestic_dem + livestock_dem
+
+    return nothing
+end
+
+update_demand_gross!(demand::NoDemand) = nothing
+
+"""
+    update_water_demand!(demand::Demand, soil)
+
+Update the return flow fraction `returnflow_fraction` of `industry`, `domestic` and
+`livestock`, gross water demand `demand_gross` of `paddy` and `nonpaddy` models, and the
+total gross water demand, total irrigation gross water demand and total non-irrigation gross
+water demand as part of the water `demand` model.
+"""
+function update_water_demand!(demand::Demand, soil)
+    (; nonpaddy, paddy, domestic, industry, livestock) = demand
+
+    return_flow_fraction!(industry)
+    return_flow_fraction!(domestic)
+    return_flow_fraction!(livestock)
+
+    update_demand_gross!(nonpaddy, soil)
+    update_demand_gross!(paddy)
+    update_demand_gross!(demand)
+
+    return nothing
+end
+update_water_demand!(demand::NoDemand, soil) = nothing
\ No newline at end of file
diff --git a/src/flextopo.jl b/src/flextopo.jl
deleted file mode 100644
index d547a34b9..000000000
--- a/src/flextopo.jl
+++ /dev/null
@@ -1,882 +0,0 @@
-@get_units @exchange @grid_type @grid_location @with_kw struct FLEXTOPO{T,N}
-    # Model time step [s]
-    dt::T | "s" | 0 | "none" | "none"
-    # Number of classes
-    nclass::Int | "-" | 0 | "none" | "none"
-    # Number of cells
-    n::Int | "-" | 0 | "none" | "none"
-    #dictionary with all possible functions for each store
-    dic_function::Dict | "-" | 0 | "none" | "none"
-    #current class
-    kclass::Vector{Int64} | "-" | 0 | "none" | "none"
-    classes::Vector{String} | "-" | 0 | "none" | "none"
-    select_snow::Vector{String} | "-" | 0 | "none" | "none"
-    select_interception::Vector{String} | "-" | 0 | "none" | "none"
-    select_hortonponding::Vector{String} | "-" | 0 | "none" | "none"
-    select_hortonrunoff::Vector{String} | "-" | 0 | "none" | "none"
-    select_rootzone::Vector{String} | "-" | 0 | "none" | "none"
-    select_fast::Vector{String} | "-" | 0 | "none" | "none"
-    select_slow::Vector{String} | "-" | 0 | "none" | "none"
-
-    #fraction of each class
-    hrufrac::Vector{SVector{N,T}} | "-"
-    ## PARAMETERS
-    ##SNOW
-    # Correction factor for precipitation [-]
-    pcorr::Vector{T} | "-"
-    # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹]
-    cfmax::Vector{T} | "mm ᵒC-1 dt-1"
-    # Threshold temperature for snowfall [ᵒC]
-    tt::Vector{T} | "ᵒC"
-    # Threshold temperature interval length [ᵒC]
-    tti::Vector{T} | "ᵒC"
-    # Threshold temperature for snowmelt [ᵒC]
-    ttm::Vector{T} | "ᵒC"
-    # Water holding capacity as fraction of current snow pack [-]
-    whc::Vector{T} | "-"
-    # Refreezing efficiency constant in refreezing of freewater in snow [-]
-    cfr::Vector{T} | "-"
-    # Correction factor for precipitation [-]
-    rfcf::Vector{T} | "-"
-    # Correction factor for snowfall [-]
-    sfcf::Vector{T} | "-"
-    ## GLACIER
-    # Threshold temperature for snowfall above glacier [ᵒC]
-    g_tt::Vector{T} | "ᵒC"
-    # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹] for glacier
-    g_cfmax::Vector{T} | "mm ᵒC-1 dt-1"
-    # Fraction of the snowpack on top of the glacier converted into ice [Δt⁻¹]
-    g_sifrac::Vector{T} | "dt-1"
-    # Water within the glacier [mm]
-    glacierstore::Vector{T} | "mm"
-    # Fraction covered by a glacier [-]
-    glacierfrac::Vector{T} | "-"
-    ##INTERCEPTION
-    # Maximum interception storage (in forested and non-forested areas) [mm]
-    imax::Vector{SVector{N,T}} | "mm"
-    # Evap correction [-]
-    ecorr::Vector{T} | "-"
-    ##HORTON
-    # Maximum storage capacity in the hortonian ponding storage [mm]
-    shmax::Vector{SVector{N,T}} | "mm"
-    #recession coefficient of the hortonian runoff storage [dt-1]
-    khf::Vector{SVector{N,T}} | "dt-1"
-    #maximum modelled accumulated frost resulting in shmin [ᵒC Δt]
-    facc0::Vector{SVector{N,T}} | "ᵒC"
-    #minimum modelled accumulated frost resulting in shmax [ᵒC Δt]
-    facc1::Vector{SVector{N,T}} | "ᵒC"
-    #exponent for the decline of infiltration capacity [-]
-    fdec::Vector{SVector{N,T}} | "-"
-    #maximum infiltration capacity from horton ponding [mm dt-1]
-    fmax::Vector{SVector{N,T}} | "mm dt-1"
-    #minimum storage capacity in horton ponding (relative to shmax) [-]
-    shmin::Vector{SVector{N,T}} | "-"
-    #melt coefficient for melt of frozen topsoil [-]
-    kmf::Vector{SVector{N,T}} | "-"
-    ##ROOTZONE
-    # maximum root-zone storage capacity [mm]
-    srmax::Vector{SVector{N,T}} | "mm"
-    # Fraction of root zone storage below which actual evaporation is potential evaporation [-]
-    lp::Vector{SVector{N,T}} | "-"
-    # Exponent in soil runoff generation equation [-]
-    beta::Vector{SVector{N,T}} | "-"
-    # maximum percolation rate [mm Δt⁻¹]
-    perc::Vector{SVector{N,T}} | "mm dt-1"
-    # maximum capillary rise rate [mm Δt⁻¹]
-    cap::Vector{SVector{N,T}} | "mm dt-1"
-    #FAST
-    # Exponent for non linear recession [-]
-    alfa::Vector{SVector{N,T}} | "-"
-    #recession coefficient of fast storage [dt-1]
-    kf::Vector{SVector{N,T}} | "dt-1"
-    # fraction of qrootzone to slowstorage (1-ds to faststorage)
-    ds::Vector{SVector{N,T}} | "-"
-    # SLOW
-    #recession coefficient of slow storage [dt-1]
-    ks::Vector{T} | "dt-1"
-
-    ## STATES
-    ##SNOW
-    # Snow water equivalent [mm]
-    snow::Vector{T} | "mm"
-    # Liquid water content in the snow pack [mm]
-    snowwater::Vector{T} | "mm"
-    # Interception storage [mm]
-    interceptionstorage::Vector{SVector{N,T}} | "mm"
-    # Storage in the hortonian ponding reservoir [mm]
-    hortonpondingstorage::Vector{SVector{N,T}} | "mm"
-    # Storage in the hortonian runoff generation [mm]
-    hortonrunoffstorage::Vector{SVector{N,T}} | "mm"
-    # Storage in the root-zone [mm]
-    rootzonestorage::Vector{SVector{N,T}} | "mm"
-    # Storage in the root-zone relative to maximum root-zone storage capacity [mm]
-    srootzone_over_srmax::Vector{SVector{N,T}} | "mm"
-    # Storage in the fast store [mm]
-    faststorage::Vector{SVector{N,T}} | "mm"
-    # Storage in the slow reservoir (for qcapillary calc) [mm]
-    slowstorage::Vector{T} | "mm"
-    #states previous time step to calc water balance [mm]
-    states_::Vector{SVector{N,T}} | "mm"
-    #states previous time step to calc water balance combined based on perc class. [mm]
-    states_m::Vector{T} | "mm"
-    #states averaged over classes
-    interceptionstorage_m::Vector{T} | "mm"
-    hortonpondingstorage_m::Vector{T} | "mm"
-    hortonrunoffstorage_m::Vector{T} | "mm"
-    srootzone_m::Vector{T} | "mm"
-    faststorage_m::Vector{T} | "mm"
-    srootzone_over_srmax_m::Vector{T} | "mm"
-
-    ## FLUXES
-    #SNOW
-    # Precipitation [mm Δt⁻¹]
-    precipitation::Vector{T} | "mm dt-1"
-    # Temperature [ᵒC]
-    temperature::Vector{T} | "ᵒC"
-    # Potential evapotranspiration [mm Δt⁻¹]
-    potential_evaporation::Vector{T} | "mm dt-1"
-    # Potential evapotranspiration corrected [mm Δt⁻¹]
-    epotcorr::Vector{T} | "mm dt-1"
-    # Precipitation corrected [mm Δt⁻¹]
-    precipcorr::Vector{T} | "mm dt-1"
-    # Snow melt + precipitation as rainfall [mm]
-    rainfallplusmelt::Vector{T} | "mm dt-1"
-    # Snowfall [mm]
-    snowfall::Vector{T} | "mm dt-1"
-    # Snowmelt [mm]
-    snowmelt::Vector{T} | "mm dt-1"
-    #INTERCEPTION
-    # Potential soil evaporation [mm Δt⁻¹]
-    potsoilevap::Vector{SVector{N,T}} | "mm dt-1"
-    # Evaporation from interception storage [mm Δt⁻¹]
-    intevap::Vector{SVector{N,T}} | "mm dt-1"
-    #effective precipitation [mm Δt⁻¹]
-    precipeffective::Vector{SVector{N,T}} | "mm dt-1"
-    # Evaporation from interception sum classes [mm Δt⁻¹]
-    intevap_m::Vector{T} | "mm dt-1"
-    #HORTONPONDING
-    # Evaporation from the hortonion ponding storage [-]
-    hortonevap::Vector{SVector{N,T}} | "mm dt-1"
-    # Flux from the hortonian ponding storage to the hortonian runoff storage [mm Δt⁻¹]
-    qhortonpond::Vector{SVector{N,T}} | "mm dt-1"
-    # Flux from the hortonian ponding storage to the root zone storage [mm Δt⁻¹]
-    qhortonrootzone::Vector{SVector{N,T}} | "mm dt-1"
-    # modeled accumulated frost [ᵒC Δt]
-    facc::Vector{SVector{N,T}} | "ᵒC Δt"
-    # Evaporation from the hortonian sum classes [mm Δt⁻¹]
-    hortonevap_m::Vector{T} | "mm dt-1"
-    #HORTONRUNOFF
-    # Flux from the hortonian runoff storage [mm Δt⁻¹]
-    qhortonrun::Vector{SVector{N,T}} | "mm dt-1"
-    #ROOTZONE
-    # Evaporation from the root-zone storage [mm Δt⁻¹]
-    rootevap::Vector{SVector{N,T}} | "mm dt-1"
-    # Flux from the root-zone storage [mm Δt⁻¹]
-    qrootzone::Vector{SVector{N,T}} | "mm dt-1"
-    # Pref. recharge to fast storage [mm Δt⁻¹]
-    qrootzonefast::Vector{SVector{N,T}} | "mm dt-1"
-    # Pref. recharge to slow storage sum classes [mm Δt⁻¹]
-    qrootzoneslow_m::Vector{T} | "mm dt-1"
-    # Capillary flux from the slow to the root-zone storage [mm Δt⁻¹]
-    qcapillary::Vector{SVector{N,T}} | "mm dt-1"
-    # Capillary flux from the slow to the root-zone storage sum classes [mm Δt⁻¹]
-    qcapillary_m::Vector{T} | "mm dt-1"
-    # Percolation flux from the root-zone to the slow storage [mm Δt⁻¹]
-    qpercolation::Vector{SVector{N,T}} | "mm dt-1"
-    # Percolation flux from the root-zone to the slow storage sum classes [mm Δt⁻¹]
-    qpercolation_m::Vector{T} | "mm dt-1"
-    # Evaporation from the root-zone storage, interception and hortonian [mm Δt⁻¹]
-    actevap::Vector{SVector{N,T}} | "mm dt-1"
-    # Evaporation from the root-zone storage, interception and hortonian sum classes [mm Δt⁻¹]
-    actevap_m::Vector{T} | "mm dt-1"
-    # Evaporation from the root-zone storage sum classes [mm Δt⁻¹]
-    rootevap_m::Vector{T} | "mm dt-1"
-    #FAST
-    # runoff from fast reservoir [mm Δt⁻¹]
-    qfast::Vector{SVector{N,T}} | "mm dt-1"
-    #SLOW
-    # runoff from slow reservoir [mm Δt⁻¹]
-    qslow::Vector{T} | "mm dt-1"
-    #Total [mm Δt⁻¹]
-    runoff::Vector{T} | "mm dt-1"
-    # fast runoff sum classes [mm Δt⁻¹]
-    qfast_tot = Vector{T} | "mm dt-1"
-
-
-    ## WATERBALANCES
-    #water balance snow store
-    wb_snow::Vector{T} | "mm dt-1"
-    #water balance interception storage [mm Δt⁻¹]
-    wb_interception::Vector{SVector{N,T}} | "mm dt-1"
-    #water balance hortonian ponding storage [mm Δt⁻¹]
-    wb_hortonponding::Vector{SVector{N,T}} | "mm dt-1"
-    #water balance hortonian runoff storage [mm Δt⁻¹]
-    wb_hortonrunoff::Vector{SVector{N,T}} | "mm dt-1"
-    #water balance root-zone storage [mm Δt⁻¹]
-    wb_rootzone::Vector{SVector{N,T}} | "mm dt-1"
-    #water balance fast storage [mm Δt⁻¹]
-    wb_fast::Vector{SVector{N,T}} | "mm dt-1"
-    #water balance slow storage [mm Δt⁻¹]
-    wb_slow::Vector{T} | "mm dt-1"
-    #total water balance [mm Δt⁻¹]
-    wb_tot::Vector{T} | "mm dt-1"
-
-end
-
-
-function common_snow_hbv(flextopo::FLEXTOPO)
-    for i = 1:flextopo.n
-        # precip correction
-        precipcorr = flextopo.precipitation[i] * flextopo.pcorr[i]
-
-        #hbv snow
-        snow, snowwater, snowmelt, rainfallplusmelt, snowfall = snowpack_hbv(
-            flextopo.snow[i],
-            flextopo.snowwater[i],
-            precipcorr,
-            flextopo.temperature[i],
-            flextopo.tti[i],
-            flextopo.tt[i],
-            flextopo.ttm[i],
-            flextopo.cfmax[i],
-            flextopo.whc[i],
-        )
-
-        #wb
-        wb_snow =
-            precipcorr - rainfallplusmelt - snow + flextopo.snow[i] - snowwater +
-            flextopo.snowwater[i]
-
-        #update stores
-        # states_ is sum of states of previous time steps to compute WB at the end (per class); states_m is for combined stores
-        flextopo.states_m[i] = flextopo.snow[i] + flextopo.snowwater[i]
-        flextopo.snowfall[i] = snowfall
-        flextopo.snowmelt[i] = snowmelt
-        flextopo.snow[i] = snow
-        flextopo.snowwater[i] = snowwater
-        flextopo.rainfallplusmelt[i] = rainfallplusmelt
-        flextopo.precipcorr[i] = precipcorr
-        flextopo.wb_snow[i] = wb_snow
-    end
-end
-
-function common_snow_no_storage(flextopo::FLEXTOPO)
-    for i = 1:flextopo.n
-        snow = 0.0
-        snowwater = 0.0
-        snowfall = 0.0
-        snowmelt = 0.0
-        # precip correction
-        precipcorr = flextopo.precipitation[i] * flextopo.pcorr[i]
-        rainfallplusmelt = precipcorr
-
-        wb_snow = precipcorr - rainfallplusmelt - snow + flextopo.snow[i]
-
-        #update stores
-        # states_ is sum of states of previous time steps to compute WB at the end (per class); states_m is for combined stores
-        flextopo.states_m[i] = flextopo.snow[i] + flextopo.snowwater[i]
-        flextopo.snowfall[i] = snowfall
-        flextopo.snowmelt[i] = snowmelt
-        flextopo.snow[i] = snow
-        flextopo.snowwater[i] = snowwater
-        flextopo.rainfallplusmelt[i] = rainfallplusmelt
-        flextopo.wb_snow[i] = wb_snow
-        flextopo.precipcorr[i] = precipcorr
-    end
-end
-
-function common_glaciers(flextopo::FLEXTOPO, config)
-    modelglacier = get(config.model, "glacier", false)::Bool
-    for i = 1:flextopo.n
-        if modelglacier
-            # Run Glacier module and add the snowpack on-top of it.
-            # Estimate the fraction of snow turned into ice (HBV-light).
-            # Estimate glacier melt.
-
-            flextopo.snow[i], snow2glacier, flextopo.glacierstore[i], glaciermelt =
-                glacier_hbv(
-                    flextopo.glacierfrac[i],
-                    flextopo.glacierstore[i],
-                    flextopo.snow[i],
-                    flextopo.temperature[i],
-                    flextopo.g_tt[i],
-                    flextopo.g_cfmax[i],
-                    flextopo.g_sifrac[i],
-                    Second(flextopo.dt),
-                )
-            # Convert to mm per grid cell and add to snowmelt
-            glaciermelt = glaciermelt * flextopo.glacierfrac[i]
-            rainfallplusmelt = flextopo.rainfallplusmelt[i] + glaciermelt
-        else
-            rainfallplusmelt = flextopo.rainfallplusmelt[i]
-        end
-    end
-end
-
-
-function interception_no_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-        intevap = 0.0
-        precipeffective = max(flextopo.rainfallplusmelt[i], 0.0)
-        interceptionstorage = 0.0
-
-        # correction for potential evaporation
-        epotcorr = flextopo.ecorr[i] * flextopo.potential_evaporation[i]
-        restevap = max(0.0, epotcorr - intevap)
-
-        #wb interceptionstore
-        wb_interception =
-            flextopo.rainfallplusmelt[i] - intevap - precipeffective - interceptionstorage +
-            flextopo.interceptionstorage[i][k]
-
-        #update stores
-        flextopo.states_[i] =
-            setindex(flextopo.states_[i], flextopo.interceptionstorage[i][k], k)
-        flextopo.interceptionstorage[i] =
-            setindex(flextopo.interceptionstorage[i], interceptionstorage, k)
-        flextopo.intevap[i] = setindex(flextopo.intevap[i], intevap, k)
-        flextopo.precipeffective[i] =
-            setindex(flextopo.precipeffective[i], precipeffective, k)
-        flextopo.potsoilevap[i] = setindex(flextopo.potsoilevap[i], restevap, k)
-        flextopo.epotcorr[i] = epotcorr
-        flextopo.wb_interception[i] =
-            setindex(flextopo.wb_interception[i], wb_interception, k)
-
-        #average storage over classes
-        flextopo.interceptionstorage_m[i] =
-            sum(flextopo.interceptionstorage[i] .* flextopo.hrufrac[i])
-        flextopo.intevap_m[i] = sum(flextopo.intevap[i] .* flextopo.hrufrac[i])
-
-    end
-end
-
-function interception_overflow(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-        # rainfall added to interception store interceptionstorage
-        interception = min(
-            flextopo.rainfallplusmelt[i],
-            flextopo.imax[i][k] - flextopo.interceptionstorage[i][k],
-        )
-        # current interception storage
-        interceptionstorage = flextopo.interceptionstorage[i][k] + interception
-        precipeffective = flextopo.rainfallplusmelt[i] - interception
-
-        # correction for potential evaporation
-        epotcorr = flextopo.ecorr[i] * flextopo.potential_evaporation[i]
-
-        # evaporation from interception storage
-        intevap = min(interceptionstorage, epotcorr)
-        interceptionstorage = interceptionstorage - intevap
-        restevap = max(0.0, epotcorr - intevap)
-
-        #wb interceptionstore
-        wb_interception =
-            flextopo.rainfallplusmelt[i] - intevap - precipeffective - interceptionstorage +
-            flextopo.interceptionstorage[i][k]
-
-        #update stores
-        flextopo.states_[i] =
-            setindex(flextopo.states_[i], flextopo.interceptionstorage[i][k], k)
-        flextopo.interceptionstorage[i] =
-            setindex(flextopo.interceptionstorage[i], interceptionstorage, k)
-        flextopo.intevap[i] = setindex(flextopo.intevap[i], intevap, k)
-        flextopo.precipeffective[i] =
-            setindex(flextopo.precipeffective[i], precipeffective, k)
-        flextopo.potsoilevap[i] = setindex(flextopo.potsoilevap[i], restevap, k)
-        flextopo.epotcorr[i] = epotcorr
-        flextopo.wb_interception[i] =
-            setindex(flextopo.wb_interception[i], wb_interception, k)
-
-        #average storage over classes
-        flextopo.interceptionstorage_m[i] =
-            sum(flextopo.interceptionstorage[i] .* flextopo.hrufrac[i])
-        flextopo.intevap_m[i] = sum(flextopo.intevap[i] .* flextopo.hrufrac[i])
-
-    end
-end
-
-
-function hortonponding_no_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-        hortonevap = 0.0
-        qhortonpond = 0.0
-        qhortonrootzone = max(flextopo.precipeffective[i][k], 0.0)
-        hortonpondingstorage = 0.0
-
-        wb_hortonponding =
-            flextopo.precipeffective[i][k] - hortonevap - qhortonpond - qhortonrootzone -
-            hortonpondingstorage + flextopo.hortonpondingstorage[i][k]
-
-        #update with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.hortonpondingstorage[i][k],
-            k,
-        )
-        flextopo.qhortonpond[i] = setindex(flextopo.qhortonpond[i], qhortonpond, k)
-        flextopo.qhortonrootzone[i] =
-            setindex(flextopo.qhortonrootzone[i], qhortonrootzone, k)
-        flextopo.hortonpondingstorage[i] =
-            setindex(flextopo.hortonpondingstorage[i], hortonpondingstorage, k)
-        flextopo.hortonevap[i] = setindex(flextopo.hortonevap[i], hortonevap, k)
-        flextopo.wb_hortonponding[i] =
-            setindex(flextopo.wb_hortonponding[i], wb_hortonponding, k)
-
-        #average storage over classes
-        flextopo.hortonpondingstorage_m[i] =
-            sum(flextopo.hortonpondingstorage[i] .* flextopo.hrufrac[i])
-        flextopo.hortonevap_m[i] = sum(flextopo.hortonevap[i] .* flextopo.hrufrac[i])
-    end
-end
-
-function hortonponding(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-
-        #calculate reduction of shmax due to frost
-        facc = min(flextopo.facc[i][k] + flextopo.temperature[i] * flextopo.kmf[i][k], 0.0)
-        ft = min(
-            max(
-                facc / (flextopo.facc1[i][k] - flextopo.facc0[i][k]) -
-                flextopo.facc0[i][k] / (flextopo.facc1[i][k] - flextopo.facc0[i][k]),
-                flextopo.shmin[i][k],
-            ),
-            1.0,
-        )
-        shmax_frost = ft * flextopo.shmax[i][k]
-
-        #add effective precipitation from interception to the horton ponding storage.
-        hortonpondingstorage =
-            flextopo.hortonpondingstorage[i][k] + flextopo.precipeffective[i][k]
-        # if soil is filled until max capacity, additional water runs of directly
-        directrunoff_h = max(hortonpondingstorage - shmax_frost, 0.0)
-        #update hortonpondingstorage
-        hortonpondingstorage = hortonpondingstorage - directrunoff_h
-        #net water which infiltrates in horton (careful, shmax_frost can decrease from one timestep to another)
-        netin_hortonpond = max(flextopo.precipeffective[i][k] - directrunoff_h, 0.0)
-
-        #evaporation from the horton ponding storage
-        hortonevap =
-            hortonpondingstorage > shmax_frost * flextopo.lp[i][k] ?
-            min(hortonpondingstorage, flextopo.potsoilevap[i][k]) :
-            min(
-                flextopo.potsoilevap[i][k] *
-                (hortonpondingstorage / (shmax_frost * flextopo.lp[i][k])),
-            )
-        #update storage
-        hortonpondingstorage = hortonpondingstorage - hortonevap
-        #update restevap
-        restevap = max(0.0, flextopo.potsoilevap[i][k] - hortonevap)
-
-        #excess water from beta function of netin_hortonpond (due to rouding hortonpondingstorage could be slightly larger than shmax_frost, make sure it is always less than 1)
-        qhorton_in =
-            netin_hortonpond * (
-                1.0 - pow(
-                    (1.0 - min(hortonpondingstorage / shmax_frost, 1.0)),
-                    flextopo.beta[i][k],
-                )
-            )
-        #update storage
-        hortonpondingstorage = hortonpondingstorage - qhorton_in
-
-        #total water out of horton ponding consists of directrunoff and excess water from beta function
-        qhortonpond = directrunoff_h + qhorton_in
-
-        #infiltration to root-zone
-        qhortonrootzone =
-            min(hortonpondingstorage / shmax_frost, 1.0) > 0.0 ?
-            min(
-                flextopo.fmax[i][k] * exp(
-                    -flextopo.fdec[i][k] *
-                    (1.0 - min(hortonpondingstorage / shmax_frost, 1.0)),
-                ),
-                hortonpondingstorage,
-            ) : 0.0
-        hortonpondingstorage = hortonpondingstorage - qhortonrootzone
-
-        #water balance
-        wb_hortonponding =
-            flextopo.precipeffective[i][k] - hortonevap - qhortonpond - qhortonrootzone -
-            hortonpondingstorage + flextopo.hortonpondingstorage[i][k]
-
-        #update states
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.hortonpondingstorage[i][k],
-            k,
-        )
-        flextopo.potsoilevap[i] = setindex(flextopo.potsoilevap[i], restevap, k)
-        flextopo.qhortonpond[i] = setindex(flextopo.qhortonpond[i], qhortonpond, k)
-        flextopo.qhortonrootzone[i] =
-            setindex(flextopo.qhortonrootzone[i], qhortonrootzone, k)
-        flextopo.hortonpondingstorage[i] =
-            setindex(flextopo.hortonpondingstorage[i], hortonpondingstorage, k)
-        flextopo.hortonevap[i] = setindex(flextopo.hortonevap[i], hortonevap, k)
-        flextopo.wb_hortonponding[i] =
-            setindex(flextopo.wb_hortonponding[i], wb_hortonponding, k)
-
-        #average storage over classes
-        flextopo.hortonpondingstorage_m[i] =
-            sum(flextopo.hortonpondingstorage[i] .* flextopo.hrufrac[i])
-        flextopo.hortonevap_m[i] = sum(flextopo.hortonevap[i] .* flextopo.hrufrac[i])
-    end
-end
-
-function hortonrunoff_no_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-        qhortonrun = 0.0
-        hortonrunoffstorage = 0.0
-
-        wb_hortonrunoff =
-            flextopo.qhortonpond[i][k] - qhortonrun - hortonrunoffstorage +
-            flextopo.hortonrunoffstorage[i][k]
-
-        #update with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.hortonrunoffstorage[i][k],
-            k,
-        )
-        flextopo.qhortonrun[i] = setindex(flextopo.qhortonrun[i], qhortonrun, k)
-        flextopo.hortonrunoffstorage[i] =
-            setindex(flextopo.hortonrunoffstorage[i], hortonrunoffstorage, k)
-        flextopo.wb_hortonrunoff[i] =
-            setindex(flextopo.wb_hortonrunoff[i], wb_hortonrunoff, k)
-
-        #average storage over classes
-        flextopo.hortonrunoffstorage_m[i] =
-            sum(flextopo.hortonrunoffstorage[i] .* flextopo.hrufrac[i])
-    end
-end
-
-function hortonrunoff(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-
-        qhortonrun = min(
-            flextopo.hortonrunoffstorage[i][k],
-            flextopo.hortonrunoffstorage[i][k] * flextopo.khf[i][k],
-        )
-        hortonrunoffstorage =
-            flextopo.hortonrunoffstorage[i][k] + flextopo.qhortonpond[i][k] - qhortonrun
-
-        wb_hortonrunoff =
-            flextopo.qhortonpond[i][k] - qhortonrun - hortonrunoffstorage +
-            flextopo.hortonrunoffstorage[i][k]
-
-        #update with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.hortonrunoffstorage[i][k],
-            k,
-        )
-        flextopo.qhortonrun[i] = setindex(flextopo.qhortonrun[i], qhortonrun, k)
-        flextopo.hortonrunoffstorage[i] =
-            setindex(flextopo.hortonrunoffstorage[i], hortonrunoffstorage, k)
-        flextopo.wb_hortonrunoff[i] =
-            setindex(flextopo.wb_hortonrunoff[i], wb_hortonrunoff, k)
-
-        #average storage over classes
-        flextopo.hortonrunoffstorage_m[i] =
-            sum(flextopo.hortonrunoffstorage[i] .* flextopo.hrufrac[i])
-
-    end
-end
-
-function rootzone_no_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-        rootevap = 0.0
-        qpercolation = 0.0
-        qcapillary = 0.0
-        qrootzone =
-            max(flextopo.qhortonrootzone[i][k] + flextopo.rootzonestorage[i][k], 0.0) #if store not empty initial conditions
-        rootzonestorage = 0.0
-
-        #compute water balance rootzonestorage storage
-        wb_rootzone =
-            flextopo.qhortonrootzone[i][k] - rootevap - qrootzone - qpercolation +
-            qcapillary - rootzonestorage + flextopo.rootzonestorage[i][k]
-
-        #update states and fluxes with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.rootzonestorage[i][k],
-            k,
-        )
-        flextopo.qrootzone[i] = setindex(flextopo.qrootzone[i], qrootzone, k)
-        flextopo.rootzonestorage[i] =
-            setindex(flextopo.rootzonestorage[i], rootzonestorage, k)
-        flextopo.srootzone_over_srmax[i] = setindex(
-            flextopo.srootzone_over_srmax[i],
-            flextopo.rootzonestorage[i][k] / flextopo.srmax[i][k],
-            k,
-        )
-        flextopo.qcapillary[i] = setindex(flextopo.qcapillary[i], qcapillary, k)
-        flextopo.qpercolation[i] = setindex(flextopo.qpercolation[i], qpercolation, k)
-        flextopo.rootevap[i] = setindex(flextopo.rootevap[i], rootevap, k)
-        flextopo.actevap[i] = setindex(
-            flextopo.actevap[i],
-            flextopo.rootevap[i][k] + flextopo.intevap[i][k] + flextopo.hortonevap[i][k],
-            k,
-        )
-        flextopo.wb_rootzone[i] = setindex(flextopo.wb_rootzone[i], wb_rootzone, k)
-
-        #average storage over classes
-        flextopo.srootzone_m[i] = sum(flextopo.rootzonestorage[i] .* flextopo.hrufrac[i])
-        flextopo.srootzone_over_srmax_m[i] =
-            sum(flextopo.srootzone_over_srmax[i] .* flextopo.hrufrac[i])
-        flextopo.rootevap_m[i] = sum(flextopo.rootevap[i] .* flextopo.hrufrac[i])
-        flextopo.actevap_m[i] = sum(flextopo.actevap[i] .* flextopo.hrufrac[i])
-    end
-end
-
-function rootzone_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-
-        #added water to the root-zone. NB: if no horton storages: qhortonrootzone is in fact Pe!! (effective precip).
-        rootzonestorage = flextopo.rootzonestorage[i][k] + flextopo.qhortonrootzone[i][k]
-        # if soil is filled until max capacity, additional water runs of directly
-        directrunoff = max(rootzonestorage - flextopo.srmax[i][k], 0.0)
-        #update rootzonestorage
-        rootzonestorage = rootzonestorage - directrunoff
-        #net water which infiltrates in root-zone
-        netin_rootzone = flextopo.qhortonrootzone[i][k] - directrunoff
-
-        #evaporation from the rootzone
-        rootevap =
-            rootzonestorage > flextopo.srmax[i][k] * flextopo.lp[i][k] ?
-            min(rootzonestorage, flextopo.potsoilevap[i][k]) :
-            min(
-                flextopo.potsoilevap[i][k] *
-                (rootzonestorage / (flextopo.srmax[i][k] * flextopo.lp[i][k])),
-            )
-        #update storage
-        rootzonestorage = rootzonestorage - rootevap
-
-        #excess water from beta function of netin_rootzone
-        qrootzone_in =
-            netin_rootzone *
-            (1.0 - pow(1.0 - rootzonestorage / flextopo.srmax[i][k], flextopo.beta[i][k]))
-        #update storage
-        rootzonestorage = rootzonestorage - qrootzone_in
-
-        #total water out of root-zone consists of directrunoff and excess water from beta function
-        qrootzone = directrunoff + qrootzone_in
-
-        #percolation
-        qpercolation = flextopo.perc[i][k] * rootzonestorage / flextopo.srmax[i][k]
-        rootzonestorage = rootzonestorage - qpercolation
-
-        #capillary rise
-        qcapillary = min(
-            flextopo.cap[i][k] * (1.0 - rootzonestorage / flextopo.srmax[i][k]),
-            flextopo.slowstorage[i],
-        )
-        rootzonestorage = rootzonestorage + qcapillary
-
-        #compute water balance rootzonestorage storage
-        wb_rootzone =
-            flextopo.qhortonrootzone[i][k] - rootevap - qrootzone - qpercolation +
-            qcapillary - rootzonestorage + flextopo.rootzonestorage[i][k]
-
-        #update states and fluxes with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.rootzonestorage[i][k],
-            k,
-        )
-        flextopo.qrootzone[i] = setindex(flextopo.qrootzone[i], qrootzone, k)
-        flextopo.rootzonestorage[i] =
-            setindex(flextopo.rootzonestorage[i], rootzonestorage, k)
-        flextopo.srootzone_over_srmax[i] = setindex(
-            flextopo.srootzone_over_srmax[i],
-            flextopo.rootzonestorage[i][k] / flextopo.srmax[i][k],
-            k,
-        )
-        flextopo.qcapillary[i] = setindex(flextopo.qcapillary[i], qcapillary, k)
-        flextopo.qpercolation[i] = setindex(flextopo.qpercolation[i], qpercolation, k)
-        flextopo.rootevap[i] = setindex(flextopo.rootevap[i], rootevap, k)
-        flextopo.actevap[i] = setindex(
-            flextopo.actevap[i],
-            flextopo.rootevap[i][k] + flextopo.intevap[i][k] + flextopo.hortonevap[i][k],
-            k,
-        )
-        flextopo.wb_rootzone[i] = setindex(flextopo.wb_rootzone[i], wb_rootzone, k)
-
-        #average storage over classes
-        flextopo.srootzone_m[i] = sum(flextopo.rootzonestorage[i] .* flextopo.hrufrac[i])
-        flextopo.srootzone_over_srmax_m[i] =
-            sum(flextopo.srootzone_over_srmax[i] .* flextopo.hrufrac[i])
-        flextopo.rootevap_m[i] = sum(flextopo.rootevap[i] .* flextopo.hrufrac[i])
-        flextopo.actevap_m[i] = sum(flextopo.actevap[i] .* flextopo.hrufrac[i])
-    end
-end
-
-
-function fast_no_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-        #make sure ds does not exceed 1
-        ds = flextopo.ds[i][k] > 1.0 ? 1.0 : flextopo.ds[i][k]
-        #calc inflow to faststorage
-        qrootzonefast = flextopo.qrootzone[i][k] * (1.0 - ds)
-        qfast = qrootzonefast + flextopo.faststorage[i][k] #if store not empty initial conditions, make sure to empty
-        faststorage = 0.0
-
-        #compute wb faststorage
-        wb_fast = qrootzonefast - qfast - faststorage + flextopo.faststorage[i][k]
-
-        #update with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.faststorage[i][k],
-            k,
-        )
-        flextopo.qrootzonefast[i] = setindex(flextopo.qrootzonefast[i], qrootzonefast, k)
-        flextopo.qfast[i] = setindex(flextopo.qfast[i], qfast, k)
-        flextopo.faststorage[i] = setindex(flextopo.faststorage[i], faststorage, k)
-        flextopo.wb_fast[i] = setindex(flextopo.wb_fast[i], wb_fast, k)
-
-        #average storage over classes
-        flextopo.faststorage_m[i] = sum(flextopo.faststorage[i] .* flextopo.hrufrac[i])
-    end
-end
-
-function fast_storage(flextopo::FLEXTOPO)
-    k = flextopo.kclass[1]
-    for i = 1:flextopo.n
-
-        #make sure ds does not exceed 1
-        ds = flextopo.ds[i][k] > 1.0 ? 1.0 : flextopo.ds[i][k]
-
-        #split part of the outflow from the root-zone to the fast runoff (and part as preferential recharge to the slow reservoir)
-        qrootzonefast = flextopo.qrootzone[i][k] * (1.0 - ds)
-
-        #fast runoff
-        qfast = min(
-            flextopo.faststorage[i][k],
-            pow(flextopo.faststorage[i][k], flextopo.alfa[i][k]) * flextopo.kf[i][k],
-        )
-        #update store
-        faststorage = flextopo.faststorage[i][k] + qrootzonefast - qfast
-
-        #compute wb faststorage
-        wb_fast = qrootzonefast - qfast - faststorage + flextopo.faststorage[i][k]
-
-        #update with setindex
-        flextopo.states_[i] = setindex(
-            flextopo.states_[i],
-            flextopo.states_[i][k] + flextopo.faststorage[i][k],
-            k,
-        )
-        flextopo.qrootzonefast[i] = setindex(flextopo.qrootzonefast[i], qrootzonefast, k)
-        flextopo.qfast[i] = setindex(flextopo.qfast[i], qfast, k)
-        flextopo.faststorage[i] = setindex(flextopo.faststorage[i], faststorage, k)
-        flextopo.wb_fast[i] = setindex(flextopo.wb_fast[i], wb_fast, k)
-
-        #average storage over classes
-        flextopo.faststorage_m[i] = sum(flextopo.faststorage[i] .* flextopo.hrufrac[i])
-    end
-end
-
-function slow_no_storage(flextopo::FLEXTOPO)
-    for i = 1:flextopo.n
-        #make sure ds does not exceed 1
-        pref_recharge = flextopo.qrootzone[i] .* min.(flextopo.ds[i], 1.0)
-        pref_recharge_sum_classes = sum(pref_recharge .* flextopo.hrufrac[i])
-
-        Qcap_sum_classes = sum(flextopo.qcapillary[i] .* flextopo.hrufrac[i])
-        Qperc_sum_classes = sum(flextopo.qpercolation[i] .* flextopo.hrufrac[i])
-
-        Qsin = pref_recharge_sum_classes + Qperc_sum_classes - Qcap_sum_classes
-        qslow = Qsin + flextopo.slowstorage[i] # if at start store is not empty
-        slowstorage = 0.0
-
-        qfast_tot =
-            sum(flextopo.qfast[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.qhortonrun[i] .* flextopo.hrufrac[i])
-        runoff = max(0.0, qslow + qfast_tot)
-
-        wb_slow = Qsin - qslow - slowstorage + flextopo.slowstorage[i]
-
-        #update
-        flextopo.qfast_tot[i] = qfast_tot
-        flextopo.states_m[i] =
-            flextopo.states_m[i] +
-            sum(flextopo.states_[i] .* flextopo.hrufrac[i]) +
-            flextopo.slowstorage[i]
-        flextopo.runoff[i] = runoff
-        flextopo.qrootzoneslow_m[i] = pref_recharge_sum_classes
-        flextopo.qpercolation_m[i] = Qperc_sum_classes
-        flextopo.qcapillary_m[i] = Qcap_sum_classes
-        flextopo.qslow[i] = qslow
-        flextopo.slowstorage[i] = slowstorage
-        flextopo.wb_slow[i] = wb_slow
-    end
-end
-
-function common_slow_storage(flextopo::FLEXTOPO)
-    for i = 1:flextopo.n
-        #make sure ds does not exceed 1
-        pref_recharge = flextopo.qrootzone[i] .* min.(flextopo.ds[i], 1.0)
-        pref_recharge_sum_classes = sum(pref_recharge .* flextopo.hrufrac[i])
-
-        Qcap_sum_classes = sum(flextopo.qcapillary[i] .* flextopo.hrufrac[i])
-        Qperc_sum_classes = sum(flextopo.qpercolation[i] .* flextopo.hrufrac[i])
-
-        Qsin = pref_recharge_sum_classes + Qperc_sum_classes - Qcap_sum_classes
-        slowstorage = flextopo.slowstorage[i] + Qsin
-
-        qslow = min(flextopo.slowstorage[i], flextopo.slowstorage[i] * flextopo.ks[i])
-        slowstorage = slowstorage - qslow
-
-        qfast_tot =
-            sum(flextopo.qfast[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.qhortonrun[i] .* flextopo.hrufrac[i])
-        runoff = max(0.0, qslow + qfast_tot)
-
-        wb_slow = Qsin - qslow - slowstorage + flextopo.slowstorage[i]
-
-        #update
-        flextopo.qfast_tot[i] = qfast_tot
-        flextopo.states_m[i] =
-            flextopo.states_m[i] +
-            sum(flextopo.states_[i] .* flextopo.hrufrac[i]) +
-            flextopo.slowstorage[i]
-        flextopo.runoff[i] = runoff
-        flextopo.qrootzoneslow_m[i] = pref_recharge_sum_classes
-        flextopo.qpercolation_m[i] = Qperc_sum_classes
-        flextopo.qcapillary_m[i] = Qcap_sum_classes
-        flextopo.qslow[i] = qslow
-        flextopo.slowstorage[i] = slowstorage
-        flextopo.wb_slow[i] = wb_slow
-    end
-end
-
-function watbal(flextopo::FLEXTOPO)
-    for i = 1:flextopo.n
-        states =
-            flextopo.snow[i] +
-            flextopo.snowwater[i] +
-            sum(flextopo.interceptionstorage[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.hortonpondingstorage[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.hortonrunoffstorage[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.rootzonestorage[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.faststorage[i] .* flextopo.hrufrac[i]) +
-            sum(flextopo.slowstorage[i] .* flextopo.hrufrac[i])
-        wb_tot =
-            flextopo.precipitation[i] - (
-                sum(flextopo.intevap[i] .* flextopo.hrufrac[i]) +
-                sum(flextopo.hortonevap[i] .* flextopo.hrufrac[i]) +
-                sum(flextopo.rootevap[i] .* flextopo.hrufrac[i])
-            ) - flextopo.runoff[i] - states + flextopo.states_m[i]
-        #update wb
-        flextopo.wb_tot[i] = wb_tot
-    end
-end
diff --git a/src/flextopo_model.jl b/src/flextopo_model.jl
deleted file mode 100644
index 8ea773d63..000000000
--- a/src/flextopo_model.jl
+++ /dev/null
@@ -1,739 +0,0 @@
-"""
-    initialize_flextopo_model(config::Config)
-
-Initial part of the FlexTopo model concept. Reads the input settings and data as defined in the
-Config object. Will return a Model that is ready to run.
-"""
-function initialize_flextopo_model(config::Config)
-    # unpack the paths to the netCDF files
-    static_path = input_path(config, config.input.path_static)
-
-    reader = prepare_reader(config)
-    clock = Clock(config, reader)
-    dt = clock.dt
-
-    do_reservoirs = get(config.model, "reservoirs", false)::Bool
-    do_lakes = get(config.model, "lakes", false)::Bool
-    do_pits = get(config.model, "pits", false)::Bool
-
-    kw_river_tstep = get(config.model, "kw_river_tstep", 0)
-    kw_land_tstep = get(config.model, "kw_land_tstep", 0)
-    kinwave_it = get(config.model, "kin_wave_iteration", false)::Bool
-
-    classes = get(config.model, "classes", "")
-    nclass = length(classes)
-    @info "Classes are set to names `$(join(classes,", "))` and have size `$nclass`."
-    kclass = [1]    #needed to initialize
-
-    # dictionary of available functions for each store
-    dic_function = Dict{String,Function}(
-        "common_snow_hbv" => common_snow_hbv,
-        "common_snow_no_storage" => common_snow_no_storage,
-        "common_glaciers" => common_glaciers,
-        "interception_overflow" => interception_overflow,
-        "interception_no_storage" => interception_no_storage,
-        "hortonponding" => hortonponding,
-        "hortonponding_no_storage" => hortonponding_no_storage,
-        "hortonrunoff" => hortonrunoff,
-        "hortonrunoff_no_storage" => hortonrunoff_no_storage,
-        "rootzone_storage" => rootzone_storage,
-        "rootzone_no_storage" => rootzone_no_storage,
-        "fast_no_storage" => fast_no_storage,
-        "fast_storage" => fast_storage,
-        "slow_no_storage" => slow_no_storage,
-        "common_slow_storage" => common_slow_storage,
-    )
-
-    select_snow = get(config.model, "select_snow", ["common_snow_hbv"])
-    select_interception =
-        get(config.model, "select_interception", ["interception_overflow"])
-    select_hortonponding =
-        get(config.model, "select_hortonponding", ["hortonponding_no_storage"])
-    select_hortonrunoff =
-        get(config.model, "select_hortonrunoff", ["hortonrunoff_no_storage"])
-    select_rootzone = get(config.model, "select_rootzone", ["rootzone_storage"])
-    select_fast = get(config.model, "select_fast", ["fast_storage"])
-    select_slow = get(config.model, "select_slow", ["common_slow_storage"])
-
-    nc = NCDataset(static_path)
-
-    subcatch_2d = ncread(nc, config, "subcatchment"; optional = false, allow_missing = true)
-    # indices based on catchment
-    inds, rev_inds = active_indices(subcatch_2d, missing)
-    n = length(inds)
-
-
-    # glacier parameters
-    g_tt = ncread(
-        nc,
-        config,
-        "vertical.g_tt";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        fill = 0.0,
-    )
-    g_cfmax =
-        ncread(
-            nc,
-            config,
-            "vertical.g_cfmax";
-            sel = inds,
-            defaults = 3.0,
-            type = Float,
-            fill = 0.0,
-        ) .* (dt / basetimestep)
-
-    g_sifrac =
-        ncread(
-            nc,
-            config,
-            "vertical.g_sifrac";
-            sel = inds,
-            defaults = 0.001,
-            type = Float,
-            fill = 0.0,
-        ) .* (dt / basetimestep)
-    glacierfrac = ncread(
-        nc,
-        config,
-        "vertical.glacierfrac";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        fill = 0.0,
-    )
-    glacierstore = ncread(
-        nc,
-        config,
-        "vertical.glacierstore";
-        sel = inds,
-        defaults = 5500.0,
-        type = Float,
-        fill = 0.0,
-    )
-
-    #snow param (single class)
-    cfmax =
-        ncread(
-            nc,
-            config,
-            "vertical.cfmax";
-            sel = inds,
-            defaults = 3.75653,
-            type = Float,
-        ) .* (dt / basetimestep)
-
-    tt = ncread(nc, config, "vertical.tt"; sel = inds, defaults = -1.41934, type = Float)
-
-    tti = ncread(nc, config, "vertical.tti"; sel = inds, defaults = 1.0, type = Float)
-
-    ttm = ncread(nc, config, "vertical.ttm"; sel = inds, defaults = -1.41934, type = Float)
-
-    whc = ncread(nc, config, "vertical.whc"; sel = inds, defaults = 0.1, type = Float)
-
-    cfr = ncread(nc, config, "vertical.cfr"; sel = inds, defaults = 0.05, type = Float)
-
-    #parameters which are not class specific
-    pcorr = ncread(nc, config, "vertical.pcorr"; sel = inds, defaults = 1.0, type = Float)
-    ecorr = ncread(nc, config, "vertical.ecorr"; sel = inds, defaults = 1.0, type = Float)
-    rfcf = ncread(nc, config, "vertical.rfcf"; sel = inds, defaults = 1.0, type = Float)
-    sfcf = ncread(nc, config, "vertical.sfcf"; sel = inds, defaults = 1.0, type = Float)
-    ks =
-        ncread(nc, config, "vertical.ks"; sel = inds, defaults = 0.006, type = Float) .*
-        (dt / basetimestep)
-
-    #initialize parameters that differ per class
-    hrufrac = ncread(
-        nc,
-        config,
-        "vertical.hrufrac";
-        sel = inds,
-        defaults = 1.0 / length(classes),
-        type = Float,
-        dimname = :classes,
-    )
-    imax = ncread(
-        nc,
-        config,
-        "vertical.imax";
-        sel = inds,
-        defaults = 3.0,
-        type = Float,
-        dimname = :classes,
-    )
-    shmax = ncread(
-        nc,
-        config,
-        "vertical.shmax";
-        sel = inds,
-        defaults = 30.0,
-        type = Float,
-        dimname = :classes,
-    )
-    khf =
-        ncread(
-            nc,
-            config,
-            "vertical.khf";
-            sel = inds,
-            defaults = 0.5,
-            type = Float,
-            dimname = :classes,
-        ) .* (dt / basetimestep)
-    facc0 = ncread(
-        nc,
-        config,
-        "vertical.facc0";
-        sel = inds,
-        defaults = -3.0,
-        type = Float,
-        dimname = :classes,
-    )
-    facc1 = ncread(
-        nc,
-        config,
-        "vertical.facc1";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        dimname = :classes,
-    )
-    fdec = ncread(
-        nc,
-        config,
-        "vertical.fdec";
-        sel = inds,
-        defaults = 0.2,
-        type = Float,
-        dimname = :classes,
-    )
-    fmax =
-        ncread(
-            nc,
-            config,
-            "vertical.fmax";
-            sel = inds,
-            defaults = 2.0,
-            type = Float,
-            dimname = :classes,
-        ) .* (dt / basetimestep)
-    shmin = ncread(
-        nc,
-        config,
-        "vertical.shmin";
-        sel = inds,
-        defaults = 0.2,
-        type = Float,
-        dimname = :classes,
-    )
-    kmf = ncread(
-        nc,
-        config,
-        "vertical.kmf";
-        sel = inds,
-        defaults = 1.0,
-        type = Float,
-        dimname = :classes,
-    )
-    srmax = ncread(
-        nc,
-        config,
-        "vertical.srmax";
-        sel = inds,
-        defaults = 260.0,
-        type = Float,
-        dimname = :classes,
-    )
-    beta = ncread(
-        nc,
-        config,
-        "vertical.beta";
-        sel = inds,
-        defaults = 0.3,
-        type = Float,
-        dimname = :classes,
-    )
-    lp = ncread(
-        nc,
-        config,
-        "vertical.lp";
-        sel = inds,
-        defaults = 0.3,
-        type = Float,
-        dimname = :classes,
-    )
-    perc =
-        ncread(
-            nc,
-            config,
-            "vertical.perc";
-            sel = inds,
-            defaults = 0.30,
-            type = Float,
-            dimname = :classes,
-        ) .* (dt / basetimestep)
-    cap =
-        ncread(
-            nc,
-            config,
-            "vertical.cap";
-            sel = inds,
-            defaults = 0.20,
-            type = Float,
-            dimname = :classes,
-        ) .* (dt / basetimestep)
-    kf =
-        ncread(
-            nc,
-            config,
-            "vertical.kf";
-            sel = inds,
-            defaults = 0.1,
-            type = Float,
-            dimname = :classes,
-        ) .* (dt / basetimestep)
-    alfa = ncread(
-        nc,
-        config,
-        "vertical.alfa";
-        sel = inds,
-        defaults = 1.0,
-        type = Float,
-        dimname = :classes,
-    )
-    ds = ncread(
-        nc,
-        config,
-        "vertical.ds";
-        sel = inds,
-        defaults = 0.2,
-        type = Float,
-        dimname = :classes,
-    )
-
-    # read x, y coordinates and calculate cell length [m]
-    y_nc = read_y_axis(nc)
-    x_nc = read_x_axis(nc)
-    y = permutedims(repeat(y_nc, outer = (1, length(x_nc))))[inds]
-    cellength = abs(mean(diff(x_nc)))
-
-
-    sizeinmetres = get(config.model, "sizeinmetres", false)::Bool
-    xl, yl = cell_lengths(y, cellength, sizeinmetres)
-
-
-    interceptionstorage = zeros(Float, nclass, n)
-    hortonpondingstorage = zeros(Float, nclass, n)
-    hortonrunoffstorage = zeros(Float, nclass, n)
-    srootzone_over_srmax = zeros(Float, nclass, n) .+ 1.0
-
-    states_ = fill(mv, nclass, n)
-
-    potsoilevap = fill(mv, nclass, n)
-    intevap = fill(mv, nclass, n)
-    precipeffective = fill(mv, nclass, n)
-    hortonevap = fill(mv, nclass, n)
-    rootevap = fill(mv, nclass, n)
-    qhortonpond = fill(mv, nclass, n)
-    qhortonrootzone = fill(mv, nclass, n)
-    facc = zeros(Float, nclass, n)
-    qhortonrun = fill(mv, nclass, n)
-    qrootzone = fill(mv, nclass, n)
-    qrootzonefast = fill(mv, nclass, n)
-    qfast = fill(mv, nclass, n)
-    actevap = fill(mv, nclass, n)
-    qpercolation = fill(mv, nclass, n)
-    qcapillary = fill(mv, nclass, n)
-
-    wb_interception = fill(mv, nclass, n)
-    wb_hortonponding = fill(mv, nclass, n)
-    wb_hortonrunoff = fill(mv, nclass, n)
-    wb_rootzone = fill(mv, nclass, n)
-    wb_fast = fill(mv, nclass, n)
-
-
-    flextopo = FLEXTOPO{Float,nclass}(
-        dt = Float(tosecond(dt)),
-        nclass = nclass,
-        n = n,
-        dic_function = dic_function,
-        kclass = kclass,
-        classes = classes,
-        select_snow = select_snow,
-        select_interception = select_interception,
-        select_hortonponding = select_hortonponding,
-        select_hortonrunoff = select_hortonrunoff,
-        select_rootzone = select_rootzone,
-        select_fast = select_fast,
-        select_slow = select_slow,
-        hrufrac = svectorscopy(hrufrac, Val{nclass}()),
-        pcorr = pcorr,
-        ecorr = ecorr,
-        #glaciers
-        g_tt = g_tt,
-        g_cfmax = g_cfmax,
-        g_sifrac = g_sifrac,
-        glacierfrac = glacierfrac,
-        glacierstore = glacierstore,
-        # snow
-        tti = tti,
-        tt = tt,
-        ttm = ttm,
-        cfmax = cfmax,
-        whc = whc,
-        cfr = cfr,
-        rfcf = rfcf,
-        sfcf = sfcf,
-        #interception
-        imax = svectorscopy(imax, Val{nclass}()),
-        #horton
-        shmax = svectorscopy(shmax, Val{nclass}()),
-        khf = svectorscopy(khf, Val{nclass}()),
-        facc0 = svectorscopy(facc0, Val{nclass}()),
-        facc1 = svectorscopy(facc1, Val{nclass}()),
-        fdec = svectorscopy(fdec, Val{nclass}()),
-        fmax = svectorscopy(fmax, Val{nclass}()),
-        shmin = svectorscopy(shmin, Val{nclass}()),
-        kmf = svectorscopy(kmf, Val{nclass}()),
-        #root zone
-        srmax = svectorscopy(srmax, Val{nclass}()),
-        lp = svectorscopy(lp, Val{nclass}()),
-        beta = svectorscopy(beta, Val{nclass}()),
-        perc = svectorscopy(perc, Val{nclass}()),
-        cap = svectorscopy(cap, Val{nclass}()),
-        #fast
-        ds = svectorscopy(ds, Val{nclass}()),
-        alfa = svectorscopy(alfa, Val{nclass}()),
-        kf = svectorscopy(kf, Val{nclass}()),
-        #slow
-        ks = ks,
-
-        # # default (cold) states:
-        snow = zeros(Float, n),
-        snowwater = zeros(Float, n),
-        interceptionstorage = svectorscopy(interceptionstorage, Val{nclass}()),
-        hortonpondingstorage = svectorscopy(hortonpondingstorage, Val{nclass}()),
-        hortonrunoffstorage = svectorscopy(hortonrunoffstorage, Val{nclass}()),
-        rootzonestorage = svectorscopy(srmax, Val{nclass}()),
-        faststorage = 0.0 .* svectorscopy(srmax, Val{nclass}()),
-        srootzone_over_srmax = svectorscopy(srootzone_over_srmax, Val{nclass}()),
-        slowstorage = zeros(Float, n) .+ 30.0,
-        #states previous time step
-        states_m = fill(mv, n),
-        states_ = svectorscopy(states_, Val{nclass}()),
-        #states averaged over all classes
-        interceptionstorage_m = zeros(Float, n),
-        hortonpondingstorage_m = zeros(Float, n),
-        hortonrunoffstorage_m = zeros(Float, n),
-        srootzone_m = zeros(Float, n),
-        faststorage_m = zeros(Float, n),
-        srootzone_over_srmax_m = zeros(Float, n),
-
-        # variables:
-        precipitation = fill(mv, n),
-        temperature = fill(mv, n),
-        potential_evaporation = fill(mv, n),
-        epotcorr = fill(mv, n),
-        precipcorr = fill(mv, n),
-        rainfallplusmelt = fill(mv, n),
-        snowfall = fill(mv, n),
-        snowmelt = fill(mv, n),
-        potsoilevap = svectorscopy(potsoilevap, Val{nclass}()),
-        precipeffective = svectorscopy(precipeffective, Val{nclass}()),
-        intevap = svectorscopy(intevap, Val{nclass}()),
-        hortonevap = svectorscopy(hortonevap, Val{nclass}()),
-        rootevap = svectorscopy(rootevap, Val{nclass}()),
-        qhortonpond = svectorscopy(qhortonpond, Val{nclass}()),
-        qhortonrootzone = svectorscopy(qhortonrootzone, Val{nclass}()),
-        facc = svectorscopy(facc, Val{nclass}()),
-        qhortonrun = svectorscopy(qhortonrun, Val{nclass}()),
-        qrootzone = svectorscopy(qrootzone, Val{nclass}()),
-        qrootzonefast = svectorscopy(qrootzonefast, Val{nclass}()),
-        qrootzoneslow_m = fill(mv, n),
-        qfast = svectorscopy(qfast, Val{nclass}()),
-        actevap = svectorscopy(actevap, Val{nclass}()),
-        qpercolation = svectorscopy(qpercolation, Val{nclass}()),
-        qcapillary = svectorscopy(qcapillary, Val{nclass}()),
-        qpercolation_m = fill(mv, n),
-        qcapillary_m = fill(mv, n),
-        # combined for the classes
-        actevap_m = fill(mv, n),
-        intevap_m = fill(mv, n),
-        hortonevap_m = fill(mv, n),
-        rootevap_m = fill(mv, n),
-        qslow = fill(mv, n),
-        qfast_tot = fill(mv, n),
-        runoff = fill(mv, n),
-        wb_snow = fill(mv, n),
-        wb_interception = svectorscopy(wb_interception, Val{nclass}()),
-        wb_hortonponding = svectorscopy(wb_hortonponding, Val{nclass}()),
-        wb_hortonrunoff = svectorscopy(wb_hortonrunoff, Val{nclass}()),
-        wb_rootzone = svectorscopy(wb_rootzone, Val{nclass}()),
-        wb_fast = svectorscopy(wb_fast, Val{nclass}()),
-        wb_slow = fill(mv, n),
-        wb_tot = fill(mv, n),
-    )
-
-    modelsize_2d = size(subcatch_2d)
-    river_2d =
-        ncread(nc, config, "river_location"; optional = false, type = Bool, fill = false)
-    river = river_2d[inds]
-    riverwidth_2d =
-        ncread(nc, config, "lateral.river.width"; optional = false, type = Float, fill = 0)
-    riverwidth = riverwidth_2d[inds]
-    riverlength_2d =
-        ncread(nc, config, "lateral.river.length"; optional = false, type = Float, fill = 0)
-    riverlength = riverlength_2d[inds]
-
-    inds_riv, rev_inds_riv = active_indices(river_2d, 0)
-    nriv = length(inds_riv)
-
-    # reservoirs
-    pits = zeros(Bool, modelsize_2d)
-    if do_reservoirs
-        reservoirs, resindex, reservoir, pits =
-            initialize_simple_reservoir(config, nc, inds_riv, nriv, pits, tosecond(dt))
-    else
-        reservoir = ()
-        reservoirs = nothing
-        resindex = fill(0, nriv)
-    end
-
-    # lakes
-    if do_lakes
-        lakes, lakeindex, lake, pits =
-            initialize_lake(config, nc, inds_riv, nriv, pits, tosecond(dt))
-    else
-        lake = ()
-        lakes = nothing
-        lakeindex = fill(0, nriv)
-    end
-
-    ldd_2d = ncread(nc, config, "ldd"; optional = false, allow_missing = true)
-    ldd = ldd_2d[inds]
-    if do_pits
-        pits_2d = ncread(nc, config, "pits"; optional = false, type = Bool, fill = false)
-        ldd = set_pit_ldd(pits_2d, ldd, inds)
-    end
-
-    landslope =
-        ncread(nc, config, "lateral.land.slope"; optional = false, sel = inds, type = Float)
-    clamp!(landslope, 0.00001, Inf)
-
-    dl = map(detdrainlength, ldd, xl, yl)
-    dw = (xl .* yl) ./ dl
-    olf = initialize_surfaceflow_land(
-        nc,
-        config,
-        inds;
-        sl = landslope,
-        dl = dl,
-        width = map(det_surfacewidth, dw, riverwidth, river),
-        iterate = kinwave_it,
-        tstep = kw_land_tstep,
-        dt = dt,
-    )
-
-    graph = flowgraph(ldd, inds, pcr_dir)
-
-    riverlength = riverlength_2d[inds_riv]
-    riverwidth = riverwidth_2d[inds_riv]
-
-    ldd_riv = ldd_2d[inds_riv]
-    if do_pits
-        ldd_riv = set_pit_ldd(pits_2d, ldd_riv, inds_riv)
-    end
-    graph_riv = flowgraph(ldd_riv, inds_riv, pcr_dir)
-
-    # the indices of the river cells in the land(+river) cell vector
-    index_river = filter(i -> !isequal(river[i], 0), 1:n)
-    frac_toriver = fraction_runoff_toriver(graph, ldd, index_river, landslope, n)
-
-    rf = initialize_surfaceflow_river(
-        nc,
-        config,
-        inds_riv;
-        dl = riverlength,
-        width = riverwidth,
-        reservoir_index = resindex,
-        reservoir = reservoirs,
-        lake_index = lakeindex,
-        lake = lakes,
-        iterate = kinwave_it,
-        tstep = kw_river_tstep,
-        dt = dt,
-    )
-
-    # setup subdomains for the land and river kinematic wave domain, if nthreads = 1
-    # subdomain is equal to the complete domain
-    toposort = topological_sort_by_dfs(graph)
-    toposort_riv = topological_sort_by_dfs(graph_riv)
-    index_pit_land = findall(x -> x == 5, ldd)
-    index_pit_river = findall(x -> x == 5, ldd_riv)
-    streamorder = stream_order(graph, toposort)
-    min_streamorder_land = get(config.model, "min_streamorder_land", 5)
-    subbas_order, indices_subbas, topo_subbas = kinwave_set_subdomains(
-        graph,
-        toposort,
-        index_pit_land,
-        streamorder,
-        min_streamorder_land,
-    )
-    min_streamorder_river = get(config.model, "min_streamorder_river", 6)
-    subriv_order, indices_subriv, topo_subriv = kinwave_set_subdomains(
-        graph_riv,
-        toposort_riv,
-        index_pit_river,
-        streamorder[index_river],
-        min_streamorder_river,
-    )
-
-    modelmap = (vertical = flextopo, lateral = (land = olf, river = rf))
-    indices_reverse = (
-        land = rev_inds,
-        river = rev_inds_riv,
-        reservoir = isempty(reservoir) ? nothing : reservoir.reverse_indices,
-        lake = isempty(lake) ? nothing : lake.reverse_indices,
-    )
-
-    # if fews_run = true, then classes are specified as Float64 index so FEWS can import
-    # NetCDF output (3D data/SVector).
-    fews_run = get(config, "fews_run", false)::Bool
-    classes = fews_run ? Float64.(collect(1:length(flextopo.classes))) : flextopo.classes
-
-    writer = prepare_writer(
-        config,
-        modelmap,
-        indices_reverse,
-        x_nc,
-        y_nc,
-        nc,
-        extra_dim = (name = "classes", value = classes),
-    )
-    close(nc)
-
-    # for each domain save:
-    # - the directed acyclic graph (graph),
-    # - the traversion order (order),
-    # - upstream_nodes,
-    # - subdomains for the kinematic wave domains for parallel execution (execution order of
-    #   subbasins (subdomain_order), traversion order per subbasin (topo_subdomain) and
-    #   Vector indices per subbasin matching the traversion order of the complete domain
-    #   (indices_subdomain))
-    # - the indices that map it back to the two dimensional grid (indices)
-
-    # for the land domain the x and y length [m] of the grid cells are stored
-    # for reservoirs and lakes indices information is available from the initialization
-    # functions
-    land = (
-        graph = graph,
-        upstream_nodes = filter_upsteam_nodes(graph, pits[inds]),
-        subdomain_order = subbas_order,
-        topo_subdomain = topo_subbas,
-        indices_subdomain = indices_subbas,
-        order = toposort,
-        indices = inds,
-        reverse_indices = rev_inds,
-        area = xl .* yl,
-    )
-    river = (
-        graph = graph_riv,
-        upstream_nodes = filter_upsteam_nodes(graph_riv, pits[inds_riv]),
-        subdomain_order = subriv_order,
-        topo_subdomain = topo_subriv,
-        indices_subdomain = indices_subriv,
-        order = toposort_riv,
-        indices = inds_riv,
-        reverse_indices = rev_inds_riv,
-    )
-
-    model = Model(
-        config,
-        (; land, river, reservoir, lake, index_river, frac_toriver),
-        (subsurface = nothing, land = olf, river = rf),
-        flextopo,
-        clock,
-        reader,
-        writer,
-        FlextopoModel(),
-    )
-
-    model = set_states(model)
-
-    return model
-end
-
-function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:FlextopoModel}
-    (; lateral, vertical, network, config) = model
-
-    inds_riv = network.index_river
-
-    #COMMON SNOW
-    vertical.dic_function[vertical.select_snow[1]](vertical)
-
-    #lateral snow transport
-    if get(config.model, "masswasting", false)::Bool
-        lateral_snow_transport!(
-            vertical.snow,
-            vertical.snowwater,
-            lateral.land.sl,
-            network.land,
-        )
-    end
-
-    if get(config.model, "glacier", false)::Bool
-        common_glaciers(vertical, config)
-    end
-
-    for (k, class) in enumerate(vertical.classes)
-        vertical.kclass[1] = k
-
-        #INTERCEPTION
-        vertical.dic_function[vertical.select_interception[k]](vertical)
-
-        #HORTON
-        vertical.dic_function[vertical.select_hortonponding[k]](vertical)
-        vertical.dic_function[vertical.select_hortonrunoff[k]](vertical)
-
-        #ROOT-ZONE
-        vertical.dic_function[vertical.select_rootzone[k]](vertical)
-
-        #FAST
-        vertical.dic_function[vertical.select_fast[k]](vertical)
-
-    end
-
-    #COMMON SLOW
-    vertical.dic_function[vertical.select_slow[1]](vertical)
-
-    # WAT BAL
-    watbal(vertical)
-
-    surface_routing(model)
-
-    return model
-end
-
-function set_states(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:FlextopoModel}
-    (; lateral, config) = model
-    reinit = get(config.model, "reinit", true)::Bool
-    # read and set states in model object if reinit=true
-    if reinit == false
-        instate_path = input_path(config, config.state.path_input)
-        set_states(instate_path, model; type = Float, dimname = :classes)
-
-        # update kinematic wave volume for river and land domain
-        lateral.land.volume .= lateral.land.h .* lateral.land.width .* lateral.land.dl
-        lateral.river.volume .= lateral.river.h .* lateral.river.width .* lateral.river.dl
-
-        if do_lakes
-            # storage must be re-initialized after loading the state with the current
-            # waterlevel otherwise the storage will be based on the initial water level
-            lakes = lateral.river.lake
-            lakes.storage .=
-                initialize_storage(lakes.storfunc, lakes.area, lakes.waterlevel, lakes.sh)
-        end
-    end
-    return model
-end
diff --git a/src/flow.jl b/src/flow.jl
index f7578cc03..dab39db52 100644
--- a/src/flow.jl
+++ b/src/flow.jl
@@ -1,68 +1,60 @@
 
 abstract type SurfaceFlow end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct SurfaceFlowRiver{T,R,L,W} <:
-                                                               SurfaceFlow
-    beta::T | "-" | 0 | "scalar"                 # constant in Manning's equation
-    sl::Vector{T} | "m m-1"                      # Slope [m m⁻¹]
-    n::Vector{T} | "s m-1/3"                     # Manning's roughness [s m⁻⅓]
-    dl::Vector{T} | "m"                          # Drain length [m]
-    q::Vector{T} | "m3 s-1"                      # Discharge [m³ s⁻¹]
-    qin::Vector{T} | "m3 s-1"                    # Inflow from upstream cells [m³ s⁻¹]
-    q_av::Vector{T} | "m3 s-1"                   # Average discharge [m³ s⁻¹]
-    qlat::Vector{T} | "m2 s-1"                   # Lateral inflow per unit length [m² s⁻¹]
-    inwater::Vector{T} | "m3 s-1"                # Lateral inflow [m³ s⁻¹]
-    inflow::Vector{T} | "m3 s-1"                 # External inflow (abstraction/supply/demand) [m³ s⁻¹]
-    inflow_wb::Vector{T} | "m3 s-1"              # inflow waterbody (lake or reservoir model) from land part [m³ s⁻¹]
-    abstraction::Vector{T} | "m3 s-1"            # Abstraction (computed as part of water demand and allocation) [m³ s⁻¹]
-    volume::Vector{T} | "m3"                     # Kinematic wave volume [m³] (based on water level h)
-    h::Vector{T} | "m"                           # Water level [m]
-    h_av::Vector{T} | "m"                        # Average water level [m]
-    bankfull_depth::Vector{T} | "m"              # Bankfull water level [m]
-    dt::T | "s" | 0 | "none" | "none"            # Model time step [s]
-    its::Int | "-" | 0 | "none" | "none"         # Number of fixed iterations
-    width::Vector{T} | "m"                       # Flow width [m]
-    alpha_pow::T | "-" | 0 | "scalar"            # Used in the power part of alpha
-    alpha_term::Vector{T} | "-"                  # Term used in computation of alpha
-    alpha::Vector{T} | "s3/5 m1/5"               # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation
-    cel::Vector{T} | "m s-1"                     # Celerity of the kinematic wave
-    reservoir_index::Vector{Int} | "-"           # map cell to 0 (no reservoir) or i (pick reservoir i in reservoir field)
-    lake_index::Vector{Int} | "-"                # map cell to 0 (no lake) or i (pick lake i in lake field)
-    reservoir::R | "-" | 0                       # Reservoir model struct of arrays
-    lake::L | "-" | 0                            # Lake model struct of arrays
-    allocation::W | "-" | 0                      # Water allocation
-    kinwave_it::Bool | "-" | 0 | "none" | "none" # Boolean for iterations kinematic wave
-
-    # TODO unclear why this causes a MethodError
-    # function SurfaceFlow{T,R,L}(args...) where {T,R,L}
-    #     equal_size_vectors(args)
-    #     return new(args...)
-    # end
+@get_units @grid_loc @with_kw struct SurfaceFlowRiver{T, R, L, A} <: SurfaceFlow
+    beta::T                                 # constant in Manning's equation [-]
+    sl::Vector{T} | "m m-1"                 # Slope [m m⁻¹]
+    n::Vector{T} | "s m-1/3"                # Manning's roughness [s m⁻⅓]
+    dl::Vector{T} | "m"                     # Drain length [m]
+    q::Vector{T} | "m3 s-1"                 # Discharge [m³ s⁻¹]
+    qin::Vector{T} | "m3 s-1"               # Inflow from upstream cells [m³ s⁻¹]
+    q_av::Vector{T} | "m3 s-1"              # Average discharge [m³ s⁻¹]
+    qlat::Vector{T} | "m2 s-1"              # Lateral inflow per unit length [m² s⁻¹]
+    inwater::Vector{T} | "m3 s-1"           # Lateral inflow [m³ s⁻¹]
+    inflow::Vector{T} | "m3 s-1"            # External inflow (abstraction/supply/demand) [m³ s⁻¹]
+    inflow_wb::Vector{T} | "m3 s-1"         # inflow waterbody (lake or reservoir model) from land part [m³ s⁻¹]
+    abstraction::Vector{T} | "m3 s-1"       # Abstraction (computed as part of water demand and allocation) [m³ s⁻¹]
+    volume::Vector{T} | "m3"                # Kinematic wave volume [m³] (based on water level h)
+    h::Vector{T} | "m"                      # Water level [m]
+    h_av::Vector{T} | "m"                   # Average water level [m]
+    bankfull_depth::Vector{T} | "m"         # Bankfull water level [m]
+    dt::T                                   # Model time step [s]
+    its::Int                                # Number of fixed iterations [-]
+    width::Vector{T} | "m"                  # Flow width [m]
+    alpha_pow::T                            # Used in the power part of alpha [-]
+    alpha_term::Vector{T} | "-"             # Term used in computation of alpha [-]
+    alpha::Vector{T} | "s3/5 m1/5"          # Constant in momentum equation A = alpha*Q^beta, based on Manning's equation
+    cel::Vector{T} | "m s-1"                # Celerity of the kinematic wave
+    reservoir_index::Vector{Int} | "-"      # map cell to 0 (no reservoir) or i (pick reservoir i in reservoir field)
+    lake_index::Vector{Int} | "-"           # map cell to 0 (no lake) or i (pick lake i in lake field)
+    reservoir::R                            # Reservoir model struct of arrays
+    lake::L                                 # Lake model struct of arrays
+    allocation::A                           # Water allocation
+    kinwave_it::Bool                        # Boolean for iterations kinematic wave
 end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct SurfaceFlowLand{T} <:
-                                                               SurfaceFlow
-    beta::T | "-" | 0 | "scalar"                       # constant in Manning's equation
-    sl::Vector{T} | "m m-1"                         # Slope [m m⁻¹]
-    n::Vector{T} | "s m-1/3"                        # Manning's roughness [s m⁻⅓]
-    dl::Vector{T} | "m"                             # Drain length [m]
-    q::Vector{T} | "m3 s-1"                         # Discharge [m³ s⁻¹]
-    qin::Vector{T} | "m3 s-1"                       # Inflow from upstream cells [m³ s⁻¹]
-    q_av::Vector{T} | "m3 s-1"                      # Average discharge [m³ s⁻¹]
-    qlat::Vector{T} | "m2 s-1"                      # Lateral inflow per unit length [m² s⁻¹]
-    inwater::Vector{T} | "m3 s-1"                   # Lateral inflow [m³ s⁻¹]
-    volume::Vector{T} | "m3"                        # Kinematic wave volume [m³] (based on water level h)
-    h::Vector{T} | "m"                              # Water level [m]
-    h_av::Vector{T} | "m"                           # Average water level [m]
-    dt::T | "s" | 0 | "none" | "none"               # Model time step [s]
-    its::Int | "-" | 0 | "none" | "none"            # Number of fixed iterations
-    width::Vector{T} | "m"                          # Flow width [m]
-    alpha_pow::T | "-" | 0 | "scalar"               # Used in the power part of alpha
-    alpha_term::Vector{T} | "-"                     # Term used in computation of alpha
-    alpha::Vector{T} | "s3/5 m1/5"                  # Constant in momentum equation A = alpha * Q^beta, based on Manning's equation
-    cel::Vector{T} | "m s-1"                        # Celerity of the kinematic wave
-    to_river::Vector{T} | "m3 s-1"                  # Part of overland flow [m³ s⁻¹] that flows to the river
-    kinwave_it::Bool | "-" | 0 | "none" | "none"    # Boolean for iterations kinematic wave
+@get_units @grid_loc @with_kw struct SurfaceFlowLand{T} <: SurfaceFlow
+    beta::T                             # constant in Manning's equation [-]
+    sl::Vector{T} | "m m-1"             # Slope [m m⁻¹]
+    n::Vector{T} | "s m-1/3"            # Manning's roughness [s m⁻⅓]
+    dl::Vector{T} | "m"                 # Drain length [m]
+    q::Vector{T} | "m3 s-1"             # Discharge [m³ s⁻¹]
+    qin::Vector{T} | "m3 s-1"           # Inflow from upstream cells [m³ s⁻¹]
+    q_av::Vector{T} | "m3 s-1"          # Average discharge [m³ s⁻¹]
+    qlat::Vector{T} | "m2 s-1"          # Lateral inflow per unit length [m² s⁻¹]
+    inwater::Vector{T} | "m3 s-1"       # Lateral inflow [m³ s⁻¹]
+    volume::Vector{T} | "m3"            # Kinematic wave volume [m³] (based on water level h)
+    h::Vector{T} | "m"                  # Water level [m]
+    h_av::Vector{T} | "m"               # Average water level [m]
+    dt::T                               # Model time step [s]
+    its::Int                            # Number of fixed iterations [-]
+    width::Vector{T} | "m"              # Flow width [m]
+    alpha_pow::T                        # Used in the power part of alpha [-]
+    alpha_term::Vector{T} | "-"         # Term used in computation of alpha [-]
+    alpha::Vector{T} | "s3/5 m1/5"      # Constant in momentum equation A = alpha * Q^beta, based on Manning's equation
+    cel::Vector{T} | "m s-1"            # Celerity of the kinematic wave
+    to_river::Vector{T} | "m3 s-1"      # Part of overland flow [m³ s⁻¹] that flows to the river
+    kinwave_it::Bool                    # Boolean for iterations kinematic wave [-]
 end
 
 function initialize_surfaceflow_land(nc, config, inds; sl, dl, width, iterate, tstep, dt)
@@ -75,7 +67,7 @@ function initialize_surfaceflow_land(nc, config, inds; sl, dl, width, iterate, t
         ncread(nc, config, "lateral.land.n"; sel = inds, defaults = 0.072, type = Float)
     n = length(inds)
 
-    sf_land = SurfaceFlowLand(
+    sf_land = SurfaceFlowLand(;
         beta = Float(0.6),
         sl = sl,
         n = n_land,
@@ -149,10 +141,9 @@ function initialize_surfaceflow_river(
     clamp!(sl, 0.00001, Inf)
 
     do_water_demand = haskey(config.model, "water_demand")
-
     n = length(inds)
 
-    sf_river = SurfaceFlowRiver(
+    sf_river = SurfaceFlowRiver(;
         beta = Float(0.6),
         sl = sl,
         n = n_river,
@@ -181,16 +172,14 @@ function initialize_surfaceflow_river(
         reservoir = reservoir,
         lake = lake,
         kinwave_it = iterate,
-        allocation = do_water_demand ? initialize_allocation_river(n) : nothing,
+        allocation = do_water_demand ? AllocationRiver(n) : nothing,
     )
 
     return sf_river
 end
 
-
-function update(sf::SurfaceFlowLand, network, frac_toriver)
-    (; subdomain_order, topo_subdomain, indices_subdomain, upstream_nodes) =
-        network
+function update!(sf::SurfaceFlowLand, network, frac_toriver)
+    (; subdomain_order, topo_subdomain, indices_subdomain, upstream_nodes) = network
 
     ns = length(subdomain_order)
 
@@ -204,10 +193,10 @@ function update(sf::SurfaceFlowLand, network, frac_toriver)
     sf.to_river .= 0.0
 
     dt, its = stable_timestep(sf)
-    for _ = 1:its
+    for _ in 1:its
         sf.qin .= 0.0
-        for k = 1:ns
-            threaded_foreach(eachindex(subdomain_order[k]), basesize = 1) do i
+        for k in 1:ns
+            threaded_foreach(eachindex(subdomain_order[k]); basesize = 1) do i
                 m = subdomain_order[k][i]
                 for (n, v) in zip(indices_subdomain[m], topo_subdomain[m])
                     # for a river cell without a reservoir or lake part of the upstream
@@ -252,12 +241,11 @@ function update(sf::SurfaceFlowLand, network, frac_toriver)
     sf.h_av ./= its
     sf.to_river ./= its
     sf.volume .= sf.dl .* sf.width .* sf.h
+    return nothing
 end
 
-function update(sf::SurfaceFlowRiver, network, doy)
-
-    (; graph, subdomain_order, topo_subdomain, indices_subdomain, upstream_nodes) =
-        network
+function update!(sf::SurfaceFlowRiver, network, doy)
+    (; graph, subdomain_order, topo_subdomain, indices_subdomain, upstream_nodes) = network
 
     ns = length(subdomain_order)
 
@@ -283,10 +271,10 @@ function update(sf::SurfaceFlowRiver, network, doy)
     end
 
     dt, its = stable_timestep(sf)
-    for _ = 1:its
+    for _ in 1:its
         sf.qin .= 0.0
-        for k = 1:ns
-            threaded_foreach(eachindex(subdomain_order[k]), basesize = 1) do i
+        for k in 1:ns
+            threaded_foreach(eachindex(subdomain_order[k]); basesize = 1) do i
                 m = subdomain_order[k][i]
                 for (n, v) in zip(indices_subdomain[m], topo_subdomain[m])
                     # sf.qin by outflow from upstream reservoir or lake location is added
@@ -318,7 +306,7 @@ function update(sf::SurfaceFlowRiver, network, doy)
                         # run reservoir model and copy reservoir outflow to inflow (qin) of
                         # downstream river cell
                         i = sf.reservoir_index[v]
-                        update(sf.reservoir, i, sf.q[v] + sf.inflow_wb[v], dt)
+                        update!(sf.reservoir, i, sf.q[v] + sf.inflow_wb[v], dt)
 
                         downstream_nodes = outneighbors(graph, v)
                         n_downstream = length(downstream_nodes)
@@ -339,7 +327,7 @@ function update(sf::SurfaceFlowRiver, network, doy)
                         # run lake model and copy lake outflow to inflow (qin) of downstream river
                         # cell
                         i = sf.lake_index[v]
-                        update(sf.lake, i, sf.q[v] + sf.inflow_wb[v], doy, dt)
+                        update!(sf.lake, i, sf.q[v] + sf.inflow_wb[v], doy, dt)
 
                         downstream_nodes = outneighbors(graph, v)
                         n_downstream = length(downstream_nodes)
@@ -361,7 +349,6 @@ function update(sf::SurfaceFlowRiver, network, doy)
                     crossarea = sf.alpha[v] * pow(sf.q[v], sf.beta)
                     sf.h[v] = crossarea / sf.width[v]
                     sf.volume[v] = sf.dl[v] * sf.width[v] * sf.h[v]
-
                     sf.q_av[v] += sf.q[v]
                     sf.h_av[v] += sf.h[v]
                 end
@@ -370,9 +357,11 @@ function update(sf::SurfaceFlowRiver, network, doy)
     end
     sf.q_av ./= its
     sf.h_av ./= its
+    sf.volume .= sf.dl .* sf.width .* sf.h
+    return nothing
 end
 
-function stable_timestep(sf::S) where {S<:SurfaceFlow}
+function stable_timestep(sf::S) where {S <: SurfaceFlow}
     n = length(sf.q)
     # two options for iteration, fixed or based on courant number.
     if sf.kinwave_it
@@ -381,7 +370,7 @@ function stable_timestep(sf::S) where {S<:SurfaceFlow}
         else
             # calculate celerity
             courant = zeros(n)
-            for v = 1:n
+            for v in 1:n
                 if sf.q[v] > 0.0
                     sf.cel[v] =
                         1.0 / (sf.alpha[v] * sf.beta * pow(sf.q[v], (sf.beta - 1.0)))
@@ -400,20 +389,38 @@ function stable_timestep(sf::S) where {S<:SurfaceFlow}
     return dt, its
 end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct LateralSSF{T}
-    kh_0::Vector{T} | "m d-1"               # Horizontal hydraulic conductivity at soil surface [m d⁻¹]
-    f::Vector{T} | "m-1"                   # A scaling parameter [m⁻¹] (controls exponential decline of kh_0)
-    kh::Vector{T} | "m d-1"                # Horizontal hydraulic conductivity [m d⁻¹]
+@get_units @grid_loc struct KhExponential{T}
+    # Horizontal hydraulic conductivity at soil surface [m d⁻¹]
+    kh_0::Vector{T} | "m d-1"
+    # A scaling parameter [m⁻¹] (controls exponential decline of kh_0)
+    f::Vector{T} | "m-1"
+end
+
+@get_units @grid_loc struct KhExponentialConstant{T}
+    # Exponential horizontal hydraulic conductivity profile type
+    exponential::KhExponential
+    # Depth [m] from soil surface for which exponential decline of kv_0 is valid
+    z_exp::Vector{T} | "m"
+end
+
+@get_units @grid_loc struct KhLayered{T}
+    # Horizontal hydraulic conductivity [m d⁻¹]
+    kh::Vector{T} | "m d-1"
+end
+
+abstract type SubsurfaceFlow end
+
+@get_units @grid_loc @with_kw struct LateralSSF{T, Kh} <: SubsurfaceFlow
+    kh_profile::Kh                         # Horizontal hydraulic conductivity profile type [-]  
     khfrac::Vector{T} | "-"                # A muliplication factor applied to vertical hydraulic conductivity `kv` [-]
     soilthickness::Vector{T} | "m"         # Soil thickness [m]
     theta_s::Vector{T} | "-"               # Saturated water content (porosity) [-]
     theta_r::Vector{T} | "-"               # Residual water content [-]
-    dt::T | "d" | 0 | "none" | "none"      # model time step [d]
-    slope::Vector{T} | "m m-1"            # Slope [m m⁻¹]
+    dt::T                                  # model time step [d]
+    slope::Vector{T} | "m m-1"             # Slope [m m⁻¹]
     dl::Vector{T} | "m"                    # Drain length [m]
     dw::Vector{T} | "m"                    # Flow width [m]
     zi::Vector{T} | "m"                    # Pseudo-water table depth [m] (top of the saturated zone)
-    z_exp::Vector{T} | "m"                 # Depth [m] from soil surface for which exponential decline of kv_0 is valid
     exfiltwater::Vector{T} | "m dt-1"      # Exfiltration [m Δt⁻¹] (groundwater above surface level, saturated excess conditions)
     recharge::Vector{T} | "m2 dt-1"        # Net recharge to saturated store [m² Δt⁻¹]
     ssf::Vector{T} | "m3 d-1"              # Subsurface flow [m³ d⁻¹]
@@ -422,20 +429,18 @@ end
     to_river::Vector{T} | "m3 d-1"         # Part of subsurface flow [m³ d⁻¹] that flows to the river
     volume::Vector{T} | "m3"               # Subsurface volume [m³]
 
-    function LateralSSF{T}(args...) where {T}
+    function LateralSSF{T, Kh}(args...) where {T, Kh}
         equal_size_vectors(args)
         return new(args...)
     end
 end
 
-function update(ssf::LateralSSF, network, frac_toriver, ksat_profile)
-    (; subdomain_order, topo_subdomain, indices_subdomain, upstream_nodes, area) =
-        network
-
+function update!(ssf::LateralSSF, network, frac_toriver)
+    (; subdomain_order, topo_subdomain, indices_subdomain, upstream_nodes, area) = network
 
     ns = length(subdomain_order)
-    for k = 1:ns
-        threaded_foreach(eachindex(subdomain_order[k]), basesize = 1) do i
+    for k in 1:ns
+        threaded_foreach(eachindex(subdomain_order[k]); basesize = 1) do i
             m = subdomain_order[k][i]
             for (n, v) in zip(indices_subdomain[m], topo_subdomain[m])
                 # for a river cell without a reservoir or lake part of the upstream
@@ -452,42 +457,21 @@ function update(ssf::LateralSSF, network, frac_toriver, ksat_profile)
                     upstream_nodes[n],
                     eltype(ssf.to_river),
                 )
-                if (ksat_profile == "exponential") ||
-                   (ksat_profile == "exponential_constant")
-                    ssf.ssf[v], ssf.zi[v], ssf.exfiltwater[v] = kinematic_wave_ssf(
-                        ssf.ssfin[v],
-                        ssf.ssf[v],
-                        ssf.zi[v],
-                        ssf.recharge[v],
-                        ssf.kh_0[v],
-                        ssf.slope[v],
-                        ssf.theta_s[v] - ssf.theta_r[v],
-                        ssf.f[v],
-                        ssf.soilthickness[v],
-                        ssf.dt,
-                        ssf.dl[v],
-                        ssf.dw[v],
-                        ssf.ssfmax[v],
-                        ssf.z_exp[v],
-                        ksat_profile,
-                    )
-                elseif (ksat_profile == "layered") ||
-                       (ksat_profile == "layered_exponential")
-                    ssf.ssf[v], ssf.zi[v], ssf.exfiltwater[v] = kinematic_wave_ssf(
-                        ssf.ssfin[v],
-                        ssf.ssf[v],
-                        ssf.zi[v],
-                        ssf.recharge[v],
-                        ssf.kh[v],
-                        ssf.slope[v],
-                        ssf.theta_s[v] - ssf.theta_r[v],
-                        ssf.soilthickness[v],
-                        ssf.dt,
-                        ssf.dl[v],
-                        ssf.dw[v],
-                        ssf.ssfmax[v],
-                    )
-                end
+                ssf.ssf[v], ssf.zi[v], ssf.exfiltwater[v] = kinematic_wave_ssf(
+                    ssf.ssfin[v],
+                    ssf.ssf[v],
+                    ssf.zi[v],
+                    ssf.recharge[v],
+                    ssf.slope[v],
+                    ssf.theta_s[v] - ssf.theta_r[v],
+                    ssf.soilthickness[v],
+                    ssf.dt,
+                    ssf.dl[v],
+                    ssf.dw[v],
+                    ssf.ssfmax[v],
+                    ssf.kh_profile,
+                    v,
+                )
                 ssf.volume[v] =
                     (ssf.theta_s[v] - ssf.theta_r[v]) *
                     (ssf.soilthickness[v] - ssf.zi[v]) *
@@ -495,61 +479,65 @@ function update(ssf::LateralSSF, network, frac_toriver, ksat_profile)
             end
         end
     end
+    return nothing
 end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct GroundwaterExchange{T}
-    dt::T | "d" | 0 | "none" | "none"   # model time step [d]
+@get_units@grid_loc @with_kw struct GroundwaterExchange{T} <: SubsurfaceFlow
+    dt::T                               # model time step [d]
     exfiltwater::Vector{T} | "m dt-1"   # Exfiltration [m Δt⁻¹]  (groundwater above surface level, saturated excess conditions)
     zi::Vector{T} | "m"                 # Pseudo-water table depth [m] (top of the saturated zone)
     to_river::Vector{T} | "m3 d-1"      # Part of subsurface flow [m³ d⁻¹] that flows to the river
     ssf::Vector{T} | "m3 d-1"           # Subsurface flow [m³ d⁻¹]
 end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct ShallowWaterRiver{T,R,L,F,W}
-    n::Int | "-" | 0 | "none" | "none"                      # number of cells
-    ne::Int | "-" | 0 | "none" | "none"                     # number of edges/links
-    active_n::Vector{Int} | "-"                             # active nodes
-    active_e::Vector{Int} | "-" | _ | "edge"                # active edges/links
-    g::T | "m s-2" | 0 | "scalar"                           # acceleration due to gravity
-    alpha::T | "-" | 0 | "scalar"                           # stability coefficient (Bates et al., 2010)
-    h_thresh::T | "m" | 0 | "scalar"                        # depth threshold for calculating flow
-    dt::T | "s" | 0 | "none" | "none"                       # model time step [s]
-    q::Vector{T} | "m3 s-1" | _ | "edge"                    # river discharge (subgrid channel)
-    q0::Vector{T} | "m3 s-1" | _ | "edge"                   # river discharge (subgrid channel) at previous time step
-    q_av::Vector{T} | "m3 s-1" | _ | "edge"                 # average river channel (+ floodplain) discharge [m³ s⁻¹]
-    q_channel_av::Vector{T} | "m3 s-1"                      # average river channel discharge [m³ s⁻¹]
-    zb_max::Vector{T} | "m"                                 # maximum channel bed elevation
-    mannings_n_sq::Vector{T} | "(s m-1/3)2" | _ | "edge"    # Manning's roughness squared at edge/link
-    mannings_n::Vector{T} | "s m-1/3"                       # Manning's roughness at node
-    h::Vector{T} | "m"                                      # water depth
-    zs_max::Vector{T} | "m" | _ | "edge"                    # maximum water elevation at edge
-    zs_src::Vector{T} | "m"                                 # water elevation of source node of edge
-    zs_dst::Vector{T} | "m"                                 # water elevation of downstream node of edge
-    hf::Vector{T} | "m" | _ | "edge"                        # water depth at edge/link
-    h_av::Vector{T} | "m"                                   # average water depth
-    dl::Vector{T} | "m"                                     # river length
-    dl_at_link::Vector{T} | "m" | _ | "edge"                # river length at edge/link
-    width::Vector{T} | "m"                                  # river width
-    width_at_link::Vector{T} | "m" | _ | "edge"             # river width at edge/link
-    a::Vector{T} | "m2" | _ | "edge"                        # flow area at edge/link
-    r::Vector{T} | "m" | _ | "edge"                         # wetted perimeter at edge/link
-    volume::Vector{T} | "m3"                                # river volume
-    error::Vector{T} | "m3"                                 # error volume
-    inwater::Vector{T} | "m3 s-1"                           # lateral inflow [m³ s⁻¹]
-    inflow::Vector{T} | "m3 s-1"                            # external inflow (abstraction/supply/demand) [m³ s⁻¹]
-    abstraction::Vector{T} | "m3 s-1"                       # abstraction (computed as part of water demand and allocation) [m³ s⁻¹]
-    inflow_wb::Vector{T} | "m3 s-1"                         # inflow waterbody (lake or reservoir model) from land part [m³ s⁻¹]
-    bankfull_volume::Vector{T} | "m3"                       # bankfull volume
-    bankfull_depth::Vector{T} | "m"                         # bankfull depth
-    zb::Vector{T} | "m"                                     # river bed elevation
-    froude_limit::Bool | "-" | 0 | "none" | "none"          # if true a check is performed if froude number > 1.0 (algorithm is modified)
-    reservoir_index::Vector{Int} | "-" | 0                  # river cell index with a reservoir (each index of reservoir_index maps to reservoir i in reservoir field)
-    lake_index::Vector{Int} | "-" | 0                       # river cell index with a lake (each index of lake_index maps to lake i in lake field)
-    waterbody::Vector{Bool} | "-"                           # water body cells (reservoir or lake)
-    reservoir::R | "-" | 0                                  # Reservoir model struct of arrays
-    lake::L | "-" | 0                                       # Lake model struct of arrays
-    floodplain::F | "-" | 0                                 # Floodplain (1D) schematization
-    allocation::W | "-" | 0                                 # Water allocation
+get_water_depth(subsurface::SubsurfaceFlow) = subsurface.zi
+get_exfiltwater(subsurface::SubsurfaceFlow) = subsurface.exfiltwater
+
+@get_units @grid_loc @with_kw struct ShallowWaterRiver{T, R, L, F, A}
+    n::Int                                              # number of cells [-]
+    ne::Int                                             # number of edges/links [-]
+    active_n::Vector{Int} | "-"                         # active nodes [-]
+    active_e::Vector{Int} | "-" | "edge"                # active edges/links [-]
+    g::T                                                # acceleration due to gravity [m s⁻²]
+    alpha::T                                            # stability coefficient (Bates et al., 2010) [-]
+    h_thresh::T                                         # depth threshold for calculating flow [m]
+    dt::T                                               # model time step [s]
+    q::Vector{T} | "m3 s-1" | "edge"                    # river discharge (subgrid channel)
+    q0::Vector{T} | "m3 s-1" | "edge"                   # river discharge (subgrid channel) at previous time step
+    q_av::Vector{T} | "m3 s-1" | "edge"                 # average river channel (+ floodplain) discharge [m³ s⁻¹]
+    q_channel_av::Vector{T} | "m3 s-1"                  # average river channel discharge [m³ s⁻¹]
+    zb_max::Vector{T} | "m"                             # maximum channel bed elevation
+    mannings_n_sq::Vector{T} | "(s m-1/3)2" | "edge"    # Manning's roughness squared at edge/link
+    mannings_n::Vector{T} | "s m-1/3"                   # Manning's roughness at node
+    h::Vector{T} | "m"                                  # water depth
+    zs_max::Vector{T} | "m" | "edge"                    # maximum water elevation at edge
+    zs_src::Vector{T} | "m"                             # water elevation of source node of edge
+    zs_dst::Vector{T} | "m"                             # water elevation of downstream node of edge
+    hf::Vector{T} | "m" | "edge"                        # water depth at edge/link
+    h_av::Vector{T} | "m"                               # average water depth
+    dl::Vector{T} | "m"                                 # river length
+    dl_at_link::Vector{T} | "m" | "edge"                # river length at edge/link
+    width::Vector{T} | "m"                              # river width
+    width_at_link::Vector{T} | "m" | "edge"             # river width at edge/link
+    a::Vector{T} | "m2" | "edge"                        # flow area at edge/link
+    r::Vector{T} | "m" | "edge"                         # wetted perimeter at edge/link
+    volume::Vector{T} | "m3"                            # river volume
+    error::Vector{T} | "m3"                             # error volume
+    inwater::Vector{T} | "m3 s-1"                       # lateral inflow [m³ s⁻¹]
+    inflow::Vector{T} | "m3 s-1"                        # external inflow (abstraction/supply/demand) [m³ s⁻¹]
+    abstraction::Vector{T} | "m3 s-1"                   # abstraction (computed as part of water demand and allocation) [m³ s⁻¹]
+    inflow_wb::Vector{T} | "m3 s-1"                     # inflow waterbody (lake or reservoir model) from land part [m³ s⁻¹]
+    bankfull_volume::Vector{T} | "m3"                   # bankfull volume
+    bankfull_depth::Vector{T} | "m"                     # bankfull depth
+    zb::Vector{T} | "m"                                 # river bed elevation
+    froude_limit::Bool                                  # if true a check is performed if froude number > 1.0 (algorithm is modified) [-]
+    reservoir_index::Vector{Int} | "-"                  # river cell index with a reservoir (each index of reservoir_index maps to reservoir i in reservoir field)
+    lake_index::Vector{Int} | "-"                       # river cell index with a lake (each index of lake_index maps to lake i in lake field)
+    waterbody::Vector{Bool} | "-"                       # water body cells (reservoir or lake)
+    reservoir::R                                        # Reservoir model struct of arrays
+    lake::L                                             # Lake model struct of arrays
+    floodplain::F                                       # Floodplain (1D) schematization
+    allocation::A                                       # Water allocation
 end
 
 function initialize_shallowwater_river(
@@ -672,7 +660,7 @@ function initialize_shallowwater_river(
     width_at_link = fill(Float(0), _ne)
     length_at_link = fill(Float(0), _ne)
     mannings_n_sq = fill(Float(0), _ne)
-    for i = 1:_ne
+    for i in 1:_ne
         src_node = nodes_at_link.src[i]
         dst_node = nodes_at_link.dst[i]
         zb_max[i] = max(zb[src_node], zb[dst_node])
@@ -689,7 +677,7 @@ function initialize_shallowwater_river(
     active_index = findall(x -> x == 0, waterbody)
 
     do_water_demand = haskey(config.model, "water_demand")
-    sw_river = ShallowWaterRiver(
+    sw_river = ShallowWaterRiver(;
         n = n,
         ne = _ne,
         active_n = active_index,
@@ -747,8 +735,7 @@ function get_inflow_waterbody(sw::ShallowWaterRiver, src_edge)
     return q_in
 end
 
-function shallowwater_river_update(sw::ShallowWaterRiver, network, dt, doy, update_h)
-
+function shallowwater_river_update!(sw::ShallowWaterRiver, network, dt, doy, update_h)
     (; nodes_at_link, links_at_node) = network
 
     sw.q0 .= sw.q
@@ -805,7 +792,7 @@ function shallowwater_river_update(sw::ShallowWaterRiver, network, dt, doy, upda
             end
         end
 
-        @tturbo for j = 1:n
+        @tturbo for j in 1:n
             i = sw.floodplain.hf_index[j]
             i_src = nodes_at_link.src[i]
             i_dst = nodes_at_link.dst[i]
@@ -891,7 +878,7 @@ function shallowwater_river_update(sw::ShallowWaterRiver, network, dt, doy, upda
         i = sw.reservoir_index[v]
 
         q_in = get_inflow_waterbody(sw, links_at_node.src[i])
-        update(sw.reservoir, v, q_in + sw.inflow_wb[i], dt)
+        update!(sw.reservoir, v, q_in + sw.inflow_wb[i], dt)
         sw.q[i] = sw.reservoir.outflow[v]
         sw.q_av[i] += sw.q[i] * dt
     end
@@ -899,13 +886,12 @@ function shallowwater_river_update(sw::ShallowWaterRiver, network, dt, doy, upda
         i = sw.lake_index[v]
 
         q_in = get_inflow_waterbody(sw, links_at_node.src[i])
-        update(sw.lake, v, q_in + sw.inflow_wb[i], doy, dt)
+        update!(sw.lake, v, q_in + sw.inflow_wb[i], doy, dt)
         sw.q[i] = sw.lake.outflow[v]
         sw.q_av[i] += sw.q[i] * dt
     end
     if update_h
         @batch per = thread minbatch = 2000 for i in sw.active_n
-
             q_src = sum_at(sw.q, links_at_node.src[i])
             q_dst = sum_at(sw.q, links_at_node.dst[i])
             sw.volume[i] =
@@ -949,9 +935,10 @@ function shallowwater_river_update(sw::ShallowWaterRiver, network, dt, doy, upda
             sw.h_av[i] += sw.h[i] * dt
         end
     end
+    return nothing
 end
 
-function update(sw::ShallowWaterRiver{T}, network, doy; update_h = true) where {T}
+function update!(sw::ShallowWaterRiver{T}, network, doy; update_h = true) where {T}
     if !isnothing(sw.reservoir)
         sw.reservoir.inflow .= 0.0
         sw.reservoir.totaloutflow .= 0.0
@@ -975,7 +962,7 @@ function update(sw::ShallowWaterRiver{T}, network, doy; update_h = true) where {
         if t + dt > sw.dt
             dt = sw.dt - t
         end
-        shallowwater_river_update(sw, network, dt, doy, update_h)
+        shallowwater_river_update!(sw, network, dt, doy, update_h)
         t = t + dt
     end
     sw.q_av ./= sw.dt
@@ -1004,33 +991,33 @@ end
 # neigbors.
 const dirs = (:yd, :xd, :xu, :yu)
 
-@get_units @exchange @grid_type @grid_location @with_kw struct ShallowWaterLand{T}
-    n::Int | "-" | 0 | "none" | "none"                      # number of cells
-    xl::Vector{T} | "m"                                     # cell length x direction
-    yl::Vector{T} | "m"                                     # cell length y direction
-    xwidth::Vector{T} | "m" | _ | "edge"                    # effective flow width x direction (floodplain)
-    ywidth::Vector{T} | "m" | _ | "edge"                    # effective flow width y direction (floodplain)
-    g::T | "m2 s-1" | 0 | "scalar"                          # acceleration due to gravity
-    theta::T | "-" | 0 | "scalar"                           # weighting factor (de Almeida et al., 2012)
-    alpha::T | "-" | 0 | "scalar"                           # stability coefficient (de Almeida et al., 2012)
-    h_thresh::T | "m" | 0 | "scalar"                        # depth threshold for calculating flow
-    dt::T | "s" | 0 | "none" | "none"                       # model time step [s]
-    qy0::Vector{T} | "m3 s-1" | _ | "edge"                  # flow in y direction at previous time step
-    qx0::Vector{T} | "m3 s-1" | _ | "edge"                  # flow in x direction at previous time step
-    qx::Vector{T} | "m3 s-1" | _ | "edge"                   # flow in x direction
-    qy::Vector{T} | "m3 s-1" | _ | "edge"                   # flow in y direction
-    zx_max::Vector{T} | "m" | _ | "edge"                    # maximum cell elevation (x direction)
-    zy_max::Vector{T} | "m" | _ | "edge"                    # maximum cell elevation (y direction)
-    mannings_n_sq::Vector{T} | "(s m-1/3)2" | _ | "edge"    # Manning's roughness squared
-    volume::Vector{T} | "m3"                                # total volume of cell (including river volume for river cells)
-    error::Vector{T} | "m3"                                 # error volume
-    runoff::Vector{T} | "m3 s-1"                            # runoff from hydrological model
-    inflow_wb::Vector{T} | "m3 s-1"                         # inflow to water body from hydrological model
-    h::Vector{T} | "m"                                      # water depth of cell (for river cells the reference is the river bed elevation `zb`)
-    z::Vector{T} | "m"                                      # elevation of cell
-    froude_limit::Bool | "-" | 0 | "none" | "none"          # if true a check is performed if froude number > 1.0 (algorithm is modified)
-    rivercells::Vector{Bool} | "-"                          # river cells
-    h_av::Vector{T} | "m"                                   # average water depth (for river cells the reference is the river bed elevation `zb`)
+@get_units @grid_loc @with_kw struct ShallowWaterLand{T}
+    n::Int                                              # number of cells [-]
+    xl::Vector{T} | "m"                                 # cell length x direction [m]
+    yl::Vector{T} | "m"                                 # cell length y direction [m]
+    xwidth::Vector{T} | "m" | "edge"                    # effective flow width x direction (floodplain) [m]
+    ywidth::Vector{T} | "m" | "edge"                    # effective flow width y direction (floodplain) [m]
+    g::T                                                # acceleration due to gravity [m s⁻²]
+    theta::T                                            # weighting factor (de Almeida et al., 2012) [-]
+    alpha::T                                            # stability coefficient (de Almeida et al., 2012) [-]
+    h_thresh::T                                         # depth threshold for calculating flow [m]
+    dt::T                                               # model time step [s]
+    qy0::Vector{T} | "m3 s-1" | "edge"                  # flow in y direction at previous time step
+    qx0::Vector{T} | "m3 s-1" | "edge"                  # flow in x direction at previous time step
+    qx::Vector{T} | "m3 s-1" | "edge"                   # flow in x direction
+    qy::Vector{T} | "m3 s-1" | "edge"                   # flow in y direction
+    zx_max::Vector{T} | "m" | "edge"                    # maximum cell elevation (x direction)
+    zy_max::Vector{T} | "m" | "edge"                    # maximum cell elevation (y direction)
+    mannings_n_sq::Vector{T} | "(s m-1/3)2" | "edge"    # Manning's roughness squared
+    volume::Vector{T} | "m3"                            # total volume of cell (including river volume for river cells)
+    error::Vector{T} | "m3"                             # error volume
+    runoff::Vector{T} | "m3 s-1"                        # runoff from hydrological model
+    inflow_wb::Vector{T} | "m3 s-1"                     # inflow to water body from hydrological model
+    h::Vector{T} | "m"                                  # water depth of cell (for river cells the reference is the river bed elevation `zb`)
+    z::Vector{T} | "m"                                  # elevation of cell
+    froude_limit::Bool                                  # if true a check is performed if froude number > 1.0 (algorithm is modified) [-]
+    rivercells::Vector{Bool} | "-"                      # river cells
+    h_av::Vector{T} | "m"                               # average water depth (for river cells the reference is the river bed elevation `zb`)
 end
 
 function initialize_shallowwater_land(
@@ -1070,7 +1057,7 @@ function initialize_shallowwater_land(
     n = length(inds)
 
     # initialize links between cells in x and y direction.
-    indices = Indices(xu = zeros(n), xd = zeros(n), yu = zeros(n), yd = zeros(n))
+    indices = Indices(; xu = zeros(n), xd = zeros(n), yu = zeros(n), yd = zeros(n))
 
     # links without neigbors are handled by an extra index (at n + 1, with n links), which
     # is set to a value of 0.0 m³ s⁻¹ for qx and qy fields at initialization.
@@ -1094,7 +1081,7 @@ function initialize_shallowwater_land(
     # determine z at links in x and y direction
     zx_max = fill(Float(0), n)
     zy_max = fill(Float(0), n)
-    for i = 1:n
+    for i in 1:n
         xu = indices.xu[i]
         if xu <= n
             zx_max[i] = max(elevation[i], elevation[xu])
@@ -1121,7 +1108,7 @@ function initialize_shallowwater_land(
         indices_reverse[inds_riv],
     )
 
-    sw_land = ShallowWaterLand{Float}(
+    sw_land = ShallowWaterLand{Float}(;
         n = n,
         xl = xlength,
         yl = ylength,
@@ -1163,7 +1150,7 @@ dt = alpha * (Δx / sqrt(g max(h))
 """
 function stable_timestep(sw::ShallowWaterRiver{T})::T where {T}
     dt_min = T(Inf)
-    @batch per = thread reduction = ((min, dt_min),) for i = 1:sw.n
+    @batch per = thread reduction = ((min, dt_min),) for i in 1:(sw.n)
         @fastmath @inbounds dt = sw.alpha * sw.dl[i] / sqrt(sw.g * sw.h[i])
         dt_min = min(dt, dt_min)
     end
@@ -1173,23 +1160,27 @@ end
 
 function stable_timestep(sw::ShallowWaterLand{T})::T where {T}
     dt_min = T(Inf)
-    @batch per = thread reduction = ((min, dt_min),) for i = 1:sw.n
-        @fastmath @inbounds dt =
-            sw.rivercells[i] == 0 ?
-            sw.alpha * min(sw.xl[i], sw.yl[i]) / sqrt(sw.g * sw.h[i]) : T(Inf)
+    @batch per = thread reduction = ((min, dt_min),) for i in 1:(sw.n)
+        @fastmath @inbounds dt = if sw.rivercells[i] == 0
+            sw.alpha * min(sw.xl[i], sw.yl[i]) / sqrt(sw.g * sw.h[i])
+        else
+            T(Inf)
+        end
         dt_min = min(dt, dt_min)
     end
     dt_min = isinf(dt_min) ? T(10.0) : dt_min
     return dt_min
 end
 
-function update(
+function update!(
     sw::ShallowWaterLand{T},
     swr::ShallowWaterRiver{T},
     network,
     doy;
     update_h = false,
 ) where {T}
+    (; nodes_at_link, links_at_node) = network.river
+
     if !isnothing(swr.reservoir)
         swr.reservoir.inflow .= 0.0
         swr.reservoir.totaloutflow .= 0.0
@@ -1212,22 +1203,23 @@ function update(
         if t + dt > swr.dt
             dt = swr.dt - t
         end
-        shallowwater_river_update(swr, network.river, dt, doy, update_h)
-        shallowwater_update(sw, swr, network, dt)
+        shallowwater_river_update!(swr, network.river, dt, doy, update_h)
+        shallowwater_update!(sw, swr, network, dt)
         t = t + dt
     end
     swr.q_av ./= swr.dt
     swr.h_av ./= swr.dt
     sw.h_av ./= sw.dt
+
+    return nothing
 end
 
-function shallowwater_update(
+function shallowwater_update!(
     sw::ShallowWaterLand{T},
     swr::ShallowWaterRiver{T},
     network,
     dt,
 ) where {T}
-
     indices = network.land.staggered_indices
     inds_riv = network.land.index_river
 
@@ -1237,7 +1229,7 @@ function shallowwater_update(
     sw.qy0 .= sw.qy
 
     # update qx
-    @batch per = thread minbatch = 6000 for i = 1:sw.n
+    @batch per = thread minbatch = 6000 for i in 1:(sw.n)
         yu = indices.yu[i]
         yd = indices.yd[i]
         xu = indices.xu[i]
@@ -1246,7 +1238,6 @@ function shallowwater_update(
         # the effective flow width is zero when the river width exceeds the cell width (dy
         # for flow in x dir) and floodplain flow is not calculated.
         if xu <= sw.n && sw.ywidth[i] != T(0.0)
-
             zs_x = sw.z[i] + sw.h[i]
             zs_xu = sw.z[xu] + sw.h[xu]
             zs_max = max(zs_x, zs_xu)
@@ -1286,7 +1277,6 @@ function shallowwater_update(
         # the effective flow width is zero when the river width exceeds the cell width (dx
         # for flow in y dir) and floodplain flow is not calculated.
         if yu <= sw.n && sw.xwidth[i] != T(0.0)
-
             zs_y = sw.z[i] + sw.h[i]
             zs_yu = sw.z[yu] + sw.h[yu]
             zs_max = max(zs_y, zs_yu)
@@ -1323,7 +1313,7 @@ function shallowwater_update(
     end
 
     # change in volume and water levels based on horizontal fluxes for river and land cells
-    @batch per = thread minbatch = 6000 for i = 1:sw.n
+    @batch per = thread minbatch = 6000 for i in 1:(sw.n)
         yd = indices.yd[i]
         xd = indices.xd[i]
 
@@ -1375,6 +1365,7 @@ function shallowwater_update(
         end
         sw.h_av[i] += sw.h[i] * dt
     end
+    return nothing
 end
 
 """
@@ -1384,35 +1375,35 @@ Floodplain `volume` is a function of `depth` (flood depth intervals). Based on t
 cumulative floodplain `volume` a floodplain profile as a function of `flood_depth` is
 derived with floodplain area `a` (cumulative) and wetted perimeter radius `p` (cumulative).
 """
-@get_units @exchange @grid_type @grid_location @with_kw struct FloodPlainProfile{T,N}
-    depth::Vector{T} | "m" | 0                # Flood depth
-    volume::Array{T,2} | "m3"                 # Flood volume (cumulative)
-    width::Array{T,2} | "m"                   # Flood width
-    a::Array{T,2} | "m2"                      # Flow area (cumulative)
-    p::Array{T,2} | "m"                       # Wetted perimeter (cumulative)
+@get_units @grid_loc @with_kw struct FloodPlainProfile{T, N}
+    depth::Vector{T} | "m"                     # Flood depth
+    volume::Array{T, 2} | "m3"                 # Flood volume (cumulative)
+    width::Array{T, 2} | "m"                   # Flood width
+    a::Array{T, 2} | "m2"                      # Flow area (cumulative)
+    p::Array{T, 2} | "m"                       # Wetted perimeter (cumulative)
 end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct FloodPlain{T,P}
-    profile::P | "-" | 0                                    # floodplain profile
-    mannings_n::Vector{T} | "s m-1/3"                       # manning's roughness
-    mannings_n_sq::Vector{T} | "(s m-1/3)2" | _ | "edge"    # manning's roughness squared
-    volume::Vector{T} | "m3"                                # volume
-    h::Vector{T} | "m"                                      # water depth
-    h_av::Vector{T} | "m"                                   # average water depth
-    error::Vector{T} | "m3"                                 # error volume
-    a::Vector{T} | "m2" | _ | "edge"                        # flow area
-    r::Vector{T} | "m" | _ | "edge"                         # hydraulic radius
-    hf::Vector{T} | "m" | _ | "edge"                        # water depth at edge/link
-    zb_max::Vector{T} | "m" | _ | "edge"                    # maximum bankfull elevation (edge/link)
-    q0::Vector{T} | "m3 s-1" | _ | "edge"                   # discharge at previous time step
-    q::Vector{T} | "m3 s-1" | _ | "edge"                    # discharge
-    q_av::Vector{T} | "m" | _ | "edge"                      # average river discharge
-    hf_index::Vector{Int} | "-" | _ | "edge"                # index with `hf` above depth threshold
+@get_units @grid_loc @with_kw struct FloodPlain{T, P}
+    profile::P                                         # floodplain profile
+    mannings_n::Vector{T} | "s m-1/3"                   # manning's roughness
+    mannings_n_sq::Vector{T} | "(s m-1/3)2" | "edge"    # manning's roughness squared
+    volume::Vector{T} | "m3"                            # volume
+    h::Vector{T} | "m"                                  # water depth
+    h_av::Vector{T} | "m"                               # average water depth
+    error::Vector{T} | "m3"                             # error volume
+    a::Vector{T} | "m2" | "edge"                        # flow area
+    r::Vector{T} | "m" | "edge"                         # hydraulic radius
+    hf::Vector{T} | "m" | "edge"                        # water depth at edge/link
+    zb_max::Vector{T} | "m" | "edge"                    # maximum bankfull elevation (edge/link)
+    q0::Vector{T} | "m3 s-1" | "edge"                   # discharge at previous time step
+    q::Vector{T} | "m3 s-1" | "edge"                    # discharge
+    q_av::Vector{T} | "m" | "edge"                      # average river discharge
+    hf_index::Vector{Int} | "-" | "edge"                # index with `hf` above depth threshold
 end
 
 "Determine the initial floodplain volume"
 function initialize_volume!(river, nriv::Int)
-    for i = 1:nriv
+    for i in 1:nriv
         i1, i2 =
             interpolation_indices(river.floodplain.h[i], river.floodplain.profile.depth)
         a = flow_area(
@@ -1488,7 +1479,6 @@ function initialize_floodplain_1d(
     n_edges,
     nodes_at_link,
 )
-
     n_floodplain = ncread(
         nc,
         config,
@@ -1526,31 +1516,32 @@ function initialize_floodplain_1d(
     incorrect_vol = 0
     riv_cells = 0
     error_vol = 0
-    for i = 1:n
+    for i in 1:n
         riv_cell = 0
         diff_volume = diff(volume[:, i])
 
-        for j = 1:(n_depths-1)
+        for j in 1:(n_depths - 1)
             # assume rectangular shape of flood depth segment
-            width[j+1, i] = diff_volume[j] / (h[j] * riverlength[i])
+            width[j + 1, i] = diff_volume[j] / (h[j] * riverlength[i])
             # check provided flood volume (floodplain width should be constant or increasing
             # as a function of flood depth)
-            if width[j+1, i] < width[j, i]
+            if width[j + 1, i] < width[j, i]
                 # raise warning only if difference is larger than rounding error of 0.01 m³
-                if ((width[j, i] - width[j+1, i]) * h[j] * riverlength[i]) > 0.01
+                if ((width[j, i] - width[j + 1, i]) * h[j] * riverlength[i]) > 0.01
                     incorrect_vol += 1
                     riv_cell = 1
                     error_vol =
-                        error_vol + ((width[j, i] - width[j+1, i]) * h[j] * riverlength[i])
+                        error_vol +
+                        ((width[j, i] - width[j + 1, i]) * h[j] * riverlength[i])
                 end
-                width[j+1, i] = width[j, i]
+                width[j + 1, i] = width[j, i]
             end
-            a[j+1, i] = width[j+1, i] * h[j]
-            p[j+1, i] = (width[j+1, i] - width[j, i]) + 2.0 * h[j]
-            segment_volume[j+1, i] = a[j+1, i] * riverlength[i]
+            a[j + 1, i] = width[j + 1, i] * h[j]
+            p[j + 1, i] = (width[j + 1, i] - width[j, i]) + 2.0 * h[j]
+            segment_volume[j + 1, i] = a[j + 1, i] * riverlength[i]
             if j == 1
                 # for interpolation wetted perimeter at flood depth 0.0 is required
-                p[j, i] = p[j+1, i] - 2.0 * h[j]
+                p[j, i] = p[j + 1, i] - 2.0 * h[j]
             end
         end
 
@@ -1562,8 +1553,8 @@ function initialize_floodplain_1d(
     end
 
     if incorrect_vol > 0
-        perc_riv_cells = round(100.0 * (riv_cells / n), digits = 2)
-        perc_error_vol = round(100.0 * (error_vol / sum(start_volume[end, :])), digits = 2)
+        perc_riv_cells = round(100.0 * (riv_cells / n); digits = 2)
+        perc_error_vol = round(100.0 * (error_vol / sum(start_volume[end, :])); digits = 2)
         @warn string(
             "The provided volume of $incorrect_vol rectangular floodplain schematization",
             " segments for $riv_cells river cells ($perc_riv_cells % of total river cells)",
@@ -1578,7 +1569,7 @@ function initialize_floodplain_1d(
     p = hcat(p, p[:, index_pit])
 
     # initialize floodplain profile parameters
-    profile = FloodPlainProfile{Float,n_depths}(
+    profile = FloodPlainProfile{Float, n_depths}(;
         volume = volume,
         width = width,
         depth = flood_depths,
@@ -1590,7 +1581,7 @@ function initialize_floodplain_1d(
     append!(n_floodplain, n_floodplain[index_pit]) # copy to ghost nodes
     mannings_n_sq = fill(Float(0), n_edges)
     zb_max = fill(Float(0), n_edges)
-    for i = 1:n_edges
+    for i in 1:n_edges
         src_node = nodes_at_link.src[i]
         dst_node = nodes_at_link.dst[i]
         mannings_n =
@@ -1602,7 +1593,7 @@ function initialize_floodplain_1d(
         zb_max[i] = max(zb[src_node], zb[dst_node])
     end
 
-    floodplain = FloodPlain(
+    floodplain = FloodPlain(;
         profile = profile,
         mannings_n = n_floodplain,
         mannings_n_sq = mannings_n_sq,
@@ -1623,16 +1614,14 @@ function initialize_floodplain_1d(
 end
 
 """
-    set_river_inwater(model::Model{N,L,V,R,W,T}, ssf_toriver) where {N,L,V,R,W,T<:Union{SbmModel,SbmGwfModel}}
+    set_river_inwater!(model::Model, ssf_toriver)
 
-Set `inwater` of the lateral river component for a `Model` of type `SbmModel` or `SbmGwfModel`.
-`ssf_toriver` is the subsurface flow to the river.
+Set `inwater` of the lateral river component for a `Model`. `ssf_toriver` is the subsurface
+flow to the river.
 """
-function set_river_inwater(
-    model::Model{N,L,V,R,W,T},
-    ssf_toriver,
-) where {N,L,V,R,W,T<:Union{SbmModel,SbmGwfModel}}
+function set_river_inwater!(model::Model, ssf_toriver)
     (; lateral, vertical, network, config) = model
+    (; net_runoff_river) = vertical.runoff.variables
     inds = network.index_river
     do_water_demand = haskey(config.model, "water_demand")
     if do_water_demand
@@ -1640,86 +1629,73 @@ function set_river_inwater(
             ssf_toriver[inds] +
             lateral.land.to_river[inds] +
             # net_runoff_river
-            (vertical.net_runoff_river[inds] * network.land.area[inds] * 0.001) /
-            vertical.dt +
-            (lateral.river.allocation.nonirri_returnflow * 0.001 * network.river.area) /
-            vertical.dt
+            (net_runoff_river[inds] * network.land.area[inds] * 0.001) / vertical.dt +
+            (
+                lateral.river.allocation.variables.nonirri_returnflow *
+                0.001 *
+                network.river.area
+            ) / vertical.dt
         )
     else
         @. lateral.river.inwater = (
             ssf_toriver[inds] +
             lateral.land.to_river[inds] +
             # net_runoff_river
-            (vertical.net_runoff_river[inds] * network.land.area[inds] * 0.001) /
-            vertical.dt
+            (net_runoff_river[inds] * network.land.area[inds] * 0.001) / vertical.dt
         )
     end
+    return nothing
 end
 
 """
-    set_river_inwater(model, ssf_toriver)
-
-Set `inwater` of the lateral river component (based on overland flow).
-"""
-function set_river_inwater(model, ssf_toriver)
-    (; lateral, network) = model
-    inds = network.index_river
-    lateral.river.inwater .= lateral.land.to_river[inds]
-end
-
-"""
-    set_land_inwater(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmGwfModel}
+    set_land_inwater!(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmGwfModel}
 
 Set `inwater` of the lateral land component for the `SbmGwfModel` type.
 """
-function set_land_inwater(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmGwfModel}
+function set_land_inwater!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: SbmGwfModel}
     (; lateral, vertical, network, config) = model
-    do_water_demand = haskey(config.model, "water_demand")
+    (; net_runoff) = vertical.soil.variables
     do_drains = get(config.model, "drains", false)::Bool
-    drainflux = zeros(vertical.n)
+    drainflux = zeros(length(net_runoff))
+    do_water_demand = haskey(config.model, "water_demand")
     if do_drains
         drainflux[lateral.subsurface.drain.index] =
             -lateral.subsurface.drain.flux ./ tosecond(basetimestep)
     end
     if do_water_demand
         @. lateral.land.inwater =
-            (vertical.net_runoff + vertical.allocation.nonirri_returnflow) *
+            (net_runoff + vertical.allocation.variables.nonirri_returnflow) *
             network.land.area *
             0.001 / lateral.land.dt + drainflux
     else
         @. lateral.land.inwater =
-            (vertical.net_runoff * network.land.area * 0.001) / lateral.land.dt + drainflux
+            (net_runoff * network.land.area * 0.001) / lateral.land.dt + drainflux
     end
+    return nothing
 end
 
 """
-    set_land_inwater(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
+    set_land_inwater!(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
 
 Set `inwater` of the lateral land component for the `SbmModel` type.
 """
-function set_land_inwater(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
+function set_land_inwater!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: SbmModel}
     (; lateral, vertical, network, config) = model
+    (; net_runoff) = vertical.soil.variables
     do_water_demand = haskey(config.model, "water_demand")
     if do_water_demand
         @. lateral.land.inwater =
-            (vertical.net_runoff + vertical.allocation.nonirri_returnflow) *
+            (net_runoff + vertical.allocation.variables.nonirri_returnflow) *
             network.land.area *
             0.001 / lateral.land.dt
     else
-        @. lateral.land.inwater =
-            (vertical.net_runoff * network.land.area * 0.001) / lateral.land.dt
+        @. lateral.land.inwater = (net_runoff * network.land.area * 0.001) / lateral.land.dt
     end
-end
-
-"""
-    set_land_inwater(model)
-
-Set `inwater` of the lateral land component, based on `runoff` of the `vertical` concept.
-"""
-function set_land_inwater(model)
-    (; lateral, vertical, network) = model
-    @. lateral.land.inwater =
-        (vertical.runoff * network.land.area * 0.001) / lateral.land.dt
+    return nothing
 end
 
 # Computation of inflow from the lateral components `land` and `subsurface` to water bodies
@@ -1731,16 +1707,16 @@ end
 # (Darcian flow in 4 directions), the lateral subsurface flow is excluded (for now) and
 # inflow consists of overland flow.
 """
-    set_inflow_waterbody(
+    set_inflow_waterbody!(
         model::Model{N,L,V,R,W,T},
     ) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,SurfaceFlow,SurfaceFlow}},V,R,W,T}
 
 Set inflow from the subsurface and land components to a water body (reservoir or lake)
 `inflow_wb` from a model type that contains the lateral components `SurfaceFlow`.
 """
-function set_inflow_waterbody(
-    model::Model{N,L,V,R,W,T},
-) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,SurfaceFlow,SurfaceFlow}},V,R,W,T}
+function set_inflow_waterbody!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L <: NamedTuple{<:Any, <:Tuple{Any, SurfaceFlow, SurfaceFlow}}, V, R, W, T}
     (; lateral, network) = model
     (; subsurface, land, river) = lateral
     inds = network.index_river
@@ -1753,10 +1729,11 @@ function set_inflow_waterbody(
             river.inflow_wb .= land.q_av[inds]
         end
     end
+    return nothing
 end
 
 """
-    set_inflow_waterbody(
+    set_inflow_waterbody!(
         model::Model{N,L,V,R,W,T},
     ) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,SurfaceFlow,ShallowWaterRiver}},V,R,W,T}
 
@@ -1764,9 +1741,16 @@ Set inflow from the subsurface and land components to a water body (reservoir or
 `inflow_wb` from a model type that contains the lateral components `SurfaceFlow` and
 `ShallowWaterRiver`.
 """
-function set_inflow_waterbody(
-    model::Model{N,L,V,R,W,T},
-) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,SurfaceFlow,ShallowWaterRiver}},V,R,W,T}
+function set_inflow_waterbody!(
+    model::Model{N, L, V, R, W, T},
+) where {
+    N,
+    L <: NamedTuple{<:Any, <:Tuple{Any, SurfaceFlow, ShallowWaterRiver}},
+    V,
+    R,
+    W,
+    T,
+}
     (; lateral, network) = model
     (; subsurface, land, river) = lateral
     inds = network.index_river
@@ -1782,10 +1766,11 @@ function set_inflow_waterbody(
             @. river.inflow_wb = lateral.land.q_av[inds] + lateral.land.to_river[inds]
         end
     end
+    return nothing
 end
 
 """
-    set_inflow_waterbody(
+    set_inflow_waterbody!(
         model::Model{N,L,V,R,W,T},
     ) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,ShallowWaterLand,ShallowWaterRiver}},V,R,W,T}
 
@@ -1793,11 +1778,18 @@ Set inflow from the subsurface and land components to a water body (reservoir or
 `inflow_wb` from a model type that contains the lateral components `ShallowWaterLand` and
 `ShallowWaterRiver`.
 """
-function set_inflow_waterbody(
-    model::Model{N,L,V,R,W,T},
-) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,ShallowWaterLand,ShallowWaterRiver}},V,R,W,T}
+function set_inflow_waterbody!(
+    model::Model{N, L, V, R, W, T},
+) where {
+    N,
+    L <: NamedTuple{<:Any, <:Tuple{Any, ShallowWaterLand, ShallowWaterRiver}},
+    V,
+    R,
+    W,
+    T,
+}
     (; lateral, network) = model
-    (; subsurface, land) = lateral
+    (; subsurface, land, river) = lateral
     inds = network.index_river
 
     if !isnothing(lateral.river.reservoir) || !isnothing(lateral.river.lake)
@@ -1806,25 +1798,27 @@ function set_inflow_waterbody(
                 (subsurface.ssf[inds] + subsurface.to_river[inds]) / tosecond(basetimestep)
         end
     end
+    return nothing
 end
 
 """
-    surface_routing(model; ssf_toriver = 0.0)
+    surface_routing!(model; ssf_toriver = 0.0)
 
 Run surface routing (land and river). Kinematic wave for overland flow and kinematic wave or
 local inertial model for river flow.
 """
-function surface_routing(model; ssf_toriver = 0.0)
+function surface_routing!(model; ssf_toriver = 0.0)
     (; lateral, network, clock) = model
 
     # run kinematic wave for overland flow
-    set_land_inwater(model)
-    update(lateral.land, network.land, network.frac_toriver)
+    set_land_inwater!(model)
+    update!(lateral.land, network.land, network.frac_toriver)
 
     # run river flow
-    set_river_inwater(model, ssf_toriver)
-    set_inflow_waterbody(model)
-    update(lateral.river, network.river, julian_day(clock.time - clock.dt))
+    set_river_inwater!(model, ssf_toriver)
+    set_inflow_waterbody!(model)
+    update!(lateral.river, network.river, julian_day(clock.time - clock.dt))
+    return nothing
 end
 
 """
@@ -1836,19 +1830,28 @@ end
 Run surface routing (land and river) for a model type that contains the lateral components
 `ShallowWaterLand` and `ShallowWaterRiver`.
 """
-function surface_routing(
-    model::Model{N,L,V,R,W,T};
+function surface_routing!(
+    model::Model{N, L, V, R, W, T};
     ssf_toriver = 0.0,
-) where {N,L<:NamedTuple{<:Any,<:Tuple{Any,ShallowWaterLand,ShallowWaterRiver}},V,R,W,T}
-
+) where {
+    N,
+    L <: NamedTuple{<:Any, <:Tuple{Any, ShallowWaterLand, ShallowWaterRiver}},
+    V,
+    R,
+    W,
+    T,
+}
     (; lateral, vertical, network, clock) = model
+    (; net_runoff) = vertical.soil.variables
+    (; net_runoff_river) = vertical.runoff.variables
 
     @. lateral.land.runoff = (
-        (vertical.net_runoff / 1000.0) * network.land.area / vertical.dt +
+        (net_runoff / 1000.0) * (network.land.area) / vertical.dt +
         ssf_toriver +
         # net_runoff_river
-        ((vertical.net_runoff_river * network.land.area * 0.001) / vertical.dt)
+        ((net_runoff_river * network.land.area * 0.001) / vertical.dt)
     )
-    set_inflow_waterbody(model)
-    update(lateral.land, lateral.river, network, julian_day(clock.time - clock.dt))
+    set_inflow_waterbody!(model)
+    update!(lateral.land, lateral.river, network, julian_day(clock.time - clock.dt))
+    return nothing
 end
diff --git a/src/forcing.jl b/src/forcing.jl
new file mode 100644
index 000000000..294aa2908
--- /dev/null
+++ b/src/forcing.jl
@@ -0,0 +1,23 @@
+"Struct to store atmospheric forcing variables"
+@get_units @with_kw struct AtmosphericForcing{T}
+    # Precipitation [mm Δt⁻¹]
+    precipitation::Vector{T}
+    # Potential reference evapotranspiration [mm Δt⁻¹]
+    potential_evaporation::Vector{T}
+    # Temperature [ᵒC]
+    temperature::Vector{T} | "°C"
+end
+
+"Initialize atmospheric forcing"
+function AtmosphericForcing(
+    n;
+    precipitation::Vector{T} = fill(mv, n),
+    potential_evaporation::Vector{T} = fill(mv, n),
+    temperature::Vector{T} = fill(mv, n),
+) where {T}
+    return AtmosphericForcing{T}(;
+        precipitation,
+        potential_evaporation,
+        temperature,
+    )
+end
\ No newline at end of file
diff --git a/src/glacier/glacier.jl b/src/glacier/glacier.jl
new file mode 100644
index 000000000..8f5be9553
--- /dev/null
+++ b/src/glacier/glacier.jl
@@ -0,0 +1,150 @@
+abstract type AbstractGlacierModel{T} end
+
+"Struct for storing glacier model variables"
+@get_units @grid_loc @with_kw struct GlacierVariables{T}
+    # Water within the glacier [mm]
+    glacier_store::Vector{T} | "mm"
+    # Glacier melt [mm Δt⁻¹]  
+    glacier_melt::Vector{T}
+end
+
+"Initialize glacier model variables"
+function GlacierVariables(nc, config, inds)
+    glacier_store = ncread(
+        nc,
+        config,
+        "vertical.glacier.variables.glacier_store";
+        sel = inds,
+        defaults = 5500.0,
+        type = Float,
+        fill = 0.0,
+    )
+    n = length(glacier_store)
+    vars = GlacierVariables(; glacier_store = glacier_store, glacier_melt = fill(mv, n))
+    return vars
+end
+
+"Struct for storing boundary condition (snow storage from a snow model) of a glacier model"
+@get_units @grid_loc @with_kw struct SnowStateBC{T}
+    # Snow storage [mm]
+    snow_storage::Vector{T} | "mm"
+end
+
+"Struct for storing glacier HBV model parameters"
+@get_units @grid_loc @with_kw struct GlacierHbvParameters{T}
+    # Threshold temperature for snowfall above glacier [ᵒC]
+    g_tt::Vector{T} | "ᵒC"
+    # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹] for glacier
+    g_cfmax::Vector{T} | "mm ᵒC-1 dt-1"
+    # Fraction of the snowpack on top of the glacier converted into ice [Δt⁻¹]
+    g_sifrac::Vector{T} | "dt-1"
+    # Fraction covered by a glacier [-]
+    glacier_frac::Vector{T} | "-"
+    # Maximum snow to glacier conversion rate [mm Δt⁻¹]
+    max_snow_to_glacier::T
+end
+
+"Glacier HBV model"
+@with_kw struct GlacierHbvModel{T} <: AbstractGlacierModel{T}
+    boundary_conditions::SnowStateBC{T}
+    parameters::GlacierHbvParameters{T}
+    variables::GlacierVariables{T}
+end
+
+struct NoGlacierModel{T} <: AbstractGlacierModel{T} end
+
+"Initialize glacier HBV model parameters"
+function GlacierHbvParameters(nc, config, inds, dt)
+    g_tt = ncread(
+        nc,
+        config,
+        "vertical.glacier.parameters.g_tt";
+        sel = inds,
+        defaults = 0.0,
+        type = Float,
+        fill = 0.0,
+    )
+    g_cfmax =
+        ncread(
+            nc,
+            config,
+            "vertical.glacier.parameters.g_cfmax";
+            sel = inds,
+            defaults = 3.0,
+            type = Float,
+            fill = 0.0,
+        ) .* (dt / basetimestep)
+    g_sifrac =
+        ncread(
+            nc,
+            config,
+            "vertical.glacier.parameters.g_sifrac";
+            sel = inds,
+            defaults = 0.001,
+            type = Float,
+            fill = 0.0,
+        ) .* (dt / basetimestep)
+    glacier_frac = ncread(
+        nc,
+        config,
+        "vertical.glacier.parameters.glacier_frac";
+        sel = inds,
+        defaults = 0.0,
+        type = Float,
+        fill = 0.0,
+    )
+    max_snow_to_glacier = 8.0 * (dt / basetimestep)
+    glacier_hbv_params = GlacierHbvParameters(;
+        g_tt = g_tt,
+        g_cfmax = g_cfmax,
+        g_sifrac = g_sifrac,
+        glacier_frac = glacier_frac,
+        max_snow_to_glacier = max_snow_to_glacier,
+    )
+    return glacier_hbv_params
+end
+
+"Initialize glacier HBV model"
+function GlacierHbvModel(nc, config, inds, dt, bc)
+    params = GlacierHbvParameters(nc, config, inds, dt)
+    vars = GlacierVariables(nc, config, inds)
+    model =
+        GlacierHbvModel(; boundary_conditions = bc, parameters = params, variables = vars)
+    return model
+end
+
+"Update glacier HBV model for a single timestep"
+function update!(model::GlacierHbvModel, atmospheric_forcing::AtmosphericForcing)
+    (; temperature) = atmospheric_forcing
+    (; glacier_store, glacier_melt) = model.variables
+    (; snow_storage) = model.boundary_conditions
+    (; g_tt, g_cfmax, g_sifrac, glacier_frac, max_snow_to_glacier) = model.parameters
+
+    n = length(temperature)
+
+    threaded_foreach(1:n; basesize = 1000) do i
+        snow_storage[i], _, glacier_store[i], glacier_melt[i] = glacier_hbv(
+            glacier_frac[i],
+            glacier_store[i],
+            snow_storage[i],
+            temperature[i],
+            g_tt[i],
+            g_cfmax[i],
+            g_sifrac[i],
+            max_snow_to_glacier,
+        )
+    end
+    return nothing
+end
+
+function update!(model::NoGlacierModel, atmospheric_forcing::AtmosphericForcing)
+    return nothing
+end
+
+# wrapper methods
+get_glacier_melt(model::NoGlacierModel) = 0.0
+get_glacier_melt(model::AbstractGlacierModel) = model.variables.glacier_melt
+get_glacier_fraction(model::NoGlacierModel) = 0.0
+get_glacier_fraction(model::AbstractGlacierModel) = model.parameters.glacier_frac
+get_glacier_store(model::NoGlacierModel) = 0.0
+get_glacier_store(model::AbstractGlacierModel) = model.variables.glacier_store
\ No newline at end of file
diff --git a/src/glacier/glacier_process.jl b/src/glacier/glacier_process.jl
new file mode 100644
index 000000000..17175b0d4
--- /dev/null
+++ b/src/glacier/glacier_process.jl
@@ -0,0 +1,56 @@
+"""
+    glacier_hbv(glacierfrac, glacierstore, snow_storage, temperature, tt, cfmax, g_sifrac, max_snow_to_glacier)
+
+HBV-light type of glacier modelling.
+First, a fraction of the snowpack is converted into ice using the HBV-light
+model (fraction between 0.001-0.005 per day).
+Glacier melting is modelled using a temperature degree factor and only
+occurs if the snow storage < 10 mm.
+
+# Arguments
+- `glacierFrac` fraction covered by glaciers [-]
+- `glacierstore` volume of the glacier [mm] w.e.
+- `snow_storage` snow storage on top of glacier [mm]
+- `temperature` air temperature [°C]
+- `tt` temperature threshold for ice melting [°C]
+- `cfmax` ice degree-day factor in [mm/(°C/day)]
+- `g_sifrac` fraction of the snow turned into ice [-]
+- `max_snow_to_glacier` maximum snow to glacier conversion rate
+
+# Output
+- `snow`
+- `snow_to_glacier`
+- `glacierstore`
+- `glaciermelt`
+
+"""
+function glacier_hbv(
+    glacierfrac,
+    glacierstore,
+    snow,
+    temperature,
+    tt,
+    cfmax,
+    g_sifrac,
+    max_snow_to_glacier,
+)
+
+    # Fraction of the snow transformed into ice (HBV-light model)
+    snow_to_glacier = g_sifrac * snow
+    snow_to_glacier = glacierfrac > 0.0 ? snow_to_glacier : 0.0
+
+    # Restrict snow_to_glacier conversion
+    snow_to_glacier = min(snow_to_glacier, max_snow_to_glacier)
+
+    snow = snow - (snow_to_glacier * glacierfrac)
+    glacierstore = glacierstore + snow_to_glacier
+
+    # Potential snow melt, based on temperature
+    potmelt = temperature > tt ? cfmax * (temperature - tt) : 0.0
+
+    # actual Glacier melt
+    glaciermelt = snow < 10.0 ? min(potmelt, glacierstore) : 0.0
+    glacierstore = glacierstore - glaciermelt
+
+    return snow, snow_to_glacier, glacierstore, glaciermelt
+end
diff --git a/src/groundwater/aquifer.jl b/src/groundwater/aquifer.jl
index ea03659c7..3a8619bf6 100644
--- a/src/groundwater/aquifer.jl
+++ b/src/groundwater/aquifer.jl
@@ -64,10 +64,8 @@ instead.
 """
 abstract type Aquifer end
 
-
 abstract type AquiferBoundaryCondition end
 
-
 """
     ConfinedAquifer{T} <: Aquifer
 
@@ -86,7 +84,7 @@ NOTA BENE: **specific** storage is per m of aquifer (conf. specific weight).
 **Storativity** or (**storage coefficient**) is for the entire aquifer (conf.
 transmissivity).
 """
-@get_units @exchange @grid_type @grid_location struct ConfinedAquifer{T} <: Aquifer
+@get_units @grid_loc struct ConfinedAquifer{T} <: Aquifer
     head::Vector{T} | "m"  # hydraulic head [m]
     k::Vector{T} | "m d-1"  # horizontal conductivity [m d⁻¹]
     top::Vector{T} | "m" # top of groundwater layer [m]
@@ -98,7 +96,6 @@ transmissivity).
     volume::Vector{T} | "m3" # total volume of water that can be released
 end
 
-
 """
     UnconfinedAquifer{T} <: Aquifer
 
@@ -110,7 +107,7 @@ aquifer will yield when all water drains and the pore volume is filled by air
 instead. Specific yield will vary roughly between 0.05 (clay) and 0.45 (peat)
 (Johnson, 1967).
 """
-@get_units @exchange @grid_type @grid_location struct UnconfinedAquifer{T} <: Aquifer
+@get_units @grid_loc struct UnconfinedAquifer{T} <: Aquifer
     head::Vector{T} | "m"  # hydraulic head [m]
     k::Vector{T} | "m d-1"  # reference horizontal conductivity [m d⁻¹]
     top::Vector{T} | "m" # top of groundwater layer [m]
@@ -124,11 +121,9 @@ instead. Specific yield will vary roughly between 0.05 (clay) and 0.45 (peat)
     # conductivity_profile is set to "exponential")
 end
 
-
 storativity(A::UnconfinedAquifer) = A.specific_yield
 storativity(A::ConfinedAquifer) = A.storativity
 
-
 """
     harmonicmean_conductance(kH1, kH2, l1, l2, width)
 
@@ -152,12 +147,11 @@ function harmonicmean_conductance(kH1, kH2, l1, l2, width)
 end
 
 function saturated_thickness(aquifer::UnconfinedAquifer, index::Int)
-    min(aquifer.top[index], aquifer.head[index]) - aquifer.bottom[index]
+    return min(aquifer.top[index], aquifer.head[index]) - aquifer.bottom[index]
 end
 
-
 function saturated_thickness(aquifer::ConfinedAquifer, index::Int)
-    aquifer.top[index] - aquifer.bottom[index]
+    return aquifer.top[index] - aquifer.bottom[index]
 end
 
 function saturated_thickness(aquifer::UnconfinedAquifer)
@@ -182,7 +176,7 @@ function horizontal_conductance(
     nzi::Int,
     aquifer::A,
     connectivity::Connectivity,
-) where {A<:Aquifer}
+) where {A <: Aquifer}
     k1 = aquifer.k[i]
     k2 = aquifer.k[j]
     H1 = aquifer.top[i] - aquifer.bottom[i]
@@ -202,8 +196,11 @@ Conductance for a confined aquifer is constant, and only has to be set once.
 For an unconfined aquifer, conductance is computed per timestep by multiplying by
 degree of saturation [0.0 - 1.0].
 """
-function initialize_conductance!(aquifer::A, connectivity::Connectivity) where {A<:Aquifer}
-    for i = 1:connectivity.ncell
+function initialize_conductance!(
+    aquifer::A,
+    connectivity::Connectivity,
+) where {A <: Aquifer}
+    for i in 1:(connectivity.ncell)
         # Loop over connections for cell j
         for nzi in connections(connectivity, i)
             j = connectivity.rowval[nzi]
@@ -213,7 +210,6 @@ function initialize_conductance!(aquifer::A, connectivity::Connectivity) where {
     end
 end
 
-
 function conductance(
     aquifer::ConfinedAquifer,
     i,
@@ -225,7 +221,6 @@ function conductance(
     return aquifer.conductance[nzi]
 end
 
-
 """
     conductance(aquifer::UnconfinedAquifer, connectivity::Connectivity)
 
@@ -260,7 +255,6 @@ function conductance(
     conductivity_profile::String,
     connectivity::Connectivity,
 )
-
     if conductivity_profile == "exponential"
         # Extract required variables
         zi1 = aquifer.top[i] - aquifer.head[i]
@@ -302,7 +296,7 @@ function conductance(
 end
 
 function flux!(Q, aquifer, connectivity, conductivity_profile)
-    for i = 1:connectivity.ncell
+    for i in 1:(connectivity.ncell)
         # Loop over connections for cell j
         for nzi in connections(connectivity, i)
             # connection from i -> j
@@ -315,13 +309,11 @@ function flux!(Q, aquifer, connectivity, conductivity_profile)
     return Q
 end
 
-
-@get_units @exchange @grid_type @grid_location struct ConstantHead{T}
+@get_units @grid_loc struct ConstantHead{T}
     head::Vector{T} | "m"
     index::Vector{Int} | "-"
 end
 
-
 """
     stable_timestep(aquifer)
 
@@ -352,8 +344,7 @@ end
 minimum_head(aquifer::ConfinedAquifer) = aquifer.head
 minimum_head(aquifer::UnconfinedAquifer) = max.(aquifer.head, aquifer.bottom)
 
-
-function update(gwf, Q, dt, conductivity_profile)
+function update!(gwf, Q, dt, conductivity_profile)
     Q .= 0.0  # TODO: Probably remove this when linking with other components
     flux!(Q, gwf.aquifer, gwf.connectivity, conductivity_profile)
     for boundary in gwf.boundaries
@@ -366,22 +357,42 @@ function update(gwf, Q, dt, conductivity_profile)
     gwf.aquifer.head .= minimum_head(gwf.aquifer)
     gwf.aquifer.volume .=
         saturated_thickness(gwf.aquifer) .* gwf.aquifer.area .* storativity(gwf.aquifer)
-    return gwf
+    return nothing
 end
 
-
-Base.@kwdef struct GroundwaterFlow{A,B}
+Base.@kwdef struct GroundwaterFlow{A, C, CH, B}
     aquifer::A
-    connectivity::Connectivity
-    constanthead::ConstantHead
-    boundaries::Vector{B}
-    function GroundwaterFlow(
-        aquifer::A,
-        connectivity,
-        constanthead,
-        boundaries::Vector{B},
-    ) where {A<:Aquifer,B<:AquiferBoundaryCondition}
-        initialize_conductance!(aquifer, connectivity)
-        new{A,B}(aquifer, connectivity, constanthead, boundaries)
-    end
+    connectivity::C
+    constanthead::CH
+    boundaries::B
 end
+
+function GroundwaterFlow{T}(;
+    aquifer::Aquifer,
+    connectivity::Connectivity{T},
+    constanthead::ConstantHead{T},
+    boundaries::Vector{AquiferBoundaryCondition},
+) where {T}
+    initialize_conductance!(aquifer, connectivity)
+    args = (aquifer, connectivity, constanthead, boundaries)
+    GroundwaterFlow{typeof.(args)...}(args...)
+end
+
+function get_water_depth(
+    gwf::GroundwaterFlow{A, C, CH, B},
+) where {A <: UnconfinedAquifer, C, CH, B}
+    gwf.aquifer.head .= min.(gwf.aquifer.head, gwf.aquifer.top)
+    gwf.aquifer.head[gwf.constanthead.index] .= gwf.constanthead.head
+    wtd = gwf.aquifer.top .- gwf.aquifer.head
+    return wtd
+end
+
+function get_exfiltwater(
+    gwf::GroundwaterFlow{A, C, CH, B},
+) where {A <: UnconfinedAquifer, C, CH, B}
+    exfiltwater =
+        (gwf.aquifer.head .- min.(gwf.aquifer.head, gwf.aquifer.top)) .*
+        storativity(gwf.aquifer)
+    exfiltwater[gwf.constanthead.index] .= 0
+    return exfiltwater
+end
\ No newline at end of file
diff --git a/src/groundwater/boundary_conditions.jl b/src/groundwater/boundary_conditions.jl
index 06a04897e..48e883527 100644
--- a/src/groundwater/boundary_conditions.jl
+++ b/src/groundwater/boundary_conditions.jl
@@ -8,12 +8,10 @@ function check_flux(flux, aquifer::UnconfinedAquifer, index::Int)
     end
 end
 
-
 # Do nothing for a confined aquifer: aquifer can always provide flux
 check_flux(flux, aquifer::ConfinedAquifer, index::Int) = flux
 
-
-@get_units @exchange @grid_type @grid_location struct River{T} <: AquiferBoundaryCondition
+@get_units @grid_loc struct River{T} <: AquiferBoundaryCondition
     stage::Vector{T} | "m"
     infiltration_conductance::Vector{T} | "m2 d-1"
     exfiltration_conductance::Vector{T} | "m2 d-1"
@@ -22,7 +20,6 @@ check_flux(flux, aquifer::ConfinedAquifer, index::Int) = flux
     index::Vector{Int} | "-"
 end
 
-
 function flux!(Q, river::River, aquifer)
     for (i, index) in enumerate(river.index)
         head = aquifer.head[index]
@@ -40,16 +37,13 @@ function flux!(Q, river::River, aquifer)
     return Q
 end
 
-
-@get_units @exchange @grid_type @grid_location struct Drainage{T} <:
-                                                      AquiferBoundaryCondition
+@get_units @grid_loc struct Drainage{T} <: AquiferBoundaryCondition
     elevation::Vector{T} | "m"
     conductance::Vector{T} | "m2 d-1"
     flux::Vector{T} | "m3 d-1"
     index::Vector{Int} | "-"
 end
 
-
 function flux!(Q, drainage::Drainage, aquifer)
     for (i, index) in enumerate(drainage.index)
         cond = drainage.conductance[i]
@@ -60,15 +54,13 @@ function flux!(Q, drainage::Drainage, aquifer)
     return Q
 end
 
-
-@get_units struct HeadBoundary{T} <: AquiferBoundaryCondition
+@get_units @grid_loc struct HeadBoundary{T} <: AquiferBoundaryCondition
     head::Vector{T} | "m"
     conductance::Vector{T} | "m2 d-1"
     flux::Vector{T} | "m3 d-1"
     index::Vector{Int} | "-"
 end
 
-
 function flux!(Q, headboundary::HeadBoundary, aquifer)
     for (i, index) in enumerate(headboundary.index)
         cond = headboundary.conductance[i]
@@ -79,15 +71,12 @@ function flux!(Q, headboundary::HeadBoundary, aquifer)
     return Q
 end
 
-
-@get_units @exchange @grid_type @grid_location struct Recharge{T} <:
-                                                      AquiferBoundaryCondition
+@get_units @grid_loc struct Recharge{T} <: AquiferBoundaryCondition
     rate::Vector{T} | "m d-1"
     flux::Vector{T} | "m3 d-1"
     index::Vector{Int} | "-"
 end
 
-
 function flux!(Q, recharge::Recharge, aquifer)
     for (i, index) in enumerate(recharge.index)
         recharge.flux[i] =
@@ -97,14 +86,12 @@ function flux!(Q, recharge::Recharge, aquifer)
     return Q
 end
 
-
-@get_units struct Well{T} <: AquiferBoundaryCondition
+@get_units @grid_loc struct Well{T} <: AquiferBoundaryCondition
     volumetric_rate::Vector{T} | "m3 d-1"
     flux::Vector{T} | "m3 d-1"
     index::Vector{Int} | "-"
 end
 
-
 function flux!(Q, well::Well, aquifer)
     for (i, index) in enumerate(well.index)
         well.flux[i] = check_flux(well.volumetric_rate[i], aquifer, index)
diff --git a/src/groundwater/connectivity.jl b/src/groundwater/connectivity.jl
index 90a9bf6f2..538aac097 100644
--- a/src/groundwater/connectivity.jl
+++ b/src/groundwater/connectivity.jl
@@ -29,14 +29,12 @@ struct Connectivity{T}
     rowval::Vector{Int}
 end
 
-
 """
     connections(C::Connectivity, id::Int)
 
 Returns connections for a single cell, identified by ``id``.
 """
-connections(C::Connectivity, id::Int) = C.colptr[id]:(C.colptr[id+1]-1)
-
+connections(C::Connectivity, id::Int) = C.colptr[id]:(C.colptr[id + 1] - 1)
 
 """
     connection_geometry(I, J, dx, dy)
@@ -59,7 +57,6 @@ function connection_geometry(I, J, dx, dy)
     return (length1, length2, width)
 end
 
-
 # Define cartesian indices for neighbors
 const neighbors = (
     CartesianIndex(0, -1),
diff --git a/src/hbv.jl b/src/hbv.jl
deleted file mode 100644
index 038514622..000000000
--- a/src/hbv.jl
+++ /dev/null
@@ -1,239 +0,0 @@
-@get_units @exchange @grid_type @grid_location @with_kw struct HBV{T}
-    dt::T | "s" | 0 | "none" | "none"   # Model time step [s]
-    n::Int | "-" | 0 | "none" | "none"  # Number of cells
-    fc::Vector{T} | "mm"                # Field capacity [mm]
-    betaseepage::Vector{T} | "-"        # Exponent in soil runoff generation equation [-]
-    lp::Vector{T} | "-"                 # Fraction of field capacity below which actual evaporation=potential evaporation [-]
-    threshold::Vector{T} | "mm"         # Threshold soilwater storage above which AE=PE [mm]
-    k4::Vector{T} | "dt-1"              # Recession constant baseflow [Δt⁻¹]
-    kquickflow::Vector{T} | "dt-1"      # Recession constant upper reservoir [Δt⁻¹]
-    suz::Vector{T} | "mm"               # Level over which k0 is used [mm]
-    k0::Vector{T} | "dt-1"              # Recession constant upper reservoir [Δt⁻¹]
-    khq::Vector{T} | "dt-1"             # Recession rate at flow hq [Δt⁻¹]
-    hq::Vector{T}                       # High flow rate hq for which recession rate of upper reservoir is known [mm Δt⁻¹]
-    alphanl::Vector{T}                  # Measure of non-linearity of upper reservoir
-    perc::Vector{T}                     # Percolation from upper to lower zone [mm Δt⁻¹]
-    cfr::Vector{T} | "-"                # Refreezing efficiency constant in refreezing of freewater in snow [-]
-    pcorr::Vector{T} | "-"              # Correction factor for precipitation [-]
-    rfcf::Vector{T} | "-"               # Correction factor for rainfall [-]
-    sfcf::Vector{T} | "-"               # Correction factor for snowfall [-]
-    cflux::Vector{T}                    # Maximum capillary rise from runoff response routine to soil moisture routine [mm Δt⁻¹]
-    icf::Vector{T} | "mm"               # Maximum interception storage (in forested and non-forested areas) [mm]
-    cevpf::Vector{T} | "-"              # Correction factor for potential evaporation [-]
-    epf::Vector{T} | "mm-1"             # Exponent of correction factor for evaporation on days with precipitation
-    ecorr::Vector{T} | "-"              # Evap correction [-]
-    tti::Vector{T} | "ᵒC"               # Critical temperature for snowmelt and refreezing [ᵒC]
-    tt::Vector{T} | "ᵒC"                # Defines interval in which precipitation falls as rainfall and snowfall [ᵒC]
-    ttm::Vector{T} | "ᵒC"               # Threshold temperature for snowmelt [ᵒC]
-    cfmax::Vector{T} | "mm ᵒC-1 dt-1"   # Meltconstant in temperature-index [-]
-    whc::Vector{T} | "-"                # Fraction of snow volume that can store water [-]
-    g_tt::Vector{T} | "ᵒC"              # Threshold temperature for snowfall above glacier [ᵒC]
-    g_cfmax::Vector{T} | "mm ᵒC-1 dt-1" # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹] for glacier
-    g_sifrac::Vector{T} | "dt-1"        # Fraction of the snowpack on top of the glacier converted into ice [Δt⁻¹]
-    glacierstore::Vector{T} | "mm"      # Water within the glacier [mm]
-    glacierfrac::Vector{T} | "-"        # Fraction covered by a glacier [-]
-    precipitation::Vector{T}            # Precipitation [mm Δt⁻¹]
-    temperature::Vector{T} | "ᵒC"       # Temperature [ᵒC]
-    potential_evaporation::Vector{T}    # Potential evapotranspiration [mm Δt⁻¹]
-    potsoilevap::Vector{T}              # Potential soil evaporation [mm Δt⁻¹]
-    soilevap::Vector{T}                 # Soil evaporation [mm Δt⁻¹]
-    intevap::Vector{T}                  # Evaporation from interception storage [mm Δt⁻¹]
-    actevap::Vector{T}                  # Total actual evapotranspiration (intevap + soilevap) [mm Δt⁻¹]
-    interceptionstorage::Vector{T} | "mm"  # Actual interception storage [mm]
-    snowwater::Vector{T} | "mm"            # Available free water in snow [mm]
-    snow::Vector{T} | "mm"                 # Snow pack [mm]
-    rainfallplusmelt::Vector{T}            # Snow melt + precipitation as rainfall [mm Δt⁻¹]
-    soilmoisture::Vector{T} | "mm"         # Actual soil moisture [mm]
-    directrunoff::Vector{T}             # Direct runoff to upper zone [mm Δt⁻¹]
-    hbv_seepage::Vector{T}              # Recharge to upper zone [mm Δt⁻¹]
-    in_upperzone::Vector{T}             # Water inflow into upper zone [mm Δt⁻¹]
-    upperzonestorage::Vector{T} | "mm"  # Water content of the upper zone [mm]
-    quickflow::Vector{T}                # Specific runoff (quickflow part) [mm Δt⁻¹]
-    real_quickflow::Vector{T}           # Specific runoff (quickflow), if K upper zone is precalculated [mm Δt⁻¹]
-    percolation::Vector{T}              # Actual percolation to the lower zone [mm Δt⁻¹]
-    capflux::Vector{T}                  # Capillary rise [mm Δt⁻¹]
-    lowerzonestorage::Vector{T} | "mm"  # Water content of the lower zone [mm]
-    baseflow::Vector{T}                 # Specific runoff (baseflow part) per cell [mm Δt⁻¹]
-    runoff::Vector{T}                   # Total specific runoff per cell [mm Δt⁻¹]
-end
-
-
-function update_until_snow(hbv::HBV, config)
-
-    for i = 1:hbv.n
-        precipitation = hbv.precipitation[i] * hbv.pcorr[i]
-        # fraction of precipitation which falls as rain
-        rainfrac = if iszero(hbv.tti[i])
-            Float64(hbv.temperature[i] > hbv.tt[i])
-        else
-            frac = (hbv.temperature[i] - (hbv.tt[i] - hbv.tti[i] / 2.0)) / hbv.tti[i]
-            min(frac, 1.0)
-        end
-        rainfrac = max(rainfrac, 0.0)
-
-        # fraction of precipitation which falls as snow
-        snowfrac = 1.0 - rainfrac
-        # different correction for rainfall and snowfall
-        precipitation =
-            hbv.sfcf[i] * snowfrac * precipitation + hbv.rfcf[i] * rainfrac * precipitation
-
-        # interception
-        interception = min(precipitation, hbv.icf[i] - hbv.interceptionstorage[i])
-        # current interception storage
-        interceptionstorage = hbv.interceptionstorage[i] + interception
-        precipitation = precipitation - interception
-
-        # correction for potential evaporation on wet days
-        potevap =
-            exp(-hbv.epf[i] * precipitation) * hbv.ecorr[i] * hbv.potential_evaporation[i]
-        # correct per landuse
-        potevap = hbv.cevpf[i] * potevap
-
-        # evaporation from interception storage
-        intevap = min(interceptionstorage, potevap)
-        interceptionstorage = interceptionstorage - intevap
-        restevap = max(0.0, potevap - intevap)
-
-        snow, snowwater, snowmelt, rainfallplusmelt, snowfall = snowpack_hbv(
-            hbv.snow[i],
-            hbv.snowwater[i],
-            precipitation,
-            hbv.temperature[i],
-            hbv.tti[i],
-            hbv.tt[i],
-            hbv.ttm[i],
-            hbv.cfmax[i],
-            hbv.whc[i],
-        )
-
-        # update the outputs and states
-        hbv.rainfallplusmelt[i] = rainfallplusmelt
-        hbv.snowwater[i] = snowwater
-        hbv.snow[i] = snow
-        hbv.interceptionstorage[i] = interceptionstorage
-        hbv.potsoilevap[i] = restevap
-        hbv.intevap[i] = intevap
-    end
-end
-
-function update_after_snow(hbv::HBV, config)
-
-    modelglacier = get(config.model, "glacier", false)::Bool
-    set_kquickflow = get(config.model, "set_kquickflow", false)::Bool
-    external_qbase = get(config.model, "external_qbase", false)::Bool
-
-    for i = 1:hbv.n
-        if modelglacier
-            # Run Glacier module and add the snowpack on-top of it.
-            # Estimate the fraction of snow turned into ice (HBV-light).
-            # Estimate glacier melt.
-
-            hbv.snow[i], _, hbv.glacierstore[i], glaciermelt = glacier_hbv(
-                hbv.glacierfrac[i],
-                hbv.glacierstore[i],
-                hbv.snow[i],
-                hbv.temperature[i],
-                hbv.g_tt[i],
-                hbv.g_cfmax[i],
-                hbv.g_sifrac[i],
-                Second(hbv.dt),
-            )
-            # Convert to mm per grid cell and add to snowmelt
-            glaciermelt = glaciermelt * hbv.glacierfrac[i]
-            rainfallplusmelt = hbv.rainfallplusmelt[i] + glaciermelt
-        else
-            rainfallplusmelt = hbv.rainfallplusmelt[i]
-        end
-
-        soilmoisture = hbv.soilmoisture[i] + rainfallplusmelt
-        # if soil is filled to capacity: abundant water runs of directly
-        directrunoff = max(soilmoisture - hbv.fc[i], 0.0)
-        soilmoisture = soilmoisture - directrunoff
-        # net water which infiltrates into soil
-        netinsoil = rainfallplusmelt - directrunoff
-
-        # soil evapotranspiration
-        soilevap =
-            soilmoisture > hbv.threshold[i] ? min(soilmoisture, hbv.potsoilevap[i]) :
-            min(hbv.potsoilevap[i] * (soilmoisture / hbv.threshold[i]))
-        # evaporation from soil moisture storage
-        soilmoisture = soilmoisture - soilevap
-        # sum of evaporation components (IntEvap+SoilEvap)
-        actevap = hbv.intevap[i] + soilevap
-        # runoff water from soil
-        hbv_seepage =
-            pow(min(soilmoisture / hbv.fc[i], 1.0), hbv.betaseepage[i]) * netinsoil
-        soilmoisture = soilmoisture - hbv_seepage
-        # correction for extremely wet periods: soil is filled to capacity
-        back_tosoil = min(hbv.fc[i] - soilmoisture, directrunoff)
-        directrunoff = directrunoff - back_tosoil
-        soilmoisture = soilmoisture + back_tosoil
-        # total water available for runoff
-        in_upperzone = directrunoff + hbv_seepage
-        ### calculations for upper zone ###
-        upperzonestorage = hbv.upperzonestorage[i] + in_upperzone
-        percolation = min(hbv.perc[i], upperzonestorage - in_upperzone / 2.0)
-        upperzonestorage = upperzonestorage - percolation
-        # capillary flux flowing back to soil
-        capflux = hbv.cflux[i] * ((hbv.fc[i] - soilmoisture) / hbv.fc[i])
-        capflux = min(hbv.fc[i] - soilmoisture, capflux)
-        upperzonestorage = upperzonestorage - capflux
-        soilmoisture = soilmoisture + capflux
-
-        real_quickflow = 0.0
-        if set_kquickflow == false
-
-            if percolation < hbv.perc[i]
-                quickflow = 0.0
-            else
-                quickflow = min(
-                    hbv.kquickflow[i] * pow(
-                        (upperzonestorage - min(in_upperzone / 2.0, upperzonestorage)),
-                        1.0 + hbv.alphanl[i],
-                    ),
-                    upperzonestorage,
-                )
-
-                upperzonestorage =
-                    percolation < hbv.perc[i] ? upperzonestorage :
-                    max(upperzonestorage - quickflow, 0.0)
-            end
-        else
-            quickflow = hbv.kquickflow[i] * upperzonestorage
-            real_quickflow = max(0.0, hbv.k0[i] * (upperzonestorage - hbv.suz[i]))
-            upperzonestorage = upperzonestorage - quickflow - real_quickflow
-
-        end
-
-        ### calculations for lower zone ###
-        lowerzonestorage = hbv.lowerzonestorage[i] + percolation
-        # baseflow in mm/timestep
-        baseflow = min(lowerzonestorage, hbv.k4[i] * lowerzonestorage)
-        lowerzonestorage = lowerzonestorage - baseflow
-
-        if external_qbase
-            directrunoffstorage = quickflow + hbv_seepage + real_quickflow
-        else
-            directrunoffstorage = quickflow + baseflow + real_quickflow
-        end
-
-        runoff = max(0.0, directrunoffstorage)
-
-        # update the outputs and states
-        hbv.runoff[i] = runoff
-        hbv.rainfallplusmelt[i] = rainfallplusmelt
-        hbv.soilmoisture[i] = soilmoisture
-        hbv.soilevap[i] = soilevap
-        hbv.actevap[i] = actevap
-        hbv.quickflow[i] = quickflow
-        hbv.real_quickflow[i] = real_quickflow
-        hbv.upperzonestorage[i] = upperzonestorage
-        hbv.lowerzonestorage[i] = lowerzonestorage
-        hbv.baseflow[i] = baseflow
-        hbv.hbv_seepage[i] = hbv_seepage
-        hbv.percolation[i] = percolation
-        hbv.capflux[i] = capflux
-        hbv.in_upperzone[i] = in_upperzone
-    end
-
-end
diff --git a/src/hbv_model.jl b/src/hbv_model.jl
deleted file mode 100644
index 7c63bc252..000000000
--- a/src/hbv_model.jl
+++ /dev/null
@@ -1,446 +0,0 @@
-"""
-    initialize_hbv_model(config::Config)
-
-Initial part of the SBM model concept. Reads the input settings and data as defined in the
-Config object. Will return a Model that is ready to run.
-"""
-function initialize_hbv_model(config::Config)
-    # unpack the paths to the netCDF files
-    static_path = input_path(config, config.input.path_static)
-
-    reader = prepare_reader(config)
-    clock = Clock(config, reader)
-    dt = clock.dt
-
-    do_reservoirs = get(config.model, "reservoirs", false)::Bool
-    do_lakes = get(config.model, "lakes", false)::Bool
-    do_pits = get(config.model, "pits", false)::Bool
-    set_kquickflow = get(config.model, "set_kquickflow", false)::Bool
-
-    kw_river_tstep = get(config.model, "kw_river_tstep", 0)
-    kw_land_tstep = get(config.model, "kw_land_tstep", 0)
-    kinwave_it = get(config.model, "kin_wave_iteration", false)::Bool
-
-    nc = NCDataset(static_path)
-
-    subcatch_2d = ncread(nc, config, "subcatchment"; optional = false, allow_missing = true)
-    # indices based on catchment
-    inds, rev_inds = active_indices(subcatch_2d, missing)
-    n = length(inds)
-
-    cfmax =
-        ncread(
-            nc,
-            config,
-            "vertical.cfmax";
-            sel = inds,
-            defaults = 3.75653,
-            type = Float,
-        ) .* (dt / basetimestep)
-    tt = ncread(nc, config, "vertical.tt"; sel = inds, defaults = -1.41934, type = Float)
-    tti = ncread(nc, config, "vertical.tti"; sel = inds, defaults = 1.0, type = Float)
-    ttm = ncread(nc, config, "vertical.ttm"; sel = inds, defaults = -1.41934, type = Float)
-    whc = ncread(nc, config, "vertical.whc"; sel = inds, defaults = 0.1, type = Float)
-    # glacier parameters
-    g_tt = ncread(
-        nc,
-        config,
-        "vertical.g_tt";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        fill = 0.0,
-    )
-    g_cfmax =
-        ncread(
-            nc,
-            config,
-            "vertical.g_cfmax";
-            sel = inds,
-            defaults = 3.0,
-            type = Float,
-            fill = 0.0,
-        ) .* (dt / basetimestep)
-    g_sifrac =
-        ncread(
-            nc,
-            config,
-            "vertical.g_sifrac";
-            sel = inds,
-            defaults = 0.001,
-            type = Float,
-            fill = 0.0,
-        ) .* (dt / basetimestep)
-    glacierfrac = ncread(
-        nc,
-        config,
-        "vertical.glacierfrac";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        fill = 0.0,
-    )
-    glacierstore = ncread(
-        nc,
-        config,
-        "vertical.glacierstore";
-        sel = inds,
-        defaults = 5500.0,
-        type = Float,
-        fill = 0.0,
-    )
-    fc = ncread(nc, config, "vertical.fc"; sel = inds, defaults = 260.0, type = Float)
-    betaseepage =
-        ncread(nc, config, "vertical.betaseepage"; sel = inds, defaults = 1.8, type = Float)
-    lp = ncread(nc, config, "vertical.lp"; sel = inds, defaults = 0.53, type = Float)
-    k4 =
-        ncread(nc, config, "vertical.k4"; sel = inds, defaults = 0.02307, type = Float) .*
-        (dt / basetimestep)
-    kquickflow =
-        ncread(
-            nc,
-            config,
-            "vertical.kquickflow";
-            sel = inds,
-            defaults = 0.09880,
-            type = Float,
-        ) .* (dt / basetimestep)
-    suz = ncread(nc, config, "vertical.suz"; sel = inds, defaults = 100.0, type = Float)
-    k0 =
-        ncread(nc, config, "vertical.k0"; sel = inds, defaults = 0.30, type = Float) .*
-        (dt / basetimestep)
-    khq =
-        ncread(nc, config, "vertical.khq"; sel = inds, defaults = 0.09880, type = Float) .*
-        (dt / basetimestep)
-    hq =
-        ncread(nc, config, "vertical.hq"; sel = inds, defaults = 3.27, type = Float) .*
-        (dt / basetimestep)
-    alphanl =
-        ncread(nc, config, "vertical.alphanl"; sel = inds, defaults = 1.1, type = Float)
-    perc =
-        ncread(nc, config, "vertical.perc"; sel = inds, defaults = 0.4, type = Float) .*
-        (dt / basetimestep)
-    cfr = ncread(nc, config, "vertical.cfr"; sel = inds, defaults = 0.05, type = Float)
-    pcorr = ncread(nc, config, "vertical.pcorr"; sel = inds, defaults = 1.0, type = Float)
-    rfcf = ncread(nc, config, "vertical.rfcf"; sel = inds, defaults = 1.0, type = Float)
-    sfcf = ncread(nc, config, "vertical.sfcf"; sel = inds, defaults = 1.0, type = Float)
-    cflux =
-        ncread(nc, config, "vertical.cflux"; sel = inds, defaults = 2.0, type = Float) .*
-        (dt / basetimestep)
-    icf = ncread(nc, config, "vertical.icf"; sel = inds, defaults = 2.0, type = Float)
-    cevpf = ncread(nc, config, "vertical.cevpf"; sel = inds, defaults = 1.0, type = Float)
-    epf = ncread(nc, config, "vertical.epf"; sel = inds, defaults = 1.0, type = Float)
-    ecorr = ncread(nc, config, "vertical.ecorr"; sel = inds, defaults = 1.0, type = Float)
-
-    # read x, y coordinates and calculate cell length [m]
-    y_nc = read_y_axis(nc)
-    x_nc = read_x_axis(nc)
-    y = permutedims(repeat(y_nc, outer = (1, length(x_nc))))[inds]
-    cellength = abs(mean(diff(x_nc)))
-
-
-    sizeinmetres = get(config.model, "sizeinmetres", false)::Bool
-    xl, yl = cell_lengths(y, cellength, sizeinmetres)
-
-    threshold = fc .* lp
-
-    hbv = HBV{Float}(
-        dt = Float(tosecond(dt)),
-        n = n,
-        fc = fc,
-        betaseepage = betaseepage,
-        lp = lp,
-        threshold = threshold,
-        k4 = k4,
-        kquickflow = set_kquickflow ? kquickflow :
-                     pow.(khq, 1.0 .+ alphanl) .* pow.(hq, -alphanl),
-        suz = suz,
-        k0 = k0,
-        khq = khq,
-        hq = hq,
-        alphanl = alphanl,
-        perc = perc,
-        cfr = cfr,
-        pcorr = pcorr,
-        rfcf = rfcf,
-        sfcf = sfcf,
-        cflux = cflux,
-        icf = icf,
-        cevpf = cevpf,
-        epf = epf,
-        ecorr = ecorr,
-        tti = tti,
-        tt = tt,
-        ttm = ttm,
-        cfmax = cfmax,
-        whc = whc,
-        # glacier parameters
-        g_tt = g_tt,
-        g_sifrac = g_sifrac,
-        g_cfmax = g_cfmax,
-        glacierstore = glacierstore,
-        glacierfrac = glacierfrac,
-        # default (cold) states:
-        interceptionstorage = zeros(Float, n),
-        snow = zeros(Float, n),
-        snowwater = zeros(Float, n),
-        soilmoisture = copy(fc),
-        upperzonestorage = 0.2 .* fc,
-        lowerzonestorage = 1.0 ./ (3.0 .* k4),
-        # variables:
-        precipitation = fill(mv, n),
-        temperature = fill(mv, n),
-        potential_evaporation = fill(mv, n),
-        potsoilevap = fill(mv, n),
-        soilevap = fill(mv, n),
-        intevap = fill(mv, n),
-        actevap = fill(mv, n),
-        rainfallplusmelt = fill(mv, n),
-        directrunoff = fill(mv, n),
-        hbv_seepage = fill(mv, n),
-        in_upperzone = fill(mv, n),
-        quickflow = fill(mv, n),
-        real_quickflow = fill(mv, n),
-        percolation = fill(mv, n),
-        capflux = fill(mv, n),
-        baseflow = fill(mv, n),
-        runoff = fill(mv, n),
-    )
-
-    modelsize_2d = size(subcatch_2d)
-    river_2d =
-        ncread(nc, config, "river_location"; optional = false, type = Bool, fill = false)
-    river = river_2d[inds]
-    riverwidth_2d =
-        ncread(nc, config, "lateral.river.width"; optional = false, type = Float, fill = 0)
-    riverwidth = riverwidth_2d[inds]
-    riverlength_2d =
-        ncread(nc, config, "lateral.river.length"; optional = false, type = Float, fill = 0)
-    riverlength = riverlength_2d[inds]
-
-    inds_riv, rev_inds_riv = active_indices(river_2d, 0)
-    nriv = length(inds_riv)
-
-    # reservoirs
-    pits = zeros(Bool, modelsize_2d)
-    if do_reservoirs
-        reservoirs, resindex, reservoir, pits =
-            initialize_simple_reservoir(config, nc, inds_riv, nriv, pits, tosecond(dt))
-    else
-        reservoir = ()
-        reservoirs = nothing
-        resindex = fill(0, nriv)
-    end
-
-    # lakes
-    if do_lakes
-        lakes, lakeindex, lake, pits =
-            initialize_lake(config, nc, inds_riv, nriv, pits, tosecond(dt))
-    else
-        lake = ()
-        lakes = nothing
-        lakeindex = fill(0, nriv)
-    end
-
-    ldd_2d = ncread(nc, config, "ldd"; optional = false, allow_missing = true)
-    ldd = ldd_2d[inds]
-    if do_pits
-        pits_2d = ncread(nc, config, "pits"; optional = false, type = Bool, fill = false)
-        ldd = set_pit_ldd(pits_2d, ldd, inds)
-    end
-
-    landslope =
-        ncread(nc, config, "lateral.land.slope"; optional = false, sel = inds, type = Float)
-    clamp!(landslope, 0.00001, Inf)
-
-    dl = map(detdrainlength, ldd, xl, yl)
-    dw = (xl .* yl) ./ dl
-    olf = initialize_surfaceflow_land(
-        nc,
-        config,
-        inds;
-        sl = landslope,
-        dl = dl,
-        width = map(det_surfacewidth, dw, riverwidth, river),
-        iterate = kinwave_it,
-        tstep = kw_land_tstep,
-        dt = dt,
-    )
-
-    graph = flowgraph(ldd, inds, pcr_dir)
-
-    riverlength = riverlength_2d[inds_riv]
-    riverwidth = riverwidth_2d[inds_riv]
-    minimum(riverlength) > 0 || error("river length must be positive on river cells")
-    minimum(riverwidth) > 0 || error("river width must be positive on river cells")
-
-    ldd_riv = ldd_2d[inds_riv]
-    if do_pits
-        ldd_riv = set_pit_ldd(pits_2d, ldd_riv, inds_riv)
-    end
-    graph_riv = flowgraph(ldd_riv, inds_riv, pcr_dir)
-
-    # the indices of the river cells in the land(+river) cell vector
-    index_river = filter(i -> !isequal(river[i], 0), 1:n)
-    frac_toriver = fraction_runoff_toriver(graph, ldd, index_river, landslope, n)
-
-    rf = initialize_surfaceflow_river(
-        nc,
-        config,
-        inds_riv;
-        dl = riverlength,
-        width = riverwidth,
-        reservoir_index = resindex,
-        reservoir = reservoirs,
-        lake_index = lakeindex,
-        lake = lakes,
-        iterate = kinwave_it,
-        tstep = kw_river_tstep,
-        dt = dt,
-    )
-
-    # setup subdomains for the land and river kinematic wave domain, if nthreads = 1
-    # subdomain is equal to the complete domain
-    toposort = topological_sort_by_dfs(graph)
-    toposort_riv = topological_sort_by_dfs(graph_riv)
-    index_pit_land = findall(x -> x == 5, ldd)
-    index_pit_river = findall(x -> x == 5, ldd_riv)
-    streamorder = stream_order(graph, toposort)
-    min_streamorder_land = get(config.model, "min_streamorder_land", 5)
-    subbas_order, indices_subbas, topo_subbas = kinwave_set_subdomains(
-        graph,
-        toposort,
-        index_pit_land,
-        streamorder,
-        min_streamorder_land,
-    )
-    min_streamorder_river = get(config.model, "min_streamorder_river", 6)
-    subriv_order, indices_subriv, topo_subriv = kinwave_set_subdomains(
-        graph_riv,
-        toposort_riv,
-        index_pit_river,
-        streamorder[index_river],
-        min_streamorder_river,
-    )
-
-    modelmap = (vertical = hbv, lateral = (land = olf, river = rf))
-    indices_reverse = (
-        land = rev_inds,
-        river = rev_inds_riv,
-        reservoir = isempty(reservoir) ? nothing : reservoir.reverse_indices,
-        lake = isempty(lake) ? nothing : lake.reverse_indices,
-    )
-    writer = prepare_writer(config, modelmap, indices_reverse, x_nc, y_nc, nc)
-    close(nc)
-
-    # for each domain save:
-    # - the directed acyclic graph (graph),
-    # - the traversion order (order),
-    # - upstream_nodes,
-    # - subdomains for the kinematic wave domains for parallel execution (execution order of
-    #   subbasins (subdomain_order), traversion order per subbasin (topo_subdomain) and
-    #   Vector indices per subbasin matching the traversion order of the complete domain
-    #   (indices_subdomain))
-    # - the indices that map it back to the two dimensional grid (indices)
-
-    # for the land domain the x and y length [m] of the grid cells are stored
-    # for reservoirs and lakes indices information is available from the initialization
-    # functions
-    land = (
-        graph = graph,
-        upstream_nodes = filter_upsteam_nodes(graph, pits[inds]),
-        subdomain_order = subbas_order,
-        topo_subdomain = topo_subbas,
-        indices_subdomain = indices_subbas,
-        order = toposort,
-        indices = inds,
-        reverse_indices = rev_inds,
-        area = xl .* yl,
-    )
-    river = (
-        graph = graph_riv,
-        upstream_nodes = filter_upsteam_nodes(graph_riv, pits[inds_riv]),
-        subdomain_order = subriv_order,
-        topo_subdomain = topo_subriv,
-        indices_subdomain = indices_subriv,
-        order = toposort_riv,
-        indices = inds_riv,
-        reverse_indices = rev_inds_riv,
-    )
-
-    model = Model(
-        config,
-        (; land, river, reservoir, lake, index_river, frac_toriver),
-        (subsurface = nothing, land = olf, river = rf),
-        hbv,
-        clock,
-        reader,
-        writer,
-        HbvModel(),
-    )
-
-    model = set_states(model)
-
-    return model
-end
-
-function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:HbvModel}
-    (; lateral, vertical, network, config) = model
-
-    # vertical hbv concept is updated until snow state, after that (optional)
-    # snow transport is possible
-    update_until_snow(vertical, config)
-
-    # lateral snow transport
-    if get(config.model, "masswasting", false)::Bool
-        lateral_snow_transport!(
-            vertical.snow,
-            vertical.snowwater,
-            lateral.land.sl,
-            network.land,
-        )
-    end
-
-    # update vertical hbv concept
-    update_after_snow(vertical, config)
-
-    surface_routing(model)
-
-    return model
-end
-
-function set_states(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:HbvModel}
-    (; lateral, config) = model
-
-    reinit = get(config.model, "reinit", true)::Bool
-    do_lakes = get(config.model, "lakes", false)::Bool
-    # read and set states in model object if reinit=true
-    if reinit == false
-        instate_path = input_path(config, config.state.path_input)
-        @info "Set initial conditions from state file `$instate_path`."
-        set_states(instate_path, model; type = Float)
-        # update kinematic wave volume for river and land domain
-        (; lateral) = model
-        # makes sure land cells with zero flow width are set to zero q and h
-        for i in eachindex(lateral.land.width)
-            if lateral.land.width[i] <= 0.0
-                lateral.land.q[i] = 0.0
-                lateral.land.h[i] = 0.0
-            end
-        end
-        lateral.land.volume .= lateral.land.h .* lateral.land.width .* lateral.land.dl
-        lateral.river.volume .= lateral.river.h .* lateral.river.width .* lateral.river.dl
-
-        if do_lakes
-            # storage must be re-initialized after loading the state with the current
-            # waterlevel otherwise the storage will be based on the initial water level
-            lakes = lateral.river.lake
-            lakes.storage .=
-                initialize_storage(lakes.storfunc, lakes.area, lakes.waterlevel, lakes.sh)
-        end
-    else
-        @info "Set initial conditions from default values."
-    end
-
-    return model
-end
diff --git a/src/horizontal_process.jl b/src/horizontal_process.jl
index 5977a667c..fe9c57252 100644
--- a/src/horizontal_process.jl
+++ b/src/horizontal_process.jl
@@ -68,36 +68,49 @@ function kin_wave!(Q, graph, toposort, Qold, q, alpha, beta, DCL, dt)
     return Q
 end
 
-"Returns water table depth `zi` based on lateral subsurface flow `ssf` and hydraulic conductivity profile `ksat_profile`"
-function ssf_water_table_depth(ssf, kh_0, slope, f, d, dw, z_exp, ksat_profile)
-    if ksat_profile == "exponential"
-        zi = log((f * ssf) / (dw * kh_0 * slope) + exp(-f * d)) / -f
-    elseif ksat_profile == "exponential_constant"
-        ssf_constant = kh_0 * slope * exp(-f * z_exp) * (d - z_exp) * dw
-        if ssf > ssf_constant
-            zi = log((f * (ssf - ssf_constant)) / (dw * kh_0 * slope) + exp(-f * z_exp)) / -f
-        else
-            zi = d - ssf / (dw * kh_0 * slope * exp(-f * z_exp))
-        end
-    end
+"Returns water table depth `zi` based on lateral subsurface flow `ssf` and hydraulic conductivity profile `KhExponential`"
+function ssf_water_table_depth(ssf, slope, d, dw, kh_profile::KhExponential, i)
+    (; f, kh_0) = kh_profile
+    zi = log((f[i] * ssf) / (dw * kh_0[i] * slope) + exp(-f[i] * d)) / -f[i]
     return zi
 end
 
-"Returns kinematic wave celecity `Cn` of lateral subsurface flow based on hydraulic conductivity profile `ksat_profile`"
-function ssf_celerity(zi, kh_0, slope, theta_e, f, z_exp, ksat_profile)
-    if ksat_profile == "exponential"
-        Cn = (kh_0 * exp(-f * zi) * slope) / theta_e
-    elseif ksat_profile == "exponential_constant"
-        Cn_const = (kh_0 * exp(-f * z_exp) * slope) / theta_e
-        if zi < z_exp
-            Cn = (kh_0 * exp(-f * zi) * slope) / theta_e + Cn_const
-        else
-            Cn = Cn_const
-        end
+"Returns water table depth `zi` based on lateral subsurface flow `ssf` and hydraulic conductivity profile `KhExponentialConstant`"
+function ssf_water_table_depth(ssf, slope, d, dw, kh_profile::KhExponentialConstant, i)
+    (; z_exp) = kh_profile
+    (; kh_0, f) = kh_profile.exponential
+    ssf_constant = kh_0[i] * slope * exp(-f[i] * z_exp[i]) * (d - z_exp[i]) * dw
+    if ssf > ssf_constant
+        zi =
+            log(
+                (f[i] * (ssf - ssf_constant)) / (dw * kh_0[i] * slope) +
+                exp(-f[i] * z_exp[i]),
+            ) / -f[i]
+    else
+        zi = d - ssf / (dw * kh_0[i] * slope * exp(-f[i] * z_exp[i]))
     end
+    return zi
+end
+
+"Returns kinematic wave celecity `Cn` of lateral subsurface flow based on hydraulic conductivity profile `KhExponential`"
+function ssf_celerity(zi, slope, theta_e, kh_profile::KhExponential, i)
+    (; kh_0, f) = kh_profile
+    Cn = (kh_0[i] * exp(-f[i] * zi) * slope) / theta_e
     return Cn
 end
 
+"Returns kinematic wave celecity `Cn` of lateral subsurface flow based on hydraulic conductivity profile `KhExponentialConstant`"
+function ssf_celerity(zi, slope, theta_e, kh_profile::KhExponentialConstant, i)
+    (; z_exp) = kh_profile
+    (; kh_0, f) = kh_profile.exponential
+    Cn_const = (kh_0[i] * exp(-f[i] * z_exp[i]) * slope) / theta_e
+    return if zi < z_exp[i]
+        (kh_0[i] * exp(-f[i] * zi) * slope) / theta_e + Cn_const
+    else
+        Cn_const
+    end
+end
+
 """
     kinematic_wave_ssf(ssfin, ssf_prev, zi_prev, r, kh_0, slope, theta_e, f, d, dt, dx, dw, ssfmax, z_exp, ksat_profile)
 
@@ -115,19 +128,16 @@ function kinematic_wave_ssf(
     ssf_prev,
     zi_prev,
     r,
-    kh_0,
     slope,
     theta_e,
-    f,
     d,
     dt,
     dx,
     dw,
     ssfmax,
-    z_exp,
-    ksat_profile,
+    kh_profile::Union{KhExponential, KhExponentialConstant},
+    i,
 )
-
     epsilon = 1.0e-12
     max_iters = 3000
 
@@ -139,9 +149,9 @@ function kinematic_wave_ssf(
         count = 1
 
         # Estimate zi on the basis of the relation between subsurface flow and zi
-        zi = ssf_water_table_depth(ssf, kh_0, slope, f, d, dw, z_exp, ksat_profile)
+        zi = ssf_water_table_depth(ssf, slope, d, dw, kh_profile, i)
         # Reciprocal of derivative delta Q/ delta z_i, constrained w.r.t. neff on the basis of the continuity equation)
-        Cn = ssf_celerity(zi, kh_0, slope, theta_e, f, z_exp, ksat_profile)
+        Cn = ssf_celerity(zi, slope, theta_e, kh_profile, i)
         # Term of the continuity equation for Newton-Raphson iteration for iteration 1
         # because celerity Cn is depending on zi, the increase or decrease of zi is moved to the recharge term of the continuity equation
         # then (1./Cn)*ssf_prev can be replaced with (1./Cn)*ssf, and thus celerity and lateral flow rate ssf are then in line
@@ -161,9 +171,9 @@ function kinematic_wave_ssf(
         # Start while loop of Newton-Raphson iteration m until continuity equation approaches zero
         while true
             # Estimate zi on the basis of the relation between lateral flow rate and groundwater level
-            zi = ssf_water_table_depth(ssf, kh_0, slope, f, d, dw, z_exp, ksat_profile)
+            zi = ssf_water_table_depth(ssf, slope, d, dw, kh_profile, i)
             # Reciprocal of derivative delta Q/ delta z_i, constrained w.r.t. neff on the basis of the continuity equation
-            Cn = ssf_celerity(zi, kh_0, slope, theta_e, f, z_exp, ksat_profile)
+            Cn = ssf_celerity(zi, slope, theta_e, kh_profile, i)
             # Term of the continuity equation for given Newton-Raphson iteration m
             # because celerity Cn is depending on zi, the increase or decrease of zi is moved to the recharge term of the continuity equation
             # then (1./Cn)*ssf_prev can be replaced with (1./Cn)*ssf, and thus celerity and lateral flow rate ssf are then in line
@@ -194,7 +204,6 @@ function kinematic_wave_ssf(
         zi = clamp(zi, 0.0, d)
 
         return ssf, zi, exfilt
-
     end
 end
 
@@ -207,8 +216,21 @@ Kinematic wave for lateral subsurface flow for a single cell and timestep, based
 Returns lateral subsurface flow `ssf`, water table depth `zi` and exfiltration rate
 `exfilt`.
 """
-function kinematic_wave_ssf(ssfin, ssf_prev, zi_prev, r, kh, slope, theta_e, d, dt, dx, dw, ssfmax)
-
+function kinematic_wave_ssf(
+    ssfin,
+    ssf_prev,
+    zi_prev,
+    r,
+    slope,
+    theta_e,
+    d,
+    dt,
+    dx,
+    dw,
+    ssfmax,
+    kh_profile::KhLayered,
+    i,
+)
     epsilon = 1.0e-12
     max_iters = 3000
 
@@ -219,7 +241,7 @@ function kinematic_wave_ssf(ssfin, ssf_prev, zi_prev, r, kh, slope, theta_e, d,
         ssf = (ssf_prev + ssfin) / 2.0
         count = 1
         # celerity (Cn)
-        Cn = (slope * kh) / theta_e
+        Cn = (slope * kh_profile.kh[i]) / theta_e
         # constant term of the continuity equation for Newton-Raphson
         c = (dt / dx) * ssfin + (1.0 / Cn) * ssf_prev + r
         # continuity equation of which solution should be zero
@@ -264,14 +286,12 @@ function kinematic_wave_ssf(ssfin, ssf_prev, zi_prev, r, kh, slope, theta_e, d,
 end
 
 """
-    accucapacitystate!(material, network, capacity) -> material
+    accucapacitystate!(material, network, capacity)
 
 Transport of material downstream with a limited transport capacity over a directed graph.
 Mutates the material input. The network is expected to hold a graph and order field, where
 the graph implements the Graphs interface, and the order is a valid topological ordering
 such as that returned by `Graphs.topological_sort_by_dfs`.
-
-Returns the material state after transport.
 """
 function accucapacitystate!(material, network, capacity)
     (; graph, order) = network
@@ -289,7 +309,7 @@ function accucapacitystate!(material, network, capacity)
             error("bifurcations not supported")
         end
     end
-    return material
+    return nothing
 end
 
 """
@@ -298,19 +318,19 @@ end
 Non mutating version of `accucapacitystate!`.
 """
 function accucapacitystate(material, network, capacity)
-    accucapacitystate!(copy(material), network, capacity)
+    material = copy(material)
+    accucapacitystate!(material, network, capacity)
+    return material
 end
 
 """
-    accucapacityflux!(flux, material, network, capacity) -> flux, material
+    accucapacityflux!(flux, material, network, capacity)
 
 Transport of material downstream with a limited transport capacity over a directed graph.
 Updates the material input, and overwrites the flux input, not using existing values. The
 network is expected to hold a graph and order field, where the graph implements the Graphs
 interface, and the order is a valid topological ordering such as that returned by
 `Graphs.topological_sort_by_dfs`.
-
-Returns the flux (material leaving each cell), and material (left after transport).
 """
 function accucapacityflux!(flux, material, network, capacity)
     (; graph, order) = network
@@ -329,7 +349,7 @@ function accucapacityflux!(flux, material, network, capacity)
             error("bifurcations not supported")
         end
     end
-    return flux, material
+    return nothing
 end
 
 """
@@ -338,7 +358,10 @@ end
 Non mutating version of `accucapacityflux!`.
 """
 function accucapacityflux(material, network, capacity)
-    accucapacityflux!(zero(material), copy(material), network, capacity)
+    flux = zero(material)
+    material = copy(material)
+    accucapacityflux!(flux, material, network, capacity)
+    return flux, material
 end
 
 """
@@ -349,9 +372,9 @@ Lateral snow transport. Transports snow downhill. Mutates `snow` and `snowwater`
 function lateral_snow_transport!(snow, snowwater, slope, network)
     snowflux_frac = min.(0.5, slope ./ 5.67) .* min.(1.0, snow ./ 10000.0)
     maxflux = snowflux_frac .* snow
-    snow = accucapacitystate!(snow, network, maxflux)
-    snowwater = accucapacitystate!(snowwater, network, snowwater .* snowflux_frac)
-    return snow, snowwater
+    accucapacitystate!(snow, network, maxflux)
+    accucapacitystate!(snowwater, network, snowwater .* snowflux_frac)
+    return nothing
 end
 
 """
@@ -373,7 +396,6 @@ function local_inertial_flow(
     froude_limit,
     dt,
 )
-
     slope = (zs1 - zs0) / length
     pow_R = cbrt(R * R * R * R)
     unit = one(hf)
@@ -411,15 +433,13 @@ function local_inertial_flow(
     froude_limit,
     dt,
 )
-
     slope = (zs1 - zs0) / length
     unit = one(theta)
     half = oftype(theta, 0.5)
     pow_hf = cbrt(hf * hf * hf * hf * hf * hf * hf)
 
     q = (
-        ((theta * q0 + half * (unit - theta) * (qu + qd)) - g * hf * width * dt * slope) /
-        (unit + g * dt * mannings_n_sq * abs(q0) / (pow_hf * width))
+        ((theta * q0 + half * (unit - theta) * (qu + qd)) - g * hf * width * dt * slope) / (unit + g * dt * mannings_n_sq * abs(q0) / (pow_hf * width))
     )
     # if froude number > 1.0, limit flow
     if froude_limit
diff --git a/src/io.jl b/src/io.jl
index 3b7c63499..93ec265a0 100644
--- a/src/io.jl
+++ b/src/io.jl
@@ -5,12 +5,15 @@ Output data can be written to netCDF or CSV files.
 For configuration files we use TOML.
 =#
 
+#TODO (v1.0): check if mapping of variables (input and output) in TOML file should be
+#simplified. Direct mapping is now used.
+
 """Turn "a.aa.aaa" into (:a, :aa, :aaa)"""
 symbols(s) = Tuple(Symbol(x) for x in split(s, '.'))
 
 """Turn symbols"a.aa.aaa" into (:a, :aa, :aaa)"""
 macro symbols_str(s)
-    Tuple(Symbol(x) for x in split(s, '.'))
+    return Tuple(Symbol(x) for x in split(s, '.'))
 end
 
 "Get a nested field using a tuple of Symbols"
@@ -34,8 +37,8 @@ if it exists. It behaves largely like a distionary, but it overloads `getpropert
 `setproperty` to support syntax like `config.model.reinit = false`.
 """
 struct Config
-    dict::Dict{String,Any}  # nested key value mapping of all settings
-    path::Union{String,Nothing}  # path to the TOML file, or nothing
+    dict::Dict{String, Any}  # nested key value mapping of all settings
+    path::Union{String, Nothing}  # path to the TOML file, or nothing
 end
 
 Config(path::AbstractString) = Config(TOML.parsefile(path), path)
@@ -52,7 +55,8 @@ end
 
 function Base.setproperty!(config::Config, f::Symbol, x)
     dict = Dict(config)
-    return dict[String(f)] = x
+    dict[String(f)] = x
+    return nothing
 end
 
 "Get a value from the Config with either the key or an alias of the key."
@@ -127,7 +131,7 @@ function ncvar_name_modifier(var; config = nothing)
                 if length(var[dim_name]) > 1
                     # if modifier is provided as a list for each dim item
                     indices = []
-                    for i = 1:length(var[dim_name])
+                    for i in 1:length(var[dim_name])
                         index = get_index_dimension(var, config, var[dim_name][i])
                         @info "NetCDF parameter `$ncname` is modified with scale `$(scale[i])` and offset `$(offset[i])` at index `$index`."
                         push!(indices, index)
@@ -178,31 +182,35 @@ function get_at(ds::CFDataset, varname::AbstractString, i)
 end
 
 function get_param_res(model)
-    Dict(
-        symbols"vertical.precipitation" => model.lateral.river.reservoir.precipitation,
-        symbols"vertical.potential_evaporation" =>
+    return Dict(
+        symbols"vertical.atmospheric_forcing.precipitation" =>
+            model.lateral.river.reservoir.precipitation,
+        symbols"vertical.atmospheric_forcing.potential_evaporation" =>
             model.lateral.river.reservoir.evaporation,
     )
 end
 
 function get_param_lake(model)
-    Dict(
-        symbols"vertical.precipitation" => model.lateral.river.lake.precipitation,
-        symbols"vertical.potential_evaporation" => model.lateral.river.lake.evaporation,
+    return Dict(
+        symbols"vertical.atmospheric_forcing.precipitation" =>
+            model.lateral.river.lake.precipitation,
+        symbols"vertical.atmospheric_forcing.potential_evaporation" =>
+            model.lateral.river.lake.evaporation,
     )
 end
 
-mover_params = (symbols"vertical.precipitation", symbols"vertical.potential_evaporation")
+mover_params = (
+    symbols"vertical.atmospheric_forcing.precipitation",
+    symbols"vertical.atmospheric_forcing.potential_evaporation",
+)
 
-function load_fixed_forcing(model)
+function load_fixed_forcing!(model)
     (; reader, network, config) = model
     (; forcing_parameters) = reader
 
     do_reservoirs = get(config.model, "reservoirs", false)::Bool
     do_lakes = get(config.model, "lakes", false)::Bool
 
-    mover_params =
-        (symbols"vertical.precipitation", symbols"vertical.potential_evaporation")
     reverse_indices = network.land.reverse_indices
     if do_reservoirs
         sel_reservoirs = network.reservoir.indices_coverage
@@ -237,6 +245,7 @@ function load_fixed_forcing(model)
             end
         end
     end
+    return nothing
 end
 
 "Get dynamic netCDF input for the given time"
@@ -303,7 +312,7 @@ function update_forcing!(model)
         param_vector .= data_sel
     end
 
-    return model
+    return nothing
 end
 
 """
@@ -327,6 +336,7 @@ function load_dynamic_input!(model)
     if haskey(model.config.input, "cyclic")
         update_cyclic!(model)
     end
+    return nothing
 end
 
 "Get cyclic netCDF input for the given time"
@@ -355,6 +365,7 @@ function update_cyclic!(model)
             end
         end
     end
+    return nothing
 end
 
 """
@@ -368,7 +379,7 @@ https://github.com/Alexander-Barth/NCDatasets.jl/issues/106
 Note that using this will prevent automatic garbage collection and thus closure of the
 NCDataset.
 """
-const nc_handles = Dict{String,NCDataset{Nothing}}()
+const nc_handles = Dict{String, NCDataset{Nothing}}()
 
 "Safely create a netCDF file, even if it has already been opened for creation"
 function create_tracked_netcdf(path)
@@ -402,7 +413,7 @@ function setup_scalar_netcdf(
         ds,
         "time",
         Float64,
-        ("time",),
+        ("time",);
         attrib = ["units" => time_units, "calendar" => calendar],
     )
     set_extradim_netcdf(ds, extra_dim)
@@ -413,7 +424,7 @@ function setup_scalar_netcdf(
             ds,
             nc.location_dim,
             nc.locations,
-            (nc.location_dim,),
+            (nc.location_dim,);
             attrib = ["cf_role" => "timeseries_id"],
         )
         v = param(modelmap, nc.par)
@@ -422,7 +433,7 @@ function setup_scalar_netcdf(
                 ds,
                 nc.var,
                 float_type,
-                (nc.location_dim, "time"),
+                (nc.location_dim, "time");
                 attrib = ["_FillValue" => float_type(NaN)],
             )
         elseif eltype(v) <: SVector
@@ -432,7 +443,7 @@ function setup_scalar_netcdf(
                     ds,
                     nc.var,
                     float_type,
-                    (nc.location_dim, "time"),
+                    (nc.location_dim, "time");
                     attrib = ["_FillValue" => float_type(NaN)],
                 )
             else
@@ -440,7 +451,7 @@ function setup_scalar_netcdf(
                     ds,
                     nc.var,
                     float_type,
-                    (nc.location_dim, extra_dim.name, "time"),
+                    (nc.location_dim, extra_dim.name, "time");
                     attrib = ["_FillValue" => float_type(NaN)],
                 )
             end
@@ -456,22 +467,16 @@ function set_extradim_netcdf(
     ds,
     extra_dim::NamedTuple{
         (:name, :value),
-        Tuple{String,Vector{T}},
-    } where {T<:Union{String,Float64}},
+        Tuple{String, Vector{T}},
+    } where {T <: Union{String, Float64}},
 )
     # the axis attribute `Z` is required to import this type of 3D data by Delft-FEWS the
     # values of this dimension `extra_dim.value` should be of type Float64
     if extra_dim.name == "layer"
         attributes =
             ["long_name" => "layer_index", "standard_name" => "layer_index", "axis" => "Z"]
-    elseif extra_dim.name == "classes"
-        attributes = [
-            "long_name" => extra_dim.name,
-            "standard_name" => extra_dim.name,
-            "axis" => "Z",
-        ]
     end
-    defVar(ds, extra_dim.name, extra_dim.value, (extra_dim.name,), attrib = attributes)
+    defVar(ds, extra_dim.name, extra_dim.value, (extra_dim.name,); attrib = attributes)
     return nothing
 end
 
@@ -490,7 +495,6 @@ function setup_grid_netcdf(
     float_type = Float32,
     deflatelevel = 0,
 )
-
     ds = create_tracked_netcdf(path)
     defDim(ds, "time", Inf)  # unlimited
     if sizeinmetres
@@ -498,7 +502,7 @@ function setup_grid_netcdf(
             ds,
             "x",
             ncx,
-            ("x",),
+            ("x",);
             attrib = [
                 "long_name" => "x coordinate of projection",
                 "standard_name" => "projection_x_coordinate",
@@ -511,7 +515,7 @@ function setup_grid_netcdf(
             ds,
             "y",
             ncy,
-            ("y",),
+            ("y",);
             attrib = [
                 "long_name" => "y coordinate of projection",
                 "standard_name" => "projection_y_coordinate",
@@ -526,7 +530,7 @@ function setup_grid_netcdf(
             ds,
             "lon",
             ncx,
-            ("lon",),
+            ("lon",);
             attrib = [
                 "long_name" => "longitude",
                 "standard_name" => "longitude",
@@ -538,7 +542,7 @@ function setup_grid_netcdf(
             ds,
             "lat",
             ncy,
-            ("lat",),
+            ("lat",);
             attrib = [
                 "long_name" => "latitude",
                 "standard_name" => "latitude",
@@ -553,7 +557,7 @@ function setup_grid_netcdf(
         ds,
         "time",
         Float64,
-        ("time",),
+        ("time",);
         attrib = ["units" => time_units, "calendar" => calendar],
         deflatelevel = deflatelevel,
     )
@@ -565,7 +569,7 @@ function setup_grid_netcdf(
                     ds,
                     key,
                     float_type,
-                    ("x", "y", "time"),
+                    ("x", "y", "time");
                     attrib = ["_FillValue" => float_type(NaN)],
                     deflatelevel = deflatelevel,
                 )
@@ -575,7 +579,7 @@ function setup_grid_netcdf(
                     ds,
                     key,
                     float_type,
-                    ("x", "y", extra_dim.name, "time"),
+                    ("x", "y", extra_dim.name, "time");
                     attrib = ["_FillValue" => float_type(NaN)],
                     deflatelevel = deflatelevel,
                 )
@@ -591,7 +595,7 @@ function setup_grid_netcdf(
                     ds,
                     key,
                     float_type,
-                    ("lon", "lat", "time"),
+                    ("lon", "lat", "time");
                     attrib = ["_FillValue" => float_type(NaN)],
                     deflatelevel = deflatelevel,
                 )
@@ -601,7 +605,7 @@ function setup_grid_netcdf(
                     ds,
                     key,
                     float_type,
-                    ("lon", "lat", extra_dim.name, "time"),
+                    ("lon", "lat", extra_dim.name, "time");
                     attrib = ["_FillValue" => float_type(NaN)],
                     deflatelevel = deflatelevel,
                 )
@@ -623,27 +627,27 @@ end
 struct NCReader{T}
     dataset::CFDataset
     dataset_times::Vector{T}
-    cyclic_dataset::Union{NCDataset,Nothing}
-    cyclic_times::Dict{Tuple{Symbol,Vararg{Symbol}},Vector{Tuple{Int,Int}}}
-    forcing_parameters::Dict{Tuple{Symbol,Vararg{Symbol}},NamedTuple}
-    cyclic_parameters::Dict{Tuple{Symbol,Vararg{Symbol}},NamedTuple}
+    cyclic_dataset::Union{NCDataset, Nothing}
+    cyclic_times::Dict{Tuple{Symbol, Vararg{Symbol}}, Vector{Tuple{Int, Int}}}
+    forcing_parameters::Dict{Tuple{Symbol, Vararg{Symbol}}, NamedTuple}
+    cyclic_parameters::Dict{Tuple{Symbol, Vararg{Symbol}}, NamedTuple}
 end
 
 struct Writer
-    dataset::Union{NCDataset,Nothing}           # dataset (netCDF) for grid data
-    parameters::Dict{String,Any}                # mapping of netCDF variable names to model parameters (arrays)
-    nc_path::Union{String,Nothing}              # path netCDF file (grid data)
-    csv_path::Union{String,Nothing}             # path of CSV file
+    dataset::Union{NCDataset, Nothing}           # dataset (netCDF) for grid data
+    parameters::Dict{String, Any}                # mapping of netCDF variable names to model parameters (arrays)
+    nc_path::Union{String, Nothing}              # path netCDF file (grid data)
+    csv_path::Union{String, Nothing}             # path of CSV file
     csv_cols::Vector                            # model parameter (arrays) and associated reducer function for CSV output
     csv_io::IO                                  # file handle to CSV file
-    state_dataset::Union{NCDataset,Nothing}     # dataset with model states (netCDF)
-    state_parameters::Dict{String,Any}          # mapping of netCDF variable names to model states (arrays)
-    state_nc_path::Union{String,Nothing}        # path netCDF file with states
-    dataset_scalar::Union{NCDataset,Nothing}    # dataset (netCDF) for scalar data
+    state_dataset::Union{NCDataset, Nothing}     # dataset with model states (netCDF)
+    state_parameters::Dict{String, Any}          # mapping of netCDF variable names to model states (arrays)
+    state_nc_path::Union{String, Nothing}        # path netCDF file with states
+    dataset_scalar::Union{NCDataset, Nothing}    # dataset (netCDF) for scalar data
     nc_scalar::Vector                           # model parameter (arrays) and associated reducer function for netCDF scalar output
     ncvars_dims::Vector                         # model parameter (String) and associated netCDF variable, location dimension and location name for scalar data
-    nc_scalar_path::Union{String,Nothing}       # path netCDF file (scalar data)
-    extra_dim::Union{NamedTuple,Nothing}        # name and values for extra dimension (to store SVectors)
+    nc_scalar_path::Union{String, Nothing}       # path netCDF file (scalar data)
+    extra_dim::Union{NamedTuple, Nothing}        # name and values for extra dimension (to store SVectors)
 end
 
 function prepare_reader(config)
@@ -672,7 +676,7 @@ function prepare_reader(config)
     if isempty(dynamic_paths)
         error("No files found with name '$glob_path' in '$glob_dir'")
     end
-    dataset = NCDataset(dynamic_paths, aggdim = "time", deferopen = false)
+    dataset = NCDataset(dynamic_paths; aggdim = "time", deferopen = false)
 
     if haskey(dataset["time"].attrib, "_FillValue")
         @warn "Time dimension contains `_FillValue` attribute, this is not in line with CF conventions."
@@ -695,7 +699,7 @@ function prepare_reader(config)
     do_cyclic = haskey(config.input, "cyclic")
 
     # create map from internal location to netCDF variable name for forcing parameters
-    forcing_parameters = Dict{Tuple{Symbol,Vararg{Symbol}},NamedTuple}()
+    forcing_parameters = Dict{Tuple{Symbol, Vararg{Symbol}}, NamedTuple}()
     for par in config.input.forcing
         fields = symbols(par)
         ncname, mod = ncvar_name_modifier(param(config.input, fields))
@@ -711,8 +715,8 @@ function prepare_reader(config)
     # (memory usage))
     if do_cyclic == true
         cyclic_dataset = NCDataset(cyclic_path)
-        cyclic_parameters = Dict{Tuple{Symbol,Vararg{Symbol}},NamedTuple}()
-        cyclic_times = Dict{Tuple{Symbol,Vararg{Symbol}},Vector{Tuple{Int,Int}}}()
+        cyclic_parameters = Dict{Tuple{Symbol, Vararg{Symbol}}, NamedTuple}()
+        cyclic_times = Dict{Tuple{Symbol, Vararg{Symbol}}, Vector{Tuple{Int, Int}}}()
         for par in config.input.cyclic
             fields = symbols(par)
             ncname, mod = ncvar_name_modifier(param(config.input, fields))
@@ -726,9 +730,9 @@ function prepare_reader(config)
             @info "Set `$par` using netCDF variable `$ncname` as cyclic parameter, with `$(length(cyclic_nc_times))` timesteps."
         end
     else
-        cyclic_parameters = Dict{Tuple{Symbol,Vararg{Symbol}},NamedTuple}()
+        cyclic_parameters = Dict{Tuple{Symbol, Vararg{Symbol}}, NamedTuple}()
         cyclic_dataset = nothing
-        cyclic_times = Dict{Tuple{Symbol,Vararg{Symbol}},Vector{Tuple{Int,Int}}}()
+        cyclic_times = Dict{Tuple{Symbol, Vararg{Symbol}}, Vector{Tuple{Int, Int}}}()
     end
 
     # check if there is overlap
@@ -759,7 +763,7 @@ function locations_map(ds, mapname, config)
         config,
         mapname;
         optional = false,
-        type = Union{Int,Missing},
+        type = Union{Int, Missing},
         allow_missing = true,
     )
     ids = unique(skipmissing(map_2d))
@@ -834,13 +838,13 @@ function flat!(d, path, el::Dict)
     for (k, v) in pairs(el)
         flat!(d, string(path, '.', k), v)
     end
-    return d
+    return nothing
 end
 
 function flat!(d, path, el)
     k = symbols(path)
     d[k] = el
-    return d
+    return nothing
 end
 
 """
@@ -872,7 +876,7 @@ Dict(
 ```
 """
 function ncnames(dict)
-    ncnames_dict = Dict{Tuple{Symbol,Vararg{Symbol}},String}()
+    ncnames_dict = Dict{Tuple{Symbol, Vararg{Symbol}}, String}()
     for (k, v) in dict
         if v isa Dict  # ignore top level values (e.g. output.path)
             flat!(ncnames_dict, k, v)
@@ -887,7 +891,7 @@ end
 Create a Dict that maps parameter netCDF names to arrays in the Model.
 """
 function out_map(ncnames_dict, modelmap)
-    output_map = Dict{String,Any}()
+    output_map = Dict{String, Any}()
     for (par, ncname) in ncnames_dict
         A = param(modelmap, par)
         output_map[ncname] = (par = par, vector = A)
@@ -895,7 +899,6 @@ function out_map(ncnames_dict, modelmap)
     return output_map
 end
 
-
 function get_reducer_func(col, rev_inds, args...)
     parameter = col["parameter"]
     if occursin("reservoir", parameter)
@@ -943,12 +946,12 @@ function prepare_writer(
             calendar,
             time_units,
             extra_dim,
-            sizeinmetres,
+            sizeinmetres;
             deflatelevel = deflatelevel,
         )
     else
         nc_path = nothing
-        output_map = Dict{String,Any}()
+        output_map = Dict{String, Any}()
         ds = nothing
     end
 
@@ -972,7 +975,7 @@ function prepare_writer(
         )
     else
         ds_outstate = nothing
-        state_map = Dict{String,Any}()
+        state_map = Dict{String, Any}()
         nc_state_path = nothing
     end
 
@@ -1037,7 +1040,6 @@ function prepare_writer(
         csv_io = IOBuffer()
     end
 
-
     return Writer(
         ds,
         output_map,
@@ -1076,7 +1078,7 @@ function write_netcdf_timestep(model, dataset)
                 dataset[nc["name"]][:, time_index] .= v
             else
                 nlayer = length(first(A))
-                for i = 1:nlayer
+                for i in 1:nlayer
                     v = nt.reducer(getindex.(A, i))
                     dataset[nc["name"]][:, i, time_index] .= v
                 end
@@ -1094,7 +1096,7 @@ function write_netcdf_timestep(model, dataset, parameters)
 
     time_index = add_time(dataset, clock.time)
 
-    buffer = zeros(Union{Float,Missing}, size(model.network.land.reverse_indices))
+    buffer = zeros(Union{Float, Missing}, size(model.network.land.reverse_indices))
     for (key, val) in parameters
         (; par, vector) = val
         sel = active_indices(network, par)
@@ -1108,7 +1110,7 @@ function write_netcdf_timestep(model, dataset, parameters)
             dataset[key][:, :, time_index] = buffer
         elseif elemtype <: SVector
             nlayer = length(first(vector))
-            for i = 1:nlayer
+            for i in 1:nlayer
                 # ensure no other information is written
                 fill!(buffer, missing)
                 buffer[sel] .= getindex.(vector, i)
@@ -1213,7 +1215,7 @@ end
 
 "Mapping from reducer strings in the TOML to functions"
 function reducerfunction(reducer::AbstractString)
-    functionmap = Dict{String,Function}(
+    functionmap = Dict{String, Function}(
         "maximum" => maximum,
         "minimum" => minimum,
         "mean" => mean,
@@ -1244,13 +1246,13 @@ function reducer(col, rev_inds, x_nc, y_nc, config, dataset, fileformat)
             config,
             mapname;
             optional = false,
-            type = Union{Int,Missing},
+            type = Union{Int, Missing},
             allow_missing = true,
         )
         @info "Adding scalar output for a map with a reducer function." fileformat param mapname reducer_name
         ids = unique(skipmissing(map_2d))
         # from id to list of internal indices
-        inds = Dict{Int,Vector{Int}}(id => Vector{Int}() for id in ids)
+        inds = Dict{Int, Vector{Int}}(id => Vector{Int}() for id in ids)
         for i in eachindex(map_2d)
             v = map_2d[i]
             ismissing(v) && continue
@@ -1326,7 +1328,7 @@ function write_csv_row(model)
             print(io, ',', el)
         end
     end
-    println(io)
+    return println(io)
 end
 
 "From a time and a calendar, create the right CFTime DateTimeX type"
@@ -1346,24 +1348,24 @@ function reset_clock!(clock::Clock, config)
     clock.time = new_clock.time
     clock.iteration = new_clock.iteration
     clock.dt = new_clock.dt
-    return clock
+    return nothing
 end
 
 function advance!(clock)
     clock.iteration += 1
     clock.time += clock.dt
-    return clock
+    return nothing
 end
 
 function rewind!(clock)
     clock.iteration -= 1
     clock.time -= clock.dt
-    return clock
+    return nothing
 end
 
 "Read a rating curve from CSV into a NamedTuple of vectors"
 function read_sh_csv(path)
-    data, header = readdlm(path, ',', Float, header = true)
+    data, header = readdlm(path, ',', Float; header = true)
     names = vec(uppercase.(header))
     idx_h = findfirst(==("H"), names)
     idx_s = findfirst(==("S"), names)
@@ -1377,14 +1379,14 @@ end
 
 "Read a specific storage curve from CSV into a NamedTuple of vectors"
 function read_hq_csv(path)
-    data = readdlm(path, ',', Float, skipstart = 1)
+    data = readdlm(path, ',', Float; skipstart = 1)
     # Q is a matrix with 365 columns, one for each day in the year
     return (H = data[:, 1], Q = data[:, 2:end])
 end
 
 # these represent the type of the rating curve and specific storage data
-const SH = NamedTuple{(:H, :S),Tuple{Vector{Float},Vector{Float}}}
-const HQ = NamedTuple{(:H, :Q),Tuple{Vector{Float},Matrix{Float}}}
+const SH = NamedTuple{(:H, :S), Tuple{Vector{Float}, Vector{Float}}}
+const HQ = NamedTuple{(:H, :Q), Tuple{Vector{Float}, Matrix{Float}}}
 
 is_increasing(v) = last(v) > first(v)
 
@@ -1426,7 +1428,6 @@ using `nc_dim_name`.
 """
 nc_dim(ds::CFDataset, name) = ds[nc_dim_name(ds, name)]
 
-
 """
     internal_dim_name(name::Symbol)
 
@@ -1437,7 +1438,7 @@ function internal_dim_name(name::Symbol)
         return :x
     elseif name in (:y, :lat, :latitude)
         return :y
-    elseif name in (:time, :layer, :flood_depth, :classes)
+    elseif name in (:time, :layer, :flood_depth)
         return name
     elseif startswith(string(name), "time")
         return :time
@@ -1497,7 +1498,7 @@ consider it increasing, going from the top layer (1) to deeper layers. This is t
 accepting data that we have accepted before.
 """
 function dim_directions(ds::CFDataset, dim_names)
-    pairs = Pair{Symbol,Bool}[]
+    pairs = Pair{Symbol, Bool}[]
     for d in dim_names
         if d == :layer && !(haskey(ds, "layer"))
             inc = true
@@ -1522,8 +1523,6 @@ function permute_data(data, dim_names)
     @assert ndims(data) == length(dim_names)
     if :layer in dim_names
         desired_order = (:x, :y, :layer)
-    elseif :classes in dim_names
-        desired_order = (:x, :y, :classes)
     elseif :flood_depth in dim_names
         desired_order = (:x, :y, :flood_depth)
     else
@@ -1567,12 +1566,6 @@ function reverse_data!(data, dims_increasing)
             y = dims_increasing.y,
             flood_depth = dims_increasing.flood_depth,
         )
-    elseif length(dims_increasing) == 3 && haskey(dims_increasing, :classes)
-        dims_increasing_ordered = (
-            x = dims_increasing.x,
-            y = dims_increasing.y,
-            classes = dims_increasing.classes,
-        )
     else
         error("Unsupported number of dimensions")
     end
@@ -1609,7 +1602,7 @@ function read_x_axis(ds::CFDataset)::Vector{Float64}
             return sort!(Float64.(ds[candidate][:]))
         end
     end
-    error("no x axis found in $(path(ds))")
+    return error("no x axis found in $(path(ds))")
 end
 
 """
@@ -1625,32 +1618,27 @@ function read_y_axis(ds::CFDataset)::Vector{Float64}
             return sort!(Float64.(ds[candidate][:]))
         end
     end
-    error("no y axis found in $(path(ds))")
+    return error("no y axis found in $(path(ds))")
 end
 
-"Get `index` for dimension name `layer` or `classes` based on `model`"
+"Get `index` for dimension name `layer` based on `model`"
 function get_index_dimension(var, model)::Int
     (; vertical) = model
     if haskey(var, "layer")
-        inds = collect(1:vertical.maxlayers)
+        inds = collect(1:(vertical.soil.parameters.maxlayers))
         index = inds[var["layer"]]
-    elseif haskey(var, "class")
-        index = findfirst(x -> x == var["class"], vertical.classes)
     else
         error("Unrecognized or missing dimension name to index $(var)")
     end
     return index
 end
 
-"Get `index` for dimension name `layer` or `classes` based on `config` (TOML file)"
+"Get `index` for dimension name `layer` based on `config` (TOML file)"
 function get_index_dimension(var, config::Config, dim_value)::Int
     if haskey(var, "layer")
         v = get(config.model, "thicknesslayers", Float[])
-        inds = collect(1:length(v)+1)
+        inds = collect(1:(length(v) + 1))
         index = inds[dim_value]
-    elseif haskey(var, "class")
-        classes = get(config.model, "classes", "")
-        index = findfirst(x -> x == dim_value, classes)
     else
         error("Unrecognized or missing dimension name to index $(var)")
     end
diff --git a/src/logging.jl b/src/logging.jl
index 3a9a3d7e4..eec1873db 100644
--- a/src/logging.jl
+++ b/src/logging.jl
@@ -46,7 +46,7 @@ function format_message(io::IO, args)::Nothing
 end
 
 "Initialize a logger, which is different if `fews_run` is set in the Config."
-function init_logger(config::Config; silent = false)::Tuple{TeeLogger,IOStream}
+function init_logger(config::Config; silent = false)::Tuple{TeeLogger, IOStream}
     loglevel = parse_loglevel(get(config, "loglevel", "info"))
     path_log = output_path(config, get(config, "path_log", "log.txt"))
     mkpath(dirname(path_log))
@@ -61,7 +61,7 @@ function init_logger(config::Config; silent = false)::Tuple{TeeLogger,IOStream}
         # https://github.com/JuliaLogging/LoggingExtras.jl/issues/46#issuecomment-803716480
         ConsoleLogger(
             IOContext(log_handle, :compact => false, :limit => false, :color => false),
-            loglevel,
+            loglevel;
             show_limited = false,
         )
     end
diff --git a/src/parameters.jl b/src/parameters.jl
new file mode 100644
index 000000000..f2d2d582a
--- /dev/null
+++ b/src/parameters.jl
@@ -0,0 +1,104 @@
+"Struct to store (shared) vegetation parameters"
+@get_units @grid_loc @with_kw struct VegetationParameters{T}
+    # Leaf area index [m² m⁻²]
+    leaf_area_index::Union{Vector{T}, Nothing} | "m2 m-2"
+    # Storage woody part of vegetation [mm]
+    storage_wood::Union{Vector{T}, Nothing} | "mm"
+    # Extinction coefficient [-] (to calculate canopy gap fraction)
+    kext::Union{Vector{T}, Nothing} | "-"
+    # Specific leaf storage [mm]
+    storage_specific_leaf::Union{Vector{T}, Nothing} | "mm"
+    # Canopy gap fraction [-]
+    canopygapfraction::Vector{T} | "-"
+    # Maximum canopy storage [mm] 
+    cmax::Vector{T} | "mm"
+    # Rooting depth [mm]
+    rootingdepth::Vector{T} | "mm"
+    # Crop coefficient Kc [-]
+    kc::Vector{T} | "-"
+end
+
+"Initialize (shared) vegetation parameters"
+function VegetationParameters(nc, config, inds)
+    n = length(inds)
+    rootingdepth = ncread(
+        nc,
+        config,
+        "vertical.vegetation_parameter_set.rootingdepth";
+        sel = inds,
+        defaults = 750.0,
+        type = Float,
+    )
+    kc = ncread(
+        nc,
+        config,
+        "vertical.vegetation_parameter_set.kc";
+        sel = inds,
+        defaults = 1.0,
+        type = Float,
+    )
+    if haskey(config.input.vertical.vegetation_parameter_set, "leaf_area_index")
+        storage_specific_leaf = ncread(
+            nc,
+            config,
+            "vertical.vegetation_parameter_set.storage_specific_leaf";
+            optional = false,
+            sel = inds,
+            type = Float,
+        )
+        storage_wood = ncread(
+            nc,
+            config,
+            "vertical.vegetation_parameter_set.storage_wood";
+            optional = false,
+            sel = inds,
+            type = Float,
+        )
+        kext = ncread(
+            nc,
+            config,
+            "vertical.vegetation_parameter_set.kext";
+            optional = false,
+            sel = inds,
+            type = Float,
+        )
+        vegetation_parameter_set = VegetationParameters(;
+            leaf_area_index = fill(mv, n),
+            storage_wood,
+            kext,
+            storage_specific_leaf,
+            canopygapfraction = fill(mv, n),
+            cmax = fill(mv, n),
+            rootingdepth,
+            kc,
+        )
+    else
+        canopygapfraction = ncread(
+            nc,
+            config,
+            "vertical.vegetation_parameter_set.canopygapfraction";
+            sel = inds,
+            defaults = 0.1,
+            type = Float,
+        )
+        cmax = ncread(
+            nc,
+            config,
+            "vertical.vegetation_parameter_set.cmax";
+            sel = inds,
+            defaults = 1.0,
+            type = Float,
+        )
+        vegetation_parameter_set = VegetationParameters(;
+            leaf_area_index = nothing,
+            storage_wood = nothing,
+            kext = nothing,
+            storage_specific_leaf = nothing,
+            canopygapfraction,
+            cmax,
+            rootingdepth,
+            kc,
+        )
+    end
+    return vegetation_parameter_set
+end
\ No newline at end of file
diff --git a/src/reservoir_lake.jl b/src/reservoir_lake.jl
index 5060c842d..edc55a91f 100644
--- a/src/reservoir_lake.jl
+++ b/src/reservoir_lake.jl
@@ -1,5 +1,5 @@
-@get_units @exchange @grid_type @grid_location @with_kw struct SimpleReservoir{T}
-    dt::T | "s" | 0 | "none" | "none"                   # Model time step [s]
+@get_units @grid_loc @with_kw struct SimpleReservoir{T}
+    dt::T                                               # Model time step [s]
     maxvolume::Vector{T} | "m3"                         # maximum storage (above which water is spilled) [m³]
     area::Vector{T} | "m2"                              # reservoir area [m²]
     maxrelease::Vector{T} | "m3 s-1"                    # maximum amount that can be released if below spillway [m³ s⁻¹]
@@ -22,7 +22,6 @@
     end
 end
 
-
 function initialize_simple_reservoir(config, nc, inds_riv, nriv, pits, dt)
     # read only reservoir data if reservoirs true
     # allow reservoirs only in river cells
@@ -132,7 +131,7 @@ function initialize_simple_reservoir(config, nc, inds_riv, nriv, pits, dt)
 
     n = length(resarea)
     @info "Read `$n` reservoir locations."
-    reservoirs = SimpleReservoir{Float}(
+    reservoirs = SimpleReservoir{Float}(;
         dt = dt,
         demand = resdemand,
         maxrelease = resmaxrelease,
@@ -167,7 +166,7 @@ Update a single reservoir at position `i`.
 This is called from within the kinematic wave loop, therefore updating only for a single
 element rather than all at once.
 """
-function update(res::SimpleReservoir, i, inflow, timestepsecs)
+function update!(res::SimpleReservoir, i, inflow, timestepsecs)
 
     # limit lake evaporation based on total available volume [m³]
     precipitation = 0.001 * res.precipitation[i] * (timestepsecs / res.dt) * res.area[i]
@@ -201,21 +200,21 @@ function update(res::SimpleReservoir, i, inflow, timestepsecs)
     res.volume[i] = vol
     res.actevap[i] += 1000.0 * (actevap / res.area[i])
 
-    return res
+    return nothing
 end
 
-@get_units @exchange @grid_type @grid_location @with_kw struct Lake{T}
-    dt::T | "s" | 0 | "none" | "none"           # Model time step [s]
+@get_units @grid_loc @with_kw struct Lake{T}
+    dt::T                                       # Model time step [s]
     lowerlake_ind::Vector{Int} | "-"            # Index of lower lake (linked lakes)
     area::Vector{T} | "m2"                      # lake area [m²]
-    maxstorage::Vector{Union{T,Missing}} | "m3" # lake maximum storage from rating curve 1 [m³]
+    maxstorage::Vector{Union{T, Missing}} | "m3" # lake maximum storage from rating curve 1 [m³]
     threshold::Vector{T} | "m"                  # water level threshold H₀ [m] below that level outflow is zero
     storfunc::Vector{Int} | "-"                 # type of lake storage curve, 1: S = AH, 2: S = f(H) from lake data and interpolation
     outflowfunc::Vector{Int} | "-"              # type of lake rating curve, 1: Q = f(H) from lake data and interpolation, 2: General Q = b(H - H₀)ᵉ, 3: Case of Puls Approach Q = b(H - H₀)²
     b::Vector{T} | "m3/2 s-1 (if e=3/2)"        # rating curve coefficient
     e::Vector{T} | "-"                          # rating curve exponent
-    sh::Vector{Union{SH,Missing}}               # data for storage curve
-    hq::Vector{Union{HQ,Missing}}               # data for rating curve
+    sh::Vector{Union{SH, Missing}}               # data for storage curve
+    hq::Vector{Union{HQ, Missing}}               # data for rating curve
     waterlevel::Vector{T} | "m"                 # waterlevel H [m] of lake
     inflow::Vector{T} | "m3"                    # inflow to the lake [m³]
     storage::Vector{T} | "m3"                   # storage lake [m³]
@@ -363,13 +362,13 @@ function initialize_lake(config, nc, inds_riv, nriv, pits, dt)
 
     @info "Read `$n_lakes` lake locations."
 
-    sh = Vector{Union{SH,Missing}}(missing, n_lakes)
-    hq = Vector{Union{HQ,Missing}}(missing, n_lakes)
+    sh = Vector{Union{SH, Missing}}(missing, n_lakes)
+    hq = Vector{Union{HQ, Missing}}(missing, n_lakes)
     lowerlake_ind = fill(0, n_lakes)
     # lake CSV parameter files are expected in the same directory as path_static
     path = dirname(input_path(config, config.input.path_static))
 
-    for i = 1:n_lakes
+    for i in 1:n_lakes
         lakeloc = lakelocs[i]
         if linked_lakelocs[i] > 0
             lowerlake_ind[i] = only(findall(x -> x == linked_lakelocs[i], lakelocs))
@@ -398,7 +397,7 @@ function initialize_lake(config, nc, inds_riv, nriv, pits, dt)
         end
     end
     n = length(lakearea)
-    lakes = Lake{Float}(
+    lakes = Lake{Float}(;
         dt = dt,
         lowerlake_ind = lowerlake_ind,
         area = lakearea,
@@ -430,7 +429,6 @@ function initialize_lake(config, nc, inds_riv, nriv, pits, dt)
     pits
 end
 
-
 "Determine the initial storage depending on the storage function"
 function initialize_storage(storfunc, area, waterlevel, sh)
     storage = similar(area)
@@ -459,7 +457,7 @@ end
 
 "Determine the maximum storage for lakes with a rating curve of type 1"
 function maximum_storage(storfunc, outflowfunc, area, sh, hq)
-    maxstorage = Vector{Union{Float,Missing}}(missing, length(area))
+    maxstorage = Vector{Union{Float, Missing}}(missing, length(area))
     # maximum storage is based on the maximum water level (H) value in the H-Q table
     for i in eachindex(maxstorage)
         if outflowfunc[i] == 1
@@ -473,7 +471,6 @@ function maximum_storage(storfunc, outflowfunc, area, sh, hq)
     return maxstorage
 end
 
-
 function interpolate_linear(x, xp, fp)
     if x <= minimum(xp)
         return minimum(fp)
@@ -494,8 +491,7 @@ Update a single lake at position `i`.
 This is called from within the kinematic wave loop, therefore updating only for a single
 element rather than all at once.
 """
-function update(lake::Lake, i, inflow, doy, timestepsecs)
-
+function update!(lake::Lake, i, inflow, doy, timestepsecs)
     lo = lake.lowerlake_ind[i]
     has_lowerlake = lo != 0
 
@@ -532,7 +528,6 @@ function update(lake::Lake, i, inflow, doy, timestepsecs)
 
     ### Linearisation for specific storage/rating curves ###
     if lake.outflowfunc[i] == 1 || lake.outflowfunc[i] == 2
-
         diff_wl = has_lowerlake ? lake.waterlevel[i] - lake.waterlevel[lo] : 0.0
 
         storage_input = (lake.storage[i] + precipitation - actevap) / timestepsecs + inflow
@@ -592,7 +587,6 @@ function update(lake::Lake, i, inflow, doy, timestepsecs)
             lake.storage[lo] = lowerlake_storage
             lake.waterlevel[lo] = lowerlake_waterlevel
         end
-
     end
 
     # update values in place
@@ -603,5 +597,5 @@ function update(lake::Lake, i, inflow, doy, timestepsecs)
     lake.storage[i] = storage
     lake.actevap[i] += 1000.0 * (actevap / lake.area[i])
 
-    return lake
+    return nothing
 end
diff --git a/src/sbm.jl b/src/sbm.jl
index baab14691..7293ec082 100644
--- a/src/sbm.jl
+++ b/src/sbm.jl
@@ -1,1326 +1,141 @@
-@get_units @exchange @grid_type @grid_location @with_kw struct SBM{
-    T,
-    N,
-    M,
-    P<:Union{Paddy,Nothing},
-    NP<:Union{NonPaddy,Nothing},
-    D<:Union{NonIrrigationDemand,Nothing},
-    L<:Union{NonIrrigationDemand,Nothing},
-    I<:Union{NonIrrigationDemand,Nothing},
-    A<:Union{AllocationLand,Nothing},
-}
-    # Model time step [s]
-    dt::T | "s" | 0 | "none" | "none"
-    # Maximum number of soil layers
-    maxlayers::Int | "-" | 0 | "none" | "none"
-    # number of cells
-    n::Int | "-" | 0 | "none" | "none"
-    # Number of soil layers
-    nlayers::Vector{Int} | "-"
-    # Number of unsaturated soil layers
-    n_unsatlayers::Vector{Int} | "-"
-    # Number of soil layers with vertical hydraulic conductivity value `kv`
-    nlayers_kv::Vector{Int} | "-"
-    # Fraction of river [-]
-    riverfrac::Vector{T} | "-"
-    # Saturated water content (porosity) [-]
-    theta_s::Vector{T} | "-"
-    # Residual water content [-]
-    theta_r::Vector{T} | "-"
-    # Vertical hydraulic conductivity [mm Δt⁻¹] at soil surface
-    kv_0::Vector{T}
-    # Vertical hydraulic conductivity [mm Δt⁻¹] per soil layer
-    kv::Vector{SVector{N,T}} | "-"
-    # Muliplication factor [-] applied to kv_z (vertical flow)
-    kvfrac::Vector{SVector{N,T}} | "-"
-    # Air entry pressure [cm] of soil (Brooks-Corey)
-    hb::Vector{T} | "cm"
-    # Soil thickness [mm]
-    soilthickness::Vector{T} | "mm"
-    # Thickness of soil layers [mm]
-    act_thickl::Vector{SVector{N,T}} | "mm"
-    # Cumulative sum of soil layers [mm], starting at soil surface (0)
-    sumlayers::Vector{SVector{M,T}} | "mm"
-    # Infiltration capacity of the compacted areas [mm Δt⁻¹]
-    infiltcappath::Vector{T}
-    # Soil infiltration capacity [mm Δt⁻¹]
-    infiltcapsoil::Vector{T}
-    # Soil infiltration reduction factor (when soil is frozen) [-]
-    soilinfredu::Vector{T} | "-"
-    # Maximum leakage [mm Δt⁻¹] from saturated zone
-    maxleakage::Vector{T}
-    # Fraction of open water (excluding rivers) [-]
-    waterfrac::Vector{T} | "-"
-    # Fraction of compacted area  [-]
-    pathfrac::Vector{T} | "-"
-    # Rooting depth [mm]
-    rootingdepth::Vector{T} | "mm"
-    # Fraction of the root length density in each soil layer [-]
-    rootfraction::Vector{SVector{N,T}} | "-"
-    # Soil water pressure head h1 of the root water uptake reduction function (Feddes) [cm]
-    h1::Vector{T} | "cm"
-    # Soil water pressure head h2 of the root water uptake reduction function (Feddes) [cm]
-    h2::Vector{T} | "cm"
-    # Soil water pressure head h3_high of the root water uptake reduction function (Feddes) [cm]
-    h3_high::Vector{T} | "cm"
-    # Soil water pressure head h3_low of the root water uptake reduction function (Feddes) [cm]
-    h3_low::Vector{T} | "cm"
-    # Soil water pressure head h4 of the root water uptake reduction function (Feddes) [cm]
-    h4::Vector{T} | "cm"
-    # Calculated soil water pressure head h3 of the root water uptake reduction function (Feddes) [cm]
-    h3::Vector{T} | "cm"
-    # Root water uptake reduction at soil water pressure head h1 (0.0 or 1.0) [-]
-    alpha_h1::Vector{T} | "-"
-    # Controls how roots are linked to water table [-]
-    rootdistpar::Vector{T} | "-"
-    # Parameter [mm] controlling capillary rise
-    cap_hmax::Vector{T} | "mm"
-    # Coefficient [-] controlling capillary rise
-    cap_n::Vector{T} | "-"
-    # Crop coefficient Kc [-]
-    kc::Vector{T} | "-"
-    # Brooks-Corey power coefﬁcient [-] for each soil layer
-    c::Vector{SVector{N,T}} | "-"
-    # Stemflow [mm Δt⁻¹]
-    stemflow::Vector{T}
-    # Throughfall [mm Δt⁻¹]
-    throughfall::Vector{T}
-    # A scaling parameter [mm⁻¹] (controls exponential decline of kv_0)
-    f::Vector{T} | "mm-1"
-    # Depth [mm] from soil surface for which exponential decline of kv_0 is valid
-    z_exp::Vector{T} | "mm"
-    # Depth [mm] from soil surface for which layered profile is valid
-    z_layered::Vector{T} | "mm"
-    # Amount of water in the unsaturated store, per layer [mm]
-    ustorelayerdepth::Vector{SVector{N,T}} | "mm"
-    # Saturated store [mm]
-    satwaterdepth::Vector{T} | "mm"
-    # Pseudo-water table depth [mm] (top of the saturated zone)
-    zi::Vector{T} | "mm"
-    # Soilwater capacity [mm]
-    soilwatercapacity::Vector{T} | "mm"
-    # Canopy storage [mm]
-    canopystorage::Vector{T} | "mm"
-    # Maximum canopy storage [mm]
-    cmax::Vector{T} | "mm"
-    # Canopy gap fraction [-]
-    canopygapfraction::Vector{T} | "-"
-    # Gash interception model parameter, ratio of the average evaporation from the
-    # wet canopy [mm Δt⁻¹] and the average precipitation intensity [mm Δt⁻¹] on a saturated canopy
-    e_r::Vector{T} | "-"
-    # Precipitation [mm Δt⁻¹]
-    precipitation::Vector{T}
-    # Temperature [ᵒC]
-    temperature::Vector{T} | "°C"
-    # Potential reference evapotranspiration [mm Δt⁻¹]
-    potential_evaporation::Vector{T}
-    # Potential transpiration (after subtracting interception from potential_evaporation)
-    pottrans::Vector{T}
-    # Transpiration [mm Δt⁻¹]
-    transpiration::Vector{T}
-    # Actual evaporation from unsaturated store [mm Δt⁻¹]
-    ae_ustore::Vector{T}
-    # Interception loss by evaporation [mm Δt⁻¹]
-    interception::Vector{T}
-    # Soil evaporation from unsaturated and saturated store [mm Δt⁻¹]
-    soilevap::Vector{T}
-    # Soil evaporation from saturated store [mm Δt⁻¹]
-    soilevapsat::Vector{T}
-    # Actual capillary rise [mm Δt⁻¹]
-    actcapflux::Vector{T}
-    # Actual transpiration from saturated store [mm Δt⁻¹]
-    actevapsat::Vector{T}
-    # Total actual evapotranspiration [mm Δt⁻¹]
-    actevap::Vector{T}
-    # Runoff from river based on riverfrac [mm Δt⁻¹]
-    runoff_river::Vector{T}
-    # Runoff from land based on waterfrac [mm Δt⁻¹]
-    runoff_land::Vector{T}
-    # Actual evaporation from open water (land) [mm Δt⁻¹]
-    ae_openw_l::Vector{T}
-    # Actual evaporation from river [mm Δt⁻¹]
-    ae_openw_r::Vector{T}
-    # Net runoff from river [mm Δt⁻¹]
-    net_runoff_river::Vector{T}
-    # Water available for infiltration [mm Δt⁻¹]
-    avail_forinfilt::Vector{T}
-    # Actual infiltration into the unsaturated zone [mm Δt⁻¹]
-    actinfilt::Vector{T}
-    # Actual infiltration non-compacted fraction [mm Δt⁻¹]
-    actinfiltsoil::Vector{T}
-    # Actual infiltration compacted fraction [mm Δt⁻¹]
-    actinfiltpath::Vector{T}
-    # Actual infiltration (compacted and the non-compacted areas) [mm Δt⁻¹]
-    infiltsoilpath::Vector{T}
-    # Infiltration excess water [mm Δt⁻¹]
-    infiltexcess::Vector{T}
-    # Water that cannot infiltrate due to saturated soil (saturation excess) [mm Δt⁻¹]
-    excesswater::Vector{T}
-    # Water exfiltrating during saturation excess conditions [mm Δt⁻¹]
-    exfiltsatwater::Vector{T}
-    # Water exfiltrating from unsaturated store because of change in water table [mm Δt⁻¹]
-    exfiltustore::Vector{T}
-    # Excess water for non-compacted fraction [mm Δt⁻¹]
-    excesswatersoil::Vector{T}
-    # Excess water for compacted fraction [mm Δt⁻¹]
-    excesswaterpath::Vector{T}
-    # Total surface runoff from infiltration and saturation excess (excluding actual open water evaporation) [mm Δt⁻¹]
-    runoff::Vector{T}
-    # Net surface runoff (surface runoff - actual open water evaporation) [mm Δt⁻¹]
-    net_runoff::Vector{T}
-    # Volumetric water content [-] per soil layer (including theta_r and saturated zone)
-    vwc::Vector{SVector{N,T}} | "-"
-    # Volumetric water content [%] per soil layer (including theta_r and saturated zone)
-    vwc_perc::Vector{SVector{N,T}} | "%"
-    # Root water storage [mm] in unsaturated and saturated zone (excluding theta_r)
-    rootstore::Vector{T} | "mm"
-    # Volumetric water content [-] in root zone (including theta_r and saturated zone)
-    vwc_root::Vector{T} | "-"
-    # Volumetric water content [%] in root zone (including theta_r and saturated zone)
-    vwc_percroot::Vector{T} | "%"
-    # Amount of available water in the unsaturated zone [mm]
-    ustoredepth::Vector{T} | "mm"
-    # Downward flux from unsaturated to saturated zone [mm Δt⁻¹]
-    transfer::Vector{T}
-    # Net recharge to saturated store [mm Δt⁻¹]
-    recharge::Vector{T}
-    # Actual leakage from saturated store [mm Δt⁻¹]
-    actleakage::Vector{T}
-    ### Snow parameters ###
-    # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹]
-    cfmax::Vector{T} | "mm ᵒC-1 dt-1"
-    # Threshold temperature for snowfall [ᵒC]
-    tt::Vector{T} | "ᵒC"
-    # Threshold temperature interval length [ᵒC]
-    tti::Vector{T} | "ᵒC"
-    # Threshold temperature for snowmelt [ᵒC]
-    ttm::Vector{T} | "ᵒC"
-    # Water holding capacity as fraction of current snow pack [-]
-    whc::Vector{T} | "-"
-    # Soil temperature smooth factor [-]
-    w_soil::Vector{T} | "-"
-    # Controls soil infiltration reduction factor when soil is frozen [-]
-    cf_soil::Vector{T} | "-"
-    # Snow storage [mm]
-    snow::Vector{T} | "mm"
-    # Liquid water content in the snow pack [mm]
-    snowwater::Vector{T} | "mm"
-    # Snow melt + precipitation as rainfall [mm]
-    rainfallplusmelt::Vector{T} | "mm"
-    # Threshold temperature for snowfall above glacier [ᵒC]
-    g_tt::Vector{T} | "ᵒC"
-    # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹] for glacier
-    g_cfmax::Vector{T} | "mm ᵒC-1 dt-1"
-    # Fraction of the snowpack on top of the glacier converted into ice [Δt⁻¹]
-    g_sifrac::Vector{T} | "dt-1"
-    # Water within the glacier [mm]
-    glacierstore::Vector{T} | "mm"
-    # Fraction covered by a glacier [-]
-    glacierfrac::Vector{T} | "-"
-    # Top soil temperature [ᵒC]
-    tsoil::Vector{T} | "ᵒC"
-    ## Interception related to leaf_area_index climatology ###
-    # Specific leaf storage [mm]
-    sl::Vector{T} | "mm"
-    # Storage woody part of vegetation [mm]
-    swood::Vector{T} | "mm"
-    # Extinction coefficient [-] (to calculate canopy gap fraction)
-    kext::Vector{T} | "-"
-    # Leaf area index [m² m⁻²]
-    leaf_area_index::Vector{T} | "m2 m-2"
-    # Water level land [mm]
-    waterlevel_land::Vector{T} | "mm"
-    # Water level river [mm]
-    waterlevel_river::Vector{T} | "mm"
-    # Total water storage (excluding floodplain volume, lakes and reservoirs) [mm]
-    total_storage::Vector{T} | "mm"
-    # Water demand structs (of arrays)
-    paddy::P | "-" | 0
-    nonpaddy::NP | "-" | 0
-    domestic::D | "-" | 0
-    livestock::L | "-" | 0
-    industry::I | "-" | 0
-    allocation::A | "-" | 0
-
-
-    function SBM{T,N,M,P,NP,D,L,I,A}(args...) where {T,N,M,P,NP,D,L,I,A}
-        equal_size_vectors(args)
-        return new(args...)
-    end
+"Land hydrology model with SBM soil model"
+@with_kw struct LandHydrologySBM{T, D, A}
+    atmospheric_forcing::AtmosphericForcing{T}
+    vegetation_parameter_set::VegetationParameters{T}
+    interception::AbstractInterceptionModel{T}
+    snow::AbstractSnowModel{T}
+    glacier::AbstractGlacierModel{T}
+    runoff::AbstractRunoffModel{T}
+    soil::SbmSoilModel
+    demand::D
+    allocation::A
+    dt::T
 end
 
-
-function initialize_canopy(nc, config, inds)
+"Initialize land hydrology model with SBM soil model"
+function LandHydrologySBM(nc, config, riverfrac, inds)
+    dt = Second(config.timestepsecs)
     n = length(inds)
-    # if leaf area index climatology provided use sl, swood and kext to calculate cmax, e_r and canopygapfraction
-    if haskey(config.input.vertical, "leaf_area_index")
-        # TODO confirm if leaf area index climatology is present in the netCDF
-        sl = ncread(
-            nc,
-            config,
-            "vertical.specific_leaf";
-            optional = false,
-            sel = inds,
-            type = Float,
-        )
-        swood = ncread(
-            nc,
-            config,
-            "vertical.storage_wood";
-            optional = false,
-            sel = inds,
-            type = Float,
-        )
-        kext =
-            ncread(nc, config, "vertical.kext"; optional = false, sel = inds, type = Float)
-        cmax = fill(mv, n)
-        e_r = fill(mv, n)
-        canopygapfraction = fill(mv, n)
-    else
-        sl = fill(mv, n)
-        swood = fill(mv, n)
-        kext = fill(mv, n)
-        # cmax, e_r, canopygapfraction only required when leaf area index climatology not provided
-        cmax = ncread(nc, config, "vertical.cmax"; sel = inds, defaults = 1.0, type = Float)
-        e_r =
-            ncread(nc, config, "vertical.eoverr"; sel = inds, defaults = 0.1, type = Float)
-        canopygapfraction = ncread(
-            nc,
-            config,
-            "vertical.canopygapfraction";
-            sel = inds,
-            defaults = 0.1,
-            type = Float,
-        )
-    end
-    return cmax, e_r, canopygapfraction, sl, swood, kext
-end
-
-function initialize_sbm(nc, config, riverfrac, inds)
 
-    dt = Second(config.timestepsecs)
-    config_thicknesslayers = get(config.model, "thicknesslayers", Float[])
-    ksat_profile = get(config.input.vertical, "ksat_profile", "exponential")::String
-    if length(config_thicknesslayers) > 0
-        thicknesslayers = SVector(Tuple(push!(Float.(config_thicknesslayers), mv)))
-        sumlayers = pushfirst(cumsum(thicknesslayers), 0.0)
-        maxlayers = length(thicknesslayers) # max number of soil layers
+    atmospheric_forcing = AtmosphericForcing(n)
+    vegetation_parameter_set = VegetationParameters(nc, config, inds)
+    if dt >= Hour(23)
+        interception_model =
+            GashInterceptionModel(nc, config, inds, vegetation_parameter_set)
     else
-        maxlayers = 1
+        interception_model = RutterInterceptionModel(vegetation_parameter_set, n)
     end
 
-    n = length(inds)
-
-    cfmax =
-        ncread(
-            nc,
-            config,
-            "vertical.cfmax";
-            sel = inds,
-            defaults = 3.75653,
-            type = Float,
-        ) .* (dt / basetimestep)
-    tt = ncread(nc, config, "vertical.tt"; sel = inds, defaults = 0.0, type = Float)
-    tti = ncread(nc, config, "vertical.tti"; sel = inds, defaults = 1.0, type = Float)
-    ttm = ncread(nc, config, "vertical.ttm"; sel = inds, defaults = 0.0, type = Float)
-    whc = ncread(nc, config, "vertical.whc"; sel = inds, defaults = 0.1, type = Float)
-    w_soil =
-        ncread(
-            nc,
-            config,
-            "vertical.w_soil";
-            sel = inds,
-            defaults = 0.1125,
-            type = Float,
-        ) .* (dt / basetimestep)
-    cf_soil =
-        ncread(nc, config, "vertical.cf_soil"; sel = inds, defaults = 0.038, type = Float)
-    # glacier parameters
-    g_tt = ncread(
-        nc,
-        config,
-        "vertical.g_tt";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        fill = 0.0,
-    )
-    g_cfmax =
-        ncread(
-            nc,
-            config,
-            "vertical.g_cfmax";
-            sel = inds,
-            defaults = 3.0,
-            type = Float,
-            fill = 0.0,
-        ) .* (dt / basetimestep)
-    g_sifrac =
-        ncread(
-            nc,
-            config,
-            "vertical.g_sifrac";
-            sel = inds,
-            defaults = 0.001,
-            type = Float,
-            fill = 0.0,
-        ) .* (dt / basetimestep)
-    glacierfrac = ncread(
-        nc,
-        config,
-        "vertical.glacierfrac";
-        sel = inds,
-        defaults = 0.0,
-        type = Float,
-        fill = 0.0,
-    )
-    glacierstore = ncread(
-        nc,
-        config,
-        "vertical.glacierstore";
-        sel = inds,
-        defaults = 5500.0,
-        type = Float,
-        fill = 0.0,
-    )
-    # soil parameters
-    theta_s =
-        ncread(nc, config, "vertical.theta_s"; sel = inds, defaults = 0.6, type = Float)
-    theta_r =
-        ncread(nc, config, "vertical.theta_r"; sel = inds, defaults = 0.01, type = Float)
-    kv_0 =
-        ncread(nc, config, "vertical.kv_0"; sel = inds, defaults = 3000.0, type = Float) .*
-        (dt / basetimestep)
-    f = ncread(nc, config, "vertical.f"; sel = inds, defaults = 0.001, type = Float)
-    hb = ncread(nc, config, "vertical.hb"; sel = inds, defaults = -10.0, type = Float)
-    h1 = ncread(nc, config, "vertical.h1"; sel = inds, defaults = 0.0, type = Float)
-    h2 = ncread(nc, config, "vertical.h2"; sel = inds, defaults = -100.0, type = Float)
-    h3_high =
-        ncread(nc, config, "vertical.h3_high"; sel = inds, defaults = -400.0, type = Float)
-    h3_low =
-        ncread(nc, config, "vertical.h3_low"; sel = inds, defaults = -1000.0, type = Float)
-    h4 = ncread(nc, config, "vertical.h4"; sel = inds, defaults = -15849.0, type = Float)
-    alpha_h1 =
-        ncread(nc, config, "vertical.alpha_h1"; sel = inds, defaults = 1.0, type = Float)
-    soilthickness = ncread(
-        nc,
-        config,
-        "vertical.soilthickness";
-        sel = inds,
-        defaults = 2000.0,
-        type = Float,
-    )
-    infiltcappath =
-        ncread(
-            nc,
-            config,
-            "vertical.infiltcappath";
-            sel = inds,
-            defaults = 10.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    infiltcapsoil =
-        ncread(
-            nc,
-            config,
-            "vertical.infiltcapsoil";
-            sel = inds,
-            defaults = 100.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    maxleakage =
-        ncread(
-            nc,
-            config,
-            "vertical.maxleakage";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-
-    c = ncread(
-        nc,
-        config,
-        "vertical.c";
-        sel = inds,
-        defaults = 10.0,
-        type = Float,
-        dimname = :layer,
-    )
-    if size(c, 1) != maxlayers
-        parname = param(config.input.vertical, "c")
-        size1 = size(c, 1)
-        error("$parname needs a layer dimension of size $maxlayers, but is $size1")
-    end
-    kvfrac = ncread(
-        nc,
-        config,
-        "vertical.kvfrac";
-        sel = inds,
-        defaults = 1.0,
-        type = Float,
-        dimname = :layer,
-    )
-    if size(kvfrac, 1) != maxlayers
-        parname = param(config.input, "vertical.kvfrac")
-        size1 = size(kvfrac, 1)
-        error("$parname needs a layer dimension of size $maxlayers, but is $size1")
+    modelsnow = get(config.model, "snow", false)::Bool
+    if modelsnow
+        snow_model = SnowHbvModel(nc, config, inds, dt)
+    else
+        snow_model = NoSnowModel{Float}()
     end
-
-    # fraction open water and compacted area (land cover)
-    waterfrac =
-        ncread(nc, config, "vertical.waterfrac"; sel = inds, defaults = 0.0, type = Float)
-    pathfrac =
-        ncread(nc, config, "vertical.pathfrac"; sel = inds, defaults = 0.01, type = Float)
-
-    # vegetation parameters
-    rootingdepth = ncread(
-        nc,
-        config,
-        "vertical.rootingdepth";
-        sel = inds,
-        defaults = 750.0,
-        type = Float,
-    )
-    # correct rooting depth for soilthickness
-    rootingdepth = @. min(0.99 * soilthickness, rootingdepth)
-    rootdistpar = ncread(
-        nc,
-        config,
-        "vertical.rootdistpar";
-        sel = inds,
-        defaults = -500.0,
-        type = Float,
-    )
-    cap_hmax =
-        ncread(nc, config, "vertical.cap_hmax"; sel = inds, defaults = 2000.0, type = Float)
-    cap_n = ncread(nc, config, "vertical.cap_n"; sel = inds, defaults = 2.0, type = Float)
-    kc = ncread(
-        nc,
-        config,
-        "vertical.kc";
-        alias = "vertical.et_reftopot",
-        sel = inds,
-        defaults = 1.0,
-        type = Float,
-    )
-    if haskey(config.input.vertical, "et_reftopot")
+    modelglacier = get(config.model, "glacier", false)::Bool
+    if modelsnow && modelglacier
+        glacier_bc = SnowStateBC{Float}(; snow_storage = snow_model.variables.snow_storage)
+        glacier_model = GlacierHbvModel(nc, config, inds, dt, glacier_bc)
+    elseif modelsnow == false && modelglacier == true
         @warn string(
-            "The `et_reftopot` key in `[input.vertical]` is now called ",
-            "`kc`. Please update your TOML file.",
+            "Glacier processes can be modelled when snow modelling is enabled. To include ",
+            "glacier modelling, set `snow` to `true` in the Model section of the TOML file.",
         )
-    end
-
-    cmax, e_r, canopygapfraction, sl, swood, kext = initialize_canopy(nc, config, inds)
-
-    theta_e = theta_s .- theta_r
-    soilwatercapacity = soilthickness .* theta_e
-    satwaterdepth = 0.85 .* soilwatercapacity # cold state value for satwaterdepth
-    zi = max.(0.0, soilthickness .- satwaterdepth ./ theta_e) # cold state value for zi
-
-    # these are filled in the loop below
-    # TODO see if we can replace this approach
-    nlayers = zeros(Int, n)
-    n_unsatlayers = zeros(Int, n)
-    act_thickl = zeros(Float, maxlayers, n)
-    s_layers = zeros(Float, maxlayers + 1, n)
-
-    for i = 1:n
-        if length(config_thicknesslayers) > 0
-            act_thickl_, nlayers_ =
-                set_layerthickness(soilthickness[i], sumlayers, thicknesslayers)
-            s_layers_ = pushfirst(cumsum(act_thickl_), 0.0)
-
-            nlayers[i] = nlayers_
-            act_thickl[:, i] = act_thickl_
-            s_layers[:, i] = s_layers_
-            _, n_unsatlayers[i] = set_layerthickness(zi[i], sumlayers, thicknesslayers)
-        else
-            nlayers[i] = 1
-            act_thickl[:, i] = SVector(soilthickness[i])
-            s_layers[:, i] = pushfirst(cumsum(SVector(soilthickness[i])), 0.0)
-        end
-    end
-
-    if length(config_thicknesslayers) > 0
-        # root fraction read from nc file, in case of multiple soil layers and TOML file
-        # includes "vertical.rootfraction"
-        if haskey(config.input.vertical, "rootfraction")
-            rootfraction = ncread(
-                nc,
-                config,
-                "vertical.rootfraction";
-                sel = inds,
-                optional = false,
-                type = Float,
-                dimname = :layer,
-            )
-        else
-            # default root fraction in case of multiple soil layers
-            rootfraction = zeros(Float, maxlayers, n)
-            for i = 1:n
-                if rootingdepth[i] > 0.0
-                    for k = 1:maxlayers
-                        if (rootingdepth[i] - s_layers[k, i]) >= act_thickl[k, i]
-                            rootfraction[k, i] = act_thickl[k, i] / rootingdepth[i]
-                        else
-                            rootfraction[k, i] =
-                                max(rootingdepth[i] - s_layers[k, i], 0.0) / rootingdepth[i]
-                        end
-                    end
-                end
-            end
-        end
+        glacier_model = NoGlacierModel{Float}()
     else
-        # for the case of 1 soil layer
-        rootfraction = ones(Float, maxlayers, n)
+        glacier_model = NoGlacierModel{Float}()
     end
+    runoff_model = OpenWaterRunoff(nc, config, inds, riverfrac)
 
-    # needed for derived parameters below
-    act_thickl = svectorscopy(act_thickl, Val{maxlayers}())
-
-    # copied to array of sarray below
-    vwc = fill(mv, maxlayers, n)
-    vwc_perc = fill(mv, maxlayers, n)
-    sumlayers = svectorscopy(s_layers, Val{maxlayers + 1}())
-
-    # ksat profiles
-    if ksat_profile == "exponential"
-        z_exp = soilthickness
-        z_layered = fill(mv, n)
-        kv = fill(mv, (maxlayers, n))
-        nlayers_kv = fill(0, n)
-    elseif ksat_profile == "exponential_constant"
-        z_exp =
-            ncread(nc, config, "vertical.z_exp"; optional = false, sel = inds, type = Float)
-        z_layered = fill(mv, n)
-        kv = fill(mv, (maxlayers, n))
-        nlayers_kv = fill(0, n)
-    elseif ksat_profile == "layered" || ksat_profile == "layered_exponential"
-        z_exp = fill(mv, n)
-        kv =
-            ncread(
-                nc,
-                config,
-                "vertical.kv";
-                sel = inds,
-                defaults = 1000.0,
-                type = Float,
-                dimname = :layer,
-            ) .* (dt / basetimestep)
-        if size(kv, 1) != maxlayers
-            parname = param(config.input.vertical, "kv")
-            size1 = size(kv, 1)
-            error("$parname needs a layer dimension of size $maxlayers, but is $size1")
-        end
-        if ksat_profile == "layered"
-            z_layered = soilthickness
-            nlayers_kv = nlayers
-        else
-            z_layered = ncread(
-                nc,
-                config,
-                "vertical.z_layered";
-                optional = false,
-                sel = inds,
-                type = Float,
-            )
-            nlayers_kv = fill(0, n)
-            for i in eachindex(nlayers_kv)
-                layers = @view sumlayers[i][2:nlayers[i]]
-                _, k = findmin(abs.(z_layered[i] .- layers))
-                nlayers_kv[i] = k
-                z_layered[i] = layers[k]
-            end
-        end
-    else
-        error("""An unknown "ksat_profile" is specified in the TOML file ($ksat_profile).
-              This should be "exponential", "exponential_constant", "layered" or
-              "layered_exponential".
-              """)
-    end
+    soil_model = SbmSoilModel(nc, config, vegetation_parameter_set, inds, dt)
+    @. vegetation_parameter_set.rootingdepth = min(
+        soil_model.parameters.soilthickness * 0.99,
+        vegetation_parameter_set.rootingdepth,
+    )
 
-    # water demand and irrigation options
     do_water_demand = haskey(config.model, "water_demand")
-    domestic = do_water_demand ? get(config.model.water_demand, "domestic", false) : false
-    industry = do_water_demand ? get(config.model.water_demand, "industry", false) : false
-    livestock = do_water_demand ? get(config.model.water_demand, "livestock", false) : false
-    paddy = do_water_demand ? get(config.model.water_demand, "paddy", false) : false
-    nonpaddy = do_water_demand ? get(config.model.water_demand, "nonpaddy", false) : false
-
-    sbm = SBM(
+    allocation =
+        do_water_demand ? AllocationLand(nc, config, inds) : NoAllocationLand{Float}()
+    demand = do_water_demand ? Demand(nc, config, inds, dt) : NoDemand{Float}()
+
+    args = (demand, allocation)
+    land_hydrology_model = LandHydrologySBM{Float, typeof.(args)...}(;
+        atmospheric_forcing = atmospheric_forcing,
+        vegetation_parameter_set = vegetation_parameter_set,
+        interception = interception_model,
+        snow = snow_model,
+        glacier = glacier_model,
+        runoff = runoff_model,
+        soil = soil_model,
+        demand = demand,
+        allocation = allocation,
         dt = tosecond(dt),
-        maxlayers = maxlayers,
-        n = n,
-        nlayers = nlayers,
-        n_unsatlayers = n_unsatlayers,
-        nlayers_kv = nlayers_kv,
-        riverfrac = riverfrac,
-        theta_s = theta_s,
-        theta_r = theta_r,
-        kv_0 = kv_0,
-        kv = svectorscopy(kv, Val{maxlayers}()),
-        kvfrac = svectorscopy(kvfrac, Val{maxlayers}()),
-        hb = hb,
-        h1 = h1,
-        h2 = h2,
-        h3_high = h3_high,
-        h3_low = h3_low,
-        h4 = h4,
-        h3 = fill(mv, n),
-        alpha_h1 = alpha_h1,
-        soilthickness = soilthickness,
-        act_thickl = act_thickl,
-        sumlayers = sumlayers,
-        infiltcappath = infiltcappath,
-        infiltcapsoil = infiltcapsoil,
-        soilinfredu = fill(Float(1), n),
-        maxleakage = maxleakage,
-        waterfrac = max.(waterfrac .- riverfrac, Float(0.0)),
-        pathfrac = pathfrac,
-        rootingdepth = rootingdepth,
-        rootfraction = svectorscopy(rootfraction, Val{maxlayers}()),
-        rootdistpar = rootdistpar,
-        cap_hmax = cap_hmax,
-        cap_n = cap_n,
-        kc = kc,
-        c = svectorscopy(c, Val{maxlayers}()),
-        stemflow = fill(mv, n),
-        throughfall = fill(mv, n),
-        f = f,
-        z_exp = z_exp,
-        z_layered = z_layered,
-        ustorelayerdepth = zero(act_thickl),
-        satwaterdepth = satwaterdepth,
-        zi = zi,
-        soilwatercapacity = soilwatercapacity,
-        canopystorage = zeros(Float, n),
-        cmax = cmax,
-        canopygapfraction = canopygapfraction,
-        e_r = e_r,
-        precipitation = fill(mv, n),
-        temperature = fill(mv, n),
-        potential_evaporation = fill(mv, n),
-        pottrans = fill(mv, n),
-        transpiration = fill(mv, n),
-        ae_ustore = fill(mv, n),
-        interception = fill(mv, n),
-        soilevap = fill(mv, n),
-        soilevapsat = fill(mv, n),
-        actcapflux = fill(mv, n),
-        actevapsat = fill(mv, n),
-        actevap = fill(mv, n),
-        runoff_river = fill(mv, n),
-        runoff_land = fill(mv, n),
-        ae_openw_l = fill(mv, n),
-        ae_openw_r = fill(mv, n),
-        avail_forinfilt = fill(mv, n),
-        actinfilt = fill(mv, n),
-        actinfiltsoil = fill(mv, n),
-        actinfiltpath = fill(mv, n),
-        infiltsoilpath = fill(mv, n),
-        infiltexcess = fill(mv, n),
-        excesswater = fill(mv, n),
-        exfiltsatwater = fill(mv, n),
-        exfiltustore = fill(mv, n),
-        excesswatersoil = fill(mv, n),
-        excesswaterpath = fill(mv, n),
-        runoff = fill(mv, n),
-        net_runoff = fill(mv, n),
-        net_runoff_river = fill(mv, n),
-        vwc = svectorscopy(vwc, Val{maxlayers}()),
-        vwc_perc = svectorscopy(vwc_perc, Val{maxlayers}()),
-        rootstore = fill(mv, n),
-        vwc_root = fill(mv, n),
-        vwc_percroot = fill(mv, n),
-        ustoredepth = fill(mv, n),
-        transfer = fill(mv, n),
-        recharge = fill(mv, n),
-        actleakage = fill(mv, n),
-        # snow parameters
-        cfmax = cfmax,
-        tt = tt,
-        tti = tti,
-        ttm = ttm,
-        whc = whc,
-        w_soil = w_soil,
-        cf_soil = cf_soil,
-        snow = zeros(Float, n),
-        snowwater = zeros(Float, n),
-        rainfallplusmelt = fill(mv, n),
-        tsoil = fill(Float(10.0), n),
-        # glacier parameters
-        g_tt = g_tt,
-        g_sifrac = g_sifrac,
-        g_cfmax = g_cfmax,
-        glacierstore = glacierstore,
-        glacierfrac = glacierfrac,
-        # Interception related to climatology (leaf_area_index)
-        sl = sl,
-        swood = swood,
-        kext = kext,
-        leaf_area_index = fill(mv, n),
-        # water level land and river domain
-        waterlevel_land = fill(mv, n),
-        waterlevel_river = zeros(Float, n), #set to zero to account for cells outside river domain
-        total_storage = zeros(Float, n), # Set the total water storage from initialized values
-        # water demand
-        paddy = paddy ? initialize_paddy(nc, config, inds, dt) : nothing,
-        nonpaddy = nonpaddy ? initialize_nonpaddy(nc, config, inds, dt) : nothing,
-        domestic = domestic ? initialize_domestic_demand(nc, config, inds, dt) : nothing,
-        industry = industry ? initialize_industry_demand(nc, config, inds, dt) : nothing,
-        livestock = livestock ? initialize_livestock_demand(nc, config, inds, dt) : nothing,
-        allocation = do_water_demand ? initialize_allocation_land(nc, config, inds) :
-                     nothing,
     )
-
-    return sbm
-
-end
-
-
-function update_until_snow(sbm::SBM, config)
-
-    do_lai = haskey(config.input.vertical, "leaf_area_index")
-    modelglacier = get(config.model, "glacier", false)::Bool
-    modelsnow = get(config.model, "snow", false)::Bool
-
-    threaded_foreach(1:sbm.n, basesize = 1000) do i
-        if do_lai
-            cmax = sbm.sl[i] * sbm.leaf_area_index[i] + sbm.swood[i]
-            canopygapfraction = exp(-sbm.kext[i] * sbm.leaf_area_index[i])
-            canopyfraction = 1.0 - canopygapfraction
-            ewet = canopyfraction * sbm.potential_evaporation[i] * sbm.kc[i]
-            e_r =
-                sbm.precipitation[i] > 0.0 ?
-                min(0.25, ewet / max(0.0001, canopyfraction * sbm.precipitation[i])) : 0.0
-        else
-            cmax = sbm.cmax[i]
-            canopygapfraction = sbm.canopygapfraction[i]
-            e_r = sbm.e_r[i]
-        end
-
-        canopy_potevap =
-            sbm.kc[i] * sbm.potential_evaporation[i] * (1.0 - canopygapfraction)
-        if Second(sbm.dt) >= Hour(23)
-            throughfall, interception, stemflow, canopystorage = rainfall_interception_gash(
-                cmax,
-                e_r,
-                canopygapfraction,
-                sbm.precipitation[i],
-                sbm.canopystorage[i],
-                canopy_potevap,
-            )
-            pottrans = max(0.0, canopy_potevap - interception) # now in mm
-        else
-            netinterception, throughfall, stemflow, leftover, interception, canopystorage =
-                rainfall_interception_modrut(
-                    sbm.precipitation[i],
-                    canopy_potevap,
-                    sbm.canopystorage[i],
-                    canopygapfraction,
-                    cmax,
-                )
-            pottrans = max(0.0, leftover)  # now in mm
-        end
-
-        if modelsnow
-            tsoil = sbm.tsoil[i] + sbm.w_soil[i] * (sbm.temperature[i] - sbm.tsoil[i])
-            snow, snowwater, snowmelt, rainfallplusmelt, snowfall = snowpack_hbv(
-                sbm.snow[i],
-                sbm.snowwater[i],
-                throughfall + stemflow,
-                sbm.temperature[i],
-                sbm.tti[i],
-                sbm.tt[i],
-                sbm.ttm[i],
-                sbm.cfmax[i],
-                sbm.whc[i],
-            )
-        end
-
-        h3 = feddes_h3(sbm.h3_high[i], sbm.h3_low[i], pottrans, Second(sbm.dt))
-
-        # update the outputs and states
-        sbm.e_r[i] = e_r
-        sbm.cmax[i] = cmax
-        sbm.canopygapfraction[i] = canopygapfraction
-        sbm.canopystorage[i] = canopystorage
-        sbm.interception[i] = interception
-        sbm.stemflow[i] = stemflow
-        sbm.throughfall[i] = throughfall
-        sbm.pottrans[i] = pottrans
-        sbm.h3[i] = h3
-        if modelsnow
-            sbm.snow[i] = snow
-            sbm.snowwater[i] = snowwater
-            sbm.tsoil[i] = tsoil
-            sbm.rainfallplusmelt[i] = rainfallplusmelt
-        end
-    end
+    return land_hydrology_model
 end
 
-function update_until_recharge(sbm::SBM, config)
-
-    # start dummy variables (should be generated from model reader and from Config.jl TOML)
-    soilinfreduction = get(config.model, "soilinfreduction", false)::Bool
-    modelglacier = get(config.model, "glacier", false)::Bool
-    modelsnow = get(config.model, "snow", false)::Bool
-    transfermethod = get(config.model, "transfermethod", false)::Bool
-    ust = get(config.model, "whole_ust_available", false)::Bool # should be removed from optional setting and code?
-    ksat_profile = get(config.input.vertical, "ksat_profile", "exponential")::String
-
-    threaded_foreach(1:sbm.n, basesize = 250) do i
-        if modelsnow
-            rainfallplusmelt = sbm.rainfallplusmelt[i]
-            if modelglacier
-                # Run Glacier module and add the snowpack on-top of it.
-                # Estimate the fraction of snow turned into ice (HBV-light).
-                # Estimate glacier melt.
-
-                snow, _, glacierstore, glaciermelt = glacier_hbv(
-                    sbm.glacierfrac[i],
-                    sbm.glacierstore[i],
-                    sbm.snow[i],
-                    sbm.temperature[i],
-                    sbm.g_tt[i],
-                    sbm.g_cfmax[i],
-                    sbm.g_sifrac[i],
-                    Second(sbm.dt),
-                )
-                # Convert to mm per grid cell and add to snowmelt
-                glaciermelt = glaciermelt * sbm.glacierfrac[i]
-                rainfallplusmelt = rainfallplusmelt + glaciermelt
-
-            end
-        else
-            rainfallplusmelt = sbm.stemflow[i] + sbm.throughfall[i]
-        end
-
-        avail_forinfilt = rainfallplusmelt
-        ustoredepth = sum(@view sbm.ustorelayerdepth[i][1:sbm.nlayers[i]])
-
-        runoff_river = min(1.0, sbm.riverfrac[i]) * avail_forinfilt
-        runoff_land = min(1.0, sbm.waterfrac[i]) * avail_forinfilt
-        if !isnothing(sbm.paddy) || !isnothing(sbm.nonpaddy)
-            avail_forinfilt = avail_forinfilt + sbm.allocation.irri_alloc[i]
-        end
-        avail_forinfilt = max(avail_forinfilt - runoff_river - runoff_land, 0.0)
-
-        ae_openw_r = min(
-            sbm.waterlevel_river[i] * sbm.riverfrac[i],
-            sbm.riverfrac[i] * sbm.potential_evaporation[i],
-        )
-        ae_openw_l = min(
-            sbm.waterlevel_land[i] * sbm.waterfrac[i],
-            sbm.waterfrac[i] * sbm.potential_evaporation[i],
-        )
-
-        # evap available for soil evaporation
-        soilevap_fraction = max(
-            sbm.canopygapfraction[i] - sbm.riverfrac[i] - sbm.waterfrac[i] -
-            sbm.glacierfrac[i],
-            0.0,
-        )
-        potsoilevap = soilevap_fraction * sbm.potential_evaporation[i]
-
-        if !isnothing(sbm.paddy) && sbm.paddy.irrigation_areas[i]
-            evap_paddy_water = min(sbm.paddy.h[i], potsoilevap)
-            sbm.paddy.h[i] -= evap_paddy_water
-            potsoilevap -= evap_paddy_water
-            avail_forinfilt += sbm.paddy.h[i] # allow infiltration of paddy water
-        else
-            evap_paddy_water = 0.0
-        end
-
-        # Calculate the initial capacity of the unsaturated store
-        ustorecapacity = sbm.soilwatercapacity[i] - sbm.satwaterdepth[i] - ustoredepth
-
-        # Calculate the infiltration flux into the soil column
-        infiltsoilpath,
-        infiltsoil,
-        infiltpath,
-        soilinf,
-        pathinf,
-        infiltexcess,
-        soilinfredu = infiltration(
-            avail_forinfilt,
-            sbm.pathfrac[i],
-            sbm.cf_soil[i],
-            sbm.tsoil[i],
-            sbm.infiltcapsoil[i],
-            sbm.infiltcappath[i],
-            ustorecapacity,
-            modelsnow,
-            soilinfreduction,
+"Update land hydrology model with SBM soil model for a single timestep"
+function update!(model::LandHydrologySBM, lateral, network, config)
+    do_water_demand = haskey(config.model, "water_demand")::Bool
+    (;
+        glacier,
+        snow,
+        interception,
+        runoff,
+        soil,
+        demand,
+        allocation,
+        atmospheric_forcing,
+        dt,
+    ) = model
+
+    update!(interception, atmospheric_forcing)
+
+    update_boundary_conditions!(snow, (; interception))
+    update!(snow, atmospheric_forcing)
+
+    # lateral snow transport
+    if get(config.model, "masswasting", false)::Bool
+        lateral_snow_transport!(
+            snow.variables.snow_storage,
+            snow.variables.snow_water,
+            network.land.slope,
+            network.land,
         )
-
-
-        usl, n_usl = set_layerthickness(sbm.zi[i], sbm.sumlayers[i], sbm.act_thickl[i])
-        z = cumsum(usl)
-        usld = sbm.ustorelayerdepth[i]
-
-        ast = 0.0
-        soilevapunsat = 0.0
-        if n_usl > 0
-            # Using the surface infiltration rate, calculate the flow rate between the
-            # different soil layers that contain unsaturated storage assuming gravity
-            # based flow only, estimate the gravity based flux rate to the saturated zone
-            # (ast) and the updated unsaturated storage for each soil layer.
-            if transfermethod && sbm.maxlayers == 1
-                ustorelayerdepth = sbm.ustorelayerdepth[i][1] + infiltsoilpath
-                kv_z = hydraulic_conductivity_at_depth(sbm, sbm.zi[i], i, 1, ksat_profile)
-                ustorelayerdepth, ast = unsatzone_flow_sbm(
-                    ustorelayerdepth,
-                    sbm.soilwatercapacity[i],
-                    sbm.satwaterdepth[i],
-                    kv_z,
-                    usl[1],
-                    sbm.theta_s[i],
-                    sbm.theta_r[i],
-                )
-                usld = setindex(usld, ustorelayerdepth, 1)
-            else
-                for m = 1:n_usl
-                    l_sat = usl[m] * (sbm.theta_s[i] - sbm.theta_r[i])
-                    kv_z = hydraulic_conductivity_at_depth(sbm, z[m], i, m, ksat_profile)
-                    ustorelayerdepth =
-                        m == 1 ? sbm.ustorelayerdepth[i][m] + infiltsoilpath :
-                        sbm.ustorelayerdepth[i][m] + ast
-                    ustorelayerdepth, ast =
-                        unsatzone_flow_layer(ustorelayerdepth, kv_z, l_sat, sbm.c[i][m])
-                    usld = setindex(usld, ustorelayerdepth, m)
-                end
-            end
-
-            # then evapotranspiration from layers
-            # Calculate saturation deficit
-            saturationdeficit = sbm.soilwatercapacity[i] - sbm.satwaterdepth[i]
-
-            # First calculate the evaporation of unsaturated storage into the
-            # atmosphere from the upper layer.
-            if sbm.maxlayers == 1
-                soilevapunsat =
-                    potsoilevap * min(1.0, saturationdeficit / sbm.soilwatercapacity[i])
-            else
-                # In case only the most upper soil layer contains unsaturated storage
-                if n_usl == 1
-                    # Check if groundwater level lies below the surface
-                    soilevapunsat =
-                        potsoilevap *
-                        min(1.0, usld[1] / (sbm.zi[i] * (sbm.theta_s[i] - sbm.theta_r[i])))
-                else
-                    # In case first layer contains no saturated storage
-                    soilevapunsat =
-                        potsoilevap *
-                        min(1.0, usld[1] / (usl[1] * ((sbm.theta_s[i] - sbm.theta_r[i]))))
-                end
-            end
-            # Ensure that the unsaturated evaporation rate does not exceed the
-            # available unsaturated moisture
-            soilevapunsat = min(soilevapunsat, usld[1])
-            # Update the additional atmospheric demand
-            potsoilevap = potsoilevap - soilevapunsat
-            usld = setindex(usld, usld[1] - soilevapunsat, 1)
-        end
-        transfer = ast
-
-        if sbm.maxlayers == 1
-            soilevapsat = 0.0
-        else
-            if n_usl == 0 || n_usl == 1
-                soilevapsat =
-                    potsoilevap *
-                    min(1.0, (sbm.act_thickl[i][1] - sbm.zi[i]) / sbm.act_thickl[i][1])
-                soilevapsat = min(
-                    soilevapsat,
-                    (sbm.act_thickl[i][1] - sbm.zi[i]) * (sbm.theta_s[i] - sbm.theta_r[i]),
-                )
-            else
-                soilevapsat = 0.0
-            end
-        end
-        soilevap = soilevapunsat + soilevapsat
-        satwaterdepth = sbm.satwaterdepth[i] - soilevapsat
-
-        # actual transpiration, first from ustore, potential transpiration is partitioned over
-        # depth based on the rootfraction
-        actevapustore = 0.0
-        rootfraction_unsat = 0.0
-        for k = 1:n_usl
-            vwc = max(usld[k] / usl[k], Float(0.0000001))
-            head = head_brooks_corey(
-                vwc,
-                sbm.theta_s[i],
-                sbm.theta_r[i],
-                sbm.c[i][k],
-                sbm.hb[i],
-            )
-            alpha = rwu_reduction_feddes(
-                head,
-                sbm.h1[i],
-                sbm.h2[i],
-                sbm.h3[i],
-                sbm.h4[i],
-                sbm.alpha_h1[i],
-            )
-            # availcap is fraction of soil layer containing roots
-            # if `ust` is `true`, the whole unsaturated store is available for transpiration
-            if ust
-                availcap = usld[k] * 0.99
-            else
-                availcap =
-                    min(1.0, max(0.0, (sbm.rootingdepth[i] - sbm.sumlayers[i][k]) / usl[k]))
-            end
-            maxextr = usld[k] * availcap
-            # the rootfraction is valid for the root length in a soil layer, if zi decreases the root length
-            # the rootfraction needs to be adapted
-            if k == n_usl && sbm.zi[i] < sbm.rootingdepth[i]
-                rootlength =
-                    min(sbm.act_thickl[i][k], sbm.rootingdepth[i] - sbm.sumlayers[i][k])
-                rootfraction_act = sbm.rootfraction[i][k] * (usl[k] / rootlength)
-            else
-                rootfraction_act = sbm.rootfraction[i][k]
-            end
-            actevapustore_layer = min(alpha * rootfraction_act * sbm.pottrans[i], maxextr)
-            rootfraction_unsat = rootfraction_unsat + rootfraction_act
-            ustorelayerdepth = usld[k] - actevapustore_layer
-            actevapustore = actevapustore + actevapustore_layer
-            usld = setindex(usld, ustorelayerdepth, k)
-        end
-
-        # transpiration from saturated store
-        wetroots = scurve(sbm.zi[i], sbm.rootingdepth[i], Float(1.0), sbm.rootdistpar[i])
-        alpha = rwu_reduction_feddes(
-            Float(0.0),
-            sbm.h1[i],
-            sbm.h2[i],
-            sbm.h3[i],
-            sbm.h4[i],
-            sbm.alpha_h1[i],
-        )
-        # include remaining root fraction if rooting depth is below water table zi
-        if sbm.zi[i] >= sbm.rootingdepth[i]
-            f_roots = wetroots
-            restevap = sbm.pottrans[i] - actevapustore
-        else
-            f_roots = wetroots * (1.0 - rootfraction_unsat)
-            restevap = sbm.pottrans[i]
-        end
-        actevapsat = min(restevap * f_roots * alpha, satwaterdepth)
-        satwaterdepth = satwaterdepth - actevapsat
-
-        # check soil moisture balance per layer
-        du = 0.0
-        for k = n_usl:-1:1
-            du = max(0.0, usld[k] - usl[k] * (sbm.theta_s[i] - sbm.theta_r[i]))
-            usld = setindex(usld, usld[k] - du, k)
-            if k > 1
-                usld = setindex(usld, usld[k-1] + du, k - 1)
-            end
-        end
-
-        actinfilt = infiltsoilpath - du
-        excesswater = avail_forinfilt - infiltsoilpath - infiltexcess + du
-
-        # Separation between compacted and non compacted areas (correction with the satflow du)
-        # This is required for D-Emission/Delwaq
-        if infiltsoil + infiltpath > 0.0
-            actinfiltsoil = infiltsoil - du * infiltsoil / (infiltpath + infiltsoil)
-            actinfiltpath = infiltpath - du * infiltpath / (infiltpath + infiltsoil)
-        else
-            actinfiltsoil = 0.0
-            actinfiltpath = 0.0
-        end
-        excesswatersoil = max(soilinf - actinfiltsoil, 0.0)
-        excesswaterpath = max(pathinf - actinfiltpath, 0.0)
-
-        actcapflux = 0.0
-        if n_usl > 0
-            ksat = hydraulic_conductivity_at_depth(sbm, sbm.zi[i], i, n_usl, ksat_profile)
-            ustorecapacity =
-                sbm.soilwatercapacity[i] - satwaterdepth - sum(@view usld[1:sbm.nlayers[i]])
-            maxcapflux = max(0.0, min(ksat, actevapustore, ustorecapacity, satwaterdepth))
-
-            if sbm.zi[i] > sbm.rootingdepth[i]
-                capflux =
-                    maxcapflux * pow(
-                        1.0 - min(sbm.zi[i], sbm.cap_hmax[i]) / (sbm.cap_hmax[i]),
-                        sbm.cap_n[i],
-                    )
-            else
-                capflux = 0.0
-            end
-
-            netcapflux = capflux
-            for k = n_usl:-1:1
-                toadd = min(
-                    netcapflux,
-                    max(usl[k] * (sbm.theta_s[i] - sbm.theta_r[i]) - usld[k], 0.0),
-                )
-                usld = setindex(usld, usld[k] + toadd, k)
-                netcapflux = netcapflux - toadd
-                actcapflux = actcapflux + toadd
-            end
-        end
-        deepksat = hydraulic_conductivity_at_depth(
-            sbm,
-            sbm.soilthickness[i],
-            i,
-            sbm.nlayers[i],
-            ksat_profile,
-        )
-        deeptransfer = min(satwaterdepth, deepksat)
-        actleakage = max(0.0, min(sbm.maxleakage[i], deeptransfer))
-
-        # recharge (mm) for saturated zone
-        recharge = (transfer - actcapflux - actleakage - actevapsat - soilevapsat)
-        transpiration = actevapsat + actevapustore
-        actevap =
-            soilevap +
-            transpiration +
-            ae_openw_r +
-            ae_openw_l +
-            sbm.interception[i] +
-            evap_paddy_water
-
-        # update the outputs and states
-        sbm.n_unsatlayers[i] = n_usl
-        sbm.net_runoff_river[i] = runoff_river - ae_openw_r
-        sbm.avail_forinfilt[i] = avail_forinfilt
-        sbm.actinfilt[i] = actinfilt
-        sbm.infiltexcess[i] = infiltexcess
-        sbm.recharge[i] = recharge
-        sbm.transpiration[i] = transpiration
-        sbm.soilevap[i] = soilevap
-        sbm.soilevapsat[i] = soilevapsat
-        sbm.ae_openw_r[i] = ae_openw_r
-        sbm.ae_openw_l[i] = ae_openw_l
-        sbm.runoff_land[i] = runoff_land
-        sbm.runoff_river[i] = runoff_river
-        sbm.actevapsat[i] = actevapsat
-        sbm.actevap[i] = actevap
-        sbm.ae_ustore[i] = actevapustore
-        sbm.ustorelayerdepth[i] = usld
-        sbm.transfer[i] = transfer
-        sbm.actcapflux[i] = actcapflux
-        sbm.actleakage[i] = actleakage
-        sbm.actinfiltsoil[i] = actinfiltsoil
-        sbm.actinfiltpath[i] = actinfiltpath
-        sbm.excesswater[i] = excesswater
-        sbm.excesswatersoil[i] = excesswatersoil
-        sbm.excesswaterpath[i] = excesswaterpath
-        sbm.rainfallplusmelt[i] = rainfallplusmelt
-        sbm.infiltsoilpath[i] = infiltsoilpath
-        sbm.satwaterdepth[i] = satwaterdepth
-        sbm.soilinfredu[i] = soilinfredu
-        if modelsnow
-            if modelglacier
-                sbm.snow[i] = snow
-                sbm.glacierstore[i] = glacierstore
-            end
-        end
     end
-end
-
-function update_after_subsurfaceflow(sbm::SBM, zi, exfiltsatwater)
-
-    threaded_foreach(1:sbm.n, basesize = 1000) do i
-        usl, n_usl = set_layerthickness(zi[i], sbm.sumlayers[i], sbm.act_thickl[i])
-        # exfiltration from ustore
-        usld = sbm.ustorelayerdepth[i]
-        exfiltustore = 0.0
-        for k = sbm.n_unsatlayers[i]:-1:1
-            if k <= n_usl
-                exfiltustore = max(0, usld[k] - usl[k] * (sbm.theta_s[i] - sbm.theta_r[i]))
-            else
-                exfiltustore = usld[k]
-            end
-            usld = setindex(usld, usld[k] - exfiltustore, k)
-            if k > 1
-                usld = setindex(usld, usld[k-1] + exfiltustore, k - 1)
-            end
-        end
-
-        ustoredepth = sum(@view usld[1:n_usl])
 
-        if !isnothing(sbm.paddy) && sbm.paddy.irrigation_areas[i]
-            paddy_h_add =
-                exfiltustore +
-                exfiltsatwater[i] +
-                sbm.excesswater[i] +
-                sbm.runoff_land[i] +
-                sbm.infiltexcess[i]
-            runoff = max(paddy_h_add - sbm.paddy.h_max[i], 0.0)
-            sbm.paddy.h[i] = paddy_h_add - runoff
-        else
-            runoff =
-                exfiltustore +
-                exfiltsatwater[i] +
-                sbm.excesswater[i] +
-                sbm.runoff_land[i] +
-                sbm.infiltexcess[i]
-        end
+    update!(glacier, atmospheric_forcing)
 
-        # volumetric water content per soil layer and root zone
-        vwc = sbm.vwc[i]
-        vwc_perc = sbm.vwc_perc[i]
-        for k = 1:sbm.nlayers[i]
-            if k <= n_usl
-                vwc = setindex(
-                    vwc,
-                    (
-                        usld[k] +
-                        (sbm.act_thickl[i][k] - usl[k]) * (sbm.theta_s[i] - sbm.theta_r[i])
-                    ) / sbm.act_thickl[i][k] + sbm.theta_r[i],
-                    k,
-                )
-            else
-                vwc = setindex(vwc, sbm.theta_s[i], k)
-            end
-            vwc_perc = setindex(vwc_perc, (vwc[k] / sbm.theta_s[i]) * 100.0, k)
-        end
+    update_boundary_conditions!(runoff, (; glacier, snow, interception), lateral, network)
+    update!(runoff, atmospheric_forcing)
 
-        rootstore_unsat = 0
-        for k = 1:n_usl
-            rootstore_unsat =
-                rootstore_unsat +
-                min(1.0, (max(0.0, sbm.rootingdepth[i] - sbm.sumlayers[i][k]) / usl[k])) *
-                usld[k]
-        end
-
-        rootstore_sat =
-            max(0.0, sbm.rootingdepth[i] - zi[i]) * (sbm.theta_s[i] - sbm.theta_r[i])
-        rootstore = rootstore_sat + rootstore_unsat
-        vwc_root = rootstore / sbm.rootingdepth[i] + sbm.theta_r[i]
-        vwc_percroot = (vwc_root / sbm.theta_s[i]) * 100.0
-
-        satwaterdepth = (sbm.soilthickness[i] - zi[i]) * (sbm.theta_s[i] - sbm.theta_r[i])
-
-        # update the outputs and states
-        sbm.n_unsatlayers[i] = n_usl
-        sbm.ustorelayerdepth[i] = usld
-        sbm.ustoredepth[i] = ustoredepth
-        sbm.satwaterdepth[i] = satwaterdepth
-        sbm.exfiltsatwater[i] = exfiltsatwater[i]
-        sbm.exfiltustore[i] = exfiltustore
-        sbm.runoff[i] = runoff
-        sbm.net_runoff[i] = runoff - sbm.ae_openw_l[i]
-        sbm.vwc[i] = vwc
-        sbm.vwc_perc[i] = vwc_perc
-        sbm.rootstore[i] = rootstore
-        sbm.vwc_root[i] = vwc_root
-        sbm.vwc_percroot[i] = vwc_percroot
-        sbm.zi[i] = zi[i]
+    if do_water_demand
+        (; potential_transpiration) = soil.boundary_conditions
+        (; h3_high, h3_low) = soil.parameters
+        potential_transpiration .= get_potential_transpiration(interception)
+        @. soil.variables.h3 = feddes_h3(h3_high, h3_low, potential_transpiration, dt)
     end
+    update_water_demand!(demand, soil)
+    update_water_allocation!(allocation, demand, lateral, network, dt)
+
+    soil_fraction!(soil, runoff, glacier)
+    update_boundary_conditions!(
+        soil,
+        atmospheric_forcing,
+        (; interception, runoff, demand, allocation),
+    )
+
+    update!(soil, atmospheric_forcing, (; snow, runoff, demand), config, dt)
+    @. soil.variables.actevap += interception.variables.interception_rate
+    return nothing
 end
 
 """
 Update the total water storage per cell at the end of a timestep.
 
 Takes the following parameters:
-- sbm:
-    The vertical concept (SBM struct)
+- model:
+    The land hydrology model with the SBM soil model `LandHydrologySBM`
 - river_network:
     The indices of the river cells in relation to the active cells, i.e. model.network.index_river
 - area:
@@ -1330,146 +145,44 @@ Takes the following parameters:
 - land_routing:
     The land routing struct, i.e. model.lateral.land
 """
-function update_total_water_storage(
-    sbm::SBM,
+function update_total_water_storage!(
+    model::LandHydrologySBM,
     river_network,
     area,
     river_routing,
     land_routing,
 )
+    (; interception, snow, glacier, runoff, soil, demand) = model
+    (; total_storage, ustoredepth, satwaterdepth) = soil.variables
+    (; riverfrac) = runoff.parameters
+
     # Set the total storage to zero
-    fill!(sbm.total_storage, 0)
+    fill!(total_storage, 0)
 
     # Burn the river routing values
     for (i, index_river) in enumerate(river_network)
-        sbm.total_storage[index_river] = (
+        total_storage[index_river] = (
             (river_routing.h_av[i] * river_routing.width[i] * river_routing.dl[i]) /
             (area[index_river]) * 1000 # Convert to mm
         )
     end
 
-    # Chunk the data for parallel computing
-    threaded_foreach(1:sbm.n, basesize = 1000) do i
+    # Add storage from interception, snow and glacier models
+    total_storage .+=
+        get_snow_storage(snow) .+ get_snow_water(snow) .+
+        get_glacier_store(glacier) .* get_glacier_fraction(glacier) .+
+        interception.variables.canopy_storage .+ get_water_depth(demand.paddy)
 
-        # Paddy water depth
-        paddy_h = isnothing(sbm.paddy) ? 0.0 : sbm.paddy.h[i]
-
-        # Cumulate per vertical type
-        # Maybe re-categorize in the future
-        surface = (
-            sbm.glacierstore[i] * sbm.glacierfrac[i] +
-            sbm.snow[i] +
-            sbm.snowwater[i] +
-            sbm.canopystorage[i] +
-            paddy_h
-        )
-        sub_surface = sbm.ustoredepth[i] + sbm.satwaterdepth[i]
+    # Chunk the data for parallel computing
+    n = length(ustoredepth)
+    threaded_foreach(1:n; basesize = 1000) do i
+        sub_surface = ustoredepth[i] + satwaterdepth[i]
         lateral = (
-            land_routing.h_av[i] * (1 - sbm.riverfrac[i]) * 1000 # convert to mm
+            land_routing.h_av[i] * (1 - riverfrac[i]) * 1000 # convert to mm
         )
 
         # Add everything to the total water storage
-        sbm.total_storage[i] += (surface + sub_surface + lateral)
-    end
-end
-
-"""
-    update_water_demand(sbm::SBM)
-
-Update water demand for vertical `SBM` concept for a single timestep. Water demand is
-computed for sectors `industry`, `domestic` and `livestock`, and `paddy` rice fields and
-`nonpaddy` (other crop) fields.
-
-Gross water demand for irrigation `irri_demand_gross` and non-irrigation
-`nonirri_demand_gross`, and total gross water demand `total_gross_demand` are updated as
-part of `SBM` water allocation (`allocation`)
-"""
-function update_water_demand(sbm::SBM)
-    for i = 1:sbm.n
-
-        industry_dem = update_non_irrigation_demand(sbm.industry, i)
-        domestic_dem = update_non_irrigation_demand(sbm.domestic, i)
-        livestock_dem = update_non_irrigation_demand(sbm.livestock, i)
-
-        irri_dem_gross = 0.0
-        if !isnothing(sbm.nonpaddy) && sbm.nonpaddy.irrigation_areas[i]
-            if sbm.nonpaddy.irrigation_trigger[i]
-                usl, _ = set_layerthickness(sbm.zi[i], sbm.sumlayers[i], sbm.act_thickl[i])
-                for k = 1:sbm.n_unsatlayers[i]
-                    # compute water demand only for root zone through root fraction per layer
-                    rootfrac = min(
-                        1.0,
-                        (max(0.0, sbm.rootingdepth[i] - sbm.sumlayers[i][k]) / usl[k]),
-                    )
-                    # vwc_f and vwc_h3 can be precalculated.
-                    vwc_fc = vwc_brooks_corey(
-                        -100.0,
-                        sbm.hb[i],
-                        sbm.theta_s[i],
-                        sbm.theta_r[i],
-                        sbm.c[i][k],
-                    )
-                    vwc_h3 = vwc_brooks_corey(
-                        sbm.h3[i],
-                        sbm.hb[i],
-                        sbm.theta_s[i],
-                        sbm.theta_r[i],
-                        sbm.c[i][k],
-                    )
-                    depletion =
-                        (vwc_fc * usl[k]) -
-                        (sbm.ustorelayerdepth[i][k] + sbm.theta_r[i] * usl[k])
-                    depletion *= rootfrac
-                    raw = (vwc_fc - vwc_h3) * usl[k] # readily available water
-                    raw *= rootfrac
-
-                    # check if maximum irrigation rate has been applied at the previous time step.
-                    max_irri_rate_applied =
-                        sbm.nonpaddy.demand_gross[i] ==
-                        sbm.nonpaddy.maximum_irrigation_rate[i]
-                    if depletion >= raw # start irrigation
-                        irri_dem_gross += depletion
-                        # add depletion to irrigation gross demand when the maximum irrigation rate has been 
-                        # applied at the previous time step (to get volumetric water content at field capacity)
-                    elseif depletion > 0.0 && max_irri_rate_applied # continue irrigation
-                        irri_dem_gross += depletion
-                    end
-                end
-                # limit irrigation demand to infiltration capacity    
-                infiltration_capacity =
-                    sbm.soilinfredu[i] * (1.0 - sbm.pathfrac[i]) * sbm.infiltcapsoil[i]
-                irri_dem_gross = min(irri_dem_gross, infiltration_capacity)
-                irri_dem_gross /= sbm.nonpaddy.irrigation_efficiency[i]
-                # limit irrigation demand to the maximum irrigation rate
-                irri_dem_gross =
-                    min(irri_dem_gross, sbm.nonpaddy.maximum_irrigation_rate[i])
-            else
-                irri_dem_gross = 0.0
-            end
-            sbm.nonpaddy.demand_gross[i] = irri_dem_gross
-        elseif !isnothing(sbm.paddy) && sbm.paddy.irrigation_areas[i]
-            if sbm.paddy.irrigation_trigger[i]
-                # check if maximum irrigation rate has been applied at the previous time step.
-                max_irri_rate_applied =
-                    sbm.paddy.demand_gross[i] == sbm.paddy.maximum_irrigation_rate[i]
-                # start irrigation
-                if sbm.paddy.h[i] < sbm.paddy.h_min[i]
-                    irr_depth_paddy = sbm.paddy.h_opt[i] - sbm.paddy.h[i]
-                elseif sbm.paddy.h[i] < sbm.paddy.h_opt[i] && max_irri_rate_applied # continue irrigation
-                    irr_depth_paddy = sbm.paddy.h_opt[i] - sbm.paddy.h[i]
-                else
-                    irr_depth_paddy = 0.0
-                end
-                irri_dem_gross += irr_depth_paddy / sbm.paddy.irrigation_efficiency[i]
-                # limit irrigation demand to the maximum irrigation rate
-                irri_dem_gross = min(irri_dem_gross, sbm.paddy.maximum_irrigation_rate[i])
-            end
-            sbm.paddy.demand_gross[i] = irri_dem_gross
-        end
-        # update gross water demands 
-        sbm.allocation.irri_demand_gross[i] = irri_dem_gross
-        sbm.allocation.nonirri_demand_gross[i] = industry_dem + domestic_dem + livestock_dem
-        sbm.allocation.total_gross_demand[i] =
-            irri_dem_gross + industry_dem + domestic_dem + livestock_dem
+        total_storage[i] += (sub_surface + lateral)
     end
-end
+    return nothing
+end
\ No newline at end of file
diff --git a/src/sbm_gwf_model.jl b/src/sbm_gwf_model.jl
index 2e4913c83..c8b52595d 100644
--- a/src/sbm_gwf_model.jl
+++ b/src/sbm_gwf_model.jl
@@ -2,7 +2,7 @@
     initialize_sbm_gwf_model(config::Config)
 
 Initial part of the sbm_gwf model concept. The model contains:
-    - the vertical SBM concept
+    - the land hydrology model with the SBM soil model
     - unconfined aquifer with groundwater flow in four directions (adjacent cells)
     - the following surface routing options:
         - 1-D kinematic wave for river flow and 1-D kinematic wave for overland flow
@@ -65,7 +65,7 @@ function initialize_sbm_gwf_model(config::Config)
     # read x, y coordinates and calculate cell length [m]
     y_nc = read_y_axis(nc)
     x_nc = read_x_axis(nc)
-    y = permutedims(repeat(y_nc, outer = (1, length(x_nc))))[inds]
+    y = permutedims(repeat(y_nc; outer = (1, length(x_nc))))[inds]
     cellength = abs(mean(diff(x_nc)))
 
     sizeinmetres = get(config.model, "sizeinmetres", false)::Bool
@@ -76,7 +76,7 @@ function initialize_sbm_gwf_model(config::Config)
     nriv = length(inds_riv)
 
     # initialize vertical SBM concept
-    sbm = initialize_sbm(nc, config, riverfrac, inds)
+    lhm = LandHydrologySBM(nc, config, riverfrac, inds)
 
     # reservoirs
     pits = zeros(Bool, modelsize_2d)
@@ -122,11 +122,11 @@ function initialize_sbm_gwf_model(config::Config)
     inds_allocation_areas = Vector{Int}[]
     inds_riv_allocation_areas = Vector{Int}[]
     if do_water_demand
-        areas = unique(sbm.allocation.areas)
+        areas = unique(lhm.allocation.parameters.areas)
         for a in areas
-            area_index = findall(x -> x == a, sbm.allocation.areas)
+            area_index = findall(==(a), lhm.allocation.parameters.areas)
             push!(inds_allocation_areas, area_index)
-            area_riv_index = findall(x -> x == a, sbm.allocation.areas[index_river])
+            area_riv_index = findall(==(a), lhm.allocation.parameters.areas[index_river])
             push!(inds_riv_allocation_areas, area_riv_index)
         end
     end
@@ -240,14 +240,14 @@ function initialize_sbm_gwf_model(config::Config)
         get(config.input.lateral.subsurface, "conductivity_profile", "uniform")::String
 
     connectivity = Connectivity(inds, rev_inds, xl, yl)
-    initial_head = altitude .- sbm.zi / 1000.0 # cold state for groundwater head based on SBM zi
+    initial_head = altitude .- lhm.soil.variables.zi / 1000.0 # cold state for groundwater head based on SBM zi
     initial_head[index_river] = altitude[index_river]
 
     if do_constanthead
         initial_head[constant_head.index] = constant_head.head
     end
 
-    bottom = altitude .- sbm.soilthickness ./ Float(1000.0)
+    bottom = altitude .- lhm.soil.parameters.soilthickness ./ Float(1000.0)
     area = xl .* yl
     volume = @. (min(altitude, initial_head) - bottom) * area * specific_yield # total volume than can be released
 
@@ -344,7 +344,12 @@ function initialize_sbm_gwf_model(config::Config)
         drain = ()
     end
 
-    gwf = GroundwaterFlow(aquifer, connectivity, constant_head, aquifer_boundaries)
+    gwf = GroundwaterFlow{Float}(;
+        aquifer,
+        connectivity,
+        constanthead = constant_head,
+        boundaries = aquifer_boundaries,
+    )
 
     # map GroundwaterFlow and its boundaries
     if do_drains
@@ -391,7 +396,7 @@ function initialize_sbm_gwf_model(config::Config)
     end
 
     modelmap =
-        (vertical = sbm, lateral = (subsurface = subsurface_map, land = olf, river = rf))
+        (vertical = lhm, lateral = (subsurface = subsurface_map, land = olf, river = rf))
     indices_reverse = (
         land = rev_inds,
         river = rev_inds_riv,
@@ -405,8 +410,8 @@ function initialize_sbm_gwf_model(config::Config)
         indices_reverse,
         x_nc,
         y_nc,
-        nc,
-        extra_dim = (name = "layer", value = Float64.(1:sbm.maxlayers)),
+        nc;
+        extra_dim = (name = "layer", value = Float64.(1:(lhm.soil.parameters.maxlayers))),
     )
     close(nc)
 
@@ -511,55 +516,28 @@ function initialize_sbm_gwf_model(config::Config)
         config,
         (; land, river, reservoir, lake, drain, index_river, frac_toriver),
         (subsurface = subsurface_map, land = olf, river = rf),
-        sbm,
+        lhm,
         clock,
         reader,
         writer,
         SbmGwfModel(),
     )
 
-    model = set_states(model)
+    set_states!(model)
 
     return model
 end
 
-
 "update the sbm_gwf model for a single timestep"
-function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmGwfModel}
-    (; lateral, vertical, network, config) = model
+function update!(model::Model{N, L, V, R, W, T}) where {N, L, V, R, W, T <: SbmGwfModel}
+    (; lateral, vertical, network, clock, config) = model
+    (; soil, runoff, demand) = vertical
 
     do_water_demand = haskey(config.model, "water_demand")
     inds_riv = network.index_river
     aquifer = lateral.subsurface.flow.aquifer
-    constanthead = lateral.subsurface.flow.constanthead
-
-    # extract water levels h_av [m] from the land and river domains
-    # this is used to limit open water evaporation
-    vertical.waterlevel_land .= lateral.land.h_av .* 1000.0
-    vertical.waterlevel_river[inds_riv] .= lateral.river.h_av .* 1000.0
-
-    # vertical sbm concept is updated until snow state, after that (optional)
-    # snow transport is possible
-    update_until_snow(vertical, config)
-
-    # lateral snow transport
-    if get(config.model, "masswasting", false)::Bool
-        lateral_snow_transport!(
-            vertical.snow,
-            vertical.snowwater,
-            network.land.slope,
-            network.land,
-        )
-    end
 
-    # optional water demand and allocation
-    if do_water_demand
-        update_water_demand(vertical)
-        update_water_allocation(model)
-    end
-
-    # update vertical sbm concept until recharge [mm]
-    update_until_recharge(vertical, config)
+    update!(vertical, lateral, network, config)
 
     # set river stage (groundwater) to average h from kinematic wave
     lateral.subsurface.river.stage .= lateral.river.h_av .+ lateral.subsurface.river.bottom
@@ -571,40 +549,27 @@ function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmGwfModel}
     dt_sbm = (vertical.dt / tosecond(basetimestep)) # vertical.dt is in seconds (Float64)
     if dt_gw < dt_sbm
         @warn(
-            "stable time step dt $dt_gw for groundwater flow is smaller than sbm dt $dt_sbm"
+            "stable time step dt $dt_gw for groundwater flow is smaller than `LandHydrologySBM` model dt $dt_sbm"
         )
     end
 
-    Q = zeros(vertical.n)
-    # exchange of recharge between vertical sbm concept and groundwater flow domain
+    Q = zeros(lateral.subsurface.flow.connectivity.ncell)
+    # exchange of recharge between SBM soil model and groundwater flow domain
     # recharge rate groundwater is required in units [m d⁻¹]
-    @. lateral.subsurface.recharge.rate = vertical.recharge / 1000.0 * (1.0 / dt_sbm)
+    @. lateral.subsurface.recharge.rate = soil.variables.recharge / 1000.0 * (1.0 / dt_sbm)
     if do_water_demand
         @. lateral.subsurface.recharge.rate -=
-            vertical.allocation.act_groundwater_abst / 1000.0 * (1.0 / dt_sbm)
+            vertical.allocation.variables.act_groundwater_abst / 1000.0 * (1.0 / dt_sbm)
     end
     # update groundwater domain
-    update(lateral.subsurface.flow, Q, dt_sbm, conductivity_profile)
-
-    # determine excess water depth [m] (exfiltwater) in groundwater domain (head > surface)
-    # and reset head
-    exfiltwater = (aquifer.head .- min.(aquifer.head, aquifer.top)) .* storativity(aquifer)
-    aquifer.head .= min.(aquifer.head, aquifer.top)
-
-    # Adjust for constant head boundary of groundwater domain
-    exfiltwater[constanthead.index] .= 0
-    aquifer.head[constanthead.index] .= constanthead.head
-
-    # update vertical sbm concept (runoff, ustorelayerdepth and satwaterdepth)
-    update_after_subsurfaceflow(
-        vertical,
-        (network.land.altitude .- aquifer.head) .* 1000.0, # zi [mm] in vertical concept SBM
-        exfiltwater .* 1000.0,
-    )
+    update!(lateral.subsurface.flow, Q, dt_sbm, conductivity_profile)
 
-    ssf_toriver = zeros(vertical.n)
+    # update SBM soil model (runoff, ustorelayerdepth and satwaterdepth)
+    update!(soil, (; runoff, demand, subsurface = lateral.subsurface.flow))
+
+    ssf_toriver = zeros(length(soil.variables.zi))
     ssf_toriver[inds_riv] = -lateral.subsurface.river.flux ./ lateral.river.dt
-    surface_routing(model, ssf_toriver = ssf_toriver)
+    surface_routing!(model; ssf_toriver = ssf_toriver)
 
-    return model
+    return nothing
 end
diff --git a/src/sbm_model.jl b/src/sbm_model.jl
index 0ad9054f0..cd14aa7dc 100644
--- a/src/sbm_model.jl
+++ b/src/sbm_model.jl
@@ -5,7 +5,6 @@ Initial part of the SBM model concept. Reads the input settings and data as defi
 Config object. Will return a Model that is ready to run.
 """
 function initialize_sbm_model(config::Config)
-
     model_type = config.model.type::String
     @info "Initialize model variables for model type `$model_type`."
 
@@ -63,18 +62,18 @@ function initialize_sbm_model(config::Config)
     # read x, y coordinates and calculate cell length [m]
     y_nc = read_y_axis(nc)
     x_nc = read_x_axis(nc)
-    y = permutedims(repeat(y_nc, outer = (1, length(x_nc))))[inds]
+    y = permutedims(repeat(y_nc; outer = (1, length(x_nc))))[inds]
     cellength = abs(mean(diff(x_nc)))
 
     sizeinmetres = get(config.model, "sizeinmetres", false)::Bool
     xl, yl = cell_lengths(y, cellength, sizeinmetres)
     riverfrac = river_fraction(river, riverlength, riverwidth, xl, yl)
 
+    lhm = LandHydrologySBM(nc, config, riverfrac, inds)
+
     inds_riv, rev_inds_riv = active_indices(river_2d, 0)
     nriv = length(inds_riv)
 
-    sbm = initialize_sbm(nc, config, riverfrac, inds)
-
     # reservoirs
     pits = zeros(Bool, modelsize_2d)
     if do_reservoirs
@@ -123,23 +122,34 @@ function initialize_sbm_model(config::Config)
             type = Float,
         )
 
-        # unit for lateral subsurface flow component is [m³ d⁻¹], sbm.kv_0 [mm Δt⁻¹]
-        kh_0 = khfrac .* sbm.kv_0 .* 0.001 .* (basetimestep / dt)
-        f = sbm.f .* 1000.0
-        zi = sbm.zi .* 0.001
-        soilthickness = sbm.soilthickness .* 0.001
-        z_exp = sbm.z_exp .* 0.001
-
-        ssf = LateralSSF{Float}(
-            kh_0 = kh_0,
-            f = f,
-            kh = fill(mv, n),
+        (; theta_s, theta_r, soilthickness) = lhm.soil.parameters
+        (; zi) = lhm.soil.variables
+        ssf_soilthickness = soilthickness .* 0.001
+        ssf_zi = zi .* 0.001
+
+        kh_profile_type = get(config.input.vertical, "ksat_profile", "exponential")::String
+        if kh_profile_type == "exponential"
+            (; kv_0, f) = lhm.soil.parameters.kv_profile
+            kh_0 = khfrac .* kv_0 .* 0.001 .* (basetimestep / dt)
+            kh_profile = KhExponential(kh_0, f .* 1000.0)
+        elseif kh_profile_type == "exponential_constant"
+            (; z_exp) = lhm.soil.parameters.kv_profile
+            (; kv_0, f) = lhm.soil.parameters.kv_profile.exponential
+            kh_0 = khfrac .* kv_0 .* 0.001 .* (basetimestep / dt)
+            exp_profile = KhExponential(kh_0, f .* 1000.0)
+            kh_profile = KhExponentialConstant(exp_profile, z_exp .* 0.001)
+        elseif kh_profile_type == "layered" || kh_profile_type == "layered_exponential"
+            kh_profile = KhLayered(fill(mv, n))
+        end
+
+        # unit for lateral subsurface flow component is [m³ d⁻¹], kv_0 [mm Δt⁻¹]
+        ssf = LateralSSF{Float, typeof(kh_profile)}(;
+            kh_profile = kh_profile,
             khfrac = khfrac,
-            zi = zi,
-            z_exp = z_exp,
-            soilthickness = soilthickness,
-            theta_s = sbm.theta_s,
-            theta_r = sbm.theta_r,
+            zi = ssf_zi,
+            soilthickness = ssf_soilthickness,
+            theta_s,
+            theta_r,
             dt = dt / basetimestep,
             slope = landslope,
             dl = dl,
@@ -150,21 +160,19 @@ function initialize_sbm_model(config::Config)
             ssfin = fill(mv, n),
             ssfmax = fill(mv, n),
             to_river = zeros(n),
-            volume = (sbm.theta_s .- sbm.theta_r) .* (soilthickness .- zi) .* (xl .* yl),
+            volume = (theta_s .- theta_r) .* (ssf_soilthickness .- ssf_zi) .* (xl .* yl),
         )
         # update variables `ssf`, `ssfmax` and `kh` (layered profile) based on ksat_profile
-        ksat_profile = get(config.input.vertical, "ksat_profile", "exponential")::String
-        if ksat_profile == "exponential"
-            initialize_lateralssf_exp!(ssf::LateralSSF)
-        elseif ksat_profile == "exponential_constant"
-            initialize_lateralssf_exp_const!(ssf::LateralSSF)
-        elseif ksat_profile == "layered" || ksat_profile == "layered_exponential"
-            initialize_lateralssf_layered!(ssf::LateralSSF, sbm::SBM, ksat_profile)
+        if kh_profile_type == "exponential" || kh_profile_type == "exponential_constant"
+            initialize_lateralssf!(ssf, kh_profile)
+        elseif kh_profile_type == "layered" || kh_profile_type == "layered_exponential"
+            (; kv_profile) = lhm.soil.parameters
+            initialize_lateralssf!(ssf, lhm.soil, kv_profile, tosecond(dt))
         end
     else
         # when the SBM model is coupled (BMI) to a groundwater model, the following
         # variables are expected to be exchanged from the groundwater model.
-        ssf = GroundwaterExchange{Float}(
+        ssf = GroundwaterExchange{Float}(;
             dt = dt / basetimestep,
             exfiltwater = fill(mv, n),
             zi = fill(mv, n),
@@ -187,11 +195,12 @@ function initialize_sbm_model(config::Config)
     inds_allocation_areas = Vector{Int}[]
     inds_riv_allocation_areas = Vector{Int}[]
     if do_water_demand
-        areas = unique(sbm.allocation.areas)
+        areas = unique(lhm.allocation.parameters.areas)
         for a in areas
-            area_index = findall(x -> x == a, sbm.allocation.areas)
+            area_index = findall(x -> x == a, lhm.allocation.parameters.areas)
             push!(inds_allocation_areas, area_index)
-            area_riv_index = findall(x -> x == a, sbm.allocation.areas[index_river])
+            area_riv_index =
+                findall(x -> x == a, lhm.allocation.parameters.areas[index_river])
             push!(inds_riv_allocation_areas, area_riv_index)
         end
     end
@@ -312,21 +321,22 @@ function initialize_sbm_model(config::Config)
         end
     end
 
-    modelmap = (vertical = sbm, lateral = (subsurface = ssf, land = olf, river = rf))
+    modelmap = (vertical = lhm, lateral = (subsurface = ssf, land = olf, river = rf))
     indices_reverse = (
         land = rev_inds,
         river = rev_inds_riv,
         reservoir = isempty(reservoir) ? nothing : reservoir.reverse_indices,
         lake = isempty(lake) ? nothing : lake.reverse_indices,
     )
+    (; maxlayers) = lhm.soil.parameters
     writer = prepare_writer(
         config,
         modelmap,
         indices_reverse,
         x_nc,
         y_nc,
-        nc,
-        extra_dim = (name = "layer", value = Float64.(1:sbm.maxlayers)),
+        nc;
+        extra_dim = (name = "layer", value = Float64.(1:(maxlayers))),
     )
     close(nc)
 
@@ -418,112 +428,76 @@ function initialize_sbm_model(config::Config)
         config,
         (; land, river, reservoir, lake, index_river, frac_toriver),
         (subsurface = ssf, land = olf, river = rf),
-        sbm,
+        lhm,
         clock,
         reader,
         writer,
         SbmModel(),
     )
 
-    model = set_states(model)
+    set_states!(model)
 
     @info "Initialized model"
     return model
 end
 
 "update SBM model for a single timestep"
-function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
-
+function update!(model::Model{N, L, V, R, W, T}) where {N, L, V, R, W, T <: SbmModel}
     (; lateral, vertical, network, config) = model
     do_water_demand = haskey(config.model, "water_demand")
-    ksat_profile = get(config.input.vertical, "ksat_profile", "exponential")::String
+    (; kv_profile) = vertical.soil.parameters
 
-    model = update_until_recharge(model)
-    # exchange of recharge between vertical sbm concept and subsurface flow domain
-    lateral.subsurface.recharge .= vertical.recharge ./ 1000.0
+    update_until_recharge!(model)
+    # exchange of recharge between SBM soil model and subsurface flow domain
+    lateral.subsurface.recharge .= vertical.soil.variables.recharge ./ 1000.0
     if do_water_demand
-        @. lateral.subsurface.recharge -= vertical.allocation.act_groundwater_abst / 1000.0
+        @. lateral.subsurface.recharge -=
+            vertical.allocation.variables.act_groundwater_abst / 1000.0
     end
     lateral.subsurface.recharge .*= lateral.subsurface.dw
-    lateral.subsurface.zi .= vertical.zi ./ 1000.0
+    lateral.subsurface.zi .= vertical.soil.variables.zi ./ 1000.0
     # update lateral subsurface flow domain (kinematic wave)
-    if (ksat_profile == "layered") || (ksat_profile == "layered_exponential")
-        for i in eachindex(lateral.subsurface.kh)
-            lateral.subsurface.kh[i] =
-                kh_layered_profile(vertical, lateral.subsurface.khfrac[i], i, ksat_profile)
-        end
-    end
-    update(lateral.subsurface, network.land, network.frac_toriver, ksat_profile)
-    model = update_after_subsurfaceflow(model)
-    model = update_total_water_storage(model)
+    kh_layered_profile!(vertical.soil, lateral.subsurface, kv_profile, vertical.dt)
+    update!(lateral.subsurface, network.land, network.frac_toriver)
+    update_after_subsurfaceflow!(model)
+    update_total_water_storage!(model)
+    return nothing
 end
 
 """
-    update_until_recharge(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
+    update_until_recharge!(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
 
 Update SBM model until recharge for a single timestep. This function is also accessible
 through BMI, to couple the SBM model to an external groundwater model.
 """
-function update_until_recharge(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
+function update_until_recharge!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: SbmModel}
     (; lateral, vertical, network, config) = model
-
-    do_water_demand = haskey(config.model, "water_demand")
-
-    inds_riv = network.index_river
-
-    # extract water levels h_av [m] from the land and river domains
-    # this is used to limit open water evaporation
-    vertical.waterlevel_land .= lateral.land.h_av .* 1000.0
-    vertical.waterlevel_river[inds_riv] .= lateral.river.h_av .* 1000.0
-
-    # vertical sbm concept is updated until snow state, after that (optional)
-    # snow transport is possible
-    update_until_snow(vertical, config)
-
-    # lateral snow transport
-    if get(config.model, "masswasting", false)::Bool
-        lateral_snow_transport!(
-            vertical.snow,
-            vertical.snowwater,
-            network.land.slope,
-            network.land,
-        )
-    end
-
-    # optional water demand and allocation
-    if do_water_demand
-        update_water_demand(vertical)
-        update_water_allocation(model)
-    end
-
-    # update vertical sbm concept until recharge [mm] to the saturated store
-    update_until_recharge(vertical, config)
-
-    return model
+    update!(vertical, lateral, network, config)
+    return nothing
 end
 
 """
-    update_after_subsurfaceflow(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
+    update_after_subsurfaceflow!(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
 
 Update SBM model after subsurface flow for a single timestep. This function is also
 accessible through BMI, to couple the SBM model to an external groundwater model.
 """
-function update_after_subsurfaceflow(
-    model::Model{N,L,V,R,W,T},
-) where {N,L,V,R,W,T<:SbmModel}
+function update_after_subsurfaceflow!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: SbmModel}
     (; lateral, vertical) = model
+    (; soil, runoff, demand) = vertical
+    (; subsurface) = lateral
 
-    # update vertical sbm concept (runoff, ustorelayerdepth and satwaterdepth)
-    update_after_subsurfaceflow(
-        vertical,
-        lateral.subsurface.zi * 1000.0,
-        lateral.subsurface.exfiltwater * 1000.0,
-    )
+    # update SBM soil model (runoff, ustorelayerdepth and satwaterdepth)
+    update!(soil, (; runoff, demand, subsurface))
 
     ssf_toriver = lateral.subsurface.to_river ./ tosecond(basetimestep)
-    surface_routing(model, ssf_toriver = ssf_toriver)
+    surface_routing!(model; ssf_toriver = ssf_toriver)
 
-    return model
+    return nothing
 end
 
 """
@@ -531,24 +505,26 @@ Update of the total water storage at the end of each timestep per model cell.
 
 This is done here at model level.
 """
-function update_total_water_storage(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SbmModel}
+function update_total_water_storage!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: SbmModel}
     (; lateral, vertical, network) = model
 
     # Update the total water storage based on vertical states
     # TODO Maybe look at routing in the near future
-    update_total_water_storage(
+    update_total_water_storage!(
         vertical,
         network.index_river,
         network.land.area,
         lateral.river,
         lateral.land,
     )
-    return model
+    return nothing
 end
 
-function set_states(
-    model::Model{N,L,V,R,W,T},
-) where {N,L,V,R,W,T<:Union{SbmModel,SbmGwfModel}}
+function set_states!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: Union{SbmModel, SbmGwfModel}}
     (; lateral, vertical, network, config) = model
 
     reinit = get(config.model, "reinit", true)::Bool
@@ -563,21 +539,16 @@ function set_states(
         nriv = length(network.river.indices)
         instate_path = input_path(config, config.state.path_input)
         @info "Set initial conditions from state file `$instate_path`."
-        if T <: SbmModel
-            @warn string(
-                "The unit of `ssf` (lateral subsurface flow) is now m3 d-1. Please update your",
-                " input state file if it was produced with a Wflow version up to v0.5.2.",
-            )
-        end
-        set_states(instate_path, model; type = Float, dimname = :layer)
-        # update zi for vertical sbm
+        set_states!(instate_path, model; type = Float, dimname = :layer)
+        # update zi for SBM soil model
         zi =
             max.(
                 0.0,
-                vertical.soilthickness .-
-                vertical.satwaterdepth ./ (vertical.theta_s .- vertical.theta_r),
+                vertical.soil.parameters.soilthickness .-
+                vertical.soil.variables.satwaterdepth ./
+                (vertical.soil.parameters.theta_s .- vertical.soil.parameters.theta_r),
             )
-        vertical.zi .= zi
+        vertical.soil.variables.zi .= zi
         if land_routing == "kinematic-wave"
             # make sure land cells with zero flow width are set to zero q and h
             for i in eachindex(lateral.land.width)
@@ -588,12 +559,6 @@ function set_states(
             end
             lateral.land.volume .= lateral.land.h .* lateral.land.width .* lateral.land.dl
         elseif land_routing == "local-inertial"
-            @warn string(
-                "The reference level for the water depth `h` and `h_av` of overland flow ",
-                "(local inertial model) for cells containing a river has changed from river",
-                " bed elevation `zb` to cell elevation `z`. Please update the input state",
-                " file if it was produced with Wflow version v0.5.2.",
-            )
             for i in eachindex(lateral.land.volume)
                 if lateral.land.rivercells[i]
                     j = network.land.index_river[i]
@@ -632,5 +597,5 @@ function set_states(
     else
         @info "Set initial conditions from default values."
     end
-    return model
+    return nothing
 end
diff --git a/src/sediment.jl b/src/sediment.jl
index 13a919114..ec5c8f8d1 100644
--- a/src/sediment.jl
+++ b/src/sediment.jl
@@ -1,7 +1,7 @@
 ### Soil erosion ###
-@get_units @exchange @grid_type @grid_location @with_kw struct LandSediment{T}
+@get_units @grid_loc @with_kw struct LandSediment{T}
     # number of cells
-    n::Int | "-" | 0 | "none" | "none"
+    n::Int | "-" | "none"
     ### Soil erosion part ###
     # length of cells in y direction [m]
     yl::Vector{T} | "m"
@@ -99,6 +99,52 @@
     end
 end
 
+# This function is not used by SBM (was part of sbm.jl)
+function initialize_canopy(nc, config, inds)
+    n = length(inds)
+    # if leaf area index climatology provided use sl, swood and kext to calculate cmax, e_r and canopygapfraction
+    if haskey(config.input.vertical, "leaf_area_index")
+        # TODO confirm if leaf area index climatology is present in the netCDF
+        sl = ncread(
+            nc,
+            config,
+            "vertical.specific_leaf";
+            optional = false,
+            sel = inds,
+            type = Float,
+        )
+        swood = ncread(
+            nc,
+            config,
+            "vertical.storage_wood";
+            optional = false,
+            sel = inds,
+            type = Float,
+        )
+        kext =
+            ncread(nc, config, "vertical.kext"; optional = false, sel = inds, type = Float)
+        cmax = fill(mv, n)
+        e_r = fill(mv, n)
+        canopygapfraction = fill(mv, n)
+    else
+        sl = fill(mv, n)
+        swood = fill(mv, n)
+        kext = fill(mv, n)
+        # cmax, e_r, canopygapfraction only required when leaf area index climatology not provided
+        cmax = ncread(nc, config, "vertical.cmax"; sel = inds, defaults = 1.0, type = Float)
+        e_r =
+            ncread(nc, config, "vertical.eoverr"; sel = inds, defaults = 0.1, type = Float)
+        canopygapfraction = ncread(
+            nc,
+            config,
+            "vertical.canopygapfraction";
+            sel = inds,
+            defaults = 0.1,
+            type = Float,
+        )
+    end
+    return cmax, e_r, canopygapfraction, sl, swood, kext
+end
 
 function initialize_landsed(nc, config, river, riverfrac, xl, yl, inds)
     # Initialize parameters for the soil loss part
@@ -189,7 +235,7 @@ function initialize_landsed(nc, config, river, riverfrac, xl, yl, inds)
         fsilt = 0.13 .* psilt ./ 100
         fsand = 0.01 .* psand .* (1 .- 0.01 .* pclay) .^ (2.4)
         fsagg = 0.28 .* (0.01 .* pclay .- 0.25) .+ 0.5
-        for i = 1:n
+        for i in 1:n
             if pclay[i] > 50.0
                 fsagg[i] = 0.57
             elseif pclay[i] < 25
@@ -232,7 +278,7 @@ function initialize_landsed(nc, config, river, riverfrac, xl, yl, inds)
         wbcover = wbcover .+ lakecoverage_2d
     end
 
-    eros = LandSediment{Float}(
+    eros = LandSediment{Float}(;
         n = n,
         yl = yl,
         xl = xl,
@@ -300,14 +346,14 @@ function initialize_landsed(nc, config, river, riverfrac, xl, yl, inds)
 end
 
 # Soil erosion
-function update_until_ols(eros::LandSediment, config)
+function update_until_ols!(eros::LandSediment, config)
     # Options from config
     do_lai = haskey(config.input.vertical, "leaf_area_index")
     rainerosmethod = get(config.model, "rainerosmethod", "answers")::String
     dt = Second(config.timestepsecs)
     ts = Float(dt.value)
 
-    for i = 1:eros.n
+    for i in 1:(eros.n)
 
         ### Splash / Rainfall erosion ###
         # ANSWERS method
@@ -392,18 +438,17 @@ function update_until_ols(eros::LandSediment, config)
         eros.erossagg[i] = soilloss * eros.fsagg[i]
         eros.eroslagg[i] = soilloss * eros.flagg[i]
     end
-
+    return nothing
 end
 
 ### Sediment transport capacity in overland flow ###
-function update_until_oltransport(ols::LandSediment, config::Config)
-
+function update_until_oltransport!(ols::LandSediment, config::Config)
     do_river = get(config.model, "runrivermodel", false)::Bool
     tcmethod = get(config.model, "landtransportmethod", "yalinpart")::String
     dt = Second(config.timestepsecs)
     ts = Float(dt.value)
 
-    for i = 1:ols.n
+    for i in 1:(ols.n)
         sinslope = sin(atan(ols.slope[i]))
 
         if !do_river
@@ -441,7 +486,7 @@ function update_until_oltransport(ols::LandSediment, config::Config)
         ols.TCsagg[i] = TCsagg
         ols.TClagg[i] = TClagg
     end
-
+    return nothing
 end
 
 function tc_govers(ols::LandSediment, i::Int, sinslope::Float, ts::Float)::Float
@@ -591,9 +636,9 @@ function tc_yalinpart(ols::LandSediment, i::Int, sinslope::Float, ts::Float)
 end
 
 ### Sediment transport in overland flow ###
-@get_units @exchange @grid_type @grid_location @with_kw struct OverlandFlowSediment{T}
+@get_units @grid_loc @with_kw struct OverlandFlowSediment{T}
     # number of cells
-    n::Int | "-" | 0 | "none" | "none"
+    n::Int | "-" | "none"
     # Filter with river cells
     rivcell::Vector{T} | "-"
     # Total eroded soil [ton Δt⁻¹]
@@ -627,14 +672,12 @@ end
     inlandsagg::Vector{T} | "t dt-1"
     inlandlagg::Vector{T} | "t dt-1"
 
-
     function OverlandFlowSediment{T}(args...) where {T}
         equal_size_vectors(args)
         return new(args...)
     end
 end
 
-
 function partial_update!(inland, rivcell, eroded)
     no_erosion = zero(eltype(eroded))
     for i in eachindex(inland)
@@ -643,7 +686,7 @@ function partial_update!(inland, rivcell, eroded)
     return inland
 end
 
-function update(ols::OverlandFlowSediment, network, config)
+function update!(ols::OverlandFlowSediment, network, config)
     do_river = get(config.model, "runrivermodel", false)::Bool
     tcmethod = get(config.model, "landtransportmethod", "yalinpart")::String
     zeroarr = fill(0.0, ols.n)
@@ -675,14 +718,15 @@ function update(ols::OverlandFlowSediment, network, config)
         accucapacityflux!(ols.olsed, ols.soilloss, network, ols.TCsed)
         ols.inlandsed .= ifelse.(ols.rivcell .== 1, ols.soilloss, zeroarr)
     end
+    return nothing
 end
 
 ### River transport and processes ###
-@get_units @exchange @grid_type @grid_location @with_kw struct RiverSediment{T}
+@get_units @grid_loc @with_kw struct RiverSediment{T}
     # number of cells
-    n::Int | "-" | 0 | "none" | "none"
+    n::Int | "-" | "none"
     # Timestep [s]
-    dt::T | "s" | 0 | "none" | "none"
+    dt::T | "s"
     # River geometry (slope [-], length [m], width [m])
     sl::Vector{T} | "m"
     dl::Vector{T} | "m"
@@ -782,7 +826,6 @@ end
     # end
 end
 
-
 function initialize_riversed(nc, config, riverwidth, riverlength, inds_riv)
     # Initialize river parameters
     nriv = length(inds_riv)
@@ -1012,7 +1055,7 @@ function initialize_riversed(nc, config, riverwidth, riverlength, inds_riv)
     ck = zeros(Float, nriv)
     dk = zeros(Float, nriv)
     if tcmethodriv == "kodatie"
-        for i = 1:nriv
+        for i in 1:nriv
             if d50riv[i] <= 0.05
                 ak[i] = 281.4
                 bk[i] = 2.622
@@ -1050,7 +1093,7 @@ function initialize_riversed(nc, config, riverwidth, riverlength, inds_riv)
     kdbank = @. Float(0.2 * TCrbank^(-0.5) * 1e-6)
     kdbed = @. Float(0.2 * TCrbed^(-0.5) * 1e-6)
 
-    rs = RiverSediment(
+    rs = RiverSediment(;
         n = nriv,
         dt = Float(dt.value),
         # Parameters
@@ -1132,7 +1175,7 @@ function initialize_riversed(nc, config, riverwidth, riverlength, inds_riv)
     return rs
 end
 
-function update(rs::RiverSediment, network, config)
+function update!(rs::RiverSediment, network, config)
     (; graph, order) = network
     tcmethod = get(config.model, "rivtransportmethod", "bagnold")::String
 
@@ -1572,7 +1615,6 @@ function update(rs::RiverSediment, network, config)
 
         rs.SSconc[v] = SS * toconc
         rs.Bedconc[v] = Bed * toconc
-
     end
-
+    return nothing
 end
diff --git a/src/sediment_model.jl b/src/sediment_model.jl
index 2c5d5ebd6..cf2b78eaf 100644
--- a/src/sediment_model.jl
+++ b/src/sediment_model.jl
@@ -5,7 +5,6 @@ Initial part of the sediment model concept. Reads the input settings and data as
 Config object. Will return a Model that is ready to run.
 """
 function initialize_sediment_model(config::Config)
-
     model_type = config.model.type::String
     @info "Initialize model variables for model type `$model_type`."
 
@@ -47,7 +46,7 @@ function initialize_sediment_model(config::Config)
     # read x, y coordinates and calculate cell length [m]
     y_nc = read_y_axis(nc)
     x_nc = read_x_axis(nc)
-    y = permutedims(repeat(y_nc, outer = (1, length(x_nc))))[inds]
+    y = permutedims(repeat(y_nc; outer = (1, length(x_nc))))[inds]
     cellength = abs(mean(diff(x_nc)))
 
     sizeinmetres = get(config.model, "sizeinmetres", false)::Bool
@@ -61,7 +60,7 @@ function initialize_sediment_model(config::Config)
 
     # # lateral part sediment in overland flow
     rivcell = float(river)
-    ols = OverlandFlowSediment{Float}(
+    ols = OverlandFlowSediment{Float}(;
         n = n,
         rivcell = rivcell,
         soilloss = fill(mv, n),
@@ -148,17 +147,17 @@ function initialize_sediment_model(config::Config)
         SedimentModel(),
     )
 
-    model = set_states(model)
+    set_states!(model)
     @info "Initialized model"
 
     return model
 end
 
-function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SedimentModel}
+function update!(model::Model{N, L, V, R, W, T}) where {N, L, V, R, W, T <: SedimentModel}
     (; lateral, vertical, network, config) = model
 
-    update_until_ols(vertical, config)
-    update_until_oltransport(vertical, config)
+    update_until_ols!(vertical, config)
+    update_until_oltransport!(vertical, config)
 
     lateral.land.soilloss .= vertical.soilloss
     lateral.land.erosclay .= vertical.erosclay
@@ -174,7 +173,7 @@ function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SedimentModel}
     lateral.land.TCsagg .= vertical.TCsagg
     lateral.land.TClagg .= vertical.TClagg
 
-    update(lateral.land, network.land, config)
+    update!(lateral.land, network.land, config)
 
     do_river = get(config.model, "runrivermodel", false)::Bool
 
@@ -186,22 +185,24 @@ function update(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SedimentModel}
         lateral.river.inlandsagg .= lateral.land.inlandsagg[inds_riv]
         lateral.river.inlandlagg .= lateral.land.inlandlagg[inds_riv]
 
-        update(lateral.river, network.river, config)
+        update!(lateral.river, network.river, config)
     end
 
-    return model
+    return nothing
 end
 
-function set_states(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:SedimentModel}
+function set_states!(
+    model::Model{N, L, V, R, W, T},
+) where {N, L, V, R, W, T <: SedimentModel}
     # read and set states in model object if reinit=false
     (; config) = model
     reinit = get(config.model, "reinit", true)::Bool
     if reinit == false
         instate_path = input_path(config, config.state.path_input)
         @info "Set initial conditions from state file `$instate_path`."
-        set_states(instate_path, model; type = Float)
+        set_states!(instate_path, model; type = Float)
     else
         @info "Set initial conditions from default values."
     end
-    return model
+    return nothing
 end
diff --git a/src/snow/snow.jl b/src/snow/snow.jl
new file mode 100644
index 000000000..a60521b13
--- /dev/null
+++ b/src/snow/snow.jl
@@ -0,0 +1,178 @@
+abstract type AbstractSnowModel{T} end
+
+"Struct for storing snow model variables"
+@get_units @grid_loc @with_kw struct SnowVariables{T}
+    # Snow storage [mm]
+    snow_storage::Vector{T} | "mm"
+    # Liquid water content in the snow pack [mm]
+    snow_water::Vector{T} | "mm"
+    # Snow water equivalent (SWE) [mm]
+    swe::Vector{T} | "mm"
+    # Snow melt [mm Δt⁻¹]
+    snow_melt::Vector{T}
+    # Runoff from snowpack [mm Δt⁻¹]
+    runoff::Vector{T}
+end
+
+"Initialize snow model variables"
+function SnowVariables(T::Type{<:AbstractFloat}, n::Int)
+    return SnowVariables{T}(;
+        snow_storage = fill(0.0, n),
+        snow_water = fill(0.0, n),
+        swe = fill(mv, n),
+        runoff = fill(mv, n),
+        snow_melt = fill(mv, n),
+    )
+end
+
+"Struct for storing snow model boundary conditions"
+@get_units @grid_loc @with_kw struct SnowBC{T}
+    # Effective precipitation [mm Δt⁻¹]
+    effective_precip::Vector{T}
+    # Snow precipitation [mm Δt⁻¹]
+    snow_precip::Vector{T}
+    # Liquid precipitation [mm Δt⁻¹]
+    liquid_precip::Vector{T}
+end
+
+"Initialize snow model boundary conditions"
+function SnowBC(T::Type{<:AbstractFloat}, n::Int)
+    return SnowBC{T}(;
+        effective_precip = fill(mv, n),
+        snow_precip = fill(mv, n),
+        liquid_precip = fill(mv, n),
+    )
+end
+
+"Struct for storing snow HBV model parameters"
+@get_units @grid_loc @with_kw struct SnowHbvParameters{T}
+    # Degree-day factor [mm ᵒC⁻¹ Δt⁻¹]
+    cfmax::Vector{T} | "mm ᵒC-1 dt-1"
+    # Threshold temperature for snowfall [ᵒC]
+    tt::Vector{T} | "ᵒC"
+    # Threshold temperature interval length [ᵒC]
+    tti::Vector{T} | "ᵒC"
+    # Threshold temperature for snowmelt [ᵒC]
+    ttm::Vector{T} | "ᵒC"
+    # Water holding capacity as fraction of current snow pack [-]
+    whc::Vector{T} | "-"
+end
+
+"Snow HBV model"
+@with_kw struct SnowHbvModel{T} <: AbstractSnowModel{T}
+    boundary_conditions::SnowBC{T}
+    parameters::SnowHbvParameters{T}
+    variables::SnowVariables{T}
+end
+
+struct NoSnowModel{T} <: AbstractSnowModel{T} end
+
+"Initialize snow HBV model parameters"
+function SnowHbvParameters(nc, config, inds, dt)
+    cfmax =
+        ncread(
+            nc,
+            config,
+            "vertical.snow.parameters.cfmax";
+            sel = inds,
+            defaults = 3.75653,
+            type = Float,
+        ) .* (dt / basetimestep)
+    tt = ncread(
+        nc,
+        config,
+        "vertical.snow.parameters.tt";
+        sel = inds,
+        defaults = 0.0,
+        type = Float,
+    )
+    tti = ncread(
+        nc,
+        config,
+        "vertical.snow.parameters.tti";
+        sel = inds,
+        defaults = 1.0,
+        type = Float,
+    )
+    ttm = ncread(
+        nc,
+        config,
+        "vertical.snow.parameters.ttm";
+        sel = inds,
+        defaults = 0.0,
+        type = Float,
+    )
+    whc = ncread(
+        nc,
+        config,
+        "vertical.snow.parameters.whc";
+        sel = inds,
+        defaults = 0.1,
+        type = Float,
+    )
+    snow_hbv_params =
+        SnowHbvParameters(; cfmax = cfmax, tt = tt, tti = tti, ttm = ttm, whc = whc)
+    return snow_hbv_params
+end
+
+"Initialize snow HBV model"
+function SnowHbvModel(nc, config, inds, dt)
+    n = length(inds)
+    params = SnowHbvParameters(nc, config, inds, dt)
+    vars = SnowVariables(Float, n)
+    bc = SnowBC(Float, n)
+    model = SnowHbvModel(; boundary_conditions = bc, parameters = params, variables = vars)
+    return model
+end
+
+"Update boundary condition (effective precipitation provided by an interception model) of a snow model for a single timestep"
+function update_boundary_conditions!(model::AbstractSnowModel, external_models::NamedTuple)
+    (; effective_precip) = model.boundary_conditions
+    (; interception) = external_models
+    @. effective_precip =
+        interception.variables.throughfall + interception.variables.stemflow
+    return nothing
+end
+
+function update_boundary_conditions!(model::NoSnowModel, external_models::NamedTuple)
+    return nothing
+end
+
+"Update snow HBV model for a single timestep"
+function update!(model::SnowHbvModel, atmospheric_forcing::AtmosphericForcing)
+    (; temperature) = atmospheric_forcing
+    (; snow_storage, snow_water, swe, snow_melt, runoff) = model.variables
+    (; effective_precip, snow_precip, liquid_precip) = model.boundary_conditions
+    (; tt, tti, ttm, cfmax, whc) = model.parameters
+
+    n = length(temperature)
+    threaded_foreach(1:n; basesize = 1000) do i
+        snow_precip[i], liquid_precip[i] =
+            precipitation_hbv(effective_precip[i], temperature[i], tti[i], tt[i])
+    end
+    threaded_foreach(1:n; basesize = 1000) do i
+        snow_storage[i], snow_water[i], swe[i], snow_melt[i], runoff[i] = snowpack_hbv(
+            snow_storage[i],
+            snow_water[i],
+            snow_precip[i],
+            liquid_precip[i],
+            temperature[i],
+            ttm[i],
+            cfmax[i],
+            whc[i],
+        )
+    end
+    return nothing
+end
+
+function update!(model::NoSnowModel, atmospheric_forcing::AtmosphericForcing)
+    return nothing
+end
+
+# wrapper methods
+get_runoff(model::NoSnowModel) = 0.0
+get_runoff(model::AbstractSnowModel) = model.variables.runoff
+get_snow_storage(model::NoSnowModel) = 0.0
+get_snow_storage(model::AbstractSnowModel) = model.variables.snow_storage
+get_snow_water(model::NoSnowModel) = 0.0
+get_snow_water(model::AbstractSnowModel) = model.variables.snow_water
\ No newline at end of file
diff --git a/src/snow/snow_process.jl b/src/snow/snow_process.jl
new file mode 100644
index 000000000..b61b0af88
--- /dev/null
+++ b/src/snow/snow_process.jl
@@ -0,0 +1,89 @@
+
+"""
+    snowpack_hbv(snow, snowwater, snow_precip, liquid_precip, temperature, ttm, cfmax, whc; cfr = 0.05)
+
+HBV type snowpack modeling using a temperature degree factor.
+The refreezing efficiency factor `cfr` is set to 0.05.
+
+# Arguments
+- `snow` (snow storage)
+- `snowwater` (liquid water content in the snow pack)
+- `snow_precip` (snow precipitation)
+- `liquid_precip` (liquid precipitation)
+- `ttm` (melting threshold)
+- `cfmax` (degree day factor, rate of snowmelt)
+- `whc` (water holding capacity of snow)
+- `cfr` refreeing efficiency constant in refreezing of liquied water in snow
+
+# Output
+- `snow`
+- `snowwater`
+- `swe` (snow water equivalent)
+- `runoff`
+"""
+function snowpack_hbv(
+    snow,
+    snowwater,
+    snow_precip,
+    liquid_precip,
+    temperature,
+    ttm,
+    cfmax,
+    whc;
+    cfr = 0.05,
+)
+    # potential snow melt, based on temperature
+    potsnowmelt = temperature > ttm ? cfmax * (temperature - ttm) : 0.0
+    # potential refreezing, based on temperature
+    potrefreezing = temperature < ttm ? cfmax * cfr * (ttm - temperature) : 0.0
+    # actual refreezing
+    refreezing = temperature < ttm ? min(potrefreezing, snowwater) : 0.0
+
+    # no landuse correction here
+    snowmelt = min(potsnowmelt, snow)  # actual snow melt
+    snow = snow + snow_precip + refreezing - snowmelt  # dry snow content
+    snowwater = snowwater - refreezing  # free water content in snow
+    maxsnowwater = snow * whc  # max water in the snow
+    snowwater = snowwater + snowmelt + liquid_precip  # add all water and potentially supersaturate the snowpack
+    runoff = max(snowwater - maxsnowwater, 0.0)  # rain + surpluss snowwater
+    snowwater = snowwater - runoff
+    swe = snowwater + snow # snow water equivalent
+
+    return snow, snowwater, swe, snowmelt, runoff
+end
+
+"""
+    precipitation_hbv(precipitation, temperature, tti, tt; rfcf = 1.0, sfcf = 1.0)
+
+HBV type precipitation routine to separate precipitation in snow precipitation and liquid precipitation. 
+All correction factors (RFCF and SFCF) are set to 1.
+
+# Arguments
+- `precipitation`
+- `temperature` (threshold temperature for snowfall)
+- `tti` (snowfall threshold interval length)
+- `tt` (threshold temperature for snowfall)
+- `rfcf` correction factor for liquid precipitation (rainfall)
+- `sfcf` correction factor for snow precipitation (snowfall)
+
+# Output
+- `snow_precip`
+- `liquid_precip`
+"""
+function precipitation_hbv(precipitation, temperature, tti, tt; rfcf = 1.0, sfcf = 1.0)
+    # fraction of precipitation which falls as rain
+    rainfrac = if iszero(tti)
+        Float(temperature > tt)
+    else
+        frac = (temperature - (tt - tti / 2.0)) / tti
+        min(frac, 1.0)
+    end
+    rainfrac = max(rainfrac, 0.0)
+
+    # fraction of precipitation which falls as snow
+    snowfrac = 1.0 - rainfrac
+    # different correction for liquid_precip and snow_precip
+    snow_precip = snowfrac * sfcf * precipitation  # snow_precip depth
+    liquid_precip = rainfrac * rfcf * precipitation  # liquid_precip depth
+    return snow_precip, liquid_precip
+end
\ No newline at end of file
diff --git a/src/soil/soil.jl b/src/soil/soil.jl
new file mode 100644
index 000000000..157869c2c
--- /dev/null
+++ b/src/soil/soil.jl
@@ -0,0 +1,1302 @@
+abstract type AbstractSoilModel{T} end
+
+"Struct for storing SBM soil model variables"
+@get_units @grid_loc @with_kw struct SbmSoilVariables{T, N}
+    # Calculated soil water pressure head h3 of the root water uptake reduction function (Feddes) [cm]
+    h3::Vector{T} | "cm"
+    # Unsaturated store capacity [mm]
+    ustorecapacity::Vector{T} | "mm"
+    # Amount of water in the unsaturated store, per layer [mm]
+    ustorelayerdepth::Vector{SVector{N, T}} | "mm"
+    # Thickness of unsaturated zone, per layer [mm]
+    ustorelayerthickness::Vector{SVector{N, T}} | "mm"
+    # Saturated store [mm]
+    satwaterdepth::Vector{T} | "mm"
+    # Pseudo-water table depth [mm] (top of the saturated zone)
+    zi::Vector{T} | "mm"
+    # Number of unsaturated soil layers
+    n_unsatlayers::Vector{Int} | "-"
+    # Transpiration [mm Δt⁻¹]
+    transpiration::Vector{T}
+    # Actual evaporation from unsaturated store [mm Δt⁻¹]
+    ae_ustore::Vector{T}
+    # Soil evaporation from unsaturated and saturated store [mm Δt⁻¹]
+    soilevap::Vector{T}
+    # Soil evaporation from saturated store [mm Δt⁻¹]
+    soilevapsat::Vector{T}
+    # Actual capillary rise [mm Δt⁻¹]
+    actcapflux::Vector{T}
+    # Actual transpiration from saturated store [mm Δt⁻¹]
+    actevapsat::Vector{T}
+    # Total actual evapotranspiration [mm Δt⁻¹]
+    actevap::Vector{T}
+    # Actual infiltration into the unsaturated zone [mm Δt⁻¹]
+    actinfilt::Vector{T}
+    # Actual infiltration non-compacted fraction [mm Δt⁻¹]
+    actinfiltsoil::Vector{T}
+    # Actual infiltration compacted fraction [mm Δt⁻¹]
+    actinfiltpath::Vector{T}
+    # Actual infiltration (compacted and the non-compacted areas) [mm Δt⁻¹]
+    infiltsoilpath::Vector{T}
+    # Infiltration excess water [mm Δt⁻¹]
+    infiltexcess::Vector{T}
+    # Water that cannot infiltrate due to saturated soil (saturation excess) [mm Δt⁻¹]
+    excesswater::Vector{T}
+    # Water exfiltrating during saturation excess conditions [mm Δt⁻¹]
+    exfiltsatwater::Vector{T}
+    # Water exfiltrating from unsaturated store because of change in water table [mm Δt⁻¹]
+    exfiltustore::Vector{T}
+    # Excess water for non-compacted fraction [mm Δt⁻¹]
+    excesswatersoil::Vector{T}
+    # Excess water for compacted fraction [mm Δt⁻¹]
+    excesswaterpath::Vector{T}
+    # Total surface runoff from infiltration and saturation excess (excluding actual open water evaporation) [mm Δt⁻¹]
+    runoff::Vector{T}
+    # Net surface runoff (surface runoff - actual open water evaporation) [mm Δt⁻¹]
+    net_runoff::Vector{T}
+    # Volumetric water content [-] per soil layer (including theta_r and saturated zone)
+    vwc::Vector{SVector{N, T}} | "-"
+    # Volumetric water content [%] per soil layer (including theta_r and saturated zone)
+    vwc_perc::Vector{SVector{N, T}} | "%"
+    # Root water storage [mm] in unsaturated and saturated zone (excluding theta_r)
+    rootstore::Vector{T} | "mm"
+    # Volumetric water content [-] in root zone (including theta_r and saturated zone)
+    vwc_root::Vector{T} | "-"
+    # Volumetric water content [%] in root zone (including theta_r and saturated zone)
+    vwc_percroot::Vector{T} | "%"
+    # Amount of available water in the unsaturated zone [mm]
+    ustoredepth::Vector{T} | "mm"
+    # Downward flux from unsaturated to saturated zone [mm Δt⁻¹]
+    transfer::Vector{T}
+    # Net recharge to saturated store [mm Δt⁻¹]
+    recharge::Vector{T}
+    # Actual leakage from saturated store [mm Δt⁻¹]
+    actleakage::Vector{T}
+    # Total water storage (excluding floodplain volume, lakes and reservoirs) [mm]
+    total_storage::Vector{T} | "mm"
+    # Top soil temperature [ᵒC]
+    tsoil::Vector{T} | "ᵒC"
+    # Soil infiltration reduction factor (when soil is frozen) [-]
+    f_infiltration_reduction::Vector{T} | "-"
+end
+
+"Initialize SBM soil model variables"
+function SbmSoilVariables(n, parameters)
+    (;
+        soilthickness,
+        maxlayers,
+        act_thickl,
+        sumlayers,
+        soilwatercapacity,
+        theta_s,
+        theta_r,
+    ) = parameters
+    satwaterdepth = 0.85 .* soilwatercapacity # cold state value for satwaterdepth
+    zi = @. max(0.0, soilthickness - satwaterdepth / (theta_s - theta_r))
+    ustorelayerthickness = set_layerthickness.(zi, sumlayers, act_thickl)
+    n_unsatlayers = number_of_active_layers.(ustorelayerthickness)
+
+    vwc = fill(mv, maxlayers, n)
+    vwc_perc = fill(mv, maxlayers, n)
+
+    vars = SbmSoilVariables(;
+        ustorelayerdepth = zero(act_thickl),
+        ustorecapacity = soilwatercapacity .- satwaterdepth,
+        ustorelayerthickness,
+        satwaterdepth,
+        zi,
+        n_unsatlayers,
+        transpiration = fill(mv, n),
+        h3 = fill(mv, n),
+        ae_ustore = fill(mv, n),
+        soilevap = fill(mv, n),
+        soilevapsat = fill(mv, n),
+        actcapflux = fill(mv, n),
+        actevapsat = fill(mv, n),
+        actevap = fill(mv, n),
+        f_infiltration_reduction = fill(Float(1), n),
+        actinfilt = fill(mv, n),
+        actinfiltsoil = fill(mv, n),
+        actinfiltpath = fill(mv, n),
+        infiltsoilpath = fill(mv, n),
+        infiltexcess = fill(mv, n),
+        excesswater = fill(mv, n),
+        exfiltsatwater = fill(mv, n),
+        exfiltustore = fill(mv, n),
+        excesswatersoil = fill(mv, n),
+        excesswaterpath = fill(mv, n),
+        runoff = fill(mv, n),
+        net_runoff = fill(mv, n),
+        vwc = svectorscopy(vwc, Val{maxlayers}()),
+        vwc_perc = svectorscopy(vwc_perc, Val{maxlayers}()),
+        rootstore = fill(mv, n),
+        vwc_root = fill(mv, n),
+        vwc_percroot = fill(mv, n),
+        ustoredepth = fill(mv, n),
+        transfer = fill(mv, n),
+        recharge = fill(mv, n),
+        actleakage = fill(mv, n),
+        tsoil = fill(Float(10.0), n),
+        total_storage = zeros(Float, n), # Set the total water storage from initialized values
+    )
+    return vars
+end
+
+"Struct for storing SBM soil model boundary conditions"
+@get_units @grid_loc @with_kw struct SbmSoilBC{T}
+    # Water flux at the soil surface [mm Δt⁻¹]
+    water_flux_surface::Vector{T}
+    # Potential transpiration rate [mm Δt⁻¹]
+    potential_transpiration::Vector{T}
+    # Potential soil evaporation rate [mm Δt⁻¹]
+    potential_soilevaporation::Vector{T}
+end
+
+"Initialize SBM soil model boundary conditions"
+function SbmSoilBC(
+    n;
+    water_flux_surface::Vector{T} = fill(mv, n),
+    potential_transpiration::Vector{T} = fill(mv, n),
+    potential_soilevaporation::Vector{T} = fill(mv, n),
+) where {T}
+    return SbmSoilBC{T}(;
+        water_flux_surface = water_flux_surface,
+        potential_transpiration = potential_transpiration,
+        potential_soilevaporation = potential_soilevaporation,
+    )
+end
+
+"Exponential depth profile of vertical hydraulic conductivity at the soil surface"
+@get_units @grid_loc struct KvExponential{T}
+    # Vertical hydraulic conductivity [mm Δt⁻¹] at soil surface
+    kv_0::Vector{T}
+    # A scaling parameter [mm⁻¹] (controls exponential decline of kv_0)
+    f::Vector{T} | "mm-1"
+end
+
+"Exponential constant depth profile of vertical hydraulic conductivity"
+@get_units @grid_loc struct KvExponentialConstant{T}
+    exponential::KvExponential{T}
+    # Depth [mm] from soil surface for which exponential decline of kv_0 is valid
+    z_exp::Vector{T} | "mm"
+end
+
+"Layered depth profile of vertical hydraulic conductivity"
+@get_units @grid_loc struct KvLayered{T, N}
+    # Vertical hydraulic conductivity [mm Δt⁻¹] per soil layer
+    kv::Vector{SVector{N, T}}
+end
+
+"Layered exponential depth profile of vertical hydraulic conductivity"
+@get_units @grid_loc struct KvLayeredExponential{T, N}
+    # A scaling parameter [mm⁻¹] (controls exponential decline of kv_0)
+    f::Vector{T} | "mm-1"
+    # Vertical hydraulic conductivity [mm Δt⁻¹] per soil layer
+    kv::Vector{SVector{N, T}}
+    # Number of soil layers with vertical hydraulic conductivity value `kv`
+    nlayers_kv::Vector{Int} | "-"
+    # Depth [mm] from soil surface for which layered profile is valid
+    z_layered::Vector{T} | "mm"
+end
+
+"Initialize SBM soil model hydraulic conductivity depth profile"
+function sbm_kv_profiles(nc, config, inds, kv_0, f, maxlayers, nlayers, sumlayers, dt)
+    kv_profile_type = get(config.input.vertical, "ksat_profile", "exponential")::String
+    n = length(inds)
+    if kv_profile_type == "exponential"
+        kv_profile = KvExponential(kv_0, f)
+    elseif kv_profile_type == "exponential_constant"
+        z_exp = ncread(
+            nc,
+            config,
+            "vertical.soil.parameters.z_exp";
+            optional = false,
+            sel = inds,
+            type = Float,
+        )
+        exp_profile = KvExponential(kv_0, f)
+        kv_profile = KvExponentialConstant(exp_profile, z_exp)
+    elseif kv_profile_type == "layered" || kv_profile_type == "layered_exponential"
+        kv =
+            ncread(
+                nc,
+                config,
+                "vertical.soil.parameters.kv";
+                sel = inds,
+                defaults = 1000.0,
+                type = Float,
+                dimname = :layer,
+            ) .* (dt / basetimestep)
+        if size(kv, 1) != maxlayers
+            parname = param(config.input.vertical.soil.parameter, "kv")
+            size1 = size(kv, 1)
+            error("$parname needs a layer dimension of size $maxlayers, but is $size1")
+        end
+        if kv_profile_type == "layered"
+            kv_profile = KvLayered(svectorscopy(kv, Val{maxlayers}()))
+        else
+            z_layered = ncread(
+                nc,
+                config,
+                "vertical.soil.parameters.z_layered";
+                optional = false,
+                sel = inds,
+                type = Float,
+            )
+            nlayers_kv = fill(0, n)
+            for i in eachindex(nlayers_kv)
+                layers = @view sumlayers[i][2:nlayers[i]]
+                _, k = findmin(abs.(z_layered[i] .- layers))
+                nlayers_kv[i] = k
+                z_layered[i] = layers[k]
+            end
+            kv_profile = KvLayeredExponential(
+                f,
+                svectorscopy(kv, Val{maxlayers}()),
+                nlayers_kv,
+                z_layered,
+            )
+        end
+    else
+        error("""An unknown "ksat_profile" is specified in the TOML file ($ksat_profile).
+              This should be "exponential", "exponential_constant", "layered" or
+              "layered_exponential".
+              """)
+    end
+    return kv_profile
+end
+
+"Struct for storing SBM soil model parameters"
+@get_units @grid_loc @with_kw struct SbmSoilParameters{T, N, M, Kv}
+    # Maximum number of soil layers [-]
+    maxlayers::Int
+    # Number of soil layers
+    nlayers::Vector{Int} | "-"
+    # Saturated water content (porosity) [-]
+    theta_s::Vector{T} | "-"
+    # Residual water content [-]
+    theta_r::Vector{T} | "-"
+    # Soilwater capacity [mm]
+    soilwatercapacity::Vector{T} | "mm"
+    # Muliplication factor [-] applied to kv_z (vertical flow)
+    kvfrac::Vector{SVector{N, T}} | "-"
+    # Air entry pressure [cm] of soil (Brooks-Corey)
+    hb::Vector{T} | "cm"
+    # Soil thickness [mm]
+    soilthickness::Vector{T} | "mm"
+    # Thickness of soil layers [mm]
+    act_thickl::Vector{SVector{N, T}} | "mm"
+    # Cumulative sum of soil layers [mm], starting at soil surface (0)
+    sumlayers::Vector{SVector{M, T}} | "mm"
+    # Infiltration capacity of the compacted areas [mm Δt⁻¹]
+    infiltcappath::Vector{T}
+    # Soil infiltration capacity [mm Δt⁻¹]
+    infiltcapsoil::Vector{T}
+    # Maximum leakage [mm Δt⁻¹] from saturated zone
+    maxleakage::Vector{T}
+    # Parameter [mm] controlling capillary rise
+    cap_hmax::Vector{T} | "mm"
+    # Coefficient [-] controlling capillary rise
+    cap_n::Vector{T} | "-"
+    # Brooks-Corey power coefﬁcient [-] for each soil layer
+    c::Vector{SVector{N, T}} | "-"
+    # Soil temperature smooth factor [-]
+    w_soil::Vector{T} | "-"
+    # Controls soil infiltration reduction factor when soil is frozen [-]
+    cf_soil::Vector{T} | "-"
+    # Fraction of compacted area  [-]
+    pathfrac::Vector{T} | "-"
+    # Controls how roots are linked to water table [-]
+    rootdistpar::Vector{T} | "-"
+    # Fraction of the root length density in each soil layer [-]
+    rootfraction::Vector{SVector{N, T}} | "-"
+    # Soil water pressure head h1 of the root water uptake reduction function (Feddes) [cm]
+    h1::Vector{T} | "cm"
+    # Soil water pressure head h2 of the root water uptake reduction function (Feddes) [cm]
+    h2::Vector{T} | "cm"
+    # Soil water pressure head h3_high of the root water uptake reduction function (Feddes) [cm]
+    h3_high::Vector{T} | "cm"
+    # Soil water pressure head h3_low of the root water uptake reduction function (Feddes) [cm]
+    h3_low::Vector{T} | "cm"
+    # Soil water pressure head h4 of the root water uptake reduction function (Feddes) [cm]
+    h4::Vector{T} | "cm"
+    # Root water uptake reduction at soil water pressure head h1 (0.0 or 1.0) [-]
+    alpha_h1::Vector{T} | "-"
+    # Soil fraction [-]
+    soil_fraction::Vector{T} | "-"
+    # Vertical hydraulic conductivity profile type
+    kv_profile::Kv
+    # Vegetation parameter set
+    vegetation_parameter_set::VegetationParameters{T}
+end
+
+"Initialize SBM soil model parameters"
+function SbmSoilParameters(nc, config, vegetation_parameter_set, inds, dt)
+    config_thicknesslayers = get(config.model, "thicknesslayers", Float[])
+    if length(config_thicknesslayers) > 0
+        thicknesslayers = SVector(Tuple(push!(Float.(config_thicknesslayers), mv)))
+        cum_depth_layers = pushfirst(cumsum(thicknesslayers), 0.0)
+        maxlayers = length(thicknesslayers) # max number of soil layers
+    else
+        thicknesslayers = SVector.(soilthickness)
+        cum_depth_layers = pushfirst(cumsum(thicknesslayers), 0.0)
+        maxlayers = 1
+    end
+    w_soil =
+        ncread(
+            nc,
+            config,
+            "vertical.soil.parameters.w_soil";
+            sel = inds,
+            defaults = 0.1125,
+            type = Float,
+        ) .* (dt / basetimestep)
+    cf_soil = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.cf_soil";
+        sel = inds,
+        defaults = 0.038,
+        type = Float,
+    )
+    # soil parameters
+    theta_s = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.theta_s";
+        sel = inds,
+        defaults = 0.6,
+        type = Float,
+    )
+    theta_r = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.theta_r";
+        sel = inds,
+        defaults = 0.01,
+        type = Float,
+    )
+    kv_0 =
+        ncread(
+            nc,
+            config,
+            "vertical.soil.parameters.kv_0";
+            sel = inds,
+            defaults = 3000.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+    f = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.f";
+        sel = inds,
+        defaults = 0.001,
+        type = Float,
+    )
+    hb = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.hb";
+        sel = inds,
+        defaults = -10.0,
+        type = Float,
+    )
+    h1 = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.h1";
+        sel = inds,
+        defaults = 0.0,
+        type = Float,
+    )
+    h2 = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.h2";
+        sel = inds,
+        defaults = -100.0,
+        type = Float,
+    )
+    h3_high = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.h3_high";
+        sel = inds,
+        defaults = -400.0,
+        type = Float,
+    )
+    h3_low = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.h3_low";
+        sel = inds,
+        defaults = -1000.0,
+        type = Float,
+    )
+    h4 = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.h4";
+        sel = inds,
+        defaults = -15849.0,
+        type = Float,
+    )
+    alpha_h1 = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.alpha_h1";
+        sel = inds,
+        defaults = 1.0,
+        type = Float,
+    )
+    soilthickness = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.soilthickness";
+        sel = inds,
+        defaults = 2000.0,
+        type = Float,
+    )
+    infiltcappath =
+        ncread(
+            nc,
+            config,
+            "vertical.soil.parameters.infiltcappath";
+            sel = inds,
+            defaults = 10.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+    infiltcapsoil =
+        ncread(
+            nc,
+            config,
+            "vertical.soil.parameters.infiltcapsoil";
+            sel = inds,
+            defaults = 100.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+    maxleakage =
+        ncread(
+            nc,
+            config,
+            "vertical.soil.parameters.maxleakage";
+            sel = inds,
+            defaults = 0.0,
+            type = Float,
+        ) .* (dt / basetimestep)
+
+    c = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.c";
+        sel = inds,
+        defaults = 10.0,
+        type = Float,
+        dimname = :layer,
+    )
+    if size(c, 1) != maxlayers
+        parname = param(config.input.vertical, "c")
+        size1 = size(c, 1)
+        error("$parname needs a layer dimension of size $maxlayers, but is $size1")
+    end
+    kvfrac = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.kvfrac";
+        sel = inds,
+        defaults = 1.0,
+        type = Float,
+        dimname = :layer,
+    )
+    if size(kvfrac, 1) != maxlayers
+        parname = param(config.input, "vertical.soil.parameters.kvfrac")
+        size1 = size(kvfrac, 1)
+        error("$parname needs a layer dimension of size $maxlayers, but is $size1")
+    end
+    # fraction compacted area
+    pathfrac = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.pathfrac";
+        sel = inds,
+        defaults = 0.01,
+        type = Float,
+    )
+
+    # vegetation parameters
+    rootdistpar = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.rootdistpar";
+        sel = inds,
+        defaults = -500.0,
+        type = Float,
+    )
+    cap_hmax = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.cap_hmax";
+        sel = inds,
+        defaults = 2000.0,
+        type = Float,
+    )
+    cap_n = ncread(
+        nc,
+        config,
+        "vertical.soil.parameters.cap_n";
+        sel = inds,
+        defaults = 2.0,
+        type = Float,
+    )
+
+    act_thickl = set_layerthickness.(soilthickness, (cum_depth_layers,), (thicknesslayers,))
+    sumlayers = @. pushfirst(cumsum(act_thickl), 0.0)
+    nlayers = number_of_active_layers.(act_thickl)
+
+    if length(config_thicknesslayers) > 0
+        # root fraction read from nc file, in case of multiple soil layers and TOML file
+        # includes "vertical.rootfraction"
+        if haskey(config.input.vertical.soil.parameters, "rootfraction")
+            rootfraction = ncread(
+                nc,
+                config,
+                "vertical.soil.parameters.rootfraction";
+                sel = inds,
+                optional = false,
+                type = Float,
+                dimname = :layer,
+            )
+        else
+            n = length(inds)
+            (; rootingdepth) = vegetation_parameter_set
+            # default root fraction in case of multiple soil layers
+            rootfraction = zeros(Float, maxlayers, n)
+            for i in 1:n
+                if rootingdepth[i] > 0.0
+                    for k in 1:maxlayers
+                        if (rootingdepth[i] - sumlayers[i][k]) >= act_thickl[i][k]
+                            rootfraction[k, i] = act_thickl[i][k] / rootingdepth[i]
+                        else
+                            rootfraction[k, i] =
+                                max(rootingdepth[i] - sumlayers[i][k], 0.0) /
+                                rootingdepth[i]
+                        end
+                    end
+                end
+            end
+        end
+    else
+        # for the case of 1 soil layer
+        rootfraction = ones(Float, maxlayers, n)
+    end
+
+    kv_profile =
+        sbm_kv_profiles(nc, config, inds, kv_0, f, maxlayers, nlayers, sumlayers, dt)
+
+    soilwatercapacity = @. soilthickness * (theta_s - theta_r)
+
+    n = length(inds)
+    sbm_params = SbmSoilParameters(;
+        maxlayers,
+        nlayers,
+        soilwatercapacity,
+        theta_s,
+        theta_r,
+        kvfrac = svectorscopy(kvfrac, Val{maxlayers}()),
+        hb,
+        h1,
+        h2,
+        h3_high,
+        h3_low,
+        h4,
+        alpha_h1,
+        soilthickness,
+        act_thickl,
+        sumlayers,
+        infiltcappath,
+        infiltcapsoil,
+        maxleakage,
+        pathfrac,
+        rootdistpar,
+        rootfraction = svectorscopy(rootfraction, Val{maxlayers}()),
+        cap_hmax,
+        cap_n,
+        c = svectorscopy(c, Val{maxlayers}()),
+        w_soil,
+        cf_soil,
+        soil_fraction = fill(mv, n),
+        kv_profile,
+        vegetation_parameter_set,
+    )
+    return sbm_params
+end
+
+"SBM soil model"
+@with_kw struct SbmSoilModel{T, N, M, Kv} <: AbstractSoilModel{T}
+    boundary_conditions::SbmSoilBC{T}
+    parameters::SbmSoilParameters{T, N, M, Kv}
+    variables::SbmSoilVariables{T, N}
+end
+
+"Initialize SBM soil model"
+function SbmSoilModel(nc, config, vegetation_parameter_set, inds, dt)
+    n = length(inds)
+    params = SbmSoilParameters(nc, config, vegetation_parameter_set, inds, dt)
+    vars = SbmSoilVariables(n, params)
+    bc = SbmSoilBC(n)
+    model = SbmSoilModel(; boundary_conditions = bc, parameters = params, variables = vars)
+    return model
+end
+
+"Return soil fraction"
+function soil_fraction!(soil, runoff, glacier)
+    (; canopygapfraction) = soil.parameters.vegetation_parameter_set
+    (; soil_fraction) = soil.parameters
+    (; waterfrac, riverfrac) = runoff.parameters
+    glacier_fraction = get_glacier_fraction(glacier)
+
+    @. soil_fraction =
+        max(canopygapfraction - waterfrac - riverfrac - glacier_fraction, 0.0)
+    return nothing
+end
+
+"Update boundary conditions of the SBM soil model for a single timestep"
+function update_boundary_conditions!(
+    model::SbmSoilModel,
+    atmospheric_forcing::AtmosphericForcing,
+    external_models::NamedTuple,
+)
+    (; interception, runoff, demand, allocation) = external_models
+    (; potential_transpiration, water_flux_surface, potential_soilevaporation) =
+        model.boundary_conditions
+
+    potential_transpiration .= get_potential_transpiration(interception)
+
+    @. potential_soilevaporation =
+        model.parameters.soil_fraction * atmospheric_forcing.potential_evaporation
+    evaporation!(demand.paddy, potential_soilevaporation)
+    potential_soilevaporation .= potential_soilevaporation .- get_evaporation(demand.paddy)
+
+    water_flux_surface .=
+        max.(
+            runoff.boundary_conditions.water_flux_surface .+
+            get_irrigation_allocated(allocation) .- runoff.variables.runoff_river .-
+            runoff.variables.runoff_land .+ get_water_depth(demand.paddy),
+            0.0,
+        )
+    return nothing
+end
+
+"Update soil temperature of the SBM soil model for a single timestep"
+function soil_temperature!(model::SbmSoilModel, snow::AbstractSnowModel, temperature)
+    v = model.variables
+    p = model.parameters
+    @. v.tsoil = soil_temperature(v.tsoil, p.w_soil, temperature)
+    return nothing
+end
+
+soil_temperature!(model::SbmSoilModel, snow::NoSnowModel, temperature) = nothing
+
+"Update total available water in the unsaturated zone of the SBM soil model for a single timestep"
+function ustoredepth!(model::SbmSoilModel)
+    v = model.variables
+    p = model.parameters
+    for i in eachindex(v.ustorelayerdepth)
+        v.ustoredepth[i] = sum(@view v.ustorelayerdepth[i][1:p.nlayers[i]])
+    end
+    return nothing
+end
+
+"Update the infiltration reduction factor of the SBM soil model for a single timestep"
+function infiltration_reduction_factor!(
+    model::SbmSoilModel;
+    modelsnow = false,
+    soilinfreduction = false,
+)
+    v = model.variables
+    p = model.parameters
+
+    n = length(v.tsoil)
+    threaded_foreach(1:n; basesize = 1000) do i
+        v.f_infiltration_reduction[i] = infiltration_reduction_factor(
+            v.tsoil[i],
+            p.cf_soil[i];
+            modelsnow = modelsnow,
+            soilinfreduction = soilinfreduction,
+        )
+    end
+    return nothing
+end
+
+"""
+    infiltration!(model::SbmSoilModel)
+
+Update the infiltration rate `infiltsoilpath` and infiltration excess water rate
+`infiltexcess` of the SBM soil model for a single timestep.
+"""
+function infiltration!(model::SbmSoilModel)
+    v = model.variables
+    p = model.parameters
+    (; water_flux_surface) = model.boundary_conditions
+
+    n = length(v.infiltsoilpath)
+    threaded_foreach(1:n; basesize = 1000) do i
+        v.infiltsoilpath[i], v.infiltexcess[i] = infiltration(
+            water_flux_surface[i],
+            p.pathfrac[i],
+            p.infiltcapsoil[i],
+            p.infiltcappath[i],
+            v.ustorecapacity[i],
+            v.f_infiltration_reduction[i],
+        )
+    end
+    return nothing
+end
+
+"""
+    unsaturated_zone_flow!(model::SbmSoilModel; transfermethod = false)
+
+Update unsaturated storage `ustorelayerdepth` and the `transfer` of water from the
+unsaturated to the saturated store of the SBM soil model for a single timestep, based on the
+original Topog_SBM formulation (`transfermethod = true` and one soil layer) or the
+Brooks-Corey approach (`transfermethod = false` and one or multiple soil layers).
+"""
+function unsaturated_zone_flow!(model::SbmSoilModel; transfermethod = false)
+    v = model.variables
+    p = model.parameters
+
+    n = length(v.transfer)
+    threaded_foreach(1:n; basesize = 250) do i
+        if v.n_unsatlayers[i] > 0
+            if transfermethod && p.maxlayers == 1
+                # original Topog_SBM formulation
+                ustorelayerdepth = v.ustorelayerdepth[i][1] + v.infiltsoilpath[i]
+                kv_z =
+                    hydraulic_conductivity_at_depth(p.kv_profile, p.kvfrac, v.zi[i], i, 1)
+                ustorelayerdepth, v.transfer[i] = unsatzone_flow_sbm(
+                    ustorelayerdepth,
+                    p.soilwatercapacity[i],
+                    v.satwaterdepth[i],
+                    kv_z,
+                    v.ustorelayerthickness[1],
+                    p.theta_s[i],
+                    p.theta_r[i],
+                )
+                v.ustorelayerdepth[i] = setindex(v.ustorelayerdepth[i], ustorelayerdepth, 1)
+            else
+                # Brooks-Corey approach
+                z = cumsum(v.ustorelayerthickness[i])
+                flow_rate = 0.0
+                for m in 1:v.n_unsatlayers[i]
+                    l_sat = v.ustorelayerthickness[i][m] * (p.theta_s[i] - p.theta_r[i])
+                    kv_z =
+                        hydraulic_conductivity_at_depth(p.kv_profile, p.kvfrac, z[m], i, m)
+                    ustorelayerdepth = if m == 1
+                        v.ustorelayerdepth[i][m] + v.infiltsoilpath[i]
+                    else
+                        v.ustorelayerdepth[i][m] + flow_rate
+                    end
+                    ustorelayerdepth, flow_rate =
+                        unsatzone_flow_layer(ustorelayerdepth, kv_z, l_sat, p.c[i][m])
+                    v.ustorelayerdepth[i] =
+                        setindex(v.ustorelayerdepth[i], ustorelayerdepth, m)
+                end
+                v.transfer[i] = flow_rate
+            end
+        else
+            v.transfer[i] = 0.0
+        end
+    end
+    return nothing
+end
+
+"""
+    soil_evaporation!(model::SbmSoilModel)
+
+Update soil evaporation from the saturated store `soilevapsat` and the total soil
+evaporation from the unsaturated and saturated store `soilevap` of the SBM soil model for a
+single timestep. Also unsaturated storage `ustorelayerdepth` and the saturated store
+`satwaterdepth` are updated.
+"""
+function soil_evaporation!(model::SbmSoilModel)
+    (; potential_soilevaporation) = model.boundary_conditions
+    v = model.variables
+    p = model.parameters
+
+    n = length(potential_soilevaporation)
+    threaded_foreach(1:n; basesize = 1000) do i
+        potsoilevap = potential_soilevaporation[i]
+        # First calculate the evaporation of unsaturated storage into the
+        # atmosphere from the upper layer.
+        if p.maxlayers == 1
+            saturationdeficit = p.soilwatercapacity[i] - v.satwaterdepth[i]
+            soilevapunsat =
+                potsoilevap * min(1.0, saturationdeficit / p.soilwatercapacity[i])
+        else
+            soilevapunsat = soil_evaporation_unsatured_store(
+                potsoilevap,
+                v.ustorelayerdepth[i][1],
+                v.ustorelayerthickness[i][1],
+                v.n_unsatlayers[i],
+                v.zi[i],
+                p.theta_s[i] - p.theta_r[i],
+            )
+        end
+        # Ensure that the unsaturated evaporation rate does not exceed the
+        # available unsaturated moisture
+        soilevapunsat = min(soilevapunsat, v.ustorelayerdepth[i][1])
+        # Update the additional atmospheric demand
+        potsoilevap = potsoilevap - soilevapunsat
+        v.ustorelayerdepth[i] =
+            setindex(v.ustorelayerdepth[i], v.ustorelayerdepth[i][1] - soilevapunsat, 1)
+
+        if p.maxlayers == 1
+            soilevapsat = 0.0
+        else
+            soilevapsat = soil_evaporation_satured_store(
+                potsoilevap,
+                v.n_unsatlayers[i],
+                p.act_thickl[i][1],
+                v.zi[i],
+                p.theta_s[i] - p.theta_r[i],
+            )
+        end
+        v.soilevapsat[i] = soilevapsat
+        v.soilevap[i] = soilevapunsat + soilevapsat
+        v.satwaterdepth[i] = v.satwaterdepth[i] - soilevapsat
+    end
+    return nothing
+end
+
+"""
+    transpiration!(model::SbmSoilModel, dt; ust = false)
+
+Update total `transpiration`, transpiration from the unsaturated store `ae_ustore` and
+saturated store `actevapsat` of the SBM soil model for a single timestep. Also unsaturated
+storage `ustorelayerdepth` and the saturated store `satwaterdepth` are updated.
+"""
+function transpiration!(model::SbmSoilModel, dt; ust = false)
+    (; potential_transpiration) = model.boundary_conditions
+    v = model.variables
+    p = model.parameters
+
+    rootingdepth = get_rootingdepth(model)
+    n = length(rootingdepth)
+    threaded_foreach(1:n; basesize = 250) do i
+        actevapustore = 0.0
+        rootfraction_unsat = 0.0
+        v.h3[i] = feddes_h3(p.h3_high[i], p.h3_low[i], potential_transpiration[i], dt)
+        for k in 1:v.n_unsatlayers[i]
+            vwc = max(
+                v.ustorelayerdepth[i][k] / v.ustorelayerthickness[i][k],
+                Float(0.0000001),
+            )
+            head = head_brooks_corey(vwc, p.theta_s[i], p.theta_r[i], p.c[i][k], p.hb[i])
+            alpha = rwu_reduction_feddes(
+                head,
+                p.h1[i],
+                p.h2[i],
+                v.h3[i],
+                p.h4[i],
+                p.alpha_h1[i],
+            )
+
+            if ust
+                availcap = ustorelayerdepth[i][k] * 0.99
+            else
+                availcap = min(
+                    1.0,
+                    max(
+                        0.0,
+                        (rootingdepth[i] - p.sumlayers[i][k]) /
+                        v.ustorelayerthickness[i][k],
+                    ),
+                )
+            end
+            maxextr = v.ustorelayerdepth[i][k] * availcap
+            # the rootfraction is valid for the root length in a soil layer, if zi decreases the root length
+            # the rootfraction needs to be adapted
+            if k == v.n_unsatlayers[i] && v.zi[i] < rootingdepth[i]
+                rootlength = min(p.act_thickl[i][k], rootingdepth[i] - p.sumlayers[i][k])
+                rootfraction_act =
+                    p.rootfraction[i][k] * (v.ustorelayerthickness[i][k] / rootlength)
+            else
+                rootfraction_act = p.rootfraction[i][k]
+            end
+            actevapustore_layer =
+                min(alpha * rootfraction_act * potential_transpiration[i], maxextr)
+            rootfraction_unsat = rootfraction_unsat + rootfraction_act
+            ustorelayerdepth = v.ustorelayerdepth[i][k] - actevapustore_layer
+            actevapustore = actevapustore + actevapustore_layer
+            v.ustorelayerdepth[i] = setindex(v.ustorelayerdepth[i], ustorelayerdepth, k)
+        end
+
+        # transpiration from saturated store
+        wetroots = scurve(v.zi[i], rootingdepth[i], Float(1.0), p.rootdistpar[i])
+        alpha = rwu_reduction_feddes(
+            Float(0.0),
+            p.h1[i],
+            p.h2[i],
+            v.h3[i],
+            p.h4[i],
+            p.alpha_h1[i],
+        )
+        # include remaining root fraction if rooting depth is below water table zi
+        if v.zi[i] >= rootingdepth[i]
+            f_roots = wetroots
+            restevap = potential_transpiration[i] - actevapustore
+        else
+            f_roots = wetroots * (1.0 - rootfraction_unsat)
+            restevap = potential_transpiration[i]
+        end
+        actevapsat = min(restevap * f_roots * alpha, v.satwaterdepth[i])
+
+        v.ae_ustore[i] = actevapustore
+        v.actevapsat[i] = actevapsat
+        v.satwaterdepth[i] = v.satwaterdepth[i] - actevapsat
+        v.transpiration[i] = actevapustore + actevapsat
+    end
+    return nothing
+end
+
+"""
+    actual_infiltration!(model::SbmSoilModel)
+
+Update the actual infiltration rate `actinfilt` of the SBM soil model for a single timestep.
+
+A soil water balance check is performed. Unsaturated storage that exceeds the maximum
+storage per unsaturated soil layer is transferred to the layer above (or surface), from the
+bottom to the top unsaturated soil layer. The resulting excess water `ustoredepth_excess` is
+subtracted from the infiltration rate `infiltsoilpath`.
+"""
+function actual_infiltration!(model::SbmSoilModel)
+    v = model.variables
+    p = model.parameters
+
+    n = length(v.actinfilt)
+    threaded_foreach(1:n; basesize = 1000) do i
+        # check soil moisture balance per layer
+        ustoredepth_excess = 0.0
+        for k in v.n_unsatlayers[i]:-1:1
+            ustoredepth_excess = max(
+                0.0,
+                v.ustorelayerdepth[i][k] -
+                v.ustorelayerthickness[i][k] * (p.theta_s[i] - p.theta_r[i]),
+            )
+            v.ustorelayerdepth[i] = setindex(
+                v.ustorelayerdepth[i],
+                v.ustorelayerdepth[i][k] - ustoredepth_excess,
+                k,
+            )
+            if k > 1
+                v.ustorelayerdepth[i] = setindex(
+                    v.ustorelayerdepth[i],
+                    v.ustorelayerdepth[i][k - 1] + ustoredepth_excess,
+                    k - 1,
+                )
+            end
+        end
+
+        v.actinfilt[i] = v.infiltsoilpath[i] - ustoredepth_excess
+    end
+    return nothing
+end
+
+"""
+    actual_infiltration_soil_path!(model::SbmSoilModel)
+
+Update the actual infiltration rate for soil `actinfiltsoil` and paved area `actinfiltpath`
+of the SBM soil model for a single timestep.
+"""
+function actual_infiltration_soil_path!(model::SbmSoilModel)
+    v = model.variables
+    p = model.parameters
+    (; water_flux_surface) = model.boundary_conditions
+
+    n = length(water_flux_surface)
+    threaded_foreach(1:n; basesize = 1000) do i
+        v.actinfiltsoil[i], v.actinfiltpath[i] = actual_infiltration_soil_path(
+            water_flux_surface[i],
+            v.actinfilt[i],
+            p.pathfrac[i],
+            p.infiltcapsoil[i],
+            p.infiltcappath[i],
+            v.f_infiltration_reduction[i],
+        )
+    end
+    return nothing
+end
+
+"""
+    capillary_flux!(model::SbmSoilModel)
+
+Update the capillary flux `actcapflux` of the SBM soil model for a single timestep.
+"""
+function capillary_flux!(model::SbmSoilModel)
+    v = model.variables
+    p = model.parameters
+    rootingdepth = get_rootingdepth(model)
+
+    n = length(rootingdepth)
+    threaded_foreach(1:n; basesize = 1000) do i
+        if v.n_unsatlayers[i] > 0
+            ksat = hydraulic_conductivity_at_depth(
+                p.kv_profile,
+                p.kvfrac,
+                v.zi[i],
+                i,
+                v.n_unsatlayers[i],
+            )
+            maxcapflux =
+                max(0.0, min(ksat, v.ae_ustore[i], v.ustorecapacity[i], v.satwaterdepth[i]))
+
+            if v.zi[i] > rootingdepth[i]
+                capflux =
+                    maxcapflux *
+                    pow(1.0 - min(v.zi[i], p.cap_hmax[i]) / (p.cap_hmax[i]), p.cap_n[i])
+            else
+                capflux = 0.0
+            end
+
+            netcapflux = capflux
+            actcapflux = 0.0
+            for k in v.n_unsatlayers[i]:-1:1
+                toadd = min(
+                    netcapflux,
+                    max(
+                        v.ustorelayerthickness[i][k] * (p.theta_s[i] - p.theta_r[i]) -
+                        v.ustorelayerdepth[i][k],
+                        0.0,
+                    ),
+                )
+                v.ustorelayerdepth[i] =
+                    setindex(v.ustorelayerdepth[i], v.ustorelayerdepth[i][k] + toadd, k)
+                netcapflux = netcapflux - toadd
+                actcapflux = actcapflux + toadd
+            end
+            v.actcapflux[i] = actcapflux
+        else
+            v.actcapflux[i] = 0.0
+        end
+    end
+    return nothing
+end
+
+"""
+    leakage!(model::SbmSoilModel)
+
+Update the actual leakage rate `actleakage` of the SBM soil model for a single timestep.
+"""
+function leakage!(model::SbmSoilModel)
+    v = model.variables
+    p = model.parameters
+
+    n = length(v.actleakage)
+    threaded_foreach(1:n; basesize = 1000) do i
+        deepksat = hydraulic_conductivity_at_depth(
+            p.kv_profile,
+            p.kvfrac,
+            p.soilthickness[i],
+            i,
+            p.nlayers[i],
+        )
+        deeptransfer = min(v.satwaterdepth[i], deepksat)
+        v.actleakage[i] = max(0.0, min(p.maxleakage[i], deeptransfer))
+    end
+    return nothing
+end
+
+"""
+    update!(
+        model::SbmSoilModel,
+        atmospheric_forcing::AtmosphericForcing,
+        external_models::NamedTuple,
+        config,
+        dt,
+    )
+
+Update the SBM soil model (infiltration, unsaturated zone flow, soil evaporation and
+transpiration, capillary flux and leakage) for a single timestep.
+"""
+function update!(
+    model::SbmSoilModel,
+    atmospheric_forcing::AtmosphericForcing,
+    external_models::NamedTuple,
+    config,
+    dt,
+)
+    soilinfreduction = get(config.model, "soilinfreduction", false)::Bool
+    modelsnow = get(config.model, "snow", false)::Bool
+    transfermethod = get(config.model, "transfermethod", false)::Bool
+    ust = get(config.model, "whole_ust_available", false)::Bool # should be removed from optional setting and code?
+
+    (; snow, runoff, demand) = external_models
+    (; temperature) = atmospheric_forcing
+    (; water_flux_surface) = model.boundary_conditions
+    v = model.variables
+    p = model.parameters
+
+    ustoredepth!(model)
+    @. v.ustorecapacity = p.soilwatercapacity - v.satwaterdepth - v.ustoredepth
+    @. v.ustorelayerthickness = set_layerthickness(v.zi, p.sumlayers, p.act_thickl)
+    @. v.n_unsatlayers = number_of_active_layers(v.ustorelayerthickness)
+
+    # infiltration
+    soil_temperature!(model, snow, temperature)
+    infiltration_reduction_factor!(
+        model;
+        modelsnow = modelsnow,
+        soilinfreduction = soilinfreduction,
+    )
+    infiltration!(model)
+    # unsaturated zone flow
+    unsaturated_zone_flow!(model; transfermethod = transfermethod)
+    # soil evaporation and transpiration
+    soil_evaporation!(model)
+    transpiration!(model, dt; ust = ust)
+    # actual infiltration and excess water
+    actual_infiltration!(model)
+    @. v.excesswater = water_flux_surface - v.actinfilt - v.infiltexcess
+    actual_infiltration_soil_path!(model)
+    @. v.excesswatersoil =
+        max(water_flux_surface * (1.0 - p.pathfrac) - v.actinfiltsoil, 0.0)
+    @. v.excesswaterpath = max(water_flux_surface * p.pathfrac - v.actinfiltpath, 0.0)
+    # recompute the unsaturated store and ustorecapacity (for capillary flux)
+    ustoredepth!(model)
+    @. v.ustorecapacity = p.soilwatercapacity - v.satwaterdepth - v.ustoredepth
+    # capillary flux and leakage
+    capillary_flux!(model)
+    leakage!(model)
+    # recharge rate to the saturated store
+    @. v.recharge =
+        (v.transfer - v.actcapflux - v.actleakage - v.actevapsat - v.soilevapsat)
+    # total actual evapotranspiration
+    v.actevap .=
+        v.soilevap .+ v.transpiration .+ runoff.variables.ae_openw_r .+
+        runoff.variables.ae_openw_l .+ get_evaporation(demand.paddy)
+    return nothing
+end
+
+"""
+    update!(model::SbmSoilModel, external_models::NamedTuple)
+
+Update the SBM soil model for a single timestep based on the update of a subsurface flow
+model, resulting in a change in water table depth and an exfiltration rate `exfiltwater`.
+
+The water table depth `zi`, unsaturated storage `ustorelayerdepth`, water exfiltrating from
+the unsaturated store `exfiltustore`, land `runoff` and `net_runoff`, the saturated store
+`satwaterdepth` and the water exfiltrating during saturation excess conditions
+`exfiltsatwater` are updated. Addionally, volumetric water content per soil layer and for
+the root zone are updated.
+"""
+function update!(model::SbmSoilModel, external_models::NamedTuple)
+    (; runoff, demand, subsurface) = external_models
+    (; runoff_land, ae_openw_l) = runoff.variables
+    p = model.parameters
+    v = model.variables
+
+    zi = get_water_depth(subsurface) * 1000.0
+    exfiltsatwater = get_exfiltwater(subsurface) * 1000.0
+    rootingdepth = get_rootingdepth(model)
+
+    n = length(model.variables.zi)
+    threaded_foreach(1:n; basesize = 1000) do i
+        ustorelayerthickness = set_layerthickness(zi[i], p.sumlayers[i], p.act_thickl[i])
+        n_unsatlayers = number_of_active_layers(ustorelayerthickness)
+        # exfiltration from ustore
+        ustorelayerdepth = v.ustorelayerdepth[i]
+        exfiltustore = 0.0
+        for k in v.n_unsatlayers[i]:-1:1
+            if k <= n_unsatlayers
+                exfiltustore = max(
+                    0,
+                    ustorelayerdepth[k] -
+                    ustorelayerthickness[k] * (p.theta_s[i] - p.theta_r[i]),
+                )
+            else
+                exfiltustore = ustorelayerdepth[k]
+            end
+            ustorelayerdepth =
+                setindex(ustorelayerdepth, ustorelayerdepth[k] - exfiltustore, k)
+            if k > 1
+                ustorelayerdepth = setindex(
+                    ustorelayerdepth,
+                    ustorelayerdepth[k - 1] + exfiltustore,
+                    k - 1,
+                )
+            end
+        end
+
+        ustoredepth = sum(@view ustorelayerdepth[1:n_unsatlayers])
+        sbm_runoff =
+            exfiltustore +
+            exfiltsatwater[i] +
+            v.excesswater[i] +
+            runoff_land[i] +
+            v.infiltexcess[i]
+
+        # volumetric water content per soil layer and root zone
+        vwc = v.vwc[i]
+        vwc_perc = v.vwc_perc[i]
+        for k in 1:p.nlayers[i]
+            if k <= n_unsatlayers
+                vwc = setindex(
+                    vwc,
+                    (
+                        ustorelayerdepth[k] +
+                        (p.act_thickl[i][k] - ustorelayerthickness[k]) *
+                        (p.theta_s[i] - p.theta_r[i])
+                    ) / p.act_thickl[i][k] + p.theta_r[i],
+                    k,
+                )
+            else
+                vwc = setindex(vwc, p.theta_s[i], k)
+            end
+            vwc_perc = setindex(vwc_perc, (vwc[k] / p.theta_s[i]) * 100.0, k)
+        end
+
+        rootstore_unsat = 0
+        for k in 1:n_unsatlayers
+            rootstore_unsat =
+                rootstore_unsat +
+                min(
+                    1.0,
+                    (
+                        max(0.0, rootingdepth[i] - p.sumlayers[i][k]) /
+                        ustorelayerthickness[k]
+                    ),
+                ) * ustorelayerdepth[k]
+        end
+
+        rootstore_sat = max(0.0, rootingdepth[i] - zi[i]) * (p.theta_s[i] - p.theta_r[i])
+        rootstore = rootstore_sat + rootstore_unsat
+        vwc_root = rootstore / rootingdepth[i] + p.theta_r[i]
+        vwc_percroot = (vwc_root / p.theta_s[i]) * 100.0
+
+        satwaterdepth = (p.soilthickness[i] - zi[i]) * (p.theta_s[i] - p.theta_r[i])
+
+        # update the outputs and states
+        v.n_unsatlayers[i] = n_unsatlayers
+        v.ustorelayerdepth[i] = ustorelayerdepth
+        v.ustoredepth[i] = ustoredepth
+        v.satwaterdepth[i] = satwaterdepth
+        v.exfiltsatwater[i] = exfiltsatwater[i]
+        v.exfiltustore[i] = exfiltustore
+        v.runoff[i] = sbm_runoff
+        v.vwc[i] = vwc
+        v.vwc_perc[i] = vwc_perc
+        v.rootstore[i] = rootstore
+        v.vwc_root[i] = vwc_root
+        v.vwc_percroot[i] = vwc_percroot
+        v.zi[i] = zi[i]
+    end
+    # update runoff and net_runoff (the runoff rate depends on the presence of paddy fields
+    # and the h_max parameter of a paddy field)
+    update_runoff!(demand.paddy, v.runoff)
+    @. v.net_runoff = v.runoff - ae_openw_l
+    return nothing
+end
+
+# wrapper method
+get_rootingdepth(model::SbmSoilModel) =
+    model.parameters.vegetation_parameter_set.rootingdepth
\ No newline at end of file
diff --git a/src/soil/soil_process.jl b/src/soil/soil_process.jl
new file mode 100644
index 000000000..845e235c4
--- /dev/null
+++ b/src/soil/soil_process.jl
@@ -0,0 +1,294 @@
+"""
+    infiltration(
+        potential_infiltration,
+        pathfrac,
+        infiltcapsoil,
+        infiltcappath,
+        ustorecapacity,
+        f_infiltration_reduction,
+    )
+
+Soil infiltration based on infiltration capacity soil `infiltcapsoil`, infiltration capacity
+paved area `infiltcappath` and unsaturated store capacity `ustorecapacity`. The infiltration
+capacity of the soil and paved area can be reduced with the infiltration reduction factor
+`f_infiltration_reduction`.
+"""
+function infiltration(
+    potential_infiltration,
+    pathfrac,
+    infiltcapsoil,
+    infiltcappath,
+    ustorecapacity,
+    f_infiltration_reduction,
+)
+    # First determine if the soil infiltration capacity can deal with the amount of water
+    # split between infiltration in undisturbed soil and paved areas (path).
+    soilinf = potential_infiltration * (1.0 - pathfrac)
+    pathinf = potential_infiltration * pathfrac
+
+    max_infiltsoil = min(infiltcapsoil * f_infiltration_reduction, soilinf)
+    max_infiltpath = min(infiltcappath * f_infiltration_reduction, pathinf)
+    infiltsoilpath = min(max_infiltpath + max_infiltsoil, max(0.0, ustorecapacity))
+
+    infiltexcess = (soilinf - max_infiltsoil) + (pathinf - max_infiltpath)
+
+    return infiltsoilpath, infiltexcess
+end
+
+"""
+    unsatzone_flow_layer(usd, kv_z, l_sat, c)
+
+Assuming a unit head gradient, the transfer of water from an unsaturated store layer `usd`
+is controlled by the vertical saturated hydraulic conductivity `kv_z` (bottom layer or water
+table), the effective saturation degree of the layer (ratio `usd` and `l_sat`), and a
+Brooks-Corey power coefficient `c`.
+"""
+function unsatzone_flow_layer(usd, kv_z, l_sat, c)
+    if usd <= 0.0
+        return 0.0, 0.0
+    end
+    sum_ast = 0.0
+    # first transfer soil water > maximum soil water capacity layer (iteration is not
+    # required because of steady theta (usd))
+    st = kv_z * min(pow(usd / l_sat, c), 1.0)
+    st_sat = max(0.0, usd - l_sat)
+    usd -= min(st, st_sat)
+    sum_ast = sum_ast + min(st, st_sat)
+    ast = max(min(st - min(st, st_sat), usd), 0.0)
+    # number of iterations (to reduce "overshooting") based on fixed maximum change in soil
+    # water per iteration step (0.2 mm / model timestep)
+    its = Int(cld(ast, 0.2))
+    for _ in 1:its
+        st = (kv_z / its) * min(pow(usd / l_sat, c), 1.0)
+        ast = min(st, usd)
+        usd -= ast
+        sum_ast += ast
+    end
+
+    return usd, sum_ast
+end
+
+"""
+    unsatzone_flow_sbm(
+        ustorelayerdepth,
+        soilwatercapacity,
+        satwaterdepth,
+        kv_z,
+        usl,
+        theta_s,
+        theta_r,
+    )
+
+The transfer of water from the unsaturated store `ustorelayerdepth` to the saturated store
+`satwaterdepth` is controlled by the vertical saturated hydraulic conductivity `kv_z` at the
+water table and the ratio between `ustorelayerdepth` and the saturation deficit
+(`soilwatercapacity` minus `satwaterdepth`). This is the original Topog_SBM vertical
+transfer formulation.
+
+"""
+function unsatzone_flow_sbm(
+    ustorelayerdepth,
+    soilwatercapacity,
+    satwaterdepth,
+    kv_z,
+    usl,
+    theta_s,
+    theta_r,
+)
+    sd = soilwatercapacity - satwaterdepth
+    if sd <= 0.00001
+        ast = 0.0
+    else
+        st = kv_z * min(ustorelayerdepth, usl * (theta_s - theta_r)) / sd
+        ast = min(st, ustorelayerdepth)
+        ustorelayerdepth = ustorelayerdepth - ast
+    end
+
+    return ustorelayerdepth, ast
+end
+
+"""
+    vwc_brooks_corey(h, hb, theta_s, theta_r, c)
+
+Return volumetric water content based on the Brooks-Corey soil hydraulic model.
+"""
+function vwc_brooks_corey(h, hb, theta_s, theta_r, c)
+    if h < hb
+        par_lambda = 2.0 / (c - 3.0)
+        vwc = (theta_s - theta_r) * pow(hb / h, par_lambda) + theta_r
+    else
+        vwc = theta_s
+    end
+    return vwc
+end
+
+"""
+    head_brooks_corey(vwc, theta_s, theta_r, c, hb)
+
+Return soil water pressure head based on the Brooks-Corey soil hydraulic model.
+"""
+function head_brooks_corey(vwc, theta_s, theta_r, c, hb)
+    par_lambda = 2.0 / (c - 3.0)
+    # Note that in the original formula, theta_r is extracted from vwc, but theta_r is not
+    # part of the numerical vwc calculation
+    h = hb / (pow(((vwc) / (theta_s - theta_r)), (1.0 / par_lambda)))
+    h = min(h, hb)
+    return h
+end
+
+"""
+    feddes_h3(h3_high, h3_low, tpot, Δt)
+
+Return soil water pressure head `h3` of Feddes root water uptake reduction function.
+"""
+function feddes_h3(h3_high, h3_low, tpot, Δt)
+    # value of h3 is a function of potential transpiration [mm/d]
+    tpot_daily = tpot * (tosecond(basetimestep) / Δt)
+    if (tpot_daily >= 0.0) && (tpot_daily <= 1.0)
+        h3 = h3_low
+    elseif (tpot_daily > 1.0) && (tpot_daily < 5.0)
+        h3 = h3_high + ((h3_low - h3_high) * (5.0 - tpot_daily)) / (5.0 - 1.0)
+    else
+        h3 = h3_high
+    end
+    return h3
+end
+
+"""
+    rwu_reduction_feddes(h, h1, h2, h3, h4, alpha_h1)
+
+Root water uptake reduction factor based on Feddes.
+"""
+function rwu_reduction_feddes(h, h1, h2, h3, h4, alpha_h1)
+    # root water uptake reduction coefficient alpha (see also Feddes et al., 1978)
+    if alpha_h1 == 0.0
+        if (h <= h4) || (h > h1)
+            alpha = 0.0
+        elseif (h > h2) && (h <= h1)
+            alpha = (h - h1) / (h2 - h1)
+        elseif (h >= h3) && (h <= h2)
+            alpha = 1.0
+        elseif (h >= h4) && (h < h3)
+            alpha = (h - h4) / (h3 - h4)
+        end
+    else
+        if h <= h4
+            alpha = 0.0
+        elseif h >= h3
+            alpha = 1.0
+        elseif (h >= h4) && (h < h3)
+            alpha = (h - h4) / (h3 - h4)
+        end
+    end
+    return alpha
+end
+
+"""
+    soil_temperature(tsoil, w_soil, temperature))
+
+Return the near surface soil temperature `tsoil` based on the near surface soil temperature
+`tsoil_prev` at the previous timestep, and the difference between air `temperature` and near
+surface soil temperature `tsoil_prev` at the previous timestep, weighted with the weighting
+coefficient `w_soil` (Wigmosta et al., 2009).
+"""
+function soil_temperature(tsoil_prev, w_soil, temperature)
+    tsoil = tsoil_prev + w_soil * (temperature - tsoil_prev)
+    return tsoil
+end
+
+"""
+    infiltration_reduction_factor(
+        tsoil,
+        cf_soil;
+        modelsnow = false,
+        soilinfreduction = false,
+    )
+
+When both `modelsnow` and `soilinfreduction` are `true` an infiltration reduction factor
+`f_infiltration_reduction` is computed. The infiltration reduction factor is based on the
+near surface soil temperature `tsoil`, parameter `cf_soil` and a s-curve to make a smooth
+transition of `f_infiltration_reduction` as a function of `tsoil` and `cf_soil`. Otherwise,
+`f_infiltration_reduction` is set to 1.0.
+"""
+function infiltration_reduction_factor(
+    tsoil,
+    cf_soil;
+    modelsnow = false,
+    soilinfreduction = false,
+)
+    if modelsnow && soilinfreduction
+        bb = 1.0 / (1.0 - cf_soil)
+        f_infiltration_reduction = scurve(tsoil, Float(0.0), bb, Float(8.0)) + cf_soil
+    else
+        f_infiltration_reduction = 1.0
+    end
+    return f_infiltration_reduction
+end
+
+"Return soil evaporation from the unsaturated store"
+function soil_evaporation_unsatured_store(
+    potential_soilevaporation,
+    ustorelayerdepth,
+    ustorelayerthickness,
+    n_unsatlayers,
+    zi,
+    theta_effective,
+)
+    if n_unsatlayers == 0
+        soilevapunsat = 0.0
+    elseif n_unsatlayers == 1
+        # Check if groundwater level lies below the surface
+        soilevapunsat =
+            potential_soilevaporation * min(1.0, ustorelayerdepth / (zi * theta_effective))
+    else
+        # In case first layer contains no saturated storage
+        soilevapunsat =
+            potential_soilevaporation *
+            min(1.0, ustorelayerdepth / (ustorelayerthickness * (theta_effective)))
+    end
+    return soilevapunsat
+end
+
+"Return soil evaporation from the saturated store"
+function soil_evaporation_satured_store(
+    potential_soilevaporation,
+    n_unsatlayers,
+    layerthickness,
+    zi,
+    theta_effective,
+)
+    if n_unsatlayers == 0 || n_unsatlayers == 1
+        soilevapsat =
+            potential_soilevaporation * min(1.0, (layerthickness - zi) / layerthickness)
+        soilevapsat = min(soilevapsat, (layerthickness - zi) * theta_effective)
+    else
+        soilevapsat = 0.0
+    end
+    return soilevapsat
+end
+
+"Return actual infiltration rate for soil `actinfiltsoil` and paved area `actinfiltpath`"
+function actual_infiltration_soil_path(
+    potential_infiltration,
+    actinfilt,
+    pathfrac,
+    infiltcapsoil,
+    infiltcappath,
+    f_infiltration_reduction,
+)
+    soilinf = potential_infiltration * (1.0 - pathfrac)
+    pathinf = potential_infiltration * pathfrac
+    if actinfilt > 0.0
+        max_infiltsoil = min(infiltcapsoil * f_infiltration_reduction, soilinf)
+        max_infiltpath = min(infiltcappath * f_infiltration_reduction, pathinf)
+
+        actinfiltsoil = actinfilt * max_infiltsoil / (max_infiltpath + max_infiltsoil)
+        actinfiltpath = actinfilt * max_infiltpath / (max_infiltpath + max_infiltsoil)
+
+    else
+        actinfiltsoil = 0.0
+        actinfiltpath = 0.0
+    end
+
+    return actinfiltsoil, actinfiltpath
+end
\ No newline at end of file
diff --git a/src/states.jl b/src/states.jl
index d33427886..4cd991654 100644
--- a/src/states.jl
+++ b/src/states.jl
@@ -1,60 +1,73 @@
 """
-    get_vertical_states(model_type; snow = false, glacier = false)
+    get_snow_states(model_type::AbstractString)
 
-Function to extract all required vertical states, given a certain model type. Passes the snow
-and glacier options only for the `sbm` model_type. Returns a tuple with the required states
-(internal names as symbols)
+Extract required snow model states, given a certain `model_type`. Returns a tuple with the
+required states (internal names as symbols).
 """
-function get_vertical_states(model_type::AbstractString; snow = false, glacier = false)
+function get_snow_states(model_type::AbstractString)
     if model_type == "sbm" || model_type == "sbm_gwf"
-        if snow && glacier
-            vertical_states = (
-                :satwaterdepth,
-                :snow,
-                :tsoil,
-                :ustorelayerdepth,
-                :snowwater,
-                :canopystorage,
-                :glacierstore,
-            )
-        elseif snow
-            vertical_states = (
-                :satwaterdepth,
-                :snow,
-                :tsoil,
-                :ustorelayerdepth,
-                :snowwater,
-                :canopystorage,
-            )
+        states = (:snow_storage, :snow_water)
+    elseif model_type == "sediment"
+        states = ()
+    else
+        throw(ArgumentError("Unknown model_type provided (`$model_type`)"))
+    end
+    return states
+end
+
+"""
+    get_glacier_states(model_type::AbstractString)
+
+Extract required glacier model states, given a certain `model_type`. Returns a tuple with
+the required states (internal names as symbols).
+"""
+function get_glacier_states(model_type::AbstractString)
+    if model_type == "sbm" || model_type == "sbm_gwf"
+        states = (:glacier_store,)
+    elseif model_type == "sediment"
+        states = ()
+    else
+        throw(ArgumentError("Unknown model_type provided (`$model_type`)"))
+    end
+    return states
+end
+
+"""
+    get_interception_states(model_type::AbstractString)
+
+Extract required interception model states, given a certain `model_type`. Returns a tuple
+with the required states (internal names as symbols).
+"""
+function get_interception_states(model_type::AbstractString)
+    if model_type == "sbm" || model_type == "sbm_gwf"
+        states = (:canopy_storage,)
+    elseif model_type == "sediment"
+        states = ()
+    else
+        throw(ArgumentError("Unknown model_type provided (`$model_type`)"))
+    end
+    return states
+end
+
+"""
+    get_soil_states(model_type::AbstractString; snow = false)
+
+Extract required soil model states, given a certain `model_type` and whether `snow` is
+modelled. Returns a tuple with the required states (internal names as symbols).
+"""
+function get_soil_states(model_type::AbstractString; snow = false)
+    if model_type == "sbm" || model_type == "sbm_gwf"
+        if snow
+            states = (:satwaterdepth, :tsoil, :ustorelayerdepth)
         else
-            vertical_states = (:satwaterdepth, :ustorelayerdepth, :canopystorage)
+            states = (:satwaterdepth, :ustorelayerdepth)
         end
-    elseif model_type == "hbv"
-        vertical_states = (
-            :soilmoisture,
-            :snow,
-            :snowwater,
-            :upperzonestorage,
-            :lowerzonestorage,
-            :interceptionstorage,
-        )
-    elseif model_type == "flextopo"
-        vertical_states = (
-            :snow,
-            :snowwater,
-            :interceptionstorage,
-            :hortonpondingstorage,
-            :hortonrunoffstorage,
-            :rootzonestorage,
-            :faststorage,
-            :slowstorage,
-        )
     elseif model_type == "sediment"
-        vertical_states = ()
+        states = ()
     else
         throw(ArgumentError("Unknown model_type provided (`$model_type`)"))
     end
-    return vertical_states
+    return states
 end
 
 """
@@ -76,8 +89,9 @@ function add_to_required_states(required_states::Tuple, key_entry::Tuple, states
     return required_states
 end
 
-add_to_required_states(required_states::Tuple, key_entry::Tuple, states::Nothing) =
-    required_states
+function add_to_required_states(required_states::Tuple, key_entry::Tuple, states::Nothing)
+    return required_states
+end
 
 """
     extract_required_states(config::Config)
@@ -100,10 +114,21 @@ function extract_required_states(config::Config)
     do_paddy = false
     if haskey(config.model, "water_demand")
         do_paddy = get(config.model.water_demand, "paddy", false)::Bool
-    end 
+    end
 
     # Extract required stated based on model configuration file
-    vertical_states = get_vertical_states(model_type; snow = do_snow, glacier = do_glaciers)
+    if do_snow
+        snow_states = get_snow_states(model_type)
+    else
+        snow_states = ()
+    end
+    if do_snow && do_glaciers
+        glacier_states = get_glacier_states(model_type)
+    else
+        glacier_states = ()
+    end
+    interception_states = get_interception_states(model_type)
+    soil_states = get_soil_states(model_type; snow = do_snow)
 
     # Subsurface states
     if model_type == "sbm_gwf"
@@ -175,8 +200,21 @@ function extract_required_states(config::Config)
     # Build required states in a tuple, similar to the keys in the output of
     # `ncnames(config.state)`
     required_states = ()
-    # Add vertical states to dict
-    required_states = add_to_required_states(required_states, (:vertical,), vertical_states)
+    # Add snow, glacier, interception and sbm soil states to dict
+    required_states =
+        add_to_required_states(required_states, (:vertical, :snow, :variables), snow_states)
+    required_states = add_to_required_states(
+        required_states,
+        (:vertical, :glacier, :variables),
+        glacier_states,
+    )
+    required_states = add_to_required_states(
+        required_states,
+        (:vertical, :interception, :variables),
+        interception_states,
+    )
+    required_states =
+        add_to_required_states(required_states, (:vertical, :soil, :variables), soil_states)
     # Add subsurface states to dict
     if model_type == "sbm_gwf"
         key_entry = (:lateral, :subsurface, :flow, :aquifer)
@@ -208,7 +246,7 @@ function extract_required_states(config::Config)
     # Add paddy states to dict
     required_states = add_to_required_states(
         required_states,
-        (:vertical, :paddy),
+        (:vertical, :demand, :paddy, :variables),
         paddy_states,
     )
     return required_states
diff --git a/src/subdomains.jl b/src/subdomains.jl
index 7428a1856..8317c9616 100644
--- a/src/subdomains.jl
+++ b/src/subdomains.jl
@@ -82,7 +82,7 @@ subbasins without upstream neighbor and distance < `max_dist`) to distance 0 (`o
 function subbasins_order(g, outlet, max_dist)
     order = Vector{Vector{Int}}(undef, max_dist + 1)
     order[1] = [outlet]
-    for i = 1:max_dist
+    for i in 1:max_dist
         v = Vector{Int}()
         for n in order[i]
             ups_nodes = inneighbors(g, n)
@@ -90,14 +90,14 @@ function subbasins_order(g, outlet, max_dist)
                 append!(v, ups_nodes)
             end
         end
-        order[i+1] = v
+        order[i + 1] = v
     end
 
     # move subbasins without upstream neighbor (headwater) to index [max_dist+1]
-    for i = 1:max_dist
+    for i in 1:max_dist
         for s in order[i]
             if isempty(inneighbors(g, s))
-                append!(order[max_dist+1], s)
+                append!(order[max_dist + 1], s)
                 filter!(e -> e ≠ s, order[i])
             end
         end
@@ -116,7 +116,7 @@ flow network for each subbasin cell.
 function graph_from_nodes(graph, subbas, subbas_fill)
     n = maximum(subbas)
     g = DiGraph(n)
-    for i = 1:n
+    for i in 1:n
         idx = findall(x -> x == i, subbas)
         ds_idx = outneighbors(graph, only(idx))
         to_node = subbas_fill[ds_idx]
@@ -149,7 +149,6 @@ streamorder, toposort, min_sto)`). Subbasins are extracted for each basin outlet
 - `topo_subbas` topological order per subbasin id stored as `Vector{Vector{Int}}`
 """
 function kinwave_set_subdomains(graph, toposort, index_pit, streamorder, min_sto)
-
     if nthreads() > 1
         # extract basins (per outlet/pit), assign unique basin id
         n_pits = length(index_pit)
@@ -169,7 +168,7 @@ function kinwave_set_subdomains(graph, toposort, index_pit, streamorder, min_sto
         topo_subbas = Vector{Vector{Int}}()
         index = Vector{Int}()
         total_subbas = 0
-        for i = 1:n_pits
+        for i in 1:n_pits
             # extract subbasins per basin, make a graph at the subbasin level, calculate the
             # maximum distance of this graph, and group and order the subbasin ids from
             # upstream to downstream
@@ -179,7 +178,7 @@ function kinwave_set_subdomains(graph, toposort, index_pit, streamorder, min_sto
             streamorder_subbas = streamorder[vmap]
             subbas = subbasins(g, streamorder_subbas, toposort_b, min_sto)
             subbas_fill = fillnodata_upstream(g, toposort_b, subbas, 0)
-            n_subbas = max(length(subbas[subbas.>0]), 1)
+            n_subbas = max(length(subbas[subbas .> 0]), 1)
             if n_subbas > 1
                 graph_subbas = graph_from_nodes(g, subbas, subbas_fill)
                 toposort_subbas = topological_sort_by_dfs(graph_subbas)
@@ -194,7 +193,7 @@ function kinwave_set_subdomains(graph, toposort, index_pit, streamorder, min_sto
             end
             # subbasins need a unique id (in case of multiple basins/outlets in the
             # kinematic wave domain)
-            for n = 1:length(v_subbas)
+            for n in 1:length(v_subbas)
                 v_subbas[n] .= v_subbas[n] .+ total_subbas
             end
             total_subbas += n_subbas
@@ -204,7 +203,7 @@ function kinwave_set_subdomains(graph, toposort, index_pit, streamorder, min_sto
             # (subgraph of the corresponding basin graph g), and the indices that match the
             # subbasin topological order
             if n_subbas > 1
-                for s = 1:n_subbas
+                for s in 1:n_subbas
                     subbas_s = findall(x -> x == s, subbas_fill)
                     sg, _ = induced_subgraph(g, subbas_s)
                     toposort_sg = topological_sort_by_dfs(sg)
@@ -219,8 +218,8 @@ function kinwave_set_subdomains(graph, toposort, index_pit, streamorder, min_sto
         # reduce the order of subbasin ids by merging groups of subbasins that have the same
         # index (multiple basins/outlets in the kinematic wave domain)
         subbas_order = Vector{Vector{Int}}(undef, maximum(index))
-        for m = 1:maximum(index)
-            subbas_order[m] = reduce(vcat, order_subbas[index.==m])
+        for m in 1:maximum(index)
+            subbas_order[m] = reduce(vcat, order_subbas[index .== m])
         end
     else
         subbas_order = [[1]]
diff --git a/src/surfacewater/runoff.jl b/src/surfacewater/runoff.jl
new file mode 100644
index 000000000..6203b4020
--- /dev/null
+++ b/src/surfacewater/runoff.jl
@@ -0,0 +1,145 @@
+abstract type AbstractRunoffModel{T} end
+
+"Struct for storing open water runoff variables"
+@get_units @grid_loc @with_kw struct OpenWaterRunoffVariables{T}
+    # Runoff from river based on riverfrac [mm Δt⁻¹]
+    runoff_river::Vector{T}
+    # Net runoff from river [mm Δt⁻¹]
+    net_runoff_river::Vector{T}
+    # Runoff from land based on waterfrac [mm Δt⁻¹]
+    runoff_land::Vector{T}
+    # Actual evaporation from open water (land) [mm Δt⁻¹]
+    ae_openw_l::Vector{T}
+    # Actual evaporation from river [mm Δt⁻¹]
+    ae_openw_r::Vector{T}
+end
+
+"Initialize open water runoff model variables"
+function OpenWaterRunoffVariables(T::Type{<:AbstractFloat}, n::Int)
+    return OpenWaterRunoffVariables{T}(;
+        runoff_river = fill(mv, n),
+        runoff_land = fill(mv, n),
+        ae_openw_l = fill(mv, n),
+        ae_openw_r = fill(mv, n),
+        net_runoff_river = fill(mv, n),
+    )
+end
+
+"Struct for storing open water runoff parameters"
+@get_units @grid_loc @with_kw struct OpenWaterRunoffParameters{T}
+    # Fraction of river [-]
+    riverfrac::Vector{T} | "-"
+    # Fraction of open water (excluding rivers) [-]
+    waterfrac::Vector{T} | "-"
+end
+
+"Initialize open water runoff parameters"
+function OpenWaterRunoffParameters(nc, config, inds, riverfrac)
+    # fraction open water
+    waterfrac = ncread(
+        nc,
+        config,
+        "vertical.runoff.parameters.waterfrac";
+        sel = inds,
+        defaults = 0.0,
+        type = Float,
+    )
+    waterfrac = max.(waterfrac .- riverfrac, Float(0.0))
+    params = OpenWaterRunoffParameters(; waterfrac = waterfrac, riverfrac = riverfrac)
+    return params
+end
+
+"Struct for storing open water runoff boundary conditions"
+@get_units @grid_loc @with_kw struct OpenWaterRunoffBC{T}
+    water_flux_surface::Vector{T}
+    waterlevel_land::Vector{T} | "mm"
+    waterlevel_river::Vector{T} | "mm"
+end
+
+"Initialize open water runoff boundary conditions"
+function OpenWaterRunoffBC(T::Type{<:AbstractFloat}, n::Int)
+    return OpenWaterRunoffBC{T}(;
+        water_flux_surface = fill(mv, n),
+        waterlevel_land = fill(mv, n),
+        waterlevel_river = zeros(T, n),
+    )
+end
+
+"Open water runoff model"
+@with_kw struct OpenWaterRunoff{T} <: AbstractRunoffModel{T}
+    boundary_conditions::OpenWaterRunoffBC{T}
+    parameters::OpenWaterRunoffParameters{T}
+    variables::OpenWaterRunoffVariables{T}
+end
+
+"Initialize open water runoff model"
+function OpenWaterRunoff(nc, config, inds, riverfrac)
+    n = length(riverfrac)
+    vars = OpenWaterRunoffVariables(Float, n)
+    bc = OpenWaterRunoffBC(Float, n)
+    params = OpenWaterRunoffParameters(nc, config, inds, riverfrac)
+    model =
+        OpenWaterRunoff(; boundary_conditions = bc, parameters = params, variables = vars)
+    return model
+end
+
+"Return the water flux at the surface (boundary condition) when snow is not modelled"
+function get_water_flux_surface!(
+    water_flux_surface,
+    snow::NoSnowModel,
+    glacier,
+    interception,
+)
+    (; throughfall, stemflow) = interception.variables
+    @. water_flux_surface = throughfall + stemflow
+    return nothing
+end
+
+"Return the water flux at the surface (boundary condition) when snow is modelled"
+function get_water_flux_surface!(
+    water_flux_surface,
+    snow::AbstractSnowModel,
+    glacier,
+    interception,
+)
+    water_flux_surface .=
+        get_runoff(snow) .+ get_glacier_melt(glacier) .* get_glacier_fraction(glacier)
+    return nothing
+end
+
+"Update boundary conditions of the open water runoff model for a single timestep"
+function update_boundary_conditions!(
+    model::OpenWaterRunoff,
+    external_models::NamedTuple,
+    lateral,
+    network,
+)
+    (; water_flux_surface, waterlevel_river, waterlevel_land) = model.boundary_conditions
+    inds_riv = network.index_river
+    (; snow, glacier, interception) = external_models
+
+    get_water_flux_surface!(water_flux_surface, snow, glacier, interception)
+
+    # extract water levels h_av [m] from the land and river domains this is used to limit
+    # open water evaporation
+    waterlevel_land .= lateral.land.h_av .* 1000.0
+    waterlevel_river[inds_riv] .= lateral.river.h_av .* 1000.0
+    return nothing
+end
+
+"Update the open water runoff model for a single timestep"
+function update!(model::OpenWaterRunoff, atmospheric_forcing::AtmosphericForcing)
+    (; potential_evaporation) = atmospheric_forcing
+    (; runoff_river, net_runoff_river, runoff_land, ae_openw_r, ae_openw_l) =
+        model.variables
+    (; riverfrac, waterfrac) = model.parameters
+    (; water_flux_surface, waterlevel_river, waterlevel_land) = model.boundary_conditions
+
+    @. runoff_river = min(1.0, riverfrac) * water_flux_surface
+    @. runoff_land = min(1.0, waterfrac) * water_flux_surface
+    @. ae_openw_r = min(waterlevel_river * riverfrac, riverfrac * potential_evaporation)
+    @. ae_openw_l = min(waterlevel_land * waterfrac, waterfrac * potential_evaporation)
+    @. net_runoff_river = runoff_river - ae_openw_r
+
+    return nothing
+end
\ No newline at end of file
diff --git a/src/utils.jl b/src/utils.jl
index b81248c9e..40c514ec1 100644
--- a/src/utils.jl
+++ b/src/utils.jl
@@ -17,6 +17,23 @@ const mv = Float(NaN)
 # timestep that the parameter units are defined in
 const basetimestep = Second(Day(1))
 
+"""
+    scurve(x, a, b, c)
+
+Sigmoid "S"-shaped curve.
+
+# Arguments
+- `x::Real`: input
+- `a::Real`: determines the centre level
+- `b::Real`: determines the amplitude of the curve
+- `c::Real`: determines the steepness or "stepwiseness" of the curve.
+             The higher c the sharper the function. A negative c reverses the function.
+"""
+function scurve(x, a, b, c)
+    s = one(x) / (b + exp(-c * (x - a)))
+    return s
+end
+
 "Set at indices pit values (default = 5) in a gridded local drainage direction vector"
 function set_pit_ldd(pits_2d, ldd, indices; pit = 5)
     pits = pits_2d[indices]
@@ -98,7 +115,7 @@ function cell_lengths(y::AbstractVector, cellength::Real, sizeinmetres::Bool)
         xl .= cellength
         yl .= cellength
     else
-        for i = 1:n
+        for i in 1:n
             longlen, latlen = lattometres(y[i])
             xl[i] = longlen * cellength
             yl[i] = latlen * cellength
@@ -116,7 +133,7 @@ function river_fraction(
 )
     n = length(river)
     riverfrac = fill(mv, n)
-    for i = 1:n
+    for i in 1:n
         riverfrac[i] = if river[i]
             min((riverlength[i] * riverwidth[i]) / (xl[i] * yl[i]), 1.0)
         else
@@ -127,7 +144,7 @@ function river_fraction(
 end
 
 """
-    set_states(instate_path, model, state_ncnames; <keyword arguments>)
+    set_states!(instate_path, model, state_ncnames; <keyword arguments>)
 
 Read states contained in `Dict` `state_ncnames` from netCDF file located in `instate_path`,
 and set states in `model` object. Active cells are selected with the corresponding network's
@@ -136,7 +153,7 @@ and set states in `model` object. Active cells are selected with the correspondi
 # Arguments
 - `type = nothing`: type to convert data to after reading. By default no conversion is done.
 """
-function set_states(instate_path, model; type = nothing, dimname = nothing)
+function set_states!(instate_path, model; type = nothing, dimname = nothing)
     (; network, config) = model
 
     # Check if required states are covered
@@ -154,8 +171,6 @@ function set_states(instate_path, model; type = nothing, dimname = nothing)
             if dims == 4
                 if dimname == :layer
                     dimensions = (x = :, y = :, layer = :, time = 1)
-                elseif dimname == :classes
-                    dimensions = (x = :, y = :, classes = :, time = 1)
                 else
                     error("Unrecognized dimension name $dimname")
                 end
@@ -195,6 +210,7 @@ function set_states(instate_path, model; type = nothing, dimname = nothing)
             end
         end
     end
+    return nothing
 end
 
 """
@@ -249,8 +265,6 @@ function ncread(
     # first timestep), that is later updated with the `update_cyclic!` function.
     if isnothing(dimname)
         dim_sel = (x = :, y = :, time = 1)
-    elseif dimname == :classes
-        dim_sel = (x = :, y = :, classes = :, time = 1)
     elseif dimname == :layer
         dim_sel = (x = :, y = :, layer = :, time = 1)
     elseif dimname == :flood_depth
@@ -297,7 +311,7 @@ function ncread(
             # provided through the TOML file.
             if length(mod.index) > 1
                 # if index, scale and offset is provided in the TOML as a list.
-                for i = 1:length(mod.index)
+                for i in 1:length(mod.index)
                     A[:, :, mod.index[i]] =
                         A[:, :, mod.index[i]] .* mod.scale[i] .+ mod.offset[i]
                 end
@@ -344,25 +358,27 @@ function ncread(
 end
 
 """
-    set_layerthickness(d::Real, sl::SVector)
+    set_layerthickness(reference_depth::Real, cum_depth::SVector, thickness::SVector)
 
-Calculate actual soil thickness of layers based on a reference depth (e.g. soil depth or water table depth) `d`,
-a SVector `sl` with cumulative soil depth starting at soil surface (0), and a SVector `tl` with actual thickness
-per soil layer.
+Calculate actual soil thickness of layers based on a reference depth (e.g. soil depth or
+water table depth) `reference_depth`, a SVector `cum_depth` with cumulative soil depth starting
+at soil surface (0), and a SVector `thickness` with thickness per soil layer.
 """
-function set_layerthickness(d::Real, sl::SVector, tl::SVector)
-
-    act_d = tl .* mv
-    for i = 1:length(act_d)
-        if d > sl[i+1]
-            act_d = setindex(act_d, tl[i], i)
-        elseif d - sl[i] > 0.0
-            act_d = setindex(act_d, d - sl[i], i)
+function set_layerthickness(reference_depth::Real, cum_depth::SVector, thickness::SVector)
+    thicknesslayers = thickness .* mv
+    for i in 1:length(thicknesslayers)
+        if reference_depth > cum_depth[i + 1]
+            thicknesslayers = setindex(thicknesslayers, thickness[i], i)
+        elseif reference_depth - cum_depth[i] > 0.0
+            thicknesslayers = setindex(thicknesslayers, reference_depth - cum_depth[i], i)
         end
     end
+    return thicknesslayers
+end
 
-    nlayers = length(act_d) - sum(isnan.(act_d))
-    return act_d, nlayers
+function number_of_active_layers(thickness::SVector)
+    nlayers = length(thickness) - sum(isnan.(thickness))
+    return nlayers
 end
 
 """
@@ -439,10 +455,10 @@ function sum_at(f::Function, inds, T)
 end
 
 # https://juliaarrays.github.io/StaticArrays.jl/latest/pages/api/#Arrays-of-static-arrays-1
-function svectorscopy(x::Matrix{T}, ::Val{N}) where {T,N}
+function svectorscopy(x::Matrix{T}, ::Val{N}) where {T, N}
     size(x, 1) == N || error("sizes mismatch")
     isbitstype(T) || error("use for bitstypes only")
-    copy(reinterpret(SVector{N,T}, vec(x)))
+    return copy(reinterpret(SVector{N, T}, vec(x)))
 end
 
 """
@@ -501,8 +517,8 @@ julia> tosecond(Day(1))
 """
 tosecond(x::Hour) = Float64(Dates.value(Second(x)))
 tosecond(x::Minute) = Float64(Dates.value(Second(x)))
-tosecond(x::T) where {T<:DatePeriod} = Float64(Dates.value(Second(x)))
-tosecond(x::T) where {T<:TimePeriod} = x / convert(T, Second(1))
+tosecond(x::T) where {T <: DatePeriod} = Float64(Dates.value(Second(x)))
+tosecond(x::T) where {T <: TimePeriod} = x / convert(T, Second(1))
 
 """
     adjacent_nodes_at_link(graph)
@@ -523,7 +539,7 @@ function adjacent_links_at_node(graph, nodes_at_link)
     nodes = vertices(graph)
     src_link = Vector{Int}[]
     dst_link = copy(src_link)
-    for i = 1:nv(graph)
+    for i in 1:nv(graph)
         push!(src_link, findall(isequal(nodes[i]), nodes_at_link.dst))
         push!(dst_link, findall(isequal(nodes[i]), nodes_at_link.src))
     end
@@ -537,6 +553,7 @@ function add_vertex_edge_graph!(graph, pits)
         add_vertex!(graph)
         add_edge!(graph, v, n + i)
     end
+    return nothing
 end
 
 """
@@ -560,7 +577,6 @@ function set_effective_flowwidth!(
     waterbody,
     inds_rev_riv,
 )
-
     toposort = topological_sort_by_dfs(graph_riv)
     n = length(we_x)
     for v in toposort
@@ -610,6 +626,7 @@ function set_effective_flowwidth!(
             we_x[idx] = waterbody[v] ? 0.0 : max(we_x[idx] - 0.5 * w, 0.0)
         end
     end
+    return nothing
 end
 
 "Return julian day of year (leap days are not counted)"
@@ -622,7 +639,7 @@ end
 "Partition indices with at least size `basesize`"
 function _partition(xs::Integer, basesize::Integer)
     n = Int(max(1, xs ÷ basesize))
-    return (Int(1 + ((i - 1) * xs) ÷ n):Int((i * xs) ÷ n) for i = 1:n)
+    return (Int(1 + ((i - 1) * xs) ÷ n):Int((i * xs) ÷ n) for i in 1:n)
 end
 
 """
@@ -658,174 +675,252 @@ function threaded_foreach(f, x::AbstractArray; basesize::Integer)
 end
 
 """
-    hydraulic_conductivity_at_depth(sbm::SBM, z, i, ksat_profile)
+    hydraulic_conductivity_at_depth(p::KvExponential, kvfrac, z, i, n)
+    hydraulic_conductivity_at_depth(p::KvExponentialConstant, kvfrac, z, i, n)
+    hydraulic_conductivity_at_depth(p::KvLayered, kvfrac, z, i, n)
+    hydraulic_conductivity_at_depth(p::KvLayeredExponential, kvfrac, z, i, n)
 
-Return vertical hydraulic conductivity `kv_z` for soil layer `n` at depth `z` for vertical
-concept `SBM` (at index `i`) based on hydraulic conductivity profile `ksat_profile`.
+Return vertical hydraulic conductivity `kv_z` at depth `z` for index `i` using muliplication
+factor `kv_frac` at soil layer `n` and vertical hydraulic conductivity profile `p`.
 """
-function hydraulic_conductivity_at_depth(sbm::SBM, z, i, n, ksat_profile)
-    if ksat_profile == "exponential"
-        kv_z = sbm.kvfrac[i][n] * sbm.kv_0[i] * exp(-sbm.f[i] * z)
-    elseif ksat_profile == "exponential_constant"
-        if z < sbm.z_exp[i]
-            kv_z = sbm.kvfrac[i][n] * sbm.kv_0[i] * exp(-sbm.f[i] * z)
-        else
-            kv_z = sbm.kvfrac[i][n] * sbm.kv_0[i] * exp(-sbm.f[i] * sbm.z_exp[i])
-        end
-    elseif ksat_profile == "layered"
-        kv_z = sbm.kvfrac[i][n] * sbm.kv[i][n]
-    elseif ksat_profile == "layered_exponential"
-        if z < sbm.z_layered[i]
-            kv_z = sbm.kvfrac[i][n] * sbm.kv[i][n]
-        else
-            n = sbm.nlayers_kv[i]
-            kv_z = sbm.kvfrac[i][n] * sbm.kv[i][n] * exp(-sbm.f[i] * (z - sbm.z_layered[i]))
-        end
+function hydraulic_conductivity_at_depth(p::KvExponential, kvfrac, z, i, n)
+    kv_z = kvfrac[i][n] * p.kv_0[i] * exp(-p.f[i] * z)
+    return kv_z
+end
+
+function hydraulic_conductivity_at_depth(p::KvExponentialConstant, kvfrac, z, i, n)
+    (; kv_0, f) = p.exponential
+    if z < p.z_exp[i]
+        kv_z = kvfrac[i][n] * kv_0[i] * exp(-f[i] * z)
     else
-        error("unknown ksat_profile")
+        kv_z = kvfrac[i][n] * kv_0[i] * exp(-f[i] * p.z_exp[i])
     end
     return kv_z
 end
 
-"""
-    kh_layered_profile(sbm::SBM, khfrac, z, i, ksat_profile)
+function hydraulic_conductivity_at_depth(p::KvLayered, kvfrac, z, i, n)
+    kv_z = kvfrac[i][n] * p.kv[i][n]
+    return kv_z
+end
+
+function hydraulic_conductivity_at_depth(p::KvLayeredExponential, kvfrac, z, i, n)
+    return if z < p.z_layered[i]
+        kvfrac[i][n] * p.kv[i][n]
+    else
+        n = p.nlayers_kv[i]
+        kvfrac[i][n] * p.kv[i][n] * exp(-p.f[i] * (z - p.z_layered[i]))
+    end
+end
 
-Return equivalent horizontal hydraulic conductivity `kh` [m d⁻¹] for a layered soil profile
-of vertical concept `SBM` (at index `i`) based on multiplication factor `khfrac` [-], water
-table depth `z` [mm] and hydraulic conductivity profile `ksat_profile`.
 """
-function kh_layered_profile(sbm::SBM, khfrac, i, ksat_profile)
+    kh_layered_profile!(soil::SbmSoilModel, subsurface::LateralSSF, kv_profile::KvLayered, dt)
+    kh_layered_profile!(soil::SbmSoilModel, subsurface::LateralSSF, kv_profile::KvLayeredExponential, dt)
 
-    m = sbm.nlayers[i]
-    t_factor = (tosecond(basetimestep) / sbm.dt)
-    if (sbm.soilthickness[i] - sbm.zi[i]) > 0.0
-        transmissivity = 0.0
-        sumlayers = @view sbm.sumlayers[i][2:end]
-        n = max(sbm.n_unsatlayers[i], 1)
+Compute equivalent horizontal hydraulic conductivity `kh` [m d⁻¹] using vertical hydraulic
+conductivity profile `kv_profile`.
+"""
+function kh_layered_profile!(
+    soil::SbmSoilModel,
+    subsurface::LateralSSF,
+    kv_profile::KvLayered,
+    dt,
+)
+    (; nlayers, sumlayers, act_thickl, soilthickness) = soil.parameters
+    (; n_unsatlayers, zi) = soil.variables
+    (; kh) = subsurface.kh_profile
+    (; khfrac) = subsurface
+
+    t_factor = (tosecond(basetimestep) / dt)
+    for i in eachindex(kh)
+        m = nlayers[i]
+
+        if soilthickness[i] > zi[i]
+            transmissivity = 0.0
+            _sumlayers = @view sumlayers[i][2:end]
+            n = max(n_unsatlayers[i], 1)
+            transmissivity += (_sumlayers[n] - zi[i]) * kv_profile.kv[i][n]
+            n += 1
+            while n <= m
+                transmissivity += act_thickl[i][n] * kv_profile.kv[i][n]
+                n += 1
+            end
+            # convert units for kh [m d⁻¹] computation (transmissivity [mm² Δt⁻¹], soilthickness
+            # [mm] and zi [mm])
+            kh[i] =
+                0.001 * (transmissivity / (soilthickness[i] - zi[i])) * t_factor * khfrac[i]
+        else
+            kh[i] = 0.001 * kv_profile.kv[i][m] * t_factor * khfrac[i]
+        end
+    end
+    return nothing
+end
 
-        if ksat_profile == "layered"
-            transmissivity += (sumlayers[n] - sbm.zi[i]) * sbm.kv[i][n]
-        elseif ksat_profile == "layered_exponential"
-            if sbm.zi[i] >= sbm.z_layered[i]
-                zt = sbm.soilthickness[i] - sbm.z_layered[i]
-                j = sbm.nlayers_kv[i]
+function kh_layered_profile!(
+    soil::SbmSoilModel,
+    subsurface::LateralSSF,
+    kv_profile::KvLayeredExponential,
+    dt,
+)
+    (; nlayers, sumlayers, act_thickl, soilthickness) = soil.parameters
+    (; nlayers_kv, z_layered, kv, f) = kv_profile
+    (; n_unsatlayers, zi) = soil.variables
+    (; kh) = subsurface.kh_profile
+    (; khfrac) = subsurface
+    t_factor = (tosecond(basetimestep) / dt)
+
+    for i in eachindex(kh)
+        m = nlayers[i]
+
+        if soilthickness[i] > zi[i]
+            transmissivity = 0.0
+            n = max(n_unsatlayers[i], 1)
+            if zi[i] >= z_layered[i]
+                zt = soilthickness[i] - z_layered[i]
+                j = nlayers_kv[i]
                 transmissivity +=
-                    sbm.kv[i][j] / sbm.f[i] *
-                    (exp(-sbm.f[i] * (sbm.zi[i] - sbm.z_layered[i])) - exp(-sbm.f[i] * zt))
+                    kv[i][j] / f[i] *
+                    (exp(-f[i] * (zi[i] - z_layered[i])) - exp(-f[i] * zt))
                 n = m
             else
-                transmissivity += (sumlayers[n] - sbm.zi[i]) * sbm.kv[i][n]
+                _sumlayers = @view sumlayers[i][2:end]
+                transmissivity += (_sumlayers[n] - zi[i]) * kv[i][n]
             end
-        else
-            error("unknown ksat_profile, `layered` or `layered_exponential` are allowed")
-        end
-        n += 1
-        while n <= m
-            if ksat_profile == "layered"
-                transmissivity += sbm.act_thickl[i][n] * sbm.kv[i][n]
-            elseif ksat_profile == "layered_exponential"
-                if n > sbm.nlayers_kv[i]
-                    zt = sbm.soilthickness[i] - sbm.z_layered[i]
-                    j = sbm.nlayers_kv[i]
-                    transmissivity += sbm.kv[i][j] / sbm.f[i] * (1.0 - exp(-sbm.f[i] * zt))
+            n += 1
+            while n <= m
+                if n > nlayers_kv[i]
+                    zt = soilthickness[i] - z_layered[i]
+                    j = nlayers_kv[i]
+                    transmissivity += kv[i][j] / f[i] * (1.0 - exp(-f[i] * zt))
                     n = m
                 else
-                    transmissivity += sbm.act_thickl[i][n] * sbm.kv[i][n]
+                    transmissivity += act_thickl[i][n] * kv[i][n]
                 end
+                n += 1
             end
-            n += 1
-        end
-        # convert units for kh [m d⁻¹] computation (transmissivity [mm² Δt⁻¹], soilthickness
-        # [mm] and zi [mm])
-        kh =
-            0.001 *
-            (transmissivity / (sbm.soilthickness[i] - sbm.zi[i])) *
-            t_factor *
-            khfrac
-    else
-        if ksat_profile == "layered"
-            kh = 0.001 * sbm.kv[i][m] * t_factor * khfrac
-        elseif ksat_profile == "layered_exponential"
-            if sbm.zi[i] >= sbm.z_layered[i]
-                j = sbm.nlayers_kv[i]
-                kh =
+            # convert units for kh [m d⁻¹] computation (transmissivity [mm² Δt⁻¹], soilthickness
+            # [mm] and zi [mm])
+            kh[i] =
+                0.001 * (transmissivity / (soilthickness[i] - zi[i])) * t_factor * khfrac[i]
+        else
+            if zi[i] >= z_layered[i]
+                j = nlayers_kv[i]
+                kh[i] =
                     0.001 *
-                    sbm.kv[i][j] *
-                    exp(-sbm.f[i] * (sbm.zi[i] - sbm.z_layered[i])) *
-                    khfrac *
+                    kv[i][j] *
+                    exp(-f[i] * (zi[i] - z_layered[i])) *
+                    khfrac[i] *
                     t_factor
             else
-                kh = 0.001 * sbm.kv[i][m] * t_factor * khfrac
+                kh[i] = 0.001 * kv[i][m] * t_factor * khfrac[i]
             end
-        else
-            error("unknown ksat_profile, `layered` or `layered_exponential` are allowed")
         end
     end
-    return kh
-end
-
-"Initialize lateral subsurface variables `ssf` and `ssfmax` with `ksat_profile` `exponential`"
-function initialize_lateralssf_exp!(ssf::LateralSSF)
-    for i in eachindex(ssf.ssf)
-        ssf.ssfmax[i] =
-            ((ssf.kh_0[i] * ssf.slope[i]) / ssf.f[i]) *
-            (1.0 - exp(-ssf.f[i] * ssf.soilthickness[i]))
-        ssf.ssf[i] =
-            ((ssf.kh_0[i] * ssf.slope[i]) / ssf.f[i]) *
-            (exp(-ssf.f[i] * ssf.zi[i]) - exp(-ssf.f[i] * ssf.soilthickness[i])) *
-            ssf.dw[i]
-    end
+    return nothing
 end
 
-"Initialize lateral subsurface variables `ssf` and `ssfmax` with `ksat_profile` `exponential_constant`"
-function initialize_lateralssf_exp_const!(ssf::LateralSSF)
-    ssf_constant = @. ssf.khfrac *
-       ssf.kh_0 *
-       exp(-ssf.f * ssf.z_exp) *
-       ssf.slope *
-       (ssf.soilthickness - ssf.z_exp)
-    for i in eachindex(ssf.ssf)
-        ssf.ssfmax[i] =
-            ((ssf.khfrac[i] * ssf.kh_0[i] * ssf.slope[i]) / ssf.f[i]) *
-            (1.0 - exp(-ssf.f[i] * ssf.z_exp[i])) + ssf_constant[i]
-        if ssf.zi[i] < ssf.z_exp[i]
-            ssf.ssf[i] =
+kh_layered_profile!(
+    soil::SbmSoilModel,
+    subsurface::LateralSSF,
+    kv_profile::Union{KvExponential, KvExponentialConstant},
+    dt,
+) = nothing
+
+"""
+    initialize_lateralssf!(model::LateralSSF, kh_profile::KhExponential)
+    initialize_lateralssf!(model::LateralSSF, kh_profile::KhExponentialConstant)
+
+Initialize lateral subsurface variables `ssf` and `ssfmax` using horizontal hydraulic
+conductivity profile `kh_profile`.
+"""
+function initialize_lateralssf!(model::LateralSSF, kh_profile::KhExponential)
+    (; kh_0, f) = kh_profile
+    (; ssf, ssfmax, zi, slope, soilthickness, dw) = model
+
+    @. ssfmax = ((kh_0 * slope) / f) * (1.0 - exp(-f * soilthickness))
+    @. ssf = ((kh_0 * slope) / f) * (exp(-f * zi) - exp(-f * soilthickness)) * dw
+    return nothing
+end
+
+function initialize_lateralssf!(model::LateralSSF, kh_profile::KhExponentialConstant)
+    (; kh_0, f) = kh_profile.exponential
+    (; z_exp) = kh_profile
+    (; ssf, ssfmax, zi, slope, soilthickness, dw) = model
+    ssf_constant = @. kh_0 * exp(-f * z_exp) * slope * (soilthickness - z_exp)
+    for i in eachindex(ssf)
+        ssfmax[i] =
+            ((kh_0[i] * slope[i]) / f[i]) * (1.0 - exp(-f[i] * z_exp[i])) + ssf_constant[i]
+        if zi[i] < z_exp[i]
+            ssf[i] =
                 (
-                    ((ssf.kh_0[i] * ssf.slope[i]) / ssf.f[i]) *
-                    (exp(-ssf.f[i] * ssf.zi[i]) - exp(-ssf.f[i] * ssf.z_exp[i])) +
-                    ssf_constant[i]
-                ) * ssf.dw[i]
+                    ((kh_0[i] * slope[i]) / f[i]) *
+                    (exp(-f[i] * zi[i]) - exp(-f[i] * z_exp[i])) + ssf_constant[i]
+                ) * dw[i]
         else
-            ssf.ssf[i] =
-                ssf.kh_0[i] *
-                exp(-ssf.f[i] * ssf.zi[i]) *
-                ssf.slope[i] *
-                (ssf.soilthickness[i] - ssf.zi[i]) *
-                ssf.dw[i]
+            ssf[i] =
+                kh_0[i] * exp(-f[i] * zi[i]) * slope[i] * (soilthickness[i] - zi[i]) * dw[i]
+        end
+    end
+    return nothing
+end
+
+"""
+    initialize_lateralssf!(subsurface::LateralSSF, soil::SbmSoilModel, kv_profile::KvLayered, dt)
+    initialize_lateralssf!(subsurface::LateralSSF, soil::SbmSoilModel, kv_profile::KvLayeredExponential, dt)
+
+Initialize lateral subsurface variables `ssf` and `ssfmax` using  vertical hydraulic
+conductivity profile `kv_profile`.
+"""
+function initialize_lateralssf!(
+    subsurface::LateralSSF,
+    soil::SbmSoilModel,
+    kv_profile::KvLayered,
+    dt,
+)
+    (; kh) = subsurface.kh_profile
+    (; nlayers, act_thickl) = soil.parameters
+    (; ssf, ssfmax, zi, khfrac, soilthickness, slope, dw) = subsurface
+    kh_layered_profile!(soil, subsurface, kv_profile, dt)
+    for i in eachindex(ssf)
+        ssf[i] = kh[i] * (soilthickness[i] - zi[i]) * slope[i] * dw[i]
+        kh_max = 0.0
+        for j in 1:nlayers[i]
+            kh_max += kv_profile.kv[i][j] * act_thickl[i][j]
         end
+        kh_max = kh_max * khfrac[i] * 0.001 * 0.001
+        ssfmax[i] = kh_max * slope[i]
     end
+    return nothing
 end
 
-"Initialize lateral subsurface variables `ssf`, `ssfmax` and `kh` with ksat_profile` `layered` or `layered_exponential`"
-function initialize_lateralssf_layered!(ssf::LateralSSF, sbm::SBM, ksat_profile)
-    for i in eachindex(ssf.ssf)
-        ssf.kh[i] = kh_layered_profile(sbm, ssf.khfrac[i], i, ksat_profile)
-        ssf.ssf[i] =
-            ssf.kh[i] * (ssf.soilthickness[i] - ssf.zi[i]) * ssf.slope[i] * ssf.dw[i]
+function initialize_lateralssf!(
+    subsurface::LateralSSF,
+    soil::SbmSoilModel,
+    kv_profile::KvLayeredExponential,
+    dt,
+)
+    (; ssf, ssfmax, zi, khfrac, soilthickness, slope, dw) = subsurface
+    (; nlayers, act_thickl) = soil.parameters
+    (; kh) = subsurface.kh_profile
+    (; kv, f, nlayers_kv, z_layered) = kv_profile
+
+    kh_layered_profile!(soil, subsurface, kv_profile, dt)
+    for i in eachindex(ssf)
+        ssf[i] = kh[i] * (soilthickness[i] - zi[i]) * slope[i] * dw[i]
         kh_max = 0.0
-        for j = 1:sbm.nlayers[i]
-            if j <= sbm.nlayers_kv[i]
-                kh_max += sbm.kv[i][j] * sbm.act_thickl[i][j]
+        for j in 1:nlayers[i]
+            if j <= nlayers_kv[i]
+                kh_max += kv[i][j] * act_thickl[i][j]
             else
-                zt = sbm.soilthickness[i] - sbm.z_layered[i]
+                zt = soil.parameters.soilthickness[i] - z_layered[i]
                 k = max(j - 1, 1)
-                kh_max += sbm.kv[i][k] / sbm.f[i] * (1.0 - exp(-sbm.f[i] * zt))
+                kh_max += kv[i][k] / f[i] * (1.0 - exp(-f[i] * zt))
                 break
             end
         end
-        kh_max = kh_max * ssf.khfrac[i] * 0.001 * 0.001
-        ssf.ssfmax[i] = kh_max * ssf.slope[i]
+        kh_max = kh_max * khfrac[i] * 0.001 * 0.001
+        ssfmax[i] = kh_max * slope[i]
     end
+    return nothing
 end
 
 """
diff --git a/src/vegetation/canopy.jl b/src/vegetation/canopy.jl
new file mode 100644
index 000000000..98762ea50
--- /dev/null
+++ b/src/vegetation/canopy.jl
@@ -0,0 +1,153 @@
+abstract type AbstractInterceptionModel{T} end
+
+"Struct for storing interception model variables"
+@get_units @grid_loc @with_kw struct InterceptionVariables{T}
+    # Canopy potential evaporation [mm Δt⁻¹]
+    canopy_potevap::Vector{T}
+    # Interception loss by evaporation [mm Δt⁻¹]
+    interception_rate::Vector{T}
+    # Canopy storage [mm]
+    canopy_storage::Vector{T} | "mm"
+    # Stemflow [mm Δt⁻¹]
+    stemflow::Vector{T}
+    # Throughfall [mm Δt⁻¹]
+    throughfall::Vector{T}
+end
+
+"Initialize interception model variables"
+function InterceptionVariables(T::Type{<:AbstractFloat}, n::Int)
+    return InterceptionVariables(;
+        canopy_potevap = fill(mv, n),
+        interception_rate = fill(mv, n),
+        canopy_storage = zeros(T, n),
+        stemflow = fill(mv, n),
+        throughfall = fill(mv, n),
+    )
+end
+
+"Struct for storing Gash interception model parameters"
+@get_units @grid_loc @with_kw struct GashParameters{T}
+    # wet canopy [mm Δt⁻¹] and the average precipitation intensity [mm Δt⁻¹] on a saturated canopy
+    e_r::Vector{T} | "-"
+    vegetation_parameter_set::VegetationParameters{T}
+end
+
+"Gash interception model"
+@with_kw struct GashInterceptionModel{T} <: AbstractInterceptionModel{T}
+    parameters::GashParameters{T}
+    variables::InterceptionVariables{T}
+end
+
+"Initialize Gash interception model"
+function GashInterceptionModel(nc, config, inds, vegetation_parameter_set)
+    e_r = ncread(
+        nc,
+        config,
+        "vertical.interception.parameters.e_r";
+        sel = inds,
+        defaults = 0.1,
+        type = Float,
+    )
+    n = length(inds)
+    params =
+        GashParameters(; e_r = e_r, vegetation_parameter_set = vegetation_parameter_set)
+    vars = InterceptionVariables(Float, n)
+    model = GashInterceptionModel(; parameters = params, variables = vars)
+    return model
+end
+
+"Update Gash interception model for a single timestep"
+function update!(model::GashInterceptionModel, atmospheric_forcing::AtmosphericForcing)
+    (; leaf_area_index, canopygapfraction, cmax, kc) =
+        model.parameters.vegetation_parameter_set
+    (; canopy_potevap, throughfall, interception_rate, stemflow, canopy_storage) =
+        model.variables
+    (; precipitation, potential_evaporation) = atmospheric_forcing
+    e_r = model.parameters.e_r
+    n = length(precipitation)
+    if !isnothing(leaf_area_index)
+        update_canopy_parameters!(model)
+        threaded_foreach(1:n; basesize = 1000) do i
+            canopyfraction = 1.0 - canopygapfraction[i]
+            ewet = canopyfraction * potential_evaporation[i] * kc[i]
+            e_r[i] =
+                precipitation[i] > 0.0 ?
+                min(0.25, ewet / max(0.0001, canopyfraction * precipitation[i])) : 0.0
+        end
+    end
+    threaded_foreach(1:n; basesize = 1000) do i
+        canopy_potevap[i] = kc[i] * potential_evaporation[i] * (1.0 - canopygapfraction[i])
+        throughfall[i], interception_rate[i], stemflow[i], canopy_storage[i] =
+            rainfall_interception_gash(
+                cmax[i],
+                e_r[i],
+                canopygapfraction[i],
+                precipitation[i],
+                canopy_storage[i],
+                canopy_potevap[i],
+            )
+    end
+    return nothing
+end
+
+"Rutter interception model"
+@with_kw struct RutterInterceptionModel{T} <: AbstractInterceptionModel{T}
+    parameters::VegetationParameters{T}
+    variables::InterceptionVariables{T}
+end
+
+"Initialize Rutter interception model"
+function RutterInterceptionModel(vegetation_parameter_set, n)
+    vars = InterceptionVariables(n)
+    model =
+        RutterInterceptionModel(; parameters = vegetation_parameter_set, variables = vars)
+    return model
+end
+
+"Update Rutter interception model for a single timestep"
+function update!(model::RutterInterceptionModel, atmospheric_forcing::AtmosphericForcing)
+    (; leaf_area_index, canopygapfraction, cmax, kc) =
+        model.parameters.vegetation_parameter_set
+    (; canopy_potevap, throughfall, interception_rate, stemflow, canopy_storage) =
+        model.variables
+    (; precipitation, potential_evaporation) = atmospheric_forcing
+    if !isnothing(leaf_area_index)
+        update_canopy_parameters!(model)
+    end
+    n = length(precipitation)
+    threaded_foreach(1:n; basesize = 1000) do i
+        canopy_potevap[i] = kc[i] * potential_evaporation[i] * (1.0 - canopygapfraction[i])
+        throughfall[i], interception_rate[i], stemflow[i], canopy_storage[i] =
+            rainfall_interception_modrut(
+                precipitation[i],
+                canopy_potevap[i],
+                canopy_storage[i],
+                canopygapfraction[i],
+                cmax[i],
+            )
+    end
+    return nothing
+end
+
+"Update canopy parameters `cmax` and `canopygapfraction` based on `leaf_area_index` for a single timestep"
+function update_canopy_parameters!(model::AbstractInterceptionModel)
+    (;
+        leaf_area_index,
+        storage_wood,
+        kext,
+        storage_specific_leaf,
+        canopygapfraction,
+        cmax,
+    ) = model.parameters.vegetation_parameter_set
+
+    n = length(leaf_area_index)
+    threaded_foreach(1:n; basesize = 1000) do i
+        cmax[i] = storage_specific_leaf[i] * leaf_area_index[i] + storage_wood[i]
+        canopygapfraction[i] = exp(-kext[i] * leaf_area_index[i])
+    end
+    return nothing
+end
+
+"Return potential transpiration rate based on the interception rate"
+get_potential_transpiration(model::AbstractInterceptionModel) =
+    @. max(0.0, model.variables.canopy_potevap - model.variables.interception_rate)
\ No newline at end of file
diff --git a/src/vegetation/rainfall_interception.jl b/src/vegetation/rainfall_interception.jl
new file mode 100644
index 000000000..920a0b3dd
--- /dev/null
+++ b/src/vegetation/rainfall_interception.jl
@@ -0,0 +1,95 @@
+
+"""
+    rainfall_interception_gash(cmax, e_r, canopygapfraction, precipitation, canopystorage, maxevap)
+
+Interception according to the Gash model (for daily timesteps). `cmax` is the maximum canopy storage and
+`e_r` is the ratio of the average evaporation from the wet canopy and the average precipitation intensity on
+a saturated canopy.
+"""
+function rainfall_interception_gash(
+    cmax,
+    e_r,
+    canopygapfraction,
+    precipitation,
+    canopystorage,
+    maxevap,
+)
+    # TODO: add other rainfall interception method (lui)
+    # TODO: include subdaily Gash model
+    # TODO: improve computation of stemflow partitioning coefficient pt (0.1 * canopygapfraction)
+    pt = min(0.1 * canopygapfraction, 1.0 - canopygapfraction)
+    pfrac = 1.0 - canopygapfraction - pt
+    p_sat = (-cmax / e_r) * log(1.0 - min((e_r / pfrac), 1.0))
+    p_sat = isinf(p_sat) ? 0.0 : p_sat
+
+    # large storms P > P_sat
+    largestorms = precipitation > p_sat
+
+    iwet = largestorms ? (pfrac * p_sat) - cmax : precipitation * pfrac
+    isat = largestorms ? (e_r) * (precipitation - p_sat) : 0.0
+    idry = largestorms ? cmax : 0.0
+    itrunc = 0.0
+
+    stemflow = pt * precipitation
+
+    throughfall = precipitation - iwet - idry - isat - itrunc - stemflow
+    interception = iwet + idry + isat + itrunc
+
+    # Now corect for area without any Interception (say open water Cmax -- zero)
+    cmaxzero = cmax <= 0.0
+    throughfall = cmaxzero ? precipitation : throughfall
+    interception = cmaxzero ? 0.0 : interception
+    stemflow = cmaxzero ? 0.0 : stemflow
+
+    # Now corect for maximum potential evap
+    canopy_drainage = interception > maxevap ? interception - maxevap : 0.0
+    interception = min(interception, maxevap)
+
+    # Add surpluss to the throughfall
+    throughfall = throughfall + canopy_drainage
+
+    return throughfall, interception, stemflow, canopystorage
+end
+
+"""
+    rainfall_interception_modrut(precipitation, potential_evaporation, canopystorage, canopygapfraction, cmax)
+
+Interception according to a modified Rutter model. The model is solved explicitly and there is no
+drainage below `cmax`.
+"""
+function rainfall_interception_modrut(
+    precipitation,
+    potential_evaporation,
+    canopystorage,
+    canopygapfraction,
+    cmax,
+)
+
+    # TODO: improve computation of stemflow partitioning coefficient pt (0.1 * canopygapfraction)
+    pt = min(0.1 * canopygapfraction, 1.0 - canopygapfraction)
+
+    # Amount of p that falls on the canopy
+    precip_canopy = (1.0 - canopygapfraction - pt) * precipitation
+
+    # Canopystorage cannot be larger than cmax, no gravity drainage below that. This check
+    # is required because cmax can change over time
+    canopy_drainage1 = canopystorage > cmax ? canopystorage - cmax : 0.0
+    canopystorage = canopystorage - canopy_drainage1
+
+    # Add the precipitation that falls on the canopy to the store
+    canopystorage = canopystorage + precip_canopy
+
+    # Evaporation, make sure the store does not get negative
+    canopy_evap = min(canopystorage, potential_evaporation) # interception rate
+    canopystorage = canopystorage - canopy_evap
+
+    # Drain the canopystorage again if needed
+    canopy_drainage2 = canopystorage > cmax ? canopystorage - cmax : 0.0
+    canopystorage = canopystorage - canopy_drainage2
+
+    # Calculate throughfall and stemflow
+    throughfall = canopy_drainage1 + canopy_drainage2 + canopygapfraction * precipitation
+    stemflow = precipitation * pt
+
+    return throughfall, canopy_evap, stemflow, canopystorage
+end
diff --git a/src/vertical_process.jl b/src/vertical_process.jl
deleted file mode 100644
index 032a0fc6c..000000000
--- a/src/vertical_process.jl
+++ /dev/null
@@ -1,439 +0,0 @@
-"""
-    scurve(x, a, b, c)
-
-Sigmoid "S"-shaped curve.
-
-# Arguments
-- `x::Real`: input
-- `a::Real`: determines the centre level
-- `b::Real`: determines the amplitude of the curve
-- `c::Real`: determines the steepness or "stepwiseness" of the curve.
-             The higher c the sharper the function. A negative c reverses the function.
-"""
-function scurve(x, a, b, c)
-    s = one(x) / (b + exp(-c * (x - a)))
-    return s
-end
-
-"""
-    rainfall_interception_gash(cmax, e_r, canopygapfraction, precipitation, canopystorage, maxevap)
-
-Interception according to the Gash model (for daily timesteps). `cmax` is the maximum canopy storage and
-`e_r` is the ratio of the average evaporation from the wet canopy and the average precipitation intensity on
-a saturated canopy.
-"""
-function rainfall_interception_gash(
-    cmax,
-    e_r,
-    canopygapfraction,
-    precipitation,
-    canopystorage,
-    maxevap,
-)
-    # TODO: add other rainfall interception method (lui)
-    # TODO: include subdaily Gash model
-    # TODO: improve computation of stemflow partitioning coefficient pt (0.1 * canopygapfraction)
-    pt = min(0.1 * canopygapfraction, 1.0 - canopygapfraction)
-    pfrac = 1.0 - canopygapfraction - pt
-    p_sat = (-cmax / e_r) * log(1.0 - min((e_r / pfrac), 1.0))
-    p_sat = isinf(p_sat) ? 0.0 : p_sat
-
-    # large storms P > P_sat
-    largestorms = precipitation > p_sat
-
-    iwet = largestorms ? (pfrac * p_sat) - cmax : precipitation * pfrac
-    isat = largestorms ? (e_r) * (precipitation - p_sat) : 0.0
-    idry = largestorms ? cmax : 0.0
-    itrunc = 0.0
-
-    stemflow = pt * precipitation
-
-    throughfall = precipitation - iwet - idry - isat - itrunc - stemflow
-    interception = iwet + idry + isat + itrunc
-
-    # Now corect for area without any Interception (say open water Cmax -- zero)
-    cmaxzero = cmax <= 0.0
-    throughfall = cmaxzero ? precipitation : throughfall
-    interception = cmaxzero ? 0.0 : interception
-    stemflow = cmaxzero ? 0.0 : stemflow
-
-    # Now corect for maximum potential evap
-    canopy_drainage = interception > maxevap ? interception - maxevap : 0.0
-    interception = min(interception, maxevap)
-
-    # Add surpluss to the throughfall
-    throughfall = throughfall + canopy_drainage
-
-    return throughfall, interception, stemflow, canopystorage
-
-end
-
-"""
-    rainfall_interception_modrut(precipitation, potential_evaporation, canopystorage, canopygapfraction, cmax)
-
-Interception according to a modified Rutter model. The model is solved explicitly and there is no
-drainage below `cmax`.
-"""
-function rainfall_interception_modrut(
-    precipitation,
-    potential_evaporation,
-    canopystorage,
-    canopygapfraction,
-    cmax,
-)
-
-    # TODO: improve computation of stemflow partitioning coefficient pt (0.1 * canopygapfraction)
-    pt = min(0.1 * canopygapfraction, 1.0 - canopygapfraction)
-
-    # Amount of p that falls on the canopy
-    precip_canopy = (1.0 - canopygapfraction - pt) * precipitation
-
-    # Canopystorage cannot be larger than cmax, no gravity drainage below that. This check
-    # is required because cmax can change over time
-    canopy_drainage1 = canopystorage > cmax ? canopystorage - cmax : 0.0
-    canopystorage = canopystorage - canopy_drainage1
-
-    # Add the precipitation that falls on the canopy to the store
-    canopystorage = canopystorage + precip_canopy
-
-    # Now do the Evap, make sure the store does not get negative
-    canopy_evap = min(canopystorage, potential_evaporation)
-    canopystorage = canopystorage - canopy_evap
-
-    # Amount of evap not used
-    leftover = potential_evaporation - canopy_evap
-
-    # Now drain the canopystorage again if needed...
-    canopy_drainage2 = canopystorage > cmax ? canopystorage - cmax : 0.0
-    canopystorage = canopystorage - canopy_drainage2
-
-    # Calculate throughfall and stemflow
-    throughfall = canopy_drainage1 + canopy_drainage2 + canopygapfraction * precipitation
-    stemflow = precipitation * pt
-
-    # Calculate interception, this is NET Interception
-    netinterception = precipitation + canopy_drainage1 - throughfall - stemflow
-    interception = canopy_evap
-
-    return netinterception, throughfall, stemflow, leftover, interception, canopystorage
-
-end
-
-"""
-    vwc_brooks_corey(h, hb, theta_s, theta_r, c)
-
-Volumetric water content based on the Brooks-Corey soil hydraulic model.
-"""
-function vwc_brooks_corey(h, hb, theta_s, theta_r, c)
-    if h < hb
-        par_lambda = 2.0 / (c - 3.0)
-        vwc = (theta_s - theta_r) * pow(hb / h, par_lambda) + theta_r
-    else
-        vwc = theta_s
-    end
-    return vwc
-end
-
-"""
-    head_brooks_corey(vwc, theta_s, theta_r, c, hb)
-
-Soil water pressure head based on the Brooks-Corey soil hydraulic model.
-"""
-function head_brooks_corey(vwc, theta_s, theta_r, c, hb)
-    par_lambda = 2.0 / (c - 3.0)
-    # Note that in the original formula, theta_r is extracted from vwc, but theta_r is not part of the numerical vwc calculation
-    h = hb / (pow(((vwc) / (theta_s - theta_r)), (1.0 / par_lambda)))
-    h = min(h, hb)
-    return h
-end
-
-"""
-    feddes_h3(h3_high, h3_low, tpot, Δt)
-
-Return soil water pressure head `h3` of Feddes root water uptake reduction function.
-"""
-function feddes_h3(h3_high, h3_low, tpot, Δt)
-    # value of h3 is a function of potential transpiration [mm/d]
-    tpot_daily = tpot * (basetimestep / Δt)
-    if (tpot_daily >= 0.0) && (tpot_daily <= 1.0)
-        h3 = h3_low
-    elseif (tpot_daily > 1.0) && (tpot_daily < 5.0)
-        h3 = h3_high + ((h3_low - h3_high) * (5.0 - tpot_daily)) / (5.0 - 1.0)
-    else
-        h3 = h3_high
-    end
-    return h3
-end
-
-"""
-    rwu_reduction_feddes(h, h1, h2, h3, h4, alpha_h1)
-
-Root water uptake reduction factor based on Feddes.
-"""
-function rwu_reduction_feddes(h, h1, h2, h3, h4, alpha_h1)
-    # root water uptake reduction coefficient alpha (see also Feddes et al., 1978)
-    if alpha_h1 == 0.0
-        if (h <= h4) || (h > h1)
-            alpha = 0.0
-        elseif (h > h2) && (h <= h1)
-            alpha = (h - h1) / (h2 - h1)
-        elseif (h >= h3) && (h <= h2)
-            alpha = 1.0
-        elseif (h >= h4) && (h < h3)
-            alpha = (h - h4) / (h3 - h4)
-        end
-    else
-        if h <= h4
-            alpha = 0.0
-        elseif h >= h3
-            alpha = 1.0
-        elseif (h >= h4) && (h < h3)
-            alpha = (h - h4) / (h3 - h4)
-        end
-    end
-    return alpha
-end
-
-"""
-    infiltration(avail_forinfilt, pathfrac, cf_soil, tsoil, infiltcapsoil, infiltcappath, ustorecapacity, modelsnow::Bool, soilinfreduction::Bool)
-
-Soil infiltration based on infiltration capacity soil `infiltcapsoil`, infiltration capacity compacted area
-`infiltcappath` and capacity unsatured zone `ustorecapacity`. The soil infiltration capacity can be adjusted
-in case the soil is frozen (`modelsnow` and `soilinfreduction` is `true`).
-
-"""
-function infiltration(
-    avail_forinfilt,
-    pathfrac,
-    cf_soil,
-    tsoil,
-    infiltcapsoil,
-    infiltcappath,
-    ustorecapacity,
-    modelsnow::Bool,
-    soilinfreduction::Bool,
-)
-    # First determine if the soil infiltration capacity can deal with the amount of water
-    # split between infiltration in undisturbed soil and compacted areas (paths)
-    soilinf = avail_forinfilt * (1.0 - pathfrac)
-    pathinf = avail_forinfilt * pathfrac
-    if modelsnow && soilinfreduction
-        bb = 1.0 / (1.0 - cf_soil)
-        soilinfredu = scurve(tsoil, Float(0.0), bb, Float(8.0)) + cf_soil
-    else
-        soilinfredu = 1.0
-    end
-    max_infiltsoil = min(infiltcapsoil * soilinfredu, soilinf)
-    max_infiltpath = min(infiltcappath * soilinfredu, pathinf)
-    infiltsoilpath = min(max_infiltpath + max_infiltsoil, max(0.0, ustorecapacity))
-
-    if max_infiltpath + max_infiltsoil > 0.0
-        infiltsoil =
-            max_infiltsoil *
-            min(1.0, max(0.0, ustorecapacity) / (max_infiltpath + max_infiltsoil))
-        infiltpath =
-            max_infiltpath *
-            min(1.0, max(0.0, ustorecapacity) / (max_infiltpath + max_infiltsoil))
-    else
-        infiltsoil = 0.0
-        infiltpath = 0.0
-    end
-
-    infiltexcess = (soilinf - max_infiltsoil) + (pathinf - max_infiltpath)
-
-    return infiltsoilpath,
-    infiltsoil,
-    infiltpath,
-    soilinf,
-    pathinf,
-    infiltexcess,
-    soilinfredu
-end
-
-"""
-    unsatzone_flow_layer(usd, kv_z, l_sat, c)
-
-Assuming a unit head gradient, the transfer of water from an unsaturated store layer `usd` is controlled by the
-vertical saturated hydraulic conductivity `kv_z` (bottom layer or water table), the effective saturation
-degree of the layer (ratio `usd` and `l_sat`), and a Brooks-Corey power coefficient `c`.
-"""
-function unsatzone_flow_layer(usd, kv_z, l_sat, c)
-    if usd <= 0.0
-        return 0.0, 0.0
-    end
-    sum_ast = 0.0
-    # first transfer soil water > maximum soil water capacity layer (iteration is not required because of steady theta (usd))
-    st = kv_z * min(pow(usd / l_sat, c), 1.0)
-    st_sat = max(0.0, usd - l_sat)
-    usd -= min(st, st_sat)
-    sum_ast = sum_ast + min(st, st_sat)
-    ast = max(min(st - min(st, st_sat), usd), 0.0)
-    # number of iterations (to reduce "overshooting") based on fixed maximum change in soil water per iteration step (0.2 mm / model timestep)
-    its = Int(cld(ast, 0.2))
-    for _ = 1:its
-        st = (kv_z / its) * min(pow(usd / l_sat, c), 1.0)
-        ast = min(st, usd)
-        usd -= ast
-        sum_ast += ast
-    end
-
-    return usd, sum_ast
-end
-
-"""
-    unsatzone_flow_sbm(ustorelayerdepth, soilwatercapacity, satwaterdepth, kv_z, usl, theta_s, theta_r)
-
-The transfer of water from the unsaturated store `ustorelayerdepth` to the saturated store `satwaterdepth`
-is controlled by the vertical saturated hydraulic conductivity `kv_z` at the water table and the ratio between
-`ustorelayerdepth` and the saturation deficit (`soilwatercapacity` minus `satwaterdepth`). This is the
-original Topog_SBM vertical transfer formulation.
-
-"""
-function unsatzone_flow_sbm(
-    ustorelayerdepth,
-    soilwatercapacity,
-    satwaterdepth,
-    kv_z,
-    usl,
-    theta_s,
-    theta_r,
-)
-
-    sd = soilwatercapacity - satwaterdepth
-    if sd <= 0.00001
-        ast = 0.0
-    else
-        st = kv_z * min(ustorelayerdepth, usl * (theta_s - theta_r)) / sd
-        ast = min(st, ustorelayerdepth)
-        ustorelayerdepth = ustorelayerdepth - ast
-    end
-
-    return ustorelayerdepth, ast
-
-end
-
-
-"""
-    snowpack_hbv(snow, snowwater, precipitation, temperature, tti, tt, ttm, cfmax, whc)
-
-HBV type snowpack modeling using a temperature degree factor.
-All correction factors (RFCF and SFCF) are set to 1.
-The refreezing efficiency factor is set to 0.05.
-
-# Arguments
-- `snow` (snow storage)
-- `snowwater` (liquid water content in the snow pack)
-- `precipitation` (throughfall + stemflow)
-- `temperature`
-- `tti` (snowfall threshold interval length)
-- `tt` (threshold temperature for snowfall)
-- `ttm` (melting threshold)
-- `cfmax` (degree day factor, rate of snowmelt)
-- `whc` (Water holding capacity of snow)
-- `rfcf` correction factor for rainfall
-- `sfcf` correction factor for snowfall
-- `cfr` refreeing efficiency constant in refreezing of freewater in snow
-
-# Output
-- `snow`
-- `snowwater`
-- `snowmelt`
-- `rainfall` (precipitation that occurs as rainfall)
-- `snowfall` (precipitation that occurs as snowfall)
-"""
-function snowpack_hbv(
-    snow,
-    snowwater,
-    precipitation,
-    temperature,
-    tti,
-    tt,
-    ttm,
-    cfmax,
-    whc;
-    rfcf = 1.0,
-    sfcf = 1.0,
-    cfr = 0.05,
-)
-
-    # fraction of precipitation which falls as rain
-    rainfrac = if iszero(tti)
-        Float(temperature > tt)
-    else
-        frac = (temperature - (tt - tti / 2.0)) / tti
-        min(frac, 1.0)
-    end
-    rainfrac = max(rainfrac, 0.0)
-
-    # fraction of precipitation which falls as snow
-    snowfrac = 1.0 - rainfrac
-    # different correction for rainfall and snowfall
-    snowfall = snowfrac * sfcf * precipitation  # snowfall depth
-    rainfall = rainfrac * rfcf * precipitation  # rainfall depth
-    # potential snow melt, based on temperature
-    potsnowmelt = temperature > ttm ? cfmax * (temperature - ttm) : 0.0
-    # potential refreezing, based on temperature
-    potrefreezing = temperature < ttm ? cfmax * cfr * (ttm - temperature) : 0.0
-    # actual refreezing
-    refreezing = temperature < ttm ? min(potrefreezing, snowwater) : 0.0
-
-    # no landuse correction here
-    snowmelt = min(potsnowmelt, snow)  # actual snow melt
-    snow = snow + snowfall + refreezing - snowmelt  # dry snow content
-    snowwater = snowwater - refreezing  # free water content in snow
-    maxsnowwater = snow * whc  # max water in the snow
-    snowwater = snowwater + snowmelt + rainfall  # add all water and potentially supersaturate the snowpack
-    rainfall = max(snowwater - maxsnowwater, 0.0)  # rain + surpluss snowwater
-    snowwater = snowwater - rainfall
-
-    return snow, snowwater, snowmelt, rainfall, snowfall
-end
-
-"""
-    glacier_hbv(glacierfrac, glacierstore, snow, temperature, tt, cfmax, g_sifrac, dt)
-
-HBV-light type of glacier modelling.
-First, a fraction of the snowpack is converted into ice using the HBV-light
-model (fraction between 0.001-0.005 per day).
-Glacier melting is modelled using a temperature degree factor and only
-occurs if the snow cover < 10 mm.
-
-# Arguments
-- `glacierFrac` fraction covered by glaciers [-]
-- `glacierstore` volume of the glacier [mm] w.e.
-- `snow` snow pack on top of glacier [mm]
-- `temperature` air temperature [°C]
-- `tt` temperature threshold for ice melting [°C]
-- `cfmax` ice degree-day factor in [mm/(°C/day)]
-- `g_sifrac` fraction of the snow turned into ice [-]
-- `dt` model timestep [s]
-
-# Output
-- `snow`
-- `snow2glacier`
-- `glacierstore`
-- `glaciermelt`
-
-"""
-function glacier_hbv(glacierfrac, glacierstore, snow, temperature, tt, cfmax, g_sifrac, dt)
-
-    # Fraction of the snow transformed into ice (HBV-light model)
-    snow2glacier = g_sifrac * snow
-    snow2glacier = glacierfrac > 0.0 ? snow2glacier : 0.0
-
-    # Max conversion to 8mm/day
-    snow2glacier = min(snow2glacier, 8.0 * (dt / basetimestep))
-
-    snow = snow - (snow2glacier * glacierfrac)
-    glacierstore = glacierstore + snow2glacier
-
-    # Potential snow melt, based on temperature
-    potmelt = temperature > tt ? cfmax * (temperature - tt) : 0.0
-
-    # actual Glacier melt
-    glaciermelt = snow < 10.0 ? min(potmelt, glacierstore) : 0.0
-    glacierstore = glacierstore - glaciermelt
-
-    return snow, snow2glacier, glacierstore, glaciermelt
-
-end
diff --git a/src/water_demand.jl b/src/water_demand.jl
deleted file mode 100644
index 432db1b69..000000000
--- a/src/water_demand.jl
+++ /dev/null
@@ -1,626 +0,0 @@
-@get_units @exchange @grid_type @grid_location @with_kw struct NonIrrigationDemand{T}
-    demand_gross::Vector{T}                 # gross water demand [mm Δt⁻¹]
-    demand_net::Vector{T}                   # net water demand [mm Δt⁻¹]
-    returnflow_fraction::Vector{T} | "-"    # return flow fraction [-]
-    returnflow::Vector{T}                   # return flow [mm Δt⁻¹]
-end
-
-@get_units @exchange @grid_type @grid_location @with_kw struct NonPaddy{T}
-    demand_gross::Vector{T}                     # irrigation gross demand [mm Δt⁻¹] 
-    irrigation_efficiency::Vector{T} | "-"      # irrigation efficiency [-]
-    maximum_irrigation_rate::Vector{T}          # maximum irrigation depth [mm Δt⁻¹]
-    irrigation_areas::Vector{Bool} | "-"        # irrigation areas [-]
-    irrigation_trigger::Vector{Bool} | "-"      # irrigation on or off [-]
-end
-
-@get_units @exchange @grid_type @grid_location @with_kw struct Paddy{T}
-    demand_gross::Vector{T}                     # irrigation gross demand [mm Δt⁻¹] 
-    irrigation_efficiency::Vector{T} | "-"      # irrigation efficiency [-]
-    maximum_irrigation_rate::Vector{T}          # maximum irrigation depth [mm Δt⁻¹]
-    irrigation_areas::Vector{Bool} | "-"        # irrigation areas [-]
-    irrigation_trigger::Vector{Bool} | "-"      # irrigation on or off [-]
-    h_min::Vector{T} | "mm"                     # minimum required water depth in the irrigated rice field [mm]
-    h_opt::Vector{T} | "mm"                     # optimal water depth in the irrigated rice fields [mm]
-    h_max::Vector{T} | "mm"                     # water depth when rice field starts spilling water (overflow) [mm]
-    h::Vector{T} | "mm"                         # actual water depth in rice field [mm]
-end
-
-@get_units @exchange @grid_type @grid_location @with_kw struct AllocationRiver{T}
-    act_surfacewater_abst::Vector{T}                    # actual surface water abstraction [mm Δt⁻¹]
-    act_surfacewater_abst_vol::Vector{T} | "m3 dt-1"    # actual surface water abstraction [m³ Δt⁻¹]
-    available_surfacewater::Vector{T} | "m3"            # available surface water [m³]
-    nonirri_returnflow::Vector{T}                       # return flow from non irrigation [mm Δt⁻¹] 
-end
-
-@get_units @exchange @grid_type @grid_location @with_kw struct AllocationLand{T}
-    irri_demand_gross::Vector{T}                        # irrigation gross demand [mm Δt⁻¹]
-    nonirri_demand_gross::Vector{T}                     # non-irrigation gross demand [mm Δt⁻¹]
-    total_gross_demand::Vector{T}                       # total gross demand [mm Δt⁻¹]
-    frac_sw_used::Vector{T} | "-"                       # fraction surface water used [-]
-    areas::Vector{Int} | "-"                            # allocation areas [-]
-    surfacewater_demand::Vector{T}                      # demand from surface water [mm Δt⁻¹]
-    surfacewater_alloc::Vector{T}                       # allocation from surface water [mm Δt⁻¹]
-    act_groundwater_abst::Vector{T}                     # actual groundwater abstraction [mm Δt⁻¹]
-    act_groundwater_abst_vol::Vector{T} | "m3 dt-1"     # actual groundwater abstraction [m³ Δt⁻¹]
-    available_groundwater::Vector{T} | "m3"             # available groundwater [m³]
-    groundwater_demand::Vector{T}                       # demand from groundwater [mm Δt⁻¹]
-    groundwater_alloc::Vector{T}                        # allocation from groundwater [mm Δt⁻¹]
-    irri_alloc::Vector{T}                               # allocated water for irrigation [mm Δt⁻¹]
-    nonirri_alloc::Vector{T}                            # allocated water for non-irrigation [mm Δt⁻¹]
-    total_alloc::Vector{T}                              # total allocated water [mm Δt⁻¹]
-    nonirri_returnflow::Vector{T}                       # return flow from non irrigation [mm Δt⁻¹]
-end
-
-"Return return flow fraction based on gross water demand `demand_gross` and net water demand `demand_net`"
-function returnflow_fraction(demand_gross, demand_net)
-    fraction = bounded_divide(demand_net, demand_gross)
-    returnflow_fraction = 1.0 - fraction
-    return returnflow_fraction
-end
-
-"Initialize water demand for the domestic sector"
-function initialize_domestic_demand(nc, config, inds, dt)
-    demand_gross =
-        ncread(
-            nc,
-            config,
-            "vertical.domestic.demand_gross";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    demand_net =
-        ncread(
-            nc,
-            config,
-            "vertical.domestic.demand_net";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    n = length(inds)
-    returnflow_f = returnflow_fraction.(demand_gross, demand_net)
-
-    domestic = NonIrrigationDemand{Float}(
-        demand_gross = demand_gross,
-        demand_net = demand_net,
-        returnflow_fraction = returnflow_f,
-        returnflow = fill(Float(0), n),
-    )
-
-    return domestic
-end
-
-"Initialize water demand for the industry sector"
-function initialize_industry_demand(nc, config, inds, dt)
-    demand_gross =
-        ncread(
-            nc,
-            config,
-            "vertical.industry.demand_gross";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    demand_net =
-        ncread(
-            nc,
-            config,
-            "vertical.industry.demand_net";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    n = length(inds)
-    returnflow_f = returnflow_fraction.(demand_gross, demand_net)
-
-    industry = NonIrrigationDemand{Float}(
-        demand_gross = demand_gross,
-        demand_net = demand_net,
-        returnflow_fraction = returnflow_f,
-        returnflow = fill(Float(0), n),
-    )
-
-    return industry
-end
-
-"Initialize water demand for the livestock sector"
-function initialize_livestock_demand(nc, config, inds, dt)
-    demand_gross =
-        ncread(
-            nc,
-            config,
-            "vertical.livestock.demand_gross";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    demand_net =
-        ncread(
-            nc,
-            config,
-            "vertical.livestock.demand_net";
-            sel = inds,
-            defaults = 0.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-    n = length(inds)
-    returnflow_f = returnflow_fraction.(demand_gross, demand_net)
-
-    livestock = NonIrrigationDemand{Float}(
-        demand_gross = demand_gross,
-        demand_net = demand_net,
-        returnflow_fraction = returnflow_f,
-        returnflow = fill(Float(0), n),
-    )
-    return livestock
-end
-
-"Initialize paddy (rice) fields for water demand and irrigation computations"
-function initialize_paddy(nc, config, inds, dt)
-    h_min = ncread(
-        nc,
-        config,
-        "vertical.paddy.h_min";
-        sel = inds,
-        defaults = 20.0,
-        type = Float,
-    )
-    h_opt = ncread(
-        nc,
-        config,
-        "vertical.paddy.h_opt";
-        sel = inds,
-        defaults = 50.0,
-        type = Float,
-    )
-    h_max = ncread(
-        nc,
-        config,
-        "vertical.paddy.h_max";
-        sel = inds,
-        defaults = 80.0,
-        type = Float,
-    )
-    efficiency = ncread(
-        nc,
-        config,
-        "vertical.paddy.irrigation_efficiency";
-        sel = inds,
-        defaults = 1.0,
-        type = Float,
-    )
-    areas = ncread(
-        nc,
-        config,
-        "vertical.paddy.irrigation_areas";
-        sel = inds,
-        optional = false,
-        type = Bool,
-    )
-    irrigation_trigger = ncread(
-        nc,
-        config,
-        "vertical.paddy.irrigation_trigger";
-        sel = inds,
-        optional = false,
-        type = Bool,
-    )
-    max_irri_rate =
-        ncread(
-            nc,
-            config,
-            "vertical.paddy.maximum_irrigation_rate";
-            sel = inds,
-            defaults = 25.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-
-    paddy = Paddy{Float}(
-        demand_gross = fill(mv, length(inds)),
-        irrigation_efficiency = efficiency,
-        maximum_irrigation_rate = max_irri_rate,
-        irrigation_trigger = irrigation_trigger,
-        h_min = h_min,
-        h_max = h_max,
-        h_opt = h_opt,
-        irrigation_areas = areas,
-        h = fill(0.0, length(inds)),
-    )
-    return paddy
-end
-
-"Initialize crop (non paddy) fields for water demand and irrigation computations"
-function initialize_nonpaddy(nc, config, inds, dt)
-    efficiency = ncread(
-        nc,
-        config,
-        "vertical.nonpaddy.irrigation_efficiency";
-        sel = inds,
-        defaults = 1.0,
-        type = Float,
-    )
-    areas = ncread(
-        nc,
-        config,
-        "vertical.nonpaddy.irrigation_areas";
-        sel = inds,
-        defaults = 1,
-        optional = false,
-        type = Int,
-    )
-    irrigation_trigger = ncread(
-        nc,
-        config,
-        "vertical.nonpaddy.irrigation_trigger";
-        sel = inds,
-        defaults = 1,
-        optional = false,
-        type = Bool,
-    )
-    max_irri_rate =
-        ncread(
-            nc,
-            config,
-            "vertical.nonpaddy.maximum_irrigation_rate";
-            sel = inds,
-            defaults = 25.0,
-            type = Float,
-        ) .* (dt / basetimestep)
-
-    nonpaddy = NonPaddy{Float}(
-        demand_gross = fill(mv, length(inds)),
-        maximum_irrigation_rate = max_irri_rate,
-        irrigation_efficiency = efficiency,
-        irrigation_areas = areas,
-        irrigation_trigger = irrigation_trigger,
-    )
-
-    return nonpaddy
-end
-
-"Initialize water allocation for the river domain"
-function initialize_allocation_river(n)
-    allocation = AllocationRiver(
-        act_surfacewater_abst = zeros(Float, n),
-        act_surfacewater_abst_vol = zeros(Float, n),
-        available_surfacewater = zeros(Float, n),
-        nonirri_returnflow = zeros(Float, n),
-    )
-    return allocation
-end
-
-"Initialize water allocation for the land domain (`vertical`)"
-function initialize_allocation_land(nc, config, inds)
-    frac_sw_used = ncread(
-        nc,
-        config,
-        "vertical.allocation.frac_sw_used";
-        sel = inds,
-        defaults = 1,
-        type = Float,
-    )
-    areas = ncread(
-        nc,
-        config,
-        "vertical.allocation.areas";
-        sel = inds,
-        defaults = 1,
-        type = Int,
-    )
-
-    n = length(inds)
-
-    allocation = AllocationLand(
-        irri_demand_gross = zeros(Float, n),
-        nonirri_demand_gross = zeros(Float, n),
-        total_gross_demand = zeros(Float, n),
-        frac_sw_used = frac_sw_used,
-        areas = areas,
-        surfacewater_demand = zeros(Float, n),
-        surfacewater_alloc = zeros(Float, n),
-        act_groundwater_abst = zeros(Float, n),
-        act_groundwater_abst_vol = zeros(Float, n),
-        available_groundwater = zeros(Float, n),
-        groundwater_demand = zeros(Float, n),
-        groundwater_alloc = zeros(Float, n),
-        irri_alloc = zeros(Float, n),
-        nonirri_alloc = zeros(Float, n),
-        total_alloc = zeros(Float, n),
-        nonirri_returnflow = zeros(Float, n),
-    )
-    return allocation
-end
-
-"Return non-irrigation gross demand and update returnflow fraction"
-function update_non_irrigation_demand(non_irri::NonIrrigationDemand, i)
-    non_irri.returnflow_fraction[i] =
-        returnflow_fraction(non_irri.demand_gross[i], non_irri.demand_net[i])
-    return non_irri.demand_gross[i]
-end
-
-# return zero (gross demand) if non-irrigation sector is not defined
-update_non_irrigation_demand(non_irri::Nothing, i) = 0.0
-
-"Update water allocation for river and land domains based on local surface water (river) availability."
-function surface_water_allocation_local(land, river, network)
-    # maps from the land domain to the internal river domain (linear index), excluding water bodies
-    index_river = network.land.index_river_wb
-    for i in eachindex(land.allocation.surfacewater_demand)
-        if index_river[i] > 0.0
-            # the available volume is limited by a fixed scaling factor of 0.8 to prevent
-            # rivers completely drying out. check for abstraction through inflow (external
-            # negative inflow) and adjust available volume.
-            if river.inflow[index_river[i]] < 0.0
-                inflow = river.inflow[index_river[i]] * land.dt
-                available_volume = max(river.volume[index_river[i]] * 0.80 + inflow, 0.0)
-            else
-                available_volume = river.volume[index_river[i]] * 0.80
-            end
-            # satisfy surface water demand with available local river volume
-            surfacewater_demand_vol =
-                land.allocation.surfacewater_demand[i] * 0.001 * network.land.area[i]
-            abstraction_vol = min(surfacewater_demand_vol, available_volume)
-            river.allocation.act_surfacewater_abst_vol[index_river[i]] = abstraction_vol
-            # remaining available surface water and demand 
-            river.allocation.available_surfacewater[index_river[i]] =
-                max(available_volume - abstraction_vol, 0.0)
-            abstraction = (abstraction_vol / network.land.area[i]) * 1000.0
-            land.allocation.surfacewater_demand[i] =
-                max(land.allocation.surfacewater_demand[i] - abstraction, 0.0)
-            # update actual abstraction from river and surface water allocation (land cell)
-            river.allocation.act_surfacewater_abst[index_river[i]] = abstraction
-            land.allocation.surfacewater_alloc[i] = abstraction
-        end
-    end
-end
-
-"Update water allocation for river and land domains based on surface water (river) availability for allocation areas."
-function surface_water_allocation_area(land, river, network)
-    inds_river = network.river.indices_allocation_areas
-    inds_land = network.land.indices_allocation_areas
-    res_index = network.river.reservoir_index
-    lake_index = network.river.lake_index
-
-    m = length(inds_river)
-    # loop over allocation areas
-    for i = 1:m
-        # surface water demand (allocation area)
-        sw_demand_vol = 0.0
-        for j in inds_land[i]
-            sw_demand_vol +=
-                land.allocation.surfacewater_demand[j] * 0.001 * network.land.area[j]
-        end
-        # surface water availability (allocation area)
-        sw_available = 0.0
-        for j in inds_river[i]
-            # for reservoir locations use reservoir volume
-            if res_index[j] > 0
-                k = res_index[j]
-                river.allocation.available_surfacewater[j] =
-                    river.reservoir.volume[k] * 0.98 # limit available reservoir volume
-                sw_available += river.allocation.available_surfacewater[j]
-                # for lake locations use lake volume
-            elseif lake_index[j] > 0
-                k = lake_index[j]
-                river.allocation.available_surfacewater[j] = river.lake.storage[k] * 0.98 # limit available lake volume
-                sw_available += river.allocation.available_surfacewater[j]
-                # river volume
-            else
-                sw_available += river.allocation.available_surfacewater[j]
-            end
-        end
-        # total actual surface water abstraction [m3] in an allocation area, minimum of
-        # available surface water and demand in an allocation area.
-        sw_abstraction = min(sw_available, sw_demand_vol)
-
-        # fraction of available surface water that can be abstracted at allocation area
-        # level
-        frac_abstract_sw = bounded_divide(sw_abstraction, sw_available)
-        # fraction of water demand that can be satisfied by available surface water at
-        # allocation area level. 
-        frac_allocate_sw = bounded_divide(sw_abstraction, sw_demand_vol)
-
-        # water abstracted from surface water at each river cell (including reservoir and
-        # lake locations).
-        for j in inds_river[i]
-            river.allocation.act_surfacewater_abst_vol[j] +=
-                frac_abstract_sw * river.allocation.available_surfacewater[j]
-            river.allocation.act_surfacewater_abst[j] =
-                (river.allocation.act_surfacewater_abst_vol[j] / network.river.area[j]) *
-                1000.0
-        end
-
-        # water allocated to each land cell.
-        for j in inds_land[i]
-            land.allocation.surfacewater_alloc[j] +=
-                frac_allocate_sw * land.allocation.surfacewater_demand[j]
-        end
-    end
-end
-
-"Update water allocation for subsurface domain based on local groundwater availability."
-function groundwater_allocation_local(land, groundwater_volume, network)
-    for i in eachindex(land.allocation.groundwater_demand)
-        # groundwater demand based on allocation from surface water.
-        land.allocation.groundwater_demand[i] = max(
-            land.allocation.total_gross_demand[i] - land.allocation.surfacewater_alloc[i],
-            0.0,
-        )
-        # land index excluding water bodies
-        if network.index_wb[i]
-            # satisfy groundwater demand with available local groundwater volume
-            groundwater_demand_vol =
-                land.allocation.groundwater_demand[i] * 0.001 * network.area[i]
-            available_volume = groundwater_volume[i] * 0.75 # limit available groundwater volume
-            abstraction_vol = min(groundwater_demand_vol, available_volume)
-            land.allocation.act_groundwater_abst_vol[i] = abstraction_vol
-            # remaining available groundwater and demand 
-            land.allocation.available_groundwater[i] =
-                max(available_volume - abstraction_vol, 0.0)
-            abstraction = (abstraction_vol / network.area[i]) * 1000.0
-            land.allocation.groundwater_demand[i] =
-                max(land.allocation.groundwater_demand[i] - abstraction, 0.0)
-            # update actual abstraction from groundwater and groundwater allocation (land cell)
-            land.allocation.act_groundwater_abst[i] = abstraction
-            land.allocation.groundwater_alloc[i] = abstraction
-        end
-    end
-end
-
-"Update water allocation for subsurface domain based on groundwater availability for allocation areas."
-function groundwater_allocation_area(land, network)
-    inds_river = network.river.indices_allocation_areas
-    inds_land = network.land.indices_allocation_areas
-    m = length(inds_river)
-    # loop over allocation areas
-    for i = 1:m
-        # groundwater demand and availability (allocation area)
-        gw_demand_vol = 0.0
-        gw_available = 0.0
-        for j in inds_land[i]
-            gw_demand_vol +=
-                land.allocation.groundwater_demand[j] * 0.001 * network.land.area[j]
-            gw_available += land.allocation.available_groundwater[j]
-        end
-        # total actual groundwater abstraction [m3] in an allocation area, minimum of
-        # available  groundwater and demand in an allocation area.
-        gw_abstraction = min(gw_available, gw_demand_vol)
-
-        # fraction of available groundwater that can be abstracted at allocation area level
-        frac_abstract_gw = bounded_divide(gw_abstraction, gw_available)
-        # fraction of water demand that can be satisfied by available groundwater at
-        # allocation area level.
-        frac_allocate_gw = bounded_divide(gw_abstraction, gw_demand_vol)
-
-        # water abstracted from groundwater and allocated.
-        for j in inds_land[i]
-            land.allocation.act_groundwater_abst_vol[j] +=
-                frac_abstract_gw * land.allocation.available_groundwater[j]
-            land.allocation.act_groundwater_abst[j] =
-                1000.0 *
-                (land.allocation.act_groundwater_abst_vol[j] / network.land.area[j])
-            land.allocation.groundwater_alloc[j] +=
-                frac_allocate_gw * land.allocation.groundwater_demand[j]
-        end
-    end
-end
-
-"Return and update non-irrigation sector (domestic, livestock, industry) return flow"
-function return_flow(non_irri::NonIrrigationDemand, allocation)
-    for i in eachindex(non_irri.returnflow)
-        frac = bounded_divide(non_irri.demand_gross[i], allocation.nonirri_demand_gross[i])
-        allocate = frac * allocation.nonirri_alloc[i]
-        non_irri.returnflow[i] = non_irri.returnflow_fraction[i] * allocate
-    end
-    return non_irri.returnflow
-end
-
-# return zero (return flow) if non-irrigation sector is not defined
-return_flow(non_irri::Nothing, allocation) = 0.0
-
-groundwater_volume(gw::LateralSSF) = gw.volume
-groundwater_volume(gw) = gw.flow.aquifer.volume
-
-"""
-    update_water_allocation(model::Model{N,L,V,R,W,T}) where {N,L,V,R,W,T<:Union{SbmModel, SbmGwfModel}}
-
-Update water allocation for model type `sbm` or `sbm_gwf` for a single timestep. First,
-surface water abstraction is computed to satisfy local water demand (non-irrigation and
-irrigation), and then updated (including lakes and reservoirs) to satisfy the remaining
-water demand for allocation areas. Then groundwater abstraction is computed to satisfy the
-remaining local water demand, and then updated to satisfy the remaining water demand for
-allocation areas. Finally, non-irrigation return flows are updated.
-"""
-function update_water_allocation(
-    model::Model{N,L,V,R,W,T},
-) where {N,L,V,R,W,T<:Union{SbmModel,SbmGwfModel}}
-
-    (; network, lateral, vertical) = model
-
-    river = lateral.river
-    index_river = network.land.index_river_wb
-    res_index_f = network.river.reservoir_index_f
-    lake_index_f = network.river.lake_index_f
-
-    vertical.allocation.surfacewater_alloc .= 0.0
-    river.allocation.act_surfacewater_abst .= 0.0
-    river.allocation.act_surfacewater_abst_vol .= 0.0
-    # total surface water demand for each land cell
-    @. vertical.allocation.surfacewater_demand =
-        vertical.allocation.frac_sw_used * vertical.allocation.nonirri_demand_gross +
-        vertical.allocation.frac_sw_used * vertical.allocation.irri_demand_gross
-
-    # local surface water demand and allocation (river, excluding reservoirs and lakes)
-    surface_water_allocation_local(vertical, river, network)
-    # surface water demand and allocation for areas
-    surface_water_allocation_area(vertical, river, network)
-
-    @. river.abstraction = river.allocation.act_surfacewater_abst_vol / vertical.dt
-
-    # for reservoir and lake locations set river abstraction at zero and abstract volume
-    # from reservoir and lake, including an update of lake waterlevel
-    if !isnothing(river.reservoir)
-        @. river.abstraction[res_index_f] = 0.0
-        @. river.reservoir.volume -= river.allocation.act_surfacewater_abst_vol[res_index_f]
-    elseif !isnothing(river.lake)
-        @. river.abstraction[lake_index_f] = 0.0
-        lakes = river.lake
-        @. lakes.storage -= river.allocation.act_surfacewater_abst_vol[lake_index_f]
-        @. lakes.waterlevel =
-            waterlevel(lakes.storfunc, lakes.area, lakes.storage, lakes.sh)
-    end
-
-    vertical.allocation.groundwater_alloc .= 0.0
-    vertical.allocation.act_groundwater_abst_vol .= 0.0
-    vertical.allocation.act_groundwater_abst .= 0.0
-    # local groundwater demand and allocation
-    groundwater_allocation_local(
-        vertical,
-        groundwater_volume(lateral.subsurface),
-        network.land,
-    )
-    # groundwater demand and allocation for areas
-    groundwater_allocation_area(vertical, network)
-
-    # irrigation allocation
-    for i in eachindex(vertical.allocation.total_alloc)
-        vertical.allocation.total_alloc[i] =
-            vertical.allocation.groundwater_alloc[i] +
-            vertical.allocation.surfacewater_alloc[i]
-        frac_irri = bounded_divide(
-            vertical.allocation.irri_demand_gross[i],
-            vertical.allocation.total_gross_demand[i],
-        )
-        vertical.allocation.irri_alloc[i] = frac_irri * vertical.allocation.total_alloc[i]
-        vertical.allocation.nonirri_alloc[i] =
-            vertical.allocation.total_alloc[i] - vertical.allocation.irri_alloc[i]
-    end
-
-    # non-irrigation return flows
-    returnflow_livestock = return_flow(vertical.livestock, vertical.allocation)
-    returnflow_domestic = return_flow(vertical.domestic, vertical.allocation)
-    returnflow_industry = return_flow(vertical.industry, vertical.allocation)
-
-    # map non-irrigation return flow to land and river water allocation
-    if (
-        !isnothing(vertical.livestock) ||
-        !isnothing(vertical.domestic) ||
-        !isnothing(vertical.industry)
-    )
-        @. vertical.allocation.nonirri_returnflow =
-            returnflow_livestock + returnflow_domestic + returnflow_industry
-
-        for i in eachindex(vertical.allocation.nonirri_returnflow)
-            if index_river[i] > 0.0
-                k = index_river[i]
-                river.allocation.nonirri_returnflow[k] =
-                    vertical.allocation.nonirri_returnflow[i]
-                vertical.allocation.nonirri_returnflow[i] = 0.0
-            else
-                vertical.allocation.nonirri_returnflow[i] =
-                    vertical.allocation.nonirri_returnflow[i]
-            end
-        end
-    end
-end


Test patch:
diff --git a/build/create_binaries/download_test_data.jl b/build/create_binaries/download_test_data.jl
index 3ff5f5437..339e19b91 100644
--- a/build/create_binaries/download_test_data.jl
+++ b/build/create_binaries/download_test_data.jl
@@ -19,12 +19,8 @@ end
 
 staticmaps_rhine_path = testdata(v"0.1", "staticmaps.nc", "staticmaps-rhine.nc")
 staticmaps_moselle_path =
-    testdata(v"0.2.8", "staticmaps-moselle.nc", "staticmaps-moselle.nc")
-staticmaps_lahn_path = testdata(v"0.2.1", "staticmaps-lahn.nc", "staticmaps-lahn.nc")
-staticmaps_meuse_path =
-    testdata(v"0.2.8", "staticmaps_flex_meuse.nc", "staticmaps_flex_meuse.nc")
+    testdata(v"0.2.9", "staticmaps-moselle.nc", "staticmaps-moselle.nc")
 forcing_moselle_path = testdata(v"0.2.6", "forcing-moselle.nc", "forcing-moselle.nc")
-forcing_lahn_path = testdata(v"0.2", "forcing-lahn.nc", "forcing-lahn.nc")
 forcing_moselle_sed_path =
     testdata(v"0.2.3", "forcing-moselle-sed.nc", "forcing-moselle-sed.nc")
 staticmaps_moselle_sed_path =
@@ -42,7 +38,6 @@ forcing_sbm_gw_path = testdata(
     "forcing-sbm-groundwater-part2.nc",
     "forcing-sbm-groundwater-part2.nc",
 )
-forcing_meuse_path = testdata(v"0.2.8", "forcing_meuse.nc", "forcing_meuse.nc")
 staticmaps_sbm_gw_path =
     testdata(v"0.2.3", "staticmaps-sbm-groundwater.nc", "staticmaps-sbm-groundwater.nc")
 instates_sbm_gw_path =
diff --git a/build/wflow_cli/test/runtests.jl b/build/wflow_cli/test/runtests.jl
index b68f71f6d..aada6ffb5 100644
--- a/build/wflow_cli/test/runtests.jl
+++ b/build/wflow_cli/test/runtests.jl
@@ -2,7 +2,8 @@ using Test
 using Wflow
 
 extension = Sys.iswindows() ? ".exe" : ""
-wflow_exe = normpath(@__DIR__, "../../create_binaries/wflow_bundle/bin/wflow_cli" * extension)
+wflow_exe =
+    normpath(@__DIR__, "../../create_binaries/wflow_bundle/bin/wflow_cli" * extension)
 
 # this assumes that the Wflow tests have already been run, so the data has been downloaded
 testdir = abspath(dirname(pathof(Wflow)), "..", "test")
@@ -14,7 +15,7 @@ outputdir = joinpath(datadir, "output")
 
 @testset "no_config" begin
     # Clean output directory
-    rm(outputdir; force=true, recursive=true)
+    rm(outputdir; force = true, recursive = true)
     @test_throws ProcessFailedException run(`$wflow_exe`)
     # Check if no files are being created
     @test !(isdir(outputdir))
@@ -22,7 +23,7 @@ end
 
 @testset "wflow_sbm" begin
     # Clean directory
-    rm(outputdir; force=true, recursive=true)
+    rm(outputdir; force = true, recursive = true)
     # Run cli with the toml
     toml = normpath(testdir, "sbm_config.toml")
     run(`$wflow_exe $toml`)
@@ -43,7 +44,7 @@ end
 
 @testset "wflow_sbm-gwf" begin
     # Clean directory
-    rm(outputdir; force=true, recursive=true)
+    rm(outputdir; force = true, recursive = true)
     # Run cli with the toml
     toml = normpath(testdir, "sbm_gwf_config.toml")
     run(`$wflow_exe $toml`)
@@ -63,7 +64,7 @@ end
 
 @testset "wflow_sediment" begin
     # Clean directory
-    rm(outputdir; force=true, recursive=true)
+    rm(outputdir; force = true, recursive = true)
     # Run cli with the toml
     toml = normpath(testdir, "sediment_config.toml")
     run(`$wflow_exe $toml`)
@@ -82,11 +83,12 @@ end
 end
 
 @testset "wflow_sbm_timing" begin
-
     toml = normpath(testdir, "sbm_config.toml")
-    time_sbm_1thread = @elapsed run(Cmd(`$wflow_exe $toml`, env=("JULIA_NUM_THREADS" => "1",)))
+    time_sbm_1thread =
+        @elapsed run(Cmd(`$wflow_exe $toml`; env = ("JULIA_NUM_THREADS" => "1",)))
 
-    time_sbm_4thread = @elapsed run(Cmd(`$wflow_exe $toml`, env=("JULIA_NUM_THREADS" => "4", )))
+    time_sbm_4thread =
+        @elapsed run(Cmd(`$wflow_exe $toml`; env = ("JULIA_NUM_THREADS" => "4",)))
 
     # Test if run with more threads is indeed faster
     @test time_sbm_4thread < time_sbm_1thread
diff --git a/server/test/client.jl b/server/test/client.jl
index 36e6b5dc5..1688af26f 100644
--- a/server/test/client.jl
+++ b/server/test/client.jl
@@ -32,11 +32,11 @@ end
 
 @testset "model information functions" begin
     @test request((fn = "get_component_name",)) == Dict("component_name" => "sbm")
-    @test request((fn = "get_input_item_count",)) == Dict("input_item_count" => 207)
-    @test request((fn = "get_output_item_count",)) == Dict("output_item_count" => 207)
+    @test request((fn = "get_input_item_count",)) == Dict("input_item_count" => 203)
+    @test request((fn = "get_output_item_count",)) == Dict("output_item_count" => 203)
     to_check = [
-        "vertical.nlayers",
-        "vertical.theta_r",
+        "vertical.soil.parameters.nlayers",
+        "vertical.soil.parameters.theta_r",
         "lateral.river.q",
         "lateral.river.reservoir.outflow",
     ]
@@ -52,22 +52,26 @@ vwc_1_size = 0
     @test request((fn = "get_var_itemsize", name = "lateral.subsurface.ssf")) ==
           Dict("var_itemsize" => sizeof(Wflow.Float))
     @test request((fn = "get_var_type", name = "vertical.n"))["status"] == "ERROR"
-    @test request((fn = "get_var_units", name = "vertical.theta_s")) == Dict("var_units" => "-")
+    @test request((fn = "get_var_units", name = "vertical.soil.parameters.theta_s")) ==
+          Dict("var_units" => "-")
     @test request((fn = "get_var_location", name = "lateral.river.q")) ==
           Dict("var_location" => "node")
-    zi_nbytes = request((fn = "get_var_nbytes", name = "vertical.zi"))["var_nbytes"]
+    zi_nbytes =
+        request((fn = "get_var_nbytes", name = "vertical.soil.variables.zi"))["var_nbytes"]
     @test zi_nbytes == 400504
-    zi_itemsize = request((fn = "get_var_itemsize", name = "vertical.zi"))["var_itemsize"]
+    zi_itemsize =
+        request((fn = "get_var_itemsize", name = "vertical.soil.variables.zi"))["var_itemsize"]
     zi_size = Int(zi_nbytes / zi_itemsize)
-    vwc_1_nbytes = request((fn = "get_var_nbytes", name = "vertical.vwc[1]"))["var_nbytes"]
+    vwc_1_nbytes =
+        request((fn = "get_var_nbytes", name = "vertical.soil.variables.vwc[1]"))["var_nbytes"]
     @test vwc_1_nbytes == 400504
     vwc_1_itemsize =
-        request((fn = "get_var_itemsize", name = "vertical.vwc[1]"))["var_itemsize"]
+        request((fn = "get_var_itemsize", name = "vertical.soil.variables.vwc[1]"))["var_itemsize"]
     vwc_1_size = Int(vwc_1_nbytes / vwc_1_itemsize)
     @test request((fn = "get_var_grid", name = "lateral.river.h")) == Dict("var_grid" => 3)
-    msg = (fn = "get_value", name = "vertical.zi", dest = fill(0.0, zi_size))
+    msg = (fn = "get_value", name = "vertical.soil.variables.zi", dest = fill(0.0, zi_size))
     @test mean(request(msg)["value"]) ≈ 277.3620724821974
-    msg = (fn = "get_value_ptr", name = "vertical.theta_s")
+    msg = (fn = "get_value_ptr", name = "vertical.soil.parameters.theta_s")
     @test mean(request(msg)["value_ptr"]) ≈ 0.4409211971535584
     msg = (
         fn = "get_value_at_indices",
@@ -77,55 +81,68 @@ vwc_1_size = 0
     )
     @test request(msg)["value_at_indices"] ≈
           [2.198747900215207f0, 2.6880427720508515f0, 3.4848783702629564f0]
-    msg = (fn = "set_value", name = "vertical.zi", src = fill(300.0, zi_size))
+    msg =
+        (fn = "set_value", name = "vertical.soil.variables.zi", src = fill(300.0, zi_size))
     @test request(msg) == Dict("status" => "OK")
-    msg = (fn = "get_value", name = "vertical.zi", dest = fill(0.0, zi_size))
+    msg = (fn = "get_value", name = "vertical.soil.variables.zi", dest = fill(0.0, zi_size))
     @test mean(request(msg)["value"]) == 300.0
     msg = (
         fn = "set_value_at_indices",
-        name = "vertical.zi",
+        name = "vertical.soil.variables.zi",
         src = [250.0, 350.0],
         inds = [1, 2],
     )
     @test request(msg) == Dict("status" => "OK")
     msg = (
         fn = "get_value_at_indices",
-        name = "vertical.zi",
+        name = "vertical.soil.variables.zi",
         dest = [0.0, 0.0, 0.0],
         inds = [1, 2, 3],
     )
     @test request(msg)["value_at_indices"] == [250.0, 350.0, 300.0]
-    msg = (fn = "get_value", name = "vertical.vwc[1]", dest = fill(0.0, vwc_1_size))
+    msg = (
+        fn = "get_value",
+        name = "vertical.soil.variables.vwc[1]",
+        dest = fill(0.0, vwc_1_size),
+    )
     @test mean(request(msg)["value"]) ≈ 0.18600013563085036f0
     msg = (
         fn = "get_value_at_indices",
-        name = "vertical.vwc[1]",
+        name = "vertical.soil.variables.vwc[1]",
         dest = [0.0, 0.0, 0.0],
         inds = [1, 2, 3],
     )
     @test request(msg)["value_at_indices"] ≈
           [0.12089607119560242f0, 0.11968416924304527f0, 0.14602328618707333f0]
-    msg = (fn = "set_value", name = "vertical.vwc[1]", src = fill(0.3, vwc_1_size))
+    msg = (
+        fn = "set_value",
+        name = "vertical.soil.variables.vwc[1]",
+        src = fill(0.3, vwc_1_size),
+    )
     @test request(msg) == Dict("status" => "OK")
-    msg = (fn = "get_value", name = "vertical.vwc[1]", dest = fill(0.0, vwc_1_size))
+    msg = (
+        fn = "get_value",
+        name = "vertical.soil.variables.vwc[1]",
+        dest = fill(0.0, vwc_1_size),
+    )
     @test mean(request(msg)["value"]) ≈ 0.3f0
     msg = (
         fn = "get_value_at_indices",
-        name = "vertical.vwc[1]",
+        name = "vertical.soil.variables.vwc[1]",
         dest = [0.0, 0.0, 0.0],
         inds = [1, 2, 3],
     )
     @test request(msg)["value_at_indices"] == [0.3, 0.3, 0.3]
     msg = (
         fn = "set_value_at_indices",
-        name = "vertical.vwc[1]",
+        name = "vertical.soil.variables.vwc[1]",
         src = [0.1, 0.25],
         inds = [1, 2],
     )
     @test request(msg) == Dict("status" => "OK")
     msg = (
         fn = "get_value_at_indices",
-        name = "vertical.vwc[1]",
+        name = "vertical.soil.variables.vwc[1]",
         dest = [0.0, 0.0],
         inds = [1, 2],
     )
diff --git a/server/test/runtests.jl b/server/test/runtests.jl
index 8d016a2b6..bb37c0a4f 100644
--- a/server/test/runtests.jl
+++ b/server/test/runtests.jl
@@ -1,12 +1,12 @@
-import ZMQ
-import JSON3
-import StructTypes
-import Wflow
-import WflowServer
+using ZMQ: ZMQ
+using JSON3: JSON3
+using StructTypes: StructTypes
+using Wflow: Wflow
+using WflowServer: WflowServer
 import Statistics: mean
 import Logging: with_logger, NullLogger
 import Test: @testset, @test
-import Downloads
+using Downloads: Downloads
 
 # ensure test data is present
 testdir = @__DIR__
diff --git a/server/test/sbm_config.toml b/server/test/sbm_config.toml
index 3ab5c92d0..39898a488 100644
--- a/server/test/sbm_config.toml
+++ b/server/test/sbm_config.toml
@@ -19,14 +19,18 @@ path_output = "outstates-moselle.nc"
 # if listed, the variable must be present in the NetCDF or error
 # if not listed, the variable can get a default value if it has one
 
-[state.vertical]
-canopystorage = "canopystorage"
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[state.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [state.lateral.river]
 h = "h_river"
 h_av = "h_av_river"
@@ -56,42 +60,52 @@ subcatchment = "wflow_subcatch"
 # specify the internal IDs of the parameters which vary over time
 # the external name mapping needs to be below together with the other mappings
 forcing = [
-  "vertical.precipitation",
-  "vertical.temperature",
-  "vertical.potential_evaporation",
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
 ]
 
-cyclic = ["vertical.leaf_area_index"]
+cyclic = ["vertical.vegetation_parameter_set.leaf_area_index"]
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
 
-[input.vertical]
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.soil.parameters]
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
 f = "f"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-leaf_area_index = "LAI"
+water_holding_capacity = "WHC"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_r = "thetaR"
-theta_s = "thetaS"
+cfmax = "Cfmax"
 
-[input.vertical.kv_0]
+[input.vertical.soil.parameters.kv_0]
 netcdf.variable.name = "KsatVer"
 scale = 1.0
 offset = 0.0
@@ -135,14 +149,18 @@ min_streamorder_land = 5
 [output]
 path = "output_moselle.nc"
 
-[output.vertical]
-canopystorage = "canopystorage"
+[output.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[output.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[output.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [output.lateral.river]
 h = "h_river"
 q = "q_river"
@@ -170,14 +188,14 @@ coordinate.x = 6.255
 coordinate.y = 50.012
 name = "temp_coord"
 location = "temp_bycoord"
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[netcdf.variable]]
 location = "temp_byindex"
 name = "temp_index"
 index.x = 100
 index.y = 264
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [csv]
 path = "output_moselle.csv"
@@ -196,20 +214,20 @@ parameter = "lateral.river.reservoir.volume"
 coordinate.x = 6.255
 coordinate.y = 50.012
 header = "temp_bycoord"
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[csv.column]]
 coordinate.x = 6.255
 coordinate.y = 50.012
 header = "vwc_layer2_bycoord"
-parameter = "vertical.vwc"
+parameter = "vertical.soil.variables.vwc"
 layer = 2
 
 [[csv.column]]
 header = "temp_byindex"
 index.x = 100
 index.y = 264
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[csv.column]]
 header = "Q"
@@ -219,12 +237,24 @@ parameter = "lateral.river.q"
 [[csv.column]]
 header = "recharge"
 map = "subcatchment"
-parameter = "vertical.recharge"
+parameter = "vertical.soil.variables.recharge"
 reducer = "mean"
 
 [API]
 components = [
-  "vertical",
+  "vertical.atmospheric_forcing",
+  "vertical.vegetation_parameter_set",
+  "vertical.runoff.boundary_conditions",
+  "vertical.runoff.variables",
+  "vertical.runoff.parameters",
+  "vertical.soil.boundary_conditions",
+  "vertical.soil.variables",
+  "vertical.soil.parameters",
+  "vertical.interception.variables",
+  "vertical.interception.parameters",
+  "vertical.snow.boundary_conditions",
+  "vertical.snow.variables",
+  "vertical.snow.parameters",
   "lateral.subsurface",
   "lateral.land",
   "lateral.river",
diff --git a/test/bmi.jl b/test/bmi.jl
index ba0f3751b..23d3c15a6 100644
--- a/test/bmi.jl
+++ b/test/bmi.jl
@@ -1,11 +1,8 @@
 
 tomlpath = joinpath(@__DIR__, "sbm_config.toml")
 
-
 @testset "BMI" begin
-
     @testset "BMI functions" begin
-
         model = BMI.initialize(Wflow.Model, tomlpath)
 
         @testset "initialization and time functions" begin
@@ -20,11 +17,11 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
 
         @testset "model information functions" begin
             @test BMI.get_component_name(model) == "sbm"
-            @test BMI.get_input_item_count(model) == 207
-            @test BMI.get_output_item_count(model) == 207
+            @test BMI.get_input_item_count(model) == 203
+            @test BMI.get_output_item_count(model) == 203
             to_check = [
-                "vertical.nlayers",
-                "vertical.theta_r",
+                "vertical.soil.parameters.nlayers",
+                "vertical.soil.parameters.theta_r",
                 "lateral.river.q",
                 "lateral.river.reservoir.outflow",
             ]
@@ -35,12 +32,12 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
         end
 
         @testset "variable information functions" begin
-            @test BMI.get_var_grid(model, "vertical.theta_s") == 6
+            @test BMI.get_var_grid(model, "vertical.soil.parameters.theta_s") == 6
             @test BMI.get_var_grid(model, "lateral.river.h") == 3
             @test BMI.get_var_grid(model, "lateral.river.reservoir.inflow") == 0
             @test_throws ErrorException BMI.get_var_grid(model, "lateral.river.lake.volume")
             @test BMI.get_var_type(model, "lateral.river.reservoir.inflow") == "$Float"
-            @test BMI.get_var_units(model, "vertical.theta_s") == "-"
+            @test BMI.get_var_units(model, "vertical.soil.parameters.theta_s") == "-"
             @test BMI.get_var_itemsize(model, "lateral.subsurface.ssf") == sizeof(Float)
             @test BMI.get_var_nbytes(model, "lateral.river.q") ==
                   length(model.lateral.river.q) * sizeof(Float)
@@ -55,34 +52,46 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
         @testset "update and get and set functions" begin
             @test BMI.get_current_time(model) == 86400.0
             @test_throws ErrorException BMI.get_value_ptr(model, "vertical.")
-            dest = zeros(Float, size(model.vertical.zi))
-            BMI.get_value(model, "vertical.zi", dest)
+            dest = zeros(Float, size(model.vertical.soil.variables.zi))
+            BMI.get_value(model, "vertical.soil.variables.zi", dest)
             @test mean(dest) ≈ 276.1625022866973
             @test BMI.get_value_at_indices(
                 model,
-                "vertical.vwc[1]",
+                "vertical.soil.variables.vwc[1]",
                 zeros(Float, 3),
                 [1, 2, 3],
-            ) ≈ getindex.(model.vertical.vwc, 1)[1:3]
+            ) ≈ getindex.(model.vertical.soil.variables.vwc, 1)[1:3]
             BMI.set_value_at_indices(
                 model,
-                "vertical.vwc[2]",
+                "vertical.soil.variables.vwc[2]",
                 [1, 2, 3],
                 [0.10, 0.15, 0.20],
-            ) ≈ getindex.(model.vertical.vwc, 2)[1:3]
+            ) ≈ getindex.(model.vertical.soil.variables.vwc, 2)[1:3]
             @test BMI.get_value_at_indices(
                 model,
                 "lateral.river.q",
                 zeros(Float, 3),
                 [1, 100, 5617],
             ) ≈ [0.623325399343309, 5.227139951657074, 0.027942874327781947]
-            BMI.set_value(model, "vertical.zi", fill(300.0, length(model.vertical.zi)))
+            BMI.set_value(
+                model,
+                "vertical.soil.variables.zi",
+                fill(300.0, length(model.vertical.soil.variables.zi)),
+            )
             @test mean(
-                BMI.get_value(model, "vertical.zi", zeros(Float, size(model.vertical.zi))),
+                BMI.get_value(
+                    model,
+                    "vertical.soil.variables.zi",
+                    zeros(Float, size(model.vertical.soil.variables.zi)),
+                ),
             ) == 300.0
-            BMI.set_value_at_indices(model, "vertical.zi", [1], [250.0])
-            @test BMI.get_value_at_indices(model, "vertical.zi", zeros(Float, 2), [1, 2]) ==
-                  [250.0, 300.0]
+            BMI.set_value_at_indices(model, "vertical.soil.variables.zi", [1], [250.0])
+            @test BMI.get_value_at_indices(
+                model,
+                "vertical.soil.variables.zi",
+                zeros(Float, 2),
+                [1, 2],
+            ) == [250.0, 300.0]
         end
 
         @testset "model grid functions" begin
@@ -125,7 +134,6 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
                 BMI.update_until(model, time - BMI.get_time_step(model))
             BMI.finalize(model)
         end
-
     end
 
     @testset "BMI grid edges" begin
@@ -150,15 +158,15 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
         model = BMI.initialize(Wflow.Model, tomlpath)
 
         # update the recharge part of the SBM model
-        model = BMI.update(model, run = "sbm_until_recharge")
+        model = BMI.update(model; run = "sbm_until_recharge")
 
         @testset "recharge part of SBM" begin
             sbm = model.vertical
-            @test sbm.interception[1] ≈ 0.32734913737568716f0
-            @test sbm.ustorelayerdepth[1][1] ≈ 0.0f0
-            @test sbm.snow[1] ≈ 3.4847899611762876f0
-            @test sbm.recharge[5] ≈ 0.0f0
-            @test sbm.zi[5] ≈ 300.0f0
+            @test sbm.interception.variables.interception_rate[1] ≈ 0.32734913737568716f0
+            @test sbm.soil.variables.ustorelayerdepth[1][1] ≈ 0.0f0
+            @test sbm.snow.variables.snow_storage[1] ≈ 3.4847899611762876f0
+            @test sbm.soil.variables.recharge[5] ≈ 0.0f0
+            @test sbm.soil.variables.zi[5] ≈ 300.0f0
         end
 
         # set zi and exfiltwater from external source (e.g. a groundwater model)
@@ -173,16 +181,16 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
             fill(1.0e-5, BMI.get_grid_node_count(model, 6)),
         )
         # update SBM after subsurface flow
-        model = BMI.update(model, run = "sbm_after_subsurfaceflow")
+        model = BMI.update(model; run = "sbm_after_subsurfaceflow")
 
         @testset "SBM after subsurface flow" begin
             sbm = model.vertical
             sub = model.lateral.subsurface
-            @test sbm.interception[1] ≈ 0.32734913737568716f0
-            @test sbm.ustorelayerdepth[1][1] ≈ 0.0f0
-            @test sbm.snow[1] ≈ 3.4847899611762876f0
-            @test sbm.recharge[5] ≈ 0.0f0
-            @test sbm.zi[5] ≈ 250.0f0
+            @test sbm.interception.variables.interception_rate[1] ≈ 0.32734913737568716f0
+            @test sbm.soil.variables.ustorelayerdepth[1][1] ≈ 0.0f0
+            @test sbm.snow.variables.snow_storage[1] ≈ 3.4847899611762876f0
+            @test sbm.soil.variables.recharge[5] ≈ 0.0f0
+            @test sbm.soil.variables.zi[5] ≈ 250.0f0
             @test sub.zi[5] ≈ 0.25f0
             @test sub.exfiltwater[1] ≈ 1.0f-5
             @test sub.ssf[1] ≈ 0.0f0
@@ -190,17 +198,15 @@ tomlpath = joinpath(@__DIR__, "sbm_config.toml")
 
         BMI.finalize(model)
     end
-
 end
 
 @testset "BMI extension functions" begin
-
     model = BMI.initialize(Wflow.Model, tomlpath)
     @test Wflow.get_start_unix_time(model) == 9.466848e8
-    satwaterdepth = mean(model.vertical.satwaterdepth)
+    satwaterdepth = mean(model.vertical.soil.variables.satwaterdepth)
     model.config.model.reinit = false
     model = Wflow.load_state(model)
-    @test satwaterdepth ≠ mean(model.vertical.satwaterdepth)
+    @test satwaterdepth ≠ mean(model.vertical.soil.variables.satwaterdepth)
     @test_logs (
         :info,
         "Write output states to netCDF file `$(model.writer.state_nc_path)`.",
diff --git a/test/flextopo_config.toml b/test/flextopo_config.toml
deleted file mode 100644
index aba6659af..000000000
--- a/test/flextopo_config.toml
+++ /dev/null
@@ -1,523 +0,0 @@
-casename = "wflow_meuse"
-calendar = "proleptic_gregorian"
-starttime = "2009-12-31T00:00:00"
-endtime = "2010-07-01T00:00:00"
-time_units = "days since 1900-01-01 00:00:00"
-timestepsecs = 86400
-dir_input = "data/input"
-dir_output = "data/output"
-loglevel = "info"
-
-[state]
-path_input = "instates.nc"
-path_output = "outstates-meuse.nc"
-
-[input]
-path_forcing = "forcing_meuse.nc"
-path_static = "staticmaps_flex_meuse.nc"
-gauges = "wflow_gauges"
-ldd = "wflow_ldd"
-river_location = "wflow_river"
-subcatchment = "wflow_subcatch"
-forcing = [ "vertical.precipitation", "vertical.temperature", "vertical.potential_evaporation",]
-#cyclic = [ "vertical.leaf_area_index",]
-gauges_grdc = "wflow_gauges_grdc"
-gauges_S01 = "wflow_gauges_S01"
-gauges_S02 = "wflow_gauges_S02"
-gauges_S03 = "wflow_gauges_S03"
-gauges_S04 = "wflow_gauges_S04"
-gauges_S05 = "wflow_gauges_S05"
-gauges_S06 = "wflow_gauges_S06"
-gauges_Sall = "wflow_gauges_Sall"
-
-sub_S01 = "wflow_subcatch_S01"
-sub_S02 = "wflow_subcatch_S02"
-sub_S03 = "wflow_subcatch_S03"
-sub_S04 = "wflow_subcatch_S04"
-sub_S05 = "wflow_subcatch_S05"
-sub_S06 = "wflow_subcatch_S06"
-sub_Sall = "wflow_subcatch_Sall"
-
-[model]
-type = "flextopo"
-masswasting = true
-snow = true
-reinit = true
-reservoirs = false
-lakes = false
-glacier = false
-kin_wave_iteration = true
-kw_river_tstep = 900
-kw_land_tstep = 3600
-classes = ["h", "p", "w"]
-
-
-select_snow = ["common_snow_hbv"]
-select_interception = ["interception_overflow", "interception_overflow", "interception_overflow"]
-select_hortonponding = ["hortonponding_no_storage", "hortonponding_no_storage", "hortonponding_no_storage"]
-select_hortonrunoff = ["hortonrunoff_no_storage", "hortonrunoff_no_storage", "hortonrunoff_no_storage"]
-select_rootzone = ["rootzone_storage", "rootzone_storage", "rootzone_storage"]
-select_fast = ["fast_storage", "fast_storage", "fast_storage"]
-select_slow = ["common_slow_storage"]
-
-[input.vertical]
-altitude = "wflow_dem"
-potential_evaporation = "PET"
-precipitation = "P"
-temperature = "TEMP"
-tt = "tth"
-ttm = "tmh"
-cfmax = "fmh"
-whc = "WHC"
-#alfa = "alfa"
-beta = "beta"
-cap = "cap"
-#ds = "d"
-fdec = "decf"
-fmax = "fmax"
-imax = "imax"
-#kf = "kf"
-#ks = "ksh"
-#lp = "lp"
-perc = "perc"
-srmax = "sumax"
-hrufrac = "hrufrac_lu"
-
-[input.vertical.lp]
-netcdf.variable.name = "lp"
-scale = [0.4, 0.4, 0.4]
-offset = [0.0, 0.0, 0.0]
-class = ["h", "p", "w"]
-
-[input.vertical.ds]
-netcdf.variable.name = "d"
-scale = [1.2, 1.2, 1.0]
-offset = [0.0, 0.0, 0.0]
-class = ["h", "p", "w"]
-
-[input.vertical.kf]
-netcdf.variable.name = "kf"
-scale = [1.0, 3.0, 3.0]
-offset = [0.0, 0.0, 0.0]
-class = ["h", "p", "w"]
-
-[input.vertical.alfa]
-netcdf.variable.name = "alfa"
-scale = 1.3
-offset = 0
-class = "p"
-
-[input.vertical.ks]
-netcdf.variable.name = "ksh"
-scale = 0.5
-offset = 0.0
-
-
-[input.lateral.river]
-length = "wflow_riverlength"
-#n = "N_River"
-n = "n_river_uniform"
-slope = "RiverSlope"
-width = "wflow_riverwidth"
-bankfull_depth = "RiverDepth"
-
-[input.lateral.land]
-#n = "N"
-n = "n_uniform"
-slope = "Slope"
-
-[input.lateral.river.reservoir]
-area = "ResSimpleArea"
-areas = "wflow_reservoirareas"
-demand = "ResDemand"
-locs = "wflow_reservoirlocs"
-maxrelease = "ResMaxRelease"
-maxvolume = "ResMaxVolume"
-targetfullfrac = "ResTargetFullFrac"
-targetminfrac = "ResTargetMinFrac"
-
-[state.lateral.river.reservoir]
-#volume = "volume_reservoir"
-
-[state.vertical]
-snow = "snow"
-snowwater = "snowwater"
-interceptionstorage = "interceptionstorage"
-hortonpondingstorage = "hortonpondingstorage"
-hortonrunoffstorage = "hortonrunoffstorage"
-rootzonestorage = "rootzonestorage"
-faststorage = "faststorage"
-slowstorage = "slowstorage"
-
-
-[state.lateral.river]
-q = "q_river"
-h = "h_river"
-h_av = "h_av_river"
-
-
-[state.lateral.land]
-q = "q_land"
-h = "h_land"
-h_av = "h_av_land"
-
-
-[output]
-path = "output-flex-meuse.nc"
-
-[output.vertical]
-#precipitation = "prec"
-#temperature = "temp"
-#potential_evaporation = "pet"
-faststorage = "faststorage"
-
-
-[output.lateral.river]
-q_av = "q_river"
-#h = "h_river"
-
-
-[output.lateral.land]
-#q = "q_land"
-#h = "h_land"
-
-
-[csv]
-path = "output-flex-meuse.csv"
-
-[[csv.column]]
-header = "Q"
-map = "gauges"
-parameter = "lateral.river.q_av"
-
-[[csv.column]]
-header = "Q"
-map = "gauges_grdc"
-parameter = "lateral.river.q_av"
-
-
-[[csv.column]]
-header = "Q"
-map = "gauges_Sall"
-parameter = "lateral.river.q_av"
-
-[[csv.column]]
-header = "H"
-map = "gauges_Sall"
-parameter = "lateral.river.h_av"
-
-[[csv.column]]
-header = "P"
-map = "sub_S06"
-parameter = "vertical.precipitation"
-reducer = "mean"
-
-[[csv.column]]
-header = "Ep"
-map = "sub_S06"
-parameter = "vertical.potential_evaporation"
-reducer = "mean"
-
-[[csv.column]]
-header = "T"
-map = "sub_S06"
-parameter = "vertical.temperature"
-reducer = "mean"
-
-[[csv.column]]
-header = "Ea"
-map = "sub_S06"
-parameter = "vertical.actevap_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Ei"
-map = "sub_S06"
-parameter = "vertical.intevap_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Er"
-map = "sub_S06"
-parameter = "vertical.rootevap_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Eh"
-map = "sub_S06"
-parameter = "vertical.hortonevap_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Sw"
-map = "sub_S06"
-parameter = "vertical.snow"
-reducer = "mean"
-
-[[csv.column]]
-header = "Sww"
-map = "sub_S06"
-parameter = "vertical.snowwater"
-reducer = "mean"
-
-[[csv.column]]
-header = "Si"
-map = "sub_S06"
-parameter = "vertical.interceptionstorage_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Sr"
-map = "sub_S06"
-parameter = "vertical.srootzone_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Sh"
-map = "sub_S06"
-parameter = "vertical.hortonpondingstorage_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Shf"
-map = "sub_S06"
-parameter = "vertical.hortonrunoffstorage_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Sf"
-map = "sub_S06"
-parameter = "vertical.faststorage_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "Ss"
-map = "sub_S06"
-parameter = "vertical.slowstorage"
-reducer = "mean"
-
-[[csv.column]]
-header = "Sr_over_srmax_p"
-map = "sub_S06"
-parameter = "vertical.srootzone_over_srmax"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "Sr_over_srmax_m"
-map = "sub_S06"
-parameter = "vertical.srootzone_over_srmax_m"
-reducer = "mean"
-
-
-
-
-[[csv.column]]
-header = "Qftotal"
-map = "sub_S06"
-parameter = "vertical.qfast_tot"
-reducer = "mean"
-
-[[csv.column]]
-header = "Qh"
-map = "sub_S06"
-parameter = "vertical.qhortonpond"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "Qhf"
-map = "sub_S06"
-parameter = "vertical.qhortonrun"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "Qs"
-map = "sub_S01"
-parameter = "vertical.qslow"
-reducer = "mean"
-
-[[csv.column]]
-header = "Qs"
-map = "sub_S06"
-parameter = "vertical.qslow"
-reducer = "mean"
-
-[[csv.column]]
-header = "Qcap"
-map = "sub_S06"
-parameter = "vertical.qcapillary_m"
-reducer = "mean"
-
-[[csv.column]]
-header = "QfP"
-map = "sub_S06"
-parameter = "vertical.qfast"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "QfW"
-map = "sub_S06"
-parameter = "vertical.qfast"
-reducer = "mean"
-class = "w"
-
-[[csv.column]]
-header = "QfH"
-map = "sub_S06"
-parameter = "vertical.qfast"
-reducer = "mean"
-class = "h"
-
-
-[[csv.column]]
-header = "percentageP"
-map = "sub_S01"
-parameter = "vertical.hrufrac"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "percentageW"
-map = "sub_S01"
-parameter = "vertical.hrufrac"
-reducer = "mean"
-class = "w"
-
-[[csv.column]]
-header = "percentageH"
-map = "sub_S01"
-parameter = "vertical.hrufrac"
-reducer = "mean"
-class = "h"
-
-[[csv.column]]
-header = "percentageP"
-map = "sub_S06"
-parameter = "vertical.hrufrac"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "percentageW"
-map = "sub_S06"
-parameter = "vertical.hrufrac"
-reducer = "mean"
-class = "w"
-
-[[csv.column]]
-header = "percentageH"
-map = "sub_S06"
-parameter = "vertical.hrufrac"
-reducer = "mean"
-class = "h"
-
-
-
-
-[[csv.column]]
-header = "QfP"
-map = "sub_S01"
-parameter = "vertical.qfast"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "QfW"
-map = "sub_S01"
-parameter = "vertical.qfast"
-reducer = "mean"
-class = "w"
-
-[[csv.column]]
-header = "QfH"
-map = "sub_S01"
-parameter = "vertical.qfast"
-reducer = "mean"
-class = "h"
-
-
-[[csv.column]]
-header = "kfP"
-map = "sub_Sall"
-parameter = "vertical.kf"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "kfW"
-map = "sub_Sall"
-parameter = "vertical.kf"
-reducer = "mean"
-class = "w"
-
-[[csv.column]]
-header = "kfH"
-map = "sub_Sall"
-parameter = "vertical.kf"
-reducer = "mean"
-class = "h"
-
-
-
-[[csv.column]]
-header = "wbtot"
-map = "sub_S06"
-parameter = "vertical.wb_tot"
-reducer = "mean"
-
-[[csv.column]]
-header = "wbSi_p"
-map = "sub_S06"
-parameter = "vertical.wb_interception"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "wbShf_p"
-map = "sub_S06"
-parameter = "vertical.wb_hortonrunoff"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "wbSh_p"
-map = "sub_S06"
-parameter = "vertical.wb_hortonponding"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "wbSr_p"
-map = "sub_S06"
-parameter = "vertical.wb_rootzone"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "wbSf_p"
-map = "sub_S06"
-parameter = "vertical.wb_fast"
-reducer = "mean"
-class = "p"
-
-[[csv.column]]
-header = "wbSs"
-map = "sub_S06"
-parameter = "vertical.wb_slow"
-reducer = "mean"
-
-[[csv.column]]
-header = "to_river_land"
-map = "gauges_S06"
-parameter = "lateral.land.to_river"
-
-[[csv.column]]
-header = "land_inwater"
-map = "sub_S06"
-parameter = "lateral.land.inwater"
-reducer = "mean"
-
-
diff --git a/test/groundwater.jl b/test/groundwater.jl
index 5a59a3e20..def5ce7e5 100644
--- a/test/groundwater.jl
+++ b/test/groundwater.jl
@@ -1,6 +1,6 @@
 
 function initial_head(x)
-    2 * √x
+    return 2 * √x
 end
 
 """
@@ -10,12 +10,11 @@ Non-steady flow in an unconfined rectangular aquifer, with Dirichlet h(0, t) = 0
 on the left edge, and a Neumann Boundary Condition (dh/dx = 0) on the right.
 """
 function transient_aquifer_1d(x, time, conductivity, specific_yield, aquifer_length, beta)
-    initial_head(x) / 1.0 +
-    (beta * conductivity * initial_head(aquifer_length) * time) /
-    (specific_yield * aquifer_length * aquifer_length)
+    return initial_head(x) / 1.0 +
+           (beta * conductivity * initial_head(aquifer_length) * time) /
+           (specific_yield * aquifer_length * aquifer_length)
 end
 
-
 """
     drawdown_theis(distance, time, discharge, transmissivity, storativity)
 
@@ -26,7 +25,6 @@ function drawdown_theis(distance, time, discharge, transmissivity, storativity)
     return discharge / (4 * pi * transmissivity) * expint(u)
 end
 
-
 function homogenous_aquifer(nrow, ncol)
     shape = (nrow, ncol)
     # Domain, geometry
@@ -62,7 +60,6 @@ function homogenous_aquifer(nrow, ncol)
     return (connectivity, conf_aqf, unconf_aqf)
 end
 
-
 @testset "groundwater" begin
     ncol = 2
     nrow = 3
@@ -343,19 +340,19 @@ end
         connectivity, aquifer, _ = homogenous_aquifer(3, 1)
         constanthead = Wflow.ConstantHead([2.0, 4.0], [1, 3])
         conductivity_profile = "uniform"
-        gwf = Wflow.GroundwaterFlow(
-            aquifer,
-            connectivity,
-            constanthead,
-            Wflow.AquiferBoundaryCondition[],
+        gwf = Wflow.GroundwaterFlow{Wflow.Float}(;
+            aquifer = aquifer,
+            connectivity = connectivity,
+            constanthead = constanthead,
+            boundaries = Wflow.AquiferBoundaryCondition[],
         )
         # Set constant head (dirichlet) boundaries
         gwf.aquifer.head[gwf.constanthead.index] .= gwf.constanthead.head
 
         Q = zeros(3)
         dt = 0.25 # days
-        for _ = 1:50
-            Wflow.update(gwf, Q, dt, conductivity_profile)
+        for _ in 1:50
+            Wflow.update!(gwf, Q, dt, conductivity_profile)
         end
 
         @test gwf.aquifer.head ≈ [2.0, 3.0, 4.0]
@@ -365,19 +362,19 @@ end
         connectivity, aquifer, _ = homogenous_aquifer(3, 1)
         constanthead = Wflow.ConstantHead([2.0, 4.0], [1, 3])
         conductivity_profile = "exponential"
-        gwf = Wflow.GroundwaterFlow(
-            aquifer,
-            connectivity,
-            constanthead,
-            Wflow.AquiferBoundaryCondition[],
+        gwf = Wflow.GroundwaterFlow{Wflow.Float}(;
+            aquifer = aquifer,
+            connectivity = connectivity,
+            constanthead = constanthead,
+            boundaries = Wflow.AquiferBoundaryCondition[],
         )
         # Set constant head (dirichlet) boundaries
         gwf.aquifer.head[gwf.constanthead.index] .= gwf.constanthead.head
 
         Q = zeros(3)
         dt = 0.25 # days
-        for _ = 1:50
-            Wflow.update(gwf, Q, dt, conductivity_profile)
+        for _ in 1:50
+            Wflow.update!(gwf, Q, dt, conductivity_profile)
         end
 
         @test gwf.aquifer.head ≈ [2.0, 3.0, 4.0]
@@ -404,7 +401,7 @@ end
         indices, reverse_indices = Wflow.active_indices(domain, false)
         connectivity = Wflow.Connectivity(indices, reverse_indices, dx, dy)
         ncell = connectivity.ncell
-        xc = collect(range(0.0, stop = aquifer_length - cellsize, step = cellsize))
+        xc = collect(range(0.0; stop = aquifer_length - cellsize, step = cellsize))
         aquifer = Wflow.UnconfinedAquifer(
             initial_head.(xc),
             fill(conductivity, ncell),
@@ -418,11 +415,11 @@ end
         )
         # constant head on left boundary, 0 at 0
         constanthead = Wflow.ConstantHead([0.0], [1])
-        gwf = Wflow.GroundwaterFlow(
-            aquifer,
-            connectivity,
-            constanthead,
-            Wflow.AquiferBoundaryCondition[],
+        gwf = Wflow.GroundwaterFlow{Wflow.Float}(;
+            aquifer = aquifer,
+            connectivity = connectivity,
+            constanthead = constanthead,
+            boundaries = Wflow.AquiferBoundaryCondition[],
         )
 
         dt = Wflow.stable_timestep(gwf.aquifer, conductivity_profile)
@@ -431,8 +428,8 @@ end
         nstep = Int(ceil(time / dt))
         time = nstep * dt
 
-        for i = 1:nstep
-            Wflow.update(gwf, Q, dt, conductivity_profile)
+        for i in 1:nstep
+            Wflow.update!(gwf, Q, dt, conductivity_profile)
             # Gradient dh/dx is positive, all flow to the left
             @test all(diff(gwf.aquifer.head) .> 0.0)
         end
@@ -472,7 +469,7 @@ end
         indices, reverse_indices = Wflow.active_indices(domain, false)
         connectivity = Wflow.Connectivity(indices, reverse_indices, dx, dy)
         ncell = connectivity.ncell
-        xc = collect(range(0.0, stop = aquifer_length - cellsize, step = cellsize))
+        xc = collect(range(0.0; stop = aquifer_length - cellsize, step = cellsize))
         aquifer = Wflow.UnconfinedAquifer(
             initial_head.(xc),
             fill(conductivity, ncell),
@@ -486,11 +483,11 @@ end
         )
         # constant head on left boundary, 0 at 0
         constanthead = Wflow.ConstantHead([0.0], [1])
-        gwf = Wflow.GroundwaterFlow(
-            aquifer,
-            connectivity,
-            constanthead,
-            Wflow.AquiferBoundaryCondition[],
+        gwf = Wflow.GroundwaterFlow{Wflow.Float}(;
+            aquifer = aquifer,
+            connectivity = connectivity,
+            constanthead = constanthead,
+            boundaries = Wflow.AquiferBoundaryCondition[],
         )
 
         dt = Wflow.stable_timestep(gwf.aquifer, conductivity_profile)
@@ -499,8 +496,8 @@ end
         nstep = Int(ceil(time / dt))
         time = nstep * dt
 
-        for i = 1:nstep
-            Wflow.update(gwf, Q, dt, conductivity_profile)
+        for i in 1:nstep
+            Wflow.update!(gwf, Q, dt, conductivity_profile)
             # Gradient dh/dx is positive, all flow to the left
             @test all(diff(gwf.aquifer.head) .> 0.0)
         end
@@ -556,12 +553,17 @@ end
             fill(0.0, ncell), # total volume that can be released, to be set
         )
 
-        cell_index = reshape(collect(range(1, ncell, step = 1)), shape)
+        cell_index = reshape(collect(range(1, ncell; step = 1)), shape)
         indices = vcat(cell_index[1, :], cell_index[end, :])# , cell_index[:, 1], cell_index[:, end],)
         constanthead = Wflow.ConstantHead(fill(10.0, size(indices)), indices)
         # Place a well in the middle of the domain
         well = Wflow.Well([discharge], [0.0], [reverse_indices[wellrow, wellrow]])
-        gwf = Wflow.GroundwaterFlow(aquifer, connectivity, constanthead, [well])
+        gwf = Wflow.GroundwaterFlow{Wflow.Float}(;
+            aquifer = aquifer,
+            connectivity = connectivity,
+            constanthead = constanthead,
+            boundaries = Wflow.AquiferBoundaryCondition[well],
+        )
 
         dt = Wflow.stable_timestep(gwf.aquifer, conductivity_profile)
         Q = zeros(ncell)
@@ -569,24 +571,23 @@ end
         nstep = Int(ceil(time / dt))
         time = nstep * dt
 
-        for i = 1:nstep
-            Wflow.update(gwf, Q, dt, conductivity_profile)
+        for i in 1:nstep
+            Wflow.update!(gwf, Q, dt, conductivity_profile)
         end
 
         # test for symmetry on x and y axes
         head = reshape(gwf.aquifer.head, shape)
-        @test head[1:halfnrow, :] ≈ head[end:-1:halfnrow+2, :]
-        @test head[:, 1:halfnrow] ≈ head[:, end:-1:halfnrow+2]
+        @test head[1:halfnrow, :] ≈ head[end:-1:(halfnrow + 2), :]
+        @test head[:, 1:halfnrow] ≈ head[:, end:-1:(halfnrow + 2)]
 
         # compare with analytical solution
         start = -0.5 * aquifer_length + 0.5 * cellsize
         stop = 0.5 * aquifer_length - 0.5 * cellsize
-        X = collect(range(start, stop = stop, step = cellsize))
+        X = collect(range(start; stop = stop, step = cellsize))
         head_analytical =
             [drawdown_theis(x, time, discharge, transmissivity, storativity) for x in X] .+ 10.0
         # compare left-side, since it's symmetric anyway. Skip the well cell, and its first neighbor
-        difference = head[1:halfnrow-1, halfnrow] - head_analytical[1:halfnrow-1]
+        difference = head[1:(halfnrow - 1), halfnrow] - head_analytical[1:(halfnrow - 1)]
         @test all(difference .< 0.02)
     end
-
 end
diff --git a/test/hbv_config.toml b/test/hbv_config.toml
deleted file mode 100644
index 1a6a584b5..000000000
--- a/test/hbv_config.toml
+++ /dev/null
@@ -1,137 +0,0 @@
-# This is a TOML configuration file for Wflow.
-# Relative file paths are interpreted as being relative to this TOML file.
-# Wflow documentation https://deltares.github.io/Wflow.jl/dev/
-# TOML documentation: https://github.com/toml-lang/toml
-
-calendar = "proleptic_gregorian"
-endtime = 2000-02-01T00:00:00
-starttime = 1999-12-31T00:00:00
-time_units = "days since 1900-01-01 00:00:00"
-timestepsecs = 86400
-
-[state]
-path_input = "data/input/instates-lahn.nc"
-path_output = "data/output/outstates-lahn.nc"
-
-# if listed, the variable must be present in the NetCDF or error
-# if not listed, the variable can get a default value if it has one
-
-[state.vertical]
-interceptionstorage = "interceptionstorage"
-lowerzonestorage = "lowerzonestorage"
-snow = "snow"
-snowwater = "snowwater"
-soilmoisture = "soilmoisture"
-upperzonestorage = "upperzonestorage"
-
-[state.lateral.river]
-h = "h_river"
-q = "q_river"
-
-[state.lateral.land]
-h = "h_land"
-q = "q_land"
-
-[input]
-path_forcing = "data/input/forcing-lahn.nc"
-path_static = "data/input/staticmaps-lahn.nc"
-
-# these are not directly part of the model
-gauges = "wflow_gauges"
-ldd = "wflow_ldd"
-river_location = "wflow_river"
-subcatchment = "wflow_subcatch"
-
-# specify the internal IDs of the parameters which vary over time
-# the external name mapping needs to be below together with the other mappings
-forcing = [
-  "vertical.precipitation",
-  "vertical.temperature",
-  "vertical.potential_evaporation",
-]
-
-[input.vertical]
-alphanl = "AlphaNL"
-altitude = "wflow_dem"
-betaseepage = "BetaSeepage"
-cevpf = "CEVPF"
-cflux = "Cflux"
-cfmax = "Cfmax"
-cfr = "CFR"
-epf = "EPF"
-fc = "FC"
-hq = "HQ"
-icf = "ICF"
-k4 = "K4"
-khq = "KHQ"
-lp = "LP"
-perc = "PERC"
-potential_evaporation = "PET"
-precipitation = "P"
-sfcf = "SFCF"
-temperature = "TEMP"
-tt = "TT"
-tti = "TTI"
-ttm = "TTM"
-whc = "WHC"
-
-[input.lateral.river]
-length = "wflow_riverlength"
-n = "N_River"
-slope = "RiverSlope"
-width = "wflow_riverwidth"
-
-[input.lateral.land]
-n = "N"
-slope = "Slope"
-
-[model]
-kin_wave_iteration = true
-masswasting = true
-reinit = true
-snow = true
-type = "hbv"
-
-[output]
-path = "data/output/output_lahn.nc"
-
-[output.vertical]
-lowerzonestorage = "lowerzonestorage"
-snow = "snow"
-snowwater = "snowwater"
-soilmoisture = "soilmoisture"
-upperzonestorage = "upperzonestorage"
-
-[output.lateral.river]
-q = "q"
-
-[csv]
-path = "data/output/output_lahn.csv"
-
-[[csv.column]]
-header = "Q"
-parameter = "lateral.river.q"
-reducer = "maximum"
-
-[[csv.column]]
-coordinate.x = 8.279
-coordinate.y = 50.534
-header = "temp_bycoord"
-parameter = "vertical.temperature"
-
-[[csv.column]]
-header = "temp_byindex"
-index.x = 88
-index.y = 95
-parameter = "vertical.temperature"
-
-[[csv.column]]
-header = "Q"
-map = "gauges"
-parameter = "lateral.river.q"
-
-[[csv.column]]
-header = "perc"
-map = "subcatchment"
-parameter = "vertical.perc"
-reducer = "mean"
diff --git a/test/horizontal_process.jl b/test/horizontal_process.jl
index eb3c96763..7cb8bc780 100644
--- a/test/horizontal_process.jl
+++ b/test/horizontal_process.jl
@@ -41,11 +41,12 @@ Q = Wflow.kin_wave!(Q, graph, toposort, Qold, q, alpha, beta, DCL, dt_sec)
 @testset "flow rate" begin
     @test sum(Q) ≈ 2.957806043289641e6
     @test Q[toposort[1]] ≈ 0.007260052312634069f0
-    @test Q[toposort[n-100]] ≈ 3945.762718338739f0
+    @test Q[toposort[n - 100]] ≈ 3945.762718338739f0
     @test Q[sink] ≈ 4131.101474418251
 end
 
 @testset "kinematic wave subsurface flow" begin
+    kh_profile = Wflow.KhExponential([18021.0], [0.0017669756])
     @test all(
         isapprox.(
             Wflow.kinematic_wave_ssf(
@@ -53,17 +54,15 @@ end
                 215395179156.82645,
                 1540.34273559,
                 1.238,
-                18021.0,
                 0.25,
                 0.346,
-                0.0017669756,
                 1800.0,
                 1.0,
                 1697.05 * 1000.0,
                 1200.0 * 1000.0,
                 2443723.716252628,
-                1.0,
-                "exponential",
+                kh_profile,
+                1,
             ),
             (7.410313985168225e10, 1540.1496836278836, -0.0),
         ),
@@ -114,7 +113,6 @@ end
 end
 
 @testset "local inertial long channel MacDonald (1997)" begin
-
     g = 9.80665
     L = 1000.0
     dx = 5.0
@@ -135,11 +133,11 @@ end
     h_a = h.([dx:dx:L;]) # water depth profile (analytical solution)
     # integrate slope to get elevation (bed level) z
     x = [dx:dx:L;]
-    zb = first.([quadgk(s, xi, L, rtol = 1e-12) for xi in x])
+    zb = first.([quadgk(s, xi, L; rtol = 1e-12) for xi in x])
 
     # initialize ShallowWaterRiver
     graph = DiGraph(n)
-    for i = 1:n
+    for i in 1:n
         add_edge!(graph, i, i + 1)
     end
 
@@ -156,7 +154,7 @@ end
     width_at_link = fill(0.0, _ne)
     length_at_link = fill(0.0, _ne)
     mannings_n_sq = fill(0.0, _ne)
-    for i = 1:_ne
+    for i in 1:_ne
         zb_max[i] = max(zb[nodes_at_link.src[i]], zb[nodes_at_link.dst[i]])
         width_at_link[i] = min(width[nodes_at_link.dst[i]], width[nodes_at_link.src[i]])
         length_at_link[i] = 0.5 * (dl[nodes_at_link.dst[i]] + dl[nodes_at_link.src[i]])
@@ -168,7 +166,6 @@ end
         mannings_n_sq[i] = mannings_n * mannings_n
     end
 
-
     network = (
         nodes_at_link = nodes_at_link,
         links_at_node = Wflow.adjacent_links_at_node(graph, nodes_at_link),
@@ -181,10 +178,10 @@ end
     h_init = zeros(n - 1)
     push!(h_init, h_a[n])
 
-    sw_river = Wflow.ShallowWaterRiver(
+    sw_river = Wflow.ShallowWaterRiver(;
         n = n,
         ne = _ne,
-        active_n = collect(1:n-1),
+        active_n = collect(1:(n - 1)),
         active_e = collect(1:_ne),
         g = 9.80665,
         alpha = alpha,
@@ -234,7 +231,7 @@ end
         sw_river.inwater[1] = 20.0
         h0 = mean(sw_river.h)
         dt = Wflow.stable_timestep(sw_river)
-        Wflow.shallowwater_river_update(sw_river, network, dt, 0.0, true)
+        Wflow.shallowwater_river_update!(sw_river, network, dt, 0.0, true)
         d = abs(h0 - mean(sw_river.h))
         if d <= epsilon
             break
@@ -243,5 +240,4 @@ end
 
     # test for mean absolute error [cm]
     @test mean(abs.(sw_river.h .- h_a)) * 100.0 ≈ 1.873574206931199
-
 end
diff --git a/test/io.jl b/test/io.jl
index 25e142de3..a5700b10a 100644
--- a/test/io.jl
+++ b/test/io.jl
@@ -10,7 +10,7 @@ parsed_toml = TOML.parsefile(tomlpath)
 config = Wflow.Config(tomlpath)
 
 @testset "configuration file" begin
-    @test parsed_toml isa Dict{String,Any}
+    @test parsed_toml isa Dict{String, Any}
     @test config isa Wflow.Config
     @test Dict(config) == parsed_toml
     @test pathof(config) == tomlpath
@@ -24,15 +24,25 @@ config = Wflow.Config(tomlpath)
     @test collect(keys(config.output)) == ["lateral", "vertical", "path"]
 
     # theta_s can also be provided under the alias theta_s
-    @test Wflow.get_alias(config.input.vertical, "theta_s", "theta_s", nothing) == "thetaS"
-    val = pop!(config.input.vertical, "theta_s")
-    config.input.vertical["theta_s"] = val
-    @test Wflow.get_alias(config.input.vertical, "theta_s", "theta_s", nothing) == "thetaS"
+    @test Wflow.get_alias(
+        config.input.vertical.soil.parameters,
+        "theta_s",
+        "theta_s",
+        nothing,
+    ) == "thetaS"
+    val = pop!(config.input.vertical.soil.parameters, "theta_s")
+    config.input.vertical.soil.parameters["theta_s"] = val
+    @test Wflow.get_alias(
+        config.input.vertical.soil.parameters,
+        "theta_s",
+        "theta_s",
+        nothing,
+    ) == "thetaS"
 
     # modifiers can also be applied
-    kvconf = Wflow.get_alias(config.input.vertical, "kv_0", "kv_0", nothing)
+    kvconf = Wflow.get_alias(config.input.vertical.soil.parameters, "kv_0", "kv_0", nothing)
     @test kvconf isa Wflow.Config
-    ncname, modifier = Wflow.ncvar_name_modifier(kvconf, config = config)
+    ncname, modifier = Wflow.ncvar_name_modifier(kvconf; config = config)
     @test ncname === "KsatVer"
     @test modifier.scale == 1.0
     @test modifier.offset == 0.0
@@ -46,14 +56,6 @@ config = Wflow.Config(tomlpath)
           joinpath(@__DIR__, "data", "input", "instates-moselle.nc")
     @test Wflow.output_path(config, config.state.path_output) ==
           joinpath(@__DIR__, "data", "output", "outstates-moselle.nc")
-    # hbv_config doesn't use dir_input and dir_output
-    hbv_config = Wflow.Config(joinpath(@__DIR__, "hbv_config.toml"))
-    @test !haskey(hbv_config, "dir_input")
-    @test !haskey(hbv_config, "dir_output")
-    @test Wflow.input_path(hbv_config, hbv_config.state.path_input) ==
-          joinpath(@__DIR__, "data", "input", "instates-lahn.nc")
-    @test Wflow.output_path(hbv_config, hbv_config.state.path_output) ==
-          joinpath(@__DIR__, "data", "output", "outstates-lahn.nc")
 end
 
 @testset "Clock constructor" begin
@@ -212,13 +214,15 @@ end
 
 @testset "warm states" begin
     @test Wflow.param(model, "lateral.river.reservoir.volume")[1] ≈ 3.2807224993363418e7
-    @test Wflow.param(model, "vertical.satwaterdepth")[9115] ≈ 477.13548089422125
-    @test Wflow.param(model, "vertical.snow")[5] ≈ 11.019233179897599
-    @test Wflow.param(model, "vertical.tsoil")[5] ≈ 0.21814478119608938
-    @test Wflow.param(model, "vertical.ustorelayerdepth")[50063][1] ≈ 9.969116007201725
-    @test Wflow.param(model, "vertical.snowwater")[5] ≈ 0.0
-    @test Wflow.param(model, "vertical.canopystorage")[50063] ≈ 0.0
-    @test Wflow.param(model, "vertical.zi")[50063] ≈ 296.8028609104624
+    @test Wflow.param(model, "vertical.soil.variables.satwaterdepth")[9115] ≈
+          477.13548089422125
+    @test Wflow.param(model, "vertical.snow.variables.snow_storage")[5] ≈ 11.019233179897599
+    @test Wflow.param(model, "vertical.soil.variables.tsoil")[5] ≈ 0.21814478119608938
+    @test Wflow.param(model, "vertical.soil.variables.ustorelayerdepth")[50063][1] ≈
+          9.969116007201725
+    @test Wflow.param(model, "vertical.snow.variables.snow_water")[5] ≈ 0.0
+    @test Wflow.param(model, "vertical.interception.variables.canopy_storage")[50063] ≈ 0.0
+    @test Wflow.param(model, "vertical.soil.variables.zi")[50063] ≈ 296.8028609104624
     @test Wflow.param(model, "lateral.subsurface.ssf")[10606] ≈ 39.972334552895816
     @test Wflow.param(model, "lateral.river.q")[149] ≈ 53.48673634956338
     @test Wflow.param(model, "lateral.river.h")[149] ≈ 1.167635369628945
@@ -252,22 +256,22 @@ end
 
 @testset "initial parameter values" begin
     (; vertical) = model
-    @test vertical.cfmax[1] ≈ 3.7565300464630127
-    @test vertical.soilthickness[1] ≈ 2000.0
-    @test vertical.precipitation[49951] ≈ 2.2100000381469727
-    @test vertical.c[1] ≈
+    @test vertical.snow.parameters.cfmax[1] ≈ 3.7565300464630127
+    @test vertical.soil.parameters.soilthickness[1] ≈ 2000.0
+    @test vertical.atmospheric_forcing.precipitation[49951] ≈ 2.2100000381469727
+    @test vertical.soil.parameters.c[1] ≈
           [9.152995289601465, 8.919674421902961, 8.70537452585209, 8.690681062890977]
 end
 
-config.input.vertical.cfmax = Dict("value" => 2.0)
-config.input.vertical.soilthickness = Dict(
+config.input.vertical.snow.parameters.cfmax = Dict("value" => 2.0)
+config.input.vertical.soil.parameters.soilthickness = Dict(
     "scale" => 3.0,
     "offset" => 100.0,
     "netcdf" => Dict("variable" => Dict("name" => "SoilThickness")),
 )
-config.input.vertical.precipitation =
+config.input.vertical.atmospheric_forcing.precipitation =
     Dict("scale" => 1.5, "netcdf" => Dict("variable" => Dict("name" => "precip")))
-config.input.vertical.c = Dict(
+config.input.vertical.soil.parameters.c = Dict(
     "scale" => [2.0, 3.0],
     "offset" => [0.0, 0.0],
     "layer" => [1, 3],
@@ -280,10 +284,10 @@ Wflow.load_dynamic_input!(model)
 
 @testset "changed parameter values" begin
     (; vertical) = model
-    @test vertical.cfmax[1] == 2.0
-    @test vertical.soilthickness[1] ≈ 2000.0 * 3.0 + 100.0
-    @test vertical.precipitation[49951] ≈ 1.5 * 2.2100000381469727
-    @test vertical.c[1] ≈ [
+    @test vertical.snow.parameters.cfmax[1] == 2.0
+    @test vertical.soil.parameters.soilthickness[1] ≈ 2000.0 * 3.0 + 100.0
+    @test vertical.atmospheric_forcing.precipitation[49951] ≈ 1.5 * 2.2100000381469727
+    @test vertical.soil.parameters.c[1] ≈ [
         2.0 * 9.152995289601465,
         8.919674421902961,
         3.0 * 8.70537452585209,
@@ -291,7 +295,7 @@ Wflow.load_dynamic_input!(model)
     ]
 end
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 @testset "NetCDF creation" begin
     path = Base.Filesystem.tempname()
@@ -303,7 +307,6 @@ end
 
 @testset "NetCDF read variants" begin
     NCDataset(staticmaps_moselle_path) do ds
-
         @test Wflow.is_increasing(ds[:lon])
         @test !Wflow.is_increasing(ds[:lat])
 
@@ -314,7 +317,7 @@ end
 
         x = collect(Wflow.nc_dim(ds, :lon))
         @test length(x) == 291
-        @test x isa Vector{Union{Missing,Float64}}
+        @test x isa Vector{Union{Missing, Float64}}
 
         @test Wflow.internal_dim_name(:lon) == :x
         @test Wflow.internal_dim_name(:latitude) == :y
@@ -323,7 +326,7 @@ end
         @test_throws ArgumentError Wflow.read_dims(ds["c"], (x = :, y = :))
         @test_throws ArgumentError Wflow.read_dims(ds["LAI"], (x = :, y = :))
         data, data_dim_order = Wflow.read_dims(ds["wflow_dem"], (x = :, y = :))
-        @test data isa Matrix{Union{Float32,Missing}}
+        @test data isa Matrix{Union{Float32, Missing}}
         @test data[end, end] === missing
         @test data[125, 1] ≈ 647.187f0
         @test data_dim_order == (:x, :y)
@@ -447,9 +450,9 @@ end
 
     # Extracting required states and test if some are covered (not all are tested!)
     required_states = Wflow.extract_required_states(config)
-    @test (:vertical, :satwaterdepth) in required_states
-    @test (:vertical, :ustorelayerdepth) in required_states
-    @test (:vertical, :canopystorage) in required_states
+    @test (:vertical, :soil, :variables, :satwaterdepth) in required_states
+    @test (:vertical, :soil, :variables, :ustorelayerdepth) in required_states
+    @test (:vertical, :interception, :variables, :canopy_storage) in required_states
     @test (:lateral, :subsurface, :ssf) in required_states
     @test (:lateral, :river, :q) in required_states
     @test (:lateral, :river, :h_av) in required_states
@@ -457,27 +460,27 @@ end
     @test !((:lateral, :river, :lake, :waterlevel) in required_states)
 
     # Adding an unused state the see if the right warning message is thrown
-    config.state.vertical.additional_state = "additional_state"
+    config.state.vertical.soil.variables.additional_state = "additional_state"
     @test_logs (
         :warn,
         string(
-            "State variable `(:vertical, :additional_state)` provided, but is not used in ",
+            "State variable `(:vertical, :soil, :variables, :additional_state)` provided, but is not used in ",
             "model setup, skipping.",
         ),
     ) Wflow.check_states(config)
 
     # Removing the unused and required state, to test the exception being thrown
-    delete!(config.state["vertical"], "additional_state")
-    delete!(config.state["vertical"], "snow")
+    delete!(config.state.vertical.soil["variables"], "additional_state")
+    delete!(config.state.vertical.snow["variables"], "snow_storage")
     @test_throws ArgumentError Wflow.check_states(config)
 
     # Extracting required states for model type sbm_gwf and test if some are covered
     tomlpath = joinpath(@__DIR__, "sbm_gwf_config.toml")
     config = Wflow.Config(tomlpath)
     required_states = Wflow.extract_required_states(config)
-    @test (:vertical, :satwaterdepth) in required_states
-    @test (:vertical, :ustorelayerdepth) in required_states
-    @test (:vertical, :canopystorage) in required_states
+    @test (:vertical, :soil, :variables, :satwaterdepth) in required_states
+    @test (:vertical, :soil, :variables, :ustorelayerdepth) in required_states
+    @test (:vertical, :interception, :variables, :canopy_storage) in required_states
     @test (:lateral, :subsurface, :flow, :aquifer, :head) in required_states
     @test (:lateral, :river, :q) in required_states
     @test (:lateral, :river, :h_av) in required_states
diff --git a/test/reservoir_lake.jl b/test/reservoir_lake.jl
index aec71a06e..261021927 100644
--- a/test/reservoir_lake.jl
+++ b/test/reservoir_lake.jl
@@ -1,24 +1,23 @@
-@testset "reservoir simple" begin
-    res = Wflow.SimpleReservoir{Float64}(
-        dt = 86400.0,
-        demand = [52.523],
-        maxrelease = [420.184],
-        maxvolume = [25_000_000.0],
-        volume = [1.925e7],
-        totaloutflow = [0.0],
-        inflow = [0.0],
-        area = [1885665.353626924],
-        targetfullfrac = [0.8],
-        targetminfrac = [0.2425554726620697],
-        precipitation = [4.2],
-        evaporation = [1.5],
-        actevap = [0.0],
-        outflow = [NaN],
-        percfull = [NaN],
-        demandrelease = [NaN],
-    )
-
-    Wflow.update(res, 1, 100.0, 86400.0)
+res = Wflow.SimpleReservoir{Float64}(;
+    dt = 86400.0,
+    demand = [52.523],
+    maxrelease = [420.184],
+    maxvolume = [25_000_000.0],
+    volume = [1.925e7],
+    totaloutflow = [0.0],
+    inflow = [0.0],
+    area = [1885665.353626924],
+    targetfullfrac = [0.8],
+    targetminfrac = [0.2425554726620697],
+    precipitation = [4.2],
+    evaporation = [1.5],
+    actevap = [0.0],
+    outflow = [NaN],
+    percfull = [NaN],
+    demandrelease = [NaN],
+)
+@testset "Update reservoir simple" begin
+    Wflow.update!(res, 1, 100.0, 86400.0)
     @test res.outflow[1] ≈ 91.3783714867453
     @test res.totaloutflow[1] ≈ 7.895091296454794e6
     @test res.volume[1] ≈ 2.0e7
@@ -29,31 +28,31 @@
     @test res.actevap[1] ≈ 1.5
 end
 
-@testset "lake" begin
-    lake = Wflow.Lake{Float64}(
-        dt = 86400.0,
-        lowerlake_ind = [0],
-        area = [180510409.0],
-        maxstorage = Wflow.maximum_storage([1], [3], [180510409.0], [missing], [missing]),
-        threshold = [0.0],
-        storfunc = [1],
-        outflowfunc = [3],
-        totaloutflow = [0.0],
-        inflow = [0.0],
-        b = [0.22],
-        e = [2.0],
-        sh = [missing],
-        hq = [missing],
-        storage = Wflow.initialize_storage([1], [180510409.0], [18.5], [missing]),
-        waterlevel = [18.5],
-        precipitation = [20.0],
-        evaporation = [3.2],
-        actevap = [0.0],
-        outflow = [NaN],
-    )
-
-    Wflow.update(lake, 1, 2500.0, 181, 86400.0)
-    @test Wflow.waterlevel(lake.storfunc, lake.area, lake.storage, lake.sh)[1] ≈ 19.672653848925634
+lake = Wflow.Lake{Float64}(;
+    dt = 86400.0,
+    lowerlake_ind = [0],
+    area = [180510409.0],
+    maxstorage = Wflow.maximum_storage([1], [3], [180510409.0], [missing], [missing]),
+    threshold = [0.0],
+    storfunc = [1],
+    outflowfunc = [3],
+    totaloutflow = [0.0],
+    inflow = [0.0],
+    b = [0.22],
+    e = [2.0],
+    sh = [missing],
+    hq = [missing],
+    storage = Wflow.initialize_storage([1], [180510409.0], [18.5], [missing]),
+    waterlevel = [18.5],
+    precipitation = [20.0],
+    evaporation = [3.2],
+    actevap = [0.0],
+    outflow = [NaN],
+)
+@testset "Update lake" begin
+    Wflow.update!(lake, 1, 2500.0, 181, 86400.0)
+    @test Wflow.waterlevel(lake.storfunc, lake.area, lake.storage, lake.sh)[1] ≈
+          19.672653848925634
     @test lake.outflow[1] ≈ 85.14292808113598
     @test lake.totaloutflow[1] ≈ 7.356348986210149e6
     @test lake.storage[1] ≈ 3.55111879238499e9
@@ -70,9 +69,9 @@ sh = [
 ]
 @testset "linked lakes (HBV)" begin
     @test keys(sh[1]) == (:H, :S)
-    @test typeof(values(sh[1])) == Tuple{Vector{Float},Vector{Float}}
+    @test typeof(values(sh[1])) == Tuple{Vector{Float}, Vector{Float}}
 
-    lake = Wflow.Lake{Float}(
+    lake = Wflow.Lake{Float}(;
         dt = 86400.0,
         lowerlake_ind = [2, 0],
         area = [472461536.0, 60851088.0],
@@ -105,8 +104,8 @@ sh = [
         ),
     )
 
-    Wflow.update(lake, 1, 500.0, 15, 86400.0)
-    Wflow.update(lake, 2, 500.0, 15, 86400.0)
+    Wflow.update!(lake, 1, 500.0, 15, 86400.0)
+    Wflow.update!(lake, 2, 500.0, 15, 86400.0)
     @test lake.outflow ≈ [214.80170846121263, 236.83281600000214] atol = 1e-2
     @test lake.totaloutflow ≈ [1.855886761104877e7, 2.0462355302400187e7] atol = 1e3
     @test lake.storage ≈ [1.2737435094769483e9, 2.6019755340159863e8] atol = 1e4
@@ -114,8 +113,8 @@ sh = [
     lake.actevap .= 0.0
     lake.totaloutflow .= 0.0
     lake.inflow .= 0.0
-    Wflow.update(lake, 1, 500.0, 15, 86400.0)
-    Wflow.update(lake, 2, 500.0, 15, 86400.0)
+    Wflow.update!(lake, 1, 500.0, 15, 86400.0)
+    Wflow.update!(lake, 2, 500.0, 15, 86400.0)
     @test lake.outflow ≈ [0.0, 239.66710359986183] atol = 1e-2
     @test lake.totaloutflow ≈ [-2.2446764487487033e7, 4.3154002238515094e7] atol = 1e3
     @test lake.storage ≈ [1.3431699662524352e9, 2.6073035986708355e8] atol = 1e4
@@ -124,7 +123,7 @@ sh = [
 end
 
 @testset "overflowing lake with sh and hq" begin
-    lake = Wflow.Lake{Float}(
+    lake = Wflow.Lake{Float}(;
         dt = 86400.0,
         lowerlake_ind = [0],
         area = [200_000_000],
@@ -152,8 +151,9 @@ end
         storage = [410_760_000],
     )
 
-    Wflow.update(lake, 1, 1500.0, 15, 86400.0)
-    @test Wflow.waterlevel(lake.storfunc, lake.area, lake.storage, lake.sh) ≈ [398.0] atol = 1e-2
+    Wflow.update!(lake, 1, 1500.0, 15, 86400.0)
+    @test Wflow.waterlevel(lake.storfunc, lake.area, lake.storage, lake.sh) ≈ [398.0] atol =
+        1e-2
     @test lake.outflow ≈ [1303.67476852] atol = 1e-2
     @test lake.totaloutflow ≈ [11.26375000e7] atol = 1e3
     @test lake.storage ≈ [4.293225e8] atol = 1e4
diff --git a/test/run.jl b/test/run.jl
index 45cd79750..5f3237d06 100644
--- a/test/run.jl
+++ b/test/run.jl
@@ -17,7 +17,7 @@ config.state.path_output =
 config.output.path =
     joinpath(dirname(tomlpath), "data/state-test/output-moselle-january.nc")
 model = Wflow.initialize_sbm_model(config)
-Wflow.run(model)
+Wflow.run!(model)
 
 # first half of January, cold start
 config.starttime = DateTime("2000-01-01T00:00:00")
@@ -28,7 +28,7 @@ config.state.path_output =
 config.output.path =
     joinpath(dirname(tomlpath), "data/state-test/output-moselle-january-1of2.nc")
 model = Wflow.initialize_sbm_model(config)
-Wflow.run(model)
+Wflow.run!(model)
 
 # second half of January, warm start
 config.starttime = DateTime("2000-01-15T00:00:00")
@@ -41,8 +41,7 @@ config.state.path_output =
 config.output.path =
     joinpath(dirname(tomlpath), "data/state-test/output-moselle-january-2of2.nc")
 model = Wflow.initialize_sbm_model(config)
-Wflow.run(model)
-
+Wflow.run!(model)
 
 # second half of January, warm start, fews_run set to true, and starttime set one day earlier
 # to match endtime of part 1
@@ -59,7 +58,7 @@ config.state.path_output = joinpath(
 config.output.path =
     joinpath(dirname(tomlpath), "data/state-test/output-moselle-january-2of2-fews_run.nc")
 model = Wflow.initialize_sbm_model(config)
-Wflow.run(model)
+Wflow.run!(model)
 
 # verify that there are minimal differences in the end state of the two runs
 endstate_one_run_path =
diff --git a/test/run_flextopo.jl b/test/run_flextopo.jl
deleted file mode 100644
index 974fc085c..000000000
--- a/test/run_flextopo.jl
+++ /dev/null
@@ -1,116 +0,0 @@
-
-tomlpath = joinpath(@__DIR__, "flextopo_config.toml")
-config = Wflow.Config(tomlpath)
-
-model = Wflow.initialize_flextopo_model(config)
-(; network) = model
-
-model = Wflow.run_timestep(model)
-
-# test if the first timestep was written to the CSV file
-flush(model.writer.csv_io)  # ensure the buffer is written fully to disk
-@testset "CSV output" begin
-    row = csv_first_row(model.writer.csv_path)
-
-    @test row.time == DateTime("2010-01-01T00:00:00")
-    @test row.Q_16 ≈ 0.0000370817920883859f0
-    @test row.percentageH_16 ≈ 0.340213f0
-    @test row.QfP_1011 ≈ 0.0f0
-    @test row.kfW_10 ≈ 0.136334478855133f0
-    @test row.kfH_10 ≈ 0.0454448275268077f0
-    @test row.Qs_503 ≈ 0.1575031550601124f0
-    @test row.Si_16 ≈ 0.0f0
-    @test row.Ss_16 ≈ 29.89794354323524f0
-    @test row.Ea_16 ≈ 0.0110627892408438f0
-end
-
-@testset "first timestep" begin
-    flextopo = model.vertical
-    @test flextopo.tt[3500] ≈ 1.3f0
-    @test model.clock.iteration == 1
-    @test flextopo.rootzonestorage[3500] ≈
-          [147.11238663084805f0, 79.08369375691255f0, 79.23637697443984f0]
-    @test flextopo.runoff[3500] ≈ 0.19008129369467497f0
-    @test flextopo.rootevap[3500] ≈
-          [0.009225917980074883f0, 0.009225917980074883f0, 0.009225917980074883f0]
-    @test flextopo.snow[3500] == 0.0
-end
-
-# run the second timestep
-model = Wflow.run_timestep(model)
-
-@testset "second timestep" begin
-    flextopo = model.vertical
-    @test flextopo.rootzonestorage[3500] ≈
-          [146.73222385533154f0, 78.55190222246516f0, 78.85739340140256f0]
-    @test flextopo.runoff[3500] ≈ 0.18887692333975817f0
-    @test flextopo.rootevap[3500] ≈
-          [0.38016277551651f0, 0.38016277551651f0, 0.38016277551651f0]
-    @test flextopo.snow[3500] == 0.0f0
-end
-
-@testset "overland domain" begin
-    q = model.lateral.land.q_av
-    land = model.lateral.land
-    @test sum(q) ≈ 202.1162497378859f0
-    @test q[10354] ≈ 0.001624108000162103f0
-    @test land.volume[10354] ≈ 66.41398729880544f0
-    @test land.inwater[10354] ≈ 0.001658182220814705f0
-    @test q[network.land.order[end]] ≈ 0.0033337667005815565f0
-end
-
-@testset "river flow" begin
-    q = model.lateral.river.q_av
-    @test sum(q) ≈ 108.45732176546284f0
-    @test q[651] ≈ 0.014755293483684526f0
-    @test q[1056] ≈ 0.0030933658056867823f0
-    @test q[network.river.order[end]] ≈ 0.0005541137960384742f0
-end
-
-#change the config to use other functions for the storages for several classes:
-config = Wflow.Config(tomlpath)
-config["model"]["select_snow"] = ["common_snow_no_storage"]
-config["model"]["select_interception"] =
-    ["interception_no_storage", "interception_overflow", "interception_overflow"]
-config["model"]["select_hortonponding"] =
-    ["hortonponding_no_storage", "hortonponding", "hortonponding_no_storage"]
-config["model"]["select_hortonrunoff"] =
-    ["hortonrunoff_no_storage", "hortonrunoff", "hortonrunoff_no_storage"]
-config["model"]["select_rootzone"] =
-    ["rootzone_storage", "rootzone_storage", "rootzone_no_storage"]
-config["model"]["select_fast"] = ["fast_no_storage", "fast_storage", "fast_storage"]
-config["model"]["select_slow"] = ["common_slow_storage"]
-
-model = Wflow.initialize_flextopo_model(config)
-(; network) = model
-
-model = Wflow.run_timestep(model)
-
-@testset "first timestep" begin
-    flextopo = model.vertical
-    @test flextopo.rootzonestorage[3500] ≈
-          [147.11238663084805f0, 79.08369375691255f0, 0.0f0]
-    @test flextopo.runoff[3500] ≈ 0.19008129369467497f0
-    @test flextopo.rootevap[3500] ≈ [0.009225917980074883f0, 0.009225917980074883f0, 0.0f0]
-    @test flextopo.snow[3500] == 0.0f0
-end
-
-@testset "overland domain" begin
-    q = model.lateral.land.q_av
-    land = model.lateral.land
-    @test sum(q) ≈ 69.51434665542678f0
-    @test q[10354] ≈ 0.0009376627801121855f0
-    @test land.volume[10354] ≈ 63.46997463442048f0
-    @test land.inwater[10354] ≈ 0.0016722689680105701f0
-    @test q[network.land.order[end]] ≈ 0.0006175366185364747f0
-end
-
-@testset "river flow" begin
-    q = model.lateral.river.q_av
-    @test sum(q) ≈ 22.236556345676156f0
-    @test q[651] ≈ 0.00035906990499781237f0
-    @test q[1056] ≈ 0.0002647264689318855f0
-    @test q[network.river.order[end]] ≈ 0.000015647838277869928f0
-end
-
-Wflow.close_files(model, delete_output = false)
diff --git a/test/run_hbv.jl b/test/run_hbv.jl
deleted file mode 100644
index b0706f316..000000000
--- a/test/run_hbv.jl
+++ /dev/null
@@ -1,66 +0,0 @@
-
-tomlpath = joinpath(@__DIR__, "hbv_config.toml")
-config = Wflow.Config(tomlpath)
-
-model = Wflow.initialize_hbv_model(config)
-(; network) = model
-
-model = Wflow.run_timestep(model)
-
-# test if the first timestep was written to the CSV file
-flush(model.writer.csv_io)  # ensure the buffer is written fully to disk
-@testset "CSV output" begin
-    row = csv_first_row(model.writer.csv_path)
-
-    @test row.time == DateTime("2000-01-01T00:00:00")
-    @test row.Q ≈ 521.8433822888003f0
-    @test row.temp_bycoord ≈ 2.965437173843384f0
-    @test row.temp_byindex ≈ 1.1716821193695068f0
-    @test row.Q_1 ≈ 505.1935875677504f0
-    @test row.perc_33 ≈ 2.308000087738037f0
-    @test row.perc_34 ≈ 1.8980000019073486f0
-    @test row.perc_35 ≈ 2.7100000381469727f0
-    @test row.perc_36 ≈ 3.818000078201294f0
-    @test row.perc_37 ≈ 2.1440000534057617f0
-end
-
-@testset "first timestep" begin
-    hbv = model.vertical
-    @test hbv.tt[4377] ≈ 0.0
-    @test model.clock.iteration == 1
-    @test hbv.soilmoisture[4377] ≈ 134.35299682617188f0
-    @test hbv.runoff[4377] ≈ 7.406898120121746f0
-    @test hbv.soilevap[4377] == 0.0
-    @test hbv.snow[4377] == 0.0
-end
-
-# run the second timestep
-model = Wflow.run_timestep(model)
-
-@testset "second timestep" begin
-    hbv = model.vertical
-    @test hbv.soilmoisture[4377] ≈ 134.35299682617188f0
-    @test hbv.runoff[4377] ≈ 4.3533463f0
-    @test hbv.soilevap[4377] == 0.0
-    @test hbv.snow[4377] == 0.0
-end
-
-@testset "overland domain" begin
-    q = model.lateral.land.q_av
-    land = model.lateral.land
-    @test sum(q) ≈ 3264.157286198723f0
-    @test q[10354] ≈ 0.2350958683414288f0
-    @test land.volume[10354] ≈ 2057.7314802432425f0
-    @test land.inwater[10354] ≈ 0.027351853491789667f0
-    @test q[network.land.order[end]] ≈ 0.27158713754634217f0
-end
-
-@testset "river flow" begin
-    q = model.lateral.river.q_av
-    @test sum(q) ≈ 52686.97949266187f0
-    @test q[651] ≈ 5.7624898709967125f0
-    @test q[1056] ≈ 8.892291220438524f0
-    @test q[network.river.order[end]] ≈ 360.40425076323913f0
-end
-
-Wflow.close_files(model, delete_output = false)
diff --git a/test/run_sbm.jl b/test/run_sbm.jl
index fe81c53ec..2d03bed96 100644
--- a/test/run_sbm.jl
+++ b/test/run_sbm.jl
@@ -6,7 +6,7 @@ config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sbm_model(config)
 (; network) = model
 
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 # test if the first timestep was written to the CSV file
 flush(model.writer.csv_io)  # ensure the buffer is written fully to disk
@@ -69,47 +69,48 @@ end
 end
 
 @testset "first timestep" begin
-    sbm = model.vertical
-
-    @test sbm.tt[50063] ≈ 0.0f0
+    sbm = model.vertical.soil
+    snow = model.vertical.snow
+    @test snow.parameters.tt[50063] ≈ 0.0f0
 
     @test model.clock.iteration == 1
 
-    @test sbm.theta_s[50063] ≈ 0.48755401372909546f0
-    @test sbm.theta_r[50063] ≈ 0.15943120419979095f0
-    @test mean(sbm.runoff) ≈ 0.04177459898728149f0
-    @test mean(sbm.soilevap) ≈ 0.02122698830889417f0
-    @test mean(sbm.actevap) ≈ 0.3353001180202587f0
-    @test mean(sbm.actinfilt) ≈ 1.6444774688444848f0
-    @test sbm.snow[5] ≈ 3.768513390588815f0
-    @test mean(sbm.snow) ≈ 0.038019723676094325f0
-    @test sbm.total_storage[50063] ≈ 559.9035608052374f0
-    @test sbm.total_storage[429] ≈ 597.4578475404879f0 # river cell
+    @test sbm.parameters.theta_s[50063] ≈ 0.48755401372909546f0
+    @test sbm.parameters.theta_r[50063] ≈ 0.15943120419979095f0
+    @test mean(sbm.variables.runoff) ≈ 0.04177459898728149f0
+    @test mean(sbm.variables.soilevap) ≈ 0.02122698830889417f0
+    @test mean(sbm.variables.actevap) ≈ 0.3353001180202587f0
+    @test mean(sbm.variables.actinfilt) ≈ 1.6444774688444848f0
+    @test snow.variables.snow_storage[5] ≈ 3.768513390588815f0
+    @test mean(snow.variables.snow_storage) ≈ 0.038019723676094325f0
+    @test sbm.variables.total_storage[50063] ≈ 559.9035608052374f0
+    @test sbm.variables.total_storage[429] ≈ 597.4578475404879f0 # river cell
 end
 
 # run the second timestep
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 @testset "second timestep" begin
-    sbm = model.vertical
-    @test sbm.theta_s[50063] ≈ 0.48755401372909546f0
-    @test sbm.theta_r[50063] ≈ 0.15943120419979095f0
-    @test mean(sbm.net_runoff) ≈ 0.23734052031823816f0
-    @test mean(sbm.runoff) ≈ 0.23770898226019577f0
-    @test mean(sbm.soilevap) ≈ 0.018750808322054897f0
-    @test mean(sbm.actevap) ≈ 0.14545276216428166f0
-    @test mean(sbm.actinfilt) ≈ 0.08863102527394363f0
-    @test sbm.snow[5] ≈ 3.843412524052313f0
-    @test mean(sbm.snow) ≈ 0.03461317061870949f0
-    @test sbm.total_storage[50063] ≈ 560.0152135062889f0
-    @test sbm.total_storage[429] ≈ 617.2238533241972f0 # river cell
+    sbm = model.vertical.soil
+    snow = model.vertical.snow
+    @test sbm.parameters.theta_s[50063] ≈ 0.48755401372909546f0
+    @test sbm.parameters.theta_r[50063] ≈ 0.15943120419979095f0
+    @test mean(sbm.variables.net_runoff) ≈ 0.23734052031823816f0
+    @test mean(sbm.variables.runoff) ≈ 0.23770898226019577f0
+    @test mean(sbm.variables.soilevap) ≈ 0.018750808322054897f0
+    @test mean(sbm.variables.actevap) ≈ 0.14545276216428166f0
+    @test mean(sbm.variables.actinfilt) ≈ 0.08863102527394363f0
+    @test snow.variables.snow_storage[5] ≈ 3.843412524052313f0
+    @test mean(snow.variables.snow_storage) ≈ 0.03461317061870949f0
+    @test sbm.variables.total_storage[50063] ≈ 560.0152135062889f0
+    @test sbm.variables.total_storage[429] ≈ 617.2238533241972f0 # river cell
 end
 
 @testset "subsurface flow" begin
     ssf = model.lateral.subsurface.ssf
     @test sum(ssf) ≈ 6.3761585406186976f7
     @test ssf[network.land.order[1]] ≈ 718.2802566393531f0
-    @test ssf[network.land.order[end-100]] ≈ 2337.771227118579f0
+    @test ssf[network.land.order[end - 100]] ≈ 2337.771227118579f0
     @test ssf[network.land.order[end]] ≈ 288.19428729403984f0
 end
 
@@ -139,12 +140,12 @@ end
 end
 
 # set these variables for comparison in "changed dynamic parameters"
-precip = copy(model.vertical.precipitation)
-evap = copy(model.vertical.potential_evaporation)
-lai = copy(model.vertical.leaf_area_index)
+precip = copy(model.vertical.atmospheric_forcing.precipitation)
+evap = copy(model.vertical.atmospheric_forcing.potential_evaporation)
+lai = copy(model.vertical.vegetation_parameter_set.leaf_area_index)
 res_evap = copy(model.lateral.river.reservoir.evaporation)
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test for setting a pit and multithreading multiple basins (by setting 2 extra pits
 # resulting in 3 basins)
@@ -176,26 +177,27 @@ end
 tomlpath = joinpath(@__DIR__, "sbm_config.toml")
 config = Wflow.Config(tomlpath)
 
-config.input.vertical.precipitation =
+config.input.vertical.atmospheric_forcing.precipitation =
     Dict("scale" => 2.0, "netcdf" => Dict("variable" => Dict("name" => "precip")))
-config.input.vertical.potential_evaporation = Dict(
+config.input.vertical.atmospheric_forcing.potential_evaporation = Dict(
     "scale" => 3.0,
     "offset" => 1.50,
     "netcdf" => Dict("variable" => Dict("name" => "pet")),
 )
-config.input.vertical.leaf_area_index =
+config.input.vertical.vegetation_parameter_set.leaf_area_index =
     Dict("scale" => 1.6, "netcdf" => Dict("variable" => Dict("name" => "LAI")))
 
 model = Wflow.initialize_sbm_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "changed dynamic parameters" begin
     res = model.lateral.river.reservoir
     vertical = model.vertical
-    @test vertical.precipitation[2] / precip[2] ≈ 2.0f0
-    @test (vertical.potential_evaporation[100] - 1.50) / evap[100] ≈ 3.0f0
-    @test vertical.leaf_area_index[100] / lai[100] ≈ 1.6f0
+    @test vertical.atmospheric_forcing.precipitation[2] / precip[2] ≈ 2.0f0
+    @test (vertical.atmospheric_forcing.potential_evaporation[100] - 1.50) / evap[100] ≈
+          3.0f0
+    @test vertical.vegetation_parameter_set.leaf_area_index[100] / lai[100] ≈ 1.6f0
     @test (res.evaporation[2] - 1.50) / res_evap[2] ≈ 3.0000012203408635f0
 end
 
@@ -203,12 +205,13 @@ end
 tomlpath = joinpath(@__DIR__, "sbm_config.toml")
 config = Wflow.Config(tomlpath)
 
-config.input.cyclic = ["vertical.leaf_area_index", "lateral.river.inflow"]
+config.input.cyclic =
+    ["vertical.vegetation_parameter_set.leaf_area_index", "lateral.river.inflow"]
 config.input.lateral.river.inflow = "inflow"
 
 model = Wflow.initialize_sbm_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "river inflow (cyclic)" begin
     @test model.lateral.river.inflow[44] ≈ 0.75
@@ -217,25 +220,25 @@ end
 
 # test fixed forcing (precipitation = 2.5)
 config = Wflow.Config(tomlpath)
-config.input.vertical.precipitation = Dict("value" => 2.5)
+config.input.vertical.atmospheric_forcing.precipitation = Dict("value" => 2.5)
 model = Wflow.initialize_sbm_model(config)
-Wflow.load_fixed_forcing(model)
+Wflow.load_fixed_forcing!(model)
 
 @testset "fixed precipitation forcing (initialize)" begin
-    @test maximum(model.vertical.precipitation) ≈ 2.5
-    @test minimum(model.vertical.precipitation) ≈ 0.0
+    @test maximum(model.vertical.atmospheric_forcing.precipitation) ≈ 2.5
+    @test minimum(model.vertical.atmospheric_forcing.precipitation) ≈ 0.0
     @test all(isapprox.(model.lateral.river.reservoir.precipitation, 2.5))
 end
 
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 @testset "fixed precipitation forcing (first timestep)" begin
-    @test maximum(model.vertical.precipitation) ≈ 2.5
-    @test minimum(model.vertical.precipitation) ≈ 0.0
+    @test maximum(model.vertical.atmospheric_forcing.precipitation) ≈ 2.5
+    @test minimum(model.vertical.atmospheric_forcing.precipitation) ≈ 0.0
     @test all(isapprox.(model.lateral.river.reservoir.precipitation, 2.5))
 end
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test local-inertial option for river flow river_routing
 tomlpath = joinpath(@__DIR__, "sbm_config.toml")
@@ -243,8 +246,8 @@ config = Wflow.Config(tomlpath)
 config.model.river_routing = "local-inertial"
 
 model = Wflow.initialize_sbm_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "river flow and depth (local inertial)" begin
     q = model.lateral.river.q_av
@@ -259,15 +262,15 @@ model = Wflow.run_timestep(model)
     q_channel = model.lateral.river.q_channel_av
     @test q ≈ q_channel
 end
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test local-inertial option for river and overland flow
 tomlpath = joinpath(@__DIR__, "sbm_swf_config.toml")
 config = Wflow.Config(tomlpath)
 
 model = Wflow.initialize_sbm_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "river and overland flow and depth (local inertial)" begin
     q = model.lateral.river.q_av
@@ -287,7 +290,8 @@ model = Wflow.run_timestep(model)
     @test h[[26, 35, 631]] ≈
           [0.07367301172613304f0, 0.009139882310161706f0, 0.0007482998926237368f0]
 end
-Wflow.close_files(model, delete_output = false)
+
+Wflow.close_files(model; delete_output = false)
 
 # test local-inertial option for river flow including 1D floodplain schematization
 tomlpath = joinpath(@__DIR__, "sbm_config.toml")
@@ -396,8 +400,8 @@ dh = diff(fp.depth)
     @test Wflow.wetted_perimeter(fp.p[i1, 4], fp.depth[i1], h) ≈ 90.11775307900271f0
 end
 
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "river flow (local inertial) with floodplain schematization simulation" begin
     q = model.lateral.river.q_av
@@ -417,8 +421,8 @@ end
 config.input.lateral.river.riverlength_bc = "riverlength_bc"
 config.input.lateral.river.riverdepth_bc = "riverdepth_bc"
 model = Wflow.initialize_sbm_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "change boundary condition for local inertial routing (including floodplain)" begin
     q = model.lateral.river.q_av
@@ -433,110 +437,94 @@ model = Wflow.run_timestep(model)
     @test h[501] ≈ 0.056707564314724804f0
     @test h[5808] ≈ 2.0000006940603936f0
 end
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test different ksat profiles
 @testset "ksat profiles (SBM)" begin
     i = 100
     tomlpath = joinpath(@__DIR__, "sbm_config.toml")
     config = Wflow.Config(tomlpath)
-    config.input.vertical.kv = "kv"
-    config.input.vertical.z_exp = Dict("value" => 400.0)
-    config.input.vertical.z_layered = Dict("value" => 400.0)
+    config.input.vertical.soil.parameters.kv = "kv"
+    config.input.vertical.soil.parameters.z_exp = Dict("value" => 400.0)
+    config.input.vertical.soil.parameters.z_layered = Dict("value" => 400.0)
 
     @testset "exponential profile" begin
         model = Wflow.initialize_sbm_model(config)
-        (; vertical) = model
-        z = vertical.zi[i]
-        kv_z = Wflow.hydraulic_conductivity_at_depth(vertical, z, i, 2, "exponential")
-        @test kv_z ≈ vertical.kvfrac[i][2] * vertical.kv_0[i] * exp(-vertical.f[i] * z)
-        @test vertical.z_exp == vertical.soilthickness
-        @test_throws ErrorException Wflow.kh_layered_profile(
-            vertical,
-            100.0,
-            i,
-            "exponential",
-        )
-        @test all(isnan.(vertical.z_layered))
-        @test all(isnan.(vertical.kv[i]))
-        @test all(vertical.nlayers_kv .== 0)
+        (; soil) = model.vertical
+        (; kv_profile) = soil.parameters
+        (; subsurface) = model.lateral
+        z = soil.variables.zi[i]
+        kvfrac = soil.parameters.kvfrac
+        kv_z = Wflow.hydraulic_conductivity_at_depth(kv_profile, kvfrac, z, i, 2)
+        @test kv_z ≈ kvfrac[i][2] * kv_profile.kv_0[i] * exp(-kv_profile.f[i] * z)
+        @test subsurface.ssfmax[i] ≈ 28.32720603576582f0
+        @test subsurface.ssf[i] ≈ 11683.330684556406f0
     end
 
     @testset "exponential constant profile" begin
         config.input.vertical.ksat_profile = "exponential_constant"
         model = Wflow.initialize_sbm_model(config)
-        (; vertical) = model
-        z = vertical.zi[i]
-        kv_z =
-            Wflow.hydraulic_conductivity_at_depth(vertical, z, i, 2, "exponential_constant")
-        @test kv_z ≈ vertical.kvfrac[i][2] * vertical.kv_0[i] * exp(-vertical.f[i] * z)
-        kv_400 = Wflow.hydraulic_conductivity_at_depth(
-            vertical,
-            400.0,
-            i,
-            2,
-            "exponential_constant",
-        )
-        kv_1000 = Wflow.hydraulic_conductivity_at_depth(
-            vertical,
-            1000.0,
-            i,
-            3,
-            "exponential_constant",
-        )
+        (; soil) = model.vertical
+        (; kv_profile) = soil.parameters
+        (; subsurface) = model.lateral
+        z = soil.variables.zi[i]
+        kvfrac = soil.parameters.kvfrac
+        kv_z = Wflow.hydraulic_conductivity_at_depth(kv_profile, kvfrac, z, i, 2)
+        @test kv_z ≈
+              kvfrac[i][2] *
+              kv_profile.exponential.kv_0[i] *
+              exp(-kv_profile.exponential.f[i] * z)
+        kv_400 = Wflow.hydraulic_conductivity_at_depth(kv_profile, kvfrac, 400.0, i, 2)
+        kv_1000 = Wflow.hydraulic_conductivity_at_depth(kv_profile, kvfrac, 1000.0, i, 3)
         @test kv_400 ≈ kv_1000
-        @test_throws ErrorException Wflow.kh_layered_profile(
-            vertical,
-            100.0,
-            i,
-            "exponential_constant",
-        )
-        @test all(isnan.(vertical.z_layered))
-        @test all(isnan.(vertical.kv[i]))
-        @test all(vertical.nlayers_kv .== 0)
-        @test all(vertical.z_exp .== 400.0)
+        @test all(kv_profile.z_exp .== 400.0)
+        @test subsurface.ssfmax[i] ≈ 49.38558575188426f0
+        @test subsurface.ssf[i] ≈ 24810.460986497365f0
     end
 
     @testset "layered profile" begin
         config.input.vertical.ksat_profile = "layered"
         model = Wflow.initialize_sbm_model(config)
-        (; vertical) = model
-        z = vertical.zi[i]
-        @test Wflow.hydraulic_conductivity_at_depth(vertical, z, i, 2, "layered") ≈
-              vertical.kv[100][2]
-        @test Wflow.kh_layered_profile(vertical, 100.0, i, "layered") ≈ 47.508932674632355f0
-        @test vertical.nlayers_kv[i] == 4
-        @test vertical.z_layered == vertical.soilthickness
-        @test all(isnan.(vertical.z_exp))
+        (; soil) = model.vertical
+        (; kv_profile) = soil.parameters
+        (; subsurface) = model.lateral
+        z = soil.variables.zi[i]
+        kvfrac = soil.parameters.kvfrac
+        @test Wflow.hydraulic_conductivity_at_depth(kv_profile, kvfrac, z, i, 2) ≈
+              kv_profile.kv[i][2]
+        Wflow.kh_layered_profile!(soil, subsurface, kv_profile, 86400.0)
+        @test subsurface.kh_profile.kh[i] ≈ 47.508932674632355f0
+        @test subsurface.ssfmax[i] ≈ 30.237094380100316f0
+        @test subsurface.ssf[i] ≈ 14546.518932613191f0
     end
 
     @testset "layered exponential profile" begin
         config.input.vertical.ksat_profile = "layered_exponential"
         model = Wflow.initialize_sbm_model(config)
-        (; vertical) = model
-        z = vertical.zi[i]
-        @test Wflow.hydraulic_conductivity_at_depth(
-            vertical,
-            z,
-            i,
-            2,
-            "layered_exponential",
-        ) ≈ vertical.kv[i][2]
-        @test vertical.nlayers_kv[i] == 2
-        @test Wflow.kh_layered_profile(vertical, 100.0, i, "layered_exponential") ≈
-              33.76026208801769f0
-        @test all(vertical.z_layered[1:10] .== 400.0)
-        @test all(isnan.(vertical.z_exp))
+        (; soil) = model.vertical
+        (; kv_profile) = soil.parameters
+        (; subsurface) = model.lateral
+        z = soil.variables.zi[i]
+        kvfrac = soil.parameters.kvfrac
+        @test Wflow.hydraulic_conductivity_at_depth(kv_profile, kvfrac, z, i, 2) ≈
+              kv_profile.kv[i][2]
+        @test kv_profile.nlayers_kv[i] == 2
+        Wflow.kh_layered_profile!(soil, subsurface, kv_profile, 86400.0)
+        @test subsurface.kh_profile.kh[i] ≈ 33.76026208801769f0
+        @test all(kv_profile.z_layered[1:10] .== 400.0)
+        @test subsurface.ssfmax[i] ≈ 23.4840490395906f0
+        @test subsurface.ssf[i] ≈ 10336.88327617503f0
     end
 
-    model = Wflow.run_timestep(model)
-    model = Wflow.run_timestep(model)
     @testset "river flow layered exponential profile" begin
+        model = Wflow.initialize_sbm_model(config)
+        Wflow.run_timestep!(model)
+        Wflow.run_timestep!(model)
         q = model.lateral.river.q_av
         @test sum(q) ≈ 3159.38300016008f0
         @test q[1622] ≈ 0.0005972577112819149f0
         @test q[43] ≈ 10.017642376280731f0
     end
 
-    Wflow.close_files(model, delete_output = false)
+    Wflow.close_files(model; delete_output = false)
 end
diff --git a/test/run_sbm_gwf.jl b/test/run_sbm_gwf.jl
index 3af207643..60b77a8de 100644
--- a/test/run_sbm_gwf.jl
+++ b/test/run_sbm_gwf.jl
@@ -5,7 +5,7 @@ config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sbm_gwf_model(config)
 (; network) = model
 
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 # test if the first timestep was written to the CSV file
 flush(model.writer.csv_io)  # ensure the buffer is written fully to disk
@@ -21,21 +21,21 @@ end
     sbm = model.vertical
 
     @test model.clock.iteration == 1
-    @test sbm.theta_s[1] ≈ 0.44999998807907104f0
-    @test sbm.runoff[1] == 0.0
-    @test sbm.soilevap[1] == 0.0
-    @test sbm.transpiration[1] ≈ 0.30587632831650247f0
+    @test sbm.soil.parameters.theta_s[1] ≈ 0.44999998807907104f0
+    @test sbm.soil.variables.runoff[1] == 0.0
+    @test sbm.soil.variables.soilevap[1] == 0.0
+    @test sbm.soil.variables.transpiration[1] ≈ 0.30587632831650247f0
 end
 
 # run the second timestep
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 @testset "second timestep" begin
     sbm = model.vertical
-    @test sbm.theta_s[1] ≈ 0.44999998807907104f0
-    @test sbm.runoff[1] == 0.0
-    @test sbm.soilevap[1] == 0.0
-    @test sbm.transpiration[4] ≈ 0.7000003898938235f0
+    @test sbm.soil.parameters.theta_s[1] ≈ 0.44999998807907104f0
+    @test sbm.soil.variables.runoff[1] == 0.0
+    @test sbm.soil.variables.soilevap[1] == 0.0
+    @test sbm.soil.variables.transpiration[4] ≈ 0.7000003898938235f0
 end
 
 @testset "overland flow (kinematic wave)" begin
@@ -76,7 +76,7 @@ end
     @test collect(keys(model.lateral.subsurface)) == [:flow, :recharge, :river]
 end
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test local-inertial option for river flow routing
 tomlpath = joinpath(@__DIR__, "sbm_gwf_config.toml")
@@ -87,8 +87,8 @@ config.input.lateral.river.bankfull_elevation = "bankfull_elevation"
 config.input.lateral.river.bankfull_depth = "bankfull_depth"
 
 model = Wflow.initialize_sbm_gwf_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "river domain (local inertial)" begin
     q = model.lateral.river.q_av
@@ -100,7 +100,7 @@ model = Wflow.run_timestep(model)
     @test q[13] ≈ 0.0004638698607639214f0
     @test q[5] ≈ 0.0064668491697542786f0
 end
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test local-inertial option for river and overland flow routing
 tomlpath = joinpath(@__DIR__, "sbm_gwf_config.toml")
@@ -118,8 +118,8 @@ config.state.lateral.land.qx = "qx_land"
 config.state.lateral.land.qy = "qy_land"
 
 model = Wflow.initialize_sbm_gwf_model(config)
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "river and land domain (local inertial)" begin
     q = model.lateral.river.q_av
@@ -136,7 +136,7 @@ model = Wflow.run_timestep(model)
     @test all(qx .== 0.0f0)
     @test all(qy .== 0.0f0)
 end
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 # test with warm start
 tomlpath = joinpath(@__DIR__, "sbm_gwf_config.toml")
@@ -146,14 +146,14 @@ config.model.reinit = false
 model = Wflow.initialize_sbm_gwf_model(config)
 (; network) = model
 
-model = Wflow.run_timestep(model)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
+Wflow.run_timestep!(model)
 
 @testset "second timestep warm start" begin
     sbm = model.vertical
-    @test sbm.runoff[1] == 0.0
-    @test sbm.soilevap[1] ≈ 0.2889306511074693f0
-    @test sbm.transpiration[1] ≈ 0.8370726722706481f0
+    @test sbm.soil.variables.runoff[1] == 0.0
+    @test sbm.soil.variables.soilevap[1] ≈ 0.2889306511074693f0
+    @test sbm.soil.variables.transpiration[1] ≈ 0.8370726722706481f0
 end
 
 @testset "overland flow warm start (kinematic wave)" begin
@@ -186,4 +186,21 @@ end
     @test gw.drain.flux[1] ≈ 0.0
     @test gw.recharge.rate[19] ≈ -0.0014241196552847502f0
 end
-Wflow.close_files(model, delete_output = false)
+
+@testset "Exchange and grid location aquifer, recharge and constant head" begin
+    aquifer = model.lateral.subsurface.flow.aquifer
+    @test Wflow.exchange(aquifer.head) == true
+    @test Wflow.exchange(aquifer.k) == true
+    @test Wflow.grid_loc(aquifer, :head) == "node"
+    @test Wflow.grid_loc(aquifer, :k) == "node"
+    recharge = model.lateral.subsurface.recharge
+    @test Wflow.exchange(recharge.rate) == true
+    @test Wflow.exchange(recharge.flux) == true
+    @test Wflow.grid_loc(recharge, :rate) == "node"
+    @test Wflow.grid_loc(recharge, :flux) == "node"
+    constanthead = model.lateral.subsurface.flow.constanthead
+    @test Wflow.exchange(constanthead) == false
+    @test Wflow.grid_loc(constanthead, :head) == "node"
+end
+
+Wflow.close_files(model; delete_output = false)
diff --git a/test/run_sbm_gwf_piave.jl b/test/run_sbm_gwf_piave.jl
index 2f39b5a90..28e31a415 100644
--- a/test/run_sbm_gwf_piave.jl
+++ b/test/run_sbm_gwf_piave.jl
@@ -1,50 +1,56 @@
 tomlpath = joinpath(@__DIR__, "sbm_gwf_piave_demand_config.toml")
 config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sbm_gwf_model(config)
-model = Wflow.run_timestep(model)
-sbm = model.vertical
+Wflow.run_timestep!(model)
 
+(; paddy, nonpaddy, industry, livestock, domestic) = model.vertical.demand
+(; total_alloc, irri_alloc, nonirri_alloc, surfacewater_alloc, act_groundwater_abst) =
+    model.vertical.allocation.variables
 @testset "piave water demand and allocation first timestep" begin
-    sum_total_alloc = sum(sbm.allocation.total_alloc)
-    @test sum(sbm.allocation.irri_alloc) + sum(sbm.allocation.nonirri_alloc) ≈
-          sum_total_alloc
-    @test sum(sbm.allocation.surfacewater_alloc) ≈ 1016.4268126167575f0
-    @test sum(sbm.allocation.act_groundwater_abst) ≈ 182.2057678312209f0
-    @test sbm.paddy.h[[45, 76, 296]] ≈
+    sum_total_alloc = sum(total_alloc)
+    @test sum(irri_alloc) + sum(nonirri_alloc) ≈ sum_total_alloc
+    @test sum(surfacewater_alloc) ≈ 1016.4268126167575f0
+    @test sum(act_groundwater_abst) ≈ 182.2057678312209f0
+    @test paddy.variables.h[[45, 76, 296]] ≈
           [33.55659436283413f0, 44.11663357735189f0, 35.232731550849486f0]
-    @test sbm.paddy.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
-    @test sbm.paddy.demand_gross[[45, 76, 296]] ≈ [0.0, 0.0, 0.0]
-    @test sbm.nonpaddy.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
-    @test sbm.nonpaddy.demand_gross[[10, 33, 1293]] ≈ [3.3014913197447964f0, 0.0f0, 0.0f0]
-    @test sbm.industry.demand_gross[[1, end]] ≈ [0.2105557769536972f0, 0.0485190823674202f0]
-    @test sbm.industry.demand_net[[1, end]] ≈
+    @test paddy.parameters.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
+    @test paddy.variables.demand_gross[[45, 76, 296]] ≈ [0.0, 0.0, 0.0]
+    @test nonpaddy.parameters.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
+    @test nonpaddy.variables.demand_gross[[10, 33, 1293]] ≈
+          [3.3014913197447964f0, 0.0f0, 0.0f0]
+    @test industry.demand.demand_gross[[1, end]] ≈
+          [0.2105557769536972f0, 0.0485190823674202f0]
+    @test industry.demand.demand_net[[1, end]] ≈
           [0.05265098437666893f0, 0.012132546864449978f0]
-    @test sbm.industry.returnflow[[1, end]] ≈ [0.15790479257702827f0, 0.03638653550297022f0]
-    @test sbm.livestock.demand_gross[[1, end]] ≈
+    @test industry.variables.returnflow[[1, end]] ≈
+          [0.15790479257702827f0, 0.03638653550297022f0]
+    @test livestock.demand.demand_gross[[1, end]] ≈
           [9.896758274408057f-5, 6.352497439365834f-5]
-    @test sbm.livestock.demand_net[[1, end]] ≈ [9.896758274408057f-5, 6.352497439365834f-5]
-    @test sbm.livestock.returnflow[[1, end]] ≈ [0.0f0, 0.0f0]
-    @test sbm.domestic.demand_gross[[1, end]] ≈ [0.5389957427978516f0, 0.0f0]
-    @test sbm.domestic.demand_net[[1, end]] ≈ [0.33949509263038635f0, 0.0f0]
-    @test sbm.domestic.returnflow[[1, end]] ≈ [0.1995004952035704f0, 0.0f0]
+    @test livestock.demand.demand_net[[1, end]] ≈
+          [9.896758274408057f-5, 6.352497439365834f-5]
+    @test livestock.variables.returnflow[[1, end]] ≈ [0.0f0, 0.0f0]
+    @test domestic.demand.demand_gross[[1, end]] ≈ [0.5389957427978516f0, 0.0f0]
+    @test domestic.demand.demand_net[[1, end]] ≈ [0.33949509263038635f0, 0.0f0]
+    @test domestic.variables.returnflow[[1, end]] ≈ [0.1995004952035704f0, 0.0f0]
 end
 
-model = Wflow.run_timestep(model)
-sbm = model.vertical
+Wflow.run_timestep!(model)
+(; paddy, nonpaddy, industry, livestock, domestic) = model.vertical.demand
+(; total_alloc, irri_alloc, nonirri_alloc, surfacewater_alloc, act_groundwater_abst) =
+    model.vertical.allocation.variables
 
 @testset "piave water demand and allocation second timestep" begin
-    sum_total_alloc = sum(sbm.allocation.total_alloc)
-    @test sum(sbm.allocation.irri_alloc) + sum(sbm.allocation.nonirri_alloc) ≈
-          sum_total_alloc
-    @test sum(sbm.allocation.surfacewater_alloc) ≈ 1057.2573971583527f0
-    @test sum(sbm.allocation.act_groundwater_abst) ≈ 189.12436534660375f0
-    @test sbm.paddy.h[[45, 76, 296]] ≈
+    sum_total_alloc = sum(total_alloc)
+    @test sum(irri_alloc) + sum(nonirri_alloc) ≈ sum_total_alloc
+    @test sum(surfacewater_alloc) ≈ 1057.2573971583527f0
+    @test sum(act_groundwater_abst) ≈ 189.12436534660375f0
+    @test paddy.variables.h[[45, 76, 296]] ≈
           [28.197082339552537f0, 25.873022895247782f0, 30.066801639786547f0]
-    @test sbm.paddy.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
-    @test sbm.paddy.demand_gross[[45, 76, 296]] ≈ [0.0f0, 0.0f0, 0.0f0]
-    @test sbm.nonpaddy.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
-    @test sbm.nonpaddy.demand_gross[[10, 33, 1293]] ≈
+    @test paddy.parameters.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
+    @test paddy.variables.demand_gross[[45, 76, 296]] ≈ [0.0f0, 0.0f0, 0.0f0]
+    @test nonpaddy.parameters.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
+    @test nonpaddy.variables.demand_gross[[10, 33, 1293]] ≈
           [4.059144161330735f0, 0.0f0, 1.9399078662788196f0]
 end
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
diff --git a/test/run_sbm_piave.jl b/test/run_sbm_piave.jl
index 1d5a2fae6..a409cad08 100644
--- a/test/run_sbm_piave.jl
+++ b/test/run_sbm_piave.jl
@@ -3,8 +3,8 @@ function run_piave(model, steps)
     q = zeros(steps)
     ssf_vol = zeros(steps)
     riv_vol = zeros(steps)
-    for i = 1:steps
-        model = Wflow.run_timestep(model)
+    for i in 1:steps
+        Wflow.run_timestep!(model)
         ssf_vol[i] = mean(model.lateral.subsurface.volume)
         riv_vol[i] = mean(model.lateral.river.volume)
         q[i] = model.lateral.river.q_av[1]
@@ -16,13 +16,13 @@ tomlpath = joinpath(@__DIR__, "sbm_piave_demand_config.toml")
 config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sbm_model(config)
 q_demand, riv_vol_demand, ssf_vol_demand = run_piave(model, 30)
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 tomlpath = joinpath(@__DIR__, "sbm_piave_config.toml")
 config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sbm_model(config)
 q_, riv_vol, ssf_vol = run_piave(model, 30)
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 @testset "piave with and without water demand" begin
     idx = 1:3:28
@@ -103,51 +103,57 @@ end
 tomlpath = joinpath(@__DIR__, "sbm_piave_demand_config.toml")
 config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sbm_model(config)
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 sbm = model.vertical
+(; paddy, nonpaddy, industry, livestock, domestic) = model.vertical.demand
+(; total_alloc, irri_alloc, nonirri_alloc, surfacewater_alloc, act_groundwater_abst) =
+    sbm.allocation.variables
 
 @testset "piave water demand and allocation first timestep" begin
-    sum_total_alloc = sum(sbm.allocation.total_alloc)
-    @test sum(sbm.allocation.irri_alloc) + sum(sbm.allocation.nonirri_alloc) ≈
-          sum_total_alloc
-    @test sum(sbm.allocation.surfacewater_alloc) ≈ 1030.1528204311428f0
-    @test sum(sbm.allocation.act_groundwater_abst) ≈ 184.47031930837645f0
-    @test sbm.paddy.h[[45, 76, 296]] ≈
+    sum_total_alloc = sum(total_alloc)
+    @test sum(irri_alloc) + sum(nonirri_alloc) ≈ sum_total_alloc
+    @test sum(surfacewater_alloc) ≈ 1030.1528204311428f0
+    @test sum(act_groundwater_abst) ≈ 184.47031930837645f0
+    @test paddy.variables.h[[45, 76, 296]] ≈
           [34.759292485507515f0, 42.517504464353635f0, 35.83766686539591f0]
-    @test sbm.paddy.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
-    @test sbm.paddy.demand_gross[[45, 76, 296]] ≈ [0.0, 0.0, 0.0]
-    @test sbm.nonpaddy.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
-    @test sbm.nonpaddy.demand_gross[[10, 33, 1293]] ≈
+    @test paddy.parameters.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
+    @test paddy.variables.demand_gross[[45, 76, 296]] ≈ [0.0, 0.0, 0.0]
+    @test nonpaddy.parameters.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
+    @test nonpaddy.variables.demand_gross[[10, 33, 1293]] ≈
           [3.031574420740574f0, 1.8618934872392217f0, 0.42233065562148375f0]
-    @test sbm.industry.demand_gross[[1, end]] ≈ [0.2105557769536972f0, 0.0485190823674202f0]
-    @test sbm.industry.demand_net[[1, end]] ≈
+    @test industry.demand.demand_gross[[1, end]] ≈
+          [0.2105557769536972f0, 0.0485190823674202f0]
+    @test industry.demand.demand_net[[1, end]] ≈
           [0.05265098437666893f0, 0.012132546864449978f0]
-    @test sbm.industry.returnflow[[1, end]] ≈ [0.15790479257702827f0, 0.03638653550297022f0]
-    @test sbm.livestock.demand_gross[[1, end]] ≈
+    @test industry.variables.returnflow[[1, end]] ≈
+          [0.15790479257702827f0, 0.03638653550297022f0]
+    @test livestock.demand.demand_gross[[1, end]] ≈
           [9.896758274408057f-5, 6.352497439365834f-5]
-    @test sbm.livestock.demand_net[[1, end]] ≈ [9.896758274408057f-5, 6.352497439365834f-5]
-    @test sbm.livestock.returnflow[[1, end]] ≈ [0.0f0, 0.0f0]
-    @test sbm.domestic.demand_gross[[1, end]] ≈ [0.5389957427978516f0, 0.0f0]
-    @test sbm.domestic.demand_net[[1, end]] ≈ [0.33949509263038635f0, 0.0f0]
-    @test sbm.domestic.returnflow[[1, end]] ≈ [0.1995004952035704f0, 0.0f0]
+    @test livestock.demand.demand_net[[1, end]] ≈
+          [9.896758274408057f-5, 6.352497439365834f-5]
+    @test livestock.variables.returnflow[[1, end]] ≈ [0.0f0, 0.0f0]
+    @test domestic.demand.demand_gross[[1, end]] ≈ [0.5389957427978516f0, 0.0f0]
+    @test domestic.demand.demand_net[[1, end]] ≈ [0.33949509263038635f0, 0.0f0]
+    @test domestic.variables.returnflow[[1, end]] ≈ [0.1995004952035704f0, 0.0f0]
 end
 
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 sbm = model.vertical
-
+(; paddy, nonpaddy, industry, livestock, domestic) = model.vertical.demand
+(; total_alloc, irri_alloc, nonirri_alloc, surfacewater_alloc, act_groundwater_abst) =
+    sbm.allocation.variables
 @testset "piave water demand and allocation second timestep" begin
-    sum_total_alloc = sum(sbm.allocation.total_alloc)
-    @test sum(sbm.allocation.irri_alloc) + sum(sbm.allocation.nonirri_alloc) ≈
-          sum_total_alloc
-    @test sum(sbm.allocation.surfacewater_alloc) ≈ 940.1941924010235f0
-    @test sum(sbm.allocation.act_groundwater_abst) ≈ 163.59123515939052f0
-    @test sbm.paddy.h[[45, 76, 296]] ≈
+    sum_total_alloc = sum(total_alloc)
+    @test sum(irri_alloc) + sum(nonirri_alloc) ≈ sum_total_alloc
+    @test sum(surfacewater_alloc) ≈ 940.1941924010235f0
+    @test sum(act_groundwater_abst) ≈ 163.59123515939052f0
+    @test paddy.variables.h[[45, 76, 296]] ≈
           [29.50674105890283f0, 38.11966817469463f0, 30.679920457042897f0]
-    @test sbm.paddy.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
-    @test sbm.paddy.demand_gross[[45, 76, 296]] ≈ [0.0f0, 0.0f0, 0.0f0]
-    @test sbm.nonpaddy.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
-    @test sbm.nonpaddy.demand_gross[[10, 33, 1293]] ≈
+    @test paddy.parameters.irrigation_trigger[[45, 76, 296]] == [1, 1, 1]
+    @test paddy.variables.demand_gross[[45, 76, 296]] ≈ [0.0f0, 0.0f0, 0.0f0]
+    @test nonpaddy.parameters.irrigation_trigger[[10, 33, 1293]] == [1, 1, 1]
+    @test nonpaddy.variables.demand_gross[[10, 33, 1293]] ≈
           [3.8551909476218054f0, 0.0f0, 1.3531153828385536f0]
 end
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
diff --git a/test/run_sediment.jl b/test/run_sediment.jl
index f8b323996..58c0096bc 100644
--- a/test/run_sediment.jl
+++ b/test/run_sediment.jl
@@ -6,7 +6,7 @@ config = Wflow.Config(tomlpath)
 model = Wflow.initialize_sediment_model(config)
 (; network) = model
 
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 @testset "first timestep sediment model (vertical)" begin
     eros = model.vertical
@@ -21,7 +21,7 @@ model = Wflow.run_timestep(model)
 end
 
 # run the second timestep
-model = Wflow.run_timestep(model)
+Wflow.run_timestep!(model)
 
 @testset "second timestep sediment model (vertical)" begin
     eros = model.vertical
@@ -51,4 +51,20 @@ end
     @test lat.river.outclay[5649] ≈ 2.359031898208781f-9
 end
 
+@testset "Exchange and grid location sediment" begin
+    @test Wflow.exchange(model.vertical.n) == false
+    @test Wflow.exchange(model.vertical.erosk) == true
+    @test Wflow.exchange(model.vertical.leaf_area_index) == true
+    @test Wflow.grid_loc(model.vertical, :n) == "none"
+    @test Wflow.grid_loc(model.vertical, :erosk) == "node"
+    @test Wflow.grid_loc(model.vertical, :leaf_area_index) == "node"
+    land = model.lateral.land
+    @test Wflow.exchange(land.n) == false
+    @test Wflow.exchange(land.soilloss) == true
+    @test Wflow.exchange(land.inlandsed) == true
+    @test Wflow.grid_loc(land, :n) == "none"
+    @test Wflow.grid_loc(land, :soilloss) == "node"
+    @test Wflow.grid_loc(land, :inlandsed) == "node"
+end
+
 Wflow.close_files(model)
diff --git a/test/runtests.jl b/test/runtests.jl
index 67947e377..6981af08d 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -10,11 +10,11 @@ using Wflow
 using Base.MathConstants: eulergamma
 using Base.Threads
 using BasicModelInterface
-import Polynomials
+using Polynomials: Polynomials
 using DelimitedFiles
 using LoggingExtras
 using QuadGK
-import Aqua
+using Aqua: Aqua
 
 const BMI = BasicModelInterface
 const Float = Wflow.Float
@@ -37,11 +37,7 @@ end
 staticmaps_rhine_path = testdata(v"0.1", "staticmaps.nc", "staticmaps-rhine.nc")
 staticmaps_moselle_path =
     testdata(v"0.2.9", "staticmaps-moselle.nc", "staticmaps-moselle.nc")
-staticmaps_lahn_path = testdata(v"0.2.1", "staticmaps-lahn.nc", "staticmaps-lahn.nc")
-staticmaps_meuse_path =
-    testdata(v"0.2.8", "staticmaps_flex_meuse.nc", "staticmaps_flex_meuse.nc")
 forcing_moselle_path = testdata(v"0.2.6", "forcing-moselle.nc", "forcing-moselle.nc")
-forcing_lahn_path = testdata(v"0.2", "forcing-lahn.nc", "forcing-lahn.nc")
 forcing_moselle_sed_path =
     testdata(v"0.2.3", "forcing-moselle-sed.nc", "forcing-moselle-sed.nc")
 staticmaps_moselle_sed_path =
@@ -59,7 +55,6 @@ forcing_sbm_gw_path = testdata(
     "forcing-sbm-groundwater-part2.nc",
     "forcing-sbm-groundwater-part2.nc",
 )
-forcing_meuse_path = testdata(v"0.2.8", "forcing_meuse.nc", "forcing_meuse.nc")
 staticmaps_sbm_gw_path =
     testdata(v"0.2.3", "staticmaps-sbm-groundwater.nc", "staticmaps-sbm-groundwater.nc")
 instates_sbm_gw_path =
@@ -72,7 +67,8 @@ forcing_calendar_noleap_path =
 forcing_piave_path = testdata(v"0.2.9", "inmaps-era5-2010-piave.nc", "forcing-piave.nc")
 staticmaps_piave_path = testdata(v"0.2.9", "staticmaps-piave.nc", "staticmaps-piave.nc")
 instates_piave_path = testdata(v"0.2.9", "instates-piave.nc", "instates-piave.nc")
-instates_piave_gwf_path = testdata(v"0.2.9", "instates-piave-gwf.nc", "instates-piave-gwf.nc")
+instates_piave_gwf_path =
+    testdata(v"0.2.9", "instates-piave-gwf.nc", "instates-piave-gwf.nc")
 
 include("testing_utils.jl")
 
@@ -89,7 +85,6 @@ with_logger(NullLogger()) do
         include("run_sbm.jl")
         include("run_sbm_piave.jl")
         include("run_sbm_gwf_piave.jl")
-        include("run_hbv.jl")
         include("run_sbm_gwf.jl")
         include("run.jl")
         include("groundwater.jl")
@@ -97,7 +92,6 @@ with_logger(NullLogger()) do
         include("bmi.jl")
         include("run_sediment.jl")
         include("subdomains.jl")
-        include("run_flextopo.jl")
 
         Aqua.test_all(Wflow; ambiguities = false, persistent_tasks = false)
     end
diff --git a/test/sbm_config.toml b/test/sbm_config.toml
index 3ab5c92d0..39898a488 100644
--- a/test/sbm_config.toml
+++ b/test/sbm_config.toml
@@ -19,14 +19,18 @@ path_output = "outstates-moselle.nc"
 # if listed, the variable must be present in the NetCDF or error
 # if not listed, the variable can get a default value if it has one
 
-[state.vertical]
-canopystorage = "canopystorage"
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[state.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [state.lateral.river]
 h = "h_river"
 h_av = "h_av_river"
@@ -56,42 +60,52 @@ subcatchment = "wflow_subcatch"
 # specify the internal IDs of the parameters which vary over time
 # the external name mapping needs to be below together with the other mappings
 forcing = [
-  "vertical.precipitation",
-  "vertical.temperature",
-  "vertical.potential_evaporation",
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
 ]
 
-cyclic = ["vertical.leaf_area_index"]
+cyclic = ["vertical.vegetation_parameter_set.leaf_area_index"]
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
 
-[input.vertical]
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.soil.parameters]
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
 f = "f"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-leaf_area_index = "LAI"
+water_holding_capacity = "WHC"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_r = "thetaR"
-theta_s = "thetaS"
+cfmax = "Cfmax"
 
-[input.vertical.kv_0]
+[input.vertical.soil.parameters.kv_0]
 netcdf.variable.name = "KsatVer"
 scale = 1.0
 offset = 0.0
@@ -135,14 +149,18 @@ min_streamorder_land = 5
 [output]
 path = "output_moselle.nc"
 
-[output.vertical]
-canopystorage = "canopystorage"
+[output.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[output.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[output.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [output.lateral.river]
 h = "h_river"
 q = "q_river"
@@ -170,14 +188,14 @@ coordinate.x = 6.255
 coordinate.y = 50.012
 name = "temp_coord"
 location = "temp_bycoord"
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[netcdf.variable]]
 location = "temp_byindex"
 name = "temp_index"
 index.x = 100
 index.y = 264
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [csv]
 path = "output_moselle.csv"
@@ -196,20 +214,20 @@ parameter = "lateral.river.reservoir.volume"
 coordinate.x = 6.255
 coordinate.y = 50.012
 header = "temp_bycoord"
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[csv.column]]
 coordinate.x = 6.255
 coordinate.y = 50.012
 header = "vwc_layer2_bycoord"
-parameter = "vertical.vwc"
+parameter = "vertical.soil.variables.vwc"
 layer = 2
 
 [[csv.column]]
 header = "temp_byindex"
 index.x = 100
 index.y = 264
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[csv.column]]
 header = "Q"
@@ -219,12 +237,24 @@ parameter = "lateral.river.q"
 [[csv.column]]
 header = "recharge"
 map = "subcatchment"
-parameter = "vertical.recharge"
+parameter = "vertical.soil.variables.recharge"
 reducer = "mean"
 
 [API]
 components = [
-  "vertical",
+  "vertical.atmospheric_forcing",
+  "vertical.vegetation_parameter_set",
+  "vertical.runoff.boundary_conditions",
+  "vertical.runoff.variables",
+  "vertical.runoff.parameters",
+  "vertical.soil.boundary_conditions",
+  "vertical.soil.variables",
+  "vertical.soil.parameters",
+  "vertical.interception.variables",
+  "vertical.interception.parameters",
+  "vertical.snow.boundary_conditions",
+  "vertical.snow.variables",
+  "vertical.snow.parameters",
   "lateral.subsurface",
   "lateral.land",
   "lateral.river",
diff --git a/test/sbm_gw.toml b/test/sbm_gw.toml
index b7431955d..a18965254 100644
--- a/test/sbm_gw.toml
+++ b/test/sbm_gw.toml
@@ -18,14 +18,18 @@ path_output = "outstates-moselle.nc"
 # if listed, the variable must be present in the NetCDF or error
 # if not listed, the variable can get a default value if it has one
 
-[state.vertical]
-canopystorage = "canopystorage"
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables] 
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[state.vertical.snow.variables] 
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [state.lateral.river]
 h = "h_river"
 h_av = "h_av_river"
@@ -52,14 +56,29 @@ subcatchment = "wflow_subcatch"
 # specify the internal IDs of the parameters which vary over time
 # the external name mapping needs to be below together with the other mappings
 forcing = [
-  "vertical.precipitation",
-  "vertical.temperature",
-  "vertical.potential_evaporation",
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
 ]
 
-cyclic = ["vertical.leaf_area_index"]
+cyclic = ["vertical.vegetation_parameter_set.leaf_area_index"]
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
 
-[input.vertical]
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.soil.parameters]
 c = "c"
 cf_soil = "cf_soil"
 cfmax = "Cfmax"
@@ -67,26 +86,19 @@ e_r = "EoverR"
 f = "f"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
 kv_0 = "KsatVer"
-leaf_area_index = "LAI"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
 rootingdepth = "RootingDepth"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+theta_r = "thetaR"
+theta_s = "thetaS"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_r = "thetaR"
-theta_s = "thetaS"
 
 [input.lateral.river]
 length = "wflow_riverlength"
@@ -120,14 +132,18 @@ type = "sbm"
 [output]
 path = "output_moselle.nc"
 
-[output.vertical]
-canopystorage = "canopystorage"
+[output.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[output.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[output.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [output.lateral.river]
 h = "h_river"
 q = "q_river"
@@ -152,14 +168,14 @@ coordinate.x = 6.255
 coordinate.y = 50.012
 name = "temp_coord"
 location = "temp_bycoord"
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[netcdf.variable]]
 location = "temp_byindex"
 name = "temp_index"
 index.x = 100
 index.y = 264
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [csv]
 path = "output_moselle.csv"
@@ -178,13 +194,13 @@ parameter = "lateral.river.reservoir.volume"
 coordinate.x = 6.255
 coordinate.y = 50.012
 header = "temp_bycoord"
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[csv.column]]
 header = "temp_byindex"
 index.x = 100
 index.y = 264
-parameter = "vertical.temperature"
+parameter = "vertical.atmospheric_forcing.temperature"
 
 [[csv.column]]
 header = "Q"
@@ -194,7 +210,7 @@ parameter = "lateral.river.q"
 [[csv.column]]
 header = "recharge"
 map = "subcatchment"
-parameter = "vertical.recharge"
+parameter = "vertical.soil.variables.recharge"
 reducer = "mean"
 
 [API]
diff --git a/test/sbm_gwf_config.toml b/test/sbm_gwf_config.toml
index d94b203bf..f08431236 100644
--- a/test/sbm_gwf_config.toml
+++ b/test/sbm_gwf_config.toml
@@ -18,8 +18,10 @@ path_output = "outstates-example-sbm-gwf.nc"
 # if listed, the variable must be present in the NetCDF or error
 # if not listed, the variable can get a default value if it has one
 
-[state.vertical]
-canopystorage = "canopystorage"
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
 ustorelayerdepth = "ustorelayerdepth"
 
@@ -49,21 +51,26 @@ altitude = "wflow_dem"
 # specify the internal IDs of the parameters which vary over time
 # the external name mapping needs to be below together with the other mappings
 forcing = [
-  "vertical.precipitation",
-  "vertical.potential_evaporation",
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.potential_evaporation",
 ]
 
-[input.vertical]
+[input.vertical.vegetation_parameter_set]
+rootingdepth = "rootingdepth"
 canopygapfraction = "canopygapfraction"
 cmax = "cmax"
+
+[input.vertical.interception.parameters]
 e_r = "EoverR"
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "PET"
+precipitation = "P"
+
+[input.vertical.soil.parameters]
 infiltcapsoil = "InfiltCapSoil"
 kv_0 = "kv"
-m = "M"
 pathfrac = "PathFrac"
-potential_evaporation = "PET"
-precipitation = "P"
-rootingdepth = "rootingdepth"
 soilthickness = "soilthickness"
 theta_r = "thetaR"
 theta_s = "thetaS"
@@ -102,12 +109,16 @@ type = "sbm_gwf"
 [output]
 path = "output_example-sbm-gwf.nc"
 
-[output.vertical]
-canopystorage = "canopystorage"
+[output.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[output.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-soilthickness = "soilthickness"
 ustorelayerdepth = "ustorelayerdepth"
 
+[output.vertical.soil.parameters]
+soilthickness = "soilthickness"
+
 [output.lateral.river]
 q = "q"
 
diff --git a/test/sbm_gwf_piave_demand_config.toml b/test/sbm_gwf_piave_demand_config.toml
index fa7e4cb44..a9a9cf25a 100644
--- a/test/sbm_gwf_piave_demand_config.toml
+++ b/test/sbm_gwf_piave_demand_config.toml
@@ -18,11 +18,26 @@ ldd = "wflow_ldd"
 river_location = "wflow_river"
 altitude = "wflow_dem"
 subcatchment = "wflow_subcatch"
-forcing = [ "vertical.precipitation", "vertical.temperature", "vertical.potential_evaporation",]
-cyclic = [ "vertical.leaf_area_index", "vertical.domestic.demand_gross", "vertical.domestic.demand_net", "vertical.industry.demand_gross", "vertical.industry.demand_net", "vertical.livestock.demand_gross", "vertical.livestock.demand_net", "vertical.paddy.irrigation_trigger", "vertical.nonpaddy.irrigation_trigger",]
 gauges = "wflow_gauges"
 gauges_grdc = "wflow_gauges_grdc"
 
+forcing = [
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
+]
+cyclic = [ 
+    "vertical.vegetation_parameter_set.leaf_area_index", 
+    "vertical.demand.domestic.demand.demand_gross", 
+    "vertical.demand.domestic.demand.demand_net", 
+    "vertical.demand.industry.demand.demand_gross", 
+    "vertical.demand.industry.demand.demand_net", 
+    "vertical.demand.livestock.demand.demand_gross", 
+    "vertical.demand.livestock.demand.demand_net", 
+    "vertical.demand.paddy.parameters.irrigation_trigger", 
+    "vertical.demand.nonpaddy.parameters.irrigation_trigger",
+]
+
 [model]
 type = "sbm_gwf"
 constanthead = true
@@ -38,65 +53,81 @@ kw_land_tstep = 3600
 thicknesslayers = [ 50, 100, 50, 200, 800,]
 river_routing = "kinematic-wave"
 
-[state.vertical]
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
-snowwater = "snowwater"
-canopystorage = "canopystorage"
-glacierstore = "glacierstore"
 
-[state.vertical.paddy]
+[state.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
+[state.vertical.glacier.variables]
+glacier_store = "glacierstore"
+
+[state.vertical.demand.paddy.variables]
 h = "h_paddy"
 
 [state.lateral.subsurface.flow.aquifer]
 head = "head"
 
-[input.vertical]
+[input.vertical.glacier.parameters]
+glacier_frac = "wflow_glacierfrac"
+g_cfmax = "G_Cfmax"
+g_tt = "G_TT"
+g_sifrac = "G_SIfrac"
+
+[input.vertical.glacier.variables]
+glacier_store = "wflow_glacierstore"
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+kc = "crop_factor"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.soil.parameters]
 alpha_h1 = "alpha_h1"
-altitude = "wflow_dem"
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
 f = "f"
+h1 = "h1"
+h2 = "h2"
+h3_high = "h3_high"
+h3_low = "h3_low"
+h4 = "h4"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-kv_0 = "KsatVer"
-leaf_area_index = "LAI"
-m = "M_"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
-soilminthickness = "SoilMinThickness"
 soilthickness = "SoilThickness_gw"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+kv_0 = "KsatVer"
+kvfrac = "kvfrac"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_s = "thetaS"
-theta_r = "thetaR"
-glacierstore = "wflow_glacierstore"
-glacierfrac = "wflow_glacierfrac"
-g_cfmax = "G_Cfmax"
-g_tt = "G_TT"
-g_sifrac = "G_SIfrac"
-kc = "crop_factor"
-kvfrac = "kvfrac"
-h1 = "h1"
-h2 = "h2"
-h3_high = "h3_high"
-h3_low = "h3_low"
-h4 = "h4"
+cfmax = "Cfmax"
 
 [model.water_demand]
 domestic = true
@@ -118,27 +149,27 @@ q = "q_land"
 h = "h_land"
 h_av = "h_av_land"
 
-[input.vertical.allocation]
+[input.vertical.allocation.parameters]
 areas = "allocation_areas"
 frac_sw_used = "SurfaceWaterFrac"
 
-[input.vertical.domestic]
+[input.vertical.demand.domestic.demand]
 demand_gross = "dom_gross"
 demand_net = "dom_net"
 
-[input.vertical.industry]
+[input.vertical.demand.industry.demand]
 demand_gross = "ind_gross"
 demand_net = "ind_net"
 
-[input.vertical.livestock]
+[input.vertical.demand.livestock.demand]
 demand_gross = "lsk_gross"
 demand_net = "lsk_net"
 
-[input.vertical.paddy]
+[input.vertical.demand.paddy.parameters]
 irrigation_areas = "paddy_irrigation_areas"
 irrigation_trigger = "irrigation_trigger"
 
-[input.vertical.nonpaddy]
+[input.vertical.demand.nonpaddy.parameters]
 irrigation_areas = "nonpaddy_irrigation_areas"
 irrigation_trigger = "irrigation_trigger"
 
@@ -169,7 +200,7 @@ path = "output-piave-gwf.nc"
 [output.lateral.river]
 q_av = "q_river"
 
-[output.vertical]
+[output.vertical.soil.variables]
 zi = "zi"
 
 [output.lateral.subsurface.flow.aquifer]
diff --git a/test/sbm_piave_config.toml b/test/sbm_piave_config.toml
index 22c5e30c8..cee9e3685 100644
--- a/test/sbm_piave_config.toml
+++ b/test/sbm_piave_config.toml
@@ -17,11 +17,17 @@ path_static = "staticmaps-piave.nc"
 ldd = "wflow_ldd"
 river_location = "wflow_river"
 subcatchment = "wflow_subcatch"
-forcing = [ "vertical.precipitation", "vertical.temperature", "vertical.potential_evaporation",]
-cyclic = [ "vertical.leaf_area_index",]
 gauges = "wflow_gauges"
 gauges_grdc = "wflow_gauges_grdc"
 
+forcing = [
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
+]
+
+cyclic = ["vertical.vegetation_parameter_set.leaf_area_index"]
+
 [model]
 type = "sbm"
 masswasting = true
@@ -36,58 +42,74 @@ kw_land_tstep = 3600
 thicknesslayers = [ 50, 100, 50, 200, 800,]
 river_routing = "kinematic-wave"
 
-[state.vertical]
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
-snowwater = "snowwater"
-canopystorage = "canopystorage"
-glacierstore = "glacierstore"
 
-[input.vertical]
+[state.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
+[state.vertical.glacier.variables]
+glacier_store = "glacierstore"
+
+[input.vertical.glacier.parameters]
+glacier_frac = "wflow_glacierfrac"
+g_cfmax = "G_Cfmax"
+g_tt = "G_TT"
+g_sifrac = "G_SIfrac"
+
+[input.vertical.glacier.variables]
+glacier_store = "wflow_glacierstore"
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+kc = "crop_factor"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.soil.parameters]
 alpha_h1 = "alpha_h1"
-altitude = "wflow_dem"
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
 f = "f"
+h1 = "h1"
+h2 = "h2"
+h3_high = "h3_high"
+h3_low = "h3_low"
+h4 = "h4"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-kv_0 = "KsatVer"
-leaf_area_index = "LAI"
-m = "M_"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
-soilminthickness = "SoilMinThickness"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+kv_0 = "KsatVer"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_s = "thetaS"
-theta_r = "thetaR"
-glacierstore = "wflow_glacierstore"
-glacierfrac = "wflow_glacierfrac"
-g_cfmax = "G_Cfmax"
-g_tt = "G_TT"
-g_sifrac = "G_SIfrac"
-kc = "crop_factor"
-h1 = "h1"
-h2 = "h2"
-h3_high = "h3_high"
-h3_low = "h3_low"
-h4 = "h4"
+cfmax = "Cfmax"
 
 [state.lateral.river]
 q = "q_river"
diff --git a/test/sbm_piave_demand_config.toml b/test/sbm_piave_demand_config.toml
index 141ef0105..b1e530b87 100644
--- a/test/sbm_piave_demand_config.toml
+++ b/test/sbm_piave_demand_config.toml
@@ -17,11 +17,26 @@ path_static = "staticmaps-piave.nc"
 ldd = "wflow_ldd"
 river_location = "wflow_river"
 subcatchment = "wflow_subcatch"
-forcing = [ "vertical.precipitation", "vertical.temperature", "vertical.potential_evaporation",]
-cyclic = [ "vertical.leaf_area_index", "vertical.domestic.demand_gross", "vertical.domestic.demand_net", "vertical.industry.demand_gross", "vertical.industry.demand_net", "vertical.livestock.demand_gross", "vertical.livestock.demand_net", "vertical.paddy.irrigation_trigger", "vertical.nonpaddy.irrigation_trigger",]
 gauges = "wflow_gauges"
 gauges_grdc = "wflow_gauges_grdc"
 
+forcing = [
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
+]
+cyclic = [ 
+    "vertical.vegetation_parameter_set.leaf_area_index", 
+    "vertical.demand.domestic.demand.demand_gross", 
+    "vertical.demand.domestic.demand.demand_net", 
+    "vertical.demand.industry.demand.demand_gross", 
+    "vertical.demand.industry.demand.demand_net", 
+    "vertical.demand.livestock.demand.demand_gross", 
+    "vertical.demand.livestock.demand.demand_net", 
+    "vertical.demand.paddy.parameters.irrigation_trigger", 
+    "vertical.demand.nonpaddy.parameters.irrigation_trigger",
+]
+
 [model]
 type = "sbm"
 masswasting = true
@@ -36,62 +51,78 @@ kw_land_tstep = 3600
 thicknesslayers = [ 50, 100, 50, 200, 800,]
 river_routing = "kinematic-wave"
 
-[state.vertical]
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
-snowwater = "snowwater"
-canopystorage = "canopystorage"
-glacierstore = "glacierstore"
 
-[state.vertical.paddy]
+[state.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
+[state.vertical.glacier.variables]
+glacier_store = "glacierstore"
+
+[state.vertical.demand.paddy.variables]
 h = "h_paddy"
 
-[input.vertical]
+[input.vertical.glacier.parameters]
+glacier_frac = "wflow_glacierfrac"
+g_cfmax = "G_Cfmax"
+g_tt = "G_TT"
+g_sifrac = "G_SIfrac"
+
+[input.vertical.glacier.variables]
+glacier_store = "wflow_glacierstore"
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+kc = "crop_factor"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.soil.parameters]
 alpha_h1 = "alpha_h1"
-altitude = "wflow_dem"
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
 f = "f"
+h1 = "h1"
+h2 = "h2"
+h3_high = "h3_high"
+h3_low = "h3_low"
+h4 = "h4"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-kv_0 = "KsatVer"
-leaf_area_index = "LAI"
-m = "M_"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
-soilminthickness = "SoilMinThickness"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+kv_0 = "KsatVer"
+kvfrac = "kvfrac"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_s = "thetaS"
-theta_r = "thetaR"
-glacierstore = "wflow_glacierstore"
-glacierfrac = "wflow_glacierfrac"
-g_cfmax = "G_Cfmax"
-g_tt = "G_TT"
-g_sifrac = "G_SIfrac"
-kc = "crop_factor"
-kvfrac = "kvfrac"
-h1 = "h1"
-h2 = "h2"
-h3_high = "h3_high"
-h3_low = "h3_low"
-h4 = "h4"
+cfmax = "Cfmax"
 
 [model.water_demand]
 domestic = true
@@ -113,27 +144,27 @@ q = "q_land"
 h = "h_land"
 h_av = "h_av_land"
 
-[input.vertical.allocation]
+[input.vertical.allocation.parameters]
 areas = "allocation_areas"
 frac_sw_used = "SurfaceWaterFrac"
 
-[input.vertical.domestic]
+[input.vertical.demand.domestic.demand]
 demand_gross = "dom_gross"
 demand_net = "dom_net"
 
-[input.vertical.industry]
+[input.vertical.demand.industry.demand]
 demand_gross = "ind_gross"
 demand_net = "ind_net"
 
-[input.vertical.livestock]
+[input.vertical.demand.livestock.demand]
 demand_gross = "lsk_gross"
 demand_net = "lsk_net"
 
-[input.vertical.paddy]
+[input.vertical.demand.paddy.parameters]
 irrigation_areas = "paddy_irrigation_areas"
 irrigation_trigger = "irrigation_trigger"
 
-[input.vertical.nonpaddy]
+[input.vertical.demand.nonpaddy.parameters]
 irrigation_areas = "nonpaddy_irrigation_areas"
 irrigation_trigger = "irrigation_trigger"
 
@@ -157,7 +188,7 @@ path = "output-piave-demand.nc"
 [output.lateral.river]
 q_av = "q_river"
 
-[output.vertical]
+[output.vertical.soil.variables]
 zi = "zi"
 
 [csv]
@@ -177,10 +208,10 @@ parameter = "lateral.river.q_av"
 coordinate.x = 12.7243
 coordinate.y = 45.5851
 header = "paddy_h_bycoord"
-parameter = "vertical.paddy.h"
+parameter = "vertical.demand.paddy.variables.h"
 
 [[csv.column]]
 coordinate.x = 12.7243
 coordinate.y = 45.5851
 header = "irri_bycoord"
-parameter = "vertical.allocation.irri_alloc"
\ No newline at end of file
+parameter = "vertical.allocation.variables.irri_alloc"
\ No newline at end of file
diff --git a/test/sbm_simple.toml b/test/sbm_simple.toml
index 7916cf114..b6e16d6d1 100644
--- a/test/sbm_simple.toml
+++ b/test/sbm_simple.toml
@@ -20,44 +20,50 @@ subcatchment = "wflow_subcatch"
 # specify the internal IDs of the parameters which vary over time
 # the external name mapping needs to be below together with the other mappings
 forcing = [
-  "vertical.precipitation",
-  "vertical.temperature",
-  "vertical.potential_evaporation",
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
 ]
 
-cyclic = ["vertical.leaf_area_index"]
+cyclic = ["vertical.vegetation_parameter_set.leaf_area_index"]
 
-[input.vertical]
-altitude = "wflow_dem"
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
+
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.soil.parameters]
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
+f = "f"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-kv_0 = "KsatVer"
-leaf_area_index = "LAI"
-m = "M"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
-soilminthickness = "SoilMinThickness"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+kv_0 = "KsatVer"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-w_soil = "wflow_soil"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_r = "thetaR"
-theta_s = "thetaS"
+cfmax = "Cfmax"
 
 [input.lateral.river]
 length = "wflow_riverlength"
@@ -97,5 +103,5 @@ parameter = "lateral.river.q"
 
 [[csv.column]]
 header = "recharge"
-parameter = "vertical.recharge"
+parameter = "vertical.soil.variables.recharge"
 reducer = "mean"
diff --git a/test/sbm_swf_config.toml b/test/sbm_swf_config.toml
index 92872f36f..79ec88b3c 100644
--- a/test/sbm_swf_config.toml
+++ b/test/sbm_swf_config.toml
@@ -14,14 +14,18 @@ dir_output = "data/output"
 # if listed, the variable must be present in the NetCDF or error
 # if not listed, the variable can get a default value if it has one
 
-[state.vertical]
-canopystorage = "canopystorage"
+[state.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[state.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[state.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [state.lateral.river]
 h = "h_river"
 h_av = "h_av_river"
@@ -52,41 +56,50 @@ subcatchment = "wflow_subcatch"
 # specify the internal IDs of the parameters which vary over time
 # the external name mapping needs to be below together with the other mappings
 forcing = [
-  "vertical.precipitation",
-  "vertical.temperature",
-  "vertical.potential_evaporation",
+  "vertical.atmospheric_forcing.precipitation",
+  "vertical.atmospheric_forcing.temperature",
+  "vertical.atmospheric_forcing.potential_evaporation",
 ]
 
-cyclic = ["vertical.leaf_area_index"]
+cyclic = ["vertical.vegetation_parameter_set.leaf_area_index"]
+
+[input.vertical.vegetation_parameter_set]
+leaf_area_index = "LAI"
+kext = "Kext"
+storage_specific_leaf = "Sl"
+storage_wood = "Swood"
+rootingdepth = "RootingDepth"
+
+[input.vertical.interception.parameters]
+e_r = "EoverR"
+
+[input.vertical.atmospheric_forcing]
+potential_evaporation = "pet"
+precipitation = "precip"
+temperature = "temp"
 
-[input.vertical]
+[input.vertical.runoff.parameters]
+waterfrac = "WaterFrac"
+
+[input.vertical.soil.parameters]
 c = "c"
 cf_soil = "cf_soil"
-cfmax = "Cfmax"
-e_r = "EoverR"
 f = "f"
 infiltcappath = "InfiltCapPath"
 infiltcapsoil = "InfiltCapSoil"
-kext = "Kext"
-kv_0 = "KsatVer"
-leaf_area_index = "LAI"
+theta_r = "thetaR"
+theta_s = "thetaS"
 maxleakage = "MaxLeakage"
 pathfrac = "PathFrac"
-potential_evaporation = "pet"
-precipitation = "precip"
 rootdistpar = "rootdistpar"
-rootingdepth = "RootingDepth"
 soilthickness = "SoilThickness"
-specific_leaf = "Sl"
-storage_wood = "Swood"
-temperature = "temp"
+kv_0 = "KsatVer"
+
+[input.vertical.snow.parameters]
 tt = "TT"
 tti = "TTI"
 ttm = "TTM"
-water_holding_capacity = "WHC"
-waterfrac = "WaterFrac"
-theta_r = "thetaR"
-theta_s = "thetaS"
+cfmax = "Cfmax"
 
 [input.lateral.river]
 length = "wflow_riverlength"
@@ -127,14 +140,18 @@ type = "sbm"
 [output]
 path = "output_moselle_swf.nc"
 
-[output.vertical]
-canopystorage = "canopystorage"
+[output.vertical.interception.variables]
+canopy_storage = "canopystorage"
+
+[output.vertical.soil.variables]
 satwaterdepth = "satwaterdepth"
-snow = "snow"
-snowwater = "snowwater"
 tsoil = "tsoil"
 ustorelayerdepth = "ustorelayerdepth"
 
+[output.vertical.snow.variables]
+snow_storage = "snow"
+snow_water = "snowwater"
+
 [output.lateral.river]
 h = "h_river"
 h_av = "hav_river"
diff --git a/test/subdomains.jl b/test/subdomains.jl
index 12b5170b3..126e6f900 100644
--- a/test/subdomains.jl
+++ b/test/subdomains.jl
@@ -19,7 +19,7 @@ subbas_order, indices_subbas, topo_subbas = Wflow.kinwave_set_subdomains(
     min_sto_land,
 )
 
-Wflow.close_files(model, delete_output = false)
+Wflow.close_files(model; delete_output = false)
 
 if nthreads() == 1
     @testset "Nonparallel subdomains kinematic wave (nthreads = 1)" begin
diff --git a/test/testing_utils.jl b/test/testing_utils.jl
index 06a660767..c7faad0e9 100644
--- a/test/testing_utils.jl
+++ b/test/testing_utils.jl
@@ -11,14 +11,14 @@
 #     https://github.com/stevengj/18S096-iap17/blob/master/pset3/pset3-solutions.ipynb
 
 # n coefficients of the Taylor series of E₁(z) + log(z), in type T:
-function E1_taylor_coefficients(::Type{T}, n::Integer) where {T<:Number}
+function E1_taylor_coefficients(::Type{T}, n::Integer) where {T <: Number}
     n < 0 && throw(ArgumentError("$n ≥ 0 is required"))
     n == 0 && return T[]
     n == 1 && return T[-eulergamma]
     # iteratively compute the terms in the series, starting with k=1
     term::T = 1
     terms = T[-eulergamma, term]
-    for k = 2:n
+    for k in 2:n
         term = -term * (k - 1) / (k * k)
         push!(terms, term)
     end
@@ -40,10 +40,10 @@ end
 # for numeric-literal coefficients: simplify to a ratio of two polynomials:
 # return (p,q): the polynomials p(x) / q(x) corresponding to E1_cf(x, a...),
 # but without the exp(-x) term
-function E1_cfpoly(n::Integer, ::Type{T} = BigInt) where {T<:Real}
+function E1_cfpoly(n::Integer, ::Type{T} = BigInt) where {T <: Real}
     q = Polynomials.Polynomial(T[1])
     p = x = Polynomials.Polynomial(T[0, 1])
-    for i = n:-1:1
+    for i in n:-1:1
         p, q = x * p + (1 + i) * q, p # from cf = x + (1+i)/cf = x + (1+i)*q/p
         p, q = p + i * q, p     # from cf = 1 + i/cf = 1 + i*q/p
     end
@@ -65,7 +65,7 @@ macro E1_cf64(z, n::Integer)
 end
 
 # exponential integral function E₁(z)
-function expint(z::Union{Float64,Complex{Float64}})
+function expint(z::Union{Float64, Complex{Float64}})
     xSq = real(z)^2
     ySq = imag(z)^2
     if real(z) > 0 && xSq + 0.233 * ySq ≥ 7.84 # use cf expansion, ≤ 30 terms
@@ -81,15 +81,24 @@ function expint(z::Union{Float64,Complex{Float64}})
         return @E1_cf64 z 30
     else # use Taylor expansion, ≤ 37 terms
         rSq = xSq + ySq
-        return rSq ≤ 0.36 ?
-               (
-            rSq ≤ 2.8e-3 ? (rSq ≤ 2e-7 ? @E1_taylor64(z, 4) : @E1_taylor64(z, 8)) :
-            @E1_taylor64(z, 15)
-        ) : @E1_taylor64(z, 37)
+        return if rSq ≤ 0.36
+            (
+                if rSq ≤ 2.8e-3
+                    (rSq ≤ 2e-7 ? @E1_taylor64(z, 4) : @E1_taylor64(z, 8))
+                else
+                    @E1_taylor64(z, 15)
+                end
+            )
+        else
+            @E1_taylor64(z, 37)
+        end
     end
 end
-expint(z::Union{T,Complex{T},Rational{T},Complex{Rational{T}}}) where {T<:Integer} =
-    expint(float(z))
+function expint(
+    z::Union{T, Complex{T}, Rational{T}, Complex{Rational{T}}},
+) where {T <: Integer}
+    return expint(float(z))
+end
 
 ######################################################################
 # exponential integral Eₙ(z)
@@ -102,7 +111,7 @@ function expint(n::Integer, z)
         zinv = inv(z)
         exp_minus_z = exp(-z)
         Ei = zinv * exp_minus_z
-        for i = 1:-n
+        for i in 1:(-n)
             Ei = zinv * (exp_minus_z + i * Ei)
         end
         return Ei
@@ -111,7 +120,7 @@ function expint(n::Integer, z)
         exp_minus_z = exp(-z)
         Ei = expint(z)
         Ei *= !isinf(Ei)
-        for i = 2:n
+        for i in 2:n
             Ei = (exp_minus_z - z * Ei) / (i - 1)
         end
         return Ei
@@ -130,10 +139,10 @@ function csv_first_row(path)
     names = Tuple(Symbol.(split(header, ',')))
     ncol = length(names)
     # this assumes the first column is a time, the rest a float
-    types = Tuple{DateTime,fill(Float64, ncol - 1)...}
+    types = Tuple{DateTime, fill(Float64, ncol - 1)...}
 
     parts = split(dataline, ',')
     values = parse.(Float64, parts[2:end])
-    row = NamedTuple{names,types}((DateTime(parts[1]), values...))
+    row = NamedTuple{names, types}((DateTime(parts[1]), values...))
     return row
 end
diff --git a/test/vertical_process.jl b/test/vertical_process.jl
index 9ce59fb45..432b2844e 100644
--- a/test/vertical_process.jl
+++ b/test/vertical_process.jl
@@ -9,13 +9,13 @@ using Dates
     @test all(
         isapprox.(
             Wflow.rainfall_interception_modrut(8.6, 3.8, 1.5, 0.45, 2.8),
-            (4.343, 3.87, 0.387, 0.0, 3.8, 2.043),
+            (3.87, 3.8, 0.387, 2.043),
         ),
     )
     @test Wflow.head_brooks_corey(0.25, 0.6, 0.15, 10.5, -10.0) ≈ -90.6299820833844
-    @test Wflow.feddes_h3(-300.0, -600.0, 3.5, Second(86400)) ≈ -412.5
-    @test Wflow.feddes_h3(-300.0, -600.0, 0.5, Second(86400)) == -600.0
-    @test Wflow.feddes_h3(-300.0, -600.0, 6.0, Second(86400)) == -300.0
+    @test Wflow.feddes_h3(-300.0, -600.0, 3.5, 86400.0) ≈ -412.5
+    @test Wflow.feddes_h3(-300.0, -600.0, 0.5, 86400.0) == -600.0
+    @test Wflow.feddes_h3(-300.0, -600.0, 6.0, 86400.0) == -300.0
     @test Wflow.rwu_reduction_feddes(0.0, -10.0, -100.0, -300.0, -15000.0, 0.0) == 0.0
     @test Wflow.rwu_reduction_feddes(0.0, -10.0, -100.0, -300.0, -15000.0, 1.0) == 1.0
     @test Wflow.rwu_reduction_feddes(-90.0, -10.0, -100.0, -412.5, -15000.0, 0.0) ≈
@@ -24,12 +24,7 @@ using Dates
     @test Wflow.rwu_reduction_feddes(-12000.0, -10.0, -100.0, -412.5, -15000.0, 0.0) ≈
           0.20565552699228792
     @test Wflow.rwu_reduction_feddes(-16000.0, -10.0, -100.0, -412.5, -15000.0, 0.0) == 0.0
-    @test all(
-        isapprox.(
-            Wflow.infiltration(27.5, 0.2, 0.038, 8.9, 50.0, 5.0, 23.5, false, false),
-            (23.5, 19.14814814814815, 4.351851851851852, 22.0, 5.5, 0.5, 1.0),
-        ),
-    )
+    @test all(isapprox.(Wflow.infiltration(27.5, 0.2, 50.0, 5.0, 23.5, 1.0), (23.5, 0.5)))
     @test all(
         isapprox.(
             Wflow.unsatzone_flow_layer(43.5, 256.0, 135.0, 12.6),
@@ -44,14 +39,20 @@ using Dates
     )
     @test all(
         isapprox.(
-            Wflow.snowpack_hbv(201.5, 15.0, 30.1, 0.54, 2.0, 0.0, 0.0, 2.5, 0.10),
-            (207.073, 20.707300000000004, 1.35, 18.819699999999997, 6.923),
+            Wflow.precipitation_hbv(30.1, 0.54, 2.0, 0.0),
+            (6.923, 23.177000000000003),
+        ),
+    )
+    @test all(
+        isapprox.(
+            Wflow.snowpack_hbv(201.5, 15.0, 6.923, 23.177, 0.54, 0.0, 2.5, 0.10),
+            (207.073, 20.707300000000004, 227.7803, 1.35, 18.819699999999997),
         ),
     )
     @test Wflow.scurve(2.0, 0.0, 3.0, 2.5) ≈ 0.3325863502664285
     @test all(
         isapprox.(
-            Wflow.glacier_hbv(0.35, 500.0, 9.5, 5.0, 0.0, 3.4, 0.2, Second(Day(1))),
+            Wflow.glacier_hbv(0.35, 500.0, 9.5, 5.0, 0.0, 3.4, 0.2, 8.0),
             (8.835, 1.9, 484.9, 17.0),
         ),
     )