//! SYNSPEC fort.55 控制卡生成器。 //! //! `generate_fort55_content` 把 [`SynspecInput`](按行分组的子结构体)序列化为 fort.55 文本。 //! 每行字段顺序严格对应 `synspec54.f` 的 `READ(55,*)` 语句,字段名一一映射——详见各 //! `Fort55Line*` 结构体的文档注释(附 `synspec54.f` 行号)。 use crate::config::SynspecInput; /// 把 [`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 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!( // 行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 { line6: Fort55Line6 { alam0: 3000.0, alast: 7000.0, ..Fort55Line6::default() }, ..SynspecInput::default() }; let content = generate_fort55_content(&cfg); 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" ); } }