时变动力学问题之FEniCS求解

FEniCS系列第五讲

【本文仅是视频中Markdown文件内容,不包括视频中讲解】

如果你希望和我一样Win10上安装FEniCS,建议采用Win10 + WSL2 + Ubuntu。

时变动力学问题之FEniCS求解

FEniCS系列讲座

  • 第1讲 有限元法求解偏微分方程之FEniCS入门讲解
  • 第2讲 混合形式泊松方程之FEniCS求解
  • 第3讲 非线性变分问题之FEniCS求解
  • 第4讲 从变分原理到变分方程之FEniCS求解【上一讲】

本讲要点

  • 瞬态变分方程
  • 广义α方法(the generalized-α method)。
  • 时间依赖的表达式定义
  • 如何区分不同的边界并标记之
  • 需要反复迭代时,使用solve函数效率低下,解决方法。

动力学方程瞬间的变分方程

弹性动力学方程

\[ \nabla\cdot\textcolor{red}{\sigma}+\rho b = \rho \ddot{u} \\ \quad \\ \sigma = \lambda\mathrm{tr}(\epsilon)I+2\mu\epsilon \\ \epsilon=\frac{\nabla u+(\nabla u)^T}{2} \\ \lambda=\frac{E\nu}{(1+\nu)(1-2\nu)},\mu=\frac{E}{2(1+\nu)} \]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 弹性参数
E = 1000.0
nu = 0.3
mu = Constant(E / (2.0*(1.0 + nu)))
lmbda = Constant(E*nu / ((1.0 + nu)*(1.0 - 2.0*nu)))
# 质量密度
rho = Constant(1.0)

# ε
def epsilon(u_):
return sym(grad(u_))
# 应力张量σ
def sigma(u_):
return 2.0*mu*epsilon(u_) + lmbda*tr(epsilon(u_))*Identity(len(u_))

在任意“冻结“的瞬间,弹性动力学方程,可以按以前的套路,改写成变分方程(待求量在左边,已知量在右边)

\[ \int_\Omega\rho\ddot{u}\cdot v dx-\int_\Omega\textcolor{red}{(\nabla\cdot\sigma)\cdot v} dx=\int_\Omega\rho b\cdot v dx \]

\[ (\nabla\cdot\sigma)\cdot v=\frac{\partial \sigma_{ij}}{\partial x_i}v_j=\frac{\partial}{\partial x_i}(\sigma_{ij}v_j)-\textcolor{red}{\sigma_{ij}\frac{\partial v_j}{\partial x_i}}=\nabla \cdot(\sigma\cdot v)-\sigma \cdot \nabla v \]

\[ \sigma \cdot \nabla v=\sigma \cdot (\nabla v)^T=\sigma(u) \cdot \textcolor{red}{\epsilon(v)} \]

\[ \int_\Omega\rho\ddot{u}\cdot v dx+\int_\Omega\sigma(u) \cdot \epsilon(v) dx=\int_\Omega\rho b\cdot v dx+\int_{\partial\Omega}(\textcolor{red}{\sigma\cdot n})\cdot v ds \]

\[ \sigma\cdot n|_{\partial\Omega}=p \]

为了后面编程方便,不妨引入几个记号(函数):

\[ m(u,v)=\int_\Omega\rho u\cdot v dx\\ k(u,v)=\int_\Omega\sigma(u) \cdot \epsilon(v) dx\\ f(v)=\int_\Omega\rho b\cdot v dx+\int_{\partial\Omega}\textcolor{red}{p}\cdot v ds \]

于是变分方程可以简单写成:

\[ m(\ddot{u},v)+k(u,v)=f(v) \]

必要的话,我们可能还需要加一个耗散项\(\textcolor{red}{c(\dot{u},v)}\)

\[ c(u,v)=\eta_m m(u,v)+\eta_k k(u,v) \]

最后有

\[ \boxed{m(\ddot{u},v)+c(\dot{u},v)+k(u,v)=f(v)} \]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 质量
def m(u_,v_):
return rho*inner(u_, v_)*dx
# 刚度
def k(u_,v_):
return inner(sigma(u_), epsilon(v_))*dx
# 耗散(姑且先设为0,要求非负)
eta_m = Constant(0.)
eta_k = Constant(0.)
def c(u_,v_):
return eta_m*m(u_,v_) + eta_k*k(u_,v_)
# 外力
def f(v_):
return rho*dot(b,v_)*dx+dot(p,v_)*ds

特定瞬间的应变,速度和加速度

已知上时刻的应变\(u_0\),速度\(v_0\)和加速度\(a_0\),可以根据前面的瞬态变分方程求解出当前时刻的应变\(u\)

进而可以通过有限差分法近似算出当前时刻的对应值。

为了更精确计算,不妨将应变\(u\)用泰勒级数展开,然后保留头3项:

\[ u=u_0+(\Delta t) v_0+\frac{1}{2}(\Delta t)^2 \textcolor{red}{a} \]

根据这个式,可估算出当前时刻加速度

