1. 课程导入与FEniCS环境搭建
偏微分方程(PDE)是描述连续介质中物理过程的通用语言:热传导、弹性变形、流体运动、电磁场分布,最终都归结为在某个区域上求解一个带边界条件的方程。传统做法是手工推导弱形式、编写有限元程序、组装刚度矩阵,工作量集中在“把数学变成代码”这一步。FEniCS 的定位正是把这一步大幅压缩:它提供了一套接近数学书写的统一接口,让用户用变分形式直接描述问题,由系统自动完成网格上的离散、矩阵组装与线性求解。换句话说,你负责写清楚“要求解什么”,FEniCS 负责“怎么算”。
理解这一点很关键,因为它决定了学习路径。使用 FEniCS 时,核心工作不是手写单元刚度矩阵,而是准确写出弱形式、选对函数空间、给定边界条件与材料参数,然后读懂求解结果。本课程后续所有内容都围绕这条主线展开,而本节要完成的是最基础也最容易被低估的一步:把环境装好,并让第一个算例真正跑通。
环境方案的选择
FEniCS 依赖 PETSc、MPI、Eigen、Boost 以及专用的网格与形式编译组件,手工从源码编译对初学者并不友好,版本不匹配是常见故障源。因此推荐两条路线:Docker 与 conda。Docker 的优势是环境完全隔离、版本固定、跨平台一致,适合“装完就能用”的场景;conda 的优势是与本地 Python 生态融合、便于调试和扩展,适合需要频繁改动代码的学习者。两者都能满足本课程需要,下面分别给出可执行的做法。
Docker 路线(以官方镜像为例,镜像标签请以实际拉取到的版本为准):
docker pull dolfinx/dolfinx:stable
docker run -it --rm -v $(pwd):/home/shared dolfinx/dolfinx:stable
# 进入容器后验证
python3 -c "import dolfinx; print(dolfinx.__version__)"
其中 -v $(pwd):/home/shared 把当前目录挂载进容器,这样在宿主机编辑的脚本可以直接在容器内运行,避免“代码在容器里、文件在本地”的割裂。conda 路线则建议新建独立环境,避免与系统 Python 冲突:
conda create -n fenics-env python=3.11 -y
conda activate fenics-env
conda install -c conda-forge fenics-dolfinx mpich -y
python -c "import dolfinx; print(dolfinx.__version__)"
两条路线验证成功的标志相同:Python 能导入 dolfinx 并打印出版本号,且不报缺少共享库的错误。若导入时报 libpetsc 或 MPI 相关错误,通常说明环境混用了不同渠道的包,最稳妥的处理是删除环境重建,而不是逐个修补依赖。
第一个泊松方程算例
取单位正方形区域 Ω=(0,1)×(0,1),方程 −Δu = 1,四周边界 u=0。这个问题的解析解在中心处约为 0.0736,正好可以作为数值结果的对照。下面给出可直接运行的完整脚本(DOLFINx 接口),关键行都附有说明:
from mpi4py import MPI
import numpy as np
import ufl
from dolfinx import mesh, fem, default_scalar_type
from dolfinx.fem.petsc import LinearProblem
# 1. 生成网格:16x16 个四边形单元
domain = mesh.create_unit_square(MPI.COMM_WORLD, 16, 16)
# 2. 定义函数空间:一阶 Lagrange 连续元
V = fem.functionspace(domain, ("Lagrange", 1))
# 3. 边界条件:四条边上 u = 0
def boundary(x):
return np.isclose(x[0], 0) | np.isclose(x[0], 1) \
| np.isclose(x[1], 0) | np.isclose(x[1], 1)
dofs = fem.locate_dofs_geometrical(V, boundary)
bc = fem.dirichletbc(default_scalar_type(0), dofs, V)
# 4. 弱形式:a(u,v) = L(v)
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
a = ufl.dot(ufl.grad(u), ufl.grad(v)) * ufl.dx
L = ufl.inner(default_scalar_type(1.0), v) * ufl.dx
# 5. 组装并求解
problem = LinearProblem(a, L, bcs=[bc],
petsc_options={"ksp_type": "preonly",
"pc_type": "lu"})
uh = problem.solve()
# 6. 输出关键数值
print("max u =", uh.x.array.max())
print("min u =", uh.x.array.min())
这段代码的结构与上一张图完全对应:create_unit_square 负责区域与网格,functionspace 选定离散空间,Dirichletbc 施加边界,a 与 L 用 UFL 写出弱形式,LinearProblem 完成组装与求解。运行后应看到 max u 约为 0.07 左右,min u 为 0(边界自由度被强制为零)。若最大值明显偏离这个量级,例如接近 1 或出现负值,通常意味着边界条件没有正确施加,或弱形式中梯度项写错。
如何判断“真的跑通了”
很多初学者把“没有报错”当作成功,这并不充分。建议按三个层次检查。第一层是程序层面:进程正常退出,求解器返回收敛状态,没有出现 DIVERGED 之类的提示。第二层是物理与数学层面:解的量级、符号和空间分布是否符合预期——本例中解应在中心最大、向四边递减至零,且处处非负。第三层是数值层面:把网格从 16×16 加密到 32×32,中心点数值应更接近解析值 0.0736,误差随网格尺寸减小而下降。三层都通过,才能说明环境、接口和算例三者都正确。
-v 挂载参数;若在 conda 环境中导入 dolfinx 失败,优先检查是否误装了旧版 fenics 包,两者不能混用。