Skip to content

Commit 4fb571c

Browse files
committed
Check backend in .scenario_setup functions
- Replace "NOTE this assumes an IXMP4Backend" with call to on_ixmp4backend(); make functions no-ops if False. - Import ixmp4 classes used only for hinting inside a TYPE_CHECKING block. - Add compose_maps() for common pattern of calling compose_*_maps(). - Remove redundant conditions in .core.Scenario, .model.MESSAGE.
1 parent ef90ad1 commit 4fb571c

File tree

3 files changed

+112
-97
lines changed

3 files changed

+112
-97
lines changed

message_ix/core.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -885,21 +885,14 @@ def rename(self, name: str, mapping: Mapping[str, str], keep: bool = False) -> N
885885
maybe_commit(self, commit, f"Rename {name!r} using mapping {mapping}")
886886

887887
def commit(self, comment: str) -> None:
888-
if on_ixmp4backend(self):
889-
# NB The following statements are similar to part of MESSAGE.run()
890-
from message_ix.util.scenario_setup import (
891-
compose_dimension_map,
892-
compose_period_map,
893-
)
894-
# JDBCBackend calls these functions as part of every commit, but they have
895-
# moved to message_ix because they handle message-specific data
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
896892

897-
# The sanity checks fail for some tests (e.g. 'node' being empty)
898-
# ensure_required_indexsets_have_data(scenario=self)
893+
# The sanity checks fail for some tests (e.g. 'node' being empty)
894+
# ensure_required_indexsets_have_data(scenario=self)
899895

900-
# Compose some auxiliary tables
901-
for dimension in ("node", "time"):
902-
compose_dimension_map(scenario=self, dimension=dimension)
903-
compose_period_map(scenario=self)
896+
compose_maps(self)
904897

905898
return super().commit(comment)

message_ix/models.py

