Modifying model layers

This notebook shows methods for modyfying model layers for MODFLOW models, for example by combining layers and splitting layers. Multiple layers can be combined into one layer or one layer can be split into sub-layers based on a fraction of the original thickness.

import matplotlib.pyplot as plt
import numpy as np
from shapely.geometry import LineString

import nlmod
from nlmod.plot import DatasetCrossSection
nlmod.util.get_color_logger("INFO")
nlmod.show_versions()
Python version     : 3.11.14
NumPy version      : 2.4.4
Xarray version     : 2026.4.0
Matplotlib version : 3.10.9
Flopy version      : 3.10.0

nlmod version      : 0.11.3dev
def compare_layer_models(
    ds1,
    line,
    colors,
    ds2=None,
    zmin=-200,
    zmax=10,
    min_label_area=1000,
    title1="REGIS original",
    title2="Modified layers",
    xlabel="Distance along x-sec (m)",
    ylabel="m NAP",
):
    if ds2 is None:
        fig, ax1 = plt.subplots(1, 1, figsize=(14, 6))
    else:
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 12), sharex=True)
    dcs1 = DatasetCrossSection(ds1, line=line, ax=ax1, zmin=zmin, zmax=zmax)
    polys2 = dcs1.plot_layers(colors=colors, min_label_area=min_label_area)
    dcs1.plot_grid(linewidth=0.5, vertical=False)
    ax1.set_ylabel(ylabel)

    if ds2 is not None:
        ax1.set_title(title1)
        dcs2 = DatasetCrossSection(ds2, line=line, ax=ax2, zmin=zmin, zmax=zmax)
        polys1 = dcs2.plot_layers(colors=colors, min_label_area=min_label_area)
        dcs2.plot_grid(linewidth=0.5, vertical=False)
        ax2.set_ylabel(ylabel)
        ax2.set_xlabel(xlabel)
        ax2.set_title(title2)
    else:
        ax1.set_xlabel(xlabel)

Get data

Define an extent to obtain REGIS

extent = [131000, 136800, 471500, 475700]

Download and cache REGIS netCDF.

regis = nlmod.read.regis.download_regis(extent)
WARNING:nlmod.dims.layers.remove_layer_dim_from_top:Botm of layer is not equal to top of deeper layer in 4 cells

Let’s take a look at the dataset

regis
<xarray.Dataset> Size: 1MB
Dimensions:      (y: 42, x: 58, layer: 34)
Coordinates:
  * y            (y) float64 336B 4.756e+05 4.756e+05 ... 4.716e+05 4.716e+05
  * x            (x) float64 464B 1.31e+05 1.312e+05 ... 1.366e+05 1.368e+05
  * layer        (layer) <U8 1kB 'HLc' 'BXz2' 'BXz3' ... 'OOz2' 'OOc' 'BRk1'
    spatial_ref  int64 8B 0
Data variables:
    top          (y, x) float32 10kB -1.35 -1.34 -1.37 ... -0.56 -0.31 -0.14
    botm         (layer, y, x) float32 331kB -5.54 -5.53 -5.52 ... -659.1 -659.3
    kh           (layer, y, x) float32 331kB nan nan nan nan ... nan nan nan nan
    kv           (layer, y, x) float32 331kB nan nan nan ... 0.002 0.002 0.002
Attributes: (12/41)
    references:                    https://www.dinoloket.nl/regis-ii-het-hydr...
    Conventions:                   CF-1.7
    creator_url:                   https://www.dinoloket.nl
    keywords_vocabulary:           NASA/GCMD Earth Science Keywords. Version 6.0
    acknowledgment:                https://www.dinoloket.nl
    project:                       REGIS v02r2s3
    ...                            ...
    geospatial_vertical_min:       -1235.92
    geospatial_vertical_max:       322.75
    geospatial_vertical_units:     m-NAP
    geospatial_vertical_positive:  up
    gridtype:                      structured
    extent:                        [131000, 136800, 471500, 475700]

Define an line to draw a cross-section

# diagonal line through extent
line = LineString([(extent[0], extent[2]), (extent[1], extent[3])])

Get colors for our cross-section plot.

colors = nlmod.read.regis.get_legend()["color"].to_dict()

Draw the cross-section for REGIS

