Groundwater flow – Solving PDEs using ODE solvers¶

Source: incomplete course notes for MatLab solvers, Session 4.2, from the NNESMO website:
https://nnesmo2019.webspace.durham.ac.uk/practicals/session42/
Dr Simon A Mathias, Department of Earth Sciences, Durham University

Contents¶

  • Groundwater flow in a confined aquifer
  • Spatial discretisation using finite differences
  • Development of the solution code
  • Plotting the results and comparing to an analytical solution
  • Stiff and non-stiff problems
  • Specifying the Jacobian pattern

Groundwater flow in a confined aquifer¶

A confined aquifer is bounded to the West by a lake and to the East by an impermeable fault zone.
The aquifer is homogenous and isotropic and characterised by a transmissivity of 800 and a storativity of 0.01.
The edge of the lake lies parallel to the fault zone and is separated by a distance of 1600 m.
Initially the lake level is 50 m AOD (metres above ordinance datum).
After a significant episode of snow melt in the mountains above, the water level in the lake is suddenly raised to 53 m AOD.

The governing equation for one dimensional groundwater flow in a (un)confined aquifer takes the form

scipy: 1.9.1 numpy: 1.22.3
Out[4]:

ddxKdhdx=SdhdtddxKdhdx=Sdhdt

where S [-] is the aquifer storativity (which relates to the bulk compressibility of the aquifer),
h [L] is the hydraulic head, t [T] is time, L [L] is distance and
Q is q∗hq∗h [m^2/s] is the volumetric flow rate per unit breadth of confined aquifer, found from

where T is the transmissivity of the aquifer (which relates to the bulk permeability of the aquifer).

Discharge per unit width of aquifer = (PiezometricHeadUpstream−PiezometricHeadDownstreamEnd)/Length∗Coefficientpermeability∗AquiferThickness(PiezometricHeadUpstream−PiezometricHeadDownstreamEnd)/Length∗Coefficientpermeability∗AquiferThickness
q=(ho−h1)L⋅k⋅bq=(ho−h1)L⋅k⋅b

The relevant initial and boundary conditions can be written as follows:

t0t0= 0
txtx= 0.1, 0.3, 0.5, 1, 3, 5 in days
T= 800
S= 0.01
L= 1600
hIhI= h1h1= 50
h0h0= 53
Tdy2/dx2−0dy/dx=SuTdy2/dx2−0dy/dx=Su and u=dtu=dt

Volume of Confined Water for a change in head = SAΔhSAΔh where:

  • Volume = volume drained from a confined aquifer for a hydraulic head change, Δh, over an area, A (L³)

  • S = storativity (dimensionless)

  • A = area over which the head change occurs (L²)

  • Δh = change in head (L)

  • Discharge per unit width of aquifer - (Measured in M³ per Second)

    • Discharge per unit width of aquifer is the rate of total discharge in the channel to the width considered.
  • Transmissivity - (Measured in M² per Second)

    • Transmissivity is the rate at which groundwater flows horizontally through an aquifer

Use the solver 'odeint' to develop a one-dimensional finite difference model to estimate the hydraulic head within the aquifer.

Show the results as a plot of hydraulic head against distance, for the following times: 0.1, 0.3, 0.5, 1, 3, 5 and 7 days.

Spatial discretisation using finite differences¶

The above problem is an example of a partial differential equation (PDE). However, if we discretise the problem in space, the problem becomes a coupled set of ordinary differential equations (ODE) with respect to time. Here we will use odeint to solve the large set of coupled ODEs that results from the spatial discretisation. We will use finite difference for spatial discretisation. However, other methods such as finite elements and pseudospectral methods can also be used in a similar way.

Let us consider a set of discrete points on the x-axis: xnxn.

The corresponding set of hydraulic heads can be written as: h(x).

An approximation of flow per unit breadth, q∗q∗, can be found from qx=−kxdh/dxqx=−kxdh/dx

but note that these flow rates are defined at an alternative set of points: xbxb.

The resulting set of ODEs to be solved takes the form d/dxKdh/dx=S⋅dh/dtd/dxKdh/dx=S⋅dh/dt

