Skip to content

Commit

Permalink
Add to_ixmp4() method (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielhuppmann authored Feb 22, 2024
1 parent 835065c commit 0762055
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 3 deletions.
3 changes: 2 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Next Release
# Next release

- [#813](https://github.com/IAMconsortium/pyam/pull/813) Fix a corner case in region-aggregation with missing data
- [#797](https://github.com/IAMconsortium/pyam/pull/797) Add `to_ixmp4()` method to write to an **ixmp4** platform

# Release v2.1.0

Expand Down
13 changes: 13 additions & 0 deletions pyam/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from pathlib import Path
from tempfile import TemporaryDirectory

import ixmp4

from pyam.ixmp4 import write_to_ixmp4
from pyam.slice import IamSlice
from pyam.filter import filter_by_time_domain, filter_by_year, filter_by_dt_arg

Expand Down Expand Up @@ -2338,6 +2341,16 @@ def diff(self, mapping, periods=1, append=False):
# append to `self` or return as `IamDataFrame`
return self._finalize(_value, append=append)

def to_ixmp4(self, platform: ixmp4.Platform):
"""Save all scenarios as new default runs in an ixmp4 platform database instance
Parameters
----------
platform : :class:`ixmp4.Platform` or str
The ixmp4 platform database instance to which the scenario data is saved
"""
write_to_ixmp4(platform, self)

def _to_file_format(self, iamc_index):
"""Return a dataframe suitable for writing to a file"""
df = self.timeseries(iamc_index=iamc_index).reset_index()
Expand Down
42 changes: 42 additions & 0 deletions pyam/ixmp4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ixmp4
from ixmp4.core.region import RegionModel
from ixmp4.core.unit import UnitModel


def write_to_ixmp4(platform: ixmp4.Platform | str, df):
"""Save all scenarios as new default runs in an ixmp4 platform database instance
Parameters
----------
platform : :class:`ixmp4.Platform` or str
The ixmp4 platform database instance to which the scenario data is saved
df : pyam.IamDataFrame
The IamDataFrame instance with scenario data
"""
if df.time_domain != "year":
raise NotImplementedError("Only time_domain='year' is supported for now")

if not isinstance(platform, ixmp4.Platform):
platform = ixmp4.Platform(platform)

# TODO: implement a try-except to roll back changes if any error writing to platform
# depends on https://github.com/iiasa/ixmp4/issues/29
# quickfix: ensure that units and regions exist before writing
for dimension, values, model in [
("regions", df.region, RegionModel),
("units", df.unit, UnitModel),
]:
platform_values = getattr(platform, dimension).tabulate().name.values
if missing := set(values).difference(platform_values):
raise model.NotFound(
", ".join(missing)
+ f". Use `Platform.{dimension}.create()` to add the missing {dimension}."
)

for model, scenario in df.index:
_df = df.filter(model=model, scenario=scenario)

run = platform.runs.create(model=model, scenario=scenario)
run.iamc.add(_df.data)
run.meta = dict(_df.meta.iloc[0])
run.set_as_default()
2 changes: 1 addition & 1 deletion pyam/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def _intuit_column_groups(df, index, include_index=False):
elif isinstance(df, pd.DataFrame):
existing_cols = existing_cols.union(df.columns)

# check that there is no column in the timeseries data with reserved names
# check that there is no unnamed column in the timeseries data
if None in existing_cols:
raise ValueError("Unnamed column in timeseries data: None")

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ python_requires = >=3.10, <3.12
# Please also add a section "Dependency changes" to the release notes
install_requires =
iam-units >= 2020.4.21
ixmp4 >= 0.4.0
ixmp4 >= 0.6.0
numpy >= 1.23.0, < 1.24
# requests included via ixmp4
# httpx[http2] included via ixmp4
Expand Down
35 changes: 35 additions & 0 deletions tests/test_ixmp4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest
from ixmp4.core import Platform
from ixmp4.data.backend import SqliteTestBackend
from ixmp4.core.region import RegionModel
from ixmp4.core.unit import UnitModel


def test_to_ixmp4_missing_region_raises(test_df_year):
"""Writing to platform raises if region not defined"""
platform = Platform(_backend=SqliteTestBackend())
with pytest.raises(RegionModel.NotFound, match="World. Use `Platform.regions."):
test_df_year.to_ixmp4(platform=platform)


def test_to_ixmp4_missing_unit_raises(test_df_year):
"""Writing to platform raises if unit not defined"""
platform = Platform(_backend=SqliteTestBackend())
platform.regions.create(name="World", hierarchy="common")
with pytest.raises(UnitModel.NotFound, match="EJ/yr. Use `Platform.units."):
test_df_year.to_ixmp4(platform=platform)


def test_ixmp4_integration(test_df):
"""Write an IamDataFrame to the platform"""
platform = Platform(_backend=SqliteTestBackend())
platform.regions.create(name="World", hierarchy="common")
platform.units.create(name="EJ/yr")

if test_df.time_domain == "year":
test_df.to_ixmp4(platform=platform)
else:
with pytest.raises(NotImplementedError):
test_df.to_ixmp4(platform=platform)

# TODO add test for reading data from ixmp4 platform

0 comments on commit 0762055

Please sign in to comment.