Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement clone() in Python #424

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 80 additions & 7 deletions ixmp/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from os import PathLike
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Dict,
Hashable,
Expand All @@ -14,6 +15,7 @@
Sequence,
Tuple,
Union,
cast,
)

import pandas as pd
Expand Down Expand Up @@ -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`.
first_model_year: Optional[int],
) -> Union[TimeSeries, Scenario]:
"""Clone `ts`.

Parameters
----------
Expand All @@ -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`.

Expand All @@ -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:
Expand Down
29 changes: 21 additions & 8 deletions ixmp/backend/jdbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,19 +817,30 @@ 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:
raise NotImplementedError(f"{msg} `keep_solution=False`")
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:
Expand Down Expand Up @@ -1122,8 +1133,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
Expand Down
5 changes: 5 additions & 0 deletions ixmp/core/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down
23 changes: 14 additions & 9 deletions ixmp/core/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down