├── tests ├── __init__.py ├── repository │ └── co2_bg.nc ├── calc_metric_test.py ├── calc_dt_test.py ├── interpolate_space_test.py ├── calc_response_test.py ├── calc_co2_test.py ├── write_output_test.py ├── calc_ch4_test.py ├── construct_conc_test.py ├── read_netcdf_test.py └── read_config_test.py ├── .github ├── ISSUE_TEMPLATE │ ├── config.yaml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── github-actions-demo.yml │ ├── install_and_test.yml │ └── build-docs.yml └── pull_request_template.md ├── img └── OAC-chart.png ├── repository ├── ch4_bg.nc ├── co2_bg.nc ├── n2o_bg.nc ├── resp_RF.nc ├── resp_ch4.nc ├── resp_cont.nc ├── resp_RF_O3.nc └── resp_cont_lf.nc ├── docs ├── img │ ├── apply_norm.png │ ├── apply_scaling.png │ ├── norm_inventories.png │ ├── scale_inventories.png │ ├── time-constraints_norm.png │ ├── time-constraints_scaling.png │ └── time-constraints_no-evolution.png ├── source │ ├── bibliography.rst │ ├── _static │ │ ├── OAC-chart.png │ │ ├── apply_norm.png │ │ ├── apply_scaling.png │ │ ├── norm_inventories.png │ │ ├── scale_inventories.png │ │ ├── emission-inventory.png │ │ ├── time-constraints_norm.png │ │ ├── time-constraints_scaling.png │ │ └── time-constraints_no-evolution.png │ ├── demos │ │ ├── input │ │ │ ├── time_norm_historic_SSP.nc │ │ │ ├── time_scaling_linear_2019-2039.nc │ │ │ ├── ELK_aviation_2019_res5deg_flat.nc │ │ │ └── ELK_aviation_2039_res5deg_flat.nc │ │ ├── 03_multi_inv │ │ │ ├── multi_inv.rst │ │ │ └── multi_inv.toml │ │ ├── 02_scaling │ │ │ ├── scaling.rst │ │ │ └── scaling.toml │ │ └── 01_norm │ │ │ ├── norm.rst │ │ │ └── historic.toml │ ├── api_ref │ │ ├── oac.main.rst │ │ ├── oac.plot.rst │ │ ├── oac.utils.rst │ │ ├── oac.calc_dt.rst │ │ ├── oac.calc_ch4.rst │ │ ├── oac.calc_co2.rst │ │ ├── oac.calc_cont.rst │ │ ├── oac.calc_metric.rst │ │ ├── oac.read_config.rst │ │ ├── oac.read_netcdf.rst │ │ ├── oac.uncertainties.rst │ │ ├── oac.write_output.rst │ │ ├── oac.calc_response.rst │ │ ├── oac.construct_cont.rst │ │ ├── oac.interpolate_time.rst │ │ └── oac.interpolate_space.rst │ ├── api_ref.rst │ ├── _templates │ │ └── footer.html │ ├── background.rst │ ├── demos.rst │ ├── publications.rst │ ├── contact_support.rst │ ├── governance.rst │ ├── user_guide.rst │ ├── index.rst │ ├── conf.py │ ├── imprint.rst │ ├── introduction.rst │ ├── installation.rst │ ├── quickstart.rst │ ├── background │ │ ├── attribution.rst │ │ └── contrails.rst │ ├── user_guide │ │ ├── 01_input.md │ │ └── 03_contrails.rst │ ├── terms-of-use.rst │ ├── accessibility-statement.rst │ └── bibliography.bib ├── workflows │ └── workflows.md ├── Makefile └── make.bat ├── openairclim ├── uncertainties.py ├── __about__.py ├── __init__.py ├── utils.py ├── calc_dt.py ├── construct_conc.py ├── interpolate_space.py ├── plot.py ├── calc_response.py └── calc_metric.py ├── CODE_OF_CONDUCT.md ├── .pylintrc ├── environment_minimal.yaml ├── utils ├── __init__.py ├── create_test_files.py ├── create_test_data.py └── create_time_evolution.py ├── SECURITY.md ├── example ├── run.py └── example.toml ├── .gitignore ├── environment_dev.yaml ├── .prospector.yaml ├── CITATION.cff ├── setup.py ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /img/OAC-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/img/OAC-chart.png -------------------------------------------------------------------------------- /repository/ch4_bg.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/ch4_bg.nc -------------------------------------------------------------------------------- /repository/co2_bg.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/co2_bg.nc -------------------------------------------------------------------------------- /repository/n2o_bg.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/n2o_bg.nc -------------------------------------------------------------------------------- /repository/resp_RF.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/resp_RF.nc -------------------------------------------------------------------------------- /docs/img/apply_norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/apply_norm.png -------------------------------------------------------------------------------- /repository/resp_ch4.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/resp_ch4.nc -------------------------------------------------------------------------------- /repository/resp_cont.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/resp_cont.nc -------------------------------------------------------------------------------- /docs/img/apply_scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/apply_scaling.png -------------------------------------------------------------------------------- /repository/resp_RF_O3.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/resp_RF_O3.nc -------------------------------------------------------------------------------- /repository/resp_cont_lf.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/repository/resp_cont_lf.nc -------------------------------------------------------------------------------- /tests/repository/co2_bg.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/tests/repository/co2_bg.nc -------------------------------------------------------------------------------- /docs/img/norm_inventories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/norm_inventories.png -------------------------------------------------------------------------------- /docs/img/scale_inventories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/scale_inventories.png -------------------------------------------------------------------------------- /docs/source/bibliography.rst: -------------------------------------------------------------------------------- 1 | Bibliography 2 | ============ 3 | 4 | .. bibliography:: 5 | :filter: cited -------------------------------------------------------------------------------- /docs/source/_static/OAC-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/OAC-chart.png -------------------------------------------------------------------------------- /docs/img/time-constraints_norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/time-constraints_norm.png -------------------------------------------------------------------------------- /docs/source/_static/apply_norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/apply_norm.png -------------------------------------------------------------------------------- /docs/img/time-constraints_scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/time-constraints_scaling.png -------------------------------------------------------------------------------- /docs/source/_static/apply_scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/apply_scaling.png -------------------------------------------------------------------------------- /docs/source/_static/norm_inventories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/norm_inventories.png -------------------------------------------------------------------------------- /docs/source/_static/scale_inventories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/scale_inventories.png -------------------------------------------------------------------------------- /docs/img/time-constraints_no-evolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/img/time-constraints_no-evolution.png -------------------------------------------------------------------------------- /docs/source/_static/emission-inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/emission-inventory.png -------------------------------------------------------------------------------- /openairclim/uncertainties.py: -------------------------------------------------------------------------------- 1 | """ 2 | Monte Carlo Simulation 3 | """ 4 | 5 | # TODO Add uncertainty assessment using Monte Carlo 6 | -------------------------------------------------------------------------------- /docs/source/_static/time-constraints_norm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/time-constraints_norm.png -------------------------------------------------------------------------------- /docs/source/_static/time-constraints_scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/time-constraints_scaling.png -------------------------------------------------------------------------------- /docs/source/demos/input/time_norm_historic_SSP.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/demos/input/time_norm_historic_SSP.nc -------------------------------------------------------------------------------- /docs/source/_static/time-constraints_no-evolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/_static/time-constraints_no-evolution.png -------------------------------------------------------------------------------- /docs/source/demos/input/time_scaling_linear_2019-2039.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/demos/input/time_scaling_linear_2019-2039.nc -------------------------------------------------------------------------------- /docs/source/demos/input/ELK_aviation_2019_res5deg_flat.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/demos/input/ELK_aviation_2019_res5deg_flat.nc -------------------------------------------------------------------------------- /docs/source/demos/input/ELK_aviation_2039_res5deg_flat.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlr-pa/oac/HEAD/docs/source/demos/input/ELK_aviation_2039_res5deg_flat.nc -------------------------------------------------------------------------------- /docs/workflows/workflows.md: -------------------------------------------------------------------------------- 1 | # Workflows 2 | 3 | This documentation describes relevant workflows implemented in OpenAirClim: 4 | 5 | [Time Evolution](evolution.md) -------------------------------------------------------------------------------- /docs/source/api_ref/oac.main.rst: -------------------------------------------------------------------------------- 1 | openairclim.main 2 | ---------------- 3 | 4 | .. automodule:: openairclim.main 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.plot.rst: -------------------------------------------------------------------------------- 1 | openairclim.plot 2 | ---------------- 3 | 4 | .. automodule:: openairclim.plot 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.utils.rst: -------------------------------------------------------------------------------- 1 | openairclim.utils 2 | ----------------- 3 | 4 | .. automodule:: openairclim.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We base our communication and actions on appreciation, respect, fairness, tolerance, team spirit, honesty, transparency and acceptance. 4 | -------------------------------------------------------------------------------- /docs/source/api_ref.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | :caption: Contents 9 | :glob: 10 | 11 | api_ref/* 12 | -------------------------------------------------------------------------------- /docs/source/api_ref/oac.calc_dt.rst: -------------------------------------------------------------------------------- 1 | openairclim.calc\_dt 2 | -------------------- 3 | 4 | .. automodule:: openairclim.calc_dt 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.calc_ch4.rst: -------------------------------------------------------------------------------- 1 | openairclim.calc\_ch4 2 | --------------------- 3 | 4 | .. automodule:: openairclim.calc_ch4 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.calc_co2.rst: -------------------------------------------------------------------------------- 1 | openairclim.calc\_co2 2 | --------------------- 3 | 4 | .. automodule:: openairclim.calc_co2 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.calc_cont.rst: -------------------------------------------------------------------------------- 1 | openairclim.calc\_cont 2 | ---------------------- 3 | 4 | .. automodule:: openairclim.calc_cont 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.calc_metric.rst: -------------------------------------------------------------------------------- 1 | openairclim.calc\_metric 2 | ------------------------ 3 | 4 | .. automodule:: openairclim.calc_metric 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.read_config.rst: -------------------------------------------------------------------------------- 1 | openairclim.read\_config 2 | ------------------------ 3 | 4 | .. automodule:: openairclim.read_config 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.read_netcdf.rst: -------------------------------------------------------------------------------- 1 | openairclim.read\_netcdf 2 | ------------------------ 3 | 4 | .. automodule:: openairclim.read_netcdf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.uncertainties.rst: -------------------------------------------------------------------------------- 1 | openairclim.uncertainties 2 | ------------------------- 3 | 4 | .. automodule:: openairclim.uncertainties 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.write_output.rst: -------------------------------------------------------------------------------- 1 | openairclim.write\_output 2 | ------------------------- 3 | 4 | .. automodule:: openairclim.write_output 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook='import sys; sys.path.append(".")' 3 | 4 | [MESSAGES CONTROL] 5 | # globally disable pylint checks (comma separated) 6 | # fixme suppresses TODO comments and similar 7 | disable=fixme -------------------------------------------------------------------------------- /docs/source/api_ref/oac.calc_response.rst: -------------------------------------------------------------------------------- 1 | openairclim.calc\_response 2 | -------------------------- 3 | 4 | .. automodule:: openairclim.calc_response 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.construct_cont.rst: -------------------------------------------------------------------------------- 1 | openairclim.construct\_conc 2 | --------------------------- 3 | 4 | .. automodule:: openairclim.construct_conc 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.interpolate_time.rst: -------------------------------------------------------------------------------- 1 | openairclim.interpolate\_time 2 | ----------------------------- 3 | 4 | .. automodule:: openairclim.interpolate_time 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/api_ref/oac.interpolate_space.rst: -------------------------------------------------------------------------------- 1 | openairclim.interpolate\_space 2 | ------------------------------ 3 | 4 | .. automodule:: openairclim.interpolate_space 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: -------------------------------------------------------------------------------- /environment_minimal.yaml: -------------------------------------------------------------------------------- 1 | name: oac_minimal 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - joblib 6 | - numpy 7 | - pandas 8 | - toml 9 | - matplotlib 10 | - python 11 | - cf-units 12 | - xarray 13 | - netcdf4 14 | - scipy 15 | - deepmerge 16 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # from utils.create_artificial_inventories import * # noqa: F401, F403 2 | # from utils.create_test_data import * # noqa: F401, F403 3 | # from utils.create_test_files import * # noqa: F401, F403 4 | # from utils.create_time_evolution import * # noqa: F401, F403 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover a security issue, please bring it to our attention right away! 4 | 5 | ## Reporting a Vulnerability 6 | 7 | **DO NOT** file a public issue to report a security vulberability. Instead, please send your report privately to openairclim@dlr.de. 8 | -------------------------------------------------------------------------------- /docs/source/_templates/footer.html: -------------------------------------------------------------------------------- 1 | {% extends "!footer.html" %} 2 | 3 | {% block extrafooter %} 4 |
5 | {% for label, href in footer_links %} 6 | {{ label }}
7 | {% endfor %} 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/run.py: -------------------------------------------------------------------------------- 1 | """Demonstration of OpenAirClim simulation run""" 2 | 3 | # if you have not added the oac folder to your PATH, then you also need to 4 | # import sys and append to PATH using sys.path.append(`.../oac`) 5 | import os 6 | import openairclim as oac 7 | 8 | # change directory to match current file 9 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 10 | oac.run("example.toml") 11 | -------------------------------------------------------------------------------- /docs/source/background.rst: -------------------------------------------------------------------------------- 1 | Scientific Background 2 | ===================== 3 | 4 | Here we provide some scientific background for many aspects of the OpenAirClim model. 5 | Please see the `publications `_ page for published research related to OpenAirClim. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :caption: Contents 10 | :glob: 11 | 12 | background/* 13 | 14 | 15 | -------------------------------------------------------------------------------- /openairclim/__about__.py: -------------------------------------------------------------------------------- 1 | """Meta information of OpenAirClim package""" 2 | 3 | __all__ = [ 4 | "__title__", 5 | "__version__", 6 | "__author__", 7 | "__email__", 8 | "__license__", 9 | "__copyright__", 10 | "__url__", 11 | ] 12 | 13 | 14 | __title__ = "OpenAirClim" 15 | __version__ = "0.13.0" 16 | __author__ = "OpenAirClim Team" 17 | __email__ = "openairclim@dlr.de" 18 | __license__ = "Apache 2.0" 19 | __copyright__ = f"2025, {__author__}" 20 | __url__ = "https://github.com/dlr-pa/oac" 21 | -------------------------------------------------------------------------------- /docs/source/demos.rst: -------------------------------------------------------------------------------- 1 | Demonstrations 2 | ============== 3 | 4 | Here we provide some demonstrations of OpenAirClim. 5 | These demonstrations were tested with oac v0.11.1. 6 | 7 | .. note:: 8 | 9 | Before running the demonstrations by yourself, make sure that the file paths, 10 | defined in the code blocks as well as those paths defined in the configuration files, 11 | have been changed according to your settings. 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | :caption: Contents 16 | :glob: 17 | 18 | demos/01_norm/* 19 | demos/02_scaling/* 20 | demos/03_multi_inv/* -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Type of issue 11 | - [x] feature request 12 | 13 | ## Description 14 | Please provide a description of the feature you are requesting. Please at least answer the following in your description: 15 | - What is the motivation/use case for this new feature? 16 | - Is the feature request related to a problem? If so, please describe. 17 | - Would you be able to help out? ♥️ Would you have the time and skills to implement the solution yourself? 18 | -------------------------------------------------------------------------------- /tests/calc_metric_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module calc_metric 3 | """ 4 | 5 | import numpy as np 6 | import openairclim as oac 7 | 8 | 9 | def test_get_metrics_dict_simple(): 10 | """Simple case with only one species and time_metrics subset of time_range""" 11 | config = {"time": {"range": [2000, 2020, 1]}} 12 | t_zero = 2000 13 | horizon = 10 14 | resp_dict = {"spec": np.arange(0, 2.0, 0.1)} 15 | expected_output = {"spec": np.arange(0, 1.0, 0.1)} 16 | computed_output = oac.get_metrics_dict(config, t_zero, horizon, resp_dict) 17 | np.testing.assert_equal(expected_output, computed_output) 18 | -------------------------------------------------------------------------------- /docs/source/publications.rst: -------------------------------------------------------------------------------- 1 | Publications 2 | ============ 3 | 4 | .. image:: https://zenodo.org/badge/851165490.svg 5 | :target: https://zenodo.org/doi/10.5281/zenodo.13682728 6 | 7 | 8 | If you make use of OpenAirClim in your research and use it in academic work, you may cite it as: 9 | 10 | .. code:: bibtex 11 | 12 | @article{voelk2026oac, 13 | author={Stefan Völk and Hiroshi Yamashita and Liam Megill and Katrin Dahlmann and Volker Grewe}, 14 | journal={TBD}, 15 | title={OpenAirClim v0.11.0: A framework for assessing the climate impact of aviation emissions}, 16 | year={2026}, 17 | volume={TBD}, 18 | pages={TBD}, 19 | doi={TBD}, 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/calc_dt_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module calc_dt 3 | """ 4 | 5 | import numpy as np 6 | import openairclim as oac 7 | 8 | 9 | class TestCalcDtempBr2008Co2: 10 | """Tests function calc_dtemp_br2008_co2(config, rf_arr)""" 11 | 12 | def test_zero_rf(self): 13 | """RF array with zeros results in temperature arrays with zeros""" 14 | config = { 15 | "time": {"range": [2000, 2100, 1]}, 16 | "temperature": {"CO2": {"lambda": 1.0}}, 17 | } 18 | spec = "CO2" 19 | rf_arr = np.zeros(100) 20 | expected_result = np.zeros(100) 21 | np.testing.assert_array_equal( 22 | oac.calc_dtemp_br2008(config, spec, rf_arr), expected_result 23 | ) 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Type of issue 11 | - [x] bug report 12 | 13 | ## Description 14 | Please provide the steps to reproduce and if possible a minimal demo of the problem. Please at least answer the following: 15 | - What is the current behaviour? 16 | - What is the expected behaviour? 17 | - Please tell us about your environment: 18 | - oac version: 19 | - Operating system: 20 | - Other information (e.g. detailed explanation, related issues, suggestions how to fix, links for us to have context) 21 | - Would you be able to help out? ♥️ Would you have the time and skills to implement a fix yourself? 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore ALL .log files 2 | *.log 3 | *LogFile 4 | 5 | # ignore cache directories 6 | .pytest_cache/ 7 | .mypy_cache/ 8 | __pycache__/ 9 | # ignore OpenAirClim cache 10 | /cache/ 11 | 12 | # ignore Jupyter Notebooks 13 | /ipynb/ 14 | 15 | # ignore repositories and input data 16 | example/input/ 17 | docs/source/demos/input/emi_inv_*.nc 18 | docs/source/demos/input/*.txt 19 | repository/h2/SSP_scenarios/*.* 20 | tests/repository/*.* 21 | tests/repository/cache 22 | # exceptions 23 | !tests/repository/co2_bg.nc 24 | 25 | # ignore local settings 26 | .vscode/ 27 | .coverage 28 | .continue/ 29 | pyproject.toml 30 | 31 | # ignore autogenerated files 32 | docs/build/ 33 | tests/coverage/ 34 | # distribution files 35 | build/ 36 | dist/ 37 | *.egg-info/ 38 | # ignore output 39 | results/ 40 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-demo.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Demo 2 | run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 3 | on: [push] 4 | jobs: 5 | Explore-GitHub-Actions: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: ["ubuntu-latest"] 10 | python-version: ["3.11.5"] 11 | steps: 12 | - name: Setup conda environment 13 | uses: conda-incubator/setup-miniconda@v3 14 | with: 15 | auto-update-conda: false 16 | python-version: ${{ matrix.python-version }} 17 | miniforge-version: latest 18 | - name: Conda info 19 | run: | 20 | conda info 21 | conda list 22 | - name: Set sys.path 23 | run: | 24 | python -c "import sys, os; sys.path.append(os.getcwd()); print(sys.path)" 25 | -------------------------------------------------------------------------------- /environment_dev.yaml: -------------------------------------------------------------------------------- 1 | name: oac 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - joblib 6 | - platformdirs 7 | - black 8 | - lazy-object-proxy 9 | - numpy 10 | - pandas 11 | - jupyterlab 12 | - openssl 13 | - sphinx 14 | - jupyter_sphinx 15 | - sphinx_rtd_theme 16 | - myst-parser 17 | - sphinxcontrib-mermaid 18 | - sphinxcontrib-bibtex 19 | - toml 20 | - ipykernel 21 | - scikit-learn 22 | - matplotlib 23 | - netcdf4 24 | - python 25 | - pytest-cov 26 | - prospector 27 | - cf-units 28 | - xarray 29 | - pytest-httpserver 30 | - ca-certificates 31 | - certifi 32 | - pylint 33 | - wrapt 34 | - mypy 35 | - beautifulsoup4 36 | - bottleneck 37 | - cartopy 38 | - pytest 39 | - pyroma 40 | - isort 41 | - scipy 42 | - ipympl 43 | - openpyxl 44 | - deepmerge 45 | - zenodo_get 46 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | # prospector configuration file 2 | 3 | 4 | #output-format: grouped 5 | output-format: vscode 6 | 7 | #strictness: veryhigh 8 | strictness: high 9 | doc-warnings: false 10 | test-warnings: true 11 | member-warnings: false 12 | 13 | ignore-paths: 14 | - docs 15 | - ipynb 16 | 17 | pyroma: 18 | run: true 19 | 20 | pep8: 21 | full: true 22 | 23 | mypy: 24 | run: true 25 | options: 26 | ignore-missing-imports: true 27 | 28 | #pycodestyle: 29 | # options: 30 | # max-doc-length: 89 31 | 32 | # Configure docstring format and disable some warnings 33 | # see https://pydocstyle.readthedocs.io/en/latest/error_codes.html 34 | # There are three conventions that may be used by pydocstyle: pep257, numpy and google. 35 | 36 | pydocstyle: 37 | disable: ['D203', 'D204', 'D213', 'D215', 'D400', 'D401', 'D404', 'D406', 'D407', 'D408', 'D409', 'D413'] 38 | -------------------------------------------------------------------------------- /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/contact_support.rst: -------------------------------------------------------------------------------- 1 | Contact and Support 2 | =================== 3 | 4 | 5 | Contact 6 | ------- 7 | 8 | Please use the `discussions tab `_ on GitHub for general questions or requests for help with your specific project. 9 | GitHub issues are reserved for bug reporting and feature requests. For everything else, send a message to openairclim@dlr.de 10 | 11 | 12 | Support 13 | ------- 14 | 15 | OpenAirClim is a joint, open-source effort. We greatly value contributions of any kind! 16 | Before submitting issues and pull requests, please familiarise yourself with our `contributing guidelines `_. 17 | 18 | 19 | Funding 20 | ------- 21 | 22 | The development of OpenAirClim has been financially supported by Airbus, the German Aerospace Center (DLR) 23 | and the German Federal Ministry for Economic Affairs and Energy (BMWK) under the Aviation Research Program LuFo VI-2 (projects DINA2030+ and 328H2FC). 24 | -------------------------------------------------------------------------------- /docs/source/governance.rst: -------------------------------------------------------------------------------- 1 | Governance 2 | ========== 3 | 4 | The development of OpenAirClim is overseen by a Steering Committee. 5 | The Steering Committee is responsible for defining high-level roadmaps, defining the core development team, deciding on partners and agreeing on governance procedures. 6 | Contributions to the OpenAirClim code base are overseen by the Scientific and Technical Boards. 7 | The Scientific Board is responsible for deciding on model extensions, new processes and verification approaches. 8 | The Technical Board decides on versioning, releases, data structures and code styles. 9 | Engagement from users and the industry is obtained at user meetings and workshops as well as through discussions on GitHub. 10 | 11 | The OpenAirClim Core Development Group consists of members from the following organisations: 12 | 13 | | Deutsches Zentrum für Luft- und Raumfahrt, Institute of Atmospheric Physics (DLR-PA) 14 | | Deutsches Zentrum für Luft- und Raumfahrt, Institute of Air Transport (DLR-LV) 15 | | Chalmers University of Technology 16 | | Delft University of Technology (TU Delft) 17 | | Royal Netherlands Aerospace Center (NLR) 18 | 19 | If you are interested in becoming part of the core development team, feel free to reach out at openairclim@dlr.de. -------------------------------------------------------------------------------- /tests/interpolate_space_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module interpolate_space 3 | """ 4 | 5 | import os 6 | import xarray as xr 7 | import pytest 8 | import openairclim as oac 9 | 10 | abspath = os.path.abspath(__file__) 11 | dname = os.path.dirname(abspath) 12 | os.chdir(dname) 13 | 14 | # CONSTANTS 15 | REPO_PATH = "repository/" 16 | INV_NAME = "test_inv.nc" 17 | RESP_NAME = "test_resp.nc" 18 | 19 | 20 | @pytest.fixture(name="setup_arguments", scope="class") 21 | def fixture_setup_arguments(): 22 | """Setup arguments for calc_weights(spec, resp, inv) 23 | 24 | Returns: 25 | str, xr.Dataset, xr.Dataset: species name, response, emission inventory 26 | """ 27 | spec = "H2O" 28 | resp = xr.load_dataset(REPO_PATH + RESP_NAME) 29 | inv = xr.load_dataset(REPO_PATH + INV_NAME) 30 | return spec, resp, inv 31 | 32 | 33 | @pytest.mark.usefixtures("setup_arguments") 34 | class TestCalcWeights: 35 | """Tests function calc_weights(spec, resp, inv)""" 36 | 37 | def test_correct_input(self, setup_arguments): 38 | """Valid input returns xr.Dataset with non-empty weights Data Variable""" 39 | spec, resp, inv = setup_arguments 40 | output = oac.calc_weights(spec, resp, inv) 41 | assert isinstance(output, xr.Dataset) 42 | assert output["weights"].values.size 43 | -------------------------------------------------------------------------------- /.github/workflows/install_and_test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will setup conda, create a virtual environment with required packages and dependencies, 2 | # execute script utils/create_test_files.py, 3 | # and run tests with pytest 4 | 5 | name: Install and run tests 6 | 7 | on: 8 | push: 9 | branches: [ "main", "dev" ] 10 | pull_request: 11 | branches: [ "main", "dev" ] 12 | 13 | #permissions: 14 | # contents: read 15 | 16 | jobs: 17 | build: 18 | name: Install and test on (${{ matrix.os }}, Miniforge) 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | os: ["ubuntu-latest"] 23 | python-version: ["3.11.5"] 24 | defaults: 25 | run: 26 | shell: bash -el {0} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Setup conda environment 30 | uses: conda-incubator/setup-miniconda@v3 31 | with: 32 | auto-update-conda: false 33 | python-version: ${{ matrix.python-version }} 34 | environment-file: environment_dev.yaml 35 | activate-environment: oac 36 | miniforge-version: latest 37 | - name: Conda info 38 | run: | 39 | conda info 40 | conda list 41 | - name: Create test files 42 | run: | 43 | cd utils/ 44 | chmod a+x create_test_files.py 45 | python create_test_files.py 46 | cd .. 47 | - name: Test with pytest 48 | run: | 49 | cd tests/ 50 | pytest 51 | -------------------------------------------------------------------------------- /openairclim/__init__.py: -------------------------------------------------------------------------------- 1 | """ Through __init__.py openairclim is recognized as a Python package. 2 | 3 | Objects defined within the submodules are made available 4 | to the user by "import openairclim". 5 | """ 6 | 7 | # from os.path import dirname, abspath 8 | from openairclim.__about__ import * # noqa: F401, F403 9 | from openairclim.main import * # noqa: F401, F403 10 | from openairclim.read_config import * # noqa: F401, F403 11 | from openairclim.read_netcdf import * # noqa: F401, F403 12 | from openairclim.construct_conc import * # noqa: F401, F403 13 | from openairclim.interpolate_space import * # noqa: F401, F403 14 | from openairclim.interpolate_time import * # noqa: F401, F403 15 | from openairclim.calc_response import * # noqa: F401, F403 16 | from openairclim.calc_co2 import * # noqa: F401, F403 17 | from openairclim.calc_ch4 import * # noqa: F401, F403 18 | from openairclim.calc_cont import * # noqa: F401, F403 19 | from openairclim.calc_dt import * # noqa: F401, F403 20 | from openairclim.calc_metric import * # noqa: F401, F403 21 | from openairclim.uncertainties import * # noqa: F401, F403 22 | from openairclim.utils import * # noqa: F401, F403 23 | from openairclim.plot import * # noqa: F401, F403 24 | from openairclim.write_output import * # noqa: F401, F403 25 | from openairclim.attribution import * # noqa: F401, F403 26 | 27 | # __all__ = ['read_config', 'read_inventories'] 28 | # ROOT_DIR = dirname(abspath(__file__)) 29 | # Logging initialisation code would go here # 30 | -------------------------------------------------------------------------------- /tests/calc_response_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module calc_response 3 | """ 4 | 5 | import os 6 | import numpy as np 7 | import xarray as xr 8 | import pytest 9 | import openairclim as oac 10 | 11 | abspath = os.path.abspath(__file__) 12 | dname = os.path.dirname(abspath) 13 | os.chdir(dname) 14 | 15 | # CONSTANTS 16 | REPO_PATH = "repository/" 17 | INV_NAME = "test_inv.nc" 18 | 19 | 20 | @pytest.fixture(name="setup_arguments", scope="class") 21 | def fixture_setup_arguments(): 22 | """Setup arguments for calc_resp 23 | 24 | Returns: 25 | str, xr.Dataset, xr.Dataset: species name, emission inventory, weights 26 | """ 27 | spec = "H2O" 28 | file_path = REPO_PATH + INV_NAME 29 | inv = xr.load_dataset(file_path) 30 | weights = xr.Dataset( 31 | data_vars={ 32 | "lat": inv.lat, 33 | "plev": inv.plev, 34 | "weights": ( 35 | ["index"], 36 | np.ones(len(inv.lat.values)), 37 | { 38 | "long_name": "weights", 39 | }, 40 | ), 41 | } 42 | ) 43 | return spec, inv, weights 44 | 45 | 46 | @pytest.mark.usefixtures("setup_arguments") 47 | class TestCalcResp: 48 | """Tests function calc_resp(spec, inv, weights)""" 49 | 50 | def test_correct_input(self, setup_arguments): 51 | """Valid input returns float value""" 52 | spec, inv, weights = setup_arguments 53 | output = oac.calc_resp(spec, inv, weights) 54 | # Check the result 55 | assert isinstance(output, float) 56 | -------------------------------------------------------------------------------- /docs/source/user_guide.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | Here you can find some useful documentation of the OpenAirClim workflows, modules and data processed. 5 | We are actively working on this guide. 6 | 7 | .. contents:: Contents 8 | :local: 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | :glob: 13 | 14 | user_guide/* 15 | 16 | 17 | General workflow 18 | ---------------- 19 | 20 | In the following flowchart, the general OpenAirClim workflow is depicted. 21 | Input files are shown in yellow, built-in data bases in grey, the OpenAirClim process in blue and output files in green. 22 | 23 | .. mermaid:: 24 | 25 | --- 26 | config: 27 | look: handDrawn 28 | theme: neutral 29 | --- 30 | flowchart LR 31 | classDef input fill:#FFFAA0 32 | classDef builtin fill:#D3D3D3 33 | classDef process fill:#0096FF 34 | classDef output fill:#32CD32 35 | CONFIG[/Configuration/]:::input 36 | INV[/Emission
inventories/]:::input 37 | EVO[/Time evolution/]:::input 38 | RESP[(Response
surfaces)]:::builtin 39 | BG[(Background
inventories)]:::builtin 40 | OAC[oac]:::process 41 | TS[/"Time series
(emis, conc, RF, dT)"/]:::output 42 | METR[/"Climate metrics
(AGTP, AGWP, ATR)"/]:::output 43 | DIAG[/Diagnostics/]:::output 44 | PLT[/Plots/]:::output 45 | CONFIG --> OAC 46 | INV --> OAC 47 | EVO -.-> OAC 48 | RESP --> OAC 49 | BG --> OAC 50 | OAC --> TS 51 | OAC --> METR 52 | OAC --> DIAG 53 | OAC --> PLT 54 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build-docs: 15 | runs-on: ubuntu-latest 16 | defaults: 17 | run: 18 | shell: bash -el {0} 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Conda 25 | uses: conda-incubator/setup-miniconda@v3 26 | with: 27 | auto-update-conda: false 28 | activate-environment: oac 29 | environment-file: ./environment_dev.yaml 30 | miniforge-version: latest 31 | 32 | - name: Verify environment 33 | run: | 34 | conda info 35 | conda list 36 | 37 | - name: Install Sphinx and doc dependencies 38 | run: | 39 | conda activate oac 40 | pip install -e . 41 | 42 | - name: Build Sphinx documentation 43 | run: | 44 | conda activate oac 45 | cd docs 46 | make html 47 | 48 | - name: Add CNAME file 49 | run: echo "openairclim.org" > ./docs/build/html/CNAME 50 | 51 | - name: Deploy to GitHub Pages 52 | uses: peaceiris/actions-gh-pages@v4 53 | if: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} 54 | with: 55 | github_token: ${{ secrets.GITHUB_TOKEN }} 56 | publish_branch: gh-pages 57 | publish_dir: ./docs/build/html 58 | force_orphan: true 59 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Before you get started 2 | - [ ] 🙋‍♀️ Create an issue to discuss what you are going to do! 3 | 4 | --- 5 | 6 | ## Description 7 | - Closes #issue_number 8 | 9 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. 10 | 11 | ## Type of change 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] New feature (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 15 | - [ ] Documentation change 16 | 17 | ## How has this been tested? 18 | Please describe the software tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | **Test configuration**: 24 | * Operating system: 25 | * Other relevant information: 26 | 27 | ## Checklist 28 | - [ ] My code follows the style guidelines of this project 29 | - [ ] I have performed a self-review of my code 30 | - [ ] I have commented my code, particularly in hard-to-understand areas 31 | - [ ] I have made corresponding changes to the documentation 32 | - [ ] My changes generate no new warnings 33 | - [ ] I have added tests that prove my fix is effective or that my feature works 34 | - [ ] New and existing unit tests pass locally with my changes 35 | - [ ] Any changed dependencies have been added or removed correctly 36 | - [ ] main branch: I have updated the CHANGELOG 37 | - [ ] main branch: I have updated the version numbering in `__about__.py` according to the [semantic versioning scheme](https://semver.org/) 38 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | OpenAirClim Documentation 2 | ========================= 3 | 4 | Welcome to the OpenAirClim documentation! 5 | 6 | OpenAirClim is a model for simplified evaluation of the approximate chemistry-climate impact of air traffic emissions. 7 | The model represents the major responses of the atmosphere to emissions in terms of composition and climate change. 8 | Development is being led by the German Aerospace Center (Deutsches Zentrum für Luft- und Raumfahrt, DLR) and includes various research and industry partners. 9 | The objective of OpenAirClim is to provide an open-source, standardised and computationally inexpensive method to analyse the climate impact of existing as well as future aircraft, in particular for company and (climate) policy decision-making as well as aircraft design optimisation. 10 | Typical research questions that can be answered by using OpenAirClim relate to: 11 | 12 | - fleet-wide scenarios, e.g. the introduction of a new aircraft type; 13 | - aviation industry scenarios, e.g. the introduction of a new fuel type; and 14 | - operational procedures, e.g. intermediate stop operations or flying lower/slower 15 | 16 | This website provides documentation and examples to help new users get started. 17 | If you need support or would like to get in touch, contact information is available `here `_. 18 | 19 | The source code can be found on `Github `__. 20 | 21 | 22 | .. toctree:: 23 | :maxdepth: 1 24 | :caption: Contents 25 | 26 | introduction 27 | installation 28 | quickstart 29 | user_guide 30 | demos 31 | background 32 | publications 33 | api_ref 34 | governance 35 | contact_support 36 | bibliography 37 | 38 | 39 | .. toctree:: 40 | :hidden: 41 | 42 | imprint 43 | accessibility-statement 44 | privacy-policy 45 | terms-of-use 46 | -------------------------------------------------------------------------------- /tests/calc_co2_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module calc_co2 3 | """ 4 | 5 | import numpy as np 6 | import pytest 7 | import openairclim as oac 8 | 9 | 10 | class TestCalcCo2Concentration: 11 | """Tests function calc_co2_concentration(config, emis_dict)""" 12 | 13 | def test_invalid_method(self): 14 | """Invalid method returns ValueError""" 15 | config = {"responses": {"CO2": {"conc": {"method": "InvalidMethod"}}}} 16 | emis_dict = { 17 | "CO2": np.array( 18 | [1000.0, 2000.0, 3000.0] 19 | ) # Example emissions in Tg 20 | } 21 | with pytest.raises(ValueError): 22 | oac.calc_co2_concentration(config, emis_dict) 23 | 24 | 25 | class TestCalcCo2Ss: 26 | """Tests function calc_co2_ss(config, emis_dict)""" 27 | 28 | def test_zero_emissions(self): 29 | """Zero CO2 emissions return zero concentration changes""" 30 | config = {"time": {"range": [2000, 2010, 1]}} 31 | emis_dict = {"CO2": np.zeros(10)} 32 | result = oac.calc_co2_ss(config, emis_dict) 33 | expected = {"CO2": np.zeros(10)} 34 | np.testing.assert_array_equal(result["CO2"], expected["CO2"]) 35 | 36 | 37 | class TestCalcCo2Rf: 38 | """Tests function calc_co2_rf(config, conc_dict, conc_co2_bg_dict)""" 39 | 40 | def test_invalid_method(self): 41 | """Invalid method returns ValueError""" 42 | config = {"responses": {"CO2": {"rf": {"method": "invalid_method"}}}} 43 | conc_dict = {"CO2": np.array([1.0, 2.0, 3.0])} 44 | with pytest.raises(ValueError): 45 | oac.calc_co2_rf(conc_dict, config) 46 | 47 | def test_empty_conc_dict(self): 48 | """Empty concentration dictionary returns KeyError""" 49 | config = {"responses": {"CO2": {"rf": {"method": "IPCC_2001_1"}}}} 50 | conc_dict = {} 51 | with pytest.raises(KeyError): 52 | oac.calc_co2_rf(conc_dict, config) 53 | -------------------------------------------------------------------------------- /tests/write_output_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module write_output 3 | """ 4 | 5 | import numpy as np 6 | import xarray as xr 7 | import pytest 8 | import openairclim as oac 9 | 10 | # CONSTANTS 11 | REPO_PATH = "repository/" 12 | CACHE_PATH = "repository/cache/weights/" 13 | INV_NAME = "test_inv.nc" 14 | RESP_NAME = "test_resp.nc" 15 | CACHE_NAME = "000.nc" 16 | 17 | 18 | class TestWriteOutputDictToNetcdf: 19 | """Tests function write_output_dict_to_netcdf(config, output_dict).""" 20 | 21 | @pytest.fixture 22 | def mock_save(self, monkeypatch): 23 | """Prevents .to_netcdf() from writing to file.""" 24 | monkeypatch.setattr( 25 | xr.Dataset, "to_netcdf", 26 | lambda self, *args, **kwargs: None 27 | ) 28 | 29 | @pytest.fixture 30 | def config(self, tmp_path): 31 | """Fixture to create a valid config.""" 32 | return { 33 | "output": { 34 | "dir": str(tmp_path) + "/", 35 | "name": "test_output" 36 | }, 37 | "time": {"range": [2000, 2020, 1]}, 38 | "aircraft": {"types": ["LR", "REG"]}, 39 | } 40 | 41 | @pytest.fixture 42 | def output_dict(self): 43 | """Fixture to create a valid output_dict.""" 44 | time_len = 20 45 | return { 46 | "LR": { 47 | "RF_CO2": np.full(time_len, 1), 48 | "RF_CH4": np.full(time_len, 2), 49 | }, 50 | "REG": { 51 | "RF_CO2": np.full(time_len, 3), 52 | "RF_CH4": np.full(time_len, 4), 53 | } 54 | } 55 | 56 | @pytest.mark.usefixtures("mock_save") 57 | def test_valid_write(self, config, output_dict): 58 | """Tests valid config and dictionary.""" 59 | ds = oac.write_output_dict_to_netcdf(config, output_dict) 60 | assert isinstance(ds, xr.Dataset) 61 | assert "RF_CO2" in ds 62 | assert "RF_CH4" in ds 63 | assert "ac" in ds.dims 64 | assert "time" in ds.dims 65 | assert ds.dims["ac"] == 2 66 | assert ds.dims["time"] == 20 67 | -------------------------------------------------------------------------------- /tests/calc_ch4_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module calc_ch4 3 | """ 4 | 5 | import numpy as np 6 | import xarray as xr 7 | import pytest 8 | import openairclim as oac 9 | 10 | 11 | class TestCalcCh4Rf: 12 | """Tests function calc_ch4_rf(config, conc_dict, conc_ch4_bg_dict, conc_no2_bg_dict)""" 13 | 14 | def test_invalid_method(self): 15 | """Invalid method returns ValueError""" 16 | config = {"responses": {"CH4": {"rf": {"method": "invalid_method"}}}} 17 | conc_dict = {"CH4": np.array([1.0, 2.0, 3.0])} 18 | with pytest.raises(ValueError): 19 | oac.calc_ch4_rf(conc_dict, config) 20 | 21 | def test_empty_conc_dict(self): 22 | """Empty concentration dictionary returns KeyError""" 23 | config = {"responses": {"CO2": {"rf": {"method": "Etminan_2016"}}}} 24 | conc_dict = {} 25 | with pytest.raises(KeyError): 26 | oac.calc_ch4_rf(conc_dict, config) 27 | 28 | 29 | @pytest.fixture(name="create_rf_dict", scope="class") 30 | def fixture_load_inv(): 31 | """Create example dictionary with computed RF values 32 | 33 | Returns: 34 | dict: Dictionary of xarray DataArray, key are species 35 | """ 36 | rf_dict = { 37 | "CH4": xr.DataArray( 38 | data=np.array([1.0, 1.0, 1.0]), 39 | coords={"time": np.array([2020, 2030, 2040])}, 40 | dims=["time"], 41 | name="RF_CH4", 42 | ) 43 | } 44 | return rf_dict 45 | 46 | 47 | class TestCalcPmoRF: 48 | """Tests function calc_pmo_rf(rf_dict)""" 49 | 50 | def test_valid_input(self): 51 | """Valid input (dictionary of xr.DataArray) returns expected dictionary""" 52 | out_dict = {"RF_CH4": np.array([1.0, 1.0, 1.0])} 53 | expected_dict = {"PMO": np.array([0.29, 0.29, 0.29])} 54 | np.testing.assert_array_almost_equal( 55 | expected_dict["PMO"], oac.calc_pmo_rf(out_dict)["PMO"] 56 | ) 57 | 58 | def test_missing_ch4(self): 59 | """out_dict without CH4 returns KeyError""" 60 | out_dict = {} 61 | with pytest.raises(KeyError): 62 | oac.calc_pmo_rf(out_dict) 63 | -------------------------------------------------------------------------------- /tests/construct_conc_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module construct_conc 3 | """ 4 | 5 | import os 6 | import numpy as np 7 | import xarray as xr 8 | import pytest 9 | import openairclim as oac 10 | 11 | abspath = os.path.abspath(__file__) 12 | dname = os.path.dirname(abspath) 13 | os.chdir(dname) 14 | 15 | # CONSTANTS 16 | REPO_PATH = "repository/" 17 | INV_NAME = "test_inv.nc" 18 | 19 | 20 | @pytest.fixture(name="load_inv", scope="class") 21 | def fixture_load_inv(): 22 | """Load example emission inventory and reuse xarray in multiple tests 23 | 24 | Returns: 25 | dict: Dictionary of xarray, key is inventory years 26 | """ 27 | file_path = REPO_PATH + INV_NAME 28 | inv = xr.load_dataset(file_path) 29 | inv_dict = {2020: inv} 30 | return inv_dict 31 | 32 | 33 | @pytest.mark.usefixtures("load_inv") 34 | class TestCalcInvSums: 35 | """Tests function calc_inv_sums(spec, inv_dict)""" 36 | 37 | def test_correct_input(self, load_inv): 38 | """Correct species name and inventory inputs returns array of sums""" 39 | inv_dict = load_inv 40 | _inv_years, inv_sums = oac.calc_inv_sums("CO2", inv_dict) 41 | assert isinstance(inv_sums, np.ndarray) 42 | 43 | def test_incorrect_input(self, load_inv): 44 | """Incorrect species name returns KeyError""" 45 | inv_dict = load_inv 46 | with pytest.raises(KeyError): 47 | oac.calc_inv_sums("not-existing-species", inv_dict) 48 | 49 | 50 | @pytest.mark.usefixtures("load_inv") 51 | class TestCheckInvValues: 52 | """Tests function check_inv_values(inv, year, spec)""" 53 | 54 | def test_negative_emissions(self, load_inv): 55 | """Load dictionary of emission inventory with positive emissions""" 56 | inv_dict = load_inv 57 | year = 2020 58 | spec = "CO2" 59 | inv = inv_dict[year] 60 | inv_arr = inv[spec].values 61 | # Convert first element of CO2 inventory array into negative emission 62 | inv_arr[0] = -inv_arr[0] 63 | inv[spec].values = inv_arr 64 | with pytest.raises(ValueError): 65 | oac.check_inv_values(inv, year, spec) 66 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # This CITATION.cff file was generated with cffinit. 2 | # Visit https://bit.ly/cffinit to generate yours today! 3 | 4 | cff-version: 1.2.0 5 | title: OpenAirClim 6 | message: >- 7 | If you use this software, please cite it using the 8 | metadata from this file. 9 | type: software 10 | authors: 11 | - given-names: Stefan 12 | family-names: Völk 13 | email: Stefan.Voelk@dlr.de 14 | orcid: 'https://orcid.org/0000-0001-9720-6504' 15 | affiliation: German Aerospace Center (DLR) 16 | - orcid: 'https://orcid.org/0000-0003-2458-1826' 17 | given-names: Hiroshi 18 | family-names: Yamashita 19 | affiliation: German Aerospace Center (DLR) 20 | email: Hiroshi.Yamashita@dlr.de 21 | - given-names: Liam 22 | family-names: Megill 23 | email: Liam.Megill@dlr.de 24 | affiliation: German Aerospace Center (DLR) 25 | orcid: 'https://orcid.org/0000-0002-4199-6962' 26 | - given-names: Katrin 27 | family-names: Dahlmann 28 | email: Katrin.Dahlmann@dlr.de 29 | orcid: 'https://orcid.org/0000-0003-3198-1713' 30 | affiliation: German Aerospace Center (DLR) 31 | - given-names: Volker 32 | family-names: Grewe 33 | email: Volker.Grewe@dlr.de 34 | affiliation: German Aerospace Center (DLR) 35 | orcid: 'https://orcid.org/0000-0002-8012-6783' 36 | abstract: >- 37 | OpenAirClim is a model for simplified evaluation of the 38 | approximate chemistry-climate impact of air traffic 39 | emissions. The model represents the major responses of the 40 | atmosphere to emissions in terms of composition and 41 | climate change. Instead of applying time-consuming 42 | climate-chemistry models, a response model is developed 43 | and applied which reproduces the response of a 44 | climate-chemistry model without actually calculating ab 45 | initio all the physical and chemical effects. The 46 | responses are non-linear relations between localized 47 | emissions and Radiative Forcing and further climate 48 | indicators. These response surfaces are contained within 49 | look-up tables. 50 | keywords: 51 | - climate impact 52 | - aviation 53 | - response modelling 54 | - assessment 55 | license: Apache-2.0 56 | version: 2.8.3 57 | date-released: '2024-08-05' 58 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Get meta information of OpenAirClim package 2 | 3 | import os 4 | import sys 5 | import locale 6 | 7 | locale.setlocale(locale.LC_ALL, "C") 8 | 9 | sys.path.insert(0, os.path.abspath("../..")) 10 | import openairclim as oac 11 | 12 | # Project information 13 | 14 | project = oac.__title__ 15 | copyright = f"%Y, {oac.__author__}" 16 | author = oac.__author__ 17 | release = oac.__version__ 18 | 19 | # General configuration 20 | 21 | extensions = [ 22 | "sphinx.ext.autodoc", 23 | "sphinx.ext.viewcode", 24 | "sphinx.ext.todo", 25 | "sphinx.ext.napoleon", 26 | "sphinx.ext.autosummary", 27 | "sphinx.ext.intersphinx", 28 | "jupyter_sphinx", 29 | "myst_parser", 30 | "sphinxcontrib.mermaid", 31 | "sphinxcontrib.bibtex", 32 | ] 33 | 34 | autodoc_default_options = { 35 | "members": True, 36 | "undoc-members": True, 37 | "show-inheritance": True, 38 | } 39 | 40 | # allow both rst and md files 41 | source_suffix = { 42 | ".rst": "restructuredtext", 43 | ".md": "markdown", 44 | } 45 | 46 | myst_enable_extensions = ["colon_fence"] 47 | 48 | # bibtex options 49 | bibtex_bibfiles = ["bibliography.bib"] 50 | bibtex_default_style = "plain" 51 | 52 | # autosummary_generate = True # Turn on sphinx.ext.autosummary 53 | autodoc_mock_imports = ["cf_units"] 54 | 55 | templates_path = ["_templates"] 56 | exclude_patterns = [] 57 | 58 | 59 | # Options for HTML output 60 | 61 | html_theme = "sphinx_rtd_theme" 62 | html_static_path = ["_static"] 63 | html_theme_options = { 64 | "style_external_links": False, 65 | "includehidden": False, 66 | "version_selector": True, 67 | } 68 | html_context = { # footer 69 | "footer_links": [ 70 | ("Imprint", "imprint.html"), 71 | ("Privacy Policy", "privacy-policy.html"), 72 | ("Terms of Use", "terms-of-use.html"), 73 | ("Accessibility Statement", "accessibility-statement.html"), 74 | ] 75 | } 76 | 77 | 78 | # Other options 79 | 80 | intersphinx_mapping = { 81 | "cartopy": ("https://scitools.org.uk/cartopy/docs/latest", None), 82 | "gedai": ("https://liammegill.github.io/gedai", None), 83 | "numpy": ("https://numpy.org/doc/stable/", None), 84 | "pandas": ("https://pandas.pydata.org/docs/", None), 85 | "pytest": ("https://docs.pytest.org/en/stable/", None), 86 | "python": ("https://docs.python.org/3", None), 87 | "xarray": ("https://docs.xarray.dev/en/stable/", None), 88 | } 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Setup file for project OpenAirClim 2 | 3 | 4 | import os 5 | from setuptools import setup 6 | 7 | about = {} 8 | with open("openairclim/__about__.py", mode="r", encoding="utf8") as fp: 9 | exec(fp.read(), about) 10 | 11 | 12 | def read(fname): 13 | """Utility function for reading the README file 14 | 15 | Args: 16 | fname (str): file name 17 | 18 | Returns: 19 | str: content of the file 20 | """ 21 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 22 | 23 | 24 | setup( 25 | name="openairclim", 26 | version=about["__version__"], 27 | author=about["__author__"], 28 | author_email=about["__email__"], 29 | description=("This setup.py file installs OpenAirClim."), 30 | keywords=["climate", "aviation"], 31 | license=about["__license__"], 32 | url=about["__url__"], 33 | packages=["openairclim"], 34 | long_description=read("README.md"), 35 | classifiers=[ 36 | "Topic :: Scientific/Engineering :: Atmospheric Science", 37 | "Programming Language :: Python :: 3 :: Only", 38 | "Development Status :: 3 - Alpha", 39 | ], 40 | python_requires=">=3.11.5", 41 | install_requires=[ 42 | "setuptools", 43 | "joblib", 44 | "numpy", 45 | "pandas", 46 | "toml", 47 | "matplotlib", 48 | "cf-units", 49 | "xarray", 50 | "netcdf4", 51 | "scipy", 52 | "deepmerge", 53 | ], 54 | extras_require={ 55 | "dev": [ 56 | "platformdirs", 57 | "black", 58 | "lazy-object-proxy", 59 | "jupyterlab", 60 | "openssl", 61 | "sphinx", 62 | "jupyter_sphinx", 63 | "sphinx_rtd_theme", 64 | "myst-parser", 65 | "sphinxcontrib-mermaid", 66 | "sphinxcontrib-bibtex", 67 | "ipykernel", 68 | "scikit-learn", 69 | "pytest-cov", 70 | "prospector", 71 | "pytest-httpserver", 72 | "ca-certificates", 73 | "certifi", 74 | "pylint", 75 | "wrapt", 76 | "mypy", 77 | "beautifulsoup4", 78 | "bottleneck", 79 | "cartopy", 80 | "pytest", 81 | "pyroma", 82 | "isort", 83 | "ipympl", 84 | "openpyxl", 85 | "zenodo_get", 86 | ] 87 | }, 88 | ) 89 | -------------------------------------------------------------------------------- /docs/source/demos/03_multi_inv/multi_inv.rst: -------------------------------------------------------------------------------- 1 | Multiple emission inventories 2 | ============================= 3 | 4 | In this example, **no** time evolution file is given, but multiple emission inventories are given as input. 5 | OpenAirClim will interpolate between discrete inventory years. 6 | 7 | 8 | Imports 9 | ------- 10 | If the openairclim package cannot be imported, make sure that you have installed the package with pip or added the oac source folder to ``PYTHONPATH``. 11 | 12 | .. jupyter-execute:: 13 | 14 | import xarray as xr 15 | import matplotlib.pyplot as plt 16 | import zenodo_get 17 | import openairclim as oac 18 | 19 | xr.set_options(display_expand_attrs=False) 20 | 21 | 22 | Input files 23 | ----------- 24 | 25 | In order to be able to execute this example simulation, two types of input are required. 26 | 27 | * Configuration file `multi_inv.toml` 28 | * Emission inventories `emi_inv_20XX.nc` 29 | 30 | Emission inventories 31 | ^^^^^^^^^^^^^^^^^^^^ 32 | 33 | * Source: DLR research study `DEPA 2050`_ 34 | * Inventory years: 2030, 2040, 2050 35 | * Available for download in suitable OpenAirClim format 36 | 37 | .. _DEPA 2050: https://elib.dlr.de/142185/ 38 | 39 | .. jupyter-execute:: 40 | 41 | %%capture 42 | # Download inventories from zenodo 43 | zenodo_get.zenodo_get(["https://doi.org/10.5281/zenodo.11442322", "-g", "emi_inv_20[3-5]0.nc", "-o", "source/demos/input/"]) 44 | 45 | 46 | Simulation run 47 | -------------- 48 | 49 | .. jupyter-execute:: 50 | 51 | oac.run("source/demos/03_multi_inv/multi_inv.toml") 52 | 53 | 54 | Results 55 | ------- 56 | 57 | Time series 58 | ^^^^^^^^^^^ 59 | 60 | * Emission sums 61 | * Concentrations 62 | * Radiative forcings 63 | * Temperature changes 64 | 65 | .. jupyter-execute:: 66 | 67 | results_ds = xr.load_dataset("source/demos/03_multi_inv/results/multi_inv.nc") 68 | display(results_ds) 69 | 70 | .. jupyter-execute:: 71 | 72 | # Plot Radiative Forcing and Temperature Changes 73 | 74 | ac = "TOTAL" 75 | rf_cont = results_ds.RF_cont.sel(ac=ac) * 1000 76 | rf_co2 = results_ds.RF_CO2.sel(ac=ac) * 1000 77 | rf_h2o = results_ds.RF_H2O.sel(ac=ac) * 1000 78 | dt_cont = results_ds.dT_cont.sel(ac=ac) * 1000 79 | dt_co2 = results_ds.dT_CO2.sel(ac=ac) * 1000 80 | dt_h2o = results_ds.dT_H2O.sel(ac=ac) * 1000 81 | 82 | fig, ax = plt.subplots(ncols=2, figsize=(10,5)) 83 | ax[0].grid(True) 84 | ax[1].grid(True) 85 | rf_cont.plot(ax=ax[0], color="deepskyblue", label="cont") 86 | rf_co2.plot(ax=ax[0], color="k", label="CO2") 87 | rf_h2o.plot(ax=ax[0], color="steelblue", label="H2O") 88 | dt_cont.plot(ax=ax[1], color="deepskyblue", label="cont") 89 | dt_co2.plot(ax=ax[1], color="k", label="CO2") 90 | dt_h2o.plot(ax=ax[1], color="steelblue", label="H2O") 91 | ax[0].set_ylabel("Radiative Forcing [mW/m²]") 92 | ax[1].set_ylabel("Temperature Change [mK]") 93 | ax[0].legend() 94 | ax[1].legend() 95 | -------------------------------------------------------------------------------- /docs/source/imprint.rst: -------------------------------------------------------------------------------- 1 | Imprint 2 | ======= 3 | 4 | **Imprint according to Section 5 Digitale-Dienste-Gesetz (German Act on Digital Services) and Section 18 Medienstaatsvertrag (German State Media Treaty)** 5 | 6 | | Deutsches Zentrum für Luft- und Raumfahrt e. V. 7 | | German Aerospace Center (DLR) 8 | | Linder Höhe 9 | | 51147 Köln (Cologne) 10 | | Germany 11 | | 12 | | Tel: +49 2203 601-0 13 | | Fax: +49 2203 67310 14 | | Email: contact-dlr [at] dlr.de 15 | | https://www.dlr.de/en 16 | 17 | DLR's Executive Board is empowered to act as DLR's representative. 18 | It consists of Prof. Dr.-Ing. Anke Kaysser-Pyzalla (Chair of the DLR Executive Board), Klaus Hamacher (Vice Chairman of the Executive Board), Prof. Dr.-Ing. Karsten Lemmer and Dr.-Ing. Walther Pelzer. 19 | 20 | **Seat of the Executive Board** 21 | 22 | The Executive Board's seat is located at DLR, Executive Board, Linder Hoehe, D-51147 Cologne. 23 | 24 | The Executive Board can also authorise DLR employees to act on behalf of DLR. 25 | The head of DLR's legal department, Linder Hoehe, 51147 Cologne, can provide information about the extent of this authorisation. 26 | 27 | **Court of registration/registration number** 28 | 29 | District court of Bonn, VR 2780. 30 | 31 | **Value added tax identification number** 32 | 33 | DE 121965658 34 | 35 | **Responsible in the sense of Section 18 2nd paragraph of Medienstaatsvertrag (German State Media Treaty)** 36 | 37 | | Prof. Dr. Markus Rapp 38 | | Institute of Atmospheric Physics 39 | | Münchener Straße 20 40 | | 82234 Weßling-Oberpfaffenhofen 41 | | Germany 42 | 43 | **Liability for DLR´s own content** 44 | 45 | As far as DLR´s "own content" (information, software or documentation) is being made available free of charge on this website [https://openairclim.org], any liability for defects as to quality or title of DLR´s own content - especially in relation to the correctness or absence of defects or the absence of claims or third party rights or in relation to completeness and/or fitness for purpose - is excluded except in cases of willful misconduct or gross negligence. 46 | 47 | **Liability for third party content via setting of hyperlinks** 48 | 49 | Cross-references or rather hyperlinks ("links") to content provided by other content providers must be distinguished from DLR´s own content. 50 | By means of hyperlinking DLR provides third-party content for use which is marked accordingly by the indication "[external]" or by a link. 51 | Before setting the link DLR checks this third-party content if it causes possible civil or criminal liability. 52 | However, it cannot be excluded that the third-party content providers change their content afterwards. 53 | DLR does not constantly check the third-party content it refers to for changes made by the third-party content providers which could engender DLR´s civil or criminal liability. 54 | If you believe that the linked external sites violate applicable laws or otherwise have inappropriate content, please let us know. 55 | -------------------------------------------------------------------------------- /openairclim/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions used over the entire framework 3 | """ 4 | 5 | from pathlib import Path 6 | import numpy as np 7 | import cf_units 8 | 9 | 10 | def find_basenames(path_lst): 11 | """Find basenames of a list of paths 12 | 13 | Args: 14 | path_arr (list): List of paths 15 | 16 | Returns: 17 | list: List of basenames 18 | """ 19 | basename_lst = [] 20 | for path in path_lst: 21 | basename = Path(path).stem 22 | basename_lst.append(basename) 23 | return basename_lst 24 | 25 | 26 | def convert_to_regular(inv): 27 | """Convert flat / unstructured xarray into xarray 28 | with regular 3D grid lon/lat/plev 29 | 30 | Args: 31 | inv (xarray): flat / unstructured xarray 32 | 33 | Returns: 34 | xarray: regular xarray with dimension lon/lat/plev 35 | """ 36 | inv_reg = inv.set_coords(["lon", "lat", "plev"]) 37 | inv_reg = inv_reg.set_xindex(["lon", "lat", "plev"]) 38 | inv_reg = inv_reg.unstack("index") 39 | return inv_reg 40 | 41 | 42 | def convert_nested_to_series(nested_dict): 43 | """Convert nested dictionary to dictionary of np.arrays / time series 44 | 45 | Args: 46 | nested_dict (dict): Dictionary of dictionaries, keys are species, years 47 | {spec: {year: np.array, ...}, ...} 48 | 49 | Returns: 50 | dict: Dictionary of np.arrays / time series, keys are species 51 | {spec: np.array, np.array, ...} 52 | """ 53 | plain_dict = {} 54 | for key, inner_dict in nested_dict.items(): 55 | plain_dict[key] = np.array(list(inner_dict.values())) 56 | return plain_dict 57 | 58 | 59 | def tgco2_to_tgc(co2): 60 | """Converts mass of CO2 in Tg to mass of C in Tg 61 | 62 | Args: 63 | co2 (float): Mass of CO2 in Tg 64 | 65 | Returns: 66 | float: Mass of C in Tg 67 | """ 68 | tgc = co2 * 12.0 / 44.0 69 | return tgc 70 | 71 | 72 | def kgco2_to_tgc(co2): 73 | """Converts mass of CO2 in kg to mass of C in Tg 74 | 75 | Args: 76 | co2 (float): Mass of CO2 in kg 77 | 78 | Returns: 79 | float: Mass of C in Tg 80 | """ 81 | tgc = co2 * 12.0 / 44.0 * 1e-9 82 | return tgc 83 | 84 | 85 | def tg_to_kg(val): 86 | """Convert mass in Tg to mass in kg 87 | 88 | Args: 89 | val (float): Mass in Tg 90 | 91 | Returns: 92 | float: Mass in kg 93 | """ 94 | # return 1.0e9 * val 95 | kilogram = cf_units.Unit("kg") 96 | teragram = cf_units.Unit("Tg") 97 | return teragram.convert(val, kilogram) 98 | 99 | 100 | def kg_to_tg(val): 101 | """Convert mass in kg to mass in Tg 102 | 103 | Args: 104 | val (float): Mass in kg 105 | 106 | Returns: 107 | float: Mass in Tg 108 | """ 109 | # return 1.0e-9 * val 110 | kilogram = cf_units.Unit("kg") 111 | teragram = cf_units.Unit("Tg") 112 | return kilogram.convert(val, teragram) 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.13.0] - 2025-11-19 4 | 5 | ### Added 6 | 7 | - Attribution methodologies for species CO2 and CH4: #96 @liammegill 8 | - Residual, marginal, proportional (default) and differential attribution 9 | - Aircraft characteristics provided from csv file: #92 @liammegill 10 | - Capability to switch on/off plots: #29 @liammegill 11 | - Capability to switch off climate metrics: #75 @liammegill 12 | 13 | ### Fixed 14 | 15 | - Normalization / scaling uses incorrect reference emission inventory: #97 @liammegill 16 | 17 | ## [0.12.0] - 2025-09-23 18 | 19 | ### Added 20 | 21 | - Online documentation on [openairclim.org](https://openairclim.org/) bundling different types of documentation: #82 @liammegill @stefan-voelk 22 | - Introduction 23 | - Installation guide 24 | - Getting started 25 | - User Guide (new) 26 | - Demonstrations (new) 27 | - Scientific Background (new) #62 @liammegill 28 | - Publications (new) 29 | - API Reference 30 | - Governance (new) 31 | - Contact and Support (new) 32 | - Bibliography 33 | 34 | ### Updates 35 | 36 | - Streamline README considering website as a new focus for documentation 37 | 38 | ## [0.11.1] - 2025-04-15 39 | 40 | ### Fixed 41 | 42 | - Fixed `PermissionError` when example input directory does not yet exist. #76 @stefan-voelk 43 | 44 | ## [0.11.0] - 2025-04-02 45 | 46 | ### Added 47 | 48 | - Capability for multiple aircraft to be present within the input emission inventory along data variable "ac" 49 | 50 | ### Updates 51 | 52 | - Added capability for multiple aircraft within same emission inventory. #16 @liammegill 53 | - Fixed logger handlers at end of OpenAirClim run. #66 @liammegill 54 | 55 | ## [0.10.0] - 2025-03-06 56 | 57 | ### Added 58 | 59 | - Contrails module: Megill_2025 methodology after [Megill & Grewe, in prep.]( https://doi.org/10.5194/egusphere-2024-3398) 60 | 61 | ### Updates 62 | 63 | - Time evolution with function `adjust_inventories(config, inv_dict)` for application on emission inventories **before** simulation, see [workflow documentation](docs/workflows/workflows.md) 64 | 65 | ## [0.9.0] - 2024-12-04 66 | 67 | ### Added 68 | 69 | - Species: $O_3$, $CH_4$, PMO and Contrails 70 | 71 | ### Limitations 72 | 73 | - Limited resolution of response surfaces and pending validation for species $O_3$, $CH_4$ and PMO 74 | - Stratospheric Water Vapor (SWV) not considered in this version 75 | - Contrails module: AirClim 2.1 methodology including simulations for $H_2$ from AHEAD project 76 | - Climate impact of longer species lifetimes in the stratosphere not considered 77 | - Overhanging effect on next year not considered for species lifetimes in the order of time step (year) 78 | 79 | ### Updates 80 | 81 | - Change of versioning scheme to [semantic versioning](https://semver.org/) 82 | - Move repository directory 83 | - Integrate default configuration settings 84 | 85 | ## [2.8.3] - 2024-09-04 86 | 87 | ### Added 88 | 89 | - Processing of 4D emission data sets: (lon, lat, plev) for multiple inventory years 90 | - Supported species: $CO_2$ and $H_2O$ 91 | - Temperature evolution and climate metrics 92 | - Some response functions available -------------------------------------------------------------------------------- /openairclim/calc_dt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates temperature changes for each species and scenario 3 | """ 4 | 5 | import logging 6 | import numpy as np 7 | 8 | # CONSTANTS 9 | # 10 | # from Boucher & Reddy (2008) 11 | # https://doi.org/10.1016/j.enpol.2007.08.039 12 | C_ARR = [0.631, 0.429] # in K / (W m-2) 13 | D_ARR = [8.4, 409.5] # in years 14 | 15 | 16 | def calc_dtemp(config, spec, rf_dict): 17 | """ 18 | Calculates the temperature changes for a single species 19 | 20 | Args: 21 | config (dict): Configuration dictionary from config 22 | spec (str): species 23 | rf_dict (dict): Dictionary of np.ndarray of radiative forcing values 24 | for time range as defined in config 25 | 26 | Returns: 27 | dict: Dictionary of np.ndarray of temperature values for time range as defined in config 28 | """ 29 | rf_arr = rf_dict[spec] 30 | if config["temperature"]["method"] == "Boucher&Reddy": 31 | dtemp_arr = calc_dtemp_br2008(config, spec, rf_arr) 32 | else: 33 | msg = "Method for temperature change calculation is not valid." 34 | logging.warning(msg) 35 | return {spec: dtemp_arr} 36 | 37 | 38 | def calc_dtemp_br2008( 39 | config: dict, spec: str, rf_arr: np.ndarray 40 | ) -> np.ndarray: 41 | """ 42 | Calculates temperature changes after Boucher and Reddy (2008) 43 | https://doi.org/10.1016/j.enpol.2007.08.039 44 | 45 | 46 | Args: 47 | config (dict): configuration dictionary from config 48 | spec (str): species 49 | rf_arr (np.ndarray): array of radiative forcing values 50 | 51 | Returns: 52 | np.ndarray: array of temperature values 53 | """ 54 | time_config = config["time"]["range"] 55 | time_range = np.arange( 56 | time_config[0], time_config[1], time_config[2], dtype=int 57 | ) 58 | delta_t = time_config[2] 59 | lambda_co2 = config["temperature"]["CO2"]["lambda"] 60 | if spec == "CO2": 61 | efficacy = 1.0 62 | else: 63 | efficacy = config["temperature"][spec]["efficacy"] 64 | lambda_spec = efficacy * lambda_co2 65 | dtemp_arr = np.zeros(len(time_range)) 66 | i = 0 67 | for year in time_range: 68 | j = 0 69 | dtemp = 0 70 | for year_dash in time_range[: (i + 1)]: 71 | dtemp = ( 72 | dtemp 73 | + (lambda_spec / lambda_co2) 74 | * rf_arr[j] 75 | * calc_delta_temp_br2008((year - year_dash), C_ARR, D_ARR) 76 | * delta_t 77 | ) 78 | j = j + 1 79 | dtemp_arr[i] = dtemp 80 | i = i + 1 81 | return dtemp_arr 82 | 83 | 84 | def calc_delta_temp_br2008(t: float, c_arr, d_arr): 85 | """ 86 | Impulse response function according to Boucher and Reddy (2008), Appendix A 87 | 88 | Args: 89 | t (float): time 90 | c_arr (list): parameter array of impulse response function, 91 | Table A1: ci in (K / (W m-2)) 92 | d_arr (list): parameter array of impulse response function, 93 | Table A1: di in (years) 94 | 95 | Returns: 96 | float: temperature change according to the Boucher and Reddy (2008) model 97 | """ 98 | delta_temp = 0.0 99 | for c, d in zip(c_arr, d_arr): 100 | delta_temp = delta_temp + (c / d) * np.exp(-t / d) 101 | return delta_temp 102 | -------------------------------------------------------------------------------- /docs/source/demos/02_scaling/scaling.rst: -------------------------------------------------------------------------------- 1 | Scaling 2 | ======= 3 | 4 | In this example, the time evolution of type **scaling** is demonstrated. 5 | In the scenario, the emissions increase linearly from the year 2019 to the year 2039. 6 | The emissions in 2039 are set to be twice as much as in 2019. 7 | 8 | Imports 9 | ------- 10 | If the openairclim package cannot be imported, make sure that you have installed the package with pip or added the oac source folder to ``PYTHONPATH``. 11 | 12 | .. jupyter-execute:: 13 | 14 | import xarray as xr 15 | import matplotlib.pyplot as plt 16 | import openairclim as oac 17 | 18 | xr.set_options(display_expand_attrs=False) 19 | 20 | 21 | Input files 22 | ----------- 23 | 24 | In order to be able to execute this example simulation, three types of input are required. 25 | 26 | * Configuration file `scaling.toml` 27 | * Emission inventories 28 | 29 | * `ELK_aviation_2019_res5deg_flat.nc` 30 | * `ELK_aviation_2039_res5deg_flat.nc` 31 | 32 | * Time evolution file for scaling: `time_scaling_linear_2019-2039.nc` 33 | 34 | Emission inventories 35 | ^^^^^^^^^^^^^^^^^^^^ 36 | 37 | * Source: DLR Project EmissionsLandKarte (`ELK`_) 38 | * Resolution down-sampled to 5 deg resolution 39 | * Converted into format suitable for OpenAirClim 40 | * Inventory years 41 | 42 | * 2019 (original) 43 | * 2039 (same inventory as original, only year changed) 44 | 45 | .. _ELK: https://elkis.dlr.de/ 46 | 47 | Time evolution 48 | ^^^^^^^^^^^^^^ 49 | 50 | * Time evolution with **scaling** of emissions 51 | * Time period: 2000 - 2050 52 | * Linear ramp-up between years 2019 and 2039 53 | 54 | .. jupyter-execute:: 55 | 56 | evo = xr.load_dataset("source/demos/input/time_scaling_linear_2019-2039.nc") 57 | display(evo) 58 | 59 | fig, ax = plt.subplots() 60 | evo.scaling.plot(ax=ax) 61 | ax.grid(True) 62 | 63 | 64 | Simulation run 65 | -------------- 66 | 67 | .. jupyter-execute:: 68 | 69 | oac.run("source/demos/02_scaling/scaling.toml") 70 | 71 | 72 | Results 73 | ------- 74 | 75 | Time series 76 | ^^^^^^^^^^^ 77 | 78 | * Emission sums 79 | * Concentrations 80 | * Radiative forcings 81 | * Temperature changes 82 | 83 | .. jupyter-execute:: 84 | 85 | results_ds = xr.load_dataset("source/demos/02_scaling/results/scaling.nc") 86 | display(results_ds) 87 | 88 | .. jupyter-execute:: 89 | 90 | # Plot Radiative Forcing and Temperature Changes 91 | 92 | ac = "TOTAL" 93 | rf_cont = results_ds.RF_cont.sel(ac=ac) * 1000 94 | rf_co2 = results_ds.RF_CO2.sel(ac=ac) * 1000 95 | rf_h2o = results_ds.RF_H2O.sel(ac=ac) * 1000 96 | dt_cont = results_ds.dT_cont.sel(ac=ac) * 1000 97 | dt_co2 = results_ds.dT_CO2.sel(ac=ac) * 1000 98 | dt_h2o = results_ds.dT_H2O.sel(ac=ac) * 1000 99 | 100 | fig, ax = plt.subplots(ncols=2, figsize=(10,5)) 101 | ax[0].grid(True) 102 | ax[1].grid(True) 103 | rf_cont.plot(ax=ax[0], color="deepskyblue", label="cont") 104 | rf_co2.plot(ax=ax[0], color="k", label="CO2") 105 | rf_h2o.plot(ax=ax[0], color="steelblue", label="H2O") 106 | dt_cont.plot(ax=ax[1], color="deepskyblue", label="cont") 107 | dt_co2.plot(ax=ax[1], color="k", label="CO2") 108 | dt_h2o.plot(ax=ax[1], color="steelblue", label="H2O") 109 | ax[0].set_ylabel("Radiative Forcing [mW/m²]") 110 | ax[1].set_ylabel("Temperature Change [mK]") 111 | ax[0].legend() 112 | ax[1].legend() 113 | -------------------------------------------------------------------------------- /docs/source/demos/01_norm/norm.rst: -------------------------------------------------------------------------------- 1 | Normalization 2 | ============= 3 | 4 | In this example, the time evolution of type **normalization** is demonstrated. 5 | A historic emission scenario is simulated with OpenAirClim. 6 | 7 | 8 | Imports 9 | ------- 10 | If the openairclim package cannot be imported, make sure that you have installed the package with pip or added the oac source folder to ``PYTHONPATH``. 11 | 12 | .. jupyter-execute:: 13 | 14 | import xarray as xr 15 | import matplotlib.pyplot as plt 16 | import openairclim as oac 17 | 18 | xr.set_options(display_expand_attrs=False) 19 | 20 | 21 | Input files 22 | ----------- 23 | 24 | In order to be able to execute this example simulation, three types of input are required. 25 | 26 | * Configuration file `historic.toml` 27 | * Emission inventory `ELK_aviation_2019_res5deg_flat.nc` 28 | * Time evolution file for fuel normalization `time_norm_historic_SSP.nc` 29 | 30 | Emission inventory 31 | ^^^^^^^^^^^^^^^^^^ 32 | 33 | * Source: DLR Project EmissionsLandKarte (`ELK`_) 34 | * Resolution down-sampled to 5 deg resolution 35 | * Converted into format suitable for OpenAirClim 36 | * Inventory year: 2019 37 | 38 | .. _ELK: https://elkis.dlr.de/ 39 | 40 | .. jupyter-execute:: 41 | 42 | inv = xr.load_dataset("source/demos/input/ELK_aviation_2019_res5deg_flat.nc") 43 | display(inv) 44 | 45 | Time evolution 46 | ^^^^^^^^^^^^^^ 47 | 48 | * Time evolution with **normalization** of fuel use 49 | * Time period: 1920 - 2019 50 | 51 | .. jupyter-execute:: 52 | 53 | evo = xr.load_dataset("source/demos/input/time_norm_historic_SSP.nc") 54 | display(evo) 55 | 56 | fig, ax = plt.subplots() 57 | evo.fuel.plot(ax=ax) 58 | ax.grid(True) 59 | 60 | 61 | Simulation run 62 | -------------- 63 | 64 | .. jupyter-execute:: 65 | 66 | oac.run("source/demos/01_norm/historic.toml") 67 | 68 | 69 | Results 70 | ------- 71 | 72 | Time series 73 | ^^^^^^^^^^^ 74 | 75 | * Emission sums 76 | * Concentrations 77 | * Radiative forcings 78 | * Temperature changes 79 | 80 | .. jupyter-execute:: 81 | 82 | results_ds = xr.load_dataset("source/demos/01_norm/results/historic.nc") 83 | display(results_ds) 84 | 85 | .. jupyter-execute:: 86 | 87 | # Plot Radiative Forcing and Temperature Changes 88 | 89 | ac = "TOTAL" 90 | rf_cont = results_ds.RF_cont.sel(ac=ac) * 1000 91 | rf_co2 = results_ds.RF_CO2.sel(ac=ac) * 1000 92 | rf_h2o = results_ds.RF_H2O.sel(ac=ac) * 1000 93 | dt_cont = results_ds.dT_cont.sel(ac=ac) * 1000 94 | dt_co2 = results_ds.dT_CO2.sel(ac=ac) * 1000 95 | dt_h2o = results_ds.dT_H2O.sel(ac=ac) * 1000 96 | 97 | fig, ax = plt.subplots(ncols=2, figsize=(10,5)) 98 | ax[0].grid(True) 99 | ax[1].grid(True) 100 | rf_cont.plot(ax=ax[0], color="deepskyblue", label="cont") 101 | rf_co2.plot(ax=ax[0], color="k", label="CO2") 102 | rf_h2o.plot(ax=ax[0], color="steelblue", label="H2O") 103 | dt_cont.plot(ax=ax[1], color="deepskyblue", label="cont") 104 | dt_co2.plot(ax=ax[1], color="k", label="CO2") 105 | dt_h2o.plot(ax=ax[1], color="steelblue", label="H2O") 106 | ax[0].set_ylabel("Radiative Forcing [mW/m²]") 107 | ax[1].set_ylabel("Temperature Change [mK]") 108 | ax[0].legend() 109 | ax[1].legend() 110 | 111 | Climate metrics 112 | ^^^^^^^^^^^^^^^ 113 | 114 | * Absolute Global Temperature Potential (AGTP) 115 | * Absolute Global Warming Potential (AGWP) 116 | * Average Temperature Response (ATR) 117 | 118 | .. jupyter-execute:: 119 | 120 | metrics_ds = xr.load_dataset("source/demos/01_norm/results/historic_metrics.nc") 121 | display(metrics_ds) 122 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | What is OpenAirClim? 2 | ==================== 3 | 4 | OpenAirClim is a model for simplified evaluation of the approximate chemistry-climate impact of air traffic emissions. 5 | The model represents the major responses of the atmosphere to emissions in terms of composition and climate change. 6 | Instead of applying time-consuming climate-chemistry models, a response model is developed and applied which reproduces the response of a climate-chemistry model without actually calculating ab initio all the physical and chemical effects. 7 | The responses are non-linear relations between localized emissions and Radiative Forcing and further climate indicators. 8 | These response surfaces are contained within look-up tables. 9 | OpenAirClim builds upon the previous AirClim framework. 10 | In comparison with AirClim, following new features are introduced: 11 | 12 | - Standardized formats for configuration file (user interface) and emission inventories (input) and program results (output) 13 | - Possibility of full 4D emission inventories (3D for several time steps) 14 | - Non-linear response functions for NOx including contribution approach (tagging) and dependency on background 15 | - Contrail formation also depending on fuels and overall efficiencies 16 | - Inclusion of different fuels 17 | - Choice of different CO2 response models 18 | - Choice of temperature models and sea-level rise 19 | - Uncertainty assessment and Robustness Metric based on Monte Carlo Simulations 20 | - Parametric scenarios as sensitivities, e.g. at post-processing level: climate optimized routings 21 | 22 | 23 | Scientific Background 24 | --------------------- 25 | 26 | The impact of aviation on climate amounts to approximately 3.5% of the total anthropogenic climate warming :cite:`leeContributionGlobalAviation2021`. 27 | A large part of the aviation's impact arises from non-CO2 effects, especially contrails :cite:`burkhardtMitigatingContrailCirrus2018, bickelContrailCirrusClimate2025` and nitrogen oxide emissions :cite:`stevensonRadiativeForcingAircraft2004, myhreRadiativeForcingDue2011`. 28 | Impact of non-CO2 effects depend in particular on the location and time of emissions :cite:`lundEmissionMetricsQuantifying2017, frommingInfluenceWeatherSituation2021`, hence a regional dependence of impacts exists. 29 | As impacts of individual non-CO2 effects show a different spatial dependence, the relationship between impacts and associated emissions can be best described in non-linear relationships, i.e. equations or algorithms based on look-up tables. 30 | Specifically, the climate impact of an aircraft depends on where (and when) an aircraft is operated. 31 | In addition, using different types of fuel generally changes the importance of the non-CO2 effects. 32 | 33 | 34 | Layout 35 | ------ 36 | 37 | .. figure:: _static/OAC-chart.png 38 | :alt: Overview of the OpenAirClim framework 39 | :align: center 40 | 41 | Overview of the OpenAirClim framework 42 | 43 | - User interface for settings in the run control and outputs (grey) 44 | - Definition of background conditions, such as aviation scenarios, uncertainty ranges and aviation inventories (orange) 45 | - A link to a pre-processor for aviation inventories (blue) 46 | - Processor for a full 4D-emission inventory at multiple timesteps (magenta) 47 | - A framework for the application of non-linear response functions (red) to these emission inventories. 48 | - Response functions for CO2 and climate / temperature and sea-level changes 49 | - Parametric scenarios as sensitivities (yellow), e.g. at post-processing level: climate optimized routings 50 | - Output: Warnings, errors (log files), climate indicators and diagnostics (green), values of climate metrics and robustness metrics (grey) 51 | -------------------------------------------------------------------------------- /docs/source/demos/01_norm/historic.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for demonstrating OpenAirClim 2 | 3 | # Species considered 4 | [species] 5 | # Species defined in emission inventories 6 | # possible values: "CO2", "H2O", "NOx", "distance" 7 | inv = ["CO2", "H2O", "distance"] 8 | # Assumed NOx species in emission inventory 9 | # possible values: "NO", "NO2" 10 | nox = "NO" 11 | # Output / response species 12 | # possible values: "CO2", "H2O", "O3", "CH4", "PMO", "cont" 13 | out = ["CO2", "H2O", "cont"] 14 | 15 | # Emission inventories 16 | [inventories] 17 | dir = "source/demos/input/" 18 | files = ["ELK_aviation_2019_res5deg_flat.nc"] 19 | # base emission inventories, only considered if rel_to_base = true 20 | rel_to_base = false 21 | base.dir = "input/" 22 | base.files = [] 23 | 24 | # Output options 25 | [output] 26 | # Full simulation run = true, climate metrics only = false 27 | run_oac = true 28 | run_metrics = true 29 | run_plots = false 30 | dir = "source/demos/01_norm/results/" 31 | name = "historic" 32 | overwrite = true 33 | # Computation of 2D concentration responses is not yet supported. 34 | # possible values: false 35 | concentrations = false 36 | 37 | # Time settings 38 | [time] 39 | dir = "source/demos/input/" 40 | # Time range in years: t_start, t_end, step, (t_end not included) 41 | range = [1920, 2020, 1] 42 | # Time evolution of emissions 43 | # either type "scaling" or type "norm" 44 | file = "time_norm_historic_SSP.nc" 45 | 46 | # Global background concentrations 47 | [background] 48 | dir = "../repository/" 49 | CO2.file = "co2_bg.nc" 50 | CO2.scenario = "SSP2-4.5" 51 | CH4.file = "ch4_bg.nc" 52 | CH4.scenario = "SSP2-4.5" 53 | N2O.file = "n2o_bg.nc" 54 | N2O.scenario = "SSP2-4.5" 55 | 56 | # Response options 57 | [responses] 58 | dir = "../repository/" 59 | CO2.response_grid = "0D" 60 | CO2.conc.method = "Sausen&Schumann" 61 | # RF method based on Etminan et al. 2016 is used by default. 62 | #CO2.rf.method = "Etminan_2016" 63 | 64 | H2O.response_grid = "2D" 65 | H2O.rf.file = "resp_RF.nc" # AirClim response surface 66 | 67 | O3.response_grid = "2D" 68 | O3.rf.file = "resp_RF_O3.nc" # tagging 69 | #O3.rf.file = "resp_RF.nc" # AirClim response surface, requires adjustment of CORR_RF_O3 ! 70 | 71 | CH4.response_grid = "2D" 72 | CH4.tau.file = "resp_ch4.nc" # tagging 73 | CH4.rf.method = "Etminan_2016" 74 | 75 | cont.response_grid = "cont" 76 | cont.resp.file = "resp_cont_lf.nc" 77 | 78 | # Temperature options 79 | [temperature] 80 | # valid methods: "Boucher&Reddy" 81 | method = "Boucher&Reddy" 82 | # Climate sensitivity parameter, Ponater et al. 2006, Table 1 83 | # https://doi.org/10.1016/j.atmosenv.2006.06.036 84 | CO2.lambda = 0.73 85 | # Efficacies, Ponater et al. 2006, Table 1 86 | H2O.efficacy = 1.14 87 | O3.efficacy = 1.37 88 | PMO.efficacy = 1.37 89 | CH4.efficacy = 1.14 90 | cont.efficacy = 0.59 91 | 92 | # Climate metrics options 93 | [metrics] 94 | # iterate over elements in lists types t_0 and H 95 | types = ["AGTP", "AGWP", "ATR"] # valid climate metrics: AGTP, AGWP, ATR 96 | H = [100] # Time horizon, t_final = t_0 + H - 1 97 | t_0 = [1920] # Start time for metrics calculation 98 | 99 | # aircraft defined in inventory 100 | # following identifiers are NOT allowed: "TOTAL" 101 | # "DEFAULT" is used if "ac" coordinate not defined in emission inventories 102 | # G_250, eff_fac and PMrel must be defined for each aircraft if contrails are 103 | # to be calculated. 104 | [aircraft] 105 | types = ["DEFAULT"] 106 | DEFAULT.G_250 = 1.70 # Schmidt-Appleman mixing line slope at 250 hPa 107 | DEFAULT.eff_fac = 1.0 # efficiency factor compared to 0.333 108 | DEFAULT.PMrel = 1.0 # relative PM emissions compared to 1e15 109 | -------------------------------------------------------------------------------- /utils/create_test_files.py: -------------------------------------------------------------------------------- 1 | """Create files for testing purposes""" 2 | 3 | import sys 4 | import os 5 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | sys.path.append(os.path.dirname(SCRIPT_DIR)) 7 | 8 | from utils.create_test_data import create_test_inv, create_test_rf_resp 9 | 10 | 11 | # CONSTANTS 12 | REPO_PATH = "../tests/repository/" 13 | INV_NAME = "test_inv.nc" 14 | RESP_NAME = "test_resp.nc" 15 | TOML_NAME = "test.toml" 16 | TOML_INVALID_NAME = "test_invalid.toml" 17 | 18 | 19 | def create_test_directories(path_arr: list): 20 | """ 21 | Create new test directories if they do not exist. 22 | 23 | Args: 24 | path_arr (list): A list of paths to be created. 25 | 26 | Returns: 27 | None 28 | 29 | Raises: 30 | OSError: If the creation of a directory fails. 31 | """ 32 | for path in path_arr: 33 | if not os.path.isdir(path): 34 | msg = f"Create new test directory {path}" 35 | print(msg) 36 | os.makedirs(path) 37 | 38 | 39 | def create_test_config_files(repo_path, valid_name, invalid_name): 40 | """ 41 | Create two configuration files for testing. 42 | 43 | Args: 44 | repo_path (str): The path to the repository. 45 | valid_name (str): The name of the valid configuration file. 46 | invalid_name (str): The name of the invalid configuration file. 47 | 48 | Returns: 49 | None 50 | 51 | Raises: 52 | OSError: If the creation of a file fails. 53 | """ 54 | file_path = repo_path + valid_name 55 | if os.path.isfile(file_path): 56 | msg = "Overwrite existing file " + file_path 57 | print(msg) 58 | with open(file_path, mode="w", encoding="utf-8") as valid_file: 59 | valid_file.write( 60 | '# Key-Value pair\ 61 | \nkey = "value"' 62 | ) 63 | file_path = repo_path + invalid_name 64 | if os.path.isfile(file_path): 65 | msg = "Overwrite existing file " + file_path 66 | print(msg) 67 | with open(file_path, mode="w", encoding="utf-8") as invalid_file: 68 | invalid_file.write( 69 | '# Invalid Toml syntax\ 70 | \nkey ! "value"' 71 | ) 72 | 73 | 74 | def create_test_inv_nc(repo_path, inv_name): 75 | """ 76 | Create an emission inventory netCDF file for testing. 77 | 78 | Args: 79 | repo_path (str): The path to the repository. 80 | inv_name (str): The name of the emission inventory file. 81 | 82 | Returns: 83 | None 84 | 85 | Raises: 86 | OSError: If the creation of a file fails. 87 | """ 88 | file_path = repo_path + inv_name 89 | if os.path.isfile(file_path): 90 | msg = "Overwrite existing file " + file_path 91 | print(msg) 92 | inv = create_test_inv() 93 | inv.to_netcdf(file_path) 94 | 95 | 96 | def create_test_resp_nc(repo_path, resp_name): 97 | """ 98 | Create a response netCDF file for testing. 99 | 100 | Args: 101 | repo_path (str): The path to the repository. 102 | resp_name (str): The name of the response file. 103 | 104 | Returns: 105 | None 106 | 107 | Raises: 108 | OSError: If the creation of a file fails. 109 | """ 110 | file_path = repo_path + resp_name 111 | if os.path.isfile(file_path): 112 | msg = "Overwrite existing file " + file_path 113 | print(msg) 114 | resp = create_test_rf_resp() 115 | resp.to_netcdf(file_path) 116 | 117 | 118 | if __name__ == "__main__": 119 | create_test_directories([REPO_PATH]) 120 | create_test_config_files(REPO_PATH, TOML_NAME, TOML_INVALID_NAME) 121 | create_test_inv_nc(REPO_PATH, INV_NAME) 122 | create_test_resp_nc(REPO_PATH, RESP_NAME) 123 | -------------------------------------------------------------------------------- /docs/source/demos/03_multi_inv/multi_inv.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for demonstrating OpenAirClim 2 | 3 | # Species considered 4 | [species] 5 | # Species defined in emission inventories 6 | # possible values: "CO2", "H2O", "NOx", "distance" 7 | inv = ["CO2", "H2O", "distance"] 8 | # Assumed NOx species in emission inventory 9 | # possible values: "NO", "NO2" 10 | nox = "NO" 11 | # Output / response species 12 | # possible values: "CO2", "H2O", "O3", "CH4", "PMO", "cont" 13 | out = ["CO2", "H2O", "cont"] 14 | 15 | # Emission inventories 16 | [inventories] 17 | dir = "source/demos/input/" 18 | files = [ 19 | "emi_inv_2030.nc", 20 | "emi_inv_2040.nc", 21 | "emi_inv_2050.nc" 22 | ] 23 | # base emission inventories, only considered if rel_to_base = true 24 | rel_to_base = false 25 | base.dir = "input/" 26 | base.files = [] 27 | 28 | # Output options 29 | [output] 30 | # Full simulation run = true, climate metrics only = false 31 | run_oac = true 32 | run_metrics = true 33 | run_plots = false 34 | dir = "source/demos/03_multi_inv/results/" 35 | name = "multi_inv" 36 | overwrite = true 37 | # Computation of 2D concentration responses is not yet supported. 38 | # possible values: false 39 | concentrations = false 40 | 41 | # Time settings 42 | [time] 43 | dir = "../repository/" 44 | # Time range in years: t_start, t_end, step, (t_end not included) 45 | range = [2030, 2051, 1] 46 | # Time evolution of emissions 47 | # either type "scaling" or type "norm" 48 | 49 | # Global background concentrations 50 | [background] 51 | dir = "../repository/" 52 | CO2.file = "co2_bg.nc" 53 | CO2.scenario = "SSP2-4.5" 54 | CH4.file = "ch4_bg.nc" 55 | CH4.scenario = "SSP2-4.5" 56 | N2O.file = "n2o_bg.nc" 57 | N2O.scenario = "SSP2-4.5" 58 | 59 | # Response options 60 | [responses] 61 | dir = "../repository/" 62 | CO2.response_grid = "0D" 63 | CO2.conc.method = "Sausen&Schumann" 64 | # RF method based on Etminan et al. 2016 is used by default. 65 | #CO2.rf.method = "Etminan_2016" 66 | 67 | H2O.response_grid = "2D" 68 | H2O.rf.file = "resp_RF.nc" # AirClim response surface 69 | 70 | O3.response_grid = "2D" 71 | O3.rf.file = "resp_RF_O3.nc" # tagging 72 | #O3.rf.file = "resp_RF.nc" # AirClim response surface, requires adjustment of CORR_RF_O3 ! 73 | 74 | CH4.response_grid = "2D" 75 | CH4.tau.file = "resp_ch4.nc" # tagging 76 | CH4.rf.method = "Etminan_2016" 77 | 78 | cont.response_grid = "cont" 79 | cont.resp.file = "resp_cont_lf.nc" 80 | 81 | # Temperature options 82 | [temperature] 83 | # valid methods: "Boucher&Reddy" 84 | method = "Boucher&Reddy" 85 | # Climate sensitivity parameter, Ponater et al. 2006, Table 1 86 | # https://doi.org/10.1016/j.atmosenv.2006.06.036 87 | CO2.lambda = 0.73 88 | # Efficacies, Ponater et al. 2006, Table 1 89 | H2O.efficacy = 1.14 90 | O3.efficacy = 1.37 91 | PMO.efficacy = 1.37 92 | CH4.efficacy = 1.14 93 | cont.efficacy = 0.59 94 | 95 | # Climate metrics options 96 | [metrics] 97 | # iterate over elements in lists types t_0 and H 98 | types = ["AGWP", "ATR", "AGTP"] # valid climate metrics: AGTP, AGWP, ATR 99 | H = [21] # Time horizon, t_final = t_0 + H - 1 100 | t_0 = [2030] # Start time for metrics calculation 101 | 102 | # aircraft defined in inventory 103 | # following identifiers are NOT allowed: "TOTAL" 104 | # "DEFAULT" is used if "ac" coordinate not defined in emission inventories 105 | # G_250, eff_fac and PMrel must be defined for each aircraft if contrails are 106 | # to be calculated. 107 | [aircraft] 108 | types = ["DEFAULT"] 109 | DEFAULT.G_250 = 1.70 # Schmidt-Appleman mixing line slope at 250 hPa 110 | DEFAULT.eff_fac = 1.0 # efficiency factor compared to 0.333 111 | DEFAULT.PMrel = 1.0 # relative PM emissions compared to 1e15 112 | -------------------------------------------------------------------------------- /docs/source/demos/02_scaling/scaling.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for demonstrating OpenAirClim 2 | 3 | # Species considered 4 | [species] 5 | # Species defined in emission inventories 6 | # possible values: "CO2", "H2O", "NOx", "distance" 7 | inv = ["CO2", "H2O", "distance"] 8 | # Assumed NOx species in emission inventory 9 | # possible values: "NO", "NO2" 10 | nox = "NO" 11 | # Output / response species 12 | # possible values: "CO2", "H2O", "O3", "CH4", "PMO", "cont" 13 | out = ["CO2", "H2O", "cont"] 14 | 15 | # Emission inventories 16 | [inventories] 17 | dir = "source/demos/input/" 18 | files = ["ELK_aviation_2019_res5deg_flat.nc", "ELK_aviation_2039_res5deg_flat.nc"] 19 | # base emission inventories, only considered if rel_to_base = true 20 | rel_to_base = false 21 | base.dir = "input/" 22 | base.files = [] 23 | 24 | # Output options 25 | [output] 26 | # Full simulation run = true, climate metrics only = false 27 | run_oac = true 28 | run_metrics = true 29 | run_plots = false 30 | dir = "source/demos/02_scaling/results/" 31 | name = "scaling" 32 | overwrite = true 33 | # Computation of 2D concentration responses is not yet supported. 34 | # possible values: false 35 | concentrations = false 36 | 37 | # Time settings 38 | [time] 39 | dir = "source/demos/input/" 40 | # Time range in years: t_start, t_end, step, (t_end not included) 41 | range = [2019, 2040, 1] 42 | # Time evolution of emissions 43 | # either type "scaling" or type "norm" 44 | file = "time_scaling_linear_2019-2039.nc" 45 | 46 | # Global background concentrations 47 | [background] 48 | dir = "../repository/" 49 | CO2.file = "co2_bg.nc" 50 | CO2.scenario = "SSP2-4.5" 51 | CH4.file = "ch4_bg.nc" 52 | CH4.scenario = "SSP2-4.5" 53 | N2O.file = "n2o_bg.nc" 54 | N2O.scenario = "SSP2-4.5" 55 | 56 | # Response options 57 | [responses] 58 | dir = "../repository/" 59 | CO2.response_grid = "0D" 60 | CO2.conc.method = "Sausen&Schumann" 61 | # RF method based on Etminan et al. 2016 is used by default. 62 | #CO2.rf.method = "Etminan_2016" 63 | 64 | H2O.response_grid = "2D" 65 | H2O.rf.file = "resp_RF.nc" # AirClim response surface 66 | 67 | O3.response_grid = "2D" 68 | O3.rf.file = "resp_RF_O3.nc" # tagging 69 | #O3.rf.file = "resp_RF.nc" # AirClim response surface, requires adjustment of CORR_RF_O3 ! 70 | 71 | CH4.response_grid = "2D" 72 | CH4.tau.file = "resp_ch4.nc" # tagging 73 | CH4.rf.method = "Etminan_2016" 74 | 75 | cont.response_grid = "cont" 76 | cont.resp.file = "resp_cont_lf.nc" 77 | 78 | # Temperature options 79 | [temperature] 80 | # valid methods: "Boucher&Reddy" 81 | method = "Boucher&Reddy" 82 | # Climate sensitivity parameter, Ponater et al. 2006, Table 1 83 | # https://doi.org/10.1016/j.atmosenv.2006.06.036 84 | CO2.lambda = 0.73 85 | # Efficacies, Ponater et al. 2006, Table 1 86 | H2O.efficacy = 1.14 87 | O3.efficacy = 1.37 88 | PMO.efficacy = 1.37 89 | CH4.efficacy = 1.14 90 | cont.efficacy = 0.59 91 | 92 | # Climate metrics options 93 | [metrics] 94 | # iterate over elements in lists types t_0 and H 95 | types = ["ATR"] # valid climate metrics: AGTP, AGWP, ATR 96 | H = [20] # Time horizon, t_final = t_0 + H - 1 97 | t_0 = [2019] # Start time for metrics calculation 98 | 99 | # aircraft defined in inventory 100 | # following identifiers are NOT allowed: "TOTAL" 101 | # "DEFAULT" is used if "ac" coordinate not defined in emission inventories 102 | # G_250, eff_fac and PMrel must be defined for each aircraft if contrails are 103 | # to be calculated. 104 | [aircraft] 105 | types = ["DEFAULT"] 106 | DEFAULT.G_250 = 1.70 # Schmidt-Appleman mixing line slope at 250 hPa 107 | DEFAULT.eff_fac = 1.0 # efficiency factor compared to 0.333 108 | DEFAULT.PMrel = 1.0 # relative PM emissions compared to 1e15 109 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | .. note:: 5 | 6 | In the future, it will be possible to install OpenAirClim with conda or pip directly. 7 | We are currently having some difficulties with dependencies that prevent us from setting this up. 8 | For now, to use OpenAirClim, you will need to clone the code from GitHub. 9 | 10 | If you build OpenAirClim from source, you first have to clone the `repository `_. 11 | The most convenient way of doing this is by using the following `Git `_ command: 12 | 13 | .. code-block:: bash 14 | 15 | git clone https://github.com/dlr-pa/oac.git 16 | 17 | Once the repository has been cloned, there are two options to install the necessary packages. 18 | 19 | 20 | Installation using conda 21 | ------------------------ 22 | 23 | If you choose to use conda, the `conda `_ or `mamba `_ package manager must first be installed. 24 | We recommend the open-source solution `Miniforge `_, which only uses packages from the community `conda-forge `_ channel. 25 | Since it is open-source, this option is generally available even if the use of Anaconda is prohibited, but we of course cannot guarantee this. 26 | Please check with your IT department (if applicable). 27 | 28 | The source code includes configuration files ``environment_xxx.yaml`` that enable the installation of a conda environment with all required dependencies. 29 | This installation method is suitable for working across platforms. 30 | Use the ``dev`` file if you are planning on making changes to the code or contributing to the development of OpenAirClim, otherwise use ``minimal``. 31 | Change directory to the root folder of the downloaded source and create a conda environment and activate it: 32 | 33 | .. code-block:: bash 34 | 35 | cd oac 36 | conda env create -f environment_xxx.yaml 37 | conda activate 38 | 39 | Replace ``xxx`` with the relevant file and ```` with the correct name of the installed conda environment, e.g ``oac`` or ``oac_minimal``. 40 | Finally, to install the openairclim package system-wide on your computer, execute one of the following commands within the activated conda environment. 41 | This last installation step isn't necessary if the user has otherwise added the path to the oac source folder to ``PYTHONPATH``. 42 | 43 | .. code-block:: bash 44 | 45 | pip install . 46 | 47 | or 48 | 49 | .. code-block:: bash 50 | 51 | pip install -e . 52 | 53 | The ``-e`` flag treats the openairclim package as an editable install, allowing you to make changes to the source code and see those changes reflected immediately. 54 | The latter command is recommended for developers. 55 | 56 | After installing the conda environment and required dependencies, proceed with the steps described in :doc:`quickstart`. 57 | 58 | 59 | Installation using pip 60 | ---------------------- 61 | 62 | .. note:: 63 | 64 | The installation with ``pip`` currently does not work due to a problem with the dependency ``cf-units``. 65 | We are working on a solution, see issue `#20 `_. 66 | 67 | The prerequisite for this installation method is have installed a python version >= 3.4. 68 | Then, the installer ``pip`` is included by default. 69 | In your console, change directory to the OpenAirClim root folder and execute the following command: 70 | 71 | .. code-block:: bash 72 | 73 | pip install . 74 | 75 | To install OpenAirClim in *editable mode*, use the ``-e`` flag: 76 | 77 | .. code-block:: bash 78 | 79 | pip install -e . 80 | 81 | If you are planning on making changes to the code or contributing to the development of OpenAirClim, extra packages are required. 82 | To install these, use (with or without the ``-e`` flag): 83 | 84 | .. code-block:: bash 85 | 86 | pip install ".[dev]" 87 | 88 | After installing the packages, proceed with the steps described in :doc:`quickstart`. 89 | -------------------------------------------------------------------------------- /openairclim/construct_conc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constructs concentrations 3 | """ 4 | 5 | import numpy as np 6 | import xarray as xr 7 | from openairclim.interpolate_time import interp_linear 8 | from openairclim.utils import kg_to_tg 9 | 10 | 11 | def get_emissions(inv_dict, species): 12 | """Get total emissions in Tg for each inventory and given species 13 | TODO Unit conversions for other units than kg 14 | 15 | Args: 16 | species (str): String or list of strings, species names 17 | inv_dict (dict): Dictionary of emission inventory xarrays, 18 | keys are inventory years 19 | Raises: 20 | TypeError: if species argument has wrong type 21 | 22 | Returns: 23 | np.ndarray, dict: Inventory years and dictionary with arrays of emissions in Tg, 24 | keys are spec 25 | """ 26 | if isinstance(species, list) and all( 27 | isinstance(ele, str) for ele in species 28 | ): 29 | pass 30 | elif not isinstance(species, list) and isinstance(species, str): 31 | species = [species] 32 | else: 33 | raise TypeError("Species argument is not of type str or list of str") 34 | emis_dict = {} 35 | for spec in species: 36 | inv_years, emis = calc_inv_sums(spec, inv_dict) 37 | if spec != "distance": # distance remains in km 38 | # Convert kg to Tg 39 | emis = kg_to_tg(emis) 40 | emis_dict[spec] = emis 41 | return inv_years, emis_dict 42 | 43 | 44 | def calc_inv_sums(spec, inv_dict): 45 | """Calculates the emission sums for a given species for a dictionary 46 | of emission inventories 47 | 48 | Args: 49 | spec (str): Name of species 50 | inv_dict (dict): Dictionary of emission inventory xarrays, 51 | keys are inventory years 52 | 53 | Returns: 54 | np.ndarray, np.ndarray: Inventory years and inventory sums for given species 55 | """ 56 | inv_years = [] 57 | inv_sums_arr = [] 58 | for year, inv in inv_dict.items(): 59 | check_inv_values(inv, year, spec) 60 | inv_years.append(year) 61 | tot = float(inv[spec].sum()) 62 | inv_sums_arr.append(tot) 63 | inv_years = np.array(inv_years) 64 | inv_sums = np.array(inv_sums_arr) 65 | return inv_years, inv_sums 66 | 67 | 68 | def check_inv_values(inv, year, spec): 69 | """ 70 | Checks values in given inventory for a specific species. 71 | 72 | Args: 73 | inv (xarray.Dataset): Emission inventory dataset for a specific year. 74 | year (str): Year of the inventory. 75 | spec (str): Species name. 76 | 77 | Raises: 78 | ValueError: If there are any negative emissions for the given species in the inventory. 79 | """ 80 | inv_arr = inv[spec].values 81 | if np.any(inv_arr < 0.0): 82 | msg = ( 83 | "Negative emissions detected for inventory year " 84 | + str(year) 85 | + " and species " 86 | + spec 87 | + ". Only positive emission values are allowed!" 88 | ) 89 | raise ValueError(msg) 90 | 91 | 92 | def interp_bg_conc(config, spec): 93 | """Interpolates background concentrations for given species 94 | within time_range, for a background file and scenario set in config 95 | TODO Take into account various conc units in background file 96 | 97 | Args: 98 | config (dict): Configuration dictionary from config 99 | spec (str): Species name 100 | 101 | Returns: 102 | dict: Dictionary with np.ndarray of interpolated concentrations, 103 | key is species 104 | """ 105 | dir_name = config["background"]["dir"] 106 | inp_file = dir_name + config["background"][spec]["file"] 107 | scenario = config["background"][spec]["scenario"] 108 | conc = xr.load_dataset(inp_file)[scenario] 109 | conc_dict = {spec: conc} 110 | years = conc["year"].values 111 | _, interp_conc = interp_linear(config, years, conc_dict) 112 | return interp_conc 113 | -------------------------------------------------------------------------------- /example/example.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for demonstrating OpenAirClim 2 | 3 | # Species considered 4 | [species] 5 | # Species defined in emission inventories 6 | # possible values: "CO2", "H2O", "NOx", "distance" 7 | inv = ["CO2", "H2O", "NOx", "distance"] 8 | # Assumed NOx species in emission inventory 9 | # possible values: "NO", "NO2" 10 | nox = "NO" 11 | # Output / response species 12 | # possible values: "CO2", "H2O", "O3", "CH4", "PMO", "cont" 13 | out = ["CO2", "H2O", "O3", "CH4", "PMO", "cont"] 14 | 15 | # Emission inventories 16 | [inventories] 17 | dir = "input/" 18 | files = [ 19 | "rnd_inv_2020.nc", 20 | "rnd_inv_2030.nc", 21 | "rnd_inv_2040.nc", 22 | "rnd_inv_2050.nc", 23 | # "rnd_inv_2060.nc", 24 | # "rnd_inv_2070.nc", 25 | # "rnd_inv_2080.nc", 26 | # "rnd_inv_2090.nc", 27 | # "rnd_inv_2100.nc", 28 | # "rnd_inv_2110.nc", 29 | # "rnd_inv_2120.nc", 30 | ] 31 | # base emission inventories, only considered if rel_to_base = true 32 | rel_to_base = false 33 | base.dir = "input/" 34 | base.files = [ 35 | "rnd_inv_2020.nc", 36 | "rnd_inv_2030.nc", 37 | "rnd_inv_2040.nc", 38 | "rnd_inv_2050.nc", 39 | ] 40 | 41 | # Output options 42 | [output] 43 | # Full simulation run = true, climate metrics only = false 44 | run_oac = true 45 | run_metrics = true 46 | run_plots = true 47 | dir = "results/" 48 | name = "example" 49 | overwrite = true 50 | # Computation of 2D concentration responses is not yet supported. 51 | # possible values: false 52 | concentrations = false 53 | 54 | # Time settings 55 | [time] 56 | dir = "input/" 57 | # Time range in years: t_start, t_end, step, (t_end not included) 58 | range = [2020, 2051, 1] 59 | # Time evolution of emissions 60 | # either type "scaling" or type "norm" 61 | # file = "time_scaling_example.nc" 62 | # file = "time_norm_example.nc" 63 | 64 | # Global background concentrations 65 | [background] 66 | dir = "../repository/" 67 | CO2.file = "co2_bg.nc" 68 | CO2.scenario = "SSP2-4.5" 69 | #CO2.scenario = "SSP1-1.9" 70 | #CO2.scenario = "SSP4-6.0" 71 | #CO2.scenario = "SSP3-7.0" 72 | CH4.file = "ch4_bg.nc" 73 | CH4.scenario = "SSP2-4.5" 74 | N2O.file = "n2o_bg.nc" 75 | N2O.scenario = "SSP2-4.5" 76 | 77 | # Response options 78 | [responses] 79 | dir = "../repository/" 80 | CO2.response_grid = "0D" 81 | CO2.conc.method = "Sausen&Schumann" 82 | # RF method based on Etminan et al. 2016 is used by default. 83 | # CO2.rf.method = "Etminan_2016" 84 | # CO2.rf.attr = "proportional" 85 | 86 | H2O.response_grid = "2D" 87 | H2O.rf.file = "resp_RF.nc" # AirClim response surface 88 | 89 | O3.response_grid = "2D" 90 | O3.rf.file = "resp_RF_O3.nc" # tagging 91 | #O3.rf.file = "resp_RF.nc" # AirClim response surface, requires adjustment of CORR_RF_O3 ! 92 | 93 | CH4.response_grid = "2D" 94 | CH4.tau.file = "resp_ch4.nc" # tagging 95 | # CH4.rf.attr = "proportional" 96 | 97 | cont.response_grid = "cont" 98 | cont.resp.file = "resp_cont_lf.nc" 99 | 100 | # Temperature options 101 | [temperature] 102 | # valid methods: "Boucher&Reddy" 103 | method = "Boucher&Reddy" 104 | # Climate sensitivity parameter, Ponater et al. 2006, Table 1 105 | # https://doi.org/10.1016/j.atmosenv.2006.06.036 106 | CO2.lambda = 0.73 107 | # Efficacies, Ponater et al. 2006, Table 1 108 | H2O.efficacy = 1.14 109 | O3.efficacy = 1.37 110 | PMO.efficacy = 1.37 111 | CH4.efficacy = 1.14 112 | cont.efficacy = 0.59 113 | 114 | # Climate metrics options 115 | [metrics] 116 | # iterate over elements in lists types t_0 and H 117 | types = ["AGWP", "ATR", "AGTP"] # valid climate metrics: AGTP, AGWP, ATR 118 | H = [31] # Time horizon, t_final = t_0 + H - 1 119 | t_0 = [2020] # Start time for metrics calculation 120 | 121 | # aircraft defined in inventory 122 | # following identifiers are NOT allowed: "TOTAL" 123 | # "DEFAULT" is used if "ac" coordinate not defined in emission inventories 124 | # G_250, eff_fac and PMrel must be defined for each aircraft if contrails are 125 | # to be calculated. 126 | [aircraft] 127 | types = ["DEFAULT"] 128 | # dir = "input/" 129 | # file = "ac_def.csv" 130 | DEFAULT.G_250 = 1.70 # Schmidt-Appleman mixing line slope at 250 hPa 131 | DEFAULT.eff_fac = 1.0 # efficiency factor compared to 0.333 132 | DEFAULT.PMrel = 1.0 # relative PM emissions compared to 1e15 133 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | 5 | Download or generate emission inventories 6 | ----------------------------------------- 7 | 8 | Four-dimensional air traffic emission inventories are essential inputs to OpenAirClim. 9 | If this is your first time using OpenAirClim, we recommend using the example or artifically generated emission inventories. 10 | Functionality to convert existing emission inventories can be found at the OpenAirClim-addon `gedai `__. 11 | If you have any questions or issues with these conversion tools, please use ``gedai``'s Issues workflow on `Github `__. 12 | 13 | The emission inventories which were created as part of the DLR internal `Development Pathways for Aviation up to 2050 (DEPA 2050) `__ project 14 | comprise realistic emission data sets for global air traffic in 5-year steps between 2020 and 2050. 15 | These example inventories can be accessed at `Zenodo `__ and downloaded using the commmand line: 16 | 17 | .. code-block:: bash 18 | 19 | zenodo_get https://doi.org/10.5281/zenodo.11442322 -o "example/input/" 20 | 21 | Depending on the settings chosen in the configuration file, the computational time of the configured simulations could be long. 22 | If you are more interested in testing or developing OpenAirClim, you might want to use artifically generated data instead. 23 | To do so using the build-in random generator, execute the following commands: 24 | 25 | .. code-block:: bash 26 | 27 | cd utils/ 28 | python create_artificial_inventories.py 29 | 30 | The script ``create_artificial_inventories.py`` creates a series of emission inventories comprising random emission data. 31 | 32 | It is also possible to create emission inventories from other sources, such as from ADS-B data or using a trajectory generator. 33 | Please check out the OpenAirClim-addon `gedai `__ if you are interested in this. 34 | However, be aware that generating OpenAirClim-compatible emission inventories in such a manner can be time-consuming and computationally expensive. 35 | 36 | 37 | Create input data 38 | ----------------- 39 | 40 | Depending on your use case, you may need to scale or normalize your emission inventories over time. 41 | A common example would be that you have a global emission inventory for a certain year, for example 2020, and want to simulate the aviation industry's emissions over time, say between 1940 and 2200. 42 | In this case, you can scale your emission inventory along a time-dependent scenario, starting from 0 in 1940 and, for example, increasing by x% per year. 43 | In OpenAirClim, this can be achieved using either a *scaling* or *normalization*. 44 | To understand the difference between normalization and scaling, see :doc:`user_guide/02_evolution`. 45 | 46 | Example normalization and scaling files can be created using the following commands: 47 | 48 | .. code-block:: bash 49 | 50 | cd utils/ 51 | python create_time_evolution.py 52 | 53 | The script ``create_time_evolution.py`` creates two time evoluation files that control the temporal evoluation of the emission data, one for normalization and the other for scaling. 54 | These files are added to the directory ``example/input``. 55 | To include the files in the OpenAirClim run, the file location must be provided in the configuration ``.toml`` file as the ``time.file`` variable. 56 | The ``example.toml`` file has the following lines commented out: 57 | 58 | .. code-block:: toml 59 | 60 | [time] 61 | # ... 62 | # file = "time_scaling_example.nc" 63 | # file = "time_norm_example.nc" 64 | 65 | To use either scaling or normalization, simple uncomment one of the lines. 66 | 67 | 68 | Create test files 69 | ----------------- 70 | 71 | If you are planning on contributing to the development of OpenAirClim, you will probably need to execute the `pytest `__ functions. 72 | These require additional test files, which you can create using: 73 | 74 | .. code-block:: bash 75 | 76 | python create_test_files.py 77 | 78 | 79 | Usage 80 | ----- 81 | 82 | After installation, the package can be imported and used in Python programs: 83 | 84 | .. code-block:: python 85 | 86 | import openairclim as oac 87 | 88 | Refer to the `example `_ folder within the repository for a minimal example 89 | and the :doc:`demos` given on this website. 90 | -------------------------------------------------------------------------------- /docs/source/background/attribution.rst: -------------------------------------------------------------------------------- 1 | Attribution Methods 2 | =================== 3 | 4 | The process of attribution refers to quantifying how much each of several causal factors contributes to a change. 5 | This is not a straightforward process for non-linear relationships, which are often found in atmospheric physics and chemistry. 6 | The attribution method used is an important consideration, since it determines the share of responsibility. 7 | Previous research has primarily focused on attribution methods for climate negotiations and policy, for example for determining each country's responsibility for current global temperature change :cite:`trudingerComparisonFormalismsAttributing2005`. 8 | Some recent work, notably :cite:`boucherContributionGlobalAviation2021`, have instead considered sector-wise attribution. 9 | 10 | In OpenAirClim, attribution methods are used to quantify the relative contribution of each aircraft identifier, whilst considering the global background and all other air traffic. 11 | Attribution is used for the calculation of CO₂ and CH₄ radiative forcing. 12 | An attribution method for contrails is currently in development. 13 | Five attribution methods are available: none, residual, marginal, proportional (default) and differential. 14 | If "none" is selected, calculations are done against pre-industrial conditions, assuming no other anthropogenic sources. 15 | 16 | The attribution method can be selected in the config file: 17 | 18 | .. code:: toml 19 | 20 | [responses] 21 | CO2.rf.attr = "proportional" # "none", "residual", "marginal", "differential" 22 | CH4.rf.attr = "proportional" 23 | cont.attr = "proportional" # work in progress 24 | 25 | 26 | 27 | Methods 28 | ------- 29 | 30 | We assume that :math:`x_\mathrm{ac}(t)` and :math:`x(t)` are the sources, for example CO₂ concentration, for a given aircraft identifer :math:`\mathrm{ac}` and all anthropogenic activities at time :math:`t`. 31 | By definition, :math:`x_\mathrm{ac}(t)` must be included within :math:`x(t)`. 32 | The resulting effect :math:`y_\mathrm{ac}(t)`, for example RF, is calculated by the function :math:`f` using one of the attribution methods as shown below. 33 | We use the notation devised by :cite:`boucherContributionGlobalAviation2021`. 34 | 35 | We differentiate between non-additive and additive methods :cite:`trudingerComparisonFormalismsAttributing2005`. 36 | If a method is non-additive, the contribution of a group of aircraft identifiers differs depending on whether they are quantified together or separately. 37 | In other words, using a non-additive method, :math:`y_\mathrm{A}(t) + y_\mathrm{B}(t) \neq y_\mathrm{A+B}(t)`. 38 | This is not ideal, since we would prefer for the contributions of different aircraft to be combinable in any manner. 39 | Therefore, OpenAirClim uses the additive *proportional attribution method* by default. 40 | 41 | 42 | Non-Additive Methods 43 | ******************** 44 | 45 | A common method is the **residual attribution method**, otherwise referred to as the "all-but-one" method. 46 | The effect attributed to a given source is the difference between two simulations: one with all anthropogenic activities and the other excluding the source in question. 47 | 48 | .. math:: 49 | 50 | y_\mathrm{ac}^R(t) = f(x(t)) - f(x(t) - x_\mathrm{ac}(t)) 51 | 52 | For small perturbations, this method is equivalent to the **marginal attribution method**, which determines the contribution "at the margin". 53 | 54 | .. math:: 55 | 56 | y_\mathrm{ac}^M(t) = \frac{\mathrm{d} f}{\mathrm{d} x} \bigg\rvert_{x(t)} \cdot x_\mathrm{ac}(t) 57 | 58 | This method requires the derivative of the function to be available in OpenAirClim and may thus not be universally applicable. 59 | The point at which the derivative is calculated is a topic of much contention (see e.g. :cite:`boucherContributionGlobalAviation2021`). 60 | We choose to use the current conditions at time :math:`t`. 61 | 62 | 63 | Additive Methods 64 | **************** 65 | 66 | By default, OpenAirClim uses the **proportional attribution method** due to its additivity and simplicity. 67 | For a single species, the proportional attribution method is also equivalent to the tagging method, used within the NOx module. 68 | 69 | .. math:: 70 | 71 | y_\mathrm{ac}^P(t) = \frac{x_\mathrm{ac}(t)}{x(t)} \cdot f(x(t)) 72 | 73 | Finally, the **differential attribution method** uses the differential of the effect with respect to the source. 74 | 75 | .. math:: 76 | 77 | y_\mathrm{ac}^D(t) = \int_0^t \frac{\partial f}{\partial x} \bigg\rvert_{x(t')} \frac{\mathrm{d} x_\mathrm{ac}(t')}{\mathrm{d} t'} \mathrm{d}t' 78 | 79 | This method requires the derivative of the function to be available in OpenAirClim and may thus not be universally applicable. 80 | -------------------------------------------------------------------------------- /docs/source/user_guide/01_input.md: -------------------------------------------------------------------------------- 1 | # Input data 2 | 3 | OpenAirClim requires several input data to be present before executing a simulation run. 4 | 5 | ## Configuration file 6 | 7 | A configuration file serves as the main user interface to the OpenAirClim framework. The [TOML](https://toml.io/en/) format is used which is known for its simple syntax and human readability. Refer to `example/example.toml` for an example configuration. 8 | 9 | The configuration file is structured using *tables* which are collections of key/value pairs. Each table is defined by a header, i.e. a `[string]` enclosed by square brackets. Each table represents a section of the configuration file. 10 | 11 | The comments in `example.toml` describe specific settings more in detail. Here, an overview over the different tables (sections) of the configuration file is given: 12 | 13 | - `[species]` Here, the atmospheric species are defined which are present in the emission inventories, and those species producing the climate impact (response). In some cases, the species defined in the `inv` array and those defined in the `out` array are the same, for example "CO2". In other cases, the response species differ from the species given in the inventory. For example, "NOx" produces a climate response through other species ("O3", "CH4" and "PMO"). By changing the arrays, the simulation of climate impacts can be switched on and off for individual species. 14 | - `[inventories]` This section specifies the input directory and an array of emission inventory files which are considered for the simulation run. Additionaly, base emission inventories can be defined (only relevant for the computation of contrail climate impacts). 15 | - `[output]` Here, settings for the output of simulation results are defined. Using the flags `run_oac` (calculate all species), `run_metrics` (calculate climate metrics), `run_plot` (generate plots) and `concentrations`, parts of the simulation workflow can be switched on and off. 16 | - `[time]` Settings regarding the time dimension are specified here. The `range` setting defines the period and step in years considered for the simulation run. If `file` is set in this section, an additional time evolution is read in and processed. Refer to the documentation *Time Evolution* for more details. 17 | - `[background]` Here, the atmospheric backgrounds of atmospheric species and considering several Shared Socioeconomic Pathway (SSP) scenarios are defined. The atmospheric background is relevant for the computation of climate impacts. 18 | - `[responses]` This section comprises settings of the implemented response surfaces and methodologies used. 19 | - `[temperature]` This section defines the climate sensitivity parameters and efficacies of atmospheric species relevant for the computation of temperature changes. 20 | - `[metrics]` The array `types` defines the climate metrics which should be computed and written to the output. The arrays `H` and `t_0` define time horizons and start times for the metrics calculations. The program iterates over these arrays permuting over all combinations. 21 | - `[aircraft]` The strings in array `types` correspond to (optional) aircraft identifiers present in the emission inventories. This functionality is convenient for the classification of different aircraft types with different properties relevant for the climate impacts. For the contrail module, a set of aircraft-specific variables are required (see the [contrail module user guide](03_contrails.rst)). This data can also be provided as a .csv file. 22 | 23 | ## Emission inventories 24 | 25 | The emission inventories comprise spatially resolved aircraft emissions on a yearly basis. Refer to the example emission inventories, either generated via script `create_artificial_inventories.py`, or the inventories available for [download](https://doi.org/10.5281/zenodo.11442323) from the DLR project [Development Pathways for Aviation up to 2050 (DEPA 2050)](https://elib.dlr.de/142185/). 26 | 27 | ![inventory](../_static/emission-inventory.png) 28 | 29 | The emission inventories are stored as netCDF files using a flat data structure, i.e. an unordered list of entries (not-gridded). Only the naming conventions and units defined in the example inventories should be used. The entry `Inventory_Year` in the attribute section of the netCDF file defines the inventory year. 30 | 31 | ## Time evolution (optional) 32 | 33 | If no extra evolution file is specified in the configuration, OpenAirClim performs a temporal interpolation between discrete inventory years. Alternatively, a time evolution of type **normalization** or **scaling** can be specified in another netCDF file. For more details on that topic, refer to the {doc}`02_evolution` documentation and the example evolution files generated via script `create_time_evolution.py`. -------------------------------------------------------------------------------- /openairclim/interpolate_space.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interpolation and Regridding methods in the space domain 3 | """ 4 | 5 | # TODO Check if one of these python packages are more suitable/flexible 6 | # for example geocat, see https://geocat-comp.readthedocs.io/en/stable/ 7 | # for pressure level interpolations geocat.comp.interpolation.interp_hybrid_to_pressure 8 | # maybe this is a more general function: geocat.comp.interpolation.interp_multidim 9 | # or that one: https://unidata.github.io/MetPy/latest/api/generated/metpy.interpolate.interpolate_to_points.html 10 | 11 | from scipy.interpolate import interpn 12 | import numpy as np 13 | import xarray as xr 14 | from openairclim.write_output import query_checksum_table 15 | from openairclim.write_output import update_checksum_table 16 | 17 | 18 | # CONSTANTS 19 | CHECKSUM_PATH = "../cache/weights/" 20 | 21 | 22 | def calc_weights(spec, resp, inv): 23 | """ 24 | Calculate the weighting factors for a given response and inventory. 25 | 26 | Args: 27 | spec (str): Name of the species for which the weights are being calculated. 28 | resp (xarray.Dataset): Response dataset 29 | inv (xarray.Dataset): Emission inventory dataset 30 | 31 | Returns: 32 | xarray.Dataset: Dataset with weighting parameters 33 | 34 | """ 35 | # Get the grid points and values from the response dataset 36 | grid_points = (resp.emi_lat.values, resp.emi_plev.values) 37 | # Transposition necessary since numpy broadcasting 38 | # matches dimensions from right (last dimension) 39 | grid_values = ( 40 | np.divide(resp[spec].values.T, resp.emi_air_mass.values.T) 41 | ).T 42 | # Get the locations from the inventory dataset 43 | locations = np.column_stack((inv.lat.values, inv.plev.values)) 44 | # Use the scipy.interpolate.interpn function to interpolate the response 45 | # data to the inventory locations 46 | weights_arr = interpn( 47 | grid_points, 48 | grid_values, 49 | locations, 50 | method="linear", 51 | bounds_error=False, 52 | fill_value=None, 53 | ) 54 | # Create the dimensions and attributes for the weights dataset 55 | weights_dims = ["index"] 56 | for dim_name in resp[spec].dims: 57 | if dim_name not in ["emi_lat", "emi_plev"]: 58 | weights_dims.append(dim_name) 59 | weights_dims = tuple(weights_dims) 60 | weights_attrs = { 61 | "Title": "Weighting factors", 62 | "Species": spec, 63 | } 64 | # Add the response type and inventory year to the attributes 65 | for resp_attrs_key, resp_attrs_value in resp.attrs.items(): 66 | if resp_attrs_key == "resp_type": 67 | weights_attrs[resp_attrs_key] = resp_attrs_value 68 | if resp_attrs_value == "rf": 69 | weights_units = "W/m²/kg" 70 | elif resp_attrs_value == "conc": 71 | weights_units = "mol/mol/kg" 72 | elif resp_attrs_value == "tau": 73 | weights_units = "1/yr/kg" 74 | else: 75 | weights_units = "undefined" 76 | for inv_attrs_key, inv_attrs_value in inv.attrs.items(): 77 | if inv_attrs_key == "Inventory_Year": 78 | weights_attrs[inv_attrs_key] = inv_attrs_value 79 | # Create the weights dataset 80 | weights_ds = xr.Dataset( 81 | data_vars={ 82 | "lat": inv.lat, 83 | "plev": inv.plev, 84 | "weights": ( 85 | weights_dims, 86 | weights_arr, 87 | { 88 | "long_name": "weights", 89 | "units": weights_units, 90 | }, 91 | ), 92 | }, 93 | attrs=weights_attrs, 94 | ) 95 | return weights_ds 96 | 97 | 98 | def find_weights(spec, resp, inv): 99 | # TODO Debug find_weights --> cache files are newly created although already present 100 | """Find weighting parameters on response grid for entire emission inventory, 101 | response grid is 2d with dimensions lat and plev 102 | First, query checksum table to get pre-calculated weights from cache 103 | If weights not pre-calculated for resp / inv combination, execute calc_weights 104 | 105 | Args: 106 | spec (str): Name of the species 107 | resp (xarray): Response Dataset with lat and plev dimensions 108 | inv (xarray): Emission inventory Dataset 109 | 110 | Returns: 111 | xarray: Dataset with weighting parameters 112 | """ 113 | checksum_path = CHECKSUM_PATH 114 | weights, index = query_checksum_table(spec, resp, inv) 115 | if weights is None: 116 | cache_file = checksum_path + f"{index:03}" + ".nc" 117 | weights = calc_weights(spec, resp, inv) 118 | weights.to_netcdf(cache_file) 119 | update_checksum_table(spec, resp, inv, cache_file) 120 | return weights 121 | -------------------------------------------------------------------------------- /docs/source/terms-of-use.rst: -------------------------------------------------------------------------------- 1 | Terms of Use 2 | ============ 3 | 4 | 1. Copyright and Terms of Use 5 | ----------------------------- 6 | 7 | The copyrights to all copyrighted content contained in this public online presence (i.e. in this website, internet application, app or this social media channel) such as - but not restricted to -, photographs, videos, images and texts are held by the Deutsches Zentrum fuer Luft- und Raumfahrt e. V. (DLR), the German Aerospace Center, unless another copyright owner is indicated. 8 | Thus, "source: DLR" means that the copyrights belong to the Deutsches Zentrum für Luft- und Raumfahrt e. V. (DLR). 9 | In each individual case and before using the material, the indicated copyright holders must be asked for permission (for a license) to use the material. 10 | 11 | 1.a. Asking for a permission to use if the copyrights are vested in DLR 12 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | 14 | If you would like to ask for an individual permission (license) to use a copyrighted content contained in this public Internet presence and if DLR is indicated as the respective copyright holder, please contact: 15 | 16 | | Deutsches Zentrum für Luft- und Raumfahrt e. V. (DLR) 17 | | Communication and Press 18 | | Linder Hoehe 19 | | 51147 Cologne (Koeln) 20 | | Germany 21 | | 22 | | Phone: +49 2203 601-2116 23 | | Fax: +49 2203 601-3249 24 | | E-Mail: bildredaktion [at] dlr.de 25 | 26 | 1.b. Rights of use for copyrighted content if DLR is not the copyright holder 27 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 28 | 29 | Certain photographs, videos, images or texts on the DLR Internet presence stem from other sources than DLR. 30 | Please contact the indicated copyright holders for information on their terms of use and for obtaining a license for use of their copyrighted content. 31 | 32 | Certain photographs, videos, images or texts have multiple copyright owners. 33 | These are indicated accordingly as originating from several rights holders. 34 | In this case, usually the terms of use of all copyright holders indicated in the source information apply. 35 | In case of doubt, please contact all indicated copyright holders. 36 | 37 | 2. As the case may be: Creative Commons license (CC license) 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 39 | Where expressly stated (in the 'information' section of the respective work, for example), DLR images and videos are covered by a `Creative Commons Attribution-NonCommercial-NoDerivatives 3.0 Germany (CC BY-NC-ND 3.0) licence `_. 40 | This licence grants permission to reproduce or distribute the work and to make the work and/or its contents publicly available, provided that you explicitly mention DLR as its source in a clearly legible format. 41 | Examples: 'Photo: DLR, CC BY-NC-ND 3.0', 'Images: DLR, CC BY-NC-ND 3.0', 'Video: DLR, CC BY-NC-ND 3.0'. 42 | However, you may not alter or edit the work and/or its contents or make commercial use of the work. 43 | 44 | Photographs, videos, images and texts that contain no reference to a CC license and for which only the respective copyright holders are indicated as the source (e.g: "Source: DLR") are not licensed under a CC license. 45 | 46 | 3. Use of DLR content by the press, radio and television stations 47 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 48 | 49 | Contents published in the DLR Internet presence for which no application of certain terms of use is prescribed, - i.e. in cases in which no terms of use are indicated to be applicable at all or in cases in which DLR is indicated as the copyright holder without any reference to specifically applicable terms of use (examples: "Image: DLR", "Video: DLR") - may be used by the press, radio stations and television stations, also in their internet presences if DLR is indicated as the copyright holder. 50 | This permission does not include usage of personal data such as photos or videos in which people can be identified or other personal information such as names, e-mail addresses, etc.. 51 | 52 | 4. No restriction of the rights of others 53 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 54 | 55 | These DLR terms of use have no influence whatsoever on the following rights: 56 | 57 | - The rights that everyone has due to the limitations of copyright law (such as the right to quote) or legal permits (established in some countries as the doctrine of fair use) 58 | - The originator´s moral rights 59 | - Rights of other people, either to the licensed object itself or with regard to its use, for example the personal rights of persons depicted, trademark rights to protected trademarks, etc.. 60 | 61 | 5. No usage rights for certain content 62 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 63 | 64 | Any license to use copyrighted material, including the Creative Commons license, extends only to the use of copyrighted material and does not include the permission to use protected trademarks - such as - the DLR logo and its components as well as other elements of the DLR corporate identity such as the “Blue Earth”. 65 | The DLR logo and its components may not be used by persons who are not DLR employees or who have not obtained permission from DLR to use it beforehand. 66 | If you are interested in using a trademark you have to ask the owner of the trademark rights separately for permission of use. 67 | -------------------------------------------------------------------------------- /docs/source/accessibility-statement.rst: -------------------------------------------------------------------------------- 1 | English version below: `Accessibility statement`_ 2 | 3 | Erklärung zur Barrierefreiheit 4 | ============================== 5 | 6 | Diese Erklärung zur Barrierefreiheit gilt für die unter `openairclim.org `_ veröffentlichten Website des Deutschen Zentrums für Luft und Raumfahrt e. V. (DLR). 7 | 8 | Als öffentliche Stelle des Bundes sind wir bemüht, unsere Websites und mobilen Anwendungen im Einklang mit den Bestimmungen des Behindertengleichstellungsgesetzes des Bundes (BGG) sowie der Barrierefreien-Informationstechnik-Verordnung (BITV 2.0) zur Umsetzung der Richtlinie (EU) 2016/2102 barrierefrei zugänglich zu machen. 9 | 10 | Stand der Vereinbarkeit mit den Anforderungen 11 | --------------------------------------------- 12 | 13 | Die Anforderungen der Barrierefreiheit ergeben sich aus §§ 3 Absätze 1 bis 4 und 4 der BITV 2.0, die auf der Grundlage von § 12d BGG erlassen wurde. 14 | 15 | Die Website ist mit den zuvor genannten Anforderungen derzeit nicht vollständig vereinbar. 16 | 17 | Die Website wird aktuell hinsichtlich der Barrierefreiheit geprüft. 18 | Wir bemühen uns, alle festgestellten Probleme der Zugänglichkeit zu beheben. 19 | 20 | Datum der Erstellung bzw. der letzten Aktualisierung der Erklärung 21 | ------------------------------------------------------------------ 22 | 23 | Diese Erklärung wurde am 10. Juli 2025 erstellt und zuletzt am 8. September 2025 aktualisiert. 24 | 25 | Barrieren melden: Kontakt zu den Feedback Ansprechpartnern 26 | ---------------------------------------------------------- 27 | 28 | Sie möchten uns bestehende Barrieren mitteilen oder Informationen zur Umsetzung der Barrierefreiheit erfragen? 29 | Für Ihr Feedback sowie alle weiteren Informationen nutzen Sie bitte das `Kontakt-Formular `_. 30 | 31 | Schlichtungsverfahren 32 | --------------------- 33 | 34 | Wenn auch nach Ihrem Feedback an den oben genannten Kontakt keine zufriedenstellende Lösung gefunden wurde, können Sie sich an die Schlichtungsstelle nach § 16 BGG wenden. 35 | Die Schlichtungsstelle BGG hat die Aufgabe, bei Konflikten zum Thema Barrierefreiheit zwischen Menschen mit Behinderungen und öffentlichen Stellen des Bundes eine außergerichtliche Streitbeilegung zu unterstützen. 36 | Das Schlichtungsverfahren ist kostenlos. Es muss kein Rechtsbeistand eingeschaltet werden. 37 | Weitere Informationen zum Schlichtungsverfahren und den Möglichkeiten der Antragstellung erhalten Sie unter: `www.schlichtungsstelle-bgg.de `_. 38 | 39 | Direkt kontaktieren können Sie die Schlichtungsstelle BGG unter `info@schlichtungsstelle-bgg.de `_. 40 | 41 | 42 | Accessibility statement 43 | ======================= 44 | 45 | This accessibility statement is valid for the website of `openairclim.org `_ from the Deutsches Zentrums für Luft und Raumfahrt e. V. (DLR). 46 | 47 | As a public sector body of the German Federal Government within the meaning of EU Directive 2016/2102, we endeavour to make our websites and mobile applications accessible in accordance with the provisions set out in the German Equality for Persons with Disabilities Act (BGG) and the German Barrier-Free Information Technology Ordinance (BITV 2.0) in order to implement EU Directive 2016/2102. 48 | 49 | Comliance status 50 | ---------------- 51 | 52 | The accessibility requirements stem from Sections 3 (1) to (4) and 4 of BITV 2.0, adopted on the basis of Section 12d BGG. 53 | 54 | At present, the website is partially compliant with the aforementioned requirements. The website is currently being reviewed with regard to accessibility. 55 | It is therefore possible that not all accessibility requirements have been fully met. We are working on addressing all identified accessibility problems. 56 | 57 | Preparation of this accessibility statement 58 | ------------------------------------------- 59 | 60 | This statement was prepared on July 10th, 2025 and last updated on September 8th, 2025. 61 | 62 | Reporting barriers: feedback and contact information 63 | ---------------------------------------------------- 64 | 65 | Would you like to notify us of existing barriers or request information regarding the implementation of accessibility? 66 | If you would like to give us your feedback and for further information, please contact us: `contact-form `_. 67 | 68 | Conciliation procedure 69 | ---------------------- 70 | 71 | If no satisfactory solution is found following your feedback, then you can turn to the conciliation body, pursuant to Section 16 BGG. The BGG conciliation body is tasked with helping to settle accessibility-related disputes out of court between people with disabilities and federal public bodies. 72 | The conciliation procedure is free of charge. No legal assistance is required. 73 | 74 | For more information on the conciliation procedure and the options for filing an application, please visit: `www.schlichtungsstelle-bgg.de `_ 75 | 76 | You can contact the BGG conciliation body directly at: `info@schlichtungsstelle-bgg.de `_ 77 | 78 | DISCLAIMER: The English version is a translation of the original in German for information purposes only. 79 | In case of a discrepancy, the German original will prevail. 80 | -------------------------------------------------------------------------------- /openairclim/plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plot routines for the OpenAirClim framework 3 | """ 4 | 5 | import re 6 | import matplotlib.pyplot as plt 7 | 8 | 9 | # %config InlineBackend.figure_format='retina' 10 | BINS = 50 11 | 12 | 13 | def plot_inventory_vertical_profiles(inv_dict): 14 | """Plots vertical emission profiles of dictionary of inventories 15 | 16 | Args: 17 | inv_dict (dict): Dictionary of xarray Datasets, 18 | keys are years of emission inventories 19 | """ 20 | n_inv = len(inv_dict.keys()) 21 | fig, axs = plt.subplots( 22 | ncols=n_inv, sharex=True, sharey=True, num="Inventories" 23 | ) 24 | if n_inv == 1: 25 | year, inv = next(iter(inv_dict.items())) 26 | axs.hist( 27 | inv.plev.values, 28 | bins=BINS, 29 | weights=inv.fuel.values, 30 | histtype="step", 31 | orientation="horizontal", 32 | ) 33 | axs.set_title(year) 34 | axs.grid(True) 35 | # axs.set_xlabel("fuel (kg)") 36 | # axs.set_ylabel("plev (hPa)") 37 | else: 38 | # axs[0].set_xlabel("fuel (kg)") 39 | # axs[0].set_ylabel("plev (hPa)") 40 | i = 0 41 | for year, inv in inv_dict.items(): 42 | axs[i].hist( 43 | inv.plev.values, 44 | bins=BINS, 45 | weights=inv.fuel.values, 46 | histtype="step", 47 | orientation="horizontal", 48 | ) 49 | axs[i].set_title(year) 50 | axs[i].grid(True) 51 | i = i + 1 52 | fig.supxlabel("fuel (kg)") 53 | fig.supylabel("plev (hPa)") 54 | plt.gca().invert_yaxis() 55 | plt.show() 56 | 57 | 58 | def plot_results(config, result_dic, ac="TOTAL", **kwargs): 59 | """Plots results from dictionary of xarrays 60 | 61 | Args: 62 | config (dic): Configuration dictionary from config file 63 | result_dic (dic): Dictionary of xarrays 64 | ac (str, optional): Aircraft identifier, defaults to TOTAL 65 | **kwargs (Line2D properties, optional): kwargs are parsed to matplotlib 66 | plot command to specify properties like a line label, linewidth, 67 | antialiasing, marker face color 68 | 69 | Raises: 70 | IndexError: If more than 9 subplots per species are parsed 71 | """ 72 | title = config["output"]["name"] 73 | output_dir = config["output"]["dir"] 74 | for result_name, result in result_dic.items(): 75 | # handle multi-aircraft results 76 | if "ac" in result.dims: 77 | if ac in result.coords["ac"].values: 78 | result = result.sel(ac=ac) 79 | else: 80 | raise ValueError( 81 | f"'ac' coordinate exists in {result_name}, but no '{ac}'" 82 | "entry found." 83 | ) 84 | fig_dic = {} 85 | pattern = "(.+)_(.+)" 86 | # Get prefixes (metric) and suffixes (species) 87 | for var_name in result.keys(): 88 | match = re.search(pattern, var_name) 89 | var_type = match.group(1) 90 | var_spec = match.group(2) 91 | # Get the names of different species 92 | if var_spec not in fig_dic: 93 | fig_dic.update({var_spec: []}) 94 | else: 95 | pass 96 | fig_dic[var_spec].append(var_type) 97 | # Iterate over species and metrics 98 | for spec, var_type_arr in fig_dic.items(): 99 | # Get number of required rows and columns for suplots 100 | num_plots = len(var_type_arr) 101 | if num_plots == 1: 102 | num_rows = 1 103 | num_cols = 1 104 | elif num_plots == 2: 105 | num_rows = 1 106 | num_cols = 2 107 | elif num_plots in (3, 4): 108 | num_rows = 2 109 | num_cols = 2 110 | elif num_plots in range(5, 10): 111 | num_rows = 3 112 | num_cols = 3 113 | else: 114 | raise ValueError( 115 | "Number of plots per species is limited to 9." 116 | ) 117 | # Generate figure and subplots 118 | fig = plt.figure((title + ": " + spec)) 119 | # fig.tight_layout() 120 | plt_i = 1 121 | for var_type in var_type_arr: 122 | axis = fig.add_subplot(num_rows, num_cols, plt_i) 123 | result[var_type + "_" + spec].plot(**kwargs) 124 | axis.ticklabel_format(axis="y", scilimits=(-3, 3)) 125 | axis.grid(True) 126 | plt_i = plt_i + 1 127 | fig.savefig(output_dir + result_name + "_" + spec + ".png") 128 | plt.show() 129 | 130 | 131 | def plot_concentrations(config, spec, conc_dict): 132 | """Plot species concentration change colormaps, one colormap for each year 133 | 134 | Args: 135 | config (dic): Configuration dictionary from config file 136 | spec (str): Species name 137 | conc_dict (dict): Dictionary of time series numpy arrays (time, lat, plev), 138 | keys are species 139 | """ 140 | output_dir = config["output"]["dir"] 141 | conc = conc_dict[spec][("conc_" + spec)] 142 | plot_object = conc.plot(x="lat", y="plev", col="time", col_wrap=4) 143 | axs = plt.gca() 144 | axs.invert_yaxis() 145 | fig = plot_object.fig 146 | fig.canvas.manager.set_window_title(spec) 147 | fig.savefig(output_dir + "conc_" + spec + ".png") 148 | plt.show() 149 | -------------------------------------------------------------------------------- /tests/read_netcdf_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module read_netcdf 3 | """ 4 | 5 | import os 6 | import xarray as xr 7 | import pytest 8 | import openairclim as oac 9 | from utils.create_test_data import create_test_inv 10 | 11 | # from unittest.mock import patch 12 | 13 | abspath = os.path.abspath(__file__) 14 | dname = os.path.dirname(abspath) 15 | os.chdir(dname) 16 | 17 | # CONSTANTS 18 | REPO_PATH = "repository/" 19 | INV_NAME = "test_inv.nc" 20 | BG_NAME = "co2_bg.nc" 21 | 22 | 23 | @pytest.fixture(name="open_nc", scope="class") 24 | def fixture_open_nc(): 25 | """Open netCDF file for multiple tests 26 | 27 | Returns: 28 | dict: Dictionary of xarrays 29 | """ 30 | xr_dict = oac.open_netcdf((REPO_PATH + BG_NAME)) 31 | return xr_dict 32 | 33 | 34 | @pytest.mark.usefixtures("open_nc") 35 | class TestOpenNetcdf: 36 | """Tests function open_netcdf(netcdf)""" 37 | 38 | def test_type(self, open_nc): 39 | """Open netcdf file and test if output is of type dictionary""" 40 | xr_dict = open_nc 41 | assert isinstance(xr_dict, dict) 42 | 43 | def test_key(self, open_nc): 44 | """Open netcdf file and test if keys of dictionary are input file basenames""" 45 | xr_dict = open_nc 46 | assert "co2_bg" in xr_dict 47 | 48 | def test_xarray(self, open_nc): 49 | """Open netcdf file and test if dictionary values are of type xarray.Dataset""" 50 | xr_dict = open_nc 51 | val = xr_dict["co2_bg"] 52 | assert isinstance(val, xr.Dataset) 53 | 54 | 55 | @pytest.fixture(name="setup_arguments", scope="class") 56 | def fixture_setup_arguments(): 57 | """Setup config and inv_dict arguments for check_spec_attributes 58 | 59 | Returns: 60 | dict: Configuration dictionary from config 61 | dict: Dictionary of inventory xarrays, keys are years of input inventories 62 | """ 63 | config = {"species": {"inv": ["CO2"], "nox": "NO", "out": ["CO2"]}} 64 | file_path = REPO_PATH + INV_NAME 65 | inv = xr.load_dataset(file_path) 66 | key = inv.attrs["Inventory_Year"] 67 | inv_dict = {key: inv} 68 | return config, inv_dict 69 | 70 | 71 | @pytest.mark.usefixtures("setup_arguments") 72 | class TestCheckSpecAttributes: 73 | """Tests function check_spec_attributes(config, inv_dict)""" 74 | 75 | def test_correct_input(self, setup_arguments): 76 | "Correct input returns no Error" 77 | config, inv_dict = setup_arguments 78 | oac.check_spec_attributes(config, inv_dict) 79 | 80 | def test_no_attributes(self, setup_arguments): 81 | """Missing attributes in inventory for species raises KeyError""" 82 | config, inv_dict = setup_arguments 83 | inv_dict[2020]["CO2"].attrs = {} 84 | with pytest.raises(KeyError): 85 | oac.check_spec_attributes(config, inv_dict) 86 | 87 | def test_no_units(self, setup_arguments): 88 | """Missing units in inventory for species raises KeyError""" 89 | config, inv_dict = setup_arguments 90 | inv_dict[2020]["CO2"].attrs = {"long_name": "CO2"} 91 | with pytest.raises(KeyError): 92 | oac.check_spec_attributes(config, inv_dict) 93 | 94 | def test_incorrect_units(self, setup_arguments): 95 | """Incorrect units in inventory for species raises KeyError""" 96 | config, inv_dict = setup_arguments 97 | inv_dict[2020]["CO2"].attrs["units"] = "incorrect-unit" 98 | with pytest.raises(KeyError): 99 | oac.check_spec_attributes(config, inv_dict) 100 | 101 | 102 | class TestSplitInventoryByAircraft: 103 | """Tests function split_inventory_by_aircraft(config, inv_dict).""" 104 | 105 | @pytest.fixture(scope="class") 106 | def inv_dict(self): 107 | """Fixture to create an example inv_dict.""" 108 | ac_lst = ["LR", "REG"] 109 | return {2020: create_test_inv(year=2020, size=100, ac_lst=ac_lst), 110 | 2030: create_test_inv(year=2030, size=100, ac_lst=ac_lst), 111 | 2040: create_test_inv(year=2040, size=100, ac_lst=ac_lst), 112 | 2050: create_test_inv(year=2050, size=100, ac_lst=ac_lst)} 113 | 114 | @pytest.fixture(scope="class") 115 | def inv_dict_no_ac(self): 116 | """Fixture to create an example inv_dict without ac coordinate.""" 117 | return {2020: create_test_inv(year=2020, size=100), 118 | 2030: create_test_inv(year=2030, size=100), 119 | 2040: create_test_inv(year=2040, size=100), 120 | 2050: create_test_inv(year=2050, size=100)} 121 | 122 | def test_valid_aircraft(self, inv_dict): 123 | """Tests function with valid aircraft identifiers.""" 124 | config = {"species": {"out": ["CO2"]}, 125 | "aircraft": {"types": ["LR", "REG"]}} 126 | result = oac.split_inventory_by_aircraft(config, inv_dict) 127 | assert "LR" in result 128 | assert "REG" in result 129 | assert 2020 in result["LR"] 130 | assert isinstance(result["LR"][2020], xr.Dataset) 131 | assert "ac" in result["LR"][2020].data_vars 132 | assert set(result["LR"][2020].ac.data) == {"LR"} 133 | 134 | def test_missing_aircraft(self, inv_dict_no_ac): 135 | """Tests function when inv_dict does not have ac data variable.""" 136 | # do not include cont as output 137 | config = {"species": {"out": []}, 138 | "aircraft": {"types": ["LR", "REG"]}} 139 | result = oac.split_inventory_by_aircraft(config, inv_dict_no_ac) 140 | assert "TOTAL" in result 141 | assert 2020 in result["TOTAL"] 142 | assert isinstance(result["TOTAL"][2020], xr.Dataset) 143 | 144 | def test_missing_contrail_vars(self, inv_dict_no_ac): 145 | """Tests missing contrail variables in config.""" 146 | config = {"species": {"out": ["cont"]}, 147 | "aircraft": {"types": []}} 148 | with pytest.raises(ValueError, match="No ac data variable"): 149 | oac.split_inventory_by_aircraft(config, inv_dict_no_ac) 150 | -------------------------------------------------------------------------------- /openairclim/calc_response.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates responses for each species and scenario 3 | """ 4 | 5 | import logging 6 | import numpy as np 7 | from openairclim.interpolate_space import calc_weights 8 | from openairclim.calc_ch4 import calc_pmo_rf 9 | 10 | 11 | # CONSTANTS 12 | # 13 | # Conversion table: out_species (response species) to inv_species (inventory species) 14 | OUT_INV_DICT = {"CO2": "CO2", "H2O": "H2O", "O3": "NOx", "CH4": "NOx"} 15 | # 16 | # CORRECTION (normalization) factors 17 | # 18 | # Correction H2O emission --> H2O concentration 19 | # Correction factor from AirClim 20 | # TODO Check correction factor 21 | # no correction seconds in year? units mol/mol or ppbv? 22 | # CORR_CONC_H2O = 1.0 / 125.0e-15 23 | # assuming ppbv as units for response surfaces: 24 | CORR_CONC_H2O = 1.0e-9 / 125.0e-15 25 | # 26 | # Correction factor for NO2 inventory emissions (instead NO) 27 | CORR_NO2 = 30.0 / 46.0 28 | # 29 | # Correction NOx emission --> O3 concentration 30 | # EMAC input setting: emission strength for box regions was 31 | # eps = 6.877E-16 kg(NO)/kg(air)/s 32 | # This translates to an emission strength for one year: 33 | # eps * (365 * 24 * 3600) 34 | # 35 | # Correction factor for O3 concentration, tagging 36 | # TODO Check if air mass normalization properly implemented --> calc_weights() 37 | CORR_CONC_O3 = 1.0 / (6.877e-16 * 365 * 24 * 3600) 38 | # 39 | # Correction factor for RF H2O, AirClim (perturbation) 40 | # 41 | # Scaling of water vapour radiative forcing by 1.5 according to findings from 42 | # De Forster, P. M., Ponater, M., & Zhong, W. Y. (2001). Testing broadband radiation schemes 43 | # for their ability to calculate the radiative forcing and temperature response to 44 | # stratospheric water vapour and ozone changes. Meteorologische Zeitschrift, 10(5), 387-393. 45 | # see also: Fichter, C. (2009). Climate impact of air traffic emissions in dependency of the 46 | # emission location and altitude. DLR. PhD thesis, Chapter 6.2 47 | # 48 | # CORR_RF_H2O = 1.5 / (31536000.0 * 125.0e-15) 49 | CORR_RF_H2O = 380517.5038 50 | # 51 | # Correction factor for RF O3, tagging 52 | CORR_RF_O3 = CORR_CONC_O3 53 | # Warning message if tagging response surface is used 54 | if CORR_RF_O3 == CORR_CONC_O3: 55 | logging.warning("O3 response surface is not validated!") 56 | # 57 | # Correction factor for RF O3, AirClim (perturbation) 58 | # CORR_RF_O3 = 1.0 / (31536000.0 * 0.45e-15) 59 | # CORR_RF_O3 = 70466204.41 60 | # 61 | # Correction factor for tau CH4, tagging 62 | CORR_TAU_CH4 = CORR_CONC_O3 63 | 64 | 65 | def calc_resp(spec: str, inv, weights) -> np.ndarray: 66 | """ 67 | Calculate response from response surfaces, emission inventories 68 | and pre-computed weighting parameters. 69 | 70 | Args: 71 | spec (str): Name of response species 72 | inv (xarray.Dataset): Emission inventory data 73 | weights (xarray.Dataset): Dataset with weighting parameters 74 | Raises: 75 | KeyError: if species not valid 76 | 77 | Returns: 78 | np.ndarray: Response array 79 | """ 80 | inv_spec = OUT_INV_DICT[spec] 81 | inv_arr = inv[inv_spec].values 82 | weights_arr = weights["weights"].values 83 | if spec in ["H2O", "O3", "CH4"]: 84 | pass 85 | else: 86 | raise KeyError("calculating response: species not valid") 87 | # Elememt-wise multiplication of inventory emissions and weights 88 | out_arr = (np.multiply(inv_arr.T, weights_arr.T)).T 89 | # Sum over index axis (all steps in emission inventory) 90 | out_arr = np.sum(out_arr, axis=0) 91 | return out_arr 92 | 93 | 94 | def calc_resp_all(config, resp_dict, inv_dict): 95 | """Loop calc_response function over elements in response dictionary 96 | 97 | Args: 98 | config (dict): Configuration dictionary from config 99 | resp_dict (dict): Dictionary of response xarray Datasets, keys are species 100 | inv_dict (dict): Dictionary of inventory xarray Datasets, keys are years 101 | 102 | Returns: 103 | dict: Dictionary of dictionary of numpy arrays of computed responses, 104 | keys are species and inventory years 105 | """ 106 | # "NO" or "NO2" in emission inventory 107 | nox = config["species"]["nox"] 108 | if nox == "NO": 109 | corr_nox = 1.0 110 | elif nox == "NO2": 111 | corr_nox = CORR_NO2 112 | else: 113 | raise KeyError("Invalid NOx assumption in config['species']['nox'].") 114 | # default correction factor 115 | corr = 1.0 116 | out_dict = {} 117 | for spec, resp in resp_dict.items(): 118 | # resp_type (str): "conc" or "rf" 119 | resp_type = resp.attrs["resp_type"] 120 | if resp_type in "conc": 121 | if spec == "H2O": 122 | corr = CORR_CONC_H2O 123 | elif spec == "O3": 124 | corr = CORR_CONC_O3 * corr_nox 125 | elif resp_type == "rf": 126 | if spec == "H2O": 127 | corr = CORR_RF_H2O 128 | elif spec == "O3": 129 | corr = CORR_RF_O3 * corr_nox 130 | elif resp_type == "tau": 131 | if spec == "CH4": 132 | corr = CORR_TAU_CH4 * corr_nox 133 | else: 134 | raise ValueError("resp_type not valid") 135 | out_inv_dict = {} 136 | for inv in inv_dict.values(): 137 | year = inv.attrs["Inventory_Year"] 138 | weights = calc_weights(spec, resp, inv) 139 | # weights = find_weights(spec, resp, inv) 140 | out_arr = corr * calc_resp(spec, inv, weights) 141 | # conc = np.sum(conc_arr) 142 | out_inv_dict[year] = out_arr 143 | out_dict[spec] = out_inv_dict 144 | return out_dict 145 | 146 | 147 | def calc_resp_sub(species_sub, output_dict, ac): 148 | """ 149 | Calculates responses for specified sub-species. 150 | The calculation of sub-species responses depends on the results 151 | of main species which must be calculated and written to output beforehand. 152 | 153 | Args: 154 | species_sub (list[str]): List of sub-species names, such as 'PMO' 155 | 156 | Returns: 157 | dict: Dictionary with computed responses, keys are sub-species 158 | 159 | Raises: 160 | KeyError: If no method defined for the sub-species 161 | """ 162 | # Get results computed for other species 163 | rf_sub_dict = {} 164 | for spec in species_sub: 165 | if spec == "PMO": 166 | rf_pmo_dict = calc_pmo_rf(output_dict[ac]) 167 | rf_sub_dict = rf_sub_dict | rf_pmo_dict 168 | else: 169 | msg = "No method defined for sub species " + spec 170 | raise KeyError(msg) 171 | return rf_sub_dict 172 | -------------------------------------------------------------------------------- /docs/source/user_guide/03_contrails.rst: -------------------------------------------------------------------------------- 1 | Contrail Module 2 | =============== 3 | 4 | This webpage describes how to run the contrail module. 5 | More information about the scientific background can be found `here <../background/contrails.html>`_. 6 | 7 | 8 | Configuration File 9 | ------------------ 10 | 11 | .. warning:: 12 | 13 | It is currently not possible to calculate the contrail climate impact for multiple different aircraft within the same emission inventory. 14 | This is the subject of ongoing work. 15 | 16 | 17 | In the species section, the following need to be selected: 18 | 19 | .. code:: toml 20 | 21 | [species] 22 | inv = ["...", "distance"] 23 | out = ["...", "cont"] 24 | 25 | This tells OpenAirClim that the "distance" variable in the input emission inventories is to be used and that the contrail climate impact should be calculated. 26 | 27 | The emission inventories should of course be defined as normal. 28 | In addition, the variable ``rel_to_base`` need to be defined: if ``false``, then only the emission inventories in ``files`` are considered; if ``true``, then the base emission inventories are also used. 29 | The base emission inventories can be used to simulate background air traffic. 30 | For example, if the contrail climate impact of a single aircraft design is to be calculated, then the base emission inventories could be the remaining air traffic. 31 | This is important because the contrail climate impact is highly non-linear. 32 | 33 | So, for example, the following would be the setup if the contrail climate impact of an A320 with background air traffic were to be simulated: 34 | 35 | .. code:: toml 36 | 37 | [inventories] 38 | dir = "path/to/inventories" 39 | files = [ 40 | "a320_inv_2020.nc", 41 | "...", 42 | "a320_inv_2045.nc", 43 | "a320_inv_2050.nc", 44 | ] 45 | rel_to_base = true 46 | base.dir = "path/to/inventories" 47 | base.files = [ 48 | "bg_inv_2020.nc", 49 | "bg_inv_2050.nc", 50 | ] 51 | 52 | The requirement is that the emission inventories and base emission inventories must align at the first and last year, in this case in 2020 and 2050. 53 | OpenAirClim features a built-in interpolator for base emission inventories. 54 | The above configuration will thus work without issue - OpenAirClim will interpolate the base emission inventories onto the years defined by the A320 emission inventories. 55 | 56 | In the response options, the following values are relevant for the contrail module: 57 | 58 | .. code:: toml 59 | 60 | [responses] 61 | dir = "path/to/responses" 62 | cont.response_grid = "cont" # should not be changed 63 | cont.resp.file = "resp_cont_lf.nc" 64 | # cont.method = "Megill_2025" # this method is chosen by default 65 | 66 | A conventional OpenAirClim configuration uses the above values. 67 | The file ``resp_cont.nc`` can be used in conjuction with ``cont.method="AirClim"`` to simulate the AirClim contrail module for testing. 68 | However, this is not generally recommended outside of unit testing, because 1) the simulated AirClim module is restrictive in its input and 2) the OpenAirClim contrail module includes many improvements. 69 | 70 | The contrail efficacy can be adapted using: 71 | 72 | .. code:: toml 73 | 74 | [temperature] 75 | cont.efficacy = 0.59 76 | 77 | Finally, the aircraft and three variables ``G_250``, ``eff_fac`` and ``PMrel`` must be defined. 78 | From OpenAirClim v0.11.0 onwards, the aircraft types are active. 79 | In principle, any aircraft identifier (except ``"TOTAL"``) can be selected, except that the first character must be a letter to comply with python. 80 | These identifiers must match with those present in the input emission inventories (see the next section). 81 | If no identifiers are present in the emission inventories, please use ``types = ["DEFAULT"]``. 82 | 83 | .. warning:: 84 | 85 | It is currently not possible to calculate the contrail climate impact of multiple different aircraft within the same emission inventory. 86 | If multiple different types are given whilst simultaneously including ``species.out = ["...", "cont"]``, OpenAirClim will produce an error. 87 | 88 | .. code:: toml 89 | 90 | [aircraft] 91 | types = ["A320", "B737"] 92 | # dir = "input/" 93 | # file = "ac_def.csv" 94 | A320.G_250 = 1.90 95 | A320.eff_fac = 1.1 96 | A320.PMrel = 1.0 97 | B737.G_250 = 1.85 98 | B737.eff_fac = 1.05 99 | B737.PMrel = 0.8 100 | 101 | It is also possible to define these parameters in an external .csv file. 102 | To do so, uncomment and update the ``dir`` and ``file`` values. 103 | The .csv file must have the columns ``"ac"`` and ``"eff_fac"``. 104 | Additionally, the file must either have the columns ``"G_250"`` and ``"PMrel"``, or additional information such that OpenAirClim can calculate these values online. 105 | For ``G_250``, the following columns must be provided: 106 | 107 | - ``"SAC_eq"``: which equation to use to calculate the SAC slope, choice of ``"CON"``, ``"HYB"``, ``"H2C"`` and ``"H2FC"``. See :cite:`megillInvestigatingLimitingAircraftdesigndependent2025` for more details; 108 | - ``"Q_h"``: Lower Heating Value of the fuel [J/kg] for ``"CON"`` (~43.6 MJ/kg), ``"HYB"``, ``"H2C"`` (~120 MJ/kg); formation enthalpy of water vapour [J/mol] for ``"H2FC"``; 109 | - ``"eta"``: Overall propulsion efficiency of the liquid fuel system (for all except ``"H2FC"``); 110 | - ``"eta_elec"``: Efficiency of the electric/fuel cell system (for ``"HYB"`` and ``"H2FC"``); 111 | - ``"EIH2O"``: Emission index of water vapour [kg/kg] (for all except ``"H2FC"``); 112 | - ``"R"``: Degree of hybridisation (for ``"HYB"``). R=1 is pure liquid fuel operation; R=0 pure electric operation. 113 | 114 | For ``"PMrel"``, a ``"PM"`` column is required, specifying the nvPM (soot) number emission index. 115 | The relative PM emissions are taken with respect to 1.5e15 #/kg. 116 | 117 | If the aircraft characteristics are simultaneously defined in the config and in the .csv file, *the config data will not be overwritten*. 118 | OpenAirClim will warn you if this is the case. 119 | 120 | 121 | Emission Inventories 122 | -------------------- 123 | 124 | To calculate a contrail climate impact, the input emission inventories must include a ``distance`` (float) variable. 125 | This corresponds with the total yearly flown distance (km). 126 | 127 | Optionally, the emission inventories can have a variable ``ac`` (str), corresponding to the aircraft identifiers defined in the configuration file. 128 | If this variable is defined, all identifiers (also in the base emission inventories) **must** be included in the configuration file. 129 | If this variable is not present, OpenAirClim will use the identifier ``DEFAULT``, which must be defined in the configuration file. 130 | -------------------------------------------------------------------------------- /utils/create_test_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Creates data objects for testing 3 | """ 4 | 5 | import sys 6 | import os 7 | import numpy as np 8 | import xarray as xr 9 | 10 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) 11 | sys.path.append(os.path.dirname(SCRIPT_DIR)) 12 | from utils.create_artificial_inventories import ArtificialInventory 13 | 14 | 15 | def create_test_conc_resp(): 16 | """ 17 | Creates an example response dataset for testing purposes 18 | with resp_type = "conc" 19 | 20 | Returns: 21 | xr.Dataset: A minimal response dataset with random data. 22 | """ 23 | lat_arr = np.arange(-75.0, 80.0, 15.0).astype("float32") 24 | dim_lat = len(lat_arr) 25 | plev_arr = np.arange(100.0, 1000.0, 100.0).astype("float32") 26 | dim_plev = len(plev_arr) 27 | emi_lat_arr = np.array([10.0, 40.0], dtype="float32") 28 | emi_plev_arr = np.array([250.0, 500.0], dtype="float32") 29 | emi_loc_arr = np.array([["p10_250", "p10_500"], ["p40_250", "p40_500"]]) 30 | p10_250_arr = np.random.randn(dim_lat, dim_plev).astype("float32") 31 | p40_250_arr = np.random.randn(dim_lat, dim_plev).astype("float32") 32 | p10_500_arr = np.random.randn(dim_lat, dim_plev).astype("float32") 33 | p40_500_arr = np.random.randn(dim_lat, dim_plev).astype("float32") 34 | resp = xr.Dataset( 35 | data_vars={ 36 | "emi_loc": (["emi_lat", "emi_plev"], emi_loc_arr), 37 | "p10_250": (["lat", "plev"], p10_250_arr), 38 | "p40_250": (["lat", "plev"], p40_250_arr), 39 | "p10_500": (["lat", "plev"], p10_500_arr), 40 | "p40_500": (["lat", "plev"], p40_500_arr), 41 | }, 42 | coords={ 43 | "lat": lat_arr, 44 | "plev": plev_arr, 45 | "emi_lat": emi_lat_arr, 46 | "emi_plev": emi_plev_arr, 47 | }, 48 | attrs={"resp_type": "conc"}, 49 | ) 50 | return resp 51 | 52 | 53 | def create_test_rf_resp(): 54 | """ 55 | Creates an example response dataset for testing purposes 56 | with resp_type = "rf" 57 | 58 | Returns: 59 | xr.Dataset: A minimal response dataset with random data. 60 | """ 61 | emi_lat_arr = np.array([10.0, 40.0], dtype="float32") 62 | emi_plev_arr = np.array([250.0, 500.0], dtype="float32") 63 | emi_loc_arr = np.array([["p10_250", "p10_500"], ["p40_250", "p40_500"]]) 64 | h2o_arr = np.random.rand(len(emi_lat_arr), len(emi_plev_arr)).astype( 65 | "float32" 66 | ) 67 | emi_air_mass_arr = np.ones_like(h2o_arr) 68 | resp = xr.Dataset( 69 | data_vars={ 70 | "emi_air_mass": (["emi_lat", "emi_plev"], emi_air_mass_arr), 71 | "emi_loc": (["emi_lat", "emi_plev"], emi_loc_arr), 72 | "H2O": (["emi_lat", "emi_plev"], h2o_arr), 73 | }, 74 | coords={ 75 | "emi_lat": emi_lat_arr, 76 | "emi_plev": emi_plev_arr, 77 | }, 78 | attrs={"resp_type": "rf"}, 79 | ) 80 | return resp 81 | 82 | 83 | def create_test_inv(year=2020, size=3, ac_lst=None): 84 | """ 85 | Creates an example inventory dataset for testing purposes. 86 | 87 | Args: 88 | year (int): inventory year 89 | size (int): The number of samples to generate. 90 | ac_lst (list, optional): List of aircraft identifiers (strings). 91 | 92 | Returns: 93 | xr.Dataset: An xarray dataset with random inventory data. 94 | 95 | """ 96 | inv = ArtificialInventory(year, size=size, ac_lst=ac_lst).create() 97 | return inv 98 | 99 | 100 | def create_test_resp_cont(method="Megill_2025", n_lat=48, n_lon=96, n_plev=39, seed=None): 101 | """Creates example precalculated contrail input data for testing purposes. 102 | 103 | Args: 104 | method (str, optional): Contrail calculation method. 105 | Options: 'Megill_2025' (default) or 'AirClim'. 106 | n_lat (int, optional): Number of latitude values. Defaults to 48. 107 | n_lon (int, optional): Number of longitude values. Defaults to 96. 108 | n_plev (int, optional): Number of pressure level values. Defaults to 39. 109 | seed (int, optional): Random seed. 110 | 111 | Returns: 112 | xr.Dataset: Example precalculated contrail input data. 113 | """ 114 | 115 | # set random seed 116 | np.random.seed(seed) 117 | 118 | # Create the coordinates 119 | lon = np.linspace(0, 360, n_lon, endpoint=False) 120 | lat = np.linspace(90, -90, n_lat + 2)[1:-1] # do not include 90 or -90 121 | plev = np.sort(np.append(np.linspace(1014, 10, n_plev-1), [250]))[::-1] 122 | 123 | # depending on method, create test resp_cont 124 | # Create the data variables with random values between 0 and 1 125 | assert method in ["Megill_2025", "AirClim"], "Unknown contrail calculation " \ 126 | "method. Should be one of 'Megill_2025' or 'AirClim'." 127 | 128 | iss = np.random.rand(n_lat, n_lon) # independent of method 129 | 130 | if method == "AirClim": 131 | sac_con = np.random.rand(n_lat, n_lon, n_plev) 132 | sac_lh2 = np.random.rand(n_lat, n_lon, n_plev) 133 | 134 | # Combine into an xarray Dataset 135 | ds_cont = xr.Dataset( 136 | { 137 | "ISS": (["lat", "lon"], iss), 138 | "SAC_CON": (["lat", "lon", "plev"], sac_con), 139 | "SAC_LH2": (["lat", "lon", "plev"], sac_lh2) 140 | }, 141 | coords={ 142 | "lon": ("lon", lon), 143 | "lat": ("lat", lat), 144 | "plev": ("plev", plev) 145 | } 146 | ) 147 | 148 | else: # Megill_2025 method 149 | # define aircraft 150 | ac = [f"oac{x}" for x in range(5)] 151 | n_ac = len(ac) 152 | 153 | # create dataset 154 | ds_cont = xr.Dataset( 155 | { 156 | "ISS": (["lat", "lon"], iss), 157 | }, 158 | coords={ 159 | "lon": ("lon", lon), 160 | "lat": ("lat", lat), 161 | "plev": ("plev", plev), 162 | "AC": ("AC", ac) 163 | } 164 | ) 165 | ds_cont.AC.attrs = {"units": "None"} 166 | 167 | # populate dataset 168 | ds_cont["ppcf"] = ( 169 | ("AC", "plev", "lat", "lon"), 170 | np.random.rand(n_ac, n_plev, n_lat, n_lon) 171 | ) 172 | ds_cont["g_250"] = (("AC"), np.random.rand(n_ac)) 173 | fit_vars = ["l_1", "k_1", "x0_1", "d_1", "l_2", "k_2", "x0_2"] 174 | for fit_var in fit_vars: 175 | ds_cont[fit_var] = (("plev"), np.random.rand(n_plev)) 176 | 177 | # add units 178 | ds_cont.lat.attrs = {"units": "degrees_north"} 179 | ds_cont.lon.attrs = {"units": "degrees_east"} 180 | ds_cont.plev.attrs = {"units": "hPa"} 181 | 182 | return ds_cont 183 | -------------------------------------------------------------------------------- /utils/create_time_evolution.py: -------------------------------------------------------------------------------- 1 | """Create netCDF files controlling time evolution: time scaling and time normalization""" 2 | 3 | import os 4 | import numpy as np 5 | import xarray as xr 6 | import matplotlib.pyplot as plt 7 | 8 | # GENERAL CONSTANTS 9 | OUT_PATH = "../example/input/" 10 | 11 | # SCALING CONSTANTS 12 | SCALING_TIME = np.arange(1990, 2200, 1) 13 | SCALING_ARR = np.sin(SCALING_TIME * 0.2) * 0.6 + 1.0 14 | SCALING_ARR = SCALING_ARR.astype("float32") 15 | 16 | # NORMALIZATION CONSTANTS 17 | NORM_TIME = np.array( 18 | [ 19 | 2020, 20 | 2025, 21 | 2030, 22 | 2035, 23 | 2040, 24 | 2045, 25 | 2050, 26 | 2055, 27 | 2060, 28 | 2065, 29 | 2070, 30 | 2075, 31 | 2080, 32 | 2085, 33 | 2090, 34 | 2095, 35 | 2100, 36 | 2105, 37 | 2110, 38 | 2115, 39 | 2120, 40 | ] 41 | ) 42 | # Reference for fuel consumption until year 2050: 43 | # Energy Insights’ Global Energy Perspective, Reference Case A3 October 2020; IATA; ICAO 44 | # (fuel consumption values beyond 2050 are customized) 45 | FUEL_ARR = np.array( 46 | [ 47 | 215, 48 | 364, 49 | 407, 50 | 446, 51 | 479, 52 | 503, 53 | 520, 54 | 536, 55 | 552, 56 | 568, 57 | 585, 58 | 603, 59 | 621, 60 | 639, 61 | 658, 62 | 678, 63 | 699, 64 | 720, 65 | 741, 66 | 763, 67 | 786, 68 | ] 69 | ).astype("float32") 70 | EI_CO2_ARR = 3.115 * np.ones(len(NORM_TIME), dtype="float32") 71 | EI_H2O_ARR = 1.25 * np.ones(len(NORM_TIME), dtype="float32") 72 | DIS_PER_FUEL_ARR = 0.3 * np.ones(len(NORM_TIME), dtype="float32") 73 | 74 | # TIME SCALING 75 | 76 | 77 | def plot_time_scaling(scaling_time: np.ndarray, scaling_arr: np.ndarray): 78 | """ 79 | Plots the time scaling factors. 80 | 81 | Args: 82 | scaling_time (np.ndarray): The time values for the scaling factors. 83 | scaling_arr (np.ndarray): The scaling factors to plot. 84 | 85 | Returns: 86 | None 87 | 88 | """ 89 | _fig, ax = plt.subplots() 90 | ax.plot(scaling_time, scaling_arr) 91 | ax.set_xlabel("year") 92 | ax.set_ylabel("scaling factor") 93 | plt.show() 94 | 95 | 96 | def create_time_scaling_xr( 97 | scaling_time: np.ndarray, scaling_arr: np.ndarray 98 | ) -> xr.Dataset: 99 | """ 100 | Create an xarray dataset containing time scaling factors. 101 | 102 | Args: 103 | scaling_time (np.ndarray): The time values for the scaling factors. 104 | scaling_arr (np.ndarray): The scaling factors to plot. 105 | 106 | Returns: 107 | xr.Dataset: The xarray dataset containing the time scaling factors. 108 | 109 | """ 110 | evolution = xr.Dataset( 111 | data_vars=dict(scaling=(["time"], scaling_arr)), 112 | coords=dict(time=scaling_time), 113 | ) 114 | evolution.time.attrs = {"units": "years"} 115 | evolution.scaling.attrs = {"species": "all"} 116 | evolution.attrs = dict( 117 | Title="Time scaling example", 118 | Convention="CF-XXX", 119 | Type="scaling", 120 | Author="Stefan Völk", 121 | Contact="stefan.voelk@dlr.de", 122 | ) 123 | return evolution 124 | 125 | 126 | # TIME NORMALIZATION 127 | 128 | 129 | def create_time_normalization_xr( 130 | time_arr: np.ndarray, 131 | fuel_arr: np.ndarray, 132 | ei_co2_arr: np.ndarray, 133 | ei_h2o_arr: np.ndarray, 134 | dis_per_fuel_arr: np.ndarray, 135 | ) -> xr.Dataset: 136 | """Create an xarray dataset containing normalization factors 137 | 138 | Args: 139 | time_arr (np.ndarray): Time values (years) 140 | fuel_arr (np.ndarray): Fuel consumption 141 | ei_co2_arr (np.ndarray): Emission indices for CO2 142 | ei_h2o_arr (np.ndarray): Emission indices for H2O 143 | dis_per_fuel_arr (np.ndarray): Distance per fuel 144 | 145 | Returns: 146 | xr.Dataset: The xarray dataset containing the normalization factors 147 | """ 148 | evolution = xr.Dataset( 149 | data_vars=dict( 150 | fuel=(["time"], fuel_arr), 151 | EI_CO2=(["time"], ei_co2_arr), 152 | EI_H2O=(["time"], ei_h2o_arr), 153 | dis_per_fuel=(["time"], dis_per_fuel_arr), 154 | ), 155 | coords=dict(time=time_arr), 156 | ) 157 | evolution.time.attrs = {"units": "years"} 158 | evolution.fuel.attrs = { 159 | "long_name": "fuel consumption", 160 | "units": "Tg yr-1", 161 | } 162 | evolution.EI_CO2.attrs = {"long_name": "CO2 emission index", "units": ""} 163 | evolution.EI_H2O.attrs = {"long_name": "H2O emission index", "units": ""} 164 | evolution.dis_per_fuel.attrs = { 165 | "long_name": "distance per fuel", 166 | "units": "km kg-1", 167 | } 168 | evolution.attrs = dict( 169 | Title="Time normalization example", 170 | Convention="CF-XXX", 171 | Type="norm", 172 | Author="Stefan Völk", 173 | Contact="stefan.voelk@dlr.de", 174 | ) 175 | return evolution 176 | 177 | 178 | def plot_time_norm(evolution): 179 | """Plot normalized values 180 | 181 | Args: 182 | evolution (xr.Dataset): The xarray Dataset containing the normalization factors. 183 | 184 | Returns: 185 | None 186 | """ 187 | co2_emi_arr = np.multiply(evolution.fuel.values, evolution.EI_CO2.values) 188 | 189 | _fig, axs = plt.subplots(nrows=2) 190 | axs[0].grid(True) 191 | axs[1].grid(True) 192 | evolution.fuel.plot.line("-o", ax=axs[0]) 193 | axs[1].plot(evolution.time.values, co2_emi_arr, "-o") 194 | axs[1].set_xlabel("time [years]") 195 | axs[1].set_ylabel("CO2 emissions [Tg]") 196 | plt.show() 197 | 198 | 199 | # WRITE OUTPUT netCDF 200 | def convert_xr_to_nc(ds: xr.Dataset, file_name: str, out_path: str = OUT_PATH): 201 | """ 202 | Convert a xarray dataset to a netCDF file and write to out_path. 203 | Create out_path if not existing. 204 | 205 | Args: 206 | ds (xr.Dataset): The xarray dataset to write to netCDF. 207 | file_name (str): The name of the output file, including the extension. 208 | out_path (str, optional): The path to the output directory. 209 | Defaults to OUT_PATH. 210 | 211 | Returns: 212 | None 213 | """ 214 | os.makedirs(out_path, exist_ok=True) 215 | out_file = out_path + file_name + ".nc" # "time_[scaling|norm]_example.nc" 216 | ds.to_netcdf(out_file) 217 | 218 | 219 | if __name__ == "__main__": 220 | scaling_ds = create_time_scaling_xr(SCALING_TIME, SCALING_ARR) 221 | convert_xr_to_nc(scaling_ds, "time_scaling_example") 222 | plot_time_scaling(SCALING_TIME, SCALING_ARR) 223 | norm_ds = create_time_normalization_xr( 224 | NORM_TIME, FUEL_ARR, EI_CO2_ARR, EI_H2O_ARR, DIS_PER_FUEL_ARR 225 | ) 226 | convert_xr_to_nc(norm_ds, "time_norm_example") 227 | plot_time_norm(norm_ds) 228 | -------------------------------------------------------------------------------- /docs/source/background/contrails.rst: -------------------------------------------------------------------------------- 1 | Contrail Module 2 | =============== 3 | 4 | Here, the OpenAirClim Contrail Module (oac.cont) is described. 5 | The first section describes the methodology, explaining the scientific basis to the submodules. 6 | Section 2 provides an overview of some important, general open questions. 7 | Section 3 describes the Global Circulation Model (GCM) simulations upon which the contrail module is based. 8 | Finally, Section 4 discusses the current limitations, future improvements and planning. 9 | 10 | To understand how to run the contrail module within OpenAirClim, please refer to the `user guide <../user_guide/contrails.html>`_. 11 | 12 | 13 | .. warning:: 14 | 15 | It is currently not possible to calculate the contrail climate impact for multiple different aircraft. 16 | This is the subject of ongoing work, in particular on attribution. 17 | If reference is made to a specific fleet :math:`n` in this text, please assume that it refers to the single aircraft design that is inputted. 18 | 19 | 20 | OpenAirClim Contrail Module Methodology 21 | --------------------------------------- 22 | 23 | The objective of the OpenAirClim Contrail Module (oac-cont) is to calculate the changes in the contrail-induced global yearly average radiative forcing and temperature due to an input emission inventory and scenario. 24 | A simple representation of the methodology is: 25 | 26 | .. mermaid:: 27 | 28 | flowchart LR 29 | inv["Emission inventory"] 30 | inv --> form["Contrail formation"] 31 | form --> cccov["Contrail-cirrus coverage"] 32 | cccov --> RF["Contrail RF"] 33 | RF --> dT["Contrail dT"] 34 | 35 | This is similar to the methodology used by AirClim :cite:`greweAirClimEfficientTool2008, dahlmannMethodeZurEffizienten2011, dahlmannCanWeReliably2016`. 36 | The biggest difference between the AirClim and OpenAirClim methods is that OpenAirClim can accept multiple different aircraft types within the same grid box. 37 | This is complex because the climate impact from contrails is non-linear. 38 | 39 | 40 | 41 | Emission Inventory and Input Files 42 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 43 | 44 | To calculate a contrail climate impact, the yearly distance flown by a given aircraft at a given location (latitude, longitude, altitude) is required within the input emission inventory. 45 | Since oac.cont is defined on a pre-calculated grid, the first step is to identify which flown distances fall within each grid box :math:`(i,j,k)` (latitude, longitude, pressure level). 46 | To reduce computational time in further steps, the emission inventories of identical aircraft-engine combinations (e.g. A320neo with LEAP-1A) are combined as much as possible. 47 | The total yearly distance flown is thus available per distinct fleet :math:`n`. 48 | 49 | For each distinct fleet, further information is required in the input configuration file, namely: 50 | 51 | - ``G_250``: The Schmidt-Appleman mixing line slope `G` [Pa/K] at 250 hPa 52 | - ``eff_fac``: Overall propulsion efficiency :math:`\eta` compared to the reference (0.333) - planned to be replaced by the actual :math:`\eta` 53 | - ``PMrel``: Particulate emissions compared to the reference (1e15) 54 | 55 | 56 | 57 | Contrail Formation 58 | ^^^^^^^^^^^^^^^^^^ 59 | 60 | Contrail formation is classified by the Schmidt-Appleman Criterion (SAC, :cite:`schumannConditionsContrailFormation1996`) and by ice supersaturation. 61 | As in AirClim 2.0, in oac.cont the concept of `Contrail Flight Distance Density (CFDD)` is used, which can be thought of as the flown distance weighted by the probability of persistent contrail formation. 62 | Currently implemented is, 63 | 64 | .. math:: 65 | 66 | CFDD(i,j) = \sum_k \left(\frac{\text{Flown distance}(i,j,k)}{\text{Grid area}(i,j)} \cdot p_\text{pcf}(G, i,j,k) \right) 67 | 68 | where: 69 | 70 | - :math:`\text{Flown distance}(i,j,k)` is the distance flown in each grid box (input from inventory); 71 | - :math:`\text{Grid area}(i,j)` is the area of each grid box as viewed from above (lat-lon); 72 | - :math:`G` is the slope of the Schmidt-Appleman mixing line [Pa/K]; and 73 | - :math:`p_\text{pcf}` is the probability of a persistent contrail forming for a fleet with slope :math:`G`. 74 | 75 | In AirClim, the :math:`p_\text{pcf}` (then called :math:`p_\text{SAC}`) was calculated for three different :math:`G` corresponding to aircraft powered by conventional kerosene, LNG and LH2. 76 | In OpenAirClim, a new, continuous :math:`p_\text{pcf}` was developed using ERA5 data as part of a study into the limiting factors of persistent contrail formation :cite:`megillInvestigatingLimitingAircraftdesigndependent2025`. 77 | The continuous function is based on the sum of two modified logistic functions and valid for :math:`0.48~\text{Pa/K} \leq G`. 78 | 79 | 80 | 81 | 82 | Contrail Coverage 83 | ^^^^^^^^^^^^^^^^^ 84 | 85 | .. warning:: 86 | 87 | The coverage submodule is in development. 88 | 89 | Currently implemented is the AirClim method, which uses the 2D CFDD to calculate a 2D contrail cirrus coverage :math:`cccov`, 90 | 91 | .. math:: 92 | 93 | cccov(i,j) = 0.128 \cdot ISS(i,j) \cdot \arctan \left( 97.7 \cdot \frac{CFFD(i,j)}{ISS(i,j)} \right) 94 | 95 | where :math:`a = 0.128` and :math:`b = 97.7` are fitted parameters and :math:`ISS` is the proportion of the grid cell :math:`(i,j,k)` that is supersaturated with respect to ice. 96 | The :math:`cccov` is further multiplied by three weighting functions :cite:`huettenhoferParametrisierungKondensstreifenzirrenFuer2013, greweAssessingClimateImpact2017`: 97 | 98 | .. math:: 99 | 100 | w_1 = 0.863 \cdot \cos\left(lat \cdot \frac{\pi}{50}\right) 101 | 102 | .. math:: 103 | 104 | w_2 = 1.0 + 15.0 \cdot | 0.045 \cdot \cos\left(0.045 \cdot lat \right) + 0.045 | \cdot (\eta_{fac} - 1.0) 105 | 106 | 107 | .. math:: 108 | 109 | w_3 = 1.0 + 0.24 \cdot \cos\left(lat \cdot \frac{\pi}{23}\right) 110 | 111 | Finally, a global contrail cirrus coverage :math:`\overline{cccov}` is obtained by area-weighting the :math:`cccov`. 112 | 113 | 114 | 115 | Development of new coverage calculations are ongoing. 116 | The response for a single fleet :math:`n` will most likely take the following form, 117 | 118 | .. math:: 119 | 120 | cccov(n,i,j,k) = a \cdot ISS(i,j,k) \cdot \arctan \left(b \cdot \frac{CFDD(n,i,j,k)}{ISS(i,j,k)}\right) 121 | 122 | The challenge then becomes how to combine the coverages together, since contrail cirrus coverage is highly non-linear due to saturation effects. 123 | Furthermore, contrails formed by different aircraft, fuels and propulsion technologies do not produce the same coverage and have differing radiative forcing impacts. 124 | For example, the values :math:`a` and :math:`b` may differ depending on the aircraft type, fuel or propulsion technology. 125 | 126 | 127 | 128 | 129 | Radiative Forcing 130 | ^^^^^^^^^^^^^^^^^ 131 | 132 | .. warning:: 133 | 134 | The radiative forcing submodule is in development. 135 | 136 | Currently, the AirClim method is implemented, 137 | 138 | .. math:: 139 | 140 | RF = 14.9 \cdot \overline{cccov} \cdot PM_{fac} 141 | 142 | where: 143 | 144 | - :math:`\overline{cccov}` is the global contrail cirrus coverage 145 | - :math:`PM_{fac} = 0.92 \cdot \arctan \left(1.902 \cdot PM_{rel} ^ {0.74} \right)` is a relationship derived from :cite:`burkhardtMitigatingContrailCirrus2018`. 146 | 147 | 148 | 149 | 150 | Roadmap 151 | ------- 152 | 153 | See the contrail-related issues on `GitHub `_. 154 | 155 | 156 | -------------------------------------------------------------------------------- /openairclim/calc_metric.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates climate metric for each species and scenario 3 | """ 4 | 5 | import numpy as np 6 | from openairclim.read_netcdf import get_results 7 | 8 | 9 | def calc_climate_metrics(config: dict) -> dict: 10 | """Get all combinations of required climate metrics 11 | 12 | Args: 13 | config (dict): Configuration from config file 14 | 15 | Returns: 16 | dict: Dictionary of dictionaries containing climate metrics values, 17 | keys are unique climate metrics identifiers for each combination 18 | and species 19 | """ 20 | metrics_type_arr = config["metrics"]["types"] 21 | t_zero_arr = config["metrics"]["t_0"] 22 | horizon_arr = config["metrics"]["H"] 23 | _emis_dict, _conc_dict, rf_dict, dtemp_dict = get_results(config) 24 | out_dict = {} 25 | for metrics_type in metrics_type_arr: 26 | for t_zero in t_zero_arr: 27 | for horizon in horizon_arr: 28 | key = ( 29 | metrics_type 30 | + "_" 31 | + format(horizon, ".0f") 32 | + "_" 33 | + format(t_zero, ".0f") 34 | ) 35 | if metrics_type == "ATR": 36 | metrics_dict = calc_atr( 37 | config, t_zero, horizon, dtemp_dict 38 | ) 39 | elif metrics_type == "AGWP": 40 | metrics_dict = calc_agwp(config, t_zero, horizon, rf_dict) 41 | elif metrics_type == "AGTP": 42 | metrics_dict = calc_agtp( 43 | config, t_zero, horizon, dtemp_dict 44 | ) 45 | else: 46 | pass 47 | out_dict[key] = metrics_dict 48 | return out_dict 49 | 50 | 51 | def calc_atr( 52 | config: dict, t_zero: float, horizon: float, dtemp_dict: dict 53 | ) -> dict: 54 | """ 55 | Calculates Average Temperature Response (ATR) climate metrics 56 | for each species and the total 57 | 58 | Args: 59 | config (dict): Configuration from config file 60 | t_zero (float): start year for metrics calculation 61 | horizon (float): time horizon in years 62 | dtemp_dict (dict): Dictionary containing temperature changes for each species 63 | 64 | Returns: 65 | dict: Dictionary containing ATR values, keys are species and total 66 | """ 67 | time_config = config["time"]["range"] 68 | delta_t = time_config[2] 69 | dtemp_metrics_dict = get_metrics_dict(config, t_zero, horizon, dtemp_dict) 70 | # Calcultate ATR for temperature array 71 | # 72 | # Dallara, E. S., Kroo, I. M., & Waitz, I. A. (2011). 73 | # Metric for comparing lifetime average climate impact of aircraft. 74 | # AIAA journal, 49(8), 1600-1613. http://dx.doi.org/10.2514/1.J050763 75 | atr_dict = {} 76 | for spec, dtemp_arr in dtemp_metrics_dict.items(): 77 | atr = 0 78 | for dtemp in dtemp_arr: 79 | atr = atr + (dtemp / horizon) * delta_t 80 | atr_dict[spec] = atr 81 | # Calcultate total ATR (sum of all species) 82 | atr_dict["total"] = sum(atr_dict.values()) 83 | return atr_dict 84 | 85 | 86 | def calc_agwp( 87 | config: dict, t_zero: float, horizon: float, rf_dict: dict 88 | ) -> dict: 89 | """ 90 | Calculates the Absolute Global Warming Potential (AGWP) climate metrics 91 | for each species and the total 92 | 93 | Args: 94 | config (dict): Configuration from the configuration file. 95 | t_zero (float): The start year for the metrics calculation. 96 | horizon (float): The time horizon in years. 97 | rf_dict (dict): A dictionary containing the RF values for 98 | each species. 99 | 100 | Returns: 101 | dict: A dictionary containing the AGWP values for each species and 102 | the total. 103 | """ 104 | # Rodhe, H. (1990). A comparison of the contribution of various gases 105 | # to the greenhouse effect. Science, 248(4960), 1217-1219. 106 | # http://dx.doi.org/10.1126/science.248.4960.1217 107 | time_config = config["time"]["range"] 108 | delta_t = time_config[2] 109 | rf_metrics_dict = get_metrics_dict(config, t_zero, horizon, rf_dict) 110 | agwp_dict = {} 111 | for spec, rf_arr in rf_metrics_dict.items(): 112 | agwp = 0 113 | for rf in rf_arr: 114 | agwp = agwp + rf * delta_t 115 | agwp_dict[spec] = agwp 116 | # Calculate total AGWP (sum of all species) 117 | agwp_dict["total"] = sum(agwp_dict.values()) 118 | return agwp_dict 119 | 120 | 121 | def calc_agtp( 122 | config: dict, t_zero: float, horizon: float, dtemp_dict: dict 123 | ) -> dict: 124 | """ 125 | Calculates the Absolute Global Temperature Change Potential (AGTP) 126 | climate metrics for each species and the total 127 | 128 | Args: 129 | config (dict): Configuration from the configuration file. 130 | t_zero (float): The start year for the metrics calculation. 131 | horizon (float): The time horizon in years. 132 | dtemp_dict (dict): A dictionary containing the temperature changes for 133 | each species. 134 | 135 | Returns: 136 | dict: A dictionary containing the AGTP values for each species and 137 | the total. 138 | """ 139 | # Shine, K. P., Fuglestvedt, J. S., Hailemariam, K., & Stuber, N. (2005). 140 | # Alternatives to the global warming potential for comparing climate impacts 141 | # of emissions of greenhouse gases. Climatic change, 68(3), 281-302. 142 | # https://doi.org/10.1007/s10584-005-1146-9 143 | dtemp_metrics_dict = get_metrics_dict(config, t_zero, horizon, dtemp_dict) 144 | agtp_dict = {} 145 | for spec, dtemp_arr in dtemp_metrics_dict.items(): 146 | agtp = dtemp_arr[-1] 147 | agtp_dict[spec] = agtp 148 | # Calculate total AGTP (sum of all species) 149 | agtp_dict["total"] = sum(agtp_dict.values()) 150 | return agtp_dict 151 | 152 | 153 | def get_metrics_dict( 154 | config: dict, t_zero: float, horizon: float, resp_dict: dict 155 | ) -> dict: 156 | """ 157 | Get subset of timeseries dictionary: only for years in time_metrics 158 | 159 | Args: 160 | config (dict): Configuration from config file 161 | t_zero (float): start year for metrics calculation 162 | horizon (float): time horizon in years 163 | resp_dict (dict): Dictionary containing response (RF or dtemp) 164 | values for each species 165 | 166 | Returns: 167 | dict: Dictionary containig metrics values only for years in time_metrics, 168 | keys are species (and total) 169 | """ 170 | time_config = config["time"]["range"] 171 | time_range = np.arange( 172 | time_config[0], time_config[1], time_config[2], dtype=int 173 | ) 174 | delta_t = time_config[2] 175 | # Metrics time range 176 | time_metrics = np.arange(t_zero, (t_zero + horizon), delta_t) 177 | # Get values in resp_dict for years in time_metrics 178 | i = 0 179 | index_arr = [] 180 | for year_config in time_range: 181 | if year_config in time_metrics: 182 | index_arr.append(i) 183 | else: 184 | pass 185 | i = i + 1 186 | resp_metrics_dict = {} 187 | for spec, resp_arr in resp_dict.items(): 188 | resp_metrics_arr = np.zeros(len(time_metrics)) 189 | i = 0 190 | for index in index_arr: 191 | resp_metrics_arr[i] = resp_arr[index] 192 | i = i + 1 193 | resp_metrics_dict[spec] = resp_metrics_arr 194 | return resp_metrics_dict 195 | -------------------------------------------------------------------------------- /docs/source/bibliography.bib: -------------------------------------------------------------------------------- 1 | @article{bickelContrailCirrusClimate2025, 2 | title = {Contrail Cirrus Climate Impact: {{From Radiative Forcing}} to Surface Temperature Change}, 3 | author = {Bickel, Marius and Ponater, Michael and Burkhardt, Ulrike and Righi, Mattia and Hendricks, Johannes and Jöckel, Patrick}, 4 | year = {2025}, 5 | journal = {Journal of Climate}, 6 | volume = {38}, 7 | number = {8}, 8 | pages = {1895--1912}, 9 | issn = {0894-8755, 1520-0442}, 10 | doi = {10.1175/JCLI-D-24-0245.1}, 11 | } 12 | 13 | @article{boucherContributionGlobalAviation2021, 14 | title = {On the Contribution of Global Aviation to the {{CO2}} Radiative Forcing of Climate}, 15 | author = {Boucher, Olivier and Borella, Audran and Gasser, Thomas and Hauglustaine, Didier}, 16 | year = {2021}, 17 | journal = {Atmospheric Environment}, 18 | volume = {267}, 19 | pages = {118762}, 20 | issn = {13522310}, 21 | doi = {10.1016/j.atmosenv.2021.118762}, 22 | } 23 | 24 | @article{burkhardtMitigatingContrailCirrus2018, 25 | title = {Mitigating the Contrail Cirrus Climate Impact by Reducing Aircraft Soot Number Emissions}, 26 | author = {Burkhardt, Ulrike and Bock, Lisa and Bier, Andreas}, 27 | year = {2018}, 28 | journal = {npj Climate and Atmospheric Science}, 29 | volume = {1}, 30 | number = {1}, 31 | pages = {37}, 32 | issn = {2397-3722}, 33 | doi = {10.1038/s41612-018-0046-4}, 34 | urldate = {2023-05-12}, 35 | langid = {english}, 36 | } 37 | 38 | @article{dahlmannCanWeReliably2016, 39 | title = {Can We Reliably Assess Climate Mitigation Options for Air Traffic Scenarios despite Large Uncertainties in Atmospheric Processes?}, 40 | author = {Dahlmann, K. and Grewe, V. and Frömming, C. and Burkhardt, U.}, 41 | year = {2016}, 42 | journal = {Transportation Research Part D: Transport and Environment}, 43 | volume = {46}, 44 | pages = {40--55}, 45 | issn = {13619209}, 46 | doi = {10.1016/j.trd.2016.03.006}, 47 | langid = {english}, 48 | } 49 | 50 | @phdthesis{dahlmannMethodeZurEffizienten2011, 51 | title = {{Eine Methode zur effizienten Bewertung von Ma{\ss}nahmen zur Klimaoptimierung des Luftverkehrs}}, 52 | author = {Dahlmann, Katrin}, 53 | year = {2011}, 54 | address = {München}, 55 | langid = {ngerman}, 56 | school = {LMU München}, 57 | } 58 | 59 | @article{frommingInfluenceWeatherSituation2021, 60 | title = {Influence of Weather Situation on Non-{{CO2}} Aviation Climate Effects: The {{REACT4C}} Climate Change Functions}, 61 | author = {Frömming, Christine and Grewe, Volker and Brinkop, Sabine and Jöckel, Patrick and Haslerud, Amund S. and Rosanka, Simon and van Manen, J. and Matthes, Sigrun}, 62 | year = {2021}, 63 | journal = {Atmospheric Chemistry and Physics}, 64 | volume = {21}, 65 | number = {11}, 66 | pages = {9151--9172}, 67 | issn = {1680-7324}, 68 | doi = {10.5194/acp-21-9151-2021}, 69 | } 70 | 71 | @article{greweAirClimEfficientTool2008, 72 | title = {{{AirClim}}: An Efficient Tool for Climate Evaluation of Aircraft Technology}, 73 | author = {Grewe, V. and Stenke, A.}, 74 | year = {2008}, 75 | journal = {Atmospheric Chemistry and Physics}, 76 | volume = {8}, 77 | number = {16}, 78 | pages = {4621--4639}, 79 | doi = {10.5194/acp-8-4621-2008} 80 | } 81 | 82 | @article{greweAssessingClimateImpact2017, 83 | title = {Assessing the Climate Impact of the {{AHEAD}} Multi-Fuel Blended Wing Body}, 84 | author = {Grewe, Volker and Bock, Lisa and Burkhardt, Ulrike and Dahlmann, Katrin and Gierens, Klaus and Hüttenhofer, Ludwig and Unterstrasser, Simon and Rao, Arvind Gangoli and Bhat, Abhishek and Yin, Feijia and Reichel, Thoralf G. and Paschereit, Oliver and Levy, Yeshayahou}, 85 | year = {2017}, 86 | journal = {Meteorologische Zeitschrift}, 87 | volume = {26}, 88 | number = {6}, 89 | pages = {711--725}, 90 | issn = {0941-2948}, 91 | doi = {10.1127/metz/2016/0758}, 92 | langid = {english} 93 | } 94 | 95 | @phdthesis{huettenhoferParametrisierungKondensstreifenzirrenFuer2013, 96 | title = {{Parametrisierung von Kondensstreifenzirren für AirClim 2.0}}, 97 | author = {Hüttenhofer, Ludwig}, 98 | year = {2013}, 99 | address = {München}, 100 | langid = {ngerman}, 101 | school = {Ludwig-Maximilians-Universität München} 102 | } 103 | 104 | @article{leeContributionGlobalAviation2021, 105 | title = {The Contribution of Global Aviation to Anthropogenic Climate Forcing for 2000 to 2018}, 106 | author = {Lee, D.S. and Fahey, D.W. and Skowron, A. and Allen, M.R. and Burkhardt, U. and Chen, Q. and Doherty, S.J. and Freeman, S. and Forster, P.M. and Fuglestvedt, J. and Gettelman, A. and De León, R.R. and Lim, L.L. and Lund, M.T. and Millar, R.J. and Owen, B. and Penner, J.E. and Pitari, G. and Prather, M.J. and Sausen, R. and Wilcox, L.J.}, 107 | year = {2021}, 108 | journal = {Atmospheric Environment}, 109 | volume = {244}, 110 | pages = {117834}, 111 | issn = {13522310}, 112 | doi = {10.1016/j.atmosenv.2020.117834}, 113 | langid = {english}, 114 | } 115 | 116 | @article{lundEmissionMetricsQuantifying2017, 117 | title = {Emission Metrics for Quantifying Regional Climate Impacts of Aviation}, 118 | author = {Lund, Marianne T. and Aamaas, Borgar and Berntsen, Terje and Bock, Lisa and Burkhardt, Ulrike and Fuglestvedt, Jan S. and Shine, Keith P.}, 119 | year = {2017}, 120 | journal = {Earth System Dynamics}, 121 | volume = {8}, 122 | number = {3}, 123 | pages = {547--563}, 124 | issn = {2190-4987}, 125 | doi = {10.5194/esd-8-547-2017}, 126 | } 127 | 128 | @article{megillInvestigatingLimitingAircraftdesigndependent2025, 129 | title = {Investigating the Limiting Aircraft-Design-Dependent and Environmental Factors of Persistent Contrail Formation}, 130 | author = {Megill, L. and Grewe, V.}, 131 | year = {2025}, 132 | journal = {Atmos. Chem. Phys.}, 133 | volume = {25}, 134 | number = {7}, 135 | pages = {4131--4149}, 136 | issn = {1680-7324}, 137 | doi = {10.5194/acp-25-4131-2025}, 138 | } 139 | 140 | @article{myhreRadiativeForcingDue2011, 141 | title = {Radiative Forcing Due to Changes in Ozone and Methane Caused by the Transport Sector}, 142 | author = {Myhre, G. and Shine, K.P. and Rädel, G. and Gauss, M. and Isaksen, I.S.A. and Tang, Q. and Prather, M.J. and Williams, J.E. and van Velthoven, P. and Dessens, O. and Koffi, B. and Szopa, S. and Hoor, P. and Grewe, V. and Borken-Kleefeld, J. and Berntsen, T.K. and Fuglestvedt, J.S.}, 143 | year = {2011}, 144 | journal = {Atmospheric Environment}, 145 | volume = {45}, 146 | number = {2}, 147 | pages = {387--394}, 148 | issn = {13522310}, 149 | doi = {10.1016/j.atmosenv.2010.10.001}, 150 | langid = {english}, 151 | } 152 | 153 | @article{schumannConditionsContrailFormation1996, 154 | title = {On Conditions for Contrail Formation from Aircraft Exhausts}, 155 | author = {Schumann, Ulrich}, 156 | year = {1996}, 157 | journal = {Meteorologische Zeitschrift}, 158 | volume = {5}, 159 | number = {1}, 160 | pages = {4--23}, 161 | doi = {10.1127/metz/5/1996/4}, 162 | } 163 | 164 | @article{stevensonRadiativeForcingAircraft2004, 165 | title = {Radiative Forcing from Aircraft {{NOx}} Emissions: Mechanisms and Seasonal Dependence}, 166 | author = {Stevenson, David S. and Doherty, Ruth M. and Sanderson, Michael G. and Collins, William J. and Johnson, Colin E. and Derwent, Richard G.}, 167 | year = {2004}, 168 | journal = {Journal of Geophysical Research}, 169 | volume = {109}, 170 | number = {D17}, 171 | pages = {D17307}, 172 | issn = {0148-0227}, 173 | doi = {10.1029/2004JD004759}, 174 | } 175 | 176 | @article{trudingerComparisonFormalismsAttributing2005, 177 | title = {Comparison of Formalisms for Attributing Responsibility for Climate Change: {{Non-linearities}} in the {{Brazilian Proposal}} Approach}, 178 | author = {Trudinger, Cathy and Enting, Ian}, 179 | year = {2005}, 180 | journal = {Climatic Change}, 181 | volume = {68}, 182 | number = {1}, 183 | pages = {67--99}, 184 | issn = {1573-1480}, 185 | doi = {10.1007/s10584-005-6012-2}, 186 | } 187 | -------------------------------------------------------------------------------- /tests/read_config_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides tests for module read_config 3 | """ 4 | 5 | import os 6 | import tomllib 7 | from unittest.mock import patch 8 | import pytest 9 | import openairclim as oac 10 | 11 | abspath = os.path.abspath(__file__) 12 | dname = os.path.dirname(abspath) 13 | os.chdir(dname) 14 | 15 | # CONSTANTS 16 | REPO_PATH = "repository/" 17 | CACHE_PATH = "repository/cache/weights/" 18 | INV_NAME = "test_inv.nc" 19 | RESP_NAME = "test_resp.nc" 20 | BG_NAME = "co2_bg.nc" 21 | CACHE_NAME = "000.nc" 22 | TOML_NAME = "test.toml" 23 | TOML_INVALID_NAME = "test_invalid.toml" 24 | 25 | 26 | class TestLoadConfig: 27 | """Tests function load_config(file_name)""" 28 | 29 | def test_type(self): 30 | """Loads correct toml file and checks if output is of type dictionary""" 31 | config = oac.load_config((REPO_PATH + TOML_NAME)) 32 | assert isinstance(config, dict) 33 | 34 | def test_invalid(self): 35 | """Loads incorrect toml file and checks for raising exception""" 36 | with pytest.raises(tomllib.TOMLDecodeError): 37 | oac.load_config((REPO_PATH + TOML_INVALID_NAME)) 38 | 39 | 40 | @pytest.fixture(name="setup_arguments", scope="class") 41 | def fixture_setup_arguments(): 42 | """Setup arguments for check_config 43 | 44 | Returns: 45 | dict, dict: Configuration template and default config 46 | """ 47 | config_template = oac.CONFIG_TEMPLATE 48 | default_config = oac.DEFAULT_CONFIG 49 | return config_template, default_config 50 | 51 | 52 | @pytest.mark.usefixtures("setup_arguments") 53 | class TestCheckConfig: 54 | """Tests function check_config(config)""" 55 | 56 | def test_correct_config(self, setup_arguments): 57 | """Correct config returns True""" 58 | config_template, default_config = setup_arguments 59 | config = { 60 | "species": {"inv": ["CO2"], "nox": "NO", "out": ["CO2"]}, 61 | "inventories": { 62 | "dir": REPO_PATH, 63 | "files": [INV_NAME], 64 | "rel_to_base": False, 65 | "base": {"dir": REPO_PATH, "files": [INV_NAME]}, 66 | }, 67 | "output": { 68 | "run_oac": True, 69 | "run_metrics": True, 70 | "run_plots": True, 71 | "dir": "results/", 72 | "name": "example", 73 | "overwrite": True, 74 | "concentrations": False, 75 | }, 76 | "time": {"range": [2020, 2121, 1]}, 77 | "background": { 78 | "dir": REPO_PATH, 79 | "CO2": {"file": (REPO_PATH + BG_NAME), "scenario": "SSP2-4.5"}, 80 | "CH4": {"file": (REPO_PATH + BG_NAME), "scenario": "SSP2-4.5"}, 81 | "N2O": {"file": (REPO_PATH + BG_NAME), "scenario": "SSP2-4.5"} 82 | }, 83 | "responses": {"dir": REPO_PATH}, 84 | "temperature": {"method": "Boucher&Reddy", "CO2": {"lambda": 1.0}}, 85 | "metrics": {"types": ["ATR"], "t_0": [2020], "H": [100]}, 86 | "aircraft": {"types": ["DEFAULT"]}, 87 | } 88 | assert isinstance( 89 | oac.check_config(config, config_template, default_config), dict 90 | ) 91 | 92 | def test_incorrect_config(self, setup_arguments): 93 | """Incorrect config returns TypeError""" 94 | config_template, default_config = setup_arguments 95 | config = { 96 | "species": {"inv": ["CO2"], "nox": "NO", "out": ["CO2"]}, 97 | "inventories": { 98 | "dir": 9, 99 | "files": [INV_NAME], 100 | "rel_to_base": 1, 101 | "base": {"dir": 9, "files": [INV_NAME]}, 102 | }, 103 | "output": { 104 | "dir": "results/", 105 | "name": "example", 106 | "overwrite": True, 107 | }, 108 | "time": {"range": [2020, 2026, 1]}, 109 | "background": { 110 | "CO2": {"file": (REPO_PATH + BG_NAME), "scenario": "SSP2-4.5"} 111 | }, 112 | "responses": {"CO2": {"response_grid": "0D"}}, 113 | "temperature": {"method": "Boucher&Reddy", "CO2": {"lambda": 1.0}}, 114 | "aircraft": {"types": ["DEFAULT"]}, 115 | } 116 | with pytest.raises(TypeError): 117 | oac.check_config(config, config_template, default_config) 118 | 119 | def test_incorrect_file_path(self, setup_arguments): 120 | """Incorrect file path of emission inventory returns False""" 121 | config_template, default_config = setup_arguments 122 | config = { 123 | "species": {"inv": ["CO2"], "nox": "NO", "out": ["CO2"]}, 124 | "inventories": { 125 | "dir": REPO_PATH, 126 | "files": ["not-existing-example.nc"], 127 | }, 128 | "output": { 129 | "dir": "results/", 130 | "name": "example", 131 | "overwrite": True, 132 | }, 133 | "time": {"range": [2020, 2026, 1]}, 134 | "background": { 135 | "CO2": {"file": (REPO_PATH + BG_NAME), "scenario": "SSP2-4.5"} 136 | }, 137 | "responses": {"CO2": {"response_grid": "0D"}}, 138 | "temperature": {"method": "Boucher&Reddy", "CO2": {"lambda": 1.0}}, 139 | "aircraft": {"types": ["DEFAULT"]}, 140 | } 141 | with pytest.raises(KeyError): 142 | oac.check_config(config, config_template, default_config) 143 | 144 | 145 | # TODO Instead of creating and removing directories, use patch or monkeypatch 146 | # fixtures for the simulation of os functionalities (test doubles) 147 | @pytest.fixture(scope="class") 148 | def make_remove_dir(request): 149 | """Arrange and Cleanup fixture, create an output directory for testing 150 | and remove it afterwards, setup and the directory name can be reused 151 | in several test functions of the same class. 152 | 153 | Args: 154 | request (_pytest.fixtures.FixtureRequest): pytest request parameter 155 | for injecting objects into test functions 156 | """ 157 | dir_path = "results/" 158 | request.cls.dir_path = dir_path 159 | if not os.path.exists(dir_path): 160 | os.makedirs(dir_path) 161 | yield 162 | os.rmdir(dir_path) 163 | 164 | 165 | @pytest.mark.usefixtures("make_remove_dir") 166 | class TestCreateOutputDir: 167 | """Tests function create_output_dir(config)""" 168 | 169 | def test_existing_dir_no_overwrite(self): 170 | """Existing output directory and "overwrite = False" raises OSError""" 171 | config = { 172 | "output": { 173 | "run_oac": True, 174 | "dir": "results/", 175 | "name": "test", 176 | "overwrite": False, 177 | } 178 | } 179 | with pytest.raises(OSError): 180 | oac.create_output_dir(config) 181 | 182 | @patch("os.path.isdir") 183 | def test_existing_dir_overwrite(self, patch_isdir): 184 | """Existing output directory and "overwrite = True" creates output dictionary""" 185 | config = { 186 | "output": { 187 | "run_oac": True, 188 | "dir": "results/", 189 | "name": "test", 190 | "overwrite": True, 191 | } 192 | } 193 | oac.create_output_dir(config) 194 | assert patch_isdir("results/") 195 | 196 | 197 | class TestClassifySpecies: 198 | """Tests function classify_species(config)""" 199 | 200 | def test_missing_response_species(self): 201 | """Species defined in "species", but not in "responses" raises KeyError""" 202 | config = { 203 | "species": { 204 | "inv": ["CO2", "H2O"], 205 | "nox": "NO", 206 | "out": ["CO2", "H2O"], 207 | }, 208 | "responses": {"CO2": {"response_grid": "0D"}}, 209 | } 210 | with pytest.raises(KeyError): 211 | oac.classify_species(config) 212 | 213 | def test_no_response_grid(self): 214 | """No response_grid for a species raises KeyError""" 215 | config = { 216 | "species": {"inv": ["CO2"], "nox": "NO", "out": ["CO2"]}, 217 | "responses": {"CO2": {}}, 218 | } 219 | with pytest.raises(KeyError): 220 | oac.classify_species(config) 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAirClim 2 | 3 | [![DOI](https://zenodo.org/badge/851165490.svg)](https://zenodo.org/doi/10.5281/zenodo.13682728) 4 | ![Install and Test workflow](https://github.com/dlr-pa/oac/actions/workflows/install_and_test.yml/badge.svg) 5 | 6 | 7 | ## Description 8 | 9 | OpenAirClim is a model for simplified evaluation of the approximate chemistry-climate impact of air traffic emissions. The model represents the major responses of the atmosphere to emissions in terms of composition and climate change. Instead of applying time-consuming climate-chemistry models, a response model is developed and applied which reproduces the response of a climate-chemistry model without actually calculating ab initio all the physical and chemical effects. The responses are non-linear relations between localized emissions and Radiative Forcing and further climate indicators. These response surfaces are contained within look-up tables. OpenAirClim builds upon the previous AirClim framework. In comparison with AirClim, following new features are introduced: 10 | 11 | - Standardized formats for configuration file (user interface) and emission inventories (input) and program results (output) 12 | - Possibility of full 4D emission inventories (3D for several time steps) 13 | - Non-linear response functions for NOx including contribution approach (tagging) and dependency on background 14 | - Contrail formation also depending on fuels and overall efficiencies 15 | - Inclusion of different fuels 16 | - Choice of different CO2 response models 17 | - Choice of temperature models and sea-level rise 18 | - Uncertainty assessment and Robustness Metric based on Monte Carlo Simulations 19 | - Parametric scenarios as sensitivities, e.g. at post-processing level: climate optimized routings 20 | 21 | ### Scientific Background 22 | 23 | The impact of aviation on climate amounts to approximately 5% of the total anthropogenic climate warming. A large part of the aviation’s impact arises from non-CO2 effects, especially contrails and nitrogen oxide emissions. Impact of non-CO2 effects depend in particular on the location and time of emissions, hence a regional dependence of impacts exists. As impacts of individual non-CO2 effects show a different spatial dependence, the relationship between impacts and associated emissions can be best described in non-linear relationships, i.e. equations or algorithms based on look-up tables. Specifically, the climate impact of an aircraft depends on where (and when) an aircraft is operated. In addition, using different types of fuel generally changes the importance of the non-CO2 effects. 24 | 25 | ## Layout 26 | 27 | ![Overview on the layout of the OpenAirClim framework](img/OAC-chart.png) 28 |
Overview on the layout of the OpenAirClim framework
29 | 30 | - User interface for settings in the run control and outputs (grey) 31 | - Definition of background conditions, such as aviation scenarios, uncertainty ranges and aviation inventories (orange) 32 | - A link to a pre-processor for aviation inventories (light blue). 33 | - Processor for a full 4D-emission inventory at multiple timesteps (violet) 34 | - A framework for the application of non-linear response functions (red) to these emission inventories. 35 | - Response functions for CO2 and climate / temperature and sea-level changes 36 | - Parametric scenarios as sensitivities (yellow), e.g. at post-processing level: climate optimized routings 37 | - Output: Warnings, errors (log files), climate indicators and diagnostics (green), values of climate metrics and robustness metrics (grey) 38 | 39 | ## Documentation 40 | 41 | Please refer to [openairclim.org](https://openairclim.org/) for the documentation of the OpenAirClim framework. 42 | This documentation includes installation manuals, quick-start and user guides, example demonstrations, an API reference, as well as information on the scientific background and OpenAirClim governance. 43 | 44 | ## Installation 45 | 46 | If you build OpenAirClim from source, you first have to access the [repository](https://github.com/dlr-pa/oac). To obtain the repository, the most convenient way is using following [Git](https://git-scm.com/) command: 47 | ``` 48 | git clone https://github.com/dlr-pa/oac.git 49 | ``` 50 | 51 | Make sure that either the [conda](https://docs.conda.io/projects/conda/en/latest/index.html) or [mamba](https://mamba.readthedocs.io/en/latest/index.html) package manager is installed on your system. 52 | 53 | The source code includes configuration files `environment_xxx.yaml` that enable the installation of a virtual conda environment with all required dependencies. This installation method is suitable for working across platforms. Change directory to the root folder of the downloaded source, create a conda environment and activate it: 54 | ``` 55 | cd oac 56 | conda env create -f environment_xxx.yaml 57 | conda activate 58 | ``` 59 | 60 | Replace `xxx` with the relevant file and `` with the correct name of the installed conda environment, e.g. `oac` or `oac_minimal`. 61 | Finally, to install the openairclim package system-wide on your computer, execute one of the following commands within the activated conda environment. 62 | This last installation step isn't necessary if the user has otherwise added the path to the oac source folder to `PYTHONPATH`. 63 | ``` 64 | pip install . 65 | ``` 66 | or 67 | ``` 68 | pip install -e . 69 | ``` 70 | The `-e` flag treats the openairclim package as an editable install, allowing you to make changes to the source code and see those changes reflected immediately. The latter command is recommended for developers. 71 | 72 | After installing the conda ennvironment and required dependencies, proceed with the steps described in section [Getting started](##getting-started). 73 | 74 | 75 | ## Getting started 76 | 77 | ### Download emission inventories 78 | Air traffic emission inventories are essential input to OpenAirClim. You can [download](https://doi.org/10.5281/zenodo.11442322) example emission inventories based on the DLR project [Development Pathways for Aviation up to 2050 (DEPA 2050)](https://elib.dlr.de/142185/). These inventories comprise realistic emission data sets. 79 | 80 | Depending on the settings made in the configuration file, the computational time of the configured simulations could be long. If you are more interested in testing or developing OpenAirClim software, you might want to generate artificial data. 81 | 82 | ### Create input data 83 | If you do not have custom input files available, input files with artificial data can be autogenerated using command line scripts. For that, change directory to [utils/](utils/) and execute following commands in order to create artificial input files: 84 | ``` 85 | cd utils/ 86 | python create_artificial_inventories.py 87 | python create_time_evolution.py 88 | ``` 89 | The script `create_artificial_inventories.py` creates a series of inventories comprising random emission data. The script `create_time_evolution.py` creates two time evolution files, controlling the temporal evolution of the emission data: one file is intended for normalizing inventory emission data, and the other file is intended for scaling inventory emission data along the time axis. Emission inventories and time evolution files are both .nc files and are located in directory [example/input](example/input/). 90 | 91 | ### Create test files 92 | If you contribute to the software development of OpenAirClim, you will probably execute the testing procedures which require additional test files. Following command creates these files: 93 | ``` 94 | python create_test_files.py 95 | ``` 96 | ### Usage 97 | 98 | After installation, the package can be imported and used in Python scripts: 99 | ``` 100 | import openairclim as oac 101 | ``` 102 | 103 | Refer to the [example/](example/) folder within the repository for a minimal example and the demonstrations given on [openairclim.org](https://openairclim.org/). 104 | 105 | 106 | ## Roadmap 107 | 108 | The scheduling of major software releases and milestone planning are partially dependent on the contractractual framework with our stakeholders. For the version history of the completed releases, see the [changelog](CHANGELOG.md). The full development stage as currently planned is described in the [layout](#layout). 109 | 110 | ## Contributing 111 | Contributions are very welcome. Please read our [contribution guidelines](CONTRIBUTING.md) to get started. 112 | 113 | ## License 114 | The license of the OpenAirClim sofware can be found [here](LICENSE). 115 | --------------------------------------------------------------------------------