【零基础学智能仿真-62】从应变—应力计算弹性常数

课程摘要

本节承接应力与数值收敛检查,以无预应力立方晶体为例,讲解如何通过正负小应变与应力响应拟合C₁₁、C₁₂和C₄₄。结合可运行Python程序与图示,辨析工程剪切与张量剪切,分析残余应力、拟合截距和应变区间对结果的影响,并进行立方晶体弹性稳定性检查。全部数据为明确标注的人工构造数据,用于验证计算流程,不代表真实材料的第一性原理计算结果。


一、从一个问题开始:材料"有多硬",到底怎样算?

上一节,我们讨论了第一性原理计算中的收敛检查,其中一个重要结论是:

总能量看起来稳定,并不自动意味着应力已经足够准确。

现在,假设我们已经获得一个合适的参考晶胞,也能够读取计算得到的应力。

把晶胞沿一个方向轻轻拉长,记录应力;再换几个拉伸和压缩幅度,继续记录。

能否从这些数据中得到材料的弹性常数?

可以。但在点击"拟合"之前,我们必须回答三个问题:

  1. 施加的究竟是什么应变?
  2. 读取的是哪个应力分量?
  3. 拟合得到的斜率,对应哪个弹性常数?

如果这些问题没有说清楚,即使拟合曲线十分漂亮,结果也可能错误。

本节要建立的分析过程是:

参考态检查 → 设计应变路径 → 获取应力 → 统一约定 → 拟合与诊断 → 稳定性检查。

本节学习目标

完成本节后,你应当能够:

  • 理解立方晶体的三个独立弹性常数;
  • 通过两类应变路径提取 \(C_{11}\)、\(C_{12}\) 和 \(C_{44}\);
  • 识别剪切定义、拟合截距与应变范围带来的问题;
  • 运行Python程序,并解释实际输出;
  • 区分"程序运行成功"和"真实材料结果可信"。

本节的计算数据全部为人工构造数据,没有运行ABACUS,也不对应某一种真实材料。 这样安排,是为了让我们先用知道答案的数据检查方法,再迁移到真实计算。


二、先统一计算中的"语言"

2.1 本节采用哪些假设?

为了控制学习难度,本节只讨论:

项目 本节约定
晶体类型 立方晶体
坐标系 与立方晶轴一致
参考状态 无预应力
变形范围 无穷小应变近似适用
应力单位 GPa
应力符号 拉应力为正
应变输入 无量纲小数
剪切约定 工程剪切应变

注意:

\ 0.2\\%=0.002 \\

程序中应输入0.002,而不是0.2。

这类错误不会引发语法报错,却会改变最终结果的数量级。

2.2 应力与应变怎样写成向量?

本节采用的Voigt分量顺序为:

\ (11,\\ 22,\\ 33,\\ 23,\\ 13,\\ 12) \\

应力向量为:

\ \\boldsymbol{s}= \\begin{bmatrix} \\sigma_{11}\\\\ \\sigma_{22}\\\\ \\sigma_{33}\\\\ \\sigma_{23}\\\\ \\sigma_{13}\\\\ \\sigma_{12} \\end{bmatrix} \\

工程应变向量为:

\ \\boldsymbol{e}= \\begin{bmatrix} \\varepsilon_{11}\\\\ \\varepsilon_{22}\\\\ \\varepsilon_{33}\\\\ 2\\varepsilon_{23}\\\\ 2\\varepsilon_{13}\\\\ 2\\varepsilon_{12} \\end{bmatrix} \\

其中:

\ \\gamma_{ij}=2\\varepsilon_{ij} \\qquad(i\\neq j) \\

这里的 \(\gamma_{ij}\) 是工程剪切应变,\(\varepsilon_{ij}\) 是应变张量的剪切分量。

剪切应变乘2,剪切应力不乘2。

这是本节最值得记住的约定之一。


三、立方晶体为什么只有三个独立弹性常数?

在线弹性范围内:

\ \\boldsymbol{s}=\\boldsymbol{C}\\boldsymbol{e} \\

在上述坐标系与剪切约定下,立方晶体的刚度矩阵为:

