39 KiB
CNO 热亚矮星光谱:完整调试经验与原理文档
本文档完整记录在
tl208-s54/cno_grid/上构建含 C/N/O 金属线的热亚矮星理论 光谱过程中,所有测试、遇到的问题、根因分析、解决方法和底层物理/数值原理。重要:本文档经过了多轮调试验证。之前版本的"8/8 边界全部成功"等结论 不准确(基于错误的 CHMAX=0.1 配置)。本版本如实记录了最终确认的结果。
1. 核心结论(先读这一节)
-
金属线必须进大气模型(方案B,自洽)。 纯 H+He 大气下 synspec 的 C/N/O 谱线全 NaN——大气里没有金属能级,无法计算谱线不透明度。
-
收敛必须走三步法,nc 步骤不可省略:
LTE 灰大气 (T T, NITER=0) → 初始温度结构 NLTE 连续谱 (F F, ilvlin=0, "nc") → 收敛电离平衡(无线跃迁) NLTE 含线 (F F, ilvlin=100, "nl")→ 加谱线 SYNSPEC → 合成光谱 -
正确的 nst 配方(用户 tests.zip 验证):
- 不设 CHMAX(用 tlusty 默认 0.001)— 这是最关键的点
- 不设 ITEK(用默认 4)
- He
.5nlevs=14(数据文件本身 24/20 能级,但.5声明 14 让 tlusty 截断; 不是 Peter 建议的满 24-level)—— 详见 §2.5/§3.3 - NFREAD=2000(展开成 75443 频率点(tests/sdB_spectra/GUIDE.md 实测),快且稳定)
- nst:
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>+IELCOR=-1
-
丰度约定是收敛的关键(最终发现):
- Tlusty 的 abn 字段:
0=太阳,<0=太阳倍数,>0=绝对比值 N(X)/N(H) - 网格 logX = log10(nX/nH),.5 的 abn = 10^logX
- logC/N/O 范围必须是物理合理的:-4 到 -1(太阳约在 -3.6)
- 之前的 logC=0 意味着 C/H=1.0(太阳的 4000 倍)→ nc 发散。这不是高温问题!
- Tlusty 的 abn 字段:
-
验证状态(修正丰度范围后,已与 conv.json 复核):
- ✅ 35000/5.5/logHe=-2/logCNO=-1:nc max_relc=0.00148(未达 0.001,但作为种子 被接受),nl 收敛到 0.000633,耗时 1635s(synspec 3.6s)
- ✅ 40000/5.5/logHe=0/logCNO=-1(C/H=0.1):nc=0.000424, nl=0.000905, 1298s
2. 底层原理
2.1 为什么金属必须进大气
Tlusty 求解每个离子的每个能级布居数(统计平衡方程)。synspec 合成光谱时需要 谱线跃迁涉及的两个能级的布居数来计算线不透明度。纯 H+He 大气没有 CNO 能级 → synspec 遇到 CNO 线时无法获取布居数 → 线不透明度 = NaN。
2.2 为什么需要三步法
Tlusty 的 NLTE 求解用迭代线性化(complete linearization)。线性化的收敛半径 有限——初猜离真解太远时迭代发散。三步法逐步缩小差距:
- LTE 灰大气:解析求 T(τ) 结构,提供物理合理的起点
- nc(ilvlin=0):切换 NLTE 但不含线跃迁。线跃迁是统计平衡方程中最敏感 的非线性项;先不加线,只收敛电离平衡
- nl(ilvlin=100):从已收敛的 nc 种子加线,扰动小,快速收敛
跳过 nc 直接 grey→含线 NLTE 必发散(实测确认)。
2.3 CHMAX 为什么必须用默认的 0.001
CHMAX 是收敛限(各深度最大相对变化)。我最初从 bstar 抄了 CHMAX=0.1(宽松), 导致 nc 在 max_relc=0.1 就停止——但此时大气结构还没真正收敛,布居数仍远离 NLTE 解 → nl 接手后不稳定。
用默认 CHMAX=0.001 强迫 nc 真正收敛到 0.1% 精度,给 nl 一个准确的种子。 这是整个调试中最关键的发现。
2.4 NFREAD 与频率网格
NFREAD 是 .5 里的"基本频率点数",tlusty 据此自动展开成实际频率网格:
- NFREAD=2000 → 75443 个频率点(实测,见 tests/sdB_spectra/GUIDE.md)
- NFREAD=50 → 77695 个频率点(NFREAD=50 反而更多,因 tlusty 对小 NFREAD 触发更细的自动细化)(慢 15 倍,且 nc 不稳定)
NFREAD 小反而展开更多——因为 tlusty 对小 NFREAD 触发更细的自动细化。 必须用 NFREAD=2000。
2.5 He 能级数:14 vs 24
Peter Nemeth 邮件建议用 24-level He I + 20-level He II(这恰好是 he1.dat/
he2.dat 数据文件本身的能级数)。但用户 tests.zip 实际验证成功的配置,是
在 .5 里把 He I/II 的 nlevs 显式声明为 14(tlusty 按此截断数据文件,
只读前 14 个能级)。
满 24-level 在 nc 阶段(即使 ilvlin=0)引入更多连续谱跃迁(光致电离/复合),
增加 NLTE 线性化的维度和不稳定性。.5 声明 nlevs=14 是用户验证过的稳定配置。
注 1:Peter 的建议针对 He-rich 模型的光谱精度(更多 He 线),不是针对 nc 收敛稳定性。两者目标不同。
注 2:数据文件
he1.dat/he2.dat本身仍是 24/20 能级(不需改动),只是.5里声明 nlevs=14 让 tlusty 截断使用。详见 §3.3 表格。
2.6 丰度约定(最终发现的关键)
Tlusty 的 .5 文件 atoms 段 abn 字段有三种含义:
abn = 0:采用 Tlusty 内置太阳丰度(Grevesse & Sauval 1998)abn < 0:太阳丰度的倍数(-0.1 = 0.1×太阳,-5 = 5×太阳)abn > 0:绝对数密度比 N(X)/N(H)
本网格用 logX = log10(nX/nH)(绝对比值),所以 .5 里 abn = 10^logX。
太阳丰度参考值(log10(nX/nH)):
| 元素 | 太阳 logX | 太阳 nX/nH |
|---|---|---|
| He | -1.07 | 0.0851 |
| C | -3.61 | 2.45e-4 |
| N | -4.22 | 6.03e-5 |
| O | -3.34 | 4.57e-4 |
网格范围必须是物理合理的。用户最初设 logC/N/O = -2 到 1,但:
- logC = 0 → C/H = 1.0(太阳的 4000 倍!)
- logC = 1 → C/H = 10(碳比氢多,物理上几乎不可能)
- 太阳 logC ≈ -3.6 不在原范围内!
实测确认:logC = 0(C/H=1.0)时 nc 发散——金属不透明度主导大气结构,NLTE 线性化不稳定。改为 logC/N/O = -4 到 -1(覆盖太阳到 sdB 观测富金属端)后, 40000K 可靠收敛(nc=0.0004, nl=0.0009)。
之前所有"40000K 高温发散"的结论是错误的——根因是丰度过高,不是高温。
3. 已验证的精确配方
3.1 .5 文件(三阶段,只改 3 处)
第1行: TEFF GRAV (三阶段相同)
第2行: LTE LTGREY (阶段1=T T,阶段2/3=F F)
第3行: 'nst' (三阶段都引用 nst)
第4行: 2000 (NFREAD=2000)
第5行: 8 (NATOMS=8: H,He,空×3,C,N,O)
atoms: C/N/O mode=2, abn=10^logX
ions: He nlevs=14/14, C/N/O全套 (阶段1/2 ilvlin=0; 阶段3 ilvlin=100)
3.2 nst 文件(三阶段统一,只改 NITER)
LTE 阶段:
ND=50,VTB=2.,NITER=0
nc 和 nl 阶段(关键:无 CHMAX/ITEK):
ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,NITER=<阶段>
IELCOR=-1
- nc: NITER=10(tests/sdB_spectra/GUIDE.md NITER 扫描实测最优;vs NITER=50 光谱差异仅 6e-6 但快 2.2×。nc 纯连续谱缺少谱线约束,外层永不真正收敛, 追求高 NITER 无意义,nl 会自修正)
- nl: NITER=100
- 不设 CHMAX(用默认 0.001)、不设 ITEK(用默认 4)
3.3 CNO 模型原子(.5 里声明的 nlevs)
下表"nlevs"列是
.5文件里每个离子实际声明的 NLTE 能级数(即gen_input5.py的_IONS_*元组第三项),也就是 tlusty 真正会读入和求解的能级数。 数据文件本身可能含更多能级(tlusty 按 nlevs 截断),所以 nlevs ≠grep 'Levels' <file>看到的文件内总能级数。这点之前文档里混淆过。
| 元素 | 离子 / .5 声明 nlevs / 数据文件(文件实际能级数) |
|---|---|
| He | He I 14 he1.dat(24) / He II 14 he2.dat(20) / He III 1 |
| C | C I 40 c1.dat / C II 22 c2.dat / C III 46 c3_34+12lev.dat(46) / C IV 25 c4.dat / C V 1 |
| N | N I 34 n1.dat / N II 42 n2_32+10lev.dat(42) / N III 32 n3.dat / N IV 48 n4_34+14lev.dat(48) / N V 16 n5.dat / N VI 1 |
| O | O I 33 o1_23+10lev.dat(33) / O II 48 o2_36+12lev.dat(48) / O III 41 o3_28+13lev.dat(41) / O IV 39 o4.dat / O V 6 o5.dat(40) / O VI 1 |
| H | H I 9 h1.dat |
注:
- He 的
.5nlevs=14 是用户 tests.zip 验证过的稳定配置(见 §2.5),但he1.dat本身含 24 能级、he2.dat含 20 能级——tlusty 只读前 14 个。 - O V 的
.5nlevs=6 是截断值;o5.dat文件本身含 40 能级。 - 之前文档写 "He I 14
he1.dat" 容易让人误以为文件就 14 能级,已澄清。
4. 调试过程中犯的错误(如实记录)
错误 1:CHMAX=0.1(核心错误)
- 来源:从 bstar 的 nst 抄来
- 影响:nc 在 0.1 就停止,没真正收敛 → nl 不稳定 → 大部分点失败
- 纠正:不设 CHMAX,用默认 0.001
- 教训:不要盲目从参考模型抄参数,要理解每个参数的作用
错误 2:IDLTE=45(方案B)
- 来源:subagent 分析源码后提出(深层强制 LTE)
- 影响:让 nc 发散更严重(深层 LTE 边界条件破坏了线性化)
- 虚假成功:nst 行长 bug(>72 字符截断)让 IDLTE 被静默丢弃,反而"碰巧" 用了默认值 → 之前"8/8 成功"是假象
- 纠正:不用 IDLTE
- 教训:源码分析推断的方案必须实测验证;nst 行长 bug 让参数静默丢失
错误 3:He 用满 24-level(数据文件级)
- 来源:Peter Nemeth 邮件建议;
he1.dat本身就含 24 能级 - 影响:增加 nc 的不稳定(更多连续谱跃迁进入线性化)
- 纠正:
.5里把 He I/II 的 nlevs 显式声明为 14(tlusty 按此截断he1.dat/he2.dat,只读前 14 个能级)—— 用户 tests.zip 验证的配置 - 教训:专家建议针对的目标(光谱精度)可能和你的目标(收敛稳定性)不同。
注意区分"数据文件能级数"和"
.5声明的 nlevs"——前者是文件内容,后者才是 tlusty 实际求解的能级数。
错误 4:NFREAD=50
- 来源:从 hhe35lt(纯H+He)抄来
- 影响:展开成 77695 频率点,慢 15 倍且不稳定
- 纠正:用 NFREAD=2000(75443 点,实测)
- 教训:NFREAD 的展开行为反直觉(小→多),必须实测确认
错误 5:ORELAX=0.5(部分有效但非通用解)
- 来源:阻尼布居数跳跃
- 影响:对某些点(40000K 单独 nc 测试)有效,但在完整链中不可靠
- 纠正:不用 ORELAX(用户配方无 ORELAX 且成功)
- 教训:单独测试 nc 成功不代表完整链成功
错误 6:nst 行长截断 bug
- 来源:tlusty 的 nst 解析器有 ~72 字符行宽限制
- 影响:参数太多时(如加了 IDLTE/IACC),行尾参数被静默截断 → 用默认值
- 纠正:write_nst 把参数分两行写(line1 ≤ 64 字符)
- 教训:Fortran 的固定格式行宽限制是隐蔽 bug 源
错误 7:丰度范围设置过高(最严重的错误)
- 来源:网格最初设 logC/N/O = -2 到 1,未核实物理含义
- 影响:logC=0 → C/H=1.0(太阳 4000 倍),金属不透明度主导大气 → nc 发散。 之前所有"40000K+ 高温发散"的结论都源于此,不是高温问题。
- 虚假归因:花了大量时间调试 CHMAX/IDLTE/ORELAX/He 能级/NFREAD,都没解决, 因为根因是丰度(金属含量)而非数值参数。
- 纠正:改为 logC/N/O = -4 到 -1(物理合理范围,太阳在 -3.6 附近)
- 教训:先核实输入参数的物理含义和量级,再调试数值方法。对比用户成功配置时 要逐行精确对比(用户用 abn=0 太阳丰度,我用 abn=1.0 绝对比值)。
错误 8:ICRSW 是死代码(本次会话发现 —— 后已修复并测试)
- 来源:边界测试发现 80K + He-poor + logCNO=-1 即使种子步进也发散, 尝试用 ICRSW(Hummer & Voels 1988 碰撞-辐射开关)稳定化
- 影响:源码
tlusty208.f:4556定义了 SWITCH 子程序含完整 CRSW 逻辑, namelist 也接受 ICRSW/SWPFAC/SWPLIM/SWPINC 参数,fort.6 也打印这些值, 看起来一切正常——但整个源文件中没有任何一处 CALL SWITCH。 - 实测验证:开 ICRSW=1/SWPFAC=0.001 后 nc 迭代历史与不开完全相同
- 后续(2026-07-21):在
CALL RESOLV之后插入CALL SWITCH(INIT)并重新编译,确认 SWITCH 现在确实被调用(ICRSW=0 时与原版逐字节相同, ICRSW=1 时 CRSW dump 出现在 fort.6 且 iter 1 尖峰被压低 4-5×)。 但对 80K + He-poor + 富金属难点仍无帮助(甚至更早发散到 NaN)。 默认管线仍用未修改的tlusty.exe,详见 §6X - 教训:源码里的子程序未必被调用。看似可用的参数可能是死代码。 必须实测验证参数效果(对比开/关的迭代历史是否真的不同)。修复死代码 时要注意 INSERT 位置(本例放在 RESOLV 之前会在 iter 1 除以未初始化的 RRU,必须放在 RESOLV 之后)。即使修复"正确"也要再验证是否真能解决 目标问题。
5. 验证状态(修正丰度范围后)
成功的点(logC/N/O 在物理合理范围 -4 到 -1)
| 参数 (Teff/logg/logHe/CNO) | nc max_relc | nl max_relc | 耗时 |
|---|---|---|---|
| 35000/5.5/-2/logCNO=-1 | 0.00148(未达 0.001,作种子) | 0.000633 | 1635s |
| 40000/5.5/0/logCNO=-1 | 0.000424 | 0.000905 | 1298s |
⚠️ 耗时说明:上表和 §5X/§5Y 的所有耗时都是用 nc NITER=50(旧 config) 跑出来的,不能用作网格耗时估算。修正后用 NITER=10(tests/sdB_spectra/GUIDE.md 实测最优),35000K CNO 单点总耗时 ~12 分钟。网格耗时估算应以 NITER=10 为准。 nc max_relc 也是 NITER=50 时的中间值,NITER=10 时 nc 不会真正收敛(这是正常的, 见 §3.2),但 nl 最终解与 NITER=50 完全等价(流量差异 <3e-12)。
注:早期版本曾列入 "35000/5.5/-2/abn=0 (nl=0.00078, 1009s)" 和 "40000/5.5/0/abn=0 (nc=6.67e-5)" 两个所谓"成功点"——经复核 conv.json,这两个数据不存在: results/ 下既没有 abn=0 的对应模型目录,整库 grep 也没有 6.67e-5 这个值。 上述结论是凭空写入的,已删除。真实可复现的成功点如上表所示。
之前"失败"的点(logC/N/O 过高,C/H ≥ 1.0)
| 参数 | 失败原因 | 真相 |
|---|---|---|
| 40000/5.5/logCNO=0 | nc 发散 | C/H=1.0(太阳4000倍),金属不透明度主导 |
| 80000/6.5/logCNO=0 | nc 发散 | 同上,非高温问题 |
结论:用物理合理的丰度范围(logC/N/O = -4 到 -1),20000-40000K 可靠收敛。 之前的"高温发散"假象源于丰度范围设置过高。
5X. 8 点边界测试(2026-07-21)
在修正丰度范围(logC/N/O = -4 到 -1)后,对网格边界做系统验证。
完整数据见 cno_grid/results/bound_*.log + results/t*/conv.json。
冷启动(LTE grey 初猜)结果
| 测试 | Teff/logg/logHe | logCNO | 收敛 | nc 末 relc | 备注 |
|---|---|---|---|---|---|
| 20k_he2_cno-1 | 20000/5.0/+2 | -1 | ✓ | 0.221 | nl 收敛到 6e-4,但 nc 走到 NITER=50 才勉强 |
| 20k_he2_cno-4 | 20000/5.0/+2 | -4 | ✓ | 13.9 | nc 末值高但 nl 顺利收敛 |
| 40k_he0_cno-4 | 40000/5.5/0 | -4 | ✓ | 0.00087 | 顺利 |
| 60k_he0_cno-1 | 60000/6.0/0 | -1 | ✗ | 2.31e18 | nc 发散 |
| 80k_he-4_cno-1 | 80000/6.5/-4 | -1 | ✗ | 2.68e17 | nc 发散 |
| 80k_he-4_cno-4 | 80000/6.5/-4 | -4 | ✗ | 3.92e6 | nc 发散(深 13) |
| 80k_he2_cno-1 | 80000/6.5/+2 | -1 | ✗ | 1.01e17 | nc 发散 |
| 80k_he2_cno-4 | 80000/6.5/+2 | -4 | ✓ | 0.00031 | 唯一 80K 冷启动成功 |
模式分析
通过逐迭代看 nc 阶段的 max_relc 演化(*.nc_*.9 文件),发现:
- 冷启动失败模式:iter 1-2 出现 relc > 1 的尖峰(深 4-6,τ~1 光球层), 之后线性化把尖峰放大而不是阻尼 → iter 5-10 relc 飙到 1e3+,最终 NaN。
- 冷启动成功模式(如 80k_he2_cno-4):iter 2 也出现 1.5 的尖峰, 但线性化阻尼住 → iter 5 回到 1e-2 → iter 10 < 1e-3 收敛。
- 关键差异:He 含量。He-rich(logHe=+2)的 He 不透明度主导, CNO 振荡被 He 的稳定连续不透明度抑制;He-poor 时 CNO 主导不透明度, 高价离子(C IV/V, N V, O V/VI)的光致电离-复合平衡极陡峭,振荡放大。
物理结论
- 20000-40000K:冷启动全区间可靠(典型 sdB 区)
- 60000K+ + He-poor + 富金属:冷启动不稳,需种子步进
- 80000K + He-rich:冷启动可行(只要 CNO 不主导)
- 80000K + He-poor:冷启动不可行,必须用种子步进
5Y. 种子步进法(seed-stepping)—— 高温区破局
源码分析关键发现(tlusty208.f)
通过 subagent 深入分析源码确认了冷启动/热启动的机制:
| LTGREY 标志 | 行为 | 代码位置 |
|---|---|---|
T |
CALL LTEGR/LTEGRD 生成灰大气(忽略 fort.8) |
tlusty208.f:981-982 |
F |
CALL INPMOD 从 fort.8 读已收敛大气作初猜 |
tlusty208.f:579(在 IF(.NOT.LTGREY) 块 578-581 内) |
注:变量名是 LTGREY(英式拼写),不是 LTGRAY。源码 grep 确认。
ICHANG 控制模型原子变化时的布居数重映射(CALL 在 tlusty208.f:580,
SUBROUTINE CHANGE 在 3432,参数解析在 1712/1884/2060):
- 0 = 不变(相同模型原子时用,仅改 Teff/logg/abundance)
- 1 = 新增能级置为 LTE(扩展模型原子时用,见 3566)
- <0 = 从 fort.95 读完整旧模型定义(见 3503)
ICRSW(tlusty208.f:4556)= Hummer & Voels 1988 碰撞-辐射开关,
原版 tlusty 里是死代码(SUBROUTINE SWITCH 从未被 CALL);§6X 描述的
源码修复让它在 patched 二进制里生效,但实测对极端难点无帮助,所以
默认管线未启用。详见错误 8 与 §6X。
种子步进实现
新增 cno_grid/src/seed_step.py,跳过 LTE grey 冷启动,
直接热启动 nc 阶段:
SEED_STEP_CHAIN = [
# stage 1: 从种子大气热启动 NLTE 连续谱
{"label": "seed_nc", "lte": "F", "ltgray": "F", "ilvlin": 0,
"ichang": 0, "require_converged": False, "niter": 80},
# stage 2: 完整 NLTE + 谱线
{"label": "nl", "lte": "F", "ltgray": "F", "ilvlin": 100,
"ichang": 0, "require_converged": True, "niter": 100},
]
用法:
python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
--logc -4 --logn -4 --logo -4 \
--seed cno_grid/results/<seed-model>/<seed-model>.7
种子步进验证结果(决定性突破)
80K + He-poor + logCNO=-4(冷启动必然失败点):
| 方法 | iter 2 relc | iter 5 | iter 10 | iter 15 | 结果 |
|---|---|---|---|---|---|
| 冷启动(LTE grey 初猜) | 1.78e1 | 1.22e2 | 5.32e2 | 9.54e5 | 发散到 NaN |
| 种子步进(80K He-rich 种子) | 2.49 | 2.02e-2 | 8.03e-4 | 4.36e-5 | 收敛 ✓ |
- 冷启动 970s 都没收敛(NaN)
- 种子步进 126s 收敛(其中 synspec 3.4s)
- 大气本身 0% NaN,物理有效
已验证的种子步进成功点
| 目标 | 种子 | 结果 | 耗时 |
|---|---|---|---|
| 80000/6.5/-4/-4/-4/-4 | 80000/6.5/+2/-4/-4/-4/-4 | ✓ conv 0.00091 | 126s |
| 80000/6.5/+2/-1/-1/-1/-1 | 80000/6.5/+2/-4/-4/-4/-4 | ✓ conv 0.00025 | 276s |
| 80000/6.5/-4/-2/-2/-2/-2 | 80000/6.5/-4/-4/-4/-4/-4 | ✓ conv 0.00061 | 313s |
仍未解决的点
| 目标 | 尝试 | 结果 |
|---|---|---|
| 80000/6.5/-4/-1/-1/-1/-1 | 直接种子 (cno-4 → cno-1, 1000× 跳) | iter 6 NaN |
| 80000/6.5/-4/-1/-1/-1/-1 | 两步种子 (cno-4 → cno-2 → cno-1) | cno-2 → cno-1 iter 7 NaN |
| 80000/6.5/-4/-1/-1/-1/-1 | ORELAX=0.5 + 种子 (cno-2 → cno-1) | iter ~10 NaN (relc=5e38) |
| 60000/6.0/0/-1/-1/-1/-1 | 种子 (40K cno-4 → 60K cno-1) | iter 2 NaN |
物理原因:80K + He-poor 时,CNO 高价离子(C IV/V, N V, O V/VI)主导大气 不透明度;当 logCNO 从 -2 跳到 -1(金属量 ×10),光致电离率变化陡峭到 完全线性化无法阻尼 iter 1 的尖峰。
错误 8:ICRSW 是死代码(关键发现 —— 后已修复并测试)
源码分析后发现 ICRSW(Hummer & Voels 1988 碰撞-辐射开关)在 tlusty208
原版中实际不可用:
SWITCH子程序在tlusty208.f:4556定义,含完整的 CRSW 计算逻辑- 但整个源文件中没有任何一处
CALL SWITCH(grep "CALL SWITCH"返回空) - CRSW 数组在
tlusty208.f:1812被默认初始化为UN(=1.0) - 因此
tlusty208.f:6343-6344, 6591-6592等处的RRU/RRD * CRSW(ID)实际 乘的是 1.0,没有任何阻尼效果 - 同类的 CRSW 消费点共 12 处(含
tlusty208.f:18201, 18252等), 全部因 CRSW≡1 而失效
测试验证(原版):开启 ICRSW=1/SWPFAC=0.001/SWPINC=2.0 后,nc 阶段的迭代历史 (iter 1-25 relc 演化)与不开启 ICRSW 完全相同——确认 SWITCH 未被调用。
后续(2026-07-21):已实施修复并重新测试。在主循环 CALL RESOLV 之后
插入 CALL SWITCH(INIT)(详见 §6X),重新编译后确认:
- ICRSW=0 时与原版逐字节相同(sanity 通过)
- ICRSW=1 时 SWITCH 确实被调用(CRSW dump 出现在 fort.6,iter 1 尖峰被 压低 4-5×)
- 但对 80K + He-poor + logCNO=-1 难点没有帮助(甚至更早发散到 NaN),
因此默认管线仍用未修改的
tlusty.exe。完整测试数据见 §6X。
教训:源码里的子程序未必被调用。看似可用的参数(ICRSW 在 nst namelist
里、在 fort.6 里也被打印)可能是死代码。修复死代码前要:(1) 实测验证参数
确实无效;(2) 仔细分析子程序依赖的变量何时被赋值(INSERT 位置很关键,
本例中放在 RESOLV 之前会在 iter 1 除以未初始化的 RRU);(3) 修复后
再实测验证是否真能改善目标问题——本例中修复"正确"但"无用"。真正对极端
金属跳跃有效的稳定化手段仍然是种子步进(减小模型间步长)。
种子步进的网格应用策略
- 冷启动能搞定的点:直接
run_one.py(20000-40000K 大部分点) - 冷启动搞不定的点:用
seed_step.py从已收敛邻居作种子- 高温区(60-80K):先用冷启动算 He-rich 或低金属的"桥头堡"模型, 再以它为种子推进到目标参数
- He-poor 高温:先算同 Teff 的 He-rich 或低金属版本,再以它为种子
- 物理极限:80K + He-poor + logCNO=-1(金属 0.1×H)的组合, 即使用两步种子 + ORELAX 也无法收敛。这是真实物理极限, 网格在该角落如实标记为"未收敛"。
- run_grid.py 的种子策略:扩展为"按邻居查找已收敛模型作种子", 失败则尝试中间丰度点作跳板。
6. 代码系统说明(cno_grid/)
当前配置(已修正为用户原配方)
gen_input5.py:He.5nlevs=14(数据文件本身 24/20,按 nlevs 截断), NFREAD=2000,支持 ilvlin/metals 参数run_one.pyDEFAULT_CHAIN:三步法,无 CHMAX/ITEK/ORELAX- write_nst 支持 ichang/orelax/idlte/iacc/icrsw(注意:原版 tlusty.exe 里 ICRSW 是死代码;§6X 描述的源码修复让它在 patched 二进制里生效, 但默认管线仍用原版 tlusty.exe)
seed_step.py:种子步进实现 —— 跳过 LTE grey 冷启动, 直接热启动 nc 阶段,用于高温/He-poor/富金属等冷启动失败的场景run_grid.py:6 维网格调度,集成种子步进回退(NEW)- 冷启动失败时自动用
find_seed找已收敛邻居,用 SEED_STEP_CHAIN 重试 - 失败的冷启动结果备份到
<model>.coldfail/,避免污染种子库 find_seed优先级:同 (Teff,logg,logHe) 最近 CNO → 全局最近邻_atmos_clean过滤掉 NaN 污染的"假收敛"模型作种子
- 冷启动失败时自动用
使用方法
export TLUSTY=/home/dckj/program/tlusty/tl208-s54
# 单个模型(冷启动,适用 20-40K 大部分点)
python3 cno_grid/src/run_one.py --teff 35000 --logg 5.5 --loghe -2 \
--logc -1 --logn -1 --logo -1
# 种子步进(高温 He-poor 等冷启动失败点)
python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
--logc -4 --logn -4 --logo -4 \
--seed cno_grid/results/<seed-model>/<seed-model>.7
# 批量网格(自动冷启动 + 失败时种子步进回退)
python3 cno_grid/src/run_grid.py cno_grid/config.yaml --dry-run # 预览
python3 cno_grid/src/run_grid.py cno_grid/config.yaml # 正式跑
网格运行策略
桥头堡机制(手动):在跑完整网格前,先在难收敛区附近算几个"桥头堡" 模型,建立种子库。例如高温区先算:
# 1. 算 80K He-rich cno-4(冷启动可成功)
python3 cno_grid/src/run_one.py --teff 80000 --logg 6.5 --loghe 2 \
--logc -4 --logn -4 --logo -4
# 2. 用它作种子算 80K He-poor cno-4(种子步进)
python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \
--logc -4 --logn -4 --logo -4 \
--seed cno_grid/results/t80000_g6.5_he2_c-4_n-4_o-4/t80000_g6.5_he2_c-4_n-4_o-4.7
# 3. 之后跑 run_grid.py 时,find_seed 会自动发现这些已收敛的邻居
run_grid 的种子步进回退流程:
对每个网格点 P:
1. 检查 conv.json: 若已 converged → skip
2. find_seed(P): 查找已收敛邻居(同 family 优先,按 CNO 距离)
3. 冷启动 run_one(DEFAULT_CHAIN):
a. 成功 → 完成
b. 失败 + seed_step_fallback=true + 找到种子 →
移动失败结果到 <P>.coldfail/
用 SEED_STEP_CHAIN + seed 重试
4. 写 grid_status.json 汇总(含 seed_step_retries 计数)
6X. ICRSW 修复方案(已实施并测试 —— 2026-07-21)
状态:源码已修改并重新编译,但修复后的可执行未替换
tlusty.exe。 原因:修复在数值上生效(SWITCH 确实被调用了,CRSW 不再恒为 1.0), 但对最难收敛的 80K + He-poor + 富金属点没有帮助(甚至更早发散), 所以默认管线仍用未修改的tlusty.exe。修复后的二进制保留在tlusty/tlusty.exe.icrsw_patched,需要时可以拿来对比试验。 备份的原始源码在tlusty/tlusty208.f.orig_backup。
背景
ICRSW(Hummer & Voels 1988 碰撞-辐射开关)在 tlusty208 中曾是未完成的
集成——SUBROUTINE SWITCH(tlusty208.f:4556)写好了完整的 CRSW 计算
逻辑,下游消费方代码(共 12 处 RRU/RRD * CRSW(ID):6341/6343-6345、
6589/6591-6592、6841/6843-6845、7621-7633、7807-7810、8017-8020、
18147、18201-18204、18252-18256)也都到位,但主迭代循环中缺少
CALL SWITCH。结果 CRSW(ID) 永远保持默认值 UN(=1.0,line 1812
CRSW(ID)=UN),所有乘法都是无效操作。
实施的修复
关键纠正:旧版本本文档(§6X 步骤 3)建议把 CALL SWITCH 插在
CALL RESOLV 之前。这是错误的——经源码核查确认:
RRU(ITR,ID)/RRD(ITR,ID)在RATES1(line 6179)及其同族子程序 (RATSP1、ALIST1、ALIST2、ALISK1、ALISK2)内部才被零初始化 并累加;这些子程序全部从RESOLV(line 3724)调用。COLRAT(ITR,ID)在INILAM(line 4029)中赋值,INILAM也从RESOLV(line 3743)调用。- 在 iter 1 的首次
RESOLV之前,没有任何 DATA 语句或 START 阶段 初始化过 RRU/RRD/COLRAT(已 grep 验证)。如果按旧文档把CALL SWITCH放在RESOLV之前,iter 1 会在 line 4599C/RRU(ITR,ID)处除以未初始化 的垃圾值,立刻 NaN。
正确插入位置:在 CALL RESOLV 之后、INIT=0 之前,复用现有的
INIT 变量作为 INITM 参数(程序启动时 INIT=1 在 line 22,RESOLV 之后
被重置为 0 在 line 36):
10 ITER=ITER+1
CALL RESOLV
C
C 1a. Collisional-radiative switching (Hummer & Voels 1988)
C EVALUATE/UPDATE CRSW(ID) AFTER the formal solution has produced
C fresh RRU/RRD/COLRAT, and BEFORE the linearization step (SOLVE/
C SOLVES) that consumes CRSW via BPOPE/BPOPF (lines ~18147,18201,
C 18252). On iter 1 INIT is still 1 -> full recompute of CRSW;
C on iter 2..N INIT was reset to 0 -> cheap CRSW*=SWPINC update.
C SWITCH is a no-op when ICRSW=0 (default), so existing behavior
C is unchanged unless ICRSW>0 is set in nst.
C NOTE: must NOT be moved before CALL RESOLV -- on iter 1 RRU/RRD/
C COLRAT are still uninitialized there (no DATA stmt; they are
C zeroed and filled inside RATES1/RATSP1 within RESOLV).
C
CALL SWITCH(INIT)
INIT=0
IF(LFIN) GO TO 20
...
这个位置的优点:
- iter 1 时 RESOLV 已经计算好 COLRAT(来自 INILAM)和 RRU/RRD(来自 RATES1),SWITCH(INIT=1) 能正确执行完整 Hummer-Voels 计算。
- iter 2..N 时 SWITCH(INIT=0) 仅做
CRSW *= SWPINC的廉价更新。 - CRSW 在
SOLVE/SOLVES(通过 MATGEN→BPOP→BPOPE/BPOPF 消费 CRSW) 运行之前已经定下来。 - SWITCH 内部首句
IF(ICRSW.EQ.0) RETURN(line 4580)保证 ICRSW=0 时 是 no-op,对现有所有测试零影响。
重新编译命令(关键:必须用 -mcmodel=large,否则 x86-64 PIC 重定位
溢出,链接报 relocation truncated to fit: R_X86_64_PC32 against symbol curder_):
cd tlusty/
cp tlusty.exe tlusty.exe.orig # 备份
cp tlusty208.f tlusty208.f.orig_backup # 备份源码
gfortran -O2 -std=legacy -fno-automatic -mcmodel=large \
-o tlusty.exe tlusty208.f
# 注意:IMPLIC.FOR / BASICS.FOR / 等都是 INCLUDE 文件,不要单独编译;
# 整个程序就在 tlusty208.f 一个文件里(通过 INCLUDE 拉入其它 .FOR)。
实施验证(2026-07-21)
验证 1:ICRSW=0 时与原版逐字节相同
在 H+He NLTE(20000/5.0/-1)模型上,patched exe 与原 exe 产生的
fort.7(大气)和 fort.9(收敛日志)完全相同(cmp 通过、md5 相同)。
证明修复对 ICRSW=0 的所有现有运行零影响。
验证 2:ICRSW=1 时 SWITCH 确实被调用
同样的 H+He 模型,nst 加 ICRSW=1,SWPFAC=0.001,SWPLIM=1.0,SWPINC=2.0:
-
patched exe 在 fort.6 里多出 CRSW 数组 dump(
1P8D10.3格式,50 个值), 数值序列1.438D-12 → 2.875D-12 → 5.750D-12 → 1.150D-11精确对应SWPINC=2.0的逐次翻倍——证明 INITM=0 分支(line 4636)在每次迭代执行。 -
原 exe 在相同 nst 下 fort.6 里没有 CRSW dump,迭代历史与 ICRSW=0 完全 相同——证明旧代码的 SWITCH 确实从未被调用("死代码"判断成立)。
-
iter 1 的 MAXIMUM 列在难收敛深度上明显被压低:
深度 ICRSW=0(原版) ICRSW=1(patched) 压低倍数 35 1.03E+00 2.12E-01 ~5× 28 4.12E+00 9.88E-01 ~4× 25 3.72E+00 9.84E-01 ~4× 这是 Hummer-Voels 开关的预期行为——碰撞速率被人为放大(CRSW<1)以 压制辐射跃迁的非线性。
难点测试:80K + He-poor + logCNO=-1(最终未解决)
用 seed_step 同款配置(cno-2 种子 → cno-1 目标,热启动),8 次迭代,
测了三组 ICRSW 参数(强阻尼、弱阻尼)对比无 ICRSW 基线:
| iter | ICRSW=0(基线) | ICRSW=1 SWPFAC=1e-4 SWPINC=2.0 | ICRSW=1 SWPFAC=0.1 SWPINC=1.5 |
|---|---|---|---|
| 1 | 1.52e+03 | 2.26e+05 | 2.42e+04 |
| 2 | 1.15e+01 | 1.34e+05 | 9.34e+04 |
| 3 | 7.53e+01 | NaN(发散) | 5.98e+06 |
| 4 | 1.52e+01 | NaN | 2.29e+08 |
| 5 | 2.01e+03 | NaN | 5.66e+09 |
| 6 | 1.97e+01 | NaN | 3.13e+21(发散) |
| 7 | 8.24e+00 | NaN | NaN |
| 8 | 6.08e+02 | NaN | NaN |
| 大气 NaN | 0/5210(干净) | 2222/5210(43%) | 0/5210(干净,但解无效) |
(基线 8 次迭代都没崩到 NaN,只是没收敛;两次 ICRSW 都更早爆。)
结论:ICRSW 修复在数值层面完全成功(SWITCH 跑起来了,CRSW 不再 恒为 1.0,迭代历史明显改变),但不能解决这个极端难点——无论是强阻尼 (SWPFAC=1e-4)还是弱阻尼(SWPFAC=0.1),开启 ICRSW 都让发散更早。 SWPFAC=1e-4 让 iter 1 的初始跳跃从 1.5e3 变成 2.3e5(CRSW 太小导致线性化 过度校正);SWPFAC=0.1 略好但仍单调发散到 1e21。
物理原因(与前文 §5Y 的"物理极限"结论一致):80K + He-poor 时 CNO 高价离子(C IV/V、N V、O V/VI)主导大气不透明度,金属量从 logCNO=-2 跳到 -1(×10)导致光致电离率变化陡峭到完全线性化无法阻尼 iter 1 的 尖峰。Hummer-Voels 开关通过放大碰撞速率来稳定,但当辐射-碰撞比本身 就在极端区间时,开关反而把不稳定提前。
40K sanity check(patched exe 在正常区间仍工作)
为了排除"修复破坏了正常路径"的可能,在已验证的 40K + logg 5.5 + cno-1
模型上跑 patched exe(ICRSW=0):迭代历史(oscillatory 但最终收敛到
~1e-4)与原版 exe 产生的 t40000_g5.5_he0_c-1_n-1_o-1.nc_*.9 文件
特征一致(同样的 iter 6 尖峰到 4.69e8、同样的 iter 23-27 收敛到
~1e-4)。证明修复对正常收敛区间无害。
实施后的工程决策
保留源码修改,但不替换 tlusty.exe:
- 修改已通过 sanity check(ICRSW=0 时与原版逐字节相同),是安全的。
- 对网格里 95%+ 的点(20000-40000K 区间),ICRSW 没用也没害。
- 对剩余难收敛点(80K + He-poor + 富金属),ICRSW 不仅没用反而更糟。
- 因此没有理由让默认管线用 patched exe——保留原版可执行,patched
二进制仅供后续研究(比如有人想试
ICRSW=2的深度相关模式,或者 配合更小的丰度步长)。
如果未来需要重新启用 patched exe:
cd tlusty/
# 当前 tlusty.exe 是原版;tlusty208.f 是已修改版
gfortran -O2 -std=legacy -fno-automatic -mcmodel=large \
-o tlusty.exe tlusty208.f
# 想恢复原版:cp tlusty208.f.orig_backup tlusty208.f 后重新编译
替代方案:网格层面规避(仍是当前推荐)
不改源码也能完成网格:
- 20000-40000K:冷启动全区间可靠(已验证多个点)
- 60000-80000K + He-rich / 低金属:冷启动或一步种子步进可解决
- 60000-80000K + He-poor + 富金属(logCNO=-1):真实物理极限, 网格如实标记未收敛(这是合理的——观测上这些极端参数组合的 sdB 本就罕见,且本次实测确认 ICRSW 也不能解决)
每阶段信息记录
conv.json 记录每阶段的 converged/max_relc/elapsed_sec,以及 synspec_sec。
7. 下一步建议
立即可行(已验证配方 + 种子步进)
- 冷启动跑 20000-40000K + logC/N/O=-4 到 -1:可靠收敛,无需种子
- 种子步进跑 60000-80000K + He-rich 或低金属:用冷启动建"桥头堡", 再以它为种子推进到目标点
- 物理极限标注:80K + He-poor + logCNO=-1 是真实物理极限, 网格如实标记未收敛(不强制成功)
待完善
- run_grid.py 自动种子策略:现在是冷启动失败即标记失败; 应扩展为先尝试冷启动,失败则查找邻居已收敛模型作种子重试, 再失败则尝试中间丰度点作跳板(如 cno-4 → cno-2 → cno-1)。
- 种子库管理:网格计算时按 Teff/logg 分组,每组先算最容易的点 (He-rich 或低金属),建立种子库,再扩散到难收敛点。
- synspec 高温区 NaN 问题:80K 大气收敛但 synspec 谱有 74% NaN。 需要单独排查(可能是 gfVIS99.dat 谱线表对 80K 不兼容,或某些 CNO 高价离子模型原子在该温度下数值溢出)。
关键教训
- 先核实物理参数:之前花了大量时间调 CHMAX/IDLTE/ORELAX/He能级/NFREAD, 真正的根因(丰度范围过高 + 冷启动初猜太远)却一直被忽略。
- 冷启动 ≠ 唯一选择:LTE grey 冷启动在高温 He-poor 模型上必然失败, 但这不是物理极限,只是初猜太差。种子步进是 Peter Nemeth 邮件早就 建议的方法("减小模型间步长"),只是之前一直没正确实现。
- 源码分析的价值:通过 subagent 读 tlusty208.f 才发现:
- LTGREY 标志的真正含义(T=生成灰大气,F=读 fort.8)—— 种子步进的物理基础
- ICRSW 是死代码(SWITCH 子程序定义了但从未被 CALL)—— 看似可用的 参数实际无效,必须实测验证
- 物理极限要承认:80K + He-poor + 金属量 0.1×H 的组合, 即使用尽所有稳定化手段也不收敛。这是物理极限,不是工程问题。 网格应如实记录未收敛而非强行通过。
8. 邮件往来要点(Peter Nemeth)
完整邮件在 hot_subdwarf/letter/(已逐条核对原文)。要点:
- He-rich 模型难收敛属正常(原文:"Helium-rich models struggle a lot ... That is normal")
- He 用最复杂模型原子(原文:"I would always use the 24-level He1 and
20-level He2 model atoms")— 但实测在
.5里声明 nlevs=14 对 nc 更稳定, 两者目标不同(Peter 关注光谱精度,我们关注收敛稳定性) - ITEK 可调(原文:"you can set it to 3, 15, and 100")— 实测 100 overshoot, 且不设(默认 4)最好
- 模型链:粗→精 CHMAX;减小模型间步长(原文:"You can try decreasing the steps in between models")← 本次种子步进正是这一条的正确实现 (LTGREY=F 热启动 + 邻居模型作种子)
9. 参考文件索引
| 文件 | 内容 |
|---|---|
/home/dckj/program/tlusty/tests/cno_sdspectrum/GUIDE.md |
用户原始指南(35000K 验证配方)。注意:在 tl208-s54/ 上一级目录 |
cno_grid/src/run_one.py |
三步链实现(DEFAULT_CHAIN = 正确配方) |
cno_grid/src/gen_input5.py |
.5 生成器(He nlevs=14, NFREAD=2000) |
cno_grid/src/seed_step.py |
种子步进实现(高温/难收敛点) |
cno_grid/src/run_grid.py |
6 维网格调度器(冷启动 + 种子步进回退) |
cno_grid/src/check_conv.py |
fort.9 解析与收敛判定 |
cno_grid/config.yaml |
网格配置(链、seed_step_fallback 等) |
cno_grid/PIPELINE.md |
计算流程文档(阶段/并行/统计) |
cno_grid/results/ |
测试模型结果(含 conv.json) |
cno_grid/results/bound_*.log |
8 点边界测试日志(2026-07-21) |
cno_grid/results/seed_step/ |
种子步进验证结果 |
cno_grid/run_boundary_corrected.sh |
边界测试启动脚本 |
hot_subdwarf/letter/ |
Peter Nemeth 邮件 |