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

Add the transport tutorial #146

Draft
wants to merge 36 commits into
base: enh/run-clone
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3ea7fb0
Reify some core docstrings
glatterf42 Nov 28, 2024
8536bb0
Include forgotten test case
glatterf42 Nov 28, 2024
9555053
Make util function more concise
glatterf42 Nov 28, 2024
b8c9b0d
Remove superfluous lines
glatterf42 Nov 28, 2024
7c463ee
Test adding data to scalar variable raises
glatterf42 Nov 28, 2024
ea60cc1
Validate var/equ data only for non-empty data
glatterf42 Nov 28, 2024
b0201c6
Introduce run.optimization.remove_solution()
glatterf42 Nov 28, 2024
8513cac
Introduce run.get_by_id()
glatterf42 Nov 28, 2024
321e81a
Remove superfluous casts
glatterf42 Nov 28, 2024
3cb5f39
Remove superfluous lines
glatterf42 Nov 29, 2024
54eb700
Make return value of indexset.data consistent
glatterf42 Nov 29, 2024
335388b
Enable all abstract EnumerateKwargs for DB-timeseries
glatterf42 Nov 29, 2024
39e1d19
Make iamc.data helper functions available in DB layer
glatterf42 Nov 29, 2024
9e2dab4
Introduce run.clone()
glatterf42 Nov 29, 2024
d517108
Update openapi schema
glatterf42 Nov 29, 2024
2b85a87
Remove outdated comment
glatterf42 Nov 29, 2024
0975ce3
Name helper file appropriately
glatterf42 Nov 29, 2024
02694e6
Add test that run.clone uses auth system
glatterf42 Jan 8, 2025
d69ff7c
Enhance readability of run.clone test
glatterf42 Jan 8, 2025
12f2e5f
Copy-paste tutorial from previous branch
danielhuppmann Jan 9, 2025
d141d90
Add an explicit notebook 0
danielhuppmann Jan 9, 2025
33a8cf6
Minor fixes
danielhuppmann Jan 9, 2025
8e1c1b4
Add reworked versions of the tutorial notebooks
danielhuppmann Jan 9, 2025
fece793
Clean up the introduction
danielhuppmann Jan 9, 2025
c4a7937
Add necessary dependencies to the tutorial group
glatterf42 Jan 10, 2025
f208407
Clean up based on current API/mypy config
glatterf42 Jan 10, 2025
f8aee2e
Bump mypy version
glatterf42 Jan 10, 2025
38ec93a
Install tutorial dependencies for pre-commit
glatterf42 Jan 10, 2025
34dbe65
Harmonize the notebook intro
danielhuppmann Jan 10, 2025
e2dd1e8
Remove the Python logo
danielhuppmann Jan 13, 2025
48b19cc
Streamline setting up the problem structure
danielhuppmann Jan 13, 2025
1e490ef
Update solving the model and reading the solution and set as default
danielhuppmann Jan 13, 2025
e9c95ad
Rework notebook 2
danielhuppmann Jan 13, 2025
bdc11a6
Fix units and harmonize documentation
danielhuppmann Jan 14, 2025
ccf12ac
🐛 fix(tutorials): Ensure parameters & indexsets are aligned
glatterf42 Jan 14, 2025
4cc277d
✨ feat(tutorials): Clean up & make consistent
glatterf42 Jan 14, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ jobs:
#------------------------------------------------
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root --with dev,server
run: poetry install --no-interaction --no-root --with dev,server,tutorial

#------------------------
# install root project
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
rev: v1.14.1
hooks:
- id: mypy
entry: bash -c "poetry run mypy ."
Expand Down
2 changes: 1 addition & 1 deletion doc/source/openapi-v1.json

Large diffs are not rendered by default.

82 changes: 8 additions & 74 deletions ixmp4/core/iamc/data.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from collections.abc import Iterable
from typing import Optional, TypeVar

import pandas as pd
import pandera as pa
from pandera.engines import pandas_engine
from pandera.typing import Series