\ \\boldsymbol{C}= \\begin{bmatrix} C_{11}\&C_{12}\&C_{12}\&0\&0\&0\\\\ C_{12}\&C_{11}\&C_{12}\&0\&0\&0\\\\ C_{12}\&C_{12}\&C_{11}\&0\&0\&0\\\\ 0\&0\&0\&C_{44}\&0\&0\\\\ 0\&0\&0\&0\&C_{44}\&0\\\\ 0\&0\&0\&0\&0\&C_{44} \\end{bmatrix} \\

虽然矩阵有36个位置,但独立参数只有三个。

可以先这样理解:

  • \(C_{11}\):其他正应变固定为零时,本方向正应力对本方向正应变的响应;
  • \(C_{12}\):不同方向之间的正应力---正应变耦合;
  • \(C_{44}\):剪应力对相应工程剪切应变的响应。

一个重要提醒:\(C_{11}\) 通常不是杨氏模量

如果你把一根试样沿长度方向拉伸,允许它横向自由收缩,那么测量的是单轴应力条件下的响应。

本节提取 \(C_{11}\) 时,横向应变被固定为零。

加载条件不同,得到的模量一般也不同。

"沿一个方向拉伸"这句话还不够完整,必须进一步说明横向是自由的,还是被限制的。


四、设计两类应变路径,把三个常数分离出来

图1:两类应变路径示意。变形已夸大,不是实际原子模拟结果。

4.1 路径A:只施加一个轴向应变

令:

\ \\varepsilon_{11}=\\delta \\

其余应变分量为零:

\ \\varepsilon_{22}=\\varepsilon_{33}=0 \\\ \\gamma_{23}=\\gamma_{13}=\\gamma_{12}=0 \\

代入刚度矩阵,得到:

\ \\sigma_{11}=C_{11}\\delta \\\ \\sigma_{22}=\\sigma_{33}=C_{12}\\delta \\

因此:

\ C_{11}= \\frac{\\mathrm{d}\\sigma_{11}}{\\mathrm{d}\\delta} \\\ C_{12}= \\frac{\\mathrm{d}\\sigma_{22}}{\\mathrm{d}\\delta} \\

同一组轴向应变计算,可以同时提供两个常数的信息。

这里尤其要注意:

只有一个应变分量非零,并不意味着只有一个应力分量非零。

因为横向变形被约束,所以可能出现横向应力。

4.2 路径B:施加工程剪切应变

令:

\ \\gamma_{12}=\\gamma \\

也就是:

\ \\varepsilon_{12} = \\varepsilon_{21} = \\frac{\\gamma}{2} \\

其余应变分量为零,则:

\ \\sigma_{12}=C_{44}\\gamma \\

所以:

\ C_{44}= \\frac{\\mathrm{d}\\sigma_{12}}{\\mathrm{d}\\gamma} \\

严格来说,在本节Voigt顺序中,12剪切对应第六分量,其刚度项通常写作 \(C_{66}\)。由于立方晶体满足:

\ C_{66}=C_{44} \\

因此可以用这条路径提取 \(C_{44}\)。

但这个相等关系不能直接推广到任意晶系。

4.3 晶胞具体怎样变形?

轴向路径可以使用:

\ \\boldsymbol{F}_{\\mathrm{axial}}= \\begin{bmatrix} 1+\\delta\&0\&0\\\\ 0\&1\&0\\\\ 0\&0\&1 \\end{bmatrix} \\

对称小剪切路径可以使用:

\ \\boldsymbol{F}_{\\mathrm{shear}}= \\begin{bmatrix} 1\&\\gamma/2\&0\\\\ \\gamma/2\&1\&0\\\\ 0\&0\&1 \\end{bmatrix} \\

第二个式子在小变形一阶近似下给出所需剪切应变;有限应变度量中还存在二阶项,不能把它当作任意大剪切下的线性描述。

如果晶格矢量按列组成矩阵 \(\boldsymbol{A}\),那么:

\ \\boldsymbol{A}'=\\boldsymbol{F}\\boldsymbol{A} \\

如果按行存储,则应使用对应的转置关系:

\ \\boldsymbol{A}'_{\\mathrm{row}} = \\boldsymbol{A}_{\\mathrm{row}}\\boldsymbol{F}\^{\\mathsf T} \\

