SpectraRust/src/tlusty/io/kurucz.rs
2026-04-01 16:35:36 +08:00

651 lines
18 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.

//! Kurucz ATLAS 格式模型读取。
//!
//! 重构自 TLUSTY `kurucz.f`。
//!
//! 从 unit 8 (fort.8) 读取 Kurucz ATLAS 格式的初始大气模型。
//! 支持 ATLAS9 和 ATLAS12 格式。
//!
//! # 功能
//!
//! - 解析 Kurucz ATLAS 模型文件格式
//! - 支持标准格式和 IFIXDE 固定格式
//! - 提取温度、密度、电子密度等物理量
use super::{IoError, Result};
use crate::tlusty::state::constants::*;
use std::io::BufRead;
// f2r_depends: LEVSOL, MOLEQ, QUIT, RATMAT, RHONEN, SABOLF, WNSTOR
/// Kurucz ATLAS format model reader wrapper (matches Fortran KURUCZ subroutine signature).
pub fn kurucz<R: BufRead>(ndpth: usize, reader: &mut R) -> Result<KuruczModel> {
let params = KuruczReadParams {
max_depth: ndpth,
..Default::default()
};
read_kurucz(&params, reader)
}
// ============================================================================
// 常量
// ============================================================================
/// 输入参数数量
const MINPUT: usize = 7;
// ============================================================================
// Kurucz 模型头部
// ============================================================================
/// Kurucz 模型头部信息
#[derive(Debug, Clone)]
pub struct KuruczHeader {
/// 有效温度 (K)
pub teff: f64,
/// 引力对数 (log g, cgs)
pub grav_log: f64,
/// 深度点数
pub nd: usize,
}
// ============================================================================
// Kurucz 深度点数据
// ============================================================================
/// 单个深度点的 Kurucz 数据(标准格式)
#[derive(Debug, Clone)]
pub struct KuruczDepthPoint {
/// 深度变量 (X1)
pub depth: f64,
/// 温度 (K)
pub temp: f64,
/// 粒子密度 × 玻尔兹曼常数 × 温度 (X3, 压力相关)
pub x3: f64,
/// 电子密度 (cm⁻³)
pub elec: f64,
}
/// 单个深度点的 Kurucz 数据IFIXDE 格式)
#[derive(Debug, Clone)]
pub struct KuruczIfixdeDepthPoint {
/// 质量深度 (g/cm²)
pub dm: f64,
/// 温度 (K)
pub temp: f64,
/// 气压 (dyn/cm²)
pub pressure: f64,
/// 中性氢密度 (cm⁻³)
pub ane0: f64,
/// 辅助量 1
pub a1: f64,
/// 辅助量 2
pub a2: f64,
/// 辅助量 3
pub a3: f64,
/// 速度场
pub vel: f64,
/// 质量密度 (g/cm³)
pub rho: f64,
}
// ============================================================================
// 输入参数
// ============================================================================
/// KURUCZ 读取参数。
pub struct KuruczReadParams {
/// 固定深度格式标志 (IFIXDE)
/// > 0: 使用固定格式读取
/// = 0: 使用标准 Kurucz 格式
pub ifixde: i32,
/// 最大深度点数 (用于数组大小检查)
pub max_depth: usize,
}
impl Default for KuruczReadParams {
fn default() -> Self {
Self {
ifixde: 0,
max_depth: MDEPTH,
}
}
}
// ============================================================================
// 输出结构
// ============================================================================
/// KURUCZ 模型读取输出。
#[derive(Debug, Clone)]
pub struct KuruczModel {
/// 深度点数
pub nd: usize,
/// 有效温度 (K)
pub teff: f64,
/// 引力对数 (log g)
pub grav_log: f64,
/// 深度点数据(标准格式)
pub depth_points: Vec<KuruczDepthPoint>,
/// 深度点数据IFIXDE 格式)
pub depth_points_ifixde: Vec<KuruczIfixdeDepthPoint>,
/// 插值标志
pub intrpl: i32,
/// 是否使用 IFIXDE 格式
pub is_ifixde: bool,
}
impl KuruczModel {
/// 获取温度数组
pub fn temperatures(&self) -> Vec<f64> {
if self.is_ifixde {
self.depth_points_ifixde.iter().map(|d| d.temp).collect()
} else {
self.depth_points.iter().map(|d| d.temp).collect()
}
}
/// 获取电子密度数组
pub fn electron_densities(&self) -> Vec<f64> {
if self.is_ifixde {
self.depth_points_ifixde.iter().map(|d| d.ane0).collect()
} else {
self.depth_points.iter().map(|d| d.elec).collect()
}
}
/// 获取质量密度数组
pub fn densities(&self) -> Vec<f64> {
if self.is_ifixde {
self.depth_points_ifixde.iter().map(|d| d.rho).collect()
} else {
// 标准格式需要从 X3 计算
self.depth_points.iter().map(|d| d.x3).collect()
}
}
}
// ============================================================================
// 辅助函数
// ============================================================================
/// 读取 Kurucz 头部信息 (标准格式)
///
/// FORMAT: A15, 6X, F8.5
fn read_kurucz_header_standard<R: BufRead>(reader: &mut R) -> Result<(String, f64)> {
let mut line = String::new();
reader.read_line(&mut line)?;
if line.len() < 22 {
return Err(IoError::ParseError(format!(
"Kurucz header line too short: {}",
line.len()
)));
}
// A15: 列 1-15 - KUR 字符串
let kur = line[0..15].trim().to_string();
// F8.5: 列 22-29 (跳过 6 个字符) - GRAVK
let gravk: f64 = if line.len() >= 29 {
line[21..29].trim().parse().unwrap_or(0.0)
} else {
0.0
};
Ok((kur, gravk))
}
/// 从 Kurucz 字符串解析 TEFF
///
/// FORMAT: 4X, F8.0
fn parse_teff_from_kur(kur: &str) -> Result<f64> {
// TEFF 在字符串中的位置: 'TEFF' 开头,后面是温度值
if !kur.starts_with("TEFF") {
return Err(IoError::ParseError(format!(
"Not a Kurucz model: expected 'TEFF', got '{}'",
kur
)));
}
// 4X, F8.0: 从第 5 个字符开始读取温度
if kur.len() < 12 {
return Err(IoError::ParseError(format!(
"Kurucz TEFF string too short: '{}'",
kur
)));
}
let teff_str = kur[4..12].trim();
teff_str
.parse()
.map_err(|e| IoError::ParseError(format!("Failed to parse TEFF: {} in '{}'", e, kur)))
}
/// 跳过直到找到 'READ DECK' 行
fn skip_to_read_deck<R: BufRead>(reader: &mut R) -> Result<i32> {
let mut kur = String::new();
loop {
kur.clear();
let bytes = reader.read_line(&mut kur)?;
if bytes == 0 {
return Err(IoError::UnexpectedEof);
}
if kur.trim().starts_with("READ DECK") {
// 解析深度点数: 10X, I3
let ndpth_str = if kur.len() >= 13 {
kur[10..13].trim()
} else {
"0"
};
let ndpth: i32 = ndpth_str.parse().unwrap_or(0);
return Ok(ndpth);
}
}
}
/// 读取固定格式深度数据头部 (IFIXDE > 0 路径)
///
/// FORMAT: 4X, F8.0, 9X, F8.5
fn read_ifixde_header<R: BufRead>(reader: &mut R) -> Result<(f64, f64)> {
let mut line = String::new();
reader.read_line(&mut line)?;
if line.len() < 26 {
return Err(IoError::ParseError(format!(
"IFIXDE header line too short: {}",
line.len()
)));
}
// F8.0: 列 5-12 - TEF
let tef: f64 = line[4..12].trim().parse().unwrap_or(0.0);
// F8.5: 列 22-29 - GRAV
let grav: f64 = if line.len() >= 29 {
line[21..29].trim().parse().unwrap_or(0.0)
} else {
0.0
};
Ok((tef, grav))
}
/// 跳过 19 行然后读取深度点数
///
/// FORMAT: ////////////////////10X,I3/ (19 个斜杠)
fn read_nd_after_skip<R: BufRead>(reader: &mut R) -> Result<i32> {
// 跳过 19 行
for _ in 0..19 {
let mut line = String::new();
reader.read_line(&mut line)?;
}
// 读取深度点数行: 10X, I3
let mut line = String::new();
reader.read_line(&mut line)?;
// 解析 I3
let nd: i32 = if line.len() >= 13 {
line[10..13].trim().parse().unwrap_or(0)
} else {
line.trim().parse().unwrap_or(0)
};
Ok(nd)
}
/// 读取自由格式深度数据 (IFIXDE 路径)
///
/// 自由格式: DM, TEMP, P, ANE0, A1, A2, A3, VEL, RHO
fn read_ifixde_depth<R: BufRead>(reader: &mut R) -> Result<KuruczIfixdeDepthPoint> {
let mut line = String::new();
reader.read_line(&mut line)?;
let values: Vec<f64> = line
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if values.len() < 9 {
return Err(IoError::ParseError(format!(
"Not enough values in IFIXDE depth line: expected 9, got {} in '{}'",
values.len(),
line
)));
}
Ok(KuruczIfixdeDepthPoint {
dm: values[0],
temp: values[1],
pressure: values[2],
ane0: values[3],
a1: values[4],
a2: values[5],
a3: values[6],
vel: values[7],
rho: values[8],
})
}
/// 读取标准格式深度数据
///
/// 自由格式: X(1)..X(7)
fn read_standard_depth<R: BufRead>(reader: &mut R) -> Result<KuruczDepthPoint> {
let mut line = String::new();
reader.read_line(&mut line)?;
let values: Vec<f64> = line
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if values.len() < MINPUT {
return Err(IoError::ParseError(format!(
"Not enough values in standard depth line: expected {}, got {} in '{}'",
MINPUT,
values.len(),
line
)));
}
Ok(KuruczDepthPoint {
depth: values[0],
temp: values[1],
x3: values[2],
elec: values[3],
})
}
// ============================================================================
// 主函数
// ============================================================================
/// 读取 Kurucz ATLAS 格式模型。
///
/// # 参数
/// - `params` - 读取参数
/// - `reader` - BufRead 读取器
///
/// # 返回
/// Kurucz 模型数据或错误
///
/// # Fortran 原始代码
/// ```fortran
/// SUBROUTINE KURUCZ(NDPTH)
/// ```
pub fn read_kurucz<R: BufRead>(
params: &KuruczReadParams,
reader: &mut R,
) -> Result<KuruczModel> {
if params.ifixde > 0 {
read_kurucz_ifixde(params, reader)
} else {
read_kurucz_standard(params, reader)
}
}
/// 读取标准 Kurucz 格式模型。
fn read_kurucz_standard<R: BufRead>(
params: &KuruczReadParams,
reader: &mut R,
) -> Result<KuruczModel> {
// 读取头部
let (kur, grav_log) = read_kurucz_header_standard(reader)?;
let teff = parse_teff_from_kur(&kur)?;
// 跳过直到 'READ DECK'
let ndpth = skip_to_read_deck(reader)?;
let nd = (ndpth - 1) as usize;
if nd > params.max_depth {
return Err(IoError::ParseError(format!(
"ND {} exceeds max depth {}",
nd, params.max_depth
)));
}
// 跳过一行 (TTT)
let mut dummy = String::new();
reader.read_line(&mut dummy)?;
// 读取每个深度点
let mut depth_points = Vec::with_capacity(nd);
for _ in 0..nd {
let dp = read_standard_depth(reader)?;
depth_points.push(dp);
}
// 查找 'BEGIN' 和插值标志
let mut intrpl = 0i32;
loop {
let mut line = String::new();
let bytes = reader.read_line(&mut line)?;
if bytes == 0 {
break;
}
if line.trim().starts_with("BEGIN") {
// 尝试读取插值标志
let mut intrpl_line = String::new();
if reader.read_line(&mut intrpl_line).is_ok() {
intrpl = intrpl_line.trim().parse().unwrap_or(0);
}
break;
}
}
Ok(KuruczModel {
nd,
teff,
grav_log,
depth_points,
depth_points_ifixde: Vec::new(),
intrpl,
is_ifixde: false,
})
}
/// 读取 IFIXDE 格式模型。
fn read_kurucz_ifixde<R: BufRead>(
params: &KuruczReadParams,
reader: &mut R,
) -> Result<KuruczModel> {
// 读取头部
let (teff, grav_log) = read_ifixde_header(reader)?;
// 读取深度点数
let nd_raw = read_nd_after_skip(reader)?;
let nd = (nd_raw - 1) as usize;
if nd > params.max_depth {
return Err(IoError::ParseError(format!(
"ND {} exceeds max depth {}",
nd, params.max_depth
)));
}
// 读取每个深度点
let mut depth_points = Vec::with_capacity(nd);
for _ in 0..nd {
let dp = read_ifixde_depth(reader)?;
depth_points.push(dp);
}
Ok(KuruczModel {
nd,
teff,
grav_log,
depth_points: Vec::new(),
depth_points_ifixde: depth_points,
intrpl: 0,
is_ifixde: true,
})
}
/// 从 FortranReader 读取 Kurucz 模型。
///
/// 注意:此函数会消耗 reader 的缓冲区。
pub fn read_kurucz_from_reader<R: BufRead>(
params: &KuruczReadParams,
reader: &mut R,
) -> Result<KuruczModel> {
read_kurucz(params, reader)
}
// ============================================================================
// 测试
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
use std::io::BufReader;
#[test]
fn test_parse_teff_from_kur() {
let kur = "TEFF 9750 ";
let teff = parse_teff_from_kur(kur).unwrap();
assert!((teff - 9750.0).abs() < 1.0);
let kur2 = "TEFF 35000 ";
let teff2 = parse_teff_from_kur(kur2).unwrap();
assert!((teff2 - 35000.0).abs() < 1.0);
}
#[test]
fn test_parse_teff_invalid() {
let kur = "NOTTEFF 123";
assert!(parse_teff_from_kur(kur).is_err());
}
#[test]
fn test_kurucz_depth_point() {
let dp = KuruczDepthPoint {
depth: 1.0e-5,
temp: 10000.0,
x3: 1.0e5,
elec: 1.0e12,
};
assert!((dp.depth - 1.0e-5).abs() < 1e-15);
assert!((dp.temp - 10000.0).abs() < 1e-10);
}
#[test]
fn test_kurucz_ifixde_depth_point() {
let dp = KuruczIfixdeDepthPoint {
dm: 1.0e-5,
temp: 10000.0,
pressure: 1.0e5,
ane0: 1.0e12,
a1: 0.0,
a2: 0.0,
a3: 0.0,
vel: 0.0,
rho: 1.0e-7,
};
assert!((dp.dm - 1.0e-5).abs() < 1e-15);
assert!((dp.temp - 10000.0).abs() < 1e-10);
assert!((dp.rho - 1.0e-7).abs() < 1e-15);
}
#[test]
fn test_read_standard_depth() {
let line = " 1.0E-05 10000.0 1.0E+05 1.0E+12 0.0 0.0 0.0\n";
let cursor = std::io::Cursor::new(line.as_bytes());
let mut reader = BufReader::new(cursor);
let dp = read_standard_depth(&mut reader).unwrap();
assert!((dp.depth - 1.0e-5).abs() < 1e-15);
assert!((dp.temp - 10000.0).abs() < 1e-10);
assert!((dp.x3 - 1.0e5).abs() < 1e-10);
assert!((dp.elec - 1.0e12).abs() < 1e-10);
}
#[test]
fn test_read_ifixde_depth() {
let line = " 1.0E-05 10000.0 1.0E+05 1.0E+12 0.0 0.0 0.0 0.0 1.0E-07\n";
let cursor = std::io::Cursor::new(line.as_bytes());
let mut reader = BufReader::new(cursor);
let dp = read_ifixde_depth(&mut reader).unwrap();
assert!((dp.dm - 1.0e-5).abs() < 1e-15);
assert!((dp.temp - 10000.0).abs() < 1e-10);
assert!((dp.pressure - 1.0e5).abs() < 1e-10);
assert!((dp.rho - 1.0e-7).abs() < 1e-15);
}
#[test]
fn test_kurucz_model() {
let model = KuruczModel {
nd: 3,
teff: 9750.0,
grav_log: 4.0,
depth_points: vec![
KuruczDepthPoint {
depth: 1.0e-5,
temp: 10000.0,
x3: 1.0e5,
elec: 1.0e12,
},
KuruczDepthPoint {
depth: 2.0e-5,
temp: 9000.0,
x3: 1.2e5,
elec: 8.0e11,
},
KuruczDepthPoint {
depth: 3.0e-5,
temp: 8000.0,
x3: 1.5e5,
elec: 6.0e11,
},
],
depth_points_ifixde: Vec::new(),
intrpl: 0,
is_ifixde: false,
};
assert_eq!(model.nd, 3);
let temps = model.temperatures();
assert_eq!(temps.len(), 3);
assert!((temps[0] - 10000.0).abs() < 1e-10);
}
#[test]
fn test_read_kurucz_header_standard() {
// 模拟 Kurucz 头部行
// FORMAT: A15, 6X, F8.5
// A15: 列 1-15, 6X: 列 16-21, F8.5: 列 22-29
// 需要至少 29 个字符
let header_line = "TEFF 9750 4.00000\n";
// 0123456789012345678901234567890
// |A15 |6X |F8.5 |
let cursor = std::io::Cursor::new(header_line.as_bytes());
let mut reader = BufReader::new(cursor);
let (kur, grav_log) = read_kurucz_header_standard(&mut reader).unwrap();
assert!(kur.starts_with("TEFF"));
assert!((grav_log - 4.0).abs() < 0.001);
}
#[test]
fn test_read_ifixde_header() {
// 模拟 IFIXDE 头部行
// FORMAT: 4X, F8.0, 9X, F8.5
// 4X: 列 1-4, F8.0: 列 5-12, 9X: 列 13-21, F8.5: 列 22-29
let header_line = " 9750. 4.00000\n";
// 0123456789012345678901234567890
// |4X |F8.0 |9X |F8.5 |
let cursor = std::io::Cursor::new(header_line.as_bytes());
let mut reader = BufReader::new(cursor);
let (teff, grav_log) = read_ifixde_header(&mut reader).unwrap();
assert!((teff - 9750.0).abs() < 1.0);
assert!((grav_log - 4.0).abs() < 0.001);
}
}