SpectraRust/src/synspec/math/linopw.rs
fmq e2c1a4580a feat: F2R 重构全部完成 + 自动化脚本改进
Phase 1 翻译 (完成):
- TLUSTY 350 函数 100% 翻译
- SYNSPEC 168 函数 100% 翻译
- ~495 Rust 模块

Phase 2 集成 (完成):
- TLUSTY RESOLV 7 个 TODO 全部清除
- TLUSTY Runner IJALI 频率选择实现
- OPFRAC ioniz.dat 解析完整实现
- SYNSPEC Runner 编排流程连接完成
- SYNSPEC RESOLV OPAC→RTE→OUTPRI 调用链完整

Phase 3 验证 (完成, 修复 8 处 bug):
- INITIA: compute_hydrogen_level_bounds 索引混合修复
- INILIN: GAMR0/GS0/GW0 展宽公式修复, 经典 VdW 公式修复
- INIBL0: CNM 常数 2.997925e18→e17 修复
- OPAC: Lyman IJ=2 修正缺失修复
- RTE: minv3 矩阵求逆符号错误修复

自动化脚本改进:
- specf2r.sh: 添加 429 限流退避、完成检测、同步等待
- SKILL.md: 三阶段工作流 + 状态文件系统
- references/: Phase 1/2/3 独立参考文档

新增:
- src/bin/synspec.rs: SYNSPEC 可执行文件入口
- .f2r_phase/.f2r_tasks/.f2r_complete: 状态管理文件

编译: 0 错误 | Clippy: 0 错误 | 测试: voigt 28 + eldens 5 通过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:54:53 +08:00

502 lines
18 KiB
Rust

