.. 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 :term:`Simulator` of a model works as our 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 communicates with 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`` helping 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_id = len(self.entities) # create empty list for created entities entities_orchestrator = [] for i in range(next_id, next_id + num): # create entity by specified name and ID eid = "%s%s%d" % (model_type, "_", i) full_id = self.sid + "." + eid # 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( eid, **init_vals[i], step_size=self.scenario_config["step_size"], # add info to the simulators entity-list and current entities self.entities[eid] = { "eid": eid, "etype": model_type, "model": entity, "full_id": full_id, } entities_orchestrator.append({"eid": eid, "type": model_type}) return entities_orchestrator ---------------------------- Stepping of models in ``step()`` ================================ Within the ``step()`` method, ``inputs`` 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 eid, 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.timestep == self.scenario_config["n_steps"] - 1: # is last timestep? data["time"] = self.timestep + 1 else: data = {"time": self.timestep} return data For each timestep, the output data is continuously stored and compared to the lastly sent data (``output_cache``) so if nothing new is to be sent out, only the timestep will be sent. In case of the last timestep, we need to adjust the signal by transmitting not the current but the next timestep to avoid a loop of endless calling between the simulators.