\[ a=\frac{2}{(\Delta t)^2}[u-u_0-(\Delta t)v_0] \]

类似地,速度\(v\)用泰勒级数展开,保留头2项:

\[ v=v_0+(\Delta t)\textcolor{red}{a} \]

当前时刻应变(求变分方程的解),加速度(如前估算),速度(如前估算),这3个量都有了。

为了数值稳定性,\(u\)的展开式中的\(a\)用“平均”值替代

\[ a\leftarrow (1-2\beta)a_0+2\beta \textcolor{red}{a} \]

得到当前时刻新的加速度估计式

\[ \boxed{a=\frac{1}{\beta(\Delta t)^2}[u-u_0-(\Delta t)v_0]-\frac{1-2\beta}{2\beta}a_0} \]

对速度\(v\)估计式中的加速度,进一步用“平均”值替代

\[ a\leftarrow (1-\gamma)a_0+\gamma a \]

得到当前时刻新的速度估计式:

\[ \boxed{v=v_0+(\Delta t)[(1-\gamma)a_0+\gamma a]} \]

这两个估计式,就是我们最终要的速度和加速度更新式,写成函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 加速度更新估计式
def update_a(u_, u_old_, v_old_, a_old_, ufl=True):
if ufl:
dt_ = dt
beta_ = beta
else:
dt_ = float(dt)
beta_ = float(beta)
return (u_-u_old_-dt_*v_old_)/beta_/dt_**2 - (1-2*beta_)/2/beta_*a_old_

# 速度更新估计式
def update_v(a_, u_old_, v_old_, a_old_, ufl=True):
if ufl:
dt_ = dt
gamma_ = gamma
else:
dt_ = float(dt)
gamma_ = float(gamma)
return v_old_ + dt_*((1-gamma_)*a_old_ + gamma_*a_)

# 每步求解后,对应变,速度和加速度更新
def update_fields(u_, u_old_, v_old_, a_old_):
# 得到矢量引用
u_vec, u0_vec = u_.vector(), u_old_.vector()
v0_vec, a0_vec = v_old_.vector(), a_old_.vector()
# 得到应变,速度和加速度要更新的矢量
a_vec = update_a(u_vec, u0_vec, v0_vec, a0_vec, ufl=False)
v_vec = update_v(a_vec, u0_vec, v0_vec, a0_vec, ufl=False)
# 实际更新 (u_old <- u)
v_old_.vector()[:], a_old_.vector()[:] = v_vec, a_vec
u_old_.vector()[:] = u_.vector()

对于要进入变分方程的\(\ddot{u},\dot{u},u\)也要用某种形式的用“平均”值替代:

\[ a\leftarrow (1-\alpha_m)a+\alpha_m a_0\\ v\leftarrow (1-\alpha_f)v+\alpha_f v_0 \\ u\leftarrow (1-\alpha_f)u+\alpha_f u_0 \]

写成代码为:

1
2
3
4
# 将出现在变分方程中的“平均”值替代
def avg(x_old, x_new, alpha):
return (1-alpha)*x_new + alpha*x_old

这个方法就是著名的广义α方法(the generalized-α method)。

注意,这个方法有4个待选定的参数:\(\alpha_m,\alpha_f,\beta,\gamma\)

一个可以保证确保无条件稳定性的热门选择是:

\[ \alpha_m,\alpha_f\le 1\\ \gamma=\frac{1}{2}+\alpha_m-\alpha_f\\ \beta=\frac{1}{4}\left(\gamma+\frac{1}{2}\right)^2 \]

1
2
3
4
5
6
# 生成α方法的参数
alpha_m = Constant(0.2)
alpha_f = Constant(0.4)
gamma = Constant(0.5+alpha_f-alpha_m)
beta = Constant((gamma+0.5)**2/4.)

输入函数,域,边界条件,初始条件

  • \(\Omega=[0,1]\times[0,0.1]\times[0,0.04]\) 【一个长条形】
  • \(t\in[0,T]\) 【演示时长T=8s】
  • 初始时刻:无应变,无速度,无加速度
  • 任意时刻,左侧面\(0\times[0,0.1]\times[0,0.04]\),固定无应变
  • 左侧面则是\(1\times[0,0.1]\times[0,0.04]\)
  • \([0,T/10]\)时间段,右侧面受到\(y\)轴方向的线性增长的面元牵引力。
  • \([T/10,T]\)时间段,右侧面不受到任何面元牵引力。
  • \(b=0\) 【不受体元外力】

FEniCS代码实现

第一步:创建网格定义函数空间

1
2
3
4
5
6
from dolfin import *
import numpy as np

mesh = BoxMesh(Point(0., 0., 0.), Point(1., 0.1, 0.04), 60, 10, 5)
V = VectorFunctionSpace(mesh, "CG", 1)

第二步:定义已提供的表达式和参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 弹性参数
E = 1000.0
nu = 0.3
mu = Constant(E / (2.0*(1.0 + nu)))
lmbda = Constant(E*nu / ((1.0 + nu)*(1.0 - 2.0*nu)))
# 质量密度
rho = Constant(1.0)