Letting ∆x → 0 and ∆t → 0 in (5) leads to the one-dimensional groundwater flow equation, ∂h2/∂x2=1/k(∂h/∂t)−R(x,t)/K∂h2/∂x2=1/k(∂h/∂t)−R(x,t)/K
R=recharge, for 0<x<a, t>0, (6) where we have introduced the hydraulic diffusivity k via k=K/Ssk=K/Ss.

Recall that the relationship between and is T=KbT=Kb

Furthermore it can be shown that

h2=53mh2=53m
c1=(h22−h21)L+(N⋅L/K)c1=(h22−h12)L+(N⋅L/K)
c2=h21c2=h12
h2(x)=+h21−(h22−h21)xL+NxK(L−x)h2(x)=+h12−(h22−h12)xL+NxK(L−x)

Development of the solution code¶

Create a new script-file:

function onedimex()¶

  1. Solves the one-dimensional confined aquifer flow equation using the python ODE solver 'odeint', when using the function argument 'odeint'.

This older solver has some perks:

  • Solves a system of ordinary differential equations using lsoda from the FORTRAN library odepack.
  • Solves the initial value problem for both stiff and non-stiff systems of first order ode's.
  1. otherwise this function solves the one-dimensional confined aquifer flow equation using the python ODE solver 'solve_ivp' .
Out[46]:
20

So far all we have done is written some comments explaining what some of the subsequent variables are and defined the model parameter values.

The next step is to determine the locations on the x-axis that we are going to solve for.

In the above variable list we have x and xB. It is planned that x and xB will be vectors containing and , respectively.

Another point to note is that xB(1)=0xB(1)=0 and xB(N+1)=LxB(N+1)=L.

Add the following code to provide a discretisation for 20 equally spaced solution points

Out[25]:
array([  0.,   8.,  16.,  24.,  32.,  40.,  48.,  56.,  64.,  72.,  80.,
        88.,  96., 104., 112., 120., 128., 136., 144., 152., 160.])
Out[26]:
array([  4.,  12.,  20.,  28.,  36.,  44.,  52.,  60.,  68.,  76.,  84.,
        92., 100., 108., 116., 124., 132., 140., 148., 156.])

Next we will add some code to define the times of interest and a vector containing the initial values for .

solve_ivp¶

This is the solver which should be used for new code, according to scipy. We need to use solver/method 'LSODA', instead of the default 'RK45' for handling possible stiffness.

stand alone code for solver_ivp()¶

so the new solver can use the finite diff. method.

Out[25]:
(20,)

Of course we need to write the ODE function as well.

function MYodefun¶

This function calculates derivatives and contains also a function for the analytical solution.

Out[48]:
array([52.76103298, 52.80304448, 52.82861781, 52.8462694 , 52.85939305,
       52.86964332, 52.8779358 , 52.8848236 , 52.89066321, 52.89569609,
       52.90009236, 52.90397583, 52.90743901, 52.91055264, 52.9133718 ,
       52.91594018, 52.91829287, 52.92045847, 52.92246053, 52.92431864])
Out[49]:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
0 52.761033 52.803044 52.828618 52.846269 52.859393 52.869643 52.877936 52.884824 52.890663 52.895696 52.900092 52.903976 52.907439 52.910553 52.913372 52.915940 52.918293 52.920458 52.922461 52.924319
1 52.292531 52.414431 52.489350 52.541334 52.580113 52.610472 52.635074 52.655535 52.672900 52.687879 52.700972 52.712545 52.722870 52.732157 52.740569 52.748235 52.755259 52.761726 52.767707 52.773258
2 51.851225 52.041288 52.160358 52.243853 52.306556 52.355871 52.395972 52.429410 52.457846 52.482416 52.503922 52.522953 52.539949 52.555249 52.569118 52.581766 52.593361 52.604042 52.613924 52.623101
3 51.451782 51.692572 51.847807 51.958393 52.042277 52.108711 52.163007 52.208459 52.247233 52.280818 52.310276 52.336388 52.359743 52.380795 52.399899 52.417337 52.433338 52.448090 52.461747 52.474438
4 51.104361 51.375391 51.556901 51.688950 51.790461 51.871603 51.938372 51.994556 52.042683 52.084508 52.121295 52.153979 52.183271 52.209718 52.233754 52.255723 52.275905 52.294530 52.311789 52.327840
Polynomial function, f(x):
           2
