Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
413cf95
Add ixmp4 to pyproject.toml optional deps
khaeru Jan 25, 2024
68f9a43
Add minimal/empty .backend.ixmp4.IXMP4Backend
khaeru Jan 25, 2024
1083b27
Adjust test_platform.test_init
khaeru Jan 25, 2024
1c608af
Add TestPlatform.test_init1()
khaeru Jan 25, 2024
89a615e
Add stub/missing methods to IXMP4Backend
khaeru Jan 25, 2024
a1cc287
Implement IXMP4Backend.__init__
khaeru Jan 25, 2024
4d8b053
Add parametrized TestPlatform.mp fixture
khaeru Jan 25, 2024
1f4a7c6
Add parametrized TestPlatform.test_scenario_list()
khaeru Jan 25, 2024
fc2ed4b
Implement IXMP4Backend.get_scenarios()
khaeru Jan 25, 2024
1183b0c
Add ixmp4 to type checking packages
khaeru Jan 25, 2024
86536c1
✨ feat: Add default ixmp4 platform
glatterf42 Jan 20, 2025
6d461f5
✨ feat: Parametrize test_mp and enable IXMP4Backend.__init__
glatterf42 Jan 20, 2025
90765e6
✨ feat: Use correct DB in test setup & add some backend functions
glatterf42 Jan 21, 2025
b336b58
♻️ refactor: Avoid installing ixmp4 for Python 3.9
glatterf42 Jan 21, 2025
8a92337
♻️ refactor: Avoid running ixmp4 tests on Python 3.9
glatterf42 Jan 21, 2025
6fd393a
✨ feat: Use specific ixmp4 branch as dependency
glatterf42 Jan 28, 2025
dd04c73
✨ feat: Make type hints more concise
glatterf42 Jan 28, 2025
8e36620
✨ feat: Add various platform functions
glatterf42 Jan 28, 2025
8dee3f6
✨ feat: Add type hints to platform tests
glatterf42 Jan 28, 2025
00c7819
♻️ refactor: Update ixmp4-branch dependency
glatterf42 Jan 28, 2025
046ea87
✨ feat: Add setup function for ixmp4-backed Platform
glatterf42 Jan 28, 2025
815cafa
✨ feat: Add has_solution()
glatterf42 Jan 28, 2025
27422d4
♻️ refactor: Add World-region definition to ixmp4 platform setup
glatterf42 Jan 28, 2025
49952cf
✨ feat: Add more platform functions
glatterf42 Jan 28, 2025
6f2d6e1
♻️ refactor: Make sure to handle defined variables
glatterf42 Jan 28, 2025
080719e
Fix item_index return value and type hints
glatterf42 Feb 6, 2025
aeccf61
Improve type hints
glatterf42 Feb 12, 2025
20fec5b
Introduce GAMS I/O for IXMP4Backend
glatterf42 Feb 12, 2025
c9d41ea
Adjust existing logic to ixmp4 gams i/o
glatterf42 Feb 12, 2025
dc49351
Merge .backend.io._add_*_to_container() functions
khaeru Feb 12, 2025
e210bea
Move TypedDict-Kwargs to dedicated file
glatterf42 Mar 3, 2025
f949537
Fix imports for TypeVar/type hints
glatterf42 Mar 3, 2025
eb8519e
Add more backend functions:
glatterf42 Mar 3, 2025
c5edf10
Add required units to ixmp4-backed test platforms
glatterf42 Mar 3, 2025
9f0e7f2
Add ContainerData to pass data structuredly to GAMS Container
glatterf42 Mar 24, 2025
affedc5
Handle `container_data` in GAMSModel class
glatterf42 Mar 24, 2025
41adec0
Fix various things to enable message_ix.Scenario.solve()
glatterf42 Mar 24, 2025
ffda46a
Improve type hints
glatterf42 Mar 24, 2025
f07bb7e
Rename columns as expected by IXMPReporter
glatterf42 Mar 24, 2025
ae1cc62
Add unit list requiring warnings to Platform
glatterf42 Mar 25, 2025
45af85f
Fix annotation for Scenario.scheme
glatterf42 Mar 25, 2025
b38e11b
Use persistent local ixmp4 DB per default
glatterf42 Mar 25, 2025
813698b
Enable tab completion for platform.__init__(backend=...)
glatterf42 Apr 9, 2025
1c33c15
Fix read-in of Equations & Variables and gams container creation
glatterf42 Apr 9, 2025
22e19d2
Update ixmp4-branch dependency
glatterf42 Apr 9, 2025
0f4e6d5
Enable all message_ix tutorials
glatterf42 Apr 9, 2025
461945b
Use specific ixmp4 branch for pre-commit
glatterf42 Apr 9, 2025
e51393e
Fix some docstrings and type hints
glatterf42 Apr 9, 2025
d0925f0
Handle missing kwargs["filters"] for base Backend file I/O
glatterf42 Apr 15, 2025
b1078ca
Provide GAMS-required Equation type
glatterf42 Apr 15, 2025
9057b46
Fix IXMP4Backend for more passing tests
glatterf42 Apr 15, 2025
866b930
Add new pytest markers to distinguish backends
glatterf42 Apr 15, 2025
81d769f
Parametrize tests based on new markers & add units for ixmp4
glatterf42 Apr 15, 2025
3de8d42
Add tests for new/private ixmp4-related functions
glatterf42 Apr 15, 2025
eb4c628
Apply new markers to test suite as needed
glatterf42 Apr 15, 2025
5c8004f
Install gams version matching gamsapi
glatterf42 Apr 15, 2025
95d9e1e
Avoid hardcoded test expectation
glatterf42 Apr 16, 2025
442ab73
Fix IXMP4Backend.__init__()
glatterf42 Apr 16, 2025
ed53038
Avoid ixmp4 imports for Python 3.9
glatterf42 Apr 16, 2025
6342d9d
Allow test platforms to already exist
glatterf42 Apr 16, 2025
0055bc8
Fix outdated test expectation
glatterf42 Apr 16, 2025
05b950a
Handle conditional import for ixmp4 IO functions
glatterf42 Apr 16, 2025
06eca42
Limit all ixmp4 tests to Python > 3.9
glatterf42 Apr 17, 2025
46f34c6
Improve new tests
glatterf42 Apr 17, 2025
812b3ef
Document stat IXMP4Backend requires Python > 3.9
glatterf42 Apr 17, 2025
300b3dd
Add PR to release notes
glatterf42 Apr 17, 2025
12d4810
Pass gams system_dir when constructing Container
glatterf42 Apr 23, 2025
be81fad
Omit `.venv` from coverage reporting
glatterf42 Apr 23, 2025
48e3b44
Guard against wrong order of ReadKwargs
glatterf42 Apr 24, 2025
691bb24
Add tests for some ixmp4 edge cases
glatterf42 Apr 24, 2025
8db95ab
Add docstring to new pytest hook
glatterf42 Apr 24, 2025
8e8b870
Use the oldest gams version possible for gamsapi in CI
glatterf42 Apr 25, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- "3.12"
- "3.13" # Latest supported by ixmp
gams-version:
- "43.4.1" # First version including a macOS arm64 distribution
- "45.7.0" # Oldest version of gamsapi allowed by pyproject.toml

