sln_line.c (18792B)
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 file is part of Star-Line. 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU General Public License for more details. 17 * 18 * You should have received a copy of the GNU General Public License 19 * along with this program. If not, see <http://www.gnu.org/licenses/>. */ 20 21 #define _POSIX_C_SOURCE 200112L /* nextafterf support */ 22 23 #include "sln_device_c.h" 24 #include "sln_line.h" 25 #include "sln_tree_c.h" 26 27 #include <lblu.h> 28 #include <star/shtr.h> 29 30 #include <rsys/algorithm.h> 31 #include <rsys/cstr.h> 32 #include <rsys/dynamic_array_double.h> 33 #include <rsys/math.h> 34 35 #include <math.h> /* nextafterf */ 36 37 #define T_REF 296.0 /* K */ 38 #define AVOGADRO_NUMBER 6.02214076e23 /* molec.mol^-1 */ 39 #define PERFECT_GAZ_CONSTANT 8.2057e-5 /* m^3.atm.mol^-1.K^-1 */ 40 41 #define MIN_NVERTICES_HINT 8 42 #define MAX_NVERTICES_HINT 128 43 STATIC_ASSERT(IS_POW2(MIN_NVERTICES_HINT), MIN_NVERTICES_HINT_is_not_a_pow2); 44 STATIC_ASSERT(IS_POW2(MIN_NVERTICES_HINT), MAX_NVERTICES_HINT_is_not_a_pow2); 45 46 /******************************************************************************* 47 * Helper function 48 ******************************************************************************/ 49 static INLINE double 50 line_intensity 51 (const double intensity_ref, /* Reference intensity [cm^-1/(molec.cm^2)] */ 52 const double lower_state_energy, /* [cm^-1] */ 53 const double partition_function, 54 const double temperature, /* [K] */ 55 const double temperature_ref, /* [K] */ 56 const double wavenumber) /* [cm^-1] */ 57 { 58 const double C2 = 1.4388; /* 2nd Planck constant [K.cm] */ 59 60 const double fol = /* TODO ask to Yaniss why this variable is named fol */ 61 (1-exp(-C2*wavenumber/temperature)) 62 / (1-exp(-C2*wavenumber/temperature_ref)); 63 64 const double tmp = 65 exp(-C2*lower_state_energy/temperature) 66 / exp(-C2*lower_state_energy/temperature_ref); 67 68 return intensity_ref * partition_function * tmp * fol ; 69 } 70 71 static res_T 72 line_profile_factor 73 (const struct sln_tree* tree, 74 const struct shtr_line* shtr_line, 75 const double concentration, 76 const double pressure, 77 const double temperature, 78 double* out_profile_factor) 79 { 80 /* Star-HITRAN data */ 81 struct shtr_molecule molecule = SHTR_MOLECULE_NULL; 82 const struct shtr_isotope* isotope = NULL; 83 84 /* Mixture parameters */ 85 const struct sln_molecule* mol_params = NULL; 86 87 /* Miscellaneous */ 88 double iso_abundance; 89 double density; /* In molec.cm^-3 */ 90 double intensity, intensity_ref; /* In cm^-1/(molec.cm^2) */ 91 double Q, Q_T, Q_Tref; /* Partition function */ 92 double nu_c; /* In cm^-1 */ 93 double profile_factor; /* In m^-1.cm^-1 */ 94 double gj; /* State independant degeneracy factor */ 95 double Ps; /* In atm */ 96 double T; /* Temperature */ 97 int molid; /* Molecule id */ 98 int isoid; /* Isotope id local to its molecule */ 99 100 res_T res = RES_OK; 101 ASSERT(tree && shtr_line && out_profile_factor); 102 103 /* Fetch the molecule data */ 104 mol_params = tree->args.molecules + shtr_line->molecule_id; 105 SHTR(isotope_metadata_find_molecule 106 (tree->args.metadata, shtr_line->molecule_id, &molecule)); 107 ASSERT(!SHTR_MOLECULE_IS_NULL(&molecule)); 108 ASSERT(molecule.nisotopes > (size_t)shtr_line->isotope_id_local); 109 isotope = molecule.isotopes + shtr_line->isotope_id_local; 110 111 nu_c = line_center(shtr_line, pressure); 112 113 /* Compute the intensity */ 114 Ps = pressure * concentration; 115 density = (AVOGADRO_NUMBER * Ps); 116 density = density / (PERFECT_GAZ_CONSTANT * temperature); 117 density = density * 1e-6; /* Convert in molec.cm^-3 */ 118 119 /* Compute the partition function */ 120 Q_Tref = isotope->Q296K; 121 molid = shtr_line->molecule_id; 122 isoid = shtr_line->isotope_id_local+1/*Local indices start at 1 in BD_TIPS*/; 123 T = temperature; 124 BD_TIPS_2017(&molid, &T, &isoid, &gj, &Q_T); 125 if(Q_T <= 0) { 126 ERROR(tree->sln, 127 "molecule %d: isotope %d: invalid partition function at T=%g\n", 128 molid, isoid, T); 129 res = RES_BAD_ARG; 130 goto error; 131 } 132 133 Q = Q_Tref/Q_T; 134 135 /* Compute the intensity */ 136 if(!mol_params->non_default_isotope_abundances) { /* Use default abundance */ 137 intensity_ref = shtr_line->intensity; 138 } else { 139 iso_abundance = mol_params->isotopes[shtr_line->isotope_id_local].abundance; 140 intensity_ref = shtr_line->intensity/isotope->abundance*iso_abundance; 141 } 142 intensity = line_intensity(intensity_ref, shtr_line->lower_state_energy, Q, 143 temperature, T_REF, nu_c); 144 145 profile_factor = 1.e2 * density * intensity; /* In m^-1.cm^-1 */ 146 147 exit: 148 *out_profile_factor = profile_factor; 149 return res; 150 error: 151 profile_factor = NaN; 152 goto exit; 153 } 154 155 /* Regularly mesh the interval [wavenumber, wavenumber+spectral_length[. Note 156 * that the upper bound is excluded, this means that the last vertex of the 157 * interval is not emitted */ 158 static INLINE res_T 159 regular_mesh 160 (const double wavenumber, /* Wavenumber where the mesh begins [cm^-1] */ 161 const double spectral_length, /* Size of the spectral interval to mesh [cm^-1] */ 162 const size_t nvertices, /* #vertices to issue */ 163 struct darray_double* wavenumbers) /* List of issued vertices */ 164 { 165 /* Do not issue the vertex on the upper bound of the spectral range. That's 166 * why we assume that the number of steps is equal to the number of vertices 167 * and not to the number of vertices minus 1 */ 168 const double step = spectral_length / (double)nvertices; 169 size_t ivtx; 170 res_T res = RES_OK; 171 ASSERT(spectral_length > 0 && wavenumbers); 172 173 FOR_EACH(ivtx, 0, nvertices) { 174 const double nu = wavenumber + (double)ivtx*step; 175 res = darray_double_push_back(wavenumbers, &nu); 176 if(res != RES_OK) goto error; 177 } 178 exit: 179 return res; 180 error: 181 goto exit; 182 } 183 184 /* The line is regularly discretized into a set of fragments of variable size. 185 * Their discretization is finer for the fragments around the center of the line 186 * and becomes coarser as the fragments move away from it. Note that a line is 187 * symmetrical in its center. As a consequence, the returned list is only the 188 * set of wavenumbers from the line center to its upper bound. */ 189 static res_T 190 regular_mesh_fragmented 191 (const struct sln_tree* tree, 192 const struct sln_line* line, 193 const size_t nvertices, 194 struct darray_double* wavenumbers) /* List of issued vertices */ 195 { 196 /* Fragment parameters */ 197 double fragment_length = 0; 198 double fragment_nu_min = 0; /* Lower bound of the fragment */ 199 size_t fragment_nvtx = 0; /* #vertices into the fragment */ 200 size_t nfragments = 0; /* Number of fragments already meshed */ 201 202 /* Miscellaneous */ 203 const struct sln_molecule* mol_params = NULL; 204 double line_nu_min = 0; /* In cm^-1 */ 205 double line_nu_max = 0; /* In cm^-1 */ 206 res_T res = RES_OK; 207 208 ASSERT(tree && line && wavenumbers); 209 ASSERT(IS_POW2(nvertices)); 210 211 /* TODO check mol params */ 212 mol_params = tree->args.molecules + line->molecule_id; 213 214 /* Compute the spectral range of the line from its center to its cutoff */ 215 line_nu_min = line->wavenumber; 216 line_nu_max = line->wavenumber + mol_params->cutoff; 217 218 /* Define the size of a fragment as the width of the line at mid-height for a 219 * Lorentz profile */ 220 fragment_length = line->gamma_l; 221 222 /* Define the number of vertices for the first interval in [nu, gamma_l] */ 223 fragment_nu_min = line_nu_min; 224 fragment_nvtx = MMAX(nvertices/2, 2); 225 226 while(fragment_nu_min < line_nu_max) { 227 const double spectral_length = 228 MMIN(fragment_length, line_nu_max - fragment_nu_min); 229 230 res = regular_mesh 231 (fragment_nu_min, spectral_length, fragment_nvtx, wavenumbers); 232 if(res != RES_OK) goto error; 233 234 /* After the third fragment, exponentially increase the fragment length */ 235 if(++nfragments >= 3) fragment_length *= 2; 236 237 fragment_nu_min += fragment_length; 238 fragment_nvtx = MMAX(fragment_nvtx/2, 2); 239 } 240 241 /* Register the last vertex, i.e. the upper bound of the spectral range */ 242 res = darray_double_push_back(wavenumbers, &line_nu_max); 243 if(res != RES_OK) goto error; 244 245 exit: 246 return res; 247 error: 248 ERROR(tree->sln, "Error meshing the line -- %s.\n", res_to_cstr(res)); 249 goto exit; 250 } 251 252 /* Calculate line values for a set of wave numbers */ 253 static res_T 254 eval_mesh 255 (const struct sln_tree* tree, 256 const struct sln_line* line, 257 const struct darray_double* wavenumbers, 258 struct darray_double* values) 259 { 260 const double* nu = NULL; 261 double* ha = NULL; 262 size_t ivertex, nvertices; 263 res_T res = RES_OK; 264 ASSERT(tree && line && wavenumbers && values); 265 266 nvertices = darray_double_size_get(wavenumbers); 267 ASSERT(nvertices); 268 269 res = darray_double_resize(values, nvertices); 270 if(res != RES_OK) goto error; 271 272 nu = darray_double_cdata_get(wavenumbers); 273 ha = darray_double_data_get(values); 274 FOR_EACH(ivertex, 0, nvertices) { 275 ha[ivertex] = sln_line_eval(tree, line, nu[ivertex]); 276 } 277 278 exit: 279 return res; 280 error: 281 ERROR(tree->sln, "Error evaluating the line mesh -- %s.\n", res_to_cstr(res)); 282 goto exit; 283 } 284 285 static void 286 snap_mesh_to_upper_bound 287 (const struct darray_double* wavenumbers, 288 struct darray_double* values) 289 { 290 double* ha = NULL; 291 size_t ivertex, nvertices; 292 ASSERT(wavenumbers && values); 293 ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values)); 294 (void)wavenumbers; 295 296 ha = darray_double_data_get(values); 297 nvertices = darray_double_size_get(wavenumbers); 298 299 /* Ensure that the stored vertex value is an exclusive upper bound of the 300 * original value. We do this by storing a value in single precision that is 301 * strictly greater than its encoding in double precision */ 302 if(ha[0] != (float)ha[0]) { 303 ha[0] = nextafterf((float)ha[0], FLT_MAX); 304 } 305 306 /* We have meshed the upper half of the line which is a strictly decreasing 307 * function. To ensure that the mesh is an upper limit of this function, 308 * simply align the value of each vertex with the value of the preceding 309 * vertex */ 310 FOR_EACH_REVERSE(ivertex, nvertices-1, 0) { 311 ha[ivertex] = ha[ivertex-1]; 312 } 313 } 314 315 static INLINE int 316 cmp_dbl(const void* a, const void* b) 317 { 318 const double key = *((const double*)a); 319 const double item = *((const double*)b); 320 if(key < item) return -1; 321 if(key > item) return +1; 322 return 0; 323 } 324 325 /* Return the value of the vertex whose wavenumber is greater than 'nu' */ 326 static INLINE double 327 next_vertex_value 328 (const double nu, 329 const struct darray_double* wavenumbers, 330 const struct darray_double* values) 331 { 332 const double* wnum = NULL; 333 size_t ivertex = 0; 334 ASSERT(wavenumbers && values); 335 336 wnum = search_lower_bound 337 (&nu, 338 darray_double_cdata_get(wavenumbers), 339 darray_double_size_get(wavenumbers), 340 sizeof(double), 341 cmp_dbl); 342 ASSERT(wnum); /* It necessary exists */ 343 344 ivertex = (size_t)(wnum - darray_double_cdata_get(wavenumbers)); 345 ASSERT(ivertex < darray_double_size_get(values)); 346 347 return darray_double_cdata_get(values)[ivertex]; 348 } 349 350 /* Append the line mesh into the vertices array */ 351 static res_T 352 save_line_mesh 353 (struct sln_tree* tree, 354 const struct sln_line* line, 355 const struct darray_double* wavenumbers, 356 const struct darray_double* values, 357 struct darray_vertex* vertices, /* buffer in which vertices are added */ 358 size_t vertices_range[2]) /* Range into which the line vertices are saved */ 359 { 360 const double* wnums = NULL; 361 const double* vals = NULL; 362 size_t nvertices = 0; 363 size_t nwavenumbers = 0; 364 size_t line_nvertices = 0; 365 size_t ivertex = 0; 366 size_t i = 0; 367 res_T res = RES_OK; 368 369 ASSERT(tree && line && wavenumbers && values && vertices && vertices_range); 370 ASSERT(darray_double_size_get(wavenumbers) == darray_double_size_get(values)); 371 372 nvertices = darray_vertex_size_get(vertices); 373 nwavenumbers = darray_double_size_get(wavenumbers); 374 375 /* Compute the overall number of vertices of the line */ 376 line_nvertices = nwavenumbers 377 * 2 /* The line is symmetrical in its center */ 378 - 1;/* Do not duplicate the line center */ 379 380 /* Allocate the list of line vertices */ 381 res = darray_vertex_resize(vertices, nvertices + line_nvertices); 382 if(res != RES_OK) goto error; 383 384 wnums = darray_double_cdata_get(wavenumbers); 385 vals = darray_double_cdata_get(values); 386 387 i = nvertices; 388 389 #define MIRROR(Nu) (2*line->wavenumber - (Nu)) 390 391 /* Copy the vertices of the line for its lower half */ 392 FOR_EACH_REVERSE(ivertex, nwavenumbers-1, 0) { 393 struct sln_vertex* vtx = darray_vertex_data_get(vertices) + i++; 394 const double nu = MIRROR(wnums[ivertex]); 395 const double ha = vals[ivertex]; 396 397 vtx->wavenumber = (float)nu; 398 vtx->ka = (float)ha; 399 } 400 401 /* Copy the vertices of the line for its upper half */ 402 FOR_EACH(ivertex, 0, nwavenumbers) { 403 struct sln_vertex* vtx = darray_vertex_data_get(vertices) + i++; 404 const double nu = wnums[ivertex]; 405 const double ha = vals[ivertex]; 406 407 vtx->wavenumber = (float)nu; 408 vtx->ka = (float)ha; 409 } 410 411 #undef MIRROR 412 413 ASSERT(i == nvertices + line_nvertices); 414 415 /* Setup the range of the line vertices */ 416 vertices_range[0] = nvertices; 417 vertices_range[1] = i-1; /* Make the bound inclusive */ 418 419 exit: 420 return res; 421 error: 422 darray_vertex_resize(vertices, nvertices); 423 ERROR(tree->sln, "Error while recording line vertices -- %s.\n", 424 res_to_cstr(res)); 425 goto exit; 426 } 427 428 /******************************************************************************* 429 * Local function 430 ******************************************************************************/ 431 res_T 432 line_setup 433 (const struct sln_tree* tree, 434 const size_t iline, 435 const struct sln_thermo_props* props, 436 struct sln_line* line) 437 { 438 struct shtr_molecule molecule = SHTR_MOLECULE_NULL; 439 struct shtr_line shtr_line = SHTR_LINE_NULL; 440 double concentration = 0; 441 double molar_mass = 0; /*[kg.mol^-1]*/ 442 double pressure = 0; /*[atm]*/ 443 double temperature = 0; /*[K]*/ 444 res_T res = RES_OK; 445 446 ASSERT(tree && line); 447 448 SHTR(line_list_at(tree->args.lines, iline, &shtr_line)); 449 SHTR(isotope_metadata_find_molecule 450 (tree->args.metadata, shtr_line.molecule_id, &molecule)); 451 ASSERT(!SHTR_MOLECULE_IS_NULL(&molecule)); 452 ASSERT(molecule.nisotopes > (size_t)shtr_line.isotope_id_local); 453 454 if(!props) { /* Use thermo properties used to build the tree */ 455 concentration = tree->args.molecules[shtr_line.molecule_id].concentration; 456 pressure = tree->args.pressure; /*[atm]*/ 457 temperature = tree->args.temperature; /*[K]*/ 458 } else { 459 concentration = props->concentrations[shtr_line.molecule_id]; 460 pressure = props->pressure; /*[atm]*/ 461 temperature = props->temperature; /*[K]*/ 462 } 463 464 /* Convert the molar mass of the line from g.mol^-1 to kg.mol^-1 */ 465 molar_mass = molecule.isotopes[shtr_line.isotope_id_local].molar_mass*1e-3; 466 467 /* Setup the line */ 468 res = line_profile_factor(tree, &shtr_line, concentration, pressure, 469 temperature, &line->profile_factor); 470 if(res != RES_OK) goto error; 471 472 line->wavenumber = line_center(&shtr_line, pressure); 473 line->gamma_d = sln_compute_line_half_width_doppler 474 (line->wavenumber, molar_mass, temperature); 475 line->gamma_l = sln_compute_line_half_width_lorentz 476 (shtr_line.gamma_air, shtr_line.gamma_self, temperature, 477 pressure, shtr_line.n_air, concentration); 478 line->molecule_id = shtr_line.molecule_id; 479 480 exit: 481 return res; 482 error: 483 goto exit; 484 } 485 486 res_T 487 line_mesh 488 (struct sln_tree* tree, 489 const size_t iline, 490 const size_t nvertices_hint, 491 struct darray_vertex* vertices, /* buffer in which vertices are added */ 492 size_t vertices_range[2]) /* out. Bounds are inclusive */ 493 { 494 /* The line */ 495 struct sln_line line = SLN_LINE_NULL; 496 497 /* Temporary mesh */ 498 struct darray_double values; /* List of evaluated values */ 499 struct darray_double wavenumbers; /* List of considered wavenumbers */ 500 size_t nvertices_adjusted = 0; /* computed from nvertices_hint */ 501 502 /* Miscellaneous */ 503 res_T res = RES_OK; 504 505 /* Pre-conditions */ 506 ASSERT(tree && vertices && nvertices_hint); 507 508 darray_double_init(tree->sln->allocator, &values); 509 darray_double_init(tree->sln->allocator, &wavenumbers); 510 511 /* Setup the line wrt molecule concentration, isotope abundance, temperature 512 * and pressure */ 513 res = line_setup(tree, iline, NULL/*default thermo props*/, &line); 514 if(res != RES_OK) goto error; 515 516 /* Adjust the hint on the number of vertices. This is not actually the real 517 * number of vertices but an adjusted hint on it. This new value ensures that 518 * it is a power of 2 included in [MIN_NVERTICES_HINT, MAX_NVERTICES_HINT] */ 519 nvertices_adjusted = CLAMP 520 (nvertices_hint, MIN_NVERTICES_HINT, MAX_NVERTICES_HINT); 521 nvertices_adjusted = round_up_pow2(nvertices_adjusted); 522 523 /* Emit the vertex coordinates, i.e. the wavenumbers */ 524 res = regular_mesh_fragmented(tree, &line, nvertices_adjusted, &wavenumbers); 525 if(res != RES_OK) goto error; 526 527 /* Evaluate the mesh vertices, i.e. define the line value for the list of 528 * wavenumbers */ 529 eval_mesh(tree, &line, &wavenumbers, &values); 530 531 switch(tree->args.mesh_type) { 532 case SLN_MESH_UPPER: 533 snap_mesh_to_upper_bound(&wavenumbers, &values); 534 break; 535 case SLN_MESH_FIT: /* Do nothing */ break; 536 default: FATAL("Unreachable code.\n"); break; 537 } 538 539 res = save_line_mesh 540 (tree, &line, &wavenumbers, &values, vertices, vertices_range); 541 if(res != RES_OK) goto error; 542 543 exit: 544 darray_double_release(&values); 545 darray_double_release(&wavenumbers); 546 return res; 547 error: 548 goto exit; 549 } 550 551 /******************************************************************************* 552 * Exported functions 553 ******************************************************************************/ 554 double 555 sln_line_eval 556 (const struct sln_tree* tree, 557 const struct sln_line* line, 558 const double wavenumber) 559 { 560 const struct sln_molecule* mol_params = NULL; 561 double profile = 0; 562 ASSERT(tree && line); 563 564 /* Retrieve the molecular parameters of the line to be mesh */ 565 mol_params = tree->args.molecules + line->molecule_id; 566 567 if(wavenumber < line->wavenumber - mol_params->cutoff 568 || wavenumber > line->wavenumber + mol_params->cutoff) { 569 return 0; 570 } 571 572 switch(tree->args.line_profile) { 573 case SLN_LINE_PROFILE_VOIGT: 574 profile = sln_compute_voigt_profile 575 (wavenumber, line->wavenumber, line->gamma_d, line->gamma_l); 576 break; 577 default: FATAL("Unreachable code.\n"); break; 578 } 579 return line->profile_factor * profile; 580 }