From 42b5f633c8fcf4999b0440e9aae6646af349ae0c Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Wed, 15 Apr 2020 10:35:02 +0200 Subject: [PATCH 01/10] Expand tests to cover convert_gwp() with same species and metric=None --- iam_units/test_all.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/iam_units/test_all.py b/iam_units/test_all.py index 447c1b6..e845505 100644 --- a/iam_units/test_all.py +++ b/iam_units/test_all.py @@ -53,14 +53,6 @@ def test_kt(): pint.UnitRegistry()('kt').to('Mt') -# 1 tonne of CH4 converted to CO2 equivalent by different metrics -EMI_DATA = [ - ('AR5GWP100', 28), - ('AR4GWP100', 25), - ('SARGWP100', 21) -] - - def test_emissions_internal(): # Dummy units can be created registry('0.5 _gwp').dimensionality == {'[_GWP]': 1.0} @@ -79,24 +71,29 @@ def test_emissions_internal(): 'Mt {} / a', # Mass rate 'kt {} / (ha * yr)', # Mass flux ]) -@pytest.mark.parametrize('metric, expected_value', EMI_DATA) -@pytest.mark.parametrize('species_out', ['CO2', 'CO2e']) -def test_convert_gwp(units, metric, expected_value, species_out): +@pytest.mark.parametrize('metric, species_in, species_out, expected_value', [ + ('AR5GWP100', 'CH4', 'CO2', 28), + ('AR5GWP100', 'CH4', 'CO2e', 28), + ('AR4GWP100', 'CH4', 'CO2', 25), + ('SARGWP100', 'CH4', 'CO2', 21), + (None, 'CO2', 'CO2', 1.), +]) +def test_convert_gwp(units, metric, species_in, species_out, expected_value): # Bare masses can be converted qty = registry.Quantity(1.0, units.format('')) expected = registry(f'{expected_value} {units}') - assert convert_gwp(metric, qty, 'CH4', species_out) == expected + assert convert_gwp(metric, qty, species_in, species_out) == expected # '[mass] [speciesname] (/ [time])' can be converted; the input species is # extracted from the *qty* argument - qty = f'1.0 ' + units.format('CH4') + qty = f'1.0 ' + units.format(species_in) expected = registry(f'{expected_value} {units}') assert convert_gwp(metric, qty, species_out) == expected # Tuple of (vector magnitude, unit expression) can be converted where the # the unit expression contains the input species name arr = np.array([1.0, 2.5, 0.1]) - qty = (arr, units.format('CH4')) + qty = (arr, units.format(species_in)) expected = arr * expected_value # Conversion works From 48063f472f13267f5476dc44edc6c867a7aeb3f9 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Wed, 15 Apr 2020 10:35:57 +0200 Subject: [PATCH 02/10] Allow convert_gwp() with metric=None --- iam_units/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/iam_units/__init__.py b/iam_units/__init__.py index 71a5954..195d6c8 100644 --- a/iam_units/__init__.py +++ b/iam_units/__init__.py @@ -63,6 +63,18 @@ def convert_gwp(metric, quantity, *species): # Re-assemble the expression for the units or whole quantity expr = q0 + q1 + # metric may be 'None' iff the input and output species are the same + if metric is None: + if species_in == species_out: + metric = 'AR5GWP100' + elif species_in in species_out: + # Both a DimensionalityError ('CO2' → 'CO2 / a') and a ValueError + # (no metric); raise the former for pyam compat + raise pint.DimensionalityError(species_in, species_out) + else: + msg = f'Must provide GWP metric for ({species_in}, {species_out})' + raise ValueError(msg) + # Ensure a pint.Quantity object: # - If tuple input was given, use the 2-arg constructor. # - If not, use the 1-arg form to convert a string. From ab84df7c47e944f1d5d10f6410c5fabce70866b8 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Wed, 15 Apr 2020 10:36:55 +0200 Subject: [PATCH 03/10] Update convert_gwp() docstring --- iam_units/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/iam_units/__init__.py b/iam_units/__init__.py index 195d6c8..27be32f 100644 --- a/iam_units/__init__.py +++ b/iam_units/__init__.py @@ -21,20 +21,21 @@ def convert_gwp(metric, quantity, *species): - """Convert *quantity* between emissions *species* with a GWP *metric*. + """Convert *quantity* between GHG *species* with a GWP *metric*. Parameters ---------- - metric : 'SARGWP100' or 'AR4GWP100' or 'AR5GWP100' - Metric conversion factors to use. + metric : 'SARGWP100' or 'AR4GWP100' or 'AR5GWP100' or None + Metric conversion factors to use. May be :obj:`None` if the input and + output species are the same. quantity : str or pint.Quantity or tuple Quantity to convert. If a tuple, the arguments are passed to the :class:`pint.Quantity` constructor. species : sequence of str, length 1 or 2 - Output, or input and output emissions species, e.g. ('CH4', 'CO2') to - convert mass of CH₄ to GWP-equivalent mass of CO₂. If only the output - species is provided, *quantity* must contain the name of the input - species in some location, e.g. 'tonne CH4 / year'. + Output, or (input, output) species, e.g. ('CH4', 'CO2') to convert + mass of CH₄ to GWP-equivalent mass of CO₂. If only the output species + is provided, *quantity* must contain the name of the input species in + some location, e.g. 'tonne CH4 / year'. Returns ------- From 6ea80447680c837dca95f5c278cc03759940542d Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Wed, 15 Apr 2020 10:43:08 +0200 Subject: [PATCH 04/10] Bump max_complexity from 5 to 8 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2e37b3..d492bb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Lint with flake8 run: | pip install flake8 - find . -name "*.py" | flake8 --count --max-complexity=5 \ + find . -name "*.py" | flake8 --count --max-complexity=8 \ --show-source --statistics - name: Install and test with pytest run: | From dd02741bf469be23137357f524d7dca79b6a2517 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 21 Apr 2020 11:42:57 +0200 Subject: [PATCH 05/10] Write emissions.EQUIV variable with sets of equivalent symbols --- iam_units/update.py | 51 +++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/iam_units/update.py b/iam_units/update.py index 3cadc05..4aa6a5d 100644 --- a/iam_units/update.py +++ b/iam_units/update.py @@ -1,3 +1,4 @@ +from itertools import chain from pathlib import Path import pandas as pd @@ -50,6 +51,11 @@ '{{symbols}}', ] +# Sets of symbols that refer to the same species and are interchangeable. +EQUIV = [ + set({{equiv}}), + ] + # Regular expression for one *SPECIES* in a pint-compatible unit string. pattern = re.compile( '(?<=[ -])(' @@ -57,6 +63,12 @@ + r')(?=[ -/]|[^\w]|$)') """ +# Equivalents: different symbols for the same species. +_EMI_EQUIV = [ + ['CO2', 'CO2_eq', 'CO2e', 'CO2eq'], + ['C', 'Ce'], +] + def emissions(): """Update emissions definitions files.""" @@ -71,32 +83,39 @@ def emissions(): .melt(id_vars=['Species', 'Symbol'], var_name='metric') \ .dropna(subset=['value']) - # Write the file containing the species defs + # List of symbols requiring a GWP context to covert symbols = sorted(data['Symbol'].unique()) - with open(data_path / 'species.txt', 'w') as f: - f.write(_EMI_HEADER + '\n') - [f.write(f'a_{symbol} = NaN\n') for symbol in symbols] + + # Format and write the species defs file + lines = [_EMI_HEADER] + for group in _EMI_EQUIV: + lines.extend(f'a_{s} = a_{group[0]}' for s in group[1:]) + lines.extend(f'a_{s} = NaN' for s in symbols) + lines.append('') + (data_path / 'species.txt').write_text('\n'.join(lines)) # Write a Python module with a regex matching the species names - symbols = ['CO2', 'CO2e', 'C', 'Ce'] + symbols - symbols = "',\n '".join(symbols) - (BASE_PATH / 'emissions.py').write_text(_EMI_CODE.format(**locals())) + + # Prepare list including all symbols + all_symbols = list(chain(*_EMI_EQUIV, symbols)) + + # Format and write + code = _EMI_CODE.format( + symbols="',\n '".join(all_symbols), + equiv="),\n set(".join(map(repr, _EMI_EQUIV)), + ) + (BASE_PATH / 'emissions.py').write_text(code) # Write one file containing a context for each metric for metric, _data in data.groupby('metric'): # Conversion factor definitions - defs = [] - for _, row in _data.iterrows(): - defs.append(f'a_{row.Symbol} = {row.value}') + defs = [f'a_{row.Symbol} = {row.value}' for _, row in _data.iterrows()] - # Join to a single string - defs = '\n '.join(defs) + # Format the template with the definitions + content = _EMI_DATA.format(metric=metric, defs='\n '.join(defs)) # Write to file - (data_path / f'{metric}.txt').write_text( - # Format the template with the definitions - _EMI_DATA.format(**locals()) - ) + (data_path / f'{metric}.txt').write_text(content) if __name__ == '__main__': From 93becee32ee9fa059c4368d56f9c5d2e27761db7 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 21 Apr 2020 11:43:19 +0200 Subject: [PATCH 06/10] Update generated files for emissions --- iam_units/data/emissions/species.txt | 4 ++++ iam_units/emissions.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/iam_units/data/emissions/species.txt b/iam_units/data/emissions/species.txt index 5842d1f..fd26803 100644 --- a/iam_units/data/emissions/species.txt +++ b/iam_units/data/emissions/species.txt @@ -2,6 +2,10 @@ # python -m iam_units.update emissions # DO NOT ALTER THIS FILE MANUALLY! +a_CO2_eq = a_CO2 +a_CO2e = a_CO2 +a_CO2eq = a_CO2 +a_Ce = a_C a_C10F18 = NaN a_C2F6 = NaN a_C3F8 = NaN diff --git a/iam_units/emissions.py b/iam_units/emissions.py index 41987ca..a0d916a 100644 --- a/iam_units/emissions.py +++ b/iam_units/emissions.py @@ -7,7 +7,9 @@ # All recognised emission species usable with convert_gwp(). See *pattern*. SPECIES = [ 'CO2', + 'CO2_eq', 'CO2e', + 'CO2eq', 'C', 'Ce', 'C10F18', @@ -96,6 +98,12 @@ 'cC4F8', ] +# Pairs of emission species symbols that are interchangeable. +EQUIV = [ + set(['CO2', 'CO2_eq', 'CO2e', 'CO2eq']), + set(['C', 'Ce']), + ] + # Regular expression for one *SPECIES* in a pint-compatible unit string. pattern = re.compile( '(?<=[ -])(' From 5283c4fa27609a2fe880cc681aa8ef5061b62795 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 21 Apr 2020 11:43:36 +0200 Subject: [PATCH 07/10] Format comments, add spaces in emissions.txt --- iam_units/data/emissions/emissions.txt | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/iam_units/data/emissions/emissions.txt b/iam_units/data/emissions/emissions.txt index 59a421e..9dfa4f1 100644 --- a/iam_units/data/emissions/emissions.txt +++ b/iam_units/data/emissions/emissions.txt @@ -1,18 +1,25 @@ -# Dummy base unit used for GWP conversions of [mass] -> [mass]. This unit has -# no physical meaning. +# Dummy base unit used for GWP conversions of [mass] -> [_GWP] -> [mass]. +# This unit has no physical meaning and should not be used on its own. + _gwp = [_GWP] -# The reference species, CO2, has a conversion factor of 1 -a_CO2 = 1.0 = a_CO2e +# The reference species, CO2, has a conversion factor of 1. + +a_CO2 = 1.0 + +# Conversion to/from carbon equivalent is the same regardless of metric. -# Conversion to/from carbon equivalent is the same regardless of metric -a_C = 44. / 12 = a_Ce +a_C = 44. / 12 + +# Define: +# - Equivalents, e.g. a_CO2e = a_CO2. +# - Conversion factors for each species, with NaN values. pint requires +# that this is done before setting context-specific values. -# Define conversion factors for each species with NaN values. pint requires -# that this is done outside of setting specific values for a context. @import species.txt # Define contexts for each set of metrics + @import AR5GWP100.txt @import AR4GWP100.txt @import SARGWP100.txt From e216deba4642bf2a783c6de44fe7b219e7d4ae12 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 21 Apr 2020 11:55:59 +0200 Subject: [PATCH 08/10] Allow equivalent species symbols in convert_gwp() --- iam_units/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iam_units/__init__.py b/iam_units/__init__.py index 27be32f..d429a06 100644 --- a/iam_units/__init__.py +++ b/iam_units/__init__.py @@ -64,9 +64,11 @@ def convert_gwp(metric, quantity, *species): # Re-assemble the expression for the units or whole quantity expr = q0 + q1 - # metric may be 'None' iff the input and output species are the same + # *metric* can only be None if the input and output species symbols are + # identical or equivalent if metric is None: - if species_in == species_out: + if (species_in == species_out or + any({species_in, species_out} <= g for g in emissions.EQUIV)): metric = 'AR5GWP100' elif species_in in species_out: # Both a DimensionalityError ('CO2' → 'CO2 / a') and a ValueError From bfedef676eae676d6815f0e702e527e78dc6115c Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 21 Apr 2020 11:58:47 +0200 Subject: [PATCH 09/10] Test conversion of equivalent GHG symbols --- iam_units/test_all.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/iam_units/test_all.py b/iam_units/test_all.py index e845505..2242a5d 100644 --- a/iam_units/test_all.py +++ b/iam_units/test_all.py @@ -76,7 +76,14 @@ def test_emissions_internal(): ('AR5GWP100', 'CH4', 'CO2e', 28), ('AR4GWP100', 'CH4', 'CO2', 25), ('SARGWP100', 'CH4', 'CO2', 21), - (None, 'CO2', 'CO2', 1.), + + # Same-species conversion with metric=None and compatible names + (None, 'CO2', 'CO2_eq', 1.), + (None, 'CO2eq', 'CO2e', 1.), + + # Species names which are substrings of one another match correctly + ('AR5GWP100', 'HFC143', 'CO2', 328.0), + ('AR5GWP100', 'HFC143a', 'CO2', 4800.0), ]) def test_convert_gwp(units, metric, species_in, species_out, expected_value): # Bare masses can be converted From 748079bcd5f85e793bcccbc0e78282a71c8bffb0 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Tue, 21 Apr 2020 11:59:09 +0200 Subject: [PATCH 10/10] Copyedit comments and docstring of convert_gwp() --- iam_units/__init__.py | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/iam_units/__init__.py b/iam_units/__init__.py index d429a06..6827b94 100644 --- a/iam_units/__init__.py +++ b/iam_units/__init__.py @@ -29,39 +29,38 @@ def convert_gwp(metric, quantity, *species): Metric conversion factors to use. May be :obj:`None` if the input and output species are the same. quantity : str or pint.Quantity or tuple - Quantity to convert. If a tuple, the arguments are passed to the - :class:`pint.Quantity` constructor. + Quantity to convert. If a tuple of (magnitude, unit), these are passed + as arguments to :class:`pint.Quantity`. species : sequence of str, length 1 or 2 - Output, or (input, output) species, e.g. ('CH4', 'CO2') to convert - mass of CH₄ to GWP-equivalent mass of CO₂. If only the output species - is provided, *quantity* must contain the name of the input species in - some location, e.g. 'tonne CH4 / year'. + Output, or (input, output) species symbols, e.g. ('CH4', 'CO2') to + convert mass of CH₄ to GWP-equivalent mass of CO₂. If only the output + species is provided, *quantity* must contain the symbol of the input + species in some location, e.g. '1.0 tonne CH4 / year'. Returns ------- pint.Quantity `quantity` converted from the input to output species. """ - # Handle *species* + # Handle *species*: either (in, out) or only out try: - # Split a 2-tuple species_in, species_out = species except ValueError: if len(species) != 1: raise ValueError('Must provide (from, to) or (to,) species') - # Only output emissions species provided species_in, species_out = None, species[0] - # Split *quantity* if it is a tuple: + # Split *quantity* if it is a tuple. After this step: # - *mag* is the magnitude, or None. # - *expr* is a string expression for either just the units, or the entire - # quantity including magnitude. + # quantity, including magnitude, as a str or pint.Quantity. mag, expr = quantity if isinstance(quantity, tuple) else (None, quantity) + # If species_in wasn't provided, then *expr* must contain it if not species_in: - # *expr* must contain the input species; extract it using the regex + # Extract it using the regex, then re-assemble the expression for the + # units or whole quantity q0, species_in, q1 = emissions.pattern.split(expr, maxsplit=1) - # Re-assemble the expression for the units or whole quantity expr = q0 + q1 # *metric* can only be None if the input and output species symbols are @@ -71,26 +70,26 @@ def convert_gwp(metric, quantity, *species): any({species_in, species_out} <= g for g in emissions.EQUIV)): metric = 'AR5GWP100' elif species_in in species_out: - # Both a DimensionalityError ('CO2' → 'CO2 / a') and a ValueError - # (no metric); raise the former for pyam compat + # Eg. 'CO2' in 'CO2 / a'. This is both a DimensionalityError and a + # ValueError (no metric); raise the former for pyam compat raise pint.DimensionalityError(species_in, species_out) else: msg = f'Must provide GWP metric for ({species_in}, {species_out})' raise ValueError(msg) # Ensure a pint.Quantity object: - # - If tuple input was given, use the 2-arg constructor. - # - If not, use the 1-arg form to convert a string. - # - If the input was already a Quantity, this is a no-op. + # - If *quantity* was a tuple, use the 2-arg constructor. + # - If a str, use the 1-arg form to parse it. + # - If already a pint.Quantity, this is a no-op. args = (expr,) if mag is None else (mag, expr) quantity = registry.Quantity(*args) - # Intermediate units with the same dimensionality as *quantity*, except - # '[mass]' replaced with the dummy unit '_gwp' + # Construct intermediate units with the same dimensionality as *quantity*, + # except '[mass]' replaced with the dummy unit '_gwp' dummy = quantity.units / registry.Unit('tonne / _gwp') - # Convert to GWP dummy units using 'a' for the input species; then back to - # the input units using 'a' for the output species. + # Convert to dummy units using 'a' for the input species; then back to the + # input units using 'a' for the output species. return quantity.to(dummy, metric, _a=f'a_{species_in}') \ .to(quantity.units, metric, _a=f'a_{species_out}')