因此,修改晶胞文件前,必须先确认晶格矢量的存储方式。


五、用一组知道答案的数据,先验证方法

本节人为设置:

\ C_{11}=200\\ \\mathrm{GPa} \\\ C_{12}=120\\ \\mathrm{GPa} \\\ C_{44}=80\\ \\mathrm{GPa} \\

选取五个采样值:

\ -0.002,\\ -0.001,\\ 0,\\ 0.001,\\ 0.002 \\

对应数据如下。

注意:表格汇总的是两个独立加载路径,并不是一次同时施加轴向与剪切应变的计算。

路径A:\(\delta\) \(\sigma_{11}\)/GPa \(\sigma_{22}\)/GPa 路径B:\(\gamma_{12}\) \(\sigma_{12}\)/GPa
-0.002 -0.400 -0.240 -0.002 -0.160
-0.001 -0.200 -0.120 -0.001 -0.080
0 0 0 0 0
0.001 0.200 0.120 0.001 0.080
0.002 0.400 0.240 0.002 0.160

例如,当:

\ \\delta=0.001 \\

有:

\ \\sigma_{11}=200\\times0.001=0.200\\ \\mathrm{GPa} \\\ \\sigma_{22}=120\\times0.001=0.120\\ \\mathrm{GPa} \\

为什么既有正应变,又有负应变,还保留零应变点?

因为我们不只需要一个斜率,还希望检查:

  • 拉伸与压缩两侧是否一致;
  • 零应变时是否存在应力偏置;
  • 改变应变范围后,斜率是否变化。

这组应变大小只是教学设置,不是所有真实材料通用的计算参数。


六、拟合时,为什么不要一开始就强制过原点?

6.1 先使用含截距的模型

一般可写为:

\ \\sigma_i=b+Cx_i+r_i \\

其中:

  • \(b\):截距;
  • \(C\):待拟合斜率;
  • \(r_i\):残差。

最小二乘斜率为:

\ C= \\frac{ \\sum_i(x_i-\\bar{x})(\\sigma_i-\\bar{\\sigma}) }{ \\sum_i(x_i-\\bar{x})\^2 } \\

截距为:

\ b=\\bar{\\sigma}-C\\bar{x} \\

由于应变无量纲、应力使用GPa,斜率单位也是GPa。

残差均方根可以写为:

\ \\mathrm{RMSE} = \\sqrt{ \\frac{1}{n} \\sum_i \\left\[\\sigma_i-(b+Cx_i)\\right^2 } \]

6.2 什么情况下才考虑过原点?

如果有充分理由将截距固定为零,斜率变为:

\ C_{\\mathrm{origin}} = \\frac{\\sum_i x_i\\sigma_i}{\\sum_i x_i\^2} \\

但不能仅仅因为理论关系写成 \(\sigma=C\varepsilon\),就无条件忽略实际数据中的截距。

非零截距可能来自:

  • 参考结构尚未充分弛豫;
  • 数值残余应力;
  • 数据转换问题;
  • 真实存在的预应力。

其中,真实预应力不能通过简单扣除,就被当作不存在。


七、运行完整程序,并逐项核对输出

7.1 文件怎样使用?

本节主程序为:

lesson62_elastic_constants.py

复制代码
"""Fit synthetic small-strain stresses; no ABACUS calculation or material prediction."""
import argparse
import json
import math
from pathlib import Path

STRAIN = [-0.002, -0.001, 0.0, 0.001, 0.002]


def finite(x):
    return type(x) in (int, float) and math.isfinite(x)


def fit_line(x, y, fit_intercept=True):
    if len(x) != len(y) or len(x) < 3:
        raise ValueError("Need at least three matched strain-stress pairs")
    if type(fit_intercept) is not bool or not all(finite(v) for v in list(x)+list(y)):
        raise ValueError("Finite numerical data and boolean fit option required")
    xm, ym = sum(x)/len(x), sum(y)/len(y)
    denominator = sum((v-xm)**2 for v in x)
    if denominator <= 0:
        raise ValueError("Strain values need a nonzero spread")
    if fit_intercept:
        slope = sum((a-xm)*(b-ym) for a, b in zip(x, y))/denominator
        intercept = ym-slope*xm
    else:
        slope = sum(a*b for a, b in zip(x, y))/sum(a*a for a in x)
        intercept = 0.0
    residuals = [b-(intercept+slope*a) for a, b in zip(x, y)]
    sse = sum(r*r for r in residuals)
    total = sum((b-ym)**2 for b in y)
    return dict(slope_GPa=slope, intercept_GPa=intercept,
                rmse_GPa=math.sqrt(sse/len(x)),
                R2=None if total == 0 else 1-sse/total)


