The Effective Vertical Anisotropy of Layered Aquifers#

Mark Bakker and Bram Bot.

Notebook to run experiments presented in the paper “The Effective Vertical Anisotropy of Layered Aquifers.”

Reference: M. Bakker and B. Bot (2024) The effective vertical anisotropy of layered aquifers. Groundwater. Available online early: doi.

import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import brentq

import timml as tml

Function to generate hydraulic conductivities

def generatek(ksection=20 * [0.1, 1], nsections=10, seed=1):
    """Generate k.

    Input:
    ksection: k values in the section
    nsection: number of sections
    seed: seed of random number generator
    """
    nk = len(ksection)
    # nlayers = nk * nsections
    kaq = np.zeros((nsections, nk))
    rng = np.random.default_rng(seed)
    for i in range(nsections):
        kaq[i] = rng.choice(ksection, nk, replace=False)
    return kaq.flatten()

Function to create a model with a canal given kx and kz.

def makemodel(kx, kz, d=4, returnmodel=False):
    """Creates model with river at center, and water supplied from infinitiy.

    d is depth of river.
    """
    H = 40  # thickness of model
    # d = 4 # depth of river
    naq = len(kx)
    ml = tml.Model3D(kaq=kx, z=np.linspace(H, 0, naq + 1), kzoverkh=kz / kx)
    tml.LineSink1D(ml, xls=0, sigls=2, layers=np.arange(int(d * 10)))
    tml.Constant(ml, xr=2000, yr=0, hr=0, layer=0)
    ml.solve(silent=True)
    if returnmodel:
        return ml
    return ml.head(0, 0)[0]


def func(kz, kx, h0, d=4, nlayers=400):
    """Computes head difference."""
    hnew = makemodel(kx * np.ones(nlayers), kz * np.ones(nlayers), d, returnmodel=False)
    return hnew - h0

Function to create a model with a partially penetrating well.

def makemodelradial(kx, kz, d=4, rw=0.1, returnmodel=False):
    """Creates model with river at center, and water supplied from infinitiy."""
    H = 40  # thickness of model
    # d = 4 # depth of river
    Qw = 1000
    naq = len(kx)
    ml = tml.Model3D(kaq=kx, z=np.linspace(H, 0, naq + 1), kzoverkh=kz / kx)
    tml.Well(ml, xw=0, yw=0, Qw=Qw, rw=rw, layers=np.arange(int(d * 10)))
    tml.Constant(ml, xr=2000, yr=0, hr=0, layer=0)
    ml.solve(silent=True)
    if returnmodel:
        return ml
    return ml.head(0, 0)[0]


def funcradial(kz, kx, h0, d=4, rw=0.1, nlayers=400):
    """Computes head difference."""
    hnew = makemodelradial(
        kx * np.ones(nlayers), kz * np.ones(nlayers), d, rw, returnmodel=False
    )
    return hnew - h0

Find effective vertical hydraulic conductivity for one realization of 400 layers and time it

kaq = 80 * [1, 3.16, 10, 31.6, 100]  # 400 k values
k = generatek(ksection=kaq, nsections=1)  # random order of k values
h0 = makemodel(k, k)  # head at canal
kx = np.mean(k)  # equivalent horizontal k
# vertical hydraulic conductivity:
%timeit kz = brentq(func, a=0.001 * kx, b=kx, args=(kx, h0)) 
2.35 s ± 6.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Run the experiment for Figure 3a. This is commented out because it takes a long time to run. The number of the realization is printed to the screen every 10 realizations.

So 1000 realizations takes on the order of 3000 seconds (on this machine), so around 50 minutes.

kaq = np.array(80 * [1, 3.16, 10, 31.6, 100])
ntot = 1000
aniso = np.zeros(ntot)

for i in range(ntot):
    k = generatek(kaq, nsections=1, seed=i)
    h0 = makemodel(k, k)
    kx = np.mean(k)
    kz = brentq(func, a=0.001 * kx, b=kx, args=(kx, h0))
    aniso[i] = kx / kz
    if i % 10 == 0:
        print(i, end=" ")
print("\n completed")
0
10
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[6], line 9
      7 h0 = makemodel(k, k)
      8 kx = np.mean(k)
----> 9 kz = brentq(func, a=0.001 * kx, b=kx, args=(kx, h0))
     10 aniso[i] = kx / kz
     11 if i % 10 == 0:

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/scipy/optimize/_zeros_py.py:846, in brentq(f, a, b, args, xtol, rtol, maxiter, full_output, disp)
    844     raise ValueError(f"rtol too small ({rtol:g} < {_rtol:g})")
    845 f = _wrap_nan_raise(f)