exclude:
# Specific version combinations that are invalid / not to be used
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ repos:
additional_dependencies:
- genno
- GitPython
- "ixmp4 @ git+https://github.com/iiasa/ixmp4@enh/remove-linked-items-when-removing-indexset-items"
- nbclient
- pandas-stubs
- pytest
Expand Down
15 changes: 11 additions & 4 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
.. Next release
.. ============
Next release
============

All changes
-----------

- Add :class:`.IXMP4Backend` as an alternative to :class:`.JDBCBackend` (:pull:`552`).
Please note:

- This requires ixmp4, which is only compatible with Python 3.10 and above.
- :class:`.IXMP4Backend` is still missing several features and documentation, which will be added in subsequent PRs.

.. All changes
.. -----------

.. _v3.10.0:

Expand Down
9 changes: 9 additions & 0 deletions doc/api-backend.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ Provided backends

.. autofunction:: ixmp.backend.jdbc.start_jvm

.. currentmodule:: ixmp.backend.ixmp4

.. warning::
`ixmp4 <https://github.com/iiasa/ixmp4/>`_ only supports Python 3.10 and above.
Please ensure you are using a sufficiently recent Python version if you want to use IXMP4Backend.
If you are restricted to Python 3.9 and below, please use JDBCBackend instead.

