边界条件¶
当你从 形式 得到了刚度矩阵 K 和载荷向量 b 之后,仍需施加问题的边界条件。在如今的 TensorMesh 中,这是通过对狄利克雷自由度做 静态凝聚 实现的——只需对 Condenser 调用一次,便能接收一个完整系统,并返回一个仅在内部自由度上的、可直接求解的缩减系统。
诺伊曼条件不需要单独的算子:它们作为边界积分自然地出现在弱形式中,由 FacetAssembler 装配。
本章其余部分将逐一讲解这两者,外加向量值问题(线弹性、Stokes 等)所需的逐分量掩码、随时间变化的边界数据,以及实践中最常遇到的陷阱。
为何采用静态凝聚¶
将自由度划分为“内部”(自由)和“外部”(被狄利克雷固定)后,可将线性系统
转化为仅在内部块上的一个更小求解:
凝聚避免了拉格朗日乘子,能让对称正定(SPD)算子保持 SPD 性质,并且端到端可微——边界值会经由右端项的修正流动,并以通常的方式获得梯度。
Condenser API¶
从一个覆盖所有自由度的布尔掩码构造 Condenser:
from tensormesh import Condenser
condenser = Condenser(mesh.boundary_mask) # zero values (default)
condenser = Condenser(mesh.boundary_mask, values) # prescribed values
dirichlet_mask: [n_dof]—— 布尔张量;在自由度被固定处取True。对于标量问题,它就是mesh.boundary_mask(长度为n_points);对于向量值问题,参见下文的 向量值问题(弹性、Stokes)。dirichlet_value—— 可选的一维张量,给出指定的边界值。它可以是[n_dof](构造函数会将其切片至边界项)或已切片好的[n_outer_dof]。默认为全零。
三个方法即可覆盖常见的工作流程:
方法 |
作用 |
|---|---|
|
一次性凝聚两者。返回 |
|
仅重新凝聚右端项,复用缓存的布局。当 |
|
将内部解与指定的边界值重新拼接成一个完整的 |
|
不涉及边界值的线性投影(在边界上以零进行丢弃/散布)。当所提升的量无论狄利克雷值如何都应在边界上为零时使用——例如 |
对于含时边界条件,condenser.update_dirichlet(new_values) 会换入一个新的边界值向量(与构造函数一样,既接受完整的 [n_dof] 形式,也接受已切片好的 [n_outer_dof] 形式)。缓存的稀疏布局与所指定的值无关,因此该调用开销很小。
首次调用 condenser(K, b) 会计算并缓存 K 稀疏模式的内部/外部索引划分;后续对具有相同布局的矩阵的调用会复用这一成果。
备注
Condenser 是一个 torch.nn.Module,它所拥有的每个张量(布尔掩码、指定值,以及惰性计算的索引缓冲区)都注册为缓冲区。因此
condenser = Condenser(mesh.boundary_mask).to(mesh.device)
可将该算子与 K、b 和网格一起迁移。如果你忘了这么做,__call__ 和 condense_rhs() 仍会在每次调用时自动提升到输入矩阵的设备/数据类型——这很方便,但在紧凑的瞬态循环中,逐次调用的 .to(...) 开销会在性能分析中显现出来。
齐次狄利克雷¶
最基本、最常见的情形——边界上取零值——正是 快速开始 所做的:
condenser = Condenser(mesh.boundary_mask)
K_, b_ = condenser(K, b)
u_inner = K_.solve(b_)
u = condenser.recover(u_inner)
连同求解在内只需三行。恢复出的 u 在边界自由度处为零,在其余各处为有限元解。
非齐次狄利克雷¶
通过将逐自由度的值传给构造函数来指定它们。可接受两种形状——选用更方便的一种即可:
# Option A: full-length [n_dof] vector (Condenser slices internally)
values = torch.zeros(mesh.n_points, dtype=mesh.dtype)
x = mesh.points[:, 0]
values[(x == 1.0) & mesh.boundary_mask] = 1.0 # u = 1 on right edge
condenser = Condenser(mesh.boundary_mask, values)
# Option B: pre-sliced [n_outer_dof] vector
sliced = values[mesh.boundary_mask]
condenser = Condenser(mesh.boundary_mask, sliced)
凝聚后的右端项会自动吸收项 \(-K_{io}\, u_o\),因此流程的其余部分保持不变。
小技巧
Mesh.gen_rectangle / gen_cube 将边界节点恰好放置在整数坐标处,因此 x == 1.0 是有效的。对于从 Gmsh / VTK 文件加载的网格,建议改用
mask = torch.isclose(x, torch.tensor(1.0, dtype=mesh.dtype)) & mesh.boundary_mask
这样网格生成器带来的 1e-16 量级扰动就不会在不报错的情况下丢掉一排节点。
向量值问题(弹性、Stokes)¶
对于弹性问题,每个节点带有 dim 个位移分量,而由 LinearElasticityElementAssembler 产生的装配器会生成一个尺寸为 n_dof = n_points * dim 的全局系统。两个额外步骤可将 mesh.boundary_mask(形状 [n_points])转化为自由度级别的狄利克雷掩码:
dim = mesh.points.shape[1]
# Per-(node, component) mask, then flatten to per-DOF
bc_mask = torch.zeros(mesh.n_points, dim, dtype=torch.bool)
# Clamp the bottom edge in all directions, leave top free
y = mesh.points[:, 1]
bc_mask[(y == 0.0) & mesh.boundary_mask, :] = True
# Pin x-displacement on the right edge (roller BC), y free
x = mesh.points[:, 0]
bc_mask[(x == 1.0) & mesh.boundary_mask, 0] = True
condenser = Condenser(bc_mask.flatten()) # [n_points * dim]
同样的展平技巧也适用于指定值:先构建一个 [n_points, dim] 的目标位移张量,再在交给构造函数之前 .flatten()。约定是自由度按节点交错排列([u_x_0, u_y_0, u_x_1, u_y_1, ...]),与 TensorMesh 弹性装配器的布局相匹配。
随时间变化的边界值¶
只需构建一次凝聚器,随后便可在每个时间步推入新值而无需重建布局:
condenser = Condenser(mesh.boundary_mask, initial_values)
K_, _ = condenser(K, torch.zeros(mesh.n_points, dtype=mesh.dtype)) # caches the split
for t in time_steps:
condenser.update_dirichlet(values_at(t)) # cheap setter
b_new = build_rhs_for(t)
b_inner = condenser.condense_rhs(b_new) # reuses cached K_io
u = condenser.recover(K_.solve(b_inner))
condense_rhs() 要求 __call__ 已在相同的矩阵布局上至少被调用过一次——正是这次调用填充了它所使用的缓存 \(K_{io}\) 块。请在循环之前调用它,哪怕你会丢弃第一次求解的结果。
将狄利克雷与诺伊曼结合¶
在 \(\partial\Omega\) 一部分上的非零牵引力或通量,对应弱形式右端的一个边界积分:
这里不涉及 Condenser——自然边界条件完全由弱形式处理。用一个 FacetAssembler 子类装配面积分,并在凝聚之前将结果加到你的载荷向量上:
import torch
from tensormesh import Mesh, Condenser, FacetAssembler
from tensormesh.assemble import LaplaceElementAssembler
mesh = Mesh.gen_rectangle(chara_length=0.1)
x = mesh.points[:, 0]
# ----------------------- volume bilinear form ----------------------------
K = LaplaceElementAssembler.from_mesh(mesh)()
# ----------------------- Neumann flux on the right edge ------------------
right_edge = (x == 1.0) & mesh.boundary_mask # [n_points] bool
class FluxAssembler(FacetAssembler):
def forward(self, v):
return v # ∫_{Γ_N} g·v with g=1
b_neumann = FluxAssembler.from_mesh(mesh, boundary_mask=right_edge)()
# Optional volume load f = 1
from tensormesh.assemble import const_node_assembler
b_volume = const_node_assembler(1.0).from_mesh(mesh)()
b = b_volume + b_neumann
# ----------------------- Dirichlet on the *complement* ------------------
dirichlet = mesh.boundary_mask & ~right_edge # bottom/top/left edges
condenser = Condenser(dirichlet)
K_, b_ = condenser(K, b)
u = condenser.recover(K_.solve(b_))
有两点值得强调:
混合区域与逐分量边界条件¶
对于相互独立的狄利克雷与诺伊曼区块(或弹性中的逐分量混合),可从坐标构建掩码,并用布尔代数将它们组合起来:
x, y = mesh.points[:, 0], mesh.points[:, 1]
left = (x == 0.0) & mesh.boundary_mask
right = (x == 1.0) & mesh.boundary_mask
# Scalar problem: fix left to zero, ramp on right
dirichlet_mask = left | right
values = torch.zeros(mesh.n_points, dtype=mesh.dtype)
values[right] = 1.0
condenser = Condenser(dirichlet_mask, values)
对于从 Gmsh 加载、带有命名物理组的网格,这些组会以布尔掩码的形式落入 mesh.point_data;你可以按键读取它们(例如 mesh.point_data["clamped_face"]),而无需手动从坐标构建掩码。
易踩的坑¶
稀疏布局在首次调用时被缓存。
Condenser会记住你首次通过__call__交给它的矩阵的行/列划分;传入具有不同稀疏模式的矩阵会触发断言错误。如果你的几何或单元类型发生了变化,请新建一个Condenser。向量值掩码必须展平。 一个
[n_points, dim]的布尔张量并不是有效的dirichlet_mask——传入前请调用.flatten()。同理,弹性问题的指定值应当是一个展平后的[n_points * dim](或[n_outer_dof])向量。浮点扰动网格上的坐标掩码。 精确比较(
x == 1.0)在 TensorMesh 的内置生成器上有效,但在导入的网格上可能失效;拿不准时请使用torch.isclose。重叠区域上狄利克雷优先。 如果某个节点既在狄利克雷掩码中,又落在某个诺伊曼面积分的支撑范围内,那么该行上的诺伊曼贡献会被凝聚丢弃(该行被移除)。当你希望两者都生效时,请将诺伊曼区块从狄利克雷掩码中排除。
求解与恢复必须由同一个掩码驱动。 不要在凝聚与恢复之间修改
mesh.boundary_mask——Condenser引用的是构造时传给它的那同一个缓冲区。
目前尚未内置的功能¶
tensormesh.operator 模块恰好只提供一个算子——通过静态凝聚施加狄利克雷条件的 Condenser。还有几种相邻的技术 并非 一等算子:
用于狄利克雷的 拉格朗日乘子 —— 凝聚的一种替代方案,它保留原始的自由度布局,但会产生一个鞍点系统。如果你自行构建增广矩阵,手动实现是可行的,但没有现成的辅助类。
周期性边界条件 —— 通常通过识别匹配的自由度、并在装配后的矩阵中合并对应的行/列来实现。没有现成的辅助工具。
作为顶层算子的 罗宾(混合)条件 —— 不过其双线性形式贡献 \(\int_{\Gamma} \alpha\, u\, v\, \mathrm{d}S\) 只需一行代码的
FacetAssembler子类即可;在凝聚之前将它加到K上就行。
罚函数接触、表面张力、压力载荷以及类似的基于能量的边界项 确实 是一等公民——参见 ContactAssembler(一个带有 element_energy 钩子以表示表面能密度的 FacetAssembler 子类)。示例库针对需要它们的情形提供了完整的实操方案。