From 572c6eaaf95eb670468937f0da921fbccf3a824b Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 22 Jul 2021 12:58:29 +0200 Subject: [PATCH 1/6] Ensure Platform.name always exists --- ixmp/core/platform.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ixmp/core/platform.py b/ixmp/core/platform.py index fc7446484..b028865d4 100644 --- a/ixmp/core/platform.py +++ b/ixmp/core/platform.py @@ -40,6 +40,9 @@ class Platform: Keyword arguments to specific to the `backend`. See :class:`.JDBCBackend`. """ + #: Name of the platform. + name: str + # Storage back end for the platform _backend: "Backend" @@ -74,6 +77,8 @@ def __init__(self, name: str = None, backend: str = None, **backend_args): if name: # Using a named platform config; retrieve it self.name, kwargs = config.get_platform_info(name) + else: + self.name = "(anonymous)" # Overwrite any platform config with explicit keyword arguments kwargs.update(backend_args) From 5db713d339365971316b194ce7216167d7758993 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 22 Jul 2021 12:59:02 +0200 Subject: [PATCH 2/6] Implement .base.Backend.clone() --- ixmp/backend/base.py | 85 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/ixmp/backend/base.py b/ixmp/backend/base.py index 441be4c02..7f83dd9e4 100644 --- a/ixmp/backend/base.py +++ b/ixmp/backend/base.py @@ -4,6 +4,7 @@ from os import PathLike from pathlib import Path from typing import ( + TYPE_CHECKING, Any, Dict, Hashable, @@ -14,6 +15,7 @@ Sequence, Tuple, Union, + cast, ) import pandas as pd @@ -776,18 +778,17 @@ def delete_geo( # Methods for ixmp.Scenario - @abstractmethod def clone( self, - s: Scenario, + ts: TimeSeries, platform_dest: Platform, model: str, scenario: str, annotation: str, keep_solution: bool, first_model_year: int = None, - ) -> Scenario: - """Clone `s`. + ) -> Union[TimeSeries, Scenario]: + """Clone `ts`. Parameters ---------- @@ -800,8 +801,8 @@ def clone( annotation : str Description for the creation of the new scenario. keep_solution : bool - If :obj:`True`, model solution data is also cloned. If - :obj:`False`, it is discarded. + If :obj:`True`, model solution data is also cloned. If :obj:`False`, it is + discarded. first_model_year : int or None If :class:`int`, must be greater than the first model year of `s`. @@ -810,6 +811,78 @@ def clone( Same class as `s` The cloned Scenario. """ + be_dest = platform_dest._backend + cls = type(ts) + + # Create the target object + target = cls( + mp=platform_dest, + model=model, + scenario=scenario, + version="new", + annotation=annotation, + scheme=getattr(ts, "scheme", None), + ) + + # TODO copy lists of identifiers + # TODO copy time series data + # TODO copy geodata + + # Copy set, parameter data + # TODO copy VAR, EQU data + for item_type in ( + (ItemType.SET, ItemType.PAR) if issubclass(cls, Scenario) else () + ): + if TYPE_CHECKING: + ts = cast(Scenario, ts) + target = cast(Scenario, target) + + type_ = ItemType(item_type).name.lower() + for name in self.list_items(ts, type_): + # Contents, dimensions, and index + data = self.item_get_elements(ts, type_, name) + dims = self.item_index(ts, name, "names") + sets = self.item_index(ts, name, "sets") + + try: + # Create the item in `target` + be_dest.init_item(target, type_, name, sets, dims) + except ValueError as e: + if "already exists" in e.args[0]: + pass + else: + raise + + # Munge data. This shouldn't be necessary; output types of + # item_get_elements should correspond to input types of + # item_set_elements + if item_type is ItemType.SET: + elements: Iterable[ + Tuple[Any, Optional[float], Optional[str], Optional[str]] + ] = map(lambda v: (v, None, None, ""), cast(pd.Series, data).values) + elif item_type is ItemType.PAR and len(dims): + elements = map( + lambda obs: ( + tuple(getattr(obs, d) for d in dims), + obs.value, + obs.unit, + "", + ), + cast(pd.DataFrame, data).itertuples(), + ) + elif item_type is ItemType.PAR: + elements = [(tuple(), data["value"], data["unit"], "")] + + # Add the data to `target` + be_dest.item_set_elements(target, type_, name, elements) + + # Store + target.commit( + f"Clone from ixmp://{ts.platform.name}/{ts.model}/{ts.scenario}" + f"#{ts.version}" + ) + + return target @abstractmethod def has_solution(self, s: Scenario) -> bool: From 4c6d7f6ef941007dd5721d367bc742abbd0d60d9 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 22 Jul 2021 13:00:07 +0200 Subject: [PATCH 3/6] Tidy format string in JDBCBackend._get_item --- ixmp/backend/jdbc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ixmp/backend/jdbc.py b/ixmp/backend/jdbc.py index 352973b06..39626b1e4 100644 --- a/ixmp/backend/jdbc.py +++ b/ixmp/backend/jdbc.py @@ -1122,8 +1122,10 @@ def _get_item(self, s, ix_type, name, load=True): return getattr(self.jindex[s], f"get{ix_type.title()}")(*args) except java.IxException as e: # Regex for similar but not consistent messages from Java code - msg = f"No (item|{ix_type.title()}) '?{name}'? exists in this " "Scenario!" - if re.match(msg, e.args[0]): + if re.match( + f"No (item|{ix_type.title()}) '?{name}'? exists in this Scenario!", + e.args[0], + ): # Re-raise as a Python KeyError raise KeyError(name) from None else: # pragma: no cover From 19d01b4010222af64a94c018201798c77e7b37ee Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 22 Jul 2021 13:12:59 +0200 Subject: [PATCH 4/6] =?UTF-8?q?Backend.clone(=E2=80=A6,=20first=5Fmodel=5F?= =?UTF-8?q?year)=20is=20mandatory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ixmp/backend/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ixmp/backend/base.py b/ixmp/backend/base.py index 7f83dd9e4..24db89148 100644 --- a/ixmp/backend/base.py +++ b/ixmp/backend/base.py @@ -786,7 +786,7 @@ def clone( scenario: str, annotation: str, keep_solution: bool, - first_model_year: int = None, + first_model_year: Optional[int], ) -> Union[TimeSeries, Scenario]: """Clone `ts`. From 89738a31c98187bf665a8c44eb32b45652aea77e Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 22 Jul 2021 13:13:22 +0200 Subject: [PATCH 5/6] Simplify Scenario.clone() --- ixmp/core/scenario.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/ixmp/core/scenario.py b/ixmp/core/scenario.py index 0864e126b..d6a873f67 100644 --- a/ixmp/core/scenario.py +++ b/ixmp/core/scenario.py @@ -716,19 +716,24 @@ def clone( Platform to clone to (default: current platform) """ if shift_first_model_year is not None: + if not isinstance(shift_first_model_year, int): + raise TypeError( + "shift_first_model_year must be int; got " + + str(type(shift_first_model_year)) + ) if keep_solution: log.warning("Override keep_solution=True for shift_first_model_year") keep_solution = False - platform = platform or self.platform - model = model or self.model - scenario = scenario or self.scenario - - args = [platform, model, scenario, annotation, keep_solution] - if check_year(shift_first_model_year, "first_model_year"): - args.append(shift_first_model_year) - - return self._backend("clone", *args) + return self._backend( + "clone", + platform or self.platform, + model or self.model, + scenario or self.scenario, + annotation, + keep_solution, + shift_first_model_year, + ) def has_solution(self) -> bool: """Return :obj:`True` if the Scenario contains model solution data.""" From 62be92cfd31842246d8dcbbfb0c84bbc37b5fca8 Mon Sep 17 00:00:00 2001 From: Paul Natsuo Kishimoto Date: Thu, 22 Jul 2021 13:16:09 +0200 Subject: [PATCH 6/6] Adjust JDBCBackend.clone() --- ixmp/backend/jdbc.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ixmp/backend/jdbc.py b/ixmp/backend/jdbc.py index 39626b1e4..1964a72c0 100644 --- a/ixmp/backend/jdbc.py +++ b/ixmp/backend/jdbc.py @@ -817,12 +817,7 @@ def clone( first_model_year=None, ): # Raise exceptions for limitations of JDBCBackend - if not isinstance(platform_dest._backend, self.__class__): - raise NotImplementedError( # pragma: no cover - f"Clone between {self.__class__} and" - f"{platform_dest._backend.__class__}" - ) - elif platform_dest._backend is not self: + if platform_dest._backend is not self: package = s.__class__.__module__.split(".")[0] msg = f"Cross-platform clone of {package}.Scenario with" if keep_solution is False: @@ -830,6 +825,22 @@ def clone( elif "message_ix" in msg and first_model_year is not None: raise NotImplementedError(f"{msg} first_model_year != None") + # # This condition replicates existing behaviour + # if not isinstance(platform_dest._backend, self.__class__): + # This condition is what is desired; see the PR + if True: + return super().clone( + s, + platform_dest, + model, + scenario, + annotation, + keep_solution, + first_model_year, + ) + + # Remaining code can be deleted once the PR is complete + # Prepare arguments args = [platform_dest._backend.jobj, model, scenario, annotation, keep_solution] if first_model_year: