//! 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, /// 权重数组 pub weights: Vec, /// Planck 函数 pub bnue: Vec, /// Edge type markers: 1=edge frequency, 2=interior frequency /// (from Fortran IJXCO array in INIFRC) pub ijxco: Vec, /// 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, } // ============================================================================ // 核心计算函数 // ============================================================================ /// 生成用于 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 = 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 = vec![0.0; cap]; let mut wco: Vec = vec![0.0; cap]; let mut ijxco: Vec = vec![0; cap]; // Helper: ensure arrays have at least `min_cap` elements let ensure_cap = |freqco: &mut Vec, wco: &mut Vec, ijxco: &mut Vec, 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 = Vec::with_capacity(nfreqc); let mut all_w: Vec = Vec::with_capacity(nfreqc); let mut all_ijxco: Vec = 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 = (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(¶ms, &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(¶ms); assert!(kappar > 0.0); } }