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

203 lines
6.0 KiB
Rust

//! Output of synthetic spectrum.
//!
//! Translated from SYNSPEC `OUTPRI` subroutine (synspec54.f:3343).
//!
//! Outputs synthetic spectrum to file units, computes equivalent widths.
/// Physical constants
const CAS: f64 = 1.0 / 2.997925e18;
const EQWC: f64 = 1.19917e22;
/// Parameters for OUTPRI calculation
pub struct OutpriParams {
/// Number of frequencies
pub nfreq: usize,
/// Frequency array (Hz)
pub freq: Vec<f64>,
/// Wavelength array (Angstroms)
pub wlam: Vec<f64>,
/// Emergent flux at each frequency [nfreq]
pub flux: Vec<f64>,
/// Frequency weights for equivalent width [nfreq]
pub w: Vec<f64>,
/// Print level
pub iprin: i32,
/// Blank flag
pub iblank: i32,
/// Number of blanks
pub nblank: i32,
/// Window mode flag
pub ifwin: i32,
/// Number of observed frequencies (window mode)
pub nfrobs: usize,
/// Observed frequencies (window mode)
pub frqobs: Vec<f64>,
/// Observed wavelengths (window mode)
pub wlobs: Vec<f64>,
}
/// Result of OUTPRI calculation
pub struct OutpriResult {
/// Equivalent width for this set (mA)
pub eqw: f64,
/// Positive equivalent width for this set (mA)
pub eqwp: f64,
/// Total equivalent width (mA)
pub eqwt: f64,
/// Total positive equivalent width (mA)
pub eqwtp: f64,
/// Spectrum data: (wavelength, flux_lambda) pairs
pub spectrum: Vec<(f64, f64)>,
/// Continuum data: (wavelength, flux_lambda) pairs
pub continuum: Vec<(f64, f64)>,
}
/// Output of synthetic spectrum.
///
/// Computes emergent flux in lambda units, writes spectrum and continuum,
/// and calculates equivalent widths.
pub fn outpri(params: &OutpriParams, eqwt_in: f64, eqwtp_in: f64) -> OutpriResult {
let nfreq = params.nfreq;
let freq = &params.freq;
let wlam = &params.wlam;
let flux = &params.flux;
let w = &params.w;
let mut spectrum = Vec::new();
let mut continuum = Vec::new();
let mut eqw = 0.0;
let mut eqwp = 0.0;
let mut eqwt = eqwt_in;
let mut eqwtp = eqwtp_in;
if params.ifwin <= 0 {
// Output synthetic spectrum
for ij in 2..nfreq - 1 {
let flam = flux[ij] * freq[ij] * freq[ij] * CAS;
spectrum.push((wlam[ij], flam));
}
// Output continuum flux
let flam = flux[0] * freq[0] * freq[0] * CAS;
continuum.push((wlam[0], flam));
if params.iblank == params.nblank {
let flam = flux[nfreq - 1] * freq[nfreq - 1] * freq[nfreq - 1] * CAS;
spectrum.push((wlam[nfreq - 1], flam));
let flam = flux[1] * freq[1] * freq[1] * CAS;
continuum.push((wlam[1], flam));
}
} else {
// Window mode
for ij in 0..params.nfrobs {
let flam = flux[ij] * params.frqobs[ij] * params.frqobs[ij] * CAS * 0.5;
let flam = flam.max(1.0e-40);
spectrum.push((params.wlobs[ij], flam));
}
}
// Compute equivalent widths
if params.iprin >= 3 {
let xx = 1.0 / (freq[1] - freq[0]);
let xxx = 1.0 / (freq[0] + freq[1]) / (freq[0] + freq[1]);
if params.ifwin <= 0 {
for ij in 0..nfreq {
let _flam = flux[ij] * freq[ij] * freq[ij] * CAS;
let cont = ((freq[ij] - freq[0]) * flux[1]
+ (freq[1] - freq[ij]) * flux[0])
* xx;
let re0 = flux[ij] / cont;
eqw += (1.0 - re0) * w[ij];
let rep = re0.min(1.0);
eqwp += (1.0 - rep) * w[ij];
}
} else {
for ij in 0..params.nfrobs {
let _flam = flux[ij] * freq[ij] * freq[ij] * CAS;
let cont = ((params.frqobs[ij] - freq[0]) * flux[1]
+ (freq[1] - params.frqobs[ij]) * flux[0])
* xx;
let re0 = flux[ij] / cont;
eqw += (1.0 - re0) * w[ij];
let rep = re0.min(1.0);
eqwp += (1.0 - rep) * w[ij];
}
}
eqw = eqw * EQWC * xxx;
eqwt += eqw;
eqwp = eqwp * EQWC * xxx;
eqwtp += eqwp;
}
OutpriResult {
eqw,
eqwp,
eqwt,
eqwtp,
spectrum,
continuum,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outpri_basic() {
let nfreq = 5;
let params = OutpriParams {
nfreq,
freq: vec![1.0e15, 2.0e15, 3.0e15, 4.0e15, 5.0e15],
wlam: vec![3000.0, 2000.0, 1500.0, 1200.0, 1000.0],
flux: vec![1.0e-10, 2.0e-10, 3.0e-10, 4.0e-10, 5.0e-10],
w: vec![0.1, 0.2, 0.3, 0.4, 0.5],
iprin: 0,
iblank: 1,
nblank: 1,
ifwin: 0,
nfrobs: 0,
frqobs: vec![],
wlobs: vec![],
};
let result = outpri(&params, 0.0, 0.0);
// When iblank == nblank, extra points are added:
// spectrum: ij=2,3 from loop + nfreq-1=4 from iblank==nblank
assert_eq!(result.spectrum.len(), 3);
// continuum: ij=0 from main path + ij=1 from iblank==nblank
assert_eq!(result.continuum.len(), 2);
}
#[test]
fn test_outpri_flux_conversion() {
// Verify FLAM = FLUX * FREQ^2 * CAS
let flux_val = 1.0e-10;
let freq_val = 3.0e15;
let expected_flam = flux_val * freq_val * freq_val * CAS;
let nfreq = 3;
let params = OutpriParams {
nfreq,
freq: vec![1.0e15, 2.0e15, 3.0e15],
wlam: vec![3000.0, 2000.0, 1500.0],
flux: vec![flux_val, flux_val, flux_val],
w: vec![0.1, 0.2, 0.3],
iprin: 0,
iblank: 1,
nblank: 1,
ifwin: 0,
nfrobs: 0,
frqobs: vec![],
wlobs: vec![],
};
let result = outpri(&params, 0.0, 0.0);
// Check that flux conversion is correct
if let Some((_, flam)) = result.spectrum.first() {
assert!((flam - expected_flam).abs() / expected_flam < 1.0e-10);
}
}
}