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