def cubic_checks(c11, c12, c44):
    if not all(finite(x) for x in (c11, c12, c44)):
        raise ValueError("Elastic constants must be finite numbers")
    margins = dict(C11_minus_C12=c11-c12, C11_plus_2C12=c11+2*c12, C44=c44)
    return dict(assumption="UNSTRESSED_CUBIC_SMALL_STRAIN", margins_GPa=margins,
                passed=all(v > 0 for v in margins.values()))


def calculate():
    x = STRAIN.copy()
    data = {name: [c*v for v in x] for name, c in (("C11", 200.), ("C12", 120.), ("C44", 80.))}
    fits = {name: fit_line(x, y) for name, y in data.items()}
    wrong_shear = fit_line([v/2 for v in x], data["C44"])
    offset_stress = [200*v+0.02 for v in x]
    positive_x = [0., 0.001, 0.002]
    positive_y = [200*v+0.02 for v in positive_x]
    nonlinear_y = [200*v+500000*v**3 for v in x]
    inner_x = [-0.001, 0., 0.001]
    inner_y = [200*v+500000*v**3 for v in inner_x]
    return dict(source="SYNTHETIC_STRESS", ABACUS_run=False, material_verified=False,
                convention=dict(voigt_order=["11", "22", "33", "23", "13", "12"],
                                shear="engineering_gamma=2*tensor_epsilon", stress_unit="GPa",
                                stress_sign="tension_positive", frame="crystal_axes"),
                strain=x, stresses_GPa=data, fits=fits, wrong_shear=wrong_shear,
                stability=cubic_checks(*(fits[k]["slope_GPa"] for k in ("C11", "C12", "C44"))),
                unstable_example=cubic_checks(100., 120., 80.),
                bulk_GPa=(fits["C11"]["slope_GPa"]+2*fits["C12"]["slope_GPa"])/3,
                offset=dict(stress_GPa=offset_stress, affine=fit_line(x, offset_stress),
                            symmetric_origin=fit_line(x, offset_stress, False),
                            positive_strain=positive_x, positive_stress_GPa=positive_y,
                            positive_affine=fit_line(positive_x, positive_y),
                            positive_origin=fit_line(positive_x, positive_y, False)),
                nonlinear=dict(stress_GPa=nonlinear_y, inner_strain=inner_x,
                               inner_stress_GPa=inner_y, inner=fit_line(inner_x, inner_y),
                               full=fit_line(x, nonlinear_y)))


