vessim 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 +327 -0
- vessim/analysis.py +111 -0
- vessim/core/__init__.py +1 -0
- vessim/core/consumer.py +99 -0
- vessim/core/microgrid.py +39 -0
- vessim/core/storage.py +122 -0
- vessim/cosim/__init__.py +15 -0
- vessim/cosim/_util.py +162 -0
- vessim/cosim/carbon_api.py +52 -0
- vessim/cosim/consumer.py +53 -0
- vessim/cosim/generator.py +48 -0
- vessim/cosim/microgrid.py +38 -0
- vessim/cosim/monitor.py +71 -0
- vessim/sil/__init__.py +4 -0
- vessim/sil/api_server.py +229 -0
- vessim/sil/http_client.py +54 -0
- vessim/sil/loop_thread.py +46 -0
- vessim/sil/node.py +31 -0
- vessim/sil/power_meter.py +44 -0
- vessim/sil/redis_docker.py +63 -0
- vessim/sil/sil_interface.py +174 -0
- vessim-0.dist-info/METADATA +146 -0
- vessim-0.dist-info/RECORD +24 -0
- vessim-0.dist-info/WHEEL +4 -0
vessim/core/storage.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Storage(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def update(self, power: float, duration: int) -> float:
|
|
10
|
+
"""Feed or draw energy for specified duration.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
power: Charging if positive, discharging if negative.
|
|
14
|
+
duration: Duration in seconds for which the storage will be (dis)charged.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The power delta, in case not all requested power could be discharged from or
|
|
18
|
+
charged into the battery. This can happen either if the batter is full/empty
|
|
19
|
+
or if the C-rate was exceeded.
|
|
20
|
+
If 0, all power was successfully (dis)charged.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def soc(self) -> float:
|
|
25
|
+
"""Returns the current State of Charge (SoC)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SimpleBattery(Storage):
|
|
29
|
+
"""(Way too) simple battery.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
capacity: Battery capacity in watt-seconds (Ws).
|
|
33
|
+
charge_level: Initial charge level in watt-seconds (Ws).
|
|
34
|
+
min_soc: Minimum allowed state of charge (SoC) for the battery.
|
|
35
|
+
c_rate: C-rate, which defines the charge and discharge rate of the battery.
|
|
36
|
+
For more information on C-rate, see `C-rate explanation <https://www.batterydesign.net/electrical/c-rate/>`_.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
capacity: float,
|
|
42
|
+
charge_level: float = 0,
|
|
43
|
+
min_soc: float = 0,
|
|
44
|
+
c_rate: Optional[float] = None,
|
|
45
|
+
):
|
|
46
|
+
self.capacity = capacity
|
|
47
|
+
assert 0 <= charge_level <= self.capacity
|
|
48
|
+
self.charge_level = charge_level
|
|
49
|
+
assert 0 <= min_soc <= self.soc()
|
|
50
|
+
self.min_soc = min_soc
|
|
51
|
+
self.c_rate = c_rate
|
|
52
|
+
|
|
53
|
+
def update(self, power: float, duration: int) -> float:
|
|
54
|
+
if duration <= 0.0:
|
|
55
|
+
raise ValueError("Duration needs to be a positive value")
|
|
56
|
+
|
|
57
|
+
max_charge_p_delta, p_delta = 0.0, 0.0
|
|
58
|
+
|
|
59
|
+
if self.c_rate is not None:
|
|
60
|
+
max_rate = self.c_rate * self.capacity / 3600
|
|
61
|
+
if power >= max_rate:
|
|
62
|
+
logger.info(
|
|
63
|
+
f"Trying to charge storage '{self.__class__.__name__}' with "
|
|
64
|
+
f"{power} W but only {max_rate} W are supported."
|
|
65
|
+
)
|
|
66
|
+
max_charge_p_delta = power - max_rate
|
|
67
|
+
power = max_rate
|
|
68
|
+
|
|
69
|
+
if power <= -max_rate:
|
|
70
|
+
logger.info(
|
|
71
|
+
f"Trying to discharge storage '{self.__class__.__name__}' "
|
|
72
|
+
f"with {power} W but only {max_rate} W are supported."
|
|
73
|
+
)
|
|
74
|
+
max_charge_p_delta = power + max_rate
|
|
75
|
+
power = -self.c_rate
|
|
76
|
+
|
|
77
|
+
charge_energy = power * duration
|
|
78
|
+
new_charge_level = self.charge_level + power * duration
|
|
79
|
+
|
|
80
|
+
abs_min_soc = self.min_soc * self.capacity
|
|
81
|
+
if new_charge_level < abs_min_soc:
|
|
82
|
+
p_delta = (new_charge_level - abs_min_soc) / duration
|
|
83
|
+
self.charge_level = abs_min_soc
|
|
84
|
+
elif new_charge_level > self.capacity:
|
|
85
|
+
p_delta = (new_charge_level - self.capacity) / duration
|
|
86
|
+
self.charge_level = self.capacity
|
|
87
|
+
else:
|
|
88
|
+
self.charge_level += charge_energy
|
|
89
|
+
|
|
90
|
+
return p_delta + max_charge_p_delta
|
|
91
|
+
|
|
92
|
+
def soc(self) -> float:
|
|
93
|
+
return self.charge_level / self.capacity
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class StoragePolicy(ABC):
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def apply(self, storage: Storage, p_delta: float, time_since_last_step: int) -> float:
|
|
99
|
+
"""(Dis)charge the storage according to the policy."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DefaultStoragePolicy(StoragePolicy):
|
|
103
|
+
"""Storage policy which tries to (dis)charge as much of the delta as possible.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
grid_power: If not 0, the battery is in "charge mode" and will draw the
|
|
107
|
+
provided power from the grid. In this case, the delta simply returned
|
|
108
|
+
together with the demand for charging.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, grid_power: float = 0):
|
|
112
|
+
self.grid_power = grid_power
|
|
113
|
+
|
|
114
|
+
def apply(self, storage: Storage, p_delta: float, time_since_last_step: int) -> float:
|
|
115
|
+
if self.grid_power == 0:
|
|
116
|
+
return storage.update(power=p_delta, duration=time_since_last_step)
|
|
117
|
+
else:
|
|
118
|
+
excess_energy = storage.update(
|
|
119
|
+
power=self.grid_power, duration=time_since_last_step
|
|
120
|
+
)
|
|
121
|
+
real_charge_power = self.grid_power - excess_energy
|
|
122
|
+
return p_delta - real_charge_power
|
vessim/cosim/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Vessim co-simulation components."""
|
|
2
|
+
|
|
3
|
+
from vessim.cosim.carbon_api import CarbonApiSim
|
|
4
|
+
from vessim.cosim.consumer import ConsumerSim
|
|
5
|
+
from vessim.cosim.generator import GeneratorSim
|
|
6
|
+
from vessim.cosim.microgrid import MicrogridSim
|
|
7
|
+
from vessim.cosim.monitor import MonitorSim
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CarbonApiSim",
|
|
11
|
+
"ConsumerSim",
|
|
12
|
+
"GeneratorSim",
|
|
13
|
+
"MicrogridSim",
|
|
14
|
+
"MonitorSim",
|
|
15
|
+
]
|
vessim/cosim/_util.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Type, Dict, Any, Union
|
|
5
|
+
|
|
6
|
+
import mosaik_api # type: ignore
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VessimModel:
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def step(self, time: int, inputs: dict) -> None:
|
|
15
|
+
"""Performs a simulation step on the model.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
time: The current simulation time
|
|
19
|
+
inputs: The inputs from other simulators
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VessimSimulator(mosaik_api.Simulator, ABC):
|
|
24
|
+
"""Utility class for single-model simulators as supported by Vessim.
|
|
25
|
+
|
|
26
|
+
Most use cases for simulators simply require setting all inputs attr values
|
|
27
|
+
to model_instance attrs and then step the model_instance. This class takes
|
|
28
|
+
care of all basic mosaik abstractions that are simple copy and paste tasks
|
|
29
|
+
for each new simulator.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
eid_prefix: The prefix to be used for entity IDs.
|
|
33
|
+
model_class: The class of the model to be simulated.
|
|
34
|
+
entities: A dictionary that maps entity IDs to their instances.
|
|
35
|
+
time: The current simulation time.
|
|
36
|
+
step_size: The simulation step size.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, meta, model_class: Type[VessimModel]):
|
|
40
|
+
"""Initialization of a basic simulator with given model.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
meta: A dictionary that describes the simulator's metadata.
|
|
44
|
+
model_class: The class of the model to be simulated. Model requires
|
|
45
|
+
step() method with no args (must only utilize object
|
|
46
|
+
attributes). Alternatively, the step() method of this class
|
|
47
|
+
must be overwritten and implemented individually.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(meta)
|
|
50
|
+
self.eid_prefix = list(self.meta["models"])[0] + "_"
|
|
51
|
+
self.model_class = model_class
|
|
52
|
+
self.entities: Dict[str, VessimModel] = {}
|
|
53
|
+
self.time = 0
|
|
54
|
+
|
|
55
|
+
def init(self, sid, time_resolution, eid_prefix=None):
|
|
56
|
+
"""Initialize Simulator and set `step_size` and `eid_prefix`."""
|
|
57
|
+
if float(time_resolution) != 1.0:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"{self.__class__.__name__} only supports time_resolution=1., "
|
|
60
|
+
f"but {time_resolution} was set."
|
|
61
|
+
)
|
|
62
|
+
if eid_prefix is not None:
|
|
63
|
+
self.eid_prefix = eid_prefix
|
|
64
|
+
return self.meta
|
|
65
|
+
|
|
66
|
+
def create(self, num, model, *args, **kwargs):
|
|
67
|
+
"""Create model instance and save it in `entities`."""
|
|
68
|
+
next_eid = len(self.entities)
|
|
69
|
+
entities = []
|
|
70
|
+
for i in range(next_eid, next_eid + num):
|
|
71
|
+
# Instantiate `model_class` specified in constructor and pass through args
|
|
72
|
+
entity = self.model_class(*args, **kwargs)
|
|
73
|
+
eid = self.eid_prefix + str(i)
|
|
74
|
+
self.entities[eid] = entity
|
|
75
|
+
entities.append({"eid": eid, "type": model})
|
|
76
|
+
return entities
|
|
77
|
+
|
|
78
|
+
def step(self, time, inputs, max_advance):
|
|
79
|
+
"""Set all `inputs` attr values to the `entity` attrs, then step the `entity`."""
|
|
80
|
+
self.time = time
|
|
81
|
+
for eid, entity in self.entities.items():
|
|
82
|
+
entity.step(time, inputs.get(eid, {}))
|
|
83
|
+
return self.next_step(time)
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def next_step(self, time):
|
|
87
|
+
"""Return time of next simulation step (None for event-based)."""
|
|
88
|
+
|
|
89
|
+
def get_data(self, outputs):
|
|
90
|
+
"""Return all requested data as attr from the `model_instance`."""
|
|
91
|
+
data = {}
|
|
92
|
+
model_name = list(self.meta["models"])[0]
|
|
93
|
+
for eid, attrs in outputs.items():
|
|
94
|
+
model = self.entities[eid]
|
|
95
|
+
data["time"] = self.time
|
|
96
|
+
data[eid] = {}
|
|
97
|
+
for attr in attrs:
|
|
98
|
+
if attr not in self.meta["models"][model_name]["attrs"]:
|
|
99
|
+
raise ValueError(f"Unknown output attribute: {attr}")
|
|
100
|
+
if hasattr(model, attr):
|
|
101
|
+
data[eid][attr] = getattr(model, attr)
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Clock:
|
|
106
|
+
def __init__(self, sim_start: Union[str, datetime]):
|
|
107
|
+
self.sim_start = pd.to_datetime(sim_start)
|
|
108
|
+
|
|
109
|
+
def to_datetime(self, simtime: int) -> datetime:
|
|
110
|
+
return self.sim_start + timedelta(seconds=simtime)
|
|
111
|
+
|
|
112
|
+
def to_simtime(self, dt: datetime) -> int:
|
|
113
|
+
return int((dt - self.sim_start).total_seconds())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def simplify_inputs(attrs: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
|
117
|
+
"""Removes Mosaik source entity from input dict.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
>>> simplify_inputs({'p': {'ComputingSystem-0.ComputingSystem_0': -50}})
|
|
121
|
+
{'p': -50}
|
|
122
|
+
"""
|
|
123
|
+
# TODO We should make this function a bit more elegant once we better evaluated
|
|
124
|
+
# our requirements for Vessim simulations.
|
|
125
|
+
result = {}
|
|
126
|
+
for key, val_dict in attrs.items():
|
|
127
|
+
result[key] = list(val_dict.values())[0]
|
|
128
|
+
# flattening dicts
|
|
129
|
+
if isinstance(result[key], dict):
|
|
130
|
+
for kk in result[key].keys():
|
|
131
|
+
result[f"{key}.{kk}"] = result[key][kk]
|
|
132
|
+
del result[key]
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def disable_mosaik_warnings(behind_threshold: float):
|
|
137
|
+
"""Disables Mosaik's incorrect Loguru warnings.
|
|
138
|
+
|
|
139
|
+
Mosaik currently deems specific attribute connections as incorrect and logs
|
|
140
|
+
them as warnings. Also the simulation is always behind by a few fractions
|
|
141
|
+
of a second (which is fine, code needs time to execute) which Mosaik also
|
|
142
|
+
logs as a Warning. These Warnings are flagged as bugs in Mosaik's current
|
|
143
|
+
developement and should be fixed within its next release. Until then, this
|
|
144
|
+
function should do.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
behind_threshold: Time the simulation is allowed to be behind schedule.
|
|
148
|
+
"""
|
|
149
|
+
# Define a function to filter out WARNING level logs
|
|
150
|
+
def filter_record(record):
|
|
151
|
+
is_warning = record["level"].name == "WARNING"
|
|
152
|
+
is_mosaik_log = record["name"].startswith("mosaik")
|
|
153
|
+
is_attribute = record["function"] == "_check_attributes_values"
|
|
154
|
+
is_below_threshold = (
|
|
155
|
+
record["function"] == "rt_check" and
|
|
156
|
+
float(record["message"].split(' - ')[1].split('s')[0]) < behind_threshold
|
|
157
|
+
)
|
|
158
|
+
return not (is_warning and is_mosaik_log and (is_below_threshold or is_attribute))
|
|
159
|
+
|
|
160
|
+
# Add the filter to the logger
|
|
161
|
+
logger.remove()
|
|
162
|
+
logger.add(sys.stdout, filter=filter_record)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from vessim import TimeSeriesApi
|
|
4
|
+
from vessim.cosim._util import VessimSimulator, VessimModel, Clock
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CarbonApiSim(VessimSimulator):
|
|
8
|
+
|
|
9
|
+
META = {
|
|
10
|
+
'type': 'time-based',
|
|
11
|
+
'models': {
|
|
12
|
+
'CarbonApi': {
|
|
13
|
+
'public': True,
|
|
14
|
+
'params': ['zone'],
|
|
15
|
+
'attrs': ['carbon_intensity'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__(self.META, _CarbonApiModel)
|
|
22
|
+
self.clock = None
|
|
23
|
+
self.sim_start = None
|
|
24
|
+
self.carbon_api = None
|
|
25
|
+
|
|
26
|
+
def init(self, sid, time_resolution, sim_start: datetime, # type: ignore
|
|
27
|
+
carbon_api: TimeSeriesApi, eid_prefix=None):
|
|
28
|
+
super().init(sid, time_resolution, eid_prefix=eid_prefix)
|
|
29
|
+
self.clock = Clock(sim_start)
|
|
30
|
+
self.carbon_api = carbon_api
|
|
31
|
+
return self.meta
|
|
32
|
+
|
|
33
|
+
def create(self, num, model, *args, **kwargs):
|
|
34
|
+
return super().create(num, model, *args, **kwargs, clock=self.clock,
|
|
35
|
+
carbon_api=self.carbon_api)
|
|
36
|
+
|
|
37
|
+
def next_step(self, time: int) -> int:
|
|
38
|
+
dt = self.clock.to_datetime(time)
|
|
39
|
+
next_dt = self.carbon_api.next_update(dt)
|
|
40
|
+
return self.clock.to_simtime(next_dt)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _CarbonApiModel(VessimModel):
|
|
44
|
+
def __init__(self, carbon_api: TimeSeriesApi, clock: Clock, zone: str):
|
|
45
|
+
self.api = carbon_api
|
|
46
|
+
self.clock = clock
|
|
47
|
+
self.zone = zone
|
|
48
|
+
self.carbon_intensity = 0.0
|
|
49
|
+
|
|
50
|
+
def step(self, time: int, inputs: dict) -> None:
|
|
51
|
+
dt = self.clock.to_datetime(time)
|
|
52
|
+
self.carbon_intensity = self.api.actual(dt, self.zone)
|
vessim/cosim/consumer.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from vessim.core.consumer import Consumer
|
|
4
|
+
from vessim.cosim._util import VessimSimulator, VessimModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConsumerSim(VessimSimulator):
|
|
8
|
+
"""Computing System simulator that executes its model."""
|
|
9
|
+
|
|
10
|
+
META = {
|
|
11
|
+
"type": "time-based",
|
|
12
|
+
"models": {
|
|
13
|
+
"Consumer": {
|
|
14
|
+
"public": True,
|
|
15
|
+
"params": ["consumer"],
|
|
16
|
+
"attrs": ["p", "info"],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.step_size = None
|
|
23
|
+
super().__init__(self.META, _ComputingSystemModel)
|
|
24
|
+
|
|
25
|
+
def init(self, sid, time_resolution, step_size, eid_prefix=None):
|
|
26
|
+
self.step_size = step_size
|
|
27
|
+
return super().init(sid, time_resolution, eid_prefix=eid_prefix)
|
|
28
|
+
|
|
29
|
+
def finalize(self) -> None:
|
|
30
|
+
"""Stops power meters' threads."""
|
|
31
|
+
super().finalize()
|
|
32
|
+
for model_instance in self.entities.values():
|
|
33
|
+
model_instance.consumer.finalize() # type: ignore
|
|
34
|
+
|
|
35
|
+
def next_step(self, time):
|
|
36
|
+
return time + self.step_size
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _ComputingSystemModel(VessimModel):
|
|
40
|
+
|
|
41
|
+
def __init__(self, consumer: Consumer):
|
|
42
|
+
self.consumer = consumer
|
|
43
|
+
self.p = 0.0
|
|
44
|
+
self.info: Dict = {}
|
|
45
|
+
|
|
46
|
+
def step(self, time: int, inputs: dict) -> None:
|
|
47
|
+
"""Updates the power consumption of the system.
|
|
48
|
+
|
|
49
|
+
The power consumption is calculated as the product of the PUE and the
|
|
50
|
+
sum of the node power of all power meters.
|
|
51
|
+
"""
|
|
52
|
+
self.p = -self.consumer.consumption()
|
|
53
|
+
self.info = self.consumer.info()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from vessim import TimeSeriesApi
|
|
5
|
+
from vessim.cosim._util import Clock, VessimSimulator, VessimModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GeneratorSim(VessimSimulator):
|
|
9
|
+
|
|
10
|
+
META = {
|
|
11
|
+
"type": "time-based",
|
|
12
|
+
"models": {
|
|
13
|
+
"Generator": {
|
|
14
|
+
"public": True,
|
|
15
|
+
"params": ["generator"],
|
|
16
|
+
"attrs": ["p"],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__(self.META, _GeneratorModel)
|
|
23
|
+
|
|
24
|
+
def init(self, sid, time_resolution, sim_start: datetime, # type: ignore
|
|
25
|
+
eid_prefix=None):
|
|
26
|
+
super().init(sid, time_resolution, eid_prefix=eid_prefix)
|
|
27
|
+
self.clock = Clock(sim_start)
|
|
28
|
+
return self.meta
|
|
29
|
+
|
|
30
|
+
def create(self, num, model, *args, **kwargs):
|
|
31
|
+
return super().create(num, model, *args, **kwargs, clock=self.clock)
|
|
32
|
+
|
|
33
|
+
def next_step(self, time: int) -> int:
|
|
34
|
+
dt = self.clock.to_datetime(time)
|
|
35
|
+
next_dt = min(e.generator.next_update(dt) # type: ignore
|
|
36
|
+
for e in self.entities.values())
|
|
37
|
+
return self.clock.to_simtime(next_dt)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _GeneratorModel(VessimModel):
|
|
41
|
+
def __init__(self, generator: TimeSeriesApi, clock: Clock):
|
|
42
|
+
self.generator = generator
|
|
43
|
+
self.clock = clock
|
|
44
|
+
self.p: Optional[float] = None
|
|
45
|
+
|
|
46
|
+
def step(self, time: int, inputs: dict) -> None:
|
|
47
|
+
dt = self.clock.to_datetime(time)
|
|
48
|
+
self.p = self.generator.actual(dt)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from vessim.core.microgrid import Microgrid
|
|
4
|
+
from vessim.cosim._util import VessimSimulator, VessimModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MicrogridSim(VessimSimulator):
|
|
8
|
+
|
|
9
|
+
META = {
|
|
10
|
+
"type": "event-based",
|
|
11
|
+
"models": {
|
|
12
|
+
"Microgrid": {
|
|
13
|
+
"public": True,
|
|
14
|
+
"params": ["microgrid"],
|
|
15
|
+
"attrs": ["p", "p_delta"],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.step_size = None
|
|
22
|
+
super().__init__(self.META, _MicrogridModel)
|
|
23
|
+
|
|
24
|
+
def next_step(self, time):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _MicrogridModel(VessimModel):
|
|
29
|
+
def __init__(self, microgrid: Microgrid):
|
|
30
|
+
self.microgrid = microgrid
|
|
31
|
+
self.p_delta = 0.0
|
|
32
|
+
self._last_step_time = 0
|
|
33
|
+
|
|
34
|
+
def step(self, time: int, inputs: Dict) -> None:
|
|
35
|
+
duration = time - self._last_step_time
|
|
36
|
+
if duration > 0:
|
|
37
|
+
self.p_delta = self.microgrid.power_flow(p=inputs["p"], duration=duration)
|
|
38
|
+
self._last_step_time = time
|
vessim/cosim/monitor.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Dict, Callable, Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from vessim.cosim._util import Clock, VessimSimulator, VessimModel, simplify_inputs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MonitorSim(VessimSimulator):
|
|
11
|
+
|
|
12
|
+
META = {
|
|
13
|
+
"type": "time-based",
|
|
14
|
+
"models": {
|
|
15
|
+
"Monitor": {
|
|
16
|
+
"public": True,
|
|
17
|
+
"any_inputs": True,
|
|
18
|
+
"params": ["out_path", "fn"],
|
|
19
|
+
"attrs": [],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
"""Simple data collector for printing data at the end of simulation."""
|
|
26
|
+
self.step_size = None
|
|
27
|
+
self.clock = None
|
|
28
|
+
super().__init__(self.META, _MonitorModel)
|
|
29
|
+
|
|
30
|
+
def init(self, sid, time_resolution, sim_start: datetime, # type: ignore
|
|
31
|
+
step_size: int, eid_prefix=None):
|
|
32
|
+
self.step_size = step_size # type: ignore
|
|
33
|
+
self.clock = Clock(sim_start) # type: ignore
|
|
34
|
+
return super().init(sid, time_resolution, eid_prefix=eid_prefix)
|
|
35
|
+
|
|
36
|
+
def create(self, num, model, *args, **kwargs): # type: ignore
|
|
37
|
+
return super().create(num, model, *args, **kwargs, clock=self.clock)
|
|
38
|
+
|
|
39
|
+
def finalize(self):
|
|
40
|
+
"""Collected data is printed to file at simulation end."""
|
|
41
|
+
for model in self.entities.values():
|
|
42
|
+
model: _MonitorModel
|
|
43
|
+
model.finalize()
|
|
44
|
+
|
|
45
|
+
def next_step(self, time):
|
|
46
|
+
return time + self.step_size
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _MonitorModel(VessimModel):
|
|
50
|
+
def __init__(self, out_path: str, fn: Callable[[], Dict[str, Any]], clock: Clock):
|
|
51
|
+
self.out_path = out_path
|
|
52
|
+
self.fn = fn
|
|
53
|
+
self._clock = clock
|
|
54
|
+
self.data: Dict = defaultdict(dict)
|
|
55
|
+
|
|
56
|
+
def step(self, time: int, inputs: Dict) -> None:
|
|
57
|
+
inputs = simplify_inputs(inputs)
|
|
58
|
+
dt = self._clock.to_datetime(time)
|
|
59
|
+
# TODO Reimplement logging on DEBUG level
|
|
60
|
+
# logger.info(f"# --- {str(dt):>5} ---")
|
|
61
|
+
|
|
62
|
+
if self.fn is not None:
|
|
63
|
+
inputs.update(self.fn())
|
|
64
|
+
|
|
65
|
+
for attr, value in inputs.items():
|
|
66
|
+
# logger.info(f"{attr}: {value}")
|
|
67
|
+
self.data[attr][dt] = value
|
|
68
|
+
|
|
69
|
+
def finalize(self):
|
|
70
|
+
"""Collected data is printed to file at simulation end."""
|
|
71
|
+
pd.DataFrame(self.data).to_csv(self.out_path)
|
vessim/sil/__init__.py
ADDED