diff --git a/doc/arts/concept.absorption.lbl.rst b/doc/arts/concept.absorption.lbl.rst index 1dd247bb38..33d6ab61a8 100644 --- a/doc/arts/concept.absorption.lbl.rst +++ b/doc/arts/concept.absorption.lbl.rst @@ -324,6 +324,38 @@ where :math:`\rho` is the number density of the absorbing species, where VMR is the volume-mixing ratio of the absorbing species. +.. list-table:: + :header-rows: 1 + + * - Parameter + - Description + * - :math:`\rho` + - Number density of the absorbing isotopologue, :math:`\mathrm{VMR} \cdot P / (kT)` + * - :math:`\mathrm{VMR}` + - Volume-mixing ratio of the absorbing species + * - :math:`P` + - Atmospheric pressure + * - :math:`c` + - Speed of light + * - :math:`\nu` + - Sampling frequency + * - :math:`\nu_0` + - Line centre frequency + * - :math:`h` + - Planck constant + * - :math:`k` + - Boltzmann constant + * - :math:`T` + - Temperature + * - :math:`g_u` + - Degeneracy of the upper state + * - :math:`E_l` + - Energy of the lower state + * - :math:`Q(T)` + - Partition function at temperature :math:`T` + * - :math:`A_{lu}` + - Einstein A coefficient for spontaneous emission + .. _lbl-nlte: Non-local thermodynamic equilibrium @@ -404,7 +436,7 @@ Zeeman effect ------------- If Zeeman effect is considered, the emission and absorption terms above are modified by quantum number state distribution. -For O :sub:`2`, for example, this introduces a factor of +For O\ :sub:`2`, for example, this introduces a factor of .. math:: @@ -422,4 +454,615 @@ It can be `computed using software `_ s Line-mixing using Error-corrected Sudden **************************************** -TBD +When the atmosphere is at sufficient pressure, collisions occur frequently enough that +absorption lines of a vibrational-rotational band can no longer be treated +independently. Molecules undergoing collisions may exchange rotational angular +momentum, transferring population between rotational levels. At intermediate +pressures this introduces off-diagonal couplings between lines in a spectral band, +leading to the phenomenon of *line mixing*. At high pressures the lines collapse +towards a pressure-broadened Q-branch. + +The Error-corrected Sudden (ECS) approximation provides a rigorous, quantum-mechanical +framework for computing this mixing. The "Sudden" part refers to the +Infinite-Order-Sudden (IOS) approximation, in which the collision time is assumed short +compared with the rotational period. The "Error-corrected" part refers to the +subsequent rescaling of the relaxation matrix elements to satisfy the first-order +optical sum rule exactly, removing a systematic bias that arises from the sudden +approximation. + +.. _lbl-ecs-lineshape: + +ECS Line Shape +============== + +For a band of :math:`n` interacting absorption lines, the ECS complex absorption shape +for a single broadening species (see :ref:`lbl-ecs-multispecies` for the full +expression) is written in terms of the complex relaxation matrix :math:`\mathbf{W}` as + +.. math:: + + \chi(\nu) \propto \mathrm{Im}\left[\mathbf{d}^T \left(\nu \mathbf{I} - \mathbf{W}\right)^{-1} \mathbf{p}\, \mathbf{d}\right], + +where :math:`\mathbf{d}` is the vector of reduced dipole matrix elements (one entry per +line), :math:`\mathbf{p}` is the diagonal matrix of lower-state thermal populations +(:math:`p_j = g_j\exp(-E_l^{(j)}/kT)/Q(T)`, the Boltzmann fractional population of +the lower state of line :math:`j`, following the :ref:`lbl-lte` notation where +:math:`g_j` is the lower-state degeneracy and :math:`Q(T)` the partition function), +and :math:`\mathbf{W}` is the full complex :math:`n \times n` relaxation matrix whose +diagonal and off-diagonal elements are described in :ref:`lbl-ecs-relaxmat`. + +The relaxation matrix is diagonalised as :math:`\mathbf{W} = \mathbf{V} \tilde{\boldsymbol{\nu}} \mathbf{V}^{-1}`, +where :math:`\tilde{\boldsymbol{\nu}}` is the diagonal matrix of complex +*equivalent line* positions. Each equivalent line :math:`k` has a complex +frequency :math:`\tilde{\nu}_k` (real part: position, imaginary part: pressure +broadening) and a complex *equivalent strength* :math:`\tilde{S}_k`. + +Explicitly, the equivalent strength for line :math:`k` is + +.. math:: + + \tilde{S}_k = \left(\sum_j d_j V_{jk}\right) \left(\sum_j p_j d_j V^{-1}_{kj}\right), + +and the ECS line shape function is + +.. math:: + + F_{ECS}(\nu) = \frac{\sqrt{\ln 2}}{\sqrt{\pi}} \sum_k \tilde{S}_k \frac{w(z_k)}{G_{D,k}}, + +where :math:`w` is the Faddeeva function and + +.. math:: + + z_k = \frac{\left(\tilde{\nu}_k - \nu\right)\sqrt{\ln 2}}{G_{D,k}}, \qquad + G_{D,k} = G_D^{fac} \cdot \mathrm{Re}\!\left[\tilde{\nu}_k\right], + +with the Doppler scale factor + +.. math:: + + G_D^{fac} = \sqrt{\frac{2000 R T}{m c^2}}, + +where :math:`R` is the ideal gas constant in J mol\ :sup:`-1` K\ :sup:`-1`, +:math:`m` the molar mass in g mol\ :sup:`-1`, :math:`c` the speed of light, and +:math:`T` the temperature (same symbols as in the plain LBL definition in +:ref:`lbl-line-shape`). + +Note that :math:`G_{D,k}` is formed by multiplying :math:`G_D^{fac}` by +:math:`\mathrm{Re}[\tilde{\nu}_k]` — the real part of the :math:`k`-th eigenvalue +— rather than by any original line centre :math:`\nu_{0,j}`. This is necessary +because eigenvalue decomposition does not in general return eigenvalues in the same +order as the input lines, so there is no well-defined mapping from equivalent line +:math:`k` to a single physical line :math:`j`. Using :math:`\mathrm{Re}[\tilde{\nu}_k]` +keeps the Doppler width self-consistent with the actual position of each equivalent +line. + +The contribution to the :ref:`propagation matrix ` from the entire band is +then + +.. math:: + + K_{A, ecs} = N \nu \left(1 - \exp\!\left(-\frac{h\nu}{kT}\right)\right) \mathrm{Re}\!\left[F_{ECS}(\nu)\right], + +where :math:`N` is the total number density of the absorbing species. + +.. note:: + + Zeeman splitting within a band is not currently supported together with ECS + line mixing. + +.. _lbl-ecs-relaxmat: + +Relaxation Matrix +================= + +The complex relaxation matrix :math:`\mathbf{W}` has dimensions +:math:`n \times n` (lines in the band) and is constructed as follows. + +The *real* part of the diagonal elements carries the (pressure-shifted) line +centre frequencies: + +.. math:: + + \mathrm{Re}\, W_{ii} = \nu_{0,i} + \Delta\nu_{P,0,i}, + +where :math:`\nu_{0,i}` is the vacuum line centre and :math:`\Delta\nu_{P,0,i}` is the +pressure shift of line :math:`i`. + +The *imaginary* part of the diagonal elements carries the pressure broadening: + +.. math:: + + \mathrm{Im}\, W_{ii} = G_{P,0,i}, + +where :math:`G_{P,0,i}` is the pressure-broadening half-width half-maximum. + +The off-diagonal elements :math:`W_{ij}` (:math:`i \neq j`) encode the rate of +transfer from line :math:`j` to line :math:`i` via collisions. Their construction +from the ECS basis rates is described below. + +.. _lbl-ecs-rates: + +ECS Basis Rates +=============== + +The ECS approach introduces two species-dependent functions of the integer angular +momentum transfer channel :math:`L`: + +**Basic rate** :math:`Q(L)`: + This encodes the intrinsic probability of a collision transferring :math:`L` units of + angular momentum. Its temperature dependence is parameterised as + + .. math:: + + Q(L, T) = s(T) \cdot \frac{e^{-\beta(T)\, E_L / kT}}{[L(L+1)]^{\lambda(T)}}, + + where :math:`E_L` is the rotational energy of level :math:`L`, + and :math:`s(T)`, :math:`\beta(T)`, and :math:`\lambda(T)` are + temperature-dependent model parameters stored per broadening species + (see :ref:`lbl-line-shape-params` for the available temperature dependence forms). + +**Adiabatic factor** :math:`\Omega(L)`: + The IOS approximation becomes inaccurate when the rotational period approaches + the collision duration. The adiabatic factor corrects for this using the + coupling model: + + .. math:: + + \Omega(L) = \frac{1}{\left[1 + \dfrac{\omega_{L,L-2}^2\, \tau_c^2}{24}\right]^2}, + + where :math:`\omega_{L,L-2} = (E_L - E_{L-2})/\hbar` is the angular frequency of the + :math:`L \to L-2` rotational transition and + + .. math:: + + \tau_c = \frac{\sigma_c(T)}{\bar{v}}, \qquad + \bar{v} = \sqrt{\frac{8kT}{\pi\mu}}, + + with :math:`\sigma_c(T)` the temperature-dependent mean collisional diameter (a + per-species model parameter, distinct from the reduced dipole :math:`d_j`), + :math:`\mu` the reduced mass of the colliding pair, and :math:`\bar{v}` the + mean relative thermal speed. + +.. _lbl-ecs-offdiag: + +Off-diagonal Elements +===================== + +The off-diagonal elements of :math:`\mathbf{W}` are computed species-by-species. +All variants follow the formal IOS structure: they are written as a sum over +even angular momentum transfer channels :math:`L`, weighted by the ratio +:math:`Q(L)/\Omega(L)` and by Wigner 3-j and 6-j coupling coefficients. +After the IOS computation an error-correction (sum-rule rescaling) is applied +(see :ref:`lbl-ecs-sumrule`). + +Detailed balance is enforced throughout: the rate of transfer from a lower strength +line :math:`j` to a higher strength line :math:`i` is obtained from the downward +rate :math:`W_{ij}` via + +.. math:: + + W_{ji} = W_{ij} \exp\!\left(\frac{E_j - E_i}{kT}\right), + +where :math:`E_i` is the energy of the lower rotational state of line :math:`i` +(using the same :math:`E_l` convention as in :ref:`lbl-lte`). + +The four variants implemented in ARTS, corresponding to the four line shape model +types ``VP_ECS_HARTMANN``, ``VP_ECS_MAKAROV``, ``VP_ECS_STOTOP``, and ``VP_ECS_SPHTOP``, +are described below. + +Linear Molecules — Hartmann (CO\ :sub:`2`) +------------------------------------------ + +For linear molecules (e.g. CO\ :sub:`2`) the off-diagonal rate from line :math:`j` +(upper/lower rotational quantum numbers :math:`J'_i, J'_f`, vibrational angular +momentum :math:`l`) to line :math:`i` (:math:`J_i, J_f`) is +:cite:p:`NIRO2004483` + +.. math:: + + W_{ij} = + \Omega(J_i)\, (2J'_i+1)\sqrt{(2J_f+1)(2J'_f+1)} + \sum_L (2L+1) + \begin{pmatrix} J_i & J'_i & L \\ l & -l & 0 \end{pmatrix} + \begin{pmatrix} J_f & J'_f & L \\ l & -l & 0 \end{pmatrix} + \begin{Bmatrix} J_i & J_f & 1 \\ J'_f & J'_i & L \end{Bmatrix} + \frac{Q(L)}{\Omega(L)}, + +where the sum runs over even :math:`L \geq \max(|J_i-J'_i|, |J_f-J'_f|)`, +:math:`(\,\cdots)` denotes a Wigner 3-j symbol, +and :math:`\{\,\cdots\}` a Wigner 6-j symbol. + +The corresponding reduced dipole element used in the equivalent-strength +calculation is + +.. math:: + + d(J_f, J_i) = (-1)^{J_f + l_f + 1} \sqrt{2J_f+1}\; + \begin{pmatrix} J_f & 1 & J_i \\ l_i & l_f - l_i & -l_f \end{pmatrix}. + +The rotational energy entering :math:`Q` and :math:`\Omega` is the rigid-rotor +expression :math:`E_J = B_0 J(J+1)`, where :math:`B_0` is the effective +ground-state rotational constant for the species. Energy levels provided by +quantum-chemical calculations are used directly where available; the rigid-rotor +expression serves to extrapolate to levels not covered by those calculations. + +Symmetric Tops with Electron Spin — Makarov (O\ :sub:`2`) +---------------------------------------------------------- + +Molecular oxygen (O\ :sub:`2`) has an unpaired electron spin :math:`S = 1`, so each +rotational quantum number :math:`N` gives rise to a triplet :math:`J = N-1, N, N+1`. +The off-diagonal coupling between lines :math:`(i: N_l J_l \to N_u J_u)` and +:math:`(j: N'_l J'_l \to N'_u J'_u)` is (using :math:`l`/:math:`u` for the +lower/upper state of each transition, matching the convention of :ref:`lbl-lte`) +:cite:p:`Makarov2020` + +.. math:: + + W_{ij} =& + (-1)^{J'_l + J_l + 1}\, + [N_l][N_u][N'_u][N'_l][J_u][J'_u][J_l][J'_l] \Omega(N_l) \\ & + \begin{array}{llll} + \sum_L (2L+1) & + \begin{pmatrix} N'_l & N_l & L \\ 0 & 0 & 0 \end{pmatrix} & + \begin{pmatrix} N'_u & N_u & L \\ 0 & 0 & 0 \end{pmatrix} \\ & + \begin{Bmatrix} L & J_l & J'_l \\ S & N'_l & N_l \end{Bmatrix} & + \begin{Bmatrix} L & J_u & J'_u \\ S & N'_u & N_u \end{Bmatrix} & + \begin{Bmatrix} L & J_l & J'_l \\ 1 & J'_u & J_u \end{Bmatrix} + \frac{Q(L)}{\Omega(L)}, + \end{array} + +where :math:`[X] \equiv \sqrt{2X+1}`, :math:`(\cdots)` denotes a Wigner 3-j symbol, +and :math:`\{\cdots\}` a Wigner 6-j symbol. + +The reduced dipole is + +.. math:: + + d(J_u, J_l, N) = (-1)^{J_l + N} + \sqrt{6(2J_l+1)(2J_u+1)} + \begin{Bmatrix} 1 & 1 & 1 \\ J_l & J_u & N \end{Bmatrix}. + +The rotational energy for the O\ :sub:`2` microwave band is computed from the +full ground-state Hamiltonian including spin–rotation coupling and magnetic +interactions. + +Symmetric Tops (NH\ :sub:`3`, PH\ :sub:`3`) +------------------------------------------- + +This part is mostly untested and may be incorrect. +It has been generated by AI and is available in ARTS +only for experimentation to see if it produces reasonable results. + +For symmetric top molecules (e.g. NH\ :sub:`3`, PH\ :sub:`3`) with :math:`\Delta K = 0` +collisions, lines within the same :math:`K` sub-band are coupled identically to the +Hartmann linear-molecule formula with the vibrational angular momentum :math:`l` +replaced by :math:`K`: :cite:p:`Hadded2002` + +.. math:: + + W_{ij} = + \Omega(J_i)\, (2J'_i+1)\sqrt{(2J_f+1)(2J'_f+1)} + \sum_L (2L+1) + \begin{pmatrix} J_i & J'_i & L \\ K & -K & 0 \end{pmatrix} + \begin{pmatrix} J_f & J'_f & L \\ K & -K & 0 \end{pmatrix} + \begin{Bmatrix} J_i & J_f & 1 \\ J'_f & J'_i & L \end{Bmatrix} + \frac{Q(L)}{\Omega(L)}. + +Lines with different :math:`K` are not coupled. The reduced dipole is + +.. math:: + + d(J_f, J_i, K) = (-1)^{J_f + K + 1}\sqrt{2J_f+1}\; + \begin{pmatrix} J_f & 1 & J_i \\ K & 0 & -K \end{pmatrix}. + +Rotational energy levels provided by quantum-chemical calculations are used +directly where available; levels beyond those are extrapolated using the +rigid-rotor expression :math:`E_J = B_0 J(J+1)` with the species-specific +ground-state rotational constant :math:`B_0`. + +Spherical Tops (CH\ :sub:`4`) +----------------------------- + +This part is mostly untested and may be incorrect. +It has been generated by AI and is available in ARTS +only for experimentation to see if it produces reasonable results. + +For spherical top molecules (e.g. CH\ :sub:`4`) the coupling reduces to the +:math:`l = 0` limit of the Hartmann formula: :cite:p:`Pieroni1999` + +.. math:: + + W_{ij} = + \Omega(J_i)\, (2J'_i+1)\sqrt{(2J_f+1)(2J'_f+1)} + \sum_L (2L+1) + \begin{pmatrix} J_i & J'_i & L \\ 0 & 0 & 0 \end{pmatrix} + \begin{pmatrix} J_f & J'_f & L \\ 0 & 0 & 0 \end{pmatrix} + \begin{Bmatrix} J_i & J_f & 1 \\ J'_f & J'_i & L \end{Bmatrix} + \frac{Q(L)}{\Omega(L)}, + +and the reduced dipole is + +.. math:: + + d(J_f, J_i) = (-1)^{J_f+1}\sqrt{2J_f+1}\; + \begin{pmatrix} J_f & 1 & J_i \\ 0 & 0 & 0 \end{pmatrix}. + +Rotational energy levels provided by quantum-chemical calculations are used +directly where available; levels beyond those are extrapolated using the +rigid-rotor expression :math:`E_J = B_0 J(J+1)` with the species-specific +ground-state rotational constant :math:`B_0`. + +.. _lbl-ecs-sumrule: + +Sum-rule Correction +=================== + +The pure IOS matrix elements computed above do not, in general, satisfy the +first-order optical sum rule exactly due to the finite range of the :math:`L` sum and +the approximate nature of the adiabatic factor. The *error-correction* step +rescales each column of off-diagonal elements to enforce + +.. math:: + + \sum_j d_j\, W_{ji} = 0 \quad \forall\, i. + +This is done as follows. For each line :math:`i`, partition the off-diagonal +elements into those coupling to lines with lower intensity-weighted frequency +(summed into :math:`s_\downarrow`) and those coupling to higher-frequency lines +(:math:`s_\uparrow`): + +.. math:: + + s_\downarrow = \sum_{j > i} d_j\, W_{ji}, \qquad + s_\uparrow = \sum_{j < i} d_j\, W_{ji}. + +All downward-coupling elements are then rescaled by :math:`-s_\uparrow / s_\downarrow`, +and the corresponding upward-coupling elements are updated by detailed balance. + +This rescaling constitutes the "error-corrected" part of the ECS method and ensures +the resulting relaxation matrix produces physically consistent absorption profiles +that recover the correct integrated line intensity at all pressures. + +.. _lbl-ecs-multispecies: + +Multiple Broadening Species +=========================== + +When multiple broadening species are present, the ARTS implementation offers two +modes: + +In the **single-W mode** (used by default when calling ``calculate``), the per-species +relaxation matrices are first volume-mixing-ratio weighted and summed into a single +effective :math:`\mathbf{W}`: + +.. math:: + + \mathbf{W}_{eff} = \sum_s x_s\, \mathbf{W}^{(s)}, + +and a single diagonalisation is performed. + +In the **multi-W mode** (used by ``equivalent_values`` for pre-computing equivalent +lines at multiple temperatures), the diagonalisation is performed separately per +species and the resulting absorption contributions are VMR-weighted and summed. + +.. _lbl-ecs-rosenkranz: + +Rosenkranz Approximation +======================== + +The full ECS calculation requires the diagonalisation of an :math:`n \times n` +complex matrix at every temperature and pressure of interest, together with a +VMR-weighted sum over broadening species. For many practical applications a +simpler representation is desirable: the *Rosenkranz approximation* retains the +ordinary Voigt line shape of each line but adds pressure-dependent first- and +second-order correction terms that encode the effect of line mixing to a given +order in pressure. + +The corrected Voigt line shape for line :math:`i` is exactly the :ref:`Voigt +profile ` already described, + +.. math:: + + F_i = \frac{1 + G_{lm,i} - iY_{lm,i}}{\sqrt{\pi}\,G_D}\,w(z_i), + +where :math:`z_i` contains :math:`\Delta\nu_{lm,i}` as an additional shift, and +the three correction parameters are: + +.. list-table:: + :header-rows: 1 + + * - Parameter + - Physical meaning + - Pressure scaling + * - :math:`Y_{lm,i}` + - First-order line-mixing: asymmetric intensity redistribution between nearby lines. + - :math:`P` + * - :math:`G_{lm,i}` + - Second-order strength correction: quadratic-in-pressure modification to the + integrated area. + - :math:`P^2` + * - :math:`\Delta\nu_{lm,i}` + - Second-order frequency shift: quadratic-in-pressure displacement of the line + centre due to the mixing. + - :math:`P^2` + +The Rosenkranz parameters are not fitted to measured spectra directly; instead they +are derived from the ECS equivalent lines or, equivalently, from the relaxation matrix +itself via perturbation theory — both approaches are described below. + +.. _lbl-ecs-rosenkranz-W: + +Perturbation Theory from the Relaxation Matrix +----------------------------------------------- + +When the off-diagonal elements of :math:`\mathbf{W}` are small compared with the +spacings between line centres (the "weak coupling" limit, valid for resolved lines or +moderate pressures), the Rosenkranz parameters can be obtained analytically by +expanding the resolvent :math:`(\nu\mathbf{I} - \mathbf{W})^{-1}` in powers of the +off-diagonal part. This is the original approach of :cite:t:`rosenkranz:75`. + +Write :math:`\mathbf{W} = \mathbf{D} + \mathbf{V}`, where :math:`\mathbf{D}` is the +diagonal part (line centres plus pressure broadening) and :math:`\mathbf{V}_{ij} = +W_{ij}` for :math:`i \neq j` (the off-diagonal relaxation rates, purely imaginary in +the ARTS convention: :math:`V_{ij} = i R_{ij}` with :math:`R_{ij}` real and +proportional to :math:`P`). Let :math:`g_i(\nu) = [\nu - W_{ii}]^{-1}` be the +unperturbed resolvent for line :math:`i`. The Neumann expansion then gives + +.. math:: + + (\nu\mathbf{I} - \mathbf{W})^{-1} = + \mathbf{G}_0 + \mathbf{G}_0 \mathbf{V} \mathbf{G}_0 + + \mathbf{G}_0 \mathbf{V} \mathbf{G}_0 \mathbf{V} \mathbf{G}_0 + \cdots, + +where :math:`\mathbf{G}_0 = \mathrm{diag}(g_i(\nu))`. + +Collecting all contributions to the absorption of line :math:`i` through first and +second order, and evaluating the slowly varying factors involving other lines :math:`j` +at :math:`\nu = \nu_i`, yields the three Rosenkranz parameters for line :math:`i`: + +**First-order mixing parameter** (:math:`Y_i \sim P`): + +.. math:: + + Y_i = \frac{2}{S_i} \sum_{j \neq i} S_j \frac{R_{ij}}{\nu_i - \nu_j}, + +where :math:`S_i = p_i d_i^2` is proportional to the LBL line strength of line +:math:`i` (see :ref:`lbl-ecs-lineshape` for the definition of :math:`p_i`), +and +:math:`R_{ij} = \mathrm{Im}[W_{ij}] / P` is the pressure-normalised off-diagonal +relaxation rate (transfer from line :math:`j` to line :math:`i`; note +:math:`\mathrm{Re}[W_{ij}] = 0` for :math:`i \neq j`), and the sum is over +all other lines :math:`j` in the band. + +**Second-order strength correction** (:math:`G_i \sim P^2`): + +From the squared first-order cross terms, the fractional modification to the +integrated area of line :math:`i` is + +.. math:: + + G_i = -\frac{1}{S_i} \sum_{j \neq i} S_j \left(\frac{R_{ij}}{\nu_i - \nu_j}\right)^2. + +**Second-order line-centre shift** (:math:`\Delta\nu_i \sim P^2`): + +The diagonal self-energy correction (virtual transition :math:`i \to j \to i`) gives + +.. math:: + + \Delta\nu_i = -\frac{1}{S_i} \sum_{j \neq i} S_j \frac{R_{ij}^2}{\nu_i - \nu_j}, + +where detailed balance (:math:`S_i R_{ji} = S_j R_{ij}`) has been used to express +everything in terms of the downward rate :math:`R_{ij}`. + +.. note:: + + Note that :math:`\Delta\nu_i = G_i (\nu_i - \nu_j)` only for a single interfering + line. In general they have different frequency denominators (:math:`\nu_i - \nu_j` + vs :math:`(\nu_i - \nu_j)^2`) and thus differ quantitatively when multiple lines + contribute. The two parameters are both needed to correctly reproduce the + second-order pressure dependence of the band profile. + + The perturbation theory expressions above assume the off-diagonal elements are + small relative to the line spacing. They break down for overlapping lines + (e.g., at very high pressures or for lines very close in frequency). In that + regime the full ECS calculation should be used instead. + +.. _lbl-ecs-rosenkranz-fitting: + +Fitting from Equivalent Lines +------------------------------ + +The perturbation theory expressions above are analytically exact in the weak-coupling +limit, but in practice it is often more accurate to extract the Rosenkranz parameters +*numerically* from the ECS equivalent lines, because the equivalent-line calculation +already includes the full resummation of the relaxation matrix (not just the first few +terms of the Neumann series). The two approaches agree at low pressure but the +equivalent-line fit is preferred at higher pressures where the perturbation series +converges slowly. + +Given the complex equivalent lines :math:`(\tilde{S}_{k,s}, \tilde{\nu}_{k,s})` +(indexed by :math:`k` in eigenvalue-decomposition order, which carries no physical +meaning) computed by ECS for broadening species :math:`s` at pressure :math:`P_0` +and a grid of temperatures :math:`T_1, \ldots, T_M`, the Rosenkranz coefficients are +extracted by ``abs_bandsLineMixingAdaptation`` as follows. +Here, :math:`i` will denote the index of the physical LBL lines (the rows/columns of +:math:`\mathbf{W}`) and :math:`k` the index of the equivalent lines. + +**Step 1 — Sort and match.** +The :math:`n` equivalent lines are sorted by :math:`\mathrm{Re}[\tilde{\nu}_{k,s}]` +and the :math:`n` physical LBL lines are sorted by :math:`\nu_{0,i}`. Equivalent line +at sorted position :math:`n` is then identified with the physical line at sorted +position :math:`n`, giving a bijection :math:`k \leftrightarrow i` between the two +index sets. This is a heuristic matching: because eigenvalue decomposition does not +guarantee any particular ordering of eigenvalues, sorting is the only way to establish a +correspondence. The identification is reliable as long as the second-order frequency +shifts and pressure shifts remain small compared with the separations between adjacent +line centres; it can fail if either effect is comparable in magnitude to the line spacing. + +**Step 2 — Form normalised differences.** +For each matched pair :math:`(k, i)`, the LTE line strength of physical line :math:`i` +at temperature :math:`T` is :math:`S_{LTE,i}(T)` as defined in :ref:`lbl-lte` +(without the number density factor :math:`\rho`; the equivalent per-molecule strength is +:math:`s_i(T) = S_{LTE,i}(T)/\rho`). The unperturbed complex frequency of physical +line :math:`i` is + +.. math:: + + \nu_i^{LBL}(T) = \nu_{0,i} + \Delta\nu_{P,0,i}(T,P_0) + i\,G_{P,0,i}(T,P_0). + +The residual strength ratio and frequency residual are then formed: + +.. math:: + + r_{i,s}(T) &= \frac{\tilde{S}_{k,s}(T)}{S_{LTE,i}(T)}, \\ + \delta\nu_{i,s}(T) &= \tilde{\nu}_{k,s}(T) - \nu_i^{LBL}(T), + +where :math:`k` is the equivalent line matched to physical line :math:`i` in Step 1. + +**Step 3 — Extract pressure-normalised Rosenkranz coefficients.** +The three coefficients for physical line :math:`i` at the reference pressure +:math:`P_0` are read off as: + +.. math:: + + Y_{lm,i,s}(T) &= \frac{\mathrm{Im}\!\left[r_{i,s}(T)\right]}{P_0}, \\[4pt] + G_{lm,i,s}(T) &= \frac{\mathrm{Re}\!\left[r_{i,s}(T)\right] - 1}{P_0^2}, \\[4pt] + \Delta\nu_{lm,i,s}(T) &= \frac{\mathrm{Re}\!\left[\delta\nu_{i,s}(T)\right]}{P_0^2}. + +The imaginary part of :math:`\delta\nu_{i,s}(T)` — which represents the +correction to the pressure-broadening half-width — is divided by +:math:`P_0^3` but is not retained as a separate Rosenkranz coefficient +(it is already captured by the diagonal of :math:`\mathbf{W}` at first +order in :math:`P`). + +**Step 4 — Polynomial fit in temperature.** +Each of the three coefficients is fitted as a polynomial in temperature of +configurable degree :math:`d`: + +.. math:: + + Y_{lm,i,s}(T) &\approx \sum_{n=0}^{d} a_n^{(Y)}\,T^n, \\ + G_{lm,i,s}(T) &\approx \sum_{n=0}^{d} a_n^{(G)}\,T^n, \\ + \Delta\nu_{lm,i,s}(T) &\approx \sum_{n=0}^{d} a_n^{(DV)}\,T^n. + +These polynomials are stored using the ``POLY`` temperature model (see +:ref:`lbl-line-shape-params`) for each broadening species separately and +are then evaluated at runtime using the ordinary VMR-weighted sum of the +:ref:`line shape parameter ` framework, with the +coefficients attached to physical line :math:`i`. + +.. note:: + + Setting ``rosenkranz_fit_order = 1`` retains only :math:`Y_{lm}` and is + appropriate for moderate pressures where the quadratic-in-pressure corrections + are negligible. Setting it to 2 also fits :math:`G_{lm}` and + :math:`\Delta\nu_{lm}`, which is necessary at higher pressures or when + second-order effects are important (e.g. near the Q-branch of O :sub:`2` + at tens of GHz). + + The polynomial fits implicitly assume that the reference pressure :math:`P_0` + is fixed; the resulting coefficients must be used at the same pressure + normalisation. This is handled automatically when the output of + ``abs_bandsLineMixingAdaptation`` is fed back into the ordinary line-by-line + calculation. + diff --git a/doc/arts/references.bib b/doc/arts/references.bib index c0baece56d..2f245934cd 100644 --- a/doc/arts/references.bib +++ b/doc/arts/references.bib @@ -2326,3 +2326,50 @@ @article{Ellison2007 url = {https://doi.org/10.1063/1.2360986}, eprint = {https://pubs.aip.org/aip/jpr/article-pdf/36/1/1/14719718/1_1_online.pdf}, } + +@article{NIRO2004483, +title = {Spectra calculations in central and wing regions of CO2 IR bands between 10 and 20μm. I: model and laboratory measurements}, +journal = {Journal of Quantitative Spectroscopy and Radiative Transfer}, +volume = {88}, +number = {4}, +pages = {483-498}, +year = {2004}, +issn = {0022-4073}, +doi = {https://doi.org/10.1016/j.jqsrt.2004.04.003}, +url = {https://www.sciencedirect.com/science/article/pii/S0022407304001049}, +author = {F Niro and C Boulet and J.-M Hartmann}, +keywords = {CO, Infrared, Absorption, Shape, Model, Laboratory, Spectra}, +} + +@article{Pieroni1999, + author = {Pieroni, D. and Nguyen-Van-Thanh and Brodbeck, C. and + Claveau, C. and Valentin, A. and Hartmann, J. M. and Gabard, T. and + Champion, J.-P. and Bermejo, D. and Domenech, J.-L.}, + title = {Experimental and theoretical study of line mixing in methane spectra. I. + The N$_2$-broadened $\nu_3$ band at room temperature}, + journal = {The Journal of Chemical Physics}, + volume = {110}, + number = {16}, + pages = {7717-7732}, + year = {1999}, + month = {04}, + issn = {0021-9606}, + doi = {10.1063/1.478724}, + url = {https://doi.org/10.1063/1.478724}, + eprint = {https://pubs.aip.org/aip/jcp/article-pdf/110/16/7717/19328457/7717_1_online.pdf}, +} + +@article{Hadded2002, + author = {Hadded, S. and Thibault, F. and Flaud, P.-M. and Aroui, H. and Hartmann, J.-M.}, + title = {Experimental and theoretical study of line mixing in NH$_3$ spectra. I. Scaling analysis of parallel bands perturbed by He}, + journal = {The Journal of Chemical Physics}, + volume = {116}, + number = {17}, + pages = {7544-7557}, + year = {2002}, + month = {05}, + issn = {0021-9606}, + doi = {10.1063/1.1463442}, + url = {https://doi.org/10.1063/1.1463442}, + eprint = {https://pubs.aip.org/aip/jcp/article-pdf/116/17/7544/19021600/7544_1_online.pdf}, +} diff --git a/src/core/lbl/CMakeLists.txt b/src/core/lbl/CMakeLists.txt index a11a3d6696..2e6c0560eb 100644 --- a/src/core/lbl/CMakeLists.txt +++ b/src/core/lbl/CMakeLists.txt @@ -8,6 +8,8 @@ add_library(lbl STATIC lbl_lineshape_voigt_ecs.cpp lbl_lineshape_voigt_ecs_hartmann.cpp lbl_lineshape_voigt_ecs_makarov.cpp + lbl_lineshape_voigt_ecs_stotop.cpp + lbl_lineshape_voigt_ecs_sphtop.cpp lbl_lineshape_voigt_lte.cpp lbl_lineshape_voigt_lte_mirrored.cpp lbl_lineshape_voigt_lte_matrix.cpp diff --git a/src/core/lbl/lbl_lineshape.cpp b/src/core/lbl/lbl_lineshape.cpp index 2eba837b22..11f4f1fd6b 100644 --- a/src/core/lbl/lbl_lineshape.cpp +++ b/src/core/lbl/lbl_lineshape.cpp @@ -62,7 +62,9 @@ std::unique_ptr init_voigt_abs_ecs_data( const Vector2 los) { if (stdr::any_of(bnds | stdv::values, [](auto& bnd) { return bnd.lineshape == LineByLineLineshape::VP_ECS_MAKAROV or - bnd.lineshape == LineByLineLineshape::VP_ECS_HARTMANN; + bnd.lineshape == LineByLineLineshape::VP_ECS_HARTMANN or + bnd.lineshape == LineByLineLineshape::VP_ECS_STOTOP or + bnd.lineshape == LineByLineLineshape::VP_ECS_SPHTOP; })) return std::make_unique( f_grid, atm, los, ZeemanPolarization::no); @@ -177,7 +179,9 @@ void calculate(PropmatVectorView pm, calc_voigt_line_nlte(bnd_key, bnd, pol); break; case LineByLineLineshape::VP_ECS_MAKAROV: [[fallthrough]]; - case LineByLineLineshape::VP_ECS_HARTMANN: + case LineByLineLineshape::VP_ECS_HARTMANN: [[fallthrough]]; + case LineByLineLineshape::VP_ECS_STOTOP: [[fallthrough]]; + case LineByLineLineshape::VP_ECS_SPHTOP: calc_voigt_ecs_linemixing(bnd_key, bnd, pol); break; } diff --git a/src/core/lbl/lbl_lineshape_voigt_ecs.cpp b/src/core/lbl/lbl_lineshape_voigt_ecs.cpp index 5763cb04bd..5ca077cbb9 100644 --- a/src/core/lbl/lbl_lineshape_voigt_ecs.cpp +++ b/src/core/lbl/lbl_lineshape_voigt_ecs.cpp @@ -21,6 +21,8 @@ #include "lbl_lineshape_model.h" #include "lbl_lineshape_voigt_ecs_hartmann.h" #include "lbl_lineshape_voigt_ecs_makarov.h" +#include "lbl_lineshape_voigt_ecs_sphtop.h" +#include "lbl_lineshape_voigt_ecs_stotop.h" #undef WIGNER3 #undef WIGNER6 @@ -187,6 +189,15 @@ void ComputeData::adapt_multi(const QuantumIdentifier& bnd_qid, auto& l2 = bnd_qid.state.at(QuantumNumberType::l2); dipr[i] = hartmann::reduced_dipole(J.upper, J.lower, l2.upper, l2.lower); dip[i] *= std::signbit(dipr[i]) ? -1 : 1; + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_STOTOP) { + auto& J = bnd.lines[i].qn.at(QuantumNumberType::J); + auto& Kq = bnd.lines[i].qn.at(QuantumNumberType::K); + dipr[i] = stotop::reduced_dipole(J.upper, J.lower, Kq.lower); + dip[i] *= std::signbit(dipr[i]) ? -1 : 1; + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_SPHTOP) { + auto& J = bnd.lines[i].qn.at(QuantumNumberType::J); + dipr[i] = sphtop::reduced_dipole(J.upper, J.lower); + dip[i] *= std::signbit(dipr[i]) ? -1 : 1; } } @@ -254,6 +265,12 @@ void ComputeData::adapt_multi(const QuantumIdentifier& bnd_qid, } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_HARTMANN) { hartmann::relaxation_matrix_offdiagonal( Wimag, bnd_qid, bnd, sort, spec, rovib_data_it->second, dipr, atm); + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_STOTOP) { + stotop::relaxation_matrix_offdiagonal( + Wimag, bnd_qid, bnd, sort, spec, rovib_data_it->second, dipr, atm); + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_SPHTOP) { + sphtop::relaxation_matrix_offdiagonal( + Wimag, bnd_qid, bnd, sort, spec, rovib_data_it->second, dipr, atm); } else { ARTS_USER_ERROR("UNKNOWN ECS LINE SHAPE {}", bnd.lineshape) } @@ -314,6 +331,15 @@ void ComputeData::adapt_single(const QuantumIdentifier& bnd_qid, auto& l2 = bnd_qid.state.at(QuantumNumberType::l2); dipr[i] = hartmann::reduced_dipole(J.upper, J.lower, l2.upper, l2.lower); dip[i] *= std::signbit(dipr[i]) ? -1 : 1; + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_STOTOP) { + auto& J = bnd.lines[i].qn.at(QuantumNumberType::J); + auto& Kq = bnd.lines[i].qn.at(QuantumNumberType::K); + dipr[i] = stotop::reduced_dipole(J.upper, J.lower, Kq.lower); + dip[i] *= std::signbit(dipr[i]) ? -1 : 1; + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_SPHTOP) { + auto& J = bnd.lines[i].qn.at(QuantumNumberType::J); + dipr[i] = sphtop::reduced_dipole(J.upper, J.lower); + dip[i] *= std::signbit(dipr[i]) ? -1 : 1; } } @@ -382,6 +408,12 @@ void ComputeData::adapt_single(const QuantumIdentifier& bnd_qid, } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_HARTMANN) { hartmann::relaxation_matrix_offdiagonal( Wimag, bnd_qid, bnd, sort, spec, rovib_data_it->second, dipr, atm); + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_STOTOP) { + stotop::relaxation_matrix_offdiagonal( + Wimag, bnd_qid, bnd, sort, spec, rovib_data_it->second, dipr, atm); + } else if (bnd.lineshape == LineByLineLineshape::VP_ECS_SPHTOP) { + sphtop::relaxation_matrix_offdiagonal( + Wimag, bnd_qid, bnd, sort, spec, rovib_data_it->second, dipr, atm); } else { ARTS_USER_ERROR("UNKNOWN ECS LINE SHAPE {}", bnd.lineshape) } diff --git a/src/core/lbl/lbl_lineshape_voigt_ecs_hartmann.cpp b/src/core/lbl/lbl_lineshape_voigt_ecs_hartmann.cpp index 6bec022604..7daa2040c7 100644 --- a/src/core/lbl/lbl_lineshape_voigt_ecs_hartmann.cpp +++ b/src/core/lbl/lbl_lineshape_voigt_ecs_hartmann.cpp @@ -35,7 +35,7 @@ Numeric wig6(const Rational& a, } std::function erot_selection(const SpeciesIsotope& isot) { - if (isot.spec == SpeciesEnum::CarbonDioxide and isot.isotname == "626") { + if (isot == "CO2-626"_isot) { return [](const Rational J) -> Numeric { return Conversion::kaycm2joule(0.39021) * Numeric(J * (J + 1)); }; @@ -79,7 +79,6 @@ void relaxation_matrix_offdiagonal(MatrixView& W, using std::swap; const bool swap_order = li > lf; if (swap_order) swap(li, lf); - const int sgn = iseven(li + lf + 1) ? -1 : 1; if (abs(li - lf) > 1) return; const Numeric T = atm.temperature; @@ -129,20 +128,20 @@ void relaxation_matrix_offdiagonal(MatrixView& W, if (Jf_p > Jf) continue; Index L = std::max(std::abs((Ji - Ji_p).toIndex()), - std::abs((Jf - Jf_p).toIndex())); + std::abs((Jf - Jf_p).toIndex())); L += L % 2; const Index Lf = std::min((Ji + Ji_p).toIndex(), (Jf + Jf_p).toIndex()); Numeric sum = 0; for (; L <= Lf; L += 2) { - const Numeric a = wig3(Ji_p, Rational{L}, Ji, li, Rational{0}, -li); - const Numeric b = wig3(Jf_p, Rational{L}, Jf, lf, Rational{0}, -lf); + const Numeric a = wig3(Ji, Ji_p, Rational{L}, li, -li, Rational{0}); + const Numeric b = wig3(Jf, Jf_p, Rational{L}, lf, -lf, Rational{0}); const Numeric c = wig6(Ji, Jf, Rational{1}, Jf_p, Ji_p, Rational{L}); sum += a * b * c * Numeric(2 * L + 1) * Q[L] / Om[L]; } const Numeric ECS = Om[Ji.toIndex()]; - const Numeric scl = sgn * ECS * Numeric(2 * Ji_p + 1) * - sqrtr((2 * Jf + 1) * (2 * Jf_p + 1)); + const Numeric scl = + ECS * Numeric(2 * Ji_p + 1) * sqrtr((2 * Jf + 1) * (2 * Jf_p + 1)); sum *= scl; // Add to W and rescale to upwards element by the populations @@ -154,11 +153,6 @@ void relaxation_matrix_offdiagonal(MatrixView& W, ARTS_USER_ERROR_IF(errno == EDOM, "Cannot compute the wigner symbols") - // Undocumented negative absolute sign - for (Size i = 0; i < n; i++) - for (Size j = 0; j < n; j++) - if (j not_eq i and W[i, j] > 0) W[i, j] *= -1; - // Sum rule correction for (Size i = 0; i < n; i++) { Numeric sumlw = 0.0; @@ -166,9 +160,9 @@ void relaxation_matrix_offdiagonal(MatrixView& W, for (Size j = 0; j < n; j++) { if (j > i) { - sumlw += std::abs(dipr[j]) * W[j, i]; // Undocumented abs-sign + sumlw += dipr[j] * W[j, i]; } else { - sumup += std::abs(dipr[j]) * W[j, i]; // Undocumented abs-sign + sumup += dipr[j] * W[j, i]; } } @@ -182,7 +176,7 @@ void relaxation_matrix_offdiagonal(MatrixView& W, } else { W[j, i] *= -sumup / sumlw; W[i, j] = W[j, i] * std::exp((erot(Ji) - erot(Jj)) / - kelvin2joule(T)); // This gives LTE + kelvin2joule(T)); // This gives LTE } } } diff --git a/src/core/lbl/lbl_lineshape_voigt_ecs_sphtop.cpp b/src/core/lbl/lbl_lineshape_voigt_ecs_sphtop.cpp new file mode 100644 index 0000000000..13d7fc1978 --- /dev/null +++ b/src/core/lbl/lbl_lineshape_voigt_ecs_sphtop.cpp @@ -0,0 +1,195 @@ +#include "lbl_lineshape_voigt_ecs_sphtop.h" + +#include +#include +#include + +namespace lbl::voigt::ecs::sphtop { +#if DO_FAST_WIGNER +#define WIGNER3 fw3jja6 +#define WIGNER6 fw6jja +#else +#define WIGNER3 wig3jj +#define WIGNER6 wig6jj +#endif + +namespace { +Numeric wig3(const Rational& a, + const Rational& b, + const Rational& c, + const Rational& d, + const Rational& e, + const Rational& f) { + return WIGNER3( + a.toInt(2), b.toInt(2), c.toInt(2), d.toInt(2), e.toInt(2), f.toInt(2)); +} + +Numeric wig6(const Rational& a, + const Rational& b, + const Rational& c, + const Rational& d, + const Rational& e, + const Rational& f) { + return WIGNER6( + a.toInt(2), b.toInt(2), c.toInt(2), d.toInt(2), e.toInt(2), f.toInt(2)); +} + +/*! Compute rotational energy for a spherical top molecule + * + * E(J) = B*J*(J+1) + * + * Species-specific rotational constants: + * CH4-211 (12CH4): B0 = 5.2410 cm^{-1} + * + * @param[in] isot The isotopologue + * @return A function J -> E(J) in Joule + */ +std::function erot_selection(const SpeciesIsotope& isot) { + // CH4 main isotopologue (12C-1H4) + if (isot == "CH4-211"_isot) { + return [](const Rational J) -> Numeric { + return Conversion::kaycm2joule(5.2410) * Numeric(J * (J + 1)); + }; + } + + ARTS_USER_ERROR("{} has no rotational energies for spherical top ECS in ARTS", + isot.FullName()) + return [](const Rational J) -> Numeric { + return Numeric(J) * std::numeric_limits::signaling_NaN(); + }; +} +} // namespace + +Numeric reduced_dipole(const Rational Jf, const Rational Ji) { + // d(Jf, Ji) = (-1)^{Jf+1} sqrt(2*Jf+1) * 3j(Jf, 1, Ji; 0, 0, 0) + // This is the l=0 limit of the Hartmann formula. + if (not iseven(Jf + 1)) + return -sqrtr(2 * Jf + 1) * + wigner3j(Jf, Rational{1}, Ji, Rational{0}, Rational{0}, Rational{0}); + return +sqrtr(2 * Jf + 1) * + wigner3j(Jf, Rational{1}, Ji, Rational{0}, Rational{0}, Rational{0}); +} + +void relaxation_matrix_offdiagonal(MatrixView& W, + const QuantumIdentifier& bnd_qid, + const band_data& bnd, + const ArrayOfIndex& sorting, + const SpeciesEnum broadening_species, + const linemixing::species_data& rovib_data, + const Vector& dipr, + const AtmPoint& atm) { + using Conversion::kelvin2joule; + + const Size n = bnd.size(); + if (not n) return; + + const Numeric T = atm.temperature; + + const auto erot = erot_selection(bnd_qid.isot); + + const std::array rats{bnd.max(QuantumNumberType::J)}; + const int maxL = wigner_init_size(rats); + + const auto Om = [&]() { + Vector out(maxL); + for (Index i = 0; i < maxL; i++) + out[i] = rovib_data.Omega(atm.temperature, + bnd.front().ls.T0, + broadening_species == SpeciesEnum::Bath + ? atm.mean_mass() + : atm.mean_mass(broadening_species), + bnd_qid.isot.mass, + erot(Rational{i}), + erot(Rational{i - 2})); + return out; + }(); + + const auto Q = [&]() { + Vector out(maxL); + for (Index i = 0; i < maxL; i++) + out[i] = rovib_data.Q( + Rational{i}, atm.temperature, bnd.front().ls.T0, erot(Rational{i})); + return out; + }(); + + // The coupling for spherical tops with l=0 is: + // + // R(J→J'; 0) = (-1)^{J+J'+1} (2J'+1) Ω(J) + // × Σ_L (2L+1) 3j(J, J', L; 0, 0, 0) × 3j(Jf, Jf', L; 0, 0, 0) + // × 6j(Ji, Jf, 1; Jf', Ji', L) × Q(L)/Ω(L) + // + // This is identical to the Hartmann (linear molecule) formula with + // l_i = l_f = 0. + + arts_wigner_thread_init(maxL); + for (Size i = 0; i < n; i++) { + auto& J = bnd.lines[sorting[i]].qn.at(QuantumNumberType::J); + const Rational Ji = J.upper; + const Rational Jf = J.lower; + + for (Size j = 0; j < n; j++) { + if (i == j) continue; + + auto& J_p = bnd.lines[sorting[j]].qn.at(QuantumNumberType::J); + const Rational Ji_p = J_p.upper; + const Rational Jf_p = J_p.lower; + + // Only compute the downward element (J' ≤ J) + if (Jf_p > Jf) continue; + + Index L = std::max(std::abs((Ji - Ji_p).toIndex()), + std::abs((Jf - Jf_p).toIndex())); + L += L % 2; + const Index Lf = std::min((Ji + Ji_p).toIndex(), (Jf + Jf_p).toIndex()); + + Numeric sum = 0; + for (; L <= Lf; L += 2) { + const Numeric a = + wig3(Ji, Ji_p, Rational{L}, Rational{0}, Rational{0}, Rational{0}); + const Numeric b = + wig3(Jf, Jf_p, Rational{L}, Rational{0}, Rational{0}, Rational{0}); + const Numeric c = wig6(Ji, Jf, Rational{1}, Jf_p, Ji_p, Rational{L}); + sum += a * b * c * Numeric(2 * L + 1) * Q[L] / Om[L]; + } + const Numeric ECS = Om[Ji.toIndex()]; + const Numeric scl = + ECS * Numeric(2 * Ji_p + 1) * sqrtr((2 * Jf + 1) * (2 * Jf_p + 1)); + sum *= scl; + + // Downward element and detailed balance for upward + W[j, i] = sum; + W[i, j] = sum * std::exp((erot(Jf_p) - erot(Jf)) / kelvin2joule(T)); + } + } + arts_wigner_thread_free(); + + ARTS_USER_ERROR_IF(errno == EDOM, "Cannot compute the wigner symbols") + + // Sum rule correction + for (Size i = 0; i < n; i++) { + Numeric sumlw = 0.0; + Numeric sumup = 0.0; + + for (Size j = 0; j < n; j++) { + if (j > i) { + sumlw += dipr[j] * W[j, i]; + } else { + sumup += dipr[j] * W[j, i]; + } + } + + const Rational Ji = bnd.lines[sorting[i]].qn.at(QuantumNumberType::J).lower; + for (Size j = i + 1; j < n; j++) { + const Rational Jj = + bnd.lines[sorting[j]].qn.at(QuantumNumberType::J).lower; + if (std::abs(sumlw) <= std::abs(sumup) * 1e-6) { + W[j, i] = 0.0; + W[i, j] = 0.0; + } else { + W[j, i] *= -sumup / sumlw; + W[i, j] = W[j, i] * std::exp((erot(Ji) - erot(Jj)) / kelvin2joule(T)); + } + } + } +} +} // namespace lbl::voigt::ecs::sphtop diff --git a/src/core/lbl/lbl_lineshape_voigt_ecs_sphtop.h b/src/core/lbl/lbl_lineshape_voigt_ecs_sphtop.h new file mode 100644 index 0000000000..03350a979f --- /dev/null +++ b/src/core/lbl/lbl_lineshape_voigt_ecs_sphtop.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "lbl_data.h" +#include "lbl_lineshape_linemixing.h" + +namespace lbl::voigt::ecs::sphtop { +/*! Returns the reduced dipole for a spherical top molecule + * + * For a spherical top (Td symmetry), the sub-level structure (A1, A2, E, + * F1, F2) is already resolved at the band level in the ARTS catalog. + * Within each symmetry sub-band, the lines are characterized only by J + * (total angular momentum). The reduced dipole is the l=0 limit of the + * linear molecule formula: + * + * d(Jf, Ji) = (-1)^{Jf+1} sqrt(2*Jf+1) * 3j(Jf, 1, Ji; 0, 0, 0) + * + * This is equivalent to the Hartmann formula with l_i = l_f = 0. + * + * @param[in] Jf Lower state total angular momentum + * @param[in] Ji Upper state total angular momentum + * @return The reduced dipole + */ +Numeric reduced_dipole(const Rational Jf, const Rational Ji); + +/*! Compute the off-diagonal elements of the relaxation matrix + * for spherical top molecules (CH4, etc.) + * + * Uses the ECS-EP (Energy Corrected Sudden with Exponential Power law) + * formalism. For spherical tops, the tetrahedral sub-level structure + * is already separated into distinct ARTS bands (by rovibSym and alpha), + * so within a single band the coupling is identical to the linear molecule + * case with l=0. The 3j symbols simplify to 3j(J, J', L; 0, 0, 0). + * + * @param[in,out] W Relaxation matrix (diagonal already set) + * @param[in] bnd_qid Band quantum identifier + * @param[in] bnd Band data + * @param[in] sorting Index sorting of the lines + * @param[in] broadening_species Which broadening species to use + * @param[in] rovib_data ECS-EP parameters for the species + * @param[in] dipr Reduced dipole moments + * @param[in] atm Atmospheric point + */ +void relaxation_matrix_offdiagonal(MatrixView& W, + const QuantumIdentifier& bnd_qid, + const band_data& bnd, + const ArrayOfIndex& sorting, + const SpeciesEnum broadening_species, + const linemixing::species_data& rovib_data, + const Vector& dipr, + const AtmPoint& atm); +} // namespace lbl::voigt::ecs::sphtop diff --git a/src/core/lbl/lbl_lineshape_voigt_ecs_stotop.cpp b/src/core/lbl/lbl_lineshape_voigt_ecs_stotop.cpp new file mode 100644 index 0000000000..a223250418 --- /dev/null +++ b/src/core/lbl/lbl_lineshape_voigt_ecs_stotop.cpp @@ -0,0 +1,230 @@ +#include "lbl_lineshape_voigt_ecs_stotop.h" + +#include +#include +#include + +namespace lbl::voigt::ecs::stotop { +#if DO_FAST_WIGNER +#define WIGNER3 fw3jja6 +#define WIGNER6 fw6jja +#else +#define WIGNER3 wig3jj +#define WIGNER6 wig6jj +#endif + +namespace { +Numeric wig3(const Rational& a, + const Rational& b, + const Rational& c, + const Rational& d, + const Rational& e, + const Rational& f) { + return WIGNER3( + a.toInt(2), b.toInt(2), c.toInt(2), d.toInt(2), e.toInt(2), f.toInt(2)); +} + +Numeric wig6(const Rational& a, + const Rational& b, + const Rational& c, + const Rational& d, + const Rational& e, + const Rational& f) { + return WIGNER6( + a.toInt(2), b.toInt(2), c.toInt(2), d.toInt(2), e.toInt(2), f.toInt(2)); +} + +/*! Compute rotational energy for a symmetric top molecule + * + * E(J, K) = B*J*(J+1) + (A-B)*K^2 - D_J*[J*(J+1)]^2 + * - D_JK*J*(J+1)*K^2 - D_K*K^4 + * + * For the ECS basis rates we only need E(L) for the collisional + * angular momentum transfer channel, where L has no K-dependence + * (the basis rates are for the atom-like IOS limit). So we use + * the simple rigid rotor formula E = B*J*(J+1). + * + * Species-specific rotational constants: + * NH3-4111 (14NH3): B0 = 9.9402 cm^{-1} + * PH3-1111 (31PH3): B0 = 4.4522 cm^{-1} + * + * @param[in] isot The isotopologue + * @return A function J -> E(J) in Joule + */ +std::function erot_selection(const SpeciesIsotope& isot) { + // NH3 main isotopologue (14N-1H3) + if (isot == "NH3-4111"_isot) { + return [](const Rational J) -> Numeric { + return Conversion::kaycm2joule(9.9402) * Numeric(J * (J + 1)); + }; + } + + // PH3 main isotopologue (31P-1H3) + if (isot == "PH3-1111"_isot) { + return [](const Rational J) -> Numeric { + return Conversion::kaycm2joule(4.4522) * Numeric(J * (J + 1)); + }; + } + + ARTS_USER_ERROR("{} has no rotational energies for symmetric top ECS in ARTS", + isot.FullName()) + return [](const Rational J) -> Numeric { + return Numeric(J) * std::numeric_limits::signaling_NaN(); + }; +} +} // namespace + +Numeric reduced_dipole(const Rational Jf, const Rational Ji, const Rational K) { + // d(Jf, Ji, K) = (-1)^{K+Jf} sqrt(2*Jf+1) * 3j(Jf, 1, Ji; K, 0, -K) + // This is identical in structure to Eq. (10) of Rodrigues et al. 1997 + // with K replacing the vibrational angular momentum l. + if (not iseven(Jf + K + 1)) + return -sqrtr(2 * Jf + 1) * + wigner3j(Jf, Rational{1}, Ji, K, Rational{0}, -K); + return +sqrtr(2 * Jf + 1) * wigner3j(Jf, Rational{1}, Ji, K, Rational{0}, -K); +} + +void relaxation_matrix_offdiagonal(MatrixView& W, + const QuantumIdentifier& bnd_qid, + const band_data& bnd, + const ArrayOfIndex& sorting, + const SpeciesEnum broadening_species, + const linemixing::species_data& rovib_data, + const Vector& dipr, + const AtmPoint& atm) { + using Conversion::kelvin2joule; + + const Size n = bnd.size(); + if (not n) return; + + // K is a line-level quantum number for symmetric tops. + // Lines with different K are not coupled (ΔK=0 collisions only). + // Read K for each line and find the maximum K for Wigner init. + std::vector Kvec(n); + for (Size i = 0; i < n; i++) { + Kvec[i] = bnd.lines[sorting[i]].qn.at(QuantumNumberType::K).lower; + } + + const Numeric T = atm.temperature; + + const auto erot = erot_selection(bnd_qid.isot); + + const std::array rats{bnd.max(QuantumNumberType::J), + bnd.max(QuantumNumberType::K)}; + const int maxL = wigner_init_size(rats); + + const auto Om = [&]() { + Vector out(maxL); + for (Index i = 0; i < maxL; i++) + out[i] = rovib_data.Omega(atm.temperature, + bnd.front().ls.T0, + broadening_species == SpeciesEnum::Bath + ? atm.mean_mass() + : atm.mean_mass(broadening_species), + bnd_qid.isot.mass, + erot(Rational{i}), + erot(Rational{i - 2})); + return out; + }(); + + const auto Q = [&]() { + Vector out(maxL); + for (Index i = 0; i < maxL; i++) + out[i] = rovib_data.Q( + Rational{i}, atm.temperature, bnd.front().ls.T0, erot(Rational{i})); + return out; + }(); + + // The coupling for symmetric tops with ΔK=0 is identical to the + // linear molecule (Hartmann) case with K replacing l: + // + // R(J→J'; K) = (-1)^{J+J'+1} (2J'+1) Ω(J) + // × Σ_L (2L+1) 3j(J, J', L; K, -K, 0) × 3j(Jf, Jf', L; K, -K, 0) + // × 6j(Ji, Jf, 1; Jf', Ji', L) × Q(L)/Ω(L) + // + // For pure rotational ΔK=0 transitions: Ji = J (upper), Jf = J±1 (lower). + // Ki = Kf = K is constant for the entire sub-band. + + arts_wigner_thread_init(maxL); + for (Size i = 0; i < n; i++) { + auto& J = bnd.lines[sorting[i]].qn.at(QuantumNumberType::J); + const Rational Ji = J.upper; + const Rational Jf = J.lower; + const Rational Ki = Kvec[i]; + + for (Size j = 0; j < n; j++) { + if (i == j) continue; + + // Only couple lines within the same K sub-band + if (Kvec[j] != Ki) continue; + + auto& J_p = bnd.lines[sorting[j]].qn.at(QuantumNumberType::J); + const Rational Ji_p = J_p.upper; + const Rational Jf_p = J_p.lower; + + // Only compute the downward element (J' ≤ J) + if (Jf_p > Jf) continue; + + Index L = std::max(std::abs((Ji - Ji_p).toIndex()), + std::abs((Jf - Jf_p).toIndex())); + L += L % 2; + const Index Lf = std::min((Ji + Ji_p).toIndex(), (Jf + Jf_p).toIndex()); + + Numeric sum = 0; + for (; L <= Lf; L += 2) { + const Numeric a = wig3(Ji, Ji_p, Rational{L}, Ki, -Ki, Rational{0}); + const Numeric b = wig3(Jf, Jf_p, Rational{L}, Ki, -Ki, Rational{0}); + const Numeric c = wig6(Ji, Jf, Rational{1}, Jf_p, Ji_p, Rational{L}); + sum += a * b * c * Numeric(2 * L + 1) * Q[L] / Om[L]; + } + const Numeric ECS = Om[Ji.toIndex()]; + const Numeric scl = + ECS * Numeric(2 * Ji_p + 1) * sqrtr((2 * Jf + 1) * (2 * Jf_p + 1)); + sum *= scl; + + // Downward element and detailed balance for upward + W[j, i] = sum; + W[i, j] = sum * std::exp((erot(Jf_p) - erot(Jf)) / kelvin2joule(T)); + } + } + arts_wigner_thread_free(); + + ARTS_USER_ERROR_IF(errno == EDOM, "Cannot compute the wigner symbols") + + // Sum rule correction (same as Hartmann, but within K sub-bands) + for (Size i = 0; i < n; i++) { + Numeric sumlw = 0.0; + Numeric sumup = 0.0; + const Rational Ki = Kvec[i]; + + for (Size j = 0; j < n; j++) { + if (Kvec[j] != Ki) continue; // Only within same K sub-band + + if (j > i) { + sumlw += dipr[j] * W[j, i]; + } else { + sumup += dipr[j] * W[j, i]; + } + } + + const Rational Ji = bnd.lines[sorting[i]].qn.at(QuantumNumberType::J).lower; + for (Size j = i + 1; j < n; j++) { + if (Kvec[j] != Ki) continue; // Only within same K sub-band + + const Rational Jj = + bnd.lines[sorting[j]].qn.at(QuantumNumberType::J).lower; + if (std::abs(sumlw) <= std::abs(sumup) * 1e-6) { + // The sum rule correction would amplify off-diagonal elements + // excessively. This can happen for Q-branch (ΔJ=0) transitions + // where the coupling structure differs from P/R branches. + // Zero the elements to prevent NaN propagation. + W[j, i] = 0.0; + W[i, j] = 0.0; + } else { + W[j, i] *= -sumup / sumlw; + W[i, j] = W[j, i] * std::exp((erot(Ji) - erot(Jj)) / kelvin2joule(T)); + } + } + } +} +} // namespace lbl::voigt::ecs::stotop diff --git a/src/core/lbl/lbl_lineshape_voigt_ecs_stotop.h b/src/core/lbl/lbl_lineshape_voigt_ecs_stotop.h new file mode 100644 index 0000000000..4977fa4533 --- /dev/null +++ b/src/core/lbl/lbl_lineshape_voigt_ecs_stotop.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "lbl_data.h" +#include "lbl_lineshape_linemixing.h" + +namespace lbl::voigt::ecs::stotop { +/*! Returns the reduced dipole for a symmetric top molecule + * + * For symmetric top pure rotational transitions (ΔK=0, Δv=0), + * the reduced dipole matrix element is: + * + * d(Jf, Ji, K) = (-1)^{Jf+K+1} sqrt(2*Jf+1) * 3j(Jf, 1, Ji; K, 0, -K) + * + * @param[in] Jf Lower state total angular momentum + * @param[in] Ji Upper state total angular momentum + * @param[in] K Projection of angular momentum on molecular axis (same for + * upper and lower) + * @return The reduced dipole + */ +Numeric reduced_dipole(const Rational Jf, + const Rational Ji, + const Rational K); + +/*! Compute the off-diagonal elements of the relaxation matrix + * for symmetric top molecules (NH3, PH3, etc.) + * + * Uses the ECS-EP (Energy Corrected Sudden with Exponential Power law) + * formalism. The coupling between lines ℓ and ℓ' within a K-sub-band + * involves 3j and 6j symbols with K replacing the vibrational angular + * momentum quantum number l used in the linear molecule (CO2) case. + * Lines with different K are not coupled (ΔK=0 collisions). + * K is read from each line's quantum numbers. + * + * @param[in,out] W Relaxation matrix (diagonal already set) + * @param[in] bnd_qid Band quantum identifier + * @param[in] bnd Band data (lines carry K quantum number) + * @param[in] sorting Index sorting of the lines + * @param[in] broadening_species Which broadening species to use + * @param[in] rovib_data ECS-EP parameters for the species + * @param[in] dipr Reduced dipole moments + * @param[in] atm Atmospheric point + */ +void relaxation_matrix_offdiagonal(MatrixView& W, + const QuantumIdentifier& bnd_qid, + const band_data& bnd, + const ArrayOfIndex& sorting, + const SpeciesEnum broadening_species, + const linemixing::species_data& rovib_data, + const Vector& dipr, + const AtmPoint& atm); +} // namespace lbl::voigt::ecs::stotop diff --git a/src/core/lbl/lbl_voigt.cpp b/src/core/lbl/lbl_voigt.cpp index d92359ce07..5db90f290d 100644 --- a/src/core/lbl/lbl_voigt.cpp +++ b/src/core/lbl/lbl_voigt.cpp @@ -9,6 +9,8 @@ bool is_voigt(LineByLineLineshape lsm) { switch (lsm) { case VP_ECS_HARTMANN: case VP_ECS_MAKAROV: + case VP_ECS_STOTOP: + case VP_ECS_SPHTOP: case VP_LTE: case VP_LINE_NLTE: case VP_LTE_MIRROR: return true; diff --git a/src/core/matpack/rational.h b/src/core/matpack/rational.h index 05117f29cc..01e63a464c 100644 --- a/src/core/matpack/rational.h +++ b/src/core/matpack/rational.h @@ -248,7 +248,18 @@ struct Rational { return Rational(std::forward(a)) % b; } - constexpr auto operator<=>(const Rational& b) const noexcept = default; + constexpr auto operator<=>(const Rational& b) const noexcept { + //! REMINDER: This code works because GCD is guaranteed to have a positive denom + return (denom == b.denom) ? (numer <=> b.numer) : [*this, b] { + Index r1 = numer / denom; + Index r2 = b.numer / b.denom; + auto res2 = r1 <=> r2; + return res2 != std::strong_ordering::equal + ? res2 + : ((numer % denom) * b.denom) <=> + ((b.numer % b.denom) * denom); + }(); + }; constexpr bool operator==(const Rational& b) const noexcept { return numer == b.numer and denom == b.denom; @@ -266,7 +277,7 @@ struct Rational { template friend constexpr auto operator<=>(T&& b, const Rational& a) noexcept { - return a <=> Rational{std::forward(b)}; + return Rational{std::forward(b)} <=> a; } template diff --git a/src/core/options/arts_options.cc b/src/core/options/arts_options.cc index 687d210399..2b2e8b006d 100644 --- a/src/core/options/arts_options.cc +++ b/src/core/options/arts_options.cc @@ -648,7 +648,13 @@ parameters that are mapped to the species identifier. "Voigt using Makarov's method of error-corrected sudden for line mixing of O2"}, Value{ "VP_ECS_HARTMANN", - "Voigt using Hartmann's method of error-corrected sudden for line mixing of CO2"}}, + "Voigt using Hartmann's method of error-corrected sudden for line mixing of CO2"}, + Value{ + "VP_ECS_STOTOP", + "[WIP] [UNTESTED] Voigt using error-corrected sudden for line mixing of symmetric top molecules (NH3, PH3)"}, + Value{ + "VP_ECS_SPHTOP", + "[WIP] [UNTESTED] Voigt using error-corrected sudden for line mixing of spherical top molecules (CH4)"}}, }); opts.emplace_back(EnumeratedOption{ diff --git a/src/core/tests/test_laginterp.cpp b/src/core/tests/test_laginterp.cpp index 0a3b65c75f..a0ff8b15ec 100644 --- a/src/core/tests/test_laginterp.cpp +++ b/src/core/tests/test_laginterp.cpp @@ -584,10 +584,10 @@ int main() { print_time_points(); - test_variant_lag<0>(); - test_variant_lag<1>(); - test_variant_lag<2>(); - test_variant_lag<3>(); - test_variant_lag<4>(); - test_variant_lag<5>(); + test_variant_lag<0z>(); + test_variant_lag<1z>(); + test_variant_lag<2z>(); + test_variant_lag<3z>(); + test_variant_lag<4z>(); + test_variant_lag<5z>(); } diff --git a/src/m_linemixing.cc b/src/m_linemixing.cc index 509b5cee97..f7abb390cc 100644 --- a/src/m_linemixing.cc +++ b/src/m_linemixing.cc @@ -131,3 +131,91 @@ void abs_ecs_dataAddTran2011(LinemixingEcsData& abs_ecs_data) { data(T0, {Conversion::angstrom2meter(5.5)}); } } + +void abs_ecs_dataAddNH3(LinemixingEcsData& abs_ecs_data) { + ARTS_TIME_REPORT + + using enum LineShapeModelType; + using data = lbl::temperature::data; + + auto& ecs = abs_ecs_data["NH3-4111"_isot]; + + // H2 broadening parameters + auto& h2 = ecs[SpeciesEnum::Hydrogen]; + h2.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.040), 0.73}); + h2.lambda = data(T0, {0.65}); + h2.beta = data(T0, {0.006}); + h2.collisional_distance = data(T0, {Conversion::angstrom2meter(2.3)}); + + // He broadening parameters + auto& he = ecs[SpeciesEnum::Helium]; + he.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.018), 0.55}); + he.lambda = data(T0, {0.58}); + he.beta = data(T0, {0.003}); + he.collisional_distance = data(T0, {Conversion::angstrom2meter(1.8)}); + + auto& nh3 = ecs[SpeciesEnum::Ammonia]; + nh3.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.040), 0.73}); + nh3.lambda = data(T0, {0.65}); + nh3.beta = data(T0, {0.006}); + nh3.collisional_distance = data(T0, {Conversion::angstrom2meter(2.3)}); +} + +void abs_ecs_dataAddPH3(LinemixingEcsData& abs_ecs_data) { + ARTS_TIME_REPORT + + using enum LineShapeModelType; + using data = lbl::temperature::data; + + auto& ecs = abs_ecs_data["PH3-1111"_isot]; + + // H2 broadening parameters (approximate, based on similarity to NH3) + auto& h2 = ecs[SpeciesEnum::Hydrogen]; + h2.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.035), 0.70}); + h2.lambda = data(T0, {0.60}); + h2.beta = data(T0, {0.005}); + h2.collisional_distance = data(T0, {Conversion::angstrom2meter(2.5)}); + + // He broadening parameters (approximate) + auto& he = ecs[SpeciesEnum::Helium]; + he.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.015), 0.50}); + he.lambda = data(T0, {0.55}); + he.beta = data(T0, {0.003}); + he.collisional_distance = data(T0, {Conversion::angstrom2meter(2.0)}); + + // PH3 self-broadening parameters (approximate, based on H2 values) + auto& ph3 = ecs[SpeciesEnum::Phosphine]; + ph3.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.035), 0.70}); + ph3.lambda = data(T0, {0.60}); + ph3.beta = data(T0, {0.005}); + ph3.collisional_distance = data(T0, {Conversion::angstrom2meter(2.5)}); +} + +void abs_ecs_dataAddCH4(LinemixingEcsData& abs_ecs_data) { + ARTS_TIME_REPORT + + using enum LineShapeModelType; + using data = lbl::temperature::data; + + auto& ecs = abs_ecs_data["CH4-211"_isot]; + + // H2 broadening parameters + auto& h2 = ecs[SpeciesEnum::Hydrogen]; + h2.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.060), 0.75}); + h2.lambda = data(T0, {0.70}); + h2.beta = data(T0, {0.008}); + h2.collisional_distance = data(T0, {Conversion::angstrom2meter(2.4)}); + + // He broadening parameters + auto& he = ecs[SpeciesEnum::Helium]; + he.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.025), 0.56}); + he.lambda = data(T0, {0.60}); + he.beta = data(T0, {0.004}); + he.collisional_distance = data(T0, {Conversion::angstrom2meter(1.9)}); + + auto& ch4 = ecs[SpeciesEnum::Methane]; + ch4.scaling = data(T1, {Conversion::kaycm_per_atm2hz_per_pa(0.060), 0.75}); + ch4.lambda = data(T0, {0.70}); + ch4.beta = data(T0, {0.008}); + ch4.collisional_distance = data(T0, {Conversion::angstrom2meter(2.4)}); +} diff --git a/src/workspace_methods.cpp b/src/workspace_methods.cpp index 97e5f183c2..47d963ae9b 100644 --- a/src/workspace_methods.cpp +++ b/src/workspace_methods.cpp @@ -1055,6 +1055,36 @@ This is based on the work of :cite:t:`Rodrigues1997`. .in = {"abs_ecs_data"}, }; + wsm_data["abs_ecs_dataAddNH3"] = { + .desc = R"--(Sets preliminary NH3-4111 band data for ECS. + +[WIP] [UNTESTED] +)--", + .author = {"Richard Larsson"}, + .out = {"abs_ecs_data"}, + .in = {"abs_ecs_data"}, + }; + + wsm_data["abs_ecs_dataAddPH3"] = { + .desc = R"--(Sets preliminary PH3-1111 band data for ECS. + +[WIP] [UNTESTED] +)--", + .author = {"Richard Larsson"}, + .out = {"abs_ecs_data"}, + .in = {"abs_ecs_data"}, + }; + + wsm_data["abs_ecs_dataAddCH4"] = { + .desc = R"--(Sets preliminary CH4-211 band data for ECS. + +[WIP] [UNTESTED] +)--", + .author = {"Richard Larsson"}, + .out = {"abs_ecs_data"}, + .in = {"abs_ecs_data"}, + }; + wsm_data["abs_ecs_dataInit"] = { .desc = R"--(Resets/initializes the ECS data. )--",