FEniCS Implementation for RII problem

7 minute read

Published:

Code up the RII equations in FEniCS

FEniCS Implementation for RII problem

FEniCS is an open-source software platform for solving partial differential equations (PDEs) using the finite element method (FEM). The primary programming language for using FEniCS is python, which is quite simple to use.

Therefore, based on the mathematical equation that describes the RII problem in the previous blog, we could code up the PDEs with FEniCS and python.

First, the FEniCS is imported.

Note: Some older tutorials might import everything the from Fenics package (e.g., from fenics import*), which is not a good practice. It is preferable to list any commands you might use and to be fully aware of their usage in your code.

from fenics import DirichletBC, Function, UserExpression, RectangleMesh, FunctionSpace, Point, Constant, project, DOLFIN_EPS, File, FacetNormal,TrialFunction, TestFunction, dx, grad, dot, Dx, inner, ds, plot, solve
import numpy as np
import matplotlib.pyplot as plt
import scipy.optimize as opt

The plotting tool and the scipy and numpy packages are also imported.

Then, create a mesh covering the rectangle square. We will let the mesh consist of 64 \(\times\) 64 squares and define function space.

mesh = RectangleMesh(Point(0, 0), Point(2.0* np.pi/10, 1), 64, 64)
V = FunctionSpace(mesh,"CG", 1)
nn = FacetNormal(mesh)       # n vector is the facet normal
z = Constant((0,1))     # unit vector z

Initial porosity is defined by linear stability analysis. (Shown in previous blog)

class Phi_Initial(UserExpression):
    def __init__(self, amplitude, A, m, w, **kwargs):
        super().__init__(**kwargs)
        self.amplitude = amplitude
        self.A = A
        self.m = m
        self.w = w

    def eval(self, values, xx):
        x = xx[0]
        y = xx[1]
        P = np.exp(1.0j * self.w * x) * sum(
            [AA * np.exp(mm * y) for AA, mm in zip(self.A, self.m)]
        )
        values[0] = 1.0 - self.amplitude * P.real

    def value_shape(self):
        return ()

Define values of parameters that we need to use

Da = 100                    # Damkohler number Da=100
Pe = 100                    # Peclet number Pe=100
b = 1e-6                    # Solubility gradient \beta
H = 1e4                     # Melting region depth H
M = b * H                   # Solubility M=0.01
d = 1e5                     # Compaction length \delta=10^5
S = M * pow(d,2) / pow(H,2) # Stiffness S=1
n = 3                       # n =2/3
w = 10                      # horizontal wavenumber w=10
K = 1 + pow(w,2) / (Da * Pe)

# define simulation time
t = 0.0
T = 0.1         # total simulation time
t_num = 200      # number of time steps
dt = T /t_num         # time stepping

Solve A, m for initial porosity condition

# Define root finding
def root_m(sigma, Da, K, n, S, w):

    coeff = [sigma / Da,
             sigma * K - n / (Da * S),
             - n * K / S - sigma / Da * pow(w, 2),
             (n - sigma * K) * pow(w, 2)]  # set up the companion matrix for eq 3.7

    m = np.roots(coeff)  # Solve all the roots m
    m1, m2, m3 = np.array_split(m, 3)  # define m1 m2 m3
    m1 = complex(m1)  # define data type of m for M_array
    m2 = complex(m2)
    m3 = complex(m3)

    return m1, m2, m3
    
# Define matrix M
def matrix_M(m1, m2, m3):
    M_array = np.array([[1, 1, 1],
                        [m1, m2, m3],
                        [m1 * np.exp(m1), m2 * np.exp(m2), m3 * np.exp(m3)]], dtype='complex')
    return M_array
   
# Solve the detM=0
def determinant(sigma):

    m1,m2,m3 = root_m(sigma, Da, K, n, S, w)
    M_array = matrix_M(m1, m2, m3)
    det = np.linalg.det(M_array)  # Calculate det(M)

    return det.imag

sigma = opt.root_scalar(determinant, bracket=[2.5, 2.9])  # find sigma
sigma = sigma.root

m1, m2, m3 = root_m(sigma, Da, K, n, S, w)
M_array = matrix_M(m1, m2, m3)

# With sigma and mj, solve Aj
def prefactor(M_array):

    u, s, vh = np.linalg.svd(M_array)
    A = np.conj(vh[2, :])
    A1 = A[0]
    A2 = A[1]
    A3 = A[2]

    return A1, A2, A3

