feat(all): 物理正确性五重硬门槛、输入文件结构化与 fort.55 错位修复、conv 诊断 DB 化与阶段归因修复、ORELAX 收敛修复与导入工具下线
物理正确性校验体系(common/conv_check.rs +494 行) - 新增 5 类硬门槛:能量守恒(.6)、温度结构(.7)、emflux 积分校验(.emflux,含全 NaN 判失败)、假收敛排查(itek 轨迹首末比)、b 因子合理性(.bfac) - runner 在 TLUSTY 阶段结束后执行全部校验,任一失败判 final_converged=false - GridConfig 新增 8 个可配阈值,经 scheduler→executor→runner 全链路透传 输入文件配置结构化重构(config.rs +1453 行) - TlustyInput 拆为 dot5/nst 分层结构,字段名严格映射 tlusty208.f READ 语句;SynspecInput 重构为 9 个 Fort55Line 子结构体 - 移除 ChainStep.metals 字段,元素集改由 dot5.atoms/ions 显式声明(gen_input5/nst_writer 同步重写为三源融合 / 分层覆盖) - fort.55 修复行结构 bug:补全分子表行(7→9 行),IDSTD 50→0 错位修正(影响全部光谱线强归一化,需重算 SYNSPEC 阶段) conv 诊断 DB 化与阶段归因修复(server) - 单点详情 conv 面板从磁盘 conv.json 改读 DB grid_points.summary_json;grid_points 新增 summary_json/last_elapsed_sec 两列(旧库幂等 ALTER) - record_task_report 阶段归因列加 CASE 守卫 + clear_synspec 对称处理,修复 synspec-only/TLUSTY-only 重跑污染统计 - 新增 summary_merge.rs 点级增量合并,避免重跑覆盖诊断字段 收敛性 ORELAX 修复与 seed_chain 可配(sdB_cno.yaml + node) - nl 阶段加 orelax=0.5、seed_nc 加 orelax=0.3,阻尼中温区 relc 振荡发散 - seed_chain 块可配,executor 优先采用用户配置而非内置默认链 导入工具下线 - 删除 import_results 客户端工具及 Windows 推送脚本;移除 /admin/import_seed 端点 - 改为服务端临时 migrate_conv 端点(扫 conv.json 增量合并入库,迁移后可删) 文档与分析 - 新增 1305 失败点根因分析、fort.14 全 NaN 物理含义分析两份深度文档 - spectrum_correctness_analysis 两次修订标注已修复项;fetch_results.sh 修 trap RETURN 的 set -u 报错
This commit is contained in:
@@ -1,41 +1,339 @@
|
||||
//! SYNSPEC fort.55 控制卡生成器。
|
||||
//!
|
||||
//! `generate_fort55_content` 把 [`SynspecInput`](按行分组的子结构体)序列化为 fort.55 文本。
|
||||
//! 每行字段顺序严格对应 `synspec54.f` 的 `READ(55,*)` 语句,字段名一一映射——详见各
|
||||
//! `Fort55Line*` 结构体的文档注释(附 `synspec54.f` 行号)。
|
||||
|
||||
use crate::config::SynspecInput;
|
||||
|
||||
/// Dynamic generator for SYNSPEC fort.55 parameter control file
|
||||
/// 把 [`SynspecInput`] 序列化为 SYNSPEC fort.55 控制卡文本(9 行)。
|
||||
///
|
||||
/// 每行对应 `synspec54.f` 的一个 `READ(55,*)` 语句,行内字段顺序与 READ 语句完全一致:
|
||||
/// - 行1 (`:253`): `IMODE IDSTD IPRIN`
|
||||
/// - 行2 (`:254`): `INMOD INTRPL ICHANG ICHEMC`
|
||||
/// - 行3 (`:255`): `IOPHLI nunalp nunbet nungam nunbal`
|
||||
/// - 行4 (`:2146`): `IFREQ INLTE ICONTL INLIST IFHE2`
|
||||
/// - 行5 (`:2148`): `IHYDPR IHE1PR IHE2PR`
|
||||
/// - 行6 (`:2149`): `ALAM0 ALAST CUTOF0 CUTOFS RELOP SPACE`
|
||||
/// - 行7 (`:2208`, IMODE∈[-3,1] 条件读取): `nmlist [iunitm...]`
|
||||
/// - 行8 (`:2251`): `VTB`
|
||||
/// - 行9 (`:2301`, IFWIN≤0): `NMU0 ANG0 IFLUX`
|
||||
///
|
||||
/// # 行7(分子表)的必要性
|
||||
/// SYNSPEC 在 IMODE∈[-3,1](默认 IMODE=0 满足)时会**额外读一行分子表**(`:2208`)。
|
||||
/// gfortran list-directed READ 满足变量列表后会消费整行剩余,故行7必须独立成行,
|
||||
/// 否则 VTB 行会被分子表 READ 吞掉,导致 vturb 被错误覆盖。见 `Fort55LineMol` 文档。
|
||||
pub fn generate_fort55_content(cfg: &SynspecInput) -> String {
|
||||
let line1 = format!(" {} {} {}", cfg.imode, cfg.idrv, cfg.ifreq);
|
||||
let line2 = " 1 0 0 0";
|
||||
let line3 = " 0 0 0 0 0";
|
||||
let line4 = " 1 1 0 0 0";
|
||||
let line5 = " 0 0 0";
|
||||
let line6 = format!(
|
||||
" {:.1} {:.1} 10 0 {} {}",
|
||||
cfg.wstart, cfg.wend, cfg.rel_cutoff, cfg.abs_cutoff
|
||||
);
|
||||
let line7 = " 0 0";
|
||||
let l1 = &cfg.line1;
|
||||
let l2 = &cfg.line2;
|
||||
let l3 = &cfg.line3;
|
||||
let l4 = &cfg.line4;
|
||||
let l5 = &cfg.line5;
|
||||
let l6 = &cfg.line6;
|
||||
let lm = &cfg.line_mol;
|
||||
let lv = &cfg.line_vtb;
|
||||
let la = &cfg.line_ang;
|
||||
|
||||
// SYNSPEC 用 list-directed READ(`READ(55,*)`)读取,对空白分隔的数值格式宽容,
|
||||
// 不要求固定列宽,只需 token 间有空白分隔即可。波长用 .1(与官方样本一致),
|
||||
// 其余浮点用通用 Display。
|
||||
format!(
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
|
||||
line1, line2, line3, line4, line5, line6, line7
|
||||
// 行1: IMODE, IDSTD, IPRIN
|
||||
"{imode} {idstd} {iprin}\n\
|
||||
{inmod} {intrpl} {ichang} {ichemc}\n\
|
||||
{iophli} {nunalp} {nunbet} {nungam} {nunbal}\n\
|
||||
{ifreq} {inlte} {icontl} {inlist} {ifhe2}\n\
|
||||
{ihydpr} {ihe1pr} {ihe2pr}\n\
|
||||
{alam0:.1} {alast:.1} {cutoff0} {cutoffs} {relop} {space}\n\
|
||||
{nmlist}\n\
|
||||
{vtb}\n\
|
||||
{nmu0} {ang0} {iflux}\n",
|
||||
// 行1
|
||||
imode = l1.imode,
|
||||
idstd = l1.idstd,
|
||||
iprin = l1.iprin,
|
||||
// 行2
|
||||
inmod = l2.inmod,
|
||||
intrpl = l2.intrpl,
|
||||
ichang = l2.ichang,
|
||||
ichemc = l2.ichemc,
|
||||
// 行3
|
||||
iophli = l3.iophli,
|
||||
nunalp = l3.nunalp,
|
||||
nunbet = l3.nunbet,
|
||||
nungam = l3.nungam,
|
||||
nunbal = l3.nunbal,
|
||||
// 行4
|
||||
ifreq = l4.ifreq,
|
||||
inlte = l4.inlte,
|
||||
icontl = l4.icontl,
|
||||
inlist = l4.inlist,
|
||||
ifhe2 = l4.ifhe2,
|
||||
// 行5
|
||||
ihydpr = l5.ihydpr,
|
||||
ihe1pr = l5.ihe1pr,
|
||||
ihe2pr = l5.ihe2pr,
|
||||
// 行6
|
||||
alam0 = l6.alam0,
|
||||
alast = l6.alast,
|
||||
cutoff0 = l6.cutoff0,
|
||||
cutoffs = l6.cutoffs,
|
||||
relop = l6.relop,
|
||||
space = l6.space,
|
||||
// 行7 分子表(nmlist=0 时仅写出 nmlist,list-directed READ 消费整行)
|
||||
nmlist = lm.nmlist,
|
||||
// 行8 VTB
|
||||
vtb = lv.vtb,
|
||||
// 行9 角度
|
||||
nmu0 = la.nmu0,
|
||||
ang0 = la.ang0,
|
||||
iflux = la.iflux,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::{
|
||||
Fort55Line1, Fort55Line2, Fort55Line3, Fort55Line4, Fort55Line5, Fort55Line6,
|
||||
Fort55LineAng, Fort55LineMol, Fort55LineVtb,
|
||||
};
|
||||
|
||||
/// 基本:自定义配置的波长值出现在生成的文本中。
|
||||
#[test]
|
||||
fn test_fort55_generation() {
|
||||
let cfg = SynspecInput {
|
||||
wstart: 3000.0,
|
||||
wend: 7000.0,
|
||||
imode: 0,
|
||||
idrv: 50,
|
||||
ifreq: 1,
|
||||
rel_cutoff: 0.0001,
|
||||
abs_cutoff: 0.01,
|
||||
line6: Fort55Line6 {
|
||||
alam0: 3000.0,
|
||||
alast: 7000.0,
|
||||
..Fort55Line6::default()
|
||||
},
|
||||
..SynspecInput::default()
|
||||
};
|
||||
let content = generate_fort55_content(&cfg);
|
||||
assert!(content.contains("3000.0"));
|
||||
assert!(content.contains("7000.0"));
|
||||
assert!(content.contains("3000.0"), "应含起始波长 3000.0");
|
||||
assert!(content.contains("7000.0"), "应含终止波长 7000.0");
|
||||
}
|
||||
|
||||
/// fort.55 必须恰好 9 行(含行7 分子表 + 行8 VTB + 行9 角度)。
|
||||
#[test]
|
||||
fn test_fort55_line_count() {
|
||||
let cfg = SynspecInput::default();
|
||||
let content = generate_fort55_content(&cfg);
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
assert_eq!(
|
||||
lines.len(),
|
||||
9,
|
||||
"fort.55 应为 9 行(行7 分子表 + 行8 VTB + 行9 角度均显式生成),实际 {} 行:\n{}",
|
||||
lines.len(),
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
/// 核心回归:字段位置与 synspec54.f READ 语句严格对齐,含行7 分子表。
|
||||
/// 重点防护历史上 idrv→IDSTD、ifreq→IPRIN、abs_cutoff→SPACE 的错位 bug,
|
||||
/// 以及行7 分子表缺失导致 VTB 被吞的问题。
|
||||
#[test]
|
||||
fn test_fort55_field_alignment() {
|
||||
let cfg = SynspecInput {
|
||||
line1: Fort55Line1 {
|
||||
imode: 0,
|
||||
idstd: 0,
|
||||
iprin: 1,
|
||||
},
|
||||
line4: Fort55Line4 {
|
||||
ifreq: 7, // 真正的 IFREQ,应在行4第1列
|
||||
inlte: 1,
|
||||
icontl: 0,
|
||||
inlist: 0,
|
||||
ifhe2: 0,
|
||||
},
|
||||
line6: Fort55Line6 {
|
||||
alam0: 3000.0,
|
||||
alast: 7000.0,
|
||||
cutoff0: 10.0,
|
||||
cutoffs: 0.0,
|
||||
relop: 0.0001,
|
||||
space: 0.01,
|
||||
},
|
||||
line_vtb: Fort55LineVtb { vtb: 5.0 },
|
||||
..SynspecInput::default()
|
||||
};
|
||||
let content = generate_fort55_content(&cfg);
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
|
||||
// 行1: IMODE(0) IDSTD(0) IPRIN(1)
|
||||
let l1: Vec<&str> = lines[0].split_whitespace().collect();
|
||||
assert_eq!(l1[0], "0", "行1第1列应为 IMODE=0");
|
||||
assert_eq!(l1[1], "0", "行1第2列应为 IDSTD=0(不再是旧的错位 idrv=50)");
|
||||
assert_eq!(l1[2], "1", "行1第3列应为 IPRIN=1");
|
||||
|
||||
// 行4: IFREQ(7) INLTE(1) ICONTL(0) INLIST(0) IFHE2(0)
|
||||
let l4: Vec<&str> = lines[3].split_whitespace().collect();
|
||||
assert_eq!(l4.len(), 5, "行4应有 5 个字段");
|
||||
assert_eq!(l4[0], "7", "行4第1列应为真正的 IFREQ=7(不再错位到行1)");
|
||||
|
||||
// 行6: ALAM0 ALAST CUTOF0 CUTOFS RELOP SPACE
|
||||
let l6: Vec<&str> = lines[5].split_whitespace().collect();
|
||||
assert_eq!(l6.len(), 6, "行6应有 6 个字段");
|
||||
assert_eq!(l6[0], "3000.0", "行6第1列 ALAM0");
|
||||
assert_eq!(l6[1], "7000.0", "行6第2列 ALAST");
|
||||
assert_eq!(l6[2], "10", "行6第3列 CUTOF0");
|
||||
assert_eq!(l6[5], "0.01", "行6第6列 SPACE");
|
||||
|
||||
// 行7 分子表:nmlist=0(单字段)
|
||||
let l7: Vec<&str> = lines[6].split_whitespace().collect();
|
||||
assert_eq!(l7.len(), 1, "行7 分子表默认 nmlist=0 应为单字段");
|
||||
assert_eq!(l7[0], "0", "行7 nmlist=0");
|
||||
|
||||
// 行8 VTB:自定义值 5.0
|
||||
let l8: Vec<&str> = lines[7].split_whitespace().collect();
|
||||
assert_eq!(l8.len(), 1, "行8 应为单字段 VTB");
|
||||
assert_eq!(
|
||||
l8[0], "5",
|
||||
"行8 VTB=5(自定义值,证明 VTB 行未被分子表吞掉)"
|
||||
);
|
||||
|
||||
// 行9 角度:NMU0 ANG0 IFLUX
|
||||
let l9: Vec<&str> = lines[8].split_whitespace().collect();
|
||||
assert_eq!(l9.len(), 3, "行9 应有 3 个字段 NMU0/ANG0/IFLUX");
|
||||
}
|
||||
|
||||
/// 关键回归:VTB 默认值必须是负值(-1.0),否则 SYNSPEC 会把 vturb 覆盖为 0。
|
||||
///
|
||||
/// 背景(审查 Critical-1):SYNSPEC `:2253` `if(vtb.ge.0.)` 为真时会用 `vtb²·1e10`
|
||||
/// 覆盖所有深度的 vturb。默认 VTB=0 会把大气自带的 vturb=2 km/s 覆盖为 0,导致
|
||||
/// 谱线多普勒宽度错误。VTB=-1 触发"不覆盖"分支,保留大气 vturb。
|
||||
#[test]
|
||||
fn test_fort55_default_vtb_is_negative() {
|
||||
let cfg = SynspecInput::default();
|
||||
assert!(
|
||||
cfg.line_vtb.vtb < 0.0,
|
||||
"默认 VTB 必须为负值(实际 {}),否则 SYNSPEC 会覆盖 vturb 为 0",
|
||||
cfg.line_vtb.vtb
|
||||
);
|
||||
let content = generate_fort55_content(&cfg);
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
let vtb_line = lines[7];
|
||||
assert!(
|
||||
vtb_line.trim_start().starts_with('-'),
|
||||
"行8 VTB 应以负号开头(实际 '{}'),确保不覆盖大气 vturb",
|
||||
vtb_line
|
||||
);
|
||||
}
|
||||
|
||||
/// 默认值生成的 fort.55 关键字段应与官方样本 `hotsd/synspec/fort.55.lin` 对齐。
|
||||
/// 注意:官方样本是 7 行(靠 EOF 跳过 VTB/角度),本生成器是 9 行(显式生成),
|
||||
/// 行数不同但 line1-6 的值与样本一致。唯一差异是 IDSTD(样本 50,默认现 0)和
|
||||
/// VTB(样本无独立行,默认现 -1)。
|
||||
#[test]
|
||||
fn test_fort55_default_key_fields() {
|
||||
let cfg = SynspecInput::default();
|
||||
let content = generate_fort55_content(&cfg);
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
assert_eq!(lines.len(), 9, "应为 9 行");
|
||||
|
||||
// 行1: 官方样本 "0 50 1";默认 "0 0 1"(idstd 修正为 0)
|
||||
let l1: Vec<&str> = lines[0].split_whitespace().collect();
|
||||
assert_eq!(l1[0], "0", "IMODE=0 与样本一致");
|
||||
assert_eq!(l1[1], "0", "IDSTD=0(样本是 50,这是本次修复的行为变化)");
|
||||
assert_eq!(l1[2], "1", "IPRIN=1 与样本一致");
|
||||
|
||||
// 行2: "1 0 0 0" 与样本一致
|
||||
let l2: Vec<&str> = lines[1].split_whitespace().collect();
|
||||
assert_eq!(l2, vec!["1", "0", "0", "0"], "行2 与样本一致");
|
||||
|
||||
// 行4: "1 1 0 0 0" 与样本一致
|
||||
let l4: Vec<&str> = lines[3].split_whitespace().collect();
|
||||
assert_eq!(l4, vec!["1", "1", "0", "0", "0"], "行4 与样本一致");
|
||||
|
||||
// 行6: 默认波长 1400/1410、CUTOF0=10、CUTOFS=0、RELOP=0.0001、SPACE=0.01
|
||||
let l6: Vec<&str> = lines[5].split_whitespace().collect();
|
||||
assert_eq!(l6[0], "1400.0", "ALAM0=1400.0 与样本一致");
|
||||
assert_eq!(l6[1], "1410.0", "ALAST=1410.0 与样本一致");
|
||||
assert_eq!(l6[2], "10", "CUTOF0=10 与样本一致");
|
||||
assert_eq!(l6[3], "0", "CUTOFS=0 与样本一致");
|
||||
assert_eq!(l6[4], "0.0001", "RELOP=0.0001 与样本一致");
|
||||
assert_eq!(l6[5], "0.01", "SPACE=0.01 与样本一致");
|
||||
|
||||
// 行7 分子表: nmlist=0(样本行7第一个值也是 0)
|
||||
let l7: Vec<&str> = lines[6].split_whitespace().collect();
|
||||
assert_eq!(l7[0], "0", "nmlist=0 与样本一致");
|
||||
|
||||
// 行8 VTB: -1(样本无独立 VTB 行,靠 EOF 跳过;本生成器用 -1 显式不覆盖)
|
||||
// 行9 角度: NMU0=0(样本无,靠 EOF 跳过;本生成器显式 0 不算比强度)
|
||||
}
|
||||
|
||||
/// 全自定义:所有字段设非默认值,验证无字段丢失或错位。
|
||||
#[test]
|
||||
fn test_fort55_full_custom() {
|
||||
let cfg = SynspecInput {
|
||||
line1: Fort55Line1 {
|
||||
imode: 1,
|
||||
idstd: -5,
|
||||
iprin: 3,
|
||||
},
|
||||
line2: Fort55Line2 {
|
||||
inmod: 0,
|
||||
intrpl: 0, // 注意:intrpl>0+inmod>0 会触发 DM 数组条件读取,生成器不支持
|
||||
ichang: 1,
|
||||
ichemc: 1,
|
||||
},
|
||||
line3: Fort55Line3 {
|
||||
iophli: 1,
|
||||
nunalp: 2,
|
||||
nunbet: 3,
|
||||
nungam: 4,
|
||||
nunbal: 5,
|
||||
},
|
||||
line4: Fort55Line4 {
|
||||
ifreq: 2,
|
||||
inlte: 25,
|
||||
icontl: 1,
|
||||
inlist: 1,
|
||||
ifhe2: 1,
|
||||
},
|
||||
line5: Fort55Line5 {
|
||||
ihydpr: 2,
|
||||
ihe1pr: 1,
|
||||
ihe2pr: 1,
|
||||
},
|
||||
line6: Fort55Line6 {
|
||||
alam0: 900.0,
|
||||
alast: 1100.0,
|
||||
cutoff0: 5.0,
|
||||
cutoffs: 3.0,
|
||||
relop: 0.001,
|
||||
space: 0.05,
|
||||
},
|
||||
line_mol: Fort55LineMol { nmlist: 0 },
|
||||
line_vtb: Fort55LineVtb { vtb: 5.0 },
|
||||
line_ang: Fort55LineAng {
|
||||
nmu0: 5,
|
||||
ang0: 0.1,
|
||||
iflux: 1,
|
||||
},
|
||||
};
|
||||
let content = generate_fort55_content(&cfg);
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
assert_eq!(lines.len(), 9);
|
||||
|
||||
// 抽查几个易错位的关键值
|
||||
assert!(
|
||||
lines[0].split_whitespace().nth(1) == Some("-5"),
|
||||
"行1第2列 IDSTD=-5 应正确写出负数"
|
||||
);
|
||||
assert!(
|
||||
lines[3].split_whitespace().next() == Some("2"),
|
||||
"行4第1列 真正的 IFREQ=2"
|
||||
);
|
||||
assert!(
|
||||
lines[7].split_whitespace().next() == Some("5"),
|
||||
"行8 VTB=5(VTB 行独立存在,未被分子表吞掉)"
|
||||
);
|
||||
assert!(
|
||||
lines[8].split_whitespace().next() == Some("5"),
|
||||
"行9 NMU0=5"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user