4.13e-10 x
Derivative, f(x)'=  
8.261e-10 x
When x=5  f(x)'= 6.608623263102572e-07
Out[112]:
array([ 1.        ,  1.47368421,  1.94736842,  2.42105263,  2.89473684,
        3.36842105,  3.84210526,  4.31578947,  4.78947368,  5.26315789,
        5.73684211,  6.21052632,  6.68421053,  7.15789474,  7.63157895,
        8.10526316,  8.57894737,  9.05263158,  9.52631579, 10.        ])
Out[114]:
array([  40.,  120.,  200.,  280.,  360.,  440.,  520.,  600.,  680.,
        760.,  840.,  920., 1000., 1080., 1160., 1240., 1320., 1400.,
       1480.])
[  40.  120.  200.  280.  360.  440.  520.  600.  680.  760.  840.  920.
 1000. 1080. 1160. 1240. 1320. 1400. 1480. 1560.] [52.2554889  52.69707656 52.77634693 52.81457874 52.83816115 52.85455826
 52.86680633 52.87640383 52.88418678 52.89066321 52.89616202 52.90090674
 52.90505534 52.90872303 52.91199607 52.91494051 52.9176079  52.92003912
 52.92226709 52.92431864] (20, 20)
Out[16]:
dtype('float64')

Analytical solution¶

3D surface subplots¶

[ 40. 120.] [0.1        0.62105263] (20, 20) [[52.2554889  52.69707656 52.77634693 52.81457874 52.83816115 52.85455826
  52.86680633 52.87640383 52.88418678 52.89066321 52.89616202 52.90090674
  52.90505534 52.90872303 52.91199607 52.91494051 52.9176079  52.92003912
  52.92226709 52.92431864]
 [51.02834513 52.11032801 52.33678319 52.44816017 52.51742924 52.56581472
  52.60206354 52.63052611 52.65364226 52.67290024 52.68926622 52.70339836
  52.71576259 52.72669922 52.73646338 52.74525063 52.75321372 52.76047389
  52.76712884 52.77325824]]
53.0 80.0
Out[25]:
120.0
[ 0.1         0.62105263  1.14210526  1.66315789  2.18421053  2.70526316
  3.22631579  3.74736842  4.26842105  4.78947368  5.31052632  5.83157895
  6.35263158  6.87368421  7.39473684  7.91578947  8.43684211  8.95789474
  9.47894737 10.        ] [40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40. 40.
 40. 40.]
Out[63]:
array([ 0., 80.])
meshgrid test¶
Out[125]:
(array([[1, 2, 3],
        [1, 2, 3],
        [1, 2, 3]]),
 array([[10, 10, 10],
        [20, 20, 20],
        [30, 30, 30]]))
difH_ [40 50]
difxB_ [-10 -28]
Out[176]:
array([-4.        , -1.78571429])

Most of the above code in the ODE function is self-explanatory. However, pay close attention to how the boundary conditions are applied.

The fixed head boundary, associated with the lake, is applied by concatenating the boundary head, h0, and its associated location, xB(1), to the h and x vectors, respectively, prior to finding the derivatives, .

The zero flux boundary, associated with the fault zone, is applied by simply concatenating a zero to the Q vector.

Plotting the results and comparing to an analytical solution¶

Whenever developing numerical models it is always important to study your results and try to compare these to analytical solutions where possible.

The problem being solved here has an analytical solution for the special case where . Our finite difference model should produce very similar results to the analytical solution until the pressure wave hits the bounday at .

The analytical solution takes the form

Add the following subfunction to your script-file, which contains an implementation of the analytical solution.

Animation of hydraulic heads over time¶

