SpectraRust/src/tlusty/math/continuum/opacf0_state.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

765 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Opacf0State — OPACF0 连续谱不透明度状态。
//!
//! 预计算 BF 截面参数和 FF 离子数据,用于在 RESOLV 频率循环中
//! 计算不透明度时使用模型种群 (POPUL) 而非重新计算 Saha 平衡。
//!
//! # 与 Fortran OPACF0 的对应
//!
//! Fortran OPACF0(ID, NFRQ) 对每个深度 ID 计算所有频率的不透明度。
//! 使用预计算的 CROSS(IBFT, IJ) 截面和 POPUL(II, ID) 模型种群。
//!
//! 本模块将静态原子数据BF 跃迁列表、FF 离子数据)预计算到 Opacf0State
//! 然后在 RESOLV 中使用 model.levpop.popul 计算不透明度。
use crate::tlusty::math::hydrogen::hephot;
use crate::tlusty::math::atomic::gfree1;
use crate::tlusty::math::special::gaunt;
use crate::tlusty::math::hydrogen::sgmer1;
use crate::tlusty::state::constants::{HK, SIGE, H, UN};
use crate::tlusty::state::model::{GffPar, ModelState};
use crate::tlusty::state::atomic::AtomicData;
// ============================================================================
// 常量 (from Fortran OPACF0)
// ============================================================================
/// SIGK 常数: SIH0 = 2.815e29 (氢原子 bf 截面基准, cm² × Hz³)
const SIH0: f64 = 2.815e29;
/// FF 基准常数: SGFF0 = 3.694e8
const SGFF0: f64 = 3.694e8;
/// C14 = 2.99793e14 (用于 Gaunt 因子计算)
const C14: f64 = 2.99793e14;
// ============================================================================
// BF 跃迁数据
// ============================================================================
/// 束缚-自由跃迁信息。
#[derive(Debug, Clone)]
pub struct BfTransition {
/// 下能级索引 (0-based)
pub ilow: usize,
/// 上能级 (连续区) 索引 (0-based)
pub iup: usize,
/// 离子索引 (0-based)
pub ion_idx: usize,
/// 有效主量子数
pub nquant: f64,
/// Z² (原子序数的平方)
pub iz2: f64,
/// 电离频率 (Hz), ν_n = ENION(n)/H
pub nu_edge: f64,
/// He I 量子数 (s, l, n) — 使用 HEPHOT OP 截面而非氢原子 SIGK
pub he1_quant: Option<(i32, i32, i32)>,
}
// ============================================================================
// FF 离子数据
// ============================================================================
/// 自由-自由离子信息。
#[derive(Debug, Clone)]
pub struct FfIon {
/// 连续区能级索引 NNEXT (0-based)
pub nnext: usize,
/// Z² (CHARG2)
pub charg2: f64,
/// FF 模式: 1=hydrogenic Gaunt=1, 2=exact Gaunt (GFREE1)
pub itra: i32,
/// FF 边缘频率 (Hz), 用于 stimulated emission switch
pub ff_edge: f64,
}
// ============================================================================
// Opacf0State
// ============================================================================
/// OPACF0 预计算状态。
///
/// 存储从原子数据预计算的 BF 跃迁和 FF 离子信息,
/// 在 RESOLV 中与 model.levpop.popul 一起使用计算不透明度。
#[derive(Debug, Clone)]
pub struct Opacf0State {
/// BF 跃迁列表 (对应 Fortran NTRANC 个 ITRBF 跃迁)
pub bf_transitions: Vec<BfTransition>,
/// FF 离子列表 (对应 Fortran NION 个离子)
pub ff_ions: Vec<FfIon>,
/// 是否使用 HEPHOT OP 截面 (替代氢原子 SIGK) 计算 He I bf
/// 注意: 启用后需要完整 OPACF0+SOLVES 管线才能收敛
pub use_hephot: bool,
}
impl Opacf0State {
/// 创建 HHe 模型的 Opacf0State。
///
/// HHe 模型结构 (Rust 0-based):
/// - Ion 0 (H I): levels 0-8 (n=1..9), continuum=9
/// - Ion 1 (He I): levels 10-23 (14 levels), continuum=24
/// - Ion 2 (He II): levels 24-37 (n=1..14), continuum=38
/// - NLEVEL=39, NION=3
pub fn new_hhe() -> Self {
let h_planck: f64 = HK * 1.3806e-16; // H = HK × k = 6.6256e-27 erg·s
let eh: f64 = 2.17853041e-11; // H 电离能 (erg)
let mut bf_transitions = Vec::new();
// === H I bf 跃迁 (n=1..9 → continuum level 9) ===
// Hydrogenic: ν_n = EH/(n² × H)
for n in 1..=9_usize {
let nn = n as f64;
let nu_n = eh / (nn * nn * h_planck); // ionization frequency of level n
bf_transitions.push(BfTransition {
ilow: n - 1,
iup: 9,
ion_idx: 0,
nquant: nn,
iz2: 1.0, // Z=1, Z²=1
nu_edge: nu_n,
he1_quant: None, // H I: use hydrogenic SIGK
});
}
// === He I bf 跃迁 (14 levels → continuum level 24) ===
// 使用实际频率数据 (from he1.dat) + HEPHOT OP 截面
// 量子数 (s, l, n) from he1.dat level definitions
let he1_freq: [f64; 14] = [
5.94503520e15, 1.15267210e15, 9.60145430e14, 8.75933720e14,
8.14536220e14, 4.51727350e14, 4.02921120e14, 3.81935640e14,
3.65836790e14, 3.65746870e14, 3.62599020e14, 2.40043860e14,
2.20797190e14, 2.12492940e14,
];
let he1_nq: [f64; 14] = [
1.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0,
];
// He I quantum numbers (s, l, n) from he1.dat / sbfhe1.f
let he1_sln: [(i32, i32, i32); 14] = [
(1, 0, 1), // 1 sing S (G=1)
(3, 0, 2), // 2 trip S (G=3)
(1, 0, 2), // 2 sing S (G=1)
(3, 1, 2), // 2 trip P (G=9)
(1, 1, 2), // 2 sing P (G=3)
(3, 0, 3), // 3 trip S (G=3)
(1, 0, 3), // 3 sing S (G=1)
(3, 1, 3), // 3 trip P (G=9)
(3, 2, 3), // 3 trip D (G=15)
(1, 2, 3), // 3 sing D (G=5)
(1, 1, 3), // 3 sing P (G=3)
(3, 0, 4), // 4 trip S (G=3)
(1, 0, 4), // 4 sing S (G=1)
(3, 1, 4), // 4 trip P (G=9)
];
for ilev in 0..14 {
bf_transitions.push(BfTransition {
ilow: 10 + ilev,
iup: 24,
ion_idx: 1,
nquant: he1_nq[ilev],
iz2: 4.0,
nu_edge: he1_freq[ilev],
he1_quant: Some(he1_sln[ilev]),
});
}
// === He II bf 跃迁 (n=1..14 → continuum level 38) ===
// Hydrogenic Z=2: ν_n = 4×EH/(n² × H)
let he2_freq: [f64; 14] = [
1.31575980e16, 3.28939940e15, 1.46195530e15, 8.22349860e14,
5.26303910e14, 3.65488820e14, 2.68522400e14, 2.05587460e14,
1.62439480e14, 1.31575980e14, 1.08740480e14, 9.13722060e13,
7.78556080e13, 6.71306000e13,
];
for n in 1..=14_usize {
let nn = n as f64;
bf_transitions.push(BfTransition {
ilow: 24 + n - 1,
iup: 38,
ion_idx: 2,
nquant: nn,
iz2: 4.0, // Z=2, Z²=4
nu_edge: he2_freq[n - 1],
he1_quant: None, // He II: hydrogenic, use SIGK
});
}
// === FF 离子 ===
let ff_ions = vec![
FfIon {
nnext: 9, // H II (continuum)
charg2: 1.0, // Z²=1 for H⁺
itra: 2, // exact Gaunt (GFREE1)
ff_edge: 0.0, // H: FF(ION)=0 → no edge
},
FfIon {
nnext: 24, // He II (continuum)
charg2: 1.0, // Z²=1 for He⁺
itra: 2,
ff_edge: 0.0,
},
FfIon {
nnext: 38, // He III (continuum)
charg2: 4.0, // Z²=4 for He²⁺
itra: 2,
ff_edge: 0.0,
},
];
Self {
bf_transitions,
ff_ions,
use_hephot: true, // He I: use Opacity Project cross-sections (HEPHOT)
}
}
/// 计算给定 (depth, frequency) 的连续谱不透明度。
///
/// 对应 Fortran OPACF0 的频率循环体 (行 168-346)。
/// 使用模型种群 POPUL(level, depth) 而非重新计算 Saha 平衡。
///
/// # 参数
///
/// * `fr` - 频率 (Hz)
/// * `t` - 温度 (K)
/// * `ne` - 电子密度 (cm⁻³)
/// * `popul` - 种群数组 popul[level][depth] (cm⁻³)
/// * `id` - 深度索引 (0-based)
/// * `gffpar` - Gaunt 因子预计算参数 (由 gfree0 填充)
///
/// # 返回
///
/// Returns (true_abs, scat, emis_pre_stim, rayleigh) where:
/// - true_abs: true absorption after stimulated emission correction (cm⁻¹)
/// - scat: total scattering = elscat + rayleigh (cm⁻¹)
/// - emis_pre_stim: accumulated emission before stimulated emission correction (cm⁻¹)
/// Needed for OPACFL finalization: EMIS1L = emis_pre_stim * XKF * Bν
/// - rayleigh: Rayleigh scattering only (cm⁻¹), needed for SCAT1 in Lucy
pub fn compute_opacity(
&self,
fr: f64,
t: f64,
ne: f64,
popul: &[Vec<f64>],
id: usize,
gffpar: &GffPar,
) -> (f64, f64, f64, f64) {
let hkt = HK / t;
let sqrt_t = t.sqrt();
let _tk1 = 1.0 / (H * t); // 1/(kT) in erg⁻¹
// 电子散射 (对应 Fortran: ELSCAT = ELEC(ID) × SIGE)
let elscat = SIGE * ne;
// 预计算频率相关量
let fr_inv = 1.0 / fr;
let fr3_inv = fr_inv * fr_inv * fr_inv;
// 受激发射因子 (XKF = exp(-hν/kT))
let xkf = if hkt * fr < 150.0 {
(-hkt * fr).exp()
} else {
0.0
};
let mut abso = elscat;
let mut emis = 0.0;
// ================================================================
// 1. 束缚-自由贡献
// ================================================================
// 对应 Fortran: DO 30 IBFT=1,NTRANC → ABSO += CROSS × ABTRA; EMIS += CROSS × EMTRA
for bt in &self.bf_transitions {
if fr < bt.nu_edge {
continue;
}
let pop_low = popul[bt.ilow][id];
if pop_low <= 0.0 {
continue;
}
// 截面计算:
// He I: HEPHOT (OP cross-sections) if enabled
// H I / He II: hydrogenic SIGK with exact Gaunt factor (IBF=1)
// SIGK = SIH0 * Z^4 / (nu^3 * n^5) * GAUNT(n, nu/Z^2)
let sigma = if self.use_hephot {
if let Some((s, l, n)) = bt.he1_quant {
hephot(s, l, n, fr)
} else {
let sigma_base = SIH0 * bt.iz2 * bt.iz2 * fr3_inv / bt.nquant.powi(5);
let g_bf = gaunt(bt.nquant as usize, fr / bt.iz2);
sigma_base * g_bf
}
} else {
let sigma_base = SIH0 * bt.iz2 * bt.iz2 * fr3_inv / bt.nquant.powi(5);
let g_bf = gaunt(bt.nquant as usize, fr / bt.iz2);
sigma_base * g_bf
};
if sigma <= 0.0 {
continue;
}
// ABTRA = POPUL(II, ID) — 下能级(束缚态)种群
abso += sigma * pop_low;
// EMTRA — LTE 中 Kirchhoff 定律: EMTRA = ABTRA = pop_low × σ
// 对应 Fortran OPACF0 行 70-71:
// ABTRA(ITR,ID) = POPUL(II,ID)
// EMTRA(ITR,ID) = POPUL(JJ,ID)*ANE*SBF(II)*WOP(II,ID)*CORR
// LTE 中 (Saha-Boltzmann 平衡, WOP=1, CORR=1): EMTRA = POPUL(II) = pop_low
emis += sigma * pop_low;
}
// ================================================================
// 2. 自由-自由贡献
// ================================================================
// 对应 Fortran: DO 40 ION=1,NION
let sgff = SGFF0 / sqrt_t * ne;
for ion in &self.ff_ions {
let pop_cont = popul[ion.nnext][id];
if pop_cont <= 0.0 {
continue;
}
// SF1 = SFF3(ION,ID) × FR3INV
let sf1 = sgff * ion.charg2 * pop_cont * fr3_inv;
// SF2 = SFF2(ION,ID) = exp(FF(ION) × HKT1(ID))
// FF(ION) = 0 for our model → SF2 = 1
let sf2 = 1.0_f64; // FF=0 → exp(0) = 1
let mut absoff = sf1 * sf2;
// Exact Gaunt factor (ITRA=2)
// Fortran OPACF0 line ~210: SFF3(ION,ID) = GFREE1(ID,X) * SFF2(ION,ID)
// where X = C14 * CHARG2(ION) / FR
if ion.itra == 2 {
let x = C14 * ion.charg2 / fr;
let gf1 = gfree1(id, x, gffpar);
// SFF3 = GF1 * SFF2 → absoff = SF1 * GF1 (replacing the default G=1)
absoff = sf1 * gf1;
}
abso += absoff;
emis += absoff; // ff: Kirchhoff (emission = absorption for thermal ff)
}
// ================================================================
// 受激发射修正
// ================================================================
// 对应 Fortran: ABSO(IJ) = ABSO(IJ) - EMIS(IJ) × XKF(ID)
abso -= emis * xkf;
// ================================================================
// 3. OPADD0 — 额外不透明度来源 (Rayleigh 散射)
// ================================================================
// 对应 Fortran OPADD0: HI Rayleigh, HeI Rayleigh
let mut rayleigh = 0.0;
// HI Rayleigh scattering (IRSCT=1 in Fortran)
// σ_Ray(HI) = (CR0 + (CR1 + CR2/X)/X) / X², X = (c/λ)²
// where λ = c/ν, so X = (c/ν)² = c²/ν²
// clamped at FRRAY = 2.463e15 Hz
{
const CLS: f64 = 2.997925e18;
const CR0: f64 = 5.799e-13;
const CR1: f64 = 1.422e-6;
const CR2: f64 = 2.784;
const FRRAY: f64 = 2.463e15;
let frm = fr.min(FRRAY);
let x = (CLS / frm) * (CLS / frm); // X = (c/λ)²
let cs_ray_hi = (CR0 + (CR1 + CR2 / x) / x) / (x * x);
// POPUL(0, id) = H I ground state population
rayleigh += popul[0][id] * cs_ray_hi;
}
// HeI Rayleigh scattering (IRSCHE=1 in Fortran)
// σ_Ray(HeI) = 5.484e-14 / X² * (1 + (2.44e5 + 5.94e10/(X-2.90e5))/X)²
// clamped at FRAYHe = 5.150e15 Hz
{
const CLS: f64 = 2.997925e18;
const FRAYHE: f64 = 5.150e15;
let frm = fr.min(FRAYHE);
let x = (CLS / frm) * (CLS / frm);
let cs_ray_he1 =
5.484e-14 / (x * x) * (1.0 + (2.44e5 + 5.94e10 / (x - 2.90e5)) / x).powi(2);
// He I ground state is level 10 (index 10)
rayleigh += popul[10][id] * cs_ray_he1;
}
// 分离散射和真吸收
// abso = elscat + true_absorption (after stimulated emission)
// true_abs = abso - elscat = emis × (1 - XKF)
let true_abs = (abso - elscat).max(0.0);
let scat = elscat + rayleigh;
(true_abs, scat, emis, rayleigh)
}
/// 使用真实模型数据BFCS 和 SGMER计算连续谱不透明度完全匹配 Fortran OPACF0
pub fn compute_opacity_from_model(
fr: f64,
ij: usize,
t: f64,
ne: f64,
popul: &[Vec<f64>],
id: usize,
gffpar: &GffPar,
model: &ModelState,
atomic: &AtomicData,
) -> (f64, f64, f64, f64) {
let hkt = HK / t;
let sqrt_t = t.sqrt();
let tk1 = 1.0 / (H * t);
let elscat = SIGE * ne;
let fr_inv = 1.0 / fr;
let fr3_inv = fr_inv * fr_inv * fr_inv;
let xkf = if hkt * fr < 150.0 {
(-hkt * fr).exp()
} else {
0.0
};
let mut abso = elscat;
let mut emis = 0.0;
// ================================================================
// 1. 束缚-自由贡献
// ================================================================
let ntranc = model.obfpar.itrbf.iter().filter(|&&x| x > 0).count();
for ibft in 0..ntranc {
let itr = model.obfpar.itrbf[ibft] as usize - 1;
if atomic.trapar.indexp[itr] == 0 { continue; }
let ii = atomic.trapar.ilow[itr] as usize - 1;
let jj = atomic.trapar.iup[itr] as usize - 1;
let it = atomic.trapar.itra[ii][jj] as usize;
if it == 0 { continue; }
let pop_low = popul[ii][id];
// 交叉截面 CROSS
let ij0 = model.frqall.ijbf[ij] as usize;
let a1 = model.phoexp.aijbf[ij];
let sig0 = model.phoexp.bfcs[ibft][ij0] as f64;
let sig1 = model.phoexp.bfcs[ibft][ij0 + 1] as f64;
let mut sigma = a1 * sig0 + (UN - a1) * sig1;
// SGMER 修正 (合并能级)
let imer = model.mrgpar.imrg[ii];
if imer > 0 {
sigma = sgmer1(fr_inv, fr3_inv, imer, id + 1, &model.mrgpar);
}
if sigma <= 0.0 { continue; }
// CORR 计算
let ie = atomic.levpar.iel[ii] as usize - 1;
let nke = atomic.ionpar.nnext[ie] as usize - 1;
let corr = if nke != jj {
let g_ratio = atomic.levpar.g[nke] / atomic.levpar.g[jj];
let delta_e = atomic.levpar.enion[nke] - atomic.levpar.enion[jj];
g_ratio * (delta_e * tk1).exp()
} else {
UN
};
let pop_up = popul[jj][id];
let wop_ii = model.wmcomp.wop[ii][id];
let emis_val = pop_up * ne * model.levpop.sbf[ii] * wop_ii * corr;
abso += sigma * pop_low;
emis += sigma * emis_val;
}
// ================================================================
// 2. 自由-自由贡献
// ================================================================
let sgff = SGFF0 / sqrt_t * ne;
let nion = atomic.ionpar.iz.iter().filter(|&&x| x > 0).count();
for ion in 0..nion {
let nnext = atomic.ionpar.nnext[ion] as usize - 1;
let pop_cont = popul[nnext][id];
if pop_cont <= 0.0 { continue; }
let charg2 = atomic.ionpar.charg2[ion];
let sf1 = sgff * charg2 * pop_cont * fr3_inv;
// sf2 = exp(ff * hkt1)
let ff_val = atomic.ionpar.ff[ion];
let mut sf2 = (ff_val * hkt).exp();
if fr < ff_val {
sf2 = UN / xkf;
}
let mut absoff = sf1 * sf2;
// Gaunt factor
let itra = atomic.trapar.itra[nnext][nnext];
if itra == 2 {
let x = C14 * charg2 / fr;
let gf1 = gfree1(id, x, gffpar);
sf2 = sf2 - UN + gf1;
absoff = sf1 * sf2;
}
abso += absoff;
emis += absoff;
}
abso -= emis * xkf;
// ================================================================
// 3. OPADD0 — 额外不透明度来源 (Rayleigh 散射)
// ================================================================
let mut rayleigh = 0.0;
// HI Rayleigh scattering
if nion > 0 {
const CLS: f64 = 2.997925e18;
const CR0: f64 = 5.799e-13;
const CR1: f64 = 1.422e-6;
const CR2: f64 = 2.784;
const FRRAY: f64 = 2.463e15;
let frm = fr.min(FRRAY);
let x = (CLS / frm) * (CLS / frm);
let cs_ray_hi = (CR0 + (CR1 + CR2 / x) / x) / (x * x);
// 假设 H I 基态为能级 0
if atomic.levpar.iel[0] == 1 {
rayleigh += popul[0][id] * cs_ray_hi;
}
}
// HeI Rayleigh scattering
if nion > 1 {
const CLS: f64 = 2.997925e18;
const FRAYHE: f64 = 5.150e15;
let frm = fr.min(FRAYHE);
let x = (CLS / frm) * (CLS / frm);
let cs_ray_he1 =
5.484e-14 / (x * x) * (1.0 + (2.44e5 + 5.94e10 / (x - 2.90e5)) / x).powi(2);
// 寻找 He I 基态
let nlevel = atomic.levpar.iel.iter().filter(|&&x| x > 0).count();
for i in 0..nlevel {
if atomic.levpar.iel[i] == 2 && atomic.ionpar.iz[1] == 1 { // He I
rayleigh += popul[i][id] * cs_ray_he1;
break;
}
}
}
let true_abs = (abso - elscat).max(0.0);
let scat = elscat + rayleigh;
(true_abs, scat, emis, rayleigh)
}
/// 计算给定 (frequency, temperature, populations) 的连续谱不透明度。
///
/// 单深度版本,直接接收 `popul[level]` 扁平数组(无需 2D 数组 + depth 索引)。
/// 用于有限差分温度导数计算(需要扰动种群但不修改全局模型)。
pub fn compute_opacity_1d(
&self,
fr: f64,
t: f64,
ne: f64,
popul: &[f64],
gffpar: &GffPar,
) -> (f64, f64, f64, f64) {
let hkt = HK / t;
let sqrt_t = t.sqrt();
let elscat = SIGE * ne;
let fr_inv = 1.0 / fr;
let fr3_inv = fr_inv * fr_inv * fr_inv;
let xkf = if hkt * fr < 150.0 {
(-hkt * fr).exp()
} else {
0.0
};
let mut abso = elscat;
let mut emis = 0.0;
// BF contributions
for bt in &self.bf_transitions {
if fr < bt.nu_edge { continue; }
let pop_low = popul[bt.ilow];
if pop_low <= 0.0 { continue; }
let sigma = if self.use_hephot {
if let Some((s, l, n)) = bt.he1_quant {
hephot(s, l, n, fr)
} else {
let sigma_base = SIH0 * bt.iz2 * bt.iz2 * fr3_inv / bt.nquant.powi(5);
let g_bf = gaunt(bt.nquant as usize, fr / bt.iz2);
sigma_base * g_bf
}
} else {
let sigma_base = SIH0 * bt.iz2 * bt.iz2 * fr3_inv / bt.nquant.powi(5);
let g_bf = gaunt(bt.nquant as usize, fr / bt.iz2);
sigma_base * g_bf
};
if sigma <= 0.0 { continue; }
abso += sigma * pop_low;
emis += sigma * pop_low;
}
// FF contributions
let sgff = SGFF0 / sqrt_t * ne;
for ion in &self.ff_ions {
let pop_cont = popul[ion.nnext];
if pop_cont <= 0.0 { continue; }
let sf1 = sgff * ion.charg2 * pop_cont * fr3_inv;
let mut absoff = sf1; // SF2=1 (FF=0)
if ion.itra == 2 {
let x = C14 * ion.charg2 / fr;
let gf1 = gfree1(0, x, gffpar);
absoff = sf1 * gf1;
}
abso += absoff;
emis += absoff;
}
abso -= emis * xkf;
let true_abs = (abso - elscat).max(0.0);
// Rayleigh scattering (1D version — same formula as compute_opacity)
let mut rayleigh = 0.0;
{
const CLS: f64 = 2.997925e18;
const CR0: f64 = 5.799e-13;
const CR1: f64 = 1.422e-6;
const CR2: f64 = 2.784;
const FRRAY: f64 = 2.463e15;
let frm = fr.min(FRRAY);
let x = (CLS / frm) * (CLS / frm);
let cs_ray_hi = (CR0 + (CR1 + CR2 / x) / x) / (x * x);
rayleigh += popul[0] * cs_ray_hi;
}
{
const CLS: f64 = 2.997925e18;
const FRAYHE: f64 = 5.150e15;
let frm = fr.min(FRAYHE);
let x = (CLS / frm) * (CLS / frm);
let cs_ray_he1 =
5.484e-14 / (x * x) * (1.0 + (2.44e5 + 5.94e10 / (x - 2.90e5)) / x).powi(2);
rayleigh += popul[10] * cs_ray_he1;
}
(true_abs, elscat + rayleigh, emis, rayleigh)
}
}
impl Default for Opacf0State {
fn default() -> Self {
Self::new_hhe()
}
}
// ============================================================================
// 测试
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use crate::tlusty::state::constants::MDEPTH;
use crate::tlusty::math::atomic::gfree0;
#[test]
fn test_opacf0_state_hhe() {
let state = Opacf0State::new_hhe();
// H I: 9 bf, He I: 14 bf, He II: 14 bf → total 37
assert_eq!(state.bf_transitions.len(), 37);
assert_eq!(state.ff_ions.len(), 3);
// Check first H I bf transition
assert_eq!(state.bf_transitions[0].ilow, 0); // H I n=1
assert_eq!(state.bf_transitions[0].iup, 9); // H II
assert_eq!(state.bf_transitions[0].iz2, 1.0);
// Check first He I bf transition
assert_eq!(state.bf_transitions[9].ilow, 10); // He I level 0
assert_eq!(state.bf_transitions[9].iup, 24); // He II
// Check first He II bf transition
assert_eq!(state.bf_transitions[23].ilow, 24); // He II n=1
assert_eq!(state.bf_transitions[23].iup, 38); // He III
assert_eq!(state.bf_transitions[23].iz2, 4.0);
}
#[test]
fn test_compute_opacity_basic() {
let state = Opacf0State::new_hhe();
let nlevel = 39;
let nd = 1;
// Create simple populations
let mut popul = vec![vec![0.0_f64; nd]; nlevel];
popul[0][0] = 1e12; // H I n=1
popul[9][0] = 1e14; // H II
popul[10][0] = 1e10; // He I n=1
popul[24][0] = 1e10; // He II n=1
popul[38][0] = 1e12; // He III
let fr = 3.28805e15; // H Lyman edge
let t = 30000.0;
let ne = 1e14;
// Precompute Gaunt factor coefficients
let mut gffpar = GffPar::new();
let temp = [30000.0_f64; MDEPTH];
gfree0(0, &temp, &mut gffpar);
let (ab, sct, _emis, ray) = state.compute_opacity(fr, t, ne, &popul, 0, &gffpar);
assert!(ab > 0.0, "absorption should be positive");
assert!(sct > 0.0, "scattering should be positive");
assert!(ab > sct, "at H Lyman edge, absorption should dominate scattering");
assert!(ray > 0.0, "Rayleigh scattering should be positive");
}
#[test]
fn test_compute_opacity_consistency() {
// Test that opacity is consistent across nearby frequencies
let state = Opacf0State::new_hhe();
let nlevel = 39;
let nd = 1;
let mut popul = vec![vec![0.0_f64; nd]; nlevel];
popul[0][0] = 1e12;
popul[9][0] = 1e14;
let t = 30000.0;
let ne = 1e14;
let mut gffpar = GffPar::new();
let temp = [30000.0_f64; MDEPTH];
gfree0(0, &temp, &mut gffpar);
// Just above H Lyman edge
let fr1 = 3.3e15;
// Just below H Lyman edge
let fr2 = 3.2e15;
let (ab1, _, _, _) = state.compute_opacity(fr1, t, ne, &popul, 0, &gffpar);
let (ab2, _, _, _) = state.compute_opacity(fr2, t, ne, &popul, 0, &gffpar);
// Above edge: bf contributes → higher opacity
// Below edge: no H I bf → lower opacity
assert!(ab1 > ab2, "opacity above Lyman edge should be higher");
}
}