只转了30°,应变却读出-13% — 大变形壳单元的应力·应变共轭对
大变形下不能把任意应力和任意应变相乘。只有 $S:\dot{E}$、$P:\dot{F}$、$J\sigma:d$ 给出同一个值。
只是转了一下,应变片却指向 -13%#
把一个壳单元在平面内转了30°。没有拉伸,也没有扭转。只有刚体转动。
可是计算工程应变 ,结果是 。 也就是13.4%的压缩。单元哪里都没有变形,应变片却读出了压缩。
这个数不是笔误,也不是离散化误差。,正好就是这个值。 小应变的定义本身,就把转动误读成了变形。
本文要讲的是:在哪里切断这种误读,切断之后又该把应力换成什么。 读完可以带走三样东西。对转动免疫的应变度量、应力与应变配错对时能量偏差的实测值, 以及闭合壳单元厚度方向未知量的那个方程。
在下面的模拟中亲手操作一下。
点击 rigid rotation only 让转动跑起来。单元形状纹丝不动,红色的 条却在左右大幅摆动。
绿色的 条始终贴在0上。把 lam 调大, 才开始动,而且这个值无论转角怎么变都不改变。
滤掉转动的不是 ,而是 #
变形的故事从变形梯度(deformation gradient,从参考构形到当前构形的局部映射)开始。
是变形后的坐标, 是变形前的坐标。用极分解(polar decomposition)可以把 拆成 。 是转动, 是纯拉伸。问题在于 本身还带着 。
直接用了 ,于是 就漏了进来。 只有转动时 ,原因就在这里。
但把 平方一下,转动就消失了。
因为 , 里只剩下 。这个 就是右柯西–格林张量。 减去单位张量再除以二,得到的就是格林–拉格朗日应变。
刚体转动下 ,所以 精确为0。上面模拟中绿色条不动的原因,就是这两行。 "换个坐标系物理不该改变"这个要求,在 本构张量的坐标变换里是靠基的变换解决的; 在这里,则是靠应变度量的定义本身解决。
应力张量是除以哪个面积得到的
应变已经拉回参考构形,应力也得跟着一起拉回来。应力是"力除以面积", 但在大变形里,这个面积是变形前的还是变形后的,答案会分岔。一张表就能理清。
| 张量 | 力作用的面 | 相除的面积 | 对称性 | 共轭的应变率 |
|---|---|---|---|---|
| Cauchy | 变形后 | 变形后 | 对称 | (但要写成 ) |
| 第一 PK | 变形后 | 变形 前 | 非对称 | |
| 第二 PK | 拉回参考构形 | 变形前 | 对称 | |
| 工程 · | 不区分 | 不区分 | 对称 | 仅在小应变极限下 |
它们之间的关系如下。
是体积比。第一皮奥拉–基尔霍夫张量 之所以非对称,是因为它的两条腿踩在不同的构形上。 一个指标指向变形后,另一个指向变形前。所以有限元程序里要存 ,就得把9个分量全部带着。 的两条腿都放在参考构形上,6个分量就够了。
共轭配对的含义是功率相等
"共轭对(conjugate pair)"不是口味问题,而是一个等式:单位参考体积的内部功率必须给出同一个值。
其中 是变形率(rate of deformation)张量。 这三种写法是同一个物理量在三个构形上的表达,数值必须严格一致。
反过来, 或 不对应任何物理量。量纲对得上,也算得出来, 但那个数不是功率。用这种组合去搭有限元残差,刚度矩阵就不再是能量泛函的海森矩阵。 这和伽辽金对称性讲的是同一件事。 没有可最小化的能量,牛顿迭代就失去二阶收敛。
用 Python 在同一时刻测了三种配对的功率#
构造一条把拉伸、剪切和转动混在一起的变形路径,在 处测三种组合的数值。 材料用圣维南–基尔霍夫模型,。
import math
I3 = [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]]
def mul(A, B):
return [[sum(A[i][k]*B[k][j] for k in range(3)) for j in range(3)] for i in range(3)]
def tr(A):
return [[A[j][i] for j in range(3)] for i in range(3)]
def add(A, B, s=1.0):
return [[A[i][j] + s*B[i][j] for j in range(3)] for i in range(3)]
def scale(A, s):
return [[s*A[i][j] for j in range(3)] for i in range(3)]
def ddot(A, B):
return sum(A[i][j]*B[i][j] for i in range(3) for j in range(3))
def trace(A):
return A[0][0] + A[1][1] + A[2][2]
def det(A):
return (A[0][0]*(A[1][1]*A[2][2] - A[1][2]*A[2][1])
- A[0][1]*(A[1][0]*A[2][2] - A[1][2]*A[2][0])
+ A[0][2]*(A[1][0]*A[2][1] - A[1][1]*A[2][0]))
def inv(A):
d = det(A)
C = [[0.0]*3 for _ in range(3)]
for i in range(3):
for j in range(3):
m = [[A[r][c] for c in range(3) if c != j] for r in range(3) if r != i]
C[j][i] = ((-1)**(i+j))*(m[0][0]*m[1][1] - m[0][1]*m[1][0])/d
return C
def sym(A):
return scale(add(A, tr(A)), 0.5)
def green_lagrange(F):
return scale(add(mul(tr(F), F), I3, -1.0), 0.5)
def linear_strain(F):
return sym(add(F, I3, -1.0))
LAM, MU = 100.0, 60.0 # 圣维南–基尔霍夫常数
def pk2_stress(E):
return add(scale(I3, LAM*trace(E)), E, 2*MU)
def cauchy_stress(F, S):
return scale(mul(mul(F, S), tr(F)), 1.0/det(F))
def rot_z(th):
c, s = math.cos(th), math.sin(th)
return [[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]]
print("--- 1. pure rotation, no stretch ---")
print(" theta eps_xx(linear) E_xx(Green-Lagrange)")
for deg in (0, 5, 10, 30, 60, 90):
F = rot_z(math.radians(deg))
print("%5.0f %14.5f %20.2e" % (deg, linear_strain(F)[0][0], green_lagrange(F)[0][0]))
def defo_path(t):
"""先施加拉伸和剪切,再做 40度 * t 的刚体转动"""
U = [[1 + 0.20*t, 0.15*t, 0.0],
[0.15*t, 1 - 0.05*t, 0.0],
[0.0, 0.0, 1 - 0.08*t]]
return mul(rot_z(math.radians(40.0)*t), U)
def rate(f, t, h=1e-6):
A, B = f(t + h), f(t - h)
return [[(A[i][j] - B[i][j])/(2*h) for j in range(3)] for i in range(3)]
print()
print("--- 2. work rate at t=0.7, three pairings ---")
t = 0.7
F = defo_path(t)
Fd = rate(defo_path, t)
E = green_lagrange(F)
Ed = rate(lambda s: green_lagrange(defo_path(s)), t)
S = pk2_stress(E)
P = mul(F, S)
J = det(F)
sig = cauchy_stress(F, S)
d = sym(mul(Fd, inv(F))) # 变形率张量
print(" J = det F = %.6f" % J)
print(" S : Edot (2nd PK x GL rate) = %12.6f" % ddot(S, Ed))
print(" P : Fdot (1st PK x F rate) = %12.6f" % ddot(P, Fd))
print(" J sigma: d (Cauchy x stretching)= %12.6f" % (J*ddot(sig, d)))
print(" sigma : Edot <- wrong pair = %12.6f" % ddot(sig, Ed))
print(" S : d <- wrong pair = %12.6f" % ddot(S, d))--- 1. pure rotation, no stretch ---
theta eps_xx(linear) E_xx(Green-Lagrange)
0 0.00000 0.00e+00
5 -0.00381 0.00e+00
10 -0.01519 -5.55e-17
30 -0.13397 0.00e+00
60 -0.50000 0.00e+00
90 -1.00000 0.00e+00
--- 2. work rate at t=0.7, three pairings ---
J = det F = 1.028087
S : Edot (2nd PK x GL rate) = 10.522306
P : Fdot (1st PK x F rate) = 10.522306
J sigma: d (Cauchy x stretching)= 10.522306
sigma : Edot <- wrong pair = 9.961790
S : d <- wrong pair = 4.8235145% 和 54% — 配错共轭对会产生的两类误差#
三种正确组合小数点后六位完全一致,都是 。同一个量只是分写在三个构形上, 这一点由数值直接证实。
两种错误组合的错法却不一样。 得到 ,低了5.3%。 和 相差一个 ,而这个变换本身不大。 ,拉伸也就20%出头,所以误差止步于这个量级。
得到 ,低了54%。这不是尺度问题,而是另一类错误。 它无视了 这个关系,直接把 塞了进去,于是转动分量也混了进来。 转角越大,这个误差越大。
工程上更危险的是那个5%。54%在第一个载荷步就发散,马上会被抓住。5%却能收敛, 只是收敛到一个略微错误的答案。加密网格也消不掉。
厚度方向由体积闭合,而不是靠第三个方程
到这里为止都是一般连续体的内容。壳还要再加一条。
三维壳单元把厚度方向的伸长当作未知量。在 Sussman 和 Bathe 的三维壳格式中, 厚度方向需要3个未知量,而平面应力条件只能给出2个方程。少了一个。
补上这一个的,是不可压缩条件。
是主伸长比。在橡胶或金属塑性这类体积几乎守恒的材料里,厚度不是独立未知量, 而成了面内伸长的因变量。面内拉伸 、 时, 厚度变为 倍,减薄20.6%。板料成形中计算减薄率,用的正是这个关系。
这个条件和 MITC 绑定有交汇之处。 如果在单元内部直接插值厚度方向伸长,薄单元上会出现体积锁死(volumetric locking)。 就像用绑定点解决剪切锁死那样,厚度伸长也只在每个单元的少数几个点上独立取值, 其余用插值绑住。
把转动截断到一阶,director 会长 16%#
壳的第二条是转动。壳单元随身带着中面法向量,也就是 director。 牛顿迭代给出转动增量 后,就要把 director 转过相应的角度。 转动矩阵由罗德里格斯(Rodrigues)公式生成。
是 的反对称矩阵,。 不少代码觉得增量很小,就截断成 。这个矩阵并不正交。 ,所以每一步 director 都会稍微变长一点。
在下面的模拟中亲手操作一下。
把 d.theta 设为0.10再让它推进,红色的一阶截断箭头会螺旋着甩出虚线单位圆之外。
绿色的闭式解则精确地留在圆上。把 d.theta 减半,漂移也减半。
也就是说,这个误差对增量大小是一阶的。
import math
def matvec(A, v):
return [sum(A[i][k]*v[k] for k in range(3)) for i in range(3)]
def matmul(A, B):
return [[sum(A[i][k]*B[k][j] for k in range(3)) for j in range(3)] for i in range(3)]
def skew(w):
return [[0.0, -w[2], w[1]],
[w[2], 0.0, -w[0]],
[-w[1], w[0], 0.0]]
def rodrigues(w, order):
"""order = 1, 2 为级数截断,0 为闭式"""
th = math.sqrt(sum(c*c for c in w))
W = skew(w)
W2 = matmul(W, W)
if order == 1:
a, b = 1.0, 0.0
elif order == 2:
a, b = 1.0, 0.5
else:
a = math.sin(th)/th
b = (1.0 - math.cos(th))/(th*th)
return [[(1.0 if i == j else 0.0) + a*W[i][j] + b*W2[i][j]
for j in range(3)] for i in range(3)]
def spin_director(dth, steps, order):
"""把中面法向绕 y 轴每步转动 dth 弧度"""
d = [0.0, 0.0, 1.0]
for _ in range(steps):
d = matvec(rodrigues([0.0, dth, 0.0], order), d)
return d
print("--- 3. director after 30 increments of 0.10 rad (exact total 171.89 deg) ---")
print(" order |d| length err % angle(deg) angle err(deg)")
for order, name in ((1, "1st"), (2, "2nd"), (0, "closed")):
d = spin_director(0.10, 30, order)
n = math.sqrt(sum(c*c for c in d))
ang = math.degrees(math.atan2(d[0], d[2]))
if ang < 0:
ang += 360.0
print(" %-6s %10.5f %11.2f %10.3f %12.3f"
% (name, n, 100*(n - 1.0), ang, ang - math.degrees(3.0)))
print()
print("--- 4. same total rotation, smaller increments (1st order) ---")
print(" steps dtheta |d| length err %")
for steps in (30, 60, 150, 300, 3000):
dth = 3.0/steps
d = spin_director(dth, steps, 1)
n = math.sqrt(sum(c*c for c in d))
print(" %5d %6.4f %8.5f %11.3f" % (steps, dth, n, 100*(n - 1.0)))
print()
print("--- 5. thickness closed by J = 1, not by a 3rd equation ---")
print(" lam1 lam2 lam3=1/(lam1 lam2) thickness change %")
for l1, l2 in ((1.20, 1.05), (1.20, 1.00), (1.10, 1.10), (1.30, 0.95)):
l3 = 1.0/(l1*l2)
print(" %4.2f %4.2f %16.5f %16.1f" % (l1, l2, l3, 100*(l3 - 1.0)))--- 3. director after 30 increments of 0.10 rad (exact total 171.89 deg) ---
order |d| length err % angle(deg) angle err(deg)
1st 1.16097 16.10 171.318 -0.570
2nd 1.00038 0.04 172.173 0.286
closed 1.00000 0.00 171.887 0.000
--- 4. same total rotation, smaller increments (1st order) ---
steps dtheta |d| length err %
30 0.1000 1.16097 16.097
60 0.0500 1.07778 7.778
150 0.0200 1.03045 3.045
300 0.0100 1.01511 1.511
3000 0.0010 1.00150 0.150
--- 5. thickness closed by J = 1, not by a 3rd equation ---
lam1 lam2 lam3=1/(lam1 lam2) thickness change %
1.20 1.05 0.79365 -20.6
1.20 1.00 0.83333 -16.7
1.10 1.10 0.82645 -17.4
1.30 0.95 0.80972 -19.0一阶截断只用30步就把 director 拉长了16.1%。多加一项的二阶降到0.04%。 长度精度提高了400倍,角度误差反而是二阶的更大。这两种误差彼此独立。
第4张表更重要。把增量缩到十分之一,误差也只降到十分之一。把载荷步切得更细, 解决不了这个问题。要么用闭式解,要么每步对 director 做归一化, 要么把转动改用四元数来携带。
如何从代码里分辨是 还是 #
打开别人的大变形代码时,光看变量名认不出应力的身份。看三个地方就能分辨。
看应力进入刚度矩阵的位置。如果 矩阵是按 构造的,与之相乘的应力就是 。 如果 是 ,那就是 。 正如本构矩阵的变换中所见, 的定义决定了应力的身份。
看积分时的雅可比。如果像 那样在参考构形体积上积分,而且没有乘 ,那就是 。 如果乘了 ,说明正在把 拉回参考构形。
看输出例程。如果后处理在算 von Mises 之前有 这一步变换, 说明内部一直是用 在跑。也有代码省掉这步变换,直接把 的分量塞进 von Mises 去画。 小变形下看不出破绽,一旦伸长超过20%,两者就分道扬镳。
三处都查不出来,还剩下一个测试。让一个单元只做刚体转动,看应力是否保持为0。 如果30°时冒出13%,就说明某个地方没有把 平方。
相关文章
如果对您有帮助,请分享。