compare_layer_models(regis, line, colors)
../_images/d6270a762d476d265fff0b99ba5d0e1e27107c3b57546c8b0791f91a8e1a056e.png

Split layers

First we determine how to split the layers. This is done by creating a list of factors, that is used to determine fractions that add up to 1. The layer will be split into sub-layers from the top down, with each sub-layer getting a thickness equal to the fraction times the original thickness.

For example, (1, 1) will split the layer into two sub-layers, each getting a thickness equal to 50% of the original layer. In this example the fractions already add up to 1 for each layer.

# split dictionary
split_dict = {
    "PZWAz2": (0.3, 0.3, 0.4),
    "PZWAz3": (0.2, 0.2, 0.2, 0.2, 0.2),
}

Calculate the new layer elevations based on the information above.

regis_split, split_reindexer = nlmod.layers.split_layers_ds(
    regis, split_dict, return_reindexer=True
)
INFO:nlmod.dims.layers.split_layers_ds:Splitting layers ['PZWAz2', 'PZWAz3']
INFO:nlmod.dims.layers.split_layers_ds:Split 'PZWAz2' into 3 sub-layers
INFO:nlmod.dims.layers.split_layers_ds:Fill values for variable 'kh' in split layers with the values from the original layer.
INFO:nlmod.dims.layers.split_layers_ds:Fill values for variable 'kv' in split layers with the values from the original layer.
INFO:nlmod.dims.layers.split_layers_ds:Split 'PZWAz3' into 5 sub-layers

View the resulting Dataset:

regis_split
<xarray.Dataset> Size: 1MB
Dimensions:      (x: 58, y: 42, layer: 40)
Coordinates:
  * x            (x) float64 464B 1.31e+05 1.312e+05 ... 1.366e+05 1.368e+05
  * y            (y) float64 336B 4.756e+05 4.756e+05 ... 4.716e+05 4.716e+05
  * layer        (layer) <U8 1kB 'HLc' 'BXz2' 'BXz3' ... 'OOz2' 'OOc' 'BRk1'
    spatial_ref  int64 8B 0
Data variables:
    top          (y, x) float32 10kB -1.35 -1.34 -1.37 ... -0.56 -0.31 -0.14
    botm         (layer, y, x) float32 390kB -5.54 -5.53 -5.52 ... -659.1 -659.3
    kh           (layer, y, x) float32 390kB nan nan nan nan ... nan nan nan nan
    kv           (layer, y, x) float32 390kB nan nan nan ... 0.002 0.002 0.002
Attributes: (12/41)
    references:                    https://www.dinoloket.nl/regis-ii-het-hydr...
    Conventions:                   CF-1.7
    creator_url:                   https://www.dinoloket.nl
    keywords_vocabulary:           NASA/GCMD Earth Science Keywords. Version 6.0
    acknowledgment:                https://www.dinoloket.nl
    project:                       REGIS v02r2s3
    ...                            ...
    geospatial_vertical_min:       -1235.92
    geospatial_vertical_max:       322.75
    geospatial_vertical_units:     m-NAP
    geospatial_vertical_positive:  up
    gridtype:                      structured
    extent:                        [131000, 136800, 471500, 475700]

The reindexer dictionary links the new layers to the old layers. This can be convenient for copying data from the original layers to the new sub-layers.

