├── .flake8 ├── .github ├── dependabot.yml └── workflows │ ├── linter.yml │ └── unit-test.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.md ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── dbschema.png │ ├── absorption_output.rst │ ├── api.rst │ ├── atmospheric_inputs.rst │ ├── conf.py │ ├── index.rst │ ├── installation.rst │ └── spectral_inputs.rst ├── pyLBL ├── __init__.py ├── arts_crossfit │ ├── __init__.py │ ├── cross_section.py │ ├── webapi.py │ └── xsec_aux_functions.py ├── atmosphere.py ├── c_lib │ ├── __init__.py │ ├── absorption.c │ ├── gas_optics.py │ ├── spectra.c │ ├── spectra.h │ ├── spectral_database.c │ ├── spectral_database.h │ ├── voigt.c │ └── voigt.h ├── database.py ├── mt_ckd │ ├── __init__.py │ ├── carbon_dioxide.py │ ├── mt-ckd.nc │ ├── nitrogen.py │ ├── oxygen.py │ ├── ozone.py │ ├── utils.py │ └── water_vapor.py ├── plugins.py ├── pyarts_frontend │ ├── __init__.py │ └── frontend.py ├── spectroscopy.py ├── tips.py └── webapi │ ├── __init__.py │ ├── hitran_api.py │ └── tips_api.py ├── setup.py └── tests ├── conftest.py ├── test_artscrossfit.py ├── test_atmosphere.py ├── test_database.py ├── test_gas_optics.py ├── test_mt_ckd.py ├── test_plugins.py ├── test_spectroscopy.py ├── test_tips.py └── test_webapi.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | build, 4 | dist, 5 | docs, 6 | .git, 7 | .github, 8 | __pycache 9 | env, 10 | env2 11 | max-complexity = 10 12 | per-file-ignores = 13 | __init__.py: F401, 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | target-branch: "main" 9 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run lint. 2 | 3 | name: linter 4 | 5 | on: [push] 6 | 7 | jobs: 8 | install_python: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | python-version: [3.8, 3.9] 13 | os: ["ubuntu-latest", "macos-latest"] 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 27 | pip install . 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F83 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-line-length=127 --statistics 34 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies and run tests 2 | 3 | name: unit-test 4 | 5 | on: [push] 6 | 7 | jobs: 8 | unit_test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | python-version: [3.8, 3.9] 13 | os: ["ubuntu-latest", "macos-latest"] 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install pytest 27 | pip install . 28 | - name: Unit test 29 | env: 30 | FTP_DB_DIR: ${{ secrets.FTP_DB_DIR }} 31 | HITRAN_API_KEY: ${{ secrets.HITRAN_API_KEY }} 32 | run: | 33 | cd tests 34 | pytest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/* 3 | docs/build 4 | docs/source/api 5 | pyLBL.egg-info/* 6 | tests/*.db 7 | tests/*.nc 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs Configuration file. 2 | 3 | # Required 4 | version: 2 5 | 6 | # Initialize submodules. 7 | submodules: 8 | include: all 9 | 10 | #Set the version of Python. 11 | build: 12 | os: ubuntu-22.04 13 | tools: 14 | python: "3.8" 15 | 16 | # Build documentation in the docs directory with Sphinx 17 | sphinx: 18 | configuration: docs/source/conf.py 19 | 20 | # Install the document dependencies and the package. 21 | python: 22 | install: 23 | - method: pip 24 | path: . 25 | extra_requirements: 26 | - docs 27 | - method: pip 28 | path: . 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyLBL: Python Line-by-line Radiative Transfer Model 2 | =================================================== 3 | 4 | |docs| 5 | 6 | This application aims to provide a simple, flexible framework for calculating line-by-line 7 | spectra. 8 | 9 | Quickstart - calculating your first spectra 10 | ------------------------------------------- 11 | Let's jump right in with a very simple example. After running through the installation 12 | steps detailed below, spectra for a point in an atmosphere can be calculated by: 13 | 14 | .. code-block:: python 15 | 16 | from numpy import arange 17 | from pyLBL import Database, HitranWebApi, Spectroscopy 18 | from xarray import Dataset 19 | 20 | # Download the line paramters and store them in a local sqlite database. 21 | # You should only have to create the database once. Subsequent runs can 22 | # then re-use the already created database. 23 | webapi = HitranWebApi("") 24 | database = Database("") 25 | database.create(webapi) # This step will take a long time. 26 | 27 | # An xarray dataset describing the atmosphere to simulate is required. This can be a 28 | # cf-compliant netcdf file or explicitly defined (as below). 29 | atmosphere = Dataset( 30 | data_vars={ 31 | "play": (["z",], [98388.,], {"units": "Pa", "standard_name": "air_pressure"}), 32 | "tlay": (["z",], [288.99,], {"units": "K", "standard_name": "air_temperature"}), 33 | "xh2o": (["z",], [0.006637074,], 34 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_water_vapor_in_air"}), 35 | "xco2": (["z",], [0.0003599889,], 36 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_carbon_dioxide_in_air"}), 37 | "xo3": (["z",], [6.859128e-08,], 38 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_ozone_in_air"}), 39 | "xn2o": (["z",], [3.199949e-07,], 40 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_nitrous_oxide_in_air"}), 41 | "xco": (["z",], [1.482969e-07,], 42 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_carbon_monoxide_in_air"}), 43 | "xch4": (["z",], [1.700002e-06,], 44 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_methane_in_air"}), 45 | "xo2": (["z",], [0.208996,], 46 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_oxygen_in_air"}), 47 | "xn2": (["z",], [0.781,], 48 | {"units": "mol mol-1", "standard_name": "mole_fraction_of_nitrogen_in_air"}), 49 | }, 50 | ) 51 | 52 | # A wavenumber grid in units of [cm-1] is also required. 53 | grid = arange(1., 5000., 0.1) 54 | 55 | # A Spectroscopy object controls the absorption coefficient calculation. 56 | s = Spectroscopy(atmosphere, grid, database) 57 | spectra = s.compute_absorption() 58 | spectra.to_netcdf("spectra.nc") 59 | 60 | Here your HITRAN api key can be located by registering/logging into HITRAN_ and 61 | looking at your profile page. More information detailing each of the above steps are located 62 | in the sections below. 63 | 64 | Installation 65 | ------------ 66 | 67 | Installing using pip 68 | ~~~~~~~~~~~~~~~~~~~~ 69 | 70 | Make sure that you have the most recent version of :code:`pip`, then run 71 | the following command in the base directory of the repository: 72 | 73 | .. code-block:: bash 74 | 75 | pip install --upgrade pip # If you need to upgrade pip. 76 | pip install . 77 | 78 | This command should install the model and the following dependencies: 79 | 80 | * matplotlib 81 | * netCDF4 82 | * numpy 83 | * scipy 84 | * SQLAlchemy 85 | * xarray 86 | 87 | High-level API 88 | -------------- 89 | 90 | This application aims to improve on existing line-by-line radiative transfer models 91 | by separating the data management and calculation. Data management is handled by 92 | a :code:`Database` object, which allows the user to construct a local database 93 | of up-to-date line parameters, continuum coefficients, and cross sections 94 | without having to explicitly interact with the ascii/data files typically 95 | needed when using existing line-by-line models. Absorption spectra calculation 96 | is handled by a :code:`Spectroscopy` object, which allows the user to specify which molecular 97 | lines, continua, and cross section models they would like to use. The details of 98 | each of these objects are discussed further in the next sections. 99 | 100 | Spectral database management 101 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 102 | 103 | A :code:`Database` object provides an interface to the current HITRAN_ database of molecular 104 | line parameters. To create a database object of transitions for a specific set of molecules, run: 105 | 106 | .. code-block:: python 107 | 108 | from pyLBL import Database 109 | 110 | # Make a connection to a database. If the database already exists and you want to 111 | # just to re-use it, this is the only step you need. 112 | database = Database("") 113 | 114 | # If however you have not already populated the database, the data can be downloaded 115 | # and inserted by running: 116 | from pyLBL import HitranWebApi 117 | webapi = HitranWebApi("") 118 | database.create(webapi) # Note that this step will take a long time. 119 | 120 | You must create an account on the HITRAN_ website in order to get 121 | an api key. It is included as part of your profile on the webite. 122 | 123 | Absorption calculation 124 | ~~~~~~~~~~~~~~~~~~~~~~ 125 | 126 | A :code:`Spectroscopy` object allow users to choose which models are used to calculate the 127 | molecular lines, various molecular continua, and absorption cross sections. Currently, 128 | the supported models are as follows: 129 | 130 | ============== ============== 131 | Component Models 132 | ============== ============== 133 | lines pyLBL_ 134 | continua mt_ckd_ 135 | cross sections arts-crossfit_ 136 | ============== ============== 137 | 138 | For example, to create a :code:`Spectroscopy` object using the native pure python spectral 139 | lines model and the MT-CKD continuum, use: 140 | 141 | .. code-block:: python 142 | 143 | from pyLBL import Spectroscopy 144 | 145 | spectroscopy = Spectroscopy(atmosphere, grid, database, mapping=mapping, 146 | lines_backend="pyLBL", continua_backend="mt_ckd") 147 | 148 | Here the :code:`database` argument is a :code:`Database` object as described above. The 149 | :code:`atmosphere`, :code:`mapping`, and :code:`grid` inputs are described in the 150 | following section. 151 | 152 | User atmospheric inputs 153 | ~~~~~~~~~~~~~~~~~~~~~~~ 154 | Atmospheric inputs should be passed in as an xarray :code:`Dataset` object. As an example, 155 | the surface layer of the first CIRC case can be described by: 156 | 157 | .. code-block:: python 158 | 159 | def variable(data, units, standard_name): 160 | return (["z",], data, {"units": units, "standard_name": standard_name}) 161 | 162 | def create_circ_xarray_dataset(): 163 | from xarray import Dataset 164 | temperature = [288.99,] # [K]. 165 | pressure = [98388.,] # [Pa]. 166 | xh2o = [0.006637074,] # [mol mol-1]. 167 | xco2 = [0.0003599889,] # [mol mol-1]. 168 | xo3 = [6.859128e-08,] # [mol mol-1]. 169 | xn2o = [3.199949e-07,] # [mol mol-1]. 170 | xco = [1.482969e-07,] # [mol mol-1]. 171 | xch4 = [1.700002e-06,] # [mol mol-1]. 172 | xo2 = [0.208996,] # [mol mol-1]. 173 | xn2 = [0.781,] # [mol mol-1]. 174 | return Dataset( 175 | data_vars={ 176 | "p": variable(pressure, "Pa", "air_pressure"), 177 | "t": variable(temperature, "K", "air_temperature"), 178 | "xh2o": variable(xh2o, "mol mol-1", "mole_fraction_of_water_vapor_in_air"), 179 | "xco2": variable(xco2, "mol mol-1", "mole_fraction_of_carbon_dioxide_in_air"), 180 | "xo3": variable(xo3, "mol mol-1", "mole_fraction_of_ozone_in_air"), 181 | "xn2o": variable(xn2o, "mol mol-1", "mole_fraction_of_nitrous_oxide_in_air"), 182 | "xco": variable(xco, "mol mol-1", "mole_fraction_of_carbon_monoxide_in_air"), 183 | "xch4": variable(xch4, "mol mol-1", "mole_fraction_of_methane_in_air"), 184 | "xo2": variable(xo2, "mol mol-1", "mole_fraction_of_oxygen_in_air"), 185 | "xn2": variable(xn2, "mol mol-1", "mole_fraction_of_nitrogen_in_air"), 186 | }, 187 | coords={ 188 | "layer": (["z",], [1,]), 189 | }, 190 | ) 191 | 192 | As shown in this example, the units of presure must be Pa, temperature must be K, 193 | and mole fraction must be mol mol-1. Users may define a dictionary specifying which 194 | variables in the dataset should be read: 195 | 196 | .. code-block:: python 197 | 198 | mapping = { 199 | "play": "p", # name of pressure variable in dataset. 200 | "tlay": "t", # name of temperature variable in dataset. 201 | "mole_fraction": { 202 | "H2O" : "xh2o", # name of water vapor mole fraction variable in dataset. 203 | "CO2" : "xco2", # name of carbon dioxided mole fraction variable in dataset. 204 | # et cetera 205 | }, 206 | } 207 | 208 | If this dictionary is not provided, the application attempts to "discover" the variables 209 | in the dataset using their CF :code:`standard_name` attributes: 210 | 211 | ============================= =============================== 212 | Variable standard_name Attribute 213 | ============================= =============================== 214 | pressure `"air_pressure"` 215 | temperature `"air_temperature"` 216 | mole fraction of molecule xxx `"mole_fraction_of_xxx_in_air"` 217 | ============================= =============================== 218 | 219 | For a full list of valid :code:`standard_name` attributes, go here_ 220 | 221 | Spectral grid input should in wavenumber [cm-1] and be defined as a numpy 222 | array, for example: 223 | 224 | .. code-block:: python 225 | 226 | from numpy import arange 227 | grid = arange(1., 5001., 0.1) 228 | 229 | Also as of now, the wavenumber grid resolution should be one divided by an integer. This 230 | requirement may be relaxed in the future. 231 | 232 | Absorption output 233 | ~~~~~~~~~~~~~~~~~ 234 | 235 | Absorption coefficients can be calculated using the :code:`Spectroscopy` object described 236 | above by running: 237 | 238 | .. code-block:: python 239 | 240 | absorption = spectroscopy.compute_absorption(output_format="all") 241 | 242 | # Optional: convert dataset to netcdf. 243 | absorption.to_netcdf("") 244 | 245 | The output is returned as an xarray :code:`Dataset`. The exact format of the output data 246 | depends on the value of the :code:`output_format` argument. When set to :code:`"all"` (which is 247 | currently the default), the dataset will return the spectra split up by molecule 248 | and mechansim (lines, continuum, cross_section). An example viewed in netCDF format 249 | would look like this: 250 | 251 | .. code-block:: none 252 | 253 | netcdf absorption { 254 | dimensions: 255 | wavenumber = 49990 ; 256 | mechanism = 2 ; 257 | z = 1 ; 258 | variables: 259 | double wavenumber(wavenumber) ; 260 | wavenumber:_FillValue = NaN ; 261 | wavenumber:units = "cm-1" ; 262 | string mechanism(mechanism) ; 263 | double H2O_absorption(z, mechanism, wavenumber) ; 264 | H2O_absorption:_FillValue = NaN ; 265 | H2O_absorption:units = "m-1" ; 266 | double CO2_absorption(z, mechanism, wavenumber) ; 267 | CO2_absorption:_FillValue = NaN ; 268 | CO2_absorption:units = "m-1" ; 269 | double O3_absorption(z, mechanism, wavenumber) ; 270 | O3_absorption:_FillValue = NaN ; 271 | O3_absorption:units = "m-1" ; 272 | double N2O_absorption(z, mechanism, wavenumber) ; 273 | N2O_absorption:_FillValue = NaN ; 274 | N2O_absorption:units = "m-1" ; 275 | double CO_absorption(z, mechanism, wavenumber) ; 276 | CO_absorption:_FillValue = NaN ; 277 | CO_absorption:units = "m-1" ; 278 | double CH4_absorption(z, mechanism, wavenumber) ; 279 | CH4_absorption:_FillValue = NaN ; 280 | CH4_absorption:units = "m-1" ; 281 | double O2_absorption(z, mechanism, wavenumber) ; 282 | O2_absorption:_FillValue = NaN ; 283 | O2_absorption:units = "m-1" ; 284 | double N2_absorption(z, mechanism, wavenumber) ; 285 | N2_absorption:_FillValue = NaN ; 286 | N2_absorption:units = "m-1" ; 287 | data: 288 | mechanism = "lines", "continuum" ; 289 | 290 | If the :code:`output_format` argument is instead set to :code:`"gas"`, the spectra for 291 | the different mechanims will be summed for each molecule, yielding output that looks 292 | like this (in netCDF format): 293 | 294 | .. code-block:: none 295 | 296 | netcdf absorption { 297 | dimensions: 298 | wavenumber = 49990 ; 299 | z = 1 ; 300 | variables: 301 | double wavenumber(wavenumber) ; 302 | wavenumber:_FillValue = NaN ; 303 | wavenumber:units = "cm-1" ; 304 | double H2O_absorption(z, wavenumber) ; 305 | H2O_absorption:_FillValue = NaN ; 306 | H2O_absorption:units = "m-1" ; 307 | double CO2_absorption(z, wavenumber) ; 308 | CO2_absorption:_FillValue = NaN ; 309 | CO2_absorption:units = "m-1" ; 310 | double O3_absorption(z, wavenumber) ; 311 | O3_absorption:_FillValue = NaN ; 312 | O3_absorption:units = "m-1" ; 313 | double N2O_absorption(z, wavenumber) ; 314 | N2O_absorption:_FillValue = NaN ; 315 | N2O_absorption:units = "m-1" ; 316 | double CO_absorption(z, wavenumber) ; 317 | CO_absorption:_FillValue = NaN ; 318 | CO_absorption:units = "m-1" ; 319 | double CH4_absorption(z, wavenumber) ; 320 | CH4_absorption:_FillValue = NaN ; 321 | CH4_absorption:units = "m-1" ; 322 | double O2_absorption(z, wavenumber) ; 323 | O2_absorption:_FillValue = NaN ; 324 | O2_absorption:units = "m-1" ; 325 | double N2_absorption(z, wavenumber) ; 326 | N2_absorption:_FillValue = NaN ; 327 | N2_absorption:units = "m-1" ; 328 | } 329 | 330 | Lastly, if the :code:`output_format` argument is set to any other value, only the total 331 | absorption spectra (summed over all molecules) will be returned. In netCDF format, the 332 | resulting dataset will appear like this: 333 | 334 | .. code-block:: none 335 | 336 | netcdf absorption { 337 | dimensions: 338 | wavenumber = 49990 ; 339 | z = 1 ; 340 | variables: 341 | double wavenumber(wavenumber) ; 342 | wavenumber:_FillValue = NaN ; 343 | wavenumber:units = "cm-1" ; 344 | double absorption(z, wavenumber) ; 345 | absorption:_FillValue = NaN ; 346 | absorption:units = "m-1" ; 347 | } 348 | 349 | .. _HITRAN: https://hitran.org 350 | .. _pyLBL: https://github.com/GRIPS-code/pyLBL/blob/new_db/pyLBL/c_lib/gas_optics.py 351 | .. _mt_ckd: https://github.com/GRIPS-code/MT_CKD/tree/fortran-90-and-python 352 | .. _arts-crossfit: https://github.com/menzel-gfdl/arts-crossfit/tree/make-package 353 | .. _here: http://cfconventions.org/Data/cf-standard-names/77/build/cf-standard-name-table.html 354 | 355 | .. |docs| image:: https://readthedocs.org/projects/pylbl-1/badge/?version=latest 356 | :target: https://pylbl-1.readthedocs.io/en/latest 357 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/dbschema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRIPS-code/pyLBL/7aa82820131fea8a5d8cda8b705e541b6530f68e/docs/source/_static/dbschema.png -------------------------------------------------------------------------------- /docs/source/absorption_output.rst: -------------------------------------------------------------------------------- 1 | Absorption Output 2 | ----------------- 3 | 4 | Absorption coefficients can be calculated using the :code:`Spectroscopy` object (described 5 | in the previous API section) by running: 6 | 7 | .. code-block:: python 8 | 9 | # Calculate the absorption coefficients [m-1] and return them in an xarray Dataset. 10 | absorption = spectroscopy.compute_absorption(output_format="all") 11 | 12 | # Optional: convert dataset to netcdf. 13 | absorption.to_netcdf("") 14 | 15 | The output is returned as an xarray :code:`Dataset`. The absorption coefficients are 16 | calculated in units of inverse meters, so that optical depths can be calculated by the user 17 | by integrationg them over any desired path. The exact format of the output data 18 | depends on the value of the :code:`output_format` argument. When set to :code:`"all"` (which 19 | is currently the default), the dataset will return the spectra split up by molecule 20 | and mechansim (lines, continuum, cross_section). If a mechanism does not apply to a specific 21 | molecule, then values are set to zero accordingly. An example viewed in netCDF format 22 | (i.e., like by running :code:`ncdump` on the dataset) would look like this: 23 | 24 | .. code-block:: none 25 | 26 | netcdf absorption { 27 | dimensions: 28 | wavenumber = 49990 ; 29 | mechanism = 3 ; 30 | z = 1 ; 31 | variables: 32 | double wavenumber(wavenumber) ; 33 | wavenumber:_FillValue = NaN ; 34 | wavenumber:units = "cm-1" ; 35 | string mechanism(mechanism) ; 36 | double H2O_absorption(z, mechanism, wavenumber) ; 37 | H2O_absorption:_FillValue = NaN ; 38 | H2O_absorption:units = "m-1" ; 39 | double CO2_absorption(z, mechanism, wavenumber) ; 40 | CO2_absorption:_FillValue = NaN ; 41 | CO2_absorption:units = "m-1" ; 42 | double O3_absorption(z, mechanism, wavenumber) ; 43 | O3_absorption:_FillValue = NaN ; 44 | O3_absorption:units = "m-1" ; 45 | double N2O_absorption(z, mechanism, wavenumber) ; 46 | N2O_absorption:_FillValue = NaN ; 47 | N2O_absorption:units = "m-1" ; 48 | double CO_absorption(z, mechanism, wavenumber) ; 49 | CO_absorption:_FillValue = NaN ; 50 | CO_absorption:units = "m-1" ; 51 | double CH4_absorption(z, mechanism, wavenumber) ; 52 | CH4_absorption:_FillValue = NaN ; 53 | CH4_absorption:units = "m-1" ; 54 | double O2_absorption(z, mechanism, wavenumber) ; 55 | O2_absorption:_FillValue = NaN ; 56 | O2_absorption:units = "m-1" ; 57 | double N2_absorption(z, mechanism, wavenumber) ; 58 | N2_absorption:_FillValue = NaN ; 59 | N2_absorption:units = "m-1" ; 60 | data: 61 | mechanism = "lines", "continuum", "cross_section" ; 62 | } 63 | 64 | If the :code:`output_format` argument is instead set to :code:`"gas"`, the spectra for 65 | the different mechanims will be summed for each molecule, yielding output that looks 66 | like this (in netCDF format): 67 | 68 | .. code-block:: none 69 | 70 | netcdf absorption { 71 | dimensions: 72 | wavenumber = 49990 ; 73 | z = 1 ; 74 | variables: 75 | double wavenumber(wavenumber) ; 76 | wavenumber:_FillValue = NaN ; 77 | wavenumber:units = "cm-1" ; 78 | double H2O_absorption(z, wavenumber) ; 79 | H2O_absorption:_FillValue = NaN ; 80 | H2O_absorption:units = "m-1" ; 81 | double CO2_absorption(z, wavenumber) ; 82 | CO2_absorption:_FillValue = NaN ; 83 | CO2_absorption:units = "m-1" ; 84 | double O3_absorption(z, wavenumber) ; 85 | O3_absorption:_FillValue = NaN ; 86 | O3_absorption:units = "m-1" ; 87 | double N2O_absorption(z, wavenumber) ; 88 | N2O_absorption:_FillValue = NaN ; 89 | N2O_absorption:units = "m-1" ; 90 | double CO_absorption(z, wavenumber) ; 91 | CO_absorption:_FillValue = NaN ; 92 | CO_absorption:units = "m-1" ; 93 | double CH4_absorption(z, wavenumber) ; 94 | CH4_absorption:_FillValue = NaN ; 95 | CH4_absorption:units = "m-1" ; 96 | double O2_absorption(z, wavenumber) ; 97 | O2_absorption:_FillValue = NaN ; 98 | O2_absorption:units = "m-1" ; 99 | double N2_absorption(z, wavenumber) ; 100 | N2_absorption:_FillValue = NaN ; 101 | N2_absorption:units = "m-1" ; 102 | } 103 | 104 | Lastly, if the :code:`output_format` argument is set to any other value, only the total 105 | absorption spectra (summed over all molecules) will be returned. In netCDF format, the 106 | resulting dataset will appear like this: 107 | 108 | .. code-block:: none 109 | 110 | netcdf absorption { 111 | dimensions: 112 | wavenumber = 49990 ; 113 | z = 1 ; 114 | variables: 115 | double wavenumber(wavenumber) ; 116 | wavenumber:_FillValue = NaN ; 117 | wavenumber:units = "cm-1" ; 118 | double absorption(z, wavenumber) ; 119 | absorption:_FillValue = NaN ; 120 | absorption:units = "m-1" ; 121 | } 122 | 123 | We hope that formatted in this way will ease interaction between this package and other 124 | pangeo tools. 125 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | --- 3 | 4 | This application aims to improve on existing line-by-line radiative transfer models 5 | by separating the data management and calculation. Data management is handled by 6 | a :code:`Database` object, as described in the previous section, to eliminate the need to 7 | explicitly interact with the ascii/data files typically needed when using existing 8 | line-by-line models. Absorption spectra calculation is handled by 9 | a :code:`Spectroscopy` object, which allows the user to specify which molecular 10 | lines, continua, and cross section models they would like to use. 11 | 12 | Absorption calculation 13 | ~~~~~~~~~~~~~~~~~~~~~~ 14 | 15 | A :code:`Spectroscopy` object allow users to choose which models are used to calculate the 16 | molecular lines, various molecular continua, and absorption cross sections. Currently, 17 | the supported models are as follows: 18 | 19 | ============== ========================== 20 | Component Models 21 | ============== ========================== 22 | lines pyLBL, pyarts(in progress) 23 | continua MT-CKD 24 | cross sections arts-crossfit 25 | ============== ========================== 26 | 27 | For example, to create a :code:`Spectroscopy` object using the default spectral 28 | lines model and the MT-CKD continuum, use: 29 | 30 | .. code-block:: python 31 | 32 | from pyLBL import Spectroscopy 33 | 34 | spectroscopy = Spectroscopy(atmosphere, grid, database, mapping=mapping, 35 | lines_backend="pyLBL", continua_backend="mt_ckd", 36 | cross_sections_backend="arts_crossfit") 37 | 38 | Here the :code:`atmosphere` and :code:`mapping` arguments are xarray :code:`Dataset` and 39 | python dictionary objects respectively that describe the input atmospheric 40 | conditions (see the previous "Atmospheric Inputs" section). 41 | The :code:`database` and :code:`grid` arguments are :code:`Database` and numpy array 42 | objects that describe the spectral inputs (see the previous "Spectral inputs" section). 43 | The last three arguments (:code:`lines_backend`, :code:`continua_backend`, 44 | and :code:`cross_sections_backend`) are strings that determine which models will be 45 | used for each component of the calculate (see the table above). 46 | 47 | Absorption coefficients can be calculated using the :code:`Spectroscopy` object 48 | by running: 49 | 50 | .. code-block:: python 51 | 52 | absorption = spectroscopy.compute_absorption(output_format="all") 53 | 54 | See the next section "Absorption Output" which discusses the output options. 55 | -------------------------------------------------------------------------------- /docs/source/atmospheric_inputs.rst: -------------------------------------------------------------------------------- 1 | Atmospheric Inputs 2 | ------------------ 3 | 4 | This package aims to integrate easily into the pangeo suite of applications. In view of 5 | that, atmospheric inputs are required to be passed in as xarray :code:`Dataset` objects. 6 | Along with the :code:`Dataset`, a user can pass in a dictionary with maps fixed names to 7 | the variable names in the file. The required keys are: 8 | 9 | .. code-block:: python 10 | 11 | user_dictionary = { 12 | "play": "", 13 | "tlay": "", 14 | "mole_fraction": { 15 | "H2O": "", 16 | "CO2": "", 17 | "O3": "", 18 | "CH4": "", 19 | "N2O": "", 20 | "CO": "", 21 | # Other molecules of interest if desired. 22 | } 23 | } 24 | 25 | If the user does not pass in the dictionary, the application attempts to "discover" the 26 | correct variables in the :code:`Dataset` by examining the variables' CF 27 | :code:`standard_name` attributes: 28 | 29 | ============================= ============================= ============== 30 | Variable standard_name Attribute Expected Units 31 | ============================= ============================= ============== 32 | pressure "air_pressure" "Pa" 33 | temperature "air_temperature" "K" 34 | mole fraction of molecule xxx "mole_fraction_of_xxx_in_air" "mol mol-1" 35 | ============================= ============================= ============== 36 | 37 | In either case, if the variables in the :code:`Dataset` do not have the expected units, 38 | the application will not function properly. 39 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Sphinx documentation builder configuration file. 2 | from os.path import abspath 3 | import sys 4 | 5 | import sphinx_pangeo_theme 6 | 7 | sys.path.insert(0, abspath("..")) 8 | 9 | # Project information. 10 | project = "pyLBL" 11 | copyright = "2021, GRIPS-code" 12 | author = "GRIPS-code team" 13 | release = "alpha" 14 | 15 | # Sphinx extension module names. 16 | extensions = ["sphinx.ext.autodoc", 17 | "sphinx.ext.autosummary", 18 | "sphinx.ext.napoleon", 19 | "sphinx.ext.viewcode", 20 | "sphinxcontrib.apidoc", 21 | "sphinx_autopackagesummary"] 22 | 23 | # Templates. 24 | templates = ["_templates"] 25 | 26 | # Patterns, files, and directories to ignore. 27 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 28 | 29 | # Apidoc settings 30 | apidoc_module_dir = "../../pyLBL" 31 | 32 | # Napoleon settings. 33 | napoleon_google_docstring = True 34 | napoleon_include_init_with_doc = False 35 | napoleon_include_private_with_doc = False 36 | napoleon_include_special_with_doc = True 37 | napoleon_numpy_docstring = False 38 | napolean_use_keyword = True 39 | 40 | # Autosummary settings. 41 | autosummary_generate = True 42 | 43 | # Theme for the HTML pages. 44 | html_theme = "pangeo" 45 | 46 | # Paths to static files (like style sheets). 47 | html_static_path = ["_static"] 48 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | pyLBL: Python Line-by-line Radiative Transfer Model 2 | =================================================== 3 | 4 | pyLBL is a python package for calculating line-by-line molecular spectra. 5 | 6 | Contents 7 | -------- 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | installation 13 | atmospheric_inputs 14 | spectral_inputs 15 | api 16 | absorption_output 17 | 18 | Indices and tables 19 | ------------------ 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | Requirements 5 | ~~~~~~~~~~~~ 6 | 7 | :code:`pyLBL` requires python 3 (>= version 3.6) and requires the following: 8 | 9 | * matplotlib 10 | * netCDF4 11 | * numpy 12 | * scipy 13 | * sqlalchemy 14 | * xarray 15 | 16 | Installing from Conda Forge 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | The preferred way to install :code:`pyLBL` is with conda_. Users may it find it useful 20 | to create their own conda environment and install the model inside it by running: 21 | 22 | .. code-block:: bash 23 | 24 | # Optional: If the user would like to set up a custom conda environment. 25 | conda config --add envs_dirs 26 | conda config --add pkgs_dirs 27 | conda create -n 28 | conda activate 29 | 30 | conda install -c conda-forge pyLBL 31 | 32 | Here the :code:`` of the environment can be anything the user desires and the 33 | :code:`` paths can be in user-writeable locations (if for example users are 34 | not allowed to write to system directories). 35 | 36 | Installing from Github 37 | ~~~~~~~~~~~~~~~~~~~~~~ 38 | 39 | :code:`pyLBL` can also be obtained from github by running: 40 | 41 | .. code-block:: bash 42 | 43 | git clone --recursive https://github.com/GRIPS-code/pyLBL.git 44 | cd pyLBL 45 | python3 setup.py install 46 | 47 | In order to contribute, please fork the repository and submit issues and pull requests. 48 | 49 | .. _conda: https://anaconda.org/conda-forge/pylbl 50 | -------------------------------------------------------------------------------- /docs/source/spectral_inputs.rst: -------------------------------------------------------------------------------- 1 | Spectral Inputs 2 | --------------- 3 | 4 | This package aims to provide a :code:`python` interface to line-by-line calculations of 5 | absorption coefficients that is “traceable to benchmarks”. The overall goal is to treat 6 | lines, cross-sections, continua, collision-induced absorption, and line mixing. The package 7 | is structured so that the user can have multiple back-ends to choose from when running the 8 | calculation (see the API section of this documentation). Spectral input data is downloaded 9 | from the web and stored in a local :code:`sqlite3` database, which we expect the users to 10 | build (or rebuild) before running any calculations using a simple API consisting of 11 | a :code:`Database` and :code:`HitranWebApi` object. 12 | 13 | Creating the local spectral database 14 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | 16 | To create the spectral database, start by creating :code:`HitranWebApi` 17 | and :code:`Database` objects: 18 | 19 | .. code-block:: python 20 | 21 | from pyLBL import Database, HitranWebApi 22 | webapi = HitranWebApi("") # Prepare connection to the data sources. 23 | database = Database("") # Make a connection to the database. 24 | database.create(webapi) # Note that this step will take some time. 25 | 26 | Line data will be downloaded from HITRAN_. Your HITRAN API key is a unique string provided 27 | to each account holder by the HITRAN_ website. You must make an account there. In order 28 | to get your api key, create an account, log in, click on the little person icon near the 29 | top right part of the screen to get to your profile page (or navigate to profile_) and copy 30 | the long string listed as your "API Key". Note that keeping track of which version of 31 | the HITRAN data that is being downloaded is still currenlty under development. 32 | 33 | For treatment of molecular continua, we use a python port of MT-CKD developed by AER. 34 | Currently, the python port is based on version 3.5 and reads data from an automatically 35 | downloaded netCDF dataset. This dataset includes the effects of collision-induced 36 | absorption. In the near future it is expected that this data will be 37 | incorporated into the HITRAN database and once it is this package will be updated to 38 | instead download it from there. 39 | 40 | For treatment of cross-sections (polynomial interpolation in temperature and/or pressure), 41 | we use a model_ developed at the University of Hamburg (UHH). This model provides data in 42 | netCDF datasets, which are downloaded automatically. 43 | 44 | Downloading the data and creating the database only needs to be done at least once, 45 | can take a significant amount of time (~30 minutes). 46 | 47 | Re-using an existing spectral database 48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | Once a local spectral database is created, it can be re-used by simply connecting to it: 51 | 52 | .. code-block:: python 53 | 54 | from pyLBL import Database 55 | database = Database("") 56 | 57 | Since the database already exists and is populated, there is no longer a need to create 58 | a :code:`HitranWebApi` object. 59 | 60 | Defining the spectral grid 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | Spectral grid input should in wavenumber [cm-1] and be defined as a numpy 64 | array, for example: 65 | 66 | .. code-block:: python 67 | 68 | from numpy import arange 69 | grid = arange(1., 5001., 0.1) 70 | 71 | Also as of now, the wavenumber grid resolution should be one divided by an integer. This 72 | requirement may be relaxed in the future. 73 | 74 | Spectral database schema 75 | ~~~~~~~~~~~~~~~~~~~~~~~~ 76 | 77 | The local spectral database uses :code:`sqlite3` and consists of a series of tables. 78 | 79 | .. image:: _static/dbschema.png 80 | 81 | The :code:`molecule` table stores all the unique molecules found in the HITRAN database. 82 | It links to all of the other tables through each molecules's unique :code:`id`. 83 | In order to improve accessibility, each molecule can have associated string aliases which are 84 | stored in the :code:`molecule_alias` table. For example, "H2O", "water vapor", etc. are 85 | all aliases for H2O, and can be used to find H2O in the database. Each molecule may have 86 | many different forms known as istolopologues. These isotopologues are stored in 87 | the :code:`isotopologue` table, and have their own ids, names, masses, and abundance amounts. 88 | Each isotopologue contains many molecular lines, or transitions between quantum states. The 89 | properties of these transitions are downloaded from HITRAN and stored in 90 | the :code:`transition` table. Furthermore, each isotopologue has a table of total 91 | internal partition functions values at various temperatures which are stored in 92 | the :code:`tips` table. For some molecules, explicit molecular lines are not treated directly, 93 | but absorption cross-sections are instead calculted using polynomial interpolation in 94 | temperature and/or pressure). Paths to the datasets that store these cross-section tables 95 | are stored in :code:`artscrossfit` table. 96 | 97 | .. _HITRAN: https://hitran.org 98 | .. _model: https://doi.org/10.1002/essoar.10511615.1 99 | .. _profile: https://hitran.org/profile 100 | -------------------------------------------------------------------------------- /pyLBL/__init__.py: -------------------------------------------------------------------------------- 1 | from .c_lib.gas_optics import Gas 2 | from .database import Database 3 | from .plugins import continua, cross_sections, models, molecular_lines 4 | from .spectroscopy import Spectroscopy 5 | from .webapi import HitranWebApi, TipsWebApi 6 | -------------------------------------------------------------------------------- /pyLBL/arts_crossfit/__init__.py: -------------------------------------------------------------------------------- 1 | from .cross_section import CrossSection 2 | from .webapi import download 3 | -------------------------------------------------------------------------------- /pyLBL/arts_crossfit/cross_section.py: -------------------------------------------------------------------------------- 1 | from numpy import shape, zeros 2 | from scipy.interpolate import interp1d 3 | from xarray import open_dataset 4 | 5 | from .xsec_aux_functions import calculate_xsec_fullmodel 6 | 7 | 8 | class CrossSection(object): 9 | def __init__(self, formula, path): 10 | """Initializes the object. 11 | 12 | Args: 13 | formula: String chemical formula. 14 | path: Path to the data file. 15 | """ 16 | self.formula = formula 17 | self.path = path 18 | 19 | def absorption_coefficient(self, grid, temperature, pressure): 20 | """Calculates absorption cross sections. 21 | 22 | Args: 23 | grid: Numpy array of wavenumbers [cm-1]. 24 | temperature: Temperature [K]. 25 | pressure: Pressure [Pa]. 26 | 27 | Returns: 28 | Numpy array of absorption cross sections in [m2]. 29 | """ 30 | with open_dataset(self.path) as xsec_data: 31 | # Convert desired wavenumber to frequency [Hz]. 32 | c0 = 299792458.0 # Speed of light [m s-1]. 33 | freq_user = grid * c0 * 100 34 | xsec_user = zeros(shape(grid)) 35 | bands = xsec_data.bands.data 36 | for m in bands: 37 | arg = f"band{m}" 38 | # frequency of data in [Hz] 39 | freq_data = xsec_data[arg + "_fgrid"].data.transpose() 40 | # fit coefficients of band m 41 | coeffs_m = xsec_data[arg + "_coeffs"].data.transpose() 42 | # Calculate the cross section on their internal frequency grid 43 | xsec_temp = calculate_xsec_fullmodel(temperature, pressure, coeffs_m) 44 | # Interpolate cross sections to user grid 45 | f_int = interp1d(freq_data, xsec_temp, fill_value=0., bounds_error=False) 46 | xsec_user_m = f_int(freq_user) 47 | xsec_user = xsec_user + xsec_user_m 48 | return xsec_user 49 | -------------------------------------------------------------------------------- /pyLBL/arts_crossfit/webapi.py: -------------------------------------------------------------------------------- 1 | from os import remove 2 | from os.path import join 3 | from urllib.request import urlopen 4 | from zipfile import ZipFile 5 | 6 | 7 | url = "https://attachment.rrz.uni-hamburg.de/df514eed/coefficients.zip" 8 | 9 | 10 | def download(directory, name="tmp.zip"): 11 | zipped = join(directory, name) 12 | with urlopen(url) as result, open(zipped, "wb") as dl: 13 | dl.write(result.read()) 14 | with ZipFile(zipped, "r") as f: 15 | f.extractall(directory) 16 | remove(zipped) 17 | -------------------------------------------------------------------------------- /pyLBL/arts_crossfit/xsec_aux_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Fri Sep 25 16:10:11 2020 3 | 4 | @author: Manfred Brath 5 | 6 | This file contains the functions that are needed for the harmonization of the Hitran 7 | absorption cross section data and for the calculations of the fit coefficients. 8 | 9 | Modified by menzel-gfdl. 10 | """ 11 | from numpy import shape, sum, zeros 12 | 13 | 14 | def calculate_xsec(temperature, pressure, coeffs): 15 | """ 16 | Low level function to calculate the absorption cross section from the fitted 17 | coefficients 18 | 19 | Args: 20 | temperature: Float temperature [K]. 21 | pressure: Float pressure [Pa]. 22 | coeffs: matrix of fit coefficients. 23 | 24 | Returns: 25 | Vector of absorption cross section [m2]. 26 | """ 27 | 28 | # The fit model 29 | # 2d quadratic fit: 30 | # z = p00 + p10*x + p01*y + p20*x*x 31 | # z = xsec 32 | # x = T 33 | # y = P 34 | 35 | # coeffs[0,:] = p00 36 | # coeffs[1,:] = p10 37 | # coeffs[2,:] = p01 38 | # coeffs[3,:] = p20 39 | 40 | # distinguish if we calculate xsec for a lot of frequencies 41 | if len(shape(coeffs)) > 1: 42 | poly = zeros(4) 43 | poly[0] = 1 44 | poly[1] = temperature 45 | poly[2] = pressure 46 | poly[3] = temperature*temperature 47 | 48 | # allocate 49 | xsec = zeros(shape(coeffs)) 50 | 51 | for i in range(4): 52 | xsec[i, :] = coeffs[i, :] * poly[i] 53 | 54 | # or for a lot of states 55 | else: 56 | poly = zeros((4, len(temperature))) 57 | poly[0, :] = 1. 58 | poly[1, :] = temperature 59 | poly[2, :] = pressure 60 | poly[3, :] = temperature*temperature 61 | 62 | # allocate 63 | xsec = zeros((len(coeffs), len(temperature))) 64 | 65 | for i in range(4): 66 | xsec[i, :] = coeffs[i] * poly[i, :] 67 | 68 | xsec = sum(xsec, axis=0) 69 | 70 | return xsec 71 | 72 | 73 | def calculate_xsec_fullmodel(temperature, pressure, coeffs): 74 | """ 75 | Function to calculate the absorption cross section from the fitted 76 | coefficients including check for negative values. 77 | 78 | Args: 79 | temperature: Float temperature [K]. 80 | pressure: Float pressure in [Pa]. 81 | coeffs: matrix fit coefficients. 82 | 83 | Returns: 84 | Vector of absorption cross section in [m2]. 85 | """ 86 | 87 | # The fit model 88 | # 2d quadratic fit: 89 | # z = p00 + p10*x + p01*y + p20*x*x 90 | 91 | # z = Xsec 92 | # x = T 93 | # y = P 94 | 95 | # coeffs[0,:] = p00 96 | # coeffs[1,:] = p10 97 | # coeffs[2,:] = p01 98 | # coeffs[3,:] = p20 99 | 100 | # calculate raw xsecs 101 | xsec = calculate_xsec(temperature, pressure, coeffs) 102 | 103 | # Check for negative values and remove them without introducing bias, meaning 104 | # the integral over the spectrum must not change. 105 | logic = xsec < 0 106 | if sum(logic) > 0: 107 | 108 | # original sum over spectrum 109 | sumx_org = sum(xsec) 110 | 111 | # remove negative values 112 | xsec[logic] = 0 113 | 114 | if sumx_org >= 0: 115 | # estimate ratio between altered and original sum of spectrum 116 | w = sumx_org / sum(xsec) 117 | 118 | # scale altered spectrum 119 | xsec = xsec * w 120 | 121 | return xsec 122 | -------------------------------------------------------------------------------- /pyLBL/atmosphere.py: -------------------------------------------------------------------------------- 1 | """Define how atmospheric inputs are handled.""" 2 | 3 | from re import match 4 | 5 | 6 | # Map of molecule standard names to chemical formulae. 7 | _standard_name_to_formula = { 8 | "carbon_dioxide": "CO2", 9 | "carbon_monoxide": "CO", 10 | "cfc11": "CFC11", 11 | "cfc12": "CFC12", 12 | "methane": "CH4", 13 | "nitrogen": "N2", 14 | "nitrous_oxide": "N2O", 15 | "oxygen": "O2", 16 | "ozone": "O3", 17 | "water_vapor": "H2O", 18 | } 19 | 20 | 21 | class Atmosphere(object): 22 | """Atmospheric data container with basic data discover methods. 23 | 24 | Attributes: 25 | dataset: Input xarray Dataset. 26 | pressure: xarray DataArray object for pressure [Pa]. 27 | temperature: xarray DataArray object for temperature [K]. 28 | gases: Dictionary of xarray DataArray objects for gas mole fractions [mol mol-1]. 29 | """ 30 | def __init__(self, dataset, mapping=None): 31 | """Initializes an atmosphere object by reading data from an input xarray Dataset. 32 | 33 | Args: 34 | dataset: xarray Dataset. 35 | mapping: User-specified mapping of dataset variable names. 36 | """ 37 | self.dataset = dataset 38 | 39 | # Find the pressure, temperature and gax mixing ratio variables. 40 | if mapping is None: 41 | self.pressure = _find_variable(dataset, "air_pressure") 42 | self.temperature = _find_variable(dataset, "air_temperature") 43 | self.gases = {x: y for x, y in _gases(dataset)} 44 | else: 45 | self.pressure = dataset[mapping["play"]] 46 | self.temperature = dataset[mapping["tlay"]] 47 | self.gases = {x: dataset[y] for x, y in mapping["mole_fraction"].items()} 48 | 49 | 50 | def _find_variable(dataset, standard_name): 51 | """Finds a variable in a dataset by its standard name attribute. 52 | 53 | Args: 54 | dataset: xarray Dataset. 55 | standard_name: String standard name. 56 | 57 | Returns: 58 | xarray DataArray object. 59 | 60 | Raises: 61 | ValueError if standard name is not found in the dataset. 62 | """ 63 | for var in dataset.data_vars.values(): 64 | try: 65 | if var.attrs["standard_name"] == standard_name: 66 | return var 67 | except KeyError: 68 | continue 69 | raise ValueError(f"{standard_name} standard name not found in dataset.") 70 | 71 | 72 | def _gases(dataset): 73 | """Finds variables that represent gas mole fractions. 74 | 75 | Args: 76 | dataset: xarray Dataset. 77 | 78 | Yields: 79 | Gas name (i.e. "H2O") and xarray DataArray. 80 | """ 81 | for var in dataset.data_vars.values(): 82 | try: 83 | m = match("mole_fraction_of_([A-Za-z0-9_]+)?_in_air", var.attrs["standard_name"]) 84 | except KeyError: 85 | continue 86 | if m: 87 | yield _standard_name_to_formula[m.group(1)], var 88 | -------------------------------------------------------------------------------- /pyLBL/c_lib/__init__.py: -------------------------------------------------------------------------------- 1 | from .gas_optics import Gas 2 | -------------------------------------------------------------------------------- /pyLBL/c_lib/absorption.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "sqlite3.h" 6 | 7 | #include "spectra.h" 8 | #include "spectral_database.h" 9 | 10 | 11 | #define check(err) { \ 12 | if (err == 1) { \ 13 | return 1; \ 14 | } \ 15 | } 16 | 17 | 18 | /*Calculate absorption coefficient spectra.*/ 19 | int absorption(double pressure, /*Pressure [Pa].*/ 20 | double temperature, /*Temperature [K].*/ 21 | double volume_mixing_ratio, /*Volume mixing ratio [mol mol-1].*/ 22 | int v0, /*Spectral grid lower bound (inclusive) [cm-1].*/ 23 | int vn, /*Spectral grid upper bound (inclusive) [cm-1].*/ 24 | int n_per_v, /*Number of spectral grid points per wavenumber.*/ 25 | double * k, /*Absorption coefficient [m-1].*/ 26 | char * database, /*Path to the database file.*/ 27 | char * formula, /*Molecue chemical formula.*/ 28 | int cut_off, /*Cut off from line center [cm-1].*/ 29 | int remove_pedestal /*Flag for removing the pedestal.*/ 30 | ) 31 | { 32 | /*Spectral grid.*/ 33 | double dv = 1./n_per_v; 34 | int n = (vn - v0)*n_per_v; 35 | double * v = malloc(sizeof(double)*n); 36 | int i; 37 | for (i=0; i vn + cut_off + 1 || parameter.nu < v0 - (cut_off + 1)) 81 | { 82 | break; 83 | } 84 | spectra(temperature, pressure, volume_mixing_ratio, parameter, tips, 85 | v, n, n_per_v, k, cut_off, remove_pedestal); 86 | } 87 | 88 | /*Clean up clear statement so another query can be made.*/ 89 | sqlite3_finalize(statement); 90 | 91 | /*Close connection to the database.*/ 92 | check(close_database(connection)); 93 | 94 | /*Clean up.*/ 95 | free(v); 96 | free(tips.temperature); 97 | free(tips.data); 98 | return 0; 99 | } 100 | -------------------------------------------------------------------------------- /pyLBL/c_lib/gas_optics.py: -------------------------------------------------------------------------------- 1 | """Manages API or the molecular lines calcultion.""" 2 | 3 | from ctypes import c_char_p, c_double, c_int, CDLL 4 | from glob import glob 5 | from pathlib import Path 6 | 7 | from numpy import zeros 8 | from numpy.ctypeslib import ndpointer 9 | 10 | 11 | library = glob(str(Path(__file__).parent / "libabsorption*.so"))[0] 12 | library = CDLL(library) 13 | 14 | 15 | def check_return_code(value): 16 | """Checks or errors occurring in the c routines. 17 | 18 | Args: 19 | value: Integer return code. 20 | 21 | Raises: 22 | ValueError if an error is encountered. 23 | """ 24 | if value != 0: 25 | raise ValueError("Error inside c functions.") 26 | return value 27 | 28 | 29 | class Gas(object): 30 | """API for gas optics calculation. 31 | 32 | Attributes: 33 | database: String path to the spectral sqlite3 database. 34 | formula: String chemical formula. 35 | """ 36 | def __init__(self, lines_database, formula): 37 | """Initializes the object. 38 | 39 | Args: 40 | lines_database: Database object. 41 | formula: String chemical formula. 42 | """ 43 | self.database = lines_database.path 44 | self.formula = formula 45 | 46 | def absorption_coefficient(self, temperature, pressure, volume_mixing_ratio, grid, 47 | remove_pedestal=False, cut_off=25): 48 | """Calculates absorption coefficient. 49 | 50 | Args: 51 | temperature: Temperature [K]. 52 | pressure: Pressure [Pa]. 53 | volume_mixing_ratio: Volume mixing ratio [mol mol-1]. 54 | grid: Numpy array defining the spectral grid [cm-1]. 55 | remove_pedestal: Flag specifying if a pedestal should be subtracted. 56 | cut_off: Wavenumber cut-off distance [cm-1] from line centers. 57 | 58 | Returns: 59 | Numpy array of absorption coefficients [m2]. 60 | """ 61 | v0 = int(round(grid[0])) 62 | vn = int(round(grid[-1]) + 1) 63 | n_per_v = int(round(1./(grid[1] - grid[0]))) 64 | remove_pedestal = 1 if remove_pedestal else 0 65 | k = zeros((vn - v0)*n_per_v) 66 | 67 | # Define argument types. 68 | library.absorption.argtypes = \ 69 | 3*[c_double,] + \ 70 | 3*[c_int,] + \ 71 | [ndpointer(c_double, flags="C_CONTIGUOUS"),] + \ 72 | 2*[c_char_p,] + \ 73 | 2*[c_int,] 74 | 75 | # Set function to run on return. 76 | library.absorption.restype = check_return_code 77 | 78 | # Call the c function. 79 | library.absorption( 80 | c_double(pressure), 81 | c_double(temperature), 82 | c_double(volume_mixing_ratio), 83 | c_int(v0), 84 | c_int(vn), 85 | c_int(n_per_v), 86 | k, 87 | bytes(self.database, encoding="utf-8"), 88 | bytes(self.formula, encoding="utf-8"), 89 | c_int(cut_off), 90 | c_int(remove_pedestal), 91 | ) 92 | return k 93 | -------------------------------------------------------------------------------- /pyLBL/c_lib/spectra.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "spectral_database.h" 5 | #include "voigt.h" 6 | 7 | 8 | void spectra(double temperature, double pressure, double abundance, 9 | LineParameter_t parameter, Tips_t tips, double * v, int n, 10 | int n_per_v, double * k, int cut_off, int remove_pedestal) 11 | { 12 | double const vlight = 2.99792458e8; /*Speed of light [m s-1].*/ 13 | double const pa_to_atm = 9.86923e-6; /*[atm pa-1].*/ 14 | double const r2 = 2*log(2)*8314.472; 15 | double const c2 = 1.4387752; 16 | 17 | double p = pressure*pa_to_atm; /*[atm].*/ 18 | double partial_pressure = p*abundance; /*[atm].*/ 19 | double tfact = 296./temperature; 20 | 21 | /*Pressure shift (often 0).*/ 22 | double nu = parameter.nu + p*parameter.delta_air; 23 | 24 | /*Convert for line width in cm-1 at 296 K and 1 atm.*/ 25 | double gamma = (parameter.gamma_air*(p - partial_pressure) + 26 | parameter.gamma_self*partial_pressure)*pow(tfact, parameter.n_air); 27 | 28 | /*Calculate Doppler half-width at half-max HWHM in cm-1.*/ 29 | double alpha = (parameter.nu/vlight)*sqrt(r2*temperature/parameter.mass); 30 | 31 | /*Convert for line strength in cm-1.(mol.cm-2)-1 at 296K.*/ 32 | /*Boltzman factor for lower state energy.*/ 33 | double sb = exp(parameter.elower*c2*(temperature - 296.)/(temperature*296.)); 34 | 35 | /*Stimulated emission.*/ 36 | double g = exp((-c2*parameter.nu)/temperature); 37 | double gref = exp((-c2*parameter.nu)/296.); 38 | double se = (1. - g)/(1. - gref); 39 | 40 | /*Nonlte calculation of absorption coefficient modifiers.*/ 41 | double sq = total_partition_function(tips, 296., parameter.local_iso_id - 1)/ 42 | total_partition_function(tips, temperature, parameter.local_iso_id - 1); 43 | 44 | /*Line strength.*/ 45 | double sw = parameter.sw*sb*se*sq*0.01*0.01; 46 | 47 | /*Find starting and ending indices for the transition.*/ 48 | int s = (floor(nu) - cut_off - v[0])*n_per_v; 49 | if (s >= n) 50 | { 51 | /*Transition does not need to be calculated.*/ 52 | return; 53 | } 54 | if (s < 0) 55 | { 56 | s = 0; 57 | } 58 | int e = (floor(nu) + cut_off + 1 - v[0])*n_per_v; 59 | if (e >= n) 60 | { 61 | e = n - 1; 62 | } 63 | 64 | /*Calculate absorption coefficient*/ 65 | voigt(v, s, e, nu, alpha, gamma, sw, k); 66 | if (remove_pedestal != 0) 67 | { 68 | double pedestal = k[s]; 69 | if (k[e] < k[s]) 70 | { 71 | pedestal = k[e]; 72 | } 73 | int i; 74 | for (i=s; i<=e; ++i) 75 | { 76 | k[i] -= pedestal; 77 | } 78 | } 79 | return; 80 | } 81 | -------------------------------------------------------------------------------- /pyLBL/c_lib/spectra.h: -------------------------------------------------------------------------------- 1 | #ifndef SPECTRA_H_ 2 | #define SPECTRA_H_ 3 | 4 | #include "spectral_database.h" 5 | 6 | 7 | void spectra(double temperature, double pressure, double abundance, 8 | LineParameter_t parameter, Tips_t tip, double * v, int n, 9 | int n_per_v, double * k, int cut_off, int remove_pedestal); 10 | 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /pyLBL/c_lib/spectral_database.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "sqlite3.h" 7 | 8 | #include "spectral_database.h" 9 | 10 | 11 | #define sql_check(err, db) { \ 12 | if (err != SQLITE_OK) { \ 13 | fprintf(stderr, "Error: %s\n", sqlite3_errmsg(db)); \ 14 | return 1; \ 15 | } \ 16 | } 17 | 18 | 19 | int open_database(char const * path, sqlite3 ** connection) 20 | { 21 | sqlite3 * db; 22 | sql_check(sqlite3_open(path, &db), db); 23 | if (db == NULL) 24 | { 25 | fprintf(stderr, "Error: failed to open %s.\n", path); 26 | return 1; 27 | } 28 | *connection = db; 29 | return 0; 30 | } 31 | 32 | 33 | int close_database(sqlite3 * connection) 34 | { 35 | sql_check(sqlite3_close(connection), connection); 36 | return 0; 37 | } 38 | 39 | 40 | int compile_statement(sqlite3 * connection, char const * command, sqlite3_stmt ** statement) 41 | { 42 | sqlite3_stmt * stmt; 43 | sql_check(sqlite3_prepare_v2(connection, command, -1, &stmt, NULL), connection); 44 | *statement = stmt; 45 | return 0; 46 | } 47 | 48 | 49 | int tips_data(sqlite3 * connection, int id, Tips_t * tips) 50 | { 51 | /*Read tips data from the database.*/ 52 | sqlite3_stmt * statement; 53 | char query[128]; 54 | snprintf(query, 128, 55 | "select isotopologue_id, temperature, data from tips where molecule_id == %d", 56 | id); 57 | compile_statement(connection, query, &statement); 58 | int const n = 15*10000; 59 | tips->num_iso = 0; 60 | tips->temperature = malloc(sizeof(double)*n); 61 | tips->data = malloc(sizeof(double)*n); 62 | int i = 0; 63 | int current_iso = -1; 64 | while (sqlite3_step(statement) != SQLITE_DONE) 65 | { 66 | if (i >= n) 67 | { 68 | fprintf(stderr, "Error: buffer is too small, increase n.\n"); 69 | return 1; 70 | } 71 | int iso = sqlite3_column_int(statement, 0); 72 | if (iso != current_iso) 73 | { 74 | tips->num_iso++; 75 | current_iso = iso; 76 | } 77 | tips->temperature[i] = sqlite3_column_double(statement, 1); 78 | tips->data[i] = sqlite3_column_double(statement, 2); 79 | i++; 80 | } 81 | if (current_iso == -1) 82 | { 83 | return -1; 84 | } 85 | tips->num_t = i / tips->num_iso; 86 | if (tips->num_t*tips->num_iso != i) 87 | { 88 | fprintf(stderr, "Error: tips data is not rectangular.\n"); 89 | return 1; 90 | } 91 | sqlite3_finalize(statement); 92 | return 0; 93 | } 94 | 95 | 96 | /*Calculate total partition function.*/ 97 | double total_partition_function(Tips_t tips, double temperature, int iso) 98 | { 99 | int i = iso*tips.num_t; 100 | double * t = tips.temperature + i; /*Pointer arithmetic.*/ 101 | double * data = tips.data + i; /*Pointer arithemtic.*/ 102 | i = (int)(floor(temperature)) - (int)(t[0]); 103 | return data[i] + (data[i+1] - data[i])*(temperature - t[i])/(t[i+1] - t[i]); 104 | } 105 | 106 | 107 | /*Reads the isotopologue masses.*/ 108 | int mass_data(sqlite3 * connection, int id, double * mass, int num_mass) 109 | { 110 | sqlite3_stmt * statement; 111 | char query[128]; 112 | snprintf(query, 128, 113 | "select isoid, mass from isotopologue where molecule_id == %d", 114 | id); 115 | compile_statement(connection, query, &statement); 116 | while (sqlite3_step(statement) != SQLITE_DONE) 117 | { 118 | int i = sqlite3_column_int(statement, 0); 119 | if (i == 0) 120 | { 121 | /*Weird HITRAN counting.*/ 122 | i = 10; 123 | } 124 | if (i >= num_mass) 125 | { 126 | fprintf(stderr, "Error: buffer is too small, increase num_mass.\n"); 127 | return 1; 128 | } 129 | mass[i-1] = sqlite3_column_double(statement, 1); 130 | } 131 | sqlite3_finalize(statement); 132 | return 0; 133 | } 134 | 135 | 136 | /*Reads the molecule id from the database.*/ 137 | int molecule_id(sqlite3 * connection, char * formula, int * id) 138 | { 139 | /*Read tips data from the database.*/ 140 | sqlite3_stmt * statement; 141 | char query[128]; 142 | snprintf(query, 128, 143 | "select molecule from molecule_alias where alias == '%s'", 144 | formula); 145 | compile_statement(connection, query, &statement); 146 | *id = -1; 147 | while (sqlite3_step(statement) != SQLITE_DONE) 148 | { 149 | *id = sqlite3_column_int(statement, 0); 150 | break; 151 | } 152 | if (*id == -1) 153 | { 154 | fprintf(stderr, "Error: molecule %s not found in database.\n", formula); 155 | return 1; 156 | } 157 | sqlite3_finalize(statement); 158 | return 0; 159 | } 160 | 161 | 162 | /*Read the HITRAN parameters for a line.*/ 163 | int line_parameters(sqlite3_stmt * statement, LineParameter_t * parameter, double * mass) 164 | { 165 | parameter->nu = sqlite3_column_double(statement, 0); 166 | parameter->sw = sqlite3_column_double(statement, 1); 167 | parameter->gamma_air = sqlite3_column_double(statement, 2); 168 | parameter->gamma_self = sqlite3_column_double(statement, 3); 169 | parameter->n_air = sqlite3_column_double(statement, 4); 170 | parameter->elower = sqlite3_column_double(statement, 5); 171 | parameter->delta_air = sqlite3_column_double(statement, 6); 172 | parameter->local_iso_id = sqlite3_column_int(statement, 7); 173 | if (parameter->local_iso_id == 0) 174 | { 175 | /*Weird HITRAN counting.*/ 176 | parameter->local_iso_id = 10; 177 | } 178 | parameter->mass = mass[parameter->local_iso_id - 1]; 179 | return 0; 180 | } 181 | -------------------------------------------------------------------------------- /pyLBL/c_lib/spectral_database.h: -------------------------------------------------------------------------------- 1 | #ifndef SPECTRAL_DATABASE_H_ 2 | #define SPECTRAL_DATABASE_H_ 3 | 4 | #include "sqlite3.h" 5 | 6 | 7 | typedef struct LineParameter 8 | { 9 | double nu; 10 | double sw; 11 | double gamma_air; 12 | double gamma_self; 13 | double n_air; 14 | double elower; 15 | double delta_air; 16 | int local_iso_id; 17 | double mass; 18 | } LineParameter_t; 19 | 20 | 21 | /*Parameters required to calculate the total parition function.*/ 22 | typedef struct Tips 23 | { 24 | int num_iso; 25 | int num_t; 26 | double * temperature; 27 | double * data; 28 | } Tips_t; 29 | 30 | 31 | int open_database(char const * path, sqlite3 ** connection); 32 | 33 | 34 | int close_database(sqlite3 * connection); 35 | 36 | 37 | int compile_statement(sqlite3 * connection, char const * command, 38 | sqlite3_stmt ** statement); 39 | 40 | 41 | int tips_data(sqlite3 * connection, int id, Tips_t * tips); 42 | 43 | 44 | /*Calculate total partition function.*/ 45 | double total_partition_function(Tips_t tips, double temperature, int iso); 46 | 47 | 48 | /*Reads the isotopologue masses.*/ 49 | int mass_data(sqlite3 * connection, int id, double * mass, int num_mass); 50 | 51 | 52 | /*Reads the molecule id from the database.*/ 53 | int molecule_id(sqlite3 * connection, char * formula, int * id); 54 | 55 | 56 | /*Read the HITRAN parameters for a line.*/ 57 | int line_parameters(sqlite3_stmt * statement, LineParameter_t * parameter, double * mass); 58 | 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /pyLBL/c_lib/voigt.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | void voigt(double * dwno, int start, int end, double nu, double alpha, double gamma, 5 | double sw, double * k) 6 | { 7 | double rsqrpi = 1./sqrt(M_PI); 8 | double sqrln2 = sqrt(log(2.)); 9 | double y0 = 1.5; 10 | double y0py0 = y0 + y0; 11 | double y0q = y0*y0; 12 | 13 | double repwid = sqrln2/alpha; 14 | double y = repwid*gamma; 15 | double yq = y*y; 16 | 17 | if (y >= 70.55) 18 | { 19 | /*region 0.*/ 20 | int i; 21 | for (i=start; i<=end; ++i) 22 | { 23 | double xi = (dwno[i] - nu)*repwid; 24 | k[i] += sw*repwid*y/(M_PI*(xi*xi + yq)); 25 | } 26 | return; 27 | } 28 | 29 | int rg1 = 1; 30 | int rg2 = 1; 31 | int rg3 = 1; 32 | 33 | double yrrtpi = y*rsqrpi; /*y/sqrt(pi)*/ 34 | double xlim0 = sqrt(15100. + y*(40. - y*3.6)); /*y < 70.55*/ 35 | double xlim1; 36 | if (y >= 8.425) 37 | { 38 | xlim1 = 0.; 39 | } 40 | else 41 | { 42 | xlim1 = sqrt(164. - y*(4.3 + y*1.8)); 43 | } 44 | double xlim2 = 6.8 - y; 45 | double xlim3 = 2.4*y; 46 | double xlim4 = 18.1*y + 1.65; 47 | 48 | if (y <= 0.000001) 49 | { 50 | /*when y<10^-6*/ 51 | xlim1 = xlim0; /*avoid w4 algorithm.*/ 52 | xlim2 = xlim0; 53 | } 54 | 55 | double c[6] = {1.0117281, -0.75197147, 0.012557727, 56 | 0.010022008, -0.00024206814, 0.00000050084806}; 57 | double s[6] = {1.393237, 0.23115241, -0.15535147, 58 | 0.0062183662, 0.000091908299, -0.00000062752596}; 59 | double t[6] = {0.31424038, 0.94778839, 1.5976826, 60 | 2.2795071, 3.0206370, 3.8897249}; 61 | double a0, d0, d2, e0, e2, e4, h0, h2, h4, h6; 62 | double p0, p2, p4, p6, p8, z0, z2, z4, z6, z8; 63 | double buf; 64 | double xp[6]; 65 | double xm[6]; 66 | double yp[6]; 67 | double ym[6]; 68 | double mq[6]; 69 | double pq[6]; 70 | double mf[6]; 71 | double pf[6]; 72 | 73 | int i; 74 | for (i=start; i<=end; ++i) 75 | { 76 | double xi = (dwno[i] - nu)*repwid; /*loop over all points*/ 77 | double abx = fabs(xi); /*|x|*/ 78 | double xq = abx*abx; /*x^2*/ 79 | if (abx >= xlim0) 80 | { 81 | /*region 0 algorithm.*/ 82 | buf = yrrtpi/(xq + yq); 83 | } 84 | else if (abx >= xlim1) 85 | { 86 | /*humlicek w4 region 1.*/ 87 | if (rg1 != 0) 88 | { 89 | /*first point in region 1.*/ 90 | rg1 = 0; 91 | a0 = yq + 0.5; /*region 1 y-dependents.*/ 92 | d0 = a0 * a0; 93 | d2 = yq + yq - 1.; 94 | } 95 | double d = rsqrpi/(d0 + xq*(d2 + xq)); 96 | buf = d*y*(a0 + xq); 97 | } 98 | else if (abx >= xlim2) 99 | { 100 | /*humlicek w4 region 2.*/ 101 | if (rg2 != 0) 102 | { 103 | /*first point in region 2.*/ 104 | rg2 = 0; 105 | h0 = 0.5625 + yq*(4.5 + yq*(10.5 + yq*(6.0 + yq))); /*region 2 y-deps.*/ 106 | h2 = -4.5 + yq*(9.0 + yq*(6.0 + yq*4.0)); 107 | h4 = 10.5 - yq*(6.0 - yq*6.0); 108 | h6 = -6.0 + yq* 4.0; 109 | e0 = 1.875 + yq*(8.25 + yq*(5.5 + yq)); 110 | e2 = 5.25 + yq*(1.0 + yq*3.0); 111 | e4 = 0.75*h6; 112 | } 113 | double d = rsqrpi/(h0 + xq*(h2 + xq*(h4 + xq*(h6 + xq)))); 114 | buf = d*y*(e0 + xq*(e2 + xq*(e4 + xq))); 115 | } 116 | else if (abx < xlim3) 117 | { 118 | /*humlicek w4 region 3*/ 119 | if (rg3 != 0) 120 | { 121 | /*first point in region 3.*/ 122 | rg3 = 0; 123 | z0 = 272.1014 + y*(1280.829 + y*(2802.870 + y*(3764.966 /*y-deps*/ 124 | + y*(3447.629 + y*(2256.981 + y*(1074.409 + y*(369.1989 125 | + y*(88.26741 + y*(13.39880 + y))))))))); 126 | z2 = 211.678 + y*(902.3066 + y*(1758.336 + y*(2037.310 127 | + y*(1549.675 + y*(793.4273 + y*(266.2987 128 | + y*(53.59518 + y*5.0))))))); 129 | z4 = 78.86585 + y*(308.1852 + y*(497.3014 + y*(479.2576 130 | + y*(269.2916 + y*(80.39278 + y*10.0))))); 131 | z6 = 22.03523 + y*(55.02933 + y*(92.75679 + y*(53.59518 132 | + y*10.0))); 133 | z8 = 1.496460 + y*(13.39880 + y*5.0); 134 | p0 = 153.5168 + y*(549.3954 + y*(919.4955 + y*(946.8970 135 | + y*(662.8097 + y*(328.2151 + y*(115.3772 + y*(27.93941 136 | + y*(4.264678 + y*0.3183291)))))))); 137 | p2 = -34.16955 + y*(-1.322256+ y*(124.5975 + y*(189.7730 138 | + y*(139.4665 + y*(56.81652 + y*(12.79458 139 | + y*1.2733163)))))); 140 | p4 = 2.584042 + y*(10.46332 + y*(24.01655 + y*(29.81482 141 | + y*(12.79568 + y*1.9099744)))); 142 | p6 = -0.07272979 + y*(0.9377051+ y*(4.266322 + y*1.273316)); 143 | p8 = 0.0005480304 + y*0.3183291; 144 | } 145 | double d = 1.7724538/(z0 + xq*(z2 + xq*(z4 + xq*(z6 + xq*(z8 + xq))))); 146 | buf = d*(p0 + xq*(p2 + xq*(p4 + xq*(p6 + xq*p8)))); 147 | } 148 | else 149 | { 150 | /*humlicek cpf12 algorithm.*/ 151 | double ypy0 = y + y0; 152 | double ypy0q = ypy0*ypy0; 153 | buf = 0.; 154 | int j; 155 | for (j=0; j<6; ++j) 156 | { 157 | double d = xi - t[j]; 158 | mq[j] = d*d; 159 | mf[j] = 1./(mq[j] + ypy0q); 160 | xm[j] = mf[j]*d; 161 | ym[j] = mf[j]*ypy0; 162 | d = xi + t[j]; 163 | pq[j] = d*d; 164 | pf[j] = 1./(pq[j] + ypy0q); 165 | xp[j] = pf[j]*d; 166 | yp[j] = pf[j]*ypy0; 167 | } 168 | if (abx <= xlim4) 169 | { 170 | /*humlicek cpf12 region i.*/ 171 | for (j=0; j<6; ++j) 172 | { 173 | buf += c[j]*(ym[j] + yp[j]) - s[j]*(xm[j] - xp[j]); 174 | } 175 | } 176 | else 177 | { 178 | /*humlicek cpf12 region ii.*/ 179 | double yf = y + y0py0; 180 | for (j=0; j<6; ++j) 181 | { 182 | buf += (c[j]*(mq[j]*mf[j] - y0*ym[j]) + s[j]*yf*xm[j])/(mq[j] + y0q) 183 | + (c[j]*(pq[j]*pf[j] - y0*yp[j]) - s[j]*yf*xp[j])/(pq[j] + y0q); 184 | } 185 | buf = y*buf + exp(-xq); 186 | } 187 | } 188 | k[i] += sw*rsqrpi*repwid*buf; 189 | } 190 | return; 191 | } 192 | -------------------------------------------------------------------------------- /pyLBL/c_lib/voigt.h: -------------------------------------------------------------------------------- 1 | #ifndef VOIGT_H_ 2 | #define VOIGT_H_ 3 | 4 | 5 | void voigt(double * dwno, int s, int e, double nu, double alpha, double gamma, 6 | double sw, double * k); 7 | 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /pyLBL/database.py: -------------------------------------------------------------------------------- 1 | """Manages how the spectral input data is acquired and handled.""" 2 | 3 | from os import listdir 4 | from os.path import abspath, join 5 | from pathlib import Path 6 | from re import match 7 | 8 | from numpy import asarray, reshape 9 | from sqlalchemy import Column, create_engine, Float, ForeignKey, Integer, select, String 10 | from sqlalchemy.orm import declarative_base, Session 11 | 12 | from .arts_crossfit.webapi import download 13 | from .tips import TotalPartitionFunction 14 | from .webapi import NoIsotopologueError, NoMoleculeError, NoTransitionsError, TipsWebApi 15 | 16 | 17 | Base = declarative_base() 18 | 19 | 20 | def _ingest_molecule_metadata(session, molecule): 21 | """Stores the molecule metadata in the database. 22 | 23 | Args: 24 | session: Session object. 25 | molecule: Struct object. 26 | """ 27 | session.add( 28 | MoleculeTable( 29 | id=molecule.id, 30 | stoichiometric_formula=molecule.stoichiometric_formula, 31 | ordinary_formula=molecule.ordinary_formula, 32 | common_name=molecule.common_name, 33 | ) 34 | ) 35 | 36 | 37 | def _ingest_molecule_aliases(session, molecule): 38 | """Stores the molecule aliases in the database. 39 | 40 | Args: 41 | session: Session object. 42 | molecule: Struct object. 43 | """ 44 | aliases = [x["alias"] for x in molecule.aliases] 45 | for alias in aliases: 46 | session.add( 47 | MoleculeAliasTable( 48 | alias=alias, 49 | molecule=molecule.id, 50 | ) 51 | ) 52 | 53 | 54 | def _ingest_isotopologues(session, molecule, hitran_webapi): 55 | """Stores the isotopologues in the database. 56 | 57 | Args: 58 | session: Session object. 59 | molecule: Struct object. 60 | hitran_webapi: HitranWepApi object. 61 | 62 | Returns: 63 | List of Struct objects. 64 | """ 65 | isotopologues = hitran_webapi.download_isotopologues(molecule) 66 | for isotopologue in isotopologues: 67 | session.add( 68 | IsotopologueTable( 69 | id=isotopologue.id, 70 | molecule_id=molecule.id, 71 | isoid=isotopologue.isoid, 72 | iso_name=isotopologue.iso_name, 73 | abundance=isotopologue.abundance, 74 | mass=isotopologue.mass, 75 | ) 76 | ) 77 | return isotopologues 78 | 79 | 80 | def _ingest_transitions(session, molecule, isotopologues, hitran_webapi): 81 | """Stores the transitions in the database. 82 | 83 | Args: 84 | session: Session object. 85 | molecule: Struct object. 86 | isotopologues: List of Struct object. 87 | hitran_webapi: HitranWepApi object. 88 | """ 89 | parameters = ["global_iso_id", "molec_id", "local_iso_id", "nu", "sw", 90 | "gamma_air", "gamma_self", "n_air", "delta_air", "elower"] 91 | transitions = hitran_webapi.download_transitions(isotopologues, 0., 1.e8, parameters) 92 | for transition in transitions: 93 | session.add( 94 | TransitionTable( 95 | global_iso_id=transition.global_iso_id, 96 | molecule_id=molecule.id, 97 | local_iso_id=transition.local_iso_id, 98 | nu=transition.nu, 99 | sw=transition.sw, 100 | gamma_air=transition.gamma_air, 101 | gamma_self=transition.gamma_self, 102 | n_air=transition.n_air, 103 | delta_air=transition.delta_air, 104 | elower=transition.elower, 105 | ) 106 | ) 107 | 108 | 109 | def _ingest_tips(session, molecule, tips_webapi): 110 | """Stores the TIPS data in the database. 111 | 112 | Args: 113 | session: Session object. 114 | molecule: Struct object. 115 | tips_webapi: TipsWepApi object. 116 | """ 117 | temperature, data = tips_webapi.download(molecule.ordinary_formula) 118 | for x in range(data.shape[0]): 119 | for y in range(data.shape[1]): 120 | session.add( 121 | TipsTable( 122 | molecule_id=molecule.id, 123 | isotopologue_id=x, 124 | temperature=temperature[y], 125 | data=data[x, y], 126 | ) 127 | ) 128 | 129 | 130 | class Database(object): 131 | """Spectral line parameter database. 132 | 133 | Attributes: 134 | engine: SQLAlchemy Engine object. 135 | """ 136 | def __init__(self, path, echo=False): 137 | """Connects to the database and creates tables. 138 | 139 | Args: 140 | path: Path to database. 141 | echo: Print database SQL commands. 142 | """ 143 | self.cross_section_directory = None 144 | self.engine = create_engine(f"sqlite+pysqlite:///{path}", echo=echo, future=True) 145 | Base.metadata.create_all(self.engine) 146 | self.path = path 147 | 148 | def create(self, hitran_webapi, molecules="all", tips_webapi=None, 149 | cross_section_directory=".cross-sections"): 150 | """Downloads data from HITRAN and TIPS and inserts it into the database tables. 151 | 152 | Args: 153 | hitran_webapi: HitranWebApi object. 154 | molecules: List of string molecule chemical formulae. 155 | tips_webapi: TipsWebApi object. 156 | cross_section_directory: Directory to download cross sections to. 157 | """ 158 | if tips_webapi is None: 159 | tips_webapi = TipsWebApi() 160 | 161 | with Session(self.engine, future=True) as session: 162 | all_molecules = hitran_webapi.download_molecules() 163 | num_molecules = len(all_molecules) if molecules == "all" else len(molecules) 164 | for i, molecule in enumerate(all_molecules): 165 | 166 | # Support for only using a subset of molecules. 167 | if molecules != "all" and molecule.ordinary_formula not in molecules: 168 | continue 169 | 170 | # Write out progress. 171 | print("Working on molecule {} / {} ({})".format( 172 | i + 1, num_molecules, molecule.ordinary_formula) 173 | ) 174 | 175 | # Store the molecule metadata. 176 | _ingest_molecule_metadata(session, molecule) 177 | 178 | # Store the molecule aliases. 179 | _ingest_molecule_aliases(session, molecule) 180 | 181 | # Store isotopologues. 182 | isotopologues = _ingest_isotopologues(session, molecule, hitran_webapi) 183 | 184 | # Store the transitions. 185 | try: 186 | _ingest_transitions(session, molecule, isotopologues, hitran_webapi) 187 | except NoIsotopologueError: 188 | print(f"No isotopologues for molecule {molecule.ordinary_formula}.") 189 | continue 190 | except NoTransitionsError: 191 | print(f"No transitions for molecule {molecule.ordinary_formula}.") 192 | continue 193 | 194 | # Store the TIPS data. 195 | try: 196 | _ingest_tips(session, molecule, tips_webapi) 197 | except NoMoleculeError: 198 | print(f"No molecule {molecule.ordinary_formula} found in TIPS database.") 199 | continue 200 | 201 | # Commit the updates to the database. 202 | session.commit() 203 | 204 | # Add in the ARTS Crossfit data files. 205 | self.cross_section_directory = cross_section_directory 206 | Path(self.cross_section_directory).mkdir(parents=True, exist_ok=True) 207 | download(self.cross_section_directory) 208 | with Session(self.engine, future=True) as session: 209 | self._ingest_arts_crossfit(session, molecules) 210 | session.commit() 211 | 212 | def _formula(self, session, molecule_id): 213 | """Helper function that retrieves a molecule's chemical formula. 214 | 215 | Args: 216 | session: SQLAlchemy Session object. 217 | molecule_id: MoleculeTable integer primary key. 218 | 219 | Returns: 220 | MoleculeTable string chemical formula (i.e., H2O). 221 | """ 222 | stmt = select(MoleculeTable.ordinary_formula).filter_by(id=molecule_id) 223 | return session.execute(stmt).all()[0][0] 224 | 225 | def _ingest_arts_crossfit(self, session, molecules): 226 | """Stores the arts-crossfit data in the database. 227 | 228 | Args: 229 | session: Session object. 230 | molecules: List of string molecule chemical formulae. 231 | """ 232 | dir_path = join(self.cross_section_directory, "coefficients") 233 | for path in listdir(dir_path): 234 | # Find chemical formula from file name. 235 | regex = match(r"([A-Za-z0-9]+).nc", path) 236 | if not regex: 237 | continue 238 | formula = regex.group(1) 239 | 240 | # Support for only using a subset of molecules. 241 | if molecules != "all" and formula not in molecules: 242 | continue 243 | 244 | try: 245 | # Get the id of the molecule in the database. 246 | molecule_id = self._molecule_id(session, formula) 247 | except AliasNotFoundError: 248 | # The molecule doesn't exist in the database, so add it in. 249 | session.add( 250 | MoleculeTable( 251 | stoichiometric_formula=formula, 252 | ordinary_formula=formula, 253 | common_name=formula, 254 | ) 255 | ) 256 | session.commit() 257 | 258 | # Query for the molecule id. 259 | stmt = select(MoleculeTable.id).filter_by(ordinary_formula=formula) 260 | molecule_id = session.execute(stmt).all()[0][0] 261 | # Add molecule to the MoleculeAliasTable. 262 | session.add( 263 | MoleculeAliasTable( 264 | alias=formula, 265 | molecule=molecule_id, 266 | ) 267 | ) 268 | session.commit() 269 | 270 | # Store path to the cross section data file. 271 | full_path = abspath(join(dir_path, path)) 272 | session.add( 273 | ArtsCrossFitTable( 274 | molecule_id=molecule_id, 275 | path=full_path, 276 | ) 277 | ) 278 | 279 | def _mass(self, session, molecule_id): 280 | """Helper function that retrieves a molecule's isotopologue masses. 281 | 282 | Args: 283 | session: SQLAlchemy Session object. 284 | molecule_id: MoleculeTable integer primary key. 285 | 286 | Returns: 287 | List of float isotopologue masses. 288 | 289 | Raises: 290 | IsotopologuesNotFoundError if no isotopologue is found. 291 | """ 292 | stmt = select(IsotopologueTable.mass).filter_by(molecule_id=molecule_id) 293 | result = [x[0] for x in session.execute(stmt).all()] 294 | if not result: 295 | raise IsotopologuesNotFoundError( 296 | f"isotopologues not found for molecule {molecule_id}." 297 | ) 298 | return result 299 | 300 | def _molecule_id(self, session, name): 301 | """Helper function that retrieves a molecules id. 302 | 303 | Args: 304 | session: SQLAlchemy Session object. 305 | name: String molecule alias. 306 | 307 | Returns: 308 | MoleculeTable integer primary key. 309 | 310 | Raises: 311 | AliasNotFoundError if no molecule alias is found. 312 | """ 313 | stmt = select(MoleculeAliasTable.molecule).filter_by(alias=name) 314 | try: 315 | return session.execute(stmt).all()[0][0] 316 | except IndexError: 317 | raise AliasNotFoundError(f"{name} not found in database.") 318 | 319 | def _transitions(self, session, molecule_id): 320 | """Helper function that retrieves a molecule's transitions. 321 | 322 | Args: 323 | session: SQLAlchemy Session object. 324 | molecule_id: MoleculeTable integer primary key. 325 | 326 | Returns: 327 | List of TransitionTable objects. 328 | 329 | Raises: 330 | TransitionsNotFoundError if no transitions are found. 331 | """ 332 | stmt = select(TransitionTable).filter_by(molecule_id=molecule_id) 333 | result = [x[0] for x in session.execute(stmt).all()] 334 | if not result: 335 | raise TransitionsNotFoundError( 336 | f"transitions not found for molecule {molecule_id}." 337 | ) 338 | return result 339 | 340 | def molecules(self): 341 | """Queries the database for all molecules. 342 | 343 | Returns: 344 | List of string moelcule chemical formulae. 345 | """ 346 | with Session(self.engine, future=True) as session: 347 | stmt = select(MoleculeTable.ordinary_formula) 348 | return [x[0] for x in session.execute(stmt).all()] 349 | 350 | def gas(self, name): 351 | """Queries the database for all parameters needed to run a line-by-line calculation. 352 | 353 | Args: 354 | name: String molecule alias. 355 | 356 | Returns: 357 | formula: String chemical formula. 358 | mass: List of float isotopologue masses. 359 | transitions: List of TransitionTable objects. 360 | TotalPartionFunction: TotalPartitionFunction object. 361 | """ 362 | with Session(self.engine, future=True) as session: 363 | id = self._molecule_id(session, name) 364 | formula = self._formula(session, id) 365 | mass = self._mass(session, id) 366 | transitions = self._transitions(session, id) 367 | return formula, mass, transitions, TotalPartitionFunction(name, *self.tips(name)) 368 | 369 | def tips(self, name): 370 | """Queries the database for all parameters needed to run TIPS. 371 | 372 | Args: 373 | name: String molecule alias. 374 | 375 | Returns: 376 | temperature: Numpy array of temperatures. 377 | data: Numpy array of data values. 378 | 379 | Raises: 380 | TipsDataNotFoundError if no TIPS data is found. 381 | """ 382 | with Session(self.engine, future=True) as session: 383 | id = self._molecule_id(session, name) 384 | stmt = select(TipsTable).filter_by(molecule_id=id) 385 | result = [x[0] for x in session.execute(stmt).all()] 386 | if not result: 387 | raise TipsDataNotFoundError(f"no tips data for {name}.") 388 | data, temperature = [], [] 389 | for value in result: 390 | data.append(value.data) 391 | if value.temperature not in temperature: 392 | temperature.append(value.temperature) 393 | data = reshape(asarray(data), (len(data)//len(temperature), len(temperature))) 394 | temperature = asarray(temperature) 395 | return temperature, data 396 | 397 | def arts_crossfit(self, name): 398 | """Queries the database for all parameters needed to run ARTS Crossfit. 399 | 400 | Args: 401 | name: String molecule alias. 402 | 403 | Returns: 404 | Path to the cross section dataset. 405 | 406 | Raises: 407 | CrossSectionNotFoundError if no cross sections are found. 408 | """ 409 | with Session(self.engine, future=True) as session: 410 | id = self._molecule_id(session, name) 411 | stmt = select(ArtsCrossFitTable).filter_by(molecule_id=id) 412 | try: 413 | return session.execute(stmt).all()[0][0].path 414 | except IndexError: 415 | raise CrossSectionNotFoundError(f"No cross sections for {name}.") 416 | 417 | 418 | class MoleculeTable(Base): 419 | """Molecule database table schema.""" 420 | __tablename__ = "molecule" 421 | id = Column("id", Integer, primary_key=True) 422 | stoichiometric_formula = Column("stoichiometric_formula", String) 423 | ordinary_formula = Column("ordinary_formula", String) 424 | common_name = Column("common_name", String) 425 | 426 | 427 | class IsotopologueTable(Base): 428 | """Isotopologue database table schema.""" 429 | __tablename__ = "isotopologue" 430 | id = Column("id", Integer, primary_key=True) 431 | molecule_id = Column("molecule_id", Integer, ForeignKey(MoleculeTable.id)) 432 | isoid = Column("isoid", Integer) 433 | iso_name = Column("iso_name", String) 434 | abundance = Column("abundance", Float) 435 | mass = Column("mass", Float) 436 | 437 | 438 | class MoleculeAliasTable(Base): 439 | """Molecule alias database table schema.""" 440 | __tablename__ = "molecule_alias" 441 | id = Column("id", Integer, primary_key=True, autoincrement=True) 442 | alias = Column("alias", String) 443 | molecule = Column("molecule", Integer, ForeignKey(MoleculeTable.id)) 444 | 445 | 446 | class TransitionTable(Base): 447 | """Transition database table schema.""" 448 | __tablename__ = "transition" 449 | id = Column("id", Integer, primary_key=True, autoincrement=True) 450 | global_iso_id = Column("global_iso_id", Integer) 451 | molecule_id = Column("molecule_id", Integer, ForeignKey(MoleculeTable.id)) 452 | local_iso_id = Column("local_iso_id", Integer) 453 | nu = Column("nu", Float) 454 | sw = Column("sw", Float) 455 | gamma_air = Column("gamma_air", Float) 456 | gamma_self = Column("gamma_self", Float) 457 | n_air = Column("n_air", Float) 458 | delta_air = Column("delta_air", Float) 459 | elower = Column("elower", Float) 460 | 461 | 462 | class TipsTable(Base): 463 | """TIPS data table schema.""" 464 | __tablename__ = "tips" 465 | id = Column("id", Integer, primary_key=True, autoincrement=True) 466 | molecule_id = Column("molecule_id", Integer, ForeignKey(MoleculeTable.id)) 467 | isotopologue_id = Column("isotopologue_id", Integer) 468 | temperature = Column("temperature", Float) 469 | data = Column("data", Float) 470 | 471 | 472 | class ArtsCrossFitTable(Base): 473 | """Arts-crossfit table schema.""" 474 | __tablename__ = "artscrossfit" 475 | id = Column("id", Integer, primary_key=True, autoincrement=True) 476 | molecule_id = Column("molcule_id", Integer, ForeignKey(MoleculeTable.id)) 477 | path = Column("path", String) 478 | 479 | 480 | class MetadataTable(Base): 481 | """Table that describes when data was downloaded.""" 482 | __tablename__ = "metadata" 483 | id = Column("id", Integer, primary_key=True, autoincrement=True) 484 | molecule_id = Column("molecule_id", Integer, ForeignKey(MoleculeTable.id)) 485 | database = Column("database", String) 486 | time = Column("time", String) 487 | 488 | 489 | class AliasNotFoundError(BaseException): 490 | pass 491 | 492 | 493 | class TipsDataNotFoundError(BaseException): 494 | pass 495 | 496 | 497 | class IsotopologuesNotFoundError(BaseException): 498 | pass 499 | 500 | 501 | class TransitionsNotFoundError(BaseException): 502 | pass 503 | 504 | 505 | class CrossSectionNotFoundError(BaseException): 506 | pass 507 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/__init__.py: -------------------------------------------------------------------------------- 1 | from .carbon_dioxide import CarbonDioxideHartmannContinuum 2 | from .nitrogen import NitrogenCIAPureRotationContinuum, NitrogenCIAFundamentalContinuum, \ 3 | NitrogenCIAFirstOvertoneContinuum 4 | from .oxygen import OxygenCIAFundamentalContinuum, OxygenCIANIRContinuum, \ 5 | OxygenCIANIR2Continuum, OxygenCIANIR3Continuum, OxygenVisibleContinuum, \ 6 | OxygenHerzbergContinuum, OxygenUVContinuum 7 | from .ozone import OzoneChappuisWulfContinuum, OzoneHartleyHugginsContinuum, OzoneUVContinuum 8 | from .water_vapor import WaterVaporIASIForeignContinuum, WaterVaporARMSelfContinuum 9 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/carbon_dioxide.py: -------------------------------------------------------------------------------- 1 | from numpy import ones, power 2 | 3 | from .utils import BandedContinuum, Continuum, dry_air_number_density, P0, radiation_term, \ 4 | Spectrum, subgrid_bounds, T0 5 | 6 | 7 | class CarbonDioxideContinuum(BandedContinuum): 8 | def __init__(self): 9 | self.bands = [CarbonDioxideHartmannContinuum(self.path),] 10 | 11 | 12 | class CarbonDioxideHartmannContinuum(Continuum): 13 | """Carbon dioxide continuum coefficients. 14 | 15 | Attributes: 16 | data: Spectrum object containing data read from an input dataset. 17 | t_correction: Array of temperature correction coefficients. 18 | xfac_co2: Array of chi-factors. 19 | """ 20 | def __init__(self, path): 21 | self.data = Spectrum(path, "bfco2") 22 | 23 | x = Spectrum(path, "tdep_bandhead") 24 | lower, upper = subgrid_bounds(self.data.grid, x.grid) 25 | self.t_correction = ones(self.data.data.size) 26 | self.t_correction[lower:upper + 1] = x.data[:] 27 | 28 | x = Spectrum(path, "x_factor_co2") 29 | lower, upper = subgrid_bounds(self.data.grid, x.grid) 30 | self.xfac_co2 = ones(self.data.data.size) 31 | self.xfac_co2[lower:upper + 1] = x.data[:] 32 | 33 | def spectra(self, temperature, pressure, vmr): 34 | nco2 = dry_air_number_density(pressure, temperature, vmr)*vmr["CO2"] 35 | rad = radiation_term(self.grid()[:], temperature) 36 | return \ 37 | nco2*1.e-20*(pressure/P0)*(T0/temperature)*rad[:] * \ 38 | self.xfac_co2[:]*power(temperature/246., self.t_correction[:]) * \ 39 | self.data.data[:] 40 | 41 | def grid(self): 42 | return self.data.wavenumbers() 43 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/mt-ckd.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GRIPS-code/pyLBL/7aa82820131fea8a5d8cda8b705e541b6530f68e/pyLBL/mt_ckd/mt-ckd.nc -------------------------------------------------------------------------------- /pyLBL/mt_ckd/nitrogen.py: -------------------------------------------------------------------------------- 1 | from numpy import power, zeros 2 | 3 | from .utils import BandedContinuum, Continuum, dry_air_number_density, LOSCHMIDT, P0, \ 4 | radiation_term, Spectrum, T0, T273 5 | 6 | 7 | class NitrogenContinuum(BandedContinuum): 8 | def __init__(self): 9 | self.bands = [NitrogenCIAPureRotationContinuum(self.path), 10 | NitrogenCIAFundamentalContinuum(self.path), 11 | NitrogenCIAFirstOvertoneContinuum(self.path)] 12 | 13 | 14 | class NitrogenCIAPureRotationContinuum(Continuum): 15 | def __init__(self, path): 16 | self.data = {296: [Spectrum(path, "ct_296"), Spectrum(path, "sf_296")], 17 | 220: [Spectrum(path, "ct_220"), Spectrum(path, "sf_220")]} 18 | 19 | def spectra(self, temperature, pressure, vmr): 20 | nn2 = dry_air_number_density(pressure, temperature, vmr)*vmr["N2"] 21 | tau_factor = (nn2/LOSCHMIDT)*(pressure/P0)*(T273/temperature) 22 | rad = radiation_term(self.grid()[:], temperature) 23 | factor = (temperature - T0)/(220. - T0) 24 | c = self.data[296][0].data[:]*power(self.data[220][0].data[:]/self.data[296][0].data[:], 25 | factor) 26 | s = self.data[296][1].data[:]*power(self.data[220][1].data[:]/self.data[296][1].data[:], 27 | factor) 28 | fo2 = (s[:] - 1.)*vmr["N2"]/vmr["O2"] 29 | return tau_factor*rad[:]*c[:]*(vmr["N2"] + fo2[:]*vmr["O2"] + vmr["H2O"]) 30 | 31 | def grid(self): 32 | return self.data[296][0].wavenumbers() 33 | 34 | 35 | class NitrogenCIAFundamentalContinuum(Continuum): 36 | def __init__(self, path): 37 | self.data = [Spectrum(path, "xn2_272"), Spectrum(path, "xn2_228"), 38 | Spectrum(path, "a_h2o")] 39 | 40 | def spectra(self, temperature, pressure, vmr): 41 | nn2 = dry_air_number_density(pressure, temperature, vmr)*vmr["N2"] 42 | tau_factor = (nn2/LOSCHMIDT)*(pressure/P0)*(T273/temperature) 43 | rad = radiation_term(self.grid()[:], temperature) 44 | 45 | xtfac = (1./temperature - 1./272.)/(1./228. - 1./272.) 46 | ao2 = 1.294 - 0.4545*temperature/T0 47 | c0 = zeros(self.data[0].data.size) 48 | c0[1: -1] = self.data[0].data[1: -1]*power(self.data[1].data[1: -1] / 49 | self.data[0].data[1: -1], xtfac) 50 | c0 = c0[:]/self.grid()[:] 51 | c1 = ao2*c0[:] 52 | c2 = (9./7.)*self.data[2].data[:]*c0[:] 53 | return tau_factor*rad[:]*(c0[:]*vmr["N2"] + vmr["O2"]*c1[:] + vmr["H2O"]*c2[:]) 54 | 55 | def grid(self): 56 | return self.data[0].wavenumbers() 57 | 58 | 59 | class NitrogenCIAFirstOvertoneContinuum(Continuum): 60 | def __init__(self, path): 61 | self.data = Spectrum(path, "xn2") 62 | 63 | def spectra(self, temperature, pressure, vmr): 64 | nn2 = dry_air_number_density(pressure, temperature, vmr)*vmr["N2"] 65 | tau_factor = (nn2/LOSCHMIDT)*(pressure/P0)*(T273/temperature) * \ 66 | (vmr["N2"] + vmr["O2"] + vmr["H2O"]) 67 | rad = radiation_term(self.grid()[:], temperature) 68 | return tau_factor*rad[:]*self.data.data[:]/self.grid()[:] 69 | 70 | def grid(self): 71 | return self.data.wavenumbers() 72 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/oxygen.py: -------------------------------------------------------------------------------- 1 | from numpy import arange, exp, log, power, zeros 2 | 3 | from .utils import air_number_density, BandedContinuum, Continuum, dry_air_number_density, \ 4 | LOSCHMIDT, P0, radiation_term, Spectrum, T0, T273 5 | 6 | 7 | class OxygenContinuum(BandedContinuum): 8 | def __init__(self): 9 | self.bands = [OxygenCIAFundamentalContinuum(self.path), 10 | OxygenCIANIRContinuum(self.path), 11 | OxygenCIANIR2Continuum(self.path), 12 | OxygenCIANIR3Continuum(self.path), 13 | OxygenVisibleContinuum(self.path), 14 | OxygenHerzbergContinuum(self.path), 15 | OxygenUVContinuum(self.path)] 16 | 17 | 18 | class OxygenCIAFundamentalContinuum(Continuum): 19 | def __init__(self, path): 20 | self.data = [Spectrum(path, "o2_f"), Spectrum(path, "o2_t")] 21 | 22 | def spectra(self, temperature, pressure, vmr): 23 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] 24 | tau_factor = no2*1.e-20*(pressure/P0)*(T273/temperature) 25 | rad = radiation_term(self.grid()[:], temperature) 26 | xktfac = (1./T0) - (1./temperature) # [K-1]. 27 | factor = (1.e20/LOSCHMIDT) # [cm3]. 28 | return \ 29 | tau_factor*rad[:]*factor*self.data[0].data[:] * \ 30 | exp(self.data[1].data[:]*xktfac)/self.grid()[:] 31 | 32 | def grid(self): 33 | return self.data[0].wavenumbers() 34 | 35 | 36 | class OxygenCIANIRContinuum(Continuum): 37 | def __init__(self, path): 38 | self.data = Spectrum(path, "o2_inf1") 39 | 40 | def spectra(self, temperature, pressure, vmr): 41 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] 42 | ao2 = 1./0.446 43 | an2 = 0.3/0.446 44 | tau_factor = \ 45 | (no2/LOSCHMIDT)*(pressure/P0)*(T273/temperature) * \ 46 | (ao2*vmr["O2"] + an2*vmr["N2"] + vmr["H2O"]) 47 | rad = radiation_term(self.grid()[:], temperature) # [cm-1]. 48 | return tau_factor*rad[:]*self.data.data[:]/self.grid()[:] 49 | 50 | def grid(self): 51 | return self.data.wavenumbers() 52 | 53 | 54 | class OxygenCIANIR2Continuum(Continuum): 55 | def __init__(self, path=None): 56 | self._grid = arange(9100., 11002., 2.) 57 | self.data = zeros(self._grid.size) 58 | hw1 = 58.96 59 | hw2 = 45.04 60 | for i in range(self._grid.size): 61 | dv1 = self._grid[i] - 9375. 62 | dv2 = self._grid[i] - 9439. 63 | damp1 = exp(dv1/176.1) if dv1 < 0. else 1. 64 | damp2 = exp(dv2/176.1) if dv2 < 0. else 1. 65 | o2inf = 0.31831*(((1.166e-04*damp1/hw1)/(1. + (dv1/hw1)*(dv1/hw1))) + 66 | ((3.086e-05*damp2/hw2)/(1. + (dv2/hw2)*(dv2/hw2))))*1.054 67 | self.data[i] = o2inf/self._grid[i] 68 | 69 | def spectra(self, temperature, pressure, vmr): 70 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] 71 | n = air_number_density(pressure, temperature, vmr) 72 | adjwo2 = (no2/n)*(1./vmr["O2"])*no2*1.e-20*(pressure/P0)*(T0/temperature) 73 | rad = radiation_term(self.grid()[:], temperature) # [cm-1]. 74 | return adjwo2*rad[:]*self.data[:] 75 | 76 | def grid(self): 77 | return self._grid[:] 78 | 79 | 80 | class OxygenCIANIR3Continuum(Continuum): 81 | def __init__(self, path): 82 | self.data = Spectrum(path, "o2_inf3") 83 | 84 | def spectra(self, temperature, pressure, vmr): 85 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] 86 | tau_factor = (no2/LOSCHMIDT)*(pressure/P0)*(T273/temperature) # [cm3]. 87 | rad = radiation_term(self.grid()[:], temperature) # [cm-1]. 88 | return tau_factor*rad[:]*self.data.data[:]/self.grid()[:] 89 | 90 | def grid(self): 91 | return self.data.wavenumbers() 92 | 93 | 94 | class OxygenVisibleContinuum(Continuum): 95 | def __init__(self, path): 96 | self.data = Spectrum(path, "o2_invis") 97 | 98 | def spectra(self, temperature, pressure, vmr): 99 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] # [cm-3]. 100 | n = air_number_density(pressure, temperature, vmr) 101 | adjwo2 = (no2/n)*no2*1.e-20*(pressure/P0)*(T273/temperature) # [cm-3]. 102 | rad = radiation_term(self.grid()[:], temperature) # [cm-1]. 103 | factor = 1./(LOSCHMIDT*1.e-20*(55.*T273/T0)*(55.*T273/T0)*89.5) # [cm3]. 104 | return adjwo2*rad[:]*factor*self.data.data[:]/self.grid()[:] 105 | 106 | def grid(self): 107 | return self.data.wavenumbers() 108 | 109 | 110 | class OxygenHerzbergContinuum(Continuum): 111 | def __init__(self, path=None): 112 | self._grid = arange(36000., 100010., 10.) 113 | self.data = zeros(self._grid.size) 114 | for i in range(self._grid.size): 115 | if self._grid[i] <= 36000.: 116 | self.data[i] = 0. 117 | else: 118 | corr = ((40000. - self._grid[i])/4000.)*7.917e-7 \ 119 | if self._grid[i] <= 40000. else 0. 120 | yratio = self._grid[i]/48811.0 121 | self.data[i] = 6.884e-4*yratio*exp(-69.738*power(log(yratio), 2)) - corr 122 | 123 | def spectra(self, temperature, pressure, vmr): 124 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] # [cm-3]. 125 | rad = radiation_term(self.grid()[:], temperature) # [cm-1]. 126 | factor = 1. + 0.83*(pressure/P0)*(T273/temperature) 127 | return 1.e-20*no2*rad[:]*factor*self.data[:]/self.grid()[:] 128 | 129 | def grid(self): 130 | return self._grid[:] 131 | 132 | 133 | class OxygenUVContinuum(Continuum): 134 | def __init__(self, path): 135 | self.data = Spectrum(path, "o2_infuv") 136 | 137 | def spectra(self, temperature, pressure, vmr): 138 | no2 = dry_air_number_density(pressure, temperature, vmr)*vmr["O2"] # [cm-3]. 139 | rad = radiation_term(self.grid()[:], temperature) # [cm-1]. 140 | return 1.e-20*no2*rad[:]*self.data.data[:]/self.grid()[:] 141 | 142 | def grid(self): 143 | return self.data.wavenumbers() 144 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/ozone.py: -------------------------------------------------------------------------------- 1 | from .utils import BandedContinuum, Continuum, dry_air_number_density, radiation_term, \ 2 | Spectrum, T273 3 | 4 | 5 | class OzoneContinuum(BandedContinuum): 6 | def __init__(self): 7 | self.bands = [OzoneChappuisWulfContinuum(self.path), 8 | OzoneHartleyHugginsContinuum(self.path), 9 | OzoneUVContinuum(self.path)] 10 | 11 | 12 | class OzoneChappuisWulfContinuum(Continuum): 13 | """Ozone continuum in the Chappuis and Wulf band. 14 | 15 | Attributes: 16 | data: List of Spectrum objects containing data read from an input dataset. 17 | """ 18 | def __init__(self, path): 19 | self.data = [Spectrum(path, "x_o3"), Spectrum(path, "y_o3"), Spectrum(path, "z_o3")] 20 | 21 | def spectra(self, temperature, pressure, vmr): 22 | no3 = dry_air_number_density(pressure, temperature, vmr)*vmr["O3"] 23 | dt = temperature - T273 24 | rad = radiation_term(self.grid()[:], temperature) 25 | return 1.e-20*no3*rad*(self.data[0].data[:] + self.data[1].data[:]*dt + 26 | self.data[2].data[:]*dt*dt)/self.grid()[:] 27 | 28 | def grid(self): 29 | return self.data[0].wavenumbers() 30 | 31 | 32 | class OzoneHartleyHugginsContinuum(Continuum): 33 | """Ozone Hartly-Huggins continuum cros sections. 34 | 35 | Attributes: 36 | data: List of Spectrum objects containing data read from an input dataset. 37 | """ 38 | def __init__(self, path): 39 | self.data = [Spectrum(path, "o3_hh0"), Spectrum(path, "o3_hh1"), 40 | Spectrum(path, "o3_hh2")] 41 | 42 | def spectra(self, temperature, pressure, vmr): 43 | no3 = dry_air_number_density(pressure, temperature, vmr)*vmr["O3"] 44 | dt = temperature - T273 45 | rad = radiation_term(self.grid()[:], temperature) 46 | return \ 47 | 1.e-20*no3*rad*(self.data[0].data[:]/self.grid()[:]) * \ 48 | (1. + self.data[1].data[:]*dt + self.data[2].data[:]*dt*dt) 49 | 50 | def grid(self): 51 | return self.data[0].wavenumbers() 52 | 53 | 54 | class OzoneUVContinuum(Continuum): 55 | """Ozone ultra-violet continuum coefficients. 56 | 57 | Attributes: 58 | data: A Spectrum object containing data read from an input dataset. 59 | """ 60 | def __init__(self, path): 61 | self.data = Spectrum(path, "o3_huv") 62 | 63 | def spectra(self, temperature, pressure, vmr): 64 | no3 = dry_air_number_density(pressure, temperature, vmr)*vmr["O3"] 65 | rad = radiation_term(self.grid()[:], temperature) 66 | return no3*rad*self.data.data[:]/self.grid()[:] 67 | 68 | def grid(self): 69 | return self.data.wavenumbers() 70 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/utils.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, join, realpath 2 | 3 | from netCDF4 import Dataset 4 | from numpy import asarray, copy, exp, interp, where, zeros 5 | 6 | 7 | LOSCHMIDT = 2.6867775e19 # Loschmidt constant [cm-3] 8 | P0 = 1013.25 # Reference pressure (1 atmosphere) [mb]. 9 | SECOND_RADIATION_CONSTANT = 1.4387752 # Second radiation constant [cm K]. 10 | T0 = 296. # Reference temperature [K]. 11 | T273 = 273.15 # Reference temperature (0 celcius) [K]. 12 | m_to_cm = 100. # [cm m-1]. 13 | Pa_to_mb = 0.01 # [mb Pa-1]. 14 | 15 | 16 | def air_number_density(pressure, temperature, volume_mixing_ratio): 17 | """Calculates the air number density. 18 | 19 | Args: 20 | pressure: Pressure [mb]. 21 | temperature: Temperature [K]. 22 | volume_mixing_ratio: Dictionary of volume mixing ratios [mol mol-1]. 23 | 24 | Returns: 25 | Number density of air [cm-3]. 26 | """ 27 | return sum([dry_air_number_density(pressure, temperature, volume_mixing_ratio)*x 28 | for x in volume_mixing_ratio.values()]) 29 | 30 | 31 | def dry_air_number_density(pressure, temperature, volume_mixing_ratio): 32 | """Calculates the dry air number density. 33 | 34 | Args: 35 | pressure: Pressure [mb]. 36 | temperature: Temperature [K]. 37 | volume_mixing_ratio: Dictionary of volume mixing ratios [mol mol-1]. 38 | 39 | Returns: 40 | Number density of dry air [cm-3]. 41 | """ 42 | return LOSCHMIDT*(pressure/P0)*(T273/temperature)*(1. - volume_mixing_ratio["H2O"]) 43 | 44 | 45 | def radiation_term(wavenumber, temperature): 46 | """Calculates the radiation term. 47 | 48 | Args: 49 | wavenumber: Array of wavenumber [cm-1]. 50 | temperature: Temperature [K]. 51 | 52 | Returns: 53 | The radiation term [cm-1]. 54 | """ 55 | t = temperature/SECOND_RADIATION_CONSTANT 56 | x = wavenumber[:]/t 57 | r = wavenumber[:] 58 | r = where(x <= 0.01, 0.5*x*wavenumber, r) 59 | return where(x <= 10., wavenumber*(1. - exp(-x))/(1. + exp(-x)), r) 60 | 61 | 62 | def subgrid_bounds(grid, subgrid): 63 | """Calculates the starting and ending grid indices of a subgrid. 64 | 65 | Args: 66 | grid: A dictionary describing the main grid. 67 | subgrid: A dictionary describing the subgrid. 68 | 69 | Returns: 70 | The starting and ending grid indices of the subgrid. 71 | """ 72 | if grid["resolution"] != subgrid["resolution"]: 73 | raise ValueError("grid and subgrid have different resolutions.") 74 | if grid["lower_bound"] > subgrid["lower_bound"] or \ 75 | grid["upper_bound"] < subgrid["upper_bound"]: 76 | raise ValueError("subgrid not contained in grid.") 77 | lower = int((subgrid["lower_bound"] - grid["lower_bound"])/grid["resolution"]) 78 | upper = int((subgrid["upper_bound"] - grid["lower_bound"])/grid["resolution"]) 79 | return lower, upper 80 | 81 | 82 | class Continuum(object): 83 | """Abstract class for gridded continuum coefficients.""" 84 | def __init__(self, path): 85 | """Reads in the necessary data from an input dataset. 86 | 87 | Args: 88 | path: Path to the netcdf dataset. 89 | """ 90 | raise NotImplementedError("You must override this class.") 91 | 92 | def spectra(self, temperature, pressure, vmr): 93 | """Calculates the spectral feature. 94 | 95 | Args: 96 | temperature: Temperature [K]. 97 | pressure: Pressure [mb]. 98 | vmr: Dictionary of volume mixing ratios [mol mol-1]. 99 | 100 | Return: 101 | An array of continuum extinction [cm-1]. 102 | """ 103 | raise NotImplementedError("You must override this method.") 104 | 105 | def grid(self): 106 | """Calculates the wavenumber grid [cm-1]. 107 | 108 | Returns: 109 | A 1d numpy array containing the wavenumber grid [cm-1]. 110 | """ 111 | raise NotImplementedError("You must override this method.") 112 | 113 | 114 | class Spectrum(object): 115 | """Helper class that reads data from a variable in the input dataset. 116 | 117 | Attributes: 118 | path: Path to the netcdf dataset. 119 | grid: Dictionary describing the wavenumber grid. 120 | """ 121 | def __init__(self, path, name): 122 | """Reads the data from a variable in the input dataset. 123 | 124 | Args: 125 | path: Path to the netcdf dataset. 126 | name: Name of the variable in the dataset. 127 | """ 128 | with Dataset(path, "r") as dataset: 129 | v = dataset.variables[name] 130 | self.data = copy(v[:]) 131 | self.grid = {x: v.getncattr("wavenumber_{}".format(x)) for x in 132 | ["lower_bound", "upper_bound", "resolution"]} 133 | # self.units = v.getncattr("units") 134 | 135 | def wavenumbers(self): 136 | """Calculates the wavenumber grid [cm-1] for the variable. 137 | 138 | Returns: 139 | A 1d numpy array containing the wavenumber grid [cm-1]. 140 | """ 141 | return asarray([self.grid["lower_bound"] + i*self.grid["resolution"] 142 | for i in range(self.data.size)]) 143 | 144 | 145 | class BandedContinuum(object): 146 | """Contains all bands for a specific molecule's continuum. 147 | 148 | Attributes: 149 | bands: List of Continuum objects. 150 | """ 151 | path = join(dirname(realpath(__file__)), "mt-ckd.nc") 152 | 153 | def __init__(self): 154 | """Reads in the necessary data from an input dataset.""" 155 | raise NotImplementedError("You must override this method.") 156 | 157 | def spectra(self, temperature, pressure, vmr, grid): 158 | """Calculates the continum spectrum and interpolates to the input grid. 159 | 160 | Args: 161 | temperature: Temperature [K]. 162 | pressure: Pressure [Pa]. 163 | vmr: Dictionary of volume mixing ratios [mol mol-1]. 164 | grid: Array containing the spectral grid [cm-1]. 165 | 166 | Return: 167 | An array of continuum extinction [m-1]. 168 | """ 169 | s = zeros(grid.size) 170 | for band in self.bands: 171 | s[:] += interp(grid, band.grid(), 172 | band.spectra(temperature, pressure*Pa_to_mb, vmr), 173 | left=0., right=0.)[:]*m_to_cm 174 | return s 175 | -------------------------------------------------------------------------------- /pyLBL/mt_ckd/water_vapor.py: -------------------------------------------------------------------------------- 1 | from numpy import power, zeros 2 | 3 | from .utils import air_number_density, BandedContinuum, Continuum, dry_air_number_density, \ 4 | P0, radiation_term, Spectrum, subgrid_bounds, T0 5 | 6 | 7 | class WaterVaporSelfContinuum(BandedContinuum): 8 | def __init__(self): 9 | self.bands = [WaterVaporARMSelfContinuum(self.path),] 10 | 11 | 12 | class WaterVaporARMSelfContinuum(Continuum): 13 | """Water vapor self continuum coefficients. 14 | 15 | Attributes: 16 | data: Dictionary that maps temperatures (keys) to Spectrum objects containing 17 | data read from an input dataset (values). 18 | """ 19 | def __init__(self, path): 20 | self.data = {296: Spectrum(path, "bs296"), 21 | 260: Spectrum(path, "bs260")} 22 | 23 | def spectra(self, temperature, pressure, vmr): 24 | t_factor = (temperature - T0)/(260. - T0) 25 | nh2o = dry_air_number_density(pressure, temperature, vmr)*vmr["H2O"] 26 | n = air_number_density(pressure, temperature, vmr) 27 | rad = radiation_term(self.grid()[:], temperature) 28 | return \ 29 | nh2o*(nh2o/n)*(pressure/P0)*(T0/temperature)*1.e-20*rad * \ 30 | self.data[296].data[:]*power(self.data[260].data[:]/self.data[296].data[:], 31 | t_factor) 32 | 33 | def grid(self): 34 | return self.data[296].wavenumbers() 35 | 36 | 37 | class WaterVaporForeignContinuum(BandedContinuum): 38 | def __init__(self): 39 | self.bands = [WaterVaporIASIForeignContinuum(self.path),] 40 | 41 | 42 | class WaterVaporIASIForeignContinuum(Continuum): 43 | """Water vapor foreign continuum coefficients. 44 | 45 | Attributes: 46 | data: Spectrum object containing data read from an input dataset. 47 | scale: Array of scaling factors. 48 | """ 49 | def __init__(self, path): 50 | self.data = Spectrum(path, "bfh2o") 51 | x = Spectrum(path, "xfac_rhu") 52 | self.scale = zeros(self.data.data.size) 53 | lower, upper = subgrid_bounds(self.data.grid, x.grid) 54 | self.scale[lower + 1:upper + 1] = x.data[1:] 55 | self.scale[lower] = self.scale[lower + 1] 56 | u = upper + 1 57 | w = self.grid()[u:] 58 | vdelsq1 = (w[:] - 255.67)*(w[:] - 255.67) 59 | vf1 = power((w[:] - 255.67)/57.83, 8) 60 | vdelmsq1 = (w[:] + 255.67)*(w[:] + 255.67) 61 | vmf1 = power((w[:] + 255.67)/57.83, 8) 62 | vf2 = power(w[:]/630., 8) 63 | self.scale[u:] = \ 64 | 1. + (0.06 - 0.42*((57600./(vdelsq1[:] + 57600. + vf1[:])) + 65 | (57600./(vdelmsq1[:] + 57600. + vmf1[:]))))/(1. + 0.3*vf2[:]) 66 | 67 | def spectra(self, temperature, pressure, vmr): 68 | nh2o = dry_air_number_density(pressure, temperature, vmr)*vmr["H2O"] 69 | n = air_number_density(pressure, temperature, vmr) 70 | rad = radiation_term(self.grid()[:], temperature) 71 | return \ 72 | (1. - (nh2o/n))*(pressure/P0)*(T0/temperature)*1.e-20*nh2o*rad * \ 73 | self.scale[:]*self.data.data[:] 74 | 75 | def grid(self): 76 | return self.data.wavenumbers() 77 | -------------------------------------------------------------------------------- /pyLBL/plugins.py: -------------------------------------------------------------------------------- 1 | """Manage the model 'back-ends' using a plug-in model.""" 2 | 3 | from pkg_resources import get_entry_map 4 | from re import match 5 | 6 | 7 | plugins = get_entry_map("pyLBL") 8 | models = plugins.keys() 9 | 10 | # Create dictionary of molecular line backend plugins. 11 | # Key = string model name, Value = Gas object. 12 | molecular_lines = {} 13 | for key, value in plugins.items(): 14 | if "Gas" in value: 15 | molecular_lines[key] = value["Gas"].load() 16 | 17 | # Create dictionary of cross section backend plugins. 18 | # Key = string model name, Value = CrossSection object. 19 | cross_sections = {} 20 | for key, value in plugins.items(): 21 | if "CrossSection" in value: 22 | cross_sections[key] = value["CrossSection"].load() 23 | 24 | # Create a dictionary of continua backend plugins. 25 | # Key = string model name, Value = *Continuum object. 26 | continua = {} 27 | for key, value in plugins.items(): 28 | local_dict = {} 29 | for k, v in value.items(): 30 | m = match(r"([A-Za-z0-9]+)Continuum", k) 31 | if m: 32 | local_dict[m.group(1)] = v.load() 33 | if local_dict: 34 | continua[key] = local_dict 35 | -------------------------------------------------------------------------------- /pyLBL/pyarts_frontend/__init__.py: -------------------------------------------------------------------------------- 1 | from .frontend import PyArtsGas 2 | -------------------------------------------------------------------------------- /pyLBL/pyarts_frontend/frontend.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | 3 | 4 | info = getLogger(__name__).info 5 | arts_installed = False 6 | try: 7 | import pyarts 8 | arts_installed = True 9 | except ImportError: 10 | info("pyarts is not installed.") 11 | 12 | 13 | def absorption_line(line): 14 | """Translates a single pyLBL Transition object to ARTS AbsorptionSingleLine. 15 | 16 | Args: 17 | line: A pyLBL Transition object. 18 | 19 | Returns: 20 | q_key: QuantumIdentifier - the pyarts ID of the absorption species 21 | ls: AbsorptionSingleLine - A single ARTS absorption line 22 | """ 23 | iso = line.local_iso_id 24 | if iso == 11: 25 | iso = 'A' 26 | elif iso == 12: 27 | iso = 'B' 28 | iso = str(iso) 29 | 30 | r = pyarts.arts.hitran.ratio(line.molecule_id, iso) 31 | qkey = pyarts.arts.hitran.quantumidentity(line.molecule_id, iso) 32 | 33 | slf = pyarts.arts.LineShapeSingleSpeciesModel( 34 | G0=pyarts.arts.LineShapeModelParameters("T1", 35 | pyarts.arts.convert.kaycm_per_atm2hz_per_pa( 36 | line.gamma_self), 37 | line.n_air), 38 | D0=pyarts.arts.LineShapeModelParameters("T0", 39 | pyarts.arts.convert.kaycm_per_atm2hz_per_pa( 40 | line.delta_air) 41 | )) 42 | 43 | air = pyarts.arts.LineShapeSingleSpeciesModel( 44 | G0=pyarts.arts.LineShapeModelParameters("T1", 45 | pyarts.arts.convert.kaycm_per_atm2hz_per_pa( 46 | line.gamma_air), 47 | line.n_air), 48 | D0=pyarts.arts.LineShapeModelParameters("T0", 49 | pyarts.arts.convert.kaycm_per_atm2hz_per_pa( 50 | line.delta_air) 51 | )) 52 | 53 | sl = pyarts.arts.AbsorptionSingleLine( 54 | F0=pyarts.arts.convert.kaycm2freq(line.nu), 55 | I0=pyarts.arts.convert.kaycm_per_cmsquared2hz_per_msquared(line.sw / r), 56 | E0=pyarts.arts.convert.kaycm2joule(line.elower), 57 | lineshape=pyarts.arts.LineShapeModel([slf, air])) 58 | 59 | return qkey, sl 60 | 61 | 62 | def absorption_lines(lines): 63 | """Translates a list of pyLBL Transition object to ARTS ArrayOfAbsorptionLines 64 | 65 | Args: 66 | lines: List of Transition database entries for all the lines. 67 | 68 | Returns: 69 | lines: ArrayOfAbsorptionLines as pyarts abs_lines 70 | """ 71 | data = {} 72 | 73 | for line in lines: 74 | qkey, sl = absorption_line(line) 75 | key = str(qkey) 76 | if key in data: 77 | data[key].append(sl) 78 | else: 79 | data[key] = [sl] 80 | 81 | aal = pyarts.arts.ArrayOfAbsorptionLines() 82 | for x in data: 83 | aal.append( 84 | pyarts.arts.AbsorptionLines( 85 | selfbroadening=True, 86 | bathbroadening=True, 87 | cutoff="None", 88 | mirroring="None", 89 | population="LTE", 90 | normalization="SFS", 91 | lineshapetype="SplitVP", 92 | quantumidentity=x, 93 | broadeningspecies=[x.split('-')[0], "Bath"], 94 | T0=296, 95 | lines=data[x] 96 | )) 97 | return aal 98 | 99 | 100 | class PyArtsGas(object): 101 | def __init__(self, lines_database, formula): 102 | if not arts_installed: 103 | raise ValueError("pyarts is not installed.") 104 | self.ws = pyarts.workspace.Workspace() 105 | self.ws.abs_speciesSet(species=[formula]) 106 | self.ws.abs_lines_per_species = [absorption_lines(lines_database.gas(formula)[2])] 107 | 108 | self.ws.jacobianOff() 109 | self.ws.Touch(self.ws.rtp_nlte) 110 | self.ws.Touch(self.ws.rtp_mag) 111 | self.ws.Touch(self.ws.rtp_los) 112 | self.ws.propmat_clearsky_agendaAuto() 113 | self.ws.lbl_checkedCalc() 114 | self.ws.stokes_dim = 1 115 | 116 | def absorption_coefficient(self, temperature, pressure, volume_mixing_ratio, grid, 117 | remove_pedestal=False, cut_off=25): 118 | """Calculates absorption coefficient. 119 | 120 | Args: 121 | temperature: Temperature [K]. 122 | pressure: Pressure [Pa]. 123 | volume_mixing_ratio: Volume mixing ratio [mol mol-1]. 124 | grid: Numpy array defining the spectral grid [cm-1]. 125 | remove_pedestal: Flag specifying if a pedestal should be subtracted. 126 | cut_off: Wavenumber cut-off distance [cm-1] from line centers. 127 | 128 | Returns: 129 | Numpy array of absorption coefficients [m2]. 130 | """ 131 | # Configure spectral grid. 132 | self.ws.f_grid = pyarts.arts.convert.kaycm2freq(grid) 133 | 134 | # Configure the atmosphere. 135 | self.ws.rtp_pressure = pressure 136 | self.ws.rtp_temperature = temperature 137 | self.ws.rtp_vmr = [volume_mixing_ratio] 138 | 139 | # Calculate the absorption coefficient. 140 | self.ws.AgendaExecute(a=self.ws.propmat_clearsky_agenda) 141 | x = pyarts.arts.physics.number_density(pressure, temperature) * volume_mixing_ratio 142 | return self.ws.propmat_clearsky.value.data.value.flatten() / x 143 | -------------------------------------------------------------------------------- /pyLBL/spectroscopy.py: -------------------------------------------------------------------------------- 1 | """Provides a simplified API for calculating molecular line spectra.""" 2 | from collections import namedtuple 3 | 4 | from numpy import sum as npsum 5 | from numpy import unravel_index, zeros 6 | from xarray import DataArray, Dataset 7 | 8 | from .atmosphere import Atmosphere 9 | from .database import AliasNotFoundError, CrossSectionNotFoundError, \ 10 | IsotopologuesNotFoundError, TipsDataNotFoundError, \ 11 | TransitionsNotFoundError 12 | from .plugins import continua, cross_sections, molecular_lines 13 | 14 | 15 | kb = 1.38064852e-23 # Boltzmann constant [J K-1]. 16 | 17 | 18 | def number_density(temperature, pressure, volume_mixing_ratio): 19 | """Calculates the number density using the ideal gas law. 20 | 21 | Args: 22 | temperature: Temperature [K]. 23 | pressure: Pressure [Pa]. 24 | volume_mixing_ratio: Volume-mixing ratio [mol mol-1]. 25 | 26 | Returns: 27 | Number density [m-3]. 28 | """ 29 | return pressure*volume_mixing_ratio/(kb*temperature) 30 | 31 | 32 | class MoleculeCache(object): 33 | """Helper class that caches molecular data so it can be reused. 34 | 35 | Attributes: 36 | cross_section: Object controlling the current absorption cross section backend plugin. 37 | gas: Object controlling the current lines backend plugin. 38 | gas_continua: List of objects controlling the curret continua backend plugin. 39 | """ 40 | def __init__(self, name, grid, lines_database, lines_engine, continua_engine, 41 | cross_sections_engine): 42 | """Initializes the internal cache. 43 | 44 | Args: 45 | name: String chemical formula. 46 | grid: Numpy array defining the spectral grid [cm-1]. 47 | lines_database: Database object controlling the spectral database. 48 | lines_engine: Gas object to use for the lines calculation. 49 | continua_engine: Continuum object to use for the molecular continua. 50 | cross_sections_engine: CrossSection object to use for the cross 51 | section calculation. 52 | """ 53 | try: 54 | self.gas = lines_engine(lines_database, name) 55 | except (AliasNotFoundError, IsotopologuesNotFoundError, 56 | TipsDataNotFoundError, TransitionsNotFoundError): 57 | self.gas = None 58 | if name == "H2O": 59 | names = ["{}{}".format(name, x) for x in ["Foreign", "Self"]] 60 | else: 61 | names = [name, ] 62 | try: 63 | self.gas_continua = [continua_engine[x]() for x in names] 64 | except KeyError: 65 | self.gas_continua = None 66 | try: 67 | self.cross_section = cross_sections_engine(name, lines_database.arts_crossfit(name)) 68 | except (AliasNotFoundError, CrossSectionNotFoundError): 69 | self.cross_section = None 70 | 71 | 72 | class Spectroscopy(object): 73 | """Line-by-line gas optics. 74 | 75 | Attributes: 76 | atmosphere: Atmosphere object describing atmospheric conditions. 77 | cache: Dictionary of MoleculeCache objects. 78 | continua_backend: String name of model to use for the continua. 79 | continua_engine: Object exposed by the current molecular continuum backed. 80 | cross_section_backend: String name of model to use for the cross sections. 81 | cross_section_engine: Object exposed by the current cross sections backend. 82 | grid: Numpy array describing the spectral grid [cm-1]. 83 | lines_backend: String name of model to use for lines calculation. 84 | lines_database: Database object controlling the spectral database. 85 | lines_engine: Object exposed by the current molecular lines backend. 86 | output: namedtuple object that stores some output dataset metadata. 87 | """ 88 | def __init__(self, atmosphere, grid, database, mapping=None, 89 | lines_backend="pyLBL", continua_backend="mt_ckd", 90 | cross_sections_backend="arts_crossfit"): 91 | """Initializes object. 92 | 93 | Example: 94 | mapping = { 95 | "play": , 96 | "tlay": , 97 | "mole_fraction: { 98 | "H2O" : , 99 | "CO2" : , 100 | ... 101 | } 102 | } 103 | 104 | Args: 105 | atmosphere: xarray Dataset describing atmospheric conditions. 106 | grid: Wavenumber grid array [cm-1]. 107 | database: Database object to use. 108 | mapping: Dictionary describing atmospheric dataset variable names. 109 | lines_backend: String name of the model to use for the lines calculation. 110 | continua_backend: String name of the model to use for the molecular continua. 111 | cross_sections_backend: String name of the model to use for the cross 112 | section calculation. 113 | """ 114 | self.atmosphere = Atmosphere(atmosphere, mapping=mapping) 115 | self.grid = grid 116 | self.lines_database = database 117 | self.lines_backend = lines_backend 118 | self.lines_engine = molecular_lines[lines_backend] 119 | self.continua_backend = continua_backend 120 | self.continua_engine = continua[continua_backend] 121 | self.cross_sections_backend = cross_sections_backend 122 | self.cross_sections_engine = cross_sections[cross_sections_backend] 123 | self.cache = {} 124 | 125 | # Prepare metadata for the ouput xarray Dataset. 126 | Output = namedtuple("Output", ["dims", "dim_sizes", "mechanisms", "units"]) 127 | mechanisms = ["lines", "continuum", "cross_section"] 128 | dims = list(self.atmosphere.temperature.dims) + ["mechanism", "wavenumber", ] 129 | dim_sizes = \ 130 | [x for x in self.atmosphere.temperature.sizes.values()] + \ 131 | [len(mechanisms), self.grid.size] 132 | units = {"units": "m-1"} 133 | self.output = Output(dims=dims, dim_sizes=dim_sizes, mechanisms=mechanisms, 134 | units=units) 135 | 136 | def list_molecules(self): 137 | """Provides a list of molecules available in the specral lines database. 138 | 139 | Returns: 140 | List of string molecule formulae available in the specral lines database. 141 | """ 142 | return self.lines_database.molecules() 143 | 144 | def compute_absorption(self, output_format="all", remove_pedestal=None): 145 | """Computes absorption coefficient [m-1] at specified wavenumbers given temperature, 146 | pressure, and gas concentrations. 147 | 148 | Args: 149 | output_format: String describing how the absorption data should be output. 150 | "all" - returns absorption spectra from all components (lines, 151 | continuum, cross section) for all gases separately. 152 | "gas" - returns total absorption spectra for all gases 153 | separately. 154 | "total" - returns the total spectra. 155 | remove_pedestal: Flag that allows the user to not subtract off the 156 | MT-CKD water vapor "pedestal" if desired. 157 | 158 | Returns: 159 | An xarray Dataset of absorption coefficients [m-1]. 160 | """ 161 | pressure = self.atmosphere.pressure.data.flat 162 | temperature = self.atmosphere.temperature.data.flat 163 | if remove_pedestal is None: 164 | remove_pedestal = self.continua_backend == "mt_ckd" 165 | beta = {} 166 | for name, mole_fraction in self.atmosphere.gases.items(): 167 | varname = "{}_absorption".format(name) 168 | beta[varname] = DataArray(zeros(self.output.dim_sizes), dims=self.output.dims, 169 | attrs=self.output.units) 170 | try: 171 | # Grab cached spectral database data. 172 | data = self.cache[name] 173 | except KeyError: 174 | # If not already cached, then cache it. 175 | data = MoleculeCache(name, self.grid, self.lines_database, 176 | self.lines_engine, self.continua_engine, 177 | self.cross_sections_engine) 178 | self.cache[name] = data 179 | for i in range(self.atmosphere.temperature.data.size): 180 | vmr = {x: y.data.flat[i] for x, y in self.atmosphere.gases.items()} 181 | n = number_density(temperature[i], pressure[i], 182 | mole_fraction.data.flat[i]) 183 | j = unravel_index(i, self.atmosphere.temperature.data.shape) 184 | 185 | # Calculate lines. 186 | if data.gas is not None: 187 | k = data.gas.absorption_coefficient(temperature[i], pressure[i], 188 | mole_fraction.data.flat[i], self.grid, 189 | remove_pedestal=remove_pedestal) 190 | indices = tuple(list(j) + [0, slice(None)]) 191 | beta[varname].values[indices] = n*k[:self.grid.size] 192 | 193 | # Calculate continua. 194 | if data.gas_continua is not None: 195 | indices = tuple(list(j) + [1, slice(None)]) 196 | for continuum in data.gas_continua: 197 | k = continuum.spectra(temperature[i], pressure[i], vmr, self.grid) 198 | beta[varname].values[indices] += k[:] 199 | 200 | # Calculate the cross section. 201 | if data.cross_section is not None: 202 | k = data.cross_section.absorption_coefficient(self.grid, temperature[i], 203 | pressure[i]) 204 | indices = tuple(list(j) + [2, slice(None)]) 205 | beta[varname].values[indices] = n*k[:] 206 | return self._create_output_dataset(beta, output_format) 207 | 208 | def _create_output_dataset(self, absorption, output_format): 209 | """Creates an xarray Dataset with the calculated absorption values. 210 | 211 | Args: 212 | absorption: Dictionary containing the absorptoin data. 213 | output_format: String describing how the data should be output. 214 | 215 | Returns: 216 | xarray Dataset containing the absorption in the desired format. 217 | """ 218 | wavenumber = DataArray(self.grid, dims=("wavenumber",), attrs={"units": "cm-1"}) 219 | data_vars = {"wavenumber": wavenumber, } 220 | dims = list(self.output.dims) 221 | units = self.output.units 222 | if output_format == "all": 223 | data_vars["mechanism"] = DataArray(self.output.mechanisms, dims=("mechanism",)) 224 | data_vars.update(absorption) 225 | elif output_format == "gas": 226 | dims.pop(-2) 227 | data_vars.update({x: DataArray(npsum(y.values, axis=-2), dims=dims, attrs=units) 228 | for x, y in absorption.items()}) 229 | else: 230 | dims.pop(-2) 231 | data = [DataArray(npsum(x.values, axis=-2), dims=dims, attrs=units) for x in 232 | absorption.values()] 233 | data_vars["absorption"] = DataArray(sum([x.values for x in data]), 234 | dims=dims, attrs=units) 235 | return Dataset(data_vars=data_vars) 236 | -------------------------------------------------------------------------------- /pyLBL/tips.py: -------------------------------------------------------------------------------- 1 | """Controls the TIPS (total internal partition function) calculations.""" 2 | 3 | from numpy import searchsorted 4 | 5 | 6 | TIPS_REFERENCE_TEMPERATURE = 296. # TIPS reference temperature [K]. 7 | 8 | 9 | class TotalPartitionFunction(object): 10 | """Total partition function, using data from TIPS 2017 (doi: 10.1016/j.jqsrt.2017.03.045). 11 | 12 | Attributes: 13 | data: Numpy array of total partition function values (isotopologue, temperature). 14 | molecule: String molecule chemical formula. 15 | temperature: Numpy array of temperatures [K]. 16 | """ 17 | def __init__(self, molecule, temperature, data): 18 | self.molecule = molecule 19 | self.temperature = temperature 20 | self.data = data 21 | 22 | @property 23 | def isotopologue(self): 24 | return [x for x in range(self.data.shape[0])] 25 | 26 | def total_partition_function(self, temperature, isotopologue): 27 | """Interpolates the total partition function values from the TIPS 2017 table. 28 | 29 | Args: 30 | temperature: Temperature [K]. 31 | isotopologue: Isotopologue id. 32 | 33 | Returns: 34 | Total partition function. 35 | """ 36 | i = isotopologue - 1 37 | j = searchsorted(self.temperature, temperature, side="left") - 1 38 | return self.data[i, j] + (self.data[i, j+1] - self.data[i, j]) * \ 39 | (temperature - self.temperature[j])/(self.temperature[j+1] - self.temperature[j]) 40 | -------------------------------------------------------------------------------- /pyLBL/webapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .hitran_api import HitranWebApi, NoIsotopologueError, NoTransitionsError 2 | from .tips_api import NoMoleculeError, TipsWebApi 3 | -------------------------------------------------------------------------------- /pyLBL/webapi/hitran_api.py: -------------------------------------------------------------------------------- 1 | """Defines the API for interacting with the HITRAN database.""" 2 | 3 | from json import loads 4 | from urllib.error import HTTPError 5 | from urllib.request import build_opener, install_opener, ProxyHandler, urlopen 6 | 7 | 8 | class HitranWebApi(object): 9 | """Controls access to HITRAN's web API. 10 | 11 | Attributes: 12 | api_key: String hitran.org api key. 13 | api_version: String version of the api to use. 14 | cross_section_directory: String directory where cross section files are located. 15 | host: URL to retrieve the data from. 16 | parameters: List of Struct objects describing the HITRAN parameters. 17 | proxy: Web proxy (optional). 18 | timestamp: String time stamp telling when the server was accessed. 19 | transition_directory: String directory where transitions files are located. 20 | """ 21 | def __init__(self, api_key, api_version="v2", host="https://hitran.org", 22 | proxy=None): 23 | """Initializes object. 24 | 25 | Args: 26 | api_key: String hitran.org api key. 27 | api_version: String version of the api to use. 28 | host: URL to retrieve the data from. 29 | proxy: Currently not used. 30 | """ 31 | self.api_key = api_key 32 | self.api_version = api_version 33 | self.host = host 34 | self.proxy = proxy 35 | server_info = self._download_server_info() 36 | self.transition_directory = server_info["content"]["data"]["results_dir"] 37 | self.cross_section_directory = server_info["content"]["data"]["xsec_dir"] 38 | self.timestamp = server_info["timestamp"] 39 | self.parameters = self._download_parameters_metadata() 40 | 41 | def _download(self, url, chunk): 42 | """Downloads data from a url. 43 | 44 | Args: 45 | url: URL to retrieve data from. 46 | chunk: Size of data reads in bytes. 47 | 48 | Returns: 49 | String containing the response data. 50 | """ 51 | if self.proxy: 52 | install_opener(build_opener(ProxyHandler(self.proxy))) 53 | response = urlopen(url) 54 | data = [] 55 | while True: 56 | buf = response.read(chunk) 57 | if not buf: 58 | break 59 | data.append(buf.decode("utf-8")) 60 | return "".join(data) 61 | 62 | def _download_file(self, prefix, name, chunk=64*1024*1024): 63 | """Downloads a data file from hitran.org. 64 | 65 | Returns: 66 | String containing the contents of the data file. 67 | """ 68 | return self._download("/".join([self.host, prefix, name]), chunk) 69 | 70 | def _download_parameters_metadata(self, pattern=None): 71 | """Downloads metadata about the available HITRAN parameters. 72 | 73 | Args: 74 | pattern: Substring pattern. 75 | 76 | Returns: 77 | List of Struct objects containing the response data. 78 | """ 79 | query = None if pattern is None else Query(name__icontains=pattern) 80 | return [Struct(**x) for x in 81 | self._download_section("parameter-metas", query)["content"]["data"]] 82 | 83 | def _download_section(self, api_section, query=None, chunk=1024*1024): 84 | """Downloads data from the hitran.org website. 85 | 86 | Args: 87 | api_section: String name of the section of the database to use. 88 | query: String describing the HTML options. 89 | chunk: Size of data reads in bytes. 90 | 91 | Returns: 92 | JSON string containing the response data. 93 | """ 94 | url = "/".join([self.host, "api", self.api_version, self.api_key, api_section]) 95 | if query is not None: 96 | url = "?".join([url, query.string]) 97 | if self.proxy: 98 | install_opener(build_opener(ProxyHandler(self.proxy))) 99 | return loads(self._download(url, chunk)) 100 | 101 | def _download_server_info(self): 102 | """Downloads internal information about the hitran.org server. 103 | 104 | Returns: 105 | JSON string containing the response data. 106 | """ 107 | return self._download_section("info") 108 | 109 | def download_data_sources(self, ids=None): 110 | """Downloads information about the source of the line data (papers, etc.) 111 | 112 | Args: 113 | ids: Isotopologue ids. 114 | 115 | Returns: 116 | JSON string containing the response data. 117 | """ 118 | query = None if ids is None else Query(id__in=ids) 119 | return self._download_section("sources", query)["content"]["data"] 120 | 121 | def download_molecules(self): 122 | """Downloads the molecules available in HITRAN. 123 | 124 | Returns: 125 | List of Struct objects containing the response data. 126 | """ 127 | return [Struct(**x) for x in self._download_section("molecules")["content"]["data"]] 128 | 129 | def download_isotopologues(self, molecules): 130 | """Downloads the isotopologues available in HITRAN. 131 | 132 | Args: 133 | molecules: List of Struct objects. 134 | 135 | Returns: 136 | List of Struct objects containing the response data. 137 | """ 138 | if type(molecules) not in [list, tuple]: 139 | molecules = [molecules, ] 140 | ids = [x.id for x in molecules] 141 | return [Struct(**x) for x in self._download_section("isotopologues", 142 | Query(molecule_id__in=ids))["content"]["data"]] 143 | 144 | def download_transitions(self, isotopologues, numin, numax, parameters=None): 145 | """Downloads transitions for isotopologues available in HITRAN. 146 | 147 | Args: 148 | isotopologues: List of Struct objects. 149 | numin: Wavenumber lower bound [cm-1]. 150 | numax: Wavenumber upper bound [cm-1]. 151 | parameters: List of parameters to download. 152 | 153 | Returns: 154 | List of Struct objects containing the response data. 155 | """ 156 | if type(isotopologues) not in [list, tuple]: 157 | isotopologues = [isotopologues, ] 158 | ids = [x.id for x in isotopologues] 159 | if not ids: 160 | raise NoIsotopologueError("no isotopologues present.") 161 | if parameters is None: 162 | parameters = [x.name for x in self.parameters][:22] 163 | query = Query(iso_ids_list=ids, numin=numin, numax=numax, head=False, 164 | fixwidth=0, request_params=",".join(parameters)) 165 | try: 166 | name = self._download_section("transitions", query)["content"]["data"] 167 | except HTTPError: 168 | raise NoTransitionsError("no transitions found for {}.".format( 169 | isotopologues[0].molecule_alias)) 170 | data = self._download_file(self.transition_directory, name) 171 | 172 | # Parse the file. 173 | transitions = [] 174 | type_mapping = {"float": float, "int": int, "str": str} 175 | types = [type_mapping[x.type] for x in self.parameters] 176 | for line in data.split("\n"): 177 | line = line.strip() 178 | if not line: 179 | continue 180 | try: 181 | transitions.append(Struct(**{x: y(z) for x, y, z in 182 | zip(parameters, types, line.split(","))})) 183 | except ValueError: 184 | print("skipping transition: {}".format(line)) 185 | return transitions 186 | 187 | def download_cross_sections(self, molecules): 188 | """Downloads cross-sections for molecules available in the HITRAN database. 189 | 190 | Args: 191 | molecules: List of Struct objcts. 192 | 193 | Returns: 194 | List of Struct objects containing the response data. 195 | """ 196 | if type(molecules) not in [list, tuple]: 197 | molecules = [molecules, ] 198 | ids = [x.id for x in molecules] 199 | query = Query(molecule_id__in=ids) 200 | bands = self._download_section("cross-sections", query)["content"]["data"] 201 | cross_sections = [] 202 | for band in bands: 203 | data = self._download_file(self.cross_section_directory, band["filename"]) 204 | attrs = {"data": data} 205 | attrs.update(band) 206 | cross_sections.append(Struct(**attrs)) 207 | return cross_sections 208 | 209 | 210 | class NoCrossSectionError(BaseException): 211 | pass 212 | 213 | 214 | class NoIsotopologueError(BaseException): 215 | pass 216 | 217 | 218 | class NoTransitionsError(BaseException): 219 | pass 220 | 221 | 222 | class Query(object): 223 | """URL parameter (query string) helper class. 224 | 225 | Attributes: 226 | string: String containing URL parameters. 227 | """ 228 | def __init__(self, **argv): 229 | self.string = "&".join(["{}={}".format(key, self.process(argv[key])) for key in argv]) 230 | 231 | def __and__(self, q): 232 | q = Query() 233 | q.string = "&".join([self.string, q.string]) 234 | return q 235 | 236 | @staticmethod 237 | def process(val): 238 | """Process argument value and convert to string.""" 239 | if type(val) in [bool, float, int, str]: 240 | return str(val) 241 | if type(val) in [list, set, tuple]: 242 | return ",".join(str(v) for v in val) 243 | raise TypeError("bad type for query: '{}'".format(val)) 244 | 245 | 246 | class Struct(object): 247 | def __init__(self, **attrs): 248 | self.__dict__.update(attrs) 249 | -------------------------------------------------------------------------------- /pyLBL/webapi/tips_api.py: -------------------------------------------------------------------------------- 1 | """Defines the API for interacting with the TIPS data stored on the web.""" 2 | 3 | from re import match 4 | from urllib.request import urlopen 5 | 6 | from numpy import asarray, float32, transpose 7 | 8 | 9 | class TipsWebApi(object): 10 | """Controls access to TIPS web API. 11 | 12 | Attributes: 13 | url: String url where data is downloaded from. 14 | """ 15 | def __init__(self): 16 | """Constructs the object.""" 17 | self.url = "http://faculty.uml.edu/Robert_Gamache/Software/temp/Supplementary_file.txt" 18 | 19 | def download(self, molecule): 20 | """Downloads the data from the internet. 21 | 22 | Args: 23 | molecule: String molecule chemical formula. 24 | 25 | Returns: 26 | temperature: Numpy array of temperatures. 27 | data: Numpy array of data values. 28 | """ 29 | return self._parse_records(self._records(urlopen(self.url), molecule)) 30 | 31 | @staticmethod 32 | def _ascii_table_records(response, block_size=512): 33 | """Reads the next line from an ascii table. 34 | 35 | Args: 36 | response: A http.client.HTTPResponse object. 37 | block_size: Size in bytes of blocks that will be read. 38 | 39 | Yields: 40 | String record of the http table. 41 | """ 42 | record = "" 43 | while True: 44 | block = response.read(block_size).decode("utf-8") 45 | lines = block.split("\n") 46 | if lines[-1] == "": 47 | # If a block ends with a new line character, delete the last 48 | # element of the list because it will be an empty string. 49 | del lines[-1] 50 | for line in lines[:-1]: 51 | # Return complete lines within a block. 52 | record += line 53 | yield record 54 | record = "" 55 | if len(block) != block_size: 56 | # This is the last block. 57 | try: 58 | yield record + lines[-1] 59 | except IndexError: 60 | yield record 61 | break 62 | elif block.endswith("\n"): 63 | # No carry-over data between blocks. 64 | yield lines[-1] 65 | record = "" 66 | else: 67 | # Carry partial last line over to next block. 68 | record = lines[-1] 69 | 70 | @staticmethod 71 | def _parse_records(records): 72 | """Parses all table records and stores the data. 73 | 74 | Args: 75 | records: A list of iterable database record values. 76 | 77 | Returns: 78 | temperature: Numpy array of temperatures. 79 | data: Numpy array of data values. 80 | """ 81 | temperature, q = [], [] 82 | for record in records: 83 | if record: 84 | temperature.append(record[0]) 85 | q.append(record[1:]) 86 | temperature = asarray(temperature, dtype=float32) 87 | data = transpose(asarray(q, dtype=float32)) 88 | return temperature, data 89 | 90 | def _records(self, response, molecule): 91 | """Parses the HTTP table for all records related to the input molecule. 92 | 93 | Args: 94 | response: A http.client.HTTPResponse object. 95 | molecule: Molecule id. 96 | 97 | Yields: 98 | A list of floats from a record from the http table. 99 | 100 | Raises: 101 | NoMoleculeError: Failed to find the input molecule. 102 | """ 103 | found_molecule = False 104 | num_isotopologues = 0 105 | for line in self._ascii_table_records(response): 106 | if found_molecule: 107 | if match(r"\s*[A-Za-z0-9+]+$", line): 108 | break 109 | elif num_isotopologues > 0: 110 | yield [float32(x.strip()) for x in line.split()[:(num_isotopologues+1)]] 111 | elif match(r"\s*T / K", line): 112 | num_isotopologues = sum(x == "Q" for x in line) 113 | elif line.startswith("c"): 114 | # Ignore comments. 115 | continue 116 | else: 117 | found_molecule = match(r"\s*{}$".format(molecule), line) 118 | if not found_molecule: 119 | raise NoMoleculeError(f"molecule {molecule} not found in TIPS 2017 tables.") 120 | 121 | 122 | class NoMoleculeError(BaseException): 123 | """No TIPS data found for this molecule.""" 124 | pass 125 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import Extension, setup 2 | 3 | 4 | def c_gas_optics_lib(): 5 | """Defines c extension library.""" 6 | directory = "pyLBL/c_lib" 7 | src = ["{}/{}".format(directory, x) for x in 8 | ["absorption.c", "spectra.c", "spectral_database.c", "voigt.c"]] 9 | return Extension("pyLBL.c_lib.libabsorption", 10 | sources=src, 11 | include_dirs=[directory,], 12 | extra_compile_args=[], 13 | extra_link_args=["-lsqlite3", "-lm"]) 14 | 15 | 16 | # Required dependencies. 17 | install_requires = [ 18 | "netCDF4", 19 | "numpy", 20 | "scipy", 21 | "sqlalchemy", 22 | "xarray", 23 | ] 24 | 25 | 26 | # Documentation dependencies. 27 | doc_requires = [ 28 | "sphinx", 29 | "sphinxcontrib-apidoc", 30 | "sphinxcontrib-napoleon", 31 | "sphinx-autopackagesummary", 32 | "sphinx_pangeo_theme", 33 | ] 34 | 35 | 36 | # Optional dependencies. 37 | extras_require = { 38 | "complete": install_requires, 39 | "docs": doc_requires, 40 | "arts": ["pyarts",] 41 | } 42 | 43 | 44 | # Entry points. 45 | entry_points = { 46 | "pyLBL": ["Gas=pyLBL.c_lib.gas_optics:Gas",], 47 | "mt_ckd": [ 48 | "CO2Continuum=pyLBL.mt_ckd.carbon_dioxide:CarbonDioxideContinuum", 49 | "H2OForeignContinuum=pyLBL.mt_ckd.water_vapor:WaterVaporForeignContinuum", 50 | "H2OSelfContinuum=pyLBL.mt_ckd.water_vapor:WaterVaporSelfContinuum", 51 | "N2Continuum=pyLBL.mt_ckd.nitrogen:NitrogenContinuum", 52 | "O2Continuum=pyLBL.mt_ckd.oxygen:OxygenContinuum", 53 | "O3Continuum=pyLBL.mt_ckd.ozone:OzoneContinuum", 54 | ], 55 | "arts_crossfit": ["CrossSection=pyLBL.arts_crossfit.cross_section:CrossSection"], 56 | "arts": ["Gas=pyLBL.pyarts_frontend.frontend:PyArtsGas",], 57 | } 58 | 59 | 60 | setup( 61 | name="pyLBL", 62 | version="0.0.1", 63 | description="Line-by-line absorption calculators.", 64 | url="https://github.com/GRIPS-code/pyLBL", 65 | author="pyLBL Developers", 66 | license="LGPL-2.1", 67 | classifiers=[ 68 | "Programming Language :: Python :: 3", 69 | "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", 70 | "Operating System :: OS Independent", 71 | "Development Status :: 3 - Alpha", 72 | ], 73 | python_requires=">=3.6", 74 | packages=[ 75 | "pyLBL", 76 | "pyLBL.c_lib", 77 | "pyLBL.webapi", 78 | "pyLBL.pyarts_frontend", 79 | "pyLBL.mt_ckd", 80 | "pyLBL.arts_crossfit", 81 | ], 82 | install_requires=install_requires, 83 | extras_require=extras_require, 84 | entry_points=entry_points, 85 | ext_modules=[c_gas_optics_lib(), ], 86 | package_data={"": ["*.nc"], }, 87 | ) 88 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from ftplib import FTP 3 | from os import environ 4 | from os.path import isfile 5 | 6 | from numpy import arange, asarray 7 | import pytest 8 | from xarray import Dataset 9 | 10 | 11 | Atmos = namedtuple("Atmos", ["p", "t", "vmr"]) 12 | 13 | 14 | def variable(data, units, standard_name): 15 | """Create a tuple to aid in creating variables in an xarray Dataset. 16 | 17 | Args: 18 | data: Numpy array of data values. 19 | units: String physical units attribute. 20 | standard_name: String standard name attribute. 21 | 22 | Returns: 23 | A tuple containing the data needed to create a variable in an xarray Dataset. 24 | """ 25 | return (["layer",], data, {"units": units, "standard_name": standard_name}) 26 | 27 | 28 | @pytest.fixture 29 | def molecule_names(): 30 | names = { 31 | "H2O": "water_vapor", 32 | "CO2": "carbon_dioxide", 33 | "O3": "ozone", 34 | "N2O": "nitrous_oxide", 35 | "CO": "carbon_monoxide", 36 | "CH4": "methane", 37 | "O2": "oxygen", 38 | "N2": "nitrogen", 39 | } 40 | return names 41 | 42 | 43 | @pytest.fixture 44 | def spectral_grid(): 45 | return arange(1., 3250., 0.1) 46 | 47 | 48 | @pytest.fixture 49 | def coarse_grid(): 50 | return arange(1., 3000., 1.) 51 | 52 | 53 | @pytest.fixture 54 | def atmosphere(molecule_names): 55 | """Set conditions for a default test atmosphere. 56 | 57 | Returns: 58 | A Numpy array of pressure, temperature, and wavenumbers and a dictionary 59 | of molecule volume mixing ratios. 60 | """ 61 | pressure = asarray([117., 1032., 11419., 98388.]) # [Pa]. 62 | temperature = asarray([269.01, 227.74, 203.37, 288.99]) # [K]. 63 | volume_mixing_ratio = { 64 | molecule_names["H2O"]: asarray([5.244536e-06, 4.763972e-06, 3.039952e-06, 65 | 6.637074e-03]), 66 | molecule_names["CO2"]: asarray([0.00036, 0.00036, 0.00036, 0.00035999]), 67 | molecule_names["O3"]: asarray([2.936688e-06, 7.415223e-06, 2.609510e-07, 68 | 6.859128e-08]), 69 | molecule_names["N2O"]: asarray([1.050928e-08, 1.319584e-07, 2.895416e-07, 70 | 3.199949e-07]), 71 | molecule_names["CH4"]: asarray([2.947482e-07, 8.817705e-07, 1.588336e-06, 72 | 1.700002e-06]), 73 | molecule_names["CO"]: asarray([3.621464e-08, 1.761450e-08, 3.315927e-08, 74 | 1.482969e-07]), 75 | molecule_names["O2"]: asarray([0.209, 0.209, 0.2090003, 0.208996]), 76 | molecule_names["N2"]: asarray([0.78, 0.78, 0.78, 0.78]), 77 | } 78 | return Atmos(p=pressure, t=temperature, vmr=volume_mixing_ratio) 79 | 80 | 81 | @pytest.fixture 82 | def atmosphere_dataset(atmosphere): 83 | """Create an xarray Dataset for a test atmosphere. 84 | 85 | Args: 86 | pressure: Numpy array of pressure values [Pa]. 87 | temperature: Numpy array of temperature values [K]. 88 | volume_mixing_ratio: Dictionary of Numpy arrays of volume mixing ratios [mol mol-1]. 89 | 90 | Returns: 91 | An xarray Dataset for a test atmosphere. 92 | """ 93 | data_vars = { 94 | "pressure": variable(atmosphere.p, "Pa", "air_pressure"), 95 | "temperature": variable(atmosphere.t, "K", "air_temperature"), 96 | } 97 | for key, value in atmosphere.vmr.items(): 98 | standard_name = f"mole_fraction_of_{key}_in_air" 99 | data_vars[key] = variable(value, "mol mol-1", standard_name) 100 | return Dataset(data_vars=data_vars) 101 | 102 | 103 | @pytest.fixture 104 | def single_layer_atmosphere(atmosphere): 105 | data_vars = { 106 | "pressure": variable(atmosphere.p[-1:], "Pa", "air_pressure"), 107 | "temperature": variable(atmosphere.t[-1:], "K", "air_temperature"), 108 | } 109 | for key, value in atmosphere.vmr.items(): 110 | standard_name = f"mole_fraction_of_{key}_in_air" 111 | data_vars[key] = variable(value[-1:], "mol mol-1", standard_name) 112 | return Dataset(data_vars=data_vars) 113 | 114 | 115 | @pytest.fixture 116 | def downloaded_database(): 117 | name = "pyLBL-2-7-23.db" 118 | if isfile(name): 119 | return name 120 | with FTP("ftp.gfdl.noaa.gov") as ftp: 121 | ftp.login() 122 | ftp.cwd(environ["FTP_DB_DIR"]) 123 | ftp.retrbinary(f"RETR {name}", open(name, "wb").write) 124 | return name 125 | -------------------------------------------------------------------------------- /tests/test_artscrossfit.py: -------------------------------------------------------------------------------- 1 | from os.path import getsize, join 2 | 3 | from numpy import log, max, sum 4 | from pyLBL.arts_crossfit import CrossSection, download 5 | import pytest 6 | 7 | 8 | def test_artscrossfit(tmpdir, atmosphere, spectral_grid): 9 | download(tmpdir) 10 | formula = "CFC11" 11 | xsec = CrossSection(formula, join(tmpdir, "coefficients", f"{formula}.nc")) 12 | i = -1 13 | cross_section = xsec.absorption_coefficient(spectral_grid, atmosphere.t[i], 14 | atmosphere.p[i]) 15 | assert log(max(cross_section)) == pytest.approx(-49.102292000921295) 16 | dv = spectral_grid[1] - spectral_grid[0] 17 | assert log(sum(cross_section)*dv) == pytest.approx(-46.04953693253788) 18 | 19 | 20 | def test_download(tmpdir): 21 | dataset = { 22 | "C2F6.nc": 6374380, 23 | "CH3CCl3.nc": 3995343, 24 | "SF6.nc": 4158480, 25 | "CHCl3.nc": 4424122, 26 | "HFC125.nc": 4170602, 27 | "HCFC142b.nc": 7882125, 28 | "HCFC141b.nc": 15765917, 29 | "HFC143a.nc": 17705524, 30 | "HFC134a.nc": 28692364, 31 | "C8F18.nc": 2335741, 32 | "HFC227ea.nc": 4031126, 33 | "Halon1211.nc": 3928946, 34 | "CF4.nc": 4537000, 35 | "CFC113.nc": 4230484, 36 | "NF3.nc": 3928939, 37 | "HFC152a.nc": 6533367, 38 | "Halon1301.nc": 3988546, 39 | "CH2Cl2.nc": 3928942, 40 | "HFC245fa.nc": 155344, 41 | "CFC114.nc": 3680966, 42 | "CCl4.nc": 5110860, 43 | "cC4F8.nc": 5153911, 44 | "Halon2402.nc": 3962146, 45 | "CFC115.nc": 523723, 46 | "HCFC22.nc": 20721462, 47 | "HFC23.nc": 17535381, 48 | "CFC12.nc": 14987301, 49 | "C5F12.nc": 5857842, 50 | "SO2F2.nc": 3995341, 51 | "HFC365mfc.nc": 942146, 52 | "HFC32.nc": 5012929, 53 | "C4F10.nc": 3995341, 54 | "C3F8.nc": 3928940, 55 | "C6F14.nc": 2335741, 56 | "CFC11.nc": 5721655, 57 | "HFC4310mee.nc": 5957528, 58 | "HFC236fa.nc": 394544, 59 | } 60 | download(tmpdir) 61 | for key, value in dataset.items(): 62 | assert value == getsize(join(tmpdir, "coefficients", key)) 63 | -------------------------------------------------------------------------------- /tests/test_atmosphere.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pyLBL.atmosphere import Atmosphere 3 | from xarray import Dataset 4 | 5 | 6 | def setup_atmosphere(names, dataset, mapping=None): 7 | atm = Atmosphere(dataset, mapping=mapping) 8 | for name in ["pressure", "temperature"]: 9 | assert getattr(atm, name).equals(dataset.data_vars[name]) 10 | for key, value in names.items(): 11 | assert atm.gases[key].equals(dataset.data_vars[value]) 12 | 13 | 14 | def test_atmosphere_without_mapping(molecule_names, atmosphere_dataset): 15 | setup_atmosphere(molecule_names, atmosphere_dataset) 16 | 17 | 18 | def test_atmosphere_with_mapping(molecule_names, atmosphere_dataset): 19 | mapping = { 20 | "play": "pressure", 21 | "tlay": "temperature", 22 | "mole_fraction": molecule_names 23 | } 24 | setup_atmosphere(molecule_names, atmosphere_dataset, mapping) 25 | 26 | 27 | def test_missing_standard_name(atmosphere_dataset): 28 | data_vars = dict(atmosphere_dataset.data_vars) 29 | del data_vars["pressure"] 30 | dataset = Dataset(data_vars=data_vars) 31 | with pytest.raises(ValueError): 32 | _ = Atmosphere(dataset) 33 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from os.path import normpath, sep 3 | 4 | from pyLBL import Database, HitranWebApi 5 | 6 | 7 | def test_database(): 8 | molecules = ["H2O", "CO2", "CFC11"] 9 | database = Database(":memory:") 10 | webapi = HitranWebApi(api_key=environ["HITRAN_API_KEY"]) 11 | database.create(webapi, molecules=molecules) 12 | 13 | db_molecules = database.molecules() 14 | for molecule in molecules: 15 | assert molecule in db_molecules 16 | 17 | formula, mass, transitions, _ = database.gas("H2O") 18 | assert formula == "H2O" 19 | assert mass[0] == 18.010565 20 | assert transitions[0].nu == 0.000134 21 | assert transitions[-1].nu == 41999.696489 22 | 23 | t, data = database.tips("CO2") 24 | assert t[0] == 1. 25 | assert t[-1] == 5000. 26 | assert data[0, 0] == 1.1722999811172485 27 | assert data[-1, -1] == 1018400.0 28 | 29 | path = database.arts_crossfit("CFC11") 30 | assert normpath(path).split(sep)[-3:] == [".cross-sections", "coefficients", "CFC11.nc"] 31 | -------------------------------------------------------------------------------- /tests/test_gas_optics.py: -------------------------------------------------------------------------------- 1 | from numpy import log, max 2 | from pyLBL import Database, Gas 3 | import pytest 4 | 5 | 6 | def test_gas_optics(molecule_names, atmosphere, spectral_grid, downloaded_database): 7 | database = Database(downloaded_database) 8 | formula = "H2O" 9 | vmr_name = molecule_names[formula] 10 | layer = -1 11 | gas = Gas(database, formula) 12 | k = gas.absorption_coefficient(temperature=atmosphere.t[layer], 13 | pressure=atmosphere.p[layer], 14 | volume_mixing_ratio=atmosphere.vmr[vmr_name][layer], 15 | grid=spectral_grid) 16 | k = k[:spectral_grid.size] 17 | assert log(max(k)) == pytest.approx(-48.159224953962244) 18 | dv = spectral_grid[1] - spectral_grid[0] 19 | assert log(sum(k)*dv) == pytest.approx(-46.496121930910135) 20 | -------------------------------------------------------------------------------- /tests/test_mt_ckd.py: -------------------------------------------------------------------------------- 1 | from numpy import sum 2 | from pyLBL.mt_ckd.carbon_dioxide import CarbonDioxideContinuum 3 | from pyLBL.mt_ckd.nitrogen import NitrogenContinuum 4 | from pyLBL.mt_ckd.oxygen import OxygenContinuum 5 | from pyLBL.mt_ckd.ozone import OzoneContinuum 6 | from pyLBL.mt_ckd.water_vapor import WaterVaporForeignContinuum 7 | from pyLBL.mt_ckd.water_vapor import WaterVaporSelfContinuum 8 | import pytest 9 | 10 | 11 | def create_vmr_dict(atmosphere, names, index): 12 | return {key: atmosphere.vmr[value][index] for key, value in names.items()} 13 | 14 | 15 | def reference(): 16 | values = { 17 | "CO2": [21.284607102488753, ], 18 | "H2OForeign": [131.87162317621952, ], 19 | "H2OSelf": [13.482864611247933, ], 20 | "N2": [0.7612890022253513, 0.5875825355004741, 0.00414557543788256, ], 21 | "O2": [0.24690308716508605, 0.11052072297118236, 0.03200556021322852, 22 | 0.04514938962400228, 0.03897535512343981, 285.7607588975901, 23 | 4419601.794329887, ], 24 | "O3": [0.0006562127133778276, 1.7334221226752753, 0.05197265302394795, ], 25 | } 26 | return values 27 | 28 | 29 | def test_mt_ckd_co2(atmosphere, molecule_names): 30 | index = -1 31 | vmr = create_vmr_dict(atmosphere, molecule_names, index) 32 | values = reference() 33 | 34 | continua_dict = { 35 | "CO2": CarbonDioxideContinuum(), 36 | "H2OForeign": WaterVaporForeignContinuum(), 37 | "H2OSelf": WaterVaporSelfContinuum(), 38 | "N2": NitrogenContinuum(), 39 | "O2": OxygenContinuum(), 40 | "O3": OzoneContinuum(), 41 | } 42 | 43 | for molecule, continua in continua_dict.items(): 44 | for band, continuum in enumerate(continua.bands): 45 | spectra = continuum.spectra(atmosphere.t[index], atmosphere.p[index], vmr) 46 | assert values[molecule][band] == pytest.approx(sum(spectra)) 47 | -------------------------------------------------------------------------------- /tests/test_plugins.py: -------------------------------------------------------------------------------- 1 | from pyLBL import continua, cross_sections, models, molecular_lines 2 | 3 | 4 | def test_plugins(): 5 | assert set(molecular_lines.keys()) == set(["arts", "pyLBL"]) 6 | assert set(continua.keys()) == set(["mt_ckd"]) 7 | assert set(cross_sections.keys()) == set(["arts_crossfit"]) 8 | assert set(models) == set(["arts", "arts_crossfit", "mt_ckd", "pyLBL"]) 9 | -------------------------------------------------------------------------------- /tests/test_spectroscopy.py: -------------------------------------------------------------------------------- 1 | from numpy import array_equal, log, max, sum 2 | from pyLBL import Database, Spectroscopy 3 | import pytest 4 | 5 | 6 | def test_spectroscopy(atmosphere_dataset, spectral_grid, downloaded_database): 7 | database = Database(downloaded_database) 8 | spec = Spectroscopy(atmosphere_dataset, spectral_grid, database) 9 | molecules = spec.list_molecules() 10 | assert molecules[0] == "H2O" 11 | assert molecules[-1] == "HFC236fa" 12 | assert len(molecules) == 88 13 | 14 | 15 | def test_absorption(single_layer_atmosphere, coarse_grid, downloaded_database): 16 | database = Database(downloaded_database) 17 | spec = Spectroscopy(single_layer_atmosphere, coarse_grid, database) 18 | beta = spec.compute_absorption(output_format="total") 19 | beta = beta.data_vars["absorption"] 20 | wavenumber = beta.coords["wavenumber"] 21 | assert max(beta.data) == pytest.approx(154.77712952851365) 22 | assert log(sum(beta.data)) == pytest.approx(7.212513759327571) 23 | assert beta.attrs["units"] == "m-1" 24 | assert array_equal(wavenumber.data, coarse_grid) 25 | assert wavenumber.attrs["units"] == "cm-1" 26 | 27 | 28 | def test_spectroscopy_bad_lines_model(atmosphere_dataset, spectral_grid, 29 | downloaded_database): 30 | database = Database(downloaded_database) 31 | with pytest.raises(KeyError): 32 | _ = Spectroscopy(atmosphere_dataset, spectral_grid, database, lines_backend="foo") 33 | 34 | 35 | def test_spectroscopy_bad_continua_model(atmosphere_dataset, spectral_grid, 36 | downloaded_database): 37 | database = Database(downloaded_database) 38 | with pytest.raises(KeyError): 39 | _ = Spectroscopy(atmosphere_dataset, spectral_grid, database, 40 | continua_backend="foo") 41 | 42 | 43 | def test_spectroscopy_bad_xsec_model(atmosphere_dataset, spectral_grid, 44 | downloaded_database): 45 | database = Database(downloaded_database) 46 | with pytest.raises(KeyError): 47 | _ = Spectroscopy(atmosphere_dataset, spectral_grid, database, 48 | cross_sections_backend="foo") 49 | -------------------------------------------------------------------------------- /tests/test_tips.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyLBL import TipsWebApi 4 | from pyLBL.tips import TotalPartitionFunction 5 | 6 | 7 | def test_tips(): 8 | t, data = TipsWebApi().download("H2O") 9 | tips = TotalPartitionFunction("H2O", t, data) 10 | value = tips.total_partition_function(279.54, 1) 11 | assert value == pytest.approx(160.2790023803711) 12 | -------------------------------------------------------------------------------- /tests/test_webapi.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import pytest 3 | from os import environ 4 | 5 | from pyLBL import HitranWebApi, TipsWebApi 6 | from pyLBL.webapi.hitran_api import NoIsotopologueError, NoTransitionsError 7 | from pyLBL.webapi.tips_api import NoMoleculeError 8 | 9 | 10 | Molecule = namedtuple("Molecule", ["id", "molecule_alias"]) 11 | 12 | 13 | def test_molecule_download(): 14 | webapi = HitranWebApi(api_key=environ["HITRAN_API_KEY"]) 15 | molecules = webapi.download_molecules() 16 | for i, id_, formula in zip([0, -1], [1, 1018], ["H2O", "Ar"]): 17 | assert molecules[i].id == int(id_) 18 | assert molecules[i].ordinary_formula == formula 19 | 20 | 21 | def test_isotopologue_download(): 22 | webapi = HitranWebApi(api_key=environ["HITRAN_API_KEY"]) 23 | iso = webapi.download_isotopologues([Molecule(1, None),]) 24 | for i, id_, molecule_id, name in zip([0, -1], [1, 129], [1, 1], ["H2", "D2"]): 25 | assert iso[i].id == id_ 26 | assert iso[i].molecule_id == molecule_id 27 | assert iso[i].iso_name == f"{name}(16O)" 28 | 29 | 30 | def test_transition_download(): 31 | webapi = HitranWebApi(api_key=environ["HITRAN_API_KEY"]) 32 | parameters = ["global_iso_id", "molec_id", "local_iso_id", "nu"] 33 | lines = webapi.download_transitions([Molecule(1, "H2O"),], 0, 3000, parameters) 34 | for i, id_, molecule_id, nu in zip([0, -1], [1, 1], [1, 1], [0.072049, 2999.90839]): 35 | assert lines[i].molec_id == molecule_id 36 | assert lines[i].local_iso_id == id_ 37 | assert lines[i].nu == nu 38 | 39 | 40 | def test_transition_download_no_iso(): 41 | webapi = HitranWebApi(api_key=environ["HITRAN_API_KEY"]) 42 | parameters = ["global_iso_id", "molec_id", "local_iso_id", "nu"] 43 | with pytest.raises(NoIsotopologueError): 44 | _ = webapi.download_transitions([], 0, 3000, parameters) 45 | 46 | 47 | def test_transition_download_no_lines(): 48 | webapi = HitranWebApi(api_key=environ["HITRAN_API_KEY"]) 49 | parameters = ["global_iso_id", "molec_id", "local_iso_id", "nu"] 50 | with pytest.raises(NoTransitionsError): 51 | _ = webapi.download_transitions([Molecule(1, "H2O"),], 0, 1.e-12, parameters) 52 | 53 | 54 | def test_tips_download(): 55 | t, data = TipsWebApi().download("H2O") 56 | assert t[0] == 1. 57 | assert t[-1] == 6000. 58 | assert data[0, 0] == 1. 59 | assert data[-1, -1] == 1.1946e+07 60 | 61 | 62 | def test_tips_download_no_molecule(): 63 | with pytest.raises(NoMoleculeError): 64 | _, _ = TipsWebApi().download("XX") 65 | --------------------------------------------------------------------------------