Source code for message_ix_models.util.scenarioinfo

""":class:`ScenarioInfo` class."""
import logging
from collections import defaultdict
from itertools import product
from typing import Dict, List

import pandas as pd
import sdmx.model

log = logging.getLogger(__name__)


[docs]class ScenarioInfo: """Information about a :class:`~message_ix.Scenario` object. Code that prepares data for a target Scenario can accept a ScenarioInfo instance. This avoids the need to load a Scenario, which can be slow under some conditions. ScenarioInfo objects can also be used (e.g. by :func:`.apply_spec`) to describe the contents of a Scenario *before* it is created. ScenarioInfo objects have the following convenience attributes: .. autosummary:: set is_message_macro N Y y0 yv_ya Parameters ---------- scenario : message_ix.Scenario If given, :attr:`.set` is initialized from this existing scenario. """ #: Elements of :mod:`ixmp`/:mod:`message_ix` sets. set: Dict[str, List] = {} #: Elements of :mod:`ixmp`/:mod:`message_ix` parameters. par: Dict[str, pd.DataFrame] = {} #: First model year, if set, else ``Y[0]``. y0: int = -1 #: :obj:`True` if a MESSAGE-MACRO scenario. is_message_macro: bool = False _yv_ya: pd.DataFrame = None def __init__(self, scenario=None): self.set = defaultdict(list) self.par = dict() if not scenario: return for name in scenario.set_list(): try: self.set[name] = scenario.set(name).tolist() except AttributeError: continue # pd.DataFrame for ≥2-D set; don't convert for name in ("duration_period",): self.par[name] = scenario.par(name) self.is_message_macro = "PRICE_COMMODITY" in scenario.par_list() # Computed once fmy = scenario.cat("year", "firstmodelyear") self.y0 = int(fmy[0]) if len(fmy) else self.set["year"][0] self._yv_ya = scenario.vintage_and_active_years() @property def yv_ya(self): """:class:`pandas.DataFrame` with valid ``year_vtg``, ``year_act`` pairs.""" if self._yv_ya is None: first = self.y0 # Product of all years yv = ya = self.set["year"] # Predicate for filtering years def _valid(elem): yv, ya = elem return first <= yv <= ya # - Cartesian product of all yv and ya. # - Filter only valid years. # - Convert to data frame. self._yv_ya = pd.DataFrame( filter(_valid, product(yv, ya)), columns=["year_vtg", "year_act"] ) return self._yv_ya @property def N(self): """Elements of the set 'node'.""" return list(map(str, self.set["node"])) @property def Y(self): """Elements of the set 'year' that are >= the first model year.""" return list(filter(lambda y: y >= self.y0, self.set["year"]))
[docs] def year_from_codes(self, codes: List[sdmx.model.Code]): """Update using a list of `codes`. The following are updated: - :attr:`.set` ``year`` - :attr:`.set` ``cat_year``, with the first model year. - :attr:`.par` ``duration_period`` Any existing values are discarded. After this, the attributes :attr:`.y0` and :attr:`.Y` give the first model year and model years, respectively. Examples -------- Get a particular code list, create a ScenarioInfo instance, and update using the codes: >>> years = get_codes("year/A") >>> info = ScenarioInfo() >>> info.year_from_codes(years) Use populated values: >>> info.y0 2020 >>> info.Y[:3] [2020, 2030, 2040] >>> info.Y[-3:] [2090, 2100, 2110] """ from message_ix_models.util import eval_anno # Clear existing values if len(self.set["year"]): log.debug(f"Discard existing 'year' elements: {repr(self.set['year'])}") self.set["year"] = [] if len(self.set["cat_year"]): log.debug( f"Discard existing 'cat_year' elements: {repr(self.set['cat_year'])}" ) self.set["cat_year"] = [] if "duration_period" in self.par: log.debug("Discard existing 'duration_period' elements") fmy_set = False duration_period: List[Dict] = [] # TODO use sorted() here once supported by sdmx for code in codes: year = int(code.id) # Store the year self.set["year"].append(year) # Check for an annotation 'firstmodelyear: true' if eval_anno(code, "firstmodelyear"): if fmy_set: # No coverage: data that triggers this should not be committed raise ValueError( # pragma: no cover "≥2 periods are annotated firstmodelyear: true" ) self.y0 = year self.set["cat_year"].append(("firstmodelyear", year)) fmy_set = True # Store the duration_period: either from an annotation, or computed vs. the # prior period duration_period.append( dict( year=year, value=eval_anno(code, "duration_period") or (year - duration_period[-1]["year"]), unit="y", ) ) # Store self.par["duration_period"] = pd.DataFrame(duration_period)