def self_test():
    d = calculate()
    for k, expected in (("C11", 200), ("C12", 120), ("C44", 80)):
        assert math.isclose(d["fits"][k]["slope_GPa"], expected)
        assert abs(d["fits"][k]["intercept_GPa"]) < 1e-12
    assert math.isclose(d["wrong_shear"]["slope_GPa"], 160) and d["wrong_shear"]["R2"] == 1
    assert d["stability"]["passed"] and not d["unstable_example"]["passed"]
    assert not cubic_checks(120, 120, 80)["passed"]
    assert math.isclose(d["offset"]["affine"]["intercept_GPa"], .02)
    assert math.isclose(d["offset"]["symmetric_origin"]["slope_GPa"], 200)
    assert math.isclose(d["offset"]["positive_origin"]["slope_GPa"], 212)
    assert math.isclose(d["nonlinear"]["inner"]["slope_GPa"], 200.5)
    assert math.isclose(d["nonlinear"]["full"]["slope_GPa"], 201.7)
    invalid = [([0, 0, 0], [0, 1, 2]), ([0, 1, 2], [1, 2]),
               ([0, 1, 2], [0, float("nan"), 2]), ([False, 1, 2], [0, 1, 2])]
    for x, y in invalid:
        try:
            fit_line(x, y)
        except ValueError:
            pass
        else:
            raise AssertionError("Invalid fit data accepted")
    print("SELF_TEST=PASS (constants, shear convention, intercepts, range and input guards)")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--self-test", action="store_true")
    parser.add_argument("--out", type=Path, default=Path("lesson62_run"))
    args = parser.parse_args()
    if args.self_test:
        self_test()
        return
    if args.out.exists():
        parser.error("Output directory exists; choose a new --out path")
    d = calculate()
    for name, fit in d["fits"].items():
        b = 0.0 if abs(fit["intercept_GPa"]) < 1e-12 else fit["intercept_GPa"]
        print(f"{name}={fit['slope_GPa']:.3f} GPa; intercept={b:.6f} GPa; RMSE={fit['rmse_GPa']:.6f} GPa")
    margins = d["stability"]["margins_GPa"]
    print("stability_margins="+", ".join(f"{v:.3f}" for v in margins.values())+f" GPa; passed={d['stability']['passed']}")
    print(f"cubic_bulk_modulus={d['bulk_GPa']:.6f} GPa")
    print(f"wrong_tensor_shear_slope={d['wrong_shear']['slope_GPa']:.3f} GPa; R2={d['wrong_shear']['R2']:.6f}")
    off = d["offset"]
    print(f"offset_affine: slope={off['affine']['slope_GPa']:.3f} GPa; intercept={off['affine']['intercept_GPa']:.3f} GPa")
    print(f"offset_origin: symmetric={off['symmetric_origin']['slope_GPa']:.3f}; positive_only={off['positive_origin']['slope_GPa']:.3f} GPa")
    print(f"nonlinear_range: inner={d['nonlinear']['inner']['slope_GPa']:.3f}; full={d['nonlinear']['full']['slope_GPa']:.3f} GPa")
    print(f"all_positive_but_unstable={not d['unstable_example']['passed']}")
    print("MODE=SYNTHETIC_STRESS; ABACUS_run=False; material_verified=False")
    args.out.mkdir(parents=True, exist_ok=False)
    (args.out/"report.json").write_text(json.dumps(d, indent=2, allow_nan=False), encoding="utf-8")
    print(f"SAVED={args.out/'report.json'}")


if __name__ == "__main__":
    main()

这是一个独立脚本,不需要插入上一节代码。

主程序只使用Python标准库,不需要安装NumPy、Pandas或ABACUS。

先在文件所在目录打开终端,执行:

复制代码
python lesson62_elastic_constants.py --self-test

预期输出:

复制代码
SELF_TEST=PASS (constants, shear convention, intercepts, range and input guards)

再执行:

复制代码
python lesson62_elastic_constants.py --out lesson62_run

7.2 预期输出结果

复制代码
C11=200.000 GPa; intercept=0.000000 GPa; RMSE=0.000000 GPa
C12=120.000 GPa; intercept=0.000000 GPa; RMSE=0.000000 GPa
C44=80.000 GPa; intercept=0.000000 GPa; RMSE=0.000000 GPa
stability_margins=80.000, 440.000, 80.000 GPa; passed=True
cubic_bulk_modulus=146.666667 GPa
wrong_tensor_shear_slope=160.000 GPa; R2=1.000000
offset_affine: slope=200.000 GPa; intercept=0.020 GPa
offset_origin: symmetric=200.000; positive_only=212.000 GPa
nonlinear_range: inner=200.500; full=201.700 GPa
all_positive_but_unstable=True
MODE=SYNTHETIC_STRESS; ABACUS_run=False; material_verified=False
SAVED=lesson62_run\report.json

程序会拒绝覆盖已有输出目录。如果重复运行,请换一个目录名。

输出中的几项含义是:

  • 前三行:恢复预设的三个弹性常数;
  • intercept:拟合截距;
  • RMSE:应力拟合残差;
  • stability_margins:三个稳定性不等式左侧的值;
  • wrong_tensor_shear_slope:剪切定义混淆的反例;
  • offset:截距处理的反例;
  • nonlinear_range:应变区间选择的反例。

这里RMSE为零,是因为基础数据被构造成严格线性,不是因为真实DFT计算没有误差。

