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

471 lines
13 KiB
Rust

//! Hydrogen line profile data initialization.
//!
//! Translated from SYNSPEC54.FOR subroutine HYDINI (line 6877).
//!
//! Initializes necessary arrays for evaluating hydrogen line profiles
//! from the Lemke, Tremblay-Bergeron, or Schoening-Butler tables.
#![allow(clippy::never_loop)]
use std::fs::File;
use std::io::{BufRead, BufReader};
use super::stark0::stark0;
/// Constants for hydrogen profile arrays
pub const NLINES_MAX: usize = 22;
pub const NLEVELS: usize = 4;
pub const NWL_MAX: usize = 100;
pub const NT_MAX: usize = 20;
pub const NE_MAX: usize = 20;
/// Hydrogen line profile table data
#[derive(Debug, Clone)]
pub struct HydProfileTable {
/// Line index (i, j)
pub i: usize,
pub j: usize,
/// Central wavelength
pub wl0: f64,
/// Number of wavelength points
pub nwl: usize,
/// Number of temperature points
pub nt: usize,
/// Number of electron density points
pub ne: usize,
/// Log10 wavelength displacements [NWL_MAX]
pub wl: [f64; NWL_MAX],
/// Log10 temperature grid [NT_MAX]
pub xt: [f64; NT_MAX],
/// Log10 electron density grid [NE_MAX]
pub xne: [f64; NE_MAX],
/// Profile values [NWL_MAX x NT_MAX x NE_MAX]
pub prf: [[[f64; NE_MAX]; NT_MAX]; NWL_MAX],
/// Asymptotic profile coefficient
pub xk: f64,
}
impl Default for HydProfileTable {
fn default() -> Self {
Self {
i: 0,
j: 0,
wl0: 0.0,
nwl: 0,
nt: 0,
ne: 0,
wl: [0.0; NWL_MAX],
xt: [0.0; NT_MAX],
xne: [0.0; NE_MAX],
prf: [[[0.0; NE_MAX]; NT_MAX]; NWL_MAX],
xk: 0.0,
}
}
}
/// Hydrogen line initialization result
#[derive(Debug, Clone)]
pub struct HydInitResult {
/// Central wavelengths for lines [NLEVELS x NLINES_MAX]
pub wline: [[f64; NLINES_MAX]; NLEVELS],
/// Line index mapping [NLEVELS x NLINES_MAX]
pub ilin0: [[usize; NLINES_MAX]; NLEVELS],
/// Profile tables
pub tables: Vec<HydProfileTable>,
/// Lemke mode flag
pub ilemke: bool,
/// Number of lines
pub nlihyd: usize,
}
impl Default for HydInitResult {
fn default() -> Self {
Self {
wline: [[0.0; NLINES_MAX]; NLEVELS],
ilin0: [[0; NLINES_MAX]; NLEVELS],
tables: Vec::new(),
ilemke: false,
nlihyd: 0,
}
}
}
/// Hydrogen line profile table source
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HydTableSource {
/// Schoening-Butler tables (ihydpr < 0)
SchoeningButler,
/// Lemke tables (ihydpr = 21)
Lemke,
/// Tremblay-Bergeron tables (ihydpr = 22)
Tremblay,
}
/// Parameters for HYDINI
pub struct HydiniParams {
/// Table source selection
pub source: HydTableSource,
/// 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 hydrogen line profile data.
///
/// # Arguments
/// * `params` - Initialization parameters
///
/// # Returns
/// Hydrogen line initialization result with profile tables
pub fn hydini(params: &HydiniParams) -> std::io::Result<HydInitResult> {
let mut result = HydInitResult::default();
// Initialize central wavelengths using STARK0
for i in 0..NLEVELS {
for j in (i + 1)..NLINES_MAX {
let stark = stark0(i as i32 + 1, j as i32 + 1, 1);
result.wline[i][j] = stark.wl0;
}
}
// Initialize line index mapping
for i in 0..NLEVELS {
for j in 0..NLINES_MAX {
result.ilin0[i][j] = 0;
}
}
match params.source {
HydTableSource::SchoeningButler => {
read_schoening_butler(params, &mut result)?;
}
HydTableSource::Lemke | HydTableSource::Tremblay => {
read_lemke_tremblay(params, &mut result)?;
}
}
Ok(result)
}
/// Read Schoening-Butler tables
fn read_schoening_butler(
params: &HydiniParams,
result: &mut HydInitResult,
) -> std::io::Result<()> {
let filename = format!("{}/hydprf.dat", params.data_dir);
let file = File::open(&filename)?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
// Skip 12 header lines
for _ in 0..12 {
lines.next();
}
let nline = 12;
result.ilemke = false;
for iline in 0..nline {
// Read line indices
let header = read_next_line(&mut lines)?;
let (i, j) = parse_line_indices(&header)?;
let j = if iline == 11 { 10 } else { j }; // Special case for last line
let wl0 = result.wline[i - 1][j - 1];
result.ilin0[i - 1][j - 1] = iline + 1;
let mut table = HydProfileTable {
i,
j,
wl0,
..Default::default()
};
// Read wavelength points
let wl_line = read_next_line(&mut lines)?;
let wl_parts = parse_data_line(&wl_line)?;
let nwl = wl_parts.len() - 1; // First value is character
table.nwl = nwl;
for k in 0..nwl.min(NWL_MAX) {
table.wl[k] = if wl_parts[k + 1] < 1.0e-4 {
(1.0e-4_f64).log10()
} else {
wl_parts[k + 1].log10()
};
}
// Read temperature points
let xt_line = read_next_line(&mut lines)?;
let xt_parts = parse_data_line(&xt_line)?;
let nt = xt_parts.len() - 1;
table.nt = nt;
for k in 0..nt.min(NT_MAX) {
table.xt[k] = xt_parts[k + 1];
}
// Read electron density points
let xne_line = read_next_line(&mut lines)?;
let xne_parts = parse_data_line(&xne_line)?;
let ne = xne_parts.len() - 1;
table.ne = ne;
for k in 0..ne.min(NE_MAX) {
table.xne[k] = xne_parts[k + 1];
}
// Skip blank line
lines.next();
// Read profile data
for ie in 0..ne.min(NE_MAX) {
for it in 0..nt.min(NT_MAX) {
lines.next(); // Skip blank line
let prf_line = read_next_line(&mut lines)?;
let prf_parts = parse_data_line(&prf_line)?;
for iwl in 0..nwl.min(NWL_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.5304
- 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(())
}
/// Read Lemke or Tremblay tables
fn read_lemke_tremblay(
params: &HydiniParams,
result: &mut HydInitResult,
) -> std::io::Result<()> {
let filename = match params.source {
HydTableSource::Lemke => format!("{}/lemke.dat", params.data_dir),
HydTableSource::Tremblay => format!("{}/tremblay.dat", params.data_dir),
_ => unreachable!(),
};
let file = File::open(&filename)?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
result.ilemke = true;
// Read number of tables
let ntab_line = read_next_line(&mut lines)?;
let ntab: usize = ntab_line.trim().parse().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("NTAB: {}", e))
})?;
let mut iline = 0;
for _ in 0..ntab {
// Read number of lines in this table
let nlly_line = read_next_line(&mut lines)?;
let nlly: usize = nlly_line.trim().parse().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("NLLY: {}", e))
})?;
let ilineb = iline;
// Read line parameters
for _ in 0..nlly {
let param_line = read_next_line(&mut lines)?;
let parts = parse_data_line(&param_line)?;
if parts.len() < 11 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid Lemke/Tremblay parameter line",
));
}
let i = parts[0] as usize;
let j = parts[1] as usize;
let almin = parts[2];
let anemin = parts[3];
let tmin = parts[4];
let dla = parts[5];
let dle = parts[6];
let dlt = parts[7];
let nwl = parts[8] as usize;
let ne = parts[9] as usize;
let nt = parts[10] as usize;
let wl0 = result.wline[i - 1][j - 1];
result.ilin0[i - 1][j - 1] = iline + 1;
let mut table = HydProfileTable {
i,
j,
wl0,
nwl,
nt,
ne,
..Default::default()
};
// Generate wavelength grid
for iwl in 0..nwl.min(NWL_MAX) {
table.wl[iwl] = almin + (iwl as f64) * dla;
}
// Generate electron density grid
for ie in 0..ne.min(NE_MAX) {
table.xne[ie] = anemin + (ie as f64) * dle;
}
// Generate temperature grid
for it in 0..nt.min(NT_MAX) {
table.xt[it] = tmin + (it as f64) * dlt;
}
result.tables.push(table);
iline += 1;
}
// Read profile data for each line
for ili in 0..nlly {
let ilne = ilineb + ili;
let table = &mut result.tables[ilne];
let nwl = table.nwl;
let ne = table.ne;
let nt = table.nt;
lines.next(); // Skip blank line
for ie in 0..ne.min(NE_MAX) {
for it in 0..nt.min(NT_MAX) {
let prf_line = read_next_line(&mut lines)?;
let parts = parse_data_line(&prf_line)?;
// First value is QLT (quality factor), skip it
for iwl in 0..nwl.min(NWL_MAX) {
if iwl + 1 < parts.len() {
table.prf[iwl][it][ie] = parts[iwl + 1];
}
}
}
}
// Compute asymptotic profile coefficient
if nwl > 0 && ne > 0 {
let xclog = table.prf[nwl - 1][0][0]
+ 2.5 * table.wl[nwl - 1].log10()
+ 31.5304
- table.xne[0]
- 2.0 * table.wl0.log10();
let xklog = 0.6666667 * (xclog - 0.176);
table.xk = (xklog * std::f64::consts::LN_10).exp();
}
}
}
result.nlihyd = iline;
Ok(())
}
/// 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",
)),
}
}
}
/// Parse line indices from header: FORMAT(12X,I1,9X,I1)
fn parse_line_indices(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 line indices: {}", line),
));
}
let i = parts[0].parse::<usize>().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("I: {}", e))
})?;
let j = parts[1].parse::<usize>().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, format!("J: {}", e))
})?;
Ok((i, j))
}
/// Parse data line (free format)
fn parse_data_line(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_hydini_default() {
let result = HydInitResult::default();
assert_eq!(result.wline.len(), NLEVELS);
assert_eq!(result.ilin0.len(), NLEVELS);
assert!(result.tables.is_empty());
}
#[test]
fn test_hyd_profile_table_default() {
let table = HydProfileTable::default();
assert_eq!(table.nwl, 0);
assert_eq!(table.nt, 0);
assert_eq!(table.ne, 0);
}
#[test]
fn test_parse_line_indices() {
let line = " 1 2";
let result = parse_line_indices(line);
assert!(result.is_ok());
let (i, j) = result.unwrap();
assert_eq!(i, 1);
assert_eq!(j, 2);
}
#[test]
fn test_parse_data_line() {
let line = " 1.0 2.0 3.0 4.0";
let result = parse_data_line(line);
assert!(result.is_ok());
let values = result.unwrap();
assert_eq!(values.len(), 4);
assert!((values[0] - 1.0).abs() < 1e-10);
}
}