.. autoclass:: ixmp.backend.ixmp4.IXMP4Backend


.. currentmodule:: ixmp.backend

Expand Down
5 changes: 5 additions & 0 deletions ixmp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
# Register Backends provided by ixmp
BACKENDS["jdbc"] = JDBCBackend

if sys.version_info >= (3, 10):
from ixmp.backend.ixmp4 import IXMP4Backend

BACKENDS["ixmp4"] = IXMP4Backend

# Register Models provided by ixmp
MODELS.update(
{
Expand Down
8 changes: 8 additions & 0 deletions ixmp/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ def _platform_default():
"driver": "hsqldb",
"path": next(_iter_config_paths())[1].joinpath("localdb", "default"),
},
"ixmp4-local": {
"class": "ixmp4",
"dsn": (
"sqlite:///"
f"{Path.home().joinpath('.local', 'share', 'ixmp4', 'databases')}"
"/ixmp4-local.sqlite3"
),
},
}


Expand Down
61 changes: 44 additions & 17 deletions ixmp/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

import pandas as pd

# TODO Import this from typing when dropping Python 3.11
from typing_extensions import Unpack

from ixmp.backend import ItemType
from ixmp.backend.io import s_read_excel, s_write_excel, ts_read_file
from ixmp.core.platform import Platform
from ixmp.core.scenario import Scenario
from ixmp.core.timeseries import TimeSeries
from ixmp.util.ixmp4 import ReadKwargs, WriteFiltersKwargs, WriteKwargs


