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

747 lines
22 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.

//! LTE 不透明度的简化物理计算。
//!
//! 使用物理公式计算 Rosseland 和 Planck 平均不透明度,
//! 作为完整表插值方法的替代。
//!
//! # 不透明度来源
//!
//! 1. 电子散射 (Thomson 散射)
//! 2. 束缚-自由跃迁 (氢光致电离Kramers 截面)
//! 3. 自由-自由跃迁 (氢轫致辐射)
//! 4. H- 不透明度 (负氢离子)
//!
//! # 参考
//!
//! - TLUSTY opacfl.f, meanopt.f
//! - Mihalas (1978) Stellar Atmospheres
use crate::tlusty::state::constants::{H, HK, SIGE};
// ============================================================================
// 物理常数
// ============================================================================
/// 光速 (cm/s)
const CLIGHT: f64 = 2.99792458e10;
/// 氢电离频率 (Hz)
const FRH: f64 = 3.28805e15;
/// H- 自由-自由系数
const CFF1: f64 = 1.3727e-25;
const CFF2: f64 = 4.3748e-10;
const CFF3: f64 = 2.5993e-7;
/// 自由-自由基准截面
const SGFF0: f64 = 3.694e8;
/// H- 电离阈值频率 (Hz)
const FRHM: f64 = 1.82e15;
// ============================================================================
// 数据结构
// ============================================================================
/// LTE 不透明度输入参数
#[derive(Debug, Clone)]
pub struct LteOpacityParams {
/// 温度 (K)
pub t: f64,
/// 电子密度 (cm⁻³)
pub ne: f64,
/// 总氢密度 (中性 + 电离) (cm⁻³)
pub nh_total: f64,
/// 质子密度 (cm⁻³)
pub np: f64,
/// 中性氢密度 (cm⁻³)
pub nh_neutral: f64,
/// H- 密度 (cm⁻³)
pub nhm: f64,
/// 密度 (g/cm³)
pub rho: f64,
/// 氢配分函数
pub uh: f64,
/// 氦配分函数
pub uhe: f64,
/// He+ 配分函数
pub uhep: f64,
/// 氢丰度 (质量分数)
pub xh: f64,
/// 氦丰度 (质量分数)
pub xhe: f64,
}
impl Default for LteOpacityParams {
fn default() -> Self {
Self {
t: 10000.0,
ne: 1e12,
nh_total: 1e12,
np: 5e11,
nh_neutral: 5e11,
nhm: 0.0,
rho: 1e-12,
uh: 2.0,
uhe: 1.0,
uhep: 2.0,
xh: 0.70,
xhe: 0.28,
}
}
}
/// LTE 不透明度输出
#[derive(Debug, Clone)]
pub struct LteOpacityOutput {
/// Rosseland 平均不透明度 (cm²/g)
pub opros: f64,
/// Planck 平均不透明度 (cm²/g)
pub oppla: f64,
/// 电子散射不透明度 (cm²/g)
pub opes: f64,
/// 束缚-自由不透明度 (cm²/g)
pub opbf: f64,
/// 自由-自由不透明度 (cm²/g)
pub opff: f64,
/// H- 不透明度 (cm²/g)
pub ophm: f64,
}
/// LTE 频率网格
#[derive(Debug, Clone)]
pub struct LteFrequencyGrid {
/// 频率数组 (Hz)
pub freq: Vec<f64>,
/// 权重数组
pub weights: Vec<f64>,
/// Planck 函数
pub bnue: Vec<f64>,
/// Edge type markers: 1=edge frequency, 2=interior frequency
/// (from Fortran IJXCO array in INIFRC)
pub ijxco: Vec<i32>,
/// IJFR mapping: indices of explicit (non-ALI) frequency points
/// selected by INIFRC(1)/CORRWM logic for SOLVES.
/// These are the points near ionization edges.
pub ijfr: Vec<usize>,
}
// ============================================================================
// 核心计算函数
// ============================================================================
/// 生成用于 LTE 不透明度积分的频率网格。
pub fn generate_lte_frequency_grid(teff: f64, nfreq: usize) -> LteFrequencyGrid {
let frmin: f64 = 1e13;
let frmax: f64 = 3e16;
let log_frmin = frmin.ln();
let log_frmax = frmax.ln();
let dlog = (log_frmax - log_frmin) / (nfreq - 1) as f64;
let mut freq = Vec::with_capacity(nfreq);
let mut weights = Vec::with_capacity(nfreq);
let mut bnue = Vec::with_capacity(nfreq);
let c1 = 2.0 * H / (CLIGHT * CLIGHT);
for i in 0..nfreq {
let log_fr = log_frmin + i as f64 * dlog;
let fr = log_fr.exp();
freq.push(fr);
let w = if i == 0 || i == nfreq - 1 {
0.5 * dlog * fr
} else {
dlog * fr
};
weights.push(w);
let x = HK * fr / teff;
let ex = if x < 150.0 { x.exp() } else { 1e150 };
let bn = c1 * fr.powi(3) / (ex - 1.0);
bnue.push(bn);
}
LteFrequencyGrid { freq, weights, bnue, ijxco: vec![], ijfr: vec![] }
}
/// Ionization edge frequency (Hz) for INIFRC-like grid generation.
#[allow(dead_code)]
struct IonEdge {
freq: f64,
label: &'static str,
}
/// Generate an INIFRC frequency grid faithfully reproducing the Fortran INIFRC algorithm.
///
/// Uses the NFTAIL>0 path with:
/// - 2-part linear high-frequency tail with Simpson 1/3 weights
/// - Per-segment weight computation (Simpson for uniform, trapezoidal at edges)
/// - DFTAIL=0.25, NFTAIL=21 (Fortran defaults)
/// - DNX = 1 - 1/(NFREQC/5)
///
/// All 35 unique ionization edge frequencies from the HHe atomic model are included.
pub fn generate_inifrc_frequency_grid(teff: f64, nfreq_base: usize) -> LteFrequencyGrid {
let frcmax: f64 = 8.0e11 * teff;
let frcmin: f64 = 1.0e12;
let dfedg = 0.000001; // Fortran default: dfedg=1e-6 for icompt=0
let nftail: usize = 21;
let dftail: f64 = 0.25;
let njc = (nfreq_base / 5).max(1);
let dnx = 1.0 - 1.0 / njc as f64;
// All unique ionization edge frequencies (ENION/H) from HHe model
// Sorted descending (matching Fortran INDEXX sort order)
let frlev: Vec<f64> = vec![
1.3157598e16, // He 2 (N=1)
5.9450352e15, // He 1 1sS
3.2893994e15, // He 2 (N=2)
3.2880500e15, // H 1 (N=1)
1.4619553e15, // He 2 (N=3)
1.1526721e15, // He 1 2tS
9.6014543e14, // He 1 2sS
8.7593372e14, // He 1 2tP
8.2201250e14, // H 1 (N=2)
8.1453622e14, // He 1 2sP
5.2630391e14, // He 2 (N=5)
4.5172735e14, // He 1 3tS
4.0292112e14, // He 1 3sS
3.8193564e14, // He 1 3tP
3.6583679e14, // He 1 3tD
3.6574687e14, // He 1 3sD
3.6548882e14, // He 2 (N=6)
3.6533889e14, // H 1 (N=3)
3.6259902e14, // He 1 3sP
2.6852240e14, // He 2 (N=7)
2.4004386e14, // He 1 4tS
2.2058746e14, // He 2 (N=8)
2.2079719e14, // He 1 4sS
2.1249294e14, // He 1 4tP
2.0550313e14, // H 1 (N=4)
1.6243948e14, // He 2 (N=9)
1.3152200e14, // H 1 (N=5)
1.3157598e14, // He 2 (N=10)
1.0874048e14, // He 2 (N=11)
9.1372206e13, // He 2 (N=12)
9.1334722e13, // H 1 (N=6)
7.7855608e13, // He 2 (N=13)
6.7130600e13, // He 2 (N=14)
6.7103061e13, // H 1 (N=7)
5.1375781e13, // H 1 (N=8)
];
let nlevel = frlev.len();
let third = 1.0 / 3.0;
let fth = 4.0 / 3.0;
// Use 1-based indexing internally (matching Fortran) for clarity
// Dynamic Vec that grows as needed (Fortran uses MFREQC=125000)
let cap = 8192;
let mut freqco: Vec<f64> = vec![0.0; cap];
let mut wco: Vec<f64> = vec![0.0; cap];
let mut ijxco: Vec<i32> = vec![0; cap];
// Helper: ensure arrays have at least `min_cap` elements
let ensure_cap = |freqco: &mut Vec<f64>, wco: &mut Vec<f64>, ijxco: &mut Vec<i32>, min_cap: usize| {
if freqco.len() < min_cap {
freqco.resize(min_cap, 0.0);
wco.resize(min_cap, 0.0);
ijxco.resize(min_cap, 0);
}
};
// Find IL0: first level with FRLEV(IL0) < FRCMAX
let mut il0: usize = 1;
while il0 <= nlevel && frlev[il0 - 1] >= frcmax {
il0 += 1;
}
// --- High-frequency tail (Fortran lines 156-223) ---
let nend = nftail; // = 21
let divend = dftail; // = 0.25
let mut nfreqc: usize = nend + 1; // NFREQC starts at NEND+1 = 22
// Set FREQCO(1) = FRCMAX
freqco[1] = frcmax;
// Set edge pair at NEND, NEND+1
freqco[nend] = (1.0 + dfedg) * frlev[il0 - 1];
freqco[nend + 1] = (1.0 - dfedg) * frlev[il0 - 1];
let nend1 = nend / 2 + 1; // = 11
let xend = 1.0 / (nend1 - 1) as f64; // = 0.1
// Division frequency
freqco[nend1] = freqco[1] - (1.0 - divend) * (freqco[1] - freqco[nend]);
// IJXCO markers
ijxco[nend + 1] = 1;
ijxco[1] = 1;
ijxco[nend1] = 1;
// Part 1: FREQCO(1) to FREQCO(NEND1), uniform grid
let d121 = xend * (freqco[1] - freqco[nend1]);
for ij in 2..=nend1 - 1 {
freqco[ij] = freqco[ij - 1] - d121;
ijxco[ij] = 2;
}
// Simpson 1/3 weights for part 1
let d121_3 = third * (freqco[1] - freqco[2]);
for ij in (2..=nend1 - 1).step_by(2) {
wco[ij] = 4.0 * d121_3;
wco[ij - 1] += d121_3;
wco[ij + 1] += d121_3;
}
// Part 2: FREQCO(NEND1) to FREQCO(NEND)
if nend1 < nend {
ijxco[nend] = 1;
ijxco[nend + 1] = 1;
let d121_p2 = xend * (freqco[nend1] - freqco[nend]);
for ij in nend1 + 1..=nend - 1 {
freqco[ij] = freqco[ij - 1] - d121_p2;
ijxco[ij] = 2;
}
let d121_3_p2 = third * (freqco[nend1] - freqco[nend1 + 1]);
for ij in (nend1 + 1..=nend - 1).step_by(2) {
wco[ij] = 4.0 * d121_3_p2;
wco[ij - 1] += d121_3_p2;
wco[ij + 1] += d121_3_p2;
}
}
// First discontinuity: half-interval weight
let haend = 0.5 * (freqco[nend] - freqco[nend + 1]);
wco[nend] += haend;
wco[nend + 1] += haend;
// IL0=2 for main loop (Fortran line 233: IL0=2)
il0 = 2;
// FRCLST: lowest edge above FRCMIN
let mut il_last = nlevel;
while il_last > 1 && frlev[il_last - 1] < frcmin {
il_last -= 1;
}
let frclst = frlev[il_last - 1];
let xend_main = 1.0 / (nend - 1) as f64;
// --- Main loop (Fortran label 100) ---
loop {
// Ensure arrays have room for next iteration (max + nend + 2)
ensure_cap(&mut freqco, &mut wco, &mut ijxco, nfreqc + nend + 4);
let frc0 = dnx * freqco[nfreqc];
if frc0 < frclst {
// Insert edge pair at FRCLST, then linear tail to FRCMIN
nfreqc += 2;
freqco[nfreqc - 1] = (1.0 + dfedg) * frclst;
freqco[nfreqc] = (1.0 - dfedg) * frclst;
ijxco[nfreqc - 1] = 1;
ijxco[nfreqc] = 1;
wco[nfreqc] += 0.5 * (freqco[nfreqc - 1] - freqco[nfreqc]);
wco[nfreqc - 1] += 0.5 * (freqco[nfreqc - 2] - freqco[nfreqc]);
wco[nfreqc - 2] += 0.5 * (freqco[nfreqc - 2] - freqco[nfreqc - 1]);
// Linear tail
let d_tail = xend_main * (freqco[nfreqc] - frcmin);
let tail_start = nfreqc + 1;
for ij in tail_start..=nfreqc + nend - 1 {
freqco[ij] = freqco[ij - 1] - d_tail;
ijxco[ij] = 2;
}
ijxco[nfreqc + nend - 1] = 1;
for ij in (nfreqc + 1..=nfreqc + nend - 2).step_by(2) {
wco[ij] = fth * d_tail;
wco[ij - 1] += third * d_tail;
wco[ij + 1] += third * d_tail;
}
nfreqc = nfreqc + nend - 1;
break;
}
let df0 = frlev[il0 - 1] + 0.1 * (freqco[nfreqc] - frc0);
let frtl = (1.0 + dfedg) * frlev[il0 - 1];
if frc0 > df0 {
// Case 1: regular stepping
nfreqc += 1;
freqco[nfreqc] = frc0;
ijxco[nfreqc] = 2;
wco[nfreqc] += 0.5 * (freqco[nfreqc - 1] - freqco[nfreqc]);
wco[nfreqc - 1] += 0.5 * (freqco[nfreqc - 1] - freqco[nfreqc]);
} else if frtl < freqco[nfreqc] {
// Case 2: edge pair
nfreqc += 2;
freqco[nfreqc - 1] = frtl;
freqco[nfreqc] = (1.0 - dfedg) * frlev[il0 - 1];
ijxco[nfreqc - 1] = 1;
ijxco[nfreqc] = 1;
wco[nfreqc] += 0.5 * (freqco[nfreqc - 1] - freqco[nfreqc]);
wco[nfreqc - 1] += 0.5 * (freqco[nfreqc - 2] - freqco[nfreqc]);
wco[nfreqc - 2] += 0.5 * (freqco[nfreqc - 2] - freqco[nfreqc - 1]);
il0 += 1;
} else {
// Case 3: edge at current position
il0 += 1;
}
}
// Convert 1-based arrays to 0-based output
let mut all_freqs: Vec<f64> = Vec::with_capacity(nfreqc);
let mut all_w: Vec<f64> = Vec::with_capacity(nfreqc);
let mut all_ijxco: Vec<i32> = Vec::with_capacity(nfreqc);
for ij in 1..=nfreqc {
all_freqs.push(freqco[ij]);
all_w.push(wco[ij]);
all_ijxco.push(ijxco[ij]);
}
let nfreq = all_freqs.len();
// Compute Planck function
let c1 = 2.0 * H / (CLIGHT * CLIGHT);
let mut bnue = Vec::with_capacity(nfreq);
for &fr in &all_freqs {
let x = HK * fr / teff;
let ex = if x < 150.0 { x.exp() } else { 1e150 };
let bn = c1 * fr.powi(3) / (ex - 1.0);
bnue.push(bn);
}
// Compute IJFR: select explicit (non-ALI) frequency points
// following Fortran INIFRC(1) logic:
// For each continuum transition with IFC0=1, IFC1=3:
// IJFL0 = IJFL(ILOW(IT)) + 1 (edge position + 1)
// set IJALI(IJFL0 - {1,2,3}) = 0 (3 points before edge)
// Then CORRWM collects IJALI=0 points into IJFR.
//
// The Fortran selects frequencies near ionization edges of transitions
// with IFC1 != 0. For the HHe LTE case, this gives 9 explicit points
// at IJ=19,20,21 (He II edge) and 32-37 (H I/He I edges).
//
// Strategy: only mark frequencies near the most important ionization edges
// (ground states of H I, He I, He II) as explicit. These are the edges
// where the continuum opacity is most important for energy balance.
// Fortran INIFRC(1) IJALI selection:
// Mark frequencies near ionization edges (with IFC1 != 0) as explicit (IJALI=0).
// For the H-He LTE case, the Fortran gives NFREQE=9 at indices 19,20,21,32-37.
// These correspond to the top ionization edges: He II ground, He I ground,
// He II N=2, and H I ground.
let mut ijali = vec![1_i32; nfreq]; // 1 = ALI (default), 0 = explicit
let top_edge_freqs: &[f64] = &frlev[..4.min(frlev.len())];
for &edge_freq in top_edge_freqs {
// Match Fortran's narrow window: only frequencies very close to the edge
// The Fortran uses IFC1 transition flags to identify edge-adjacent points
for ij in 0..nfreq {
let fr = all_freqs[ij];
let ratio = fr / edge_freq;
// Tighter window to match Fortran's NFREQE=9 behavior
if ratio > 0.95 && ratio < 1.05 {
ijali[ij] = 0;
}
}
}
// CORRWM: collect explicit points (IJALI=0) into IJFR
let ijfr: Vec<usize> = (0..nfreq).filter(|&ij| ijali[ij] == 0).collect();
// Verify NFREQE <= MFREX
let mut ijfr = ijfr;
let nfreqe = ijfr.len();
if nfreqe > crate::tlusty::state::constants::MFREX {
eprintln!("WARNING: NFREQE={} > MFREX={}, truncating explicit frequencies", nfreqe, crate::tlusty::state::constants::MFREX);
ijfr.truncate(crate::tlusty::state::constants::MFREX);
}
eprintln!("INIFRC grid: {} points, range [{:.4e}, {:.4e}], NFREQE={} explicit",
nfreq, all_freqs[0], all_freqs[nfreq - 1], ijfr.len());
LteFrequencyGrid { freq: all_freqs, weights: all_w, bnue, ijxco: all_ijxco, ijfr }
}
/// 计算 LTE 模式的完整不透明度。
pub fn lte_meanopt(params: &LteOpacityParams, grid: &LteFrequencyGrid) -> LteOpacityOutput {
let t = params.t;
let ne = params.ne;
let nh = params.nh_neutral;
let np = params.np;
let nhm = params.nhm;
let rho = params.rho;
if rho <= 0.0 {
return LteOpacityOutput {
opros: 0.4,
oppla: 0.4,
opes: 0.0,
opbf: 0.0,
opff: 0.0,
ophm: 0.0,
};
}
let hkt = HK / t;
let sqrt_t = t.sqrt();
let sgff = SGFF0 / sqrt_t * ne;
let mut abr = 0.0;
let mut sumdb = 0.0;
let mut abp = 0.0;
let mut sumb = 0.0;
for (ij, &fr) in grid.freq.iter().enumerate() {
let w = grid.weights[ij];
let bnue = grid.bnue[ij];
let x = hkt * fr;
let x_clamped = x.min(150.0);
let ex = x_clamped.exp();
let e1 = 1.0 / (ex - 1.0);
let plan = bnue * e1 * w;
let dplan = plan * hkt * fr * ex * e1 * e1;
let (ab, sct) = compute_opacity_at_frequency(fr, t, ne, nh, np, nhm, hkt, sgff, params);
let total = ab + sct;
if total > 0.0 {
abr += dplan / total;
}
sumdb += dplan;
abp += plan * ab;
sumb += plan;
}
let oprol = if abr > 0.0 { sumdb / abr } else { 0.0 };
let opplal = if sumb > 0.0 { abp / sumb } else { 0.0 };
let opros = oprol / rho;
let oppla = opplal / rho;
let opes = SIGE * ne / rho;
let (opbf, opff, ophm) = compute_mean_opacities_per_gram(params, sqrt_t);
LteOpacityOutput {
opros,
oppla,
opes,
opbf,
opff,
ophm,
}
}
/// 计算给定频率点的吸收和散射系数 (per cm³)。
pub fn compute_opacity_at_frequency(
fr: f64,
t: f64,
ne: f64,
nh: f64,
np: f64,
nhm: f64,
hkt: f64,
sgff: f64,
params: &LteOpacityParams,
) -> (f64, f64) {
let mut ab = 0.0;
let sct = SIGE * ne;
// 1. 氢束缚-自由
if fr >= FRH && nh > 0.0 {
let sigma_bf0 = 6.3e-18;
let sigma_bf = sigma_bf0 * (FRH / fr).powi(3);
let gaunt_bf = hydrogen_gaunt_bf(fr);
ab += sigma_bf * gaunt_bf * nh;
}
// 2. 氢自由-自由
if np > 0.0 && ne > 0.0 {
let frinv = 1.0 / fr;
let fr3inv = frinv * frinv * frinv;
let sf1 = sgff * fr3inv;
let exp_factor = (-hkt * fr).exp();
let sf2 = 1.0 / (1.0 - exp_factor).max(1e-30);
let absoff = sf1 * sf2 * np;
ab += absoff;
}
// 3. H- 自由-自由
if nhm > 0.0 && ne > 0.0 {
let frinv = 1.0 / fr;
let cfft = CFF2 - CFF3 / t;
let abhm_ff = (CFF1 + cfft * frinv) * nhm * ne * frinv;
ab += abhm_ff;
}
// 4. H- 束缚-自由
if nhm > 0.0 && fr >= FRHM {
let sigma_hm = compute_hm_photodetachment_cross_section(fr);
ab += sigma_hm * nhm;
}
// 5. He 束缚-自由
if params.xhe > 0.0 {
let he_abundance = params.xhe / 4.0 * params.nh_total / params.xh.max(0.1);
if fr >= 1.81e15 && he_abundance > 0.0 {
let sigma_he = 7.83e-18 * (1.81e15 / fr).powi(3);
let he_neutral = he_abundance * 0.9;
ab += sigma_he * he_neutral;
}
}
(ab, sct)
}
/// 计算氢束缚-自由 Gaunt 因子。
fn hydrogen_gaunt_bf(fr: f64) -> f64 {
let u = fr / FRH;
if u < 1.0 {
0.0
} else if u < 2.0 {
0.9
} else if u < 10.0 {
0.85
} else {
0.8
}
}
/// 计算 H- 光致分离截面。
fn compute_hm_photodetachment_cross_section(fr: f64) -> f64 {
if fr < FRHM {
return 0.0;
}
let x = fr / FRHM - 1.0;
if x <= 0.0 {
return 0.0;
}
let sqrt_x = x.sqrt();
let sigma_0 = 4.0e-17;
let a = 1.0 + 0.5 * x - 0.1 * x * x;
sigma_0 * sqrt_x * a
}
/// 计算平均不透明度分量 (每克)。
fn compute_mean_opacities_per_gram(params: &LteOpacityParams, _sqrt_t: f64) -> (f64, f64, f64) {
let rho = params.rho;
if rho <= 0.0 {
return (0.0, 0.0, 0.0);
}
let t = params.t;
let t4 = t / 1e4;
let t_factor = t4.powf(-3.5);
let ionization = if params.nh_total > 0.0 {
(params.np / params.nh_total).min(1.0)
} else {
1.0
};
let kappa_bf_h = 4.3e-25 * (1.0 - ionization) * t_factor * params.xh;
let kappa_bf_he = 1.0e-25 * (1.0 - ionization) * t_factor * params.xhe;
let opbf = kappa_bf_h + kappa_bf_he;
let kappa_ff = 1.0e-26 * ionization * (1.0 + ionization) * t_factor;
let opff = kappa_ff;
let ophm = if params.nhm > 0.0 && t < 10000.0 {
let t4_inv = 1e4 / t;
let sigma_hm = 4e-17;
sigma_hm * params.nhm / rho * t4_inv * t4_inv
} else {
0.0
};
(opbf, opff, ophm)
}
/// 快速计算 LTE Rosseland 平均不透明度(解析近似)。
pub fn quick_lte_rosseland(params: &LteOpacityParams) -> f64 {
let rho = params.rho;
if rho <= 0.0 {
return 0.4;
}
let t = params.t;
let ne = params.ne;
let np = params.np;
let nh_neutral = params.nh_neutral;
let kappa_es = SIGE * ne / rho;
let t4 = t / 1e4;
let t_factor = t4.powf(-3.5);
let nh_total = np + nh_neutral;
let ionization = if nh_total > 0.0 {
(np / nh_total).min(1.0)
} else {
1.0
};
let kramer_bf = 4.3e-25 * (1.0 - ionization) * t_factor;
let kramer_ff = 1.0e-26 * ionization * (1.0 + ionization) * t_factor;
let nh_factor = nh_total / rho.max(1e-30);
kappa_es + (kramer_bf + kramer_ff) * nh_factor
}
// ============================================================================
// 测试
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lte_opacity_hot_star() {
let params = LteOpacityParams {
t: 30000.0,
ne: 1e14,
nh_total: 1e14,
np: 9e13,
nh_neutral: 1e13,
nhm: 0.0,
rho: 1e-10,
uh: 2.0,
uhe: 1.0,
uhep: 2.0,
xh: 0.70,
xhe: 0.28,
};
let grid = generate_lte_frequency_grid(35000.0, 100);
let result = lte_meanopt(&params, &grid);
assert!(result.opros > 0.0);
assert!(result.opes / result.opros > 0.3);
}
#[test]
fn test_quick_lte_rosseland() {
let params = LteOpacityParams {
t: 10000.0,
ne: 1e13,
nh_total: 1e15,
np: 5e12,
nh_neutral: 5e14,
nhm: 0.0,
rho: 1e-10,
..Default::default()
};
let kappar = quick_lte_rosseland(&params);
assert!(kappar > 0.0);
}
}