7.3 把计算结果画出来

图2:根据程序实际生成的人工数据绘制。横坐标以百分数显示,拟合时使用无量纲小数。

左图显示:

\ C_{11}=200\\ \\mathrm{GPa}, \\qquad C_{12}=120\\ \\mathrm{GPa} \\

右图则揭示一个很容易被忽略的问题:同一组应力,换一种横坐标定义,斜率就会改变。


八、三个反例:让错误变得看得见

8.1 反例一:\(R^2=1\),结果仍可能错一倍

正确关系是:

\ \\sigma_{12}=C_{44}\\gamma_{12} \\

又因为:

\ \\gamma_{12}=2\\varepsilon_{12} \\

所以:

\ \\sigma_{12}=2C_{44}\\varepsilon_{12} \\

因此:

\ \\frac{\\mathrm{d}\\sigma_{12}} {\\mathrm{d}\\varepsilon_{12}} = 2C_{44} = 160\\ \\mathrm{GPa} \\

相对张量剪切应变的斜率为160 GPa,这个数学结果没有错。

错误在于:把160 GPa未经转换,直接报告成工程剪切约定下的 \(C_{44}\)。

两种横坐标下的数据都严格成直线,因而都能得到:

\ R\^2=1 \\

所以:

拟合优度可以评价数据与模型的吻合程度,却不能替你检查物理定义。

8.2 反例二:忽略一个很小的偏置,会发生什么?

人为构造:

\ \\sigma_{11}=200\\delta+0.02 \\

应力单位为GPa。

保留截距拟合,可以恢复:

\ C=200\\ \\mathrm{GPa} \\\ b=0.02\\ \\mathrm{GPa} \\

如果使用正负对称的采样点,却强制过原点,由于:

\ \\sum_i\\delta_i=0 \\

常量偏置对斜率的影响恰好抵消,斜率仍为200 GPa。

但是,偏置仍然存在,此时RMSE为0.02 GPa。

斜率碰巧正确,不等于参考态无应力。

如果只使用:

\ \\delta=0,\\ 0.001,\\ 0.002 \\

再强制过原点,就会得到:

\ C_{\\mathrm{origin}} = 200+ 0.02 \\frac{\\sum_i\\delta_i}{\\sum_i\\delta_i\^2} = 212\\ \\mathrm{GPa} \\

由此可见,采样方式与拟合方式需要一起考虑。

8.3 反例三:曲线看起来近似直线,斜率却依赖区间

构造弱非线性关系:

\ \\sigma_{11} = 200\\delta+500000\\delta\^3 \\

其中应变无量纲,应力单位为GPa。

零应变处的切线斜率为:

\ \\left. \\frac{\\mathrm{d}\\sigma_{11}}{\\mathrm{d}\\delta} \\right\|_{\\delta=0} = 200\\ \\mathrm{GPa} \\

但使用不同范围做线性拟合,得到:

拟合数据 斜率/GPa
\(-0.001,0,0.001\) 200.500
\(-0.002,-0.001,0,0.001,0.002\) 201.700

图3:左图展示忽略截距的影响;右图展示弱非线性导致的区间依赖。

应变是不是越小越好?

也不是。

应变过大,高阶非线性影响增加;应变过小,应力变化可能接近数值噪声。

中心差分近似为:

\ C\\approx \\frac{\\sigma(+\\delta)-\\sigma(-\\delta)} {2\\delta} \\

假设两个应力值各自的绝对误差不超过 \(\eta\),则最坏情况下:

\ \|\\Delta C\| \\leq \\frac{\\eta}{\|\\delta\|} \\

例如:

\ \\eta=0.01\\ \\mathrm{GPa}, \\qquad \|\\delta\|=0.001 \\

则误差上界为:

\ 10\\ \\mathrm{GPa} \\

这是假设已知应力误差界时的估计,不能把相邻两组数值设置的差异直接当成严格误差界。

真正要寻找的是:

非线性影响和数值噪声都可接受的应变窗口。


九、所有弹性常数都为正,就代表弹性稳定吗?

不够。

对于无预应力的立方晶体,均匀小应变下弹性能严格正定的条件为:

