Tool for simulating 1D Conservative Transport (Laboratory Column)
Contents
7. Tool for simulating 1D Conservative Transport (Laboratory Column)#
Contributed by Ms. Anne Pförtner and Sophie Pförtner. The original concept from Prof. R. Liedl spreasheet code.
7.1. Info on the tool#
The worksheet addresses 1D conservative transport of a solute through a porous medium, e.g. in a laboratory column.
Water flow through the porous medium is assumed to be steady-state.
Advection and dispersion are considered as transport processes.
Dirac injection, finite pulse injection or continuous injection may be used as upgradient boundary condition.
The worksheet calculates solute breakthrough at the column outlet (sheet “model”) and allows for comparison with measured data (to be provided in sheet “data”).
Computations are based on analytical solutions involving the complementary error function.
input parameters |
dimension |
remarks |
---|---|---|
column length |
[L] |
enter positive number |
column radius |
[L] |
enter positive number |
effective porosity |
[-] |
enter positive number not bigger than 1 |
dispersivity |
[L] |
enter non-negative number |
flow rate |
[L³/T] |
enter positive number |
initial concentration |
[M/L³] |
enter non-negative number |
input mass |
[M] |
enter positive number or “inf” |
input concentration |
[M/L³] |
enter non-negative number or “inf” |
bulk density |
[M/L³] |
leave empty or enter positive number |
starting time |
[T] |
enter non-negative number |
This tool can also be downloaded and run locally. For that download the _1D_advection_dispersion.ipynb file from the book GitHub site, and execute the process in any editor (e.g., JUPYTER notebook, JUPYTER lab) that is able to read and execute this file-type.
The codes are licensed under CC by 4.0 (use anyways, but acknowledge the original work)
7.1.1. Python Libraries
Cell#
numpy
for computation, matplotlib.pyplot
for plotting and pandas
for tabulation, are most general libraries for our works.
ipywidget
- for interactive activities, are special functions used in this tool.
Please execute the cell before moving to the next step.
import math
import numpy as np
from ipywidgets import *
import matplotlib.pyplot as plt
7.1.2. Input Data
Cell#
In the cell below you can change input values. Pls. read the info above and the table before making the changes the input. Also check the boundary type after executing the cell.
Make sure to execute the cell if you change any input value
#input data
L = 50 #cm, column lenght
R = 3 #cm, column radius
ne = 0.25 #eff. porosity
alpha = 0.1 #dispersivity
Q = 0.167 #cm³/h, flow rate
c0 = 0 #mg/cm³, initital concentration
mi = 2000 #mg, input mass
ci = 1.25e+1 #mg/cm³, input concentration
delta_t = 70 #h, time increment
A = math.pi * R * R #cm², area-cross section
vf = Q/A #cm/h, darcy velocity
va = vf/ne #cm/h, average linear velocity
D = alpha * va #cm²/h, dispersion coeff.
Vp = L/va #pore volume
#intermediate Results
##boundary condition
if mi == math.inf:
print("The type of boundary condition is a continuous injection.")
elif ci == math.inf:
print("The type of boundary condition is a dirac pulse injection.")
else:
print("The type of boundary condition is a finite pulse injection.")
The type of boundary condition is a finite pulse injection.
7.1.3. The main function
cell#
You do not have to change the two cells below only have to execute only once.
#Definition of the function
def transport(L, R, ne, alpha, Q, c0, mi, ci, delta_t, t):
A = np.pi * R * R #cm², area-cross section
vf = Q/A #cm/h, darcy velocity
va = vf/ne #cm/h, average linear velocity
D = alpha * va #cm²/h, dispersion coeff.
Vp = L/va #pore volume
##peclet
if alpha == 0:
Pe = np.inf
else:
Pe = L/alpha
##input duration
if mi==np.inf or ci==c0:
ti=np.inf
else:
if ci==np.inf:
ti=0
else:
ti=mi/Q/abs(ci-c0)
Vp = L/va #pore volume
##rel. input duration
if ti == None or Vp == None:
ti_rel = None
else:
if ti == np.inf:
ti_rel = np.inf
else:
ti_rel=ti/Vp
if Vp == None:
delta_t_rel = None
else:
delta_t_rel = delta_t / Vp #rel. time increment
#rel. time
if Vp == None:
t_rel = None
else:
t_rel=t/Vp
##initial condition
###arg-
if Pe == None or t_rel == 0 or Pe == np.inf or t_rel == None:
arg_n1_IC=None
else:
arg_n1_IC=np.sqrt(0.25*Pe/t_rel)*(1-t_rel)
if Pe == None or arg_n1_IC == None or Pe == np.inf or t_rel == 0:
arg_n2_IC = None
else:
if arg_n1_IC > 0:
arg_n2_IC= 1-1*(1-math.erfc(min(abs(arg_n1_IC),27)))
elif arg_n1_IC < 0:
arg_n2_IC= 1+1*(1-math.erfc(min(abs(arg_n1_IC),27)))
else:
arg_n2_IC= 1-0*(1-math.erfc(min(abs(arg_n1_IC),27)))
###arg+
if Pe == None or t_rel == None or Pe == np.inf or t_rel == 0:
arg_p1_IC = None
else:
arg_p1_IC = np.sqrt(0.25*Pe/t_rel)*(1+t_rel)
if Pe == None or arg_p1_IC == None or Pe == np.inf or t_rel == 0:
arg_p2_IC = None
else:
if arg_p1_IC>0:
arg_p2_IC = 1-1*(1-math.erfc(min(abs(arg_p1_IC),27)))
elif arg_p1_IC<0:
arg_p2_IC = 1+1*(1-math.erfc(min(abs(arg_p1_IC),27)))
else:
arg_p2_IC = 1-0*(1-math.erfc(min(abs(arg_p1_IC),27)))
###arg_IC
if Pe == None or arg_n2_IC == None or arg_p2_IC == None or Pe == np.inf or t_rel == 0:
arg_IC = None
else:
if arg_p2_IC == 0:
arg_IC = arg_n2_IC
else:
arg_IC = arg_n2_IC + np.exp(Pe) * arg_p2_IC
##boundary condition
###positive pulse
####arg-
if Pe == np.inf or t_rel==0 or Pe == None or ti_rel == None or t_rel == None or ti_rel == 0:
arg_n1_BC_pp = None
else:
arg_n1_BC_pp = np.sqrt(0.25*Pe/t_rel)*(1-t_rel)
if Pe == None or Pe == np.inf or ti_rel == None or ti_rel == 0 or arg_n1_BC_pp == None or t_rel == 0:
arg_n2_BC_pp = None
else:
if arg_n1_BC_pp>0:
arg_n2_BC_pp = 1-1*(1-math.erfc(min(abs(arg_n1_BC_pp),27)))
elif arg_n1_BC_pp<0:
arg_n2_BC_pp = 1+1*(1-math.erfc(min(abs(arg_n1_BC_pp),27)))
else:
arg_n2_BC_pp = 1-1*(0-math.erfc(min(abs(arg_n1_BC_pp),27)))
####arg+
if Pe == None or Pe == np.inf or ti_rel == None or ti_rel == 0 or t_rel == None or t_rel ==0:
arg_p1_BC_pp = None
else:
arg_p1_BC_pp = np.sqrt(0.25*Pe/ t_rel)*(1+t_rel)
if Pe == None or Pe == np.inf or ti_rel == None or ti_rel == 0 or arg_n1_BC_pp == None or t_rel == 0:
arg_p2_BC_pp = None
else:
arg_p2_BC_pp = math.erfc(min(arg_p1_BC_pp,27))
####arg_BC_pp
if Pe==np.inf or t_rel==0 or Pe==None or ti_rel==None or t_rel==None or ti_rel==0 or arg_n2_BC_pp == None or arg_p2_BC_pp == None :
arg_BC_pp=None
else:
if arg_p2_BC_pp==0:
arg_BC_pp = arg_n2_BC_pp
else:
arg_BC_pp = arg_n2_BC_pp + (np.exp(Pe)*arg_p2_BC_pp)
###negative pulse
####arg-
arg_n1_BC_np=None
arg_n2_BC_np=None
if Pe==None or ti_rel==None or Pe==np.inf or ti_rel==np.inf or ti_rel == 0 or t_rel==0 or t_rel == None:
arg_n1_BC_np = None
else:
if t_rel>ti_rel:
arg_n1_BC_np = np.sqrt(0.25*Pe/(t_rel-ti_rel))*(1-(t_rel-ti_rel))
else:
arg_n1_BC_np = None
if Pe==None or ti_rel==None or arg_n1_BC_np == None or Pe== np.inf or ti_rel==0 or ti_rel == np.inf or t_rel == 0:
arg_n2_BC_np = None
else:
if arg_n1_BC_np > 0:
arg_n2_BC_np = 1-(1-math.erfc(min(abs(arg_n1_BC_np),27)))
elif arg_n1_BC_np < 0:
arg_n2_BC_np = 1+(1-math.erfc(min(abs(arg_n1_BC_np),27)))
else:
arg_n2_BC_np = 1-(0-math.erfc(min(abs(arg_n1_BC_np),27)))
####arg+
if Pe==np.inf or Pe==None or ti_rel==0 or ti_rel==np.inf or t_rel==0 or ti_rel==None or t_rel==None:
arg_p1_BC_np=None
else:
if t_rel>ti_rel:
arg_p1_BC_np=np.sqrt(0.25*Pe/(t_rel-ti_rel))*(1+(t_rel-ti_rel))
else:
arg_p1_BC_np = None
if Pe==None or ti_rel==None or arg_p1_BC_np == None or Pe == np.inf or ti_rel == np.inf or ti_rel == 0 or t_rel ==0:
arg_p2_BC_np = None
else:
if t_rel > ti_rel:
arg_p2_BC_np = math.erfc(min((arg_p1_BC_np),27))
else:
arg_p2_BC_np = None
####arg_BC_np
arg_BC_np = None
if Pe == None or ti_rel == None or arg_n2_BC_np == None or arg_p2_BC_np == None or Pe == np.inf or ti_rel == 0 or ti_rel == np.inf or t_rel == 0:
arg_BC_np = None
else:
if t_rel > ti_rel:
if arg_p2_BC_np == 0:
arg_BC_np = arg_n2_BC_np
else:
arg_BC_np = arg_n2_BC_np+ np.exp(Pe) * arg_p2_BC_np
else:
arg_BC_np = None
##rel conc due initial condition
if Pe == None or t_rel == None:
c_rel_IC = None
else:
if t_rel>0:
if Pe==np.inf:
if t_rel<1:
c_rel_IC = 1
else:
c_rel_IC = 0
else:
c_rel_IC = 1-0.5*arg_IC
else:
c_rel_IC = None
##rel conc due boundary condition
if Pe == None or ti_rel == None or t_rel == None:
c_rel_BC = None
else:
if t_rel > 0:
if Pe == np.inf:
if t_rel > 1:
if ti_rel == np.inf:
if t_rel>1:
c_rel_BC = 1
else:
c_rel_BC = 0
elif t_rel <= 1+ti_rel:
c_rel_BC = 1
else:
c_rel_BC = 0
else:
c_rel_BC = 0
else:
if ti_rel == 0:
c_rel_BC = np.sqrt(0.25/np.pi*Pe/t_rel^3)*np.exp(-0.25*Pe/t_rel*(1-t_rel)^2)
else:
if ti_rel==np.inf or t_rel<=ti_rel:
c_rel_BC = 0.5 * arg_BC_pp
else:
c_rel_BC = 0.5*(arg_BC_pp-arg_BC_np)
else:
c_rel_BC = None
if ti_rel == None or c_rel_IC == None or c_rel_BC == None:
c=None
else:
if t_rel==0:
c=c0
else:
if ti_rel == 0:
c=c0*c_rel_IC+mi/(ne*A*L)*c_rel_BC
else:
c=c0*c_rel_IC+ci*c_rel_BC
return c
7.1.4. The Interactive
cell#
Just execute it
def curve_data(L, R, ne, alpha, Q, c0, mi, ci, delta_t, t_max):
plot_c = []
plot_t = np.arange(0, t_max, delta_t)
for t in np.arange(0, t_max, delta_t):
plot_c.append(transport(L, R, ne, alpha, Q, c0, mi, ci, delta_t, t))
return plot_t, plot_c
def plot(L, R, ne, alpha, Q, c0, mi, ci, delta_t, t_max):
plot_t, plot_c = curve_data(L, R, ne, alpha, Q, c0, mi, ci, delta_t, t_max)
plt.plot(plot_t, plot_c)
plt.ylabel('concentration [mg/cm³]')
plt.ylim(-10,30)
plt.xlabel('Time [h]')
plt.xlim(-1, t_max)
plt.show
interact(plot,
L=widgets.FloatSlider(value=50, min=0, max=500, step=1, description='column lenght [cm]:', disabled=False),
R=widgets.FloatSlider(value=3, min=0, max=250, step=0.1, description='column radius [cm]:', disabled=False),
ne= widgets.FloatSlider(value=0.25,min=0, max=1,step=0.05, description='eff. porosity [-]:' , disabled=False),
alpha=widgets.FloatSlider(value=0.1, min=0, max=100, step=0.01, description='dispersivity [cm]:', disabled=False),
Q=widgets.FloatSlider(value=0.167, min=0, max=10, step=0.05, description='flow rate [cm³/h]:', disabled=False),
c0= widgets.FloatSlider(value=0,min=0, max=1000,step=0.5, description='initital concentration [mg/cm³]:', disabled=True),
mi=widgets.FloatSlider(value=2000, min=0, max=10000, step=10, description='input mass [mg]:', disabled=False),
ci=widgets.FloatSlider(value=12.5, min=0, max=1000, step=0.5, description='input concentration [mg/cm³]:', disabled=False),
delta_t= widgets.FloatSlider(value=70,min=0, max=100,step=0.5, description='time increment [h]:' , disabled=False),
t_max = widgets.FloatSlider(value=8400,min=0, max=10000,step=24, description='time [h]:' , disabled=False),
)
<function __main__.plot(L, R, ne, alpha, Q, c0, mi, ci, delta_t, t_max)>