Skip to content

Commit 626af7d

Browse files
Issue #949 allocate river layers (#952)
Fixes #949 # Description - Adds functionality to allocate river cells. - These can also be used to allocate DRN, GHB, and RCH cells - I had to move the ``polygonize`` util function to work around a nasty circular import. The solution also prevents changes to the public API. - A decorator was added to enforce an iMOD Python compatible dimension order (Related to: #239 ) - A decorator was added to preserve grid type (xu.UgridDataArray or xr.DataArray). # Checklist - [x] Links to correct issue - [x] Update changelog, if changes affect users - [x] PR title starts with ``Issue #nr``, e.g. ``Issue #737`` - [x] Unit tests were added - [ ] **If feature added**: Added/extended example
1 parent 7e94850 commit 626af7d

File tree

17 files changed

+994
-141
lines changed

17 files changed

+994
-141
lines changed

docs/api/changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ The format is based on `Keep a Changelog`_, and this project adheres to
99
[Unreleased]
1010
------------
1111

12+
Added
13+
~~~~~
14+
- Added functions to allocate planar grids over layers for the topsystem in
15+
:func:`imod.prepare.allocate_drn_cells`,
16+
:func:`imod.prepare.allocate_ghb_cells`,
17+
:func:`imod.prepare.allocate_rch_cells`,
18+
:func:`imod.prepare.allocate_riv_cells`, for this multiple options can be
19+
selected, available in :func:`imod.prepare.ALLOCATION_OPTION`.
20+
1221
Fixed
1322
~~~~~
1423
- No ``ValidationError`` thrown anymore in :class:`imod.mf6.River` when

docs/api/prepare.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ Prepare model input
3131
get_lower_active_layer_number
3232
get_upper_active_grid_cells
3333
get_upper_active_layer_number
34+
create_layered_top
35+
36+
ALLOCATION_OPTION
37+
allocate_drn_cells
38+
allocate_ghb_cells
39+
allocate_rch_cells
40+
allocate_riv_cells
41+
c_leakage
42+
c_radial

imod/mf6/utilities/grid.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import xugrid as xu
88

