.. Author: elenia@TUBS Copyright 2024 elenia This file is part of eELib, which is free software under the terms of the GNU GPL Version 3. ############################ Configuration of a Simulator ############################ The simulator of a model, as explained in the :ref:`glossary `, works as out interface to communicate between the orchestrator of the simulation and the model entities. This means, that the simulator for the PV system model sends input values to the pv systems, initiates the model calculations and collects output values to return to the orchestrator (e.g. for sending the power values to the connected energy management systems). As there may be multiple entities of a model connected to a single simulator, you only need one simulator of a model type for a simulation, as this simulator makes the communication for all entities of its model type. Hence, a ``_simulator.py`` is always paired with a :ref:`_model.py `. This page explains the structure of a ``_simulator.py`` to enable you to create a simulator for your own model. .. note:: All code-blocks derive from ``charging_station_simulator.py`` as of (01/24) if not stated otherwise. Import mosaik and model ======================= .. code-block:: :lineno-start: 1 :caption: ``Import`` of the mosaik simulator base class ``mosaik_api_v3`` and the corresponding model + additional imports import mosaik_api_v3 from eelib.core.devices.charging_station import charging_station_model import eelib.utils.validation as vld from copy import deepcopy ---------------------------- Listing of model ``META`` ========================= .. code-block:: :lineno-start: 17 :caption: State of simulator (whether it is time-discrete or event-based/hybrid), see the :ref:`glossary ` META = { "type": "hybrid", .. code-block:: :lineno-start: 19 :caption: Listing of **ALL** attributes for each modeltype: At first, all attributes of the model class... "models": { "ChargingStation": { "public": True, "params": ["init_vals"], "attrs": [ "p", "step_size", "time", ... .. code-block:: :lineno-start: 46 :caption: ... **input attributes** also listed in ``"trigger"`` list ... "trigger": [ "p_set", "ev_data", ], # input attributes .. code-block:: :lineno-start: 56 :caption: ... **output attributes** also listed in ``""non-persistent"`` list. "non-persistent": [ "p", "p_min", "p_max", "cs_data", ], # output attributes ---------------------------- Initialization of simulator class ================================= .. code-block:: :lineno-start: 72 :caption: Constructor ``__init__`` for this simulator and the initialization function ``init()`` class Sim(mosaik_api_v3.Simulator): """Simulator class for eELib charging station model.""" def __init__(self): """Constructs an object of the Charging-Station:Sim class.""" super(Sim, self).__init__(META) ... def init(self, sid, scenario_config, time_resolution=1.0): """Initializes parameters for an object of the Charging-Station:Sim class.""" ... --------------------------- Following are the so called **core functions** :meth:`~eelib.core.devices.charging_station.charging_station_simulator.Sim.create()`, :meth:`~eelib.core.devices.charging_station.charging_station_simulator.Sim.step()` and :meth:`~eelib.core.devices.charging_station.charging_station_simulator.Sim.get_data()` that are found in every ``_simulator.py``. .. caution:: All **core functions** of the simulators are called by mosaik and need to be existent! ---------------------------- Creation of model entities in ``create()`` ========================================== .. code-block:: :lineno-start: 111 :caption: Creation of model entities by assigning ID and name, creating it by its model type, and storing the entity data :emphasize-lines: 3 def create(self, num, model_type, init_vals): """Creates entities of the eELib charging station model.""" # generated next unused ID for entity next_eid = len(self.entities) # create empty list for created entities entities_orchestrator = [] for i in range(next_eid, next_eid + num): # create entity by specified name and ID ename = "%s%s%d" % (model_type, "_", i) full_id = self.sid + "." + ename # get class of specific model and create entity with init values after validation entity_cls = getattr(charging_station_model, model_type) vld.validate_init_parameters(entity_cls, init_vals[i]) entity = entity_cls( ename, **init_vals[i], step_size=self.scenario_config["step_size"], # add info to the simulators entity-list and current entities self.entities[ename] = { "ename": ename, "etype": model_type, "model": entity, "full_id": full_id, } entities_orchestrator.append({"eid": ename, "type": model_type}) return entities_orchestrator ---------------------------- Stepping of models in ``step()`` ================================ Withint the ``step()`` method, ``inputs`` coming from mosaik are set into the model entity attributes they are intended for. Afterwards, the entities are called and their calculation is executed with the models' ``step()`` method. This example is coming from the ``storage_simulator.py`` (01/24). .. code-block:: :lineno-start: 175 :caption: Take input data and set values of entities, then call the model calculation # assign property values for each entity and attribute with entity ID # process input signals: for the entities (eid), attr is a dict for attributes to be set for eid, attrs in inputs.items(): # for the attributes, setter is a dict for entities with corresponding set values for attr, setter in attrs.items(): # set values corresponding to its type ... # call step function for each entity in the list for ename, entity_dict in self.entities.items(): entity_dict["model"].step(time) .. note:: You might ``return`` the next timestep for when this model should be called again. This is needed for time-based models (e.g. when reading data from a csv file, the next value should be read and send out) and optional for event-based models (e.g. the adaption of energy levels for a storage model is needed, but a heatpump could maybe just be triggered by a new set value). ---------------------------- Handling of output data in ``get_data()`` ========================================= .. code-block:: :lineno-start: 233 :caption: From a defined set of output properties given by the ``outputs`` parameter from mosaik, the values of the transmitting entities are read and stored into a data dict that is returned for transmitter_ename, attrs in outputs.items(): # get name for current entity and create dict field entry = self.entities[transmitter_ename] if transmitter_ename not in self.output_cache: self.output_cache[transmitter_ename] = {} # loop over all targeted attributes and check if info is available for attr in attrs: if attr not in self.meta["models"][type(entry["model"]).__name__]["attrs"]: raise ValueError("Unknown output attribute: %s" % attr) # create empty field for cache and output data if attr not in self.output_cache[transmitter_ename]: self.output_cache[transmitter_ename][attr] = {} output_data_to_save = getattr(entry["model"], attr) # check if this value changed ... # check if nothing is to be send out - send output 1 step later to avoid waiting for data if not flag_output_changed: if self.time == self.scenario_config["n_steps"] - 1: # is last time step? data["time"] = self.time + 1 else: data = {"time": self.time} return data For each time step, the output data is continuously stored and compared to the lastly sent data (``output_cache``) such that if nothing new is to be send out, only the time step will be send. In case of the last time step, we need to adjust the signal by transmitting not the current but the next time step to avoid a loop of endless calling between the simulators.