Comparing modelled heads to observations

This notebook showcases some methods to compare modelled heads to head observations.

from pathlib import Path

import hydropandas as hpd
import matplotlib.pyplot as plt
import pandas as pd
import xarray as xr

import nlmod

First read in model results from the IJmuiden model (03_local_grid_refinement.ipynb).

model_ws = Path("../examples/03_local_grid_refinement")
model_name = "IJm_planeten"

# read model dataset
ds = xr.open_dataset(model_ws / f"{model_name}.nc")
ds.attrs["model_ws"] = model_ws

# read heads
head = nlmod.gwf.output.get_heads_da(ds)

Compute the groundwater level and plot the result for the first timestep.

# compute the groundwater level in each time step
gwl = nlmod.gwf.output.get_gwl_from_wet_cells(head)
# plot the heads in the first aquifer
ax = nlmod.plot.map_array(
    gwl.isel(time=0), ds=ds, cmap="RdBu_r", colorbar_label="head [m+NAP]"
)
../_images/ea861e3ccbdff8bd785990c72e5a9a5b0f6c52d10d568c33df1ee389c3f9b643.png

Load the measurements and plot the locations of the observation wells.

# df = pd.read_pickle("./data/20250428_bro_ijmuiden_np1_26_4.pklz", compression="zip")
df = hpd.read_excel("../examples/data/20250428_bro_ijmuiden.xlsx")
df.head()
x y location filename source unit tube_nr screen_top screen_bottom ground_level tube_top metadata_available obs
name
GMW000000004503_001 101959.9 497438.0 GMW000000004503 NaN BRO mNAP 1 4.770 3.770 7.640 7.490 True GroundwaterObs GMW000000004503_001 -----metada...
GMW000000004504_001 104691.0 497251.5 GMW000000004504 NaN BRO mNAP 1 0.000 -1.000 2.130 2.000 True GroundwaterObs GMW000000004504_001 -----metada...
GMW000000004505_001 100251.4 496653.8 GMW000000004505 NaN BRO mNAP 1 2.936 1.936 4.795 4.696 True GroundwaterObs GMW000000004505_001 -----metada...
GMW000000007301_001 104348.2 495624.2 GMW000000007301 NaN BRO mNAP 1 1.810 0.810 3.720 3.670 True GroundwaterObs GMW000000007301_001 -----metada...
GMW000000013100_001 104554.0 499841.0 GMW000000013100 NaN BRO mNAP 1 1.880 0.880 3.970 3.880 True GroundwaterObs GMW000000013100_001 -----metada...
f, ax = nlmod.plot.get_map(nlmod.grid.get_extent(ds), background=True)
ax.plot(df.x, df.y, "ko");
../_images/440b8db96da3afe32314e6a2e3e536f90af8e2f694be45f51f6b14cd509e7db1.png

Get the modeled heads

Get the heads from the cells in which the observation wells are located.

For this we use the nlmod.layers.get_modellayers_indexer() method which takes a model dataset (defining the model grid) and a dataframe (with the observation well metadata) as input.

The dataframe must define the x,y locations of the observation wells, and the top and bottom of the screens. By default it is assumed these column names follow the hydropandas standard: x, y screen_top and screen_bottom.

idx = nlmod.layers.get_modellayers_indexer(ds, df)
idx
<xarray.Dataset> Size: 5kB
Dimensions:  (name: 114)
Coordinates:
  * name     (name) object 912B 'GMW000000004503_001' ... 'GMW000000083586_001'
Data variables:
    icell2d  (name) int64 912B 2569 2796 3409 4624 195 ... 5773 5226 5227 5227
    layer    (name) <U8 4kB 'NASC' 'NAZA' 'NASC' 'NAZA' ... 'NAZA' 'NAZA' 'NAZA'

This indexer can be used directly (if no warnings were raised or if drop_nan_layers=True) to obtain the heads in the cells with observation wells.

Note

If warnings were raised, this means there are observation wells for which the corresponding model layer could not be determined (these probably lie above or below the model). In this case the modellayer is returned as a float array and contains NaNs.

Some post-processing will be necessary to be able to use the indexer e.g. dropping the NaN values:

idx.dropna("name", subset=["layer"])

Additionally, the layer might also have to be renamed to get the layer names corresponding to the layer indices:

idx["layer"].values = ds["layer"].values[idx["layer"].astype(int)]

Try using the indexer to get the modelled heads for each observation well