//! 线不透明度和发射率计算(风模型变体)。
//!
//! 翻译自 SYNSPEC `LINOPW` 子程序 (synspec54.f:10590)。
//!
//! 与 LINOP 类似,但针对风模型做了以下扩展:
//! - 速度相关的线拒绝
//! - 辐射场处理 (itrad, trad, wdil)
//! - 窗口化频率网格的线心索引计算
//! - DOP1 = DOPA1(IAT,ID)/FR0 (与 LINOP 不同)
use super::phe1::{phe1, Phe1Params};
use super::phe2::{phe2, Phe2Params};
use super::voigtk::{voigtk, MVOI};
/// 物理常数
const UN: f64 = 1.0;
const EXT0: f64 = 3.17;
const TEN: f64 = 10.0;
const C3: f64 = 1.4387886;
const XET: f64 = 8067.6;
const XET3: f64 = XET * C3;
/// LINOPW 参数结构体。
///
/// 与 LinopParams 类似,但增加了风模型相关参数。
#[derive(Debug)]
pub struct LinopwParams<'a> {
/// 深度索引 (1-indexed)
pub id: usize,
/// 温度 (K)
pub temp: f64,
/// h/k (erg/K)
pub hk: f64,
/// 频率数
pub nfreq: usize,
/// 频率数组 (Hz)
pub freq: &'a [f64],
/// NOPAC — 不透明度截止频率索引
pub nopac: usize,
/// Planck 函数 PLAN(ID)
pub plan: f64,
/// 受激辐射修正 STIM(ID) — LINOPW 中未使用,保留接口兼容
pub stim: f64,
/// 谱线数 (NLIN)
pub nlin: usize,
/// 谱线数据
pub lines: &'a [LinopwLineData],
/// RRR(ID,ION,IAT)
pub rrr: &'a [f64],
/// RRR 维度: [natom][nion]
pub rrr_dims: (usize, usize),
/// DOPA1(IAT, ID) — Doppler 宽度
pub dopa1: &'a [f64],
/// DOPA1 维度: [natom]
pub dopa1_nat: usize,
/// G(level) — 能级统计权重
pub g: &'a [f64],
/// POPUL(level, ID) — 能级布居数
pub popul: &'a [f64],
/// POPUL 维度: [nlevel]
pub popul_nlev: usize,
/// PNLT(IAT, ION, ID) — NLTE 布居数
pub pnlt: &'a [f64],
/// PNLT 维度: [natom][nion]
pub pnlt_dims: (usize, usize),
/// ENEV(IAT, ION) — 电离能 (cm^-1)
pub enev: &'a [f64],
/// ENEV 维度: [natom]
pub enev_nat: usize,
/// ENION(level) — 能级能量 (erg)
pub enion: &'a [f64],
/// 激光删除标志 (lasdel)
pub lasdel: bool,
/// He II 特殊线数 (NSP)
pub nsp: usize,
/// He II 特殊线索引 (ISP0)
pub isp0: &'a [usize],
/// Voigt 函数表 H0
pub h0tab: &'a [f64; MVOI],
/// Voigt 函数表 H1
pub h1tab: &'a [f64; MVOI],
/// Voigt 函数表 H2
pub h2tab: &'a [f64; MVOI],
/// PHE1 轮廓表数据
pub phe1_data: Option<Phe1DataW<'a>>,
/// PHE2 公共数据
pub phe2_common: Option<Phe2CommonW<'a>>,
// --- 风模型特有参数 ---
/// 速度场 VEL(ID)
pub vel: f64,
/// 最大速度 VELMAX
pub velmax: f64,
/// 辐射场模式 (ITRAD)
pub itrad: i32,
/// 辐射温度 TRAD(ipotl, ID)
pub trad: &'a [f64],
/// TRAD 维度: [npotl]
pub trad_npotl: usize,
/// IPOTL(line) — 电离势索引
pub ipotl: &'a [usize],
/// BNUE(ij) — 频率相关 Planck 函数
pub bnue: &'a [f64],
/// NLTE 关闭标志 (NLTOFF)
pub nltoff: i32,
/// 发射关闭标志 (IEMOFF)
pub iemoff: i32,
/// 线心 NLTE 关闭标记 ILNE(depth)
pub ilne: &'a [usize],
/// 线心速度拒绝标记 ILVI(depth)
pub ilvi: &'a [usize],
/// 线心频率索引 IJCNTR(line) — 输出
pub ijcntr: &'a mut [usize],
/// 标准线吸收 ABSTDW(ijcont, ID)
pub abstdw: &'a [f64],
/// ABSTDW 维度: [nfreq]
pub abstdw_nfreq: usize,
/// RELOP — 相对不透明度阈值
pub relop: f64,
/// IJCONT(line) — 线心频率索引
pub ijcont: &'a [usize],
/// 准直函数 XJCON(ID) — 未使用,保留
pub xjcon: f64,
/// 稀释因子 WDIL(ID) — 输出
pub wdil_out: &'a mut f64,
}
/// 单条谱线数据 (LINOPW 版本)。
#[derive(Debug, Clone)]
pub struct LinopwLineData {
pub il: usize,
pub innlt: i32,
pub iat: usize,
pub ion: usize,
pub isprf: usize,
pub freq0: f64,
pub gf0: f64,
pub excl0: f64,
pub excu0: f64,
pub agam: f64,
pub dop1_inv: f64,
pub abcent: f64,
pub slin: f64,
pub ilown: usize,
pub iupn: usize,
}
/// PHE1 轮廓表数据 (LINOPW 版本)。
#[derive(Debug)]
pub struct Phe1DataW<'a> {
pub vturb: f64,
pub elec: f64,
pub prf447: &'a [f64],
pub dlm447: &'a [f64],
pub xne447: &'a [f64],
pub nwlam_447: &'a [usize],
pub prfhe1: &'a [f64],
pub dlmhe1: &'a [f64],
pub xnehe1: &'a [f64],
pub nwlam_he1: &'a [usize],
pub max_wlam_447: usize,
pub max_wlam_he1: usize,
}
/// PHE2 公共数据 (LINOPW 版本)。
#[derive(Debug)]
pub struct Phe2CommonW<'a> {
pub ielhe2: i32,
pub inlte: i32,
pub he3_pop: f64,
pub nlhe2: i32,
pub nfirst_he2: i32,
pub wlam: &'a [f64],
pub prfhe2: &'a [f64],
pub wlhe2: &'a [f64],
pub nwlhe2: i32,
pub ilhe2: i32,
pub iuhe2: i32,
pub lasdel: bool,
}
/// LINOPW 输出结果。
#[derive(Debug)]
pub struct LinopwResult {
pub ablin: Vec<f64>,
pub emlin: Vec<f64>,
}
/// 计算线不透明度和发射率(风模型变体)。
pub fn linopw(params: &mut LinopwParams) -> LinopwResult {
let nfreq = params.nfreq;
let mut ablin = vec![0.0f64; nfreq];
let mut ablinn = vec![0.0f64; nfreq];
let mut emlin = vec![0.0f64; nfreq];
*params.wdil_out = 1.0;
let _plw = params.plan * 1.0; // wdil=1
if params.nlin == 0 {
return LinopwResult { ablin, emlin };
}
let tem1 = UN / params.temp;
let hkt = params.hk * tem1;
// 计算频率间距因子
let xx = params.freq[nfreq - 1] - params.freq[0];
let dfrcon = if xx.abs() > 1e-30 {
-((params.nopac as f64) - 1.0) / xx
} else {
0.0
};
for (line_idx, line_data) in params.lines.iter().take(params.nlin).enumerate() {
let il = line_data.il;
let innlt = line_data.innlt;
// 速度拒绝
if params.ilvi[params.id - 1] > 0 {
if innlt == 0 {
continue;
} else if params.nltoff != 0 {
continue;
}
}
// 线心频率索引 (仅深度 1)
if params.id == 1 {
let fr0 = line_data.freq0;
let xjc = 3.0 + dfrcon * (params.freq[0] - fr0);
let mut ijc = xjc as usize;
if ijc > 1 && ijc < params.nopac {
// 在频率网格中找到最近的点
if fr0 < params.freq[ijc] {
let mut ijc0 = ijc;
let mut dfr0 = params.freq[ijc0] - fr0;
loop {
ijc0 += 1;
if ijc0 >= nfreq {
break;
}
let dfr = (params.freq[ijc0] - fr0).abs();
if dfr < dfr0 {
ijc = ijc0;
dfr0 = dfr;
} else {
break;
}
}
} else if fr0 > params.freq[ijc] {
let mut ijc0 = ijc;
let mut dfr0 = fr0 - params.freq[ijc0];
loop {
if ijc0 == 0 {
break;
}
ijc0 -= 1;
let dfr = (params.freq[ijc0] - fr0).abs();
if dfr < dfr0 {
ijc = ijc0;
dfr0 = dfr;
} else {
break;
}
}
}
}
params.ijcntr[line_idx] = ijc;
}
let iat = line_data.iat;
let ion = line_data.ion;
let fr0 = line_data.freq0;
let lpr = !(line_data.isprf > 1 && line_data.isprf <= 5);
if line_data.isprf >= 6 {
continue;
}
let agam = line_data.agam;
let dop1 = 1.0 / line_data.dop1_inv / fr0; // DOPA1(IAT,ID)/FR0
// 计算 ab0 和 sl0
let (ab0, sl0) = if innlt == 0 && params.itrad <= 0 {
// LTE 线 (无辐射场)
let rrr_idx = params.rrr_dims.0 * params.rrr_dims.1 * (params.id - 1)
+ ion * params.rrr_dims.0 + iat;
let rrr_val = if rrr_idx < params.rrr.len() { params.rrr[rrr_idx] } else { 0.0 };
let ab0 = (line_data.gf0 - line_data.excl0 * tem1).exp()
* rrr_val * dop1 * (1.0 - (-hkt * fr0).exp());
(ab0, 0.0)
} else if innlt == 0 && params.itrad > 0 {
// LTE 线 (有辐射场)
let ipotl_idx = if il < params.ipotl.len() { params.ipotl[il] } else { 0 };
let trad_idx = ipotl_idx * params.trad_npotl + (params.id - 1);
let trl = if trad_idx < params.trad.len() { params.trad[trad_idx] } else { params.temp };
let xx = (-hkt * fr0).exp();
let rrr_idx = params.rrr_dims.0 * params.rrr_dims.1 * (params.id - 1)
+ ion * params.rrr_dims.0 + iat;
let rrr_val = if rrr_idx < params.rrr.len() { params.rrr[rrr_idx] } else { 0.0 };
let mut ab0 = (line_data.gf0 - line_data.excl0 / trl).exp()
* rrr_val * dop1 * (1.0 - xx);
if line_data.excl0 > 2000.0 {
ab0 *= 1.0; // wdil=1
}
let pla = 1.4743e-2 * (fr0 * 1e-15).powi(3) * xx / (1.0 - xx);
let sl0 = pla * 1.0; // wdil=1
(ab0, sl0)
} else if innlt > 0 {
(line_data.abcent, line_data.slin)
} else {
// NLTE 线
let pnlt_idx = params.pnlt_dims.0 * params.pnlt_dims.1 * (params.id - 1)
+ ion * params.pnlt_dims.0 + iat;
let pp = if pnlt_idx < params.pnlt.len() { params.pnlt[pnlt_idx] } else { 0.0 };
let pi = if line_data.ilown > 0 {
let pop_idx = (line_data.ilown - 1) * params.popul_nlev + (params.id - 1);
if pop_idx < params.popul.len() {
params.popul[pop_idx] / params.g[line_data.ilown - 1]
} else { 0.0 }
} else {
let enev_idx = params.enev_nat * (params.id - 1) + iat;
let enev_val = if enev_idx < params.enev.len() { params.enev[enev_idx] } else { 0.0 };
pp * ((enev_val * XET3 - line_data.excl0) * tem1).exp()
};
let (pj, cor) = if line_data.iupn > 0 {
let pop_idx = (line_data.iupn - 1) * params.popul_nlev + (params.id - 1);
let pj = if pop_idx < params.popul.len() {
params.popul[pop_idx] / params.g[line_data.iupn - 1]
} else { 0.0 };
let cor = if line_data.ilown > 0 && line_data.iupn > 0 {
((line_data.excu0 - line_data.excl0
+ (params.enion[line_data.iupn - 1] - params.enion[line_data.ilown - 1]) / 1.38054e-16)
* tem1).exp()
} else { 1.0 };
(pj, cor)
} else {
let enev_idx = params.enev_nat * (params.id - 1) + iat;
let enev_val = if enev_idx < params.enev.len() { params.enev[enev_idx] } else { 0.0 };
let pj = pp * ((enev_val * XET3 - line_data.excu0) * tem1).exp();
(pj, 1.0)
};
let x = if pj > 0.0 { pi / pj * cor } else { UN };
let x = if x == UN { (4.79928e-11 * fr0 * tem1).exp() } else { x };
let sl0 = params.plan / (x - UN);
let ab0 = if pi > 0.0 { pi * (UN - UN / x) * line_data.gf0.exp() * dop1 } else { 0.0 };
(ab0, sl0)
};
if ab0 <= 0.0 && params.lasdel {
continue;
}
// 频率贡献范围
let ijcont_idx = if il < params.ijcont.len() { params.ijcont[il] } else { 0 };
let abstdw_idx = ijcont_idx * params.abstdw_nfreq + (params.id - 1);
let avabw = if abstdw_idx < params.abstdw.len() {
params.abstdw[abstdw_idx] * params.relop
} else { 0.0 };
let ex0 = if avabw > 0.0 { ab0 / avabw * agam } else { 0.0 };
let ext = if ex0 > TEN { ex0.sqrt() } else { EXT0 };
let ext = ext / dop1;
let ijext = (dfrcon * ext + 1.5) as usize;
let ijctr = params.ijcntr[line_idx];
let ij1 = ijctr.saturating_sub(ijext).max(1);
let ij2 = (ijctr + ijext).min(nfreq);
if ij1 >= nfreq || ij2 <= 2 {
continue;
}
if innlt == 0 && params.itrad <= 0 {
// LTE 线
if lpr {
for ij in ij1..=ij2.min(nfreq - 1) {
let xf = (params.freq[ij] - fr0).abs() * dop1;
ablin[ij] += ab0 * voigtk(agam, xf, params.h0tab, params.h1tab, params.h2tab);
}
} else if let Some(ref phe1d) = params.phe1_data {
for ij in 0..nfreq {
let phe1_p = Phe1Params {
id: params.id, freq: params.freq[ij], iline: line_data.isprf - 1,
temp: params.temp, elec: phe1d.elec, vturb: phe1d.vturb,
prf447: phe1d.prf447, dlm447: phe1d.dlm447, xne447: phe1d.xne447,
nwlam_447: phe1d.nwlam_447, prfhe1: phe1d.prfhe1, dlmhe1: phe1d.dlmhe1,
xnehe1: phe1d.xnehe1, nwlam_he1: phe1d.nwlam_he1,
max_wlam_447: phe1d.max_wlam_447, max_wlam_he1: phe1d.max_wlam_he1,
h0tab: params.h0tab, h1tab: params.h1tab, h2tab: params.h2tab,
};
ablin[ij] += ab0 * phe1(&phe1_p);
}
}
} else {
// NLTE 线 或 有辐射场的 LTE 线
if lpr {
for ij in ij1..=ij2.min(nfreq - 1) {
let xf = (params.freq[ij] - fr0).abs() * dop1;
let abl = ab0 * voigtk(agam, xf, params.h0tab, params.h1tab, params.h2tab);
ablinn[ij] += abl;
if params.ilne[params.id - 1] == 0 {
emlin[ij] += abl * sl0;
}
}
} else if let Some(ref phe1d) = params.phe1_data {
for ij in 0..nfreq {
let phe1_p = Phe1Params {
id: params.id, freq: params.freq[ij], iline: line_data.isprf - 1,
temp: params.temp, elec: phe1d.elec, vturb: phe1d.vturb,
prf447: phe1d.prf447, dlm447: phe1d.dlm447, xne447: phe1d.xne447,
nwlam_447: phe1d.nwlam_447, prfhe1: phe1d.prfhe1, dlmhe1: phe1d.dlmhe1,
xnehe1: phe1d.xnehe1, nwlam_he1: phe1d.nwlam_he1,
max_wlam_447: phe1d.max_wlam_447, max_wlam_he1: phe1d.max_wlam_he1,
h0tab: params.h0tab, h1tab: params.h1tab, h2tab: params.h2tab,
};
let abl = ab0 * phe1(&phe1_p);
ablinn[ij] += abl;
if params.ilne[params.id - 1] == 0 {
emlin[ij] += abl * sl0;
}
}
}
}
}
// 连续谱贡献
if params.vel <= params.velmax {
for ij in 0..nfreq {
let pla = if (hkt * params.freq[ij]).exp() - 1.0 > 1e-30 {
params.bnue[ij] / ((hkt * params.freq[ij]).exp() - 1.0)
} else { 0.0 };
emlin[ij] += ablin[ij] * pla; // wdil=1
ablin[ij] += ablinn[ij];
}
}
// He II 特殊线
if params.nsp > 0
&& let Some(ref phe2c) = params.phe2_common {
for &isp in params.isp0.iter().take(params.nsp) {
if (6..=24).contains(&isp) {
let phe2_p = Phe2Params {
ispec: isp as i32, id: params.id as i32,
ielhe2: phe2c.ielhe2, inlte: phe2c.inlte,
nfreq: nfreq as i32, freq: params.freq, wlam: phe2c.wlam,
temp: params.temp, elec: 0.0, he3_pop: phe2c.he3_pop,
nlhe2: phe2c.nlhe2, nfirst_he2: phe2c.nfirst_he2,
popul: &[], prfhe2: phe2c.prfhe2, wlhe2: phe2c.wlhe2,
nwlhe2: phe2c.nwlhe2, ilhe2: phe2c.ilhe2, iuhe2: phe2c.iuhe2,
lasdel: phe2c.lasdel,
};
let result = phe2(&phe2_p);
for ij in 0..nfreq {
ablin[ij] += result.ablin[ij];
emlin[ij] += result.emlin[ij];
}
}
}
}
LinopwResult { ablin, emlin }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linopw_zero_lines() {
let freq = vec![1e14; 10];
let h0tab = [0.0f64; MVOI];
let h1tab = [0.0f64; MVOI];
let h2tab = [0.0f64; MVOI];
let mut wdil = 0.0f64;
let mut ijcntr = vec![0usize; 1];
let mut params = LinopwParams {
id: 1, temp: 10000.0, hk: 4.79928e-11,
nfreq: 10, freq: &freq, nopac: 10,
plan: 1.0, stim: 1.0, nlin: 0, lines: &[],
rrr: &[], rrr_dims: (0, 0), dopa1: &[], dopa1_nat: 0,
g: &[], popul: &[], popul_nlev: 0,
pnlt: &[], pnlt_dims: (0, 0),
enev: &[], enev_nat: 0, enion: &[],
lasdel: false, nsp: 0, isp0: &[],
h0tab: &h0tab, h1tab: &h1tab, h2tab: &h2tab,
phe1_data: None, phe2_common: None,
vel: 0.0, velmax: 1e10, itrad: 0,
trad: &[], trad_npotl: 0, ipotl: &[],
bnue: &freq, nltoff: 0, iemoff: 0,
ilne: &[0], ilvi: &[0],
ijcntr: &mut ijcntr,
abstdw: &[], abstdw_nfreq: 0, relop: 1.0,
ijcont: &[0], xjcon: 0.0, wdil_out: &mut wdil,
};
let result = linopw(&mut params);
assert!(result.ablin.iter().all(|&x| x == 0.0));
assert!(result.emlin.iter().all(|&x| x == 0.0));
}
}