课程摘要
本节承接应力与数值收敛检查,以无预应力立方晶体为例,讲解如何通过正负小应变与应力响应拟合C₁₁、C₁₂和C₄₄。结合可运行Python程序与图示,辨析工程剪切与张量剪切,分析残余应力、拟合截距和应变区间对结果的影响,并进行立方晶体弹性稳定性检查。全部数据为明确标注的人工构造数据,用于验证计算流程,不代表真实材料的第一性原理计算结果。
一、从一个问题开始:材料"有多硬",到底怎样算?
上一节,我们讨论了第一性原理计算中的收敛检查,其中一个重要结论是:
总能量看起来稳定,并不自动意味着应力已经足够准确。
现在,假设我们已经获得一个合适的参考晶胞,也能够读取计算得到的应力。
把晶胞沿一个方向轻轻拉长,记录应力;再换几个拉伸和压缩幅度,继续记录。
能否从这些数据中得到材料的弹性常数?
可以。但在点击"拟合"之前,我们必须回答三个问题:
- 施加的究竟是什么应变?
- 读取的是哪个应力分量?
- 拟合得到的斜率,对应哪个弹性常数?
如果这些问题没有说清楚,即使拟合曲线十分漂亮,结果也可能错误。
本节要建立的分析过程是:
参考态检查 → 设计应变路径 → 获取应力 → 统一约定 → 拟合与诊断 → 稳定性检查。
本节学习目标
完成本节后,你应当能够:
- 理解立方晶体的三个独立弹性常数;
- 通过两类应变路径提取 \(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:检查剪切定义
已知:
\ \\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这三个数,而是形成以下判断习惯:
- 先说清加载条件,再解释斜率;
- 先统一单位、坐标与剪切定义,再拟合;
- 同时检查截距、残差与应变区间;
- 稳定性结论必须注明适用条件;
- 人工数据、软件输出与真实材料验证必须分开记录。
一条漂亮的拟合直线,是分析的开始,不是科学验证的终点。
