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 }