# TODO Import this from typing when dropping Python 3.11
from typing_extensions import Unpack
Expand All @@ -13,67 +9,17 @@
from ixmp4.data.abstract import Run
from ixmp4.data.abstract.iamc.datapoint import EnumerateKwargs
from ixmp4.data.backend import Backend
from ixmp4.data.db.iamc.utils import (
AddDataPointFrameSchema,
RemoveDataPointFrameSchema,
normalize_df,
)

from ..base import BaseFacade
from ..utils import substitute_type
from .variable import VariableRepository


class RemoveDataPointFrameSchema(pa.DataFrameModel):
type: Optional[Series[pa.String]] = pa.Field(isin=[t for t in DataPointModel.Type])
step_year: Optional[Series[pa.Int]] = pa.Field(coerce=True, nullable=True)
step_datetime: Optional[Series[pandas_engine.DateTime]] = pa.Field(
coerce=True, nullable=True
)
step_category: Optional[Series[pa.String]] = pa.Field(nullable=True)

region: Optional[Series[pa.String]] = pa.Field(coerce=True)
unit: Optional[Series[pa.String]] = pa.Field(coerce=True)
variable: Optional[Series[pa.String]] = pa.Field(coerce=True)


class AddDataPointFrameSchema(RemoveDataPointFrameSchema):
value: Series[pa.Float] = pa.Field(coerce=True)


MAP_STEP_COLUMN = {
"ANNUAL": "step_year",
"CATEGORICAL": "step_year",
"DATETIME": "step_datetime",
}


def convert_to_std_format(df: pd.DataFrame, join_runs: bool) -> pd.DataFrame:
df.rename(columns={"step_category": "subannual"}, inplace=True)

if set(df.type.unique()).issubset(["ANNUAL", "CATEGORICAL"]):
df.rename(columns={"step_year": "year"}, inplace=True)
time_col = "year"
else:
T = TypeVar("T", bool, float, int, str)

def map_step_column(df: "pd.Series[T]") -> "pd.Series[T]":
df["time"] = df[MAP_STEP_COLUMN[str(df.type)]]
return df

df = df.apply(map_step_column, axis=1)
time_col = "time"

columns = ["model", "scenario", "version"] if join_runs else []
columns += ["region", "variable", "unit"] + [time_col]
if "subannual" in df.columns:
columns += ["subannual"]
return df[columns + ["value"]]


def normalize_df(df: pd.DataFrame, raw: bool, join_runs: bool) -> pd.DataFrame:
if not df.empty:
df = df.drop(columns=["time_series__id"])
if raw is False:
return convert_to_std_format(df, join_runs)
return df


