star-line

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

sln_line.c (17900B)


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