# 耗散(姑且先设为0,要求非负)
eta_m = Constant(0.)
eta_k = Constant(0.)

# 生成α方法的参数
alpha_m = Constant(0.2)
alpha_f = Constant(0.4)
gamma = Constant(0.5+alpha_f-alpha_m)
beta = Constant((gamma+0.5)**2/4.)

# 时间单步参数
T = 8.0
Nsteps = 100
dt = Constant(T/Nsteps)
time = np.linspace(0, T, Nsteps+1)

# 定义时间依赖的表达式,用于表达面元牵引力的变化
p0 = 1.
cutoff_Tc = T/10
# p.t = t
p = Expression(("0", "t <= tc ? p0*t/tc : 0", "0"), t=0, tc=cutoff_Tc, p0=p0, degree=0)

b = Constant((0.0, 0.0, 0.0)) # 无体元外力
u0 = Constant((0.0, 0.0, 0.0)) # 无初始应变

第三步:创建和应用基本边界条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 左侧面
def left(x, on_boundary):
return near(x[0], 0.) and on_boundary
# 右侧面
def right(x, on_boundary):
return near(x[0], 1.) and on_boundary

# 标记右侧面
boundary_subdomains = MeshFunction("size_t", mesh, mesh.topology().dim() - 1)
boundary_subdomains.set_all(0)
force_boundary = AutoSubDomain(right)
force_boundary.mark(boundary_subdomains, 3)
# 右侧面: dss(3)
dss = ds(subdomain_data=boundary_subdomains)

# 左侧面始终固定不动
bc = DirichletBC(V, u0, left)

第四步:定义变分问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# ε
def epsilon(u_):
return sym(grad(u_))
# 应力张量σ
def sigma(u_):
return 2.0*mu*epsilon(u_) + lmbda*tr(epsilon(u_))*Identity(len(u_))

# 质量
def m(u_,v_):
return rho*inner(u_, v_)*dx
# 刚度
def k(u_,v_):
return inner(sigma(u_), epsilon(v_))*dx
# 耗散
def c(u_,v_):
return eta_m*m(u_,v_) + eta_k*k(u_,v_)
# 外力
def f(v_):
return rho*dot(b,v_)*dx+dot(p,v_)*dss(3)

# 加速度更新估计式
def update_a(u_, u_old_, v_old_, a_old_, ufl=True):
if ufl:
dt_ = dt
beta_ = beta
else:
dt_ = float(dt)
beta_ = float(beta)
return (u_-u_old_-dt_*v_old_)/beta_/dt_**2 - (1-2*beta_)/2/beta_*a_old_

# 速度更新估计式
def update_v(a_, u_old_, v_old_, a_old_, ufl=True):
if ufl:
dt_ = dt
gamma_ = gamma
else:
dt_ = float(dt)
gamma_ = float(gamma)
return v_old_ + dt_*((1-gamma_)*a_old_ + gamma_*a_)

# 每步求解后,对应变,速度和加速度更新
def update_fields(u_, u_old_, v_old_, a_old_):
# 得到矢量引用
u_vec, u0_vec = u_.vector(), u_old_.vector()
v0_vec, a0_vec = v_old_.vector(), a_old_.vector()
# 得到应变,速度和加速度要更新的矢量
a_vec = update_a(u_vec, u0_vec, v0_vec, a0_vec, ufl=False)
v_vec = update_v(a_vec, u0_vec, v0_vec, a0_vec, ufl=False)
# 实际更新 (u_old <- u)
v_old_.vector()[:], a_old_.vector()[:] = v_vec, a_vec
u_old_.vector()[:] = u_.vector()

# 将出现在变分方程中的“平均”值替代
def avg(x_old, x_new, alpha):
return (1-alpha)*x_new + alpha*x_old

# 定义变分问题
u = TrialFunction(V)
v = TestFunction(V)

u_ = Function(V)
u_old = Function(V)
v_old = Function(V)
a_old = Function(V)
a_new = update_a(u, u_old, v_old, a_old, ufl=True)
v_new = update_v(a_new, u_old, v_old, a_old, ufl=True)

F = m(avg(a_old, a_new, alpha_m),v)+c(avg(v_old, v_new, alpha_f),v)+k(avg(u_old, u, alpha_f),v) - f(v)
a = lhs(F)
L = rhs(F)

第五步:循环迭代求解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 从初值开始,逐步计算
vtkfile = File("elastodynamics.pvd")

K, res = assemble_system(a, L, bc)
solver = LUSolver(K, "mumps")
solver.parameters["symmetric"] = True

for (i, dt) in enumerate(np.diff(time)):
t = time[i+1]
print("Time: ", t)
# 时间绑定,保持和前面应变u方法一致
p.t = t-float(alpha_f*dt)

# 求解
#solve(a == L,u_,bc)
res = assemble(L)
bc.apply(res)
solver.solve(K, u_.vector(), res)

# 保存到文件,并叠加式绘图
vtkfile << (u_, t)

# 更新上一时刻的解
update_fields(u_, u_old, v_old, a_old)

END