class RunIamcData(BaseFacade):
"""IAMC data.

Expand Down Expand Up @@ -106,29 +52,17 @@ def _get_or_create_ts(self, df: pd.DataFrame) -> pd.DataFrame:

# merge on the identity columns
return pd.merge(
df,
ts_df,
how="left",
on=id_cols,
suffixes=(None, "_y"),
df, ts_df, how="left", on=id_cols, suffixes=(None, "_y")
) # tada, df with 'time_series__id' added from the database.

def add(
self,
df: pd.DataFrame,
type: Optional[DataPointModel.Type] = None,
) -> None:
def add(self, df: pd.DataFrame, type: DataPointModel.Type | None = None) -> None:
df = AddDataPointFrameSchema.validate(df) # type: ignore[assignment]
df["run__id"] = self.run.id
df = self._get_or_create_ts(df)
substitute_type(df, type)
self.backend.iamc.datapoints.bulk_upsert(df)

def remove(
self,
df: pd.DataFrame,
type: Optional[DataPointModel.Type] = None,
) -> None:
def remove(self, df: pd.DataFrame, type: DataPointModel.Type | None = None) -> None:
df = RemoveDataPointFrameSchema.validate(df) # type: ignore[assignment]
df["run__id"] = self.run.id
df = self._get_or_create_ts(df)
Expand Down
6 changes: 6 additions & 0 deletions ixmp4/core/optimization/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ def __init__(self, run: Run, **kwargs: Backend) -> None:
self.scalars = ScalarRepository(_backend=self.backend, _run=run)
self.tables = TableRepository(_backend=self.backend, _run=run)
self.variables = VariableRepository(_backend=self.backend, _run=run)

def remove_solution(self) -> None:
for equation in self.equations.list():
equation.remove_data()
for variable in self.variables.list():
variable.remove_data()
4 changes: 2 additions & 2 deletions ixmp4/core/optimization/equation.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def data(self) -> dict[str, Any]:
return self._model.data

def add(self, data: dict[str, Any] | pd.DataFrame) -> None:
"""Adds data to an existing Equation."""
"""Adds data to the Equation."""
self.backend.optimization.equations.add_data(
equation_id=self._model.id, data=data
)
Expand All @@ -49,7 +49,7 @@ def add(self, data: dict[str, Any] | pd.DataFrame) -> None:
).data

def remove_data(self) -> None:
"""Removes data from an existing Equation."""
"""Removes all data from the Equation."""
self.backend.optimization.equations.remove_data(equation_id=self._model.id)
self._model.data = self.backend.optimization.equations.get(
run_id=self._model.run__id, name=self._model.name
Expand Down
2 changes: 1 addition & 1 deletion ixmp4/core/optimization/indexset.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def name(self) -> str:
return self._model.name

@property
def data(self) -> list[float | int | str]:
def data(self) -> list[float] | list[int] | list[str]:
return self._model.data

def add(
Expand Down
2 changes: 1 addition & 1 deletion ixmp4/core/optimization/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def data(self) -> dict[str, Any]:
return self._model.data

def add(self, data: dict[str, Any] | pd.DataFrame) -> None:
"""Adds data to an existing Parameter."""
"""Adds data to the Parameter."""
self.backend.optimization.parameters.add_data(
parameter_id=self._model.id, data=data
)
Expand Down
2 changes: 1 addition & 1 deletion ixmp4/core/optimization/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def data(self) -> dict[str, Any]:
return self._model.data

def add(self, data: dict[str, Any] | pd.DataFrame) -> None:
"""Adds data to an existing Table."""
"""Adds data to the Table."""
self.backend.optimization.tables.add_data(table_id=self._model.id, data=data)
self._model.data = self.backend.optimization.tables.get(
run_id=self._model.run__id, name=self._model.name
Expand Down
4 changes: 2 additions & 2 deletions ixmp4/core/optimization/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def data(self) -> dict[str, Any]:
return self._model.data

def add(self, data: dict[str, Any] | pd.DataFrame) -> None:
"""Adds data to an existing Variable."""
"""Adds data to the Variable."""
self.backend.optimization.variables.add_data(
variable_id=self._model.id, data=data
)
Expand All @@ -49,7 +49,7 @@ def add(self, data: dict[str, Any] | pd.DataFrame) -> None:
).data

def remove_data(self) -> None:
"""Removes data from an existing Variable."""
"""Removes all data from the Variable."""
self.backend.optimization.variables.remove_data(variable_id=self._model.id)
self._model.data = self.backend.optimization.variables.get(
run_id=self._model.run__id, name=self._model.name
Expand Down
38 changes: 23 additions & 15 deletions ixmp4/core/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,27 +73,35 @@ def unset_as_default(self) -> None:
"""Unsets this run as the default version."""
self.backend.runs.unset_as_default_version(self._model.id)

def clone(
self,
model: str | None = None,
scenario: str | None = None,
keep_solution: bool = True,
) -> "Run":
return Run(
_backend=self.backend,
_model=self.backend.runs.clone(
run_id=self.id,
model_name=model,
scenario_name=scenario,
keep_solution=keep_solution,
),
)


class RunRepository(BaseFacade):
def create(
self,
model: str,
scenario: str,
) -> Run:
def create(self, model: str, scenario: str) -> Run:
return Run(
_backend=self.backend, _model=self.backend.runs.create(model, scenario)
)

def get(
self,
model: str,
scenario: str,
version: int | None = None,
) -> Run:
if version is None:
_model = self.backend.runs.get_default_version(model, scenario)
else:
_model = self.backend.runs.get(model, scenario, version)
def get(self, model: str, scenario: str, version: int | None = None) -> Run:
_model = (
self.backend.runs.get_default_version(model, scenario)
if version is None
else self.backend.runs.get(model, scenario, version)
)
return Run(_backend=self.backend, _model=_model)

def list(self, **kwargs: Unpack[EnumerateKwargs]) -> list[Run]:
Expand Down
48 changes: 48 additions & 0 deletions ixmp4/data/abstract/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,26 @@ def get_default_version(self, model_name: str, scenario_name: str) -> Run:
"""
...

