vessim 0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vessim-0/PKG-INFO ADDED
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.1
2
+ Name: vessim
3
+ Version: 0
4
+ Summary: A simulator for carbon-aware applications and systems.
5
+ Home-page: https://github.com/dos-group/vessim
6
+ License: MIT
7
+ Keywords: simulation,energy system,testbed,carbon-aware computing,software-in-the-loop
8
+ Author: Philipp Wiesner
9
+ Author-email: wiesner@tu-berlin.de
10
+ Requires-Python: >=3.8,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Topic :: Education
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Topic :: Software Development :: Testing
25
+ Provides-Extra: analysis
26
+ Provides-Extra: dev
27
+ Provides-Extra: docs
28
+ Provides-Extra: sil
29
+ Requires-Dist: black ; extra == "dev"
30
+ Requires-Dist: docker ; extra == "sil"
31
+ Requires-Dist: fastapi ; extra == "sil"
32
+ Requires-Dist: furo ; extra == "docs"
33
+ Requires-Dist: jupyterlab ; extra == "analysis"
34
+ Requires-Dist: loguru
35
+ Requires-Dist: matplotlib ; extra == "analysis"
36
+ Requires-Dist: mosaik
37
+ Requires-Dist: mosaik-api
38
+ Requires-Dist: mypy ; extra == "dev"
39
+ Requires-Dist: pandas
40
+ Requires-Dist: pandas-stubs ; extra == "dev"
41
+ Requires-Dist: pytest ; extra == "dev"
42
+ Requires-Dist: redis ; extra == "sil"
43
+ Requires-Dist: requests ; extra == "sil"
44
+ Requires-Dist: ruff ; extra == "dev"
45
+ Requires-Dist: seaborn (==0.12.2) ; extra == "analysis"
46
+ Requires-Dist: sphinx-copybutton ; extra == "docs"
47
+ Requires-Dist: types-psutil ; extra == "dev"
48
+ Requires-Dist: types-requests ; extra == "dev"
49
+ Requires-Dist: uvicorn ; extra == "sil"
50
+ Project-URL: Bug Reports, https://github.com/dos-group/vessim/issues
51
+ Project-URL: Documentation, https://github.com/dos-group/vessim
52
+ Project-URL: Repository, https://github.com/dos-group/vessim
53
+ Description-Content-Type: text/markdown
54
+
55
+ # Vessim
56
+
57
+ [![PyPI version](https://img.shields.io/pypi/v/vessim.svg?color=52c72b)](https://pypi.org/project/vessim/)
58
+ ![Tests](https://github.com/dos-group/vessim/actions/workflows/linting-and-testing.yml/badge.svg)
59
+ [![License](https://img.shields.io/pypi/l/vessim.svg)](https://pypi.org/project/vessim/)
60
+ [![Supported versions](https://img.shields.io/pypi/pyversions/vessim.svg)](https://pypi.org/project/vessim/)
61
+
62
+ Vessim is a versatile **co-simulation testbed for carbon-aware applications and systems**.
63
+ It lets users connect domain-specific simulators for energy system components like renewable power generation,
64
+ energy storage, and power flow analysis with real software and hardware.
65
+
66
+ Vessim is in alpha stage and under active development.
67
+ Functionality and documentation will improve in the next weeks and months.
68
+
69
+
70
+ ## Installation
71
+
72
+ If you are using Vessim for the first time, we recommend to clone and install this repository, so you have all
73
+ code and examples at hand:
74
+
75
+ ```
76
+ pip install -e .
77
+ ```
78
+
79
+ Alternatively, you can also install our [latest release](https://pypi.org/project/vessim/)
80
+ via [pip](https://pip.pypa.io/en/stable/quickstart/):
81
+
82
+ ```
83
+ pip install vessim
84
+ ```
85
+
86
+
87
+ ## Getting started
88
+
89
+ To execute our exemplary co-simulation scenario, run:
90
+
91
+ ```
92
+ python examples/cosim_example.py
93
+ ```
94
+
95
+ Software-in-the-Loop (SiL) simulation allows real computing systems to interact with Vessim at runtime.
96
+ We're currently working on better documentation on how to set up a full SiL scenario, but you can experiment with the existing
97
+ functionality by installing the "sil" extension (`pip install vessim[sil]`) and running:
98
+
99
+ ```
100
+ python examples/sil_example.py
101
+ ```
102
+
103
+
104
+ ## Work in progress
105
+
106
+ Our team at the [Distributed and Operating Systems](https://distributedsystems.berlin/) group at TU Berlin is actively working to improve Vessim.
107
+ We are currently working on the following aspects and features:
108
+
109
+ - **Better documentation**: You can find the current WiP documentation [here](https://vessim.readthedocs.io/en/latest/)
110
+ - **Improving the scenario API**: We currently heavily rely on [Mosaik](https://mosaik.offis.de/)'s scenario interface for defining experiment, but want to offer a more opinionated, high-level API to improve usability.
111
+ - **Software-in-the-loop (SiL) capabilities**: The current SiL implementation is focussed around our exemplary use case presented in our [journal paper](https://doi.org/10.1002/spe.3275). We want this to become more general purpose, so users can implement custom interfaces for the communication of computing and energy systems.
112
+ - **Prodiving access to relevant datasets**: We're currently collectig relevant datasets for carbon-aware test cases such as solar production or carbon intensity traces to simplify the setup of test cases.
113
+ - **Integrating the SAM**: NREL's [System Advisor Model (SAM)](https://sam.nrel.gov/) will soon be available as a subsystem in Vessim.
114
+
115
+
116
+ ## Publications
117
+
118
+ If you use Vessim in your research, please cite our vision paper:
119
+
120
+ - Philipp Wiesner, Ilja Behnke and Odej Kao. "[A Testbed for Carbon-Aware Applications and Systems](https://arxiv.org/pdf/2306.09774.pdf)" arXiv:2302.08681 [cs.DC]. 2023.
121
+ ```
122
+ @misc{wiesner2023vessim,
123
+ title={A Testbed for Carbon-Aware Applications and Systems},
124
+ author={Wiesner, Philipp and Behnke, Ilja and Kao, Odej},
125
+ year={2023},
126
+ eprint={2306.09774},
127
+ archivePrefix={arXiv},
128
+ primaryClass={cs.DC}
129
+ }
130
+ ```
131
+
132
+ Or our journal paper on software-in-the-loop similation for carbon-aware applications:
133
+ - Philipp Wiesner, Marvin Steinke, Henrik Nickel, Yazan Kitana, and Odej Kao. "[Software-in-the-Loop Simulation for Developing and Testing Carbon-Aware Applications](https://doi.org/10.1002/spe.3275)" Software: Practice and Experience, 53 (12). 2023.
134
+ ```
135
+ @article{wiesner2023sil,
136
+ author = {Wiesner, Philipp and Steinke, Marvin and Nickel, Henrik and Kitana, Yazan and Kao, Odej},
137
+ title = {Software-in-the-loop simulation for developing and testing carbon-aware applications},
138
+ journal = {Software: Practice and Experience},
139
+ year = {2023},
140
+ volume = {53},
141
+ number = {12},
142
+ pages = {2362-2376},
143
+ doi = {https://doi.org/10.1002/spe.3275}
144
+ }
145
+ ```
146
+
vessim-0/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Vessim
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/vessim.svg?color=52c72b)](https://pypi.org/project/vessim/)
4
+ ![Tests](https://github.com/dos-group/vessim/actions/workflows/linting-and-testing.yml/badge.svg)
5
+ [![License](https://img.shields.io/pypi/l/vessim.svg)](https://pypi.org/project/vessim/)
6
+ [![Supported versions](https://img.shields.io/pypi/pyversions/vessim.svg)](https://pypi.org/project/vessim/)
7
+
8
+ Vessim is a versatile **co-simulation testbed for carbon-aware applications and systems**.
9
+ It lets users connect domain-specific simulators for energy system components like renewable power generation,
10
+ energy storage, and power flow analysis with real software and hardware.
11
+
12
+ Vessim is in alpha stage and under active development.
13
+ Functionality and documentation will improve in the next weeks and months.
14
+
15
+
16
+ ## Installation
17
+
18
+ If you are using Vessim for the first time, we recommend to clone and install this repository, so you have all
19
+ code and examples at hand:
20
+
21
+ ```
22
+ pip install -e .
23
+ ```
24
+
25
+ Alternatively, you can also install our [latest release](https://pypi.org/project/vessim/)
26
+ via [pip](https://pip.pypa.io/en/stable/quickstart/):
27
+
28
+ ```
29
+ pip install vessim
30
+ ```
31
+
32
+
33
+ ## Getting started
34
+
35
+ To execute our exemplary co-simulation scenario, run:
36
+
37
+ ```
38
+ python examples/cosim_example.py
39
+ ```
40
+
41
+ Software-in-the-Loop (SiL) simulation allows real computing systems to interact with Vessim at runtime.
42
+ We're currently working on better documentation on how to set up a full SiL scenario, but you can experiment with the existing
43
+ functionality by installing the "sil" extension (`pip install vessim[sil]`) and running:
44
+
45
+ ```
46
+ python examples/sil_example.py
47
+ ```
48
+
49
+
50
+ ## Work in progress
51
+
52
+ Our team at the [Distributed and Operating Systems](https://distributedsystems.berlin/) group at TU Berlin is actively working to improve Vessim.
53
+ We are currently working on the following aspects and features:
54
+
55
+ - **Better documentation**: You can find the current WiP documentation [here](https://vessim.readthedocs.io/en/latest/)
56
+ - **Improving the scenario API**: We currently heavily rely on [Mosaik](https://mosaik.offis.de/)'s scenario interface for defining experiment, but want to offer a more opinionated, high-level API to improve usability.
57
+ - **Software-in-the-loop (SiL) capabilities**: The current SiL implementation is focussed around our exemplary use case presented in our [journal paper](https://doi.org/10.1002/spe.3275). We want this to become more general purpose, so users can implement custom interfaces for the communication of computing and energy systems.
58
+ - **Prodiving access to relevant datasets**: We're currently collectig relevant datasets for carbon-aware test cases such as solar production or carbon intensity traces to simplify the setup of test cases.
59
+ - **Integrating the SAM**: NREL's [System Advisor Model (SAM)](https://sam.nrel.gov/) will soon be available as a subsystem in Vessim.
60
+
61
+
62
+ ## Publications
63
+
64
+ If you use Vessim in your research, please cite our vision paper:
65
+
66
+ - Philipp Wiesner, Ilja Behnke and Odej Kao. "[A Testbed for Carbon-Aware Applications and Systems](https://arxiv.org/pdf/2306.09774.pdf)" arXiv:2302.08681 [cs.DC]. 2023.
67
+ ```
68
+ @misc{wiesner2023vessim,
69
+ title={A Testbed for Carbon-Aware Applications and Systems},
70
+ author={Wiesner, Philipp and Behnke, Ilja and Kao, Odej},
71
+ year={2023},
72
+ eprint={2306.09774},
73
+ archivePrefix={arXiv},
74
+ primaryClass={cs.DC}
75
+ }
76
+ ```
77
+
78
+ Or our journal paper on software-in-the-loop similation for carbon-aware applications:
79
+ - Philipp Wiesner, Marvin Steinke, Henrik Nickel, Yazan Kitana, and Odej Kao. "[Software-in-the-Loop Simulation for Developing and Testing Carbon-Aware Applications](https://doi.org/10.1002/spe.3275)" Software: Practice and Experience, 53 (12). 2023.
80
+ ```
81
+ @article{wiesner2023sil,
82
+ author = {Wiesner, Philipp and Steinke, Marvin and Nickel, Henrik and Kitana, Yazan and Kao, Odej},
83
+ title = {Software-in-the-loop simulation for developing and testing carbon-aware applications},
84
+ journal = {Software: Practice and Experience},
85
+ year = {2023},
86
+ volume = {53},
87
+ number = {12},
88
+ pages = {2362-2376},
89
+ doi = {https://doi.org/10.1002/spe.3275}
90
+ }
91
+ ```
@@ -0,0 +1,110 @@
1
+ [tool.poetry]
2
+ name = "vessim"
3
+ version = "0" # Poetry requires a version configuration to be present. The poetry-version-plugin extracts the correct version from the git-tag.
4
+ description = "A simulator for carbon-aware applications and systems."
5
+ keywords = ["simulation", "energy system", "testbed", "carbon-aware computing", "software-in-the-loop"]
6
+ authors = ["Philipp Wiesner <wiesner@tu-berlin.de>"]
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ homepage = "https://github.com/dos-group/vessim"
10
+ repository = "https://github.com/dos-group/vessim"
11
+ documentation = "https://github.com/dos-group/vessim"
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Science/Research",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Topic :: Scientific/Engineering",
18
+ "Topic :: Software Development :: Testing",
19
+ "Topic :: Education",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ ]
27
+
28
+ [tool.poetry-version-plugin]
29
+ source = "git-tag"
30
+
31
+ [tool.poetry.urls]
32
+ "Bug Reports" = "https://github.com/dos-group/vessim/issues"
33
+
34
+ [tool.poetry.dependencies]
35
+ python = "^3.8"
36
+ # Mandatory dependencies
37
+ pandas = "*"
38
+ mosaik = "*"
39
+ mosaik-api = "*"
40
+ loguru = "*"
41
+ # Optional dependencies (software-in-the-loop)
42
+ requests = {version = "*", optional = true}
43
+ fastapi = {version = "*", optional = true}
44
+ docker = {version = "*", optional = true}
45
+ redis = {version = "*", optional = true}
46
+ uvicorn = {version = "*", optional = true}
47
+ # Optional dependencies (analysis)
48
+ jupyterlab = {version = "*", optional = true}
49
+ matplotlib = {version = "*", optional = true}
50
+ seaborn = {version = "0.12.2", optional = true}
51
+ # Optional dependencies (development)
52
+ pytest = {version = "*", optional = true}
53
+ mypy = {version = "*", optional = true}
54
+ types-psutil = {version = "*", optional = true}
55
+ pandas-stubs = {version = "*", optional = true}
56
+ types-requests = {version = "*", optional = true}
57
+ black = {version = "*", optional = true}
58
+ ruff = {version = "*", optional = true}
59
+ # Optional dependencies (documentation)
60
+ furo = {version = "*", optional = true}
61
+ sphinx-copybutton = {version = "*", optional = true}
62
+
63
+ [tool.poetry.extras]
64
+ sil = ["requests", "fastapi", "docker", "redis", "uvicorn"]
65
+ analysis = ["jupyterlab", "matplotlib", "seaborn"]
66
+ dev = ["pytest", "mypy", "types-psutil", "pandas-stubs", "types-requests", "black", "ruff"]
67
+ docs = ["furo", "sphinx-copybutton"]
68
+
69
+ [build-system]
70
+ requires = ["poetry-core>=1.1.0"]
71
+ build-backend = "poetry.core.masonry.api"
72
+
73
+ [tool.black]
74
+ line-length = 90 # Set the maximum characters per line
75
+ target-version = ['py38'] # Set python versions that black should format the code for
76
+ include = '\.py$' # Format only python files
77
+
78
+ [tool.ruff]
79
+ # Enable Ruff to check for issues (https://beta.ruff.rs/docs/rules)
80
+ select = [
81
+ "E", "W", # pycodestyle (error and warning)
82
+ "D", # docstrings
83
+ "F", # Pyflakes
84
+ "N", # variable naming
85
+ # "B", # flake8-bugbear
86
+ ]
87
+
88
+ # For now the following rules are disabled:
89
+ # D100: Missing docstring in public module
90
+ # D101: Missing docstring in public class
91
+ # D102: Missing docstring in public method
92
+ # D103: Missing docstring in public function
93
+ # D107: Missing docstring in __init__
94
+ ignore = ["D100", "D101", "D102", "D103", "D107", "E722"]
95
+
96
+ # Allow autofix for all enabled rules (when `--fix`) is provided.
97
+ fixable = ["D", "E", "W"]
98
+ unfixable = ["F"]
99
+
100
+ line-length = 90 # Set the maximum characters per line (similar to black)
101
+ target-version = "py38" # Set python version for linting (similar to black)
102
+ include = ["*.py"] # Only enable linting for python files (similar to black)
103
+
104
+ # Configure Ruff to enforce Google-style docstrings when checking documentation
105
+ [tool.ruff.pydocstyle]
106
+ convention = "google"
107
+
108
+ # Configure mypy to ignore missing imports
109
+ [mypy]
110
+ ignore_missing_imports = true
@@ -0,0 +1,327 @@
1
+ """A simulator for carbon-aware applications and systems."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Union, List, Optional, Any, Literal
5
+
6
+ import pandas as pd
7
+
8
+ DatetimeLike = Union[str, datetime]
9
+
10
+
11
+ class TimeSeriesApi:
12
+ """Simulates an API for time series data like solar irradiance or carbon intensity.
13
+
14
+ Args:
15
+ actual: The actual time-series data to be used. It should contain a datetime-like
16
+ index marking the time, and each column should represent a different zone
17
+ containing the data. The name of the zone is equal to the column name.
18
+ Note that while interpolation is possible when retrieving forecasts, the
19
+ actual data between timestamps is computed using either `ffill` or `bfill`.
20
+ If you wish a different behavior, you have to change your actual data
21
+ beforehand (e.g. by resampling into a different frequency).
22
+
23
+ forecast: An optional time-series dataset representing forecasted values. The
24
+ data should contain two datetime-like indices. One is the
25
+ `Request Timestamp`, marking the time when the forecast was made. One is the
26
+ `Forecast Timestamp`, indicating the time the forecast is made for.
27
+
28
+ - If data does not include a `Request Timestamp`, it is treated as a static
29
+ forecast that does not change over time.
30
+
31
+ - If `forecast` is not provided, predictions are derived from the actual
32
+ data when requesting forecasts (actual data is treated as static forecast).
33
+
34
+ - When using a non-static forecast, you have to be aware that all the
35
+ zones/column names also have to appear in the actual dataframe because some
36
+ actual values are used for interpolation.
37
+
38
+ fill_method: Either `ffill` or `bfill`. Determines how actual data is acquired in
39
+ between timestamps. Some data providers like `Solcast` index their data with a
40
+ timestamp marking the end of the time-period that the data is valid for.
41
+ In those cases, `bfill` should be chosen, but the default is `ffill`.
42
+
43
+ Example:
44
+ >>> actual_data = [
45
+ ... ["2020-01-01T00:00:00", 100, 800],
46
+ ... ["2020-01-01T00:30:00", 110, 850],
47
+ ... ["2020-01-01T01:00:00", 105, 820],
48
+ ... ["2020-01-01T01:30:00", 108, 840],
49
+ ... ]
50
+ >>> actual = pd.DataFrame(
51
+ ... actual_data, columns=["timestamp", "zone_a", "zone_b"]
52
+ ... )
53
+ >>> actual.set_index(["timestamp"], inplace=True)
54
+
55
+ >>> forecast_data = [
56
+ ... ["2020-01-01T00:00:00", "2020-01-01T00:30:00", 115, 850],
57
+ ... ["2020-01-01T00:00:00", "2020-01-01T01:00:00", 110, 870],
58
+ ... ["2020-01-01T00:00:00", "2020-01-01T01:30:00", 110, 860],
59
+ ... ["2020-01-01T00:00:00", "2020-01-01T02:00:00", 120, 830],
60
+ ... ["2020-01-01T01:00:00", "2020-01-01T01:30:00", 115, 840],
61
+ ... ["2020-01-01T01:00:00", "2020-01-01T02:00:00", 110, 830],
62
+ ... ]
63
+ >>> forecast = pd.DataFrame(
64
+ ... forecast_data, columns=["req_time", "forecast_time", "zone_a", "zone_b"]
65
+ ... )
66
+ >>> forecast.set_index(["req_time", "forecast_time"], inplace=True)
67
+
68
+ >>> time_series = TimeSeriesApi(actual, forecast)
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ actual: Union[pd.Series, pd.DataFrame],
74
+ forecast: Optional[Union[pd.Series, pd.DataFrame]] = None,
75
+ fill_method: Literal["ffill", "bfill"] = "ffill",
76
+ ):
77
+ actual.index = pd.to_datetime(actual.index)
78
+ actual.sort_index(inplace=True)
79
+ if isinstance(actual, pd.Series):
80
+ self._actual = actual.to_frame()
81
+ elif isinstance(actual, pd.DataFrame):
82
+ self._actual = actual
83
+ else:
84
+ raise ValueError(f"Incompatible type {type(actual)} for 'actual'.")
85
+
86
+ if isinstance(forecast, (pd.Series, pd.DataFrame)):
87
+ # Convert all indices (either one or two columns) to datetime
88
+ if isinstance(forecast.index, pd.MultiIndex):
89
+ forecast_index: pd.MultiIndex = forecast.index
90
+ for level in range(forecast.index.nlevels):
91
+ forecast.index = forecast_index.set_levels(
92
+ pd.to_datetime(forecast_index.levels[level]), level=level
93
+ )
94
+ else:
95
+ forecast.index = pd.to_datetime(forecast.index)
96
+
97
+ forecast.sort_index(inplace=True)
98
+ self._forecast: Optional[pd.DataFrame]
99
+ if isinstance(forecast, pd.Series):
100
+ self._forecast = forecast.to_frame()
101
+ else:
102
+ self._forecast = forecast # type: ignore
103
+ self._fill_method = fill_method
104
+
105
+ def zones(self) -> List:
106
+ """Returns a list of all zones, where actual data is available."""
107
+ return list(self._actual.columns)
108
+
109
+ def actual(self, dt: DatetimeLike, zone: Optional[str] = None) -> Any:
110
+ """Retrieves actual data point of zone at given time.
111
+
112
+ If queried timestamp is not available in the `actual` dataframe, the last valid
113
+ datapoint is being returned.
114
+
115
+ Args:
116
+ dt: Timestamp, at which the data is needed.
117
+ zone: Optional zone for the data. Has to be provided if there is more than one
118
+ zone specified in the data. Defaults to None.
119
+
120
+ Raises:
121
+ ValueError: If there is no available data at specified zone or time.
122
+ """
123
+ data_at_time = self._get_actual_values_at_time(dt)
124
+ zone_index = self._get_zone_index_from_dataframe(data_at_time, zone)
125
+
126
+ data_point = data_at_time.iloc[0, zone_index]
127
+ if pd.isna(data_point):
128
+ raise ValueError(f"Cannot retrieve actual data at '{dt}' in zone '{zone}'.")
129
+ return data_point
130
+
131
+ def forecast(
132
+ self,
133
+ start_time: DatetimeLike,
134
+ end_time: DatetimeLike,
135
+ zone: Optional[str] = None,
136
+ frequency: Optional[Union[str, pd.DateOffset, timedelta]] = None,
137
+ resample_method: Optional[str] = None,
138
+ ) -> pd.Series:
139
+ """Retrieves of forecasted data points within window at a frequency.
140
+
141
+ - If no forecast time-series data is provided, actual data is used.
142
+ - Specified timestamps are always rounded down to the nearest existing.
143
+ - If frequency is not specified, all existing data in the window will be returned.
144
+ - For various resampling methods, the actual value valid at `start_time` is used.
145
+ So you have to make sure that there is a valid actual value.
146
+ - If there is more than one zone present in the data, zone has to be specified.
147
+ (Note that zone name must also appear in actual data for non-static forecast.)
148
+ - The forecast does not include the value at `start_time` (see example).
149
+
150
+ Args:
151
+ start_time: Starting time of the window.
152
+ end_time: End time of the window.
153
+ zone: Optional geographical zone of the forecast. Defaults to None.
154
+ frequency: Optional interval, in which the forecast data is to be provided.
155
+ Defaults to None.
156
+ resample_method: Optional method, to deal with holes in resampled data.
157
+ Can be either `bfill`, `ffill` or an interpolation method.
158
+ For more information on interpolation methods, see the
159
+ `pandas documentation <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html>`_.
160
+
161
+ Returns:
162
+ pd.Series of forecasted data with timestamps of forecast as index.
163
+
164
+ Raises:
165
+ ValueError: If no data is available for the specified zone or time, or if
166
+ insufficient data exists for the frequency, without `resample_method`
167
+ specified.
168
+
169
+ Example:
170
+ >>> index = pd.date_range(
171
+ ... "2020-01-01T00:00:00", "2020-01-01T03:00:00", freq="1H"
172
+ ... )
173
+ >>> actual = pd.DataFrame({"zone_a": [4, 6, 2, 8]}, index=index)
174
+
175
+ >>> forecast_data = [
176
+ ... ["2020-01-01T00:00:00", "2020-01-01T01:00:00", 5],
177
+ ... ["2020-01-01T00:00:00", "2020-01-01T02:00:00", 2],
178
+ ... ["2020-01-01T00:00:00", "2020-01-01T03:00:00", 6],
179
+ ... ]
180
+ >>> forecast = pd.DataFrame(
181
+ ... forecast_data, columns=["req_time", "forecast_time", "zone_a"]
182
+ ... )
183
+ >>> forecast.set_index(["req_time", "forecast_time"], inplace=True)
184
+
185
+ >>> time_series = TimeSeriesApi(actual, forecast)
186
+
187
+ Forward-fill resampling between 2020-01-01T00:00:00 (actual value = 4.0) and
188
+ forecasted values between 2020-01-01T01:00:00 and 2020-01-01T02:00:00:
189
+
190
+ >>> time_series.forecast(
191
+ ... start_time="2020-01-01T00:00:00",
192
+ ... end_time="2020-01-01T02:00:00",
193
+ ... frequency="30T",
194
+ ... resample_method="ffill",
195
+ ... )
196
+ 2020-01-01 00:30:00 4.0
197
+ 2020-01-01 01:00:00 5.0
198
+ 2020-01-01 01:30:00 5.0
199
+ 2020-01-01 02:00:00 2.0
200
+ Freq: 30T, Name: zone_a, dtype: float64
201
+
202
+ Time interpolation between 2020-01-01T01:10:00 (actual value = 6.0) and
203
+ 2020-01-01T02:00:00 (forecasted value = 2.0):
204
+
205
+ >>> time_series.forecast(
206
+ ... start_time="2020-01-01T01:10:00",
207
+ ... end_time="2020-01-01T01:55:00",
208
+ ... zone="zone_a",
209
+ ... frequency=timedelta(minutes=20),
210
+ ... resample_method="time",
211
+ ... )
212
+ 2020-01-01 01:30:00 4.4
213
+ 2020-01-01 01:50:00 2.8
214
+ Freq: 20T, Name: zone_a, dtype: float64
215
+ """
216
+ data_src = self._get_forecast_data_source(start_time)
217
+
218
+ zone_index = self._get_zone_index_from_dataframe(data_src, zone)
219
+
220
+ # Get data of zone and convert to pd.Series
221
+ forecast: pd.Series = data_src.iloc[:, zone_index].squeeze()
222
+
223
+ # Resample the data to get the data to specified frequency
224
+ if frequency is not None:
225
+ forecast = self._resample_to_frequency(
226
+ forecast, start_time, end_time, frequency, resample_method=resample_method
227
+ )
228
+ return forecast.iloc[1:]
229
+
230
+ return forecast.loc[(forecast.index > start_time) & (forecast.index <= end_time)]
231
+
232
+ def _get_forecast_data_source(self, start_time: DatetimeLike):
233
+ """Returns dataframe used to derive forecast prediction."""
234
+ data_src: pd.DataFrame
235
+ if self._forecast is None:
236
+ # Get all data points beginning at the nearest existing timestamp
237
+ # lower than start time from actual data
238
+ data_src = self._actual.loc[self._actual.index.asof(start_time) :]
239
+ elif self._forecast.index.nlevels == 1:
240
+ # Forecast data does not include request_timestamp
241
+ data_src = self._forecast.loc[self._forecast.index.asof(start_time) :]
242
+ else:
243
+ # Get forecasts of the nearest existing timestamp lower than start time
244
+ first_index = self._forecast.index.get_level_values(0)
245
+ request_time = first_index[first_index <= start_time][-1]
246
+ data_src = self._forecast.loc[request_time] # type: ignore
247
+
248
+ # Retrieve the actual value at start_time and attach it to the front of the
249
+ # forecast data with the timestamp (for interpolation at a later stage)
250
+ actual_values = self._get_actual_values_at_time(start_time)
251
+ data_src = pd.concat(
252
+ [actual_values, data_src.loc[(data_src.index > start_time)]]
253
+ )
254
+
255
+ if data_src.empty:
256
+ raise ValueError(f"Cannot retrieve forecast data at '{start_time}'.")
257
+
258
+ return data_src
259
+
260
+ def _get_actual_values_at_time(self, dt: DatetimeLike) -> pd.DataFrame:
261
+ """Return actual data that is valid at a specific time."""
262
+ return self._actual.reindex(index=[pd.to_datetime(dt)], method=self._fill_method)
263
+
264
+ def _get_zone_index_from_dataframe(self, df: pd.DataFrame, zone: Optional[str]):
265
+ """Return column index of zone from given dataframe.
266
+
267
+ If zone is not specified, but there is only one column in the dataframe, the
268
+ first index is returned.
269
+
270
+ Raises:
271
+ ValueError: If zone index can not be determined.
272
+ """
273
+ if zone is None:
274
+ if len(df.columns) == 1:
275
+ zone_index = 0
276
+ else:
277
+ raise ValueError("Zone needs to be specified.")
278
+ else:
279
+ try:
280
+ zone_index = df.columns.get_loc(zone)
281
+ except KeyError:
282
+ raise ValueError(f"Cannot retrieve data for zone '{zone}'.")
283
+
284
+ return zone_index
285
+
286
+ def _resample_to_frequency(
287
+ self,
288
+ df: pd.Series,
289
+ start_time: DatetimeLike,
290
+ end_time: DatetimeLike,
291
+ frequency: Optional[Union[str, pd.DateOffset, timedelta]] = None,
292
+ resample_method: Optional[str] = None,
293
+ ):
294
+ """Transform series into the desired frequency between start and end time."""
295
+ frequency = pd.tseries.frequencies.to_offset(frequency)
296
+ if frequency is None:
297
+ raise ValueError(f"Frequency '{frequency}' invalid.")
298
+
299
+ # Add NaN values in the specified frequency
300
+ new_index = pd.date_range(start=start_time, end=end_time, freq=frequency)
301
+ combined_index = df.index.union(new_index).sort_values()
302
+ df = df.reindex(combined_index)
303
+
304
+ # Use specific resample method if specified to fill NaN values
305
+ if resample_method == "ffill":
306
+ df.ffill(inplace=True)
307
+ elif resample_method == "bfill":
308
+ df.bfill(inplace=True)
309
+ elif resample_method is not None:
310
+ df.interpolate(method=resample_method, inplace=True) # type: ignore
311
+ elif df.isnull().values.any(): # type: ignore
312
+ # Check if there are NaN values if resample method is not specified
313
+ raise ValueError(
314
+ f"Not enough data at frequency '{frequency}'. Specify resample_method"
315
+ )
316
+
317
+ # Get the data to the desired frequency after interpolation
318
+ return df.loc[(df.index >= start_time) & (df.index <= end_time)].asfreq(frequency)
319
+
320
+ def next_update(self, dt: DatetimeLike) -> datetime:
321
+ """Returns the next time of when the actual trace will change.
322
+
323
+ This method is being called in the time-based simulation model for Mosaik.
324
+ """
325
+ current_index = self._actual.index.asof(dt)
326
+ next_iloc = self._actual.index.get_loc(current_index) + 1
327
+ return self._actual.index[next_iloc]