import logging
from copy import copy
from functools import partial
from urllib.parse import urlunsplit
import message_ix
from sdmx.model.v21 import Code
import message_ix_models
from message_ix_models import ScenarioInfo, Spec
from .build import apply_spec
from .config import Config
from .data import get_data
from .structure import codelists, get_codes
log = logging.getLogger(__name__)
def _default_first(kind, default):
return [default] + list(filter(lambda id: id != default, codelists(kind)))
#: Deprecated; use :class:`.model.Config` instead.
SETTINGS = dict(
# Place the default value first
regions=_default_first("node", "R14"),
years=_default_first("year", "B"),
res_with_dummies=[False, True],
)
[docs]
def create_res(context, quiet=True):
"""Create a 'bare' MESSAGEix-GLOBIOM reference energy system (RES).
Parameters
----------
context : .Context
:attr:`.model.Config.scenario_info` determines the model name and scenario name
of the created Scenario. If not provided, the defaults are:
- Model name generated by :func:`name`.
- Scenario name "baseline".
quiet : bool, optional
Passed to `quiet` argument of :func:`.build.apply_spec`.
Returns
-------
message_ix.Scenario
A scenario as described by :func:`.bare.get_spec`, prepared using
:func:`.apply_spec`.
"""
mp = context.get_platform()
# Retrieve the spec; this also sets defaults expected by name()
spec = get_spec(context)
# Model and scenario name for the RES
args = dict(
mp=mp,
model=context.scenario_info.get("model", name(context)),
scenario=context.scenario_info.get("scenario", "baseline"),
version="new",
)
# TODO move this to ixmp as a method similar to ixmp.util.parse_url()
url = urlunsplit(
("ixmp", mp.name, args["model"] + "/" + args["scenario"], "", args["version"])
)
log.info(f"Create {repr(url)}")
# Create the Scenario
scenario = message_ix.Scenario(**args)
# TODO move to message_ix
scenario.init_par("MERtoPPP", ["node", "year"])
# Apply the spec
apply_spec(
scenario,
spec,
data=partial(get_data, context=context, spec=spec),
quiet=quiet,
message=f"Create using message-ix-models {message_ix_models.__version__}",
)
return scenario
[docs]
def get_spec(context) -> Spec:
"""Return the spec for the MESSAGE-GLOBIOM global model RES.
If :attr:`.Config.res_with_dummies` is set, additional elements are added:
- ``commodity``: "dummy"
- ``technology``: "dummy", "dummy supply"
These **may** be used for testing purposes, but **should not** be used in production
models.
Returns
-------
:class:`dict` of :class:`.ScenarioInfo` objects
"""
context.setdefault("model", Config())
add = ScenarioInfo()
# Add technologies
add.set["technology"] = copy(get_codes("technology"))
# Add regions
# Load configuration for the specified region mapping
nodes = get_codes(f"node/{context.model.regions}")
# Top-level "World" node
world = nodes[nodes.index("World")]
# Set elements: World, followed by the direct children of World
add.set["node"] = [world] + world.child
# Add relations
add.set["relation"] = get_codes(f"relation/{context.model.relations}")
# Initialize time periods
add.year_from_codes(get_codes(f"year/{context.model.years}"))
# Add levels
add.set["level"] = get_codes("level")
# Add commodities
add.set["commodity"] = get_codes("commodity")
# Add units, associated with commodities
units = set(
commodity.eval_annotation(id="unit") for commodity in add.set["commodity"]
)
# Deduplicate by converting to a set and then back; not strictly necessary,
# but reduces duplicate log entries
add.set["unit"] = sorted(filter(None, units))
if context.model.res_with_dummies:
# Add dummy technologies
add.set["technology"].extend([Code(id="dummy"), Code(id="dummy source")])
# Add a dummy commodity
add.set["commodity"].append(Code(id="dummy"))
# The RES is the base, so does not require or remove any elements
return Spec(add=add)
[docs]
def name(context, *, unique: bool = False) -> str:
"""Generate a candidate name for a model given `context`.
The name has a form like::
MESSAGEix-GLOBIOM R99 YA +D
MESSAGEix-GLOBIOM R99 YA a1b2c
where:
- "R99" is the node list/regional aggregation.
- "YA" indicates the year codelist (:doc:`/pkg-data/year`).
- "+D" appears if `unique` is :any:`False` and :attr:`.Config.res_with_dummies` is
:any:`True`.
- A hexidecimal hash digest like "12b2c" appears if `unique` is :any:`True`. This
value is unique for every possible combination of settings on
:class:`.model.Config`; see :meth:`.Config.hexdigest`.
"""
cfg = context.model
result = f"MESSAGEix-GLOBIOM {cfg.regions} Y{cfg.years}"
if not unique:
result += " +D" if cfg.res_with_dummies else ""
else:
result += " " + cfg.hexdigest(5)
return result