Skip to content

Commit fc8f9e0

Browse files
authored
Merge pull request #894 from iiasa/enh/ixmp4
Test against ixmp4
2 parents 3b896d3 + a191e05 commit fc8f9e0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+28491
-357
lines changed

.github/workflows/pytest.yaml

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,27 @@ concurrency:
1616
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
1717
cancel-in-progress: true
1818

19+
defaults:
20+
run:
21+
shell: bash
22+
1923
env:
20-
GAMS_VERSION: 43.4.1 # First version including a macOS arm64 distribution
24+
GAMS_VERSION: 45.7.0 # Oldest version of gamsapi allowed by pyproject.toml
2125
depth: 100 # Must be large enough to include the most recent release
2226
label: "safe to test" # Label that must be applied to run on PRs from forks
2327
python-version: "3.13" # For non-matrixed jobs
28+
# Install:
29+
# - ixmp: from its `main` branch.
30+
# - all other dependencies from PyPI.
31+
#
32+
# To test against other code, such as other branches for open PRs, temporarily
33+
# add lines as appropriate. These lines SHOULD NOT be merged to `main`.
34+
#
35+
# - ixmp4: pending https://github.com/iiasa/ixmp4/pull/171.
36+
# TODO Remove once the PR is merged and a new version is released.
37+
upstream-versions: |
38+
"ixmp4 @ git+https://github.com/iiasa/ixmp4@enh/remove-linked-items-when-removing-indexset-items; python_version > '3.9'" \
39+
"ixmp @ git+https://github.com/iiasa/ixmp.git@main" \
2440
2541
jobs:
2642
check:
@@ -113,12 +129,7 @@ jobs:
113129
license: ${{ secrets.GAMS_LICENSE }}
114130

115131
- name: Install the package and dependencies
116-
# By default, the below installs ixmp from the main branch. To run against
117-
# other code, e.g. other branches for open PRs), temporarily edit as
118-
# appropriate. DO NOT merge such changes to `main`.
119-
run: |
120-
uv pip install --upgrade "ixmp @ git+https://github.com/iiasa/ixmp.git@main"
121-
uv pip install .[tests]
132+
run: uv pip install --upgrade ${{ env.upstream-versions }} .[tests]
122133

123134
- name: Run test suite using pytest
124135
env:
@@ -130,7 +141,6 @@ jobs:
130141
--color=yes --durations=20 -rA --verbose \
131142
--cov-report=xml \
132143
--numprocesses=auto --dist=loadgroup
133-
shell: bash
134144
135145
- name: Upload test coverage to Codecov.io
136146
uses: codecov/codecov-action@v5
@@ -169,7 +179,6 @@ jobs:
169179
- name: Set RETICULATE_PYTHON
170180
# Retrieve the Python executable set up above
171181
run: echo "RETICULATE_PYTHON=$(uv python find)" >> "$GITHUB_ENV"
172-
shell: bash
173182

174183
- uses: r-lib/actions/setup-r@v2
175184
id: setup-r
@@ -193,12 +202,7 @@ jobs:
193202
license: ${{ secrets.GAMS_LICENSE }}
194203

195204
- name: Install the package and dependencies
196-
# By default, the below installs ixmp from the main branch. To run against
197-
# other code, e.g. other branches for open PRs), temporarily edit as
198-
# appropriate. DO NOT merge such changes to `main`.
199-
run: |
200-
uv pip install --upgrade "ixmp @ git+https://github.com/iiasa/ixmp.git@main"
201-
uv pip install .[tests]
205+
run: uv pip install --upgrade ${{ env.upstream-versions }} .[tests]
202206

203207
- name: Install R dependencies and tutorial requirements
204208
run: |
@@ -213,7 +217,6 @@ jobs:
213217
--color=yes --durations=20 -rA --verbose \
214218
--cov-report=xml \
215219
--numprocesses=auto --dist=loadgroup
216-
shell: bash
217220
218221
- name: Upload test coverage to Codecov.io
219222
uses: codecov/codecov-action@v5

RELEASE_NOTES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Users **should**:
2828
All changes
2929
-----------
3030

