soil_dataprovider_nl 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ # Copyright 2026 Wageningen Environmental Research, Wageningen-UR
2
+
3
+ # Licensed under the EUPL, Version 1.2 or – as soon they
4
+ # will be approved by the European Commission - subsequent
5
+ # versions of the EUPL (the "Licence");
6
+ # You may not use this work except in compliance with the
7
+ # Licence.
8
+ # You may obtain a copy of the Licence at:
9
+
10
+ # https://interoperable-europe.ec.europa.eu/collection/eupl/news/understanding-eupl-v12
11
+
12
+ # Unless required by applicable law or agreed to in
13
+ # writing, software distributed under the Licence is
14
+ # distributed on an "AS IS" basis,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16
+ # express or implied.
17
+ # See the Licence for the specific language governing
18
+ # permissions and limitations under the Licence.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: soil_dataprovider_nl
3
+ Version: 1.0.0
4
+ Summary: This package provides a set of data providers that can derive the soil parameters for running crop/soil models in PCSE based on the Dutch soil map.
5
+ Author-email: Allard de Wit <allard.dewit@wur.nl>, Herman Berghuijs <herman.berghuijs@wur.nl>
6
+ Maintainer-email: Allard de Wit <allard.dewit@wur.nl>, Herman Berghuijs <herman.berghuijs@wur.nl>
7
+ License-Expression: EUPL-1.2
8
+ Project-URL: Homepage, https://github.com/ajwdewit/soil_dataprovider_nl
9
+ Project-URL: documentation, https://github.com/ajwdewit/soil_dataprovider_nl
10
+ Project-URL: repository, https://github.com/ajwdewit/soil_dataprovider_nl
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pandas
15
+ Requires-Dist: pyproj==3.8
16
+ Requires-Dist: duckdb==1.5
17
+ Provides-Extra: notebook
18
+ Requires-Dist: matplotlib; extra == "notebook"
19
+ Requires-Dist: ipyleaflet; extra == "notebook"
20
+ Requires-Dist: pcse>=6.0.13; extra == "notebook"
21
+ Dynamic: license-file
22
+
23
+ # Soil data providers for Dutch soils for PCSE
24
+
25
+ ---
26
+ Allard de Wit
27
+ Wageningen Environmental Research
28
+ September 2026
29
+ ---
30
+
31
+ This package provides a set of data providers that can provide the soil parameters for the Netherlands as input
32
+ for running crop/soil models in PCSE. It takes the input from the [Dutch soil map](https://app.pdok.nl/viewer/#x=160000.00&y=455000.00&z=3.0000&background=BRT-A%20standaard&layers=d9cc67ba-5491-4640-86ac-b8d392250270;soilarea;_;1)
33
+ but the soil data itself is hosted in a [duckdb file on github](https://github.com/ajwdewit/collections/raw/refs/heads/main/BodemkaartNL/bofek_soil_nl.ddb).
34
+
35
+ Getting soil information has always been a bit of a hassle for models in [PCSE](https://pcse.readthedocs.io).
36
+ While crop parameters are well managed through [YAML files on github](https://github.com/ajwdewit/WOFOST_crop_parameters),
37
+ soil parameters are more difficult as they are spatially variable and thus location dependent. Moreover, soil
38
+ maps and definitions are variable between countries making it hard to develop dataproviders for soils.
39
+
40
+ This soil data provider derives soil parameters from the Dutch soil map and derives soil hydraulic properties as well
41
+ as soil mineralogic properties (bulk density, organic matter content) depending on the type of PCSE soil component used.
42
+ Several data providers are available from this package:
43
+ - `SoilDataProviderNL_CWB`
44
+ - `SoilDataProviderNL_MLWB` *under development*
45
+ - `SoilDataProviderNL_MLWB_SNOMIN` *under development*
46
+
47
+ These dataproviders map to the different soil modules in pcse: the classic soil water balance (CWB), the multi-layered
48
+ soil water balance (MLWB) and the multi-layered water balance plus the SNOMIN carbon/nitrogen model (MLWB_SNOMIN).
49
+ The correct soil dataprovider will be selected automatically based on the pcse model that you pass in to the call
50
+ to `soil_dataprovider_nl.SoilDataProviderNL`, however they can also be imported manually from the package.
51
+
52
+ # Setting up the package
53
+
54
+ ## Dependencies
55
+
56
+ This package has been developed using python 3.10 and has dependencies on several other packages:
57
+ - pandas
58
+ - numpy
59
+ - duckdb == 1.5
60
+ - matplotlib
61
+ - pyproj == 3.7
62
+
63
+
64
+ ## Installing
65
+
66
+ The package can be pip-installed from PyPI: `pip install soil_dataprovider_nl` should be sufficient.
67
+
68
+ Dependencies should be automatically downloaded and installed during installing the package, although the `pyproj` dependency
69
+ can be problematic sometimes. In that case, install `pyproj` manually in a conda environment with the conda package
70
+ manager.
71
+
72
+ ## Usage
73
+
74
+ Using the soil dataprovider is easy:
75
+ ```python
76
+ >> from soil_dataprovider_nl import SoilDataProviderNL
77
+ >> from pcse.models import Wofost72_WLP_CWB
78
+ >> soild = SoilDataProviderNL(Wofost72_WLP_CWB, xcoord=170342, ycoord=438503)
79
+ >> print(soild)
80
+ Soil properties for location at X/Y: 170342/438503 - lon/lat: 5.611/51.936
81
+ Soil rootable depth estimated at 120 cm (from maximum soil profile depth)
82
+ Soil profile characteristics:
83
+ thickness pclay psilt psand SMW SMFCF SM0
84
+ 25 0.23 0.40 0.37 0.122168 0.387381 0.429530
85
+ 35 0.23 0.40 0.37 0.142978 0.413028 0.472326
86
+ 30 0.20 0.25 0.55 0.142978 0.413028 0.472326
87
+ 30 0.04 0.07 0.89 0.040080 0.269736 0.387057
88
+ Actual CWB parameter values:
89
+ - SMW: 0.113 [-]
90
+ - SMFCF: 0.372 [-]
91
+ - SM0: 0.442 [-]
92
+ - CRAIRC: 0.035 [-]
93
+ - RDMSOL: 120.000 [cm]
94
+ - SOPE: 8.436 [cm day-1]
95
+ - KSUB: 8.436 [cm day-1]
96
+ ```
97
+
98
+ The derived soildata can then be used in the usual procedure for running a model (in pseudo code):
99
+ ```python
100
+ >> params = ParameterProvider(cropdata=cropd, soildata=soild, sitedata=sited)
101
+ >> agro = get_agromanagement(...)
102
+ >> wdp = get_weatherdata(...)
103
+ >> wofost = Wofost72_WLP_CWB(params, wdp, agro)
104
+ >> wofost.run_till_terminate()
105
+ ...
106
+ ```
107
+
108
+ See for more elaborate examples the [notebook on the repository](https://github.com/ajwdewit/soil_dataprovider_nl).
@@ -0,0 +1,86 @@
1
+ # Soil data providers for Dutch soils for PCSE
2
+
3
+ ---
4
+ Allard de Wit
5
+ Wageningen Environmental Research
6
+ September 2026
7
+ ---
8
+
9
+ This package provides a set of data providers that can provide the soil parameters for the Netherlands as input
10
+ for running crop/soil models in PCSE. It takes the input from the [Dutch soil map](https://app.pdok.nl/viewer/#x=160000.00&y=455000.00&z=3.0000&background=BRT-A%20standaard&layers=d9cc67ba-5491-4640-86ac-b8d392250270;soilarea;_;1)
11
+ but the soil data itself is hosted in a [duckdb file on github](https://github.com/ajwdewit/collections/raw/refs/heads/main/BodemkaartNL/bofek_soil_nl.ddb).
12
+
13
+ Getting soil information has always been a bit of a hassle for models in [PCSE](https://pcse.readthedocs.io).
14
+ While crop parameters are well managed through [YAML files on github](https://github.com/ajwdewit/WOFOST_crop_parameters),
15
+ soil parameters are more difficult as they are spatially variable and thus location dependent. Moreover, soil
16
+ maps and definitions are variable between countries making it hard to develop dataproviders for soils.
17
+
18
+ This soil data provider derives soil parameters from the Dutch soil map and derives soil hydraulic properties as well
19
+ as soil mineralogic properties (bulk density, organic matter content) depending on the type of PCSE soil component used.
20
+ Several data providers are available from this package:
21
+ - `SoilDataProviderNL_CWB`
22
+ - `SoilDataProviderNL_MLWB` *under development*
23
+ - `SoilDataProviderNL_MLWB_SNOMIN` *under development*
24
+
25
+ These dataproviders map to the different soil modules in pcse: the classic soil water balance (CWB), the multi-layered
26
+ soil water balance (MLWB) and the multi-layered water balance plus the SNOMIN carbon/nitrogen model (MLWB_SNOMIN).
27
+ The correct soil dataprovider will be selected automatically based on the pcse model that you pass in to the call
28
+ to `soil_dataprovider_nl.SoilDataProviderNL`, however they can also be imported manually from the package.
29
+
30
+ # Setting up the package
31
+
32
+ ## Dependencies
33
+
34
+ This package has been developed using python 3.10 and has dependencies on several other packages:
35
+ - pandas
36
+ - numpy
37
+ - duckdb == 1.5
38
+ - matplotlib
39
+ - pyproj == 3.7
40
+
41
+
42
+ ## Installing
43
+
44
+ The package can be pip-installed from PyPI: `pip install soil_dataprovider_nl` should be sufficient.
45
+
46
+ Dependencies should be automatically downloaded and installed during installing the package, although the `pyproj` dependency
47
+ can be problematic sometimes. In that case, install `pyproj` manually in a conda environment with the conda package
48
+ manager.
49
+
50
+ ## Usage
51
+
52
+ Using the soil dataprovider is easy:
53
+ ```python
54
+ >> from soil_dataprovider_nl import SoilDataProviderNL
55
+ >> from pcse.models import Wofost72_WLP_CWB
56
+ >> soild = SoilDataProviderNL(Wofost72_WLP_CWB, xcoord=170342, ycoord=438503)
57
+ >> print(soild)
58
+ Soil properties for location at X/Y: 170342/438503 - lon/lat: 5.611/51.936
59
+ Soil rootable depth estimated at 120 cm (from maximum soil profile depth)
60
+ Soil profile characteristics:
61
+ thickness pclay psilt psand SMW SMFCF SM0
62
+ 25 0.23 0.40 0.37 0.122168 0.387381 0.429530
63
+ 35 0.23 0.40 0.37 0.142978 0.413028 0.472326
64
+ 30 0.20 0.25 0.55 0.142978 0.413028 0.472326
65
+ 30 0.04 0.07 0.89 0.040080 0.269736 0.387057
66
+ Actual CWB parameter values:
67
+ - SMW: 0.113 [-]
68
+ - SMFCF: 0.372 [-]
69
+ - SM0: 0.442 [-]
70
+ - CRAIRC: 0.035 [-]
71
+ - RDMSOL: 120.000 [cm]
72
+ - SOPE: 8.436 [cm day-1]
73
+ - KSUB: 8.436 [cm day-1]
74
+ ```
75
+
76
+ The derived soildata can then be used in the usual procedure for running a model (in pseudo code):
77
+ ```python
78
+ >> params = ParameterProvider(cropdata=cropd, soildata=soild, sitedata=sited)
79
+ >> agro = get_agromanagement(...)
80
+ >> wdp = get_weatherdata(...)
81
+ >> wofost = Wofost72_WLP_CWB(params, wdp, agro)
82
+ >> wofost.run_till_terminate()
83
+ ...
84
+ ```
85
+
86
+ See for more elaborate examples the [notebook on the repository](https://github.com/ajwdewit/soil_dataprovider_nl).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=42",
4
+ "wheel"
5
+ ]
6
+ build-backend = "setuptools.build_meta"
7
+
8
+ [project]
9
+ name = "soil_dataprovider_nl"
10
+ version = "1.0.0"
11
+ authors = [
12
+ { name="Allard de Wit", email="allard.dewit@wur.nl" },
13
+ { name="Herman Berghuijs", email="herman.berghuijs@wur.nl" },
14
+ ]
15
+ maintainers = [
16
+ { name="Allard de Wit", email="allard.dewit@wur.nl" },
17
+ { name="Herman Berghuijs", email="herman.berghuijs@wur.nl" },
18
+ ]
19
+ description = "This package provides a set of data providers that can derive the soil parameters for running crop/soil models in PCSE based on the Dutch soil map."
20
+ readme = "README.md"
21
+ requires-python = ">=3.12"
22
+ dependencies = [
23
+ "pandas",
24
+ "pyproj==3.8",
25
+ "duckdb == 1.5",
26
+ ]
27
+ license = "EUPL-1.2"
28
+ license-files = ["LICENSE"]
29
+
30
+ [project.optional-dependencies]
31
+ notebook = ["matplotlib", "ipyleaflet", "pcse>=6.0.13"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/ajwdewit/soil_dataprovider_nl"
35
+ documentation = "https://github.com/ajwdewit/soil_dataprovider_nl"
36
+ repository = "https://github.com/ajwdewit/soil_dataprovider_nl"
37
+
38
+ [[tool.uv.index]]
39
+ name = "testpypi"
40
+ url = "https://test.pypi.org/simple/"
41
+ publish-url = "https://test.pypi.org/legacy/"
42
+ explicit = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ # Copyright (c) 2026 Wageningen Environmental Research, Wageningen-UR
2
+ # Allard de Wit (allard.dewit@wur.nl), April 2026
3
+ from .soil_dataprovider_nl import (
4
+ SoilDataProviderNL_CWB,
5
+ SoilDataProviderNL_MLWB,
6
+ SoilDataProviderNL_MLWB_SNOMIN,
7
+ )
8
+
9
+ __version__ = "1.0.0"
10
+
11
+
12
+ class SoilDataProviderNL:
13
+ """Convenience class that selected the right soil data provider based on the model annotations.
14
+ """
15
+
16
+ def __new__(cls, model, *args, **kwargs):
17
+
18
+ if model.__nitrogenbalance__ == "SNOMIN":
19
+ final_cls = SoilDataProviderNL_MLWB_SNOMIN
20
+ elif model.__waterbalance__ == "MLWB":
21
+ final_cls = SoilDataProviderNL_MLWB
22
+ elif model.__waterbalance__ == "CWB":
23
+ final_cls = SoilDataProviderNL_CWB
24
+ else:
25
+ raise RuntimeError("soil module not recognized")
26
+
27
+ # Here we instantiate the selected class with __new__()
28
+ instance = final_cls.__new__(final_cls, *args, **kwargs)
29
+ # Here we initialize the instance with an explicit call to __init__()
30
+ instance.__init__(*args, **kwargs)
31
+ return instance
32
+
@@ -0,0 +1,64 @@
1
+ # Copyright (c) 2026 Wageningen Environmental Research, Wageningen-UR
2
+ # Allard de Wit (allard.dewit@wur.nl), April 2026
3
+ import pyproj
4
+
5
+
6
+ class CoordinateStore:
7
+ """This class stores the coordinates and provides several function
8
+ - It determines if provided coordinates are lat/lon or Dutch RD-stelsel coordinates.
9
+ - Converts RD to lat/lon and vice versa
10
+ - It checks if the coordinates are within the boundinng box for NL
11
+
12
+ self.xcoord, self.ycoord provide the coordinates Dutch RD-stelsel coordinates
13
+ self.lon, self.lat provide the coordinates in longitude/latitude
14
+ """
15
+ rd_stelsel = "epsg:7415"
16
+
17
+ def __init__(self, x, y):
18
+ self._conv = pyproj.Proj(self.rd_stelsel)
19
+
20
+ if self._is_valid_xcoord_rd(x) and self._is_valid_ycoord_rd(y):
21
+ self.xcoord = x
22
+ self.ycoord = y
23
+ self.lon, self.lat = self.from_RD(x, y)
24
+ elif self._is_valid_longitude(x) and self._is_valid_latitude(y):
25
+ self.lon = x
26
+ self.lat = y
27
+ self.xcoord, self.ycoord = self.from_lonlat(x, y)
28
+ else:
29
+ raise ValueError("Coordinates out of bounding box. Did you swap longitude/latitude?")
30
+
31
+ def from_RD(self, x, y):
32
+ return self._conv(x, y, inverse=True)
33
+
34
+ def from_lonlat(self, lon, lat):
35
+ return self._conv(lon, lat)
36
+
37
+ @staticmethod
38
+ def _is_valid_xcoord_rd(xcoord):
39
+ rdx_min = 7000
40
+ rdx_max = 289000
41
+ return rdx_min < xcoord < rdx_max
42
+
43
+ @staticmethod
44
+ def _is_valid_ycoord_rd(ycoord):
45
+ rdy_min = 289000
46
+ rdy_max = 629000
47
+ return rdy_min < ycoord < rdy_max
48
+
49
+ @staticmethod
50
+ def _is_valid_longitude(lon):
51
+ lon_min = 3.2
52
+ lon_max = 7.3
53
+ return lon_min < lon < lon_max
54
+
55
+ @staticmethod
56
+ def _is_valid_latitude(lat):
57
+ lat_min = 50.7
58
+ lat_max = 53.6
59
+ return lat_min < lat < lat_max
60
+
61
+ def __str__(self):
62
+ msg = f"RD coordinates for current location: {self.xcoord}, {self.ycoord}\n"
63
+ msg += f"Lon/Lat coordinates for current location: {self.lon}, {self.lat}\n"
64
+ return msg
@@ -0,0 +1,98 @@
1
+ # Copyright (c) 2026 Wageningen Environmental Research, Wageningen-UR
2
+ # Allard de Wit (allard.dewit@wur.nl), April 2026
3
+ # Mualem van Genuchten functions based on R code provided by Martin Mulder
4
+ from dataclasses import dataclass
5
+
6
+ # #' Get water content based on pressure head
7
+ # #'
8
+ # #' @param H pressure head [cm].
9
+ # #' @param WCR residual water content [cm3 cm-3].
10
+ # #' @param WCS saturated water content [cm3 cm-3].
11
+ # #' @param ALPHA curve shape parameter [-].
12
+ # #' @param NPAR curve shape parameter [-].
13
+ # #' @return return water content [cm3 cm-3].
14
+ # #' @keywords internal
15
+ # get_wc_MvG <- function(H, WCR, WCS, ALPHA, NPAR) {
16
+ #
17
+ # # --- main part of procedure ---
18
+ #
19
+ # m <- 1 - 1 / NPAR
20
+ # wc <- WCR + (WCS - WCR) / ((1 + (ALPHA * H)^NPAR)^m)
21
+ #
22
+ # # --- return of procedure ---
23
+ #
24
+ # return (wc)
25
+
26
+ @dataclass()
27
+ class MualemvanGenuchten:
28
+ wcr: float
29
+ wcs:float
30
+ alpha: float
31
+ npar: float
32
+ lamda: float
33
+ ksat: float
34
+
35
+
36
+ def get_water_content_from_MvG(H, p):
37
+ """return volumetric water content for given pressure head H [cm] and MvG parameters p.
38
+
39
+ :param H: pressure head [cm].
40
+ :param p: Object representing MvG parameters having attributes:
41
+ - wcr: residual water content [cm3 cm-3].
42
+ - wcs: saturated water content [cm3 cm-3].
43
+ - alpha: curve shape parameter [-].
44
+ - npar: curve shape parameter [-].
45
+ :return: (array of) water content values for given MvG parameters
46
+ """
47
+ m = 1.0 - 1.0 / p.npar
48
+ wc = p.wcr + (p.wcs - p.wcr) / ((1 + (p.alpha * H) ** p.npar) ** m)
49
+
50
+ return wc
51
+
52
+
53
+
54
+ #' Get conductivity based on pressure head
55
+ #'
56
+ #' @param H pressure head [cm]
57
+ #' @param ALPHA curve shape parameter [-]
58
+ #' @param NPAR curve shape parameter [-]
59
+ #' @param LAMBDA exponent in hydraulic conductivity function [-]
60
+ #' @param KSAT hydraulic conductivity of saturated soil [cm d-1]
61
+ #' @return return conductivity [cm d-1]
62
+ #' @keywords internal
63
+ # get_cond_MvG <- function(H, ALPHA, NPAR, LAMBDA, KSAT) {
64
+ #
65
+ # # --- main part of procedure ---
66
+ #
67
+ # m <- 1 - 1 / NPAR
68
+ # ah <- ALPHA * H
69
+ # h1 <- (1 + ah^NPAR)^m
70
+ # h2 <- ah**(NPAR - 1)
71
+ # denom <- (1 + ah^NPAR)^(m*(LAMBDA + 2))
72
+ # cond <- KSAT * (h1 - h2)^2 / denom
73
+ #
74
+ # # --- return of procedure ---
75
+ #
76
+ # return (cond)
77
+ # }
78
+
79
+ def get_conductivity_from_MvG(H, p):
80
+ """Get conductivity from pressure head H [cm] and MvG parameters p.
81
+
82
+ :param H: pressure head [cm]
83
+ :param p: Object representing MvG parameters having attributes:
84
+ - alpha: curve shape parameter [-]
85
+ - npar: curve shape parameter [-]
86
+ - lamda: exponent in hydraulic conductivity function [-]
87
+ - ksat: hydraulic conductivity of saturated soil [cm d-1]
88
+ :return: conductivity [cm d-1]
89
+ """
90
+
91
+ m = 1 - 1 / p.npar
92
+ ah = p.alpha * H
93
+ h1 = (1 + ah ** p.npar) ** m
94
+ h2 = ah ** (p.npar - 1)
95
+ denom = (1 + ah ** p.npar) ** (m * (p.lamda + 2))
96
+ cond = p.ksat * (h1 - h2)**2 / denom
97
+
98
+ return cond
@@ -0,0 +1,397 @@
1
+ # Copyright (c) 2026 Wageningen Environmental Research, Wageningen-UR
2
+ # Allard de Wit (allard.dewit@wur.nl), April 2026
3
+ import hashlib
4
+ import tempfile
5
+ import urllib.request
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ import pandas as pd
10
+
11
+ pd.options.mode.chained_assignment = None
12
+ import duckdb
13
+ import numpy as np
14
+
15
+ from .coords import CoordinateStore
16
+ from .mualemvangenuchten import (
17
+ MualemvanGenuchten,
18
+ get_conductivity_from_MvG,
19
+ get_water_content_from_MvG,
20
+ )
21
+
22
+ non_soil_codes = {99980, 99990, 99991}
23
+
24
+ this_dir = Path(__file__).parent
25
+ top_dir = this_dir.parent.absolute()
26
+ tmp_dir = Path(tempfile.gettempdir())
27
+
28
+
29
+ def plot_pF_curves(pF_results, fname_figure=None):
30
+ """Plots the pF curves based on input from the function `compute_pF_curves`
31
+
32
+ :param pF_results: the pF results from the function `compute_pF_curves`
33
+ :param fname_figure: optionally the file name where the figure will be written. Output format
34
+ will be derived from the filename extension png|jpg|etc.
35
+ :return: a matplotlib figure and axes
36
+ """
37
+
38
+ try:
39
+ import matplotlib.pyplot as plt
40
+ except ImportError:
41
+ msg = "Install matplotlib for generating figures"
42
+ print(msg)
43
+ return None
44
+
45
+ fig, axes = plt.subplots(ncols=2, figsize=(10, 5))
46
+ for layer in pF_results:
47
+ label = f"layer {layer.layer_top}-{layer.layer_bottom}"
48
+ axes[0].plot(layer.pF_range, layer.pF_WC, label=label)
49
+ axes[1].plot(layer.pF_range, layer.pF_Cond, label=label)
50
+
51
+ axes[0].legend()
52
+ axes[0].set_xlabel("pF value")
53
+ axes[0].set_ylabel("Soil moisture content [-]")
54
+ axes[1].set_xlabel("pF value")
55
+ axes[1].set_ylabel("Soil conductivity [cm/day]")
56
+ fig.suptitle("PF curves")
57
+ if fname_figure is not None:
58
+ fig.savefig(fname_figure)
59
+
60
+ return fig, axes
61
+
62
+
63
+ def compute_pF_curves(profile_characteristics):
64
+ """Computes the pF curves for water retention and conductivey for all layeres in the profile
65
+
66
+ :param profile_characteristics: the profile characteristics from BOFEK
67
+ :return: a list the dicts containing the pF curves and additional info for each layer.
68
+ """
69
+
70
+ pF_range = np.arange(-1.0, 7.1, 0.1)
71
+ H = 10 ** pF_range
72
+ pF_results = []
73
+ for row in profile_characteristics.itertuples():
74
+ p_mvg = MualemvanGenuchten(wcr=row.ores, wcs=row.osat, alpha=row.alfa, npar=row.npar,
75
+ lamda=row.lexp, ksat=row.ksatfit)
76
+ pF_WC = get_water_content_from_MvG(H, p_mvg)
77
+ pF_Cond = get_conductivity_from_MvG(H, p_mvg)
78
+ pF_results.append(SimpleNamespace(idlayer=row.idlayer, pF_WC=pF_WC,pF_Cond=pF_Cond,
79
+ layer_top=row.layer_top, layer_bottom=row.layer_bottom,
80
+ pF_range=pF_range))
81
+
82
+ return pF_results
83
+
84
+
85
+ def find_profile_characteristics(DBconn, profile_code):
86
+ """Reads the soil profile characteristics from the database
87
+
88
+ :param profile_code: the profile code
89
+ :return: a dataframe with soil profile characteristics
90
+ """
91
+ sql = """SELECT t1.*, t2.*
92
+ FROM
93
+ soildb.soil_profiles t1
94
+ INNER JOIN
95
+ soildb.soil_physical_description t2 ON t1.soil_physical_code=t2.soil_physical_code
96
+ WHERE
97
+ t1.profile_code = ?
98
+ ORDER BY
99
+ t1.idlayer
100
+ """
101
+ df = DBconn.execute(sql, (profile_code,)).df()
102
+ if len(df) == 0:
103
+ msg = f"No valid soil profile description found for this soil profile code: {profile_code})!"
104
+ raise RuntimeError(msg)
105
+
106
+ df["thickness"] = df.layer_bottom - df.layer_top
107
+
108
+ return df
109
+
110
+
111
+ def find_soil_profile(DBconn, coords):
112
+ """Find the soil profile code based on given coordinates
113
+
114
+ :param DBconn: The database connection
115
+ :param coords: the coordinates in Dutch RD system.
116
+ :return: a profile code for the soil (integer)
117
+ """
118
+ sql = """SELECT profile_code FROM soildb.bofek_soil_nl t1
119
+ WHERE ST_intersects(t1.geom, ST_Point(?, ?))
120
+ """
121
+
122
+ cursor = DBconn.execute(sql, (coords.xcoord, coords.ycoord))
123
+ row = cursor.fetchone()
124
+ if not row:
125
+ msg = (f"No soil profile found for this location: (X:{coords.xcoord:.0f}, Y:{coords.ycoord:.0f})! "
126
+ f"Is this location on land?")
127
+ raise RuntimeError(msg)
128
+
129
+ profile_code = row[0]
130
+ if profile_code in non_soil_codes:
131
+ msg = (f"Not a valid soil profile found for this location: (X:{coords.xcoord:.0f}, Y:{coords.ycoord:.0f})! "
132
+ f"Probably an urban area, land fill or other location with no soil description!")
133
+ raise RuntimeError(msg)
134
+
135
+ return profile_code
136
+
137
+
138
+ class SoilBDconnector:
139
+ """Takes care of opening the connection to the duckdb file on github and manages the cached version of that file.
140
+ """
141
+ _bofek_soil_source = "https://github.com/ajwdewit/collections/raw/refs/heads/main/BodemkaartNL/bofek_soil_nl.ddb"
142
+ _bofek_soil_source_shasum = "https://raw.githubusercontent.com/ajwdewit/collections/main/BodemkaartNL/bofek_soil_nl.shasum"
143
+ _bofek_soil_cache = tmp_dir / "bofek_soil_nl.ddb"
144
+ _upstream_shasum = None
145
+
146
+ def __init__(self):
147
+ connected = self._has_connection()
148
+ if not connected:
149
+ msg = "No internet connection to retrieve BOFEK soildb and compare DB checksum!"
150
+ raise RuntimeError(msg)
151
+
152
+ if not self._bofek_soil_cache.exists():
153
+ print("Downloading Soil DB (~135 Mb)...")
154
+ urllib.request.urlretrieve(self._bofek_soil_source, self._bofek_soil_cache)
155
+ else:
156
+ local_shasum = self._compute_cache_sha1()
157
+ if local_shasum != self._upstream_shasum:
158
+ print("Updating Soil DB (~135 Mb)...")
159
+ urllib.request.urlretrieve(self._bofek_soil_source, self._bofek_soil_cache)
160
+ else:
161
+ print(f"Found soil DB at {self._bofek_soil_cache}")
162
+
163
+ self.connection = None
164
+
165
+ def __enter__(self):
166
+
167
+ sql1 = "install spatial; load spatial; install httpfs; load httpfs;"
168
+ sql2 = f"attach '{self._bofek_soil_cache}' as soildb (READONLY)"
169
+ self.connection = duckdb.connect()
170
+ self.connection.sql(sql1)
171
+ self.connection.sql(sql2)
172
+ return self.connection
173
+
174
+ def __exit__(self, exc_type, exc_value, exc_traceback):
175
+
176
+ self.connection.close()
177
+
178
+ def _has_connection(self):
179
+ """Checks the connection by trying to download the database SHA1 checksum from github.
180
+ """
181
+ try:
182
+ self._upstream_shasum = self._get_upstream_sha1()
183
+ return True
184
+ except urllib.error.URLError:
185
+ return False
186
+
187
+ def _compute_cache_sha1(self):
188
+ """Computes a SHA1 hash on the local cached DB file to check if the upstream DB has changed.
189
+ """
190
+ fname_hash = self._bofek_soil_cache.with_suffix(".sha1")
191
+ if fname_hash.exists():
192
+ return open(fname_hash, "r").read()
193
+
194
+ m = hashlib.sha1()
195
+ blocksize = 2 ** 20
196
+ with open(self._bofek_soil_cache, "rb") as fp:
197
+ while True:
198
+ buf = fp.read(blocksize)
199
+ if not buf:
200
+ break
201
+ m.update(buf)
202
+
203
+ with open(fname_hash, "w") as fp:
204
+ fp.write(m.hexdigest())
205
+
206
+ return m.hexdigest()
207
+
208
+ def _get_upstream_sha1(self):
209
+ """Downloads the SHA1 has from the database file on github.
210
+ """
211
+ request_url = urllib.request.urlopen(self._bofek_soil_source_shasum)
212
+ r = request_url.read()
213
+ hash_value = r.decode().split()[0]
214
+ return hash_value
215
+
216
+
217
+ class SoilDataProviderNL_CWB(dict):
218
+ """A SoilDataProvider that retrieves soil parameters from the Dutch BOFEK soil database for use
219
+ with the PCSE classic waterbalance.
220
+
221
+ :param xcoord: the X coordinate. Either in Dutch RD coordinates or as longitude
222
+ :param ycoord: the Y coordinates. Either in Dutch RD coordinates or as latitude
223
+ :param max_root_depth: user defined rootable depth in cm, otherwise the whole soil is assumed rootable.
224
+
225
+ Since the classic water balance uses a single soil layer, the soil parameters for each layer
226
+ have to be aggregated. The following assumptions have been made:
227
+ - SMW is calculated from a layer-weighted average of the wilting point, the latter is assumed at pF=4.2
228
+ - SMFCF is computed by calculating the water holding capacity for all layers. The latter is defined as the amount
229
+ of volume between wilting point (SMW) and field capacity (pF=2) for each layer. The SMFCF is than calculated
230
+ as the value with an equivalent water holding capacity given the soil rootable depth (RDMSOL).
231
+ - SM0 is computed as the value required to store an equivalent volume of water given the rootable depth.
232
+ - SOPE/KSUB are computed as the conductivity of the bottom layer at pF=1. There is no physical basis for this
233
+ but since the water balance starts draining water when the water content is above field capacity we just
234
+ take pF=1 as representative for that proces.
235
+ - RDMSOL is derived from the maximum soil depth or from the user-defined max_root_depth. The smallest of the
236
+ two values is taken.
237
+ """
238
+
239
+ param_units = {"SMW": "[-]",
240
+ "SMFCF": "[-]",
241
+ "SM0": "[-]",
242
+ "CRAIRC": "[-]",
243
+ "RDMSOL": "[cm]",
244
+ "SOPE": "[cm day-1]",
245
+ "KSUB": "[cm day-1]",
246
+ }
247
+
248
+ def __init__(self, *, xcoord=None, ycoord=None, max_root_depth=1E6):
249
+ super().__init__()
250
+
251
+ if max_root_depth < 0:
252
+ raise RuntimeError("max_root_depth must be positive")
253
+ if max_root_depth < 20:
254
+ raise RuntimeError("max_root_depth must at least be than 20 [cm]")
255
+
256
+ self.crds = CoordinateStore(xcoord, ycoord)
257
+ with SoilBDconnector() as DBconn:
258
+ self.soil_profile = find_soil_profile(DBconn, self.crds)
259
+ profile_characteristics = find_profile_characteristics(DBconn, self.soil_profile)
260
+
261
+ self.profile_characteristics = self._add_water_content_at_referencepoints(profile_characteristics)
262
+ self.rootable_depth, self._root_depth_limit_forced = self._determine_rootable_depth(max_root_depth)
263
+ self._compute_CWB_volumetric_parameters()
264
+ self._compute_CWB_conductivity_parameters()
265
+
266
+ def _compute_CWB_volumetric_parameters(self):
267
+ """Computes the soil volumetric parameters for the WOFOST classic waterbalance as layer-weighted values
268
+
269
+ :return: a dict with relevant parameters
270
+ """
271
+ df = self.profile_characteristics
272
+
273
+ # Determine which layers are rooted
274
+ df["is_rooted"] = df.layer_top < self.rootable_depth
275
+ if not df.is_rooted.all():
276
+ df = df[df.is_rooted]
277
+ df.loc[df.index[-1], 'layer_bottom'] = self.rootable_depth
278
+
279
+ # Recompute layer thickness as bottom layer may be bounded by rootable depth.
280
+ df["thickness"] = df.layer_bottom - df.layer_top
281
+
282
+ # wilting point as layer-weighted value
283
+ SMW = (df.SMW * df.thickness).sum() / df.thickness.sum()
284
+ # compute total water holding capacity (AWC) for all layers
285
+ AWC = ((df.SMFCF - df.SMW) * df.thickness).sum()
286
+ # Recomputed field capacity as required to store AWC above wilting point SMW
287
+ SMFCF = SMW + AWC/self.rootable_depth
288
+ # pore space up till porosity (AWC0)
289
+ AWC0 = ((df.SM0 - df.SMW) * df.thickness).sum()
290
+ # Recompute soil porosity (SM0) as required to store AWC0 above field capacity
291
+ SM0 = SMW + AWC0/self.rootable_depth
292
+ # Critical air content as halfway between SMFCF and SM0
293
+ CRAIRC = (SM0 - SMFCF) * 0.5
294
+
295
+ self.update(dict(SMW=SMW, SMFCF=SMFCF, SM0=SM0, CRAIRC=CRAIRC, RDMSOL=self.rootable_depth))
296
+
297
+ def _compute_CWB_conductivity_parameters(self):
298
+ """Computes the soil conductivity parameters for the WOFOST classic waterbalance
299
+
300
+ There are three parameters in WOFOST CWB that have to be estimated:
301
+ - SOPE : maximum percolation rate root zone[cm day-1]
302
+ - KSUB : maximum percolation rate subsoil [cm day-1]
303
+
304
+ It is unclear how this parameters have to be estimated physically. In practice, they
305
+ were probably used as a calibration parameter in order to limit excessive drainage.
306
+ In the classic water balance drainage occurs only when soil moisture is above
307
+ field capacity (pF=2.0), therefore we estimate both parameters as the conducitivity
308
+ at pF=1.0. Moreover, since drainage happens mostly in the lower soil layers, we
309
+ use the MvG parameters of the bottom layer to estimate the conductivity.
310
+
311
+ :return: a dict with relevant parameters
312
+ """
313
+ bottom_layer = self.profile_characteristics.iloc[-1]
314
+ p_mvg = MualemvanGenuchten(wcr=bottom_layer.ores, wcs=bottom_layer.osat, alpha=bottom_layer.alfa,
315
+ npar=bottom_layer.npar, lamda=bottom_layer.lexp, ksat=bottom_layer.ksatfit)
316
+ pF = 1.0
317
+ H = 10**pF
318
+ cond = get_conductivity_from_MvG(H, p_mvg)
319
+ self.update(dict(SOPE=cond, KSUB=cond))
320
+
321
+ def _add_water_content_at_referencepoints(self, profile_characteristics):
322
+ """Computes the water content at saturation, field capacity and wilting point based on the profile
323
+ characteristics and the Mualem van Genugten parameters.
324
+
325
+ :param profile_characteristics: a dataframe with soil profile characteristics
326
+ :return: The updated dataframe with water content at reference points added.
327
+ """
328
+
329
+ ref_point_names = ["SM0", "SMFCF", "SMW"]
330
+ ref_point_pF = np.array([-1, 2, 4.2])
331
+ H = 10**ref_point_pF
332
+ layer_ref_points = []
333
+ for layer in profile_characteristics.itertuples():
334
+ p_mvg = MualemvanGenuchten(wcr=layer.ores, wcs=layer.osat, alpha=layer.alfa, npar=layer.npar,
335
+ lamda=layer.lexp, ksat=layer.ksatfit)
336
+ water_content = get_water_content_from_MvG(H, p_mvg)
337
+ layer_ref_points.append(dict(zip(ref_point_names, water_content)))
338
+ df_water_content = pd.DataFrame.from_dict(layer_ref_points)
339
+ profile_characteristics = pd.concat([profile_characteristics, df_water_content], axis=1)
340
+
341
+ return profile_characteristics
342
+
343
+ def _determine_rootable_depth(self, max_rootable_depth):
344
+ """Determines the rootable depth as the minimum of the sum of the layer thickness and the user defined
345
+ max_rootable_depth parameter.
346
+
347
+ :param max_rootable_depth: the user-defined maximum rootable depth.
348
+ :return: the rootable depth
349
+ """
350
+ soil_rootable_depth = self.profile_characteristics.thickness.sum()
351
+ rootable_depth = min(max_rootable_depth, soil_rootable_depth)
352
+ if rootable_depth < soil_rootable_depth:
353
+ return rootable_depth, True
354
+ else:
355
+ return rootable_depth, False
356
+
357
+ def __str__(self):
358
+ msg = (f"Soil properties for location at X/Y: {self.crds.xcoord:.0f}/{self.crds.ycoord:.0f} - "
359
+ f"lon/lat: {self.crds.lon:.3f}/{self.crds.lat:.3f}\n")
360
+ if self._root_depth_limit_forced:
361
+ msg += f"Soil rootable depth estimated at {self.rootable_depth} cm (forced by `max_root_depth` parameter)\n"
362
+ else:
363
+ msg += f"Soil rootable depth estimated at {self.rootable_depth} cm (from maximum soil profile depth)\n"
364
+ msg += "Soil profile characteristics:\n"
365
+ s = self.profile_characteristics.to_string(index=False, columns=["thickness", "pclay", "psilt", "psand", "SMW",
366
+ "SMFCF", "SM0"],
367
+ max_rows=5)
368
+ msg += (s + "\n")
369
+ msg += "Actual CWB parameter values:\n"
370
+ for name, value in self.items():
371
+ unit = self.param_units[name]
372
+ msg += f"- {name}: {value:.3f} {unit}\n"
373
+ return msg
374
+
375
+
376
+ class SoilDataProviderNL_MLWB(dict):
377
+ """A SoilDataProvider that retrieves soil parameters from the Dutch BOFEK soil database for use
378
+ with the PCSE multi-layered waterbalance.
379
+ """
380
+ non_soil_codes = {99980, 99990, 99991}
381
+
382
+ def __init__(self, *, xcoord=None, ycoord=None, max_root_depth=1E6, cache_soildb=False):
383
+ super().__init__()
384
+
385
+ raise NotImplementedError("Soil dataprovider for the multi-layer waterbalance not yet implemented.")
386
+
387
+
388
+ class SoilDataProviderNL_MLWB_SNOMIN(dict):
389
+ """A SoilDataProvider that retrieves soil parameters from the Dutch BOFEK soil database for use
390
+ with the PCSE multi-layered waterbalance with SNOMIN C/N soil model.
391
+ """
392
+ non_soil_codes = {99980, 99990, 99991}
393
+
394
+ def __init__(self, *, xcoord=None, ycoord=None, max_root_depth=1E6, cache_soildb=False):
395
+ super().__init__()
396
+
397
+ raise NotImplementedError("Soil dataprovider for the multi-layer waterbalance and SNOMIN not yet implemented.")
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: soil_dataprovider_nl
3
+ Version: 1.0.0
4
+ Summary: This package provides a set of data providers that can derive the soil parameters for running crop/soil models in PCSE based on the Dutch soil map.
5
+ Author-email: Allard de Wit <allard.dewit@wur.nl>, Herman Berghuijs <herman.berghuijs@wur.nl>
6
+ Maintainer-email: Allard de Wit <allard.dewit@wur.nl>, Herman Berghuijs <herman.berghuijs@wur.nl>
7
+ License-Expression: EUPL-1.2
8
+ Project-URL: Homepage, https://github.com/ajwdewit/soil_dataprovider_nl
9
+ Project-URL: documentation, https://github.com/ajwdewit/soil_dataprovider_nl
10
+ Project-URL: repository, https://github.com/ajwdewit/soil_dataprovider_nl
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pandas
15
+ Requires-Dist: pyproj==3.8
16
+ Requires-Dist: duckdb==1.5
17
+ Provides-Extra: notebook
18
+ Requires-Dist: matplotlib; extra == "notebook"
19
+ Requires-Dist: ipyleaflet; extra == "notebook"
20
+ Requires-Dist: pcse>=6.0.13; extra == "notebook"
21
+ Dynamic: license-file
22
+
23
+ # Soil data providers for Dutch soils for PCSE
24
+
25
+ ---
26
+ Allard de Wit
27
+ Wageningen Environmental Research
28
+ September 2026
29
+ ---
30
+
31
+ This package provides a set of data providers that can provide the soil parameters for the Netherlands as input
32
+ for running crop/soil models in PCSE. It takes the input from the [Dutch soil map](https://app.pdok.nl/viewer/#x=160000.00&y=455000.00&z=3.0000&background=BRT-A%20standaard&layers=d9cc67ba-5491-4640-86ac-b8d392250270;soilarea;_;1)
33
+ but the soil data itself is hosted in a [duckdb file on github](https://github.com/ajwdewit/collections/raw/refs/heads/main/BodemkaartNL/bofek_soil_nl.ddb).
34
+
35
+ Getting soil information has always been a bit of a hassle for models in [PCSE](https://pcse.readthedocs.io).
36
+ While crop parameters are well managed through [YAML files on github](https://github.com/ajwdewit/WOFOST_crop_parameters),
37
+ soil parameters are more difficult as they are spatially variable and thus location dependent. Moreover, soil
38
+ maps and definitions are variable between countries making it hard to develop dataproviders for soils.
39
+
40
+ This soil data provider derives soil parameters from the Dutch soil map and derives soil hydraulic properties as well
41
+ as soil mineralogic properties (bulk density, organic matter content) depending on the type of PCSE soil component used.
42
+ Several data providers are available from this package:
43
+ - `SoilDataProviderNL_CWB`
44
+ - `SoilDataProviderNL_MLWB` *under development*
45
+ - `SoilDataProviderNL_MLWB_SNOMIN` *under development*
46
+
47
+ These dataproviders map to the different soil modules in pcse: the classic soil water balance (CWB), the multi-layered
48
+ soil water balance (MLWB) and the multi-layered water balance plus the SNOMIN carbon/nitrogen model (MLWB_SNOMIN).
49
+ The correct soil dataprovider will be selected automatically based on the pcse model that you pass in to the call
50
+ to `soil_dataprovider_nl.SoilDataProviderNL`, however they can also be imported manually from the package.
51
+
52
+ # Setting up the package
53
+
54
+ ## Dependencies
55
+
56
+ This package has been developed using python 3.10 and has dependencies on several other packages:
57
+ - pandas
58
+ - numpy
59
+ - duckdb == 1.5
60
+ - matplotlib
61
+ - pyproj == 3.7
62
+
63
+
64
+ ## Installing
65
+
66
+ The package can be pip-installed from PyPI: `pip install soil_dataprovider_nl` should be sufficient.
67
+
68
+ Dependencies should be automatically downloaded and installed during installing the package, although the `pyproj` dependency
69
+ can be problematic sometimes. In that case, install `pyproj` manually in a conda environment with the conda package
70
+ manager.
71
+
72
+ ## Usage
73
+
74
+ Using the soil dataprovider is easy:
75
+ ```python
76
+ >> from soil_dataprovider_nl import SoilDataProviderNL
77
+ >> from pcse.models import Wofost72_WLP_CWB
78
+ >> soild = SoilDataProviderNL(Wofost72_WLP_CWB, xcoord=170342, ycoord=438503)
79
+ >> print(soild)
80
+ Soil properties for location at X/Y: 170342/438503 - lon/lat: 5.611/51.936
81
+ Soil rootable depth estimated at 120 cm (from maximum soil profile depth)
82
+ Soil profile characteristics:
83
+ thickness pclay psilt psand SMW SMFCF SM0
84
+ 25 0.23 0.40 0.37 0.122168 0.387381 0.429530
85
+ 35 0.23 0.40 0.37 0.142978 0.413028 0.472326
86
+ 30 0.20 0.25 0.55 0.142978 0.413028 0.472326
87
+ 30 0.04 0.07 0.89 0.040080 0.269736 0.387057
88
+ Actual CWB parameter values:
89
+ - SMW: 0.113 [-]
90
+ - SMFCF: 0.372 [-]
91
+ - SM0: 0.442 [-]
92
+ - CRAIRC: 0.035 [-]
93
+ - RDMSOL: 120.000 [cm]
94
+ - SOPE: 8.436 [cm day-1]
95
+ - KSUB: 8.436 [cm day-1]
96
+ ```
97
+
98
+ The derived soildata can then be used in the usual procedure for running a model (in pseudo code):
99
+ ```python
100
+ >> params = ParameterProvider(cropdata=cropd, soildata=soild, sitedata=sited)
101
+ >> agro = get_agromanagement(...)
102
+ >> wdp = get_weatherdata(...)
103
+ >> wofost = Wofost72_WLP_CWB(params, wdp, agro)
104
+ >> wofost.run_till_terminate()
105
+ ...
106
+ ```
107
+
108
+ See for more elaborate examples the [notebook on the repository](https://github.com/ajwdewit/soil_dataprovider_nl).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ soil_dataprovider_nl/__init__.py
5
+ soil_dataprovider_nl/coords.py
6
+ soil_dataprovider_nl/mualemvangenuchten.py
7
+ soil_dataprovider_nl/soil_dataprovider_nl.py
8
+ soil_dataprovider_nl.egg-info/PKG-INFO
9
+ soil_dataprovider_nl.egg-info/SOURCES.txt
10
+ soil_dataprovider_nl.egg-info/dependency_links.txt
11
+ soil_dataprovider_nl.egg-info/requires.txt
12
+ soil_dataprovider_nl.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ pandas
2
+ pyproj==3.8
3
+ duckdb==1.5
4
+
5
+ [notebook]
6
+ matplotlib
7
+ ipyleaflet
8
+ pcse>=6.0.13
@@ -0,0 +1 @@
1
+ soil_dataprovider_nl