498 lines
14 KiB
Rust
498 lines
14 KiB
Rust
//! 准分子不透明度计算(Lyman alpha, beta, gamma 和 Balmer alpha)。
|
||
//!
|
||
//! 重构自 TLUSTY `allard.f`
|
||
//!
|
||
//! 计算氢线的准分子不透明度轮廓,基于 Allard 等人的数据表。
|
||
|
||
use crate::tlusty::state::model::{CalphatD, CallardA, CallardB, CallardC, CallardG, ModelState};
|
||
|
||
// 归一化常数
|
||
// 8.8528e-29 * lambda_0^2 * f_ij
|
||
const XNORMA: f64 = 8.8528e-29 * 1215.6 * 1215.6 * 0.41618; // Lyman alpha
|
||
const XNORMB: f64 = 8.8528e-29 * 1025.73 * 1025.7 * 0.0791; // Lyman beta
|
||
const XNORMG: f64 = 8.8528e-29 * 972.53 * 972.53 * 0.0290; // Lyman gamma
|
||
const XNORMC: f64 = 8.8528e-29 * 6562.0 * 6562.0 * 0.6407; // Balmer alpha
|
||
|
||
/// 准分子不透明度线轮廓计算。
|
||
///
|
||
/// 计算 Lyman alpha, beta, gamma 和 Balmer alpha 线的准分子轮廓。
|
||
///
|
||
/// # 参数
|
||
///
|
||
/// * `xl` - 波长 (Å)
|
||
/// * `t` - 温度 (K)
|
||
/// * `hneutr` - 中性氢粒子密度 (cm⁻³)
|
||
/// * `hcharg` - 电离氢粒子密度 (cm⁻³)
|
||
/// * `iq` - 下能级量子数
|
||
/// * `jq` - 上能级量子数
|
||
/// - iq=1, jq=2: Lyman alpha
|
||
/// - iq=1, jq=3: Lyman beta
|
||
/// - iq=1, jq=4: Lyman gamma
|
||
/// - iq=2, jq=3: Balmer alpha
|
||
/// * `model` - 模型状态(包含 Allard 数据表)
|
||
///
|
||
/// # 返回值
|
||
///
|
||
/// 返回归一化的线轮廓值。如果波长超出范围或其他条件不满足,返回 0.0。
|
||
pub fn allard(
|
||
xl: f64,
|
||
t: f64,
|
||
hneutr: f64,
|
||
hcharg: f64,
|
||
iq: i32,
|
||
jq: i32,
|
||
model: &ModelState,
|
||
) -> f64 {
|
||
let mut prof = 0.0;
|
||
|
||
// Lyman alpha
|
||
if iq == 1 && jq == 2 {
|
||
if model.quasun.nunalp < 0 {
|
||
// 使用温度相关计算
|
||
return allardt_temp(xl, t, hneutr, hcharg, &model.calphatd);
|
||
} else {
|
||
prof = lyman_alpha(xl, hneutr, hcharg, &model.callarda);
|
||
}
|
||
}
|
||
// Lyman beta
|
||
else if iq == 1 && jq == 3 {
|
||
if model.callardb.nxbet == 0 {
|
||
return 0.0;
|
||
}
|
||
prof = lyman_beta(xl, hneutr, hcharg, &model.callardb);
|
||
}
|
||
// Lyman gamma
|
||
else if iq == 1 && jq == 4 {
|
||
if model.callardg.nxgam == 0 {
|
||
return 0.0;
|
||
}
|
||
prof = lyman_gamma(xl, hneutr, hcharg, &model.callardg);
|
||
}
|
||
// Balmer alpha
|
||
else if iq == 2 && jq == 3 {
|
||
prof = balmer_alpha(xl, hcharg, &model.callardc);
|
||
}
|
||
|
||
prof
|
||
}
|
||
|
||
/// Lyman alpha 线轮廓计算。
|
||
fn lyman_alpha(xl: f64, hneutr: f64, hcharg: f64, data: &CallardA) -> f64 {
|
||
let nxalp = data.nxalp as usize;
|
||
|
||
// 检查波长范围
|
||
if xl < data.xlalp[0] || xl > data.xlalp[nxalp - 1] {
|
||
return 0.0;
|
||
}
|
||
|
||
// 计算归一化密度
|
||
let vn1 = hneutr / data.stnnea;
|
||
let vn2 = hcharg / data.stncha;
|
||
let vns = vn1 * data.vneua + vn2 * data.vchaa;
|
||
|
||
// 检查密度是否过高(只警告一次)
|
||
// 注意:原始 Fortran 代码有警告输出,这里保留标志检查但不输出
|
||
// if data.iwarna == 0 && (vn1 * data.vneua > 0.3 || vn2 * data.vchaa > 0.3) {
|
||
// // 可以在这里添加警告逻辑
|
||
// }
|
||
|
||
let vn11 = vn1 * vn1;
|
||
let vn22 = vn2 * vn2;
|
||
let vn12 = vn1 * vn2;
|
||
let xnorm = 1.0 / (1.0 + vns + 0.5 * vns * vns);
|
||
|
||
// 二分查找波长索引
|
||
let j = binary_search(xl, &data.xlalp, nxalp);
|
||
|
||
// 线性插值
|
||
let a1 = (xl - data.xlalp[j - 1]) / (data.xlalp[j] - data.xlalp[j - 1]);
|
||
|
||
let p1 = vn1 * ((1.0 - a1) * data.plalp[j - 1][0] + a1 * data.plalp[j][0]);
|
||
let p11 = vn11 * ((1.0 - a1) * data.plalp[j - 1][1] + a1 * data.plalp[j][1]);
|
||
let p2 = vn2 * ((1.0 - a1) * data.plalp[j - 1][2] + a1 * data.plalp[j][2]);
|
||
let p22 = vn22 * ((1.0 - a1) * data.plalp[j - 1][3] + a1 * data.plalp[j][3]);
|
||
let p12 = vn12 * ((1.0 - a1) * data.plalp[j - 1][4] + a1 * data.plalp[j][4]);
|
||
|
||
(p1 + p2 + p11 + p22 + p12) * xnorm * XNORMA
|
||
}
|
||
|
||
/// Lyman beta 线轮廓计算。
|
||
fn lyman_beta(xl: f64, hneutr: f64, hcharg: f64, data: &CallardB) -> f64 {
|
||
let nxbet = data.nxbet as usize;
|
||
|
||
// 检查波长范围
|
||
if xl < data.xlbet[0] || xl > data.xlbet[nxbet - 1] {
|
||
return 0.0;
|
||
}
|
||
|
||
// 计算归一化密度
|
||
let vn1 = hneutr / data.stnneb;
|
||
let vn2 = hcharg / data.stnchb;
|
||
let vns = vn1 * data.vneub + vn2 * data.vchab;
|
||
|
||
let vn11 = vn1 * vn1;
|
||
let vn22 = vn2 * vn2;
|
||
let vn12 = vn1 * vn2;
|
||
let xnorm = 1.0 / (1.0 + vns + 0.5 * vns * vns);
|
||
|
||
// 二分查找波长索引
|
||
let j = binary_search(xl, &data.xlbet, nxbet);
|
||
|
||
// 线性插值
|
||
let a1 = (xl - data.xlbet[j - 1]) / (data.xlbet[j] - data.xlbet[j - 1]);
|
||
|
||
let p1 = vn1 * ((1.0 - a1) * data.plbet[j - 1][0] + a1 * data.plbet[j][0]);
|
||
let p11 = vn11 * ((1.0 - a1) * data.plbet[j - 1][1] + a1 * data.plbet[j][1]);
|
||
let p2 = vn2 * ((1.0 - a1) * data.plbet[j - 1][2] + a1 * data.plbet[j][2]);
|
||
let p22 = vn22 * ((1.0 - a1) * data.plbet[j - 1][3] + a1 * data.plbet[j][3]);
|
||
let p12 = vn12 * ((1.0 - a1) * data.plbet[j - 1][4] + a1 * data.plbet[j][4]);
|
||
|
||
(p1 + p2 + p11 + p22 + p12) * xnorm * XNORMB
|
||
}
|
||
|
||
/// Lyman gamma 线轮廓计算。
|
||
fn lyman_gamma(xl: f64, hneutr: f64, hcharg: f64, data: &CallardG) -> f64 {
|
||
let nxgam = data.nxgam as usize;
|
||
|
||
// 检查波长范围
|
||
if xl < data.xlgam[0] || xl > data.xlgam[nxgam - 1] {
|
||
return 0.0;
|
||
}
|
||
|
||
// 计算归一化密度
|
||
let vn1 = hneutr / data.stnneg;
|
||
let vn2 = hcharg / data.stnchg;
|
||
let vns = vn1 * data.vneug + vn2 * data.vchag;
|
||
|
||
let vn11 = vn1 * vn1;
|
||
let vn22 = vn2 * vn2;
|
||
let vn12 = vn1 * vn2;
|
||
let xnorm = 1.0 / (1.0 + vns + 0.5 * vns * vns);
|
||
|
||
// 二分查找波长索引
|
||
let j = binary_search(xl, &data.xlgam, nxgam);
|
||
|
||
// 线性插值
|
||
let a1 = (xl - data.xlgam[j - 1]) / (data.xlgam[j] - data.xlgam[j - 1]);
|
||
|
||
let p1 = vn1 * ((1.0 - a1) * data.plgam[j - 1][0] + a1 * data.plgam[j][0]);
|
||
let p11 = vn11 * ((1.0 - a1) * data.plgam[j - 1][1] + a1 * data.plgam[j][1]);
|
||
let p2 = vn2 * ((1.0 - a1) * data.plgam[j - 1][2] + a1 * data.plgam[j][2]);
|
||
let p22 = vn22 * ((1.0 - a1) * data.plgam[j - 1][3] + a1 * data.plgam[j][3]);
|
||
let p12 = vn12 * ((1.0 - a1) * data.plgam[j - 1][4] + a1 * data.plgam[j][4]);
|
||
|
||
(p1 + p2 + p11 + p22 + p12) * xnorm * XNORMG
|
||
}
|
||
|
||
/// Balmer alpha 线轮廓计算。
|
||
fn balmer_alpha(xl: f64, hcharg: f64, data: &CallardC) -> f64 {
|
||
let nxbal = data.nxbal as usize;
|
||
|
||
// 检查波长范围
|
||
if xl < data.xlbal[0] || xl > data.xlbal[nxbal - 1] {
|
||
return 0.0;
|
||
}
|
||
|
||
// Balmer alpha 只考虑电离氢
|
||
let vn1 = 0.0;
|
||
let vn2 = hcharg / data.stnchc;
|
||
let vns = vn1 * data.vneuc + vn2 * data.vchac;
|
||
|
||
let vn11 = vn1 * vn1;
|
||
let vn22 = vn2 * vn2;
|
||
let vn12 = vn1 * vn2;
|
||
let xnorm = 1.0 / (1.0 + vns + 0.5 * vns * vns);
|
||
|
||
// 二分查找波长索引
|
||
let j = binary_search(xl, &data.xlbal, nxbal);
|
||
|
||
// 线性插值
|
||
let a1 = (xl - data.xlbal[j - 1]) / (data.xlbal[j] - data.xlbal[j - 1]);
|
||
|
||
let p1 = vn1 * ((1.0 - a1) * data.plbal[j - 1][0] + a1 * data.plbal[j][0]);
|
||
let p11 = vn11 * ((1.0 - a1) * data.plbal[j - 1][1] + a1 * data.plbal[j][1]);
|
||
let p2 = vn2 * ((1.0 - a1) * data.plbal[j - 1][2] + a1 * data.plbal[j][2]);
|
||
let p22 = vn22 * ((1.0 - a1) * data.plbal[j - 1][3] + a1 * data.plbal[j][3]);
|
||
let p12 = vn12 * ((1.0 - a1) * data.plbal[j - 1][4] + a1 * data.plbal[j][4]);
|
||
|
||
(p1 + p2 + p11 + p22 + p12) * xnorm * XNORMC
|
||
}
|
||
|
||
/// 温度相关的 Lyman alpha 线轮廓计算。
|
||
///
|
||
/// 当 nunalp < 0 时使用此函数。
|
||
fn allardt_temp(xl: f64, t: f64, hneutr: f64, hcharg: f64, data: &CalphatD) -> f64 {
|
||
let ntalpd = data.ntalpd as usize;
|
||
|
||
// 找到接近实际温度的两个部分表
|
||
let mut it0 = 0usize;
|
||
for it in 1..=ntalpd {
|
||
it0 = it;
|
||
if t < data.talpd[it - 1] {
|
||
it0 = it - 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 处理边界情况
|
||
if it0 == 0 {
|
||
it0 = 1;
|
||
return compute_single_table_temp(xl, hneutr, hcharg, it0 - 1, data);
|
||
}
|
||
if it0 >= ntalpd {
|
||
it0 = ntalpd;
|
||
return compute_single_table_temp(xl, hneutr, hcharg, it0 - 1, data);
|
||
}
|
||
|
||
// 在不同温度的表之间进行插值
|
||
let prof0 = compute_single_table_temp(xl, hneutr, hcharg, it0 - 1, data);
|
||
if prof0 == 0.0 {
|
||
return 0.0;
|
||
}
|
||
|
||
let prof1 = compute_single_table_temp(xl, hneutr, hcharg, it0, data);
|
||
if prof1 == 0.0 {
|
||
return 0.0;
|
||
}
|
||
|
||
// 温度插值
|
||
let dt = data.talpd[it0] - data.talpd[it0 - 1];
|
||
if dt.abs() < 1e-10 {
|
||
prof0
|
||
} else {
|
||
(prof0 * (data.talpd[it0] - t) + prof1 * (t - data.talpd[it0 - 1])) / dt
|
||
}
|
||
}
|
||
|
||
/// 计算单个温度表的轮廓值(温度相关版本)。
|
||
fn compute_single_table_temp(
|
||
xl: f64,
|
||
hneutr: f64,
|
||
hcharg: f64,
|
||
it_idx: usize,
|
||
data: &CalphatD,
|
||
) -> f64 {
|
||
let nx = data.nxalpd[it_idx] as usize;
|
||
if nx == 0 {
|
||
return 0.0;
|
||
}
|
||
|
||
// 检查波长是否在范围内
|
||
if xl < data.xlalpd[0][it_idx] || xl > data.xlalpd[nx - 1][it_idx] {
|
||
return 0.0;
|
||
}
|
||
|
||
// 计算归一化密度
|
||
let vn1 = hneutr / data.stnead[it_idx];
|
||
let vn2 = hcharg / data.stnchd[it_idx];
|
||
let vns = vn1 * data.vneuad[it_idx] + vn2 * data.vchaad[it_idx];
|
||
let vn11 = vn1 * vn1;
|
||
let vn22 = vn2 * vn2;
|
||
let vn12 = vn1 * vn2;
|
||
let xnorm = 1.0 / (1.0 + vns + 0.5 * vns * vns);
|
||
|
||
// 二分查找波长索引
|
||
let mut jl = 0usize;
|
||
let mut ju = nx + 1;
|
||
while ju - jl > 1 {
|
||
let jm = (ju + jl) / 2;
|
||
if xl > data.xlalpd[jm - 1][it_idx] {
|
||
jl = jm;
|
||
} else {
|
||
ju = jm;
|
||
}
|
||
}
|
||
let j = if jl == 0 { 1 } else if jl == nx { nx - 1 } else { jl };
|
||
|
||
// 线性插值
|
||
let a1 = (xl - data.xlalpd[j - 1][it_idx])
|
||
/ (data.xlalpd[j][it_idx] - data.xlalpd[j - 1][it_idx]);
|
||
|
||
let p1 = vn1 * ((1.0 - a1) * data.plalpd[j - 1][0][it_idx] + a1 * data.plalpd[j][0][it_idx]);
|
||
let p11 = vn11 * ((1.0 - a1) * data.plalpd[j - 1][1][it_idx] + a1 * data.plalpd[j][1][it_idx]);
|
||
let p2 = vn2 * ((1.0 - a1) * data.plalpd[j - 1][2][it_idx] + a1 * data.plalpd[j][2][it_idx]);
|
||
let p22 = vn22 * ((1.0 - a1) * data.plalpd[j - 1][3][it_idx] + a1 * data.plalpd[j][3][it_idx]);
|
||
let p12 = vn12 * ((1.0 - a1) * data.plalpd[j - 1][4][it_idx] + a1 * data.plalpd[j][4][it_idx]);
|
||
|
||
(p1 + p2 + p11 + p22 + p12) * xnorm * XNORMA
|
||
}
|
||
|
||
/// 二分查找波长索引。
|
||
///
|
||
/// 返回索引 j,使得 x[j-1] <= xl < x[j]。
|
||
/// Fortran 原始代码使用的是 1-indexed。
|
||
fn binary_search(xl: f64, x: &[f64], n: usize) -> usize {
|
||
let mut jl = 0usize;
|
||
let mut ju = n + 1;
|
||
|
||
// 判断数组是升序还是降序
|
||
let ascending = x[n - 1] > x[0];
|
||
|
||
while ju - jl > 1 {
|
||
let jm = (ju + jl) / 2;
|
||
if ascending == (xl > x[jm - 1]) {
|
||
jl = jm;
|
||
} else {
|
||
ju = jm;
|
||
}
|
||
}
|
||
|
||
// 边界检查
|
||
if jl == 0 {
|
||
1
|
||
} else if jl == n {
|
||
n - 1
|
||
} else {
|
||
jl
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use approx::assert_relative_eq;
|
||
|
||
fn create_test_callard_a() -> CallardA {
|
||
let mut data = CallardA::default();
|
||
data.nxalp = 3;
|
||
data.xlalp[0] = 1210.0;
|
||
data.xlalp[1] = 1215.0;
|
||
data.xlalp[2] = 1220.0;
|
||
data.stnnea = 1e14;
|
||
data.stncha = 1e12;
|
||
data.vneua = 1.0;
|
||
data.vchaa = 1.0;
|
||
data.iwarna = 1; // 禁用警告
|
||
|
||
for i in 0..3 {
|
||
for j in 0..5 {
|
||
data.plalp[i][j] = 1e-20;
|
||
}
|
||
}
|
||
data
|
||
}
|
||
|
||
fn create_test_callard_b() -> CallardB {
|
||
let mut data = CallardB::default();
|
||
data.nxbet = 3;
|
||
data.xlbet[0] = 1020.0;
|
||
data.xlbet[1] = 1025.0;
|
||
data.xlbet[2] = 1030.0;
|
||
data.stnneb = 1e14;
|
||
data.stnchb = 1e12;
|
||
data.vneub = 1.0;
|
||
data.vchab = 1.0;
|
||
data.iwarnb = 1;
|
||
|
||
for i in 0..3 {
|
||
for j in 0..5 {
|
||
data.plbet[i][j] = 1e-20;
|
||
}
|
||
}
|
||
data
|
||
}
|
||
|
||
fn create_test_callard_g() -> CallardG {
|
||
let mut data = CallardG::default();
|
||
data.nxgam = 3;
|
||
data.xlgam[0] = 970.0;
|
||
data.xlgam[1] = 972.0;
|
||
data.xlgam[2] = 975.0;
|
||
data.stnneg = 1e14;
|
||
data.stnchg = 1e12;
|
||
data.vneug = 1.0;
|
||
data.vchag = 1.0;
|
||
data.iwarng = 1;
|
||
|
||
for i in 0..3 {
|
||
for j in 0..5 {
|
||
data.plgam[i][j] = 1e-20;
|
||
}
|
||
}
|
||
data
|
||
}
|
||
|
||
fn create_test_callard_c() -> CallardC {
|
||
let mut data = CallardC::default();
|
||
data.nxbal = 3;
|
||
data.xlbal[0] = 6550.0;
|
||
data.xlbal[1] = 6562.0;
|
||
data.xlbal[2] = 6580.0;
|
||
data.stnchc = 1e12;
|
||
data.vneuc = 1.0;
|
||
data.vchac = 1.0;
|
||
|
||
for i in 0..3 {
|
||
for j in 0..5 {
|
||
data.plbal[i][j] = 1e-20;
|
||
}
|
||
}
|
||
data
|
||
}
|
||
|
||
fn create_test_model() -> ModelState {
|
||
let mut model = ModelState::default();
|
||
model.callarda = create_test_callard_a();
|
||
model.callardb = create_test_callard_b();
|
||
model.callardg = create_test_callard_g();
|
||
model.callardc = create_test_callard_c();
|
||
model.quasun.nunalp = 0; // 使用非温度相关计算
|
||
model
|
||
}
|
||
|
||
#[test]
|
||
fn test_binary_search() {
|
||
let x = vec![1210.0, 1215.0, 1220.0];
|
||
|
||
// 升序数组
|
||
let j = binary_search(1212.0, &x, 3);
|
||
assert_eq!(j, 1);
|
||
|
||
let j = binary_search(1217.0, &x, 3);
|
||
assert_eq!(j, 2);
|
||
}
|
||
|
||
#[test]
|
||
fn test_allard_lyman_alpha_in_range() {
|
||
let model = create_test_model();
|
||
let prof = allard(1215.0, 10000.0, 1e14, 1e12, 1, 2, &model);
|
||
assert!(prof > 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_allard_lyman_alpha_out_of_range() {
|
||
let model = create_test_model();
|
||
let prof = allard(1000.0, 10000.0, 1e14, 1e12, 1, 2, &model);
|
||
assert_relative_eq!(prof, 0.0, epsilon = 1e-30);
|
||
}
|
||
|
||
#[test]
|
||
fn test_allard_lyman_beta_in_range() {
|
||
let model = create_test_model();
|
||
let prof = allard(1025.0, 10000.0, 1e14, 1e12, 1, 3, &model);
|
||
assert!(prof > 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_allard_lyman_gamma_in_range() {
|
||
let model = create_test_model();
|
||
let prof = allard(972.0, 10000.0, 1e14, 1e12, 1, 4, &model);
|
||
assert!(prof > 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_allard_balmer_alpha_in_range() {
|
||
let model = create_test_model();
|
||
let prof = allard(6562.0, 10000.0, 1e14, 1e12, 2, 3, &model);
|
||
assert!(prof >= 0.0); // Balmer alpha 只考虑电离氢
|
||
}
|
||
|
||
#[test]
|
||
fn test_allard_invalid_transition() {
|
||
let model = create_test_model();
|
||
// 无效的量子数组合
|
||
let prof = allard(6562.0, 10000.0, 1e14, 1e12, 3, 4, &model);
|
||
assert_relative_eq!(prof, 0.0, epsilon = 1e-30);
|
||
}
|
||
}
|