\ C_{11}-C_{12}\>0 \\\ C_{11}+2C_{12}\>0 \\\ C_{44}\>0 \\

这些条件的适用假设以及严格不等式都不能省略。弹性稳定性条件原始论文

9.1 检查本节数据

\ C_{11}-C_{12} = 200-120 = 80\\ \\mathrm{GPa} \\\ C_{11}+2C_{12} = 200+240 = 440\\ \\mathrm{GPa} \\\ C_{44}=80\\ \\mathrm{GPa} \\

三项均为正,因此通过上述条件检查。

9.2 一个"常数都为正,却不稳定"的例子

设:

\ C_{11}=100,\\quad C_{12}=120,\\quad C_{44}=80 \\qquad(\\mathrm{GPa}) \\

虽然三个常数都是正数,但:

\ C_{11}-C_{12}=-20\\ \\mathrm{GPa} \\

因此不满足上述稳定性条件。

程序输出:

复制代码
all_positive_but_unstable=True

说的就是这个反例。

还要注意:

  • 取等号属于临界情形,不满足严格正定;
  • 有限预应力下不能直接照搬这些条件;
  • 弹性稳定性通过,不等于声子全波矢稳定、热力学稳定或有限温度下相稳定。

9.3 顺便计算体积模量

对本节立方晶体模型:

\ B= \\frac{C_{11}+2C_{12}}{3} \\

代入:

\ B= \\frac{200+2\\times120}{3} = 146.666667\\ \\mathrm{GPa} \\

这里与第60节示例中的160 GPa不同,因为两节使用了不同的人工构造数据,并不对应同一种材料。


十、怎样迁移到真正的ABACUS计算?

本节程序验证的是分析方法。迁移到真实计算,还需要完成以下工作。

10.1 准备合适的参考态

保存参考晶胞、原子坐标、坐标系、完整应力张量及计算设置。

需要强调:

平均压力接近零,不等于所有应力分量都接近零。

还应区分两种材料响应:

  • Clamped-ion:原子随晶胞应变作仿射变形,不进一步弛豫内部坐标;
  • Relaxed-ion:在固定已施加应变的晶胞下,允许内部原子坐标按定义弛豫。

不能把两种定义下的数据混在一起拟合。

10.2 检查应力所需的数值精度

延续上一节,检查基组、k点、电子收敛阈值及目标弹性常数的变化。

应力和弹性常数可能需要比常规总能量判断更严格的数值检查。参考结构、施加应变、计算应力和拟合张量的基本流程,可以对照Materials Project弹性计算方法说明。

其中针对其他软件的参数,不应直接当作ABACUS推荐设置。

10.3 保留施加的应变

对于每个应变构型,记录:

  • 路径名称;
  • 应变值;
  • 实际晶格;
  • 原子坐标;
  • 对应计算目录。

如果要计算relaxed-ion响应,应在固定应变后的晶胞下弛豫内部坐标。

如果对每个构型都完全放开晶胞重新弛豫,外加应变可能被消除,计算就偏离了原先设计的路径。

10.4 按实际软件版本启用、读取应力

本课程查阅的ABACUS v3.8.5手册中,cal_stress 用于控制应力计算。实际使用时,应核对自己安装版本的文档和日志,而不是仅依据任务正常结束就认为应力数据可用。ABACUS v3.8.5官方手册

这里引用固定版本是为了明确来源,并不表示它是最新版本。

10.5 建立明确的数据转换层

拟合之前,逐项核对:

  • 应力单位;
  • 拉伸与压缩的正负号;
  • 分量顺序;
  • 坐标系;
  • 剪切定义;
  • 应变与应力记录的对应关系。

例如,若原始输出明确使用kbar:

\ 1\\ \\mathrm{kbar} = 0.1\\ \\mathrm{GPa} \\

但单位换算正确,并不代表符号约定也正确。

不能为了让弹性常数变正,就对原始应力取绝对值。

本节脚本没有实现ABACUS日志解析,而是直接生成已统一约定的GPa教学数据。

10.6 做完整性检查,而不只检查三个数字

真实计算还应检查:

  1. 参考应力和拟合截距;
  2. 不同应变区间得到的常数;
  3. 不同加载方向的一致性;
  4. 对称性要求为零的耦合项;
  5. 刚度矩阵对称性;
  6. 失败计算、剔除数据及相关理由。

