import numpy as np
import pandas as pd
import xarray as xr
import primod
import imod
from imod import mf6, msw
Example
This example illustrates how to setup a simple MetaSWAP model coupled to a Modflow 6 model model using the imod
package and associated packages.
Overview of steps made:
- Create Modflow 6 model
- Create MetaSWAP model
- Write coupled models
We’ll start with the following imports:
Modflow 6 model
Next, we initiate the Modflow 6 groundwater model:
= mf6.GroundwaterFlowModel() gwf_model
Create grid
We’ll then define the Modflow 6 grid. It consists of 3 layers of 9 by 9 cells rasters.
= nlay, nrow, ncol = 3, 9, 9
shape
= 10.0
dx = -10.0
dy = np.array([1.0, 2.0, 10.0])
dz = 0.0
xmin = dx * ncol
xmax = 0.0
ymin = abs(dy) * nrow
ymax = ("layer", "y", "x")
dims
= np.arange(1, nlay + 1)
layer = np.arange(ymax, ymin, dy) + 0.5 * dy
y = np.arange(xmin, xmax, dx) + 0.5 * dx
x = {"layer": layer, "y": y, "x": x}
coords
= xr.DataArray(np.ones(shape, dtype=int), coords=coords, dims=dims)
idomain
= 0.0
top = top - xr.DataArray(
bottom * dz), coords={"layer": layer}, dims="layer"
np.cumsum(layer )
"dis"] = mf6.StructuredDiscretization(idomain=idomain, top=top, bottom=bottom) gwf_model[
Hydrogeology
Hydraulic conductivity
Assign the node property flow package, which specifies the hydraulic conductivities. The middle layer is an aquitard.
= xr.DataArray([10.0, 0.1, 10.0], {"layer": layer}, ("layer",))
k = xr.DataArray([1.0, 0.01, 1.0], {"layer": layer}, ("layer",))
k33 "npf"] = mf6.NodePropertyFlow(
gwf_model[=0,
icelltype=k,
k=k33,
k33=True,
save_flows )
Storage
Cells are set to non-convertible (convertible = 0). This is a requirement for MetaSWAP, because, once coupled, MetaSWAP is responsible for computing the storage coefficient instead of Modflow.
"sto"] = mf6.SpecificStorage(
gwf_model[=1e-3, specific_yield=0.0, transient=True, convertible=0
specific_storage )
Initial conditions
"ic"] = mf6.InitialConditions(start=0.5) gwf_model[
Output Control
"oc"] = mf6.OutputControl(save_head="last", save_budget="last") gwf_model[
Boundary conditions
Constant head
We’ll create constant head cells at the most left and right columns of the grid, representing two ditches.
= xr.full_like(idomain, np.nan, dtype=float)
head 0, :, 0] = -1.0
head[0, :, -1] = -1.0
head[
"chd"] = mf6.ConstantHead(
gwf_model[=True, print_flows=True, save_flows=True
head, print_input
)
=0).plot() head.isel(layer
Dummy boundary conditions
The iMOD Coupler requires a dummy recharge package, and well package if MetaSWAP’s sprinkling is enabled. This to let Modflow 6 allocate the appropriate matrices needed in the exchange of states during model computation.
Recharge
We’ll start off with the recharge package, which has no recharge cells at the location of our ditches.
= xr.zeros_like(idomain.sel(layer=1), dtype=float)
recharge 0] = np.nan
recharge[:, -1] = np.nan
recharge[:,
"rch_msw"] = mf6.Recharge(recharge)
gwf_model[
recharge.plot()
Wells
We’ll create a dummy well package as well. imod.mf6.WellDisStructured needs its input data provided as long tables instead of grids to so therefore we’ll create 1d arrays by calling np.tile
on the column indices, and np.repeat
on the row indices.
= 3
wel_layer
= np.tile(np.arange(ncol) + 1, nrow)
ix = np.repeat(np.arange(nrow) + 1, ncol)
iy = np.zeros(ix.shape)
rate = np.full_like(ix, wel_layer)
layer
"wells_msw"] = mf6.WellDisStructured(
gwf_model[=layer, row=iy, column=ix, rate=rate
layer )
Initiate a Modflow 6 simulation and attach the groundwater model to it.
= mf6.Modflow6Simulation("test")
simulation "GWF_1"] = gwf_model
simulation[
# Define solver settings, we'll use a preset that is sufficient for this example.
"solver"] = mf6.SolutionPresetSimple(modelnames=["GWF_1"]) simulation[
Create time discretization, we’ll model 2 days.
= "D"
freq = pd.date_range(start="1/1/1971", end="1/3/1971", freq=freq)
times
=times)
simulation.create_time_discretization(additional_times
times
DatetimeIndex(['1971-01-01', '1971-01-02', '1971-01-03'], dtype='datetime64[ns]', freq='D')
MetaSWAP model
The next step is initiating a MetaSwapModel
. Critical is setting the right path to MetaSWAP’s soil physical database, which contains the lookup table with the soil physical relationships. Without access to this database MetaSWAP cannot function. The full database can be downloaded here.
= msw.MetaSwapModel(unsaturated_database="./path/to/unsaturated/database")
msw_model
# Create grid
# ```````````
#
# We'll start off specifying the grids required for MetaSWAP. The x,y values
# of this grid should be identical as the Modflow6 model, but it should
# not have a layer dimension.
= idomain.sel(layer=1, drop=True).astype(float) msw_grid
We do not want MetaSWAP cells in the cells where the ditches are located in Modflow 6. We can specify where MetaSWAP cells are active with the “active” grid, which is a grid of booleans (i.e. True/False).
= msw_grid.astype(bool)
active 0] = False
active[..., -1] = False
active[...,
active
<xarray.DataArray (y: 9, x: 9)> Size: 81B array([[False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False], [False, True, True, True, True, True, True, True, False]]) Coordinates: * y (y) float64 72B 85.0 75.0 65.0 55.0 45.0 35.0 25.0 15.0 5.0 * x (x) float64 72B 5.0 15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0
Another crucial grid is the “area” grid. The area grid denotes the area in each cell, for each “subunit”. A subunit represent a separate landuse in the grid. We’ll create a grid with two separate land uses.
Each grid which specifies parameters related to landuse (e.g. landuse, rootzone_depth, ponding depth) requires a subunit dimension. In contrast, grids specifying parameters not induced by landuse (e.g. soil type, elevation, precipitation) cannot contain a subunit dimension.
= [0, 1]
subunit
= abs(dx * dy)
total_cell_area = total_cell_area / len(subunit)
equal_area_per_subunit
total_cell_area
# Create a full grid equal to the msw_grid. And expand_dims() to broadcast this
# grid along a new dimension, named "subunit"
= (
area =float)
xr.full_like(msw_grid, equal_area_per_subunit, dtype=subunit)
.expand_dims(subunit# expand_dims creates a view, so copy it to allow setting values.
.copy()
)
# To the left we only have subunit 0
0, :, :3] = total_cell_area
area[1, :, :3] = np.nan
area[
# To the right we only have subunit 1
0, :, -3:] = np.nan
area[1, :, -3:] = total_cell_area
area[
area
<xarray.DataArray (subunit: 2, y: 9, x: 9)> Size: 1kB array([[[100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan], [100., 100., 100., 50., 50., 50., nan, nan, nan]], [[ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.], [ nan, nan, nan, 50., 50., 50., 100., 100., 100.]]]) Coordinates: * subunit (subunit) int64 16B 0 1 * y (y) float64 72B 85.0 75.0 65.0 55.0 45.0 35.0 25.0 15.0 5.0 * x (x) float64 72B 5.0 15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0
Landuse
Define a grid with landuse classes.
= xr.full_like(area, 1, dtype=np.int16)
landuse 1, :, :] = 2
landuse[
landuse
<xarray.DataArray (subunit: 2, y: 9, x: 9)> Size: 324B array([[[1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1]], [[2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2], [2, 2, 2, 2, 2, 2, 2, 2, 2]]], dtype=int16) Coordinates: * subunit (subunit) int64 16B 0 1 * y (y) float64 72B 85.0 75.0 65.0 55.0 45.0 35.0 25.0 15.0 5.0 * x (x) float64 72B 5.0 15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0
Soil types
Define soil type classes. These will be looked up in MetaSWAP’s giant lookup table for the national Staring series describing Dutch soils. The full database can be downloaded here. <https://download.deltares.nl/metaswap>
In previous examples we set values in our DataArray using numpy indexing. But we can also use xarray’s where()
method to set values.
= xr.full_like(msw_grid, 1, dtype=np.int16)
slt # Set all cells on the right half to 2.
= slt.where((slt.x < (xmax / 2)), 2)
slt
slt
<xarray.DataArray (y: 9, x: 9)> Size: 162B array([[1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2], [1, 1, 1, 1, 2, 2, 2, 2, 2]], dtype=int16) Coordinates: * y (y) float64 72B 85.0 75.0 65.0 55.0 45.0 35.0 25.0 15.0 5.0 * x (x) float64 72B 5.0 15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0
Finishing the grid
To finish specifying the landuse grid data, we’ll require a rootzone depth for each subunit, and a grid with surface elevations
= xr.full_like(area, 0.5)
rootzone_depth = xr.full_like(msw_grid, 2.0)
surface_elevation
"grid"] = msw.GridData(
msw_model[=area,
area=landuse,
landuse=rootzone_depth,
rootzone_depth=surface_elevation,
surface_elevation=slt,
soil_physical_unit=active,
active )
Initial Condition
There are four options to specify initial conditions, see this for page for an explanation —link-here—. In this case we opt for an initial pF value of 2.2.
"ic"] = msw.InitialConditionsRootzonePressureHead(initial_pF=2.2) msw_model[
Meteorology
Meteorological information should be provided as grids with a time
dimension.
= msw_grid.expand_dims(time=times[:-1])
precipitation = precipitation * 1.5
evapotranspiration
"meteo_grid"] = msw.MeteoGrid(precipitation, evapotranspiration)
msw_model["mapping_prec"] = msw.PrecipitationMapping(precipitation)
msw_model["mapping_evt"] = msw.EvapotranspirationMapping(evapotranspiration) msw_model[
Ponding
"ponding"] = msw.Ponding(
msw_model[=xr.full_like(area, 0.0),
ponding_depth=xr.full_like(area, 1.0),
runon_resistance=xr.full_like(area, 1.0),
runoff_resistance )
Scaling Factors
Scaling factors can be defined to adapt some parameters in the soil physical database. With this you can investigate the sensitivity of parameters in soil physical database. Furthermore, with this package you can specify the depth of the perched water table.
"scaling"] = msw.ScalingFactors(
msw_model[=xr.full_like(area, 1.0),
scale_soil_moisture=xr.full_like(area, 1.0),
scale_hydraulic_conductivity=xr.full_like(area, 1.0),
scale_pressure_head=xr.full_like(msw_grid, 1.0),
depth_perched_water_table )
Infiltration Factors
Set the infiltration parameters. We set the resistances to -9999.0, which makes MetaSWAP ignore them.
"infiltration"] = msw.Infiltration(
msw_model[=xr.full_like(area, 1.0),
infiltration_capacity=xr.full_like(msw_grid, -9999.0),
downward_resistance=xr.full_like(msw_grid, -9999.0),
upward_resistance=xr.full_like(msw_grid, -9999.0),
bottom_resistance=xr.full_like(msw_grid, 0.1),
extra_storage_coefficient )
Landuse options
The landuse option class constructs a lookup table which is used to map landuse indices to a set of parameters. In this example, 3 stands for potatoes. This means that for every cell in the landuse
grid with a 3, the parameters for a crop with vegetation_index == 3
are associate, which in this case are potatoes.
= [1, 2, 3]
vegetation_index = ["grassland", "maize", "potatoes"]
names
= [1, 2, 3]
landuse_index = {"landuse_index": landuse_index}
coords
= xr.DataArray(data=names, coords=coords, dims=("landuse_index",))
landuse_names = xr.DataArray(
vegetation_index_da =vegetation_index, coords=coords, dims=("landuse_index",)
data )
Because there are a lot of parameters to define, we’ll create a DataArray of ones (lu
) to more easily broadcast all the different parameters.
= xr.ones_like(vegetation_index_da, dtype=float)
lu
"landuse_options"] = msw.LanduseOptions(
msw_model[=landuse_names,
landuse_name=vegetation_index_da,
vegetation_index=xr.ones_like(lu),
jarvis_o2_stress=xr.ones_like(lu),
jarvis_drought_stress=xr.full_like(lu, 99.0),
feddes_p1=xr.full_like(lu, 99.0),
feddes_p2=lu * [-2.0, -4.0, -3.0],
feddes_p3h=lu * [-8.0, -5.0, -5.0],
feddes_p3l=lu * [-80.0, -100.0, -100.0],
feddes_p4=xr.full_like(lu, 5.0),
feddes_t3h=xr.full_like(lu, 1.0),
feddes_t3l=lu * [-8.0, -5.0, -5.0],
threshold_sprinkling=xr.full_like(lu, 0.05),
fraction_evaporated_sprinkling=xr.full_like(lu, 20.0),
gift=xr.full_like(lu, 0.25),
gift_duration=lu * [10, 7, 7],
rotational_period=lu * [120, 180, 150],
start_sprinkling_season=lu * [230, 230, 240],
end_sprinkling_season=xr.ones_like(lu, dtype=int),
interception_option=xr.zeros_like(lu),
interception_capacity_per_LAI=xr.ones_like(lu),
interception_intercept )
Crop Growth
Crop growth tables are specified as a two-dimensional array, with the day of year as one dimension, and the vegetation index on the other. In the vegetation factors, we’ll show how to bring some distinction between different crops.
We’ll start off specifying the coordinates:
= np.arange(1, 367)
day_of_year = np.arange(1, 4)
vegetation_index
= {"day_of_year": day_of_year, "vegetation_index": vegetation_index} coords
We can use the coordinates to specify the soil cover of each plant. We’ll start with a grid of zeros
= xr.DataArray(
soil_cover =np.zeros(day_of_year.shape + vegetation_index.shape),
data=coords,
coords=("day_of_year", "vegetation_index"),
dims )
The simplest soil cover specification is a step function. In this case soil cover equals 1.0 for days 133 to 255 (mind Python’s 0-based index here), and for the rest of the days it equals zero.
132:254, :] = 1.0
soil_cover[
=1).plot() soil_cover.sel(vegetation_index
We’ll simply triple the soil cover to get a leaf area index
= soil_cover * 3 leaf_area_index
Vegetation factors are used to convert the Makkink reference evapotranspiration to a potential evapotranspiration for a certain vegetation type. We’ll specify some simple crop schemes for the three crops as vegetation factors. Mind that the vegetation factor array has two dimensions: day_of_year
and vegetation_index
= ["grass", "maize", "potatoes"]
vegetation_names
= xr.zeros_like(soil_cover)
vegetation_factor
120:132, :] = [1.0, 0.5, 0.0]
vegetation_factor[132:142, :] = [1.0, 0.7, 0.7]
vegetation_factor[142:152, :] = [1.0, 0.8, 0.9]
vegetation_factor[152:162, :] = [1.0, 0.9, 1.0]
vegetation_factor[162:172, :] = [1.0, 1.0, 1.2]
vegetation_factor[172:182, :] = [1.0, 1.2, 1.2]
vegetation_factor[182:192, :] = [1.0, 1.3, 1.2]
vegetation_factor[192:244, :] = [1.0, 1.2, 1.1]
vegetation_factor[244:254, :] = [1.0, 1.2, 0.7]
vegetation_factor[254:283, :] = [1.0, 1.2, 0.0]
vegetation_factor[
# Since grass is the reference crop, force all grass to 1.0
0] = 1.0
vegetation_factor[:,
# Assign vegetation names for the plot
vegetation_factor.assign_coords(=("vegetation_index", vegetation_names)
vegetation_names="day_of_year", hue="vegetation_names") ).plot.line(x
We’ll leave the interception capacity at zero, and the other factors at one, and assign these to the AnnualCropFactors package.
"crop_factors"] = msw.AnnualCropFactors(
msw_model[=soil_cover,
soil_cover=leaf_area_index,
leaf_area_index=xr.zeros_like(soil_cover),
interception_capacity=vegetation_factor,
vegetation_factor=xr.ones_like(soil_cover),
interception_factor=xr.ones_like(soil_cover),
bare_soil_factor=xr.ones_like(soil_cover),
ponding_factor )
Output Control
"oc_idf"] = msw.IdfMapping(area, -9999.0)
msw_model["oc_var"] = msw.VariableOutputControl()
msw_model["oc_time"] = msw.TimeOutputControl(time=times) msw_model[
MetaSWAP Mappings
MetaSWAP requires its own mapping of SVAT to MODFLOW cells, for internal use. We therefore provide the mf6.StructuredDiscretization and mf6.Well package to mf6.CouplerMapping.
"mod2svat"] = msw.CouplerMapping(
msw_model[=gwf_model["dis"], well=gwf_model["wells_msw"]
modflow_dis )
The sprinkling package also requires the Modflow6 wells.
"sprinkling"] = msw.Sprinkling(
msw_model[=xr.full_like(msw_grid, 100.0),
max_abstraction_groundwater=xr.full_like(msw_grid, 100.0),
max_abstraction_surfacewater=gwf_model["wells_msw"],
well )
Coupler mapping
The MetaSWAP model and Modflow 6 simulation are provided to the MetaMod class, which takes care of connecting (= “mapping”) the two models. Make sure to provide the keys of the dummy Modflow 6 boundary conditions where MetaSWAP is coupled to, so iMOD Python knows where to look: It is technically possible to define multiple WEL and RCH packages in Modflow 6.
= primod.MetaModDriverCoupling(
driver_coupling ="GWF_1",
mf6_model="rch_msw",
mf6_recharge_package="wells_msw",
mf6_wel_package
)
= primod.MetaMod(
metamod =msw_model,
msw_model=simulation,
mf6_simulation=[driver_coupling],
coupling_list )
We can write the coupled models by providing the following necessary paths to iMOD Coupler:
- modflow 6 library
- metaswap library
- directory with dependent libraries for metaswap
You can download the modflow and metaswap libraries (.dll
’s) as part of the the last iMOD Coupler release for Windows for free. Please contact imod.support@deltares.nl if you require the libraries for Linux.
= imod.util.temporary_directory()
metamod_dir = "./path/to/mf6.dll"
mf6_dll = "./path/to/metaswap.dll"
metaswap_dll = "./path/to/metaswap/dll/dependency/directory"
metaswap_dll_dependency_dir
metamod.write(metamod_dir, mf6_dll, metaswap_dll, metaswap_dll_dependency_dir)
Running the models
In order to run the models, make sure you install imod_coupler
. You can find the installation instructions here.