├── .github ├── CONTRIBUTING.md └── workflows │ └── publish-gh-pages.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .zenodo.json ├── CITATION.cff ├── LICENSE ├── README.md ├── ci └── environment.yml ├── climkern ├── __init__.py ├── __main__.py ├── download.py ├── frontend.py ├── tests │ ├── conftest.py │ └── test_frontend.py └── util.py ├── docs ├── Makefile ├── make.bat └── source │ ├── climkern.rst │ ├── conf.py │ └── index.rst ├── pyproject.toml └── setup.py /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ClimKern 2 | 3 | ## 1. Introduction 4 | The ClimKern Python package was designed to make calculating radiative feedbacks with kernels simple, intuitive, and reproducible. **We welcome virtually any type of contribution within the scope of ClimKern**: feature additions, bug fixes, enhancements, documentation improvements, etc. 5 | 6 | ## 2. How to Contribute: 7 | 1. [Open up a GitHub Issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-an-issue) to document your proposed change (if it's new). 8 | 2. [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). 9 | 3. Create a feature branch *stemming from* `dev`, which is the current development version of ClimKern. `main` is reserved for releases and is behind `dev`. 10 | 4. Make your changes. 11 | 5. Run tests locally ([`pytest`](https://docs.pytest.org/en/stable/how-to/usage.html)) to check for errors before pushing. 12 | 6. [Submit a pull request (PR) to the `dev` branch.](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) 13 | 14 | ## 3. Coding Guidelines 15 | ClimKern uses automated tooling to ensure a consistent style, including the [Black](https://github.com/psf/black) formatter, [Ruff](https://github.com/astral-sh/ruff) linter, and [MyPy](https://mypy.readthedocs.io/en/stable/) type checker. These are configured in `pyproject.toml` and can be run automatically with `pre-commit`. 16 | 17 | ```bash 18 | # Install pre-commit 19 | pip install pre-commit 20 | 21 | # Install hooks 22 | pre-commit install 23 | 24 | # Run on all files 25 | pre-commit run --all-files 26 | ``` 27 | 28 | Here are some other considerations when contributing: 29 | - Use meaningful names for functions and variables. 30 | - Include formal docstrings for functions in `frontend.py`. 31 | - Explain logic with inline comments. 32 | 33 | ## 4. Testing 34 | 35 | As of version 1.2.0, automated testing via GitHub Actions is not yet implemented. Please test your code using the built-in test suite and `pytest`. 36 | 37 | ## 5. Review Process 38 | 39 | All proposed changes should be submitted as PRs to the dev branch. A maintainer will review (and may edit) the submission. Only maintainers may approve and merge PRs, although reviews from all contributors are welcome. 40 | 41 | ## 6. Community 42 | 43 | GitHub Issues is the ideal place to discuss proposed changes. For additional assistance, please email Ty Janoski or Ivan Mitevski directly. -------------------------------------------------------------------------------- /.github/workflows/publish-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Sphinx documentation to Pages 2 | 3 | on: push 4 | 5 | jobs: 6 | pages: 7 | runs-on: ubuntu-latest 8 | environment: 9 | name: github-pages 10 | url: ${{ steps.deployment.outputs.page_url }} 11 | permissions: 12 | pages: write 13 | id-token: write 14 | defaults: 15 | run: 16 | shell: bash -l {0} 17 | steps: 18 | - name: Checkout source 19 | uses: actions/checkout@v4 20 | - name: Create conda environment 21 | uses: mamba-org/provision-with-micromamba@main 22 | with: 23 | cache-downloads: true 24 | micromamba-version: '1.4.1' 25 | environment-file: ci/environment.yml 26 | - name: Install editable climkern 27 | run: | 28 | python -m pip install --no-deps -e . 29 | - id: deployment 30 | uses: sphinx-notes/pages@v3 31 | with: 32 | documentation_path: ./docs/source 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints/ 2 | __pycache__/ 3 | build/ 4 | climkern.egg-info/ 5 | climkern/.ipynb_checkpoints/ 6 | climkern/__pycache__/ 7 | climkern/climkern.egg-info/ 8 | climkern/data/ 9 | dist/ 10 | *.nc 11 | .venv/ 12 | climkern/__init__.pyc 13 | climkern/util.pyc 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | - repo: https://github.com/psf/black 4 | rev: 23.9.1 5 | hooks: 6 | - id: black 7 | 8 | - repo: https://github.com/charliermarsh/ruff-pre-commit 9 | rev: v0.0.291 10 | hooks: 11 | - id: ruff 12 | args: ["--fix"] 13 | 14 | - repo: https://github.com/pre-commit/mirrors-mypy 15 | rev: v1.5.1 16 | hooks: 17 | - id: mypy -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "ClimKern", 3 | "version": "1.1.2", 4 | "doi": "10.5281/zenodo.10291284", 5 | "authors": [ 6 | { 7 | "name": "Janoski, Tyler P.", 8 | "affiliation": "Dept. of Earth & Environmental Sciences, Columbia University, New York, NY, USA; Lamont-Doherty Earth Observatory, Columbia University, Palisades, NY, USA; NOAA Center for Earth System Science and Remote Sensing Technologies (CESSRST-II), New York, NY, USA; City College of New York, New York, NY, USA; NOAA National Severe Storms Laboratory, Norman, OK, USA", 9 | "orcid": "0000-0003-4344-355X" 10 | }, 11 | { 12 | "name": "Mitevski, Ivan", 13 | "affiliation": "Dept. of Geosciences, Princeton University, Princeton, NJ, USA", 14 | "orcid": "0000-0001-9172-3236" 15 | }, 16 | { 17 | "name": "Wen, Kaitlyn", 18 | "affiliation": "Dept. of Computer Science, Princeton University, Princeton, NJ, USA" 19 | } 20 | ], 21 | "publication_date": "2024-08-08", 22 | "license": "MIT", 23 | "upload_type": "software" 24 | } 25 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use ClimKern, please cite the software and accompanying paper." 3 | title: "ClimKern" 4 | version: "1.2.0" 5 | doi: "10.5281/zenodo.14743210" 6 | repository-code: https://github.com/tyfolino/climkern 7 | license: MIT 8 | date-released: "2025-01-26" 9 | 10 | authors: 11 | - family-names: "Janoski" 12 | given-names: "Tyler P." 13 | affiliation: 14 | - "Dept. of Earth & Environmental Sciences, Columbia University, New York, NY, USA" 15 | - "Lamont-Doherty Earth Observatory, Columbia University, Palisades, NY, USA" 16 | - "NOAA Center for Earth System Science and Remote Sensing Technologies (CESSRST-II), New York, NY, USA" 17 | - "City College of New York, New York, NY, USA" 18 | - "NOAA National Severe Storms Laboratory, Norman, OK, USA" 19 | orcid: "0000-0003-4344-355X" 20 | email: "tyfolino@gmail.com" 21 | - family-names: "Mitevski" 22 | given-names: "Ivan" 23 | affiliation: "Dept. of Geosciences, Princeton University, Princeton, NJ, USA" 24 | orcid: "0000-0001-9172-3236" 25 | 26 | contributors: 27 | - family-names: "Wen" 28 | given-names: "Kaitlyn" 29 | affiliation: "Dept. of Computer Science, Princeton University, Princeton, NJ, USA" 30 | 31 | preferred-citation: 32 | type: article 33 | title: "ClimKern: A Python package for calculating radiative feedbacks using climate model kernels" 34 | authors: 35 | - family-names: "Janoski" 36 | given-names: "Tyler P." 37 | orcid: "0000-0003-4344-355X" 38 | - family-names: "Mitevski" 39 | given-names: "Ivan" 40 | orcid: "0000-0001-9172-3236" 41 | - family-names: "Kramer" 42 | given-names: "Ryan J." 43 | orcid: "0000-0002-9377-0674" 44 | - family-names: "Previdi" 45 | given-names: "Michael" 46 | orcid: "0000-0001-7701-1849" 47 | - family-names: "Polvani" 48 | given-names: "Lorenzo M." 49 | orcid: "0000-0003-4775-8110" 50 | journal: "Geoscientific Model Development" 51 | volume: "18" 52 | issue: "10" 53 | pages: "3065–3080" 54 | year: 2025 55 | doi: "10.5194/gmd-18-3065-2025" 56 | url: "https://gmd.copernicus.org/articles/18/3065/2025/" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ty Janoski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClimKern: a Python package for calculating radiative feedbacks 2 | 3 | [![DOI](https://zenodo.org/badge/588323813.svg)](https://doi.org/10.5281/zenodo.10291284) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | ## Overview 7 | 8 | The radiative kernel technique outlined in [Soden & Held (2006)](https://journals.ametsoc.org/view/journals/clim/19/14/jcli3799.1.xml) and [Soden et al. (2008)](https://journals.ametsoc.org/view/journals/clim/21/14/2007jcli2110.1.xml) is commonly used to calculate climate feedbacks. The "kernels" refer to datasets containing the radiative sensitivities of TOA (or surface) radiation to changes in fields such as temperature, specific humidity, and surface albedo; they are typically computed using offline radiative transfer calculations. 9 | 10 | ClimKern 11 | * standardizes the assumptions used in producing radiative feedbacks using kernels 12 | * simplifies the calculations by giving users access to functions tailored for climate model and reanalysis output 13 | * provides access to a repository of **11 different radiative kernels** to quantify interkernel spread 14 | 15 | The below information is meant to be a quickstart guide, but all functions and capabilities can be found at ClimKern's [documentation site](https://tyfolino.github.io/climkern/). 16 | 17 | ## Installation 18 | 19 | ClimKern is built on the Xarray architecture and requires several other packages for 20 | regridding and climate model output compatibility. The easiest method is to create a 21 | new conda environment using [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html) or [mamba](https://mamba-framework.readthedocs.io/en/latest/installation_guide.html): 22 | 23 | `conda create -n ck_env python=3.11 esmpy -c conda-forge` 24 | 25 | A conda environment is necessary because [ESMPy](https://earthsystemmodeling.org/esmpy/), which is required for regridding kernels, is unavailable via `pip`. 26 | 27 | Next, activate the new environment: 28 | 29 | `conda activate ck_env` 30 | 31 | Finally, install ClimKern with [pip](https://pip.pypa.io/en/stable/#): 32 | 33 | `pip install climkern` 34 | 35 | Once installed, ClimKern requires kernels found on [Zenodo](https://zenodo.org/doi/10.5281/zenodo.10223376). These kernels (and tutorial data) are stored separately because of PyPI size limitations. You can download the kernels easily using the download script included in the package: 36 | 37 | `python -m climkern download` 38 | 39 | Note: The kernels & tutorial data are approximately 5 GB. 40 | 41 | > **IMPORTANT:** SSL Certificate Errors 42 | > It is possible to get an SSL certificate error when trying to run the download script. You may try updating your certificate authorities with `pip install --upgrade certifi`. 43 | > 44 | > If that does not work, you can manually download the `data.zip` file from Zenodo and unzip it in your ClimKern package directory. 45 | 46 | 47 | Optional: 48 | 49 | You can test your installation via pytest. 50 | 51 | ``` 52 | pip install pytest 53 | pytest -v --pyargs climkern 54 | ``` 55 | 56 | All three tests should pass. 57 | 58 | ## Basic tutorial 59 | ### Temperature, water vapor, and surface albedo feedbacks 60 | 61 | This brief tutorial will cover the basics of using ClimKern. Please check the [documentation](https://tyfolino.github.io/climkern/) for a more complete list of available functions. We start by importing ClimKern and accessing our tutorial data: 62 | ```python 63 | import climkern as ck 64 | 65 | ctrl, pert = ck.tutorial_data("ctrl"), ck.tutorial_data("pert") 66 | ``` 67 | 68 | These datasets have all the necessary variables for computing feedbacks. Let's start with temperature feedbacks. 69 | ```python 70 | LR, Planck = ck.calc_T_feedbacks( 71 | ctrl.T, ctrl.TS, ctrl.PS, pert.T, pert.TS, pert.PS, pert.TROP_P, kern="GFDL" 72 | ) 73 | ``` 74 | To produce succinct output, let's use ClimKern's spatial average function. Additionally, we will normalize the feedbacks by global average surface temperature change to convert from Wm-2, the output of ClimKern functions, to the more commonly used units of Wm-2K-1. 75 | ```python 76 | # compute global average surface temperature change 77 | dTS_glob_avg = ck.spat_avg(pert.TS - ctrl.TS) 78 | 79 | # normalize temperature feedbacks by temperature change and take 80 | # the annual average 81 | print("The global average lapse rate feedback is {val:.2f} W/m^2/K.".format( 82 | val=(ck.spat_avg(LR)/dTS_glob_avg).mean())) 83 | print("The global average Planck feedback is {val:.2f} W/m^2/K.".format( 84 | val=(ck.spat_avg(Planck)/dTS_glob_avg).mean())) 85 | ``` 86 | Expected result with the GFDL kernel: 87 | > `The global average lapse rate feedback is -0.41 W/m^2/K.` 88 | > 89 | > `The global average Planck feedback is -3.12 W/m^2/K.` 90 | 91 | The water vapor and surface albedo feedbacks are calculated similarly: 92 | ```python 93 | q_lw,q_sw = ck.calc_q_feedbacks(ctrl.Q,ctrl.T,ctrl.PS, 94 | pert.Q,pert.PS,pert.TROP_P, 95 | kern="GFDL",method=1) 96 | alb = ck.calc_alb_feedback(ctrl.FSUS,ctrl.FSDS, 97 | pert.FSUS,pert.FSDS, 98 | kern="GFDL") 99 | 100 | print("The global average water vapor feedback is {val:.2f} W/m^2/K.".format( 101 | val=(ck.spat_avg(q_lw+q_sw)/dTS_glob_avg).mean())) 102 | print("The global average surface albedo feedback is {val:.2f} W/m^2/K." 103 | .format( 104 | val=(ck.spat_avg(alb)/dTS_glob_avg).mean())) 105 | ``` 106 | Expected result: 107 | >`The global average water vapor feedback is 1.44 W/m^2/K.` 108 | > 109 | >`The global average surface albedo feedback is 0.38 W/m^2/K.` 110 | 111 | ### Cloud feedbacks 112 | The cloud feedbacks, calculated using [Soden et al. (2008)](https://journals.ametsoc.org/view/journals/clim/21/14/2007jcli2110.1.xml) adjustment method, require all-sky and clear-sky versions of other feedbacks and the instantaneous radiative forcing. 113 | 114 | First, we need the longwave and shortwave cloud radiative effects, which ClimKern can calculate. 115 | ```python 116 | dCRE_LW = ck.calc_dCRE_LW(ctrl.FLNT,pert.FLNT,ctrl.FLNTC,pert.FLNTC) 117 | dCRE_SW = ck.calc_dCRE_SW(ctrl.FSNT,pert.FSNT,ctrl.FSNTC,pert.FSNTC) 118 | ``` 119 | Let's also read in the tutorial erf. 120 | ```python 121 | erf = ck.tutorial_data('ERF') 122 | ``` 123 | Next, we need the clear-sky versions of the temperature, water vapor, and surface albedo feedbacks. 124 | ```python 125 | #_cs means clear-sky 126 | LR_cs,Planck_cs = ck.calc_T_feedbacks(ctrl.T,ctrl.TS,ctrl.PS, 127 | pert.T,pert.TS,pert.PS,pert.TROP_P, 128 | kern="GFDL",sky="clear-sky") 129 | q_lw_cs,q_sw_cs = ck.calc_q_feedbacks(ctrl.Q,ctrl.T,ctrl.PS, 130 | pert.Q,pert.PS,pert.TROP_P, 131 | kern="GFDL",method=1,sky="clear-sky") 132 | alb_cs = ck.calc_alb_feedback(ctrl.FSUS,ctrl.FSDS, 133 | pert.FSUS,pert.FSDS, 134 | kern="GFDL",sky="clear-sky") 135 | ``` 136 | At last, we can calculate the longwave and shortwave cloud feedbacks. 137 | ```python 138 | cld_lw = ck.calc_cloud_LW(LR + Planck,LR_cs+Planck_cs,q_lw,q_lw_cs,dCRE_LW, 139 | erf.erf_lwas,erf.erf_lwcs) 140 | cld_sw = ck.calc_cloud_SW(alb,alb_cs,q_sw,q_sw_cs,dCRE_SW,erf.erf_swas, 141 | erf.erf_swcs) 142 | 143 | print("The global average SW cloud feedback is {val:.2f} W/m^2/K.".format( 144 | val=(ck.spat_avg(cld_sw)/dTS_glob_avg).mean())) 145 | print("The global average LW cloud feedback is {val:.2f} W/m^2/K.".format( 146 | val=(ck.spat_avg(cld_lw)/dTS_glob_avg).mean())) 147 | ``` 148 | Expected result: 149 | >`The global average SW cloud feedback is 0.38 W/m^2/K.` 150 | > 151 | >`The global average LW cloud feedback is 0.03 W/m^2/K.` 152 | 153 | ## Troubleshooting 154 | 155 | If you are having issues downloading dependencies with `pip`, you can also try adding them to your conda environment with `conda`, i.e.: 156 | 157 | `conda install xesmf -c conda-forge` 158 | 159 | If you are having trouble downloading the kernels and tutorial data using the package's download function, you can also download the data directly from the [Zenodo repository](https://zenodo.org/doi/10.5281/zenodo.10223376) and put it in the climkern/data directory located wherever your conda/mamba environments are stored. 160 | 161 | ## Other features & coming soon 162 | ClimKern has several other useful features: 163 | - Four different methods for calculating water vapor feedbacks. 164 | - The ability to calculate the "relative humidity" version of all feedbacks following [Held & Shell (2012)](https://journals.ametsoc.org/view/journals/clim/25/8/jcli-d-11-00721.1.xml) and [Zelinka et al. (2020)](https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2019GL085782). 165 | - Functions to calculate stratospheric temperature and water vapor feedbacks. 166 | 167 | We are continuously updating the package. Please check out the [GitHub issues page](https://github.com/tyfolino/climkern/issues) for this repository for plans for new features. 168 | 169 | ## Want to help? Get involved! 170 | 171 | We deeply appreciate contributions from other scientists and programmers and are happy to attribute credit accordingly. If you wish to contribute, please read our [CONTRIBUTING.md](.github/CONTRIBUTING.md) for guidelines on how to get started. 172 | 173 | **tl;dr**: 174 | - Work from the `dev` branch, **not** `main` 175 | - Submit a pull request when ready. 176 | 177 | ## 📖 How to Cite ClimKern 178 | If you use ClimKern in your work, please cite our paper: 179 | 180 | Janoski, T. P., Mitevski, I., Kramer, R. J., Previdi, M., & Polvani, L. M. (2025). ClimKern v1.2: a new Python package and kernel repository for calculating radiative feedbacks. *Geoscientific Model Development*, *18*(10), 3065–3079. https://doi.org/10.5194/gmd-18-3065-2025 181 | 182 |
183 | BibTeX 184 | 185 | ```bibtex 186 | @article{janoski2025climkern, 187 | AUTHOR = {Janoski, T. P. and Mitevski, I. and Kramer, R. J. and Previdi, M. and Polvani, L. M.}, 188 | TITLE = {ClimKern v1.2: a new Python package and kernel repository for calculating radiative feedbacks}, 189 | JOURNAL = {Geoscientific Model Development}, 190 | VOLUME = {18}, 191 | YEAR = {2025}, 192 | NUMBER = {10}, 193 | PAGES = {3065--3079}, 194 | URL = {https://gmd.copernicus.org/articles/18/3065/2025/}, 195 | DOI = {10.5194/gmd-18-3065-2025} 196 | } 197 | ``` 198 | 199 |
200 | 201 | If you are citing the software itself (e.g., for reproducibility), use the citation metadata included in our [`CITATION.cff`](CITATION.cff) file. GitHub also provides downloadable citation formats via the "Cite this repository" button on the right-hand sidebar. 202 | 203 | -------------------------------------------------------------------------------- /ci/environment.yml: -------------------------------------------------------------------------------- 1 | name: climkern 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - esmpy 6 | - xarray 7 | - xesmf 8 | - cftime 9 | - pooch 10 | - tqdm 11 | - importlib-resources 12 | - plac 13 | - netcdf4 -------------------------------------------------------------------------------- /climkern/__init__.py: -------------------------------------------------------------------------------- 1 | from . import util 2 | from .frontend import ( 3 | calc_alb_feedback, 4 | calc_cloud_LW, 5 | calc_cloud_LW_res, 6 | calc_cloud_SW, 7 | calc_cloud_SW_res, 8 | calc_dCRE_LW, 9 | calc_dCRE_SW, 10 | calc_q_feedbacks, 11 | calc_RH_feedback, 12 | calc_strato_q, 13 | calc_strato_T, 14 | calc_T_feedbacks, 15 | spat_avg, 16 | tutorial_data, 17 | ) 18 | 19 | __all__ = [ 20 | "calc_alb_feedback", 21 | "calc_T_feedbacks", 22 | "calc_q_feedbacks", 23 | "calc_dCRE_SW", 24 | "calc_dCRE_LW", 25 | "calc_cloud_LW", 26 | "calc_cloud_SW", 27 | "calc_cloud_LW_res", 28 | "calc_cloud_SW_res", 29 | "calc_strato_T", 30 | "calc_strato_q", 31 | "calc_RH_feedback", 32 | "tutorial_data", 33 | "spat_avg", 34 | "util", 35 | ] 36 | -------------------------------------------------------------------------------- /climkern/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import sys 3 | 4 | import plac 5 | 6 | from .download import download 7 | 8 | commands = { 9 | "download": download, 10 | } 11 | 12 | if len(sys.argv) == 1: 13 | print("Available commands:", ", ".join(commands)) 14 | sys.exit(1) 15 | 16 | command = sys.argv.pop(1) 17 | 18 | if command in commands: 19 | plac.call(commands[command], sys.argv[1:]) 20 | else: 21 | print("Unknown command:", command) 22 | print("Available commands:", ", ".join(commands)) 23 | sys.exit(1) 24 | -------------------------------------------------------------------------------- /climkern/download.py: -------------------------------------------------------------------------------- 1 | # import statements 2 | 3 | import os 4 | 5 | from pooch import Unzip, retrieve 6 | 7 | import climkern as ck # if this doesn't work, it's probably the wrong env 8 | 9 | 10 | def download(): 11 | # get path of climkern package 12 | path = ck.__file__.replace("/__init__.py", "") 13 | fname = "data.zip" # name of file to save before unzipping 14 | 15 | retrieve( 16 | url="doi:10.5281/zenodo.10223376/data.zip", 17 | known_hash=None, 18 | fname=fname, 19 | path=path, 20 | processor=Unzip(extract_dir="data"), 21 | progressbar=True, 22 | ) 23 | 24 | # delete the zip file after unzipping 25 | os.remove(path + "/data.zip") 26 | -------------------------------------------------------------------------------- /climkern/frontend.py: -------------------------------------------------------------------------------- 1 | # import required packages 2 | import warnings 3 | 4 | import numpy as np 5 | import xarray as xr 6 | import xesmf as xe 7 | from importlib_resources import files 8 | from xarray import DataArray 9 | 10 | from .util import * 11 | 12 | # change warning format 13 | warnings.formatwarning = custom_formatwarning 14 | 15 | # Apply warning filter to prevent xarray renaming warming 16 | # Ideally, this will be removed in the future 17 | warnings.filterwarnings("ignore", ".*does not create an index anymore.*") 18 | 19 | 20 | # def calc_alb_feedback(ctrl_rsus,ctrl_rsds,pert_rsus,pert_rsds,kern='GFDL', 21 | # sky="all-sky"): 22 | def calc_alb_feedback( 23 | ctrl_rsus: DataArray, 24 | ctrl_rsds: DataArray, 25 | pert_rsus: DataArray, 26 | pert_rsds: DataArray, 27 | kern: str = "GFDL", 28 | sky: str = "all-sky", 29 | ) -> DataArray: 30 | """ 31 | Calculate the radiative perturbation (W/m^2) from changes in surface 32 | albedo using user-specified radiative kernel. Horizontal resolution 33 | is kept at input data's resolution. 34 | 35 | Parameters 36 | ---------- 37 | ctrl_rsus : xarray DataArray 38 | DataArray containing the upwelling SW radiation at the surface in the 39 | control simulation. Must be 3D with coordinates of time, latitude, and 40 | longitude with units of W/m^2. 41 | 42 | ctrl_rsds : xarray DataArray 43 | DataArray containing the downwelling SW radiation at the surface in 44 | the control simulation. Must be 3D with coordinates of time, latitude, 45 | and longitude with units of W/m^2. 46 | 47 | pert_rsus : xarray DataArray 48 | DataArray containing the upwelling SW radiation at the surface in the 49 | perturbed simulation. Must be 3D with coordinates of time, latitude, 50 | and longitude with units of W/m^2. 51 | 52 | pert_rsds : xarray DataArray 53 | DataArray containing the downwelling SW radiation at the surface in 54 | the perturbed simulation. Must be 3D with coordinates of time, 55 | latitude, and longitude with units of W/m^2. 56 | 57 | kern : string, optional 58 | String specifying the name of the desired kernel. Defaults to "GFDL". 59 | 60 | sky : string, optional 61 | String, either "all-sky" or "clear-sky", specifying whether to 62 | calculate the all-sky or clear-sky feedbacks. Defaults to "all-sky". 63 | 64 | Returns 65 | ------- 66 | alb_feedback : xarray DataArray 67 | 3D DataArray containing radiative perturbations at TOA caused by 68 | changes in surface albedo with coordinates of time, latitude, and 69 | longitude. 70 | """ 71 | # get correct key for all-sky or clear-sky 72 | alb_key = "sw_a" if check_sky(sky) == "all-sky" else "swclr_a" 73 | 74 | # check input coordinates 75 | ctrl_rsus = check_coords(ctrl_rsus) 76 | ctrl_rsds = check_coords(ctrl_rsds) 77 | pert_rsus = check_coords(pert_rsus) 78 | pert_rsds = check_coords(pert_rsds) 79 | 80 | # calculate albedo and create control climatology 81 | ctrl_alb_clim = make_clim(get_albedo(ctrl_rsus, ctrl_rsds)) 82 | pert_alb = get_albedo(pert_rsus, pert_rsds) 83 | 84 | # tile climatology and take difference 85 | ctrl_alb_clim_tiled = tile_data(ctrl_alb_clim, pert_alb) 86 | diff_alb = pert_alb - ctrl_alb_clim_tiled 87 | 88 | # read in and regrid surface albedo kernel 89 | kernel = get_kern(kern) 90 | regridder = xe.Regridder( 91 | kernel[alb_key], 92 | diff_alb, 93 | method="bilinear", 94 | reuse_weights=False, 95 | periodic=True, 96 | extrap_method="nearest_s2d", 97 | ) 98 | kernel = tile_data(regridder(kernel[alb_key]), diff_alb) 99 | 100 | # calculate feedbacks 101 | # the 100 is to convert albedo to percent 102 | alb_feedback = diff_alb * kernel * 100 103 | return alb_feedback 104 | 105 | 106 | def calc_T_feedbacks( 107 | ctrl_ta, 108 | ctrl_ts, 109 | ctrl_ps, 110 | pert_ta, 111 | pert_ts, 112 | pert_ps, 113 | pert_trop=None, 114 | kern="GFDL", 115 | sky="all-sky", 116 | fixRH=False, 117 | ): 118 | """ 119 | Calculate the raditive pertubations (W/m^2) at the TOA from changes in 120 | surface skin and air temperature using user-specified kernel. Horizontal 121 | resolution is kept at input data's resolution. 122 | 123 | Parameters 124 | ---------- 125 | ctrl_ta : xarray DataArray 126 | DataArray containing air temperature on pressure levels in the 127 | control simulation. 4D with coordinates of time, latitude, longitude, 128 | and pressure level and units of K. 129 | 130 | ctrl_ts : xarray DataArray 131 | DataArray containing surface skin temperature in the control 132 | simulation. 3D with coordinates of time, latitude, and longitude 133 | and units of K. 134 | 135 | ctrl_ps : xarray DataArray 136 | DataArray containing surface pressure in the control simulation. 3D 137 | with coordinates of time, latitude, and longitude and units of Pa or 138 | hPa. 139 | 140 | pert_ta : xarray DataArray 141 | DataArray containing air temperature on pressure levels in the 142 | perturbed simulation. 4D with coordinates of time, latitude, 143 | longitude, and pressure level and units of K. 144 | 145 | pert_ts : xarray DataArray 146 | DataArray containing surface skin temperature in the perturbed 147 | simulation. 3D with coordinates of time, latitude, and longitude 148 | and units of K. 149 | 150 | pert_ps : xarray DataArray 151 | DataArray containing surface pressure in the perturbed simulation. 3D 152 | with coordinates of time, latitude, and longitude and units of Pa or 153 | hPa. 154 | 155 | pert_trop : xarray DataArray, optional 156 | DataArray containing tropopause pressure in the perturbed simulation. 157 | 3D with coordinates of time, latitude, and longitude and units of Pa 158 | or hPa. If not provided, ClimKern will assume a tropopause height of 159 | 100 hPa at the equator, linearly descending with the cosine of 160 | latitude to 300 hPa at the poles. 161 | 162 | kern : string, optional 163 | String specifying the name of the desired kernel. Defaults to "GFDL". 164 | 165 | sky : string, optional 166 | String, either "all-sky" or "clear-sky", specifying whether to 167 | calculate the all-sky or clear-sky feedbacks. Defaults to "all-sky". 168 | 169 | fixRH : boolean, optional 170 | Specifies whether to calculate alternative Planck and lapse rate 171 | feedbacks using relative humidity as a state variable, as outlined 172 | in Held & Shell (2012). 173 | 174 | Returns 175 | ------- 176 | lr_feedback : xarray DataArray 177 | 3D DataArray containing radiative perturbations at TOA caused by 178 | changes in tropospheric lapse rate with coordinates of time, latitude, 179 | and longitude. 180 | 181 | planck_feedback : xarray DataArray 182 | 3D DataArray containing radiative perturbations at TOA caused by 183 | vertically-uniform temperature change with coordinates of time, 184 | latitude, and longitude. 185 | """ 186 | # get correct keys for all-sky or clear-sky 187 | t_key = "lw_t" if check_sky(sky) == "all-sky" else "lwclr_t" 188 | ts_key = "lw_ts" if sky == "all-sky" else "lwclr_ts" 189 | 190 | # if using RH as a state variable, read in water vapor kernels too 191 | if fixRH is True: 192 | qlw_key = "lw_q" if sky == "all-sky" else "lwclr_q" 193 | qsw_key = "sw_q" if sky == "all-sky" else "swclr_q" 194 | 195 | # check input coordinates 196 | ctrl_ta = check_coords(ctrl_ta, ndim=4) 197 | pert_ta = check_coords(pert_ta, ndim=4) 198 | ctrl_ts = check_coords(ctrl_ts) 199 | ctrl_ps = check_coords(ctrl_ps) 200 | pert_ts = check_coords(pert_ts) 201 | pert_ps = check_coords(pert_ps) 202 | 203 | # check input units 204 | ctrl_ta = check_var_units(check_plev_units(ctrl_ta), "T") 205 | pert_ta = check_var_units(check_plev_units(pert_ta), "T") 206 | ctrl_ts = check_var_units(ctrl_ts, "T") 207 | pert_ts = check_var_units(pert_ts, "T") 208 | ctrl_ps = check_pres_units(ctrl_ps, "ctrl PS") 209 | pert_ps = check_pres_units(pert_ps, "pert PS") 210 | 211 | # check tropopause units if provided by user, else create dummy tropopause 212 | if type(pert_trop) == type(None): 213 | pert_trop = make_tropo(pert_ps) 214 | else: 215 | pert_trop = check_coords(pert_trop) 216 | pert_trop = check_pres_units(pert_trop, "pert tropopause") 217 | 218 | # mask values below the surface & make climatology 219 | ctrl_ps_clim = make_clim(ctrl_ps) 220 | ctrl_ta_clim = make_clim(ctrl_ta) 221 | ctrl_ta_clim = ctrl_ta_clim.where(ctrl_ta_clim.plev < ctrl_ps_clim) 222 | ctrl_ts_clim = make_clim(ctrl_ts) 223 | 224 | # calculate change in air and surface skin temperature 225 | diff_ta = pert_ta - tile_data(ctrl_ta_clim, pert_ta) 226 | diff_ts = pert_ts - tile_data(ctrl_ts_clim, pert_ts) 227 | 228 | # read in and regrid temperature kernel 229 | kernel = check_plev(get_kern(kern)) 230 | regridder = xe.Regridder( 231 | kernel[t_key], 232 | diff_ts, 233 | method="bilinear", 234 | reuse_weights=False, 235 | periodic=True, 236 | extrap_method="nearest_s2d", 237 | ) 238 | ta_kernel = tile_data(regridder(kernel[t_key]), diff_ta) 239 | ts_kernel = tile_data(regridder(kernel[ts_key], skipna=True), diff_ta) 240 | 241 | # repeat process for water vapor kernels if using RH 242 | if fixRH is True: 243 | qlw_kernel = tile_data(regridder(kernel[qlw_key]), diff_ta) 244 | qsw_kernel = tile_data(regridder(kernel[qsw_key]), diff_ta) 245 | # overwrite ta_kernel to include q kernel 246 | ta_kernel = ta_kernel + qlw_kernel + qsw_kernel 247 | 248 | # regrid temperature differences to kernel pressure levels 249 | # we have to extrapolate in case the lowest model plev is above the 250 | # kernel's. 251 | 252 | diff_ta = diff_ta.interp_like(ta_kernel, kwargs={"fill_value": "extrapolate"}) 253 | 254 | # use get_dp in climkern.util to calculate layer thickness 255 | dp = get_dp(diff_ta, pert_ps, pert_trop, layer="troposphere") 256 | 257 | # calculate feedbacks 258 | # for lapse rate, use the deviation of the air temperature response 259 | # from vertically uniform warming 260 | lr_feedback = ((ta_kernel * (diff_ta - diff_ts).fillna(0)) * dp / 10000).sum( 261 | dim="plev", min_count=1 262 | ) 263 | 264 | # for planck, assume vertically uniform warming and 265 | # account for surface temperature change 266 | planck_feedback = (ts_kernel * diff_ts) + ( 267 | ta_kernel * xr.broadcast(diff_ts, diff_ta)[0].fillna(0) * dp / 10000 268 | ).sum(dim="plev", min_count=1) 269 | 270 | return (lr_feedback, planck_feedback) 271 | 272 | 273 | def calc_q_feedbacks( 274 | ctrl_q, 275 | ctrl_ta, 276 | ctrl_ps, 277 | pert_q, 278 | pert_ps, 279 | pert_trop=None, 280 | kern="GFDL", 281 | sky="all-sky", 282 | method=1, 283 | ): 284 | """ 285 | Calculate the raditive pertubations (W/m^2), LW & SW, at the TOA from 286 | changes in specific humidity using user-specified kernel. Horizontal 287 | resolution is kept at input data's resolution. 288 | 289 | Parameters 290 | ---------- 291 | ctrl_q : xarray DataArray 292 | DataArray containing specific humidity on pressure levels in the 293 | control simulation. 4D with coordinates of time, latitude, longitude, 294 | and pressure level and units of "1", "kg/kg", or "g/kg". 295 | 296 | ctrl_ta : xarray DataArray 297 | DataArray containing air temperature on pressure levels in the 298 | control simulation. 4D with coordinates of time, latitude, longitude, 299 | and pressure level and units of K. 300 | 301 | ctrl_ps : xarray DataArray 302 | DataArray containing surface pressure in the control simulation. 3D 303 | with coordinates of time, latitude, and longitude and units of Pa or 304 | hPa. 305 | 306 | pert_q : xarray DataArray 307 | DataArray containing specific humidity on pressure levels in the 308 | perturbed simulation. 4D with coordinates of time, latitude, 309 | longitude, and pressure level and units of "1", "kg/kg", or "g/kg". 310 | 311 | pert_ps : xarray DataArray 312 | DataArray containing surface pressure in the perturbed simulation. 3D 313 | with coordinates of time, latitude, and longitude and units of Pa or 314 | hPa. 315 | 316 | pert_trop : xarray DataArray, optional 317 | DataArray containing tropopause pressure in the perturbed simulation. 318 | 3D with coordinates of time, latitude, and longitude and units of Pa 319 | or hPa. If not provided, ClimKern will assume a tropopause height of 320 | 100 hPa at the equator, linearly descending with the cosine of 321 | latitude to 300 hPa at the poles. 322 | 323 | kern : string, optional 324 | String specifying the name of the desired kernel. Defaults to "GFDL". 325 | 326 | sky : string, optional 327 | String, either "all-sky" or "clear-sky", specifying whether to 328 | calculate the all-sky or clear-sky feedbacks. Defaults to "all-sky". 329 | 330 | method : int, optional 331 | Specifies the method to use to calculate the specific humidity 332 | feedback. Options 1, 2, and 3 use the change in the natural logarithm of 333 | specific humidity, while 4 uses the linear change. The options are: 334 | 1 -- Uses the actual logarithm for both the specific humidity response 335 | and the normalization factor. 336 | 2 -- Uses the fractional change approximation of logarithms only in the 337 | normalization factor, with the actual logarithm used in the specific humidity 338 | response. 339 | 3 -- Uses the fractional change approximation of logarithms in the specific 340 | humidity response & normalization factor. 341 | 4 -- Uses the linear change in specific humidity for both. 342 | 343 | Returns 344 | ------- 345 | lw_q_feedback : xarray DataArray 346 | 3D DataArray containing LW radiative perturbations at TOA caused by 347 | changes in specific humidity with coordinates of time, latitude, 348 | and longitude. 349 | 350 | sw_q_feedback : xarray DataArray 351 | 3D DataArray containing SW radiative perturbations at TOA caused by 352 | changes in specific humidity with coordinates of time, latitude, 353 | and longitude. 354 | """ 355 | # Issue warning if user provides old keywords for the method argument 356 | if method in ["pendergrass", "kramer", "zelinka", "linear"]: 357 | warnings.warn( 358 | "Name keywords are deprecated and will be removed" 359 | + ' from future versions of ClimKern. Please use "1", ' 360 | + '"2", "3", or "4" instead.', 361 | FutureWarning, 362 | ) 363 | mapping = {"pendergrass": 3, "kramer": 2, "zelinka": 1, "linear": 4} 364 | method = mapping[method] 365 | # get correct keys for all-sky or clear-sky 366 | qlw_key = "lw_q" if check_sky(sky) == "all-sky" else "lwclr_q" 367 | qsw_key = "sw_q" if sky == "all-sky" else "swclr_q" 368 | 369 | # check input coordinates 370 | ctrl_q = check_coords(ctrl_q, ndim=4) 371 | ctrl_ta = check_coords(ctrl_ta, ndim=4) 372 | pert_q = check_coords(pert_q, ndim=4) 373 | ctrl_ps = check_coords(ctrl_ps) 374 | pert_ps = check_coords(pert_ps) 375 | 376 | # check input units 377 | ctrl_ta = check_var_units(check_plev_units(ctrl_ta), "T") 378 | ctrl_q = check_var_units(check_plev_units(ctrl_q), "q") 379 | pert_q = check_var_units(check_plev_units(pert_q), "q") 380 | ctrl_ps = check_pres_units(ctrl_ps, "ctrl PS") 381 | pert_ps = check_pres_units(pert_ps, "pert PS") 382 | 383 | # check tropopause units if provided by user, else create dummy tropopause 384 | if type(pert_trop) == type(None): 385 | pert_trop = make_tropo(pert_ps) 386 | else: 387 | pert_trop = check_coords(pert_trop) 388 | pert_trop = check_pres_units(pert_trop, "pert tropopause") 389 | 390 | # mask values below the surface & make climatology 391 | ctrl_ps_clim = make_clim(ctrl_ps) 392 | ctrl_q_clim = make_clim(ctrl_q) 393 | ctrl_q_clim = ctrl_q_clim.where(ctrl_q_clim.plev < ctrl_ps_clim) 394 | ctrl_ta_clim = make_clim(ctrl_ta) 395 | ctrl_ta_clim = ctrl_ta_clim.where(ctrl_ta_clim.plev < ctrl_ps_clim) 396 | 397 | # if q has units of unity or kg/kg, we will have to 398 | # multiply by 1000 later on to make it g/kg 399 | if ctrl_q.units in ["1", "kg/kg"]: 400 | conv_factor = 1000 401 | elif ctrl_q.units in ["g/kg"]: 402 | conv_factor = 1 403 | else: 404 | warnings.warn("Cannot determine units of q. Assuming kg/kg.") 405 | conv_factor = 1000 406 | 407 | # tile control climatology to match length of pert simulation time dim 408 | ctrl_q_clim_tiled = tile_data(ctrl_q_clim, pert_q) 409 | 410 | # calculate specific humidity response using user-specified method 411 | if method == 3: 412 | diff_q = (pert_q - ctrl_q_clim_tiled) / ctrl_q_clim_tiled 413 | elif method == 4: 414 | diff_q = pert_q - ctrl_q_clim_tiled 415 | elif method in [2, 1]: 416 | diff_q = np.log(pert_q.where(pert_q > 0)) - np.log( 417 | ctrl_q_clim_tiled.where(ctrl_q_clim_tiled > 0) 418 | ) 419 | else: 420 | raise ValueError("Please select a valid choice for the method argument.") 421 | 422 | # read in and regrid water vapor kernel 423 | kernel = check_plev(get_kern(kern)) 424 | regridder = xe.Regridder( 425 | kernel[qlw_key], 426 | diff_q, 427 | method="bilinear", 428 | extrap_method="nearest_s2d", 429 | reuse_weights=False, 430 | periodic=True, 431 | ) 432 | 433 | qlw_kernel = tile_data(regridder(kernel[qlw_key], skipna=True), diff_q) 434 | qsw_kernel = tile_data(regridder(kernel[qsw_key], skipna=True), diff_q) 435 | 436 | # regrid T/q to kernel pressure levels 437 | # we have to extrapolate in case the lowest model plev is above the 438 | # kernel's. 439 | kwargs = {"fill_value": "extrapolate"} 440 | diff_q = diff_q.interp_like(qlw_kernel, kwargs=kwargs) 441 | ctrl_q_clim = ctrl_q_clim.interp(plev=qlw_kernel.plev, kwargs=kwargs) 442 | ctrl_q_clim.plev.attrs["units"] = ctrl_q.plev.units 443 | ctrl_ta_clim = ctrl_ta_clim.interp(plev=qlw_kernel.plev, kwargs=kwargs) 444 | ctrl_ta_clim.plev.attrs["units"] = ctrl_ta.plev.units 445 | 446 | # create the normalization factor, which corresponds to the increase 447 | # in specific humidity for a 1K increate in temperature 448 | norm = tile_data(calc_q_norm(ctrl_ta_clim, ctrl_q_clim, method=method), diff_q) 449 | 450 | # use get_dp in climkern.util to calculate layer thickness 451 | dp = get_dp(diff_q, pert_ps, pert_trop, layer="troposphere") 452 | 453 | # calculate feedbacks 454 | qlw_feedback = ( 455 | (qlw_kernel / norm * diff_q * conv_factor * dp / 10000) 456 | .sum(dim="plev", min_count=1) 457 | .drop_vars("units") 458 | ) 459 | qsw_feedback = ( 460 | (qsw_kernel / norm * diff_q * conv_factor * dp / 10000) 461 | .sum(dim="plev", min_count=1) 462 | .fillna(0) 463 | ).drop_vars("units") 464 | 465 | # one complication: CloudSat needs to be masked so we don't fill the NaNs 466 | # with zeros 467 | if kern == "CloudSat": 468 | qsw_feedback = qsw_feedback.where( 469 | qsw_kernel.sum(dim="plev", min_count=1).notnull() 470 | ) 471 | 472 | return (qlw_feedback, qsw_feedback) 473 | 474 | 475 | def calc_dCRE_SW(ctrl_FSNT, pert_FSNT, ctrl_FSNTC, pert_FSNTC): 476 | """ 477 | Calculate the change in the SW cloud radiative effect at the TOA. 478 | 479 | Parameters 480 | ---------- 481 | ctrl_FSNT : xarray DataArray 482 | Three-dimensional DataArray containing the all-sky net shortwave flux 483 | at the top-of-atmosphere in the control simulation 484 | with coords of time, lat, and lon and units of Wm^-2. It should be 485 | oriented such that positive = downwards/incoming. 486 | 487 | pert_FSNT : xarray DataArray 488 | Three-dimensional DataArray containing the all-sky net shortwave flux 489 | at the top-of-atmosphere in the perturbed simulation 490 | with coords of time, lat, and lon and units of Wm^-2. It should be 491 | oriented such that positive = downwards/incoming. 492 | 493 | ctrl_FSNTC : xarray DataArray 494 | Three-dimensional DataArray containing the clear-sky net shortwave 495 | flux at the top-of-atmosphere in the control simulation 496 | with coords of time, lat, and lon and units of Wm^-2. It should be 497 | oriented such that positive = downwards/incoming. 498 | 499 | pert_FSNTC : xarray DataArray 500 | Three-dimensional DataArray containing the clear-sky net shortwave 501 | flux at the top-of-atmosphere in the perturbed simulation 502 | with coords of time, lat, and lon and units of Wm^-2. It should be 503 | oriented such that positive = downwards/incoming. 504 | 505 | 506 | Returns 507 | ------- 508 | dCRE_SW : xarray DataArray 509 | Three-dimensional DataArray containing the change in shortwave cloud 510 | radiative effect at the top-of-atmosphere with coords of time, lat, 511 | and lon and units of Wm^-2. positive = downwards. 512 | """ 513 | # double check the signs of SW fluxes 514 | sw_coeff = -1 if ctrl_FSNT.mean() < 0 else 1 515 | 516 | ctrl_CRE_SW = sw_coeff * (ctrl_FSNT - ctrl_FSNTC) 517 | pert_CRE_SW = sw_coeff * (pert_FSNT - pert_FSNTC) 518 | 519 | return pert_CRE_SW - ctrl_CRE_SW 520 | 521 | 522 | def calc_dCRE_LW(ctrl_FLNT, pert_FLNT, ctrl_FLNTC, pert_FLNTC): 523 | """ 524 | Calculate the change in the LW cloud radiative effect at the TOA. 525 | 526 | Parameters 527 | ---------- 528 | ctrl_FLNT : xarray DataArray 529 | Three-dimensional DataArray containing the all-sky net longwave flux 530 | at the top-of-atmosphere in the control simulation 531 | with coords of time, lat, and lon and units of Wm^-2. It should be 532 | oriented such that positive = downwards. 533 | 534 | pert_FLNT : xarray DataArray 535 | Three-dimensional DataArray containing the all-sky net longwave flux 536 | at the top-of-atmosphere in the perturbed simulation 537 | with coords of time, lat, and lon and units of Wm^-2. It should be 538 | oriented such that positive = downwards. 539 | 540 | ctrl_FLNTC : xarray DataArray 541 | Three-dimensional DataArray containing the clear-sky net longwave flux 542 | at the top-of-atmosphere in the control simulation 543 | with coords of time, lat, and lon and units of Wm^-2. It should be 544 | oriented such that positive = downwards. 545 | 546 | pert_FLNTC : xarray DataArray 547 | Three-dimensional DataArray containing the clear-sky net longwave flux 548 | at the top-of-atmosphere in the perturbed simulation 549 | with coords of time, lat, and lon and units of Wm^-2. It should be 550 | oriented such that positive = downwards. 551 | 552 | 553 | Returns 554 | ------- 555 | dCRE_LW : xarray DataArray 556 | Three-dimensional DataArray containing the change in longwave cloud 557 | radiative effect at the top-of-atmosphere with coords of time, lat, 558 | and lon and units of Wm^-2. positive = downwards. 559 | """ 560 | # double check the signs of LW/SW fluxes 561 | lw_coeff = -1 if ctrl_FLNT.mean() > 0 else 1 562 | 563 | ctrl_CRE_LW = lw_coeff * (ctrl_FLNT - ctrl_FLNTC) 564 | pert_CRE_LW = lw_coeff * (pert_FLNT - pert_FLNTC) 565 | 566 | return pert_CRE_LW - ctrl_CRE_LW 567 | 568 | 569 | def calc_cloud_LW(t_as, t_cs, q_lwas, q_lwcs, dCRE_lw, rf_lwas=None, rf_lwcs=None): 570 | """ 571 | Calculate the radiative perturbation from the longwave cloud feedback 572 | using the adjustment method outlined in Soden et al. (2008). 573 | 574 | Parameters 575 | ---------- 576 | t_as : xarray DataArray 577 | DataArray containing the vertically integrated all-sky radiative 578 | perturbation at the TOA from the total temperature feedback. The 579 | total temperature feedback is the sum of the Planck and lapse rate 580 | feedbacks. Should have dims of lat, lon, and time. 581 | 582 | t_cs : xarray DataArray 583 | DataArray containing the vertically integrated clear-sky radiative 584 | perturbation at the TOA from the total temperature feedback. The 585 | total temperature feedback is the sum of the Planck and lapse rate 586 | feedbacks. Should have dims of lat, lon, and time. 587 | 588 | q_lwas : xarray DataArray 589 | DataArray containing the vertically integrated LW all-sky radiative 590 | perturbation at the TOA from the water vapor feedback. Should have 591 | coords of lat, lon, and time. 592 | 593 | q_lwcs : xarray DataArray 594 | DataArray containing the vertically integrated LW clear-sky radiative 595 | perturbation at the TOA from the water vapor feedback. Should have 596 | coords of lat, lon, and time. 597 | 598 | dCRE_lw : xarray DataArray 599 | DataArray containing the change in LW cloud radiative effect at the 600 | TOA with coords of time, lat, and lon and units of Wm^-2. positive 601 | = downwards. 602 | 603 | rf_lwas : xarray DataArray 604 | DataArray containing the LW all-sky radiative forcing 605 | in units of Wm^-2 with coords of lat, lon, and time. Defaults to DataArray of 606 | 0 if not provided by user. 607 | 608 | rf_lwcs : xarray DataArray 609 | DataArray containing the LW clear-sky radiative forcing 610 | in units of Wm^-2 with coords of lat, lon, and time. Defaults to DataArray of 611 | 0 if not provided by user. 612 | 613 | Returns 614 | ------- 615 | lw_cld_feedback : xarray DataArray 616 | Three-dimensional DataArray containing the TOA radiative perturbation 617 | from the longwave cloud feedback. 618 | """ 619 | # Assume all are on the same horizontal grid. 620 | # water vapor cloud masking term 621 | dq_lw = q_lwcs - q_lwas 622 | 623 | # Check to make sure that either both or neither of rfs were provided 624 | if (rf_lwas is None) != (rf_lwcs is None): 625 | raise ValueError("Either both or neither of rf_lw terms must be specified.") 626 | elif rf_lwas is None and rf_lwcs is None: 627 | rf_lwas = xr.zeros_like(dq_lw) 628 | rf_lwcs = xr.zeros_like(dq_lw) 629 | 630 | # temperature cloud masking term 631 | dt = t_cs - t_as 632 | 633 | # RF cloud masking term 634 | # first double check that the LW RF is positive 635 | rf_coeff = -1 if rf_lwcs.mean() < 0 else 1 636 | dRF_lw = rf_coeff * (rf_lwcs - rf_lwas) 637 | 638 | # calculate longwave cloud feedback 639 | lw_cld_feedback = dCRE_lw + dt + dq_lw + dRF_lw 640 | 641 | return lw_cld_feedback 642 | 643 | 644 | def calc_cloud_SW(alb_as, alb_cs, q_swas, q_swcs, dCRE_sw, rf_swas=None, rf_swcs=None): 645 | """ 646 | Calculate the radiative perturbation from the shortwave cloud feedback 647 | using the adjustment method outlined in Soden et al. (2008). 648 | 649 | Parameters 650 | ---------- 651 | alb_as : xarray DataArray 652 | DataArray containing the all-sky radiative perturbation at the TOA 653 | from the surface albedo feedback. Should have coords of lat, lon, and 654 | time. 655 | 656 | alb_cs : xarray DataArray 657 | DataArray containing the clear-sky radiative perturbation at the TOA 658 | from the surface albedo feedback. Should have coords of lat, lon, and 659 | time. 660 | 661 | q_swas : xarray DataArray 662 | DataArray containing the vertically integrated all-sky LW radiative 663 | perturbation at the TOA from the shortwave water vapor feedback. 664 | Should have coords of lat, lon, and time. 665 | 666 | q_swcs : xarray DataArray 667 | DataArray containing the vertically integrated clear-sky LW radiative 668 | perturbation at the TOA from the shortwave water vapor feedback. 669 | Should have coords of lat, lon, and time. 670 | 671 | dCRE_sw : xarray DataArray 672 | Three-dimensional DataArray containing the change in shortwave cloud 673 | radiative effect at the top-of-atmosphere with coords of time, lat, 674 | and lon and units of Wm^-2. positive = downwards. 675 | 676 | rf_swas : xarray DataArray 677 | The shortwave all-sky radiative forcing in units of 678 | Wm^-2 with coords of lat, lon, and time. Defaults to DataArray of 0 679 | if not provided by the user. 680 | 681 | rf_swcs : xarray DataArray 682 | The shortwave clear-sky radiative forcing in units of 683 | Wm^-2 with coords of lat, lon, and time. Defaults to DataArray of 0 684 | if not provided by the user. 685 | 686 | Returns 687 | ------- 688 | sw_cld_feedback : xarray DataArray 689 | Three-dimensional DataArray containing the TOA radiative perturbation 690 | from the shortwave cloud feedback. 691 | """ 692 | # For now, we will assume all are on the same grid. 693 | # water vapor cloud masking term 694 | dq_sw = q_swcs - q_swas 695 | 696 | # Check to make sure that either both or neither of rfs were provided 697 | if (rf_swas is None) != (rf_swcs is None): 698 | raise ValueError("Either both or neither of rf_sw terms must be specified.") 699 | elif rf_swas is None and rf_swcs is None: 700 | rf_swas = xr.zeros_like(dq_sw) 701 | rf_swcs = xr.zeros_like(dq_sw) 702 | 703 | # surface albedo cloud masking term 704 | dalb = alb_cs - alb_as 705 | 706 | # RF cloud masking term 707 | dRF_sw = rf_swcs - rf_swas 708 | 709 | # calculate longwave cloud feedback 710 | sw_cld_feedback = dCRE_sw + dalb + dq_sw + dRF_sw 711 | 712 | return sw_cld_feedback 713 | 714 | 715 | def calc_cloud_LW_res(ctrl_FLNT, pert_FLNT, t_lw, q_lw, rf_lw=None): 716 | """ 717 | Calculate the radiative perturbation from the shortwave cloud feedback 718 | using the residual method outlined in Soden & Held (2006). 719 | 720 | Parameters 721 | ---------- 722 | ctrl_FLNT : xarray DataArray 723 | Three-dimensional DataArray containing the all-sky net longwave flux 724 | at the top-of-atmosphere in the control simulation 725 | with coords of time, lat, and lon and units of Wm^-2. It should be 726 | oriented such that positive = downwards. 727 | 728 | pert_FLNT : xarray DataArray 729 | Three-dimensional DataArray containing the all-sky net longwave flux 730 | at the top-of-atmosphere in the perturbed simulation 731 | with coords of time, lat, and lon and units of Wm^-2. It should be 732 | oriented such that positive = downwards. 733 | 734 | t_lw : xarray DataArray 735 | DataArray containing the vertically integrated all-sky radiative 736 | perturbation at the TOA from the total temperature feedback. The 737 | total temperature feedback is the sum of the Planck and lapse rate 738 | feedbacks. Should have dims of lat, lon, and time. 739 | 740 | q_lw : xarray DataArray 741 | DataArray containing the vertically integrated LW all-sky radiative 742 | perturbation at the TOA from the water vapor feedback. Should have 743 | coords of lat, lon, and time. 744 | 745 | rf_lw : xarray DataArray 746 | The longwave all-sky radiative forcing in units of Wm^-2 747 | with coords of lat, lon, and time. Defaults to DataArray of 0 748 | if not provided by the user. 749 | 750 | Returns 751 | ------- 752 | lw_cld_feedback : xarray DataArray 753 | Three-dimensional DataArray containing the TOA radiative perturbation 754 | from the longwave cloud feedback. 755 | """ 756 | # Calculate ΔR as the difference in net longwave flux 757 | # double check that sign is correct first, though 758 | lw_coeff = 1 if ctrl_FLNT.mean() < 0 else -1 759 | dR_lw = lw_coeff * (pert_FLNT - ctrl_FLNT) 760 | 761 | # Set rf to 0 if not provided 762 | if rf_lw is None: 763 | rf_lw = xr.zeros_like(dR_lw) 764 | 765 | rf_coeff = -1 if rf_lw.mean() < 0 else 1 766 | lw_cld_feedback = dR_lw - (rf_coeff * rf_lw) - t_lw - q_lw 767 | return lw_cld_feedback 768 | 769 | 770 | def calc_cloud_SW_res(ctrl_FSNT, pert_FSNT, q_sw, alb_sw, rf_sw=None): 771 | """ 772 | Calculate the radiative perturbation from the shortwave cloud feedback 773 | using the residual method outlined in Soden & Held (2006). 774 | 775 | Parameters 776 | ---------- 777 | ctrl_FSNT : xarray DataArray 778 | Three-dimensional DataArray containing the all-sky net shortwave flux 779 | at the top-of-atmosphere in the control simulation 780 | with coords of time, lat, and lon and units of Wm^-2. It should be 781 | oriented such that positive = downwards/incoming. 782 | 783 | pert_FSNT : xarray DataArray 784 | Three-dimensional DataArray containing the all-sky net shortwave flux 785 | at the top-of-atmosphere in the perturbed simulation 786 | with coords of time, lat, and lon and units of Wm^-2. It should be 787 | oriented such that positive = downwards/incoming. 788 | 789 | q_sw : xarray DataArray 790 | DataArray containing the vertically integrated all-sky LW radiative 791 | perturbation at the TOA from the shortwave water vapor feedback. 792 | Should have coords of lat, lon, and time. 793 | 794 | alb_sw : xarray DataArray 795 | DataArray containing the all-sky radiative perturbation at the TOA 796 | from the surface albedo feedback. Should have coords of lat, lon, and 797 | time. 798 | 799 | rf_sw : xarray DataArray 800 | The shortwave all-sky radiative forcing in units of Wm^-2 801 | with coords of lat, lon, and time. This is usually the stratosphere- 802 | adjusted RF. Defaults to DataArray of 0 if not provided by the user. 803 | 804 | Returns 805 | ------- 806 | sw_cld_feedback : xarray DataArray 807 | Three-dimensional DataArray containing the TOA radiative perturbation 808 | from the shortwave cloud feedback. 809 | """ 810 | # Calculate ΔR as the difference in net shortwave flux 811 | dR_sw = pert_FSNT - ctrl_FSNT 812 | 813 | # Set rf to 0 if not provided 814 | if rf_sw is None: 815 | rf_sw = xr.zeros_like(dR_sw) 816 | 817 | sw_cld_feedback = dR_sw - rf_sw - q_sw - alb_sw 818 | return sw_cld_feedback 819 | 820 | 821 | def calc_strato_T( 822 | ctrl_ta, pert_ta, pert_ps, pert_trop=None, kern="GFDL", sky="all-sky" 823 | ): 824 | """ 825 | Calculate the raditive pertubations (W/m^2) at the TOA from changes in 826 | statosphere air temperature using user-specified kernel. Horizontal 827 | resolution is kept at input data's resolution. 828 | 829 | Parameters 830 | ---------- 831 | ctrl_ta : xarray DataArray 832 | DataArray containing air temperature on pressure levels in the 833 | control simulation. 4D with coordinates of time, latitude, longitude, 834 | and pressure level and units of K. 835 | 836 | pert_ta : xarray DataArray 837 | DataArray containing air temperature on pressure levels in the 838 | perturbed simulation. 4D with coordinates of time, latitude, 839 | longitude, and pressure level and units of K. 840 | 841 | pert_ps : xarray DataArray 842 | DataArray containing surface pressure in the perturbed simulation. 3D 843 | with coordinates of time, latitude, and longitude and units of Pa or 844 | hPa. 845 | 846 | pert_trop : xarray DataArray, optional 847 | DataArray containing tropopause pressure in the perturbed simulation. 848 | 3D with coordinates of time, latitude, and longitude and units of Pa 849 | or hPa. If not provided, ClimKern will assume a tropopause height of 850 | 100 hPa at the equator, linearly descending with the cosine of 851 | latitude to 300 hPa at the poles. 852 | 853 | kern : string, optional 854 | String specifying the name of the desired kernel. Defaults to "GFDL". 855 | 856 | sky : string, optional 857 | String, either "all-sky" or "clear-sky", specifying whether to 858 | calculate the all-sky or clear-sky feedbacks. Defaults to "all-sky". 859 | 860 | Returns 861 | ------- 862 | T_feedback : xarray DataArray 863 | 3D DataArray containing the vertically integrated radiative 864 | perturbations caused by changes in stratospheric temperature. 865 | Has coordinates of time, lat, and lon. 866 | """ 867 | # get correct keys for all-sky or clear-sky 868 | t_key = "lw_t" if check_sky(sky) == "all-sky" else "lwclr_t" 869 | 870 | # check input coordinates 871 | ctrl_ta = check_coords(ctrl_ta, ndim=4) 872 | pert_ta = check_coords(pert_ta, ndim=4) 873 | pert_ps = check_coords(pert_ps) 874 | 875 | # check input units 876 | ctrl_ta = check_var_units(check_plev_units(ctrl_ta), "T") 877 | pert_ta = check_var_units(check_plev_units(pert_ta), "T") 878 | pert_ps = check_pres_units(pert_ps, "pert PS") 879 | 880 | # check tropopause units if provided by user, else create dummy tropopause 881 | if type(pert_trop) == type(None): 882 | pert_trop = make_tropo(pert_ps) 883 | else: 884 | pert_trop = check_coords(pert_trop) 885 | pert_trop = check_pres_units(pert_trop, "pert tropopause") 886 | 887 | # make climatology 888 | ctrl_ta_clim = make_clim(ctrl_ta) 889 | 890 | # calculate change in air temperature 891 | diff_ta = pert_ta - tile_data(ctrl_ta_clim, pert_ta) 892 | 893 | # read in and regrid temperature kernel 894 | kernel = check_plev(get_kern(kern)) 895 | regridder = xe.Regridder( 896 | kernel[t_key], 897 | diff_ta, 898 | method="bilinear", 899 | reuse_weights=False, 900 | periodic=True, 901 | extrap_method="nearest_s2d", 902 | ) 903 | ta_kernel = tile_data(regridder(kernel[t_key]), diff_ta) 904 | 905 | # regrid diff_ta to kernel pressure levels 906 | diff_ta = diff_ta.interp_like(ta_kernel, kwargs={"fill_value": "extrapolate"}) 907 | 908 | # use get_function in climkern.util to calculate layer thickness 909 | dp = get_dp(diff_ta, pert_ps, pert_trop, layer="stratosphere") 910 | 911 | # calculate total temperature feedback 912 | T_feedback = ((ta_kernel * diff_ta.fillna(0)) * dp / 10000).sum( 913 | dim="plev", min_count=1 914 | ) 915 | 916 | return T_feedback 917 | 918 | 919 | def calc_strato_q( 920 | ctrl_q, 921 | ctrl_ta, 922 | pert_q, 923 | pert_ps, 924 | pert_trop=None, 925 | kern="GFDL", 926 | sky="all-sky", 927 | method=1, 928 | ): 929 | """ 930 | Calculate the raditive pertubations (W/m^2), LW & SW, at the TOA from 931 | changes in stratospheric specific humidity using user-specified kernel. 932 | Horizontal resolution is kept at input data's resolution. 933 | 934 | Parameters 935 | ---------- 936 | ctrl_q : xarray DataArray 937 | DataArray containing specific humidity on pressure levels in the 938 | control simulation. 4D with coordinates of time, latitude, longitude, 939 | and pressure level and units of "1", "kg/kg", or "g/kg". 940 | 941 | ctrl_ta : xarray DataArray 942 | DataArray containing air temperature on pressure levels in the 943 | control simulation. 4D with coordinates of time, latitude, longitude, 944 | and pressure level and units of K. 945 | 946 | pert_q : xarray DataArray 947 | DataArray containing specific humidity on pressure levels in the 948 | perturbed simulation. 4D with coordinates of time, latitude, 949 | longitude, and pressure level and units of "1", "kg/kg", or "g/kg". 950 | 951 | pert_ps : xarray DataArray 952 | DataArray containing surface pressure in the perturbed simulation. 3D 953 | with coordinates of time, latitude, and longitude and units of Pa or 954 | hPa. 955 | 956 | pert_trop : xarray DataArray, optional 957 | DataArray containing tropopause pressure in the perturbed simulation. 958 | 3D with coordinates of time, latitude, and longitude and units of Pa 959 | or hPa. If not provided, ClimKern will assume a tropopause height of 960 | 100 hPa at the equator, linearly descending with the cosine of 961 | latitude to 300 hPa at the poles. 962 | 963 | kern : string, optional 964 | String specifying the name of the desired kernel. Defaults to "GFDL". 965 | 966 | sky : string, optional 967 | String, either "all-sky" or "clear-sky", specifying whether to 968 | calculate the all-sky or clear-sky feedbacks. Defaults to "all-sky". 969 | 970 | method : int, optional 971 | Specifies the method to use to calculate the specific humidity 972 | feedback. Options 1, 2, and 3 use the change in the natural logarithm of 973 | specific humidity, while 4 uses the linear change. The options are: 974 | 1 -- Uses the actual logarithm for both the specific humidity response 975 | and the normalization factor. 976 | 2 -- Uses the fractional change approximation of logarithms only in the 977 | normalization factor, with the actual logarithm used in the specific humidity 978 | response. 979 | 3 -- Uses the fractional change approximation of logarithms in the specific 980 | humidity response & normalization factor. 981 | 4 -- Uses the linear change in specific humidity for both. 982 | 983 | Returns 984 | ------- 985 | lw_q_feedback : xarray DataArray 986 | 3D DataArray containing the vertically integrated radiative 987 | perturbations from changes in specific humidity in the stratosphere 988 | (longwave). Has coordinates of time, lat, and lon. 989 | 990 | sw_q_feedback : xarray DataArray 991 | 3D DataArray containing the vertically integrated radiative 992 | perturbations from changes in specific humidity in the stratosphere 993 | (shortwave). Has coordinates of time, lat, and lon. 994 | """ 995 | # Issue warning if user provides old keywords for the method argument 996 | if method in ["pendergrass", "kramer", "zelinka", "linear"]: 997 | warnings.warn( 998 | "Name keywords are deprecated and will be removed" 999 | + ' from future versions of ClimKern. Please use "1", ' 1000 | + '"2", "3", or "4" instead.', 1001 | FutureWarning, 1002 | ) 1003 | mapping = {"pendergrass": 3, "kramer": 2, "zelinka": 1, "linear": 4} 1004 | method = mapping[method] 1005 | 1006 | # get correct keys for all-sky or clear-sky 1007 | qlw_key = "lw_q" if check_sky(sky) == "all-sky" else "lwclr_q" 1008 | qsw_key = "sw_q" if sky == "all-sky" else "swclr_q" 1009 | 1010 | # check input coordinates 1011 | ctrl_q = check_coords(ctrl_q, ndim=4) 1012 | ctrl_ta = check_coords(ctrl_ta, ndim=4) 1013 | pert_q = check_coords(pert_q, ndim=4) 1014 | pert_ps = check_coords(pert_ps) 1015 | 1016 | # check input units 1017 | ctrl_ta = check_var_units(check_plev_units(ctrl_ta), "T") 1018 | ctrl_q = check_var_units(check_plev_units(ctrl_q), "q") 1019 | pert_q = check_var_units(check_plev_units(pert_q), "q") 1020 | pert_ps = check_pres_units(pert_ps, "pert PS") 1021 | 1022 | # check tropopause units if provided by user, else create dummy tropopause 1023 | if type(pert_trop) == type(None): 1024 | pert_trop = make_tropo(pert_ps) 1025 | else: 1026 | pert_trop = check_coords(pert_trop) 1027 | pert_trop = check_pres_units(pert_trop, "pert tropopause") 1028 | 1029 | # make climatology 1030 | ctrl_q_clim = make_clim(ctrl_q) 1031 | ctrl_ta_clim = make_clim(ctrl_ta) 1032 | 1033 | # if q has units of unity or kg/kg, we will have to 1034 | # multiply by 1000 later on to make it g/kg 1035 | if ctrl_q.units in ["1", "kg/kg"]: 1036 | conv_factor = 1000 1037 | elif ctrl_q.units in ["g/kg"]: 1038 | conv_factor = 1 1039 | else: 1040 | warnings.warn("Cannot determine units of q. Assuming kg/kg.") 1041 | conv_factor = 1000 1042 | 1043 | # tile control climatology to match length of pert simulation time dim 1044 | ctrl_q_clim_tiled = tile_data(ctrl_q_clim, pert_q) 1045 | 1046 | if method == 3: 1047 | diff_q = (pert_q - ctrl_q_clim_tiled) / ctrl_q_clim_tiled 1048 | elif method == 4: 1049 | diff_q = pert_q - ctrl_q_clim_tiled 1050 | elif method in [2, 1]: 1051 | diff_q = np.log(pert_q) - np.log(ctrl_q_clim_tiled) 1052 | else: 1053 | raise ValueError("Please select a valid choice for the method argument.") 1054 | 1055 | # read in and regrid water vapor kernel 1056 | kernel = check_plev(get_kern(kern)) 1057 | regridder = xe.Regridder( 1058 | kernel[qlw_key], 1059 | diff_q, 1060 | method="bilinear", 1061 | extrap_method="nearest_s2d", 1062 | reuse_weights=False, 1063 | periodic=True, 1064 | ) 1065 | 1066 | qlw_kernel = tile_data(regridder(kernel[qlw_key], skipna=True), diff_q) 1067 | qsw_kernel = tile_data(regridder(kernel[qsw_key], skipna=True), diff_q) 1068 | 1069 | # regrid diff_q, ctrl_q_clim, and ctrl_ta_clim to kernel pressure levels 1070 | kwargs = {"fill_value": "extrapolate"} 1071 | diff_q = diff_q.interp_like(qlw_kernel, kwargs=kwargs) 1072 | ctrl_q_clim = ctrl_q_clim.interp(plev=qlw_kernel.plev, kwargs=kwargs) 1073 | ctrl_q_clim.plev.attrs["units"] = ctrl_q.plev.units 1074 | ctrl_ta_clim = ctrl_ta_clim.interp(plev=qlw_kernel.plev, kwargs=kwargs) 1075 | ctrl_ta_clim.plev.attrs["units"] = ctrl_ta.plev.units 1076 | 1077 | # create the normalization factor, which corresponds to the increase 1078 | # in specific humidity for a 1K increate in temperature 1079 | norm = tile_data(calc_q_norm(ctrl_ta_clim, ctrl_q_clim, method=method), diff_q) 1080 | 1081 | # use get_function in climkern.util to calculate layer thickness 1082 | dp = get_dp(diff_q, pert_ps, pert_trop, layer="stratosphere") 1083 | 1084 | # calculate feedbacks 1085 | qlw_feedback = (qlw_kernel / norm * diff_q * conv_factor * dp / 10000).sum( 1086 | dim="plev", min_count=1 1087 | ) 1088 | qsw_feedback = ( 1089 | (qsw_kernel / norm * diff_q * conv_factor * dp / 10000) 1090 | .sum(dim="plev", min_count=1) 1091 | .fillna(0) 1092 | ) 1093 | 1094 | # one complication: CloudSat needs to be masked so we don't fill the NaNs 1095 | # with zeros 1096 | if kern == "CloudSat": 1097 | qsw_feedback = qsw_feedback.where( 1098 | qsw_kernel.sum(dim="plev", min_count=1).notnull() 1099 | ) 1100 | 1101 | return (qlw_feedback, qsw_feedback) 1102 | 1103 | 1104 | def calc_RH_feedback( 1105 | ctrl_q, 1106 | ctrl_ta, 1107 | ctrl_ps, 1108 | pert_q, 1109 | pert_ta, 1110 | pert_ps, 1111 | pert_trop=None, 1112 | kern="GFDL", 1113 | sky="all-sky", 1114 | method=1, 1115 | ): 1116 | """ 1117 | Calculate the TOA radiative perturbations from changes in relative 1118 | humidity following Held & Shell (2012). Horizontal resolution is 1119 | kept at input data's resolution. 1120 | 1121 | Parameters 1122 | ---------- 1123 | ctrl_q : xarray DataArray 1124 | DataArray containing specific humidity on pressure levels in the 1125 | control simulation. Must be 4D with coords of time, lat, lon, and plev 1126 | with units of either "1", "kg/kg", or "g/kg". 1127 | 1128 | ctrl_ta : xarray DataArray 1129 | DataArray containing air temperature on pressure levels in the control 1130 | simulation. Must be 4D with coords of time, lat, lon, and plev with 1131 | units "K". 1132 | 1133 | ctrl_ps : xarray DataArray 1134 | DataArray containing surface pressure in the control simulation. 3D 1135 | with coordinates of time, latitude, and longitude and units of Pa or 1136 | hPa. 1137 | 1138 | pert_q : xarray DataArray 1139 | DataArray containing specific humidity on pressure levels in the 1140 | perturbed simulation. 4D with coordinates of time, latitude, 1141 | longitude, and pressure level and units of "1", "kg/kg", or "g/kg". 1142 | 1143 | pert_ps : xarray DataArray 1144 | DataArray containing surface pressure in the perturbed simulation. 3D 1145 | with coordinates of time, latitude, and longitude and units of Pa or 1146 | hPa. 1147 | 1148 | pert_trop : xarray DataArray, optional 1149 | DataArray containing tropopause pressure in the perturbed simulation. 1150 | 3D with coordinates of time, latitude, and longitude and units of Pa 1151 | or hPa. If not provided, ClimKern will assume a tropopause height of 1152 | 100 hPa at the equator, linearly descending with the cosine of 1153 | latitude to 300 hPa at the poles. 1154 | 1155 | kern : string, optional 1156 | String specifying the name of the desired kernel. Defaults to "GFDL". 1157 | 1158 | sky : string, optional 1159 | String, either "all-sky" or "clear-sky", specifying whether to 1160 | calculate the all-sky or clear-sky feedbacks. Defaults to "all-sky". 1161 | 1162 | method : int, optional 1163 | Specifies the method to use to calculate the specific humidity 1164 | feedback. Options 1, 2, and 3 use the change in the natural logarithm of 1165 | specific humidity, while 4 uses the linear change. The options are: 1166 | 1 -- Uses the actual logarithm for both the specific humidity response 1167 | and the normalization factor. 1168 | 2 -- Uses the fractional change approximation of logarithms only in the 1169 | normalization factor, with the actual logarithm used in the specific humidity 1170 | response. 1171 | 3 -- Uses the fractional change approximation of logarithms in the specific 1172 | humidity response & normalization factor. 1173 | 4 -- Uses the linear change in specific humidity for both. 1174 | 1175 | Returns 1176 | ------- 1177 | RH_feedback : xarray DataArray 1178 | 3D DataArray containing the vertically integrated radiative 1179 | perturbations from changes in relative humidity (LW+SW). 1180 | Has coordinates of time, lat, and lon. 1181 | """ 1182 | # Issue warning if user provides old keywords for the method argument 1183 | if method in ["pendergrass", "kramer", "zelinka", "linear"]: 1184 | warnings.warn( 1185 | "Name keywords are deprecated and will be removed" 1186 | + ' from future versions of ClimKern. Please use "1", ' 1187 | + '"2", "3", or "4" instead.', 1188 | FutureWarning, 1189 | ) 1190 | mapping = {"pendergrass": 3, "kramer": 2, "zelinka": 1, "linear": 4} 1191 | method = mapping[method] 1192 | 1193 | # get correct keys for all-sky or clear-sky 1194 | qlw_key = "lw_q" if check_sky(sky) == "all-sky" else "lwclr_q" 1195 | qsw_key = "sw_q" if sky == "all-sky" else "swclr_q" 1196 | 1197 | # check input coordinates 1198 | ctrl_q = check_coords(ctrl_q, ndim=4) 1199 | ctrl_ta = check_coords(ctrl_ta, ndim=4) 1200 | pert_q = check_coords(pert_q, ndim=4) 1201 | pert_ta = check_coords(pert_ta, ndim=4) 1202 | ctrl_ps = check_coords(ctrl_ps) 1203 | pert_ps = check_coords(pert_ps) 1204 | 1205 | # check input units 1206 | ctrl_ta = check_var_units(check_plev_units(ctrl_ta), "T") 1207 | pert_ta = check_var_units(check_plev_units(pert_ta), "T") 1208 | ctrl_q = check_var_units(check_plev_units(ctrl_q), "q") 1209 | pert_q = check_var_units(check_plev_units(pert_q), "q") 1210 | ctrl_ps = check_pres_units(ctrl_ps, "ctrl PS") 1211 | pert_ps = check_pres_units(pert_ps, "pert PS") 1212 | 1213 | # check tropopause units if provided by user, else create dummy tropopause 1214 | if type(pert_trop) == type(None): 1215 | pert_trop = make_tropo(pert_ps) 1216 | else: 1217 | pert_trop = check_coords(pert_trop) 1218 | pert_trop = check_pres_units(pert_trop, "pert tropopause") 1219 | 1220 | # make climatology 1221 | ctrl_ps_clim = make_clim(ctrl_ps) 1222 | ctrl_q_clim = make_clim(ctrl_q) 1223 | ctrl_q_clim = ctrl_q_clim.where(ctrl_q_clim.plev < ctrl_ps_clim) 1224 | ctrl_ta_clim = make_clim(ctrl_ta) 1225 | ctrl_ta_clim = ctrl_ta_clim.where(ctrl_ta_clim.plev < ctrl_ps_clim) 1226 | 1227 | # if q has units of unity or kg/kg, we will have to 1228 | # multiply by 1000 later on to make it g/kg 1229 | if ctrl_q.units in ["1", "kg/kg"]: 1230 | conv_factor = 1000 1231 | elif ctrl_q.units in ["g/kg"]: 1232 | conv_factor = 1 1233 | else: 1234 | warnings.warn("Cannot determine units of q. Assuming kg/kg.") 1235 | conv_factor = 1000 1236 | 1237 | # tile control climatology to match length of pert simulation time dim 1238 | ctrl_q_clim_tiled = tile_data(ctrl_q_clim, pert_q) 1239 | 1240 | if method == 3: 1241 | diff_q = (pert_q - ctrl_q_clim_tiled) / ctrl_q_clim_tiled 1242 | elif method == 4: 1243 | diff_q = pert_q - ctrl_q_clim_tiled 1244 | elif method in [2, 1]: 1245 | diff_q = np.log(pert_q) - np.log(ctrl_q_clim_tiled) 1246 | else: 1247 | raise ValueError("Please select a valid choice for the method argument.") 1248 | 1249 | # for the RH feedback, we also need the T response 1250 | diff_ta = pert_ta - tile_data(ctrl_ta_clim, pert_ta) 1251 | 1252 | # read in and regrid water vapor kernel 1253 | kernel = check_plev(get_kern(kern)) 1254 | regridder = xe.Regridder( 1255 | kernel[qlw_key], 1256 | diff_q, 1257 | method="bilinear", 1258 | extrap_method="nearest_s2d", 1259 | reuse_weights=False, 1260 | periodic=True, 1261 | ) 1262 | q_kernel = kernel[qlw_key] + kernel[qsw_key] 1263 | q_kernel = tile_data(regridder(q_kernel, skipna=True), diff_q) 1264 | 1265 | # regrid diff_q, ctrl_q_clim, and ctrl_ta_clim to kernel pressure levels 1266 | kwargs = {"fill_value": "extrapolate"} 1267 | diff_q = diff_q.interp_like(q_kernel, kwargs=kwargs) 1268 | diff_ta = diff_ta.interp_like(q_kernel, kwargs=kwargs) 1269 | ctrl_q_clim = ctrl_q_clim.interp(plev=q_kernel.plev, kwargs=kwargs) 1270 | ctrl_q_clim.plev.attrs["units"] = ctrl_q.plev.units 1271 | ctrl_ta_clim = ctrl_ta_clim.interp(plev=q_kernel.plev, kwargs=kwargs) 1272 | ctrl_ta_clim.plev.attrs["units"] = ctrl_ta.plev.units 1273 | 1274 | # create the normalization factor, which corresponds to the increase 1275 | # in specific humidity for a 1K increate in temperature 1276 | norm = tile_data(calc_q_norm(ctrl_ta_clim, ctrl_q_clim, method=method), diff_q) 1277 | 1278 | # use get_dp in climkern.util to calculate layer thickness 1279 | dp = get_dp(diff_q, pert_ps, pert_trop, layer="troposphere") 1280 | 1281 | # calculate RH_feedback 1282 | RH_feedback = ( 1283 | ( 1284 | (q_kernel / norm * diff_q * conv_factor * dp / 10000) 1285 | - (q_kernel * diff_ta * dp / 10000) 1286 | ) 1287 | .sum(dim="plev", min_count=1) 1288 | .fillna(0) 1289 | ) 1290 | 1291 | # one complication: CloudSat needs to be masked so we don't fill the NaNs 1292 | # with zeros 1293 | if kern == "CloudSat": 1294 | RH_feedback = RH_feedback.where(q_kernel.sum(dim="plev", min_count=1).notnull()) 1295 | 1296 | return RH_feedback 1297 | 1298 | 1299 | def tutorial_data(label): 1300 | """ 1301 | Retrieve tutorial data which should be located in the package's 1302 | "data" folder. 1303 | 1304 | Parameters 1305 | ---------- 1306 | label : string 1307 | Specifies which data to access. Choices are "ctrl", "pert", "IRF", "adjRF", 1308 | or "ERF" for the 1xCO2, 2xCO2, instantaneous radiative forcing, 1309 | stratosphere-adjusted radiative forcing, and effective radiative forcing, 1310 | respectively. 1311 | 1312 | Returns 1313 | ------- 1314 | data : xarray Dataset 1315 | Requested tutorial dataset. 1316 | """ 1317 | if label not in ["ctrl", "pert", "IRF", "adjRF", "ERF"]: 1318 | raise ValueError("Invalid data name. See docstring for options.") 1319 | path = "data/tutorial_data/" + label + ".nc" 1320 | data = xr.open_dataset(files("climkern").joinpath(path)) 1321 | return data 1322 | 1323 | 1324 | def spat_avg(data, lat_bound_s=-90, lat_bound_n=90): 1325 | """ 1326 | Compute the spatial average while weighting for cos(latitude), optionally 1327 | specifying latitudinal boundaries. 1328 | 1329 | Parameters 1330 | ---------- 1331 | data : Xarray DataArray 1332 | 3D input data over which to compute the spatial average. 1333 | 1334 | lat_bound_s : float, optional 1335 | Latitude of the southern boundary of the area over which to average. 1336 | Defaults to -90 to compute a global average. 1337 | 1338 | lat_bound_n : float, optional 1339 | Latitude of the northern boundary of the area over which to average. 1340 | Defaults to 90 to compute a global average. 1341 | 1342 | Returns 1343 | ------- 1344 | avg : Xarray DataArray 1345 | New spatially averaged DataArray with latitude and longitude 1346 | coordinates now removed. 1347 | """ 1348 | # check coords, constrain area of interest, and take zonal mean 1349 | data = check_coords(data).sel(lat=slice(lat_bound_s, lat_bound_n)).mean(dim="lon") 1350 | # compute weights 1351 | weights = np.cos(np.deg2rad(data.lat)) / np.cos(np.deg2rad(data.lat)).sum() 1352 | 1353 | # compute average 1354 | avg = (data * weights).sum(dim="lat") 1355 | 1356 | # return avg 1357 | return avg 1358 | -------------------------------------------------------------------------------- /climkern/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import xarray as xr 3 | 4 | import climkern as ck 5 | 6 | 7 | @pytest.fixture 8 | def ctrl() -> xr.Dataset: 9 | """Read in the control tutorial data.""" 10 | return ck.tutorial_data("ctrl") 11 | 12 | 13 | @pytest.fixture 14 | def pert() -> xr.Dataset: 15 | """Read in the 2xCO2 tutorial data.""" 16 | return ck.tutorial_data("pert") 17 | 18 | 19 | @pytest.fixture 20 | def dTS_glob_avg(ctrl: xr.Dataset, pert: xr.Dataset) -> xr.Dataset: 21 | return ck.spat_avg(pert.TS - ctrl.TS) 22 | -------------------------------------------------------------------------------- /climkern/tests/test_frontend.py: -------------------------------------------------------------------------------- 1 | import xarray as xr 2 | 3 | from climkern.frontend import * 4 | 5 | 6 | def test_calc_T_feedbacks( 7 | ctrl: xr.Dataset, pert: xr.Dataset, dTS_glob_avg: xr.DataArray 8 | ) -> None: 9 | LR, Planck = calc_T_feedbacks( 10 | ctrl.T, ctrl.TS, ctrl.PS, pert.T, pert.TS, pert.PS, pert.TROP_P, kern="GFDL" 11 | ) 12 | LR_val = (spat_avg(LR) / dTS_glob_avg).mean() 13 | Planck_val = (spat_avg(Planck) / dTS_glob_avg).mean() 14 | xr.testing.assert_allclose(LR_val, xr.DataArray(-0.41), atol=0.01) 15 | xr.testing.assert_allclose(Planck_val, xr.DataArray(-3.12), atol=0.01) 16 | 17 | 18 | def test_albedo_feedbacks( 19 | ctrl: xr.Dataset, pert: xr.Dataset, dTS_glob_avg: xr.DataArray 20 | ) -> None: 21 | alb = calc_alb_feedback(ctrl.FSUS, ctrl.FSDS, pert.FSUS, pert.FSDS, kern="GFDL") 22 | val_to_test = (spat_avg(alb) / dTS_glob_avg).mean() 23 | xr.testing.assert_allclose(val_to_test, xr.DataArray(0.38), atol=0.01) 24 | 25 | 26 | def test_calc_q_feedbacks( 27 | ctrl: xr.Dataset, pert: xr.Dataset, dTS_glob_avg: xr.DataArray 28 | ) -> None: 29 | lw, sw = calc_q_feedbacks( 30 | ctrl.Q, ctrl.T, ctrl.PS, pert.Q, pert.PS, pert.TROP_P, kern="GFDL", method=1 31 | ) 32 | q_val = (spat_avg(lw + sw) / dTS_glob_avg).mean() 33 | xr.testing.assert_allclose(q_val, xr.DataArray(1.44), atol=0.01) 34 | -------------------------------------------------------------------------------- /climkern/util.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import numpy as np 4 | import xarray as xr 5 | from importlib_resources import files 6 | 7 | 8 | # monkey patch Python warnings format function 9 | def custom_formatwarning(msg, cat, *args, **kwargs): 10 | return str(cat.__name__) + ": " + str(msg) + "\n" 11 | 12 | 13 | warnings.formatwarning = custom_formatwarning 14 | 15 | # filter out warnings from xarray when using the rename function 16 | # ideally, this can be removed in the future 17 | warnings.filterwarnings("ignore", ".*does not create an index anymore.*") 18 | 19 | 20 | def get_dp(ds_4D, ps, tropo, layer="troposphere"): 21 | """Calculate layer thickness using model pressure levels, surface 22 | pressure, and tropopause pressure. Also specify the layer as either 23 | 'troposphere' or 'stratosphere'. 24 | """ 25 | # construct a 4D DataArray corresponding to layer thickness 26 | # for vertical integration later 27 | # this is achieved by finding the midpoints between pressure levels 28 | # and bounding that array with surface pressure below 29 | # and TOA (p=0) above 30 | aligned = xr.align(ds_4D.plev[1:], ds_4D.plev[:-1], join="override") 31 | mids = xr.broadcast((aligned[0] + aligned[1]) / 2, ps)[0] 32 | ps_expand = ps.expand_dims(dim={"plev": [ds_4D.plev[0]]}, axis=1) 33 | 34 | TOA = xr.zeros_like(ps_expand) 35 | TOA["plev"] = ps_expand.plev * 0 36 | 37 | # this if/else statement accounts for potentially 38 | # reversed pressure axis direction 39 | if ds_4D.plev[0] > ds_4D.plev[-1]: 40 | ilevs = xr.concat([ps_expand, mids, TOA], dim="plev") 41 | sign_change = -1 42 | else: 43 | ilevs = xr.concat([TOA, mids, ps_expand], dim="plev") 44 | sign_change = 1 45 | 46 | if layer == "troposphere": 47 | # make points above tropopause equal to tropopause height 48 | # make points below surface pressure equal to surface pressure 49 | ilevs = ilevs.where(ilevs > tropo, tropo).where(ilevs < ps, ps) 50 | elif layer == "stratosphere": 51 | # make points below tropopause equal to tropopause height 52 | ilevs = ilevs.where(ilevs < tropo, tropo) 53 | 54 | # get the layer thickness by taking finite difference 55 | # along pressure axis 56 | dp = sign_change * ilevs.diff(dim="plev", label="lower") 57 | 58 | # override pressure axis so xarray doesn't throw a fit 59 | dp["plev"] = ds_4D.plev 60 | 61 | # return dp 62 | return dp 63 | 64 | 65 | def check_var_units(da, var): 66 | """Check to see if the xarray DataArray has a units attribute.""" 67 | if "units" not in da.attrs: 68 | if var == "q": 69 | warnings.warn("No units found for input q. Assuming kg/kg.") 70 | return da.assign_attrs({"units": "kg/kg"}) 71 | elif var == "T": 72 | warnings.warn("No units found for input T. Assuming K.") 73 | return da.assign_attrs({"units": "K"}) 74 | else: 75 | return da 76 | 77 | 78 | def make_tropo(da): 79 | """Use the a DataArray containing model lat and lon to make a makeshift 80 | tropopause. 81 | """ 82 | tropo = (3e4 - 2e4 * np.cos(np.deg2rad(da.lat))).broadcast_like(da) 83 | return tropo 84 | 85 | 86 | def check_plev_units(da): 87 | if "units" not in da.plev.attrs: 88 | warnings.warn("No units found for input vertical coordinate. Assuming Pa.") 89 | plev = da.plev.assign_attrs({"units": "Pa"}) 90 | return da.assign_coords({"plev": plev}) 91 | elif da.plev.units in ["hPa", "mb", "millibars"]: 92 | da["plev"] = da.plev * 100 93 | da.plev.attrs["units"] = "Pa" 94 | return da 95 | else: 96 | return da 97 | 98 | 99 | def check_pres_units(da, var_name): 100 | if "units" not in da.attrs: 101 | warnings.warn("Could not determine units of " + var_name + ". Assuming Pa.") 102 | return da.assign_attrs({"units": "Pa"}) 103 | elif da.units in ["hPa", "mb", "millibars"]: 104 | da = da * 100 105 | da.attrs["units"] = "Pa" 106 | return da 107 | else: 108 | return da 109 | 110 | 111 | def tile_data(to_tile, new_shape): 112 | """Tile dataset along time axis to match another dataset.""" 113 | # new_shape = _check_time(new_shape) 114 | if len(new_shape.time) % 12 != 0: 115 | raise ValueError("dataset time dimension must be divisible by 12") 116 | if "month" not in to_tile.coords: 117 | tiled = xr.concat( 118 | [to_tile for i in range(int(len(new_shape.time) / 12))], dim="time" 119 | ) 120 | if "month" in to_tile.dims and len(to_tile.month) == 12: 121 | to_tile = to_tile.rename({"month": "time"}) 122 | tiled = xr.concat( 123 | [to_tile for i in range(int(len(new_shape.time) / 12))], dim="time" 124 | ) 125 | tiled["time"] = new_shape.time 126 | return tiled 127 | 128 | 129 | def get_kern(name, loc="TOA"): 130 | """Read in kernel from local directory.""" 131 | path = "data/kernels/" + name + "/" + loc + "_" + str(name) + "_Kerns.nc" 132 | try: 133 | data = xr.open_dataset(files("climkern").joinpath(path)) 134 | except ValueError: 135 | data = xr.open_dataset(files("climkern").joinpath(path), decode_times=False) 136 | return check_coords(data) 137 | 138 | 139 | def make_clim(da): 140 | "Produce monthly climatology of model field." 141 | try: 142 | clim = ( 143 | da.groupby(da.time.dt.month) 144 | .mean(dim="time", skipna=True) 145 | .rename({"month": "time"}) 146 | ) 147 | except AttributeError: 148 | # AttributeError if time is not datetime object 149 | clim = da 150 | return clim 151 | 152 | 153 | def get_albedo(SWup, SWdown): 154 | """Calculate the surface albedo as the ratio of upward to 155 | downward sfc shortwave. 156 | """ 157 | # avoid dividing by 0 and assign 0 to those grid boxes 158 | return (SWup / SWdown.where(SWdown > 0)).fillna(0) 159 | 160 | 161 | def check_plev(kern): 162 | """Make sure the vertical pressure units of the kernel are in Pa.""" 163 | if kern.plev.units != "Pa": 164 | kern["plev"] = kern.plev * 100 165 | kern.plev.attrs["units"] = "Pa" 166 | else: 167 | pass 168 | return kern 169 | 170 | 171 | def __calc_qs__(temp): 172 | """Calculate the saturated specific humidity 173 | given temperature and pressure. 174 | """ 175 | if temp.plev.units == "Pa": 176 | pres = temp.plev / 100 177 | elif temp.plev.units in ["hPa", "millibars"]: 178 | pres = temp.plev 179 | else: 180 | warnings.warn( 181 | "Cannot determine units of pressure \ 182 | coordinate. Assuming units are Pa." 183 | ) 184 | pres = temp.plev / 100 185 | 186 | if temp.units == "K": 187 | temp_c = temp - 273.15 188 | temp_c.attrs = temp.attrs 189 | temp_c["units"] = "C" 190 | elif temp.units == "C": 191 | temp_c = temp 192 | else: 193 | warnings.warn( 194 | "Warning: Cannot determine units of temperature. \ 195 | Assuming Kelvin." 196 | ) 197 | temp_c = temp - 273.15 198 | temp_c.attrs = temp.attrs 199 | temp_c["units"] = "C" 200 | 201 | # Buck 1981 equation for saturated vapor pressure 202 | esl = ( 203 | (1.0007 + 3.46e-6 * pres) 204 | * 6.1121 205 | * np.exp((17.502 * temp_c) / (240.97 + temp_c)) 206 | ) 207 | esi = ( 208 | (1.0003 + 4.18e-6 * pres) 209 | * 6.1115 210 | * np.exp((22.452 * temp_c) / (272.55 + temp_c)) 211 | ) 212 | 213 | # conversion from vapor pressure to mixing ratio 214 | wsl = 0.622 * esl / (pres - esl) 215 | wsi = 0.622 * esi / (pres - esi) 216 | 217 | # use liquid water w when temp is above freezing 218 | ws = xr.where(temp_c > 0, wsl, wsi) 219 | 220 | # convert to specific humidity 221 | qs = ws / (1 + ws) 222 | qs["units"] = "kg/kg" 223 | return qs 224 | 225 | 226 | def calc_q_norm(ctrl_ta, ctrl_q, method): 227 | """Calculate the change in specific humidity for 1K warming 228 | assuming fixed relative humidity. 229 | """ 230 | if ctrl_q.units == "g/kg": 231 | ctrl_q = ctrl_q / 1000 232 | 233 | # get saturated specific humidity from control air temps 234 | qs0 = __calc_qs__(ctrl_ta) 235 | 236 | # RH = specific humidity / sat. specific humidity 237 | RH = ctrl_q / qs0 238 | 239 | # make a DataArray for the temperature plus 1K 240 | ta1K = ctrl_ta + 1 241 | ta1K.attrs = ctrl_ta.attrs 242 | qs1K = __calc_qs__(ta1K) 243 | 244 | if method == 4: 245 | # get the new specific humidity using the same RH 246 | q1K = qs1K * RH 247 | q1K["units"] = "kg/kg" 248 | 249 | # take the difference 250 | dq1K = 1000 * (q1K - ctrl_q) 251 | return dq1K 252 | 253 | elif method in [3, 2]: 254 | dqsdT = qs1K - qs0 255 | dqdT = RH * dqsdT 256 | 257 | dlogqdT = 1000 * (dqdT / ctrl_q) 258 | return dlogqdT 259 | 260 | elif method == 1: 261 | dlogqdT = 1000 * (np.log(qs1K.where(qs1K > 0)) - np.log(qs0.where(qs0 > 0))) 262 | return dlogqdT 263 | 264 | 265 | def check_sky(sky): 266 | """Make sure the sky argument is either all-sky or clear-sky.""" 267 | if sky not in ["all-sky", "clear-sky"]: 268 | raise ValueError("The sky argument must either be all-sky or clear-sky.") 269 | else: 270 | return sky 271 | 272 | 273 | def check_coords(ds, ndim=3): 274 | """Universal function to check that dataset coordinates are in line with 275 | what the package requires. 276 | """ 277 | # time 278 | if "time" in ds.dims: 279 | pass 280 | elif "month" in ds.dims: 281 | ds = ds.rename({"month": "time"}) 282 | else: 283 | raise AttributeError( 284 | "There is no 'time' or 'month' dimension in" 285 | + "one of the input DataArrays. Please rename your time dimension(s)." 286 | ) 287 | 288 | # lat and lon 289 | if "lat" not in ds.dims and "latitude" not in ds.dims: 290 | raise AttributeError( 291 | "There is no 'lat' or 'latitude' dimension in\ 292 | one of the input DataArrays. Please rename your lat dimension(s)." 293 | ) 294 | 295 | if "lon" not in ds.dims and "longitude" not in ds.dims: 296 | raise AttributeError( 297 | "There is no 'lon' or 'longitude' dimension in\ 298 | one of the input DataArrays. Please rename your lat dimension(s)." 299 | ) 300 | 301 | if ndim == 4: 302 | if "plev" in ds.dims: 303 | pass 304 | else: 305 | bool = False 306 | for n in ["lev", "player", "level"]: 307 | if n in ds.dims: 308 | ds = ds.rename({n: "plev"}) 309 | bool = True 310 | if bool is False: 311 | raise AttributeError( 312 | "Cannot find the name of the pressure\ 313 | coordinate. Please rename it to 'plev'." 314 | ) 315 | return ds 316 | -------------------------------------------------------------------------------- /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 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 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/climkern.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | climkern 3 | ======== 4 | 5 | .. automodule:: climkern 6 | :members: -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = "climkern" 10 | copyright = "2024, Ty Janoski" 11 | author = "Ty Janoski" 12 | release = "v1.0.1" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"] 18 | 19 | autodoc_default_options = {"members": True} 20 | autoclass_content = "class" 21 | napoleon_numpy_docstring = True 22 | napoleon_google_docstring = False 23 | 24 | autodoc_mock_imports = [ 25 | "xarray", 26 | "cf-xarray", 27 | "cftime", 28 | "xesmf", 29 | "importlib_resources", 30 | "pooch", 31 | "tqdm", 32 | "plac", 33 | "netCDF4", 34 | ] 35 | 36 | 37 | # -- Options for HTML output ------------------------------------------------- 38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 39 | 40 | html_theme = "alabaster" 41 | html_static_path = ["_static"] 42 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. climkern documentation master file, created by 2 | sphinx-quickstart on Sun Jan 7 17:42:43 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to climkern's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | climkern 14 | 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "climkern" 7 | authors = [ 8 | {name = "Ty Janoski", email="tyfolino@gmail.com"}, 9 | ] 10 | requires-python = ">= 3.9" 11 | version = "1.2" 12 | dependencies = [ 13 | "xarray>=0.16.2", 14 | "cf-xarray>=0.5.1", 15 | "cftime", 16 | "xesmf>=0.7.1", 17 | "importlib_resources", 18 | "pooch", 19 | "tqdm", 20 | "plac", 21 | "netCDF4", 22 | ] 23 | readme="README.md" 24 | 25 | [project.optional-dependencies] 26 | test = [ 27 | "pytest>=7,<8", 28 | ] 29 | 30 | lint = [ 31 | "precommit>=2.20.0" 32 | ] 33 | 34 | # tools 35 | [tool.black] 36 | line-length = 88 37 | target-version = ["py39","py310", "py311", "py312"] 38 | 39 | # https://github.com/charliermarsh/ruff 40 | [tool.ruff] 41 | line-length = 88 42 | target-version = "py311" 43 | extend-select = [ 44 | "E", # style errors 45 | "F", # flakes 46 | "D", # pydocstyle 47 | "I001", # isort 48 | "UP", # pyupgrade 49 | "N", # pep8-naming 50 | "C", # flake8-comprehensions 51 | "B", # flake8-bugbear 52 | "A001", # flake8-builtins 53 | "RUF", # ruff-specific rules 54 | "RUF100", # Unused noqa directive 55 | ] 56 | extend-ignore = [ 57 | "D100", # Missing docstring in public module 58 | "D101", # Missing docstring in public class 59 | "D103", # Missing docstring in public function 60 | "D107", # Missing docstring in __init__ 61 | "D203", # 1 blank line required before class docstring 62 | "D205", # 1 blank line required between summary line and description 63 | "D212", # Multi-line docstring summary should start at the first line 64 | "D213", # Multi-line docstring summary should start at the second line 65 | "D413", # Missing blank line after last section 66 | "D416", # Section name should end with a colon 67 | "N806", # Variable in function should be lowercase 68 | ] 69 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | --------------------------------------------------------------------------------