From 97cd7aeea98666cf6793da80bb8d477db7ebd41c Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Mon, 23 Sep 2024 17:43:40 +0200 Subject: [PATCH 1/9] create core package for model construtor --- src/illuminator/core/__init__.py | 0 {Models => src/illuminator/core}/construct_model.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/illuminator/core/__init__.py rename {Models => src/illuminator/core}/construct_model.py (100%) diff --git a/src/illuminator/core/__init__.py b/src/illuminator/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Models/construct_model.py b/src/illuminator/core/construct_model.py similarity index 100% rename from Models/construct_model.py rename to src/illuminator/core/construct_model.py From cc58c2b4cde9b5198c31ef6db575c3d7bff568ad Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Tue, 24 Sep 2024 18:29:58 +0200 Subject: [PATCH 2/9] refactor: replace construct_model with model_constructor for improved abstraction --- src/illuminator/core/construct_model.py | 105 ----------------- src/illuminator/core/model_constructor.py | 137 ++++++++++++++++++++++ tests/core/test_model_constructor.py | 18 +++ 3 files changed, 155 insertions(+), 105 deletions(-) delete mode 100644 src/illuminator/core/construct_model.py create mode 100644 src/illuminator/core/model_constructor.py create mode 100644 tests/core/test_model_constructor.py diff --git a/src/illuminator/core/construct_model.py b/src/illuminator/core/construct_model.py deleted file mode 100644 index 5b24989..0000000 --- a/src/illuminator/core/construct_model.py +++ /dev/null @@ -1,105 +0,0 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -from enum import Enum -from mosaik_api import Simulator - -class SimulatorType(Enum): - TIME_BASED = 'time-based' - EVENT_BASED = 'event-based' - HYBRID = 'hybrid' - -# NOTE: I have also inherited from the mosaik_api.Simulator class to integrate this model -# This seems naïve to me. Lots of redundancy with defining which methods are mandatory. -# As for the: the models created seem to have a wide variety of attributes and parameters that are defined. -# In addition, the naming convection of the methods created in them is also non-existant -@dataclass -class IlluminatorModel(Simulator, ABC): - """ - A common interface for adding energy models to the Illuminator. - This class combines the creation of model and simulators for the Mosaik engine. - - ... - - Parameters - ---------- - simulator_type: SimulatorType.TIME_BASED - Defines how the simulator is advanced through time and whether its attributes are persistent in time or transient - model_name: str - The name of the model - public: bool - Determines whether a model can be instantiated by a user - parameters: list - Variables which values can be set when instantiating the model (can this be always considered inputs?) - attrs: list - Model properties that can be read from (can these be always considered outputs?) - trigger: list - Attribute names that cause the simulator to be stepped when another simulator provides output which is connected to one of those. - Empty if no triggers. - inputs: list - ??? - outputs: list - ??? - states: list - ??? - """ - - # TODO:the ideal solution provides a single interface for creating a model and its simulator - - ### meta data ### - model_name: str - parameters: list # variables which values can be set when instantiating the model (can this be always considered inputs?) - attrs: list # model properties that can be read from (can these be always considered outputs?) - trigger: list - - inputs: list - outputs: list - states: list - - simulator_type: SimulatorType = SimulatorType.TIME_BASED - public: bool = True - ################# - - @classmethod - def __test__(cls, subclass): - return hasattr(subclass, "__init__") - - def __post_init__(self): - """ - Post-init method which creates and initializes the metadata used by mosaik_api - """ - META = { - 'type': self.simulator_type, - 'models': { - self.model_name: { - 'public': self.public, - 'params': self.parameters, - 'attrs': self.attrs, - 'trigger': self.trigger - }, - }, - } - super().__init__(META) - - @abstractmethod - def init(self, sid, time_resolution, *args): - """ Initialize the simulator with the ID `sid` and pass the `time_resolution` and additional parameters sent by mosaik. """ - raise NotImplementedError - - @abstractmethod - def create(self, num, model, *args): - """ Create `num` instances of `model` using the provided additional arguments """ - raise NotImplementedError - - @abstractmethod - def step(self, time, inputs, max_advance): - """ - Perform the next simulation step from time `time` using input values - from `inputs` and return the new simulation time (the time at which - `step()` should be called again) - """ - raise NotImplementedError - - @abstractmethod - def get_data(self, outputs): - """ Return the data for the requested outputs """ - raise NotImplementedError \ No newline at end of file diff --git a/src/illuminator/core/model_constructor.py b/src/illuminator/core/model_constructor.py new file mode 100644 index 0000000..d3ba0c3 --- /dev/null +++ b/src/illuminator/core/model_constructor.py @@ -0,0 +1,137 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from mosaik_api import Simulator +from typing import List, Dict, Optional, Any + +class SimulatorType(Enum): + TIME_BASED = 'time-based' + EVENT_BASED = 'event-based' + HYBRID = 'hybrid' + + +@dataclass +class ModelConstructor(ABC): + """ + A abstract class for adding models to the Illuminator. + ... + + Parameters + ---------- + + model_parameters: dict + Properties of the object being modeled, e.g. material the object is made of + inputs: dict + One or more name:data_type pairs to be regarded as inputs to the model. + Inputs allow users to connect a model to other models. + outputs: dict + One or more name:data_type pairs to be regarded as outputs of the model. + Outputs allow users to connect a model to other models. + states: dict + One or more model_parameters, inputs or outputs and their initial value that are of + interest for monitoring or logging during a simulation. + triggers: list + A flag [input's or output's name] that causes the compute_step method to be called. + Only relevant when a model is part of an event-based or hybrid simulator. + public: bool + ??? + + Returns + ------- + None + + Raises + ------ + NotImplementedError + If the compute_step method is not implemented + + """ + + # When useing dataclasses, these become instance attributes + # they are part of the __init__ method + model_parameters = {} # properties of the object being modeled, e.g. material the object is made of + inputs= {} # zero or many + outputs= {} # zero or many + states= {} # zero or many + + # TODO: add validation for triggers, it should be a list of strings, and trigger must be in inputs or outputs(?) + triggers= [] + + # TODO: add validation for time_step, it should be a positive integer and not zero + time_step = 1 + time = None + public = True # TODO: when using type validation, attributes cannot be changed by the concrete class. We need + # to find a way to validate this if that is a limiation of dataclasses + + def __post_init__(self): + """ + This method will be called after the instance has been created + """ + self.step_function = self.compute_step + + def get_meta(self) -> Dict: + """ + Returns the model meta data in MoSaik format + """ + # TODO: this should be to call the mosaik_api method to create a simulator + + @abstractmethod + def compute_step(self, *args) -> Any: + """ + Defines the conputations that need to be performed in each + simulation step. + Computations must be defined in terms of inputs, outputs and states. + """ + + # TODO: valid arguments are inputs, outputs, states, + # that are defined by the concrete class. We need to write code + # to validate this. + + raise NotImplementedError + + +if __name__ == "__main__": + + # Example of adding a simple battery model + + class Battery(ModelConstructor): + """ + A simple battery model + """ + + model_parameters = { + 'capacity': 1000, + 'voltage': 12 + } + + inputs = { + 'incoming_power': float + } + + outputs = { + 'outgoing_power': float + } + + time_step = 1 + time = 100 + + states = { + 'current_charge': 10 + } + + def compute_step(self, current_charge, incoming_power, outgoing_power) -> None: + """ + Updates the current capacity of the battery + """ + # Not a sensible or meaningful example + self.states['current_charge'] = current_charge + (incoming_power - outgoing_power)/100 + + return self.time + self.time_step + + + battery = Battery() + + print(battery.model_parameters, battery.triggers) + print(battery.time_step, battery.time) + + print(battery.step_function(10, 10, 10)) \ No newline at end of file diff --git a/tests/core/test_model_constructor.py b/tests/core/test_model_constructor.py new file mode 100644 index 0000000..b043f6e --- /dev/null +++ b/tests/core/test_model_constructor.py @@ -0,0 +1,18 @@ +""" +Unit test for the construct_model module +""" + +from illuminator.core.model_constructor import ModelConstructor + + +class TestConstructModel: + """ + Test for the abstract class + """ + + def test_abstract_class(self): + """ + Test that the abstract class cannot be instantiated + """ + # TODO: Implement this test + pass From 7fccc98ff4e10c8c8d3e45af913793d9311f6ed8 Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Wed, 25 Sep 2024 13:14:27 +0200 Subject: [PATCH 3/9] add todos --- src/illuminator/core/model_constructor.py | 24 ++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/illuminator/core/model_constructor.py b/src/illuminator/core/model_constructor.py index d3ba0c3..69e32ad 100644 --- a/src/illuminator/core/model_constructor.py +++ b/src/illuminator/core/model_constructor.py @@ -10,6 +10,11 @@ class SimulatorType(Enum): HYBRID = 'hybrid' + + + + + @dataclass class ModelConstructor(ABC): """ @@ -75,6 +80,15 @@ def get_meta(self) -> Dict: """ # TODO: this should be to call the mosaik_api method to create a simulator + # TODO: this should be part of a test when registering a model in the library + def validate_args(self, *args) -> None: + """ + Validates the arguments passed to the compute_step method + """ + for arg in args: + if arg not in self.inputs or arg not in self.outputs or arg not in self.states: + raise ValueError(f"{arg} is not a valid argument") + @abstractmethod def compute_step(self, *args) -> Any: """ @@ -83,6 +97,8 @@ def compute_step(self, *args) -> Any: Computations must be defined in terms of inputs, outputs and states. """ + + # TODO: valid arguments are inputs, outputs, states, # that are defined by the concrete class. We need to write code # to validate this. @@ -90,6 +106,8 @@ def compute_step(self, *args) -> Any: raise NotImplementedError + + if __name__ == "__main__": # Example of adding a simple battery model @@ -119,11 +137,11 @@ class Battery(ModelConstructor): 'current_charge': 10 } - def compute_step(self, current_charge, incoming_power, outgoing_power) -> None: + def compute_step(self, current_charge, incoming_power, outgoing_power, voltage) -> None: """ Updates the current capacity of the battery """ - # Not a sensible or meaningful example + self.states['current_charge'] = current_charge + (incoming_power - outgoing_power)/100 return self.time + self.time_step @@ -134,4 +152,4 @@ def compute_step(self, current_charge, incoming_power, outgoing_power) -> None: print(battery.model_parameters, battery.triggers) print(battery.time_step, battery.time) - print(battery.step_function(10, 10, 10)) \ No newline at end of file + print(battery.compute_step(10, 10, 10, 5)) \ No newline at end of file From 900d3f3ca248e4d37719eefaa59628ce6c0289c1 Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Fri, 27 Sep 2024 10:53:32 +0200 Subject: [PATCH 4/9] add scratch and test files --- src/illuminator/core/mosaik_tutorial.py | 109 ++++++++++++++++++++++++ src/illuminator/core/scratch.py | 27 ++++++ src/illuminator/models/test_model.py | 97 +++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 src/illuminator/core/mosaik_tutorial.py create mode 100644 src/illuminator/core/scratch.py create mode 100644 src/illuminator/models/test_model.py diff --git a/src/illuminator/core/mosaik_tutorial.py b/src/illuminator/core/mosaik_tutorial.py new file mode 100644 index 0000000..ac4df3f --- /dev/null +++ b/src/illuminator/core/mosaik_tutorial.py @@ -0,0 +1,109 @@ + +""" +From the mosiak tutorial +""" +from typing import Any + + +# a new model most be a class + +############################################### +class Model: + + + def __init__(self, init_val=0): # with any mumbers of arguments (considered as model_parameters) + + self.val = init_val + self.delta = 1 + + + def step(self): + + self.val = self.val + self.delta + + return self.val + + +############################################### + + + +# Create and configure a simulator for the model + +import mosaik_api_v3 + +META = { +'type': 'hybrid', + 'models': { + 'ExampleModel': { # this name does not have to match the class name + 'public': True, + 'params': ['init_val'], + 'attrs': ['delta', 'val'], + 'trigger': ['delta'], + }, + }, +} + +###################################################### + + +# create a simulator with configuration META, and the Model class +# it must be a subclass of mosaik_api_v3.Simulator + +class Simulator(mosaik_api_v3.Simulator): + def __init__(self): + super().__init__(META) + self.eid_prefix = 'Model_' + self.entities = {} # Maps EIDs to model instances + self.time = 0 + + def init(self) -> dict: # this is used for initialization tasks, if any. It must return 'self.meta' + + return self.meta + + def create(self, num: int, model: str, init_val: Any) -> list: # use to create a number of instances of the same MODEL. If the model has model parameters, their initial values might be passed here, + # model parameters must appead in META + next_eid = len(self.entities) + entities = [] # NOTE: this is not the same as self.entities + + for i in range(next_eid, next_eid + num): + model_instance = Model(init_val) + eid = '%s%d' % (self.eid_prefix, i) + self.entities[eid] = model_instance + entities.append({'eid': eid, 'type': model}) # QUESTION: what is model here? + + return entities # must return a list of entities + + def step(self, time, inputs, max_advance): # max_advance is for special cases. + + # inputs: a dictionary with input values from other simulators (if there are any) + + # time (current simulation time) + #tells your simulator to perform a simulation step. + # It receives its current simulation time, a dictionary with input values from other simulators (if there are any), + # and the time until the simulator can safely advance its internal time without creating a causality error + + # inputs: a dictionary with input values from other simulators (if there are any) + # {'Model_0': { + # 'delta': {'src_id_0': 23}, + # }, + # 'Model_1': { + # 'delta': {'src_id_1': 42}, + # 'val': {'src_id_1': 20}, + # } + # } + + self.time = time + # Check for new delta and do step for each model instance: + for eid, model_instance in self.entities.items(): + if eid in inputs: # inputs are values from other simulators, must be a dictionary + attrs = inputs[eid] + for attr, values in attrs.items(): + new_delta = sum(values.values()) + model_instance.delta = new_delta # NOTE: this modifies the value in the model instance + + model_instance.step() + + return time + 1 # Step size is 1 second. This update the currrent simulation time + + diff --git a/src/illuminator/core/scratch.py b/src/illuminator/core/scratch.py new file mode 100644 index 0000000..f69d572 --- /dev/null +++ b/src/illuminator/core/scratch.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +import dataclasses + +dataclasses +class Simulation(ABC): + inputs = [] + outputs = [] + + @abstractmethod + def compute(self): + """Method to be implemented by each new simulation""" + pass + +# User must implement a new simulation by inheriting from the base class. +class NewSimulation(Simulation): + inputs = [1, 2] + outputs = [3, 4] + + def compute(self): + # Define how this particular simulation works + return self.inputs[0] + self.inputs[1] * self.outputs[0] + self.outputs[1] + +# Instantiate a new simulation +sim = NewSimulation() +print(sim.inputs) +result = sim.compute() +print(result) \ No newline at end of file diff --git a/src/illuminator/models/test_model.py b/src/illuminator/models/test_model.py new file mode 100644 index 0000000..f05df6a --- /dev/null +++ b/src/illuminator/models/test_model.py @@ -0,0 +1,97 @@ +# example_model.py +""" +This module contains a simple example model. + +""" + +import mosaik_api_v3 + +mosaik_api_v3.Simulator + +class MyModel: + """Simple model that increases its value *val* with some *delta* every + step. + + You can optionally set the initial value *init_val*. It defaults to ``0``. + + """ + def __init__(self, init_val=0): + self.val = init_val + self.delta = 1 + + def step(self): + """Perform a simulation step by adding *delta* to *val*.""" + self.val += self.delta + + +META = {'type': 'hybrid', + 'models': { + 'ExampleModel': { + 'public': True, + 'params': ['init_val'], + 'attrs': ['delta', 'val'], + 'trigger': ['delta'], + }, + }, +} + +class ExampleSim(mosaik_api_v3.Simulator): + def __init__(self): + super().__init__(META) + # ADDITIONAL METADATA + self.eid_prefix = 'Model_' + self.entities = {} # Maps EIDs to model instances/entities + self.time = 0 + + + # API CALLS + + def init(self, sid, time_resolution, eid_prefix=None): + if float(time_resolution) != 1.: + raise ValueError('ExampleSim only supports time_resolution=1., but' + ' %s was set.' % time_resolution) + if eid_prefix is not None: + self.eid_prefix = eid_prefix + return self.meta + + def create(self, num, model, init_val): + next_eid = len(self.entities) + entities = [] + + for i in range(next_eid, next_eid + num): + model_instance = MyModel(init_val) + eid = '%s%d' % (self.eid_prefix, i) + self.entities[eid] = model_instance + entities.append({'eid': eid, 'type': model}) + + return entities + + + def step(self, time, inputs, max_advance): + self.time = time + # Check for new delta and do step for each model instance: + for eid, model_instance in self.entities.items(): + if eid in inputs: + attrs = inputs[eid] + for attr, values in attrs.items(): + new_delta = sum(values.values()) + model_instance.delta = new_delta + + model_instance.step() + + return time + 1 # Step size is 1 second + + def get_data(self, outputs): + data = {} + for eid, attrs in outputs.items(): + model = self.entities[eid] + data['time'] = self.time + data[eid] = {} + for attr in attrs: + if attr not in self.meta['models']['ExampleModel']['attrs']: + raise ValueError('Unknown output attribute: %s' % attr) + + # Get model.val or model.delta: + data[eid][attr] = getattr(model, attr) + + return data From ec734f648ff8a3b983a483928a45b098489009e8 Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Fri, 27 Sep 2024 17:05:47 +0200 Subject: [PATCH 5/9] create custome schema validation for start and end times in scenario --- configs/modelling_schema.py | 39 +++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/configs/modelling_schema.py b/configs/modelling_schema.py index d4f9378..eaf4de7 100644 --- a/configs/modelling_schema.py +++ b/configs/modelling_schema.py @@ -2,30 +2,48 @@ A schema for validating a simulation configuration file in the Illuminaotr. """ +from typing import Any, Dict from schema import Schema, And, Use, Regex, Optional, SchemaError +import datetime # format: YYYY-MM-DD HH:MM:SS" valid_start_time = r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$' + +class ScenarioSchema(Schema): + """ + A schema for validating the scenario section of the simulation configuration file. + """ + def validate(self, data, _is_scenario_schema=True): # _is_scenario_schema is a flag to limit validation of the scenario schema + data = super(ScenarioSchema, self).validate(data, _is_scenario_schema=False) + + if _is_scenario_schema and data.get("start_time", None) and data.get("end_time", None): + # convert strings to datetime objects + start_time_ = datetime.datetime.strptime(data["start_time"], "%Y-%m-%d %H:%M:%S") + end_time_ = datetime.datetime.strptime(data["end_time"], "%Y-%m-%d %H:%M:%S") + if start_time_ >= end_time_: + raise SchemaError("End time cannot be less than or equal to the start time.") + return data + # Define the schema for the simulation configuration file schema = Schema( # a mapping of mappings { - "scenario": Schema( + "scenario": ScenarioSchema( { "name": And(str, len), - "scenario_data": And(str, len), - "start_time": Regex(valid_start_time, error="Invalid start time format. Must be in the format: YYYY-MM-DDTHH:MM:SS"), - "end_time": And(Use(int), lambda n: n > 0), + "start_time": Regex(valid_start_time, error="Invalid start_time format. Must be in the format: YYYY-MM-DDTHH:MM:SS"), + "end_time": Regex(valid_start_time, error="Invalid end_time format. Must be in the format: YYYY-MM-DDTHH:MM:SS"), } ), "models": Schema( # a sequence of mappings [ { "name": And(str, len), "type": And(str, len), - "inputs": And(list, lambda l: all(isinstance(x, str) for x in l)), - "outputs": And(list, lambda l: all(isinstance(x, str) for x in l)), - "parameters": And(list, lambda l: all(isinstance(x, str) for x in l)), - "states": And(list, lambda l: all(isinstance(x, str) for x in l)), + "inputs": And(dict, len, error="if 'inputs' is used, it must contain at least one key-value pair"), + "outputs": And(dict, len, error="if 'outputs' is used, it must contain at least one key-value pair"), + Optional("parameters"): And(list, lambda l: all(isinstance(x, str) for x in l)), + Optional("states"): And(list, lambda l: all(isinstance(x, str) for x in l)), + Optional("scenario_data"): And(str, len), } ] ), "connections": Schema( # a sequence of mappings @@ -51,9 +69,10 @@ def at_least_one_set(d): return any(d.get(key) for key in ["inputs", "outputs", "states"]) -def validate_monitor(data): + +def validate_monitor(data) -> bool: for item in data.get("monitor", []): print(item) if not at_least_one_set(item): - raise SchemaError("At least one of 'inputs', 'outputs', or 'states' must be set in each monitor item.") + raise SchemaError("At least one of 'inputs', 'outputs', or 'states' must be set in each monitor item.") return True From a61a9866969f30939438b0236d4d37a5f8b43ed8 Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Fri, 27 Sep 2024 18:08:16 +0200 Subject: [PATCH 6/9] rename parser.py to validate_yaml.py and simplify validation logic --- configs/{parser.py => validate_yaml.py} | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) rename configs/{parser.py => validate_yaml.py} (67%) diff --git a/configs/parser.py b/configs/validate_yaml.py similarity index 67% rename from configs/parser.py rename to configs/validate_yaml.py index 83627f5..2603b37 100644 --- a/configs/parser.py +++ b/configs/validate_yaml.py @@ -1,18 +1,16 @@ from ruamel.yaml import YAML import sys import json -from modelling_schema import schema, validate_monitor +from modelling_schema import schema + +# this if a yaml file complies with the modelling schema _file = open('./configs/modelling-example.yaml', 'r') yaml = YAML(typ='safe') data = yaml.load(_file) -# yaml.dump(data, sys.stdout) - val = schema.validate(data) -validate_monitor(data) -print(val) json_data = json.dumps(data, indent=4) print(json_data) From effb352c0272e457ddba5d8a70455e497e703cda Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Fri, 27 Sep 2024 18:08:23 +0200 Subject: [PATCH 7/9] refactor modelling_schema.py to improve validation logic and error handling --- configs/modelling_schema.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/configs/modelling_schema.py b/configs/modelling_schema.py index eaf4de7..86e7444 100644 --- a/configs/modelling_schema.py +++ b/configs/modelling_schema.py @@ -41,38 +41,26 @@ def validate(self, data, _is_scenario_schema=True): # _is_scenario_schema is a f "type": And(str, len), "inputs": And(dict, len, error="if 'inputs' is used, it must contain at least one key-value pair"), "outputs": And(dict, len, error="if 'outputs' is used, it must contain at least one key-value pair"), - Optional("parameters"): And(list, lambda l: all(isinstance(x, str) for x in l)), - Optional("states"): And(list, lambda l: all(isinstance(x, str) for x in l)), - Optional("scenario_data"): And(str, len), + Optional("parameters"): And(dict, len, error="if 'parameters' is used, it must contain at least one key-value pair"), + Optional("states"): And(dict, len, error="if 'states' is used, it must contain at least one key-value pair"), + Optional("scenario_data"): And(str, len, error="you must provide a scenario data file if using 'scenario_data'"), } ] ), "connections": Schema( # a sequence of mappings [{ "from": And(str, len), "to": And(str, len), - "data": And(list, lambda l: all(And(isinstance(x, tuple), len(x) == 2 ) for x in l)), }] ), - "monitor": Schema( # a sequence of mappings - [{ - "name": And(str, len), - Optional("inputs"): And(list, lambda l: all(And(isinstance(x, str)) for x in l)), - Optional("outputs"): And(list, lambda l: all(And(isinstance(x, str)) for x in l)), - Optional("states"): And(list, lambda l: all(And(isinstance(x, str)) for x in l)), - }] - ) + "monitor": And(list, len, error="you must provide an item to monitor"), } ) -# validades monitor contains at least one set of inputs, outputs, or states -def at_least_one_set(d): - return any(d.get(key) for key in ["inputs", "outputs", "states"]) +# TODO: Write a more rubust validator for the monitor section +# Any input, output, or state declared in the monitor section must be declared in the models section +# TODO: Write a more rubust validator for connections section +# Any input, output, or state declared in the connection section must be declared in the models section -def validate_monitor(data) -> bool: - for item in data.get("monitor", []): - print(item) - if not at_least_one_set(item): - raise SchemaError("At least one of 'inputs', 'outputs', or 'states' must be set in each monitor item.") - return True +# TODO: Write a validator for the scenario data file. It should check that the fiel exists From c9480e7ab2b4bf03a7fa8248c6a717557146ac6b Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Fri, 27 Sep 2024 18:09:51 +0200 Subject: [PATCH 8/9] update example yaml with @Mano-Rom ideas. --- configs/modelling-example.yaml | 86 +++++++++++++++++----------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/configs/modelling-example.yaml b/configs/modelling-example.yaml index 3b1805d..44411e7 100644 --- a/configs/modelling-example.yaml +++ b/configs/modelling-example.yaml @@ -1,55 +1,53 @@ scenario: name: "ExampleScenario" # in mosaik so called world - scenario_data: 'path/to/file' #path to the scenario data file () start_time: '2012-01-02 00:00:00' # ISO 8601 start time of the simulation - end_time: 10 # duration in seconds -models: # list of model for the energy network + end_time: '2012-01-02 00:00:10' # duration in seconds +models: # list of models for the energy network - name: Battery1 # name for the model (must be unique) type: BatteryModel # name of the model registered in the Illuminator - inputs: [input1=0] - outputs: [output1=0, output2=None] - parameters: [charge_power_max=100, discharge_power_max=200, soc_min=0.1, soc_max=0.9, capacity=1000] - states: [initial_soc=0.5] + inputs: + input1: 0 # input-name: initial value, default value will be used if not defined + outputs: + output1: 0 + output2: null + parameters: + charge_power_max: 100 + discharge_power_max: 200 + soc_min: 0.1 + soc_max: 0.9 + capacity: 1000 + states: + initial_soc: 0.5 + scenario_data: 'path/to/file' #path to the scenario data file for the model. This should be optional - name: Battery2 type: BatteryModel # models can reuse the same type - inputs: [input1=0] - outputs: [output1=0, output2=None] - parameters: [charge_power_max=100, discharge_power_max=200, soc_min=0.1, soc_max=0.9, capacity=5000] - states: [initial_soc=0.5] + inputs: + input1: 0 # input-name: initial value, default value will be used if not defined + outputs: + output1: 0 + output2: null + parameters: + charge_power_max: 100 + states: + soc: 0.5 # initial value for the state, optional - name: PV1 type: PVModel - inputs: [input3=0, input4=0] - outputs: [output4=0, output5=None] - parameters: [charge_power_max=100, discharge_power_max=200, soc_min=0.1, soc_max=0.9, capacity=1000] - states: [initial_soc=0.5] + inputs: + input1: 0 # input-name: initial value, default value will be used if not defined + input2: 0 + outputs: + output1: 0 + parameters: + power_max: 100 + states: + initial_soc: 0.5 connections: -- from: Battery1 # start model - to: PV1 # end model - data: !!pairs # a list values to be exchanged between the model. 'pairs' is a YAML keyword - - output1: input3 # output of 'from model': input of 'to model' - - output2: input4 -- from: Battery2 - to: PV1 - data: !!pairs # using the generic 'data' keyword to define what it is exchanged between the nodes - - output1: input3 - - output2: input4 +- from: Battery1.output2 # start model, pattern: model_name.output_name/input_name + to: PV1.input1 # end model +- from: Battery2.output + to: PV1.input2 monitor: # a list of models, its inputs, output and states to be monitored and logged -- name: Battery1 # name of the model - inputs: [input1] - outputs: [output1, output2] # list of input, output, state names to be monitored - states: [initial_soc] -- name: Battery2 - # not all inputs, outputs and states are required, but at least one must be defined - outputs: [output1, output2] -- name: PV1 - outputs: [output4, output5] - - - - - - - - - - +- Battery1.input1 # pattern model_name.state_name +- Battery2.output1 # no duplicates allowed +- PV1.soc # pattern model_name.state_name +- PV1.output1 From c6ecffaad04f58dd2c61f485dbe576e20e65018d Mon Sep 17 00:00:00 2001 From: Manuel Garcia Date: Fri, 27 Sep 2024 18:17:47 +0200 Subject: [PATCH 9/9] Add triggers to the schema --- configs/modelling-example.yaml | 3 +++ configs/modelling_schema.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/configs/modelling-example.yaml b/configs/modelling-example.yaml index 44411e7..4a123b6 100644 --- a/configs/modelling-example.yaml +++ b/configs/modelling-example.yaml @@ -18,6 +18,9 @@ models: # list of models for the energy network capacity: 1000 states: initial_soc: 0.5 + triggers: # list of triggers for the of another model??. It must be an input, output or state of the model + - capacity + - soc_min scenario_data: 'path/to/file' #path to the scenario data file for the model. This should be optional - name: Battery2 type: BatteryModel # models can reuse the same type diff --git a/configs/modelling_schema.py b/configs/modelling_schema.py index 86e7444..adfd6bf 100644 --- a/configs/modelling_schema.py +++ b/configs/modelling_schema.py @@ -43,6 +43,7 @@ def validate(self, data, _is_scenario_schema=True): # _is_scenario_schema is a f "outputs": And(dict, len, error="if 'outputs' is used, it must contain at least one key-value pair"), Optional("parameters"): And(dict, len, error="if 'parameters' is used, it must contain at least one key-value pair"), Optional("states"): And(dict, len, error="if 'states' is used, it must contain at least one key-value pair"), + Optional("triggers"): And(list, len, error="if 'trigger' is used, it must contain at least one key-value pair"), Optional("scenario_data"): And(str, len, error="you must provide a scenario data file if using 'scenario_data'"), } ] ), @@ -64,3 +65,4 @@ def validate(self, data, _is_scenario_schema=True): # _is_scenario_schema is a f # Any input, output, or state declared in the connection section must be declared in the models section # TODO: Write a validator for the scenario data file. It should check that the fiel exists +# TODO: write a validator for triggers. It should check that the trigger is a valid input, output, or state.