SpectraRust/src/tlusty/math/odf/odfhys.rs
2026-04-04 09:36:14 +08:00

682 lines
23 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.

//! 氢线 ODF 初始化。
//!
//! 重构自 TLUSTY `odfhys.f`
//! 设置氢线的频率网格、权重和 Stark 展宽参数。
//!
//! 注意:此模块是 ODF 处理的核心模块,涉及频率网格设置和 Stark 展宽参数计算。
use crate::tlusty::math::odf::odffr::{self, OdffrAtomicData, OdffrModelData, OdffrOutputState, OdffrParams};
use crate::tlusty::math::ali::IjalisParams;
use crate::tlusty::math::stark0;
use crate::tlusty::state::atomic::{AtoPar, IonPar, LevPar, TraAli, TraCor, TraPar};
use crate::tlusty::state::config::BasNum;
use crate::tlusty::state::constants::{NLMX, MFRO};
use crate::tlusty::state::model::{CompIf, FreAux};
use crate::tlusty::state::odfpar::{OdfCtr, OdfFrq, OdfMod, OdfStk};
// f2r_depends: IJALIS, ODFFR, STARK0
/// Hydrogen line ODF initialization wrapper.
///
/// 根据 ISPODF 选择简化模式或完整模式。
pub fn odfhys(dopo: f64, params: &mut OdfhysParams, freq: &mut [f64], weight: &mut [f64]) {
if params.basnum.ispodf >= 1 {
odfhys_simplified(params);
} else {
odfhys_full(dopo, params, freq, weight);
}
}
/// ODFHYS 参数结构体
pub struct OdfhysParams<'a> {
/// 基本数值
pub basnum: &'a mut BasNum,
/// 离子参数(包含 iz
pub ionpar: &'a IonPar,
/// 能级参数
pub levpar: &'a LevPar,
/// 跃迁参数
pub trapar: &'a mut TraPar,
/// ODF 控制(包含 JNDODF
pub odfctr: &'a mut OdfCtr,
/// ODF 频率数据
pub odffrq: &'a mut OdfFrq,
/// ODF 模型数据
pub odfmod: &'a mut OdfMod,
/// ODF Stark 数据
pub odfstk: &'a mut OdfStk,
/// 计算标志(包含 LINEXP
pub compif: &'a mut CompIf,
/// XI2 数组0-based: xi2[n-1] = 1/n²
pub xi2: &'a [f64],
// --- ODFFR/IJALIS 额外参数 ---
/// 有效温度
pub teff: f64,
/// 原子参数IJALIS 需要)
pub atopar: &'a AtoPar,
/// ALI 跃迁标志IJALIS 需要)
pub traali: &'a TraAli,
/// 频率辅助数据IJALIS 需要)
pub freaux: &'a mut FreAux,
/// 跃迁修正标志IJALIS 需要)
pub tracor: &'a mut TraCor,
}
// 常量
const CCM: f64 = 1.0 / 2.997925e10;
const THIRD: f64 = 1.0 / 3.0;
const FRH: f64 = 3.28805e15;
const HALF: f64 = 0.5;
/// 初始化氢线 ODF简化模式ISPODF >= 1
///
/// 设置氢线的 Stark 展宽参数和振子强度。
/// 对应 Fortran 中 ISPODF >= 1 的分支(直接 RETURN
pub fn odfhys_simplified(params: &mut OdfhysParams) {
let ntrans = params.basnum.ntrans as usize;
let izzh: usize = 1; // 氢的原子序数
for itr in 0..ntrans {
// Fix: 使用 JNDODF 而非 IJTF且检查 <= 0包括负数
let jnd_raw = params.odfctr.jndodf[itr];
if jnd_raw <= 0 {
continue;
}
let jnd = jnd_raw as usize;
let mode = params.trapar.indexp[itr].abs();
if mode != 2 {
continue;
}
// Fix: 设置 LINEXP = falseFortran: LINEXP(ITR)=.FALSE.
params.compif.linexp[itr] = false;
params.trapar.lcomp[itr] = 0;
params.trapar.intmod[itr] = 6;
// I=ILOW(ITR), J=IUP(ITR) — Fortran 1-indexed → Rust 0-indexed
let i = (params.trapar.ilow[itr] - 1) as usize;
let j = (params.trapar.iup[itr] - 1) as usize;
// 设置量子数
params.odfmod.nqlodf[i] = params.trapar.iprof[itr].abs();
if params.odfmod.nqlodf[i] == 0 {
params.odfmod.nqlodf[i] = params.levpar.nquant[j];
}
// 计算振子强度
params.trapar.osc0[itr] = 0.0;
let is_quant = params.levpar.nquant[i] as usize;
let j_quant = params.levpar.nquant[j] as usize;
// Fix: JNDODF 是 1-based 索引,转为 0-based
let jnd_idx = jnd - 1;
if jnd_idx >= params.odfstk.xkij.len() {
continue;
}
for k in j_quant..=NLMX {
if k < params.odfstk.xkij[jnd_idx].len() {
let (xkij_val, wl0_val, fij_val) = stark0(is_quant, k, izzh);
params.odfstk.xkij[jnd_idx][k] = xkij_val;
params.odfstk.wl0[jnd_idx][k] = wl0_val;
params.odfstk.fij[jnd_idx][k] = fij_val;
params.trapar.osc0[itr] += fij_val;
}
}
}
}
/// 初始化氢线 ODF完整模式
///
/// 设置氢线的频率网格、权重和 Stark 展宽参数。
/// 对应 Fortran 中 ISPODF < 1 的完整分支。
///
/// # 参数
/// * `dopo` - 多普勒宽度参数
/// * `params` - 参数结构体
/// * `freq` - 频率数组(输出)
/// * `weight` - 权重数组(输出)
pub fn odfhys_full(
dopo: f64,
params: &mut OdfhysParams,
freq: &mut [f64],
weight: &mut [f64],
) {
let ntrans = params.basnum.ntrans as usize;
let izzh: usize = 1;
let mut nlaste = params.basnum.nfreq as usize;
let mut ffro = vec![0.0_f64; MFRO];
for itr in 0..ntrans {
// Fix: 使用 JNDODF 而非 IJTF且检查 <= 0
let jnd_raw = params.odfctr.jndodf[itr];
if jnd_raw <= 0 {
continue;
}
let jnd = jnd_raw as usize;
let mode = params.trapar.indexp[itr].abs();
if mode != 2 {
continue;
}
params.trapar.lcomp[itr] = 0;
params.trapar.intmod[itr] = 6;
// I=ILOW(ITR), J=IUP(ITR) — Fortran 1-indexed → Rust 0-indexed
let i = (params.trapar.ilow[itr] - 1) as usize;
let j = (params.trapar.iup[itr] - 1) as usize;
if i >= params.levpar.nquant.len() || j >= params.levpar.nquant.len() {
continue;
}
// 设置量子数
params.odfmod.nqlodf[i] = params.trapar.iprof[itr].abs();
if params.odfmod.nqlodf[i] == 0 {
params.odfmod.nqlodf[i] = params.levpar.nquant[j];
}
// Fix: XI2 是 0-based 数组xi2[n-1] 对应 Fortran XI2(n)
// Fortran: XJ2A=HALF*(XI2(NQUANT(J))+XI2(NQUANT(J)-1))
let nquant_j = params.levpar.nquant[j] as usize;
if nquant_j == 0 || nquant_j > params.xi2.len() {
continue;
}
let xj2a = HALF * (params.xi2[nquant_j - 1] + params.xi2[nquant_j - 2]);
// Fix: JNDODF 是 1-based转为 0-based 访问 ODF 数组
let jnd_idx = jnd - 1;
if jnd_idx >= params.odffrq.kdo.len() {
continue;
}
// kdo[jnd_idx][ifq] 对应 Fortran KDO(ifq+1, jnd)
let mut nfro: usize = 0;
for ifq in 0..4 {
nfro += params.odffrq.kdo[jnd_idx][ifq] as usize;
}
nfro = nfro.saturating_sub(2);
// 计算频率参数
let iel_idx = (params.levpar.iel[i].saturating_sub(1)) as usize;
if iel_idx >= params.ionpar.iz.len() {
continue;
}
let nquant_i = params.levpar.nquant[i] as usize;
if nquant_i == 0 || nquant_i > params.xi2.len() {
continue;
}
let frion = FRH * (params.ionpar.iz[iel_idx] as f64).powi(2);
let fra = frion * (params.xi2[nquant_i - 1] - xj2a);
let dopi = dopo * fra * CCM;
let frb = 0.99999999 * frion * params.xi2[nquant_i - 1];
let _ifrq0 = params.trapar.ifr0[itr];
let _ifrq1 = params.trapar.ifr1[itr];
params.trapar.ifr0[itr] = (nlaste + 1) as i32;
params.trapar.ifr1[itr] = (nlaste + nfro) as i32;
params.odfmod.i1odf[i] = params.trapar.ifr0[itr];
params.odfmod.i2odf[i] = (params.trapar.ifr1[itr] - 1) as i32;
// Fix: FFRO 使用 0-based 索引
// Fortran: FFRO(1)=..., FFRO(2)=..., IJ00=1
// Rust: ffro[0]=..., ffro[1]=..., ij00=0 (0-based)
ffro[0] = 0.99999999 * fra;
ffro[1] = fra;
let mut ij00: usize = 0; // Fortran IJ00=1 → Rust 0-based = 0
for ik in 0..3 {
let kdo_val = params.odffrq.kdo[jnd_idx][ik] as usize;
// Fortran: DO IJ=2,KDO(IK,JND) → Rust: DO ij=2..=kdo_val
// ijq = ij00 + ij但因为 ij00 已经是 0-based所以直接用
for ij in 2..=kdo_val {
let ijq = ij00 + ij;
if ijq < MFRO {
ffro[ijq] = ffro[ijq - 1] + params.odffrq.xdo[jnd_idx][ik] * dopi;
}
}
ij00 += kdo_val.saturating_sub(1);
}
// 查找 FRB 位置
// Fix: 0-based 搜索范围 0..=ij00
let mut nfrb: usize = ij00;
for ij in 0..=ij00 {
if ffro[ij] < frb {
nfrb = ij;
}
}
if nfrb == ij00 && nfro > 0 && nfro < MFRO {
// 扩展频率数组
// Fortran: IJ00=IJ00+1, FFRO(NFRO)=...
// Rust 0-based: ij00+=1, ffro[nfro-1] 对应 Fortran FFRO(NFRO)
ij00 += 1;
ffro[nfro - 1] = 0.99999999 * frion * params.xi2[nquant_i - 1];
while ij00 < MFRO && ffro[ij00] >= ffro[nfro - 1] {
// Fix: xdo[jnd_idx][2] 对应 Fortran XDO(3,JND),不是 xdo[2][jnd_idx]
params.odffrq.xdo[jnd_idx][2] *= 0.75;
let kdo3 = params.odffrq.kdo[jnd_idx][2] as usize;
ij00 = ij00.saturating_sub(kdo3);
for ij in 2..=kdo3 {
let ijq = ij00 + ij;
if ijq < MFRO {
ffro[ijq] = ffro[ijq - 1] + params.odffrq.xdo[jnd_idx][2] * dopi;
}
}
ij00 += kdo3;
}
// Fix: kdo[jnd_idx][3] 对应 Fortran KDO(4,JND)
let kdo4 = params.odffrq.kdo[jnd_idx][3];
if kdo4 > 1 {
let tido = (ffro[nfro - 1] - ffro[ij00]) / (kdo4 - 1) as f64;
for ij in 1..=((kdo4 - 2) as usize) {
// Fortran: IJQ=NFRO-IJ, FFRO(IJQ)=FFRO(NFRO)-FLOAT(IJ)*TIDO
// Rust: ijq = nfro-1-ij (0-based)
let ijq = nfro.saturating_sub(1 + ij);
if ijq < MFRO {
ffro[ijq] = ffro[nfro - 1] - ij as f64 * tido;
}
}
}
} else if nfrb + 3 < MFRO {
// nfrb + 1/2/3 的相对偏移在 0-based 下不变
let tido = (frb - ffro[nfrb]) * THIRD;
ffro[nfrb + 1] = ffro[nfrb] + tido;
ffro[nfrb + 2] = frb - tido;
ffro[nfrb + 3] = frb;
nfro = nfrb + 3;
params.trapar.ifr1[itr] = (nlaste + nfro) as i32;
params.odfmod.i2odf[i] = (params.trapar.ifr1[itr] - 1) as i32;
}
// 存储频率(反转)
// Fortran: DO IJ=1,NFRO → FREQ(NLASTE+IJ)=FFRO(NFRO-IJ+1)
// Rust 0-based: freq[nlaste+ij-1] = ffro[nfro-ij]
for ij in 1..=nfro {
let dest_idx = nlaste + ij - 1;
let src_idx = nfro - ij;
if dest_idx < freq.len() && src_idx < MFRO {
freq[dest_idx] = ffro[src_idx];
}
}
// 计算权重
// Fortran: W(NLASTE+NFRO) = HALF*(FREQ(NLASTE+NFRO-1)-FREQ(NLASTE+NFRO))
// Rust 0-based: weight[nlaste+nfro-1] = 0.5*(freq[nlaste+nfro-2]-freq[nlaste+nfro-1])
if nfro >= 2 {
let w_idx = nlaste + nfro - 1;
if w_idx < weight.len() && w_idx > 0 {
weight[w_idx] = HALF * (freq[w_idx - 1] - freq[w_idx]);
weight[w_idx - 1] = weight[w_idx];
}
// Fortran: DO IJ=2,NFRO-2,2
for ij in (2..=(nfro - 2)).step_by(2) {
// FREQ(NLASTE+IJ) → freq[nlaste+ij-1]
// FREQ(NLASTE+IJ+1) → freq[nlaste+ij]
// W(NLASTE+IJ-1) → weight[nlaste+ij-2]
// W(NLASTE+IJ) → weight[nlaste+ij-1]
// W(NLASTE+IJ+1) → weight[nlaste+ij]
let fi = nlaste + ij - 1;
if fi >= 1 && fi + 1 < weight.len() {
let tido = (freq[fi] - freq[fi + 1]) * THIRD;
weight[fi - 1] += tido;
weight[fi] += 4.0 * tido;
weight[fi + 1] += tido;
}
}
}
nlaste = params.trapar.ifr1[itr] as usize;
// CALL ODFFR(I,J) — 设置内部频率
// Fortran: I, J 是 1-based 能级索引
let il_1based = i + 1; // 转回 1-based
let iu_1based = j + 1;
{
let odffr_params = OdffrParams {
il: il_1based,
iu: iu_1based,
teff: params.teff,
nlmx: NLMX,
};
// 类型适配: iz 是 Vec<i32>ODFFR 需要 &[f64]
let iz_f64: Vec<f64> = params.ionpar.iz.iter().map(|&x| x as f64).collect();
let odffr_atomic = OdffrAtomicData {
iel: &params.levpar.iel,
iz: &iz_f64,
enion: &params.levpar.enion,
nquant: &params.levpar.nquant,
};
// 类型适配: itra 是 Vec<Vec<i32>> (2D)ODFFR 需要 &[i32] (flat, row-major)
let nlevel = params.levpar.iel.len();
let itra_flat: Vec<i32> = params.trapar.itra.iter().flatten().copied().collect();
let odffr_model = OdffrModelData {
itra: &itra_flat,
jndodf: &params.odfctr.jndodf,
};
// 类型适配: fros/wnus 是 Vec<Vec<f64>> (2D)ODFFR 需要 &mut [f64] (flat)
let num_odf = params.odfctr.nfrodf.len();
let mut fros_flat = vec![0.0_f64; MFRO * num_odf];
let mut wnus_flat = vec![0.0_f64; MFRO * num_odf];
// 复制当前值
for (fi, row) in params.odffrq.fros.iter().enumerate().take(MFRO) {
for (ki, &val) in row.iter().enumerate().take(num_odf) {
fros_flat[fi * num_odf + ki] = val;
}
}
for (fi, row) in params.odffrq.wnus.iter().enumerate().take(MFRO) {
for (ki, &val) in row.iter().enumerate().take(num_odf) {
wnus_flat[fi * num_odf + ki] = val;
}
}
{
let mut odffr_output = OdffrOutputState {
nfrodf: &mut params.odfctr.nfrodf,
fros: &mut fros_flat,
wnus: &mut wnus_flat,
};
odffr::odffr(&odffr_params, &odffr_atomic, &odffr_model, &mut odffr_output);
}
// 复制回 2D 数组
for (fi, row) in params.odffrq.fros.iter_mut().enumerate().take(MFRO) {
for (ki, val) in row.iter_mut().enumerate().take(num_odf) {
*val = fros_flat[fi * num_odf + ki];
}
}
for (fi, row) in params.odffrq.wnus.iter_mut().enumerate().take(MFRO) {
for (ki, val) in row.iter_mut().enumerate().take(num_odf) {
*val = wnus_flat[fi * num_odf + ki];
}
}
let _ = nlevel; // 避免未使用警告
}
// IF(INDEXP(ITR).NE.0) CALL IJALIS(ITR,IFRQ0,IFRQ1)
if params.trapar.indexp[itr] != 0 {
let mut ijalis_params = IjalisParams {
trapar: params.trapar,
levpar: params.levpar,
atopar: params.atopar,
traali: params.traali,
freaux: params.freaux,
tracor: params.tracor,
};
let _ijalis_out = crate::tlusty::math::ali::ijalis(itr, _ifrq0, _ifrq1, &mut ijalis_params);
}
params.trapar.osc0[itr] = 0.0;
let is_quant = params.levpar.nquant[i] as usize;
let j_quant = params.levpar.nquant[j] as usize;
if jnd_idx < params.odfstk.xkij.len() {
for k in j_quant..=NLMX {
if k < params.odfstk.xkij[jnd_idx].len() {
let (xkij_val, wl0_val, fij_val) = stark0(is_quant, k, izzh);
params.odfstk.xkij[jnd_idx][k] = xkij_val;
params.odfstk.wl0[jnd_idx][k] = wl0_val;
params.odfstk.fij[jnd_idx][k] = fij_val;
params.trapar.osc0[itr] += fij_val;
}
}
}
}
params.basnum.nfreq = nlaste as i32;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tlusty::state::atomic::{AtoPar, IonPar, LevPar, TraAli, TraCor, TraPar};
use crate::tlusty::state::config::BasNum;
use crate::tlusty::state::constants::NLMX;
use crate::tlusty::state::model::{CompIf, FreAux};
use crate::tlusty::state::odfpar::{OdfCtr, OdfFrq, OdfMod, OdfStk};
fn create_test_state() -> (BasNum, IonPar, LevPar, TraPar, OdfCtr, OdfFrq, OdfMod, OdfStk, CompIf, Vec<f64>, AtoPar, TraAli, FreAux, TraCor) {
let mut basnum = BasNum::default();
basnum.ntrans = 2;
basnum.nfreq = 10;
basnum.ispodf = 1;
let mut ionpar = IonPar::default();
ionpar.iz[0] = 1; // H
let mut levpar = LevPar::default();
levpar.nquant[0] = 1;
levpar.nquant[1] = 2;
levpar.nquant[2] = 3;
levpar.iel[0] = 1;
levpar.iel[1] = 1;
levpar.iel[2] = 1;
let mut trapar = TraPar::default();
trapar.indexp[0] = 2;
trapar.ilow[0] = 1;
trapar.iup[0] = 2;
trapar.iprof[0] = 0;
trapar.ifr0[0] = 1;
trapar.ifr1[0] = 5;
trapar.line[0] = 1;
// Fix: 使用 OdfCtr.jndodf 而非 TraPar.ijtf
let mut odfctr = OdfCtr::new();
odfctr.jndodf[0] = 1; // 第一个跃迁对应 jnd=1 (1-based)
let mut odffrq = OdfFrq::new();
// kdo[jnd_idx][ik] 对应 Fortran KDO(ik+1, jnd)
odffrq.kdo[0][0] = 10;
odffrq.kdo[0][1] = 10;
odffrq.kdo[0][2] = 10;
odffrq.kdo[0][3] = 10;
odffrq.xdo[0][0] = 0.1;
odffrq.xdo[0][1] = 0.1;
odffrq.xdo[0][2] = 0.1;
let odfmod = OdfMod::new();
let odfstk = OdfStk::new(NLMX);
let compif = CompIf::default();
// XI2: 0-based, xi2[n-1] = 1/n²
let mut xi2 = vec![0.0; 50];
for n in 1..=10 {
xi2[n - 1] = 1.0 / (n as f64).powi(2);
}
let atopar = AtoPar::default();
let traali = TraAli::default();
let freaux = FreAux::default();
let tracor = TraCor::default();
(basnum, ionpar, levpar, trapar, odfctr, odffrq, odfmod, odfstk, compif, xi2, atopar, traali, freaux, tracor)
}
macro_rules! make_params {
($basnum:ident, $ionpar:ident, $levpar:ident, $trapar:ident, $odfctr:ident,
$odffrq:ident, $odfmod:ident, $odfstk:ident, $compif:ident, $xi2:ident,
$atopar:ident, $traali:ident, $freaux:ident, $tracor:ident) => {
OdfhysParams {
basnum: &mut $basnum,
ionpar: &$ionpar,
levpar: &$levpar,
trapar: &mut $trapar,
odfctr: &mut $odfctr,
odffrq: &mut $odffrq,
odfmod: &mut $odfmod,
odfstk: &mut $odfstk,
compif: &mut $compif,
xi2: &mut $xi2,
teff: 35000.0,
atopar: &$atopar,
traali: &$traali,
freaux: &mut $freaux,
tracor: &mut $tracor,
}
};
}
#[test]
fn test_odfhys_simplified_mode() {
let (
mut basnum,
ionpar,
levpar,
mut trapar,
mut odfctr,
mut odffrq,
mut odfmod,
mut odfstk,
mut compif,
mut xi2,
atopar,
traali,
mut freaux,
mut tracor,
) = create_test_state();
let mut params = make_params!(
basnum, ionpar, levpar, trapar, odfctr,
odffrq, odfmod, odfstk, compif, xi2,
atopar, traali, freaux, tracor
);
odfhys_simplified(&mut params);
// 验证振子强度被计算
assert!(
params.trapar.osc0[0] > 0.0,
"Oscillator strength should be positive, got {}",
params.trapar.osc0[0]
);
// 验证 INTMOD 被设置
assert_eq!(params.trapar.intmod[0], 6);
// 验证 LCOMP 被设置为 false
assert_eq!(params.trapar.lcomp[0], 0);
// Fix: 验证 LINEXP 被设置为 false
assert!(!params.compif.linexp[0]);
}
#[test]
fn test_odfhys_skip_non_mode2() {
let (
mut basnum,
ionpar,
levpar,
mut trapar,
mut odfctr,
mut odffrq,
mut odfmod,
mut odfstk,
mut compif,
mut xi2,
atopar,
traali,
mut freaux,
mut tracor,
) = create_test_state();
// 设置为非 mode 2
trapar.indexp[0] = 1;
let mut params = make_params!(
basnum, ionpar, levpar, trapar, odfctr,
odffrq, odfmod, odfstk, compif, xi2,
atopar, traali, freaux, tracor
);
odfhys_simplified(&mut params);
// 振子强度应该保持 0被跳过
assert_eq!(params.trapar.osc0[0], 0.0);
}
#[test]
fn test_odfhys_skip_negative_jndodf() {
let (
mut basnum,
ionpar,
levpar,
mut trapar,
mut odfctr,
mut odffrq,
mut odfmod,
mut odfstk,
mut compif,
mut xi2,
atopar,
traali,
mut freaux,
mut tracor,
) = create_test_state();
// Fix: 测试负数 JNDODF 被正确跳过
odfctr.jndodf[0] = -1;
let mut params = make_params!(
basnum, ionpar, levpar, trapar, odfctr,
odffrq, odfmod, odfstk, compif, xi2,
atopar, traali, freaux, tracor
);
odfhys_simplified(&mut params);
// 应该被跳过osc0 保持 0
assert_eq!(params.trapar.osc0[0], 0.0);
}
#[test]
fn test_stark_parameters_computed() {
let (
mut basnum,
ionpar,
levpar,
mut trapar,
mut odfctr,
mut odffrq,
mut odfmod,
mut odfstk,
mut compif,
mut xi2,
atopar,
traali,
mut freaux,
mut tracor,
) = create_test_state();
let mut params = make_params!(
basnum, ionpar, levpar, trapar, odfctr,
odffrq, odfmod, odfstk, compif, xi2,
atopar, traali, freaux, tracor
);
odfhys_simplified(&mut params);
// 验证 Stark 参数被计算
// jnd_idx=0 (jnd=1, 0-based), k from j_quant=2 to NLMX
assert!(params.odfstk.xkij[0][2] > 0.0);
assert!(params.odfstk.wl0[0][2] > 0.0);
assert!(params.odfstk.fij[0][2] > 0.0);
}
#[test]
fn test_xi2_0based_indexing() {
// 验证 XI2 使用 0-based 索引: xi2[n-1] = 1/n²
let xi2: Vec<f64> = (1..=10).map(|n| 1.0 / (n as f64).powi(2)).collect();
assert!((xi2[0] - 1.0).abs() < 1e-15); // n=1: xi2[0] = 1.0
assert!((xi2[1] - 0.25).abs() < 1e-15); // n=2: xi2[1] = 0.25
assert!((xi2[2] - 1.0 / 9.0).abs() < 1e-15); // n=3: xi2[2] = 1/9
}
}