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 (18022B)


      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->isotope_abundances[shtr_line->isotope_id_local];
    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(wavenumber >= 0  && 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 fixed size.
    180  * Their discretization is finer for the fragments around the center of the
    181  * line and becomes coarser as the fragments move away from it. Note that a
    182  * line is symmetrical in its center. As a consequence, the returned list is
    183  * only the 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 
    196   /* Miscellaneous */
    197   const struct sln_molecule* mol_params = NULL;
    198   double line_nu_min = 0; /* In cm^-1 */
    199   double line_nu_max = 0; /* In cm^-1 */
    200   res_T res = RES_OK;
    201 
    202   ASSERT(tree && line && wavenumbers);
    203   ASSERT(IS_POW2(nvertices));
    204 
    205   /* TODO check mol params */
    206   mol_params = tree->args.molecules + line->molecule_id;
    207 
    208   /* Compute the spectral range of the line from its center to its cutoff */
    209   line_nu_min = line->wavenumber;
    210   line_nu_max = line->wavenumber + mol_params->cutoff;
    211 
    212   /* Define the size of a fragment as the width of the line at mid-height for a
    213    * Lorentz profile */
    214   fragment_length = line->gamma_l;
    215 
    216   /* Define the number of vertices for the first interval in [nu, gamma_l] */
    217   fragment_nu_min = line_nu_min;
    218   fragment_nvtx = MMAX(nvertices/2, 2);
    219 
    220   while(fragment_nu_min < line_nu_max) {
    221     const double spectral_length =
    222       MMIN(fragment_length, line_nu_max - fragment_nu_min);
    223 
    224     res = regular_mesh
    225       (fragment_nu_min, spectral_length, fragment_nvtx, wavenumbers);
    226     if(res != RES_OK) goto error;
    227 
    228     fragment_nu_min += fragment_length;
    229     fragment_nvtx = MMAX(fragment_nvtx/2, 2);
    230   }
    231 
    232   /* Register the last vertex, i.e. the upper bound of the spectral range */
    233   res = darray_double_push_back(wavenumbers, &line_nu_max);
    234   if(res != RES_OK) goto error;
    235 
    236 exit:
    237   return res;
    238 error:
    239   ERROR(tree->sln, "Error meshing the line -- %s.\n", res_to_cstr(res));
    240   goto exit;
    241 }
    242 
    243 /* Calculate line values for a set of wave numbers */
    244 static res_T
    245 eval_mesh
    246   (const struct sln_tree* tree,
    247    const struct line* line,
    248    const struct darray_double* wavenumbers,
    249    struct darray_double* values)
    250 {
    251   const double* nu = NULL;
    252   double* ha = NULL;
    253   size_t ivertex, nvertices;
    254   res_T res = RES_OK;
    255   ASSERT(tree && line && wavenumbers && values);
    256 
    257   nvertices = darray_double_size_get(wavenumbers);
    258   ASSERT(nvertices);
    259 
    260   res = darray_double_resize(values, nvertices);
    261   if(res != RES_OK) goto error;
    262 
    263   nu = darray_double_cdata_get(wavenumbers);
    264   ha = darray_double_data_get(values);
    265   FOR_EACH(ivertex, 0, nvertices) {
    266     ha[ivertex] = line_value(tree, line, nu[ivertex]);
    267   }
    268 
    269 exit:
    270   return res;
    271 error:
    272   ERROR(tree->sln, "Error evaluating the line mesh -- %s.\n", res_to_cstr(res));
    273   goto exit;
    274 }
    275 
    276 static void
    277 snap_mesh_to_upper_bound
    278   (const struct darray_double* wavenumbers,
    279    struct darray_double* values)
    280 {
    281   double* ha = NULL;
    282   size_t ivertex, nvertices;
    283   size_t ivertex_1st;
    284   ASSERT(wavenumbers && values);
    285   ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values));
    286   (void)wavenumbers;
    287 
    288   ha = darray_double_data_get(values);
    289   nvertices = darray_double_size_get(wavenumbers);
    290 
    291   /* Search for the first (non clipped) vertex */
    292   FOR_EACH(ivertex_1st, 0, nvertices) {
    293     if(ha[ivertex_1st] > 0) break;
    294   }
    295   ASSERT(ivertex_1st < nvertices);
    296 
    297   /* Ensure that the stored vertex value is an exclusive upper bound of the
    298    * original value. We do this by storing a value in single precision that is
    299    * strictly greater than its encoding in double precision */
    300   if(ha[ivertex_1st] != (float)ha[ivertex_1st]) {
    301     ha[ivertex_1st] = nextafterf((float)ha[ivertex_1st], FLT_MAX);
    302   }
    303 
    304   /* We have meshed the upper half of the line which is a strictly decreasing
    305    * function. To ensure that the mesh is an upper limit of this function,
    306    * simply align the value of each vertex with the value of the preceding
    307    * vertex */
    308   FOR_EACH_REVERSE(ivertex, nvertices-1, ivertex_1st) {
    309     if(ha[ivertex] < 0) continue; /* The vertex is clipped */
    310     ha[ivertex] = ha[ivertex-1];
    311   }
    312 }
    313 
    314 static INLINE int
    315 cmp_dbl(const void* a, const void* b)
    316 {
    317   const double key = *((const double*)a);
    318   const double item = *((const double*)b);
    319   if(key < item) return -1;
    320   if(key > item) return +1;
    321   return 0;
    322 }
    323 
    324 /* Return the value of the vertex whose wavenumber is greater than 'nu' */
    325 static INLINE double
    326 next_vertex_value
    327   (const double nu,
    328    const struct darray_double* wavenumbers,
    329    const struct darray_double* values)
    330 {
    331   const double* wnum = NULL;
    332   size_t ivertex = 0;
    333   ASSERT(wavenumbers && values);
    334 
    335   wnum = search_lower_bound
    336     (&nu,
    337      darray_double_cdata_get(wavenumbers),
    338      darray_double_size_get(wavenumbers),
    339      sizeof(double),
    340      cmp_dbl);
    341   ASSERT(wnum); /* It necessary exists */
    342 
    343   ivertex = (size_t)(wnum - darray_double_cdata_get(wavenumbers));
    344   ASSERT(ivertex < darray_double_size_get(values));
    345 
    346   return darray_double_cdata_get(values)[ivertex];
    347 }
    348 
    349 /* Append the line mesh into the vertices array */
    350 static res_T
    351 save_line_mesh
    352   (struct sln_tree* tree,
    353    const struct line* line,
    354    const struct darray_double* wavenumbers,
    355    const struct darray_double* values,
    356    size_t vertices_range[2]) /* Range into which the line vertices are saved */
    357 {
    358   const double* wnums = NULL;
    359   const double* vals = NULL;
    360   size_t nvertices = 0;
    361   size_t nwavenumbers = 0;
    362   size_t line_nvertices = 0;
    363   size_t ivertex = 0;
    364   size_t i = 0;
    365   res_T res = RES_OK;
    366 
    367   ASSERT(tree && line && wavenumbers && values && vertices_range);
    368   ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values));
    369 
    370   nvertices = darray_vertex_size_get(&tree->vertices);
    371   nwavenumbers = darray_double_size_get(wavenumbers);
    372 
    373   /* Compute the overall number of vertices of the line */
    374   line_nvertices = nwavenumbers
    375     * 2 /* The line is symmetrical in its center */
    376     - 1;/* Do not duplicate the line center */
    377 
    378   /* Allocate the list of line vertices */
    379   res = darray_vertex_resize(&tree->vertices, nvertices + line_nvertices);
    380   if(res != RES_OK) goto error;
    381 
    382   wnums = darray_double_cdata_get(wavenumbers);
    383   vals = darray_double_cdata_get(values);
    384 
    385   i = nvertices;
    386 
    387   #define MIRROR(Nu) (2*line->wavenumber - (Nu))
    388 
    389   /* Copy the vertices of the line for its lower half */
    390   FOR_EACH_REVERSE(ivertex, nwavenumbers-1, 0) {
    391     struct sln_vertex* vtx = darray_vertex_data_get(&tree->vertices) + i++;
    392     const double nu = MIRROR(wnums[ivertex]);
    393     const double ha = vals[ivertex];
    394 
    395     vtx->wavenumber = (float)nu;
    396     vtx->ka = (float)ha;
    397   }
    398 
    399   /* Copy the vertices of the line for its upper half */
    400   FOR_EACH(ivertex, 0, nwavenumbers) {
    401     struct sln_vertex* vtx = darray_vertex_data_get(&tree->vertices) + i++;
    402     const double nu = wnums[ivertex];
    403     const double ha = vals[ivertex];
    404 
    405     vtx->wavenumber = (float)nu;
    406     vtx->ka = (float)ha;
    407   }
    408 
    409   #undef MIRROR
    410 
    411   ASSERT(i == nvertices + line_nvertices);
    412 
    413   /* Setup the range of the line vertices */
    414   vertices_range[0] = nvertices;
    415   vertices_range[1] = i-1; /* Make the bound inclusive */
    416 
    417 exit:
    418   return res;
    419 error:
    420   darray_vertex_resize(&tree->vertices, nvertices);
    421   ERROR(tree->sln, "Error while recording line vertices -- %s.\n",
    422     res_to_cstr(res));
    423   goto exit;
    424 }
    425 
    426 /*******************************************************************************
    427  * Local function
    428  ******************************************************************************/
    429 res_T
    430 line_setup
    431   (const struct sln_tree* tree,
    432    const size_t iline,
    433    struct line* line)
    434 {
    435   struct shtr_molecule molecule = SHTR_MOLECULE_NULL;
    436   struct shtr_line shtr_line = SHTR_LINE_NULL;
    437   double molar_mass = 0; /* In kg.mol^-1 */
    438   const struct sln_molecule* mol_params = NULL;
    439   res_T res = RES_OK;
    440 
    441   ASSERT(tree && line);
    442 
    443   SHTR(line_list_at(tree->args.lines, iline, &shtr_line));
    444   SHTR(isotope_metadata_find_molecule
    445     (tree->args.metadata, shtr_line.molecule_id, &molecule));
    446   ASSERT(!SHTR_MOLECULE_IS_NULL(&molecule));
    447   ASSERT(molecule.nisotopes > (size_t)shtr_line.isotope_id_local);
    448 
    449   mol_params = tree->args.molecules + shtr_line.molecule_id;
    450 
    451   /* Convert the molar mass of the line from g.mol^-1 to kg.mol^-1 */
    452   molar_mass = molecule.isotopes[shtr_line.isotope_id_local].molar_mass*1e-3;
    453 
    454   /* Setup the line */
    455   res = line_profile_factor(tree, &shtr_line, &line->profile_factor);
    456   if(res != RES_OK) goto error;
    457 
    458   line->wavenumber = line_center(&shtr_line, tree->args.pressure);
    459   line->gamma_d = sln_compute_line_half_width_doppler
    460     (line->wavenumber, molar_mass, tree->args.temperature);
    461   line->gamma_l = sln_compute_line_half_width_lorentz(shtr_line.gamma_air,
    462     shtr_line.gamma_self, tree->args.pressure, mol_params->concentration);
    463   line->molecule_id = shtr_line.molecule_id;
    464 
    465 exit:
    466   return res;
    467 error:
    468   goto exit;
    469 }
    470 
    471 double
    472 line_value
    473   (const struct sln_tree* tree,
    474    const struct line* line,
    475    const double wavenumber)
    476 {
    477   const struct sln_molecule* mol_params = NULL;
    478   double profile = 0;
    479   ASSERT(tree && line);
    480 
    481   /* Retrieve the molecular parameters of the line to be mesh */
    482   mol_params = tree->args.molecules + line->molecule_id;
    483 
    484   if(wavenumber < line->wavenumber - mol_params->cutoff
    485   || wavenumber > line->wavenumber + mol_params->cutoff) {
    486     return 0;
    487   }
    488 
    489   switch(tree->args.line_profile) {
    490     case SLN_LINE_PROFILE_VOIGT:
    491       profile = sln_compute_voigt_profile
    492         (wavenumber, line->wavenumber, line->gamma_d, line->gamma_l);
    493       break;
    494     default: FATAL("Unreachable code.\n"); break;
    495   }
    496   return line->profile_factor * profile;
    497 }
    498 
    499 res_T
    500 line_mesh
    501   (struct sln_tree* tree,
    502    const size_t iline,
    503    const size_t nvertices_hint,
    504    size_t vertices_range[2]) /* out. Bounds are inclusive */
    505 {
    506   /* The line */
    507   struct line line = LINE_NULL;
    508 
    509   /* Temporary mesh */
    510   struct darray_double values; /* List of evaluated values */
    511   struct darray_double wavenumbers; /* List of considered wavenumbers */
    512   size_t nvertices_adjusted = 0; /* computed from nvertices_hint */
    513 
    514   /* Miscellaneous */
    515   res_T res = RES_OK;
    516 
    517   /* Pre-conditions */
    518   ASSERT(tree && nvertices_hint);
    519 
    520   darray_double_init(tree->sln->allocator, &values);
    521   darray_double_init(tree->sln->allocator, &wavenumbers);
    522 
    523   /* Setup the line wrt molecule concentration, isotope abundance, temperature
    524    * and pressure */
    525   res = line_setup(tree, iline, &line);
    526   if(res != RES_OK) goto error;
    527 
    528   /* Adjust the hint on the number of vertices. This is not actually the real
    529    * number of vertices but an adjusted hint on it. This new value ensures that
    530    * it is a power of 2 included in [MIN_NVERTICES_HINT, MAX_NVERTICES_HINT] */
    531   nvertices_adjusted = CLAMP
    532     (nvertices_hint, MIN_NVERTICES_HINT, MAX_NVERTICES_HINT);
    533   nvertices_adjusted = round_up_pow2(nvertices_adjusted);
    534 
    535   /* Emit the vertex coordinates, i.e. the wavenumbers */
    536   res = regular_mesh_fragmented(tree, &line, nvertices_adjusted, &wavenumbers);
    537   if(res != RES_OK) goto error;
    538 
    539   /* Evaluate the mesh vertices, i.e. define the line value for the list of
    540    * wavenumbers */
    541   eval_mesh(tree, &line, &wavenumbers, &values);
    542 
    543   switch(tree->args.mesh_type) {
    544     case SLN_MESH_UPPER:
    545       snap_mesh_to_upper_bound(&wavenumbers, &values);
    546       break;
    547     case SLN_MESH_FIT: /* Do nothing */ break;
    548     default: FATAL("Unreachable code.\n"); break;
    549   }
    550 
    551   res = save_line_mesh(tree, &line, &wavenumbers, &values, vertices_range);
    552   if(res != RES_OK) goto error;
    553 
    554 exit:
    555   darray_double_release(&values);
    556   darray_double_release(&wavenumbers);
    557   return res;
    558 error:
    559   goto exit;
    560 }