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 }