99
import imod
10+
from imod.prepare.layer import create_layered_top
1011
from imod.typing import GridDataArray
1112
from imod.typing.grid import zeros_like
1213
from imod.util.spatial import spatial_reference
@@ -48,17 +49,6 @@ def broadcast_to_full_domain(
4849
return top, bottom
4950

5051

51-
def create_layered_top(bottom: GridDataArray, top: GridDataArray) -> GridDataArray:
52-
"""
53-
Create a top array with layers from a single top array and a full bottom array
54-
"""
55-
new_top = zeros_like(bottom)
56-
new_top[0] = top
57-
new_top[1:] = bottom[0:-1].values
58-
59-
return new_top
60-
61-
6252
def to_cell_idx(idomain: xr.DataArray) -> xr.DataArray:
6353
"""
6454
Assigns an unique index to each cell in the domain

imod/mf6/wel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
from imod.mf6.mf6_wel_adapter import Mf6Wel
2222
from imod.mf6.package import Package
2323
from imod.mf6.utilities.dataset import remove_inactive
24-
from imod.mf6.utilities.grid import create_layered_top
2524
from imod.mf6.utilities.regrid import RegridderType
2625
from imod.mf6.validation import validation_pkg_error_message
2726
from imod.mf6.write_context import WriteContext
2827
from imod.prepare import assign_wells
28+
from imod.prepare.layer import create_layered_top
2929
from imod.schemata import (
3030
AnyNoDataSchema,
3131
DTypeSchema,

imod/prepare/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from imod.prepare import spatial, subsoil, surface_water
1717
from imod.prepare.layer import (
18+
create_layered_top,
1819
get_lower_active_grid_cells,
1920
get_lower_active_layer_number,
2021
get_upper_active_grid_cells,
@@ -34,5 +35,14 @@
3435
zonal_aggregate_polygons,
3536
zonal_aggregate_raster,
3637
)
38+
from imod.prepare.topsystem import (
39+
ALLOCATION_OPTION,
40+
allocate_drn_cells,
41+
allocate_ghb_cells,
42+
allocate_rch_cells,
43+
allocate_riv_cells,
44+
c_leakage,
45+
c_radial,
46+
)
3747
from imod.prepare.voxelize import Voxelizer
3848
from imod.prepare.wells import assign_wells

imod/prepare/layer.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"""
44

55
from imod.typing import GridDataArray
6+
from imod.typing.grid import preserve_gridtype, zeros_like
67

78

9+
@preserve_gridtype
810
def get_upper_active_layer_number(active: GridDataArray) -> GridDataArray:
911
"""
1012
Returns planar grid of integers with the layer number of the upper most
@@ -22,6 +24,7 @@ def get_upper_active_layer_number(active: GridDataArray) -> GridDataArray:
2224
return layer.where(active, nodata).min("layer")
2325

2426

27+
@preserve_gridtype
2528
def get_upper_active_grid_cells(active: GridDataArray) -> GridDataArray:
2629
"""
2730
Returns grid of booleans designating location of the uppermost active grid
@@ -37,6 +40,7 @@ def get_upper_active_grid_cells(active: GridDataArray) -> GridDataArray:
3740
return layer == upper_active_layer
3841

3942

43+
@preserve_gridtype
4044
def get_lower_active_layer_number(active: GridDataArray) -> GridDataArray:
4145
"""
4246
Returns two-dimensional grid of integers with the layer number of the lower
@@ -54,6 +58,7 @@ def get_lower_active_layer_number(active: GridDataArray) -> GridDataArray:
5458
return layer.where(active, nodata).max("layer")
5559

5660

61+
@preserve_gridtype
5762
def get_lower_active_grid_cells(active: GridDataArray) -> GridDataArray:
5863
"""
5964
Returns grid of booleans designating location of the lowermost active grid
@@ -67,3 +72,28 @@ def get_lower_active_grid_cells(active: GridDataArray) -> GridDataArray:
6772
layer = active.coords["layer"]
6873
lower_active_layer = get_lower_active_layer_number(active)
6974
return layer == lower_active_layer
75+
76+
77+
def create_layered_top(bottom: GridDataArray, top: GridDataArray) -> GridDataArray:
78+
"""
79+
Create a top array with a layer dimension, from a top array with no layer
80+
dimension and a bottom array with a layer dimension. The (output) top of
81+
layer n is assigned the bottom of layer n-1.
82+
83+
Parameters
84+
----------
85+
bottom: {DataArray, UgridDataArray}
86+
Bottoms with layer dimension
87+
top: {DataArray, UgridDataArray}
88+
Top, without layer dimension
89+
90+
Returns
91+
-------
92+
new_top: {DataArray, UgridDataArray}
93+
Top with layer dimension.
94+
"""
95+
new_top = zeros_like(bottom)
96+
new_top[0] = top
97+
new_top[1:] = bottom[0:-1].values
98+
99+
return new_top

imod/prepare/spatial.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pathlib
2-
from typing import Callable, Union
2+
from typing import TYPE_CHECKING, Callable, Union
33

44
import dask
55
import numba
@@ -11,6 +11,7 @@
1111
import imod
1212
from imod.prepare import common, pcg
1313
from imod.util.imports import MissingOptionalModule
14+
from imod.util.spatial import _polygonize
1415

1516
# since rasterio is a big dependency that is sometimes hard to install
1617
# and not always required, we made this an optional dependency
@@ -21,6 +22,9 @@
2122
except ImportError:
2223
rasterio = MissingOptionalModule("rasterio")
2324

25+
if TYPE_CHECKING:
26+
import geopandas as gpd
27+
2428

2529
def round_extent(extent, cellsize):
2630
"""Increases the extent until all sides lie on a coordinate
@@ -252,7 +256,7 @@ def rasterize(geodataframe, like, column=None, fill=np.nan, **kwargs):
252256
return xr.DataArray(raster, like.coords, like.dims)
253257

254258

255-
def polygonize(da):
259+
def polygonize(da: xr.DataArray) -> "gpd.GeoDataFrame":
256260
"""
257261
Polygonize a 2D-DataArray into a GeoDataFrame of polygons.
258262
@@ -264,28 +268,7 @@ def polygonize(da):
264268
-------
265269
polygonized : geopandas.GeoDataFrame
266270
"""
267-
import geopandas as gpd
268-
import shapely.geometry as sg
269-
270-
if da.dims != ("y", "x"):
271-
raise ValueError('Dimensions must be ("y", "x")')
272-
273-
values = da.values
274-
if values.dtype == np.float64:
275-
values = values.astype(np.float32)
276-
277-
transform = imod.util.spatial.transform(da)
278-
shapes = rasterio.features.shapes(values, transform=transform)
279-
280-
geometries = []
281-
colvalues = []
282-
for geom, colval in shapes:
283-
geometries.append(sg.Polygon(geom["coordinates"][0]))
284-
colvalues.append(colval)
285-
286-
gdf = gpd.GeoDataFrame({"value": colvalues, "geometry": geometries})
287-
gdf.crs = da.attrs.get("crs")
288-
return gdf
271+
return _polygonize(da)
289272

290273

291274
def _handle_dtype(dtype, nodata):

imod/prepare/surface_water.py

Lines changed: 14 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,21 @@
1-
import numpy as np
1+
# Import for backwards compatibility
2+
import warnings
23

4+
from imod.prepare.topsystem import c_leakage as _c_leakage
5+
from imod.prepare.topsystem import c_radial as _c_radial
36

4-
def c_radial(L, kh, kv, B, D):
5-
"""
6-
Ernst's radial resistance term to a drain.
7+
WARNING_MESSAGE = (
8+
"function has been moved from imod.prepare.surface_water to"
9+
"imod.prepare.topsystem, please update your scripts."
10+
"imod.prepare.surface_water is going to be removed in version 1.0"
11+
)
712

8-
Parameters
9-
----------
10-
L : xr.DataArray of floats
11-
distance between water features
12-
kh : xr.DataArray of floats
13-
horizontal conductivity
14-
kv : xr.DataArray of floats
15-
vertical conductivity
16-
B : xr.DataArray of floats
17-
water feature wetted perimeter
18-
D : xr.DataArray of floats
19-
saturated thickness of the top system
2013

21-
Returns
22-
-------
23-
radial_c : xr.DataArray
24-
Ernst's radial resistance for a drain
25-
"""
26-
# Take anisotropy into account fully
27-
c = (
28-
L
29-
/ (np.pi * np.sqrt(kh * kv))
30-
* np.log((4.0 * D) / (np.pi * B) * np.sqrt(kh / kv))
31-
)
32-
c = c.where(~(c < 0.0), other=0.0)
33-
return c
14+
def c_radial(L, kh, kv, B, D):
15+
warnings.warn(WARNING_MESSAGE, DeprecationWarning)
16+
return _c_radial(L, kh, kv, B, D)
3417

3518

3619
def c_leakage(kh, kv, D, c0, c1, B, length, dx, dy):
37-
"""
38-
Computes the phreatic leakage resistance.
39-
40-
Parameters
41-
----------
42-
kh : xr.DataArray of floats
43-
horizontal conductivity of phreatic aquifer
44-
kv : xr.DataArray of floats
45-
vertical conductivity of phreatic aquifer
46-
c0 : xr.DataArray of floats
47-
hydraulic bed resistance of water feature
48-
c1 : xr.DataArray of floats
49-
hydraulic resistance of the first aquitard
50-
D : xr.DataArray of floats
51-
saturated thickness of the top system
52-
B : xr.DataArray of floats
53-
water feature wetted perimeter
54-
length : xr.DataArray of floats
55-
water feature length per cell
56-
dx : xr.DataArray of floats
57-
cellsize in x
58-
dy : xr.DataArray of floats
59-
cellsize in y
60-
61-
Returns
62-
-------
63-
c_leakage: xr.DataArray of floats
64-
Hydraulic resistance of water features corrected for intra-cell
65-
hydraulic resistance and surface water interaction.
66-
"""
67-
68-
def f(x):
69-
"""
70-
two x times cotangens hyperbolicus of x
71-
"""
72-
# Avoid overflow for large x values
73-
# 20 is safe for 32 bit floats
74-
x = x.where(~(x > 20.0), other=20.0)
75-
exp2x = np.exp(2.0 * x)
76-
return x * (exp2x + 1) / (exp2x - 1.0)
77-
78-
# TODO: raise ValueError when cells are far from square?
79-
# Since method of average ditch distance will not work well
80-
# Compute geometry of ditches within cells
81-
cell_area = abs(dy * dx)
82-
wetted_area = length * B
83-
fraction_wetted = wetted_area / cell_area
84-
# Compute average distance between ditches
85-
L = (cell_area - wetted_area) / length
86-
87-
# Compute total resistance to aquifer
88-
c1_prime = c1 + (D / kv)
89-
# Compute influence for the part below the ditch
90-
labda_B = np.sqrt((kh * D * c1_prime * c0) / (c1_prime + c0))
91-
# ... and the field
92-
labda_L = np.sqrt(c1_prime * kh * D)
93-
94-
x_B = B / (2.0 * labda_B)
95-
x_L = L / (2.0 * labda_L)
96-
97-
# Compute feeding resistance
98-
c_rad = c_radial(L, kv, kh, B, D)
99-
c_L = (c0 + c1_prime) * f(x_L) + (c0 * L / B) * f(x_B)
100-
c_B = (c1_prime + c0) / (c_L - c0 * L / B) * c_L
101-
# total feeding resistance equals the harmonic mean of resistances below
102-
# ditch (B) and field (L) plus the radical resistance
103-
# Can also be regarded as area weighted sum of conductances of B and L
104-
c_total = 1.0 / (fraction_wetted / c_B + (1.0 - fraction_wetted) / c_L) + c_rad
105-
# Subtract aquifer and aquitard resistance from feeding resistance
106-
c = c_total - c1_prime
107-
# Replace areas where cells are fully covered by water
108-
c = c.where(~(L == 0.0), other=c0)
109-
return c
20+
warnings.warn(WARNING_MESSAGE, DeprecationWarning)
21+
return _c_leakage(kh, kv, D, c0, c1, B, length, dx, dy)

imod/prepare/topsystem/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from imod.prepare.topsystem.allocation import (
2+
ALLOCATION_OPTION,
3+
allocate_drn_cells,
4+
allocate_ghb_cells,
5+
allocate_rch_cells,
6+
allocate_riv_cells,
7+
)
8+
from imod.prepare.topsystem.resistance import c_leakage, c_radial

0 commit comments

Comments
 (0)