SpectraRust/src/tlusty/math/continuum/lte_opacity.rs

804 lines
26 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);
// BNUE = (2H/c²)·ν³ — prefactor only (matches Fortran BNUE=BN*ν³, tlusty208.f:30748).
// The (exp(hν/kT)-1) divisor is applied by consumers at the LOCAL T
// (plan = bnue * e1 * w, e1=1/(exp(hν/kT_local)-1)). Baking the TEFF divisor in
// here left a frequency-dependent spurious factor that biased the Rosseland mean.
bnue.push(c1 * fr.powi(3));
}
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 prefactor BNUE = (2H/c²)·ν³.
//
// IMPORTANT: matches Fortran BNUE (tlusty208.f:30748 `BNUE(IJ)=BN*FR15³`, BN=2H/c²):
// BNUE is the PREFACTOR ONLY — the (exp(hν/kT)-1) divisor is applied separately
// at the LOCAL temperature by every consumer (MEANOP, meanopt, lte_meanopt,
// ltegr call_meanopt all compute `plan = bnue * e1 * w` with e1=1/(exp(hν/kT_local)-1)).
//
// Earlier code divided by (exp(hν/kT_eff)-1) here, leaving a frequency-dependent
// spurious factor that biased the Rosseland mean weight toward low-ν and inflated
// κ_R at depth (grey-start DM grid too small). The divisor must NOT be baked in.
let c1 = 2.0 * H / (CLIGHT * CLIGHT);
let mut bnue = Vec::with_capacity(nfreq);
for &fr in &all_freqs {
bnue.push(c1 * fr.powi(3));
}
// 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);
// ∂B_ν/∂T·w = plan·u·ex·e1 (u=hkt·fr); the constant T it implicitly omits
// cancels in the Rosseland ratio. Single e1 — matches faithful meanopt.rs
// and Fortran tlusty208.f:22433 (DPLAN=PLAN*X/T/(UN-UN/EX)). A previous
// `*e1*e1` here was the "κ_R 偏高 ~2-3x" compensation error noted at
// main.rs:1573 — the extra frequency-dependent e1 re-weights the mean.
let plan = bnue * e1 * w;
let dplan = plan * hkt * fr * ex * 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. 氢自由-自由
// 注意: 受激发射因子当前为 1/(1-e^-hν/kT). 诊断显示此式在高温深层使 Rosseland
// 平均 κ_R 发散 (gold-ref 条件下 d69 κ_R≈140, 物理上应 ~1). 但简单改为乘以
// (1-e^-hν/kT) 虽修正孤立 κ 值, 却因 κ-ρ-Ne-Saha 耦合使灰大气深度网格更差
// (DM[69] 163→126, gold=298), 且 Fortran H- 自由-自由亦用除法 (tlusty208.f:11147).
// 忠实修复需移植 Fortran SFF2/SFF3 表格化系数, 非简单因子翻转. 暂保留原式.
// [2026-06-18 复核] 同时翻转 sf2 与改 bnue 为本地 T 仍非稳健: 灰大气 DM[69]
// 163→216 (改善) 但 gold-ref 深层诊断 κ_R 140→500 (恶化), 单元测试 es_frac 失败.
// ⇒ 简化 lte_meanopt 有相互补偿的误差, 局部物理修正会交换误差而非收敛.
// 真正修复 = 移植 TLUSTY 完整 ROSSOP/COMOP 不透明度机制 (含 SFF2=EXP(FF·HK/T)
// 约定, exact Gaunt, WOP 占据概率, H- SFFHMI 表). 暂保留原式.
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::*;
/// Diagnostic: compute LTE Rosseland opacity breakdown for gold-ref (T,Ne,ρ)
/// profile points, compare to effective κ = tau/DM from the converged grid.
/// Purpose: find which opacity source is overestimated at depth (grey-start
/// grid compressed because κ_rust ≈ 3× too high at depth vs gold κ≈1.06).
#[test]
fn diag_goldref_opacity_breakdown() {
// (label, T, Ne, rho, eff_kappa=tau/DM) from tests/tlusty/hhe_fortran/fort.7.ref
let pts: &[(&str, f64, f64, f64, f64)] = &[
("d0 surf", 26306.2, 3.764e8, 7.304e-16, 0.343),
("d20 ", 27030.2, 2.258e11, 4.455e-13, 0.343),
("d34 ", 27800.0, 5.0e12, 1.0e-11, 0.396),
("d49 ", 40000.0, 1.0e14, 2.0e-10, 0.951),
("d60 ", 90000.0, 2.0e15, 4.0e-9, 1.145),
("d69 deep ", 137872.1, 5.679e16, 1.102e-7, 1.061),
];
let wmm = 1.3 * 1.67e-24; // mean molecular weight
let grid = generate_lte_frequency_grid(35000.0, 200);
eprintln!("\n{:>9} {:>10} {:>10} {:>10} {:>10} {:>10} | {:>8}",
"label","kappa_R","opes","opff","opbf","ophm","eff_k");
for (label, t, ne, rho, eff_k) in pts {
// H fully ionized at these T: np ~ n_H, neutral H tiny
let n_heavy = rho / wmm;
let nh_total = 0.70 * n_heavy * 2.0; // X=0.70 fraction, nucleon count
let np = nh_total * 0.999;
let nh_neutral = nh_total * 0.001;
let params = LteOpacityParams {
t: *t, ne: *ne, nh_total, np, nh_neutral, nhm: 0.0, rho: *rho,
uh: 2.0, uhe: 1.0, uhep: 2.0, xh: 0.70, xhe: 0.28,
};
let r = lte_meanopt(&params, &grid);
eprintln!("{:>9} {:10.3} {:10.3} {:10.3} {:10.3} {:10.3} | {:8.3}",
label, r.opros, r.opes, r.opff, r.opbf, r.ophm, eff_k);
}
}
#[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);
}
}