diff --git a/RELEASE_NOTES.rst b/RELEASE_NOTES.rst index bcf579c37..d6f44844a 100644 --- a/RELEASE_NOTES.rst +++ b/RELEASE_NOTES.rst @@ -1,8 +1,10 @@ -.. Next release -.. ============ +Next release +============ -.. All changes -.. ----------- +All changes +----------- + +- Add :meth:`.enforce` to the :class:`~.base.Model` API for enforcing structure/data consistency before :meth:`.Model.solve` (:pull:`450`). .. _v3.5.0: diff --git a/ixmp/core/scenario.py b/ixmp/core/scenario.py index a8b3dc50b..6f1a07b8a 100644 --- a/ixmp/core/scenario.py +++ b/ixmp/core/scenario.py @@ -300,14 +300,14 @@ def add_set( # Combine iterators to tuples. If the lengths are mismatched, the sentinel # value 'False' is filled in - to_add = list(zip_longest(keys, comments, fillvalue=False)) + to_add = list(zip_longest(keys, comments, fillvalue=(False,))) # Check processed arguments for e, c in to_add: # Check for sentinel values - if e is False: + if e == (False,): raise ValueError(f"Comment {repr(c)} without matching key") - elif c is False: + elif c == (False,): raise ValueError(f"Key {repr(e)} without matching comment") elif len(idx_names) and len(idx_names) != len(e): raise ValueError( diff --git a/ixmp/model/base.py b/ixmp/model/base.py index 6af4c038d..c5a07aad1 100644 --- a/ixmp/model/base.py +++ b/ixmp/model/base.py @@ -34,6 +34,26 @@ def clean_path(cls, value: str) -> str: chars = r'<>"/\|?*' + (":" if os.name == "nt" else "") return re.sub("[{}]+".format(re.escape(chars)), "_", value) + @staticmethod + def enforce(scenario): + """Enforce data consistency in `scenario`. + + Optional. Implementations of :meth:`enforce`: + + - **should** modify the contents of sets and parameters so that `scenario` + contains structure and data that is consistent with the underlying model. + - **must not** add or remove sets or parameters; for that, use + :meth:`initiatize`. + + :meth:`enforce` is always called by :meth:`run` before the model is run or + solved; it **may** be called manually at other times. + + Parameters + ---------- + scenario : .Scenario + Object on which to enforce data consistency. + """ + @classmethod def initialize(cls, scenario): """Set up *scenario* with required items. @@ -42,8 +62,8 @@ def initialize(cls, scenario): - **may** add sets, set elements, and/or parameter values. - **may** accept any number of keyword arguments to control behaviour. - - **must not** modify existing parameter data in *scenario*, either by - deleting or overwriting values. + - **must not** modify existing parameter data in *scenario*, either by deleting + or overwriting values; for that, use :meth:`enforce`. Parameters ---------- @@ -60,12 +80,12 @@ def initialize(cls, scenario): def initialize_items(cls, scenario, items): """Helper for :meth:`initialize`. - All of the `items` are added to `scenario`. Existing items are not - modified. Errors are logged if the description in `items` conflicts - with the index set(s) and/or index name(s) of existing items. + All of the `items` are added to `scenario`. Existing items are not modified. + Errors are logged if the description in `items` conflicts with the index set(s) + and/or index name(s) of existing items. - initialize_items may perform one commit. `scenario` is in the same - state (checked in, or checked out) after initialize_items is complete. + initialize_items may perform one commit. `scenario` is in the same state + (checked in, or checked out) after initialize_items is complete. Parameters ---------- @@ -73,15 +93,15 @@ def initialize_items(cls, scenario, items): Object to initialize. items : dict of (str -> dict) Each key is the name of an ixmp item (set, parameter, equation, or - variable) to initialize. Each dict **must** have the key 'ix_type'; - one of 'set', 'par', 'equ', or 'var'; any other entries are keyword - arguments to the methods :meth:`.init_set` etc. + variable) to initialize. Each dict **must** have the key 'ix_type'; one of + 'set', 'par', 'equ', or 'var'; any other entries are keyword arguments to + the methods :meth:`.init_set` etc. Raises ------ ValueError - if `scenario` has a solution, i.e. :meth:`~.Scenario.has_solution` - is :obj:`True`. + if `scenario` has a solution, i.e. :meth:`~.Scenario.has_solution` is + :obj:`True`. See also -------- @@ -135,10 +155,9 @@ def initialize_items(cls, scenario, items): try: checkout = maybe_check_out(scenario, checkout) except ValueError as exc: # pragma: no cover - # The Scenario has a solution. This indicates an inconsistent - # situation: the Scenario lacks the item *name*, but somehow it - # was successfully solved without it, and the solution stored. - # Can't proceed further. + # The Scenario has a solution. This indicates an inconsistent situation: + # the Scenario lacks the item *name*, but somehow it was successfully + # solved without it, and the solution stored. Can't proceed further. log.error(str(exc)) return @@ -161,6 +180,10 @@ def initialize_items(cls, scenario, items): def run(self, scenario): """Execute the model. + Implementations of :meth:`run`: + + - **must** call :meth:`enforce`. + Parameters ---------- scenario : .Scenario diff --git a/ixmp/reporting/reporter.py b/ixmp/reporting/reporter.py index 16b7f0c33..263081dd4 100644 --- a/ixmp/reporting/reporter.py +++ b/ixmp/reporting/reporter.py @@ -6,16 +6,25 @@ from genno.core.computer import Computer, Key from ixmp.core.scenario import Scenario -from ixmp.reporting import computations from ixmp.reporting.util import RENAME_DIMS, keys_for_quantity class Reporter(Computer): """Class for describing and executing computations.""" - # Append ixmp.reporting.computations to the modules in which the Computer will look - # up computations names. - modules = list(Computer.modules) + [computations] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Append ixmp.reporting.computations to the modules in which the Computer will + # look up computations names. + # genno <= 1.11 + from ixmp.reporting import computations + + if computations not in self.modules: + self.modules.append(computations) + + # TODO use this once genno >= 1.12.0 is released + # self.require_compat("ixmp.reporting.computations") @classmethod def from_scenario(cls, scenario: Scenario, **kwargs) -> "Reporter":