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/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
@@ -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)
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ """Vessim Software-in-the-Loop (SiL) components.
2
+
3
+ This module is highly experimental, the public API might change at any time.
4
+ """