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


      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_fit
    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 = 0;
    258   size_t nvertices = 0;
    259   res_T res = RES_OK;
    260   ASSERT(tree && line && wavenumbers && values);
    261 
    262   nvertices = darray_double_size_get(wavenumbers);
    263   ASSERT(nvertices);
    264 
    265   res = darray_double_resize(values, nvertices);
    266   if(res != RES_OK) goto error;
    267 
    268   nu = darray_double_cdata_get(wavenumbers);
    269   ha = darray_double_data_get(values);
    270   FOR_EACH(ivertex, 0, nvertices) {
    271     ha[ivertex] = line_value(tree, line, nu[ivertex]);
    272   }
    273 
    274 exit:
    275   return res;
    276 error:
    277   ERROR(tree->sln, "Error evaluating the line mesh -- %s.\n", res_to_cstr(res));
    278   goto exit;
    279 }
    280 
    281 /* Calculate an upper bound for the line values for a set of wave numbers */
    282 static res_T
    283 eval_mesh_upper
    284   (const struct sln_tree* tree,
    285    const struct line* line,
    286    const struct darray_double* wavenumbers,
    287   /* #vertices used to mesh the line on its first interval, from the center of
    288    * the line to the width of the line at mid-height. This parameter is used to
    289    * calculate an upper bound mesh for the line in accordance with its width.
    290    * This user-defined discretization parameter controls the desired mesh
    291    * approximation. Thus, the mesh will be upper bounding “but not too much”
    292    * with respect to this numerical parameter */
    293    const size_t nvertices_around_line_center,
    294    struct darray_double* values)
    295 {
    296   const double* nu = NULL;
    297   double* ha = NULL;
    298   double nu_offset = 0;
    299   size_t ivertex = 0;
    300   size_t nvertices = 0;
    301   res_T res = RES_OK;
    302   ASSERT(tree && line && wavenumbers && values && nvertices_around_line_center);
    303 
    304   /* The line is meshed on its upper half, which is a strictly decreasing
    305    * function. To ensure that the mesh is an upper bound of the line, it is
    306    * therefore sufficient to evaluate its value at a wave number slightly lower
    307    * than the current wave number.
    308    *
    309    * This refers to the behavior of the following nu_offset. It corresponds to
    310    * an offset to be applied to the current nu so as to evaluate the line
    311    * slightly before the actual nu, and thus have a line value greater than its
    312    * actual value. */
    313   nu_offset = line->gamma_l / (double)nvertices_around_line_center;
    314 
    315   nvertices = darray_double_size_get(wavenumbers);
    316   ASSERT(nvertices);
    317 
    318   res = darray_double_resize(values, nvertices);
    319   if(res != RES_OK) goto error;
    320 
    321   nu = darray_double_cdata_get(wavenumbers);
    322   ha = darray_double_data_get(values);
    323 
    324   FOR_EACH(ivertex, 0, nvertices) {
    325     double nu_adjusted = nu[ivertex];
    326 
    327    /* The first node is actually the center of the line, so it is the absolute
    328     * upper limit of the line. No offset should therefore be applied to its wave
    329     * number to ensure that the resulting mesh is an upper bound of the line */
    330     if(ivertex == 0) {
    331       ASSERT(line->wavenumber == nu_adjusted);
    332     } else {
    333       nu_adjusted -= nu_offset;
    334 
    335       if(nu_adjusted == nu[ivertex]) {
    336         /* Offset was too small regarding the numeric precision */
    337         nu_adjusted = nextafter(-DBL_MAX, nu_adjusted);
    338         nu_adjusted = MMIN(nu_adjusted, line->wavenumber);
    339       }
    340     }
    341 
    342     ha[ivertex] = line_value(tree, line, nu_adjusted);
    343 
    344 #ifndef NDEBUG
    345     if(ivertex != 0) {
    346       double ha_ref = line_value(tree, line, nu[ivertex]);
    347       ASSERT(ha_ref < ha[ivertex]);
    348     }
    349 #endif
    350 
    351     /* Ensure that the value of the vertex, stored in single precision, is
    352      * indeed an exclusive upper bound of the original value. */
    353     if(ha[ivertex] != (float)ha[ivertex]) {
    354       ha[ivertex] = nextafterf((float)ha[ivertex], FLT_MAX);
    355     }
    356   }
    357 
    358 exit:
    359   return res;
    360 error:
    361   ERROR(tree->sln, "Error evaluating the line mesh -- %s.\n", res_to_cstr(res));
    362   goto exit;
    363 }
    364 
    365 static INLINE int
    366 cmp_dbl(const void* a, const void* b)
    367 {
    368   const double key = *((const double*)a);
    369   const double item = *((const double*)b);
    370   if(key < item) return -1;
    371   if(key > item) return +1;
    372   return 0;
    373 }
    374 
    375 /* Return the value of the vertex whose wavenumber is greater than 'nu' */
    376 static INLINE double
    377 next_vertex_value
    378   (const double nu,
    379    const struct darray_double* wavenumbers,
    380    const struct darray_double* values)
    381 {
    382   const double* wnum = NULL;
    383   size_t ivertex = 0;
    384   ASSERT(wavenumbers && values);
    385 
    386   wnum = search_lower_bound
    387     (&nu,
    388      darray_double_cdata_get(wavenumbers),
    389      darray_double_size_get(wavenumbers),
    390      sizeof(double),
    391      cmp_dbl);
    392   ASSERT(wnum); /* It necessary exists */
    393 
    394   ivertex = (size_t)(wnum - darray_double_cdata_get(wavenumbers));
    395   ASSERT(ivertex < darray_double_size_get(values));
    396 
    397   return darray_double_cdata_get(values)[ivertex];
    398 }
    399 
    400 /* Append the line mesh into the vertices array */
    401 static res_T
    402 save_line_mesh
    403   (struct sln_tree* tree,
    404    const struct line* line,
    405    const struct darray_double* wavenumbers,
    406    const struct darray_double* values,
    407    size_t vertices_range[2]) /* Range into which the line vertices are saved */
    408 {
    409   const double* wnums = NULL;
    410   const double* vals = NULL;
    411   size_t nvertices = 0;
    412   size_t nwavenumbers = 0;
    413   size_t line_nvertices = 0;
    414   size_t ivertex = 0;
    415   size_t i = 0;
    416   res_T res = RES_OK;
    417 
    418   ASSERT(tree && line && wavenumbers && values && vertices_range);
    419   ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values));
    420 
    421   nvertices = darray_vertex_size_get(&tree->vertices);
    422   nwavenumbers = darray_double_size_get(wavenumbers);
    423 
    424   /* Compute the overall number of vertices of the line */
    425   line_nvertices = nwavenumbers
    426     * 2 /* The line is symmetrical in its center */
    427     - 1;/* Do not duplicate the line center */
    428 
    429   /* Allocate the list of line vertices */
    430   res = darray_vertex_resize(&tree->vertices, nvertices + line_nvertices);
    431   if(res != RES_OK) goto error;
    432 
    433   wnums = darray_double_cdata_get(wavenumbers);
    434   vals = darray_double_cdata_get(values);
    435 
    436   i = nvertices;
    437 
    438   #define MIRROR(Nu) (2*line->wavenumber - (Nu))
    439 
    440   /* Copy the vertices of the line for its lower half */
    441   FOR_EACH_REVERSE(ivertex, nwavenumbers-1, 0) {
    442     struct sln_vertex* vtx = darray_vertex_data_get(&tree->vertices) + i++;
    443     const double nu = MIRROR(wnums[ivertex]);
    444     const double ha = vals[ivertex];
    445 
    446     vtx->wavenumber = (float)nu;
    447     vtx->ka = (float)ha;
    448   }
    449 
    450   /* Copy the vertices of the line for its upper half */
    451   FOR_EACH(ivertex, 0, nwavenumbers) {
    452     struct sln_vertex* vtx = darray_vertex_data_get(&tree->vertices) + i++;
    453     const double nu = wnums[ivertex];
    454     const double ha = vals[ivertex];
    455 
    456     vtx->wavenumber = (float)nu;
    457     vtx->ka = (float)ha;
    458   }
    459 
    460   #undef MIRROR
    461 
    462   ASSERT(i == nvertices + line_nvertices);
    463 
    464   /* Setup the range of the line vertices */
    465   vertices_range[0] = nvertices;
    466   vertices_range[1] = i-1; /* Make the bound inclusive */
    467 
    468 exit:
    469   return res;
    470 error:
    471   darray_vertex_resize(&tree->vertices, nvertices);
    472   ERROR(tree->sln, "Error while recording line vertices -- %s.\n",
    473     res_to_cstr(res));
    474   goto exit;
    475 }
    476 
    477 /*******************************************************************************
    478  * Local function
    479  ******************************************************************************/
    480 res_T
    481 line_setup
    482   (const struct sln_tree* tree,
    483    const size_t iline,
    484    struct line* line)
    485 {
    486   struct shtr_molecule molecule = SHTR_MOLECULE_NULL;
    487   struct shtr_line shtr_line = SHTR_LINE_NULL;
    488   double molar_mass = 0; /* In kg.mol^-1 */
    489   const struct sln_molecule* mol_params = NULL;
    490   res_T res = RES_OK;
    491 
    492   ASSERT(tree && line);
    493 
    494   SHTR(line_list_at(tree->args.lines, iline, &shtr_line));
    495   SHTR(isotope_metadata_find_molecule
    496     (tree->args.metadata, shtr_line.molecule_id, &molecule));
    497   ASSERT(!SHTR_MOLECULE_IS_NULL(&molecule));
    498   ASSERT(molecule.nisotopes > (size_t)shtr_line.isotope_id_local);
    499 
    500   mol_params = tree->args.molecules + shtr_line.molecule_id;
    501 
    502   /* Convert the molar mass of the line from g.mol^-1 to kg.mol^-1 */
    503   molar_mass = molecule.isotopes[shtr_line.isotope_id_local].molar_mass*1e-3;
    504 
    505   /* Setup the line */
    506   res = line_profile_factor(tree, &shtr_line, &line->profile_factor);
    507   if(res != RES_OK) goto error;
    508 
    509   line->wavenumber = line_center(&shtr_line, tree->args.pressure);
    510   line->gamma_d = sln_compute_line_half_width_doppler
    511     (line->wavenumber, molar_mass, tree->args.temperature);
    512   line->gamma_l = sln_compute_line_half_width_lorentz(shtr_line.gamma_air,
    513     shtr_line.gamma_self, tree->args.pressure, mol_params->concentration);
    514   line->molecule_id = shtr_line.molecule_id;
    515 
    516 exit:
    517   return res;
    518 error:
    519   goto exit;
    520 }
    521 
    522 double
    523 line_value
    524   (const struct sln_tree* tree,
    525    const struct line* line,
    526    const double wavenumber)
    527 {
    528   const struct sln_molecule* mol_params = NULL;
    529   double profile = 0;
    530   ASSERT(tree && line);
    531 
    532   /* Retrieve the molecular parameters of the line to be mesh */
    533   mol_params = tree->args.molecules + line->molecule_id;
    534 
    535   if(wavenumber < line->wavenumber - mol_params->cutoff
    536   || wavenumber > line->wavenumber + mol_params->cutoff) {
    537     return 0;
    538   }
    539 
    540   switch(tree->args.line_profile) {
    541     case SLN_LINE_PROFILE_VOIGT:
    542       profile = sln_compute_voigt_profile
    543         (wavenumber, line->wavenumber, line->gamma_d, line->gamma_l);
    544       break;
    545     default: FATAL("Unreachable code.\n"); break;
    546   }
    547   return line->profile_factor * profile;
    548 }
    549 
    550 res_T
    551 line_mesh
    552   (struct sln_tree* tree,
    553    const size_t iline,
    554    const size_t nvertices_hint,
    555    size_t vertices_range[2]) /* out. Bounds are inclusive */
    556 {
    557   /* The line */
    558   struct line line = LINE_NULL;
    559 
    560   /* Temporary mesh */
    561   struct darray_double values; /* List of evaluated values */
    562   struct darray_double wavenumbers; /* List of considered wavenumbers */
    563   size_t nvertices_adjusted = 0; /* computed from nvertices_hint */
    564 
    565   /* Miscellaneous */
    566   res_T res = RES_OK;
    567 
    568   /* Pre-conditions */
    569   ASSERT(tree && nvertices_hint);
    570 
    571   darray_double_init(tree->sln->allocator, &values);
    572   darray_double_init(tree->sln->allocator, &wavenumbers);
    573 
    574   /* Setup the line wrt molecule concentration, isotope abundance, temperature
    575    * and pressure */
    576   res = line_setup(tree, iline, &line);
    577   if(res != RES_OK) goto error;
    578 
    579   /* Adjust the hint on the number of vertices. This is not actually the real
    580    * number of vertices but an adjusted hint on it. This new value ensures that
    581    * it is a power of 2 included in [MIN_NVERTICES_HINT, MAX_NVERTICES_HINT] */
    582   nvertices_adjusted = CLAMP
    583     (nvertices_hint, MIN_NVERTICES_HINT, MAX_NVERTICES_HINT);
    584   nvertices_adjusted = round_up_pow2(nvertices_adjusted);
    585 
    586   /* Emit the vertex coordinates, i.e. the wavenumbers */
    587   res = regular_mesh_fragmented(tree, &line, nvertices_adjusted, &wavenumbers);
    588   if(res != RES_OK) goto error;
    589 
    590   /* Evaluate the mesh vertices, i.e. define the line value for the list of
    591    * wavenumbers */
    592   switch(tree->args.mesh_type) {
    593     case SLN_MESH_UPPER:
    594       eval_mesh_upper(tree, &line, &wavenumbers, nvertices_adjusted, &values);
    595       break;
    596     case SLN_MESH_FIT:
    597       eval_mesh_fit(tree, &line, &wavenumbers, &values);
    598       break;
    599     default: FATAL("Unreachable code.\n"); break;
    600   }
    601 
    602   res = save_line_mesh(tree, &line, &wavenumbers, &values, vertices_range);
    603   if(res != RES_OK) goto error;
    604 
    605 exit:
    606   darray_double_release(&values);
    607   darray_double_release(&wavenumbers);
    608   return res;
    609 error:
    610   goto exit;
    611 }