A = prefactor(M_array)
m = root_m(sigma, Da, K, n, S, w)

# define initial value of \phi
phi_i = Phi_Initial(0.01, A, m, w, degree=2)
h_n = project(phi_i, V)
#plot(h_n, mesh=mesh)
#plt.show()

Define boundary conditions with

\[\phi =1, \chi =1, {\partial P\over \partial z} =0, (z =0)\\ {\partial P\over \partial z} =0, (z =1)\]
# define boundary condition
def on_bottom(x, on_boundary):
    return x[1] <= DOLFIN_EPS and on_boundary

C_bottom = 1.0   # \chi =1
bc_C = DirichletBC(V, C_bottom, on_bottom)

h_bottom = 1.0   # \phi =1
bc_h = DirichletBC(V, h_bottom, on_bottom)

bcs = [bc_C, bc_h]

Define buoyancy-driven compacting problem

\[\int_\Omega MP P_1 + \int_\Omega KS \nabla P_1 \cdot \nabla P = \int_\Omega K {\partial P_1 \over \partial z} -\int_\Gamma P_1 K(\widehat z\cdot n)\]

In FEniCS, the variational formulation is defined by trial and test functions. The trial function is a function that approximates the solution to the PDE, and the test function is a function that is used to integrate the weak form of the PDE over the domain.

def compaction(V, h_n, n, M, S, nn, z):
    # Test and Trial function
    P_n = TrialFunction(V)
    P_test = TestFunction(V)
    k_n = pow(h_n, n)

    # define linear and bilinear form
    aP = (M * P_n * P_test + k_n * S * inner(grad(P_n), grad(P_test))) * dx
    LP = k_n * Dx(P_test, 1) * dx - k_n * P_test * dot(z, nn) * ds

    return aP, LP

Define reaction-infiltration problem

\[\int_\Omega q \cdot {\nabla \chi \over Da} C_1 + \int_\Omega{1 \over DaPe} [\phi {\partial \chi \over \partial x} {\partial C_1 \over \partial x} -{\partial \phi \over \partial x}{\partial \chi \over \partial x} C_1]+\int_\Omega \chi C_1 = \int_\Omega q \cdot \hat z C_1\]
def reaction(V, Da, Pe, n, h_n, P_n, z, S):

    C_n = TrialFunction(V)  # Trial function undersaturation \chi
    C_test = TestFunction(V)  # test function
    q_n = pow(h_n, n) * (z - S * grad(P_n))

    # Define bilinear and linear forms
    aC = ((1 / Da) * inner(q_n, grad(C_n)) * C_test + 1 / (Da * Pe) * (h_n * Dx(C_n, 0) * Dx(C_test,0) - Dx(h_n, 0) * Dx(C_n, 0) * C_test) + C_n * C_test) * dx
    LC = dot(q_n, z) * C_test * dx

    return aC, LC

Define Time-dependent infiltration problem

\[a(\phi,R)=\int_\Omega \phi R dx \\ L_n(R) = \int_\Omega [\phi^n +(P+\chi)^n\Delta t] R dx\]
def time(V, h_n, P_n, C_n, dt):

    h = TrialFunction(V)  # porosity \phi
    h_test = TestFunction(V)  # test function

    ah = h * h_test * dx
    Lh = (h_n + (P_n + C_n) * dt) * h_test * dx

    return ah, Lh
P_n = Function(V)
C_n = Function(V)
h = Function(V)

# define variation form
aP, LP = compaction(V, h_n, n, M, S, nn, z)
aC, LC = reaction(V, Da, Pe, n, h_n, P_n, z, S)
ah, Lh = time(V, h_n, P_n, C_n, dt)

Define output file

# Output file
plt.figure()

Pfile = File("result/Pressure.pvd")
Cfile = File("result/undersaturation.pvd")
hfile = File("result/porosity.pvd")

Define time-stepping main loop

h = h_n

# main loop, Time-stepping
for n in range(t_num):

    # Solve
	solve(aP == LP, P_n)
	solve(aC == LC, C_n, bc_C)
	solve(ah == Lh, h)

    # save file
    Pfile << P_n
    Cfile << C_n
    hfile << h

    # update current time
    t += dt# 

    h_n.assign(h)