31+
- Some MESSAGEix :doc:`tutorials <tutorials>` are runnable with the :class:`.IXMP4Backend` introduced in :mod:`ixmp` version 3.11 (:pull:`894`).
32+
See `Support roadmap for ixmp4 <https://github.com/iiasa/message_ix/discussions/939>`__ for details.
3133
- Adjust use of :ref:`type_tec <mapping-sets>` in :ref:`equation_emission_equivalence` (:pull:`930`, :issue:`929`, :pull:`935`).
3234

3335
This change reduces the size of the ``EMISS`` variable,

ci/rtd-requirements.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1+
# Requirements for documentation build on RTD
2+
#
3+
# These *should* generally align with env.upstream-versions
4+
# in .github/workflows/pytest.yaml
5+
6+
# Temporary, pending https://github.com/iiasa/ixmp4/pull/171.
7+
# TODO Remove once the PR is merged and a new version is released.
8+
git+https://github.com/iiasa/ixmp4@enh/remove-linked-items-when-removing-indexset-items
9+
10+
gamsapi [core,transfer] >= 45.7.0
111
git+https://github.com/iiasa/ixmp.git@main#egg=ixmp

message_ix/core.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
from collections.abc import Iterable, Mapping, Sequence
44
from functools import lru_cache, partial
55
from itertools import chain, product, zip_longest
6-
from typing import Optional, Union
6+
from typing import Optional, TypeVar, Union
77
from warnings import warn
88

99
import ixmp
1010
import numpy as np
1111
import pandas as pd
1212
from ixmp.backend import ItemType
13+
from ixmp.backend.jdbc import JDBCBackend
1314
from ixmp.util import as_str_list, maybe_check_out, maybe_commit
1415

16+
from message_ix.util.ixmp4 import on_ixmp4backend
17+
18+
# from message_ix.util.scenario_data import PARAMETERS
19+
1520
log = logging.getLogger(__name__)
1621

