//! Output of selected molecular line parameters (identification table). //! //! Translated from SYNSPEC54.FOR subroutine IDMTAB (line 16380). //! //! Computes and formats molecular line parameters for the identification //! table output, including equivalent widths and line strengths. use crate::synspec::math::inibla::{CL, BOLK}; // ============================================================================ // Constants // ============================================================================ /// Conversion factor: ln(10) for log-gf const C1: f64 = 2.302_585_1; /// Conversion factor: gf offset const C2: f64 = 4.201_467_2; /// Conversion factor: energy to temperature const C3: f64 = 1.438_788_6; /// Strength category labels const APB: &str = " "; const AP0: &str = " ."; const AP1: &str = " *"; const AP2: &str = " **"; const AP3: &str = " ***"; const AP4: &str = "****"; // ============================================================================ // IDMTAB parameters // ============================================================================ /// Parameters for a single molecular line. pub struct IdmtabLine { /// Wavelength (Å) pub alam: f64, /// Molecule index pub imol: usize, /// Lower excitation potential (cm⁻¹) pub excl: f64, /// Log gf value pub gfm: f64, /// Van der Waals broadening pub grm: f64, /// Stark broadening pub gsm: f64, /// Van der Waals broadening (depth-dependent) pub gvdw: f64, /// Doppler width pub dop1: f64, /// Continuum opacity at line center pub absta: f64, /// Stimulated emission factor pub stim: f64, /// Molecular population ratio pub rrmol: f64, /// Temperature at standard depth (K) pub temp: f64, /// Electron density at standard depth pub elec: f64, } /// Result of IDMTAB computation for a single line. pub struct IdmtabResult { /// Wavelength (Å) pub alam: f64, /// Molecule name pub molecule: String, /// Log gf pub gf: f64, /// Lower excitation energy (K) pub excl_k: f64, /// Line-to-continuum ratio (STR0) pub str0: f64, /// Equivalent width (mÅ) pub eqw: f64, /// Strength category label pub apr: &'static str, /// Depth index pub id: usize, /// Total broadening parameter pub agam: f64, } // ============================================================================ // IDMTAB implementation // ============================================================================ /// Compute molecular line parameters for the identification table. /// /// For a given molecular line, computes the line strength, equivalent width, /// and strength category. /// /// # Fortran original /// /// ```fortran /// SUBROUTINE IDMTAB /// DO IL0=1,NLINML /// ...compute STR0, EQW, APR... /// END DO /// END /// ``` pub fn idmtab_compute(line: &IdmtabLine) -> IdmtabResult { let IdmtabLine { alam, imol: _, excl, gfm, grm, gsm, gvdw, dop1, absta, stim, rrmol, temp, elec, } = *line; // Total broadening parameter // Fortran: AGAM=(GRM+GSM*ANE+GVDW)*DOP1 let agam = (grm + gsm * elec + gvdw) * dop1; // Absorption at line center // Fortran: ABCNT=EXP(GFM-EXCL/TEMP)*RRMOL*DOP1*STIM let abcnt = (gfm - excl / temp).exp() * rrmol * dop1 * stim; // Line-to-continuum ratio // Fortran: STR0=ABCNT/ABSTA let str0 = if absta > 0.0 { abcnt / absta } else { 0.0 }; // Log gf let gf = (gfm + C2) / C1; // Lower excitation energy in K let excl_k = excl / C3; // Equivalent width estimate let ww1 = if str0 <= 1.2 { 0.886 * str0 * (1.0 - str0 * (0.707 - str0 * 0.577)) } else { str0.ln().sqrt() }; let ww1 = if str0 > 55.0 { let ww2 = 0.5 * (std::f64::consts::PI * agam * str0).sqrt(); if ww2 > ww1 { ww2 } else { ww1 } } else { ww1 }; // Equivalent width in mÅ // Fortran: EQW=ALAM/FREQ0*1.E3/DOP1*WW1 // Since ALAM is wavelength and we don't have FREQ0 directly, // we use the relation: EQW ≈ ALAM * WW1 / (c/ALAM) / DOP1 * 1e3 // Simplified: EQW = ALAM^2 / CL * 1e3 / DOP1 * WW1 let eqw = alam * alam / CL * 1e3 / dop1 * ww1; // Strength category let str = eqw * 10.0; let apr = if str >= 1e4 { AP4 } else if str >= 1e3 { AP3 } else if str >= 1e2 { AP2 } else if str >= 1e1 { AP1 } else if str >= 1e0 { AP0 } else { APB }; IdmtabResult { alam, molecule: String::new(), // Filled by caller gf, excl_k, str0, eqw, apr, id: 0, // Filled by caller agam, } } #[cfg(test)] mod tests { use super::*; fn create_test_line() -> IdmtabLine { IdmtabLine { alam: 5000.0, imol: 1, excl: 10000.0, gfm: -2.0, grm: 0.1, gsm: 0.01, gvdw: 0.05, dop1: 0.01, absta: 1e-10, stim: 1.0, rrmol: 1e-5, temp: 10000.0, elec: 1e14, } } #[test] fn test_idmtab_basic() { let line = create_test_line(); let result = idmtab_compute(&line); assert!(result.alam > 0.0); assert!(result.str0.is_finite()); assert!(result.eqw.is_finite()); assert!(result.eqw >= 0.0); assert!(result.agam.is_finite()); } #[test] fn test_idmtab_weak_line() { let mut line = create_test_line(); line.rrmol = 1e-20; // Very weak line let result = idmtab_compute(&line); // Weak line should have small STR0 assert!(result.str0 < 1.0); assert_eq!(result.apr, APB); } #[test] fn test_idmtab_strong_line() { let mut line = create_test_line(); line.rrmol = 1e10; // Very strong line line.absta = 1e-20; let result = idmtab_compute(&line); // Strong line should have large STR0 assert!(result.str0 > 1.0); } #[test] fn test_idmtab_strength_categories() { // Test that different strength values produce correct categories let mut line = create_test_line(); // Weak line line.rrmol = 1e-15; let r = idmtab_compute(&line); if r.str0 <= 1.2 { // For weak lines, EQW is small assert!(r.eqw < 1.0 || r.apr == APB); } } }