Skip to content

Commit b1a9994

Browse files
authored
Merge pull request #991 from iiasa/issue/988
Fix 2 reporting bugs
2 parents ae12261 + a7440e2 commit b1a9994

File tree

12 files changed

+22140
-565
lines changed

12 files changed

+22140
-565
lines changed

RELEASE_NOTES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ All changes
4747
- :mod:`message_ix.message` includes :class:`.MESSAGE`.
4848
- :mod:`message_ix.message_macro` includes :class:`.MESSAGE_MACRO`.
4949

50+
- Improve :class:`.Reporter` and its documentation (:pull:`991`).
51+
52+
- Handle older scenarios—for instance, those without |input_cap|—in :meth:`.Reporter.from_scenario` (:issue:`988`).
53+
- Expand and test keys for multiple methods of calculating :ref:`reporter-historical` (:issue:`989`).
54+
5055
- Document the :ref:`minimum version of Java <install-java>` required for :class:`ixmp.JDBCBackend <ixmp.backend.jdbc.JDBCBackend>` (:pull:`962`).
5156
- Document :ref:`how to run a local PostgreSQL instance <install-postgres>`
5257
for local testing using :class:`ixmp.IXMP4Backend <ixmp.backend.ixmp4.IXMP4Backend>` (:pull:`981`).

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
gh_ref = "main" if ".dev" in version else f"v{version}"
185185

186186
extlinks = {
187-
"issue": ("https://github.com/iiasa/message_ix/issue/%s", "#%s"),
187+
"issue": ("https://github.com/iiasa/message_ix/issues/%s", "#%s"),
188188
"pull": ("https://github.com/iiasa/message_ix/pull/%s", "PR #%s"),
189189
"tut": (f"https://github.com/iiasa/message_ix/blob/{gh_ref}/tutorial/%s", None),
190190
}

doc/reporting.rst

Lines changed: 189 additions & 74 deletions
Large diffs are not rendered by default.

doc/requirements.txt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ idna==3.7
4040
# via requests
4141
imagesize==1.4.1
4242
# via sphinx
43-
importlib-metadata==8.7.0
44-
# via ixmp
45-
ixmp @ git+https://github.com/iiasa/ixmp.git@1730afd24238b4dad88a89c2d58d0fed2cd19dd0
43+
ixmp @ git+https://github.com/iiasa/ixmp.git@51ccb3276ea1d85a437ad14232890f3f3e8dec70
4644
# via -r requirements.in
4745
jinja2==3.1.6
4846
# via sphinx
@@ -165,5 +163,3 @@ xarray==2025.7.1
165163
# via
166164
# genno
167165
# ixmp
168-
zipp==3.23.0
169-
# via importlib-metadata

message_ix/common.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from contextlib import contextmanager
66
from copy import copy
77
from dataclasses import InitVar, dataclass, field
8+
from functools import cache
89
from pathlib import Path
910
from typing import TYPE_CHECKING, Any
1011

@@ -15,6 +16,7 @@
1516
if TYPE_CHECKING:
1617
from logging import LogRecord
1718

19+
from genno import Key
1820
from ixmp.types import InitializeItemsKwargs
1921

2022

@@ -68,9 +70,11 @@
6870
"yr": ("year", "year_rel"),
6971
"yv": ("year", "year_vtg"),
7072
}
73+
# Inverse mapping
74+
DIMS_INVERSE = {v[1]: k for k, v in DIMS.items()}
7175

7276

73-
@dataclass
77+
@dataclass(unsafe_hash=True)
7478
class Item:
7579
"""Description of an :mod:`ixmp` item: equation, parameter, set, or variable.
7680
@@ -118,6 +122,18 @@ def ix_type(self) -> str:
118122
"""
119123
return str(self.type.name).lower()
120124