class Backend(ABC):
Expand Down Expand Up @@ -204,12 +208,13 @@ def set_node(
"""

@abstractmethod
def get_nodes(self) -> Iterable[tuple[str, Optional[str], str, str]]:
def get_nodes(self) -> Iterable[tuple[str, Optional[str], Optional[str], str]]:
"""Iterate over all nodes stored on the Platform.

Yields
-------
tuple

The members of each tuple are:

========= =========== ===
Expand Down Expand Up @@ -378,7 +383,9 @@ def get_units(self) -> list[str]:
set_unit
"""

def read_file(self, path: PathLike, item_type: ItemType, **kwargs) -> None:
def read_file(
self, path: PathLike, item_type: ItemType, **kwargs: Unpack[ReadKwargs]
) -> None:
"""OPTIONAL: Read Platform, TimeSeries, or Scenario data from file.

A backend **may** implement read_file for one or more combinations of the `path`
Expand Down Expand Up @@ -418,16 +425,21 @@ def read_file(self, path: PathLike, item_type: ItemType, **kwargs) -> None:
--------
write_file
"""
s, filters = self._handle_rw_filters(kwargs.pop("filters", {}))
filters = kwargs["filters"] if "filters" in kwargs else WriteFiltersKwargs()
s, _ = self._handle_rw_filters(filters=filters)
_kwargs = {k: v for (k, v) in kwargs.items() if k != "filters"}

path = Path(path)
if path.suffix in (".csv", ".xlsx") and item_type is ItemType.TS and s:
ts_read_file(s, path, **kwargs)
ts_read_file(s, path, **_kwargs)
elif path.suffix == ".xlsx" and item_type is ItemType.MODEL and s:
s_read_excel(self, s, path, **kwargs)
s_read_excel(self, s, path, **_kwargs)
else:
raise NotImplementedError

def write_file(self, path: PathLike, item_type: ItemType, **kwargs) -> None:
def write_file(
self, path: PathLike, item_type: ItemType, **kwargs: Unpack[WriteKwargs]
) -> None:
"""OPTIONAL: Write Platform, TimeSeries, or Scenario data to file.

A backend **may** implement write_file for one or more combinations of the
Expand Down Expand Up @@ -460,12 +472,19 @@ def write_file(self, path: PathLike, item_type: ItemType, **kwargs) -> None:
"""
# Use the "scenario" filter to retrieve the Scenario `s` to be written; reappend
# any other filters
s, kwargs["filters"] = self._handle_rw_filters(kwargs.pop("filters", {}))
filters = kwargs["filters"] if "filters" in kwargs else WriteFiltersKwargs()
s, kwargs["filters"] = self._handle_rw_filters(filters=filters)

xlsx_types = (ItemType.SET | ItemType.PAR, ItemType.MODEL)
path = Path(path)
if path.suffix == ".xlsx" and item_type in xlsx_types and s:
s_write_excel(self, s, path, item_type, **kwargs)
s_write_excel(
be=self,
s=s,
path=path,
item_type=item_type,
filters=kwargs["filters"],
)
else:
# All other combinations of arguments
raise NotImplementedError
Expand Down Expand Up @@ -581,18 +600,20 @@ def preload(self, ts: TimeSeries) -> None:
"""OPTIONAL: Load `ts` data into memory."""

@staticmethod
def _handle_rw_filters(filters: dict) -> tuple[Optional[TimeSeries], dict]:
def _handle_rw_filters(
filters: WriteFiltersKwargs,
) -> tuple[Optional[TimeSeries], WriteFiltersKwargs]:
"""Helper for :meth:`read_file` and :meth:`write_file`.

The `filters` argument is unpacked if the 'scenarios' key is a single
:class:`.TimeSeries` object. A 2-tuple is returned of the object (or
:obj:`None`) and the remaining filters.
"""
ts = None
ts: Optional[TimeSeries] = None
filters = copy(filters)
try:
if isinstance(filters["scenario"], TimeSeries):
ts = filters.pop("scenario")
ts = filters["scenario"]
except KeyError:
pass # Don't modify filters at all

Expand Down Expand Up @@ -820,7 +841,7 @@ def has_solution(self, s: Scenario) -> bool:
"""

@abstractmethod
def list_items(self, s: Scenario, type: str) -> list[str]:
def list_items(self, s: Scenario, type: Literal["set", "par", "equ"]) -> list[str]:
"""Return a list of names of items of `type`.

Parameters
Expand All @@ -832,7 +853,7 @@ def list_items(self, s: Scenario, type: str) -> list[str]:
def init_item(
self,
s: Scenario,
type: str,
type: Literal["set", "par", "equ", "var"],
name: str,
idx_sets: Sequence[str],
idx_names: Optional[Sequence[str]],
Expand Down Expand Up @@ -860,7 +881,9 @@ def init_item(
"""

@abstractmethod
def delete_item(self, s: Scenario, type: str, name: str) -> None:
def delete_item(
self, s: Scenario, type: Literal["set", "par", "equ"], name: str
) -> None:
"""Remove an item `name` of `type`.

Parameters
Expand All @@ -871,7 +894,9 @@ def delete_item(self, s: Scenario, type: str, name: str) -> None:
"""

@abstractmethod
def item_index(self, s: Scenario, name: str, sets_or_names: str) -> list[str]:
def item_index(
self, s: Scenario, name: str, sets_or_names: Literal["sets", "names"]
) -> list[str]:
"""Return the index sets or names of item `name`.

Parameters
Expand Down Expand Up @@ -932,7 +957,7 @@ def item_get_elements(
def item_set_elements(
self,
s: Scenario,
type: str,
type: Literal["par", "set"],
name: str,
elements: Iterable[tuple[Any, Optional[float], Optional[str], Optional[str]]],
) -> None:
Expand Down Expand Up @@ -974,7 +999,9 @@ def item_set_elements(
"""

@abstractmethod
def item_delete_elements(self, s: Scenario, type: str, name: str, keys) -> None:
def item_delete_elements(
self, s: Scenario, type: Literal["par", "set"], name: str, keys
) -> None:
"""Remove elements of item `name`.

Parameters
Expand Down
Loading