vessim 0.9__0-py3-none-any.whl
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/__init__.py +34 -0
- vessim/_data.py +136 -0
- vessim/_util.py +50 -0
- vessim/actor.py +135 -0
- vessim/controller.py +195 -0
- vessim/cosim.py +250 -0
- vessim/policy.py +101 -0
- vessim/signal.py +456 -0
- vessim/sil.py +260 -0
- vessim/storage.py +287 -0
- vessim-0.9.0.dist-info/LICENSE +9 -0
- vessim-0.9.0.dist-info/METADATA +163 -0
- vessim-0.9.0.dist-info/RECORD +14 -0
- vessim-0.9.0.dist-info/WHEEL +4 -0
vessim/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""A simulator for carbon-aware applications and systems."""
|
|
2
|
+
|
|
3
|
+
from vessim.actor import ActorBase, Actor, ComputingSystem
|
|
4
|
+
from vessim.controller import Controller, Monitor
|
|
5
|
+
from vessim.cosim import Microgrid, Environment
|
|
6
|
+
from vessim.policy import MicrogridPolicy, DefaultMicrogridPolicy
|
|
7
|
+
from vessim.signal import Signal, HistoricalSignal, MockSignal, CollectorSignal
|
|
8
|
+
from vessim.storage import Storage, SimpleBattery, ClcBattery
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ActorBase",
|
|
12
|
+
"Actor",
|
|
13
|
+
"ComputingSystem",
|
|
14
|
+
"Controller",
|
|
15
|
+
"Monitor",
|
|
16
|
+
"Microgrid",
|
|
17
|
+
"Environment",
|
|
18
|
+
"MicrogridPolicy",
|
|
19
|
+
"DefaultMicrogridPolicy",
|
|
20
|
+
"CollectorSignal",
|
|
21
|
+
"MockSignal",
|
|
22
|
+
"Signal",
|
|
23
|
+
"HistoricalSignal",
|
|
24
|
+
"Storage",
|
|
25
|
+
"ClcBattery",
|
|
26
|
+
"SimpleBattery",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from vessim.sil import Broker, SilController, WatttimeSignal, get_latest_event # noqa: F401
|
|
31
|
+
|
|
32
|
+
__all__.extend(["Broker", "SilController", "WatttimeSignal", "get_latest_event"])
|
|
33
|
+
except ImportError:
|
|
34
|
+
pass
|
vessim/_data.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import urllib.request
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from zipfile import ZipFile
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
VESSIM_DATASETS: dict[str, dict[str, str]] = {
|
|
13
|
+
"solcast2022_germany": {
|
|
14
|
+
"actual": "solcast2022_germany_actual.csv",
|
|
15
|
+
"forecast": "solcast2022_germany_forecast.csv",
|
|
16
|
+
"fill_method": "bfill",
|
|
17
|
+
"url": "https://raw.githubusercontent.com/dos-group/vessim/main/datasets/solcast2022_germany.zip",
|
|
18
|
+
},
|
|
19
|
+
"solcast2022_global": {
|
|
20
|
+
"actual": "solcast2022_global_actual.csv",
|
|
21
|
+
"forecast": "solcast2022_global_forecast.csv",
|
|
22
|
+
"fill_method": "bfill",
|
|
23
|
+
"url": "https://raw.githubusercontent.com/dos-group/vessim/main/datasets/solcast2022_global.zip",
|
|
24
|
+
},
|
|
25
|
+
"watttime2023_caiso-north": {
|
|
26
|
+
"actual": "watttime2023_caiso-north_actual.csv",
|
|
27
|
+
"forecast": "watttime2023_caiso-north_forecast.csv",
|
|
28
|
+
"fill_method": "ffill",
|
|
29
|
+
"url": "https://raw.githubusercontent.com/dos-group/vessim/main/datasets/watttime2023_caiso-north.zip",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_dataset(dataset: str, dir_path: Path, params: Optional[dict] = None) -> dict:
|
|
35
|
+
"""Downloads a dataset from the vessim repository, unpacks it and loads data."""
|
|
36
|
+
if dataset not in VESSIM_DATASETS:
|
|
37
|
+
raise ValueError(f"Dataset '{dataset}' not found. Available datasets are: "
|
|
38
|
+
f"{', '.join(list(VESSIM_DATASETS.keys()))}")
|
|
39
|
+
|
|
40
|
+
if params is not None:
|
|
41
|
+
allowed_parameters = ["scale", "start_time", "use_forecast"]
|
|
42
|
+
for key in params.keys():
|
|
43
|
+
if key not in allowed_parameters:
|
|
44
|
+
raise ValueError(f"Parameter '{key}' not allowed. "
|
|
45
|
+
f"Allowed parameters are: {allowed_parameters}.")
|
|
46
|
+
|
|
47
|
+
scale = _get_parameter(params, "scale", default=1.0)
|
|
48
|
+
start_time = _get_parameter(params, "start_time", default=None)
|
|
49
|
+
use_forecast = _get_parameter(params, "use_forecast", default=True)
|
|
50
|
+
|
|
51
|
+
dataset_config = VESSIM_DATASETS[dataset]
|
|
52
|
+
required_files = [dataset_config["actual"]]
|
|
53
|
+
if use_forecast:
|
|
54
|
+
required_files.append(dataset_config["forecast"])
|
|
55
|
+
|
|
56
|
+
if not _check_files(required_files, dir_path):
|
|
57
|
+
print("Required data files not present locally. Try downloading...")
|
|
58
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
59
|
+
zip_path = dir_path / "dataset.zip"
|
|
60
|
+
try:
|
|
61
|
+
urllib.request.urlretrieve(dataset_config["url"], zip_path)
|
|
62
|
+
except Exception:
|
|
63
|
+
raise RuntimeError(f"Dataset could not be retrieved from url: "
|
|
64
|
+
f"{dataset_config['url']}")
|
|
65
|
+
|
|
66
|
+
with ZipFile(zip_path, "r") as zip_ref:
|
|
67
|
+
zip_ref.extractall(path=dir_path)
|
|
68
|
+
os.remove(zip_path)
|
|
69
|
+
print("Successfully downloaded and unpacked data files.")
|
|
70
|
+
|
|
71
|
+
actual = _read_data_from_csv(
|
|
72
|
+
dir_path / dataset_config["actual"], index_cols=[0], scale=scale
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
forecast: Optional[pd.Series | pd.DataFrame] = None
|
|
76
|
+
if use_forecast:
|
|
77
|
+
forecast = _read_data_from_csv(
|
|
78
|
+
dir_path / dataset_config["forecast"], index_cols=[0, 1], scale=scale
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if start_time is not None:
|
|
82
|
+
shift = pd.to_datetime(start_time) - actual.index[0]
|
|
83
|
+
print(f"Data is being shifted by {shift}")
|
|
84
|
+
actual.index += shift
|
|
85
|
+
if use_forecast:
|
|
86
|
+
forecast = _shift(forecast, shift) # type: ignore
|
|
87
|
+
|
|
88
|
+
return dict(
|
|
89
|
+
actual=actual,
|
|
90
|
+
forecast=None if not use_forecast else forecast,
|
|
91
|
+
fill_method=dataset_config.get("fill_method", "ffill"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_parameter(params: Optional[dict], key: str, default):
|
|
96
|
+
if params is None:
|
|
97
|
+
return default
|
|
98
|
+
return params.get(key, default)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_files(files: list[str], base_dir: Path) -> bool:
|
|
102
|
+
"""Check whether files are present in specified base directory."""
|
|
103
|
+
for file in files:
|
|
104
|
+
path = os.path.join(base_dir, file)
|
|
105
|
+
if not os.path.isfile(path):
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_data_from_csv(
|
|
111
|
+
path: Path, index_cols: list[int], scale: float = 1.0
|
|
112
|
+
) -> pd.Series | pd.DataFrame:
|
|
113
|
+
"""Retrieves a dataframe from a csv file and transforms it."""
|
|
114
|
+
df = pd.read_csv(path, index_col=index_cols)
|
|
115
|
+
if isinstance(df.index, pd.MultiIndex):
|
|
116
|
+
index: pd.MultiIndex = df.index
|
|
117
|
+
for i, level in enumerate(index.levels):
|
|
118
|
+
index = index.set_levels(pd.to_datetime(level), level=i)
|
|
119
|
+
df.index = index
|
|
120
|
+
else:
|
|
121
|
+
df.index = pd.to_datetime(df.index)
|
|
122
|
+
|
|
123
|
+
df.sort_index(inplace=True)
|
|
124
|
+
return (df * scale).astype(float)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _shift(df: pd.Series | pd.DataFrame, shift: timedelta) -> pd.Series | pd.DataFrame:
|
|
128
|
+
"""Shifts indices of the given DataFrame by a timedelta."""
|
|
129
|
+
if isinstance(df.index, pd.MultiIndex):
|
|
130
|
+
index: pd.MultiIndex = df.index
|
|
131
|
+
for i, level in enumerate(index.levels):
|
|
132
|
+
index = index.set_levels(level + shift, level=i)
|
|
133
|
+
df.index = index
|
|
134
|
+
else:
|
|
135
|
+
df.index += shift
|
|
136
|
+
return df
|
vessim/_util.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Union
|
|
5
|
+
from loguru import logger
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
DatetimeLike = Union[str, datetime, np.datetime64]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Clock:
|
|
15
|
+
def __init__(self, sim_start: str | datetime):
|
|
16
|
+
self.sim_start = pd.to_datetime(sim_start)
|
|
17
|
+
|
|
18
|
+
def to_datetime(self, simtime: int) -> datetime:
|
|
19
|
+
return self.sim_start + timedelta(seconds=simtime)
|
|
20
|
+
|
|
21
|
+
def to_simtime(self, dt: datetime) -> int:
|
|
22
|
+
return int((dt - self.sim_start).total_seconds())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def disable_rt_warnings(behind_threshold: float):
|
|
26
|
+
"""Disables Mosaik's rt_check warnings.
|
|
27
|
+
|
|
28
|
+
Mosaik's current implementation of the real-time checks are faulty and a new
|
|
29
|
+
implementation is already outlined in
|
|
30
|
+
https://gitlab.com/mosaik/mosaik/-/issues/172. As this issue was opened in
|
|
31
|
+
June 2023, it is unlikely that the issue will be resolved soon. Therefore,
|
|
32
|
+
we disable the warnings with a threshold after which these offsets are
|
|
33
|
+
logged.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
behind_threshold: Time the simulation is allowed to be behind schedule.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def filter_record(record):
|
|
40
|
+
is_warning = record["level"].name == "WARNING"
|
|
41
|
+
is_mosaik_log = record["name"].startswith("mosaik")
|
|
42
|
+
is_below_threshold = (
|
|
43
|
+
record["function"] == "rt_check"
|
|
44
|
+
and float(record["message"].split(" - ")[1].split("s")[0]) < behind_threshold
|
|
45
|
+
)
|
|
46
|
+
return not (is_warning and is_mosaik_log and is_below_threshold)
|
|
47
|
+
|
|
48
|
+
# Add the filter to the logger
|
|
49
|
+
logger.remove()
|
|
50
|
+
logger.add(sys.stdout, filter=filter_record)
|
vessim/actor.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from itertools import count
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
import mosaik_api_v3 # type: ignore
|
|
9
|
+
|
|
10
|
+
from vessim.signal import Signal
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ActorBase(ABC):
|
|
14
|
+
"""Abstract base class representing a power consumer or producer."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, name: str, step_size: Optional[int] = None) -> None:
|
|
17
|
+
self.name = name
|
|
18
|
+
self.step_size = step_size
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def p(self, now: datetime) -> float:
|
|
22
|
+
"""Current power consumption/production to be used in the grid simulation."""
|
|
23
|
+
|
|
24
|
+
def state(self, now: datetime) -> dict:
|
|
25
|
+
"""Current state of the actor to be used in controllers."""
|
|
26
|
+
return {}
|
|
27
|
+
|
|
28
|
+
def finalize(self) -> None:
|
|
29
|
+
"""Perform any finalization tasks for the consumer.
|
|
30
|
+
|
|
31
|
+
This method can be overridden by subclasses to implement necessary
|
|
32
|
+
finalization steps.
|
|
33
|
+
"""
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Actor(ActorBase):
|
|
38
|
+
"""Actor that represents a consumer of producer based on a single Signal."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, name: str, signal: Signal, step_size: Optional[int] = None) -> None:
|
|
41
|
+
super().__init__(name, step_size)
|
|
42
|
+
self.signal = signal
|
|
43
|
+
|
|
44
|
+
def p(self, now: datetime) -> float:
|
|
45
|
+
return self.signal.now(at=now)
|
|
46
|
+
|
|
47
|
+
def state(self, now: datetime) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"p": self.p(now),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ComputingSystem(ActorBase):
|
|
54
|
+
"""Model of the computing system.
|
|
55
|
+
|
|
56
|
+
This model considers the power usage effectiveness (PUE) and power
|
|
57
|
+
consumption of a list of power meters.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
nodes: list of consumer node signals that constitute the computing system's demand.
|
|
61
|
+
pue: The power usage effectiveness of the system.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
_ids = count(0)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
nodes: list[Signal],
|
|
69
|
+
name: Optional[str] = None,
|
|
70
|
+
step_size: Optional[int] = None,
|
|
71
|
+
pue: float = 1,
|
|
72
|
+
):
|
|
73
|
+
if name is None:
|
|
74
|
+
name = f"ComputingSystem-{next(self._ids)}"
|
|
75
|
+
super().__init__(name, step_size)
|
|
76
|
+
self.nodes = nodes
|
|
77
|
+
node_ids = count(0)
|
|
78
|
+
for node in self.nodes:
|
|
79
|
+
if not node.name:
|
|
80
|
+
node.name = f"Node-{next(node_ids)}"
|
|
81
|
+
self.pue = pue
|
|
82
|
+
|
|
83
|
+
def p(self, now: datetime) -> float:
|
|
84
|
+
return self.pue * sum(-signal.now(at=now) for signal in self.nodes)
|
|
85
|
+
|
|
86
|
+
def state(self, now: datetime) -> dict:
|
|
87
|
+
return {
|
|
88
|
+
"p": self.p(now),
|
|
89
|
+
"nodes": {signal.name: -signal.now(at=now) for signal in self.nodes},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _ActorSim(mosaik_api_v3.Simulator):
|
|
94
|
+
META = {
|
|
95
|
+
"type": "time-based",
|
|
96
|
+
"models": {
|
|
97
|
+
"Actor": {
|
|
98
|
+
"public": True,
|
|
99
|
+
"params": ["actor"],
|
|
100
|
+
"attrs": ["p", "state"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def __init__(self):
|
|
106
|
+
super().__init__(self.META)
|
|
107
|
+
self.eid = None
|
|
108
|
+
self.step_size = None
|
|
109
|
+
self.clock = None
|
|
110
|
+
self.actor = None
|
|
111
|
+
self.p = 0
|
|
112
|
+
self.state = {}
|
|
113
|
+
|
|
114
|
+
def init(self, sid, time_resolution=1.0, **sim_params):
|
|
115
|
+
self.step_size = sim_params["step_size"]
|
|
116
|
+
self.clock = sim_params["clock"]
|
|
117
|
+
return self.meta
|
|
118
|
+
|
|
119
|
+
def create(self, num, model, **model_params):
|
|
120
|
+
assert num == 1, "Only one instance per simulation is supported"
|
|
121
|
+
self.actor = model_params["actor"]
|
|
122
|
+
self.eid = self.actor.name
|
|
123
|
+
return [{"eid": self.eid, "type": model}]
|
|
124
|
+
|
|
125
|
+
def step(self, time, inputs, max_advance):
|
|
126
|
+
assert self.clock is not None
|
|
127
|
+
now = self.clock.to_datetime(time)
|
|
128
|
+
assert self.actor is not None
|
|
129
|
+
self.p = self.actor.p(now)
|
|
130
|
+
self.state = self.actor.state(now)
|
|
131
|
+
assert self.step_size is not None
|
|
132
|
+
return time + self.step_size
|
|
133
|
+
|
|
134
|
+
def get_data(self, outputs):
|
|
135
|
+
return {self.eid: {"p": self.p, "state": self.state}}
|
vessim/controller.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from csv import DictWriter
|
|
8
|
+
from itertools import count
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from typing import Any, MutableMapping, Optional, Callable, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import mosaik_api_v3 # type: ignore
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from vessim.signal import Signal
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from vessim.cosim import Microgrid
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Controller(ABC):
|
|
22
|
+
_counters: dict[type[Controller], Iterator[int]] = {}
|
|
23
|
+
|
|
24
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
25
|
+
"""Initializes the subclass and sets up a counter for naming."""
|
|
26
|
+
super().__init_subclass__(**kwargs)
|
|
27
|
+
cls._counters[cls] = count()
|
|
28
|
+
|
|
29
|
+
def __init__(self, step_size: Optional[int] = None) -> None:
|
|
30
|
+
cls = self.__class__
|
|
31
|
+
self.name: str = f"{cls.__name__}-{next(cls._counters[cls])}"
|
|
32
|
+
self.step_size = step_size
|
|
33
|
+
self.set_parameters: dict[str, Any] = {}
|
|
34
|
+
|
|
35
|
+
def start(self, microgrid: Microgrid) -> None:
|
|
36
|
+
"""Function to be executed before simulation is started. Can be overridden."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def step(self, time: datetime, p_delta: float, e_delta: float, state: dict) -> None:
|
|
41
|
+
"""Performs a simulation step.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
time: Current datetime.
|
|
45
|
+
p_delta: Power delta in W based on the consumption and production of all actors.
|
|
46
|
+
e_delta: Total energy in Ws that has been drawn from/ fed to the utility grid
|
|
47
|
+
in the previous time step.
|
|
48
|
+
state: Contains the last state dictionaries by all actors, the policy, and the storage
|
|
49
|
+
in the microgrid. The state dictionary is defined by the microgrid components and
|
|
50
|
+
can contain any information about their custom defined state.
|
|
51
|
+
The keys are the actor names, `policy`, and `storage` respectively.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def finalize(self) -> None:
|
|
55
|
+
"""Function to be executed after simulation has ended. Can be overridden for clean-up."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Monitor(Controller):
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
step_size: Optional[int] = None,
|
|
63
|
+
outfile: Optional[str | Path] = None,
|
|
64
|
+
grid_signals: Optional[dict[str, Signal]] = None,
|
|
65
|
+
):
|
|
66
|
+
super().__init__(step_size=step_size)
|
|
67
|
+
self.outpath: Optional[Path] = None
|
|
68
|
+
if outfile:
|
|
69
|
+
self.outpath = Path(outfile).expanduser()
|
|
70
|
+
self._fieldnames: Optional[list] = None
|
|
71
|
+
|
|
72
|
+
self.monitor_log: dict[datetime, dict] = defaultdict(dict)
|
|
73
|
+
self.custom_monitor_fns: list[Callable] = []
|
|
74
|
+
|
|
75
|
+
if grid_signals is not None:
|
|
76
|
+
for signal_name, signal_api in grid_signals.items():
|
|
77
|
+
|
|
78
|
+
def fn(time):
|
|
79
|
+
return {signal_name: signal_api.now(time)}
|
|
80
|
+
|
|
81
|
+
self.add_monitor_fn(fn)
|
|
82
|
+
|
|
83
|
+
def add_monitor_fn(self, fn: Callable[[float], dict[str, Any]]):
|
|
84
|
+
self.custom_monitor_fns.append(fn)
|
|
85
|
+
|
|
86
|
+
def step(self, time: datetime, p_delta: float, e_delta: float, state: dict) -> None:
|
|
87
|
+
log_entry = dict(
|
|
88
|
+
p_delta=p_delta,
|
|
89
|
+
e_delta=e_delta,
|
|
90
|
+
)
|
|
91
|
+
log_entry.update(state)
|
|
92
|
+
for monitor_fn in self.custom_monitor_fns:
|
|
93
|
+
log_entry.update(monitor_fn(time))
|
|
94
|
+
self.monitor_log[time] = log_entry
|
|
95
|
+
|
|
96
|
+
if self.outpath:
|
|
97
|
+
if not self._fieldnames:
|
|
98
|
+
log_dict = _flatten_dict(log_entry)
|
|
99
|
+
self._fieldnames = list(log_dict.keys())
|
|
100
|
+
self._fieldnames.insert(0, "time")
|
|
101
|
+
log_dict["time"] = time
|
|
102
|
+
with self.outpath.open("w") as csvfile:
|
|
103
|
+
writer = DictWriter(csvfile, fieldnames=self._fieldnames)
|
|
104
|
+
writer.writeheader()
|
|
105
|
+
writer.writerow(log_dict)
|
|
106
|
+
else:
|
|
107
|
+
with self.outpath.open('a', newline='') as csvfile:
|
|
108
|
+
writer = DictWriter(csvfile, fieldnames=self._fieldnames)
|
|
109
|
+
log_dict = _flatten_dict(log_entry)
|
|
110
|
+
log_dict["time"] = time
|
|
111
|
+
writer.writerow(log_dict)
|
|
112
|
+
|
|
113
|
+
def to_csv(self, out_path: str):
|
|
114
|
+
df = pd.DataFrame({k: _flatten_dict(v) for k, v in self.monitor_log.items()}).T
|
|
115
|
+
df.to_csv(out_path)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _flatten_dict(d: MutableMapping, parent_key: str = "") -> MutableMapping:
|
|
119
|
+
items: list[tuple[str, Any]] = []
|
|
120
|
+
for k, v in d.items():
|
|
121
|
+
new_key = parent_key + "." + k if parent_key else k
|
|
122
|
+
if isinstance(v, MutableMapping):
|
|
123
|
+
items.extend(_flatten_dict(v, str(new_key)).items())
|
|
124
|
+
else:
|
|
125
|
+
items.append((new_key, v))
|
|
126
|
+
return dict(items)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _ControllerSim(mosaik_api_v3.Simulator):
|
|
130
|
+
META = {
|
|
131
|
+
"type": "time-based",
|
|
132
|
+
"models": {
|
|
133
|
+
"Controller": {
|
|
134
|
+
"public": True,
|
|
135
|
+
"any_inputs": True,
|
|
136
|
+
"params": ["controller"],
|
|
137
|
+
"attrs": ["set_parameters"],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
def __init__(self):
|
|
143
|
+
super().__init__(self.META)
|
|
144
|
+
self.eid = "Controller"
|
|
145
|
+
self.step_size = None
|
|
146
|
+
self.clock = None
|
|
147
|
+
self.controller = None
|
|
148
|
+
self.e = 0.0
|
|
149
|
+
|
|
150
|
+
def init(self, sid, time_resolution=1.0, **sim_params):
|
|
151
|
+
self.step_size = sim_params["step_size"]
|
|
152
|
+
self.clock = sim_params["clock"]
|
|
153
|
+
return self.meta
|
|
154
|
+
|
|
155
|
+
def create(self, num, model, **model_params):
|
|
156
|
+
assert num == 1, "Only one instance per simulation is supported"
|
|
157
|
+
self.controller = model_params["controller"]
|
|
158
|
+
return [{"eid": self.eid, "type": model}]
|
|
159
|
+
|
|
160
|
+
def step(self, time, inputs, max_advance):
|
|
161
|
+
assert self.controller is not None
|
|
162
|
+
assert self.clock is not None
|
|
163
|
+
assert self.step_size is not None
|
|
164
|
+
now = self.clock.to_datetime(time)
|
|
165
|
+
self.controller.step(now, *self._parse_controller_inputs(inputs[self.eid]))
|
|
166
|
+
self.set_parameters = self.controller.set_parameters.copy()
|
|
167
|
+
self.controller.set_parameters = {}
|
|
168
|
+
return time + self.step_size
|
|
169
|
+
|
|
170
|
+
def get_data(self, outputs):
|
|
171
|
+
return {self.eid: {"set_parameters": self.set_parameters}}
|
|
172
|
+
|
|
173
|
+
def finalize(self) -> None:
|
|
174
|
+
"""Stops the api server and the collector thread when the simulation finishes."""
|
|
175
|
+
assert self.controller is not None
|
|
176
|
+
self.controller.finalize()
|
|
177
|
+
|
|
178
|
+
def _parse_controller_inputs(
|
|
179
|
+
self, inputs: dict[str, dict[str, Any]]
|
|
180
|
+
) -> tuple[float, float, dict]:
|
|
181
|
+
p_delta = _get_val(inputs, "p_delta")
|
|
182
|
+
last_e = self.e
|
|
183
|
+
self.e = _get_val(inputs, "e")
|
|
184
|
+
actor_keys = [k for k in inputs.keys() if k.startswith("actor")]
|
|
185
|
+
actors: defaultdict[str, Any] = defaultdict(dict)
|
|
186
|
+
for k in actor_keys:
|
|
187
|
+
_, actor_name = k.split(".")
|
|
188
|
+
actors[actor_name] = _get_val(inputs, k)
|
|
189
|
+
state = dict(actors)
|
|
190
|
+
state.update(_get_val(inputs, "state"))
|
|
191
|
+
return p_delta, self.e - last_e, state
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _get_val(inputs: dict, key: str) -> Any:
|
|
195
|
+
return list(inputs[key].values())[0]
|