//! Opacity and emissivity calculation for SYNSPEC. //! //! Translated from SYNSPEC `OPAC` subroutine (synspec54.f:4541). //! //! Calculates absorption, emission, and scattering coefficients //! at a given depth for multiple frequencies. //! //! This routine calls downstream functions for line opacity (LINOP), //! molecular opacity (MOLOP), hydrogen lines (HYDLIN), He II lines (HE2LIN), //! and photoionization (PHTION, PHTX). use crate::synspec::math::{ gfree, he2lin, hydlin, linop, lymlin, molop, phtion, phtx, He2linParams, HydlinParams, LinopParams, LymlinParams, MolLineData, MolopConfig, MolopFreqParams, MolopModelState, MolopOutput, PhtionParams, PhtionOutput, PhtxParams, PhtxOutput, }; use crate::synspec::math::voigtk::MVOI; use crate::tlusty::math::hydrogen::sffhmi; /// Physical constants const UN: f64 = 1.0; const TEN15: f64 = 1.0e-15; const CSB: f64 = 2.0706e-16; const CFF: f64 = 3.694e8; /// Line opacity data for LINOP call. /// /// Corresponds to Fortran COMMON/LINDAT/ data needed by OPAC → LINOP. pub struct OpacLinopData<'a> { /// Temperature at depth ID pub temp: f64, /// Total frequency count (NFREQS) pub nfreqs: usize, /// Frequency spacing factor (DFRCON) pub dfrcon: f64, /// Planck function at depth ID pub plan: f64, /// Stimulated emission correction pub stim: f64, /// Number of lines pub nlin: usize, /// Line data array pub lines: &'a [crate::synspec::math::LineData], /// RRR(ID,ION,IAT) — Saha/Boltzmann factors pub rrr: &'a [f64], /// RRR dimensions: (natom, nion) pub rrr_dims: (usize, usize), /// Doppler width DOPA1(IAT, ID) pub dopa1: &'a [f64], /// DOPA1 number of atoms pub dopa1_nat: usize, /// Level statistical weights G(level) pub g: &'a [f64], /// Level populations POPUL(level, ID) pub popul: &'a [f64], /// POPUL number of levels pub popul_nlev: usize, /// NLTE populations PNLT(IAT, ION, ID) pub pnlt: &'a [f64], /// PNLT dimensions: (natom, nion) pub pnlt_dims: (usize, usize), /// Ionization energies ENEV(IAT, ION) pub enev: &'a [f64], /// ENEV number of atoms pub enev_nat: usize, /// Level energies ENION(level) pub enion: &'a [f64], /// Laser deletion flag pub lasdel: bool, /// He II special line count pub nsp: usize, /// He II special line indices pub isp0: &'a [usize], /// Voigt table H0 pub h0tab: &'a [f64; MVOI], /// Voigt table H1 pub h1tab: &'a [f64; MVOI], /// Voigt table H2 pub h2tab: &'a [f64; MVOI], } /// Hydrogen line data for HYDLIN call. pub struct OpacHydlinData { /// Lower level for H lines pub ilowh: i32, /// Upper level limits pub m10: usize, pub m20: usize, /// Turbulent velocity (cm/s) pub vturb: f64, /// wnHint factors pub wn_hint: Vec>, } /// He II line data for HE2LIN call. pub struct OpacHe2linData { /// He II common data pub common: crate::synspec::math::He2Common<'static>, /// He II lowest series index pub ilwhe2: usize, /// Max principal quantum number pub mhe10: usize, /// Upper limit for He II lines pub mhe20: usize, } /// Molecular opacity data for MOLOP call. pub struct OpacMolopData<'a> { /// MOLOP configuration pub config: &'a MolopConfig, /// Molecular line data pub line_data: &'a MolLineData<'a>, /// Voigt tables (H0, H1, H2) pub voigt_tables: (&'a [f64; MVOI], &'a [f64; MVOI], &'a [f64; MVOI]), } /// Photoionization data for PHTION call. pub struct OpacPhtionData<'a> { /// Photoionization cross-section data pub photcs: &'a crate::synspec::math::PhotcsData, /// Atomic number density RRR pub rrr: &'a [f64], /// Level populations POPUL pub popul: &'a [f64], } /// Extended photoionization data for PHTX call. pub struct OpacPhtxData<'a> { /// Level photoionization data pub level_photo: &'a crate::synspec::math::LevelPhotoData, /// H/He photoionization data pub hhe_photo: &'a crate::synspec::math::HhePhotoData, /// Level populations POPUL pub popul: &'a [f64], /// Atomic number density RRR pub rrr: &'a [f64], /// Total frequency count pub nfreq_total: usize, /// Continuous frequency count pub nfreqc: usize, /// IASV flag pub iasv: i32, /// NQHT flag pub nqht: i32, /// PHTX state for interpolation caching pub state: &'a mut crate::synspec::math::PhtxState, } /// Parameters for opacity calculation. /// /// Includes optional data for downstream functions (LINOP, MOLOP, HYDLIN, /// HE2LIN, PHTION, PHTX). When `None`, the corresponding call is skipped. pub struct OpacParams<'a> { /// Depth index pub id: usize, /// Temperature at depth ID (K) pub t: f64, /// Electron density at depth ID pub ane: f64, /// Number of frequencies pub nfreq: usize, /// Frequency array (Hz) pub freq: Vec, /// Wavelength array (Å) pub wlam: Vec, /// Frequency interpolation weights pub frx1: Vec, pub frx2: Vec, /// Mode flag pub imode: i32, /// Standard depth index pub idstd: usize, /// Continuum opacity switch pub icontl: i32, /// Lyman line treatment switch pub iophli: i32, /// Hydrogen line mode (0=interpolated, >0=detailed) pub ihyll: i32, /// He II line switch pub ihe2l: i32, /// Molecular line switch pub ifmol: i32, /// Number of molecular lists pub nmlist: usize, /// H atom index pub iath: i32, /// H ground level population pub pop_h: f64, /// H continuum level population pub pop_h_cont: f64, /// Electron scattering coefficient pub sce: f64, /// Planck function at depth ID pub plan: f64, /// H/kT pub hkt: f64, /// h/k pub hk: f64, /// Boltzmann constant * temperature pub bn: f64, /// wnHint factors for Lyman lines pub wn_hint: [f64; 5], /// RELOP factor for AVAB calculation pub relop: f64, // --- Downstream function data (optional) --- /// Line opacity data for LINOP pub linop_data: Option>, /// Hydrogen line data for HYDLIN pub hydlin_data: Option, /// He II line data for HE2LIN pub he2lin_data: Option, /// Molecular opacity data for MOLOP pub molop_data: Option>, /// Photoionization data for PHTION pub phtion_data: Option>, /// Extended photoionization data for PHTX pub phtx_data: Option>, } /// Result of opacity calculation pub struct OpacResult { /// Absorption coefficient array pub abso: Vec, /// Emission coefficient array pub emis: Vec, /// Scattering coefficient array pub scat: Vec, /// Average absorption for line selection pub avab: f64, } /// Calculate opacity and emissivity. /// /// This is the main opacity calculation routine for SYNSPEC. /// It computes absorption, emission, and scattering coefficients /// including continuum, line, and molecular contributions. /// /// # Arguments /// * `params` - Input parameters (including optional downstream data) /// /// # Returns /// Opacity coefficients for all frequencies pub fn opac(params: &mut OpacParams) -> OpacResult { let nfreq = params.nfreq; let mut abso = vec![0.0; nfreq]; let mut emis = vec![0.0; nfreq]; let mut scat = vec![0.0; nfreq]; // Skip if not needed if params.imode == -1 && params.id != params.idstd { return OpacResult { abso, emis, scat, avab: 0.0, }; } let t = params.t; let ane = params.ane; let t1 = UN / t; let hkt = params.hkt; let _tk = hkt / params.hk; let srt = UN / t.sqrt(); let sgff = CFF * srt; let con = CSB * t1 * srt; let conts = 1.0e-36 / con; // Continuum opacity (first and last frequency) let ij0 = if nfreq == 1 { 1 } else if params.imode == 2 { nfreq } else { 2 }; let mut ably1 = 0.0; let mut emly1 = 0.0; let mut scly1 = 0.0; for ij in 0..ij0 { let fr = params.freq[ij]; let fr15 = fr * TEN15; let bnu = params.bn * fr15 * fr15 * fr15; let hkf = hkt * fr; // Bound-free and free-free continuum let mut abf = 0.0; let mut ebf = 0.0; let mut aff = 0.0; // Free-free Gaunt factor contribution let gfree_val = gfree(t, fr); let sf1 = sgff / (fr * fr * fr); let hktm = hkt * fr.min(fr); let sf2 = hktm.exp() + gfree_val - UN; let sff = sf1 * sf2; aff += sff; // H- opacity let sffhmi_val = sffhmi(params.pop_h, fr, t); aff += sffhmi_val; // Additional opacities (OPADD placeholder) let abad = 0.0; let emad = 0.0; let scad = 0.0; // Lyman line wings let (ably_ij, emly_ij, scly_ij) = if params.iophli != 0 { let lymlin_params = LymlinParams { id: params.id, freq: fr, pop_h: params.pop_h, pop_excited: [params.pop_h_cont; 4], t, ane, iath: params.iath, iophli: params.iophli, wn_hint: params.wn_hint, }; let result = lymlin(&lymlin_params); (result.ably, result.emly, result.scly) } else { (0.0, 0.0, 0.0) }; // Total opacity and emissivity let x = (-hkf).exp(); let x1 = UN - x; let bne = bnu * x * ane; abso[ij] = abf + ane * (x1 * aff - x * ebf) + abad; emis[ij] = bne * (aff + ebf) + emad + emly_ij; scat[ij] = scad + scly_ij + params.sce; if ij == 0 { ably1 = ably_ij; emly1 = emly_ij; scly1 = scly_ij; } } let avab = (abso[0] + abso[1] + scat[0] + scat[1]) * 0.5 * params.relop; if nfreq <= 2 || params.imode == -1 { return OpacResult { abso, emis, scat, avab, }; } // Determine M (start index for line accumulation) let m: usize = if params.icontl == 1 { 1 } else { 3 }; if params.imode != 2 { // Interpolated continuum for all frequencies for ij in 2..nfreq { abso[ij] = params.frx1[ij] * abso[1] + params.frx2[ij] * abso[0]; emis[ij] = params.frx1[ij] * emis[1] + params.frx2[ij] * emis[0]; scat[ij] = params.frx1[ij] * scat[1] + params.frx2[ij] * scat[0]; } // Hydrogen lines for IHYL=0 (interpolated mode) if params.ihyll == 0 { if let Some(ref hyd_data) = params.hydlin_data { let hyd_params = HydlinParams { id: params.id, i0: 0, i1: 1, nfreq, wlam: params.wlam.clone(), freq: params.freq.clone(), t, ane, iath: params.iath, ilowh: hyd_data.ilowh, m10: hyd_data.m10, m20: hyd_data.m20, pop_h: params.pop_h, pop_h_cont: params.pop_h_cont, vturb: hyd_data.vturb, wn_hint: hyd_data.wn_hint.clone(), }; let result = hydlin(&hyd_params); for ij in m..nfreq { abso[ij] += params.frx1[ij] * result.absoh[1] + params.frx2[ij] * result.absoh[0]; emis[ij] += params.frx1[ij] * result.emish[1] + params.frx2[ij] * result.emish[0]; } } } // Line opacity (LINOP) if let Some(ref linop_d) = params.linop_data { let linop_params = LinopParams { id: params.id, temp: linop_d.temp, nfreq, freq: ¶ms.freq, nfreqs: linop_d.nfreqs, dfrcon: linop_d.dfrcon, avab, plan: linop_d.plan, stim: linop_d.stim, nlin: linop_d.nlin, lines: linop_d.lines, rrr: linop_d.rrr, rrr_dims: linop_d.rrr_dims, dopa1: linop_d.dopa1, dopa1_nat: linop_d.dopa1_nat, g: linop_d.g, popul: linop_d.popul, popul_nlev: linop_d.popul_nlev, pnlt: linop_d.pnlt, pnlt_dims: linop_d.pnlt_dims, enev: linop_d.enev, enev_nat: linop_d.enev_nat, enion: linop_d.enion, lasdel: linop_d.lasdel, nsp: linop_d.nsp, isp0: linop_d.isp0, h0tab: linop_d.h0tab, h1tab: linop_d.h1tab, h2tab: linop_d.h2tab, phe1_data: None, phe2_common: None, }; let result = linop(&linop_params); for ij in 3..nfreq { abso[ij] += result.ablin[ij]; emis[ij] += result.emlin[ij]; } } // Molecular line opacity (MOLOP) if params.ifmol > 0 { if let Some(ref mol_d) = params.molop_data { for ilist in 0..params.nmlist { let mol_params = MolopFreqParams { nfreq, nfreqs: nfreq, freq: ¶ms.freq, }; let result = molop( mol_d.config, &MolopModelState { temp: t, elec: ane, stim: 1.0, plan: params.plan, mol: crate::synspec::math::MolModelState { rrmol: &[], dopmol: &[], }, }, mol_d.line_data, &mol_params, avab, mol_d.voigt_tables, ); for ij in 3..nfreq { abso[ij] += result.ablin[ij]; emis[ij] += result.emlin[ij]; } let _ = ilist; // suppress unused warning } } } } // Detailed hydrogen line opacity (IHYL>0 or IMODE=2) if params.ihyll > 0 || params.imode == 2 { if let Some(ref hyd_data) = params.hydlin_data { let hyd_params = HydlinParams { id: params.id, i0: m.saturating_sub(1), i1: nfreq.saturating_sub(1), nfreq, wlam: params.wlam.clone(), freq: params.freq.clone(), t, ane, iath: params.iath, ilowh: hyd_data.ilowh, m10: hyd_data.m10, m20: hyd_data.m20, pop_h: params.pop_h, pop_h_cont: params.pop_h_cont, vturb: hyd_data.vturb, wn_hint: hyd_data.wn_hint.clone(), }; let result = hydlin(&hyd_params); for ij in m..nfreq { abso[ij] += result.absoh[ij]; emis[ij] += result.emish[ij]; } } } // Detailed He II line opacity (IHE2L>0) if params.ihe2l > 0 { if let Some(ref he2_d) = params.he2lin_data { let he2_params = He2linParams { common: he2_d.common.clone(), i0: m.saturating_sub(1), i1: nfreq.saturating_sub(1), ilwhe2: he2_d.ilwhe2, mhe10: he2_d.mhe10, mhe20: he2_d.mhe20, }; let result = he2lin(&he2_params); for ij in m..nfreq { abso[ij] += result.absoh[ij]; emis[ij] += result.emish[ij]; } } } // Photoionization opacity (PHTION) if let Some(ref phtion_d) = params.phtion_data { let mut phtion_out = PhtionOutput { abso: &mut abso, emis: &mut emis, }; let phtion_params = PhtionParams { temp: t, fre: ¶ms.freq, nfre: nfreq, rrr: phtion_d.rrr, popul: phtion_d.popul, photcs: phtion_d.photcs, }; phtion(&phtion_params, &mut phtion_out); } // Photoionization opacity extended (PHTX) if let Some(ref mut phtx_d) = params.phtx_data { let mut phtx_out = PhtxOutput { abso: &mut abso, emis: &mut emis, }; let phtx_params = PhtxParams { temp: t, depth_id: params.id, fre: ¶ms.freq, nfre: nfreq, nfreq, nfreqc: phtx_d.nfreqc, icon: 0, iasv: phtx_d.iasv, nqht: phtx_d.nqht, popul: phtx_d.popul, rrr: phtx_d.rrr, level_photo: phtx_d.level_photo, hhe_photo: phtx_d.hhe_photo, }; phtx(&phtx_params, &mut phtx_out, phtx_d.state); } // Add scattering to absorption for positive imode if params.imode >= 0 { for ij in 0..nfreq { abso[ij] += scat[ij]; } } // Correct for Lyman line wings if params.icontl != 1 { abso[0] -= ably1; emis[0] -= emly1; scat[0] -= scly1; } OpacResult { abso, emis, scat, avab, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_opac_basic() { let mut params = OpacParams { id: 0, t: 6000.0, ane: 1.0e13, nfreq: 5, freq: vec![1.0e14, 2.0e14, 3.0e14, 4.0e14, 5.0e14], wlam: vec![30000.0, 15000.0, 10000.0, 7500.0, 6000.0], frx1: vec![0.0, 0.0, 0.5, 0.5, 0.5], frx2: vec![0.0, 0.0, 0.5, 0.5, 0.5], imode: 0, idstd: 0, icontl: 0, iophli: 0, ihyll: 0, ihe2l: 0, ifmol: 0, nmlist: 0, iath: 1, pop_h: 1.0e16, pop_h_cont: 1.0e10, sce: 1.0e-3, plan: 1.0, hkt: 4.79928144e-11 / 6000.0, hk: 4.79928144e-11, bn: 1.0, wn_hint: [1.0, 1.0, 1.0, 1.0, 1.0], relop: 1.0, linop_data: None, hydlin_data: None, he2lin_data: None, molop_data: None, phtion_data: None, phtx_data: None, }; let result = opac(&mut params); assert!(result.abso.iter().all(|&x| x.is_finite())); assert!(result.emis.iter().all(|&x| x.is_finite())); assert!(result.scat.iter().all(|&x| x.is_finite())); assert!(result.avab.is_finite()); } }