Skip to content

Commit 9491eb5

Browse files
authored
Merge pull request #946 from iiasa/ixmp4/quiet-init
Quiet logging from MESSAGE.initialize
2 parents bd44bd5 + 35f41da commit 9491eb5

File tree

3 files changed

+121
-26
lines changed

3 files changed

+121
-26
lines changed

RELEASE_NOTES.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
.. Next release
2-
.. ============
1+
Next release
2+
============
33

4-
.. All changes
5-
.. -----------
4+
All changes
5+
-----------
6+
7+
- Filter log noise generated when loading scenarios created with :mod:`message_ix`/:mod:`ixmp` v3.10 and earlier (:pull:`946`).
68

79
.. _v3.11.0:
810

@@ -32,7 +34,6 @@ Users **should**:
3234
and adjust |growth_new_capacity_up| or |initial_new_capacity_up| values as necessary.
3335
For the latter, :func:`.initial_new_capacity_up_v311` may be used.
3436

35-
3637
All changes
3738
-----------
3839

message_ix/models.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
2+
import re
23
from collections import ChainMap
3-
from collections.abc import Mapping, MutableMapping
4+
from collections.abc import Iterator, Mapping, MutableMapping
5+
from contextlib import contextmanager
46
from copy import copy
57
from dataclasses import InitVar, dataclass, field
6-
from functools import partial
8+
from functools import cache, partial
79
from pathlib import Path
810
from typing import TYPE_CHECKING, Optional
911
from warnings import warn
@@ -18,6 +20,8 @@
1820
from message_ix.util.scenario_data import REQUIRED_EQUATIONS, REQUIRED_VARIABLES
1921

2022
if TYPE_CHECKING:
23+
from logging import LogRecord
24+
2125
import ixmp.core.scenario
2226

2327
import message_ix.core
@@ -283,6 +287,48 @@ def _check_structure(scenario: "ixmp.core.scenario.Scenario"):
283287
yield name, info.ix_type, N, message
284288

285289

290+
@cache
291+
def _log_filter_pattern(cls: type[GAMSModel]) -> "re.Pattern":
292+
"""Return a compiled :class:`re.Pattern` for filtering log messages.
293+
294+
Messages like the following are matched:
295+
296+
- "Existing index sets of 'FOO' [] do not match …"
297+
- "Existing index names of 'BAR' [] do not match …"
298+
299+
…where "FOO" or "BAR" are :any:`ItemType.EQU` or :any:`ItemType.VAR` among
300+
the :attr:`cls.items <GAMSModel.items>`. Such log entries are generated by
301+
:meth:`ixmp.
302+
303+
The result is :func:`functools.cache`'d, thus only generated once.
304+
"""
305+
# Names of EQU or VAR-type items
306+
names = sorted(k for k, v in cls.items.items() if v.type & ItemType.SOLUTION)
307+
308+
# Expression matching log message from ixmp.model.base.Model.initialize_items()
309+
return re.compile(
310+
rf"Existing index (set|name)s of '({'|'.join(names)})' \[\] do not match"
311+
)
312+
313+
314+
@contextmanager
315+
def _filter_log_initialize_items(cls: type[GAMSModel]) -> Iterator[None]:
316+
"""Context manager to filter log messages related to existing items."""
317+
pattern = _log_filter_pattern(cls)
318+
319+
def _filter(record: "LogRecord") -> bool:
320+
# Retrieve the compiled expression
321+
return not pattern.match(record.msg)
322+
323+
# Attach the filter to the ixmp.model logger
324+
logger = logging.getLogger("ixmp.model.base")
325+
logger.addFilter(_filter)
326+
try:
327+
yield
328+
finally:
329+
logger.removeFilter(_filter)
330+
331+
286332
class MESSAGE(GAMSModel):
287333
"""Model class for MESSAGE."""
288334

@@ -358,8 +404,11 @@ def initialize(cls, scenario: "ixmp.core.scenario.Scenario") -> None:
358404
if isinstance(scenario.platform._backend, JDBCBackend):
359405
items.pop("balance_equality")
360406

361-
# Initialize the ixmp items for MESSAGE
362-
cls.initialize_items(scenario, items)
407+
# Hide verbose log messages if `scenario` was created with message_ix <3.10 and
408+
# is being loaded with v3.11 or later
409+
with _filter_log_initialize_items(cls):
410+
# Initialize the ixmp items for MESSAGE
411+
cls.initialize_items(scenario, items)
363412

364413
if not isinstance(scenario, Scenario):
365414
# Narrow type of `scenario`

