SpectraRust/src/synspec/math/he2ini.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

287 lines
8.2 KiB
Rust

//! He II line profile data initialization.
//!
//! Translated from SYNSPEC54.FOR subroutine HE2INI (line 7535).
//!
//! Initializes necessary arrays for evaluating the He II line
//! absorption profiles using data calculated by Schoening and Butler.
#![allow(clippy::never_loop)]
use std::fs::File;
use std::io::{BufRead, BufReader};
/// Constants for He II profile arrays
pub const NLINE_HE2: usize = 19;
pub const NWL_HE2_MAX: usize = 36;
pub const NT_HE2: usize = 6;
pub const NE_HE2: usize = 11;
/// He II line profile table data
#[derive(Debug, Clone)]
pub struct He2ProfileTable {
/// Lower level index
pub il: usize,
/// Upper level index
pub iu: usize,
/// Central wavelength
pub wl0: f64,
/// Number of wavelength points
pub nwl: usize,
/// Log10 wavelength displacements [NWL_HE2_MAX]
pub wl: [f64; NWL_HE2_MAX],
/// Log10 temperature grid [NT_HE2]
pub xt: [f64; NT_HE2],
/// Log10 electron density grid [NE_HE2]
pub xne: [f64; NE_HE2],
/// Profile values [NWL_HE2_MAX x NT_HE2 x NE_HE2]
pub prf: [[[f64; NE_HE2]; NT_HE2]; NWL_HE2_MAX],
/// Asymptotic profile coefficient
pub xk: f64,
}
impl Default for He2ProfileTable {
fn default() -> Self {
Self {
il: 0,
iu: 0,
wl0: 0.0,
nwl: 0,
wl: [0.0; NWL_HE2_MAX],
xt: [0.0; NT_HE2],
xne: [0.0; NE_HE2],
prf: [[[0.0; NE_HE2]; NT_HE2]; NWL_HE2_MAX],
xk: 0.0,
}
}
}
/// He II line initialization result
#[derive(Debug, Clone)]
pub struct He2InitResult {
/// Profile tables for each line
pub tables: Vec<He2ProfileTable>,
/// Number of wavelength points per line [NLINE_HE2]
pub nwlhe2: [usize; NLINE_HE2],
/// Lower level indices [NLINE_HE2]
pub ilhe2: [usize; NLINE_HE2],
/// Upper level indices [NLINE_HE2]
pub iuhe2: [usize; NLINE_HE2],
}
impl Default for He2InitResult {
fn default() -> Self {
Self {
tables: Vec::new(),
nwlhe2: [0; NLINE_HE2],
ilhe2: [0; NLINE_HE2],
iuhe2: [0; NLINE_HE2],
}
}
}
/// Parameters for HE2INI
pub struct He2iniParams {
/// Path to data directory
pub data_dir: String,
/// Model depth points
pub nd: usize,
/// Temperature array [nd]
pub temp: Vec<f64>,
/// Electron density array [nd]
pub elec: Vec<f64>,
/// Turbulent velocity array [nd]
pub vturb: Vec<f64>,
}
/// Initialize He II line profile data.
///
/// # Arguments
/// * `params` - Initialization parameters
///
/// # Returns
/// He II line initialization result with profile tables
pub fn he2ini(params: &He2iniParams) -> std::io::Result<He2InitResult> {
let filename = format!("{}/he2prf.dat", params.data_dir);
let file = File::open(&filename)?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let mut result = He2InitResult::default();
for iline in 0..NLINE_HE2 {
// Read line indices: FORMAT(//14X,I2,9X,I2/)
let header = read_next_nonblank(&mut lines)?;
let (il, iu) = parse_he2_header(&header)?;
result.ilhe2[iline] = il;
result.iuhe2[iline] = iu;
// Compute central wavelength
let wl00 = if il <= 2 { 227.838 } else { 227.7776 };
let wl0 = wl00 / (1.0 / (il as f64).powi(2) - 1.0 / (iu as f64).powi(2));
let mut table = He2ProfileTable {
il,
iu,
wl0,
..Default::default()
};
// Read wavelength points
let wl_line = read_next_line(&mut lines)?;
let wl_parts = parse_he2_data(&wl_line)?;
let nwl = wl_parts[0] as usize;
table.nwl = nwl;
result.nwlhe2[iline] = nwl;
for i in 0..nwl.min(NWL_HE2_MAX) {
table.wl[i] = if wl_parts[i + 1] < 1.0e-4 {
(1.0e-4_f64).log10()
} else {
wl_parts[i + 1].log10()
};
}
// Read temperature points: FORMAT(2X,I4,F10.3,5F12.3)
let xt_line = read_next_line(&mut lines)?;
let xt_parts = parse_he2_data(&xt_line)?;
let nt = xt_parts[0] as usize;
for i in 0..nt.min(NT_HE2) {
table.xt[i] = xt_parts[i + 1];
}
// Read electron density points: FORMAT(2X,I4,F10.2,5F12.2/4X,5F12.2)
let xne_line = read_next_line(&mut lines)?;
let xne_parts = parse_he2_data(&xne_line)?;
let ne = xne_parts[0] as usize;
for i in 0..ne.min(NE_HE2) {
table.xne[i] = xne_parts[i + 1];
}
// Skip blank line
lines.next();
// Read profile data: FORMAT(10F8.3)
for ie in 0..ne.min(NE_HE2) {
for _it in 0..nt.min(NT_HE2) {
lines.next(); // Skip blank line
let prf_line = read_next_line(&mut lines)?;
let prf_parts = parse_he2_data(&prf_line)?;
for iwl in 0..nwl.min(NWL_HE2_MAX) {
if iwl < prf_parts.len() {
table.prf[iwl][_it][ie] = prf_parts[iwl];
}
}
}
}
// Compute asymptotic profile coefficient
if nwl > 0 && ne > 0 {
let xclog = table.prf[nwl - 1][0][0]
+ 2.5 * table.wl[nwl - 1]
+ 31.831
- table.xne[0]
- 2.0 * wl0.log10();
let xklog = 0.6666667 * (xclog - 0.176);
table.xk = (xklog * std::f64::consts::LN_10).exp();
}
result.tables.push(table);
}
Ok(result)
}
/// Read next non-empty line
fn read_next_line(lines: &mut impl Iterator<Item = std::io::Result<String>>) -> std::io::Result<String> {
loop {
match lines.next() {
Some(Ok(line)) => return Ok(line),
Some(Err(e)) => return Err(e),
None => return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Unexpected end of file",
)),
}
}
}
/// Read next non-blank line (skip empty lines)
fn read_next_nonblank(lines: &mut impl Iterator<Item = std::io::Result<String>>) -> std::io::Result<String> {
loop {
let line = read_next_line(lines)?;
if !line.trim().is_empty() {
return Ok(line);
}
}
}
/// Parse He II header line: FORMAT(//14X,I2,9X,I2/)
fn parse_he2_header(line: &str) -> std::io::Result<(usize, usize)> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid He II header: {}", line),
));
}
let il = parts[0].parse::<usize>().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("IL: {}", e))
})?;
let iu = parts[1].parse::<usize>().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("IU: {}", e))
})?;
Ok((il, iu))
}
/// Parse He II data line (free format)
fn parse_he2_data(line: &str) -> std::io::Result<Vec<f64>> {
let values: Vec<f64> = line
.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect();
Ok(values)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_he2ini_default() {
let result = He2InitResult::default();
assert_eq!(result.nwlhe2.len(), NLINE_HE2);
assert_eq!(result.ilhe2.len(), NLINE_HE2);
assert!(result.tables.is_empty());
}
#[test]
fn test_he2_profile_table_default() {
let table = He2ProfileTable::default();
assert_eq!(table.nwl, 0);
assert_eq!(table.il, 0);
assert_eq!(table.iu, 0);
}
#[test]
fn test_parse_he2_header() {
let line = " 1 2";
let result = parse_he2_header(line);
assert!(result.is_ok());
let (il, iu) = result.unwrap();
assert_eq!(il, 1);
assert_eq!(iu, 2);
}
#[test]
fn test_parse_he2_data() {
let line = " 19 0.123 0.456 0.789";
let result = parse_he2_data(line);
assert!(result.is_ok());
let values = result.unwrap();
assert_eq!(values.len(), 4);
assert!((values[0] - 19.0).abs() < 1e-10);
}
}