# key = new layer index
# value = original layer index: repeats where layer was split
split_reindexer
{np.str_('HLc'): np.str_('HLc'),
 np.str_('BXz2'): np.str_('BXz2'),
 np.str_('BXz3'): np.str_('BXz3'),
 np.str_('BXz4'): np.str_('BXz4'),
 np.str_('KRz3'): np.str_('KRz3'),
 np.str_('DRz1'): np.str_('DRz1'),
 np.str_('DRz2'): np.str_('DRz2'),
 np.str_('DRz3'): np.str_('DRz3'),
 np.str_('DTc'): np.str_('DTc'),
 np.str_('URz1'): np.str_('URz1'),
 np.str_('URz2'): np.str_('URz2'),
 np.str_('URz3'): np.str_('URz3'),
 np.str_('URz4'): np.str_('URz4'),
 np.str_('URz5'): np.str_('URz5'),
 np.str_('STz1'): np.str_('STz1'),
 np.str_('STz2'): np.str_('STz2'),
 np.str_('APz1'): np.str_('APz1'),
 np.str_('PZWAz1'): np.str_('PZWAz1'),
 np.str_('WAk1'): np.str_('WAk1'),
 'PZWAz2_1': 'PZWAz2',
 'PZWAz2_2': 'PZWAz2',
 'PZWAz2_3': 'PZWAz2',
 'PZWAz3_1': 'PZWAz3',
 'PZWAz3_2': 'PZWAz3',
 'PZWAz3_3': 'PZWAz3',
 'PZWAz3_4': 'PZWAz3',
 'PZWAz3_5': 'PZWAz3',
 np.str_('PZWAz4'): np.str_('PZWAz4'),
 np.str_('MSz1'): np.str_('MSz1'),
 np.str_('MSk1'): np.str_('MSk1'),
 np.str_('MSz2'): np.str_('MSz2'),
 np.str_('MSk2'): np.str_('MSk2'),
 np.str_('MSz3'): np.str_('MSz3'),
 np.str_('MSc'): np.str_('MSc'),
 np.str_('MSz4'): np.str_('MSz4'),
 np.str_('OOz1'): np.str_('OOz1'),
 np.str_('OOk1'): np.str_('OOk1'),
 np.str_('OOz2'): np.str_('OOz2'),
 np.str_('OOc'): np.str_('OOc'),
 np.str_('BRk1'): np.str_('BRk1')}

Plot the cross-section of the original and the new layer model.

compare_layer_models(regis, line, colors, ds2=regis_split, title2="Split layers")
../_images/037509329de9c5059554e9f87968270c2967c8240b7f857a5510d8769253da09.png

Combine layers

Example how to combine model layers. First find the indices of the layers to combine.

combine_layers = [
    tuple(np.argwhere(regis.layer.str.startswith("URz").data).squeeze().tolist()),
    tuple(np.argwhere(regis.layer.isin(["PZWAz2", "PZWAz3"]).data).squeeze().tolist()),
]
combine_layers
[(9, 10, 11, 12, 13), (19, 20)]

Combine layers using the combine_layers_ds() function and passing the layer dataset and the list of layer numbers to combine.

regis_combined = nlmod.layers.combine_layers_ds(regis, combine_layers, kD=None, c=None)
INFO:nlmod.dims.layers.combine_layers_ds:Calculating new layer tops and bottoms...
INFO:nlmod.dims.layers.combine_layers_ds:Calculate equivalent 'kh' for combined layers.
INFO:nlmod.dims.layers.combine_layers_ds:Calculate equivalent 'kv' for combined layers.
INFO:nlmod.dims.layers.combine_layers_ds:Done! Created new dataset with combined layers!

Take a look a the resulting dataset

regis_combined
<xarray.Dataset> Size: 2MB
Dimensions:  (y: 42, x: 58, layer: 29)
Coordinates:
  * y        (y) float64 336B 4.756e+05 4.756e+05 ... 4.716e+05 4.716e+05
  * x        (x) float64 464B 1.31e+05 1.312e+05 ... 1.366e+05 1.368e+05
  * layer    (layer) <U6 696B 'HLc' 'BXz2' 'BXz3' 'BXz4' ... 'OOz2' 'OOc' 'BRk1'
Data variables:
    top      (y, x) float64 19kB -1.35 -1.34 -1.37 -1.42 ... -0.56 -0.31 -0.14
    botm     (layer, y, x) float64 565kB -5.54 -5.53 -5.52 ... -659.1 -659.3
    kh       (layer, y, x) float64 565kB nan nan nan nan nan ... nan nan nan nan
    kv       (layer, y, x) float64 565kB nan nan nan nan ... 0.002 0.002 0.002
Attributes: (12/41)
    references:                    https://www.dinoloket.nl/regis-ii-het-hydr...
    Conventions:                   CF-1.7
    creator_url:                   https://www.dinoloket.nl
    keywords_vocabulary:           NASA/GCMD Earth Science Keywords. Version 6.0
    acknowledgment:                https://www.dinoloket.nl
    project:                       REGIS v02r2s3
    ...                            ...
    geospatial_vertical_min:       -1235.92
    geospatial_vertical_max:       322.75
    geospatial_vertical_units:     m-NAP
    geospatial_vertical_positive:  up
    gridtype:                      structured
    extent:                        [131000, 136800, 471500, 475700]

