Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert to a Python package #16

Merged
merged 13 commits into from Apr 6, 2020
Merged
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
@@ -0,0 +1,31 @@
name: Python package

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Lint with flake8
run: |
pip install flake8
find . -name "*.py" | flake8 --count --max-complexity=5 \
--show-source --statistics
- name: Install and test with pytest
run: |
pip install pytest setuptools-scm
pip install --editable .
pytest --verbose
- name: Test package build
run: |
pip install twine wheel
python3 setup.py bdist_wheel sdist
twine check dist/*
6 changes: 6 additions & 0 deletions .gitignore
@@ -1,4 +1,10 @@
# Python
*.egg-info
__pycache__
.benchmarks
.pytest_cache
build
dist

# Editors
.idea/*
3 changes: 3 additions & 0 deletions AUTHORS
@@ -0,0 +1,3 @@
Paul Natsuo Kishimoto <kishimot@iiasa.ac.at> (@khaeru, IIASA)
Daniel Huppmann <huppmann@iiasa.ac.at> (@danielhuppmann, IIASA)
Franceso Lovat <lovat@iiasa.ac.at> (@francescolovat, IIASA)
38 changes: 38 additions & 0 deletions DEVELOPING.rst
@@ -0,0 +1,38 @@
Development notes
*****************

The repository and package aim to be ruthlessly simple, and thus as easy as possible to maintain.
Thus:

- No built documentation; like `pycountry <https://pypi.org/project/pycountry/>`_, the README *is* the documentation.
- Actual code (in \_\_init\_\_.py) kept to a minimum.
- `setuptools-scm <https://pypi.org/project/setuptools-scm/>`_ and git tags used for all versioning.
- Minimal CI configuration: one service/OS/Python version.
- Versioning: similar to pycountry: ``<YYYY>.<M>.<D>``.
- AUTHORS: anyone adding a commit to the repo should also add their name to AUTHORS.


Test the package build
======================

.. code-block:: shell

$ rm -r build dist
$ python3 setup.py bdist_wheel sdist
$ twine check dist/

Ensure there are no warnings from twine.


Generated data files for GWP contexts
=====================================

iam_units/data/emissions/emissions.txt defines the base units for Pint, and imports the files iam_units/data/emissions/gwp\_\*.txt.
These files each define one context, and contain a notice that they should not be edited manually.

Update these files using the command::

$ python -m iam_units.update emissions

The update submodule parses metric_conversions.csv and writes the context files.
When adding a new context file, make sure to ``@import`` it in emissions.txt and write a unit test.
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

45 changes: 0 additions & 45 deletions README.md

This file was deleted.

122 changes: 122 additions & 0 deletions README.rst
@@ -0,0 +1,122 @@
Unit definitions for integrated-assessment research
***************************************************

© 2020 `IAM-units authors`_; licensed under the `GNU GPL version 3`_.

The file `definitions.txt`_ gives `Pint`_-compatible definitions of energy, climate, and related units to supplement the SI and other units included in Pint's `default_en.txt`_.
These definitions are used by:

- the IIASA Energy Program `MESSAGEix-GLOBIOM`_ integrated assessment model (IAM),
- the Python package `pyam`_ for analysis and visualization of integrated-assessment scenarios (see `pyam.IamDataFrame.convert_unit()`_ for details)

and may be used for research in integrated assessment, energy systems, transportation, or other, related fields.
(Please open a `pull request`_ to add your usage to this README!)

Usage
=====

.. code-block:: python

>>> from iam_units import registry

>>> qty = registry('1.2 tce')
>>> qty
1.2 <Unit('tonne_of_coal_equivalent')>

>>> qty.to('GJ')
29.308 <Unit('gigajoule')>

To make the ``registry`` from this package the default:

.. code-block:: python

>>> import pint
>>> pint.set_application_registry(registry)

# Now used by default for pint top-level classes and methods
>>> pint.Quantity('1.2 tce')
1.2 <Unit('tonne_of_coal_equivalent')>

Warnings
========

``iam_units`` overwrites Pint's default definitions in the following cases:

.. list-table::
:header-rows: 1

- - ``pint`` default
- ``iam_units``
- Note
- - 'C' = Coulomb
- 'C' = carbon
- See `emissions.txt`_ at line 10.

Technical details
=================

Emissions and GWP
-----------------

`emissions.txt`_ defines some greenhouse gases (GHGs) as Pint base units.
Conversion of masses of these GHGs to CO₂ equivalents use selectable global warming potential (GWP) metrics, implemented as Pint `contexts`_ in the other files in the same directory.
The contexts have names like ``gwp_<IPCC report>GWP<years>``, where ``<years>`` is `100` and:

.. list-table::
:header-rows: 1

- - ``<IPCC report>``
- Meaning
- - ``SAR``
- Second Assessment Report (1995)
- - ``AR4``
- Fourth Assessment Report (2007)
- - ``AR5``
- Fifth Assessment Report (2014)

To use one of these contexts, give its name as the second argument to the ``pint.Quantity.to()`` method:

.. code-block:: python

>>> qty = registry('3.5e3 t N20')
>>> qty
3500 <Unit('metric_ton * nitrous_oxide')>

>>> qty.to('Mt CO2', 'gwp_AR4GWP100')
0.9275 <Unit('carbon_dioxide * megametric_ton')>

# Using a different metric
>>> qty.to('Mt CO2', 'gwp_SARGWP100')
1.085 <Unit('carbon_dioxide * megametric_ton')>

Data sources
~~~~~~~~~~~~
The GWP unit definitions are generated using the file metric_conversions.csv.
The file is copied from `lewisjared/scmdata`_ v0.4, authored by `@lewisjared <https://github.com/lewisjared>`_, `@swillner <https://github.com/swillner>`_, and `@znicholls <https://github.com/znicholls>`_ and licensed under BSD-3.
The version in scmdata was transcribed from `this source`_ (PDF link).

See `<DEVELOPING.rst>`_ for details on updating the definitions.

.. _contexts: https://pint.readthedocs.io/en/latest/contexts.html
.. _lewisjared/scmdata: https://github.com/lewisjared/scmdata/tree/v0.4.0/src/scmdata/data
.. _this source: https://www.ghgprotocol.org/sites/default/files/ghgp/Global-Warming-Potential-Values%20%28Feb%2016%202016%29_1.pdf


Tests and development
=====================

Use ``pytest iam_units`` to check that the definitions can be loaded.
Example unit expressions in `checks.csv`_ are also checked.
See `<DEVELOPING.rst>`_ for further details.

.. _IAM-units authors: ./AUTHORS
.. _GNU GPL version 3: ./LICENSE
.. _definitions.txt: ./iam_units/data/definitions.txt
.. _emissions.txt: ./iam_units/data/emissions/emissions.txt
.. _checks.csv: ./iam_units/data/checks.csv
.. _Pint: https://pint.readthedocs.io
.. _default_en.txt: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
.. _MESSAGEix-GLOBIOM: https://message.iiasa.ac.at
.. _pyam: https://pyam-iamc.readthedocs.io
.. _pyam.IamDataFrame.convert_unit(): https://pyam-iamc.readthedocs.io/en/stable/api/iamdataframe.html#pyam.IamDataFrame.convert_unit
.. _pull request: https://github.com/IAMconsortium/units/pulls
7 changes: 7 additions & 0 deletions iam_units/__init__.py
@@ -0,0 +1,7 @@
from pathlib import Path

import pint

registry = pint.UnitRegistry()
registry.load_definitions(
str(Path(__file__).parent / 'data' / 'definitions.txt'))
File renamed without changes.
2 changes: 1 addition & 1 deletion definitions.txt → iam_units/data/definitions.txt
Expand Up @@ -49,4 +49,4 @@ tkm = tonne_freight * kilometer

# please add any emissions-related definitions to modules/emissions

@import modules/emissions/emissions.txt
@import emissions/emissions.txt
Expand Up @@ -25,7 +25,7 @@ ammonia = [ammonia] = nh3 = NH3
sulfur = [sulfur] = S

# Importing contexts to define conversions to co2-equivalents (=co2)
# see the README of this module for more information
# See DEVELOPING.rst for more information.

@import gwp_AR5GWP100.txt
@import gwp_AR4GWP100.txt
Expand Down
@@ -1,4 +1,4 @@
# This file was created using the script `write_gwp_context.py`
# This file was generated using python -m iam_units.update emissions
# DO NOT ALTER THIS FILE MANUALLY!

@context gwp_AR4GWP100
Expand All @@ -25,4 +25,4 @@
[nitrous_oxide] / [time] -> [carbon_dioxide] / [time]: value * 298.0 * CO2 / N2O
[carbon_dioxide] / [time] -> [nitrous_oxide] / [time]: value / 298.0 * N2O / CO2

@end
@end
@@ -1,4 +1,4 @@
# This file was created using the script `write_gwp_context.py`
# This file was generated using python -m iam_units.update emissions
# DO NOT ALTER THIS FILE MANUALLY!

@context gwp_AR5GWP100
Expand All @@ -25,4 +25,4 @@
[nitrous_oxide] / [time] -> [carbon_dioxide] / [time]: value * 265.0 * CO2 / N2O
[carbon_dioxide] / [time] -> [nitrous_oxide] / [time]: value / 265.0 * N2O / CO2

@end
@end
@@ -1,4 +1,4 @@
# This file was created using the script `write_gwp_context.py`
# This file was generated using python -m iam_units.update emissions
# DO NOT ALTER THIS FILE MANUALLY!

@context gwp_SARGWP100
Expand All @@ -25,4 +25,4 @@
[nitrous_oxide] / [time] -> [carbon_dioxide] / [time]: value * 310.0 * CO2 / N2O
[carbon_dioxide] / [time] -> [nitrous_oxide] / [time]: value / 310.0 * N2O / CO2

@end
@end
18 changes: 6 additions & 12 deletions test.py → iam_units/test_all.py
Expand Up @@ -3,16 +3,18 @@

import pint
from pint.util import UnitsContainer

import pytest

from iam_units import registry


DATA_PATH = Path(__file__).parent / 'data'
defaults = pint.get_application_registry()


# Read units to check from file
PARAMS = []
with open(Path(__file__).with_name('checks.csv')) as f:
with open(DATA_PATH / 'checks.csv') as f:
for row in csv.reader(f, skipinitialspace=True, quoting=csv.QUOTE_MINIMAL):
try:
unit_str, dims, new_def = row
Expand All @@ -33,17 +35,9 @@
PARAMS.append((unit_str, dims, new_def))


@pytest.fixture(scope='session')
def registry():
"""UnitRegistry including definitions from definitions.txt."""
reg = pint.UnitRegistry()
reg.load_definitions(str(Path(__file__).with_name('definitions.txt')))
yield reg


@pytest.mark.parametrize('unit_str, dim, new_def', PARAMS,
ids=lambda v: v if isinstance(v, str) else '')
def test_units(registry, unit_str, dim, new_def):
def test_units(unit_str, dim, new_def):
if new_def:
# Units defined in dimensions.txt are not recognized by base pint
with pytest.raises(pint.UndefinedUnitError):
Expand All @@ -60,7 +54,7 @@ def test_units(registry, unit_str, dim, new_def):
[('AR5GWP100', 28),
('AR4GWP100', 25),
('SARGWP100', 21)])
def test_units_emissions(registry, context, value):
def test_units_emissions(context, value):
# The registry shouldn't convert with specifying a valid context
with pytest.raises(pint.DimensionalityError):
registry['ch4'].to('co2')
Expand Down