Lines changed: 57 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ def initialize(cls, scenario: "ixmp.core.scenario.Scenario") -> None:
334334
--------
335335
:attr:`items`
336336
"""
337+
from message_ix.util.scenario_setup import add_default_data
338+
337339
# Check for storage items that may contain incompatible data or need to be
338340
# re-initialized
339341
state = None
@@ -355,81 +357,70 @@ def initialize(cls, scenario: "ixmp.core.scenario.Scenario") -> None:
355357
# Initialize the ixmp items for MESSAGE
356358
cls.initialize_items(scenario, items)
357359

358-
if on_ixmp4backend(scenario):
359-
from message_ix.util.scenario_setup import add_default_data
360-
361-
assert isinstance(scenario, Scenario)
362-
363-
if not scenario.platform._units_to_warn_about:
364-
scenario.platform._units_to_warn_about = REQUIRED_UNITS.copy()
365-
366-
# NOTE I tried transcribing this from ixmp_source as-is, but the MESSAGE
367-
# class defined in models.py takes care of setting up the Scenario -- except
368-
# for adding default data.
369-
# ixmp_source does other things, too, which I don't think we need here, but
370-
# I've kept them in for completeness for now.
371-
372-
# ixmp_source first sets up a Scenario and adds default data
373-
# models.MESSAGE seems to do the setup for us in all cases, while
374-
# add_default_data() only adds missing items, so can always run.
375-
# TODO Is this correct?
376-
# if version == "new":
377-
# # If the Scenario already exists, we don't need these two
378-
# set_up_scenario(s=self)
379-
add_default_data(scenario=scenario)
380-
381-
# TODO We don't seem to need this, but if we do, give them better names
382-
# self.tecParList = [
383-
# parameter_info for parameter_info in PARAMETERS if parameter_info.is_tec # noqa: E501
384-
# ]
385-
# self.tecActParList = [
386-
# parameter_info
387-
# for parameter_info in PARAMETERS
388-
# if parameter_info.is_tec_act
389-
# ]
390-
391-
# TODO the following could be activated in ixmp_source through the flag
392-
# parameter `sanity_checks`. This 'sanity_check' (there are more, s.b.) is
393-
# generally only active when loading a scenario from the DB (unless
394-
# explicitly loading via ID, in which case it's also inactive). We don't
395-
# distinguish loading from the DB and some tutorials failed, so disable.
396-
# ensure_required_indexsets_have_data(s=self)
397-
398-
# TODO It does not seem useful to construct these because some required
399-
# indexsets won't have any data in them yet. They do get run in imxp_source
400-
# at this point, though.
401-
# compose_dimension_map(s=self, dimension="node")
402-
# compose_dimension_map(s=self, dimension="time")
403-
# compose_period_map(s=self)
360+
assert isinstance(scenario, Scenario)
361+
362+
if not scenario.platform._units_to_warn_about:
363+
scenario.platform._units_to_warn_about = REQUIRED_UNITS.copy()
364+
365+
# NOTE I tried transcribing this from ixmp_source as-is, but the MESSAGE class
366+
# defined in models.py takes care of setting up the Scenario -- except for
367+
# adding default data.
368+
# ixmp_source does other things, too, which I don't think we need here, but I've
369+
# kept them in for completeness for now.
370+
371+
# ixmp_source first sets up a Scenario and adds default data
372+
# models.MESSAGE seems to do the setup for us in all cases, while
373+
# add_default_data() only adds missing items, so can always run.
374+
# TODO Is this correct?
375+
# if version == "new":
376+
# # If the Scenario already exists, we don't need these two
377+
# set_up_scenario(s=self)
378+
add_default_data(scenario=scenario)
379+
380+
# TODO We don't seem to need this, but if we do, give them better names
381+
# self.tecParList = [
382+
# parameter_info for parameter_info in PARAMETERS if parameter_info.is_tec
383+
# ]
384+
# self.tecActParList = [
385+
# parameter_info
386+
# for parameter_info in PARAMETERS
387+
# if parameter_info.is_tec_act
388+
# ]
389+
390+
# TODO the following could be activated in ixmp_source through the flag
391+
# parameter `sanity_checks`. This 'sanity_check' (there are more, s.b.) is
392+
# generally only active when loading a scenario from the DB (unless explicitly
393+
# loading via ID, in which case it's also inactive). We don't distinguish
394+
# loading from the DB and some tutorials failed, so disable.
395+
# ensure_required_indexsets_have_data(s=self)
396+
397+
# TODO It does not seem useful to construct these because some required
398+
# indexsets won't have any data in them yet. They do get run in imxp_source at
399+
# this point, though.
400+
# compose_maps(scenario=scenario)
404401

405402
# Commit if anything was removed
406403
maybe_commit(scenario, state, f"{cls.__name__}.initialize")
407404

408405
def run(self, scenario: "ixmp.core.scenario.Scenario") -> None:
409-
if on_ixmp4backend(scenario):
410-
from message_ix.util.gams_io import (
411-
add_auxiliary_items_to_container_data_list,
412-
add_default_data_to_container_data_list,
413-
store_message_version,
414-
)
415-
from message_ix.util.scenario_setup import (
416-
compose_dimension_map,
417-
compose_period_map,
418-
ensure_required_indexsets_have_data,
419-
)
420-
421-
assert isinstance(scenario, Scenario) # Narrow type
406+
from message_ix.util.gams_io import (
407+
add_auxiliary_items_to_container_data_list,
408+
add_default_data_to_container_data_list,
409+
store_message_version,
410+
)
411+
from message_ix.util.scenario_setup import (
412+
compose_maps,
413+
ensure_required_indexsets_have_data,
414+
)
422415

423-
# Run the sanity checks
424-
ensure_required_indexsets_have_data(scenario=scenario)
416+
assert isinstance(scenario, Scenario) # Narrow type
425417

426-
# NB The following are similar to statements in .core.Scenario.commit()
427-
# Compose some auxiliary tables
428-
for dimension in ("node", "time"):
429-
compose_dimension_map(scenario=scenario, dimension=dimension)
418+
# Run the sanity checks
419+
ensure_required_indexsets_have_data(scenario=scenario)
430420

431-
compose_period_map(scenario=scenario)
421+
compose_maps(scenario=scenario)
432422

423+
if on_ixmp4backend(scenario):
433424
# ixmp.model.gams.GAMSModel.__init__() creates the container_data attribute
434425
# from its .defaults and any user kwargs
435426

message_ix/util/scenario_setup.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import pandas as pd
55

66
if TYPE_CHECKING:
7+
from ixmp4 import Run
8+
from ixmp4.core import IndexSet, Parameter, Table
9+
710
from message_ix.core import Scenario
811

912
from ixmp import Platform
10-
from ixmp4 import Run
11-
from ixmp4.core import IndexSet, Parameter, Table
13+
14+
from message_ix.util.ixmp4 import on_ixmp4backend
1215

1316
from .scenario_data import (
1417
DEFAULT_INDEXSET_DATA,
@@ -28,9 +31,11 @@
2831

2932
# (According to ixmp_source.)
3033
# """
31-
# # NOTE this assumes an IXMP4Backend
34+
# if not on_ixmp4backend(scenario):
35+
# return
36+
#
3237
# # Get the Run associated with the Scenario
33-
# run = cast(Run, scenario.platform._backend.index[scenario])
38+
# run = cast("Run", scenario.platform._backend.index[scenario])
3439

3540
# # Add all required IndexSets
3641
# for indexset_name in REQUIRED_INDEXSETS:
@@ -71,9 +76,11 @@
7176

7277
def add_default_data(scenario: "Scenario") -> None:
7378
"""Add default data expected in a MESSAGEix Scenario."""
74-
# NOTE this assumes an IXMP4Backend
79+
if not on_ixmp4backend(scenario):
80+
return
81+
7582
# Get the Run associated with the Scenario
76-
run = cast(Run, scenario.platform._backend.index[scenario])
83+
run = cast("Run", scenario.platform._backend.index[scenario])
7784