hsim = head.sel(**idx)
hsim
<xarray.DataArray 'head' (time: 6, name: 114)> Size: 5kB
array([[ 1.09010564,  1.39928643,  1.43258501,  2.85551797,  1.3533678 ,
         1.70784255,  1.        ,  1.67610717,  1.17642116,  2.01915568,
         1.28301974,  0.71477995,  2.2319453 ,  1.23639789,  1.12483195,
         2.73074945,  2.77703465,  1.97130616,  1.92459264,  3.3052092 ,
         2.05190367,  2.65102168,  1.7624203 ,  3.44951217,  2.10696258,
         2.43449446,  2.39039195,  1.48672321,  2.24327515,  1.93501609,
         2.84198741,  3.06245864,  1.76152455,  3.12525763,  2.88956555,
         2.9754541 ,  1.63717556,  1.4790152 ,  1.02034932,  3.05452561,
         2.02688873,  3.66685363,  1.        ,  0.92840141,  1.51531802,
         1.73170309,  1.80284579,  1.99039628,  2.00050052,  1.80284579,
         2.05197211,  1.68635636,  2.87407379,  2.05190367,  3.3052092 ,
         2.73074945,  3.85172902,  1.92459264,  2.06842626,  2.02688873,
         1.33703625,  1.87005095,  3.4194908 ,  1.53744633,  2.06842626,
         2.44120241,  2.06833218,  1.71826081,  1.22035302,  1.63920745,
         1.        ,  3.31632905,  1.28921923,  2.03424576,  2.84198741,
         1.        ,  2.46191195,  2.34438063,  3.62343098,  1.0099972 ,
         1.8980955 ,  1.38976512,  1.27599336,  1.        ,  1.93582334,
         2.03424576,  2.62847225,  1.06025254,  1.99741901,  2.84842844,
         2.55693482,  2.05190367,  1.        ,  2.65100458,  3.44022101,
         1.3533678 ,  1.18944211,  1.07827447,  1.26945472,  1.44430704,
...
         2.2694793 ,  2.29630247,  1.58817229,  1.60078215,  2.65306581,
         1.79795084,  2.22360158,  1.57941582,  2.78650805,  1.85949189,
         2.07410103,  2.00880122,  1.26710019,  1.80979743,  1.61562886,
         2.33185057,  2.47698567,  1.46521123,  2.51892283,  2.35328355,
         2.4070142 ,  1.34196625,  1.19554568,  0.99834095,  2.42925621,
         1.79141855,  2.93248157,  1.        ,  0.91416313,  1.3265559 ,
         1.5817896 ,  1.54048038,  1.62184451,  1.63258747,  1.54048038,
         1.73357427,  1.41636526,  2.36663525,  1.79795084,  2.65306581,
         2.2694793 ,  3.06237739,  1.60078215,  1.81145578,  1.79141855,
         1.13855734,  1.56683194,  2.75231449,  1.31038901,  1.81145578,
         1.97083442,  1.78831813,  1.41455254,  0.99280699,  1.48828668,
         1.        ,  2.62425383,  1.1770168 ,  1.69446926,  2.33185057,
         1.        ,  2.0602053 ,  1.96525277,  2.89144956,  0.99525614,
         1.57853711,  1.18693709,  1.09129788,  1.        ,  1.58483885,
         1.69446926,  2.11394479,  0.87294285,  1.64055847,  2.27585644,
         2.04268253,  1.79795084,  1.        ,  2.22062489,  2.76070181,
         1.2588423 ,  1.13174039,  1.04789346,  1.14875473,  1.27271541,
         1.30556235,  1.30556235,  2.13716386,  1.43564494,  1.04419843,
         1.13174039,  1.12064264,  1.29200496,  1.31886252,  1.34351739,
         1.10531659,  2.07410103,  2.03859008,  2.03859008]])
Coordinates:
  * time         (time) datetime64[ns] 48B 2015-01-02 2015-01-03 ... 2015-01-07
  * name         (name) object 912B 'GMW000000004503_001' ... 'GMW00000008358...
    y            (name) float64 912B 4.974e+05 4.972e+05 ... 4.95e+05 4.95e+05
    x            (name) float64 912B 1.02e+05 1.046e+05 ... 1.046e+05 1.046e+05
    layer        (name) <U8 4kB 'NASC' 'NAZA' 'NASC' ... 'NAZA' 'NAZA' 'NAZA'
    spatial_ref  int64 8B 0
Attributes:
    units:    m NAP

Get and plot the result for the a random observation well.

i = 20
hsim.isel(name=i)
<xarray.DataArray 'head' (time: 6)> Size: 48B
array([2.05190367, 1.10360664, 1.51119841, 0.75373465, 0.92884497,
       1.79795084])