1722
# Also print warnings to stderr
@@ -62,7 +67,7 @@ def __init__(
6267
# Utility methods used by .equ(), .par(), .set(), and .var()
6368

6469
@lru_cache()
65-
def _year_idx(self, name):
70+
def _year_idx(self, name: str):
6671
"""Return a sequence of (idx_set, idx_name) for 'year'-indexed dims.
6772
6873
Since item dimensionality does not change, the the return value is
@@ -78,7 +83,10 @@ def _year_idx(self, name):
7883
)
7984
)
8085

81-
def _year_as_int(self, name, df):
86+
data_type = TypeVar("data_type", pd.Series, pd.DataFrame, dict)
87+
88+
# NOTE super().equ() etc hint that pd objects return, but they may also return dict
89+
def _year_as_int(self, name: str, data: data_type) -> data_type:
8290
"""Convert 'year'-indexed columns of *df* to :obj:`int` dtypes.
8391
8492
:meth:`_year_idx` is used to retrieve a sequence of (idx_set, idx_name)
@@ -90,12 +98,18 @@ def _year_as_int(self, name, df):
9098
year_idx = self._year_idx(name)
9199

92100
if len(year_idx):
93-
return df.astype({col_name: "int" for _, col_name in year_idx})
101+
assert isinstance(data, pd.DataFrame)
102+
# NOTE With the IXMP4Backend, we call scenario.par() for parameters that
103+
# might be empty, which fails df.astype() below.
104+
if data.empty:
105+
return data
106+
return data.astype({col_name: "int" for _, col_name in year_idx})
94107
elif name == "year":
95108
# The 'year' set itself
96-
return df.astype(int)
109+
assert isinstance(data, pd.Series)
110+
return data.astype(int)
97111
else:
98-
return df
112+
return data
99113

100114
# Override ixmp methods to convert 'year'-indexed columns to int
101115

@@ -251,6 +265,28 @@ def add_par(
251265
# accepts int for "year"-like dimensions. Proxy the call to avoid type check
252266
# failures.
253267
# TODO Move this upstream, to ixmp
268+
269+
if on_ixmp4backend(self):
270+
from message_ix.util.scenario_setup import check_existence_of_units
271+
272+
# Check for existence of required units
273+
# NOTE these checks are similar to those in super().add_par(), but we
274+
# need them here for access to self
275+
# NOTE it seems to me that only dict or pd.DataFrame key_or_data could
276+
# contain 'unit' already, else it's supplied by keyword or defaults to
277+
# "???"
278+
if isinstance(key_or_data, dict):
279+
_data = pd.DataFrame.from_dict(key_or_data, orient="columns")
280+
elif isinstance(key_or_data, pd.DataFrame):
281+
_data = key_or_data
282+
else:
283+
_data = pd.DataFrame()
284+
285+
if "unit" not in _data.columns:
286+
_data["unit"] = unit or "???"
287+
288+
check_existence_of_units(platform=self.platform, data=_data)
289+
254290
super().add_par(name, key_or_data, value, unit, comment) # type: ignore [arg-type]
255291

256292
add_par.__doc__ = ixmp.Scenario.add_par.__doc__
@@ -323,6 +359,7 @@ def recurse(k, v, parent="World"):
323359
recurse(k, v)
324360

325361
self.add_set("node", nodes)
362+
# TODO do we handle levels being added multiple times correctly for ixmp4?
326363
self.add_set("lvl_spatial", levels)
327364
self.add_set("map_spatial_hierarchy", hierarchy)
328365

@@ -420,8 +457,11 @@ def add_horizon(
420457
# Add the year set elements and first model year
421458
year = sorted(year)
422459
self.add_set("year", year)
460+
461+
# Avoid removing default data on IXMP4Backend
462+
is_unique = True if isinstance(self.platform._backend, JDBCBackend) else False
423463
self.add_cat(
424-
"year", "firstmodelyear", firstmodelyear or year[0], is_unique=True
464+
"year", "firstmodelyear", firstmodelyear or year[0], is_unique=is_unique
425465
)
426466

427467
# Calculate the duration of all periods
@@ -560,7 +600,7 @@ def vintage_and_active_years(
560600
vintages = sorted(
561601
self.par(
562602
"technical_lifetime",
563-
filters={"node_loc": ya_args[0], "technology": ya_args[1]},
603+
filters={"node_loc": [ya_args[0]], "technology": [ya_args[1]]},
564604
)["year_vtg"].unique()
565605
)
566606
ya_max = max(vintages) if tl_only else np.inf
@@ -585,6 +625,7 @@ def vintage_and_active_years(
585625

586626
# Minimum value for year_act
587627
if "in_horizon" in kwargs:
628+
# FIXME I don't receive this in the tutorials
588629
warn(
589630
"'in_horizon' argument to .vintage_and_active_years() will be removed "
590631
"in message_ix>=4.0. Use .query(…) instead per documentation examples.",
@@ -842,3 +883,16 @@ def rename(self, name: str, mapping: Mapping[str, str], keep: bool = False) -> N
842883
self.remove_set(name, list(mapping.keys()))
843884

844885
maybe_commit(self, commit, f"Rename {name!r} using mapping {mapping}")
886+
887+
def commit(self, comment: str) -> None:
888+
from message_ix.util.scenario_setup import compose_maps
889+
890+
# JDBCBackend calls these functions as part of every commit, but they have moved
891+
# to message_ix because they handle message-specific data
892+
893+
# The sanity checks fail for some tests (e.g. 'node' being empty)
894+
# ensure_required_indexsets_have_data(scenario=self)
895+
896+
compose_maps(self)
897+
898+
return super().commit(comment)

message_ix/model/includes/period_parameter_assignment.gms

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Sets
1818

1919
Parameter
2020
duration_period_sum(year_all,year_all2) number of years between two periods ('year_all' must precede 'year_all2')
21-
duration_time_rel(time,time2) relative duration of subannual time slice 'time2' relative to parent 'time' (only for 'time' specified in set 'time_relative')')
21+
duration_time_rel(time,time2) relative duration of subannual time slice 'time2' relative to parent 'time' (only for 'time' specified in set 'time_relative')
2222
elapsed_years(year_all) elapsed years since the start of the model horizon (not including 'year_all' period)
2323
remaining_years(year_all) remaining years until the end of the model horizon (including last period)
2424
year_order(year_all) order for members of set 'year_all'

0 commit comments

Comments
 (0)