# CNO 热亚矮星光谱:完整调试经验与原理文档 > 本文档完整记录在 `tl208-s54/cno_grid/` 上构建含 C/N/O 金属线的热亚矮星理论 > 光谱过程中,**所有测试、遇到的问题、根因分析、解决方法和底层物理/数值原理**。 > > **重要**:本文档经过了多轮调试验证。之前版本的"8/8 边界全部成功"等结论 > **不准确**(基于错误的 CHMAX=0.1 配置)。本版本如实记录了最终确认的结果。 --- ## 1. 核心结论(先读这一节) 1. **金属线必须进大气模型(方案B,自洽)。** 纯 H+He 大气下 synspec 的 C/N/O 谱线全 NaN——大气里没有金属能级,无法计算谱线不透明度。 2. **收敛必须走三步法,nc 步骤不可省略:** ``` LTE 灰大气 (T T, NITER=0) → 初始温度结构 NLTE 连续谱 (F F, ilvlin=0, "nc") → 收敛电离平衡(无线跃迁) NLTE 含线 (F F, ilvlin=100, "nl")→ 加谱线 SYNSPEC → 合成光谱 ``` 3. **正确的 nst 配方(用户 tests.zip 验证):** - **不设 CHMAX**(用 tlusty 默认 0.001)— 这是最关键的点 - **不设 ITEK**(用默认 4) - **He `.5` nlevs=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` 4. **丰度约定是收敛的关键(最终发现):** - 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 发散。这不是高温问题! 5. **验证状态(修正丰度范围后,已与 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' ` 看到的文件内总能级数。这点之前文档里混淆过。 | 元素 | 离子 / `.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 的 `.5` nlevs=14 是用户 tests.zip 验证过的稳定配置(见 §2.5),但 `he1.dat` 本身含 24 能级、`he2.dat` 含 20 能级——tlusty 只读前 14 个。 - O V 的 `.5` nlevs=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 阶段: ```python 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}, ] ``` 用法: ```bash python3 cno_grid/src/seed_step.py --teff 80000 --logg 6.5 --loghe -4 \ --logc -4 --logn -4 --logo -4 \ --seed cno_grid/results//.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) 修复后 再实测验证是否真能改善目标问题——本例中修复"正确"但"无用"。真正对极端 金属跳跃有效的稳定化手段仍然是种子步进(减小模型间步长)。 ### 种子步进的网格应用策略 1. **冷启动能搞定的点**:直接 `run_one.py`(20000-40000K 大部分点) 2. **冷启动搞不定的点**:用 `seed_step.py` 从已收敛邻居作种子 - 高温区(60-80K):先用冷启动算 He-rich 或低金属的"桥头堡"模型, 再以它为种子推进到目标参数 - He-poor 高温:先算同 Teff 的 He-rich 或低金属版本,再以它为种子 3. **物理极限**:80K + He-poor + logCNO=-1(金属 0.1×H)的组合, 即使用两步种子 + ORELAX 也无法收敛。这是真实物理极限, 网格在该角落如实标记为"未收敛"。 4. **run_grid.py 的种子策略**:扩展为"按邻居查找已收敛模型作种子", 失败则尝试中间丰度点作跳板。 --- ## 6. 代码系统说明(`cno_grid/`) ### 当前配置(已修正为用户原配方) - `gen_input5.py`:He `.5` nlevs=14(数据文件本身 24/20,按 nlevs 截断), NFREAD=2000,支持 ilvlin/metals 参数 - `run_one.py` DEFAULT_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 重试 - 失败的冷启动结果备份到 `.coldfail/`,避免污染种子库 - `find_seed` 优先级:同 (Teff,logg,logHe) 最近 CNO → 全局最近邻 - `_atmos_clean` 过滤掉 NaN 污染的"假收敛"模型作种子 ### 使用方法 ```bash 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//.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 # 正式跑 ``` ### 网格运行策略 **桥头堡机制**(手动):在跑完整网格前,先在难收敛区附近算几个"桥头堡" 模型,建立种子库。例如高温区先算: ```bash # 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 + 找到种子 → 移动失败结果到

.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 4599 `C/RRU(ITR,ID)` 处除以未初始化 的垃圾值,立刻 NaN。 **正确插入位置:在 `CALL RESOLV` 之后、`INIT=0` 之前**,复用现有的 `INIT` 变量作为 `INITM` 参数(程序启动时 INIT=1 在 line 22,RESOLV 之后 被重置为 0 在 line 36): ```fortran 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 ... ``` 这个位置的优点: 1. iter 1 时 RESOLV 已经计算好 COLRAT(来自 INILAM)和 RRU/RRD(来自 RATES1),SWITCH(INIT=1) 能正确执行完整 Hummer-Voels 计算。 2. iter 2..N 时 SWITCH(INIT=0) 仅做 `CRSW *= SWPINC` 的廉价更新。 3. CRSW 在 `SOLVE`/`SOLVES`(通过 MATGEN→BPOP→BPOPE/BPOPF 消费 CRSW) 运行之前已经定下来。 4. 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_`): ```bash 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`**: 1. 修改已通过 sanity check(ICRSW=0 时与原版逐字节相同),是安全的。 2. 对网格里 95%+ 的点(20000-40000K 区间),ICRSW 没用也没害。 3. 对剩余难收敛点(80K + He-poor + 富金属),ICRSW 不仅没用反而更糟。 4. 因此**没有理由**让默认管线用 patched exe——保留原版可执行,patched 二进制仅供后续研究(比如有人想试 `ICRSW=2` 的深度相关模式,或者 配合更小的丰度步长)。 **如果未来需要重新启用 patched exe**: ```bash 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 高价离子模型原子在该温度下数值溢出)。 ### 关键教训 1. **先核实物理参数**:之前花了大量时间调 CHMAX/IDLTE/ORELAX/He能级/NFREAD, 真正的根因(丰度范围过高 + 冷启动初猜太远)却一直被忽略。 2. **冷启动 ≠ 唯一选择**:LTE grey 冷启动在高温 He-poor 模型上必然失败, 但这**不是物理极限**,只是初猜太差。种子步进是 Peter Nemeth 邮件早就 建议的方法("减小模型间步长"),只是之前一直没正确实现。 3. **源码分析的价值**:通过 subagent 读 tlusty208.f 才发现: - LTGREY 标志的真正含义(T=生成灰大气,F=读 fort.8)—— 种子步进的物理基础 - ICRSW 是**死代码**(SWITCH 子程序定义了但从未被 CALL)—— 看似可用的 参数实际无效,必须实测验证 4. **物理极限要承认**:80K + He-poor + 金属量 0.1×H 的组合, 即使用尽所有稳定化手段也不收敛。这是物理极限,不是工程问题。 网格应如实记录未收敛而非强行通过。 --- ## 8. 邮件往来要点(Peter Nemeth) 完整邮件在 `hot_subdwarf/letter/`(已逐条核对原文)。要点: 1. He-rich 模型难收敛属正常(原文:"Helium-rich models struggle a lot ... That is normal") 2. He 用最复杂模型原子(原文:"I would always use the 24-level He1 and 20-level He2 model atoms")— 但实测在 `.5` 里声明 nlevs=14 对 nc 更稳定, 两者目标不同(Peter 关注光谱精度,我们关注收敛稳定性) 3. ITEK 可调(原文:"you can set it to 3, 15, and 100")— 实测 100 overshoot, 且不设(默认 4)最好 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 邮件 |