125+
@property
126+
@cache
127+
def key(self) -> "Key":
128+
""":class:`genno.Key` for this Item in a :class:`.Reporter`.
129+
130+
Read-only.
131+
"""
132+
from genno import Key
133+
134+
dims = [DIMS_INVERSE.get(d, d) for d in self.dims or self.coords]
135+
return Key(self.name, dims)
136+
121137
def to_dict(self) -> "InitializeItemsKwargs":
122138
"""Return the :class:`dict` representation used internally in :mod:`ixmp`."""
123139
result: "InitializeItemsKwargs" = dict(

message_ix/report/__init__.py

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import logging
22
from collections.abc import Mapping
33
from functools import lru_cache, partial
4+
from itertools import product
45
from operator import itemgetter
56
from typing import TYPE_CHECKING, cast
67

8+
from genno import ComputationError, Key, KeyExistsError, Keys, MissingKeyError
79
from genno.operator import broadcast_map
8-
from ixmp.report import (
9-
ComputationError,
10-
Key,
11-
KeyExistsError,
12-
MissingKeyError,
13-
Quantity,
14-
configure,
15-
)
10+
from ixmp.backend import ItemType
11+
from ixmp.model import get_model
12+
from ixmp.report import Quantity, configure
1613
from ixmp.report import Reporter as IXMPReporter
1714

1815
from message_ix.common import DIMS
1916

2017
from .pyam import collapse_message_cols
2118

2219
if TYPE_CHECKING:
20+
from message_ix.common import GAMSModel
21+
2322
from .pyam import CollapseMessageColsKw
2423

2524
__all__ = [
2625
"ComputationError",
2726
"Key",
27+
"Keys",
2828
"KeyExistsError",
2929
"MissingKeyError",
3030
"Quantity",
@@ -61,6 +61,9 @@
6161
("map_tec", "map_as_qty", "cat_tec", "t"),
6262
("map_year", "map_as_qty", "cat_year", "y"),
6363
#
64+
# Derived set contents
65+
("current:ya-yv", "yv_ya_current", "y"),
66+
#
6467
# Products
6568
("out", "mul", "output", "ACT"),
6669
("in", "mul", "input", "ACT"),
@@ -69,6 +72,8 @@
6972
("rel", "mul", "relation_activity", "ACT"),
7073
("emi", "mul", "emission_factor", "ACT"),
7174
("inv", "mul", "inv_cost", "CAP_NEW"),
75+
("inv::historical", "mul", "inv_cost", "historical_activity"),
76+
("inv::ref", "mul", "inv_cost", "ref_activity"),
7277
("fom", "mul", "fix_cost", "CAP"),
7378
("vom", "mul", "var_cost", "ACT"),
7479
("land_out", "mul", "land_output", "LAND"),
@@ -154,6 +159,8 @@
154159
@lru_cache(1)
155160
def get_tasks() -> list[tuple[tuple, Mapping]]:
156161
"""Return a list of tasks describing MESSAGE reporting calculations."""
162+
from message_ix.message import MESSAGE
163+
157164
# Assemble queue of items to add. Each element is a 2-tuple of (positional, keyword)
158165
# arguments for Reporter.add()
159166
to_add: list[tuple[tuple, Mapping]] = []
@@ -162,15 +169,46 @@ def get_tasks() -> list[tuple[tuple, Mapping]]:
162169

163170
for t in TASKS0:
164171
if len(t) == 2 and isinstance(t[1], dict):
165-
# (args, kwargs) → update kwargs with strict
166-
t[1].update(strict)
167-
to_add.append(cast(tuple[tuple, Mapping], t))
172+
# (args, kwargs) → use strict unless already set
173+
to_add.append((t[0], strict | t[1]))
168174
else:
169175
# args only → use strict as kwargs
170176
to_add.append((t, strict))
171177

172-
# Conversions to IAMC data structure and pyam objects
178+
# Products using {historical,ref}_activity instead of ACT
179+
for (name, io), act in product(
180+
[
181+
("emi", "emission_factor"),
182+
("in", "input"),
183+
("out", "output"),
184+
("vom", "var_cost"),
185+
],
186+
["historical", "ref"],
187+
):
188+
# Construct some keys
189+
k_io = MESSAGE.items[io].key
190+
k_act = MESSAGE.items[f"{act}_activity"].key
191+
k_share = Key("share", k_act.dims, f"{name}+{act}") * "yv"
192+
k = Key(name, sorted(set(k_io.dims + k_act.dims)), act)
193+
desc = f"share of contribution of each vy to {k_act.name} in each ya"
194+
195+
to_add.extend(
196+
[
197+
((k["full"], "mul", k_io, k_act), strict),
198+
((k["current"], "mul", k["full"], "current:ya-yv"), strict),
199+
((k_share, "missing_data"), dict(key=k_share, description=desc)),
200+
(
201+
(k["weighted+full"], "mul", k["full"], k_share),
202+
dict(sums=False) | strict,
203+
),
204+
(
205+
(k["weighted"] / "yv", "sum", k["weighted+full"]),
206+
dict(dimensions=["yv"], sums=True) | strict,
207+
),
208+
]
209+
)
173210

211+
# Conversions to IAMC data structure and pyam objects
174212
for qty, collapse_kw in PYAM_CONVERT:
175213
# Column to set as year dimension + standard renames from MESSAGE to IAMC dims
176214
rename = {
@@ -213,9 +251,9 @@ def from_scenario(cls, scenario, **kwargs) -> "Reporter":
213251
.Reporter
214252
A reporter for `scenario`.
215253
"""
216-
solved = scenario.has_solution()
254+
import genno
217255

218-
if not solved:
256+
if not scenario.has_solution():
219257
log.warning(
220258
f'Scenario "{scenario.model}/{scenario.scenario}" has no solution'
221259
)
@@ -227,6 +265,22 @@ def from_scenario(cls, scenario, **kwargs) -> "Reporter":
227265
# Invoke the ixmp method
228266
rep = cast("Reporter", super().from_scenario(scenario, **kwargs))
229267

268+
# Handle missing parameters
269+
missing = set()
270+
for name, item in cast("GAMSModel", get_model(scenario.scheme)).items.items():
271+
if item.type is not ItemType.PAR or item.key in rep:
272+
continue
273+
rep.add(item.key, lambda: genno.Quantity())
274+
missing.add(item.name)
275+
276+
if missing:
277+
log.warning(
278+
f"Scenario {scenario.url!r} is missing {len(missing)} parameter(s):"
279+
+ "\n- ".join(sorted({""} | missing))
280+
+ "\n…possibly added by a newer version of message_ix. These keys will "
281+
"return empty Quantity()."
282+
)
283+
230284
# Add the MESSAGEix calculations
231285
rep.add_tasks(fail_action)
232286

message_ix/report/operator.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
from collections.abc import Mapping
22
from typing import TYPE_CHECKING, Literal, overload
33

4+
import genno
45
import pandas as pd
56

67
from message_ix.util import make_df
78

89
if TYPE_CHECKING:
9-
from genno.types import AnyQuantity
10+
from genno.types import AnyQuantity, KeyLike
1011

1112
__all__ = [
1213
"as_message_df",
14+
"missing_data",
1315
"model_periods",
1416
"plot_cumulative",
1517
"stacked_bar",
18+
"yv_ya_current",
1619
]
1720

1821

@@ -80,6 +83,16 @@ def as_message_df(qty, name, dims, common, wrap=True):
8083
return {name: df} if wrap else df
8184

8285

86+
def missing_data(key: "KeyLike", description: str = "") -> None:
87+
"""Raise an exception for missing user-supplied data."""
88+
raise RuntimeError(
89+
f"Must supply data for {key!r}. Replace {key!r} in the Reporter with a task "
90+
"returning genno.Quantity with the same dimensions"
91+
+ (f" representing {description}" if description else "")
92+
+ "."
93+
)
94+
95+
8396
def model_periods(y: list[int], cat_year: pd.DataFrame) -> list[int]:
8497
"""Return the elements of `y` beyond the firstmodelyear of `cat_year`."""
8598
return list(
@@ -200,3 +213,9 @@ def stacked_bar(
200213
ax.legend(loc="center left", bbox_to_anchor=(1.0, 0.5))
201214

202215
return ax
216+
217+
218+
def yv_ya_current(years: list[int]) -> "AnyQuantity":
219+
"""2-dimensional Quantity with value 1.0 where :math:`y^V = y^A`."""
220+
idx = pd.MultiIndex.from_tuples([(y, y) for y in years], names=["yv", "ya"])
221+
return genno.Quantity(pd.Series(1.0, index=idx))

0 commit comments

Comments
 (0)