star-line

Structure for accelerating line importance sampling
git clone git://git.meso-star.com/star-line.git
Log | Files | Refs | README | LICENSE

sln_line.c (18792B)


      1 /* Copyright (C) 2022, 2026 |Méso|Star> (contact@meso-star.com)
      2  * Copyright (C) 2026 Université de Lorraine
      3  * Copyright (C) 2022 Centre National de la Recherche Scientifique
      4  * Copyright (C) 2022 Université Paul Sabatier
      5  *
      6  * This file is part of Star-Line.
      7  *
      8  * This program is free software: you can redistribute it and/or modify
      9  * it under the terms of the GNU General Public License as published by
     10  * the Free Software Foundation, either version 3 of the License, or
     11  * (at your option) any later version.
     12  *
     13  * This program is distributed in the hope that it will be useful,
     14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
     16  * GNU General Public License for more details.
     17  *
     18  * You should have received a copy of the GNU General Public License
     19  * along with this program. If not, see <http://www.gnu.org/licenses/>. */
     20 
     21 #define _POSIX_C_SOURCE 200112L /* nextafterf support */
     22 
     23 #include "sln_device_c.h"
     24 #include "sln_line.h"
     25 #include "sln_tree_c.h"
     26 
     27 #include <lblu.h>
     28 #include <star/shtr.h>
     29 
     30 #include <rsys/algorithm.h>
     31 #include <rsys/cstr.h>
     32 #include <rsys/dynamic_array_double.h>
     33 #include <rsys/math.h>
     34 
     35 #include <math.h> /* nextafterf */
     36 
     37 #define T_REF 296.0 /* K */
     38 #define AVOGADRO_NUMBER 6.02214076e23 /* molec.mol^-1 */
     39 #define PERFECT_GAZ_CONSTANT 8.2057e-5 /* m^3.atm.mol^-1.K^-1 */
     40 
     41 #define MIN_NVERTICES_HINT 8
     42 #define MAX_NVERTICES_HINT 128
     43 STATIC_ASSERT(IS_POW2(MIN_NVERTICES_HINT), MIN_NVERTICES_HINT_is_not_a_pow2);
     44 STATIC_ASSERT(IS_POW2(MIN_NVERTICES_HINT), MAX_NVERTICES_HINT_is_not_a_pow2);
     45 
     46 /*******************************************************************************
     47  * Helper function
     48  ******************************************************************************/
     49 static INLINE double
     50 line_intensity
     51   (const double intensity_ref, /* Reference intensity [cm^-1/(molec.cm^2)] */
     52    const double lower_state_energy, /* [cm^-1] */
     53    const double partition_function,
     54    const double temperature, /* [K] */
     55    const double temperature_ref, /* [K] */
     56    const double wavenumber) /* [cm^-1] */
     57 {
     58   const double C2 = 1.4388; /* 2nd Planck constant [K.cm] */
     59 
     60   const double fol = /* TODO ask to Yaniss why this variable is named fol */
     61     (1-exp(-C2*wavenumber/temperature))
     62   / (1-exp(-C2*wavenumber/temperature_ref));
     63 
     64   const double tmp =
     65     exp(-C2*lower_state_energy/temperature)
     66   / exp(-C2*lower_state_energy/temperature_ref);
     67 
     68   return intensity_ref * partition_function * tmp * fol ;
     69 }
     70 
     71 static res_T
     72 line_profile_factor
     73   (const struct sln_tree* tree,
     74    const struct shtr_line* shtr_line,
     75    const double concentration,
     76    const double pressure,
     77    const double temperature,
     78    double* out_profile_factor)
     79 {
     80   /* Star-HITRAN data */
     81   struct shtr_molecule molecule = SHTR_MOLECULE_NULL;
     82   const struct shtr_isotope* isotope = NULL;
     83 
     84   /* Mixture parameters */
     85   const struct sln_molecule* mol_params = NULL;
     86 
     87   /* Miscellaneous */
     88   double iso_abundance;
     89   double density; /* In molec.cm^-3 */
     90   double intensity, intensity_ref; /* In cm^-1/(molec.cm^2) */
     91   double Q, Q_T, Q_Tref; /* Partition function */
     92   double nu_c; /* In cm^-1 */
     93   double profile_factor; /* In m^-1.cm^-1 */
     94   double gj; /* State independant degeneracy factor */
     95   double Ps; /* In atm */
     96   double T; /* Temperature */
     97   int molid; /* Molecule id */
     98   int isoid; /* Isotope id local to its molecule */
     99 
    100   res_T res = RES_OK;
    101   ASSERT(tree && shtr_line && out_profile_factor);
    102 
    103   /* Fetch the molecule data */
    104   mol_params = tree->args.molecules + shtr_line->molecule_id;
    105   SHTR(isotope_metadata_find_molecule
    106     (tree->args.metadata, shtr_line->molecule_id, &molecule));
    107   ASSERT(!SHTR_MOLECULE_IS_NULL(&molecule));
    108   ASSERT(molecule.nisotopes > (size_t)shtr_line->isotope_id_local);
    109   isotope = molecule.isotopes + shtr_line->isotope_id_local;
    110 
    111   nu_c = line_center(shtr_line, pressure);
    112 
    113   /* Compute the intensity */
    114   Ps = pressure * concentration;
    115   density = (AVOGADRO_NUMBER * Ps);
    116   density = density / (PERFECT_GAZ_CONSTANT * temperature);
    117   density = density * 1e-6; /* Convert in molec.cm^-3 */
    118 
    119   /* Compute the partition function */
    120   Q_Tref = isotope->Q296K;
    121   molid = shtr_line->molecule_id;
    122   isoid = shtr_line->isotope_id_local+1/*Local indices start at 1 in BD_TIPS*/;
    123   T = temperature;
    124   BD_TIPS_2017(&molid, &T, &isoid, &gj, &Q_T);
    125   if(Q_T <= 0) {
    126     ERROR(tree->sln,
    127       "molecule %d: isotope %d: invalid partition function at T=%g\n",
    128       molid, isoid, T);
    129     res = RES_BAD_ARG;
    130     goto error;
    131   }
    132 
    133   Q = Q_Tref/Q_T;
    134 
    135   /* Compute the intensity */
    136   if(!mol_params->non_default_isotope_abundances) { /* Use default abundance */
    137     intensity_ref = shtr_line->intensity;
    138   } else {
    139     iso_abundance = mol_params->isotopes[shtr_line->isotope_id_local].abundance;
    140     intensity_ref = shtr_line->intensity/isotope->abundance*iso_abundance;
    141   }
    142   intensity = line_intensity(intensity_ref, shtr_line->lower_state_energy, Q,
    143     temperature, T_REF, nu_c);
    144 
    145   profile_factor = 1.e2 * density * intensity; /* In m^-1.cm^-1 */
    146 
    147 exit:
    148   *out_profile_factor = profile_factor;
    149   return res;
    150 error:
    151   profile_factor = NaN;
    152   goto exit;
    153 }
    154 
    155 /* Regularly mesh the interval [wavenumber, wavenumber+spectral_length[. Note
    156  * that the upper bound is excluded, this means that the last vertex of the
    157  * interval is not emitted */
    158 static INLINE res_T
    159 regular_mesh
    160   (const double wavenumber, /* Wavenumber where the mesh begins [cm^-1] */
    161    const double spectral_length, /* Size of the spectral interval to mesh [cm^-1] */
    162    const size_t nvertices, /* #vertices to issue */
    163    struct darray_double* wavenumbers) /* List of issued vertices */
    164 {
    165   /* Do not issue the vertex on the upper bound of the spectral range. That's
    166    * why we assume that the number of steps is equal to the number of vertices
    167    * and not to the number of vertices minus 1 */
    168   const double step = spectral_length / (double)nvertices;
    169   size_t ivtx;
    170   res_T res = RES_OK;
    171   ASSERT(spectral_length > 0 && wavenumbers);
    172 
    173   FOR_EACH(ivtx, 0, nvertices) {
    174     const double nu = wavenumber + (double)ivtx*step;
    175     res = darray_double_push_back(wavenumbers, &nu);
    176     if(res != RES_OK) goto error;
    177   }
    178 exit:
    179   return res;
    180 error:
    181   goto exit;
    182 }
    183 
    184 /* The line is regularly discretized into a set of fragments of variable size.
    185  * Their discretization is finer for the fragments around the center of the line
    186  * and becomes coarser as the fragments move away from it. Note that a line is
    187  * symmetrical in its center. As a consequence, the returned list is only the
    188  * set of wavenumbers from the line center to its upper bound. */
    189 static res_T
    190 regular_mesh_fragmented
    191   (const struct sln_tree* tree,
    192    const struct sln_line* line,
    193    const size_t nvertices,
    194    struct darray_double* wavenumbers) /* List of issued vertices */
    195 {
    196   /* Fragment parameters */
    197   double fragment_length = 0;
    198   double fragment_nu_min = 0; /* Lower bound of the fragment */
    199   size_t fragment_nvtx = 0; /* #vertices into the fragment */
    200   size_t nfragments = 0; /* Number of fragments already meshed */
    201 
    202   /* Miscellaneous */
    203   const struct sln_molecule* mol_params = NULL;
    204   double line_nu_min = 0; /* In cm^-1 */
    205   double line_nu_max = 0; /* In cm^-1 */
    206   res_T res = RES_OK;
    207 
    208   ASSERT(tree && line && wavenumbers);
    209   ASSERT(IS_POW2(nvertices));
    210 
    211   /* TODO check mol params */
    212   mol_params = tree->args.molecules + line->molecule_id;
    213 
    214   /* Compute the spectral range of the line from its center to its cutoff */
    215   line_nu_min = line->wavenumber;
    216   line_nu_max = line->wavenumber + mol_params->cutoff;
    217 
    218   /* Define the size of a fragment as the width of the line at mid-height for a
    219    * Lorentz profile */
    220   fragment_length = line->gamma_l;
    221 
    222   /* Define the number of vertices for the first interval in [nu, gamma_l] */
    223   fragment_nu_min = line_nu_min;
    224   fragment_nvtx = MMAX(nvertices/2, 2);
    225 
    226   while(fragment_nu_min < line_nu_max) {
    227     const double spectral_length =
    228       MMIN(fragment_length, line_nu_max - fragment_nu_min);
    229 
    230     res = regular_mesh
    231       (fragment_nu_min, spectral_length, fragment_nvtx, wavenumbers);
    232     if(res != RES_OK) goto error;
    233 
    234     /* After the third fragment, exponentially increase the fragment length */
    235     if(++nfragments >= 3) fragment_length *= 2;
    236 
    237     fragment_nu_min += fragment_length;
    238     fragment_nvtx = MMAX(fragment_nvtx/2, 2);
    239   }
    240 
    241   /* Register the last vertex, i.e. the upper bound of the spectral range */
    242   res = darray_double_push_back(wavenumbers, &line_nu_max);
    243   if(res != RES_OK) goto error;
    244 
    245 exit:
    246   return res;
    247 error:
    248   ERROR(tree->sln, "Error meshing the line -- %s.\n", res_to_cstr(res));
    249   goto exit;
    250 }
    251 
    252 /* Calculate line values for a set of wave numbers */
    253 static res_T
    254 eval_mesh
    255   (const struct sln_tree* tree,
    256    const struct sln_line* line,
    257    const struct darray_double* wavenumbers,
    258    struct darray_double* values)
    259 {
    260   const double* nu = NULL;
    261   double* ha = NULL;
    262   size_t ivertex, nvertices;
    263   res_T res = RES_OK;
    264   ASSERT(tree && line && wavenumbers && values);
    265 
    266   nvertices = darray_double_size_get(wavenumbers);
    267   ASSERT(nvertices);
    268 
    269   res = darray_double_resize(values, nvertices);
    270   if(res != RES_OK) goto error;
    271 
    272   nu = darray_double_cdata_get(wavenumbers);
    273   ha = darray_double_data_get(values);
    274   FOR_EACH(ivertex, 0, nvertices) {
    275     ha[ivertex] = sln_line_eval(tree, line, nu[ivertex]);
    276   }
    277 
    278 exit:
    279   return res;
    280 error:
    281   ERROR(tree->sln, "Error evaluating the line mesh -- %s.\n", res_to_cstr(res));
    282   goto exit;
    283 }
    284 
    285 static void
    286 snap_mesh_to_upper_bound
    287   (const struct darray_double* wavenumbers,
    288    struct darray_double* values)
    289 {
    290   double* ha = NULL;
    291   size_t ivertex, nvertices;
    292   ASSERT(wavenumbers && values);
    293   ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values));
    294   (void)wavenumbers;
    295 
    296   ha = darray_double_data_get(values);
    297   nvertices = darray_double_size_get(wavenumbers);
    298 
    299   /* Ensure that the stored vertex value is an exclusive upper bound of the
    300    * original value. We do this by storing a value in single precision that is
    301    * strictly greater than its encoding in double precision */
    302   if(ha[0] != (float)ha[0]) {
    303     ha[0] = nextafterf((float)ha[0], FLT_MAX);
    304   }
    305 
    306   /* We have meshed the upper half of the line which is a strictly decreasing
    307    * function. To ensure that the mesh is an upper limit of this function,
    308    * simply align the value of each vertex with the value of the preceding
    309    * vertex */
    310   FOR_EACH_REVERSE(ivertex, nvertices-1, 0) {
    311     ha[ivertex] = ha[ivertex-1];
    312   }
    313 }
    314 
    315 static INLINE int
    316 cmp_dbl(const void* a, const void* b)
    317 {
    318   const double key = *((const double*)a);
    319   const double item = *((const double*)b);
    320   if(key < item) return -1;
    321   if(key > item) return +1;
    322   return 0;
    323 }
    324 
    325 /* Return the value of the vertex whose wavenumber is greater than 'nu' */
    326 static INLINE double
    327 next_vertex_value
    328   (const double nu,
    329    const struct darray_double* wavenumbers,
    330    const struct darray_double* values)
    331 {
    332   const double* wnum = NULL;
    333   size_t ivertex = 0;
    334   ASSERT(wavenumbers && values);
    335 
    336   wnum = search_lower_bound
    337     (&nu,
    338      darray_double_cdata_get(wavenumbers),
    339      darray_double_size_get(wavenumbers),
    340      sizeof(double),
    341      cmp_dbl);
    342   ASSERT(wnum); /* It necessary exists */
    343 
    344   ivertex = (size_t)(wnum - darray_double_cdata_get(wavenumbers));
    345   ASSERT(ivertex < darray_double_size_get(values));
    346 
    347   return darray_double_cdata_get(values)[ivertex];
    348 }
    349 
    350 /* Append the line mesh into the vertices array */
    351 static res_T
    352 save_line_mesh
    353   (struct sln_tree* tree,
    354    const struct sln_line* line,
    355    const struct darray_double* wavenumbers,
    356    const struct darray_double* values,
    357    struct darray_vertex* vertices, /* buffer in which vertices are added */
    358    size_t vertices_range[2]) /* Range into which the line vertices are saved */
    359 {
    360   const double* wnums = NULL;
    361   const double* vals = NULL;
    362   size_t nvertices = 0;
    363   size_t nwavenumbers = 0;
    364   size_t line_nvertices = 0;
    365   size_t ivertex = 0;
    366   size_t i = 0;
    367   res_T res = RES_OK;
    368 
    369   ASSERT(tree && line && wavenumbers && values && vertices && vertices_range);
    370   ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values));
    371 
    372   nvertices = darray_vertex_size_get(vertices);
    373   nwavenumbers = darray_double_size_get(wavenumbers);
    374 
    375   /* Compute the overall number of vertices of the line */
    376   line_nvertices = nwavenumbers
    377     * 2 /* The line is symmetrical in its center */
    378     - 1;/* Do not duplicate the line center */
    379 
    380   /* Allocate the list of line vertices */
    381   res = darray_vertex_resize(vertices, nvertices + line_nvertices);
    382   if(res != RES_OK) goto error;
    383 
    384   wnums = darray_double_cdata_get(wavenumbers);
    385   vals = darray_double_cdata_get(values);
    386 
    387   i = nvertices;
    388 
    389   #define MIRROR(Nu) (2*line->wavenumber - (Nu))
    390 
    391   /* Copy the vertices of the line for its lower half */
    392   FOR_EACH_REVERSE(ivertex, nwavenumbers-1, 0) {
    393     struct sln_vertex* vtx = darray_vertex_data_get(vertices) + i++;
    394     const double nu = MIRROR(wnums[ivertex]);
    395     const double ha = vals[ivertex];
    396 
    397     vtx->wavenumber = (float)nu;
    398     vtx->ka = (float)ha;
    399   }
    400 
    401   /* Copy the vertices of the line for its upper half */
    402   FOR_EACH(ivertex, 0, nwavenumbers) {
    403     struct sln_vertex* vtx = darray_vertex_data_get(vertices) + i++;
    404     const double nu = wnums[ivertex];
    405     const double ha = vals[ivertex];
    406 
    407     vtx->wavenumber = (float)nu;
    408     vtx->ka = (float)ha;
    409   }
    410 
    411   #undef MIRROR
    412 
    413   ASSERT(i == nvertices + line_nvertices);
    414 
    415   /* Setup the range of the line vertices */
    416   vertices_range[0] = nvertices;
    417   vertices_range[1] = i-1; /* Make the bound inclusive */
    418 
    419 exit:
    420   return res;
    421 error:
    422   darray_vertex_resize(vertices, nvertices);
    423   ERROR(tree->sln, "Error while recording line vertices -- %s.\n",
    424     res_to_cstr(res));
    425   goto exit;
    426 }
    427 
    428 /*******************************************************************************
    429  * Local function
    430  ******************************************************************************/
    431 res_T
    432 line_setup
    433   (const struct sln_tree* tree,
    434    const size_t iline,
    435    const struct sln_thermo_props* props,
    436    struct sln_line* line)
    437 {
    438   struct shtr_molecule molecule = SHTR_MOLECULE_NULL;
    439   struct shtr_line shtr_line = SHTR_LINE_NULL;
    440   double concentration = 0;
    441   double molar_mass = 0; /*[kg.mol^-1]*/
    442   double pressure = 0; /*[atm]*/
    443   double temperature = 0; /*[K]*/
    444   res_T res = RES_OK;
    445 
    446   ASSERT(tree && line);
    447 
    448   SHTR(line_list_at(tree->args.lines, iline, &shtr_line));
    449   SHTR(isotope_metadata_find_molecule
    450     (tree->args.metadata, shtr_line.molecule_id, &molecule));
    451   ASSERT(!SHTR_MOLECULE_IS_NULL(&molecule));
    452   ASSERT(molecule.nisotopes > (size_t)shtr_line.isotope_id_local);
    453 
    454   if(!props) { /* Use thermo properties used to build the tree */
    455     concentration = tree->args.molecules[shtr_line.molecule_id].concentration;
    456     pressure = tree->args.pressure; /*[atm]*/
    457     temperature = tree->args.temperature; /*[K]*/
    458   } else {
    459     concentration = props->concentrations[shtr_line.molecule_id];
    460     pressure = props->pressure; /*[atm]*/
    461     temperature = props->temperature; /*[K]*/
    462   }
    463 
    464   /* Convert the molar mass of the line from g.mol^-1 to kg.mol^-1 */
    465   molar_mass = molecule.isotopes[shtr_line.isotope_id_local].molar_mass*1e-3;
    466 
    467   /* Setup the line */
    468   res = line_profile_factor(tree, &shtr_line, concentration, pressure,
    469     temperature, &line->profile_factor);
    470   if(res != RES_OK) goto error;
    471 
    472   line->wavenumber = line_center(&shtr_line, pressure);
    473   line->gamma_d = sln_compute_line_half_width_doppler
    474     (line->wavenumber, molar_mass, temperature);
    475   line->gamma_l = sln_compute_line_half_width_lorentz
    476     (shtr_line.gamma_air, shtr_line.gamma_self, temperature,
    477      pressure, shtr_line.n_air, concentration);
    478   line->molecule_id = shtr_line.molecule_id;
    479 
    480 exit:
    481   return res;
    482 error:
    483   goto exit;
    484 }
    485 
    486 res_T
    487 line_mesh
    488   (struct sln_tree* tree,
    489    const size_t iline,
    490    const size_t nvertices_hint,
    491    struct darray_vertex* vertices, /* buffer in which vertices are added */
    492    size_t vertices_range[2]) /* out. Bounds are inclusive */
    493 {
    494   /* The line */
    495   struct sln_line line = SLN_LINE_NULL;
    496 
    497   /* Temporary mesh */
    498   struct darray_double values; /* List of evaluated values */
    499   struct darray_double wavenumbers; /* List of considered wavenumbers */
    500   size_t nvertices_adjusted = 0; /* computed from nvertices_hint */
    501 
    502   /* Miscellaneous */
    503   res_T res = RES_OK;
    504 
    505   /* Pre-conditions */
    506   ASSERT(tree && vertices && nvertices_hint);
    507 
    508   darray_double_init(tree->sln->allocator, &values);
    509   darray_double_init(tree->sln->allocator, &wavenumbers);
    510 
    511   /* Setup the line wrt molecule concentration, isotope abundance, temperature
    512    * and pressure */
    513   res = line_setup(tree, iline, NULL/*default thermo props*/, &line);
    514   if(res != RES_OK) goto error;
    515 
    516   /* Adjust the hint on the number of vertices. This is not actually the real
    517    * number of vertices but an adjusted hint on it. This new value ensures that
    518    * it is a power of 2 included in [MIN_NVERTICES_HINT, MAX_NVERTICES_HINT] */
    519   nvertices_adjusted = CLAMP
    520     (nvertices_hint, MIN_NVERTICES_HINT, MAX_NVERTICES_HINT);
    521   nvertices_adjusted = round_up_pow2(nvertices_adjusted);
    522 
    523   /* Emit the vertex coordinates, i.e. the wavenumbers */
    524   res = regular_mesh_fragmented(tree, &line, nvertices_adjusted, &wavenumbers);
    525   if(res != RES_OK) goto error;
    526 
    527   /* Evaluate the mesh vertices, i.e. define the line value for the list of
    528    * wavenumbers */
    529   eval_mesh(tree, &line, &wavenumbers, &values);
    530 
    531   switch(tree->args.mesh_type) {
    532     case SLN_MESH_UPPER:
    533       snap_mesh_to_upper_bound(&wavenumbers, &values);
    534       break;
    535     case SLN_MESH_FIT: /* Do nothing */ break;
    536     default: FATAL("Unreachable code.\n"); break;
    537   }
    538 
    539   res = save_line_mesh
    540     (tree, &line, &wavenumbers, &values, vertices, vertices_range);
    541   if(res != RES_OK) goto error;
    542 
    543 exit:
    544   darray_double_release(&values);
    545   darray_double_release(&wavenumbers);
    546   return res;
    547 error:
    548   goto exit;
    549 }
    550 
    551 /*******************************************************************************
    552  * Exported functions
    553  ******************************************************************************/
    554 double
    555 sln_line_eval
    556   (const struct sln_tree* tree,
    557    const struct sln_line* line,
    558    const double wavenumber)
    559 {
    560   const struct sln_molecule* mol_params = NULL;
    561   double profile = 0;
    562   ASSERT(tree && line);
    563 
    564   /* Retrieve the molecular parameters of the line to be mesh */
    565   mol_params = tree->args.molecules + line->molecule_id;
    566 
    567   if(wavenumber < line->wavenumber - mol_params->cutoff
    568   || wavenumber > line->wavenumber + mol_params->cutoff) {
    569     return 0;
    570   }
    571 
    572   switch(tree->args.line_profile) {
    573     case SLN_LINE_PROFILE_VOIGT:
    574       profile = sln_compute_voigt_profile
    575         (wavenumber, line->wavenumber, line->gamma_d, line->gamma_l);
    576       break;
    577     default: FATAL("Unreachable code.\n"); break;
    578   }
    579   return line->profile_factor * profile;
    580 }