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>
747 lines
22 KiB
Rust
747 lines
22 KiB
Rust
//! 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(¶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);
|
||
}
|
||
}
|