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:
import numpy as npimport pandas as pdimport xarray as xrimport primodimport imodfrom imod import mf6, msw
/home/runner/work/iMOD-Documentation/iMOD-Documentation/.pixi/envs/default/lib/python3.14/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
Modflow 6 model
Next, we initiate the Modflow 6 groundwater model:
gwf_model = mf6.GroundwaterFlowModel()
Create grid
We’ll then define the Modflow 6 grid. It consists of 3 layers of 9 by 9 cells rasters.
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.
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.
We’ll create a dummy well package as well. imod.mf6.LayeredWell its input data provided as long tables instead of grids to so therefore we’ll create 1d arrays by calling np.meshgrid and then fancy indexing this with the boolean index is_active.
Initiate a Modflow 6 simulation and attach the groundwater model to it.
simulation = mf6.Modflow6Simulation("test")simulation["GWF_1"] = gwf_model# Define solver settings, we'll use a preset that is sufficient for this example.simulation["solver"] = mf6.SolutionPresetSimple(modelnames=["GWF_1"])
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_model = msw.MetaSwapModel(unsaturated_database="./path/to/unsaturated/database")# 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.msw_grid = idomain.sel(layer=1, drop=True).astype(float)
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).
active = msw_grid.astype(bool)active[..., 0] =Falseactive[..., -1] =Falseactive
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.
subunit = [0, 1]total_cell_area =abs(dx * dy)equal_area_per_subunit = total_cell_area /len(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 = ( xr.full_like(msw_grid, equal_area_per_subunit, dtype=float) .expand_dims(subunit=subunit) .copy() # expand_dims creates a view, so copy it to allow setting values.)# To the left we only have subunit 0area[0, :, :3] = total_cell_areaarea[1, :, :3] = np.nan# To the right we only have subunit 1area[0, :, -3:] = np.nanarea[1, :, -3:] = total_cell_areaarea
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.
slt = xr.full_like(msw_grid, 1, dtype=np.int16)# Set all cells on the right half to 2.slt = slt.where((slt.x < (xmax /2)), 2)slt
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.
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.
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.
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.
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.
We’ll simply triple the soil cover to get a leaf area index
leaf_area_index = soil_cover *3
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
vegetation_names = ["grass", "maize", "potatoes"]vegetation_factor = 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]# Since grass is the reference crop, force all grass to 1.0vegetation_factor[:, 0] =1.0# Assign vegetation names for the plotvegetation_factor.assign_coords( vegetation_names=("vegetation_index", vegetation_names)).plot.line(x="day_of_year", hue="vegetation_names")
We’ll leave the interception capacity at zero, and the other factors at one, and assign these to the AnnualCropFactors package.
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.