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
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
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 [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 =
The relevant initial and boundary conditions can be written as follows:
= 0
= 0.1, 0.3, 0.5, 1, 3, 5 in days
T= 800
S= 0.01
L= 1600
= = 50
= 53
and
Volume of Confined Water for a change in head = 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)
Transmissivity - (Measured in M² per Second)
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.
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: .
The corresponding set of hydraulic heads can be written as: h(x).
An approximation of flow per unit breadth, , can be found from
but note that these flow rates are defined at an alternative set of points: .
The resulting set of ODEs to be solved takes the form
Letting ∆x → 0 and ∆t → 0 in (5) leads to the one-dimensional groundwater flow equation,
R=recharge, for 0<x<a, t>0, (6) where we have introduced the hydraulic diffusivity k via .
Recall that the relationship between and is
Furthermore it can be shown that
Create a new script-file:
This older solver has some perks:
lsoda
from the FORTRAN library odepack.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 and .
Add the following code to provide a discretisation for 20 equally spaced solution points
array([ 0., 8., 16., 24., 32., 40., 48., 56., 64., 72., 80., 88., 96., 104., 112., 120., 128., 136., 144., 152., 160.])
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 .
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.
so the new solver can use the finite diff. method.
(20,)
Of course we need to write the ODE function as well.
This function calculates derivatives and contains also a function for the analytical solution.
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])
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
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. ])
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)
dtype('float64')
[ 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
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.]
array([ 0., 80.])
(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]
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.
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.
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 |
<IPython.core.display.Image object>
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
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]
(20, 20)
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. ])
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,)
(20, 20)
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. ]])
20 21 20 20
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 |
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 |
array([ 40., 120., 200., 280., 360., 440., 520., 600., 680., 760., 840., 920., 1000., 1080., 1160., 1240., 1320., 1400., 1480., 1560.])
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. ])
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:
which logarithmically spaces the finite different points over three orders of magnitude, with the smallest grid spacing around .
Add the above code for 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.
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)
array([[1., 1., 1.], [1., 1., 1.], [1., 1., 1.], [1., 1., 1.]])
array([[-1, 0, 0], [ 0, 0, 0], [ 0, 0, 1]])
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.]])
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.]])
(20, 20)
<400x400 sparse matrix of type '<class 'numpy.float64'>' with 1920 stored elements in Compressed Sparse Row format>