Plot the new and the old cross-section. Use the layer code and color from the first layer name for the combined layer

compare_layer_models(regis, line, colors, ds2=regis_combined, title2="Combined layers")
../_images/8cde8089fd756004113d05d153a75dbb717fa2a947fdfaa7900fe2b8ce8c78a3.png

Set new model top

The nlmod.layers.set_model_top changes the top of the model. When the new top is lower than the old top, the new top is burned in the layer model, lowering the top of the top layer(s). Top layers can become incactive, when the thickness is reduced to 0. When the new top is higher than the old top, the thickness of the most upper active layer (not necessarily the first) is increased. This method can be used to change the model top to a digital terrain model with a higher accuracy.

First transform the regis-date to a model Dataset, as the next methods need a model Dataset.

ds = nlmod.to_model_ds(regis)
INFO:nlmod.dims.base.to_model_ds:resample layer model data to structured modelgrid
INFO:nlmod.dims.layers.get_kh_kv:kv and kh both undefined in layer HLc
INFO:nlmod.dims.layers.get_kh_kv:kv and kh both undefined in layer DTc
INFO:nlmod.dims.layers._fill_var:Filling 11886 values in active cells of kh by multipying kv with an anisotropy of 10
INFO:nlmod.dims.layers._fill_var:Filling 51924 values in active cells of kv by dividing kh by an anisotropy of 10
INFO:nlmod.dims.layers._fill_var:Filling 2485 values in active cells of kh with a value of 1.0 m/day
INFO:nlmod.dims.layers._fill_var:Filling 2485 values in active cells of kv with a value of 0.1 m/day
ds_new = nlmod.layers.set_model_top(ds.copy(deep=True), 5.0)
compare_layer_models(ds, line, colors, ds2=ds_new, title2="New top")
../_images/dfd87c8ef403cc9c0e858b5ca9c3483c1daaf597141792a35cf29f6992a4f1ad.png

Set layer top

nlmod.layers.set_layer_top sets the layer top to a specified value or array.

This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before.

ds_new = nlmod.layers.set_layer_top(ds.copy(deep=True), "WAk1", -40.0)
compare_layer_models(ds, line, colors, ds2=ds_new, title2="Modified")
../_images/23b5a6773f6ef0bcf52c58ed086889ecc89277e7b5972246594bc046cec17c2f.png

Set layer bottom

nlmod.layers.set_layer_botm sets the layer botm to a specified value or array.

This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before.

# set the botm of 'WAk1' to -70 m NAP
ds_new = nlmod.layers.set_layer_botm(ds.copy(deep=True), "WAk1", -70.0)
compare_layer_models(ds, line, colors, ds2=ds_new, title2="Modified")
../_images/2e9503752b9356a4dad710b88a4098c1727df7f0f0c8936c3102cbd7b90c749f.png

Set layer thickness

nlmod.layers.set_layer_thickness sets the thickness of a layer to a specified value or array. With a parameter called ‘change’ you can specify in which direction the layer is changed. The only supported option for now is ‘botm’, which changes the layer botm.

This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before.

# set the thickness of 'WAk1' to 20 m NAP
ds_new = nlmod.layers.set_layer_thickness(ds.copy(deep=True), "WAk1", 20)
compare_layer_models(ds, line, colors, ds2=ds_new, title2="Modified")
../_images/9b7eddb5ec9fca93b7c874ccbbcd773638dcd0410db6c38b4627111b403004d0.png

Set minimum layer thickness

nlmod.layers.set_minimum layer_thickness increases the thickness of a layer if the thickness is less than a specified value. With a parameter called ‘change’ you can specify in which direction the layer is changed. The only supported option for now is ‘botm’, which changes the layer botm.

This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before.

# set the minimum thickness of 'PZWAz2' to 20 m
ds_new = nlmod.layers.set_minimum_layer_thickness(ds.copy(deep=True), "PZWAz2", 20.0)
compare_layer_models(ds, line, colors, ds2=ds_new, title2="Modified")
../_images/b9574e40784bef1ca314a82424e4cbb7474188077f3b97e3968d1f9010d25400.png