diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a63a402c5..226ca8638 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ # Next Release +- [#952](https://github.com/IAMconsortium/pyam/pull/952) Add an `aggregate_kyoto_gases()` method + # Release v3.3.0 ## Highlights diff --git a/pyam/aggregation.py b/pyam/aggregation.py index b4b4f289b..efe17fede 100644 --- a/pyam/aggregation.py +++ b/pyam/aggregation.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -def _aggregate(df, variable, components=None, method="sum"): +def aggregate_data(df, variable, components=None, method="sum"): """Internal implementation of the `aggregate` function""" if components is not None: @@ -74,7 +74,7 @@ def _aggregate_recursive(df, variable, recursive): var_list = {reduce_hierarchy(v, -1) for v in components} # a temporary dataframe allows to distinguish between full data and new data - _data_agg = _aggregate(_df, variable=var_list) + _data_agg = aggregate_data(_df, variable=var_list) # check if data for intermediate variables already exists with adjust_log_level("pyam.core"): diff --git a/pyam/core.py b/pyam/core.py index cbfd3e497..2757ce752 100755 --- a/pyam/core.py +++ b/pyam/core.py @@ -11,6 +11,7 @@ import pandas as pd from pandas.api.types import is_integer +from pyam.emissions import aggregate_kyoto_ghg from pyam.netcdf import to_xarray try: @@ -23,11 +24,11 @@ from pyam._compare import _compare from pyam.aggregation import ( - _aggregate, _aggregate_recursive, _aggregate_region, _aggregate_time, _group_and_agg, + aggregate_data, ) from pyam.compute import IamComputeAccessor from pyam.exceptions import format_log_message, raise_data_error @@ -246,6 +247,7 @@ def _finalize(self, data, append, **args): else: if data is None or data.empty: return _empty_iamframe(self.dimensions + ["value"]) + return IamDataFrame(data, meta=self.meta, **args) def __getitem__(self, key): @@ -1459,7 +1461,7 @@ def aggregate( _aggregate_recursive(self, variable, recursive), meta=self.meta ) else: - _df = _aggregate(self, variable, components=components, method=method) + _df = aggregate_data(self, variable, components=components, method=method) # append to `self` or return as `IamDataFrame` return self._finalize(_df, append=append) @@ -1499,7 +1501,7 @@ def check_aggregate( """ # compute aggregate from components, return None if no components - df_components = _aggregate(self, variable, components, method) + df_components = aggregate_data(self, variable, components, method) if df_components is None: return @@ -1864,6 +1866,31 @@ def check_internal_consistency(self, components=False, **kwargs): ] ] + def aggregate_kyoto_ghg(self, *, metric: str, append: bool = False): + """Compute the aggregate Kyoto gases from a set of species using a GWP metric + + metric: str + A global warming potential (GWP) metric supported by :mod:`iam_units`, + e.g. 'AR6GWP100'. + append : bool, optional + Append the aggregate emissions timeseries to `self` and return None, + else return aggregated emissions timeseries as new :class:`IamDataFrame`. + + See Also + -------- + pyam.IamDataFrame.convert_unit + + """ + return self._finalize( + aggregate_kyoto_ghg( + self, + metric, + f"Emissions|Kyoto Gases [{metric}]", + "Mt CO2-equiv/yr", + ), + append=append, + ) + def slice(self, *, keep=True, **kwargs): """Return a (filtered) slice object of the IamDataFrame timeseries data index diff --git a/pyam/emissions.py b/pyam/emissions.py new file mode 100644 index 000000000..507ad08d3 --- /dev/null +++ b/pyam/emissions.py @@ -0,0 +1,50 @@ +from pyam.aggregation import aggregate_data +from pyam.exceptions import raise_data_error + +REQUIRED_KYOTO_SPECIES = ["Emissions|CO2", "Emissions|CH4", "Emissions|N2O"] + + +ALL_KYOTO_SPECIES = { + "Emissions|CO2", + "Emissions|CH4", + "Emissions|N2O", + "Emissions|HFC|HFC125", + "Emissions|HFC|HFC134a", + "Emissions|HFC|HFC143a", + "Emissions|HFC|HFC152a", + "Emissions|HFC|HFC227ea", + "Emissions|HFC|HFC23", + "Emissions|HFC|HFC236fa", + "Emissions|HFC|HFC245fa", + "Emissions|HFC|HFC32", + "Emissions|HFC|HFC365mfc", + "Emissions|HFC|HFC4310mee", + "Emissions|NF3", + "Emissions|SF6", + "Emissions|C2F6", + "Emissions|C3F8", + "Emissions|C4F10", + "Emissions|C5F12", + "Emissions|C6F14", + "Emissions|C7F16", + "Emissions|C8F18", + "Emissions|CF4", + "Emissions|cC4F8", +} + + +def aggregate_kyoto_ghg(df, metric: str, target_variable: str, target_unit: str): + """Internal implementation of the `aggregate_kyoto_ghg` function""" + + _df = df.filter(variable=ALL_KYOTO_SPECIES) + + missing = _df.require_data(variable=REQUIRED_KYOTO_SPECIES) + if missing is not None: + raise_data_error( + "Missing emission species required for Kyoto GHG aggregation", missing + ) + + for unit in _df.unit: + _df.convert_unit(unit, target_unit, context=metric, inplace=True) + + return aggregate_data(_df, target_variable, components=_df.variable) diff --git a/tests/test_emissions.py b/tests/test_emissions.py new file mode 100644 index 000000000..ee2bb35c0 --- /dev/null +++ b/tests/test_emissions.py @@ -0,0 +1,60 @@ +import pandas as pd +import pytest + +from pyam import IamDataFrame +from pyam.testing import assert_iamframe_equal + +EMISSIONS_SPECIES_DATA = pd.DataFrame( + [ + ["Emissions|CO2", "Mt CO2/yr", 42885.41, 33011.87, 24642.81], + ["Emissions|CH4", "Mt CH4/yr", 413.63, 287.42, 233.97], + ["Emissions|N2O", "kt N2O/yr", 11623.95, 9005.23, 8177.40], + ["Emissions|SF6", "kt SF6/yr", 8.01, 5.26, 2.60], + ["Emissions|HFC|HFC125", "kt HFC125/yr", 98.76, 57.44, 16.71], + ["Emissions|HFC|HFC134a", "kt HFC134a/yr", 248.84, 144.53, 42.41], + ["Emissions|HFC|HFC143a", "kt HFC143a/yr", 40.59, 23.61, 6.87], + ["Emissions|HFC|HFC23", "kt HFC23/yr", 7.13, 4.24, 1.55], + ["Emissions|HFC|HFC32", "kt HFC32/yr", 61.18, 35.55, 10.29], + ], + columns=["variable", "unit", 2020, 2025, 2030], +) + + +EXP_GHG_DATA = pd.DataFrame( + [ + [ + "Emissions|Kyoto Gases [AR6GWP100]", + "Mt CO2-equiv/yr", + 58938.34, + 44284.49, + 33666.64, + ] + ], + columns=["variable", "unit", 2020, 2025, 2030], +) + + +@pytest.mark.parametrize("append", ((False, True))) +def test_kyoto_ghg(append): + df_args = dict(model="model_a", scenario="scenario_a", region="World") + df = IamDataFrame(EMISSIONS_SPECIES_DATA, **df_args) + exp = IamDataFrame(EXP_GHG_DATA, **df_args) + + if append: + obs = df.copy() + obs.aggregate_kyoto_ghg(metric="AR6GWP100", append=append) + exp = df.append(exp) + else: + obs = df.aggregate_kyoto_ghg(metric="AR6GWP100") + + assert_iamframe_equal(exp, obs) + + +def test_kyoto_ghg_raises(): + df_args = dict(model="model_a", scenario="scenario_a", region="World") + df = IamDataFrame(EMISSIONS_SPECIES_DATA, **df_args) + df.filter(variable="Emissions|CH4", keep=False, inplace=True) + + match = "Missing species for aggregation:.* scenario_a Emissions|CH4" + with pytest.raises(ValueError, match=match): + df.aggregate_kyoto_ghg(metric="AR6GWP100")