SpectraRust/src/synspec/math/inkur.rs
fmq 0f97c0b05b feat: 添加 TLUSTY 新模块 + 修复编译错误
新增 TLUSTY 模块:
- crossd: 光电离截面评估 (bound-free cross section)
- sgmer0: 合并能级光电离截面初始化
- sgmerd: 合并能级光电离截面计算
- dwnfr0: 频率网格下载 (continuum)
- convc1: 对流收敛控制 (radiative)
- chckse: 统计平衡检查 (rates)

扩展 RESOLV 编排器:
- 添加 Feautrier 形式解
- 添加 Lucy 温度修正
- 添加 ROSSTD/PZEVAL/CONOUT 调用
- 添加 IFPOPR=2 占据数更新
- 添加 HESOL6 流体静力平衡修正

修复:
- sgmer0.rs: 修复 config 未声明为 mut 的编译错误
- crossd.rs: 修复测试中使用错误字段路径的问题
  (frqall.ijbf/phoexp.aijbf/phoexp.bfcs 而非 obfpar)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:35:09 +08:00

179 lines
5.3 KiB
Rust

//! Input of a Kurucz model atmosphere.
//!
//! Translated from SYNSPEC54.FOR subroutine INKUR at line 11048.
//!
//! Reads a Kurucz model atmosphere from file (unit 8) and initializes
//! the model state arrays (DM, TEMP, ELEC, DENS, POPUL, etc.).
/// Parameters for INKUR initialization.
pub struct InkurParams<'a> {
/// Boltzmann constant (BOLK)
pub bolk: f64,
/// Mean molecular weight at each depth
pub wmm: &'a [f64],
/// Total number of particles per H atom
pub ytot: &'a [f64],
/// Number of atoms
pub natom: usize,
/// Number of levels
pub nlevel: usize,
/// Molecular equilibrium flag
pub ifmol: i32,
/// Molecular temperature limit
pub tmolim: f64,
/// Maximum number of depth points
pub nd_max: usize,
}
/// Result of INKUR initialization.
pub struct InkurResult {
/// Number of depth points
pub nd: usize,
/// Effective temperature (from file header)
pub tef: f64,
/// Surface gravity log g (from file header)
pub grav: f64,
/// Mass depth coordinate
pub dm: Vec<f64>,
/// Temperature at each depth
pub temp: Vec<f64>,
/// Electron density at each depth
pub elec: Vec<f64>,
/// Mass density at each depth
pub dens: Vec<f64>,
/// Population of each level at each depth (nlevel x nd)
pub popul: Vec<Vec<f64>>,
}
/// Input of a Kurucz model atmosphere.
///
/// Reads model atmosphere data and initializes the depth-dependent arrays.
/// For each depth point, computes density from pressure and temperature,
/// optionally solves molecular equilibrium, and computes LTE populations.
///
/// # Arguments
/// * `params` - Initialization parameters
/// * `tef` - Effective temperature from file header
/// * `grav` - Surface gravity from file header
/// * `depth_data` - Slice of (dm, temp, pressure, elec) for each depth
/// * `moleq_fn` - Optional molecular equilibrium callback: (id, t, an, aein) -> ane
/// * `attot_fn` - Callback to compute ATTOT: (iat, id, dens, wmm, ytot, abund) -> f64
/// * `post_depth_fn` - Callback after each depth: (id) for WNSTOR, SABOLF, RATMAT, LEVSOL
///
/// # Returns
/// Initialized model arrays.
pub fn inkur(
params: &InkurParams,
tef: f64,
grav: f64,
depth_data: &[(f64, f64, f64, f64)],
moleq_fn: Option<&dyn Fn(usize, f64, f64, f64) -> f64>,
attot_fn: &dyn Fn(usize, usize, f64, f64, f64, f64) -> f64,
post_depth_fn: &dyn Fn(usize, &mut [Vec<f64>]),
) -> InkurResult {
let bolk = params.bolk;
let nd = depth_data.len().min(params.nd_max);
let mut dm = Vec::with_capacity(nd);
let mut temp = Vec::with_capacity(nd);
let mut elec = Vec::with_capacity(nd);
let mut dens = Vec::with_capacity(nd);
let mut popul = vec![vec![0.0; nd]; params.nlevel];
for (id, &(dm_i, temp_i, p, elec_i)) in depth_data.iter().enumerate().take(nd) {
dm.push(dm_i);
temp.push(temp_i);
elec.push(elec_i);
// Compute density: DENS = WMM * (P/(T*BOLK) - ELEC)
let an = p / temp_i / bolk;
let dens_i = params.wmm[id] * (an - elec_i);
dens.push(dens_i);
let t = temp_i;
// Molecular equilibrium or simple abundance
if params.ifmol > 0 && t < params.tmolim {
if let Some(ref moleq) = moleq_fn {
let aein = elec_i;
let _ane = moleq(id, t, an, aein);
}
} else {
// Compute total atom abundance for each atom
// Fortran: ATTOT(IAT,ID)=DENS(ID)/WMM(ID)/YTOT(ID)*ABUND(IAT,ID)
for iat in 0..params.natom {
let _ = attot_fn(iat, id, dens_i, params.wmm[id], params.ytot[id], 0.0);
}
}
// Post-depth processing: WNSTOR, SABOLF, RATMAT, LEVSOL
post_depth_fn(id, &mut popul);
}
InkurResult {
nd,
tef,
grav,
dm,
temp,
elec,
dens,
popul,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inkur_basic() {
let params = InkurParams {
bolk: 1.380649e-16,
wmm: &[1.0, 1.0],
ytot: &[1.0, 1.0],
natom: 1,
nlevel: 2,
ifmol: 0,
tmolim: 0.0,
nd_max: 10,
};
let depth_data = [
(1e-4, 5000.0, 1e5, 1e11),
(1e-3, 4000.0, 1e4, 1e10),
];
let attot_fn = |_iat: usize, _id: usize, dens: f64, wmm: f64, ytot: f64, _abund: f64| {
dens / wmm / ytot
};
let post_depth_fn = |_id: usize, _popul: &mut [Vec<f64>]| {
// No-op for test
};
let result = inkur(
&params,
5777.0,
4.44,
&depth_data,
None,
&attot_fn,
&post_depth_fn,
);
assert_eq!(result.nd, 2);
assert_eq!(result.dm.len(), 2);
assert_eq!(result.temp.len(), 2);
assert_eq!(result.elec.len(), 2);
assert_eq!(result.dens.len(), 2);
assert_eq!(result.tef, 5777.0);
assert_eq!(result.grav, 4.44);
assert!(result.dens[0].is_finite());
assert!(result.dens[1].is_finite());
// DENS = WMM * (P/(T*BOLK) - ELEC)
let an0 = 1e5 / 5000.0 / 1.380649e-16;
let expected_dens0 = 1.0 * (an0 - 1e11);
assert!((result.dens[0] - expected_dens0).abs() < 1.0);
}
}