message_ix/tests/test_models.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,78 @@
1+
"""Tests of :class:`ixmp.model.base.Model` classes in :mod:`message_ix.models`."""
2+
3+
import re
14
from collections import defaultdict
5+
from typing import TYPE_CHECKING
26

37
import ixmp
48
import pytest
59
from ixmp.backend.jdbc import JDBCBackend
610

711
from message_ix.models import MESSAGE, MESSAGE_MACRO
812

13+
if TYPE_CHECKING:
14+
from ixmp import Platform
15+
16+
17+
class TestMESSAGE:
18+
def test_initialize(self, caplog, test_mp: "Platform") -> None:
19+
# Expected numbers of items by type
20+
exp = defaultdict(list)
21+
for name, spec in MESSAGE.items.items():
22+
exp[str(spec.type.name).lower()].append(name)
23+
24+
# balance_equality is removed in initialize() for JDBC
25+
if isinstance(test_mp._backend, JDBCBackend):
26+
exp["set"].remove("balance_equality")
27+
28+
# Use ixmp.Scenario to avoid invoking ixmp_source/Java code that automatically
29+
# populates empty scenarios
30+
s = ixmp.Scenario(test_mp, model="m", scenario="s", version="new")
31+
32+
# Initialization succeeds on a totally empty scenario
33+
MESSAGE.initialize(s)
34+
35+
# The expected items exist
36+
for ix_type, exp_names in exp.items():
37+
obs_names = getattr(s, f"{ix_type}_list")()
38+
assert sorted(obs_names) == sorted(exp_names)
39+
40+
def test_initialize_filter_log(self, caplog, test_mp: "Platform") -> None:
41+
"""Test :meth:`MESSAGE.initialize` logging under some conditions.
42+
43+
For :class:`.Scenario` created with message_ix v3.10 or earlier, equations and
44+
variables may be initialized but have zero dimensions, thus empty lists of
45+
"index sets" and "index names". When :class:`.Scenario` is instantiated,
46+
:meth:`MESSAGE.initialize` is invoked, and in turn
47+
:meth:`ixmp.model.base.Model.initialize_items`. This method generates many log
48+
messages on level :data:`~logging.WARNING`.
949
10-
def test_initialize(test_mp: ixmp.Platform) -> None:
11-
# Expected numbers of items by type
12-
exp = defaultdict(list)
13-
for name, spec in MESSAGE.items.items():
14-
exp[str(spec.type.name).lower()].append(name)
50+
In order to prevent this log noise, :func:`.models._filter_log_initialize_items`
51+
is used. This test checks that it is effective.
52+
"""
53+
# Use ixmp.Scenario to avoid invoking ixmp_source/Java code that automatically
54+
# populates empty scenarios
55+
s = ixmp.Scenario(test_mp, model="m", scenario="s", version="new")
1556

16-
# balance_equality is removed in initialize() for JDBC
17-
if isinstance(test_mp._backend, JDBCBackend):
18-
exp["set"].remove("balance_equality")
57+
# Initialize an equation with no dimensions. This mocks the state of a Scenario
58+
# created with message_ix v3.10 or earlier.
59+
s.init_equ("NEW_CAPACITY_BOUND_LO", idx_sets=[], idx_names=[])
1960

20-
# Use ixmp.Scenario to avoid invoking ixmp_source/Java code that
21-
# automatically populates empty scenarios
22-
s = ixmp.Scenario(test_mp, "test_initialize", "test_initialize", version="new")
61+
s.commit("")
62+
s.set_as_default()
63+
caplog.clear()
2364

24-
# Initialization succeeds on a totally empty scenario
25-
MESSAGE.initialize(s)
65+
# Initialize items.
66+
MESSAGE.initialize(s)
2667

27-
# The expected items exist
28-
for ix_type, exp_names in exp.items():
29-
obs_names = getattr(s, f"{ix_type}_list")()
30-
assert sorted(obs_names) == sorted(exp_names)
68+
# Messages related to re-initializing items with 0 dimensions are filtered and
69+
# do not reach `caplog`. This assertion fails with message_ix v3.10.
70+
message_pattern = re.compile(
71+
r"Existing index (name|set)s of 'NEW_CAPACITY_BOUND_LO' \[\] do not match "
72+
r"\('node', '.*', 'year'\)"
73+
)
74+
extra = list(filter(message_pattern.match, caplog.messages))
75+
assert not extra, f"{len(extra)} unwanted log messages: {extra}"
3176

3277

3378
def test_message_macro() -> None:

0 commit comments

Comments
 (0)