SpectraRust/src/synspec/math/pffe.rs
fmq e2c1a4580a feat: F2R 重构全部完成 + 自动化脚本改进
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>
2026-06-08 14:54:53 +08:00

413 lines
17 KiB
Rust

//! PFFE - Partition functions for Fe IV to Fe IX
//!
//! After Fischel and Sparks, 1971, NASA SP-3066.
//! Bilinear interpolation in (electron pressure, temperature) space.
/// Evaluates partition function for Fe IV to Fe IX.
///
/// # Arguments
/// * `ion` - Ionization stage (4-9, where 4=Fe IV, 9=Fe IX)
/// * `t` - Temperature in K
/// * `ane` - Electron density in cm^-3
///
/// # Returns
/// * Partition function (linear scale)
pub fn pffe(ion: usize, t: f64, ane: f64) -> f64 {
let xen = std::f64::consts::LN_10;
let xmil = 0.001_f64;
let xbtz = 1.380_54e-16_f64;
let nne = 10_usize;
// Table offsets: nca[ion-4] gives size of temperature-only part pXa
// For ions 4..9: na = 22, 30, 37, 40, 41, 45
let nca: [usize; 6] = [22, 30, 37, 40, 41, 45];
// 50 temperature grid points (in units of 1000 K)
let tt: [f64; 50] = [
3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18., 19.,
20., 21., 22., 23., 24., 25., 26., 27., 28., 29., 30.,
32., 34., 36., 38., 40., 42., 44., 46., 48.,
50., 55., 60., 65., 70., 75., 80., 85., 90., 95., 100., 125., 150.,
];
// 10 electron pressure grid points (log10 Pe)
let pn: [f64; 10] = [-2., -1., 0., 1., 2., 3., 4., 5., 6., 7.];
// Temperature-only tables pXa (log10 partition function)
let p4a: [f64; 22] = [
0.778, 0.778, 0.778, 0.779, 0.783, 0.789, 0.801, 0.818,
0.842, 0.871, 0.906, 0.945, 0.987, 1.030, 1.074, 1.117,
1.160, 1.201, 1.242, 1.280, 1.317, 1.353,
];
let p5a: [f64; 30] = [
1.235, 1.276, 1.301, 1.321, 1.339, 1.359, 1.381, 1.405,
1.432, 1.460, 1.489, 1.518, 1.546, 1.574, 1.601, 1.627,
1.652, 1.675, 1.697, 1.718, 1.738, 1.757, 1.775, 1.792,
1.808, 1.823, 1.838, 1.851, 1.877, 1.900,
];
let p6a: [f64; 37] = [
1.218, 1.273, 1.309, 1.335, 1.358, 1.379, 1.400, 1.421,
1.442, 1.463, 1.484, 1.504, 1.523, 1.542, 1.560, 1.577,
1.594, 1.609, 1.624, 1.638, 1.652, 1.664, 1.677, 1.688,
1.699, 1.709, 1.719, 1.729, 1.746, 1.762, 1.777, 1.790,
1.803, 1.814, 1.825, 1.834, 1.843,
];
let p7a: [f64; 40] = [
1.074, 1.130, 1.167, 1.194, 1.215, 1.234, 1.250, 1.266, 1.280, 1.293,
1.306, 1.318, 1.329, 1.340, 1.350, 1.360, 1.369, 1.378, 1.386, 1.394,
1.401, 1.408, 1.415, 1.421, 1.427, 1.433, 1.439, 1.444, 1.454, 1.463,
1.471, 1.479, 1.486, 1.492, 1.498, 1.504, 1.509, 1.514, 1.525, 1.534,
];
let p8a: [f64; 41] = [
0.809, 0.849, 0.875, 0.894, 0.908, 0.918, 0.927, 0.934, 0.939, 0.944,
0.948, 0.952, 0.955, 0.958, 0.960, 0.962, 0.964, 0.966, 0.967, 0.969,
0.970, 0.971, 0.973, 0.974, 0.975, 0.975, 0.976, 0.977, 0.978, 0.980,
0.981, 0.982, 0.983, 0.984, 0.984, 0.985, 0.986, 0.986, 0.987, 0.988,
0.989,
];
let p9a: [f64; 45] = [
0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000,
0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.001,
0.002, 0.005, 0.008, 0.014, 0.021,
];
// 2D tables stored flat in Fortran column-major order.
// Fortran pXb(10, nb): pXb(j, k) where j=1..10 (pressure), k=1..nb (temperature)
// Column-major: data fills column by column (k varies slowest).
// Flat index = (k-1)*10 + (j-1) for 1-based (k-1)*10 + (j-1).
// Access function: get_pb(flat, j_0based, k_0based) = flat[k*10 + j]
// p4b(10, 28) - 28 columns of 10 values
let p4b_flat: [f64; 280] = [
1.406,1.393,1.389,1.387,1.387,1.387,1.387,1.387,1.387,1.387,
1.464,1.434,1.424,1.421,1.420,1.419,1.419,1.419,1.419,1.419,
1.546,1.483,1.461,1.454,1.451,1.451,1.450,1.450,1.450,1.450,
1.665,1.547,1.503,1.488,1.482,1.481,1.480,1.480,1.480,1.480,
1.826,1.636,1.553,1.524,1.514,1.510,1.509,1.509,1.509,1.509,
2.024,1.755,1.618,1.564,1.546,1.540,1.538,1.537,1.537,1.537,
2.480,2.087,1.814,1.674,1.619,1.599,1.593,1.591,1.590,1.590,
2.945,2.489,2.105,1.846,1.717,1.667,1.649,1.643,1.641,1.640,
3.379,2.897,2.452,2.089,1.859,1.751,1.710,1.696,1.691,1.689,
3.774,3.283,2.808,2.381,2.054,1.864,1.782,1.751,1.741,1.738,
4.133,3.637,3.150,2.688,2.292,2.015,1.871,1.814,1.793,1.786,
4.460,3.962,3.468,2.989,2.549,2.199,1.984,1.886,1.848,1.835,
4.757,4.258,3.762,3.274,2.809,2.406,2.121,1.972,1.908,1.886,
5.029,4.530,4.032,3.539,3.061,2.624,2.279,2.073,1.976,1.939,
5.279,4.780,4.281,3.785,3.299,2.840,2.450,2.189,2.051,1.996,
5.510,5.010,4.511,4.013,3.522,3.050,2.628,2.318,2.136,2.057,
6.014,5.514,5.014,4.515,4.018,3.530,3.065,2.666,2.381,2.228,
6.435,5.935,5.435,4.936,4.437,3.943,3.460,3.022,2.658,2.422,
6.794,6.294,5.794,5.294,4.794,4.297,3.807,3.343,2.939,2.631,
7.102,6.602,6.102,5.602,5.102,4.604,4.110,3.638,3.194,2.845,
7.370,6.870,6.370,5.870,5.370,4.871,4.375,3.892,3.439,3.052,
7.606,7.106,6.606,6.106,5.605,5.106,4.608,4.125,3.661,3.249,
7.815,7.315,6.814,6.314,5.814,5.314,4.816,4.333,3.851,3.418,
8.001,7.501,7.001,6.500,6.000,5.500,5.001,4.511,4.032,3.586,
8.168,7.668,7.168,6.668,6.168,5.667,5.168,4.680,4.197,3.741,
8.319,7.819,7.319,6.819,6.319,5.818,5.319,4.832,4.347,3.884,
8.900,8.399,7.899,7.399,6.899,6.398,5.898,5.405,4.917,4.431,
9.294,8.794,8.294,7.793,7.293,6.793,6.292,5.799,5.306,4.824,
];
// p5b(10, 20) - 20 columns of 10 values
let p5b_flat: [f64; 200] = [
1.943,1.928,1.923,1.921,1.921,1.921,1.921,1.921,1.921,1.921,
2.011,1.964,1.947,1.942,1.941,1.940,1.940,1.940,1.940,1.940,
2.144,2.025,1.980,1.965,1.960,1.958,1.957,1.957,1.957,1.957,
2.361,2.137,2.032,1.993,1.980,1.976,1.975,1.974,1.974,1.974,
2.646,2.315,2.121,2.035,2.004,1.994,1.991,1.990,1.989,1.989,
2.960,2.553,2.260,2.102,2.037,2.015,2.007,2.005,2.004,2.004,
3.274,2.823,2.450,2.205,2.086,2.040,2.025,2.020,2.018,2.018,
3.575,3.101,2.674,2.348,2.158,2.075,2.045,2.036,2.032,2.031,
4.251,3.757,3.275,2.829,2.466,2.234,2.124,2.083,2.069,2.064,
4.822,4.324,3.829,3.346,2.895,2.522,2.278,2.161,2.116,2.100,
5.308,4.808,4.310,3.816,3.334,2.888,2.525,2.297,2.187,2.145,
5.725,5.225,4.726,4.228,3.736,3.260,2.828,2.496,2.294,2.206,
6.088,5.589,5.089,4.590,4.093,3.604,3.139,2.733,2.447,2.291,
6.407,5.907,5.407,4.908,4.409,3.915,3.433,2.988,2.629,2.399,
6.689,6.189,5.689,5.189,4.690,4.193,3.704,3.236,2.832,2.535,
6.940,6.440,5.940,5.440,4.941,4.443,3.949,3.469,3.038,2.687,
7.166,6.666,6.166,5.666,5.166,4.667,4.171,3.684,3.237,2.847,
7.370,6.870,6.369,5.869,5.369,4.870,4.373,3.882,3.417,3.008,
8.150,7.649,7.149,6.649,6.149,5.649,5.149,4.651,4.167,3.700,
8.677,8.177,7.676,7.176,6.676,6.176,5.676,5.176,4.687,4.203,
];
// p6b(10, 13) - 13 columns of 10 values
let p6b_flat: [f64; 130] = [
1.862,1.855,1.853,1.852,1.852,1.852,1.852,1.852,1.852,1.852,
1.958,1.900,1.880,1.874,1.872,1.871,1.871,1.871,1.871,1.871,
2.264,2.045,1.944,1.906,1.894,1.890,1.888,1.888,1.888,1.888,
2.776,2.386,2.119,1.984,1.930,1.912,1.906,1.904,1.903,1.903,
3.321,2.856,2.453,2.165,2.012,1.949,1.927,1.920,1.918,1.917,
3.821,3.333,2.868,2.465,2.178,2.025,1.963,1.941,1.934,1.932,
4.266,3.771,3.285,2.825,2.434,2.164,2.027,1.972,1.953,1.947,
4.662,4.164,3.670,3.187,2.739,2.372,2.135,2.022,1.980,1.965,
5.015,4.516,4.019,3.527,3.052,2.624,2.295,2.102,2.019,1.988,
5.332,4.832,4.344,3.838,3.351,2.889,2.493,2.217,2.075,2.017,
5.618,5.118,4.619,4.121,3.628,3.149,2.711,2.364,2.155,2.058,
6.710,6.210,5.710,5.210,4.711,4.213,3.719,3.241,2.807,2.462,
7.446,6.946,6.446,5.946,5.446,4.946,4.447,3.952,3.474,3.022,
];
// p7b(10, 10) - 10 columns of 10 values
let p7b_flat: [f64; 100] = [
1.555,1.546,1.544,1.543,1.542,1.542,1.542,1.542,1.542,1.542,
1.617,1.572,1.557,1.552,1.550,1.550,1.549,1.549,1.549,1.549,
1.798,1.648,1.587,1.566,1.559,1.557,1.556,1.556,1.556,1.556,
2.134,1.832,1.666,1.597,1.573,1.565,1.563,1.562,1.561,1.561,
2.550,2.138,1.836,1.671,1.602,1.578,1.570,1.568,1.567,1.567,
2.968,2.504,2.102,1.816,1.665,1.603,1.582,1.575,1.572,1.572,
3.359,2.875,2.419,2.037,1.779,1.651,1.601,1.584,1.579,1.577,
3.718,3.224,2.745,2.305,1.953,1.736,1.636,1.599,1.586,1.582,
5.097,4.598,4.098,3.601,3.110,2.638,2.217,1.899,1.719,1.643,
6.026,5.526,5.026,4.527,4.028,3.531,3.042,2.576,2.170,1.885,
];
// p8b(10, 9) - 9 columns of 10 values
let p8b_flat: [f64; 90] = [
0.992,0.991,0.990,0.990,0.990,0.990,0.990,0.990,0.990,0.990,
1.000,0.994,0.992,0.991,0.991,0.991,0.991,0.991,0.991,0.991,
1.032,1.005,0.996,0.993,0.992,0.991,0.991,0.991,0.991,0.991,
1.129,1.040,1.008,0.997,0.993,0.992,0.992,0.992,0.992,0.992,
1.335,1.132,1.042,1.009,0.998,0.994,0.993,0.993,0.992,0.992,
1.640,1.312,1.121,1.038,1.007,0.998,0.994,0.993,0.993,0.993,
1.987,1.573,1.269,1.101,1.030,1.005,0.997,0.994,0.993,0.993,
3.514,3.017,2.526,2.053,1.628,1.305,1.119,1.039,1.010,1.000,
4.569,4.069,3.569,3.072,2.580,2.103,1.671,1.336,1.136,1.048,
];
// p9b(10, 5) - 5 columns of 10 values
let p9b_flat: [f64; 50] = [
0.032,0.032,0.031,0.031,0.031,0.031,0.031,0.031,0.031,0.031,
0.048,0.045,0.044,0.044,0.044,0.044,0.044,0.044,0.044,0.044,
0.076,0.065,0.061,0.060,0.059,0.059,0.059,0.059,0.059,0.059,
1.128,0.722,0.429,0.271,0.207,0.184,0.177,0.174,0.173,0.173,
2.696,2.200,1.712,1.249,0.848,0.564,0.415,0.354,0.333,0.327,
];
// Helper: access pXb flat array as pXb(j, k) with 0-based indices
// Fortran pXb(10, nb) column-major: flat[k*10 + j]
let get_pb = |flat: &[f64], j: usize, k: usize| -> f64 { flat[k * 10 + j] };
// --- Algorithm ---
let ion_idx = ion - 4; // 0-based index into nca and data arrays
let na = nca[ion_idx]; // size of temperature-only table
let _nb = 50 - na; // size of 2D table (temperature dimension)
let pne = (ane * xbtz * t).log10();
let t0 = xmil * t;
// Find bracketing indices in pn[] (10 points, 0-based)
// Fortran: j starts at 1, scans 1..nne-1 (1-based). Sets j1, j2.
// We use 0-based j1, j2.
let (j1, j2) = if pne < pn[0] {
(0, 0)
} else if pne > pn[nne - 1] {
(nne - 1, nne - 1)
} else {
let mut jj = 0;
for k in 0..nne - 1 {
if pne >= pn[k] && pne < pn[k + 1] {
jj = k;
break;
}
}
(jj, jj + 1)
};
// Find bracketing indices in tt[] (50 points, 0-based)
// Fortran: i scans 1..49 (1-based). Sets i1, i2.
let (i1, i2) = if t0 >= tt[49] {
// At or above the highest grid point: clamp to last point
(49, 49)
} else {
let mut ii = 0;
for k in 0..49 {
if t0 >= tt[k] && t0 < tt[k + 1] {
ii = k;
break;
}
}
(ii, ii + 1)
};
// The three cases from Fortran (translated to 0-based):
// Fortran i1 (1-based) = our i1 + 1, Fortran i2 = our i2 + 1
// Fortran: if(i2.le.na) -> our: i2 + 1 <= na -> i2 < na -> i1 <= na - 2
// Fortran: if(i1.eq.na) -> our: i1 + 1 == na -> i1 == na - 1
// Fortran: else -> our: i1 + 1 > na -> i1 >= na
//
// For pXa: Fortran pXa(i) where i is 1-based -> Rust pXa[i-1]
// Our i1 (0-based) corresponds to Fortran index i1+1, so pXa[i1] (0-based in Rust)
// For pXb: Fortran pXb(j, i-na) where j=1..10, i is 1-based Fortran index
// k_0based = (i_fortran - 1) - na = our_i - na
// Get pa (temperature-only) and pb (2D) references
let pa: &[f64] = match ion_idx {
0 => &p4a,
1 => &p5a,
2 => &p6a,
3 => &p7a,
4 => &p8a,
5 => &p9a,
_ => unreachable!(),
};
let pb: &[f64] = match ion_idx {
0 => &p4b_flat,
1 => &p5b_flat,
2 => &p6b_flat,
3 => &p7b_flat,
4 => &p8b_flat,
5 => &p9b_flat,
_ => unreachable!(),
};
let (px1, px2, py1, py2) = if i2 < na {
// Both indices in temperature-only range
// Fortran: px1=pXa(i1), px2=pXa(i1), py1=pXa(i2), py2=pXa(i2)
// 0-based: pa[i1], pa[i1], pa[i2], pa[i2]
let v1 = pa[i1];
let v2 = pa[i2];
(v1, v1, v2, v2)
} else if i1 == na - 1 {
// Boundary: i1 in temperature-only, i2 in 2D range
// Fortran: px1=pXa(i1), px2=pXa(i1), py1=pXb(j1,i2-na), py2=pXb(j2,i2-na)
// 0-based: pa[i1], pa[i1], pb at (j1, i2-na), pb at (j2, i2-na)
let v = pa[i1];
let k2 = i2 - na; // 0-based 2D table column index
let w1 = get_pb(pb, j1, k2);
let w2 = get_pb(pb, j2, k2);
(v, v, w1, w2)
} else {
// Both in 2D range
// Fortran: pXb(j1,i1-na), pXb(j2,i1-na), pXb(j1,i2-na), pXb(j2,i2-na)
let k1 = i1 - na;
let k2 = i2 - na;
let v1 = get_pb(pb, j1, k1);
let v2 = get_pb(pb, j2, k1);
let w1 = get_pb(pb, j1, k2);
let w2 = get_pb(pb, j2, k2);
(v1, v2, w1, w2)
};
// Bilinear interpolation
// First interpolate in electron pressure
let dlgunx = px2 - px1;
let px = px1 + (pne - pn[j1]) * dlgunx;
let dlguny = py2 - py1;
let py = py1 + (pne - pn[j1]) * dlguny;
// Then interpolate in temperature
let delt = tt[i2] - tt[i1];
let pf = if delt != 0.0 {
let dlgut = (py - px) / delt;
px + (t0 - tt[i1]) * dlgut
} else {
px
};
(xen * pf).exp()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pffe_fe_iv_normal() {
// Fe IV at T=5000 K, ne=1e14
let pf = pffe(4, 5000.0, 1e14);
assert!(pf > 0.0, "PF should be positive for Fe IV");
assert!(pf.is_finite(), "PF should be finite for Fe IV");
// PF values are typically in range ~1-1000 for these ions at these conditions
assert!(pf < 1e10, "PF should be reasonable for Fe IV");
}
#[test]
fn test_pffe_fe_ix_normal() {
// Fe IX at T=50000 K, ne=1e14
let pf = pffe(9, 50000.0, 1e14);
assert!(pf > 0.0, "PF should be positive for Fe IX");
assert!(pf.is_finite(), "PF should be finite for Fe IX");
}
#[test]
fn test_pffe_boundary_low_pressure() {
// Very low electron density -> pne below pn[0]=-2
let pf = pffe(4, 10000.0, 1e-20);
assert!(pf > 0.0);
assert!(pf.is_finite());
}
#[test]
fn test_pffe_boundary_high_pressure() {
// Very high electron density -> pne above pn[9]=7
let pf = pffe(4, 10000.0, 1e20);
assert!(pf > 0.0);
assert!(pf.is_finite());
}
#[test]
fn test_pffe_boundary_low_temp() {
// At lowest temperature grid point (3000 K = tt[0]*1000)
let pf = pffe(4, 3000.0, 1e14);
assert!(pf > 0.0);
assert!(pf.is_finite());
}
#[test]
fn test_pffe_boundary_high_temp() {
// At highest temperature grid point (150000 K = tt[49]*1000)
let pf = pffe(4, 150000.0, 1e14);
assert!(pf > 0.0);
assert!(pf.is_finite());
}
#[test]
fn test_pffe_all_ions() {
// All ionization stages should return valid results
for ion in 4..=9 {
let pf = pffe(ion, 20000.0, 1e14);
assert!(pf > 0.0, "PF should be positive for Fe {}", ion);
assert!(pf.is_finite(), "PF should be finite for Fe {}", ion);
}
}
#[test]
fn test_pffe_fe_iv_low_temp_extrapolation() {
// At T=3000 K (tt[0]) with low density, should use p4a[0]
// p4a[0] = 0.778, so pf = exp(2.302585093 * 0.778)
let pf = pffe(4, 3000.0, 1e-10);
let expected = (2.302_585_093_f64 * 0.778_f64).exp();
let rel_err = (pf - expected).abs() / expected;
assert!(
rel_err < 0.01,
"PF at boundary should match p4a[0]: got {}, expected {}, rel_err={}",
pf, expected, rel_err
);
}
#[test]
fn test_pffe_high_temp_boundary() {
// At T=150000 K (tt[49]*1000) with low density, should clamp to last grid point
// t0=150 >= tt[49]=150, so i1=i2=49, delt=0, pf=px
// For Fe IV (na=22), k=49-22=27. p4b(1,28) in Fortran = p4b_flat[27*10+0] = 9.294
let pf = pffe(4, 150000.0, 1e-10);
let expected = (2.302_585_093_f64 * 9.294_f64).exp();
let rel_err = (pf - expected).abs() / expected;
assert!(
rel_err < 0.01,
"PF at high T boundary: got {}, expected {}, rel_err={}",
pf, expected, rel_err
);
}
}