def get_by_id(self, id: int) -> Run:
"""Retrieves a Run by its id.

Parameters
----------
id : int
Unique integer id.

Raises
------
:class:`ixmp4.data.abstract.Run.NotFound`.
If the Run with `id` does not exist.

Returns
-------
:class:`ixmp4.data.abstract.Run`:
The retrieved Run.
"""
...

def list(self, **kwargs: Unpack[EnumerateKwargs]) -> list[Run]:
r"""Lists runs by specified criteria.

Expand Down Expand Up @@ -206,3 +226,31 @@ def unset_as_default_version(self, id: int) -> None:

"""
...

def clone(
self,
run_id: int,
model_name: str | None = None,
scenario_name: str | None = None,
keep_solution: bool = True,
) -> Run:
"""Clone all data from one run to a new one.

Parameters
----------
run_id: int
The unique integer id of the base run.
model_name: str | None
The new name of the model used in the new run, optional.
scenario_name: str | None
The new name of the scenario used in the new run, optional.
keep_solution: bool
Whether to keep the solution data from the base run. Optional, defaults to
`True`.

Returns
-------
:class:`ixmp4.data.abstract.Run`:
The clone of the base run.
"""
...
24 changes: 5 additions & 19 deletions ixmp4/data/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,9 +359,7 @@ def _handle_pagination(
return [data.pop("results")] + results

def _list(
self,
params: ParamType | None = None,
json: JsonType | None = None,
self, params: ParamType | None = None, json: JsonType | None = None
) -> list[ModelType]:
data = self._request_enumeration(params=params, table=False, json=json)
if isinstance(data, dict):
Expand All @@ -375,9 +373,7 @@ def _list(
return [self.model_class(**i) for i in results]

def _tabulate(
self,
params: ParamType | None = {},
json: JsonType | None = None,
self, params: ParamType | None = {}, json: JsonType | None = None
) -> pd.DataFrame:
# we can assume this type on table endpoints
data: dict[str, Any] = self._request_enumeration(
Expand All @@ -398,9 +394,7 @@ def _tabulate(
return DataFrame(**data).to_pandas()

def _create(
self,
*args: Unpack[tuple[str]],
**kwargs: Unpack[_RequestKwargs],
self, *args: Unpack[tuple[str]], **kwargs: Unpack[_RequestKwargs]
) -> dict[str, Any]:
# we can assume this type on create endpoints
return self._request("POST", *args, **kwargs) # type: ignore[return-value]
Expand Down Expand Up @@ -455,10 +449,7 @@ def create(
| float
| None,
) -> ModelType:
res = self._create(
self.prefix,
json=kwargs,
)
res = self._create(self.prefix, json=kwargs)
return self.model_class(**res)


Expand Down Expand Up @@ -539,9 +530,4 @@ def bulk_delete(
def bulk_delete_chunk(self, df: pd.DataFrame, **kwargs: Any) -> None:
dict_ = df_to_dict(df)
json_ = DataFrame(**dict_).model_dump_json()
self._request(
"PATCH",
self.prefix + "bulk/",
params=kwargs,
content=json_,
)
self._request("PATCH", self.prefix + "bulk/", params=kwargs, content=json_)
Loading
Loading