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/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/create_binaries/precompile.jl b/build/create_binaries/precompile.jl index 78ea170d1..984017d8c 100644 --- a/build/create_binaries/precompile.jl +++ b/build/create_binaries/precompile.jl @@ -8,5 +8,4 @@ testdir = abspath(dirname(pathof(Wflow)), "..", "test") Wflow.run(joinpath(testdir, "sbm_config.toml")) Wflow.run(joinpath(testdir, "sbm_gwf_config.toml")) -Wflow.run(joinpath(testdir, "hbv_config.toml")) Wflow.run(joinpath(testdir, "sediment_config.toml")) 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/docs/changelog.qmd b/docs/changelog.qmd index d9244485d..4558f18c2 100644 --- a/docs/changelog.qmd +++ b/docs/changelog.qmd @@ -7,17 +7,31 @@ 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). -## v1.0.0 +## 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`, `grid_type` and `grid_location`. The macro `grid_type` - is not required because this functionality is already part of `BMI`. The macros `exchange` - and `grid_location` are replaced by functions resulting in cleaner code. +- 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.x releases @@ -92,7 +106,10 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - For improved code readability it is now discouraged to use non-ASCII characters for the names of variables, structs, functions and macros. Using the non-ASCII character for built-in operators is still allowed. This change in naming convention is now in effect and - all invalid uses of non-ASCII characters have been replaced by ASCII equivalents. + all invalid uses of non-ASCII characters have been replaced by ASCII equivalents. This + change also impacts the TOML keys `kv₀`, `θᵣ` and `θₛ`. These keys have been replaced with + the ASCII versions `kv_0`, `theta_r` and `theta_s` in v0.4.0 and the old keys are not + supported anymore. - Docs: 1) improved description of different model configurations in model-setup.md, also in relation to hydromt\_wflow in docs, 2) citing info related to wflow\_sbm publication in Geosci. Model Dev. (from in review to published). diff --git a/docs/home/publications.qmd b/docs/home/publications.qmd index a05714cac..50fc4e36c 100644 --- a/docs/home/publications.qmd +++ b/docs/home/publications.qmd @@ -59,6 +59,11 @@ R., Francés, F., Kollet, S., Rigon, R., Weerts, A. & Samaniego, L, 2024. Multi- hydrological reference dataset over continental Europe and an African basin. Sci Data, 11, 1009\. . +Droppers, B., Rakovec, O., Avila, L., Azimi, S., Cortés-Torres N., De León Pérez, D., +Imhoff, R., Francés, F., Kollet, S., Rigon, R., Weerts, A. & Samaniego, L, 2024. Multi-model +hydrological reference dataset over continental Europe and an African basin. Sci Data, 11, +1009\. . + Emerton, R.E., Stephens, E.M., Pappenberger, F., Pagano, T.C., Weerts, A.H., Wood, A.W., Salamon, P., Brown, J.D., Hjerdt, N., Donnelly, C., Baugh, C.A., Cloke, H.L., 2016. Continental and global scale flood forecasting systems. WIREs Water 3, 391–418. @@ -167,6 +172,12 @@ van der Laan, E., P. Hazenberg, A.H. Weerts, 2024. Simulation of long-term stora headwater reservoirs across the globe using public cloud computing infrastructure. Science of The Total Environment, 172678, . +Pranoto, B., Soekarno, H., Hartulistiyoso, E., Nur Aidi, M., Sutrisno, D., Pohan, D., +Radhika, Sutejo, B., Heru Kuncoro, A., Nahib, I., 2024. Integrating Flood Early Warning +System (FEWS) for Optimizing Small Hydropower Sites: A West Java Case Study. EVERGREEN Joint +Journal of Novel Carbon Resource Sciences & Green Asia Strategy, 11, 3, 2691-2699. + + van Osnabrugge, B., Weerts, A.H., Uijlenhoet, R., 2017. genRE: A method to extend gridded precipitation climatology data sets in near real-time for hydrological forecasting purposes. Water Resources Research, 53. . diff --git a/docs/user_guide/toml_file.qmd b/docs/user_guide/toml_file.qmd index 307ca16fa..d67f46b7a 100644 --- a/docs/user_guide/toml_file.qmd +++ b/docs/user_guide/toml_file.qmd @@ -153,7 +153,7 @@ e_r = "EoverR" infiltcappath = "InfiltCapPath" infiltcapsoil = "InfiltCapSoil" kext = "Kext" -"kv_0" = "KsatVer" +kv_0 = "KsatVer" leaf_area_index = "LAI" # Cyclic variable m = "M" maxleakage = "MaxLeakage" @@ -173,8 +173,8 @@ ttm = "TTM" w_soil = "wflow_soil" water_holding_capacity = "WHC" waterfrac = "WaterFrac" -"theta_r" = "thetaR" -"theta_s" = "thetaS" +theta_r = "thetaR" +theta_s = "thetaS" [input.lateral.river] length = "wflow_riverlength" diff --git a/pixi.lock b/pixi.lock index deaa35ab0..2eb64b424 100644 --- a/pixi.lock +++ b/pixi.lock @@ -7,300 +7,44 @@ environments: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.6.2.post1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py313h536fd9c_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h46c70d0_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.7-py313h46c70d0_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.27.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.5.0-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-8.5.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.29.0-pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.9.25-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/juliaup-1.17.6-h8fae777_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.10.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.3-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-hc0a3c3a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.48-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.48-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-6.1.0-py313h536fd9c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.0-h9ebbce0_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h536fd9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.2.0-py313h8e95178_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.6-hc5c86c4_2_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.35.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py313h920b4c0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.1-py313h536fd9c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241003-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.8.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_6.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h80202fe_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda win-64: - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.6.2.post1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py313ha7868ed_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313h5813708_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.8.30-h56e8100_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.0-py313hd8ed1ab_100.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.7-py313h5813708_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.27.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.5.0-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-8.5.0-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.29.0-pyh7428d3b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.9.25-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/juliaup-1.17.6-ha073cba_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh5737063_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.10.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.3-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.46.1-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py313hb4c8b1a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.2.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.2-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.48-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.48-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-6.1.0-py313ha7868ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.0-hf5aa216_100_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.20.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py313h5813708_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.14-py313h5813708_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313ha7868ed_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-26.2.0-py313h2100fd5_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.35.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.20.0-py313hf3b5b86_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.6-hce54a09_2_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.4.1-py313ha7868ed_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241003-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.8.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_7.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_6.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313h574b89f_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.6-h0ea2cb4_0.conda packages: - kind: conda name: _libgcc_mutex @@ -331,244 +75,6 @@ packages: license_family: BSD size: 23621 timestamp: 1650670423406 -- kind: conda - name: anyio - version: 4.6.2.post1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/anyio-4.6.2.post1-pyhd8ed1ab_0.conda - sha256: 4b54b7ce79d818e3cce54ae4d552dba51b7afac160ceecdefd04b3917a37c502 - md5: 688697ec5e9588bdded167d19577625b - depends: - - exceptiongroup >=1.0.2 - - idna >=2.8 - - python >=3.9 - - sniffio >=1.1 - - typing_extensions >=4.1 - constrains: - - uvloop >=0.21.0b1 - - trio >=0.26.1 - license: MIT - license_family: MIT - size: 109864 - timestamp: 1728935803440 -- kind: conda - name: argon2-cffi - version: 23.1.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_0.conda - sha256: 130766446f5507bd44df957b6b5c898a8bd98f024bb426ed6cb9ff1ad67fc677 - md5: 3afef1f55a1366b4d3b6a0d92e2235e4 - depends: - - argon2-cffi-bindings - - python >=3.7 - - typing-extensions - constrains: - - argon2_cffi ==999 - license: MIT - license_family: MIT - size: 18602 - timestamp: 1692818472638 -- kind: conda - name: argon2-cffi-bindings - version: 21.2.0 - build: py313h536fd9c_5 - build_number: 5 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py313h536fd9c_5.conda - sha256: b17e5477dbc6a01286ea736216f49039d35335ea3283fa0f07d2c7cea57002ae - md5: 49fa2ed332b1239d6b0b2fe5e0393421 - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.0.1 - - libgcc >=13 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: MIT - license_family: MIT - size: 34900 - timestamp: 1725356714671 -- kind: conda - name: argon2-cffi-bindings - version: 21.2.0 - build: py313ha7868ed_5 - build_number: 5 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-21.2.0-py313ha7868ed_5.conda - sha256: 36b79f862177b3a104762f68664e445615e7c831ca5fe76dc4596ad531ed46a3 - md5: 6d6dbb065c660e9e358b32bdab9ada31 - depends: - - cffi >=1.0.1 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - size: 34467 - timestamp: 1725357154522 -- kind: conda - name: arrow - version: 1.3.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_0.conda - sha256: ff49825c7f9e29e09afa6284300810e7a8640d621740efb47c4541f4dc4969db - md5: b77d8c2313158e6e461ca0efb1c2c508 - depends: - - python >=3.8 - - python-dateutil >=2.7.0 - - types-python-dateutil >=2.8.10 - license: Apache-2.0 - license_family: Apache - size: 100096 - timestamp: 1696129131844 -- kind: conda - name: asttokens - version: 2.4.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda - sha256: 708168f026df19a0344983754d27d1f7b28bb21afc7b97a82f02c4798a3d2111 - md5: 5f25798dcefd8252ce5f9dc494d5f571 - depends: - - python >=3.5 - - six >=1.12.0 - license: Apache-2.0 - license_family: Apache - size: 28922 - timestamp: 1698341257884 -- kind: conda - name: async-lru - version: 2.0.4 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda - sha256: 7ed83731979fe5b046c157730e50af0e24454468bbba1ed8fc1a3107db5d7518 - md5: 3d081de3a6ea9f894bbb585e8e3a4dcb - depends: - - python >=3.8 - - typing_extensions >=4.0.0 - license: MIT - license_family: MIT - size: 15342 - timestamp: 1690563152778 -- kind: conda - name: attrs - version: 24.2.0 - build: pyh71513ae_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda - sha256: 28dba85a7e0f7fb57d7315e13f603d1e41b83c5b88aa2a602596b52c833a2ff8 - md5: 6732fa52eb8e66e5afeb32db8701a791 - depends: - - python >=3.7 - license: MIT - license_family: MIT - size: 56048 - timestamp: 1722977241383 -- kind: conda - name: babel - version: 2.14.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda - sha256: 8584e3da58e92b72641c89ff9b98c51f0d5dbe76e527867804cbdf03ac91d8e6 - md5: 9669586875baeced8fc30c0826c3270e - depends: - - python >=3.7 - - pytz - - setuptools - license: BSD-3-Clause - license_family: BSD - size: 7609750 - timestamp: 1702422720584 -- kind: conda - name: beautifulsoup4 - version: 4.12.3 - build: pyha770c72_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda - sha256: 7b05b2d0669029326c623b9df7a29fa49d1982a9e7e31b2fea34b4c9a4a72317 - md5: 332493000404d8411859539a5a630865 - depends: - - python >=3.6 - - soupsieve >=1.2 - license: MIT - license_family: MIT - size: 118200 - timestamp: 1705564819537 -- kind: conda - name: bleach - version: 6.1.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda - sha256: 845e77ef495376c5c3c328ccfd746ca0ef1978150cae8eae61a300fe7755fb08 - md5: 0ed9d7c0e9afa7c025807a9a8136ea3e - depends: - - packaging - - python >=3.6 - - setuptools - - six >=1.9.0 - - webencodings - license: Apache-2.0 - license_family: Apache - size: 131220 - timestamp: 1696630354218 -- kind: conda - name: brotli-python - version: 1.1.0 - build: py313h46c70d0_2 - build_number: 2 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h46c70d0_2.conda - sha256: da92e5e904465fce33a7a55658b13caa5963cc463c430356373deeda8b2dbc46 - md5: f6bb3742e17a4af0dc3c8ca942683ef6 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - constrains: - - libbrotlicommon 1.1.0 hb9d3cd8_2 - license: MIT - license_family: MIT - size: 350424 - timestamp: 1725267803672 -- kind: conda - name: brotli-python - version: 1.1.0 - build: py313h5813708_2 - build_number: 2 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313h5813708_2.conda - sha256: e89803147849d429f1ba3eec880b487c2cc4cac48a221079001a2ab1216f3709 - md5: c1a5d95bf18940d2b1d12f7bf2fb589b - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - libbrotlicommon 1.1.0 h2466b09_2 - license: MIT - license_family: MIT - size: 322309 - timestamp: 1725268431915 - kind: conda name: bzip2 version: 1.0.8 @@ -624,730 +130,6 @@ packages: license: ISC size: 159003 timestamp: 1725018903918 -- kind: conda - name: cached-property - version: 1.5.2 - build: hd8ed1ab_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 - md5: 9b347a7ec10940d3f7941ff6c460b551 - depends: - - cached_property >=1.5.2,<1.5.3.0a0 - license: BSD-3-Clause - license_family: BSD - size: 4134 - timestamp: 1615209571450 -- kind: conda - name: cached_property - version: 1.5.2 - build: pyha770c72_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 - md5: 576d629e47797577ab0f1b351297ef4a - depends: - - python >=3.6 - license: BSD-3-Clause - license_family: BSD - size: 11065 - timestamp: 1615209567874 -- kind: conda - name: certifi - version: 2024.8.30 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.8.30-pyhd8ed1ab_0.conda - sha256: 7020770df338c45ac6b560185956c32f0a5abf4b76179c037f115fc7d687819f - md5: 12f7d00853807b0531775e9be891cb11 - depends: - - python >=3.7 - license: ISC - size: 163752 - timestamp: 1725278204397 -- kind: conda - name: cffi - version: 1.17.1 - build: py313ha7868ed_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py313ha7868ed_0.conda - sha256: b19f581fe423858f1f477c52e10978be324c55ebf2e418308d30d013f4a476ff - md5: 519a29d7ac273f8c165efc0af099da42 - depends: - - pycparser - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - size: 291828 - timestamp: 1725561211547 -- kind: conda - name: cffi - version: 1.17.1 - build: py313hfab6e84_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py313hfab6e84_0.conda - sha256: 73cd6199b143a8a6cbf733ce124ed57defc1b9a7eab9b10fd437448caf8eaa45 - md5: ce6386a5892ef686d6d680c345c40ad1 - depends: - - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 - - pycparser - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: MIT - license_family: MIT - size: 295514 - timestamp: 1725560706794 -- kind: conda - name: charset-normalizer - version: 3.4.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.0-pyhd8ed1ab_0.conda - sha256: 1873ac45ea61f95750cb0b4e5e675d1c5b3def937e80c7eebb19297f76810be8 - md5: a374efa97290b8799046df7c5ca17164 - depends: - - python >=3.7 - license: MIT - license_family: MIT - size: 47314 - timestamp: 1728479405343 -- kind: conda - name: colorama - version: 0.4.6 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 - md5: 3faab06a954c2a04039983f2c4a50d99 - depends: - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - size: 25170 - timestamp: 1666700778190 -- kind: conda - name: comm - version: 0.2.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_0.conda - sha256: e923acf02708a8a0b591f3bce4bdc11c8e63b73198b99b35fe6cd96bfb6a0dbe - md5: 948d84721b578d426294e17a02e24cbb - depends: - - python >=3.6 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - size: 12134 - timestamp: 1710320435158 -- kind: conda - name: cpython - version: 3.13.0 - build: py313hd8ed1ab_100 - build_number: 100 - subdir: noarch - noarch: generic - url: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.0-py313hd8ed1ab_100.conda - sha256: f75a981dbaadc0196a275ddeb10e1cf23340589ee49e6f494bc60247468f70b3 - md5: 150059fe488fb313446030b75672e5db - depends: - - python 3.13.0.* - - python_abi * *_cp313 - license: Python-2.0 - size: 46133 - timestamp: 1728417498093 -- kind: conda - name: debugpy - version: 1.8.7 - build: py313h46c70d0_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.7-py313h46c70d0_0.conda - sha256: a18ad8895deb52de9bf5969efbe98f6dad276ebd62d65666836bc4cbc90aa179 - md5: 20476f3c3a8a61560fa249e0d6514ab4 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: MIT - license_family: MIT - size: 2679576 - timestamp: 1728594270223 -- kind: conda - name: debugpy - version: 1.8.7 - build: py313h5813708_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.7-py313h5813708_0.conda - sha256: d203fef84c2130ea0a4fe3d1c535cc4c6b2d48a275fca2ddf3f77ce522074fda - md5: 198042ea668cff413825bcaf34293574 - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - size: 3588809 - timestamp: 1728594850379 -- kind: conda - name: decorator - version: 5.1.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2 - sha256: 328a6a379f9bdfd0230e51de291ce858e6479411ea4b0545fb377c71662ef3e2 - md5: 43afe5ab04e35e17ba28649471dd7364 - depends: - - python >=3.5 - license: BSD-2-Clause - license_family: BSD - size: 12072 - timestamp: 1641555714315 -- kind: conda - name: defusedxml - version: 0.7.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be - md5: 961b3a227b437d82ad7054484cfa71b2 - depends: - - python >=3.6 - license: PSF-2.0 - license_family: PSF - size: 24062 - timestamp: 1615232388757 -- kind: conda - name: entrypoints - version: '0.4' - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2 - sha256: 2ec4a0900a4a9f42615fc04d0fb3286b796abe56590e8e042f6ec25e102dd5af - md5: 3cf04868fee0a029769bd41f4b2fbf2d - depends: - - python >=3.6 - license: MIT - license_family: MIT - size: 9199 - timestamp: 1643888357950 -- kind: conda - name: exceptiongroup - version: 1.2.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - sha256: e0edd30c4b7144406bb4da975e6bb97d6bc9c0e999aa4efe66ae108cada5d5b5 - md5: d02ae936e42063ca46af6cdad2dbd1e0 - depends: - - python >=3.7 - license: MIT and PSF-2.0 - size: 20418 - timestamp: 1720869435725 -- kind: conda - name: executing - version: 2.1.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/executing-2.1.0-pyhd8ed1ab_0.conda - sha256: a52d7516e2e11d3eb10908e10d3eb3f8ef267fea99ed9b09d52d96c4db3441b8 - md5: d0441db20c827c11721889a241df1220 - depends: - - python >=2.7 - license: MIT - license_family: MIT - size: 28337 - timestamp: 1725214501850 -- kind: conda - name: fqdn - version: 1.5.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_0.tar.bz2 - sha256: 6cfd1f9bcd2358a69fb571f4b3af049b630d52647d906822dbedac03e84e4f63 - md5: 642d35437078749ef23a5dca2c9bb1f3 - depends: - - cached-property >=1.3.0 - - python >=2.7,<4 - license: MPL-2.0 - license_family: MOZILLA - size: 14395 - timestamp: 1638810388635 -- kind: conda - name: h11 - version: 0.14.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_0.tar.bz2 - sha256: 817d2c77d53afe3f3d9cf7f6eb8745cdd8ea76c7adaa9d7ced75c455a2c2c085 - md5: b21ed0883505ba1910994f1df031a428 - depends: - - python >=3 - - typing_extensions - license: MIT - license_family: MIT - size: 48251 - timestamp: 1664132995560 -- kind: conda - name: h2 - version: 4.1.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - sha256: bfc6a23849953647f4e255c782e74a0e18fe16f7e25c7bb0bc57b83bb6762c7a - md5: b748fbf7060927a6e82df7cb5ee8f097 - depends: - - hpack >=4.0,<5 - - hyperframe >=6.0,<7 - - python >=3.6.1 - license: MIT - license_family: MIT - size: 46754 - timestamp: 1634280590080 -- kind: conda - name: hpack - version: 4.0.0 - build: pyh9f0ad1d_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 - sha256: 5dec948932c4f740674b1afb551223ada0c55103f4c7bf86a110454da3d27cb8 - md5: 914d6646c4dbb1fd3ff539830a12fd71 - depends: - - python - license: MIT - license_family: MIT - size: 25341 - timestamp: 1598856368685 -- kind: conda - name: httpcore - version: 1.0.6 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.6-pyhd8ed1ab_0.conda - sha256: 8952c3f1eb18bf4d7e813176c3b23e0af4e863e8b05087e73f74f371d73077ca - md5: b8e1901ef9a215fc41ecfb6bef7e0943 - depends: - - anyio >=3.0,<5.0 - - certifi - - h11 >=0.13,<0.15 - - h2 >=3,<5 - - python >=3.8 - - sniffio 1.* - license: BSD-3-Clause - license_family: BSD - size: 45711 - timestamp: 1727821031365 -- kind: conda - name: httpx - version: 0.27.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/httpx-0.27.2-pyhd8ed1ab_0.conda - sha256: 1a33f160548bf447e15c0273899d27e4473f1d5b7ca1441232ec2d9d07c56d03 - md5: 7e9ac3faeebdbd7b53b462c41891e7f7 - depends: - - anyio - - certifi - - httpcore 1.* - - idna - - python >=3.8 - - sniffio - license: BSD-3-Clause - license_family: BSD - size: 65085 - timestamp: 1724778453275 -- kind: conda - name: hyperframe - version: 6.0.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 - sha256: e374a9d0f53149328134a8d86f5d72bca4c6dcebed3c0ecfa968c02996289330 - md5: 9f765cbfab6870c8435b9eefecd7a1f4 - depends: - - python >=3.6 - license: MIT - license_family: MIT - size: 14646 - timestamp: 1619110249723 -- kind: conda - name: idna - version: '3.10' - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_0.conda - sha256: 8c57fd68e6be5eecba4462e983aed7e85761a519aab80e834bbd7794d4b545b2 - md5: 7ba2ede0e7c795ff95088daf0dc59753 - depends: - - python >=3.6 - license: BSD-3-Clause - license_family: BSD - size: 49837 - timestamp: 1726459583613 -- kind: conda - name: importlib-metadata - version: 8.5.0 - build: pyha770c72_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.5.0-pyha770c72_0.conda - sha256: 7194700ce1a5ad2621fd68e894dd8c1ceaff9a38723e6e0e5298fdef13017b1c - md5: 54198435fce4d64d8a89af22573012a8 - depends: - - python >=3.8 - - zipp >=0.5 - license: Apache-2.0 - license_family: APACHE - size: 28646 - timestamp: 1726082927916 -- kind: conda - name: importlib_metadata - version: 8.5.0 - build: hd8ed1ab_0 - subdir: noarch - noarch: generic - url: https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-8.5.0-hd8ed1ab_0.conda - sha256: 313b8a05211bacd6b15ab2621cb73d7f41ea5c6cae98db53367d47833f03fef1 - md5: 2a92e152208121afadf85a5e1f3a5f4d - depends: - - importlib-metadata >=8.5.0,<8.5.1.0a0 - license: Apache-2.0 - license_family: APACHE - size: 9385 - timestamp: 1726082930346 -- kind: conda - name: importlib_resources - version: 6.4.5 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.4.5-pyhd8ed1ab_0.conda - sha256: 2cb9db3e40033c3df72d3defc678a012840378fd55a67e4351363d4b321a0dc1 - md5: c808991d29b9838fb4d96ce8267ec9ec - depends: - - python >=3.8 - - zipp >=3.1.0 - constrains: - - importlib-resources >=6.4.5,<6.4.6.0a0 - license: Apache-2.0 - license_family: APACHE - size: 32725 - timestamp: 1725921462405 -- kind: conda - name: ipykernel - version: 6.29.5 - build: pyh3099207_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - sha256: 33cfd339bb4efac56edf93474b37ddc049e08b1b4930cf036c893cc1f5a1f32a - md5: b40131ab6a36ac2c09b7c57d4d3fbf99 - depends: - - __linux - - comm >=0.1.1 - - debugpy >=1.6.5 - - ipython >=7.23.1 - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* - - matplotlib-inline >=0.1 - - nest-asyncio - - packaging - - psutil - - python >=3.8 - - pyzmq >=24 - - tornado >=6.1 - - traitlets >=5.4.0 - license: BSD-3-Clause - license_family: BSD - size: 119084 - timestamp: 1719845605084 -- kind: conda - name: ipykernel - version: 6.29.5 - build: pyh4bbf305_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda - sha256: dc569094125127c0078aa536f78733f383dd7e09507277ef8bcd1789786e7086 - md5: 18df5fc4944a679e085e0e8f31775fc8 - depends: - - __win - - comm >=0.1.1 - - debugpy >=1.6.5 - - ipython >=7.23.1 - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* - - matplotlib-inline >=0.1 - - nest-asyncio - - packaging - - psutil - - python >=3.8 - - pyzmq >=24 - - tornado >=6.1 - - traitlets >=5.4.0 - license: BSD-3-Clause - license_family: BSD - size: 119853 - timestamp: 1719845858082 -- kind: conda - name: ipython - version: 8.29.0 - build: pyh707e725_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/ipython-8.29.0-pyh707e725_0.conda - sha256: 606723272a208cca1036852e04fbb61741b78451784746e75edd1becb70347d2 - md5: 56db21d7d51410fcfbfeca3d1a6b4269 - depends: - - __unix - - decorator - - exceptiongroup - - jedi >=0.16 - - matplotlib-inline - - pexpect >4.3 - - pickleshare - - prompt-toolkit >=3.0.41,<3.1.0 - - pygments >=2.4.0 - - python >=3.10 - - stack_data - - traitlets >=5.13.0 - - typing_extensions >=4.6 - license: BSD-3-Clause - license_family: BSD - size: 599356 - timestamp: 1729866495921 -- kind: conda - name: ipython - version: 8.29.0 - build: pyh7428d3b_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/ipython-8.29.0-pyh7428d3b_0.conda - sha256: 2208dbe96e94ba653c4e0a5f302e36f16df73eec1968cfb85eff2d9775c9ced1 - md5: 9dc505b3569b4c26cffc241c50695f75 - depends: - - __win - - colorama - - decorator - - exceptiongroup - - jedi >=0.16 - - matplotlib-inline - - pickleshare - - prompt-toolkit >=3.0.41,<3.1.0 - - pygments >=2.4.0 - - python >=3.10 - - stack_data - - traitlets >=5.13.0 - - typing_extensions >=4.6 - license: BSD-3-Clause - license_family: BSD - size: 600237 - timestamp: 1729866942619 -- kind: conda - name: ipywidgets - version: 8.1.5 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.5-pyhd8ed1ab_0.conda - sha256: ae27447f300c85a184d5d4fa08674eaa93931c12275daca981eb986f5d7795b3 - md5: a022d34163147d16b27de86dc53e93fc - depends: - - comm >=0.1.3 - - ipython >=6.1.0 - - jupyterlab_widgets >=3.0.13,<3.1.0 - - python >=3.7 - - traitlets >=4.3.1 - - widgetsnbextension >=4.0.13,<4.1.0 - license: BSD-3-Clause - license_family: BSD - size: 113497 - timestamp: 1724334989324 -- kind: conda - name: isoduration - version: 20.11.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_0.tar.bz2 - sha256: 7bb5c4d994361022f47a807b5e7d101b3dce16f7dd8a0af6ffad9f479d346493 - md5: 4cb68948e0b8429534380243d063a27a - depends: - - arrow >=0.15.0 - - python >=3.7 - license: MIT - license_family: MIT - size: 17189 - timestamp: 1638811664194 -- kind: conda - name: jedi - version: 0.19.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda - sha256: 362f0936ef37dfd1eaa860190e42a6ebf8faa094eaa3be6aa4d9ace95f40047a - md5: 81a3be0b2023e1ea8555781f0ad904a2 - depends: - - parso >=0.8.3,<0.9.0 - - python >=3.6 - license: MIT - license_family: MIT - size: 841312 - timestamp: 1696326218364 -- kind: conda - name: jinja2 - version: 3.1.4 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda - sha256: 27380d870d42d00350d2d52598cddaf02f9505fb24be09488da0c9b8d1428f2d - md5: 7b86ecb7d3557821c649b3c31e3eb9f2 - depends: - - markupsafe >=2.0 - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - size: 111565 - timestamp: 1715127275924 -- kind: conda - name: json5 - version: 0.9.25 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/json5-0.9.25-pyhd8ed1ab_0.conda - sha256: 0c75e428970e8bb72ba1dd3a6dc32b8d68f6534b4fe16b38e53364963fdc8e38 - md5: 5d8c241a9261e720a34a07a3e1ac4109 - depends: - - python >=3.7,<4.0 - license: Apache-2.0 - license_family: APACHE - size: 27995 - timestamp: 1712986338874 -- kind: conda - name: jsonpointer - version: 3.0.0 - build: py313h78bf25f_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_1.conda - sha256: 18d412dc91ee7560f0f94c19bb1c3c23f413b9a7f55948e2bb3ce44340439a58 - md5: 668d64b50e7ce7984cfe09ed7045b9fa - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - size: 17568 - timestamp: 1725303033801 -- kind: conda - name: jsonpointer - version: 3.0.0 - build: py313hfa70ccb_1 - build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_1.conda - sha256: a0625cb0e86775b8996b4ee7117f1912b2fa3d76be8d10bf1d7b39578f5d99f7 - md5: 001efbf150f0ca5fd0a0c5e6e713c1d1 - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - size: 42805 - timestamp: 1725303293802 -- kind: conda - name: jsonschema - version: 4.23.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_0.conda - sha256: 7d0c4c0346b26be9f220682b7c5c0d84606d48c6dbc36fc238e4452dda733aff - md5: da304c192ad59975202859b367d0f6a2 - depends: - - attrs >=22.2.0 - - importlib_resources >=1.4.0 - - jsonschema-specifications >=2023.03.6 - - pkgutil-resolve-name >=1.3.10 - - python >=3.8 - - referencing >=0.28.4 - - rpds-py >=0.7.1 - license: MIT - license_family: MIT - size: 74323 - timestamp: 1720529611305 -- kind: conda - name: jsonschema-specifications - version: 2024.10.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_0.conda - sha256: 82f8bed0f21dc0b3aff40dd4e39d77e85b93b0417bc5659b001e0109341b8b98 - md5: 720745920222587ef942acfbc578b584 - depends: - - python >=3.8 - - referencing >=0.31.0 - license: MIT - license_family: MIT - size: 16165 - timestamp: 1728418976382 -- kind: conda - name: jsonschema-with-format-nongpl - version: 4.23.0 - build: hd8ed1ab_0 - subdir: noarch - noarch: generic - url: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_0.conda - sha256: 007a0a506a0d1805b099629cb0ee743ad0afe7d9749e57339f32c168119e0139 - md5: 16b37612b3a2fd77f409329e213b530c - depends: - - fqdn - - idna - - isoduration - - jsonpointer >1.13 - - jsonschema >=4.23.0,<4.23.1.0a0 - - rfc3339-validator - - rfc3986-validator >0.1.0 - - uri-template - - webcolors >=24.6.0 - license: MIT - license_family: MIT - size: 7143 - timestamp: 1720529619500 - kind: conda name: juliaup version: 1.17.6 @@ -1381,340 +163,6 @@ packages: license_family: MIT size: 1517497 timestamp: 1725415742700 -- kind: conda - name: jupyter - version: 1.1.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_0.conda - sha256: 5d92eb46552af180cd27a5e916206eb3f6725a0ae3d4bafa7a5f44adfada4332 - md5: 255a8fe52d1c57a6b46d0d16851883db - depends: - - ipykernel - - ipywidgets - - jupyter_console - - jupyterlab - - nbconvert-core - - notebook - - python >=3.6 - license: BSD-3-Clause - license_family: BSD - size: 8728 - timestamp: 1725037618526 -- kind: conda - name: jupyter-lsp - version: 2.2.5 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhd8ed1ab_0.conda - sha256: 2151c2c63e0442a4c69ee0ad8a634195eedab10b7b74c0ec8266471842239a93 - md5: 885867f6adab3d7ecdf8ab6ca0785f51 - depends: - - importlib-metadata >=4.8.3 - - jupyter_server >=1.1.2 - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - size: 55539 - timestamp: 1712707521811 -- kind: conda - name: jupyter_client - version: 8.6.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_0.conda - sha256: 4419c85e209a715f551a5c9bead746f29ee9d0fc41e772a76db3868622795671 - md5: a14218cfb29662b4a19ceb04e93e298e - depends: - - importlib-metadata >=4.8.3 - - jupyter_core >=4.12,!=5.0.* - - python >=3.8 - - python-dateutil >=2.8.2 - - pyzmq >=23.0 - - tornado >=6.2 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - size: 106055 - timestamp: 1726610805505 -- kind: conda - name: jupyter_console - version: 6.6.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_0.conda - sha256: 4e51764d5fe2f6e43d83bcfbcf8b4da6569721bf82eaf4d647be8717cd6be75a - md5: 7cf6f52a66f8e3cd9d8b6c231262dcab - depends: - - ipykernel >=6.14 - - ipython - - jupyter_client >=7.0.0 - - jupyter_core >=4.12,!=5.0.* - - prompt_toolkit >=3.0.30 - - pygments - - python >=3.7 - - pyzmq >=17 - - traitlets >=5.4 - license: BSD-3-Clause - license_family: BSD - size: 26484 - timestamp: 1678118234022 -- kind: conda - name: jupyter_core - version: 5.7.2 - build: pyh31011fe_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - sha256: 732b1e8536bc22a5a174baa79842d79db2f4956d90293dd82dc1b3f6099bcccd - md5: 0a2980dada0dd7fd0998f0342308b1b1 - depends: - - __unix - - platformdirs >=2.5 - - python >=3.8 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - size: 57671 - timestamp: 1727163547058 -- kind: conda - name: jupyter_core - version: 5.7.2 - build: pyh5737063_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh5737063_1.conda - sha256: 7c903b2d62414c3e8da1f78db21f45b98de387aae195f8ca959794113ba4b3fd - md5: 46d87d1c0ea5da0aae36f77fa406e20d - depends: - - __win - - cpython - - platformdirs >=2.5 - - python >=3.8 - - pywin32 >=300 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - size: 58269 - timestamp: 1727164026641 -- kind: conda - name: jupyter_events - version: 0.10.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.10.0-pyhd8ed1ab_0.conda - sha256: cd3f41dc093162a41d4bae171e40a1b9b115c4d488e9bb837a8fa9d084931fb9 - md5: ed45423c41b3da15ea1df39b1f80c2ca - depends: - - jsonschema-with-format-nongpl >=4.18.0 - - python >=3.8 - - python-json-logger >=2.0.4 - - pyyaml >=5.3 - - referencing - - rfc3339-validator - - rfc3986-validator >=0.1.1 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - size: 21475 - timestamp: 1710805759187 -- kind: conda - name: jupyter_server - version: 2.14.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.14.2-pyhd8ed1ab_0.conda - sha256: edab71a05feceac54bdb90e755a257545af7832b9911607c1a70f09be44ba985 - md5: ca23c71f70a7c7935b3d03f0f1a5801d - depends: - - anyio >=3.1.0 - - argon2-cffi >=21.1 - - jinja2 >=3.0.3 - - jupyter_client >=7.4.4 - - jupyter_core >=4.12,!=5.0.* - - jupyter_events >=0.9.0 - - jupyter_server_terminals >=0.4.4 - - nbconvert-core >=6.4.4 - - nbformat >=5.3.0 - - overrides >=5.0 - - packaging >=22.0 - - prometheus_client >=0.9 - - python >=3.8 - - pyzmq >=24 - - send2trash >=1.8.2 - - terminado >=0.8.3 - - tornado >=6.2.0 - - traitlets >=5.6.0 - - websocket-client >=1.7 - license: BSD-3-Clause - license_family: BSD - size: 323978 - timestamp: 1720816754998 -- kind: conda - name: jupyter_server_terminals - version: 0.5.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_0.conda - sha256: 038efbc7e4b2e72d49ed193cfb2bbbe9fbab2459786ce9350301f466a32567db - md5: 219b3833aa8ed91d47d1be6ca03f30be - depends: - - python >=3.8 - - terminado >=0.8.3 - license: BSD-3-Clause - license_family: BSD - size: 19818 - timestamp: 1710262791393 -- kind: conda - name: jupyterlab - version: 4.2.5 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.2.5-pyhd8ed1ab_0.conda - sha256: db08036a6fd846c178ebdce7327be1130bda10ac96113c17b04bce2bc4d67dda - md5: 594762eddc55b82feac6097165a88e3c - depends: - - async-lru >=1.0.0 - - httpx >=0.25.0 - - importlib_metadata >=4.8.3 - - importlib_resources >=1.4 - - ipykernel >=6.5.0 - - jinja2 >=3.0.3 - - jupyter-lsp >=2.0.0 - - jupyter_core - - jupyter_server >=2.4.0,<3 - - jupyterlab_server >=2.27.1,<3 - - notebook-shim >=0.2 - - packaging - - python >=3.8 - - setuptools >=40.1.0 - - tomli >=1.2.2 - - tornado >=6.2.0 - - traitlets - license: BSD-3-Clause - license_family: BSD - size: 7361961 - timestamp: 1724745262468 -- kind: conda - name: jupyterlab_pygments - version: 0.3.0 - build: pyhd8ed1ab_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_1.conda - sha256: 4aa622bbcf97e44cd1adf0100b7ff71b7e20268f043bdf6feae4d16152f1f242 - md5: afcd1b53bcac8844540358e33f33d28f - depends: - - pygments >=2.4.1,<3 - - python >=3.7 - constrains: - - jupyterlab >=4.0.8,<5.0.0 - license: BSD-3-Clause - license_family: BSD - size: 18776 - timestamp: 1707149279640 -- kind: conda - name: jupyterlab_server - version: 2.27.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_0.conda - sha256: a23b26d1a35bccdb91b9232119e5f402624e1e1a252b0e64cc20c6eb5b87cefb - md5: af8239bf1ba7e8c69b689f780f653488 - depends: - - babel >=2.10 - - importlib-metadata >=4.8.3 - - jinja2 >=3.0.3 - - json5 >=0.9.0 - - jsonschema >=4.18 - - jupyter_server >=1.21,<3 - - packaging >=21.3 - - python >=3.8 - - requests >=2.31 - constrains: - - openapi-core >=0.18.0,<0.19.0 - license: BSD-3-Clause - license_family: BSD - size: 49355 - timestamp: 1721163412436 -- kind: conda - name: jupyterlab_widgets - version: 3.0.13 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.13-pyhd8ed1ab_0.conda - sha256: 0e7ec7936d766f39d5a0a8eafc63f5543f488883ad3645246bc22db6d632566e - md5: ccea946e6dce9f330fbf7fca97fe8de7 - depends: - - python >=3.7 - constrains: - - jupyterlab >=3,<5 - license: BSD-3-Clause - license_family: BSD - size: 186024 - timestamp: 1724331451102 -- kind: conda - name: keyutils - version: 1.6.1 - build: h166bdaf_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb - md5: 30186d27e2c9fa62b45fb1476b7200e3 - depends: - - libgcc-ng >=10.3.0 - license: LGPL-2.1-or-later - size: 117831 - timestamp: 1646151697040 -- kind: conda - name: krb5 - version: 1.21.3 - build: h659f571_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 - md5: 3f43953b7d3fb3aaa1d0d0723d91e368 - depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 - license: MIT - license_family: MIT - size: 1370023 - timestamp: 1719463201255 -- kind: conda - name: krb5 - version: 1.21.3 - build: hdf4eb48_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 - md5: 31aec030344e962fbd7dbbbbd68e60a9 - depends: - - openssl >=3.3.1,<4.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - size: 712034 - timestamp: 1719463874284 - kind: conda name: ld_impl_linux-64 version: '2.43' @@ -1732,22 +180,6 @@ packages: license_family: GPL size: 669616 timestamp: 1727304687962 -- kind: conda - name: libedit - version: 3.1.20191231 - build: he28a2e2_2 - build_number: 2 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 - sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf - md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 - depends: - - libgcc-ng >=7.5.0 - - ncurses >=6.2,<7.0.0a0 - license: BSD-2-Clause - license_family: BSD - size: 123878 - timestamp: 1597616541093 - kind: conda name: libexpat version: 2.6.3 @@ -1816,112 +248,67 @@ packages: timestamp: 1636489106777 - kind: conda name: libgcc - version: 14.2.0 + version: 14.1.0 build: h77fa898_1 build_number: 1 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h77fa898_1.conda - sha256: 53eb8a79365e58849e7b1a068d31f4f9e718dc938d6f2c03e960345739a03569 - md5: 3cb76c3f10d3bc7f1105b2fc9db984df + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + sha256: 10fa74b69266a2be7b96db881e18fa62cfa03082b65231e8d652e897c4b335a3 + md5: 002ef4463dd1e2b44a94a4ace468f5d2 depends: - _libgcc_mutex 0.1 conda_forge - _openmp_mutex >=4.5 constrains: - - libgomp 14.2.0 h77fa898_1 - - libgcc-ng ==14.2.0=*_1 + - libgomp 14.1.0 h77fa898_1 + - libgcc-ng ==14.1.0=*_1 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 848745 - timestamp: 1729027721139 + size: 846380 + timestamp: 1724801836552 - kind: conda name: libgcc-ng - version: 14.2.0 + version: 14.1.0 build: h69a702a_1 build_number: 1 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_1.conda - sha256: 3a76969c80e9af8b6e7a55090088bc41da4cffcde9e2c71b17f44d37b7cb87f7 - md5: e39480b9ca41323497b05492a63bc35b + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + sha256: b91f7021e14c3d5c840fbf0dc75370d6e1f7c7ff4482220940eaafb9c64613b7 + md5: 1efc0ad219877a73ef977af7dbb51f17 depends: - - libgcc 14.2.0 h77fa898_1 + - libgcc 14.1.0 h77fa898_1 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 54142 - timestamp: 1729027726517 + size: 52170 + timestamp: 1724801842101 - kind: conda name: libgomp - version: 14.2.0 + version: 14.1.0 build: h77fa898_1 build_number: 1 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h77fa898_1.conda - sha256: 1911c29975ec99b6b906904040c855772ccb265a1c79d5d75c8ceec4ed89cd63 - md5: cc3573974587f12dda90d96e3e55a702 + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + sha256: c96724c8ae4ee61af7674c5d9e5a3fbcf6cd887a40ad5a52c99aa36f1d4f9680 + md5: 23c255b008c4f2ae008f81edcabaca89 depends: - _libgcc_mutex 0.1 conda_forge license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 460992 - timestamp: 1729027639220 -- kind: conda - name: libmpdec - version: 4.0.0 - build: h2466b09_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - sha256: fc529fc82c7caf51202cc5cec5bb1c2e8d90edbac6d0a4602c966366efe3c7bf - md5: 74860100b2029e2523cf480804c76b9b - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-2-Clause - license_family: BSD - size: 88657 - timestamp: 1723861474602 -- kind: conda - name: libmpdec - version: 4.0.0 - build: h4bc722e_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda - sha256: d02d1d3304ecaf5c728e515eb7416517a0b118200cd5eacbe829c432d1664070 - md5: aeb98fdeb2e8f25d43ef71fbacbeec80 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - license: BSD-2-Clause - license_family: BSD - size: 89991 - timestamp: 1723817448345 -- kind: conda - name: libsodium - version: 1.0.20 - build: h4ab18f5_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 - md5: a587892d3c13b6621a6091be690dbca2 - depends: - - libgcc-ng >=12 - license: ISC - size: 205978 - timestamp: 1716828628198 -- kind: conda - name: libsodium - version: 1.0.20 - build: hc70643c_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - sha256: 7bcb3edccea30f711b6be9601e083ecf4f435b9407d70fc48fbcf9e5d69a0fc6 - md5: 198bb594f202b205c7d18b936fa4524f - depends: - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: ISC - size: 202344 - timestamp: 1716828757533 + size: 460218 + timestamp: 1724801743478 +- kind: conda + name: libnsl + version: 2.0.1 + build: hd590300_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 + md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 + depends: + - libgcc-ng >=12 + license: LGPL-2.1-only + license_family: GPL + size: 33408 + timestamp: 1697359010159 - kind: conda name: libsqlite version: 3.46.1 @@ -1952,36 +339,6 @@ packages: license: Unlicense size: 865214 timestamp: 1725353659783 -- kind: conda - name: libstdcxx - version: 14.2.0 - build: hc0a3c3a_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-hc0a3c3a_1.conda - sha256: 4661af0eb9bdcbb5fb33e5d0023b001ad4be828fccdcc56500059d56f9869462 - md5: 234a5554c53625688d51062645337328 - depends: - - libgcc 14.2.0 h77fa898_1 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - size: 3893695 - timestamp: 1729027746910 -- kind: conda - name: libstdcxx-ng - version: 14.2.0 - build: h4852527_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_1.conda - sha256: 25bb30b827d4f6d6f0522cc0579e431695503822f144043b93c50237017fffd8 - md5: 8371ac6457591af2cf6159439c1fd051 - depends: - - libstdcxx 14.2.0 hc0a3c3a_1 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - size: 54105 - timestamp: 1729027780628 - kind: conda name: libuuid version: 2.38.1 @@ -1996,6 +353,20 @@ packages: license_family: BSD size: 33601 timestamp: 1680112270483 +- kind: conda + name: libxcrypt + version: 4.4.36 + build: hd590300_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + size: 100393 + timestamp: 1702724383534 - kind: conda name: libzlib version: 1.3.1 @@ -2032,149 +403,6 @@ packages: license_family: Other size: 61574 timestamp: 1716874187109 -- kind: conda - name: markupsafe - version: 3.0.2 - build: py313h8060acc_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py313h8060acc_0.conda - sha256: 9158873dbd5b715d5683dd9241b3eab35e896e31ea7842052f1b4e8c3945bf45 - md5: ab825f8b676368beb91350c6a2da6e11 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - size: 25104 - timestamp: 1729351477153 -- kind: conda - name: markupsafe - version: 3.0.2 - build: py313hb4c8b1a_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py313hb4c8b1a_0.conda - sha256: 2b7500300aba9726f785781ded5fb1205c76d3047a93cd30868712e1e02f8c6e - md5: 4ab654528518cea7e94f53af79bd3171 - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - constrains: - - jinja2 >=3.0.0 - license: BSD-3-Clause - license_family: BSD - size: 27813 - timestamp: 1729351491668 -- kind: conda - name: matplotlib-inline - version: 0.1.7 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_0.conda - sha256: 7ea68676ea35fbb095420bbcc1c82c4767b8be7bb56abb6989b7f89d957a3bab - md5: 779345c95648be40d22aaa89de7d4254 - depends: - - python >=3.6 - - traitlets - license: BSD-3-Clause - license_family: BSD - size: 14599 - timestamp: 1713250613726 -- kind: conda - name: mistune - version: 3.0.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda - sha256: f95cb70007e3cc2ba44e17c29a056b499e6dadf08746706d0c817c8e2f47e05c - md5: 5cbee699846772cc939bef23a0d524ed - depends: - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - size: 66022 - timestamp: 1698947249750 -- kind: conda - name: nbclient - version: 0.10.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.0-pyhd8ed1ab_0.conda - sha256: 589d72d36d61a23b39d6fff2c488f93e29e20de4fc6f5d315b5f2c16e81028bf - md5: 15b51397e0fe8ea7d7da60d83eb76ebc - depends: - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* - - nbformat >=5.1 - - python >=3.8 - - traitlets >=5.4 - license: BSD-3-Clause - license_family: BSD - size: 27851 - timestamp: 1710317767117 -- kind: conda - name: nbconvert-core - version: 7.16.4 - build: pyhd8ed1ab_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.4-pyhd8ed1ab_1.conda - sha256: 074d858c5808e0a832acc0da37cd70de1565e8d6e17a62d5a11b3902b5e78319 - md5: e2d2abb421c13456a9a9f80272fdf543 - depends: - - beautifulsoup4 - - bleach - - defusedxml - - entrypoints >=0.2.2 - - jinja2 >=3.0 - - jupyter_core >=4.7 - - jupyterlab_pygments - - markupsafe >=2.0 - - mistune >=2.0.3,<4 - - nbclient >=0.5.0 - - nbformat >=5.1 - - packaging - - pandocfilters >=1.4.1 - - pygments >=2.4.1 - - python >=3.8 - - tinycss2 - - traitlets >=5.0 - constrains: - - nbconvert =7.16.4=*_1 - - pandoc >=2.9.2,<4.0.0 - license: BSD-3-Clause - license_family: BSD - size: 189599 - timestamp: 1718135529468 -- kind: conda - name: nbformat - version: 5.10.4 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_0.conda - sha256: 36fe73da4d37bc7ac2d1540526ecd294fbd09acda04e096181ab8f1ccd2b464c - md5: 0b57b5368ab7fc7cdc9e3511fa867214 - depends: - - jsonschema >=2.6 - - jupyter_core >=4.12,!=5.0.* - - python >=3.8 - - python-fastjsonschema >=2.15 - - traitlets >=5.1 - license: BSD-3-Clause - license_family: BSD - size: 101232 - timestamp: 1712239122969 - kind: conda name: ncurses version: '6.5' @@ -2190,57 +418,6 @@ packages: license: X11 AND BSD-3-Clause size: 889086 timestamp: 1724658547447 -- kind: conda - name: nest-asyncio - version: 1.6.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda - sha256: 30db21d1f7e59b3408b831a7e0417b83b53ee6223afae56482c5f26da3ceb49a - md5: 6598c056f64dc8800d40add25e4e2c34 - depends: - - python >=3.5 - license: BSD-2-Clause - license_family: BSD - size: 11638 - timestamp: 1705850780510 -- kind: conda - name: notebook - version: 7.2.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/notebook-7.2.2-pyhd8ed1ab_0.conda - sha256: 613242d5151a4d70438bb2d65041c509e4376b7e18c06c3795c52a18176e41dc - md5: c4d5a58f43ce9ffa430e6ecad6c30a42 - depends: - - jupyter_server >=2.4.0,<3 - - jupyterlab >=4.2.0,<4.3 - - jupyterlab_server >=2.27.1,<3 - - notebook-shim >=0.2,<0.3 - - python >=3.8 - - tornado >=6.2.0 - license: BSD-3-Clause - license_family: BSD - size: 3904930 - timestamp: 1724861465900 -- kind: conda - name: notebook-shim - version: 0.2.4 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_0.conda - sha256: 9b5fdef9ebe89222baa9da2796ebe7bc02ec6c5a1f61327b651d6b92cf9a0230 - md5: 3d85618e2c97ab896b5b5e298d32b5b3 - depends: - - jupyter_server >=1.8,<3 - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - size: 16880 - timestamp: 1707957948029 - kind: conda name: openssl version: 3.3.2 @@ -2274,314 +451,15 @@ packages: license_family: Apache size: 2891789 timestamp: 1725410790053 -- kind: conda - name: overrides - version: 7.7.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_0.conda - sha256: 5e238e5e646414d517a13f6786c7227206ace58271e3ef63f6adca4d6a4c2839 - md5: 24fba5a9d161ad8103d4e84c0e1a3ed4 - depends: - - python >=3.6 - - typing_utils - license: Apache-2.0 - license_family: APACHE - size: 30232 - timestamp: 1706394723472 -- kind: conda - name: packaging - version: '24.1' - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda - sha256: 36aca948219e2c9fdd6d80728bcc657519e02f06c2703d8db3446aec67f51d81 - md5: cbe1bb1f21567018ce595d9c2be0f0db - depends: - - python >=3.8 - license: Apache-2.0 - license_family: APACHE - size: 50290 - timestamp: 1718189540074 -- kind: conda - name: pandocfilters - version: 1.5.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f - md5: 457c2c8c08e54905d6954e79cb5b5db9 - depends: - - python !=3.0,!=3.1,!=3.2,!=3.3 - license: BSD-3-Clause - license_family: BSD - size: 11627 - timestamp: 1631603397334 -- kind: conda - name: parso - version: 0.8.4 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_0.conda - sha256: bfe404eebb930cc41782d34f8fc04c0388ea692eeebe2c5fc28df8ec8d4d61ae - md5: 81534b420deb77da8833f2289b8d47ac - depends: - - python >=3.6 - license: MIT - license_family: MIT - size: 75191 - timestamp: 1712320447201 -- kind: conda - name: pexpect - version: 4.9.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_0.conda - sha256: 90a09d134a4a43911b716d4d6eb9d169238aff2349056f7323d9db613812667e - md5: 629f3203c99b32e0988910c93e77f3b6 - depends: - - ptyprocess >=0.5 - - python >=3.7 - license: ISC - size: 53600 - timestamp: 1706113273252 -- kind: conda - name: pickleshare - version: 0.7.5 - build: py_1003 - build_number: 1003 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2 - sha256: a1ed1a094dd0d1b94a09ed85c283a0eb28943f2e6f22161fb45e128d35229738 - md5: 415f0ebb6198cc2801c73438a9fb5761 - depends: - - python >=3 - license: MIT - license_family: MIT - size: 9332 - timestamp: 1602536313357 -- kind: conda - name: pkgutil-resolve-name - version: 1.3.10 - build: pyhd8ed1ab_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_1.conda - sha256: fecf95377134b0e8944762d92ecf7b0149c07d8186fb5db583125a2705c7ea0a - md5: 405678b942f2481cecdb3e010f4925d9 - depends: - - python >=3.6 - license: MIT AND PSF-2.0 - size: 10778 - timestamp: 1694617398467 -- kind: conda - name: platformdirs - version: 4.3.6 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_0.conda - sha256: c81bdeadc4adcda216b2c7b373f0335f5c78cc480d1d55d10f21823590d7e46f - md5: fd8f2b18b65bbf62e8f653100690c8d2 - depends: - - python >=3.8 - license: MIT - license_family: MIT - size: 20625 - timestamp: 1726613611845 -- kind: conda - name: prometheus_client - version: 0.21.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.0-pyhd8ed1ab_0.conda - sha256: 01f0c3dd00081637ed920a922b17bcc8ed49608404ee466ced806856e671f6b9 - md5: 07e9550ddff45150bfc7da146268e165 - depends: - - python >=3.8 - license: Apache-2.0 - license_family: Apache - size: 49024 - timestamp: 1726902073034 -- kind: conda - name: prompt-toolkit - version: 3.0.48 - build: pyha770c72_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.48-pyha770c72_0.conda - sha256: 44e4e6108d425a666856a52d1523e5d70890256a8920bb0dcd3d55cc750f3207 - md5: 4c05134c48b6a74f33bbb9938e4a115e - depends: - - python >=3.7 - - wcwidth - constrains: - - prompt_toolkit 3.0.48 - license: BSD-3-Clause - license_family: BSD - size: 270271 - timestamp: 1727341744544 -- kind: conda - name: prompt_toolkit - version: 3.0.48 - build: hd8ed1ab_0 - subdir: noarch - noarch: generic - url: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.48-hd8ed1ab_0.conda - sha256: a26eed22badba036b35b8f0a3cc4d17130d7e43c80d3aa258b465dd7d69362a0 - md5: 60a2aeff42b5d629d45cc1be38ec1c5d - depends: - - prompt-toolkit >=3.0.48,<3.0.49.0a0 - license: BSD-3-Clause - license_family: BSD - size: 6664 - timestamp: 1727341747447 -- kind: conda - name: psutil - version: 6.1.0 - build: py313h536fd9c_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/psutil-6.1.0-py313h536fd9c_0.conda - sha256: 4afc1ebb9325389df1ff3260fcef8078c8552aba26d0fbefd3aa2b3f04a407b8 - md5: b50a00ebd2fda55306b8a095363ce27f - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - size: 494158 - timestamp: 1729847232458 -- kind: conda - name: psutil - version: 6.1.0 - build: py313ha7868ed_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/psutil-6.1.0-py313ha7868ed_0.conda - sha256: d58defe84d46da1a7e80283e165d6a9d09378fd830b48917751a318ab5a5d4ce - md5: d13841485f9159f317a4e8bb974e9c8e - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - size: 508489 - timestamp: 1729847497486 -- kind: conda - name: ptyprocess - version: 0.7.0 - build: pyhd3deb0d_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2 - sha256: fb31e006a25eb2e18f3440eb8d17be44c8ccfae559499199f73584566d0a444a - md5: 359eeb6536da0e687af562ed265ec263 - depends: - - python - license: ISC - size: 16546 - timestamp: 1609419417991 -- kind: conda - name: pure_eval - version: 0.2.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_0.conda - sha256: dcfcb3cee1ae0a89729601582cc3edea20ba13c9493967a03a693c67567af0c8 - md5: 0f051f09d992e0d08941706ad519ee0e - depends: - - python >=3.5 - license: MIT - license_family: MIT - size: 16551 - timestamp: 1721585805256 -- kind: conda - name: pycparser - version: '2.22' - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - sha256: 406001ebf017688b1a1554b49127ca3a4ac4626ec0fd51dc75ffa4415b720b64 - md5: 844d9eb3b43095b031874477f7d70088 - depends: - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - size: 105098 - timestamp: 1711811634025 -- kind: conda - name: pygments - version: 2.18.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - sha256: 78267adf4e76d0d64ea2ffab008c501156c108bb08fecb703816fb63e279780b - md5: b7f5c092b8f9800150d998a71b76d5a1 - depends: - - python >=3.8 - license: BSD-2-Clause - license_family: BSD - size: 879295 - timestamp: 1714846885370 -- kind: conda - name: pysocks - version: 1.7.1 - build: pyh0701188_6 - build_number: 6 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2 - sha256: b3a612bc887f3dd0fb7c4199ad8e342bd148cf69a9b74fd9468a18cf2bef07b7 - md5: 56cd9fe388baac0e90c7149cfac95b60 - depends: - - __win - - python >=3.8 - - win_inet_pton - license: BSD-3-Clause - license_family: BSD - size: 19348 - timestamp: 1661605138291 -- kind: conda - name: pysocks - version: 1.7.1 - build: pyha2e5f31_6 - build_number: 6 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b - md5: 2a7de29fb590ca14b5243c4c812c8025 - depends: - - __unix - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - size: 18981 - timestamp: 1661604969727 - kind: conda name: python - version: 3.13.0 - build: h9ebbce0_100_cp313 - build_number: 100 + version: 3.12.6 + build: hc5c86c4_2_cpython + build_number: 2 subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.0-h9ebbce0_100_cp313.conda - sha256: 6ab5179679f0909db828d8316f3b8b379014a82404807310fe7df5a6cf303646 - md5: 08e9aef080f33daeb192b2ddc7e4721f + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.6-hc5c86c4_2_cpython.conda + sha256: dda1e75f5227654c78d9143562366eff04444cc8b887cf8f0cc4f6236996b744 + md5: cebe1534cdebcac43acca87bec946b01 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 @@ -2589,257 +467,49 @@ packages: - libexpat >=2.6.3,<3.0a0 - libffi >=3.4,<4.0a0 - libgcc >=13 - - libmpdec >=4.0.0,<5.0a0 + - libnsl >=2.0.1,<2.1.0a0 - libsqlite >=3.46.1,<4.0a0 - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - openssl >=3.3.2,<4.0a0 - - python_abi 3.13.* *_cp313 - readline >=8.2,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 license: Python-2.0 - size: 33112481 - timestamp: 1728419573472 + size: 31531222 + timestamp: 1727721840884 - kind: conda name: python - version: 3.13.0 - build: hf5aa216_100_cp313 - build_number: 100 + version: 3.12.6 + build: hce54a09_2_cpython + build_number: 2 subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/python-3.13.0-hf5aa216_100_cp313.conda - sha256: 18f3f0bd514c9101d38d57835b2d027958f3ae4b3b65c22d187a857aa26b3a08 - md5: 3c2f7ad3f598480fe2a09e4e33cb1a2a + url: https://conda.anaconda.org/conda-forge/win-64/python-3.12.6-hce54a09_2_cpython.conda + sha256: 41325d4c2b5f8bda2b5dd4a71555ad12f3c78b7f0a00e41e57475822e7e89a73 + md5: c30b76855225babfbf18595408a377f3 depends: - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.3,<3.0a0 - - libffi >=3.4,<4.0a0 - - libmpdec >=4.0.0,<5.0a0 - - libsqlite >=3.46.1,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.3.2,<4.0a0 - - python_abi 3.13.* *_cp313 - - tk >=8.6.13,<8.7.0a0 - - tzdata - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - xz >=5.2.6,<6.0a0 - license: Python-2.0 - size: 16641177 - timestamp: 1728417810202 -- kind: conda - name: python-dateutil - version: 2.9.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - sha256: f3ceef02ac164a8d3a080d0d32f8e2ebe10dd29e3a685d240e38b3599e146320 - md5: 2cf4264fffb9e6eff6031c5b6884d61c - depends: - - python >=3.7 - - six >=1.5 - license: Apache-2.0 - license_family: APACHE - size: 222742 - timestamp: 1709299922152 -- kind: conda - name: python-fastjsonschema - version: 2.20.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.20.0-pyhd8ed1ab_0.conda - sha256: 7d8c931b89c9980434986b4deb22c2917b58d9936c3974139b9c10ae86fdfe60 - md5: b98d2018c01ce9980c03ee2850690fab - depends: - - python >=3.3 - license: BSD-3-Clause - license_family: BSD - size: 226165 - timestamp: 1718477110630 -- kind: conda - name: python-json-logger - version: 2.0.7 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - sha256: 4790787fe1f4e8da616edca4acf6a4f8ed4e7c6967aa31b920208fc8f95efcca - md5: a61bf9ec79426938ff785eb69dbb1960 - depends: - - python >=3.6 - license: BSD-2-Clause - license_family: BSD - size: 13383 - timestamp: 1677079727691 -- kind: conda - name: python_abi - version: '3.13' - build: 5_cp313 - build_number: 5 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.13-5_cp313.conda - sha256: 438225b241c5f9bddae6f0178a97f5870a89ecf927dfca54753e689907331442 - md5: 381bbd2a92c863f640a55b6ff3c35161 - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - size: 6217 - timestamp: 1723823393322 -- kind: conda - name: python_abi - version: '3.13' - build: 5_cp313 - build_number: 5 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.13-5_cp313.conda - sha256: 0c12cc1b84962444002c699ed21e815fb9f686f950d734332a1b74d07db97756 - md5: 44b4fe6f22b57103afb2299935c8b68e - constrains: - - python 3.13.* *_cp313 - license: BSD-3-Clause - license_family: BSD - size: 6716 - timestamp: 1723823166911 -- kind: conda - name: pytz - version: '2024.2' - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.2-pyhd8ed1ab_0.conda - sha256: 81c16d9183bb4a6780366ce874e567ee5fc903722f85b2f8d1d9479ef1dafcc9 - md5: 260009d03c9d5c0f111904d851f053dc - depends: - - python >=3.7 - license: MIT - license_family: MIT - size: 186995 - timestamp: 1726055625738 -- kind: conda - name: pywin32 - version: '307' - build: py313h5813708_3 - build_number: 3 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py313h5813708_3.conda - sha256: 0a68b324ea47ae720c62522c5d0bb5ea3e4987e1c5870d6490c7f954fbe14cbe - md5: 7113bd6cfe34e80d8211f7c019d14357 - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: PSF-2.0 - license_family: PSF - size: 6060096 - timestamp: 1728636763526 -- kind: conda - name: pywinpty - version: 2.0.14 - build: py313h5813708_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.14-py313h5813708_0.conda - sha256: eeee0a592dbadeb5b54fc53194984f6b1698f073f49b0d2402e474a620069542 - md5: be81b7b3aa13aeffd1cc26a372ef5cfc - depends: - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - winpty - license: MIT - license_family: MIT - size: 210042 - timestamp: 1729202970817 -- kind: conda - name: pyyaml - version: 6.0.2 - build: py313h536fd9c_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py313h536fd9c_1.conda - sha256: 86ae34bf2bab82c0fff2e31a37318c8977297776436df780a83c6efa5f84749d - md5: 3789f360de131c345e96fbfc955ca80b - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - size: 205855 - timestamp: 1725456273924 -- kind: conda - name: pyyaml - version: 6.0.2 - build: py313ha7868ed_1 - build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py313ha7868ed_1.conda - sha256: ffa21c4715aa139d20c96ae7274fbb7de12a546f3332eb8d07cc794741fcbde6 - md5: c1743e5c4c7402a14b515cf276778e59 - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - yaml >=0.2.5,<0.3.0a0 - license: MIT - license_family: MIT - size: 181722 - timestamp: 1725456802746 -- kind: conda - name: pyzmq - version: 26.2.0 - build: py313h2100fd5_3 - build_number: 3 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/pyzmq-26.2.0-py313h2100fd5_3.conda - sha256: 971bea2fd92920327f4a44e69393643193b435c37e7528d93c32071e531fc9ba - md5: d0ce06d0a38f8ad0dc9b71e14137deee - depends: - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 + - libexpat >=2.6.3,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.46.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.3.2,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata - ucrt >=10.0.20348.0 - vc >=14.2,<15 - vc14_runtime >=14.29.30139 - - zeromq >=4.3.5,<4.3.6.0a0 - license: BSD-3-Clause - license_family: BSD - size: 367463 - timestamp: 1728643113504 -- kind: conda - name: pyzmq - version: 26.2.0 - build: py313h8e95178_3 - build_number: 3 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.2.0-py313h8e95178_3.conda - sha256: 0b26fe1cf10d3511b1ef72faedebfe57256e464a51d23e07153f09c6300ec42c - md5: 8ab50c9c9c3824ac0ffac9e9dcf5619e - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - - python >=3.13,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - zeromq >=4.3.5,<4.4.0a0 - license: BSD-3-Clause - license_family: BSD - size: 384582 - timestamp: 1728642439746 + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 15854309 + timestamp: 1727719258211 - kind: conda name: readline version: '8.2' @@ -2856,278 +526,6 @@ packages: license_family: GPL size: 281456 timestamp: 1679532220005 -- kind: conda - name: referencing - version: 0.35.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/referencing-0.35.1-pyhd8ed1ab_0.conda - sha256: be8d6d9e86b1a3fef5424127ff81782f8ca63d3058980859609f6f1ecdd34cb3 - md5: 0fc8b52192a8898627c3efae1003e9f6 - depends: - - attrs >=22.2.0 - - python >=3.8 - - rpds-py >=0.7.0 - license: MIT - license_family: MIT - size: 42210 - timestamp: 1714619625532 -- kind: conda - name: requests - version: 2.32.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda - sha256: 5845ffe82a6fa4d437a2eae1e32a1ad308d7ad349f61e337c0a890fe04c513cc - md5: 5ede4753180c7a550a443c430dc8ab52 - depends: - - certifi >=2017.4.17 - - charset-normalizer >=2,<4 - - idna >=2.5,<4 - - python >=3.8 - - urllib3 >=1.21.1,<3 - constrains: - - chardet >=3.0.2,<6 - license: Apache-2.0 - license_family: APACHE - size: 58810 - timestamp: 1717057174842 -- kind: conda - name: rfc3339-validator - version: 0.1.4 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_0.tar.bz2 - sha256: 7c7052b51de0b5c558f890bb11f8b5edbb9934a653d76be086b1182b9f54185d - md5: fed45fc5ea0813240707998abe49f520 - depends: - - python >=3.5 - - six - license: MIT - license_family: MIT - size: 8064 - timestamp: 1638811838081 -- kind: conda - name: rfc3986-validator - version: 0.1.1 - build: pyh9f0ad1d_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - sha256: 2a5b495a1de0f60f24d8a74578ebc23b24aa53279b1ad583755f223097c41c37 - md5: 912a71cc01012ee38e6b90ddd561e36f - depends: - - python - license: MIT - license_family: MIT - size: 7818 - timestamp: 1598024297745 -- kind: conda - name: rpds-py - version: 0.20.0 - build: py313h920b4c0_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py313h920b4c0_1.conda - sha256: d0794a48c97c930d94fa7985b04cddcbfe7059d45f700956011cb33df7831f5a - md5: 3588c602a679eb85c19be526705e5d46 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - size: 334430 - timestamp: 1725327282979 -- kind: conda - name: rpds-py - version: 0.20.0 - build: py313hf3b5b86_1 - build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.20.0-py313hf3b5b86_1.conda - sha256: c6a181b26b7493485416a742477dff29b0f8cd2e4b2c0e0a5cffb808545b458e - md5: b755b464a25fa3584fb56cddcab72c7c - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MIT - license_family: MIT - size: 208863 - timestamp: 1725328077851 -- kind: conda - name: send2trash - version: 1.8.3 - build: pyh0d859eb_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_0.conda - sha256: c4401b071e86ddfa0ea4f34b85308db2516b6aeca50053535996864cfdee7b3f - md5: 778594b20097b5a948c59e50ae42482a - depends: - - __linux - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - size: 22868 - timestamp: 1712585140895 -- kind: conda - name: send2trash - version: 1.8.3 - build: pyh5737063_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_0.conda - sha256: d8aa230501a33250af2deee03006a2579f0335e7240a9c7286834788dcdcfaa8 - md5: 5a86a21050ca3831ec7f77fb302f1132 - depends: - - __win - - python >=3.7 - - pywin32 - license: BSD-3-Clause - license_family: BSD - size: 23319 - timestamp: 1712585816346 -- kind: conda - name: setuptools - version: 75.1.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.1.0-pyhd8ed1ab_0.conda - sha256: 6725235722095c547edd24275053c615158d6163f396550840aebd6e209e4738 - md5: d5cd48392c67fb6849ba459c2c2b671f - depends: - - python >=3.8 - license: MIT - license_family: MIT - size: 777462 - timestamp: 1727249510532 -- kind: conda - name: six - version: 1.16.0 - build: pyh6c4a22f_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 - md5: e5f25f8dbc060e9a8d912e432202afc2 - depends: - - python - license: MIT - license_family: MIT - size: 14259 - timestamp: 1620240338595 -- kind: conda - name: sniffio - version: 1.3.1 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_0.conda - sha256: bc12100b2d8836b93c55068b463190505b8064d0fc7d025e89f20ebf22fe6c2b - md5: 490730480d76cf9c8f8f2849719c6e2b - depends: - - python >=3.7 - license: Apache-2.0 - license_family: Apache - size: 15064 - timestamp: 1708953086199 -- kind: conda - name: soupsieve - version: '2.5' - build: pyhd8ed1ab_1 - build_number: 1 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - sha256: 54ae221033db8fbcd4998ccb07f3c3828b4d77e73b0c72b18c1d6a507059059c - md5: 3f144b2c34f8cb5a9abd9ed23a39c561 - depends: - - python >=3.8 - license: MIT - license_family: MIT - size: 36754 - timestamp: 1693929424267 -- kind: conda - name: stack_data - version: 0.6.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda - sha256: a58433e75229bec39f3be50c02efbe9b7083e53a1f31d8ee247564f370191eec - md5: e7df0fdd404616638df5ece6e69ba7af - depends: - - asttokens - - executing - - pure_eval - - python >=3.5 - license: MIT - license_family: MIT - size: 26205 - timestamp: 1669632203115 -- kind: conda - name: terminado - version: 0.18.1 - build: pyh0d859eb_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - sha256: b300557c0382478cf661ddb520263508e4b3b5871b471410450ef2846e8c352c - md5: efba281bbdae5f6b0a1d53c6d4a97c93 - depends: - - __linux - - ptyprocess - - python >=3.8 - - tornado >=6.1.0 - license: BSD-2-Clause - license_family: BSD - size: 22452 - timestamp: 1710262728753 -- kind: conda - name: terminado - version: 0.18.1 - build: pyh5737063_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda - sha256: 8cb078291fd7882904e3de594d299c8de16dd3af7405787fce6919a385cfc238 - md5: 4abd500577430a942a995fd0d09b76a2 - depends: - - __win - - python >=3.8 - - pywinpty >=1.1.0 - - tornado >=6.1.0 - license: BSD-2-Clause - license_family: BSD - size: 22883 - timestamp: 1710262943966 -- kind: conda - name: tinycss2 - version: 1.4.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 - md5: f1acf5fdefa8300de697982bcb1761c9 - depends: - - python >=3.5 - - webencodings >=0.4 - license: BSD-3-Clause - license_family: BSD - size: 28285 - timestamp: 1729802975370 - kind: conda name: tk version: 8.6.13 @@ -3161,132 +559,6 @@ packages: license_family: BSD size: 3318875 timestamp: 1699202167581 -- kind: conda - name: tomli - version: 2.0.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.2-pyhd8ed1ab_0.conda - sha256: 5e742ba856168b606ac3c814d247657b1c33b8042371f1a08000bdc5075bc0cc - md5: e977934e00b355ff55ed154904044727 - depends: - - python >=3.7 - license: MIT - license_family: MIT - size: 18203 - timestamp: 1727974767524 -- kind: conda - name: tornado - version: 6.4.1 - build: py313h536fd9c_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.1-py313h536fd9c_1.conda - sha256: 29630b1f5452628b661a7cdde2c54aa7d9e31874d4ddb8080ad060c10e79063d - md5: 70b5b6dfd7d1760cd59849e2271d937b - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - license: Apache-2.0 - license_family: Apache - size: 863224 - timestamp: 1724960831827 -- kind: conda - name: tornado - version: 6.4.1 - build: py313ha7868ed_1 - build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/tornado-6.4.1-py313ha7868ed_1.conda - sha256: 63771acac59edb00920e69053e1452c2bea5906f12b9dfde2e7063ef5873e88a - md5: ef4504fee3b2345096fec32898bd0275 - depends: - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: Apache-2.0 - license_family: Apache - size: 868535 - timestamp: 1724961251062 -- kind: conda - name: traitlets - version: 5.14.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_0.conda - sha256: 8a64fa0f19022828513667c2c7176cfd125001f3f4b9bc00d33732e627dd2592 - md5: 3df84416a021220d8b5700c613af2dc5 - depends: - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - size: 110187 - timestamp: 1713535244513 -- kind: conda - name: types-python-dateutil - version: 2.9.0.20241003 - build: pyhff2d567_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241003-pyhff2d567_0.conda - sha256: 8489af986daebfbcd13d3748ba55431259206e37f184ab42a57e107fecd85e02 - md5: 3d326f8a2aa2d14d51d8c513426b5def - depends: - - python >=3.6 - license: Apache-2.0 AND MIT - size: 21765 - timestamp: 1727940339297 -- kind: conda - name: typing-extensions - version: 4.12.2 - build: hd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - sha256: d3b9a8ed6da7c9f9553c5fd8a4fca9c3e0ab712fa5f497859f82337d67533b73 - md5: 52d648bd608f5737b123f510bb5514b5 - depends: - - typing_extensions 4.12.2 pyha770c72_0 - license: PSF-2.0 - license_family: PSF - size: 10097 - timestamp: 1717802659025 -- kind: conda - name: typing_extensions - version: 4.12.2 - build: pyha770c72_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - sha256: 0fce54f8ec3e59f5ef3bb7641863be4e1bf1279623e5af3d3fa726e8f7628ddb - md5: ebe6952715e1d5eb567eeebf25250fa7 - depends: - - python >=3.8 - license: PSF-2.0 - license_family: PSF - size: 39888 - timestamp: 1717802653893 -- kind: conda - name: typing_utils - version: 0.1.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2 - sha256: 9e3758b620397f56fb709f796969de436d63b7117897159619b87938e1f78739 - md5: eb67e3cace64c66233e2d35949e20f92 - depends: - - python >=3.6.1 - license: Apache-2.0 - license_family: APACHE - size: 13829 - timestamp: 1622899345711 - kind: conda name: tzdata version: 2024a @@ -3314,40 +586,6 @@ packages: license_family: PROPRIETARY size: 1283972 timestamp: 1666630199266 -- kind: conda - name: uri-template - version: 1.3.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda - sha256: b76904b53721dc88a46352324c79d2b077c2f74a9f7208ad2c4249892669ae94 - md5: 0944dc65cb4a9b5b68522c3bb585d41c - depends: - - python >=3.7 - license: MIT - license_family: MIT - size: 23999 - timestamp: 1688655976471 -- kind: conda - name: urllib3 - version: 2.2.3 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.3-pyhd8ed1ab_0.conda - sha256: b6bb34ce41cd93956ad6eeee275ed52390fb3788d6c75e753172ea7ac60b66e5 - md5: 6b55867f385dd762ed99ea687af32a69 - depends: - - brotli-python >=1.0.9 - - h2 >=4,<5 - - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.8 - - zstandard >=0.18.0 - license: MIT - license_family: MIT - size: 98076 - timestamp: 1726496531769 - kind: conda name: vc version: '14.3' @@ -3397,112 +635,6 @@ packages: license_family: BSD size: 17241 timestamp: 1725984096440 -- kind: conda - name: wcwidth - version: 0.2.13 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda - sha256: b6cd2fee7e728e620ec736d8dfee29c6c9e2adbd4e695a31f1d8f834a83e57e3 - md5: 68f0738df502a14213624b288c60c9ad - depends: - - python >=3.8 - license: MIT - license_family: MIT - size: 32709 - timestamp: 1704731373922 -- kind: conda - name: webcolors - version: 24.8.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.8.0-pyhd8ed1ab_0.conda - sha256: ec71f97c332a7d328ae038990b8090cbfa772f82845b5d2233defd167b7cc5ac - md5: eb48b812eb4fbb9ff238a6651fdbbcae - depends: - - python >=3.5 - license: BSD-3-Clause - license_family: BSD - size: 18378 - timestamp: 1723294800217 -- kind: conda - name: webencodings - version: 0.5.1 - build: pyhd8ed1ab_2 - build_number: 2 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda - sha256: 2adf9bd5482802837bc8814cbe28d7b2a4cbd2e2c52e381329eaa283b3ed1944 - md5: daf5160ff9cde3a468556965329085b9 - depends: - - python >=2.6 - license: BSD-3-Clause - license_family: BSD - size: 15600 - timestamp: 1694681458271 -- kind: conda - name: websocket-client - version: 1.8.0 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_0.conda - sha256: 44a5e3b97feef24cd719f7851cca9af9799dc9c17d3e0298d5856baab2d682f5 - md5: f372c576b8774922da83cda2b12f9d29 - depends: - - python >=3.8 - license: Apache-2.0 - license_family: APACHE - size: 47066 - timestamp: 1713923494501 -- kind: conda - name: widgetsnbextension - version: 4.0.13 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_0.conda - sha256: d155adc10f8c96f76d4468dbe37b33b4334dadf5cd4a95841aa009ca9bced5fa - md5: 6372cd99502721bd7499f8d16b56268d - depends: - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - size: 898656 - timestamp: 1724331433259 -- kind: conda - name: win_inet_pton - version: 1.1.0 - build: pyh7428d3b_7 - build_number: 7 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_7.conda - sha256: c5297692ab34aade5e21107abaf623d6f93847662e25f655320038d2bfa1a812 - md5: c998c13b2f998af57c3b88c7a47979e0 - depends: - - __win - - python >=3.6 - license: LicenseRef-Public-Domain - size: 9602 - timestamp: 1727796413384 -- kind: conda - name: winpty - version: 0.4.3 - build: '4' - build_number: 4 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 - md5: 1cee351bf20b830d991dbe0bc8cd7dfe - arch: x86_64 - platform: win - license: MIT - license_family: MIT - size: 1176306 - kind: conda name: xz version: 5.2.6 @@ -3530,163 +662,3 @@ packages: license: LGPL-2.1 and GPL-2.0 size: 217804 timestamp: 1660346976440 -- kind: conda - name: yaml - version: 0.2.5 - build: h7f98852_2 - build_number: 2 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 - md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae - depends: - - libgcc-ng >=9.4.0 - license: MIT - license_family: MIT - size: 89141 - timestamp: 1641346969816 -- kind: conda - name: yaml - version: 0.2.5 - build: h8ffe710_2 - build_number: 2 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 - sha256: 4e2246383003acbad9682c7c63178e2e715ad0eb84f03a8df1fbfba455dfedc5 - md5: adbfb9f45d1004a26763652246a33764 - depends: - - vc >=14.1,<15.0a0 - - vs2015_runtime >=14.16.27012 - license: MIT - license_family: MIT - size: 63274 - timestamp: 1641347623319 -- kind: conda - name: zeromq - version: 4.3.5 - build: h3b0a872_6 - build_number: 6 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_6.conda - sha256: e67288b1c98a31ee58a5c07bdd873dbe08e75f752e1ad605d5e8c0697339903e - md5: 113506c8d2d558e733f5c38f6bf08c50 - depends: - - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - license: MPL-2.0 - license_family: MOZILLA - size: 335528 - timestamp: 1728364029042 -- kind: conda - name: zeromq - version: 4.3.5 - build: ha9f60a1_6 - build_number: 6 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_6.conda - sha256: c37130692742cc43eedf4e23270c7d1634235acff50760025e9583f8b46b64e6 - md5: 33a78bbc44d6550c361abb058a0556e2 - depends: - - krb5 >=1.21.3,<1.22.0a0 - - libsodium >=1.0.20,<1.0.21.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: MPL-2.0 - license_family: MOZILLA - size: 2701749 - timestamp: 1728364260886 -- kind: conda - name: zipp - version: 3.20.2 - build: pyhd8ed1ab_0 - subdir: noarch - noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.2-pyhd8ed1ab_0.conda - sha256: 1e84fcfa41e0afdd87ff41e6fbb719c96a0e098c1f79be342293ab0bd8dea322 - md5: 4daaed111c05672ae669f7036ee5bba3 - depends: - - python >=3.8 - license: MIT - license_family: MIT - size: 21409 - timestamp: 1726248679175 -- kind: conda - name: zstandard - version: 0.23.0 - build: py313h574b89f_1 - build_number: 1 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py313h574b89f_1.conda - sha256: 1d2744ec0e91da267ce749e19142081472539cb140a7dad0646cd249246691fe - md5: 8e017aca933f4dd25491151edd3e7820 - depends: - - cffi >=1.11 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - - zstd >=1.5.6,<1.5.7.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD - size: 325703 - timestamp: 1725305947138 -- kind: conda - name: zstandard - version: 0.23.0 - build: py313h80202fe_1 - build_number: 1 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py313h80202fe_1.conda - sha256: ea82f2b8964150a3aa7373b4697e48e64f2200fe68ae554ee85c641c692d1c97 - md5: c178558ff516cd507763ffee230c20b2 - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.11 - - libgcc >=13 - - python >=3.13.0rc1,<3.14.0a0 - - python_abi 3.13.* *_cp313 - - zstd >=1.5.6,<1.5.7.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: BSD-3-Clause - license_family: BSD - size: 424424 - timestamp: 1725305749031 -- kind: conda - name: zstd - version: 1.5.6 - build: h0ea2cb4_0 - subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.6-h0ea2cb4_0.conda - sha256: 768e30dc513568491818fb068ee867c57c514b553915536da09e5d10b4ebf3c3 - md5: 9a17230f95733c04dc40a2b1e5491d74 - depends: - - libzlib >=1.2.13,<2.0.0a0 - - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-3-Clause - license_family: BSD - size: 349143 - timestamp: 1714723445995 -- kind: conda - name: zstd - version: 1.5.6 - build: ha6fb4c9_0 - subdir: linux-64 - url: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda - sha256: c558b9cc01d9c1444031bd1ce4b9cff86f9085765f17627a6cd85fc623c8a02b - md5: 4d056880988120e29d75bfff282e0f45 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - libzlib >=1.2.13,<2.0.0a0 - license: BSD-3-Clause - license_family: BSD - size: 554846 - timestamp: 1714722996770 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/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/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 coefficient [-] 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_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 coefficient [-] 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; ) + set_states!(instate_path, model, state_ncnames; ) 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 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), ), )