【零基础学智能仿真-42】二维随机有限元实战——把随机材料场赋给网格单元

课程摘要

上一节生成了沿杆长变化的随机弹性模量场,本节将这一方法扩展到二维。我们以受拉矩形薄板为例,在三角形单元上生成空间相关的弹性模量,反复组装刚度矩阵并求解,统计右边界位移。课程还通过均匀材料解析解和整体力平衡检查程序,说明随机有限元中"生成样本、映射网格、验证求解、解释概率"缺一不可。

一、从一维求和,进入真正的二维求解

第四十一节的拉杆有一个便利之处:每个单元承受同样的轴力。因此,端部位移可以写成各单元伸长量之和。

二维薄板则不同。某块区域变软后,周围区域可能分担更多载荷;位移和应力必须通过整体刚度矩阵共同求解。我们要完成的计算链条是:

\ \\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}\)。这提醒我们:二维模型的右边界不再必然像一维杆端点那样只对应一个位移值。因此,在统计前必须先说清楚关注的是平均位移、最大位移,还是某个特定位置的位移。

七、这段程序检查了什么,还没检查什么?

已完成的检查有两项:

  1. 当所有单元取相同弹性模量时,数值解与上述解析位移场一致。
  2. 随机材料首个样本的左端水平反力,与右端 \(2000\ \mathrm N\) 拉力平衡。

但这不等于模型已经达到工程使用精度。6×3矩形、36个三角形的网格主要用于教学;500次抽样也只够展示方法。进一步使用前,至少应分别改变网格密度、单元尺度与相关长度的比例、抽样次数,并检查所关心响应量是否稳定。尤其要注意:网格细化后,不能把"重新抽到一组不同随机材料"造成的差异,全部误判为有限元网格误差。

八、如何迁移到 FEniCSx?

本节最值得迁移的不是 NumPy 的矩阵循环,而是清晰的数据对应关系:

\ \\text{网格单元编号} \\longleftrightarrow \\text{随机场样本中的 }E_e \\longleftrightarrow \\text{弱形式中的材料系数} \\

在 FEniCSx 中,逐单元常值材料参数可用 DG0(零次间断)函数空间 表示;官方教学示例说明了这种空间的单元值与自由度之间的对应方式。FEniCSx 多材料子区域教程 然后对每个随机场样本更新材料系数、求解弹性问题、提取同一个响应量。二维或三维线弹性弱形式及应力定义可参考 FEniCSx 线弹性教程。具体实现时还须核对所用版本的 API;并行计算下尤其要处理好单元编号与本地自由度的映射。

九、课后练习

  1. 将关注量从"右边界平均位移"改成"右边界最大位移"。同一个 \(0.050\ \mathrm{mm}\) 限值下,超限比例会怎样变化?先预测,再修改程序验证。
  2. 将 CORRELATION_LENGTH 分别设为10 mm和60 mm,对比位移标准差。注意每次都要保持其他假设不变。
  3. 增加 N_SAMPLES,观察概率标准误;再增加 NX、NY,观察网格变化。尝试分别记录这两类变化,不要把它们合并为一次实验。

本节完成了从二维随机材料场到二维有限元响应分布的闭环。下一节可以继续研究随机有限元的收敛与可信度:多少单元、多少随机场模态、多少抽样次数,才足以支撑一个具体结论?

相关推荐
xsd202411181 小时前
OOOSplat:把手机环绕视频一键变成3D高斯泼溅的开源桌面应用
人工智能
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】C++ ChatSDK使用手册
开发语言·网络·c++·人工智能·学习·大模型
2501_933670793 小时前
2027校招销售运营面试:大数据管理与应用专业如何拆解漏斗分析
大数据·人工智能·面试
罗湖老棍子5 小时前
Biorhythms(信息学奥赛一本通- P1639)
算法·数论·裴蜀定理·扩展欧几里得·扩展中国剩余定理
198******126349 小时前
2026深度解读:Work Agent长程任务的技术机制与落地能力
人工智能
老金带你玩AI9 小时前
别只让AI解释,让它做个你能看懂的东西
人工智能
iffy110 小时前
Deepseek hardness 桌面版 + ollama
人工智能
Xiaofeng369310 小时前
知漫剧入门科普:一站式工作台一句话生成漫剧
人工智能
“AI国潮设计-小江”10 小时前
《Python+SDXL实战:用ControlNet精准控制“英歌舞翻糖吐司”构图,附批量生成脚本》
开发语言·人工智能·python·prompt·aigc