--> 846 r = _zeros._brentq(f, a, b, xtol, rtol, maxiter, args, full_output, disp)
    847 return results_c(full_output, r, "brentq")

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/scipy/optimize/_zeros_py.py:94, in _wrap_nan_raise.<locals>.f_raise(x, *args)
     93 def f_raise(x, *args):
---> 94     fx = f(x, *args)
     95     f_raise._function_calls += 1
     96     if np.isnan(fx):

Cell In[3], line 20, in func(kz, kx, h0, d, nlayers)
     18 def func(kz, kx, h0, d=4, nlayers=400):
     19     """Computes head difference."""
---> 20     hnew = makemodel(kx * np.ones(nlayers), kz * np.ones(nlayers), d, returnmodel=False)
     21     return hnew - h0

Cell In[3], line 12, in makemodel(kx, kz, d, returnmodel)
     10 tml.LineSink1D(ml, xls=0, sigls=2, layers=np.arange(int(d * 10)))
     11 tml.Constant(ml, xr=2000, yr=0, hr=0, layer=0)
---> 12 ml.solve(silent=True)
     13 if returnmodel:
     14     return ml

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/timml/model.py:433, in Model.solve(self, printmat, sendback, silent)
    431 """Compute solution."""
    432 # Initialize elements
--> 433 self.initialize()
    434 # Compute number of equations
    435 self.neq = np.sum([e.nunknowns for e in self.elementlist])

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/timml/model.py:72, in Model.initialize(self)
     69 def initialize(self):
     70     # remove inhomogeneity elements (they are added again)
     71     self.elementlist = [e for e in self.elementlist if not e.inhomelement]
---> 72     self.aq.initialize()
     73     for e in self.elementlist:
     74         e.initialize()

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/timml/aquifer.py:197, in Aquifer.initialize(self)
    195 def initialize(self):
    196     # cause we are going to call initialize for inhoms
--> 197     AquiferData.initialize(self)
    198     for inhom in self.inhomlist:
    199         inhom.initialize()

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/timml/aquifer.py:94, in AquiferData.initialize(self)
     92 dm1 = -1.0 / (self.c[1:] * self.T[:-1])
     93 A = np.diag(dm1, -1) + np.diag(d0, 0) + np.diag(dp1, 1)
---> 94 w, v = np.linalg.eig(A)
     95 # take the real part for the rare occasion that the eig
     96 # function returns a complex answer with very small
     97 # imaginary part
     98 w = w.real

File ~/checkouts/readthedocs.org/user_builds/timml/envs/stable/lib/python3.11/site-packages/numpy/linalg/_linalg.py:1523, in eig(a)
   1519 signature = 'D->DD' if isComplexType(t) else 'd->DD'
   1520 with errstate(call=_raise_linalgerror_eigenvalues_nonconvergence,
   1521               invalid='call', over='ignore', divide='ignore',
   1522               under='ignore'):
-> 1523     w, vt = _umath_linalg.eig(a, signature=signature)
   1525 if not isComplexType(t) and all(w.imag == 0.0):
   1526     w = w.real

KeyboardInterrupt: 

Create figure 3a

from scipy.stats import lognorm


def create_fig3a():
    plt.figure(figsize=(3, 3))
    plt.subplot(211)
    plt.hist(aniso, bins=np.arange(2, 20, 0.5), density=True)
    p5, p50, p95 = np.percentile(aniso, [5, 50, 95])
    # print('p5, p50, p95', p5, p50, p95)
    plt.axvline(p5, color="C1")
    plt.axvline(p95, color="C1")
    plt.axvline(p50, color="C2")
    kheq = np.mean(kaq)
    kveq = len(kaq) / np.sum(1 / kaq)
    anisoeq = kheq / kveq
    plt.axvline(anisoeq, color="k", linestyle=":", linewidth=1)
    #
    shape, loc, scale = lognorm.fit(aniso)
    # print('shape, loc, scale: ', shape, loc, scale)
    x = np.linspace(0, 20, 100)
    pdf1 = lognorm.pdf(x, shape, loc, scale)
    plt.plot(x, pdf1, "k--", lw=1)
    plt.xlim(0, 20)
    plt.ylim(0, 0.25)
    plt.xticks(np.arange(0, 21, 4))
    plt.xlabel(r"$\alpha_{eff}$ for $d=4$ m")
    plt.ylabel("pdf")
    plt.tight_layout()
# Only run if anisotropy has been computed
create_fig3a()
../_images/3a6cfaf8a5fef49ec72c16b02fe769c7c541bc23e7090c43bc2d9c42b553a52f.png