Coordinates:
  * time         (time) datetime64[ns] 48B 2015-01-02 2015-01-03 ... 2015-01-07
    y            float64 8B 4.948e+05
    x            float64 8B 1.048e+05
    spatial_ref  int64 8B 0
    layer        <U8 32B 'NAZA'
    name         <U19 76B 'GMW000000022508_001'
Attributes:
    units:    m NAP
hsim.isel(name=i).plot(marker="o", figsize=(10, 3))
# plot observations
df.obs.loc[hsim["name"].values[i]].loc[
    pd.Timestamp(hsim.time[0].item()) : pd.Timestamp(hsim.time[-1].item())
].plot(y="value", ax=plt.gca(), marker="x");
/home/docs/checkouts/readthedocs.org/user_builds/nlmod/envs/latest/lib/python3.11/site-packages/pandas/plotting/_matplotlib/core.py:981: UserWarning: This axis already has a converter set and is updating to a potentially incompatible converter
  return ax.plot(*args, **kwds)
../_images/82bed0dcddf0150101d04a7d84e62f726873aa7340a768beee5ea5dc980b3888.png
hmean = hsim.mean("time")

f, ax = nlmod.plot.get_map(nlmod.grid.get_extent(ds), background=True)
cm = ax.scatter(
    hmean.x,
    hmean.y,
    s=75,
    c=hmean.values,
    cmap="RdBu_r",
    edgecolors="k",
    linewidths=0.75,
)
cbar = f.colorbar(cm, ax=ax, label="head [m+NAP]", shrink=0.85)
../_images/d024dbf9ae4e042dc16f5a58ae4eeb72dc8ccba92560801fc073c839676c1250.png

Interpolating heads

It is also possible to interpolate the heads at the locations of the observation wells. For this we need the original x, y coordinates of the observation wells as well as the layer each well is measuring in.

To get all this information it can be useful to use full_output=True in nlmod.layers.get_modellayers_indexer(). This returns every variable that is necessary to compute the layer for each observation well.

Note

This also returns the model layer for the screen_top and screen_bottom separately, allowing you to identify observation wells spanning multiple layers.

idx_full = nlmod.layers.get_modellayers_indexer(ds, df, full_output=True)
idx_full
<xarray.Dataset> Size: 48kB
Dimensions:         (name: 114, ilayer: 40)
Coordinates:
  * name            (name) object 912B 'GMW000000004503_001' ... 'GMW00000008...
Dimensions without coordinates: ilayer
Data variables:
    x               (name) float64 912B 1.02e+05 1.047e+05 ... 1.047e+05
    y               (name) float64 912B 4.974e+05 4.973e+05 ... 4.95e+05
    screen_top      (name) float64 912B 4.77 0.0 2.936 1.81 ... 1.43 1.43 1.42
    screen_bottom   (name) float64 912B 3.77 -1.0 1.936 0.81 ... 0.43 0.43 0.42
    icell2d         (name) int64 912B 2569 2796 3409 4624 ... 5226 5227 5227
    top             (name) float64 912B 7.75 2.25 4.75 3.75 ... 2.75 3.75 3.75
    botm            (ilayer, name) float64 36kB 7.25 1.75 4.25 ... -221.0 -221.0
    modellayer_top  (name) float64 912B 1.0 3.0 1.0 3.0 3.0 ... 3.0 3.0 3.0 3.0
    modellayer_bot  (name) float64 912B 1.0 3.0 1.0 3.0 3.0 ... 3.0 3.0 3.0 3.0
    layer           (name) <U8 4kB 'NASC' 'NAZA' 'NASC' ... 'NAZA' 'NAZA' 'NAZA'

Now we can use nlmod.observations.interpolate_points_ds() to compute the interpolated heads at each observation well. The first argument is the data array we want to interpolate. The second argument is a dataset containing information about the location and layer for each observation well.

We need to pass the correct names for each variable:

  • x, y: the coordinate names for the locations of the computed heads, the default is "x" and "y"

  • xi, yi: the coordinate names of the observation wells in idx_full, the default is "x" and "y"

  • layer: the layer dimension, the default is “layer”

Our data matches the default so we don’t need to adjust anything.

Note

For structured grids the returned x and y-coordinates in nlmod.layers.get_modellayers_indexer() are the coordinates corresponding to the cell centers. This way the result can be directly used for indexing a data array. The original locations of the observation wells are stored under x_obs y_obs. When using structured grids make sure to pass the correct coordinate names for xi and yi to the interpolate function.