Out[164]:
19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
19 50.652405 50.615754 50.577671 50.538120 50.497082 50.454564 50.410614 50.365339 50.318934 50.271725 50.224222 50.177202 50.131821 50.089735 50.053198 50.024955 50.007481 50.000789 50.000002 50.0
18 50.725951 50.688357 50.649121 50.608170 50.565442 50.520891 50.474504 50.426316 50.376437 50.325096 50.272702 50.219934 50.167882 50.118221 50.073431 50.036887 50.012352 50.001608 50.000008 50.0
17 50.805145 50.766853 50.726721 50.684639 50.640497 50.594196 50.545653 50.494822 50.441715 50.386442 50.329276 50.270749 50.211807 50.154043 50.100021 50.053623 50.019946 50.003170 50.000027 50.0
16 50.890079 50.851363 50.810626 50.767720 50.722489 50.674775 50.624426 50.571305 50.515313 50.456425 50.394747 50.330612 50.264745 50.198534 50.134453 50.076669 50.031504 50.006048 50.000085 50.0
15 50.980805 50.941964 50.900944 50.857558 50.811607 50.762874 50.711133 50.656156 50.597725 50.535664 50.469884 50.400476 50.327868 50.253112 50.178385 50.107832 50.048679 50.011169 50.000251 50.0
Out[176]:
Out[8]:
<IPython.core.display.Image object>
Out[177]:
19    50.652405
18    50.615754
17    50.577671
16    50.538120
15    50.497082
14    50.454564
13    50.410614
12    50.365339
11    50.318934
10    50.271725
9     50.224222
8     50.177202
7     50.131821
6     50.089735
5     50.053198
4     50.024955
3     50.007481
2     50.000789
1     50.000002
0     50.000000
Name: 19, dtype: float64
Out[133]:
array([50.65240468, 50.615754  , 50.57767139, 50.53812045, 50.49708209,
       50.45456389, 50.41061358, 50.36533864, 50.3189345 , 50.27172522,
       50.22422158, 50.1772022 , 50.13182137, 50.08973532, 50.05319785,
       50.02495515, 50.00748092, 50.00078882, 50.00000224, 50.        ])
[  40.  120.  200.  280.  360.  440.  520.  600.  680.  760.  840.  920.
 1000. 1080. 1160. 1240. 1320. 1400. 1480. 1560.] 20 [40.0, 120.0, 200.0, 280.0, 360.0, 440.0, 520.0, 600.0, 680.0, 760.0, 840.0, 920.0, 1000.0, 1080.0, 1160.0, 1240.0, 1320.0, 1400.0, 1480.0, 1560.0]
Out[18]:
(20, 20)
Out[62]:
array([   50.   ,  2660.873,  5111.034,  7400.485,  9529.224, 11497.253,
       13304.571, 14951.177, 16437.073, 17762.258, 18926.731, 19930.494,
       20773.546, 21455.886, 21977.516, 22338.435, 22538.643, 22578.139,
       22456.925, 22175.   ])
Out[176]:
array([   1.6       ,    2.30151982,    3.31062093,    4.76216231,
          6.85013184,    9.85357138,   14.17386865,   20.38839977,
         29.32769137,   42.18641438,   60.68304305,   87.2895165 ,
        125.56159526,  180.61406267,  259.80427827,  373.71543505,
        537.57092581,  773.26883817, 1112.30847388, 1600.        ])
(20, 20) (20,) (21,)
Out[34]:
(20, 20)
Out[39]:
array([[ 1.        ,  1.47368421,  1.94736842,  2.42105263,  2.89473684,
         3.36842105,  3.84210526,  4.31578947,  4.78947368,  5.26315789,
         5.73684211,  6.21052632,  6.68421053,  7.15789474,  7.63157895,
         8.10526316,  8.57894737,  9.05263158,  9.52631579, 10.        ],
       [ 1.        ,  1.47368421,  1.94736842,  2.42105263,  2.89473684,
         3.36842105,  3.84210526,  4.31578947,  4.78947368,  5.26315789,
         5.73684211,  6.21052632,  6.68421053,  7.15789474,  7.63157895,
         8.10526316,  8.57894737,  9.05263158,  9.52631579, 10.        ]])

DataFrame composition¶

