课程摘要
上一节生成了沿杆长变化的随机弹性模量场,本节将这一方法扩展到二维。我们以受拉矩形薄板为例,在三角形单元上生成空间相关的弹性模量,反复组装刚度矩阵并求解,统计右边界位移。课程还通过均匀材料解析解和整体力平衡检查程序,说明随机有限元中"生成样本、映射网格、验证求解、解释概率"缺一不可。
一、从一维求和,进入真正的二维求解
第四十一节的拉杆有一个便利之处:每个单元承受同样的轴力。因此,端部位移可以写成各单元伸长量之和。
二维薄板则不同。某块区域变软后,周围区域可能分担更多载荷;位移和应力必须通过整体刚度矩阵共同求解。我们要完成的计算链条是:
\ \\boxed{ \\text{生成二维随机材料场} \\longrightarrow \\text{赋值给三角形单元} \\longrightarrow K\^{(s)}u\^{(s)}=f \\longrightarrow \\text{统计结构响应} } \\
上标 \(s\) 表示第 \(s\) 次材料抽样。载荷和几何保持不变,每次改变的是单元弹性模量,因而刚度矩阵也随之改变。
本节沿用第二十三节介绍过的 NumPy 常应变三角形单元(CST)思路,让新增的"随机场---单元映射"过程容易核查。**本节代码是二维 NumPy 教学求解器,并非声称已经在 FEniCSx 中运行。**文末再说明如何迁移到 FEniCSx。
二、案例:随机材料矩形薄板
建立长 \(100\ \mathrm{mm}\)、高 \(20\ \mathrm{mm}\)、厚 \(1\ \mathrm{mm}\) 的薄板,采用平面应力假设:
- 左边界约束水平位移;左下角额外约束竖向位移,以消除整体刚体运动。
- 右边界施加 \(100\ \mathrm{MPa}\) 的均布水平拉应力,对应合力 \(2000\ \mathrm N\)。
- 泊松比固定为 \(\nu=0.3\)。
- 弹性模量平均值设为 \(210000\ \mathrm{MPa}\),变异系数设为 \(10\%\)。
- 材料对数场的相关长度设为 \(25\ \mathrm{mm}\)。
教学网格由6×3个矩形组成,每个矩形剖分成两个三角形,共36个三角形单元。每个三角形取一个弹性模量值。
我们关心的输出不是某个任意节点的位移,而是右边界各节点水平位移的平均值:
\ Q\^{(s)} = \\frac{1}{n_R} \\sum_{a\\in\\Gamma_R}u_x\^{(s)}(a) \\
这样可以得到一个明确的、可重复统计的响应量。请注意:这并不表示右边界所有节点的位移都相同。
三、先建立确定性"标准答案"
在均匀材料、单轴均布拉伸的理想条件下,平面应力解析位移场为:
\ u_x(x,y)=\\frac{p}{E}x,\\qquad u_y(x,y)=-\\nu\\frac{p}{E}y \\
其中 \(p=100\ \mathrm{MPa}\)。所以右端水平位移应为:
\ u_x(L,y) = \\frac{pL}{E} = \\frac{100\\times100}{210000} = 0.047619\\ \\mathrm{mm} \\
这一步很重要:如果程序连均匀材料都算不对,随后运行500次随机求解,也只会得到500个值得怀疑的数字。本节代码会先用这个解析场检查单元刚度、载荷与边界约束。
四、把空间随机场映射到二维单元
对第 \(e\) 个三角形,取其中心坐标 \(\boldsymbol c_e\)。与上一节类似,我们为对数弹性模量设置距离相关的协方差:
\ C_{ef} = \\sigma_G\^2 \\exp\\left( -\\frac{\\\|\\boldsymbol c_e-\\boldsymbol c_f\\\|}{\\ell} \\right) \\
这一次距离是二维欧氏距离,而不再只是沿杆长的距离。对协方差矩阵做特征值分解,再生成各单元的随机弹性模量 \(E_e^{(s)}>0\)。本课保留全部离散模态,避免在讲"映射与求解"的同时混入 KL 截断误差。
平面应力单元刚度为:
\ K_e\^{(s)} = tA_e B_e\^{\\mathsf T}D(E_e\^{(s)},\\nu)B_e \\
其中 \(t\) 为厚度,\(A_e\) 为三角形面积,\(B_e\) 是应变---位移矩阵。把每个 \(K_e^{(s)}\) 放到对应的整体自由度位置,便得到本次样本的整体刚度矩阵。
五、完整可运行代码
"""第四十二节:二维平面应力 CST 有限元与随机弹性模量场。"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
import numpy as np
# 单位:N、mm、MPa;右侧均布拉应力为 100 MPa。
L, H, THICKNESS, PRESSURE = 100.0, 20.0, 1.0, 100.0
E_MEAN, NU, CV, CORRELATION_LENGTH = 210000.0, 0.3, 0.10, 25.0
NX, NY, N_SAMPLES, LIMIT = 6, 3, 500, 0.050
x, y = np.meshgrid(
np.linspace(0, L, NX + 1),
np.linspace(0, H, NY + 1)
)
nodes = np.column_stack((x.ravel(), y.ravel()))
triangles = []
for j in range(NY):
for i in range(NX):
a = j * (NX + 1) + i
b, c, d = a + 1, a + NX + 1, a + NX + 2
triangles.extend(((a, b, d), (a, d, c)))
triangles = np.asarray(triangles, dtype=int)
centers = nodes[triangles].mean(axis=1)
ndof = 2 * len(nodes)
# 平面应力的单位弹性模量本构矩阵。
d_unit = np.array([
[1, NU, 0],
[NU, 1, 0],
[0, 0, (1 - NU) / 2]
]) / (1 - NU**2)
element_dofs, element_k_unit = [], []
for tri in triangles:
xy = nodes[tri]
xx, yy = xy[:, 0], xy[:, 1]
area = np.linalg.det(
np.column_stack((np.ones(3), xx, yy))
) / 2
assert area > 0
b = np.roll(yy, -1) - np.roll(yy, 1)
c = np.roll(xx, 1) - np.roll(xx, -1)
bmat = np.zeros((3, 6))
bmat[0, 0::2], bmat[1, 1::2] = b, c
bmat[2, 0::2], bmat[2, 1::2] = c, b
bmat /= 2 * area
element_dofs.append(np.array([
2 * node + k
for node in tri
for k in (0, 1)
]))
element_k_unit.append(
THICKNESS * area * bmat.T @ d_unit @ bmat
)
# 右侧边均匀施加拉应力。
load = np.zeros(ndof)
for j in range(NY):
lower = j * (NX + 1) + NX
upper = (j + 1) * (NX + 1) + NX
edge_force = PRESSURE * THICKNESS * (H / NY) / 2
load[2 * lower] += edge_force
load[2 * upper] += edge_force
left = np.flatnonzero(np.isclose(nodes[:, 0], 0.0))
right = np.flatnonzero(np.isclose(nodes[:, 0], L))
fixed = np.r_[2 * left, 1]
free = np.setdiff1d(np.arange(ndof), fixed)
def solve(modulus_by_element):
stiffness = np.zeros((ndof, ndof))
for modulus, dofs, k_unit in zip(
modulus_by_element, element_dofs, element_k_unit
):
stiffness[np.ix_(dofs, dofs)] += modulus * k_unit
displacement = np.zeros(ndof)
displacement[free] = np.linalg.solve(
stiffness[np.ix_(free, free)],
load[free]
)
return displacement, stiffness
# 均匀材料:先用解析位移场校核。
uniform_u, _ = solve(np.full(len(triangles), E_MEAN))
exact_u = np.column_stack((
PRESSURE * nodes[:, 0] / E_MEAN,
-NU * PRESSURE * nodes[:, 1] / E_MEAN
)).ravel()
np.testing.assert_allclose(
uniform_u, exact_u, rtol=1e-8, atol=1e-10
)
nominal_tip = uniform_u[2 * right].mean()
# 在三角形中心构建二维随机材料场。
distance = np.linalg.norm(
centers[:, None, :] - centers[None, :, :],
axis=2
)
sigma_log = np.sqrt(np.log1p(CV**2))
covariance = sigma_log**2 * np.exp(
-distance / CORRELATION_LENGTH
)
eigenvalues, eigenvectors = np.linalg.eigh(covariance)
eigenvalues = np.maximum(eigenvalues, 0.0)
rng = np.random.default_rng(2042)
normal_samples = rng.standard_normal(
(N_SAMPLES, len(triangles))
)
log_modulus = (
np.log(E_MEAN) - 0.5 * sigma_log**2
+ normal_samples
@ (eigenvectors * np.sqrt(eigenvalues)).T
)
modulus_samples = np.exp(log_modulus)
assert np.all(modulus_samples > 0)
tips = np.empty(N_SAMPLES)
for sample_no, modulus in enumerate(modulus_samples):
u, stiffness = solve(modulus)
tips[sample_no] = u[2 * right].mean()
if sample_no == 0:
reaction = stiffness @ u - load
np.testing.assert_allclose(
reaction[2 * left].sum(),
-PRESSURE * H * THICKNESS,
atol=1e-7
)
first_right_range = np.ptp(u[2 * right])
p_exceed = np.mean(tips > LIMIT)
probability_se = np.sqrt(
p_exceed * (1 - p_exceed) / N_SAMPLES
)
print(
f"节点/三角形/自由度: "
f"{len(nodes)}/{len(triangles)}/{ndof}"
)
print(f"右边界合力: {load[0::2].sum():.3f} N")
print(f"均匀材料右边平均位移: {nominal_tip:.6f} mm")
print(
f"首个随机样本右边位移极差: "
f"{first_right_range:.6f} mm"
)
print(f"Monte Carlo 样本数: {N_SAMPLES}")
print(
f"随机材料右边平均位移的样本均值: "
f"{tips.mean():.6f} mm"
)
print(
f"随机材料右边平均位移的样本标准差: "
f"{tips.std(ddof=1):.6f} mm"
)
print(
f"P(右边平均位移 > {LIMIT:.3f} mm): "
f"{p_exceed:.2%}"
)
print(
f"上述概率的 Monte Carlo 标准误: "
f"{probability_se:.2%}"
)
print(
"检查通过:均匀材料解析解及首样本整体力平衡。"
)
out = Path(__file__).parent
mesh = mtri.Triangulation(
nodes[:, 0], nodes[:, 1], triangles
)
fig, ax = plt.subplots(figsize=(9, 3.3), dpi=160)
field = ax.tripcolor(
mesh,
facecolors=modulus_samples[0] / 1000,
edgecolors="#64748b",
linewidth=0.6,
cmap="viridis"
)
fig.colorbar(
field, ax=ax, label="Elastic modulus E (GPa)"
)
ax.set(
xlabel="x (mm)",
ylabel="y (mm)",
title="One realization of the element-wise random material field"
)
ax.set_aspect("equal")
fig.tight_layout()
fig.savefig(out / "lesson42_material_field.png")
plt.close(fig)
fig, ax = plt.subplots(figsize=(9, 5), dpi=160)
ax.hist(tips, bins=35, color="#287d9d", alpha=0.85)
ax.axvline(
nominal_tip,
color="#183b56",
linestyle="--",
linewidth=2,
label=f"Uniform-material result: {nominal_tip:.4f} mm"
)
ax.axvline(
LIMIT,
color="#c2410c",
linewidth=2,
label=f"Illustrative limit: {LIMIT:.3f} mm"
)
ax.set(
xlabel="Mean right-edge displacement (mm)",
ylabel="Number of samples",
title="Monte Carlo response of the 2D tensile plate"
)
ax.grid(axis="y", alpha=0.18)
ax.legend(frameon=False)
fig.tight_layout()
fig.savefig(out / "lesson42_displacement_histogram.png")
plt.close(fig)
print(
"图片已保存:lesson42_material_field.png、"
"lesson42_displacement_histogram.png"
)
实际运行输出
节点/三角形/自由度: 28/36/56
右边界合力: 2000.000 N
均匀材料右边平均位移: 0.047619 mm
首个随机样本右边位移极差: 0.002392 mm
Monte Carlo 样本数: 500
随机材料右边平均位移的样本均值: 0.047999 mm
随机材料右边平均位移的样本标准差: 0.002855 mm
P(右边平均位移 > 0.050 mm): 25.00%
上述概率的 Monte Carlo 标准误: 1.94%
检查通过:均匀材料解析解及首样本整体力平衡。
图片已保存:lesson42_material_field.png、lesson42_displacement_histogram.png
不同 NumPy 版本可能使随机统计值的末几位略有差别。
六、先看材料场,再看结构响应
下图是一次抽样得到的单元弹性模量。颜色只表示各三角形单元在这次抽样中的材料参数,不是应力图;也不能凭颜色直接断定哪里已经失效。

把500次求解得到的右边界平均位移放在一起,就形成下图的分布。虚线是均匀材料结果,橙线是本课为了练习概率统计而设的位移限值。

这次样本中有 \(25\%\) 的位移超过 \(0.050\ \mathrm{mm}\),即500次中约125次。但估计值的 Monte Carlo 标准误约为1.94个百分点 ;如果工程上关心的是很小的超限概率,500次抽样远远不够。更重要的是,这个概率完全依赖于本课假设的材料分布、变异系数和相关长度,不能当成真实构件的失效概率。
首个随机样本中,右边各节点的水平位移极差为 \(0.002392\ \mathrm{mm}\)。这提醒我们:二维模型的右边界不再必然像一维杆端点那样只对应一个位移值。因此,在统计前必须先说清楚关注的是平均位移、最大位移,还是某个特定位置的位移。
七、这段程序检查了什么,还没检查什么?
已完成的检查有两项:
- 当所有单元取相同弹性模量时,数值解与上述解析位移场一致。
- 随机材料首个样本的左端水平反力,与右端 \(2000\ \mathrm N\) 拉力平衡。
但这不等于模型已经达到工程使用精度。6×3矩形、36个三角形的网格主要用于教学;500次抽样也只够展示方法。进一步使用前,至少应分别改变网格密度、单元尺度与相关长度的比例、抽样次数,并检查所关心响应量是否稳定。尤其要注意:网格细化后,不能把"重新抽到一组不同随机材料"造成的差异,全部误判为有限元网格误差。
八、如何迁移到 FEniCSx?
本节最值得迁移的不是 NumPy 的矩阵循环,而是清晰的数据对应关系:
\ \\text{网格单元编号} \\longleftrightarrow \\text{随机场样本中的 }E_e \\longleftrightarrow \\text{弱形式中的材料系数} \\
在 FEniCSx 中,逐单元常值材料参数可用 DG0(零次间断)函数空间 表示;官方教学示例说明了这种空间的单元值与自由度之间的对应方式。FEniCSx 多材料子区域教程 然后对每个随机场样本更新材料系数、求解弹性问题、提取同一个响应量。二维或三维线弹性弱形式及应力定义可参考 FEniCSx 线弹性教程。具体实现时还须核对所用版本的 API;并行计算下尤其要处理好单元编号与本地自由度的映射。
九、课后练习
- 将关注量从"右边界平均位移"改成"右边界最大位移"。同一个 \(0.050\ \mathrm{mm}\) 限值下,超限比例会怎样变化?先预测,再修改程序验证。
- 将
CORRELATION_LENGTH分别设为10 mm和60 mm,对比位移标准差。注意每次都要保持其他假设不变。 - 增加
N_SAMPLES,观察概率标准误;再增加NX、NY,观察网格变化。尝试分别记录这两类变化,不要把它们合并为一次实验。
本节完成了从二维随机材料场到二维有限元响应分布的闭环。下一节可以继续研究随机有限元的收敛与可信度:多少单元、多少随机场模态、多少抽样次数,才足以支撑一个具体结论?
