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.
- soil_dataprovider_nl-1.0.0/LICENSE +18 -0
- soil_dataprovider_nl-1.0.0/PKG-INFO +108 -0
- soil_dataprovider_nl-1.0.0/README.md +86 -0
- soil_dataprovider_nl-1.0.0/pyproject.toml +42 -0
- soil_dataprovider_nl-1.0.0/setup.cfg +4 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl/__init__.py +32 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl/coords.py +64 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl/mualemvangenuchten.py +98 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl/soil_dataprovider_nl.py +397 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl.egg-info/PKG-INFO +108 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl.egg-info/SOURCES.txt +12 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl.egg-info/dependency_links.txt +1 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl.egg-info/requires.txt +8 -0
- soil_dataprovider_nl-1.0.0/soil_dataprovider_nl.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
soil_dataprovider_nl
|