本节的两类路径是在已知立方对称的假设下提取最小独立参数,不能单独用来证明未知材料具有立方对称性。


十一、课堂练习

练习1:检查剪切定义

已知:

\ \\gamma_{12}=0.002, \\qquad \\sigma_{12}=0.16\\ \\mathrm{GPa} \\

求 \(C_{44}\)。

参考答案:

\ C_{44} = \\frac{0.16}{0.002} = 80\\ \\mathrm{GPa} \\

如果横坐标改为张量剪切应变:

\ \\varepsilon_{12}=0.001 \\

斜率将变为160 GPa,需要除以2才能恢复本节工程剪切约定下的 \(C_{44}\)。

练习2:本节杨氏模量是不是200 GPa?

不是。

立方晶体沿100方向,在单轴应力条件下:

\ E_{\[100}= \frac{ (C_{11}-C_{12})(C_{11}+2C_{12}) }{ C_{11}+C_{12} } \]

代入本节数据:

\ E_{\[100} = \frac{80\times440}{320} = 110\ \mathrm{GPa} \]

这个结果进一步说明:加载条件不同,模量的含义也不同。

练习3:截距不为零,直接减掉是否就解决了问题?

不能这样下结论。

应先判断它来自数值残余、参考态问题,还是需要保留的真实预应力。

含截距拟合可以帮助发现问题,却不能替代参考态与应力收敛检查。

练习4:修改剪切常数,观察哪些输出变化

复制主程序到新的练习文件,把构造数据中的:

\ C_{44}=80\\ \\mathrm{GPa} \\

改成:

\ C_{44}=60\\ \\mathrm{GPa} \\

并同步修改相关自测期望值。

预期:

  • 正确剪切拟合得到60 GPa;
  • 相对张量剪切应变的斜率变为120 GPa;
  • \(C_{44}>0\) 仍成立;
  • 体积模量保持146.666667 GPa。

因为体积模量只涉及 \(C_{11}\) 和 \(C_{12}\)。


十二、本节总结与下一节安排

本节最重要的,不是记住200、120和80这三个数,而是形成以下判断习惯:

  • 先说清加载条件,再解释斜率;
  • 先统一单位、坐标与剪切定义,再拟合;
  • 同时检查截距、残差与应变区间;
  • 稳定性结论必须注明适用条件;
  • 人工数据、软件输出与真实材料验证必须分开记录。

一条漂亮的拟合直线,是分析的开始,不是科学验证的终点。

相关推荐
二宝哥1 小时前
02.springboot项目对接国内模型实现对话聊天
数据库·人工智能·spring boot
kaixin_啊啊1 小时前
【零基础学AI】第 17 章——一个机器学习项目怎样完成
人工智能·机器学习
Elastic 中国社区官方博客1 小时前
5 分钟内将 56 页扫描文档转化为带引用的答案:面向搜索无法读取的文档的检索增强生成流水线
大数据·运维·人工智能·elasticsearch·搜索引擎·信息可视化·全文检索
天天代码码天天1 小时前
我用 C# 做了个运动计数器:打开摄像头,让电脑帮你数次数
人工智能
STLearner1 小时前
KDD 2026 | (2月轮)时空数据(Spatial-Temporal)论文总结时空【下】(人群移动,城市科学(资源调度,)
大数据·人工智能·python·深度学习·机器学习·语言模型·数据挖掘
云和数据.ChenGuang1 小时前
python sorted函数详细剖析
开发语言·人工智能·windows·python·1024程序员节
打工仔折腾 AI2 小时前
AI Agent 工程化实战(二):5 分钟跑通 Deep Agent 并逐层拆解底层链路
java·数据库·人工智能·后端·python·langchain·ai agent 实战
程序猿编码2 小时前
YOLOv5/v7/YOLOX 全支持:基于 TensorRT 的工业视觉推理底座详解
c++·yolo·机器学习·推理·模型推理
云和数据.ChenGuang2 小时前
Python map() 函数详解:从语法、原理到实战用法
开发语言·人工智能·python·langchain·1024程序员节