7885
# Add IndexSet data
7986
for indexset_data_info in DEFAULT_INDEXSET_DATA:
@@ -121,11 +128,13 @@ def ensure_required_indexsets_have_data(scenario: "Scenario") -> None:
121128
ValueError
122129
If the required IndexSets are empty.
123130
"""
131+
if not on_ixmp4backend(scenario):
132+
return
133+
124134
indexsets_to_check = ("node", "technology", "year", "time")
125135

126-
# NOTE this assumes an IXMP4Backend
127136
# Get the Run associated with the Scenario
128-
run = cast(Run, scenario.platform._backend.index[scenario])
137+
run = cast("Run", scenario.platform._backend.index[scenario])
129138

130139
# Raise an error if any of the checked IndexSets are empty
131140
for name in indexsets_to_check:
@@ -136,7 +145,7 @@ def ensure_required_indexsets_have_data(scenario: "Scenario") -> None:
136145

137146

138147
def _maybe_add_to_table(
139-
table: Table, data: Union[dict[str, Any], pd.DataFrame]
148+
table: "Table", data: Union[dict[str, Any], pd.DataFrame]
140149
) -> None:
141150
"""Add (parts of) `data` to `table` if they are missing."""
142151
# NOTE This function doesn't handle empty data as internally, this won't happen
@@ -166,9 +175,11 @@ def compose_dimension_map(
166175
dimension: 'node' or 'time'
167176
Whether to handle the spatial or temporal dimension.
168177
"""
169-
# NOTE this assumes an IXMP4Backend
178+
if not on_ixmp4backend(scenario):
179+
return
180+
170181
# Get the Run associated with the Scenario
171-
run = cast(Run, scenario.platform._backend.index[scenario])
182+
run = cast("Run", scenario.platform._backend.index[scenario])
172183

173184
# Handle both spatial and temporal dimensions
174185
name_part = "spatial" if dimension == "node" else "temporal"
@@ -236,15 +247,15 @@ def _find_all_descendants(parent: T) -> list[T]:
236247

237248

238249
def _maybe_add_single_item_to_indexset(
239-
indexset: IndexSet, data: Union[float, int, str]
250+
indexset: "IndexSet", data: Union[float, int, str]
240251
) -> None:
241252
"""Add `data` to `indexset` if it is missing."""
242253
if data not in list(indexset.data):
243254
indexset.add(data=data)
244255

245256

246257
def _maybe_add_list_to_indexset(
247-
indexset: IndexSet, data: Union[list[float], list[int], list[str]]
258+
indexset: "IndexSet", data: Union[list[float], list[int], list[str]]
248259
) -> None:
249260
"""Add missing parts of `data` to `indexset`."""
250261
# NOTE missing will always only have one type, but how to tell mypy?
@@ -255,7 +266,8 @@ def _maybe_add_list_to_indexset(
255266

256267

257268
def _maybe_add_to_indexset(
258-
indexset: IndexSet, data: Union[float, int, str, list[float], list[int], list[str]]
269+
indexset: "IndexSet",
270+
data: Union[float, int, str, list[float], list[int], list[str]],
259271
) -> None:
260272
"""Add (parts of) `data` to `indexset` if they are missing."""
261273
# NOTE This function doesn't handle empty data as internally, this won't happen
@@ -268,7 +280,7 @@ def _maybe_add_to_indexset(
268280
# NOTE this could be combined with `_maybe_add_to_table()`, but that function would be
269281
# slower than necessary (though likely not by much). Is the maintenance effort worth it?
270282
def _maybe_add_to_parameter(
271-
parameter: Parameter, data: Union[dict[str, Any], pd.DataFrame]
283+
parameter: "Parameter", data: Union[dict[str, Any], pd.DataFrame]
272284
) -> None:
273285
"""Add (parts of) `data` to `parameter` if they are missing."""
274286
# NOTE This function doesn't handle empty data as internally, this won't happen
@@ -291,14 +303,33 @@ def _maybe_add_to_parameter(
291303
parameter.add(data=new_data)
292304

293305

306+
def compose_maps(scenario: "Scenario") -> None:
307+
"""Compose maps.
308+
309+
- Call :func:`compose_dimension_map` for:
310+
311+
- :py:`dimension="node"`
312+
- :py:`dimension="time"`
313+
314+
- Call :func:`compose_period_map`.
315+
"""
316+
# Compose some auxiliary tables
317+
for dimension in ("node", "time"):
318+
compose_dimension_map(scenario=scenario, dimension=dimension)
319+
320+
compose_period_map(scenario=scenario)
321+
322+
294323
def compose_period_map(scenario: "Scenario") -> None:
295324
"""Add data to the 'duration_period' Parameter in `scenario`.
296325
297326
This covers `assignPeriodMaps()` from ixmp_source.
298327
"""
299-
# NOTE this assumes an IXMP4Backend
328+
if not on_ixmp4backend(scenario):
329+
return
330+
300331
# Get the Run associated with the Scenario
301-
run = cast(Run, scenario.platform._backend.index[scenario])
332+
run = cast("Run", scenario.platform._backend.index[scenario])
302333

303334
# TODO Included here in ixmp_source; this should likely move to add_default_data
304335
# Add one default item to 'type_year'

0 commit comments

Comments
 (0)