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 }