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

Quickfix for numpy.types as meta indicators #30

Merged
merged 8 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions ixmp4/core/run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import UserDict
from typing import ClassVar, Iterable

import numpy as np
import pandas as pd

from ixmp4.data.abstract import Run as RunModel
Expand Down Expand Up @@ -114,7 +115,10 @@ def _set(self, meta: dict):
df = pd.DataFrame({"key": self.data.keys()})
df["run__id"] = self.run.id
self.backend.meta.bulk_delete(df)
df = pd.DataFrame({"key": meta.keys(), "value": meta.values()})
df = pd.DataFrame(
{"key": meta.keys(), "value": [numpy_to_pytype(v) for v in meta.values()]}
)
df.dropna(axis=0, inplace=True)
df["run__id"] = self.run.id
self.backend.meta.bulk_upsert(df)
self.df, self.data = self._get()
Expand All @@ -125,10 +129,22 @@ def __setitem__(self, key, value: int | float | str | bool):
except KeyError:
pass

self.backend.meta.create(self.run.id, key, value)
value = numpy_to_pytype(value)
if value is not None:
self.backend.meta.create(self.run.id, key, value)
self.df, self.data = self._get()

def __delitem__(self, key):
id = dict(zip(self.df["key"], self.df["id"]))[key]
self.backend.meta.delete(id)
self.df, self.data = self._get()


def numpy_to_pytype(value):
"""Cast numpy-types to basic Python types"""
if value is np.nan: # np.nan is cast to 'float', not None
return None
elif isinstance(value, np.generic):
return value.item()
else:
return value
61 changes: 56 additions & 5 deletions tests/core/test_meta.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import numpy as np
import pandas as pd
import pandas.testing as pdt
import pytest

from ..utils import all_platforms


@all_platforms
def test_run_meta(test_mp):
run1 = test_mp.Run(
"Model",
"Scenario",
version="new",
)
run1 = test_mp.Run("Model", "Scenario", version="new")
run1.set_as_default()

# set and update different types of meta indicators
Expand Down Expand Up @@ -84,3 +82,56 @@ def test_run_meta(test_mp):
run1.meta = {"mstr": "baz", "mfloat": 3.1415926535897}
exp = pd.DataFrame([[1, "mstr", "baz"]], columns=["run_id", "key", "value"])
pdt.assert_frame_equal(test_mp.meta.tabulate(key="mstr"), exp)


@all_platforms
@pytest.mark.parametrize(
"npvalue1, pyvalue1, npvalue2, pyvalue2",
[
(np.int64(1), 1, np.int64(13), 13),
(np.float64(1.9), 1.9, np.float64(13.9), 13.9),
],
)
def test_run_meta_numpy(test_mp, npvalue1, pyvalue1, npvalue2, pyvalue2):
"""Test that numpy types are cast to simple types"""
run1 = test_mp.Run("Model", "Scenario", version="new")
run1.set_as_default()

# set multiple meta indicators of same type ("value"-column of numpy-type)
run1.meta = {"key": npvalue1, "other key": npvalue1}
assert run1.meta["key"] == pyvalue1

# set meta indicators of different types ("value"-column of type `object`)
run1.meta = {"key": npvalue1, "other key": "some value"}
assert run1.meta["key"] == pyvalue1

# set meta via setter
run1.meta["key"] = npvalue2
assert run1.meta["key"] == pyvalue2

# assert that meta values were saved and updated correctly
run2 = test_mp.Run("Model", "Scenario")
assert dict(run2.meta) == {"key": pyvalue2, "other key": "some value"}


@all_platforms
@pytest.mark.parametrize("nonevalue", (None, np.nan))
def test_run_meta_none(test_mp, nonevalue):
"""Test that None-values are handled correctly"""
run1 = test_mp.Run("Model", "Scenario", version="new")
run1.set_as_default()

# set multiple indicators where one value is None
run1.meta = {"mint": 13, "mnone": nonevalue}
assert run1.meta["mint"] == 13
with pytest.raises(KeyError, match="'mnone'"):
run1.meta["mnone"]

assert dict(test_mp.Run("Model", "Scenario").meta) == {"mint": 13}

# delete indicator via setter
run1.meta["mint"] = nonevalue
with pytest.raises(KeyError, match="'mint'"):
run1.meta["mint"]

assert not dict(test_mp.Run("Model", "Scenario").meta)