20 21 20 20
Out[29]:
x xB tt5 tt10 hFD Q dhdt t_day
1 40.0 0.0 0.100000 0.100000 50.000000 739.413143 616.177620 0.000001
2 120.0 80.0 0.621053 0.621053 141.426454 246.471048 123.235524 0.000007
3 200.0 160.0 1.142105 1.142105 231.947922 147.882629 52.815225 0.000013
4 280.0 240.0 1.663158 1.663158 321.564404 105.630449 29.341791 0.000019
5 360.0 320.0 2.184211 2.184211 410.275900 82.157016 18.672049 0.000025
Out[36]:
tt5 tt10 tt15 dhdt x
1 0.100000 0.100000 0.100000 616.177620 40.0
2 0.621053 0.621053 0.621053 123.235524 120.0
3 1.142105 1.142105 1.142105 52.815225 200.0
4 1.663158 1.663158 1.663158 29.341791 280.0
5 2.184211 2.184211 2.184211 18.672049 360.0
Out[66]:
array([  40.,  120.,  200.,  280.,  360.,  440.,  520.,  600.,  680.,
        760.,  840.,  920., 1000., 1080., 1160., 1240., 1320., 1400.,
       1480., 1560.])
Out[141]:
array([  50.        ,  131.73130194,  212.71468144,  292.9501385 ,
        372.43767313,  451.17728532,  529.16897507,  606.41274238,
        682.90858726,  758.6565097 ,  833.6565097 ,  907.90858726,
        981.41274238, 1054.16897507, 1126.17728532, 1197.43767313,
       1267.9501385 , 1337.71468144, 1406.73130194, 1475.        ])

Step changes in surface water level: Edelman formula's¶

basic plot head and Qx vs x¶

basic plot head and Qx vs time¶


Stiff and non-stiff problems¶

At the moment, we are solving the diffusion problem using a uniform space-step. Just as we can benefit from having time-step refinement during periods of high activity, we can also benefit from spatial grid refinement in areas of high activity.

In this particular case, most of the activity is occurring around where the fixed head boundary exists. Therefore a more appropriate spatial discretisation is arguably achieved using the code:

xB=[0,np.logspace(−3,0,N)∗L]xB=[0,np.logspace(−3,0,N)∗L]

which logarithmically spaces the finite different points over three orders of magnitude, with the smallest grid spacing around h(53)h(53).

Add the above code for xBxB to the existing code and run.

You will find the solution takes forever to complete. The reason is that the simulation has become very stiff. Type "ctrl c" in the command window to terminate the simulation.

Sets of coupled ODEs are said to be stiff when the ODEs move at very different rates. By refining the spatial grid, we have created a system whereby the ODEs near are very fast whilst the ODEs near are very slow.

The ode45 solver is not good for stiff problems. Instead, try and use the solver ode15s by changing the code to say

You can read more about how ode15s works in the help files.

Specifying the Jacobian pattern¶

For large sets of ODEs, the stiff solvers are much more efficient if you specify the Jacobian pattern a priori. So what is the Jacobian pattern?

For the example under consideration, let us define

Now consider the vectors and .

Therefore, the Jacobian pattern for this problem can be seen to be a tri-diagonal sparse matrix of the form:

To implement this Jacobian pattern within your code, replace the line

S = spdiags(Bin,d,m,n) creates an m-by-n sparse matrix S by taking the columns of Bin and placing them along the diagonals specified by d. spdiags(data, diags, m, n, format)

Out[2]:
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
Out[5]:
array([[-1,  0,  0],
       [ 0,  0,  0],
       [ 0,  0,  1]])
Out[3]:
array([[ 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.],
       [ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,
         1.,  1.,  1.,  1.,  1.,  1.,  1.]])
Out[4]:
array([[-2.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 1., -2.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  1., -2.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  1., -2.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  1., -2.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  1., -2.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  1., -2.,  1.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  1., -2.,  1.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1., -2.,  1.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1., -2.,  1.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1., -2.,  1.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1., -2.,  1.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1., -2.,
         1.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
        -2.,  1.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         1., -2.,  1.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  1., -2.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  1., -2.,  1.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  1., -2.,  1.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  1., -2.,  1.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  1., -2.]])
Out[5]:
(20, 20)
Out[6]:
<400x400 sparse matrix of type '<class 'numpy.float64'>'
	with 1920 stored elements in Compressed Sparse Row format>