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 +146 -0
- vessim-0/README.md +91 -0
- vessim-0/pyproject.toml +110 -0
- vessim-0/vessim/__init__.py +327 -0
- vessim-0/vessim/analysis.py +111 -0
- vessim-0/vessim/core/__init__.py +1 -0
- vessim-0/vessim/core/consumer.py +99 -0
- vessim-0/vessim/core/microgrid.py +39 -0
- vessim-0/vessim/core/storage.py +122 -0
- vessim-0/vessim/cosim/__init__.py +15 -0
- vessim-0/vessim/cosim/_util.py +162 -0
- vessim-0/vessim/cosim/carbon_api.py +52 -0
- vessim-0/vessim/cosim/consumer.py +53 -0
- vessim-0/vessim/cosim/generator.py +48 -0
- vessim-0/vessim/cosim/microgrid.py +38 -0
- vessim-0/vessim/cosim/monitor.py +71 -0
- vessim-0/vessim/sil/__init__.py +4 -0
- vessim-0/vessim/sil/api_server.py +229 -0
- vessim-0/vessim/sil/http_client.py +54 -0
- vessim-0/vessim/sil/loop_thread.py +46 -0
- vessim-0/vessim/sil/node.py +31 -0
- vessim-0/vessim/sil/power_meter.py +44 -0
- vessim-0/vessim/sil/redis_docker.py +63 -0
- vessim-0/vessim/sil/sil_interface.py +174 -0
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
|
+
[](https://pypi.org/project/vessim/)
|
|
58
|
+

|
|
59
|
+
[](https://pypi.org/project/vessim/)
|
|
60
|
+
[](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
|
+
[](https://pypi.org/project/vessim/)
|
|
4
|
+

|
|
5
|
+
[](https://pypi.org/project/vessim/)
|
|
6
|
+
[](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
|
+
```
|
vessim-0/pyproject.toml
ADDED
|
@@ -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]
|