hsim_i = nlmod.observations.interpolate_to_points(head, idx_full)
hsim_i
<xarray.DataArray 'interpolated' (time: 6, name: 114)> Size: 5kB
array([[ 1.08859729e+00,  1.36521920e+00,  1.42751658e+00,
         2.85389136e+00,  1.37450418e+00,  1.76154497e+00,
                    nan,  1.64195433e+00,  1.19219718e+00,
         2.07280201e+00,  1.23036662e+00,  9.30510621e-01,
         2.26381644e+00,  1.21727456e+00,             nan,
         2.70654863e+00,  2.77809513e+00,  1.96798861e+00,
         1.94790693e+00,  3.33954987e+00,  2.08525086e+00,
         2.67232589e+00,             nan,  3.38342789e+00,
         2.11742862e+00,  2.46980307e+00,  2.50048847e+00,
         1.47805184e+00,  2.09834926e+00,  1.89407371e+00,
         2.82413937e+00,  3.05591631e+00,  1.77468731e+00,
         3.10724708e+00,  2.86047726e+00,  2.94856112e+00,
         1.67952780e+00,  1.53767624e+00,  1.04937076e+00,
         3.11264800e+00,  2.03097566e+00,  3.66380940e+00,
                    nan,  9.95164375e-01,  1.41296683e+00,
         1.75872794e+00,  1.76225203e+00,  1.99109200e+00,
         1.97064926e+00,  1.81611675e+00,  2.02639847e+00,
         1.69922668e+00,  2.87118934e+00,  2.08506546e+00,
         3.33885099e+00,  2.70632991e+00,  3.93601652e+00,
         1.94759966e+00,  2.03463387e+00,  2.02753480e+00,
...
         2.67664520e+00,  2.25326040e+00,  3.12115629e+00,
         1.61926297e+00,  1.78481012e+00,  1.79129381e+00,
         1.14657668e+00,  1.56878268e+00,  2.66875481e+00,
         1.30479933e+00,  1.78497901e+00,  1.95173814e+00,
         1.78370263e+00,  1.48088561e+00,  9.76096800e-01,
         1.55242419e+00,  1.16793263e+00,  2.56136955e+00,
         1.11225426e+00,  1.68503166e+00,  2.32187535e+00,
                    nan,  2.34150870e+00,  1.91622742e+00,
         2.86290990e+00,  7.97971884e-01,  1.54601620e+00,
         1.18557309e+00,  1.12077942e+00,             nan,
         1.51191366e+00,  1.69747638e+00,  2.07975944e+00,
         8.44669413e-01,  1.66516857e+00,  2.24044108e+00,
         2.21606459e+00,  1.80844882e+00,             nan,
         2.13127501e+00,  2.79910667e+00,  1.26098566e+00,
                    nan,  1.02394673e+00,  1.15441746e+00,
         1.28009684e+00,  1.32470402e+00,  1.32094573e+00,
         2.14710639e+00,  1.33778002e+00,  1.05090884e+00,
                    nan,  1.12347079e+00,  1.29243104e+00,
         1.30924184e+00,  1.32922267e+00,  1.11826164e+00,
         2.08133773e+00,  2.02348235e+00,  2.03132784e+00]])
Coordinates:
  * time         (time) datetime64[ns] 48B 2015-01-02 2015-01-03 ... 2015-01-07
  * name         (name) object 912B 'GMW000000004503_001' ... 'GMW00000008358...
    spatial_ref  int64 8B 0
Attributes:
    units:    m NAP

Compare the interpolated result to the earlier result.

hsim.isel(name=i).plot(marker="o", figsize=(10, 3))
hsim_i.isel(name=i).plot(marker="o", ax=plt.gca());
../_images/49527a04296c04e16a6033331852f3e7dca7fdaea43314c65933b389e2eb28d6.png

Plot the location of the observation well in the grid:

obswell = idx_full.isel(name=i)
extent = [obswell.x - 200, obswell.x + 200, obswell.y - 200, obswell.y + 200]
f, ax = nlmod.plot.get_map(extent, background=True, figsize=6)
nlmod.plot.modelgrid(ds, ax=ax)
ax.plot(head.x, head.y, "k.", label="cell centers")
ax.plot(obswell.x, obswell.y, "ro", markersize=10, label=obswell.name.item())
ax.legend(loc=(0, 1), frameon=False, ncol=2, fontsize="small");
../_images/200287fc6579589c4b4e38188ac12daf9778c9b3c954d499c63ce68deb031c56.png