├── .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 | [](https://doi.org/10.5281/zenodo.10291284)
4 | [](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 |
--------------------------------------------------------------------------------