diff --git a/.gitignore b/.gitignore index 3f8454a..12a6b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ dmypy.json # Log File *.log +*.csv diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8352304 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "tests.test_devstone", + // "env": {"PYTHONPATH": "${workspaceFolder}/libs/"} + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bfaa2cd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "python.autoComplete.extraPaths": ["${workspaceFolder}/xdevs"], + "python.testing.cwd": "${workspaceFolder/xdevs}", + "python.analysis.include": [ + "${workspaceFolder}/xdevs" + ], + "python.analysis.extraPaths": [ + "${workspaceFolder}" + ], +} \ No newline at end of file diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..df8a95e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "xdevs" +version = "3.0.0" +requires-python = ">=3.8" +authors = [ + {name = "Román Cárdenas"}, + {name = "Óscar Fernández Sebastián"}, + {name = "Kevin Henares"}, + {name = "José L. Risco-Martín"}, +] +maintainers = [ + {name = "Román Cárdenas", email = "r.cardenas@upm.es"}, +] +description = "xDEVS M&S framework" +readme = "README.md" +license = {file = "LICENSE.txt"} +keywords = ["DEVS", "modeling", "simulation"] + +[project.optional-dependencies] +sql = ["sqlalchemy"] +elasticsearch = ["elasticsearch"] +mqtt = ["paho-mqtt"] + +[project.urls] +Homepage = "https://github.com/iscar-ucm/xdevs" +Documentation = "https://github.com/iscar-ucm/xdevs" +Repository = "https://github.com/iscar-ucm/xdevs.py.git" +"Bug Tracker" = "https://github.com/iscar-ucm/xdevs.py/issues" +Changelog = "https://github.com/iscar-ucm/xdevs.py/blob/main/CHANGELOG.md" + +[project.entry-points."xdevs.transducers"] +csv = "xdevs.plugins.transducers.csv:CSVTransducer" +sql = "xdevs.plugins.transducers.sql:SQLTransducer" +elasticsearch = "xdevs.plugins.transducers.elasticsearch:ElasticsearchTransducer" + +[project.entry-points."xdevs.input_handlers"] +function = "xdevs.plugins.input_handlers.function:CallableFunction" +csv = "xdevs.plugins.input_handlers.csv:CSVInputHandler" +tcp = "xdevs.plugins.input_handlers.tcp:TCPInputHandler" +mqtt = "xdevs.plugins.input_handlers.mqtt:MQTTInputHandler" + +[project.entry-points."xdevs.output_handlers"] +csv = "xdevs.plugins.output_handlers.csv:CSVOutputHandler" +tcp = "xdevs.plugins.output_handlers.tcp:TCPOutputHandler" +mqtt = "xdevs.plugins.output_handlers.mqtt:MQTTOutputHandler" + +[project.entry-points."xdevs.components"] +generator = "xdevs.examples.gpt.gpt:Generator" +transducer = "xdevs.examples.gpt.gpt:Transducer" +processor = "xdevs.examples.gpt.gpt:Processor" +gpt = "xdevs.examples.gpt.gpt:Gpt" +ef = "xdevs.examples.gpt.efp:Ef" +efp = "xdevs.examples.gpt.efp:Efp" + +[project.entry-points."xdevs.wrappers"] +pypdevs = "xdevs.plugins.wrappers.pypdevs:PyPDEVSWrapper" + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.packages.find] +include = ["xdevs*"] +exclude = ["xdevs.tests*"] diff --git a/setup.py b/setup.py deleted file mode 100644 index b226a2e..0000000 --- a/setup.py +++ /dev/null @@ -1,47 +0,0 @@ -from setuptools import setup, find_packages - - -setup( - name='xdevs', - version='2.2.2', - description='xDEVS M&S framework', - url='https://github.com/iscar-ucm/xdevs.py', - author='Román Cárdenas, Kevin Henares', - author_email='r.cardenas@upm.es, khenares@ucm.es', - packages=find_packages(exclude=['xdevs.tests']), - entry_points={ - 'xdevs.plugins.transducers': [ - 'csv = xdevs.plugins.transducers.csv_transducer:CSVTransducer', - 'sql = xdevs.plugins.transducers.sql_transducer:SQLTransducer', - 'elasticsearch = xdevs.plugins.transducers.elasticsearch_transducer:ElasticsearchTransducer', - ], - 'xdevs.plugins.input_handlers': [ - 'function = xdevs.plugins.input_handlers.callable_function:CallableFunction', - 'csv_handler = xdevs.plugins.input_handlers.csv_input_handler:CSVInputHandler', - 'tcp_handler = xdevs.plugins.input_handlers.tcp_input_handler:TCPInputHandler', - 'mqtt_handler = xdevs.plugins.input_handlers.mqtt_input_handler:MQTTInputHandler', - - ], - 'xdevs.plugins.output_handlers': [ - 'csv_out_handler = xdevs.plugins.output_handlers.csv_output_handler:CSVOutputHandler', - 'tcp_out_handler = xdevs.plugins.output_handlers.tcp_output_handler:TCPOutputHandler', - 'mqtt_handler = xdevs.plugins.output_handlers.mqtt_output_handler:MQTTOutputHandler', - ], - 'xdevs.plugins.components': [ - 'generator = xdevs.examples.basic.basic:Generator', - 'transducer = xdevs.examples.basic.basic:Transducer', - 'processor = xdevs.examples.basic.basic:Processor', - 'gpt = xdevs.examples.basic.basic:Gpt', - 'ef = xdevs.examples.basic.efp_model:EF', - 'efp = xdevs.examples.basic.efp_model:EFP' - ], - 'xdevs.plugins.wrappers': [ - 'pypdevs = xdevs.plugins.wrappers.pypdevs:PyPDEVSWrapper' - ]}, - extras_require={ - 'sql': ['sqlalchemy==1.3.22'], - 'elasticsearch': ['elasticsearch==7.10.1'], - 'mqtt': ['paho-mqtt==1.5.1'], - }, - zip_safe=False -) diff --git a/xdevs/__init__.py b/xdevs/__init__.py index 77136ad..baf4dd9 100644 --- a/xdevs/__init__.py +++ b/xdevs/__init__.py @@ -1,18 +1,23 @@ -import math +import functools import logging +import math import sys +from typing import TypeVar +import warnings + +T = TypeVar('T') -INFINITY = math.inf -PHASE_PASSIVE = "passive" -PHASE_ACTIVE = "active" +INFINITY: float = math.inf +PHASE_PASSIVE: str = "passive" +PHASE_ACTIVE: str = "active" -DEBUG_LEVEL = None -loggers = dict() +DEBUG_LEVEL: int | str | None = None +LOGGERS: dict[str, logging.Logger] = dict() -def get_logger(name, dl=None): - if name in loggers: - return loggers[name] +def get_logger(name: str, dl: int | str = None): + if name in LOGGERS: + return LOGGERS[name] else: logger = logging.getLogger(name) @@ -24,5 +29,13 @@ def get_logger(name, dl=None): else: logger.disabled = True - loggers[name] = logger + LOGGERS[name] = logger return logger + + +def deprecated(func): + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.warn(f"Call to deprecated function {func.__name__}.", category=DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + return new_func diff --git a/xdevs/abc/__init__.py b/xdevs/abc/__init__.py new file mode 100644 index 0000000..ce2b8cf --- /dev/null +++ b/xdevs/abc/__init__.py @@ -0,0 +1,2 @@ +from .handler import InputHandler, OutputHandler +from .transducer import Transducer diff --git a/xdevs/abc/handler.py b/xdevs/abc/handler.py new file mode 100644 index 0000000..ed989bf --- /dev/null +++ b/xdevs/abc/handler.py @@ -0,0 +1,138 @@ +import queue +import sys +from abc import ABC, abstractmethod +from typing import Callable, Any + + +class Connector: + def __init__(self, conections: dict[str, str]): + """ + Función para conectar de forma correcta los puertos (que usen protocolo MQTT) + + :param conections: dict[key: str, value: str]. Donde la key es el puerto de al que me quiero conectar y el + value es el puerto de mi acoplado. + """ + self.connections: dict[str, str] = conections + + def input_handler(self, port: str): + if self.connections is not None: + value = self.connections.get(port) + if value is not None: + return value + return port + + +class InputHandler(ABC): + def __init__(self, *args, **kwargs): + """ + Handler interface for injecting external events to the system. + + :param queue: used to collect and inject all external events joining the system. + :param Callable[[Any], tuple[str, str]] event_parser: event parser function. It transforms incoming events + into tuples (port, message). Note that both are represented as strings. Messages need further parsing. + :param dict[str, Callable[[str], Any]] msg_parsers: message parsers. Keys are port names, and values are + functions that take a string and returns an object of the corresponding port type. If a parser is not + defined, the input handler assumes that the port type is str and forward the message as is. By default, all + the ports are assumed to accept str objects. + """ + self.queue = kwargs.get('queue') + if self.queue is None: + raise ValueError('queue is mandatory') + self.event_parser: Callable[[Any], tuple[str, str]] | None = kwargs.get('event_parser') + self.msg_parsers: dict[str, Callable[[str], Any]] = kwargs.get('msg_parsers', dict()) + + self.connections: dict[str, str] = kwargs.get('connections', dict()) + self.connector = Connector(conections=self.connections) + + def initialize(self): + """Performs any task before calling the run method. It is implementation-specific. By default, it is empty.""" + pass + + def exit(self): + """Performs any task after the run method. It is implementation-specific. By default, it is empty.""" + pass + + @abstractmethod + def run(self): + """Execution of the input handler. It is implementation-specific""" + pass + + def push_event(self, event: Any): + """Parses event as tuple port-message and pushes it to the queue.""" + try: + port, msg = self.event_parser(event) + # AQUI IRIA EL CONECTOR MQTT; para corregir el puerto en cuestion + port = self.connector.input_handler(port) + except Exception as e: + # if an exception is triggered while parsing the event, we ignore it + print(f'error parsing input event ("{event}"): {e}. Event will be ignored', file=sys.stderr) + return + self.push_msg(port, msg) + + def push_msg(self, port: str, msg: str): + """Parses the message as the proper object and pushes it to the queue.""" + try: + # if parser is not defined, we forward the message as is (i.e., in string format) + msg = self.msg_parsers.get(port, lambda x: x)(msg) + except Exception as e: + # if an exception is triggered while parsing the message, we ignore it + print(f'error parsing input msg ("{msg}") in port {port}: {e}. Message will be ignored', file=sys.stderr) + return + self.queue.put((port, msg)) + + +class OutputHandler(ABC): + def __init__(self, *args, **kwargs): + """ + Handler interface for ejecting internal events from the system. + + :param queue.SimpleQueue() queue: is the queue where all the desired events to be ejected are put. + :param Callable[[str, str], Any] event_parser: event parser function. It transforms incoming tuples + (port, message) into events. Note that both are represented as strings. + :param dict[str, Callable[[Any], str]] msg_parser: message parsers. Keys are port names, and values are + functions that take a string and returns an object of the corresponding port type. If a parser is not + defined, the output handler assumes that the port type is str and forward the message as is. By default, all + the ports are assumed to accept str objects. + + TODO documentation + """ + self.queue = queue.SimpleQueue() + self.event_parser: Callable[[str, str], Any] | None = kwargs.get('event_parser') + self.msg_parsers: dict[str, Callable[[Any], str]] = kwargs.get('msg_parsers', dict()) + + def initialize(self): + """Performs any task before calling the run method. It is implementation-specific. By default, it is empty.""" + pass + + def exit(self): + """Performs any task before calling the run method. It is implementation-specific. By default, it is empty.""" + pass + + @abstractmethod + def run(self): + """Execution of the output handler. It is implementation-specific""" + pass + + def pop_event(self) -> Any: + """Waits until it receives an outgoing event and parses it with the desired format.""" + while True: + port, msg = self.pop_msg() + # print(f'POP_EVENT: recibo port = {port} y msg = {msg}') + try: + event = self.event_parser(port, msg) + except Exception as e: + print(f'error parsing output event ("{port}","{msg}"): {e}. Event will be ignored', file=sys.stderr) + continue + return event + + def pop_msg(self) -> tuple[str, str]: + """Waits until it receives an outgoing message and returns the port and message in string format.""" + while True: + port, msg = self.queue.get() + # print(f'POP_MSG: recibo port = {port} y msg = {msg}') + try: + msg = self.msg_parsers.get(port, lambda x: str(x))(msg) + except Exception as e: + print(f'error parsing output msg ("{msg}"): {e}. Message will be ignored', file=sys.stderr) + continue + return port, msg diff --git a/xdevs/transducers.py b/xdevs/abc/transducer.py similarity index 81% rename from xdevs/transducers.py rename to xdevs/abc/transducer.py index 6325edd..c88d785 100644 --- a/xdevs/transducers.py +++ b/xdevs/abc/transducer.py @@ -1,18 +1,14 @@ -from __future__ import annotations import inspect import itertools import logging -import pkg_resources import re from abc import ABC, abstractmethod from math import isinf, isnan -from typing import Any, Callable, ClassVar, Type, TypeVar, Iterable +from typing import Any, Callable, Type, Iterable +from xdevs import T from xdevs.models import Atomic, Component, Coupled, Port -T = TypeVar('T') - - class Transducible(ABC): @staticmethod @abstractmethod @@ -25,10 +21,10 @@ class Transducer(ABC): state_mapper: dict[str, tuple[Type[T], Callable[[Atomic], T]]] event_mapper: dict[str, tuple[Type[T], Callable[[Any], T]]] - def __init__(self, **kwargs): + def __init__(self, *args, **kwargs): """ Transducer for the xDEVS M&S tool. - :param str transducer_id: ID of the transducer. + :param str transducer_id: ID of the transducer. This parameter is mandatory. :param str sim_time_id: ID of the data field containing the simulation time. By default, it is "sim_time". :param bool include_names: when True, the logs include DEVS port and component names. By default, it is True. :param str model_name_id: ID of the data field containing the DEVS model name. By default, it is "model_name". @@ -36,17 +32,14 @@ def __init__(self, **kwargs): :param bool exhaustive: determines if the output contains the state of the target components for each iteration (True) or only includes the change states (False). """ - self.active: bool = True - self.transducer_id: str = kwargs.get('transducer_id', None) - if self.transducer_id is None: - raise AttributeError("You must specify a transducer ID.") - + self.transducer_id: str = kwargs['transducer_id'] self.sim_time_id: str = kwargs.get('sim_time_id', 'sim_time') self.include_names: bool = kwargs.get('include_names', True) self.model_name_id: str = kwargs.get('model_name_id', 'model_name') self.port_name_id: str = kwargs.get('port_name_id', 'port_name') - self.exhaustive: bool = kwargs.get('exhaustive', False) + + self.active: bool = True self.target_components: set[Atomic] = set() self.target_ports: set[Port] = set() self.imminent_components: list[Atomic] | None = None if self.exhaustive else [] @@ -63,30 +56,32 @@ def __init__(self, **kwargs): self._remove_special_numbers: bool = False def activate_remove_special_numbers(self): - logging.warning('Transducer {} does not support special number values (e.g., infinity). ' - 'It will automatically substitute these values by None'.format(self.transducer_id)) + logging.warning(f'Transducer {self.transducer_id} does not support special number values (e.g., infinity). ' + 'It will automatically substitute these values with None') self._remove_special_numbers = True - def add_target_component(self, component: Atomic or Coupled, *filters): + def add_target_component(self, component: Component, *filters): components = self._iterate_components(component) self.target_components |= self._apply_filters(filters, components) - def _iterate_components(self, root_comp, include_coupled=False): + def _iterate_components(self, root_comp: Component, include_coupled: bool = False): if isinstance(root_comp, Atomic): yield root_comp - else: # Coupled + elif isinstance(root_comp, Coupled): # Coupled if include_coupled: yield root_comp for child_comp in root_comp.components: yield from self._iterate_components(child_comp, include_coupled=include_coupled) + else: + raise ValueError(f'Component {root_comp.name} is not an Atomic nor a Coupled') def add_target_port(self, port: Port): parent: Component | None = port.parent if parent is None: - raise ValueError('Port {} does not have a parent component'.format(port.name)) + raise ValueError(f'Port {port.name} does not have a parent component') self.target_ports.add(port) - def add_target_ports_by_component(self, component, component_filters=None, port_filters=None): + def add_target_ports_by_component(self, component: Component, component_filters=None, port_filters=None): components = self._iterate_components(component, include_coupled=True) filtered_components = Transducer._apply_filters(component_filters, components) for comp in filtered_components: @@ -94,11 +89,11 @@ def add_target_ports_by_component(self, component, component_filters=None, port_ filtered_comp_ports = Transducer._apply_filters(port_filters, comp_ports) self.target_ports |= filtered_comp_ports - def add_imminent_model(self, component): + def add_imminent_model(self, component: Atomic): if not self.exhaustive and self.active: self.imminent_components.append(component) - def add_imminent_port(self, port): + def add_imminent_port(self, port: Port): if not self.exhaustive and self.active: self.imminent_ports.append(port) @@ -114,9 +109,9 @@ def add_event_field(self, field_name: str, field_type: Type[T], field_getter: Ca :raise KeyError: if field name is already in event mapper. """ if field_name == self.sim_time_id: - raise KeyError('Field name {} is reserved for the simulation time field'.format(field_name)) + raise KeyError(f'Field name {field_name} is reserved for the simulation time field') elif self.include_names and field_name in (self.model_name_id, self.port_name_id): - raise KeyError('Field name {} is reserved for DEVS element name field'.format(field_name)) + raise KeyError(f'Field name {field_name} is reserved for DEVS element name field') self._add_field(self.event_mapper, field_name, field_type, field_getter) def add_state_field(self, field_name: str, field_type: Type[T], field_getter: Callable[[Atomic], T]): @@ -128,16 +123,16 @@ def add_state_field(self, field_name: str, field_type: Type[T], field_getter: Ca :raise KeyError: if field name is already in state mapper. """ if field_name == self.sim_time_id: - raise KeyError('Field name {} is reserved for the simulation time field'.format(field_name)) + raise KeyError(f'Field name {field_name} is reserved for the simulation time field') elif self.include_names and field_name == self.model_name_id: - raise KeyError('Field name {} is reserved for DEVS component name field'.format(field_name)) + raise KeyError(f'Field name {field_name} is reserved for DEVS component name field') self._add_field(self.state_mapper, field_name, field_type, field_getter) @staticmethod def _add_field(field_mapper: dict[str, tuple[Type[T], Callable[[Any], T]]], field_name: str, field_type: Type[T], field_getter: Callable[[Any], T]): if field_name in field_mapper: - raise KeyError('Field name {} is already included in field mapper'.format(field_name)) + raise KeyError(f'Field name {field_name} is already included in field mapper') field_mapper[field_name] = (field_type, field_getter) def drop_event_field(self, field_name: str): @@ -255,23 +250,5 @@ def map_extra_fields(self, target: Any, field_mapper: dict) -> dict[str, Any]: return extra_fields def _log_unknown_data(self, data_type: type, field_name: str): - logging.warning('Transducer {} does not support data type {} of field {}. ' - 'It will cast it to string'.format(self.transducer_id, data_type, field_name)) - - -class Transducers: - _plugins: ClassVar[dict[str, Type[Transducer]]] = { - ep.name: ep.load() for ep in pkg_resources.iter_entry_points('xdevs.plugins.transducers') - } - - @staticmethod - def add_plugin(name: str, plugin: Type[Transducer]): - if name in Transducers._plugins: - raise ValueError(f'xDEVS transducer plugin with name "{name}" already exists') - Transducers._plugins[name] = plugin - - @staticmethod - def create_transducer(name: str, **kwargs) -> Transducer: - if name not in Transducers._plugins: - raise ValueError('xDEVS transducer plugin with name "{name}" not found') - return Transducers._plugins[name](**kwargs) + logging.warning(f'Transducer {self.transducer_id} does not support data type {data_type} of field {field_name}. ' + 'It will cast it to string') diff --git a/xdevs/components.py b/xdevs/components.py deleted file mode 100644 index df2afea..0000000 --- a/xdevs/components.py +++ /dev/null @@ -1,147 +0,0 @@ -import pkg_resources -import json -from typing import ClassVar -from xdevs.models import Component, Port, Coupled - - -class Components: - """ - This class creates components from unique identifiers called "component_id". - - In order to identify each component_id check the "entry_point" dict in the file setup.py and look for the - list 'xdevs.plugins.components'. - """ - _plugins: ClassVar[dict[str, type[Component]]] = { - ep.name: ep.load() for ep in pkg_resources.iter_entry_points('xdevs.plugins.components') - } - - @staticmethod - def add_plugin(component_id: str, plugin: type[Component]): - if component_id in Components._plugins: - raise ValueError(f'xDEVS component plugin with name "{component_id}" already exists') - Components._plugins[component_id] = plugin - - @staticmethod - def create_component(component_id: str, *args, **kwargs) -> Component: - if component_id not in Components._plugins: - raise ValueError(f'xDEVS component plugin with name "{component_id}" not found') - return Components._plugins[component_id](*args, **kwargs) - - @staticmethod - def _nested_component(name: str, config: dict) -> Component: - if 'component_id' in config: - # Predefined component, use factory - component_id: str = config['component_id'] - args = config.get('args', []) - kwargs = config.get('kwargs', {}) - kwargs['name'] = name - return Components.create_component(component_id, *args, **kwargs) - elif 'components' in config: - # It is a coupled model - component = Coupled(name) - children: dict[str, Component] = dict() - # Create children components - for component_name, component_config in config['components'].items(): - child = Components._nested_component(component_name, component_config) - children[component_name] = child - component.add_component(child) - # Create connections - for connection in config.get('connections', []): - child_from = connection.get('componentFrom') - child_to = connection.get('componentTo') - if child_from is not None: - child_from = children[child_from] - port_from = child_from.get_out_port(connection['portFrom']) - if port_from is None: - raise Exception(f'Invalid connection in: {connection}. Reason: portFrom not found') - if child_to is not None: - # this is an IC - child_to = children[child_to] - port_to = child_to.get_in_port(connection['portTo']) - if port_to is None: - raise Exception(f'Invalid connection in: {connection}. Reason: portTo not found') - else: - # this is an EOC - port_to = child_to.get_in_port(connection['portTo']) - if port_to is None: - port_to = Port(p_type=port_from.p_type, name=connection['portTo']) - component.add_out_port(port_to) - elif child_to is not None: - # this is an EIC - child_to = children[child_to] - port_to = child_to.get_in_port(connection['portTo']) - if port_to is None: - raise Exception(f'Invalid connection in: {connection}. Reason: portTo not found') - port_from = component.get_in_port(connection['portFrom']) - if port_from is None: - port_from = Port(p_type=port_to.p_type, name=connection['portFrom']) - component.add_in_port(port_from) - else: - raise Exception( - f'Invalid connection in: {connection}. Reason: componentFrom and componentTo are None') - - component.add_coupling(port_from, port_to) - else: - raise Exception('No component found') - return component - - @staticmethod - def from_json(file_path: str): - """ - A function to parser a json file into a DEVS model - - Considering the following constrains, the json file structure is as follows: - - When adding a component if it contains the key "component_id", the component will be created using it and the - kwargs associated with it. The "component_id" value refers to the key to identify each component in the class - Components. - - When the component does not have the key "component_id" it must have the pair keys "components" and "connections". - This component will be implementing several components and their connections inside itself. - - The connections are created using four keys: - - If both componentFrom/To keys are added, the connection will be of the type IC. - - If componentFrom key is missing, the connection will be of the type EIC. - - If componentTo key is missing, the connection will be of the type EOC. - - If any portFrom/To value is missing the connections is not established. - - Structure: - - - 'MasterComponentName' (dict): The master component. - - 'components' (dict): A dictionary containing multiple components. - - 'ComponentName1' (dict): Iterative component. - - 'components' (dict): Nested components if any. - - 'connections' (list): List of connection dictionaries. - - 'componentFrom' (str): Name of the component where the connection starts. - - 'portFrom' (str): Port name from 'componentFrom'. - - 'componentTo' (str): Name of the component where the connection ends. - - 'portTo' (str): Port name from 'componentTo'. - - 'ComponentName2' (dict): Single component. - - 'component_id' (str): ID read from the factory for this component. - - 'kwargs' (dict): Keyword arguments for the component. - - 'a_parameter' (any): A parameter for the component. - - ... : Other keyword arguments if any. - - ... : Additional components if any. - - 'connections' (list): List of connection dictionaries at the top level. - - 'componentFrom' (str): Name of the component where the connection starts. - - 'portFrom' (str): Port name from 'componentFrom'. - - 'componentTo' (str): Name of the component where the connection ends. - - 'portTo' (str): Port name from 'componentTo'. - - :param file_path: Path to the JSON file - :return: a Coupled DEVS model - """ - - with open(file_path) as f: - data = json.load(f) - - name = list(data.keys())[0] # Gets the actual component name - config = data[name] # Gets the actual component config - - return Components._nested_component(name, config) - - -if __name__ == '__main__': - - C = Components.from_json('coupled_model_v2.json') - print(C) diff --git a/xdevs/coupled_model.json b/xdevs/coupled_model.json deleted file mode 100644 index 26c1bca..0000000 --- a/xdevs/coupled_model.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "EFP" : { - "components": { - - "processor": { - "component_id": "processor", - "kwargs": { - "proc_time": 10 - } - }, - - "ef": { - "component_id": "ef", - "kwargs": { - "period": 5, - "obs_time": 300 - } - - } - }, - - "connections" : [ - { - "componentFrom": "ef", - "portFrom": "p_out_ef", - "componentTo": "processor", - "portTo": "i_in" - }, - { - "componentFrom": "processor", - "portFrom": "o_out", - "componentTo": "ef", - "portTo": "p_in_ef" - } - - ] - } -} diff --git a/xdevs/coupled_model_v2.json b/xdevs/coupled_model_v2.json deleted file mode 100644 index 56ebac4..0000000 --- a/xdevs/coupled_model_v2.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "coupled/MasterModel": { - "components": { - "name/GPT": { - "components": { - - "Processor": { - "component_id": "processor", - "kwargs": { - "proc_time": 3.0 - } - }, - "Generator": { - "component_id": "generator", - "kwargs": { - "period": 1.0 - } - }, - "Transducer": { - "component_id": "transducer", - "kwargs": { - "obs_time": 100.0 - } - } - }, - - "connections": [ - { - "componentFrom": "Processor", - "portFrom": "o_out", - "componentTo": "Transducer", - "portTo": "i_solved" - }, - { - "componentFrom": "Generator", - "portFrom": "o_out", - "componentTo": "Processor", - "portTo": "i_in" - }, - { - "componentFrom": "Generator", - "portFrom": "o_out", - "componentTo": "Transducer", - "portTo": "i_arrived" - }, - { - "componentFrom": "Transducer", - "portFrom": "o_out", - "componentTo": "Generator", - "portTo": "i_stop" - }, - { - "componentFrom": null, - "portFrom": "i_in_gpt", - "componentTo": "Generator", - "portTo": "i_stop" - } - ] - }, - - "Processor_2": { - "component_id": "processor", - "kwargs": { - "proc_time": 300.0 - } - }, - - "c/gpt" : { - "component_id": "gpt", - "kwargs": { - "period": 3, - "obs_time": 100 - } - } - }, - - "connections": [ - { - "componentFrom": "Processor_2", - "portFrom": "o_out", - "componentTo": "name/GPT", - "portTo": "i_in_gpt" - } - ] - } -} \ No newline at end of file diff --git a/xdevs/examples/async_rt/basic.py b/xdevs/examples/async_rt/basic.py index 2e83b16..5d2bf1c 100644 --- a/xdevs/examples/async_rt/basic.py +++ b/xdevs/examples/async_rt/basic.py @@ -5,7 +5,7 @@ from xdevs import PHASE_ACTIVE, PHASE_PASSIVE, get_logger from xdevs.models import Atomic, Coupled, Port -from xdevs.rt_sim import RealTimeCoordinator, RealTimeManager +from xdevs.rt import RealTimeCoordinator, RealTimeManager logger = get_logger(__name__, logging.INFO) @@ -243,7 +243,7 @@ def inject_messages(q: queue.SimpleQueue): 'i_extern': lambda x: Job(x), # le digo al input handler como convertir el string a Job con una función 'tcp': lambda x: x.decode().split('.'), } - # manager.add_input_handler('csv_handler', file="prueba.csv", msg_parsers=parsers) + manager.add_input_handler('csv_handler', file="prueba.csv", msg_parsers=parsers) # manager.add_input_handler('function', function=inject_messages) # Si no quiero ir repitiendo parsers, se lo tendria que meter al manager @@ -263,7 +263,7 @@ def inject_messages(q: queue.SimpleQueue): c = RealTimeCoordinator(gpt, manager) t_ini = time.time() print(f' >>> COMENZAMOS : {t_ini}') - c.simulate(time_interv=execution_time) + c.simulate_iters(time_interv=execution_time) print(f' >>> FIN : {time.time()}') print(f' Tiempo a ejecutar (s) = {execution_time * time_scale}') print(f' Tiempo ejecutado (s) = {(time.time() - t_ini)}') diff --git a/xdevs/examples/basic/basic.py b/xdevs/examples/basic/basic.py deleted file mode 100644 index 011484c..0000000 --- a/xdevs/examples/basic/basic.py +++ /dev/null @@ -1,194 +0,0 @@ -import logging - - -from xdevs import PHASE_ACTIVE, PHASE_PASSIVE, get_logger -from xdevs.models import Atomic, Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim import RealTimeCoordinator -import time - -logger = get_logger(__name__, logging.DEBUG) - -PHASE_DONE = "done" - - -class Job: - def __init__(self, name): - self.name = name - self.time = 0 - - -class Generator(Atomic): - - def __init__(self, name, period): - super().__init__(name) - self.i_start = Port(Job, "i_start") - self.i_stop = Port(Job, "i_stop") - self.o_out = Port(Job, "o_out") - - self.add_in_port(self.i_start) - self.add_in_port(self.i_stop) - self.add_out_port(self.o_out) - - self.period = period - self.job_counter = 1 - - def initialize(self): - self.hold_in(PHASE_ACTIVE, self.period) - - def exit(self): - pass - - def deltint(self): - self.job_counter += 1 - self.hold_in(PHASE_ACTIVE, self.period) - - def deltext(self, e): - self.passivate() - - def lambdaf(self): - self.o_out.add(Job(str(self.job_counter))) - - -class Processor(Atomic): - def __init__(self, name, proc_time): - super().__init__(name) - - self.i_in = Port(Job, "i_in") - self.o_out = Port(Job, "o_out") - - self.add_in_port(self.i_in) - self.add_out_port(self.o_out) - - self.current_job = None - self.proc_time = proc_time - - def initialize(self): - self.passivate() - - def exit(self): - pass - - def deltint(self): - self.passivate() - - def deltext(self, e): - if self.phase == PHASE_PASSIVE: - self.current_job = self.i_in.get() - self.hold_in(PHASE_ACTIVE, self.proc_time) - self.continuef(e) - - def lambdaf(self): - self.o_out.add(self.current_job) - - -class Transducer(Atomic): - - def __init__(self, name, obs_time): - super().__init__(name) - - self.i_arrived = Port(Job, "i_arrived") - self.i_solved = Port(Job, "i_solved") - self.o_out = Port(Job, "o_out") - - self.add_in_port(self.i_arrived) - self.add_in_port(self.i_solved) - self.add_out_port(self.o_out) - - self.jobs_arrived = [] - self.jobs_solved = [] - - self.total_ta = 0 - self.clock = 0 - self.obs_time = obs_time - - def initialize(self): - self.hold_in(PHASE_ACTIVE, self.obs_time) - - def exit(self): - pass - - def deltint(self): - self.clock += self.sigma - - if self.phase == PHASE_ACTIVE: - if self.jobs_solved: - avg_ta = self.total_ta / len(self.jobs_solved) - throughput = len(self.jobs_solved) / self.clock if self.clock > 0 else 0 - else: - avg_ta = 0 - throughput = 0 - - logger.info("End time: %f" % self.clock) - logger.info("Jobs arrived: %d" % len(self.jobs_arrived)) - logger.info("Jobs solved: %d" % len(self.jobs_solved)) - logger.info("Average TA: %f" % avg_ta) - logger.info("Throughput: %f\n" % throughput) - - self.hold_in(PHASE_DONE, 0) - else: - self.passivate() - - def deltext(self, e): - self.clock += e - - if self.phase == PHASE_ACTIVE: - if self.i_arrived: - job = self.i_arrived.get() - logger.info("Starting job %s @ t = %d @ t = %d" % (job.name, self.clock, time.time_ns())) - job.time = self.clock - self.jobs_arrived.append(job) - - if self.i_solved: - job = self.i_solved.get() - logger.info("Job %s finished @ t = %d @ t = %d" % (job.name, self.clock, time.time())) - self.total_ta += self.clock - job.time - self.jobs_solved.append(job) - - self.continuef(e) - - def lambdaf(self): - if self.phase == PHASE_DONE: - self.o_out.add(Job("null")) - - -class Gpt(Coupled): - def __init__(self, name, period, obs_time): - super().__init__(name) - - if period < 1: - raise ValueError("period has to be greater than 0") - - if obs_time < 0: - raise ValueError("obs_time has to be greater or equal than 0") - - gen = Generator("generator", period) - proc = Processor("processor", 3 * period) - trans = Transducer("transducer", obs_time) - - self.add_component(gen) - self.add_component(proc) - self.add_component(trans) - - self.add_coupling(gen.o_out, proc.i_in) - self.add_coupling(gen.o_out, trans.i_arrived) - self.add_coupling(proc.o_out, trans.i_solved) - self.add_coupling(trans.o_out, gen.i_stop) - - -class Wrap(Coupled): - def __init__(self, name, period, obs_time): - super().__init__("wrap") - - gpt = Gpt(name, period, obs_time) - - self.add_component(gpt) - - -if __name__ == '__main__': - gpt = Gpt("gpt", 2, 100) - coord = Coordinator(gpt) - coord.initialize() - # coord.simulate() - coord.simulate() - # coord.simulate_rt(50) diff --git a/xdevs/examples/basic/basic_inter.py b/xdevs/examples/basic/basic_inter.py deleted file mode 100644 index cc40eaa..0000000 --- a/xdevs/examples/basic/basic_inter.py +++ /dev/null @@ -1,211 +0,0 @@ -from pypdevs.DEVS import AtomicDEVS - -from xdevs import PHASE_ACTIVE, PHASE_PASSIVE, get_logger -from xdevs.models import Atomic, Coupled, Port -from xdevs.plugins.wrappers.pypdevs import PyPDEVSWrapper -from xdevs.sim import Coordinator, ParallelCoordinator -from pypdevs.infinity import INFINITY -import logging - -logger = get_logger(__name__, logging.DEBUG) - -PHASE_DONE = "done" - - -class Job: - def __init__(self, name): - self.name = name - self.time = 0 - - -class Generator(Atomic): - - def __init__(self, name, period): - super().__init__(name) - self.i_start = Port(Job, "i_start") - self.i_stop = Port(Job, "i_stop") - self.o_out = Port(Job, "o_out") - - self.add_in_port(self.i_start) - self.add_in_port(self.i_stop) - self.add_out_port(self.o_out) - - self.period = period - self.job_counter = 1 - - def initialize(self): - self.hold_in(PHASE_ACTIVE, self.period) - - def exit(self): - pass - - def deltint(self): - self.job_counter += 1 - self.hold_in(PHASE_ACTIVE, self.period) - - def deltext(self, e): - self.passivate() - - def lambdaf(self): - self.o_out.add(Job(str(self.job_counter))) - - -# -# class Processor(Atomic): -# def __init__(self, name, proc_time): -# super().__init__(name) -# -# self.i_in = Port(Job, "i_in") -# self.o_out = Port(Job, "o_out") -# -# self.add_in_port(self.i_in) -# self.add_out_port(self.o_out) -# -# self.current_job = None -# self.proc_time = proc_time -# -# def initialize(self): -# self.passivate() -# -# def exit(self): -# pass -# -# def deltint(self): -# self.passivate() -# -# def deltext(self, e): -# if self.phase == PHASE_PASSIVE: -# self.current_job = self.i_in.get() -# self.hold_in(PHASE_ACTIVE, self.proc_time) -# -# def lambdaf(self): -# self.o_out.add(self.current_job) - -class Processor(AtomicDEVS): - def __init__(self, name, proc_time): - super().__init__(name) - - self.i_in = self.addInPort("i_in") - self.o_out = self.addOutPort("o_out") - - self.current_job = None - self.proc_time = proc_time - self.state = "passive" - - def intTransition(self): - return "passive" - - def extTransition(self, inputs): - if self.state == "passive": - self.current_job = inputs[self.i_in][0] - return "active" - - def timeAdvance(self): - if self.state == "active": - return self.proc_time - else: - return INFINITY - - def outputFnc(self): - return {self.o_out: [self.current_job]} - - -class Transducer(Atomic): - - def __init__(self, name, obs_time): - super().__init__(name) - - self.i_arrived = Port(Job, "i_arrived") - self.i_solved = Port(Job, "i_solved") - self.o_out = Port(Job, "o_out") - - self.add_in_port(self.i_arrived) - self.add_in_port(self.i_solved) - self.add_out_port(self.o_out) - - self.jobs_arrived = [] - self.jobs_solved = [] - - self.total_ta = 0 - self.clock = 0 - self.obs_time = obs_time - - def initialize(self): - self.hold_in(PHASE_ACTIVE, self.obs_time) - - def exit(self): - pass - - def deltint(self): - self.clock += self.sigma - - if self.phase == PHASE_ACTIVE: - if self.jobs_solved: - avg_ta = self.total_ta / len(self.jobs_solved) - throughput = len(self.jobs_solved) / self.clock if self.clock > 0 else 0 - else: - avg_ta = 0 - throughput = 0 - - logger.info("End time: %f" % self.clock) - logger.info("Jobs arrived: %d" % len(self.jobs_arrived)) - logger.info("Jobs solved: %d" % len(self.jobs_solved)) - logger.info("Average TA: %f" % avg_ta) - logger.info("Throughput: %f\n" % throughput) - - self.hold_in(PHASE_DONE, 0) - else: - self.passivate() - - def deltext(self, e): - self.clock += e - - if self.phase == PHASE_ACTIVE: - if self.i_arrived: - job = self.i_arrived.get() - logger.info("Starting job %s @ t = %d" % (job.name, self.clock)) - job.time = self.clock - self.jobs_arrived.append(job) - - if self.i_solved: - job = self.i_solved.get() - logger.info("Job %s finished @ t = %d" % (job.name, self.clock)) - self.total_ta += self.clock - job.time - self.jobs_solved.append(job) - - self.continuef(e) - - def lambdaf(self): - if self.phase == PHASE_DONE: - self.o_out.add(Job("null")) - - -class Gpt(Coupled): - def __init__(self, name, period, obs_time): - super().__init__(name) - - if period < 1: - raise ValueError("period has to be greater than 0") - - if obs_time < 0: - raise ValueError("obs_time has to be greater or equal than 0") - - gen = Generator("generator", period) - proc = PyPDEVSWrapper(Processor("processor", 3 * period)) - trans = Transducer("transducer", obs_time) - - self.add_component(gen) - self.add_component(proc) - self.add_component(trans) - - self.add_coupling(gen.o_out, proc.i_in) - self.add_coupling(gen.o_out, trans.i_arrived) - self.add_coupling(proc.o_out, trans.i_solved) - self.add_coupling(trans.o_out, gen.i_stop) - - -if __name__ == '__main__': - gpt = Gpt("gpt", 3, 1000) - coord = Coordinator(gpt, flatten=False, chain=False) - coord.initialize() - coord.simulate() diff --git a/xdevs/examples/basic/efp_model.py b/xdevs/examples/basic/efp_model.py deleted file mode 100644 index acb9dad..0000000 --- a/xdevs/examples/basic/efp_model.py +++ /dev/null @@ -1,54 +0,0 @@ -from xdevs.examples.basic.basic import Generator, Transducer, Job, Processor -from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator - - -class EF(Coupled): - def __init__(self, name, period, obs_time): - super().__init__(name) - - if period < 1: - raise ValueError("period has to be greater than 0") - - if obs_time < 0: - raise ValueError("obs_time has to be greater or equal than 0") - - gen = Generator(name='EF_gen', period=period) - trans = Transducer(name='EF_trans', obs_time=obs_time) - - self.add_component(gen) - self.add_component(trans) - - self.p_in_ef = Port(Job, name='p_in_ef') - self.p_out_ef = Port(Job, name='p_out_ef') - - self.add_in_port(self.p_in_ef) - self.add_out_port(self.p_out_ef) - - self.add_coupling(gen.o_out, trans.i_arrived) - self.add_coupling(gen.o_out, self.p_out_ef) - self.add_coupling(trans.o_out, gen.i_stop) - self.add_coupling(self.p_in_ef, trans.i_solved) - - -class EFP(Coupled): - def __init__(self, name, period, obs_time, proc_time): - super().__init__(name) - - ef = EF(name='EF', period=period, obs_time=obs_time) - - proc = Processor(name='EFP_proc', proc_time=proc_time) - - self.add_component(ef) - self.add_component(proc) - - self.add_coupling(ef.p_out_ef, proc.i_in) - self.add_coupling(proc.o_out, ef.p_in_ef) - - -if __name__ == '__main__': - - EFP = EFP('efp', 3, 100, 5) - Coord = Coordinator(EFP) - Coord.initialize() - Coord.simulate() diff --git a/xdevs/examples/basic/json_gpt_model.json b/xdevs/examples/basic/json_gpt_model.json deleted file mode 100644 index e1f2cb1..0000000 --- a/xdevs/examples/basic/json_gpt_model.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "nameGPT": { - "components": { - "atomic/Processor": { - "name": "p0", - "class": "xdevs.core.examples.efp.Processor", - "kwargs": { - "proc_time": 3.0 - } - }, - "atomic/generator": { - "name": "g0", - "class": "xdevs.core.examples.efp.Generator", - "kwargs": { - "period": 1.0 - } - }, - "atomic/transducer": { - "name": "t0", - "class": "xdevs.core.examples.efp.Transducer", - "kwargs": { - "obs_time": 100.0 - } - } - }, - "connections": [ - { - "componentFrom": "p0", - "portFrom": "o_out", - "componentTo": "t0", - "portTo": "i_solved" - }, - { - "componentFrom": "g0", - "portFrom": "o_out", - "componentTo": "p0", - "portTo": "i_in" - }, - { - "componentFrom": "g0", - "portFrom": "o_out", - "componentTo": "t0", - "portTo": "i_arrived" - }, - { - "componentFrom": "t0", - "portFrom": "o_out", - "componentTo": "g0", - "portTo": "i_stop" - } - ] - } -} \ No newline at end of file diff --git a/xdevs/examples/devstone/devstone.py b/xdevs/examples/devstone/devstone.py index 2884f08..769787d 100644 --- a/xdevs/examples/devstone/devstone.py +++ b/xdevs/examples/devstone/devstone.py @@ -49,6 +49,8 @@ def exit(self): class AbstractDEVStone(Coupled, ABC): + components: list[AbstractDEVStone | DelayedAtomic] + def __init__(self, name: str, width: int, depth: int, int_delay: float, ext_delay: float, test: bool = False): super().__init__(name) if width < 1: @@ -290,4 +292,4 @@ def n_events(self) -> int: coord = Coordinator(root) coord.initialize() coord.inject(root.i_in, 0) - coord.simulate() + coord.simulate_iters() diff --git a/xdevs/examples/devstone/execution_loop.py b/xdevs/examples/devstone/execution_loop.py deleted file mode 100644 index bda1593..0000000 --- a/xdevs/examples/devstone/execution_loop.py +++ /dev/null @@ -1,54 +0,0 @@ -import csv -import sys -import time - -from devstone import LI, HI, HO -from xdevs.sim import Coordinator - -sys.setrecursionlimit(10000) - -sim_max_time = 1e10 -int_delay = 0 -ext_delay = 0 -flatten = False -num_execs = int(sys.argv[1]) if len(sys.argv) > 1 else 10 - -depths_widths = [(300, 10), (10, 300), (300, 300)] - -filename = "xdevs_devstone_%s_%dc_%di_%de_%d.csv" % ("flatten" if flatten else "noflatten", len(depths_widths), - int_delay, ext_delay, int(time.time())) - -with open(filename, "w") as csv_file: - csv_writer = csv.writer(csv_file, delimiter=';') - csv_writer.writerow(("model", "depth", "width", "chained", "model_time", "runner_time", "sim_time", "total_time")) - - for depth, width in depths_widths: - for model_type in (LI, HI, HO): - class_name = model_type.__name__ - row = (class_name, depth, width) - - for chain_activated in (False, True): - for i_exec in range(num_execs): - - start = time.time() - root_model = model_type("root", depth, width, int_delay, ext_delay) - middle = time.time() - coord = Coordinator(root_model, flatten=flatten, chain=chain_activated) - coord.initialize() - coord.inject(root_model.i_in, 0) # TODO How many input? - middle2 = time.time() - coord.simulate_time(sim_max_time) - end = time.time() - - model_time = middle - start - runner_time = middle2 - middle - sim_time = end - middle2 - total_time = end - start - # print("acc: %d" % i_exec, model_time, runner_time, sim_time, total_time) - - # row = row + (model_time/num_execs, runner_time/num_execs, sim_time/num_execs, total_time/num_execs) - curr_row = row + (chain_activated, model_time, runner_time, sim_time, total_time) - - print(curr_row) - csv_writer.writerow(curr_row) - diff --git a/xdevs/examples/devstone/generator.py b/xdevs/examples/devstone/generator.py new file mode 100644 index 0000000..aaddbeb --- /dev/null +++ b/xdevs/examples/devstone/generator.py @@ -0,0 +1,34 @@ +from xdevs.models import Atomic, Port + + +class Generator(Atomic): + def __init__(self, name: str, num_outputs: int = 1, period: float = float('inf'), num_ports: int = 1): + super(Generator, self).__init__(name=name) + if num_outputs < 1: + raise ValueError('Number of outputs must greater than zero') + if num_ports < 1: + raise ValueError('Number of ports must be greater than zero') + if period <= 0: + raise ValueError('Period must be greater than zero') + self.num_outputs: int = num_outputs + self.period: float = period + + self.o_out: list[Port[int]] = [Port(int, f'o_out_{i}') for i in range(num_ports)] + for port in self.o_out: + self.add_out_port(port) + + def deltint(self): + self.sigma = self.period + + def deltext(self, e: float): + pass + + def lambdaf(self): + for port in self.o_out: + port.extend(range(self.num_outputs)) + + def initialize(self): + self.activate() + + def exit(self): + pass diff --git a/xdevs/examples/devstone/main.py b/xdevs/examples/devstone/main.py index 0e305c9..2220214 100644 --- a/xdevs/examples/devstone/main.py +++ b/xdevs/examples/devstone/main.py @@ -5,27 +5,25 @@ from xdevs.sim import Coordinator from xdevs.examples.devstone.devstone import LI, HO, HI, HOmod +from xdevs.examples.devstone.generator import Generator from xdevs.models import Coupled -from xdevs.utils import Generator + sys.setrecursionlimit(10000) +MODEL_TYPES = ("LI", "HI", "HO", "HOmod") -class DEVStoneEnvironment(Coupled): - def __init__(self, name, devstone_model, num_gen_outputs=1, gen_period=None): +class DEVStoneEnvironment(Coupled): + def __init__(self, name, devstone_model, num_gen_outputs=1): super(DEVStoneEnvironment, self).__init__(name=name) - generator = Generator("generator", num_gen_outputs, gen_period) + generator = Generator("generator", num_gen_outputs) self.add_component(generator) self.add_component(devstone_model) - self.add_coupling(generator.o_out, devstone_model.i_in) - + self.add_coupling(generator.o_out[0], devstone_model.i_in) if isinstance(devstone_model, HO) or isinstance(devstone_model, HOmod): - self.add_coupling(generator.o_out, devstone_model.i_in2) - - -MODEL_TYPES = ("LI", "HI", "HO", "HOmod") + self.add_coupling(generator.o_out[0], devstone_model.i_in2) def parse_args(): @@ -47,11 +45,8 @@ def parse_args(): if __name__ == '__main__': - args = parse_args() - devstone_model = None - if args.model_type == "LI": devstone_model = LI("LI_root", args.depth, args.width, args.int_cycles, args.ext_cycles) elif args.model_type == "HI": @@ -60,6 +55,8 @@ def parse_args(): devstone_model = HO("HO_root", args.depth, args.width, args.int_cycles, args.ext_cycles) elif args.model_type == "HOmod": devstone_model = HOmod("HOmod_root", args.depth, args.width, args.int_cycles, args.ext_cycles) + else: + raise RuntimeError("Unrecognized model type.") start_time = time.time() env = DEVStoneEnvironment("DEVStoneEnvironment", devstone_model) @@ -69,10 +66,9 @@ def parse_args(): coord.initialize() engine_setup_time = time.time() - coord.simulate() + coord.simulate_iters() sim_time = time.time() - print("Model creation time: {}".format(model_created_time - start_time)) - print("Engine setup time: {}".format(engine_setup_time - model_created_time)) - print("Simulation time: {}".format(sim_time - engine_setup_time)) - + print(f"Model creation time: {model_created_time - start_time}") + print(f"Engine setup time: {engine_setup_time - model_created_time}") + print(f"Simulation time: {sim_time - engine_setup_time}") diff --git a/xdevs/examples/basic/__init__.py b/xdevs/examples/gpt/__init__.py similarity index 100% rename from xdevs/examples/basic/__init__.py rename to xdevs/examples/gpt/__init__.py diff --git a/xdevs/examples/gpt/efp.py b/xdevs/examples/gpt/efp.py new file mode 100644 index 0000000..87421ff --- /dev/null +++ b/xdevs/examples/gpt/efp.py @@ -0,0 +1,47 @@ +from xdevs.examples.gpt.gpt import Generator, Transducer, Job, Processor +from xdevs.models import Coupled, Port + + +class Ef(Coupled): + def __init__(self, name: str, gen_t: float, obs_t: float): + super().__init__(name) + + gen = Generator('generator', gen_t) + trans = Transducer('transducer', obs_t) + + self.add_component(gen) + self.add_component(trans) + + self.p_in_ef = Port(Job, name='p_in_ef') + self.p_out_ef = Port(Job, name='p_out_ef') + + self.add_in_port(self.p_in_ef) + self.add_out_port(self.p_out_ef) + + self.add_coupling(gen.o_job, trans.i_arrived) + self.add_coupling(gen.o_job, self.p_out_ef) + self.add_coupling(trans.o_out, gen.i_stop) + self.add_coupling(self.p_in_ef, trans.i_solved) + + +class Efp(Coupled): + def __init__(self, name: str, gen_t: float, proc_t: float, obs_t: float): + super().__init__(name) + + ef = Ef('ef', gen_t, obs_t) + proc = Processor('processor', proc_t) + + self.add_component(ef) + self.add_component(proc) + + self.add_coupling(ef.p_out_ef, proc.i_in) + self.add_coupling(proc.o_out, ef.p_in_ef) + + +if __name__ == '__main__': + from xdevs.sim import Coordinator + + efp = Efp('efp', 3, 5, 100) + coord = Coordinator(efp) + coord.initialize() + coord.simulate_iters() diff --git a/xdevs/examples/gpt/gpt.py b/xdevs/examples/gpt/gpt.py new file mode 100644 index 0000000..e46bba6 --- /dev/null +++ b/xdevs/examples/gpt/gpt.py @@ -0,0 +1,198 @@ +import logging +import time +from xdevs import PHASE_ACTIVE, PHASE_PASSIVE, get_logger +from xdevs.models import Atomic, Coupled, Port + +logger = get_logger(__name__, logging.DEBUG) + +PHASE_DONE = "done" + + +class Job: + def __init__(self, name: str): + """ + Job event class. It represents a job sent by the generator and processed by the processor. + :param name: job name + """ + self.name: str = name + self.time: float = 0 + + +class Generator(Atomic): + def __init__(self, name: str, gen_t: float): + """ + Generator model. It generates jobs at a given period. + :param name: model name + :param gen_t: period between job generations + """ + super().__init__(name) + + if gen_t < 1: + raise ValueError('gen_t must be greater than 0') + + self.i_stop: Port[bool] = Port(bool, "i_stop") + self.o_job: Port[Job] = Port(Job, "o_out") + + self.add_in_port(self.i_stop) + self.add_out_port(self.o_job) + + self.gen_t: float = gen_t + self.job_counter: int = 1 + + def initialize(self): + self.hold_in(PHASE_ACTIVE, self.gen_t) + + def exit(self): + pass + + def deltint(self): + self.job_counter += 1 + self.hold_in(PHASE_ACTIVE, self.gen_t) + + def deltext(self, e): + self.continuef(e) + if self.i_stop.get(): + self.passivate() + elif self.sigma == float('inf'): + self.hold_in(PHASE_ACTIVE, self.gen_t) + + def lambdaf(self): + self.o_job.add(Job(str(self.job_counter))) + + +class Processor(Atomic): + def __init__(self, name: str, proc_t: float): + """ + Processor model. It processes jobs with a given processing time. + :param name: model name + :param proc_t: processing time + """ + super().__init__(name) + + if proc_t < 1: + raise ValueError('proc_t must be greater than 0') + + self.i_in: Port[Job] = Port(Job, "i_in") + self.o_out: Port[Job] = Port(Job, "o_out") + + self.add_in_port(self.i_in) + self.add_out_port(self.o_out) + + self.current_job: Job | None = None + self.proc_t: float = proc_t + + def initialize(self): + self.passivate() + + def exit(self): + pass + + def deltint(self): + self.passivate() + + def deltext(self, e): + if self.phase == PHASE_PASSIVE: + self.current_job = self.i_in.get() + self.hold_in(PHASE_ACTIVE, self.proc_t) + self.continuef(e) + + def lambdaf(self): + self.o_out.add(self.current_job) + + +class Transducer(Atomic): + def __init__(self, name: str, obs_t: float): + super().__init__(name) + + if obs_t < 0: + raise ValueError('obs_t must be greater or equal than 0') + + self.i_arrived: Port[Job] = Port(Job, "i_arrived") + self.i_solved: Port[Job] = Port(Job, "i_solved") + self.o_out: Port[bool] = Port(bool, "o_out") + + self.add_in_port(self.i_arrived) + self.add_in_port(self.i_solved) + self.add_out_port(self.o_out) + + self.jobs_arrived: list[Job] = [] + self.jobs_solved: list[Job] = [] + + self.total_ta: float = 0 + self.clock: float = 0 + self.obs_t: float = obs_t + + def initialize(self): + self.hold_in(PHASE_ACTIVE, self.obs_t) + + def exit(self): + pass + + def deltint(self): + self.clock += self.sigma + + if self.phase == PHASE_ACTIVE: + avg_ta = 0 + throughput = 0 + if self.jobs_solved: + avg_ta = self.total_ta / len(self.jobs_solved) + throughput = len(self.jobs_solved) / self.clock if self.clock > 0 else 0 + + logger.info(f'End time: {self.clock}') + logger.info(f'Jobs arrived: {len(self.jobs_arrived)}') + logger.info(f'Jobs solved: {len(self.jobs_solved)}') + logger.info(f'Average TA: {avg_ta}') + logger.info(f'Throughput: {throughput}') + + self.hold_in(PHASE_DONE, 0) + else: + self.passivate() + + def deltext(self, e): + self.clock += e + + if self.phase == PHASE_ACTIVE: + if self.i_arrived: + job = self.i_arrived.get() + logger.info(f'Starting job {job.name} @ t = {self.clock} @ t = {time.time_ns()}') + job.time = self.clock + self.jobs_arrived.append(job) + + if self.i_solved: + job = self.i_solved.get() + logger.info(f'Job {job.name} finished @ t = {self.clock} @ t = {time.time()}') + self.total_ta += self.clock - job.time + self.jobs_solved.append(job) + + self.continuef(e) + + def lambdaf(self): + if self.phase == PHASE_DONE: + self.o_out.add(True) + + +class Gpt(Coupled): + def __init__(self, name: str, gen_t: float, proc_t: float, obs_t: float): + super().__init__(name) + + gen = Generator('generator', gen_t) + proc = Processor('processor', proc_t) + trans = Transducer('transducer', obs_t) + + self.add_component(gen) + self.add_component(proc) + self.add_component(trans) + + self.add_coupling(gen.o_job, proc.i_in) + self.add_coupling(gen.o_job, trans.i_arrived) + self.add_coupling(proc.o_out, trans.i_solved) + self.add_coupling(trans.o_out, gen.i_stop) + + +if __name__ == '__main__': + from xdevs.sim import Coordinator + + gpt = Gpt("gpt", 3, 5, 100) + coord = Coordinator(gpt) + coord.initialize() + coord.simulate_iters() diff --git a/xdevs/examples/store_cashier/__init__.py b/xdevs/examples/json/__init__.py similarity index 100% rename from xdevs/examples/store_cashier/__init__.py rename to xdevs/examples/json/__init__.py diff --git a/xdevs/examples/json/efp.json b/xdevs/examples/json/efp.json new file mode 100644 index 0000000..a172b88 --- /dev/null +++ b/xdevs/examples/json/efp.json @@ -0,0 +1,33 @@ +{ + "efp": { + "components": { + "ef": { + "component_id": "ef", + "kwargs": { + "gen_t": 3.0, + "obs_t": 100.0 + } + }, + "processor": { + "component_id": "processor", + "kwargs": { + "proc_t": 5.0 + } + } + }, + "couplings": [ + { + "componentFrom": "ef", + "portFrom": "p_out_ef", + "componentTo": "processor", + "portTo": "i_in" + }, + { + "componentFrom": "processor", + "portFrom": "o_out", + "componentTo": "ef", + "portTo": "p_in_ef" + } + ] + } +} diff --git a/xdevs/examples/json/gpt.json b/xdevs/examples/json/gpt.json new file mode 100644 index 0000000..7480c41 --- /dev/null +++ b/xdevs/examples/json/gpt.json @@ -0,0 +1,50 @@ +{ + "gpt": { + "components": { + "generator": { + "component_id": "generator", + "kwargs": { + "gen_t": 3.0 + } + }, + "processor": { + "component_id": "processor", + "kwargs": { + "proc_t": 5.0 + } + }, + "transducer": { + "component_id": "transducer", + "kwargs": { + "obs_t": 100.0 + } + } + }, + "couplings": [ + { + "componentFrom": "processor", + "portFrom": "o_out", + "componentTo": "transducer", + "portTo": "i_solved" + }, + { + "componentFrom": "generator", + "portFrom": "o_out", + "componentTo": "processor", + "portTo": "i_in" + }, + { + "componentFrom": "generator", + "portFrom": "o_out", + "componentTo": "transducer", + "portTo": "i_arrived" + }, + { + "componentFrom": "transducer", + "portFrom": "o_out", + "componentTo": "generator", + "portTo": "i_stop" + } + ] + } +} diff --git a/xdevs/examples/json/main.py b/xdevs/examples/json/main.py new file mode 100644 index 0000000..f713acf --- /dev/null +++ b/xdevs/examples/json/main.py @@ -0,0 +1,12 @@ +import sys +from xdevs.sim import Coordinator +from xdevs.factory import Components + + +if __name__ == '__main__': + file_path = sys.argv[1] if len(sys.argv) > 1 else 'gpt.json' + + component = Components.from_json(file_path) + coord = Coordinator(component) + coord.initialize() + coord.simulate_iters() diff --git a/xdevs/examples/store/1_vt_simulation.py b/xdevs/examples/store/1_vt_simulation.py new file mode 100644 index 0000000..6a71e0a --- /dev/null +++ b/xdevs/examples/store/1_vt_simulation.py @@ -0,0 +1,58 @@ +import sys +import time +from xdevs.sim import Coordinator +from xdevs.examples.store.models.store import Store + + +def get_sec(time_str): + h, m, s = time_str.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + + +if __name__ == '__main__': + sim_time: float = 13 + n_employees = 3 + mean_employees = 5 + mean_generator = 3 + stddev_employees = 0 + stddev_clients = 0 + + if len(sys.argv) > 8: + print("Program used with more arguments than accepted. Last arguments will be ignored.") + elif len(sys.argv) < 8: + print("Program used with less arguments than accepted. Missing parameters will be set to their default value.") + if len(sys.argv) != 8: + print("Correct usage:") + print("\t" "python3 " + sys.argv[ + 0] + " ") + try: + sim_time = get_sec(sys.argv[1]) + n_employees = int(sys.argv[2]) + mean_employees = float(sys.argv[3]) + mean_generator = float(sys.argv[4]) + stddev_employees = float(sys.argv[5]) + stddev_clients = float(sys.argv[6]) + force_chain = bool(int(sys.argv[7])) + except IndexError: + pass + + print("CONFIGURATION OF THE SCENARIO:") + print("\tSimulation time: {} seconds".format(sim_time)) + print("\tNumber of Employees: {}".format(n_employees)) + print("\tMean time required by employee to dispatch clients: {} seconds (standard deviation of {})".format( + mean_employees, stddev_employees)) + print("\tMean time between new clients: {} seconds (standard deviation of {})".format(mean_generator, + stddev_employees)) + + # PURE SIMULATION + start = time.time() + store = Store(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) + middle = time.time() + print("Model Created. Elapsed time: {} sec".format(middle - start)) + coord = Coordinator(store) + coord.initialize() + middle = time.time() + print("Coordinator Created. Elapsed time: {} sec".format(middle - start)) + coord.simulate(sim_time) + end = time.time() + print("Simulation took: {} sec".format(end - start)) diff --git a/xdevs/examples/store/2_rt_simulation_csv_output_handler.py b/xdevs/examples/store/2_rt_simulation_csv_output_handler.py new file mode 100644 index 0000000..882a046 --- /dev/null +++ b/xdevs/examples/store/2_rt_simulation_csv_output_handler.py @@ -0,0 +1,60 @@ +import sys +from xdevs.rt import RealTimeCoordinator, RealTimeManager +import time + +from xdevs.examples.store.models.store import Store + + +def get_sec(time_str): + h, m, s = time_str.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + + +if __name__ == '__main__': + sim_time: float = 52 + n_employees = 3 + mean_employees = 5 + mean_generator = 3 + stddev_employees = 0.8 + stddev_clients = 0.5 + + if len(sys.argv) > 8: + print("Program used with more arguments than accepted. Last arguments will be ignored.") + elif len(sys.argv) < 8: + print("Program used with less arguments than accepted. Missing parameters will be set to their default value.") + if len(sys.argv) != 8: + print("Correct usage:") + print("\t" "python3 " + sys.argv[ + 0] + " " + " ") + try: + sim_time = get_sec(sys.argv[1]) + n_employees = int(sys.argv[2]) + mean_employees = float(sys.argv[3]) + mean_generator = float(sys.argv[4]) + stddev_employees = float(sys.argv[5]) + stddev_clients = float(sys.argv[6]) + force_chain = bool(int(sys.argv[7])) + except IndexError: + pass + + print("CONFIGURATION OF THE SCENARIO:") + print(f'\tSimulation time: {sim_time} seconds') + print(f'\tNumber of Employees: {n_employees}') + print(f'\tMean time required to dispatch clients: {mean_employees} seconds (stddev of {stddev_employees})') + print(f'\tMean time between new clients: {mean_generator} seconds (standard deviation of {stddev_employees})') + + start = time.time() + store = Store(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) + middle = time.time() + print(f'Model Created. Elapsed time: {middle - start} sec') + rt_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) + rt_manager.add_output_handler('csv', file='rt_simulation_csv_output_handler.csv') + c = RealTimeCoordinator(store, rt_manager) + middle = time.time() + print(f'Coordinator, Manager and Handlers Created. Elapsed time: {middle - start} sec') + c.simulate_iters(time_interv=sim_time) + end = time.time() + print(f'Simulation time (s) = {sim_time}') + print(f'Simulation took: {end - start} sec') + print(f'Error (%) = {((time.time() - start - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store/3_rt_simulation_tcp_input_handler.py b/xdevs/examples/store/3_rt_simulation_tcp_input_handler.py new file mode 100644 index 0000000..4780d4f --- /dev/null +++ b/xdevs/examples/store/3_rt_simulation_tcp_input_handler.py @@ -0,0 +1,73 @@ +import sys +import time +from xdevs.examples.store.models.msg import NewClient +from xdevs.rt import RealTimeCoordinator, RealTimeManager +from xdevs.examples.store.models.store import Store + + +def get_sec(time_str): + h, m, s = time_str.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + + +def new_client_parser(msg: str): + client_id, t_entered = msg.split('?') + + c = NewClient(client_id=client_id, t_entered=time.time()) + return c + + +if __name__ == '__main__': + sim_time: float = 52 + n_employees = 3 + mean_employees = 5 + mean_generator = 3 + stddev_employees = 0.8 + stddev_clients = 0.5 + + if len(sys.argv) > 8: + print("Program used with more arguments than accepted. Last arguments will be ignored.") + elif len(sys.argv) < 8: + print("Program used with less arguments than accepted. Missing parameters will be set to their default value.") + if len(sys.argv) != 8: + print("Correct usage:") + print("\t" "python3 " + sys.argv[0] + + " " + " ") + try: + sim_time = get_sec(sys.argv[1]) + n_employees = int(sys.argv[2]) + mean_employees = float(sys.argv[3]) + mean_generator = float(sys.argv[4]) + stddev_employees = float(sys.argv[5]) + stddev_clients = float(sys.argv[6]) + force_chain = bool(int(sys.argv[7])) + except IndexError: + pass + + print("CONFIGURATION OF THE SCENARIO:") + print(f'\tSimulation time: {sim_time} seconds') + print(f'\tNumber of Employees: {n_employees}') + print(f'\tMean time required to dispatch clients: {mean_employees} seconds (stddev of {stddev_employees})') + print(f'\tMean time between new clients: {mean_generator} seconds (standard deviation of {stddev_clients})') + + msg_parser = { + 'IP_NewClient': new_client_parser, + } + + # Real Time simulation: + start = time.time() + store = Store(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) + middle = time.time() + print(f'Model Created. Elapsed time: {middle - start} sec') + rt_manager = RealTimeManager(max_jitter=0.2, event_window=3) + rt_manager.add_input_handler('tcp', port=4321, max_clients=5, msg_parsers=msg_parser) + + c = RealTimeCoordinator(store, rt_manager) + middle = time.time() + print(f'Coordinator, Manager and Handlers Created. Elapsed time: {middle - start} sec') + c.simulate_iters(time_interv=sim_time) + end = time.time() + print(f'Simulation time (s) = {sim_time}') + print(f'Simulation took: {end - start} sec') + print(f'Error (%) = {((time.time() - start - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store/4_1_rt_simulation_mqtt_input_handler.py b/xdevs/examples/store/4_1_rt_simulation_mqtt_input_handler.py new file mode 100644 index 0000000..92f3eea --- /dev/null +++ b/xdevs/examples/store/4_1_rt_simulation_mqtt_input_handler.py @@ -0,0 +1,68 @@ +import sys +import time +from xdevs.examples.store.models.msg import NewClient +from xdevs.rt import RealTimeCoordinator, RealTimeManager +from xdevs.examples.store.models.store import StoreWithoutGen + + +def get_sec(time_str): + h, m, s = time_str.split(':') + return int(h) * 3600 + int(m) * 60 + int(s) + + +def mqtt_parser(msg: str): + c_id, t = msg.split(';') + return NewClient(c_id, t) + + +if __name__ == '__main__': + sim_time: float = 52 + n_employees = 3 + mean_employees = 5 + stddev_employees = 0.8 + + if len(sys.argv) > 8: + print("Program used with more arguments than accepted. Last arguments will be ignored.") + elif len(sys.argv) < 8: + print( + "Program used with less arguments than accepted. Missing parameters will be set to their default value.") + if len(sys.argv) != 8: + print("Correct usage:") + print("\tpython3 " + sys.argv[0] + " ") + try: + sim_time = get_sec(sys.argv[1]) + n_employees = int(sys.argv[2]) + mean_employees = float(sys.argv[3]) + stddev_employees = float(sys.argv[4]) + except IndexError: + pass + + print("CONFIGURATION OF THE SCENARIO:") + print(f"\tSimulation time: {sim_time} seconds") + print(f"\tNumber of Employees: {n_employees}") + print(f"\tMean time required to dispatch clients: {mean_employees} seconds (stddev of {stddev_employees})") + + conexiones = { + 'Gen_ClientOut': 'Queue_ClientGen' + } + topics = {'RTsys/Output/Gen_ClientOut': 0} + + msg_parser = { + 'Queue_ClientGen': mqtt_parser, + } + + start = time.time() + storeNOGEN = StoreWithoutGen(n_employees, mean_employees, stddev_employees) + middle = time.time() + print(f"Model Created. Elapsed time: {middle - start} sec") + rt_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) + rt_manager.add_input_handler('mqtt', subscriptions=topics, connections=conexiones, msg_parsers=msg_parser) + c = RealTimeCoordinator(storeNOGEN, rt_manager) + middle = time.time() + print(f"Coordinator and Manager Created. Elapsed time: {middle - start} sec") + t_ini = time.time() + c.simulate_iters(time_interv=sim_time) + end = time.time() + print(f' Simulation time (s) = {sim_time}') + print(f"Simulation took: {end - start} sec") + print(f' Error (%) = {((time.time() - start - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store_cashier/trial_two_models_MQTT_gen.py b/xdevs/examples/store/4_2_rt_simulation_mqtt_output_handler.py similarity index 85% rename from xdevs/examples/store_cashier/trial_two_models_MQTT_gen.py rename to xdevs/examples/store/4_2_rt_simulation_mqtt_output_handler.py index 7967b9c..23b0414 100644 --- a/xdevs/examples/store_cashier/trial_two_models_MQTT_gen.py +++ b/xdevs/examples/store/4_2_rt_simulation_mqtt_output_handler.py @@ -1,16 +1,11 @@ import sys -import threading -from xdevs.examples.store_cashier.msg import NewClient, ClientToEmployee +from xdevs.examples.store.models.msg import NewClient from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim.rt_coord import RealTimeCoordinator -from xdevs.rt_sim.rt_manager import RealTimeManager +from xdevs.rt import RealTimeCoordinator, RealTimeManager import time -from client_generator import ClientGenerator -from store_queue import StoreQueue -from employee import Employee +from xdevs.examples.store.models.clients import ClientGenerator class GenSys(Coupled): @@ -25,10 +20,12 @@ def __init__(self, mean_clients: float = 1, stddev_clients: float = 0, name=None self.add_coupling(generator.output_new_client, self.out_gen_port) + def get_sec(time_str): h, m, s = time_str.split(':') return int(h) * 3600 + int(m) * 60 + int(s) + if __name__ == '__main__': sim_time: float = 52 n_employees = 3 @@ -73,9 +70,8 @@ def get_sec(time_str): c = RealTimeCoordinator(gensys, rt_manager) middle = time.time() print("Coordinator and Manager Created. Elapsed time: {} sec".format(middle - start)) - c.simulate(time_interv=sim_time) + c.simulate_iters(time_interv=sim_time) end = time.time() print(f' Simulation time (s) = {sim_time}') print("Simulation took: {} sec".format(end - start)) - print(f' Error (%) = ' - f'{((time.time() - start - sim_time) / sim_time) * 100}') \ No newline at end of file + print(f' Error (%) = {((time.time() - start - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store/README.md b/xdevs/examples/store/README.md new file mode 100644 index 0000000..3550d00 --- /dev/null +++ b/xdevs/examples/store/README.md @@ -0,0 +1,46 @@ +# Instructions for running the store examples + +## Virtual time (regular simulation) + +To run the store examples with virtual time, you can use the following command: + +```bash +$ cd xdevs/examples/store +$ python3 1_vt_simulation.py +``` + +## Real time with CSV output log + +This example runs the store model in real time and uses an output handler to store output events. +The CSV file is saved in the `output` directory. +You can run the example with the following command: + +```bash +$ cd xdevs/examples/store +$ python3 2_rt_simulation_csv_output_handler.py +``` + +## Real time with TCP input clients + +This example runs the store model in real time and uses a TCP input handler to inject extra clients. +You can run the example with the following command: + +```bash +$ cd xdevs/examples/store +$ python3 3_rt_simulation_tcp_input_handler.py +``` + +This executable will start a TCP server that listens for incoming connections on `localhost:4321`. +You can connect to the server using a TCP client, such as `netcat`: + +```bash +$ nc localhost 4321 +``` + +The client can send messages to the server in the following format: + +``` +,? +``` + +The model only has one input port, called `IP_NewClient`. \ No newline at end of file diff --git a/xdevs/examples/store/__init__.py b/xdevs/examples/store/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xdevs/examples/store/models/__init__.py b/xdevs/examples/store/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xdevs/examples/store_cashier/client_generator.py b/xdevs/examples/store/models/clients.py similarity index 74% rename from xdevs/examples/store_cashier/client_generator.py rename to xdevs/examples/store/models/clients.py index 3a94333..645f448 100644 --- a/xdevs/examples/store_cashier/client_generator.py +++ b/xdevs/examples/store/models/clients.py @@ -1,31 +1,30 @@ -import logging from random import gauss from xdevs.models import Atomic, Port import time -from msg import NewClient +from .msg import NewClient class ClientGeneratorStatus: def __init__(self): - self.next_client_id = 0 - self.time_to_next = 0 + self.next_client_id: int = 0 + self.time_to_next: float = 0 def __str__(self): - return ''.format(self.next_client_id, self.time_to_next) + return f'' class ClientGenerator(Atomic): - def __init__(self, mean: float = 10, stddev: float = 0, name: str = None): super().__init__(name) - self.mean = mean - self.stddev = stddev - self.clock = 0 + self.mean: float = mean + self.stddev: float = stddev + self.clock: float = 0 self.state: ClientGeneratorStatus = ClientGeneratorStatus() - self.output_new_client = Port(NewClient) + self.output_new_client: Port[NewClient] = Port(NewClient) self.add_out_port(self.output_new_client) - self.time_started = time.time() + + self.time_started: float = time.time() def deltint(self): self.clock += self.sigma diff --git a/xdevs/examples/store_cashier/employee.py b/xdevs/examples/store/models/employee.py similarity index 83% rename from xdevs/examples/store_cashier/employee.py rename to xdevs/examples/store/models/employee.py index 4a2da21..45d1851 100644 --- a/xdevs/examples/store_cashier/employee.py +++ b/xdevs/examples/store/models/employee.py @@ -1,10 +1,8 @@ -import datetime -import logging import time from random import gauss from xdevs.models import Atomic, Port, INFINITY -from msg import ClientToEmployee, LeavingClient +from .msg import ClientToEmployee, LeavingClient class EmployeeState: @@ -14,11 +12,10 @@ def __init__(self): self.time_remaining = 0 def __str__(self): - return ''.format(self.clients_so_far, bool(self.client), self.time_remaining) + return f'' class Employee(Atomic): - def __init__(self, employee_id: int, mean: float = 30, stddev: float = 0, name: str = None): super().__init__(name) self.name += str(employee_id) @@ -28,11 +25,11 @@ def __init__(self, employee_id: int, mean: float = 30, stddev: float = 0, name: self.clock = 0 self.state = EmployeeState() - self.input_client = Port(ClientToEmployee) + self.input_client = Port(ClientToEmployee, 'in_client') self.add_in_port(self.input_client) - self.output_ready = Port(int) - self.output_client = Port(LeavingClient) + self.output_ready = Port(int, 'out_ready') + self.output_client = Port(LeavingClient, 'out_client') self.add_out_port(self.output_ready) self.add_out_port(self.output_client) @@ -63,13 +60,12 @@ def deltext(self, e): def lambdaf(self): clock = self.clock + self.state.time_remaining - #print(f'METO LEAVING CLIENT EN :{datetime.datetime.now()}') if self.state.client is not None: self.output_client.add(LeavingClient(self.state.client.client_id, self.state.client.t_entered, clock)) self.output_ready.add(self.employee_id) def initialize(self): - self.activate(0) + self.activate() def exit(self): pass diff --git a/xdevs/examples/store_cashier/msg.py b/xdevs/examples/store/models/msg.py similarity index 95% rename from xdevs/examples/store_cashier/msg.py rename to xdevs/examples/store/models/msg.py index ec2b58f..67d0067 100644 --- a/xdevs/examples/store_cashier/msg.py +++ b/xdevs/examples/store/models/msg.py @@ -6,6 +6,7 @@ def __init__(self, client_id, t_entered): def __str__(self): return f'id::{self.client_id}; t_entered::{self.t_entered}' + class ClientToEmployee: def __init__(self, new_client, employee_id): self.client = new_client @@ -14,6 +15,7 @@ def __init__(self, new_client, employee_id): def __str__(self): return f'Client::{self.client} to Employee::{self.employee_id}' + class LeavingClient: def __init__(self, client_id, t_entered, t_exited): self.client_id = client_id @@ -21,4 +23,4 @@ def __init__(self, client_id, t_entered, t_exited): self.t_exited = t_exited def __str__(self): - return f'Client::{self.client_id}; t_entered::{self.t_entered}; t_exited::{self.t_exited}' \ No newline at end of file + return f'Client::{self.client_id}; t_entered::{self.t_entered}; t_exited::{self.t_exited}' diff --git a/xdevs/examples/store_cashier/store_queue.py b/xdevs/examples/store/models/queue.py similarity index 83% rename from xdevs/examples/store_cashier/store_queue.py rename to xdevs/examples/store/models/queue.py index f5edb4a..c93344a 100644 --- a/xdevs/examples/store_cashier/store_queue.py +++ b/xdevs/examples/store/models/queue.py @@ -1,9 +1,7 @@ from _collections import deque from xdevs.models import Atomic, Port, INFINITY import time -import logging - -from msg import NewClient, ClientToEmployee +from .msg import NewClient, ClientToEmployee class QueueStatus: @@ -13,19 +11,17 @@ def __init__(self): self.pairings = deque() def __str__(self): - return ''.format(len(self.clients), len(self.employees), - len(self.pairings)) + return f'' class StoreQueue(Atomic): - def __init__(self, name: str = None): super().__init__(name) self.clock = 0 self.state = QueueStatus() - self.input_new_client = Port(NewClient) - self.input_available_employee = Port(int) + self.input_new_client = Port(NewClient, 'in_new_client') + self.input_available_employee = Port(int, 'in_available_employee') self.add_in_port(self.input_new_client) self.add_in_port(self.input_available_employee) @@ -35,7 +31,7 @@ def __init__(self, name: str = None): self.time_started = time.time() def deltint(self): - self.clock += self.ta + self.clock += self.ta() self.state.pairings.clear() self.passivate() diff --git a/xdevs/examples/store/models/store.py b/xdevs/examples/store/models/store.py new file mode 100644 index 0000000..1dcb1ad --- /dev/null +++ b/xdevs/examples/store/models/store.py @@ -0,0 +1,79 @@ +from xdevs.models import Coupled, Port +from xdevs.examples.store.models.msg import NewClient, ClientToEmployee, LeavingClient +from xdevs.examples.store.models.clients import ClientGenerator +from xdevs.examples.store.models.queue import StoreQueue +from xdevs.examples.store.models.employee import Employee + + +class Store(Coupled): + def __init__(self, n_employees: int = 10000, mean_employees: float = 30, mean_clients: float = 1, + stddev_employees: float = 0, stddev_clients: float = 0, name=None): + super().__init__(name) + + generator = ClientGenerator(mean_clients, stddev_clients) + queue = StoreQueue() + + self.input_new_client = Port(NewClient, 'IP_NewClient') + self.add_in_port(self.input_new_client) + + self.output_port_queue = Port(ClientToEmployee, 'OP_LeavingQueue') + self.output_port_gen = Port(NewClient, 'OP_LeavingGenerator') + self.output_port_employee = Port(LeavingClient, 'OP_LeavingEmployee') + + self.add_out_port(self.output_port_queue) + self.add_out_port(self.output_port_gen) + self.add_out_port(self.output_port_employee) + + self.add_component(generator) + self.add_component(queue) + + self.add_coupling(self.input_new_client, queue.input_new_client) + self.add_coupling(generator.output_new_client, queue.input_new_client) + self.add_coupling(queue.output_client_to_employee, self.output_port_queue) + self.add_coupling(generator.output_new_client, self.output_port_gen) + + for i in range(n_employees): + employee = Employee(i, mean_employees, stddev_employees) + self.add_component(employee) + self.add_coupling(queue.output_client_to_employee, employee.input_client) + self.add_coupling(employee.output_ready, queue.input_available_employee) + self.add_coupling(employee.output_client, self.output_port_employee) + + +class GenSys(Coupled): + def __init__(self, mean_clients: float = 1, stddev_clients: float = 0, name=None): + super().__init__(name) + generator = ClientGenerator(mean_clients, stddev_clients) + + self.out_gen_port = Port(NewClient) + self.add_out_port(self.out_gen_port) + + self.add_component(generator) + + self.add_coupling(generator.output_new_client, self.out_gen_port) + + +class StoreWithoutGen(Coupled): + def __init__(self, n_employees: int = 10000, mean_employees: float = 30, stddev_employees: float = 0, + name=None): + super().__init__(name) + + queue = StoreQueue() + + self.o_p_queue = Port(ClientToEmployee) + self.add_out_port(self.o_p_queue) + + self.i_port_gen = Port(NewClient) + self.add_in_port(self.i_port_gen) + + self.add_component(queue) + + self.add_coupling(self.i_port_gen, queue.input_new_client) + + self.add_coupling(queue.output_client_to_employee, self.o_p_queue) + + for i in range(n_employees): + employee = Employee(i, mean_employees, stddev_employees) + self.add_component(employee) + self.add_coupling(queue.output_client_to_employee, employee.input_client) + self.add_coupling(employee.output_ready, queue.input_available_employee) diff --git a/xdevs/examples/store_cashier/gen.py b/xdevs/examples/store/system_clients.py similarity index 63% rename from xdevs/examples/store_cashier/gen.py rename to xdevs/examples/store/system_clients.py index e2aeeda..d6e4ffe 100644 --- a/xdevs/examples/store_cashier/gen.py +++ b/xdevs/examples/store/system_clients.py @@ -1,16 +1,10 @@ -import sys -import threading - -from xdevs.examples.store_cashier.msg import NewClient, ClientToEmployee +from xdevs.examples.store.models.msg import NewClient from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim.rt_coord import RealTimeCoordinator -from xdevs.rt_sim.rt_manager import RealTimeManager +from xdevs.rt import RealTimeManager, RealTimeCoordinator import time -from client_generator import ClientGenerator -from store_queue import StoreQueue -from employee import Employee +from xdevs.examples.store.models.clients import ClientGenerator + class GenSys(Coupled): def __init__(self, mean_clients: float = 1, stddev_clients: float =0, name=None): @@ -33,16 +27,14 @@ def __init__(self, mean_clients: float = 1, stddev_clients: float =0, name=None gen = GenSys(mean_clients=mean_clients, stddev_clients=stddev_clients) gen_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) - gen_manager.add_output_handler('tcp_out_handler', PORT=5055) + gen_manager.add_output_handler('tcp', PORT=5055) gen_coord = RealTimeCoordinator(gen, gen_manager) - t_ini = time.time() print(f' >>> COMENZAMOS : {t_ini}') - gen_coord.simulate(time_interv=sim_time) + gen_coord.simulate_iters(time_interv=sim_time) print(f' >>> FIN : {time.time()}') print(f' Tiempo a ejecutar (s) = {sim_time }') print(f' Tiempo ejecutado (s) = {(time.time() - t_ini)}') - print(f' Error (%) = ' - f'{((time.time() - t_ini - sim_time) / sim_time) * 100}') + print(f' Error (%) = {((time.time() - t_ini - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store_cashier/employeesSys.py b/xdevs/examples/store/system_employees.py similarity index 68% rename from xdevs/examples/store_cashier/employeesSys.py rename to xdevs/examples/store/system_employees.py index 693dc82..8f5bb23 100644 --- a/xdevs/examples/store_cashier/employeesSys.py +++ b/xdevs/examples/store/system_employees.py @@ -1,10 +1,10 @@ import datetime import time -from xdevs.examples.store_cashier.employee import Employee -from xdevs.examples.store_cashier.msg import LeavingClient, ClientToEmployee, NewClient +from xdevs.examples.store.models.employee import Employee +from xdevs.examples.store.models.msg import LeavingClient, ClientToEmployee, NewClient from xdevs.models import Coupled, Port -from xdevs.rt_sim import RealTimeManager, RealTimeCoordinator +from xdevs.rt import RealTimeManager, RealTimeCoordinator class EmployeesSys(Coupled): @@ -32,21 +32,13 @@ def __init__(self, n_employees: int = 3, mean_employees: float = 10, self.add_coupling(employee.output_ready, self.output_ready) self.add_coupling(employee.output_client, self.output_client) - # class ClientToEmployee: - # def __init__(self, new_client, employee_id): - # self.client = new_client - # self.employee_id = employee_id - - # Estoy pasando : MqttClient?i?t def input_client_parser(msg: str): - # ("Client::id::3; t_entered::time.time to Employee::3") Formato de entrada client = msg.split("::id::")[1].split(";")[0] - #t = time.time() - float(msg.split("t_entered::")[1].split(" t")[0]) + # t = time.time() - float(msg.split("t_entered::")[1].split(" t")[0]) t = time.time() - t_ini e_id = msg.split("Employee::")[1] - return ClientToEmployee(NewClient(client, t), int(e_id)) @@ -55,7 +47,6 @@ def input_client_parser(msg: str): E = EmployeesSys() - e_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) msg_parser = { 'InputClient': input_client_parser, @@ -73,20 +64,17 @@ def input_client_parser(msg: str): 'Client2Employee': 'InputClient' } - e_manager.add_input_handler('mqtt_handler', subscriptions=sub_input, msg_parsers=msg_parser, connections=connections) - e_manager.add_output_handler('mqtt_handler', subscriptions=sub_output) - - file = 'C:/Users/Usuario/Desktop/00 UNI/01 CUARTO/00 TFG/05 Resultados simulaciones/05 Simulacion final/csv_fin.csv' - - e_manager.add_output_handler('csv_out_handler', file=file) + e_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) + e_manager.add_input_handler('mqtt', subscriptions=sub_input, msg_parsers=msg_parser, connections=connections) + e_manager.add_output_handler('mqtt', subscriptions=sub_output) + e_manager.add_output_handler('csv', file='employees.csv') e_coord = RealTimeCoordinator(E, e_manager) t_ini = time.time() print(f' >>> COMENZAMOS : {t_ini} : {datetime.datetime.now()}') - e_coord.simulate(time_interv=sim_time) + e_coord.simulate_iters(time_interv=sim_time) print(f' >>> FIN : {time.time()}') print(f' Tiempo a ejecutar (s) = {sim_time}') print(f' Tiempo ejecutado (s) = {(time.time() - t_ini)}') - print(f' Error (%) = ' - f'{((time.time() - t_ini - sim_time) / sim_time) * 100}') + print(f' Error (%) = {((time.time() - t_ini - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store_cashier/queueSys.py b/xdevs/examples/store/system_queue.py similarity index 69% rename from xdevs/examples/store_cashier/queueSys.py rename to xdevs/examples/store/system_queue.py index db7688e..8a239a6 100644 --- a/xdevs/examples/store_cashier/queueSys.py +++ b/xdevs/examples/store/system_queue.py @@ -1,9 +1,8 @@ import time - -from xdevs.examples.store_cashier.msg import ClientToEmployee, NewClient -from xdevs.examples.store_cashier.store_queue import StoreQueue +from xdevs.examples.store.models.msg import ClientToEmployee, NewClient +from xdevs.examples.store.models.queue import StoreQueue from xdevs.models import Coupled, Port -from xdevs.rt_sim import RealTimeManager, RealTimeCoordinator +from xdevs.rt import RealTimeManager, RealTimeCoordinator class QueueSys(Coupled): @@ -31,11 +30,8 @@ def __init__(self, name=None): def parser_new_client(msg: str): - #print('¿?¿?¿?¿?¿?') client_id, t_entered = msg.split('?') - - c = NewClient(client_id=client_id,t_entered=t_entered) - #print(f'DEVUELVO:{c}, c_id = {client_id}, t = {t_entered}') + c = NewClient(client_id=client_id, t_entered=t_entered) return c @@ -61,19 +57,16 @@ def parser_new_client(msg: str): 'OutputReady': 'AvailableEmployee' } - q_manager.add_input_handler('tcp_handler', port=4321, max_clients=5, msg_parsers=msg_parser, connections=connections) - - q_manager.add_input_handler('mqtt_handler', subscriptions=subs_input, connections=connections, msg_parsers=msg_parser) - - q_manager.add_output_handler('mqtt_handler', subscriptions=subs_output) + q_manager.add_input_handler('tcp', port=4321, max_clients=5, msg_parsers=msg_parser, connections=connections) + q_manager.add_input_handler('mqtt', subscriptions=subs_input, connections=connections, msg_parsers=msg_parser) + q_manager.add_output_handler('mqtt', subscriptions=subs_output) q_coord = RealTimeCoordinator(q, q_manager) t_ini = time.time() print(f' >>> COMENZAMOS : {t_ini}') - q_coord.simulate(time_interv=sim_time) + q_coord.simulate_iters(time_interv=sim_time) print(f' >>> FIN : {time.time()}') print(f' Tiempo a ejecutar (s) = {sim_time}') print(f' Tiempo ejecutado (s) = {(time.time() - t_ini)}') - print(f' Error (%) = ' - f'{((time.time() - t_ini - sim_time) / sim_time) * 100}') + print(f' Error (%) = {((time.time() - t_ini - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store_cashier/STORE_CASHIER b/xdevs/examples/store_cashier/STORE_CASHIER deleted file mode 100644 index 60e2bba..0000000 Binary files a/xdevs/examples/store_cashier/STORE_CASHIER and /dev/null differ diff --git a/xdevs/examples/store_cashier/execution_loop.py b/xdevs/examples/store_cashier/execution_loop.py deleted file mode 100644 index d2b3798..0000000 --- a/xdevs/examples/store_cashier/execution_loop.py +++ /dev/null @@ -1,35 +0,0 @@ -import subprocess -import re -import csv - -CMD_XDEVS = "python3 store_cashier.py 00:30:00 {n_employees} 10 {t_new_clients} 0 0 {activate_chain}" -CMD_CADMIUM = "./STORE_CASHIER 00:30:00 {n_employees} 10 {t_new_clients} 0 0" - -n_employees_range = range(10, 1001, 10) -t_new_clients_range = range(10, 0, -1) - -with open("out.csv", "w") as csv_file: - csv_writer = csv.writer(csv_file, delimiter=';') - csv_writer.writerow(("engine", "n_employees", "t_new_clients", "chained", "model_time", "runner_time", "sim_time")) - - for engine_cmd in (CMD_CADMIUM, CMD_XDEVS): - for chain_activated in (0, 1): - if engine_cmd == CMD_CADMIUM and chain_activated == 1: - continue - - for n_employees in range(10, 10011, 100): - for t_new_clients in range(10, 0, -1): - exec_cmd = engine_cmd.format(n_employees=n_employees, t_new_clients=t_new_clients, activate_chain=chain_activated) - print("\n" + exec_cmd) - result = subprocess.run(exec_cmd.split(), stdout=subprocess.PIPE) - #print(result.stdout) - - found = re.search("Elapsed time: ([0-9.e-]+) ?sec.*Elapsed time: ([0-9.e-]+) ?sec.*Simulation took: ([0-9.e-]+) ?sec", str(result.stdout)) - if found: - print("Times: " + str(found.groups())) - else: - raise RuntimeError("Simulation times not found") - - engine = "xdevs" if engine_cmd == CMD_XDEVS else "cadmium" - csv_writer.writerow((engine, n_employees, t_new_clients, chain_activated) + found.groups()) - csv_file.flush() diff --git a/xdevs/examples/store_cashier/output.csv b/xdevs/examples/store_cashier/output.csv deleted file mode 100644 index 6e626e8..0000000 --- a/xdevs/examples/store_cashier/output.csv +++ /dev/null @@ -1,161 +0,0 @@ -t,port,msg -0.0030002593994140625,OutputReady,0 -0.0030002593994140625,OutputReady,1 -0.0030002593994140625,OutputReady,2 -0.0030002593994140625,OutputReady,3 -0.0030002593994140625,OutputReady,4 -0.0030002593994140625,OutputReady,5 -0.0030002593994140625,OutputReady,6 -0.003999233245849609,OutputReady,7 -0.003999233245849609,OutputReady,8 -0.003999233245849609,OutputReady,9 -0.003999233245849609,OutputReady,10 -0.003999233245849609,OutputReady,11 -0.003999233245849609,OutputReady,12 -0.003999233245849609,OutputReady,13 -0.003999233245849609,OutputReady,14 -0.003999233245849609,OutputReady,15 -0.003999233245849609,OutputReady,16 -0.003999233245849609,OutputReady,17 -0.003999233245849609,OutputReady,18 -0.003999233245849609,OutputReady,19 -0.003999233245849609,OutputReady,20 -0.003999233245849609,OutputReady,21 -0.003999233245849609,OutputReady,22 -0.003999233245849609,OutputReady,23 -0.003999233245849609,OutputReady,24 -0.003999233245849609,OutputReady,25 -0.003999233245849609,OutputReady,26 -0.003999233245849609,OutputReady,27 -0.003999233245849609,OutputReady,28 -0.003999233245849609,OutputReady,29 -0.003999233245849609,OutputReady,30 -0.003999233245849609,OutputReady,31 -0.003999233245849609,OutputReady,32 -0.003999233245849609,OutputReady,33 -0.003999233245849609,OutputReady,34 -0.003999233245849609,OutputReady,35 -0.003999233245849609,OutputReady,36 -0.003999233245849609,OutputReady,37 -0.003999233245849609,OutputReady,38 -0.003999233245849609,OutputReady,39 -0.003999233245849609,OutputReady,40 -0.004997968673706055,OutputReady,41 -0.004997968673706055,OutputReady,42 -0.004997968673706055,OutputReady,43 -0.004997968673706055,OutputReady,44 -0.004997968673706055,OutputReady,45 -0.004997968673706055,OutputReady,46 -0.004997968673706055,OutputReady,47 -0.004997968673706055,OutputReady,48 -0.004997968673706055,OutputReady,49 -0.004997968673706055,OutputReady,50 -0.004997968673706055,OutputReady,51 -0.004997968673706055,OutputReady,52 -0.004997968673706055,OutputReady,53 -0.004997968673706055,OutputReady,54 -0.004997968673706055,OutputReady,55 -0.004997968673706055,OutputReady,56 -0.004997968673706055,OutputReady,57 -0.004997968673706055,OutputReady,58 -0.004997968673706055,OutputReady,59 -0.004997968673706055,OutputReady,60 -0.004997968673706055,OutputReady,61 -0.004997968673706055,OutputReady,62 -0.004997968673706055,OutputReady,63 -0.004997968673706055,OutputReady,64 -0.004997968673706055,OutputReady,65 -0.004997968673706055,OutputReady,66 -0.004997968673706055,OutputReady,67 -0.004997968673706055,OutputReady,68 -0.004997968673706055,OutputReady,69 -0.004997968673706055,OutputReady,70 -0.004997968673706055,OutputReady,71 -0.004997968673706055,OutputReady,72 -0.004997968673706055,OutputReady,73 -0.004997968673706055,OutputReady,74 -0.005997657775878906,OutputReady,75 -0.005997657775878906,OutputReady,76 -0.005997657775878906,OutputReady,77 -0.005997657775878906,OutputReady,78 -0.005997657775878906,OutputReady,79 -0.005997657775878906,OutputReady,80 -0.005997657775878906,OutputReady,81 -0.005997657775878906,OutputReady,82 -0.005997657775878906,OutputReady,83 -0.005997657775878906,OutputReady,84 -0.005997657775878906,OutputReady,85 -0.005997657775878906,OutputReady,86 -0.005997657775878906,OutputReady,87 -0.005997657775878906,OutputReady,88 -0.006997108459472656,OutputReady,89 -0.006997108459472656,OutputReady,90 -0.006997108459472656,OutputReady,91 -0.006997108459472656,OutputReady,92 -0.006997108459472656,OutputReady,93 -0.006997108459472656,OutputReady,94 -0.006997108459472656,OutputReady,95 -0.006997108459472656,OutputReady,96 -0.006997108459472656,OutputReady,97 -0.006997108459472656,OutputReady,98 -0.006997108459472656,OutputReady,99 -6.154195547103882,LeavingClient,Client::0; t_entered::0.0010004043579101562; t_exited::6.144615411758423 -6.154195547103882,LeavingClient,Client::1; t_entered::1.5143980979919434; t_exited::6.144615411758423 -6.154195547103882,LeavingClient,Client::2; t_entered::3.0254054069519043; t_exited::6.144615411758423 -6.154195547103882,LeavingClient,Client::3; t_entered::4.526482820510864; t_exited::6.144615411758423 -6.155194282531738,OutputReady,0 -6.155194282531738,OutputReady,1 -6.155194282531738,OutputReady,2 -6.155194282531738,OutputReady,3 -7.81133770942688,LeavingClient,Client::4; t_entered::6.037675380706787; t_exited::7.800191164016724 -7.81133770942688,OutputReady,4 -9.323095083236694,LeavingClient,Client::5; t_entered::7.550443649291992; t_exited::9.31480860710144 -9.323095083236694,OutputReady,5 -10.857501029968262,LeavingClient,Client::6; t_entered::9.060180425643921; t_exited::10.855368852615356 -10.858490705490112,OutputReady,6 -12.320794343948364,LeavingClient,Client::7; t_entered::10.574152708053589; t_exited::12.308090209960938 -12.321786642074585,OutputReady,7 -13.847427368164062,LeavingClient,Client::8; t_entered::12.076507568359375; t_exited::13.841029405593872 -13.847427368164062,OutputReady,8 -15.358103036880493,LeavingClient,Client::9; t_entered::13.588427782058716; t_exited::15.35494351387024 -15.358103036880493,OutputReady,9 -16.872090578079224,LeavingClient,Client::10; t_entered::15.091552734375; t_exited::16.866944789886475 -16.872090578079224,OutputReady,10 -18.417977333068848,LeavingClient,Client::11; t_entered::16.601346492767334; t_exited::18.409807920455933 -18.417977333068848,OutputReady,11 -19.893912315368652,LeavingClient,Client::12; t_entered::18.112578630447388; t_exited::19.882812976837158 -19.894919633865356,OutputReady,12 -21.39572048187256,LeavingClient,Client::13; t_entered::19.62247347831726; t_exited::21.39280390739441 -21.39572048187256,OutputReady,13 -22.918955087661743,LeavingClient,Client::14; t_entered::21.137441873550415; t_exited::22.90504288673401 -22.919960021972656,OutputReady,14 -24.436033487319946,LeavingClient,Client::15; t_entered::22.651875495910645; t_exited::24.421077013015747 -24.436033487319946,OutputReady,15 -25.951406002044678,LeavingClient,Client::16; t_entered::24.159263849258423; t_exited::25.93886137008667 -25.951406002044678,OutputReady,16 -27.443901300430298,LeavingClient,Client::17; t_entered::25.66021490097046; t_exited::27.438814640045166 -27.443901300430298,OutputReady,17 -28.948714017868042,LeavingClient,Client::18; t_entered::27.16807770729065; t_exited::28.9385404586792 -28.948714017868042,OutputReady,18 -30.490768909454346,LeavingClient,Client::19; t_entered::28.66822123527527; t_exited::30.477773904800415 -30.490768909454346,OutputReady,19 -31.94276738166809,LeavingClient,Client::20; t_entered::30.16935157775879; t_exited::31.939603805541992 -31.94276738166809,OutputReady,20 -33.52330040931702,LeavingClient,Client::21; t_entered::31.6766140460968; t_exited::33.50881505012512 -33.52330040931702,OutputReady,21 -34.967496156692505,LeavingClient,Client::22; t_entered::33.18162560462952; t_exited::34.95107936859131 -34.96849489212036,OutputReady,22 -36.47815656661987,LeavingClient,Client::23; t_entered::34.69177031517029; t_exited::36.47166299819946 -36.47915601730347,OutputReady,23 -37.97754096984863,LeavingClient,Client::24; t_entered::36.19194841384888; t_exited::37.96942210197449 -37.97754096984863,OutputReady,24 -39.49344801902771,LeavingClient,Client::25; t_entered::37.69249367713928; t_exited::39.48461389541626 -39.49344801902771,OutputReady,25 -40.97653865814209,LeavingClient,Client::26; t_entered::39.20053577423096; t_exited::40.965234994888306 -40.977537631988525,OutputReady,26 -42.67374396324158,LeavingClient,Client::27; t_entered::40.7106728553772; t_exited::42.66540455818176 -42.674750566482544,OutputReady,27 -44.00695443153381,LeavingClient,Client::28; t_entered::42.22624850273132; t_exited::43.993196964263916 -44.00695443153381,OutputReady,28 -45.54259276390076,LeavingClient,Client::29; t_entered::43.72702383995056; t_exited::45.53678750991821 -45.54259276390076,OutputReady,29 diff --git a/xdevs/examples/store_cashier/store_cashier.py b/xdevs/examples/store_cashier/store_cashier.py deleted file mode 100644 index a12839f..0000000 --- a/xdevs/examples/store_cashier/store_cashier.py +++ /dev/null @@ -1,168 +0,0 @@ -import sys -import threading - -from xdevs.examples.store_cashier.msg import NewClient, ClientToEmployee -from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim.rt_coord import RealTimeCoordinator -from xdevs.rt_sim.rt_manager import RealTimeManager -import time - -from client_generator import ClientGenerator -from store_queue import StoreQueue -from employee import Employee - - -class StoreCashier(Coupled): - def __init__(self, n_employees: int = 10000, mean_employees: float = 30, mean_clients: float = 1, - stddev_employees: float = 0, stddev_clients: float = 0, - name=None): - super().__init__(name) - - generator = ClientGenerator(mean_clients, stddev_clients) - queue = StoreQueue() - - self.o_p_queue = Port(ClientToEmployee) - - self.add_out_port(self.o_p_queue) - - self.add_component(generator) - self.add_component(queue) - self.add_coupling(generator.output_new_client, queue.input_new_client) - - self.add_coupling(queue.output_client_to_employee, self.o_p_queue) - - for i in range(n_employees): - employee = Employee(i, mean_employees, stddev_employees) - self.add_component(employee) - self.add_coupling(queue.output_client_to_employee, employee.input_client) - self.add_coupling(employee.output_ready, queue.input_available_employee) - - -class GenSys(Coupled): - def __init__(self, mean_clients: float = 1, stddev_clients: float = 0, name=None): - super().__init__(name) - generator = ClientGenerator(mean_clients, stddev_clients) - - self.out_gen_port = Port(NewClient) - self.add_out_port(self.out_gen_port) - - self.add_component(generator) - - self.add_coupling(generator.output_new_client, self.out_gen_port) - - -class StoreWithoutGen(Coupled): - def __init__(self, n_employees: int = 10000, mean_employees: float = 30, stddev_employees: float = 0, - name=None): - super().__init__(name) - - queue = StoreQueue() - - self.o_p_queue = Port(ClientToEmployee) - self.add_out_port(self.o_p_queue) - - self.i_port_gen = Port(NewClient) - self.add_in_port(self.i_port_gen) - - self.add_component(queue) - - self.add_coupling(self.i_port_gen, queue.input_new_client) - - self.add_coupling(queue.output_client_to_employee, self.o_p_queue) - - for i in range(n_employees): - employee = Employee(i, mean_employees, stddev_employees) - self.add_component(employee) - self.add_coupling(queue.output_client_to_employee, employee.input_client) - self.add_coupling(employee.output_ready, queue.input_available_employee) - - -def get_sec(time_str): - h, m, s = time_str.split(':') - return int(h) * 3600 + int(m) * 60 + int(s) - - -if __name__ == '__main__': - sim_time: float = 13 - n_employees = 3 - mean_employees = 5 - mean_generator = 3 - stddev_employees = 0 - stddev_clients = 0 - - if len(sys.argv) > 8: - print("Program used with more arguments than accepted. Last arguments will be ignored.") - elif len(sys.argv) < 8: - print("Program used with less arguments than accepted. Missing parameters will be set to their default value.") - if len(sys.argv) != 8: - print("Correct usage:") - print("\t" "python3 " + sys.argv[ - 0] + " ") - try: - sim_time = get_sec(sys.argv[1]) - n_employees = int(sys.argv[2]) - mean_employees = float(sys.argv[3]) - mean_generator = float(sys.argv[4]) - stddev_employees = float(sys.argv[5]) - stddev_clients = float(sys.argv[6]) - force_chain = bool(int(sys.argv[7])) - except IndexError: - pass - - print("CONFIGURATION OF THE SCENARIO:") - print("\tSimulation time: {} seconds".format(sim_time)) - print("\tNumber of Employees: {}".format(n_employees)) - print("\tMean time required by employee to dispatch clients: {} seconds (standard deviation of {})".format( - mean_employees, stddev_employees)) - print("\tMean time between new clients: {} seconds (standard deviation of {})".format(mean_generator, - stddev_employees)) - -""" - start = time.time() - store = StoreCashier(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) - middle = time.time() - print("Model Created. Elapsed time: {} sec".format(middle - start)) - coord = Coordinator(store, flatten=True) - coord.initialize() - middle = time.time() - print("Coordinator Created. Elapsed time: {} sec".format(middle - start)) - coord.simulate_time(sim_time) - end = time.time() - print("Simulation took: {} sec".format(end - start)) - -""" -# Real Time simulation: -""" - st = StoreWithoutGen(n_employees, mean_employees, stddev_employees) - - st_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) - st_manager.add_input_handler('tcp_handler', PORT=5055) - st_coord = RealTimeCoordinator(st, st_manager) -""" - -start = time.time() -store = StoreCashier(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) -middle = time.time() -print("Model Created. Elapsed time: {} sec".format(middle - start)) -rt_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) -c = RealTimeCoordinator(store, rt_manager) -middle = time.time() -print("Coordinator and Manager Created. Elapsed time: {} sec".format(middle - start)) -c.simulate(time_interv=sim_time) -end = time.time() -print(f' Simulation time (s) = {sim_time}') -print("Simulation took: {} sec".format(end - start)) -print(f' Error (%) = ' - f'{((time.time() - start - sim_time) / sim_time) * 100}') - -""" - t_ini = time.time() - print(f' >>> COMENZAMOS : {t_ini}') - c.simulate(time_interv=sim_time) - print(f' >>> FIN : {time.time()}') - print(f' Tiempo a ejecutar (s) = {sim_time }') - print(f' Tiempo ejecutado (s) = {(time.time() - t_ini)}') - print(f' Error (%) = ' - f'{((time.time() - t_ini - sim_time) / sim_time) * 100}') -""" diff --git a/xdevs/examples/store_cashier/trial_CSV_store_cashier.py b/xdevs/examples/store_cashier/trial_CSV_store_cashier.py deleted file mode 100644 index 04547a9..0000000 --- a/xdevs/examples/store_cashier/trial_CSV_store_cashier.py +++ /dev/null @@ -1,105 +0,0 @@ -import sys -import threading - -from xdevs.examples.store_cashier.msg import NewClient, ClientToEmployee, LeavingClient -from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim.rt_coord import RealTimeCoordinator -from xdevs.rt_sim.rt_manager import RealTimeManager -import time - -from client_generator import ClientGenerator -from store_queue import StoreQueue -from employee import Employee - - -class StoreCashier(Coupled): - def __init__(self, n_employees: int = 10000, mean_employees: float = 30, mean_clients: float = 1, - stddev_employees: float = 0, stddev_clients: float = 0, - name=None): - super().__init__(name) - - generator = ClientGenerator(mean_clients, stddev_clients) - queue = StoreQueue() - - self.output_port_queue = Port(ClientToEmployee, 'OP_LeavingQueue') - self.output_port_gen = Port(NewClient, 'OP_LeavingGenerator') - self.output_port_employee = Port(LeavingClient, 'OP_LeavingEmployee') - - self.add_out_port(self.output_port_queue) - self.add_out_port(self.output_port_gen) - self.add_out_port(self.output_port_employee) - - self.add_component(generator) - self.add_component(queue) - self.add_coupling(generator.output_new_client, queue.input_new_client) - - self.add_coupling(queue.output_client_to_employee, self.output_port_queue) - self.add_coupling(generator.output_new_client, self.output_port_gen) - - for i in range(n_employees): - employee = Employee(i, mean_employees, stddev_employees) - self.add_component(employee) - self.add_coupling(queue.output_client_to_employee, employee.input_client) - self.add_coupling(employee.output_ready, queue.input_available_employee) - self.add_coupling(employee.output_client, self.output_port_employee) - - -def get_sec(time_str): - h, m, s = time_str.split(':') - return int(h) * 3600 + int(m) * 60 + int(s) - - -if __name__ == '__main__': - sim_time: float = 52 - n_employees = 3 - mean_employees = 5 - mean_generator = 3 - stddev_employees = 0.8 - stddev_clients = 0.5 - - if len(sys.argv) > 8: - print("Program used with more arguments than accepted. Last arguments will be ignored.") - elif len(sys.argv) < 8: - print("Program used with less arguments than accepted. Missing parameters will be set to their default value.") - if len(sys.argv) != 8: - print("Correct usage:") - print("\t" "python3 " + sys.argv[ - 0] + " ") - try: - sim_time = get_sec(sys.argv[1]) - n_employees = int(sys.argv[2]) - mean_employees = float(sys.argv[3]) - mean_generator = float(sys.argv[4]) - stddev_employees = float(sys.argv[5]) - stddev_clients = float(sys.argv[6]) - force_chain = bool(int(sys.argv[7])) - except IndexError: - pass - - print("CONFIGURATION OF THE SCENARIO:") - print("\tSimulation time: {} seconds".format(sim_time)) - print("\tNumber of Employees: {}".format(n_employees)) - print("\tMean time required by employee to dispatch clients: {} seconds (standard deviation of {})".format( - mean_employees, stddev_employees)) - print("\tMean time between new clients: {} seconds (standard deviation of {})".format(mean_generator, - stddev_employees)) - -# Real Time simulation: - - -start = time.time() -store = StoreCashier(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) -middle = time.time() -print("Model Created. Elapsed time: {} sec".format(middle - start)) -rt_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) -rt_manager.add_output_handler('csv_out_handler', file='C:/Users/Usuario/Desktop/00 UNI/01 CUARTO/00 TFG/05 Resultados simulaciones/02 Simulacion CSV OH/52s_rt_csv') -c = RealTimeCoordinator(store, rt_manager) -middle = time.time() -print("Coordinator, Manager and Handlers Created. Elapsed time: {} sec".format(middle - start)) -c.simulate(time_interv=sim_time) -end = time.time() -print(f' Simulation time (s) = {sim_time}') -print("Simulation took: {} sec".format(end - start)) -print(f' Error (%) = ' - f'{((time.time() - start - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store_cashier/trial_TCP_store_cashier.py b/xdevs/examples/store_cashier/trial_TCP_store_cashier.py deleted file mode 100644 index 1bd0b2b..0000000 --- a/xdevs/examples/store_cashier/trial_TCP_store_cashier.py +++ /dev/null @@ -1,120 +0,0 @@ -import sys -import threading - -from xdevs.examples.store_cashier.msg import NewClient, ClientToEmployee, LeavingClient -from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim.rt_coord import RealTimeCoordinator -from xdevs.rt_sim.rt_manager import RealTimeManager -import time - -from client_generator import ClientGenerator -from store_queue import StoreQueue -from employee import Employee - - -class StoreCashier(Coupled): - def __init__(self, n_employees: int = 10000, mean_employees: float = 30, mean_clients: float = 1, - stddev_employees: float = 0, stddev_clients: float = 0, - name=None): - super().__init__(name) - - generator = ClientGenerator(mean_clients, stddev_clients) - queue = StoreQueue() - - self.add_component(generator) - self.add_component(queue) - self.add_coupling(generator.output_new_client, queue.input_new_client) - - self.new_client = Port(NewClient, 'NewClient') - self.add_in_port(self.new_client) - self.add_coupling(self.new_client, queue.input_new_client) - - for i in range(n_employees): - employee = Employee(i, mean_employees, stddev_employees) - self.add_component(employee) - self.add_coupling(queue.output_client_to_employee, employee.input_client) - self.add_coupling(employee.output_ready, queue.input_available_employee) - - - -def get_sec(time_str): - h, m, s = time_str.split(':') - return int(h) * 3600 + int(m) * 60 + int(s) - -def new_client_parser(msg: str): - #print('¿?¿?¿?¿?¿?') - client_id, t_entered = msg.split('?') - - c = NewClient(client_id=client_id,t_entered=time.time()) - #print(f'DEVUELVO:{c}, c_id = {client_id}, t = {t_entered} ahora {time.time()}') - return c - -def tcp_parser(event): - pass - - - -if __name__ == '__main__': - sim_time: float = 52 - n_employees = 3 - mean_employees = 5 - mean_generator = 3 - stddev_employees = 0.8 - stddev_clients = 0.5 - - if len(sys.argv) > 8: - print("Program used with more arguments than accepted. Last arguments will be ignored.") - elif len(sys.argv) < 8: - print("Program used with less arguments than accepted. Missing parameters will be set to their default value.") - if len(sys.argv) != 8: - print("Correct usage:") - print("\t" "python3 " + sys.argv[ - 0] + " ") - try: - sim_time = get_sec(sys.argv[1]) - n_employees = int(sys.argv[2]) - mean_employees = float(sys.argv[3]) - mean_generator = float(sys.argv[4]) - stddev_employees = float(sys.argv[5]) - stddev_clients = float(sys.argv[6]) - force_chain = bool(int(sys.argv[7])) - except IndexError: - pass - - print("CONFIGURATION OF THE SCENARIO:") - print("\tSimulation time: {} seconds".format(sim_time)) - print("\tNumber of Employees: {}".format(n_employees)) - print("\tMean time required by employee to dispatch clients: {} seconds (standard deviation of {})".format( - mean_employees, stddev_employees)) - print("\tMean time between new clients: {} seconds (standard deviation of {})".format(mean_generator, - stddev_clients)) - -##### - - -msg_parser = { - 'NewClient': new_client_parser, -} - - -# Real Time simulation: - - - -start = time.time() -store = StoreCashier(n_employees, mean_employees, mean_generator, stddev_employees, stddev_clients) -middle = time.time() -print("Model Created. Elapsed time: {} sec".format(middle - start)) -rt_manager = RealTimeManager(max_jitter=0.2, event_window=3) -rt_manager.add_input_handler('tcp_handler', port=4321, max_clients=5, msg_parsers=msg_parser) - -c = RealTimeCoordinator(store, rt_manager) -middle = time.time() -print("Coordinator, Manager and Handlers Created. Elapsed time: {} sec".format(middle - start)) -c.simulate(time_interv=sim_time) -end = time.time() -print(f' Simulation time (s) = {sim_time}') -print("Simulation took: {} sec".format(end - start)) -print(f' Error (%) = ' - f'{((time.time() - start - sim_time) / sim_time) * 100}') diff --git a/xdevs/examples/store_cashier/trial_two_models_MQTT_store.py b/xdevs/examples/store_cashier/trial_two_models_MQTT_store.py deleted file mode 100644 index 6f19dcb..0000000 --- a/xdevs/examples/store_cashier/trial_two_models_MQTT_store.py +++ /dev/null @@ -1,110 +0,0 @@ -import sys -import threading - -from xdevs.examples.store_cashier.msg import NewClient, ClientToEmployee -from xdevs.models import Coupled, Port -from xdevs.sim import Coordinator -from xdevs.rt_sim.rt_coord import RealTimeCoordinator -from xdevs.rt_sim.rt_manager import RealTimeManager -import time - -from client_generator import ClientGenerator -from store_queue import StoreQueue -from employee import Employee - -class StoreWithoutGen(Coupled): - def __init__(self, n_employees: int = 10000, mean_employees: float = 30, stddev_employees: float = 0, - name=None): - super().__init__(name) - - queue = StoreQueue() - - self.o_p_queue = Port(ClientToEmployee) - self.add_out_port(self.o_p_queue) - - self.i_port_gen = Port(NewClient, 'Queue_ClientGen') - self.add_in_port(self.i_port_gen) - - self.add_component(queue) - - self.add_coupling(self.i_port_gen, queue.input_new_client) - - self.add_coupling(queue.output_client_to_employee, self.o_p_queue) - - for i in range(n_employees): - employee = Employee(i, mean_employees, stddev_employees) - self.add_component(employee) - self.add_coupling(queue.output_client_to_employee, employee.input_client) - self.add_coupling(employee.output_ready, queue.input_available_employee) -def get_sec(time_str): - h, m, s = time_str.split(':') - return int(h) * 3600 + int(m) * 60 + int(s) - - -def mqtt_parser(msg: str): - c_id, t = msg.split(';') - return NewClient(c_id, t) - -if __name__ == '__main__': - sim_time: float = 52 - n_employees = 3 - mean_employees = 5 - mean_generator = 3 - stddev_employees = 0.8 - stddev_clients = 0.5 - - if len(sys.argv) > 8: - print("Program used with more arguments than accepted. Last arguments will be ignored.") - elif len(sys.argv) < 8: - print( - "Program used with less arguments than accepted. Missing parameters will be set to their default value.") - if len(sys.argv) != 8: - print("Correct usage:") - print("\t" "python3 " + sys.argv[ - 0] + " ") - try: - sim_time = get_sec(sys.argv[1]) - n_employees = int(sys.argv[2]) - mean_employees = float(sys.argv[3]) - mean_generator = float(sys.argv[4]) - stddev_employees = float(sys.argv[5]) - stddev_clients = float(sys.argv[6]) - force_chain = bool(int(sys.argv[7])) - except IndexError: - pass - - print("CONFIGURATION OF THE SCENARIO:") - print("\tSimulation time: {} seconds".format(sim_time)) - print("\tNumber of Employees: {}".format(n_employees)) - print( - "\tMean time required by employee to dispatch clients: {} seconds (standard deviation of {})".format( - mean_employees, stddev_employees)) - print("\tMean time between new clients: {} seconds (standard deviation of {})".format(mean_generator, - stddev_employees)) - #### - conexiones = { - 'Gen_ClientOut': 'Queue_ClientGen' - } - topics = {'RTsys/Output/Gen_ClientOut': 0} - - msg_parser = { - 'Queue_ClientGen': mqtt_parser, - } - - - start = time.time() - storeNOGEN = StoreWithoutGen(n_employees, mean_employees, stddev_employees) - middle = time.time() - print("Model Created. Elapsed time: {} sec".format(middle - start)) - rt_manager = RealTimeManager(max_jitter=0.2, event_window=0.5) - rt_manager.add_input_handler('mqtt_handler', subscriptions=topics, connections=conexiones, msg_parsers=msg_parser) - c = RealTimeCoordinator(storeNOGEN, rt_manager) - middle = time.time() - print("Coordinator and Manager Created. Elapsed time: {} sec".format(middle - start)) - t_ini = time.time() - c.simulate(time_interv=sim_time) - end = time.time() - print(f' Simulation time (s) = {sim_time}') - print("Simulation took: {} sec".format(end - start)) - print(f' Error (%) = ' - f'{((time.time() - start - sim_time) / sim_time) * 100}') \ No newline at end of file diff --git a/xdevs/factory.py b/xdevs/factory.py new file mode 100644 index 0000000..74671c1 --- /dev/null +++ b/xdevs/factory.py @@ -0,0 +1,238 @@ +from __future__ import annotations +import json +from importlib.metadata import entry_points +from typing import ClassVar +from xdevs.abc import InputHandler, OutputHandler, Transducer +from xdevs.models import Atomic, Component, Port, Coupled + + +class InputHandlers: + _plugins: ClassVar[dict[str, type[InputHandler]]] = { + ep.name: ep.load() for ep in entry_points(group='xdevs.input_handlers') + } + + @staticmethod + def add_plugin(name: str, plugin: type[InputHandler]): + """ + Registers a custom input handler to the plugin system. + + :param name: name used to identify the custom input handler. It must be unique. + :param plugin: custom input handler type. Note that it must not be an object, just the class. + """ + if name in InputHandlers._plugins: + raise ValueError(f'xDEVS input_handler plugin with name "{name}" already exists') + InputHandlers._plugins[name] = plugin + + @staticmethod + def create_input_handler(name: str, *args, **kwargs) -> InputHandler: + """ + Creates a new input handler. Note that this is done by the real-time manager. + Users do not directly create input handlers using this method. + + :param name: unique ID of the input handler to be created. + :param kwargs: any additional configuration parameter needed for creating the input handler. + :return: an instance of the InputHandler class. + """ + if name not in InputHandlers._plugins: + raise ValueError(f'xDEVS input_handler plugin with name "{name}" not found') + return InputHandlers._plugins[name](*args, **kwargs) + + +class OutputHandlers: + _plugins: ClassVar[dict[str, type[OutputHandler]]] = { + ep.name: ep.load() for ep in entry_points(group='xdevs.output_handlers') + } + + @staticmethod + def add_plugin(name: str, plugin: type[OutputHandler]): + """ + Registers a custom output handler to the plugin system. + + :param name: name used to identify the custom input handler. It must be unique. + :param plugin: custom input handler type. Note that it must not be an object, just the class. + """ + if name in OutputHandlers._plugins: + raise ValueError(f'xDEVS output_handler plugin with name "{name}" already exists') + OutputHandlers._plugins[name] = plugin + + @staticmethod + def create_output_handler(name: str, *args, **kwargs) -> OutputHandler: + """ + + Creates a new output handler. Note that this is done by the real-time manager. + Users do not directly create output handlers using this method. + + :param name: unique ID of the output handler to be created. + :param kwargs: any additional configuration parameter needed for creating the output handler. + :return: an instance of the OutputHandler class. + """ + if name not in OutputHandlers._plugins: + raise ValueError(f'xDEVS output_handler plugin with name "{name}" not found') + return OutputHandlers._plugins[name](*args, **kwargs) + + +class Wrappers: + _plugins: ClassVar[dict[str, type[Atomic]]] = { + ep.name: ep.load() for ep in entry_points(group='xdevs.wrappers') + } + + @staticmethod + def add_plugin(name: str, plugin: type[Atomic]): + if name in Wrappers._plugins: + raise ValueError(f'xDEVS wrapper plugin with name "{name}" already exists') + Wrappers._plugins[name] = plugin + + @staticmethod + def create_wrapper(name: str, *args, **kwargs) -> Atomic: + if name not in Wrappers._plugins: + raise ValueError(f'xDEVS wrapper plugin with name "{name}" not found') + return Wrappers._plugins[name](*args, **kwargs) + + +class Transducers: + _plugins: ClassVar[dict[str, type[Transducer]]] = { + ep.name: ep.load() for ep in entry_points(group='xdevs.transducers') + } + + @staticmethod + def add_plugin(name: str, plugin: type[Transducer]): + if name in Transducers._plugins: + raise ValueError(f'xDEVS transducer plugin with name "{name}" already exists') + Transducers._plugins[name] = plugin + + @staticmethod + def create_transducer(name: str, *args, **kwargs) -> Transducer: + if name not in Transducers._plugins: + raise ValueError(f'xDEVS transducer plugin with name "{name}" not found') + return Transducers._plugins[name](*args, **kwargs) + + +class Components: + """This class creates components from unique identifiers called "component_id".""" + _plugins: ClassVar[dict[str, type[Component]]] = { + ep.name: ep.load() for ep in entry_points(group='xdevs.components') + } + + @staticmethod + def add_plugin(component_id: str, plugin: type[Component]): + if component_id in Components._plugins: + raise ValueError(f'xDEVS component plugin with name "{component_id}" already exists') + Components._plugins[component_id] = plugin + + @staticmethod + def create_component(component_id: str, *args, **kwargs) -> Component: + if component_id not in Components._plugins: + raise ValueError(f'xDEVS component plugin with name "{component_id}" not found') + return Components._plugins[component_id](*args, **kwargs) + + @staticmethod + def _nested_component(name: str, config: dict) -> Component: + if 'component_id' in config: + # Predefined component, use factory + component_id: str = config['component_id'] + args = config.get('args', []) + kwargs = config.get('kwargs', {}) + kwargs['name'] = name + return Components.create_component(component_id, *args, **kwargs) + elif 'components' in config: + # It is a coupled model + component = Coupled(name) + children: dict[str, Component] = dict() + # Create children components + for component_name, component_config in config['components'].items(): + child = Components._nested_component(component_name, component_config) + children[component_name] = child + component.add_component(child) + # Create connections + for coupling in config.get('couplings', []): + child_from = coupling.get('componentFrom') + child_to = coupling.get('componentTo') + if child_from is not None: + child_from = children[child_from] + port_from = child_from.get_out_port(coupling['portFrom']) + if port_from is None: + raise Exception(f'Invalid coupling in: {coupling}. Reason: portFrom not found') + if child_to is not None: + # this is an IC + child_to = children[child_to] + port_to = child_to.get_in_port(coupling['portTo']) + if port_to is None: + raise Exception(f'Invalid coupling in: {coupling}. Reason: portTo not found') + else: + # this is an EOC + port_to = child_to.get_in_port(coupling['portTo']) + if port_to is None: + port_to = Port(p_type=port_from.p_type, name=coupling['portTo']) + component.add_out_port(port_to) + elif child_to is not None: + # this is an EIC + child_to = children[child_to] + port_to = child_to.get_in_port(coupling['portTo']) + if port_to is None: + raise Exception(f'Invalid coupling in: {coupling}. Reason: portTo not found') + port_from = component.get_in_port(coupling['portFrom']) + if port_from is None: + port_from = Port(p_type=port_to.p_type, name=coupling['portFrom']) + component.add_in_port(port_from) + else: + raise Exception( + f'Invalid coupling in: {coupling}. Reason: componentFrom and componentTo are None') + + component.add_coupling(port_from, port_to) + else: + raise Exception('No component found') + return component + + @staticmethod + def from_json(file_path: str): + """ + A function to parser a JSON file into a DEVS model. The JSON file structure should follow the next rules: + + When adding a component, if it contains the key "component_id", the component will be created using it and the + args and kwargs associated with it. The "component_id" value refers to the key to identify each component in + the class Components. + + When the component does not have the key "component_id", it is assumed to be a coupled model. + Therefore, it must have the keys "components" and "couplings". + This component will be implementing several components and their couplings inside itself. + + The couplings are created using four keys: + - If both componentFrom/To keys are added, the connection will be of the type IC. + - If componentFrom key is missing, the connection will be of the type EIC. + - If componentTo key is missing, the connection will be of the type EOC. + - If any portFrom/To value is missing the connections is not valid. + + Structure: + + - 'MasterComponentName' (dict): The master component. + - 'components' (dict): A dictionary containing multiple components. + - 'ComponentName1' (dict): Iterative component. + - 'components' (dict): Nested components if any. + - 'couplings' (list): List of connection dictionaries. + - 'componentFrom' (str): Name of the component where the connection starts. + - 'portFrom' (str): Port name from 'componentFrom'. + - 'componentTo' (str): Name of the component where the connection ends. + - 'portTo' (str): Port name from 'componentTo'. + - 'ComponentName2' (dict): Single component. + - 'component_id' (str): ID read from the factory for this component. + - 'args' (list): Positional arguments for the component. + - 'kwargs' (dict): Keyword arguments for the component. + - 'a_parameter' (any): A parameter for the component. + - ... : Other keyword arguments if any. + - ... : Additional components if any. + - 'couplings' (list): List of couplings. + - 'componentFrom' (str): Name of the component where the connection starts. + - 'portFrom' (str): Port name from 'componentFrom'. + - 'componentTo' (str): Name of the component where the connection ends. + - 'portTo' (str): Port name from 'componentTo'. + + :param file_path: Path to the JSON file + :return: a DEVS model according to the JSON file + """ + with open(file_path) as f: + data = json.load(f) + + name = list(data.keys())[0] # Gets the actual component name + config = data[name] # Gets the actual component config + + return Components._nested_component(name, config) diff --git a/xdevs/models.py b/xdevs/models.py index 3898a2f..692cbaa 100644 --- a/xdevs/models.py +++ b/xdevs/models.py @@ -3,14 +3,12 @@ import pickle from abc import ABC, abstractmethod from collections import deque, defaultdict -from typing import Generator, Generic, Iterator, Type, TypeVar, Optional -from xdevs import PHASE_ACTIVE, PHASE_PASSIVE, INFINITY - -T = TypeVar('T') +from typing import Generator, Generic, Iterator +from xdevs import PHASE_ACTIVE, PHASE_PASSIVE, INFINITY, T class Port(Generic[T]): - def __init__(self, p_type: Optional[Type[T]] = None, name: Optional[str] = None, serve: bool = False): + def __init__(self, p_type: type[T] | None = None, name: str = None, serve: bool = False): """ xDEVS implementation of DEVS Port. :param p_type: data type of events to be sent/received via the new port instance. @@ -18,7 +16,7 @@ def __init__(self, p_type: Optional[Type[T]] = None, name: Optional[str] = None, :param serve: set to True if the port is going to be accessible via RPC server. Defaults to False. """ self.name: str = name if name else self.__class__.__name__ # Name of the port - self.p_type: Optional[Type[T]] = p_type # Port type. If None, it can contain any type of event. + self.p_type: type[T] | None = p_type # Port type. If None, it can contain any type of event. self.serve: bool = serve # True if port is going to be accessible via RPC server self.parent: Component | None = None # xDEVS Component that owns the port self._values: deque[T] = deque() # Bag containing events directly written to the port @@ -88,13 +86,13 @@ def add_to_bag(self, port: Port[T]): class Component(ABC): - def __init__(self, name: Optional[str] = None): + def __init__(self, name: str = None): """ Abstract Base Class for an xDEVS model. :param name: name of the xDEVS model. Defaults to the name of the component's class. """ self.name: str = name if name else self.__class__.__name__ - self.parent: Optional[Coupled] = None # Parent component of this component + self.parent: Coupled | None = None # Parent component of this component self.input: dict[str, Port] = dict() # Dictionary containing all the component's input ports by name self.output: dict[str, Port] = dict() # Dictionary containing all the component's output ports by name # TODO make these lists private @@ -104,7 +102,7 @@ def __init__(self, name: Optional[str] = None): def __str__(self) -> str: in_str = " ".join([p.name for p in self.in_ports]) out_str = " ".join([p.name for p in self.out_ports]) - return '%s: InPorts[%s] OutPorts[%s]' % (self.name, in_str, out_str) + return f'{self.name}: InPorts[{in_str}] OutPorts[{out_str}]' def __repr__(self): return self.name @@ -161,11 +159,11 @@ def add_out_port(self, port: Port): def get_in_port(self, name) -> Port | None: """:return: Input port with the given name. If port is not found, returns None.""" - self.input.get(name) + return self.input.get(name) def get_out_port(self, name) -> Port | None: """:return: Output port with the given name. If port is not found, returns None.""" - self.output.get(name) + return self.output.get(name) class Coupling(Generic[T]): @@ -202,7 +200,7 @@ def propagate(self): class Atomic(Component, ABC): - def __init__(self, name: Optional[str] = None): + def __init__(self, name: str = None): """ xDEVS implementation of DEVS Atomic Model. :param name: name of the atomic model. If no name is provided, it will take the class's name by default. @@ -212,7 +210,6 @@ def __init__(self, name: Optional[str] = None): self.phase: str = PHASE_PASSIVE self.sigma: float = INFINITY - @property def ta(self) -> float: """:return: remaining time for the atomic model's internal transition.""" return self.sigma @@ -238,11 +235,8 @@ def lambdaf(self): """Describes the output function of the atomic model.""" pass - def deltcon(self, e: float): - """ - Describes the confluent transitions of the atomic model. By default, the internal transition is triggered first. - :param e: elapsed time between last transition and the confluent transition. - """ + def deltcon(self): + """Confluent transitions of the atomic model. By default, internal transition is triggered first.""" self.deltint() self.deltext(0) @@ -280,7 +274,7 @@ def continuef(self, e: float): class Coupled(Component, ABC): - def __init__(self, name: Optional[str] = None): + def __init__(self, name: str = None): """ xDEVS implementation of DEVS Coupled Model. :param name: name of the coupled model. If no name is provided, it will take the class's name by default. diff --git a/xdevs/plugins/input_handlers/bad_dependencies.py b/xdevs/plugins/input_handlers/bad_dependencies.py index e972b1b..6c864c3 100644 --- a/xdevs/plugins/input_handlers/bad_dependencies.py +++ b/xdevs/plugins/input_handlers/bad_dependencies.py @@ -1,5 +1,5 @@ from abc import ABC -from xdevs.rt_sim.input_handler import InputHandler +from xdevs.abc.handler import InputHandler class BadDependenciesHandler(InputHandler, ABC): diff --git a/xdevs/plugins/input_handlers/csv_input_handler.py b/xdevs/plugins/input_handlers/csv.py similarity index 97% rename from xdevs/plugins/input_handlers/csv_input_handler.py rename to xdevs/plugins/input_handlers/csv.py index 2d8b5b9..581614f 100644 --- a/xdevs/plugins/input_handlers/csv_input_handler.py +++ b/xdevs/plugins/input_handlers/csv.py @@ -1,7 +1,7 @@ import csv import sys import time -from xdevs.rt_sim.input_handler import InputHandler +from xdevs.abc.handler import InputHandler class CSVInputHandler(InputHandler): diff --git a/xdevs/plugins/input_handlers/callable_function.py b/xdevs/plugins/input_handlers/function.py similarity index 60% rename from xdevs/plugins/input_handlers/callable_function.py rename to xdevs/plugins/input_handlers/function.py index ac8d671..880b0d2 100644 --- a/xdevs/plugins/input_handlers/callable_function.py +++ b/xdevs/plugins/input_handlers/function.py @@ -1,12 +1,10 @@ -from xdevs.rt_sim.input_handler import InputHandler +from xdevs.abc.handler import InputHandler class CallableFunction(InputHandler): def __init__(self, **kwargs): super().__init__(**kwargs) - self.function = kwargs.get('function') - if self.function is None: - raise ValueError('function is mandatory') + self.function = kwargs['function'] self.args = kwargs.get('f_args', list()) self.kwargs = kwargs.get('f_kwargs', dict()) diff --git a/xdevs/plugins/input_handlers/mqtt_input_handler.py b/xdevs/plugins/input_handlers/mqtt.py similarity index 95% rename from xdevs/plugins/input_handlers/mqtt_input_handler.py rename to xdevs/plugins/input_handlers/mqtt.py index 885a5af..0cb0e2e 100644 --- a/xdevs/plugins/input_handlers/mqtt_input_handler.py +++ b/xdevs/plugins/input_handlers/mqtt.py @@ -1,12 +1,9 @@ -import datetime import queue import threading try: from paho.mqtt.client import Client - from xdevs.rt_sim.input_handler import InputHandler - - + from xdevs.abc.handler import InputHandler # Desde este input handler me subscribo a topics para ver los mensajes que entran # ruta: RTsys/coupled_name/input/port_name y to_do lo que llegue a ese puerto se inyecta. @@ -95,9 +92,9 @@ def run(self): IN.run() except ImportError: - from xdevs.plugins.input_handlers.bad_dependencies import BadDependenciesHandler + from .bad_dependencies import BadDependenciesHandler + - class MQTTInputHandler(BadDependenciesHandler): def __init__(self, **kwargs): super().__init__(handler_type='mqtt', **kwargs) diff --git a/xdevs/plugins/input_handlers/tcp_input_handler.py b/xdevs/plugins/input_handlers/tcp.py similarity index 97% rename from xdevs/plugins/input_handlers/tcp_input_handler.py rename to xdevs/plugins/input_handlers/tcp.py index 259b3f8..2b1a2b5 100644 --- a/xdevs/plugins/input_handlers/tcp_input_handler.py +++ b/xdevs/plugins/input_handlers/tcp.py @@ -1,12 +1,12 @@ from __future__ import annotations import queue -import socket import threading from typing import Any from xdevs.plugins.util.socket_server import SocketServer -from xdevs.rt_sim.input_handler import InputHandler +from xdevs.abc.handler import InputHandler import socket + class TCPInputHandler(InputHandler): # TODO cambiar a SocketServerInputHandler (más generico que TCP, abre la puerta a SocketClientInputHandler) def __init__(self, **kwargs): """ diff --git a/xdevs/plugins/output_handlers/bad_dependencies.py b/xdevs/plugins/output_handlers/bad_dependencies.py index 2af008e..b1fe605 100644 --- a/xdevs/plugins/output_handlers/bad_dependencies.py +++ b/xdevs/plugins/output_handlers/bad_dependencies.py @@ -1,5 +1,5 @@ from abc import ABC -from xdevs.rt_sim.output_handler import OutputHandler +from xdevs.abc.handler import OutputHandler class BadDependenciesHandler(OutputHandler, ABC): @@ -9,7 +9,7 @@ def __init__(self, **kwargs): :param str handler_type: transducer type. """ super().__init__(**kwargs) - raise ImportError(f'{kwargs.get('handler_type')} input handler specific dependencies are not imported') + raise ImportError(f'{kwargs['handler_type']} input handler specific dependencies are not imported') def run(self): pass diff --git a/xdevs/plugins/output_handlers/csv_output_handler.py b/xdevs/plugins/output_handlers/csv.py similarity index 80% rename from xdevs/plugins/output_handlers/csv_output_handler.py rename to xdevs/plugins/output_handlers/csv.py index 3e09701..701b0ac 100644 --- a/xdevs/plugins/output_handlers/csv_output_handler.py +++ b/xdevs/plugins/output_handlers/csv.py @@ -1,7 +1,6 @@ import csv -import datetime import time -from xdevs.rt_sim.output_handler import OutputHandler +from xdevs.abc.handler import OutputHandler class CSVOutputHandler(OutputHandler): @@ -14,11 +13,10 @@ def __init__(self, **kwargs): :param str delimiter: column delimiter in CSV file. By default, it is set to ','. """ super().__init__() - self.file = kwargs.get('file', 'output.csv') # better to have a fixed name for automation + self.file = kwargs.get('file', 'output.csv') self.delimiter: str = kwargs.get('delimiter', ',') def run(self): - print('CSV running...') initial_time = time.time() # in general, it is not a good idea to append to an existing file when logging a simulation with open(self.file, 'w', newline='') as file: @@ -28,4 +26,3 @@ def run(self): port, msg = self.queue.get() # blocks indefinitely until it receives a message writer.writerow((time.time() - initial_time, port, msg)) file.flush() - # no need to close the file, the with open block does the trick diff --git a/xdevs/plugins/output_handlers/mqtt.py b/xdevs/plugins/output_handlers/mqtt.py new file mode 100644 index 0000000..a6d6040 --- /dev/null +++ b/xdevs/plugins/output_handlers/mqtt.py @@ -0,0 +1,38 @@ +from typing import Callable, Any + +try: + from xdevs.abc.handler import OutputHandler + from ..input_handlers import MQTTClient + + + class MQTTOutputHandler(OutputHandler): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.host = kwargs.get('host', 'test.mosquitto.org') + self.port = kwargs.get('port', 1883) + self.keepalive = kwargs.get('keepalive', 60) + + self.client = MQTTClient() + + self.topic: str = kwargs.get('topic', 'RTsys') + + self.event_parser: Callable[[str, Any], str] = kwargs.get('event_parser', + lambda port, msg: (f'{self.topic}/output/{port}', msg)) + + def initialize(self): + self.client.connect(self.host, self.port, self.keepalive) + + def run(self): + while True: + topic, payload = self.pop_event() + self.client.publish(topic, payload) + + +except ImportError: + from .bad_dependencies import BadDependenciesHandler + + + class MQTTOutputHandler(BadDependenciesHandler): + def __init__(self, **kwargs): + super().__init__(handler_type='mqtt', **kwargs) diff --git a/xdevs/plugins/output_handlers/mqtt_output_handler.py b/xdevs/plugins/output_handlers/mqtt_output_handler.py deleted file mode 100644 index 1457bd0..0000000 --- a/xdevs/plugins/output_handlers/mqtt_output_handler.py +++ /dev/null @@ -1,63 +0,0 @@ -import datetime -import threading -import time -from typing import Callable, Any - -try: - from xdevs.plugins.input_handlers.mqtt_input_handler import MQTTClient - from xdevs.rt_sim.output_handler import OutputHandler - - - class MQTTOutputHandler(OutputHandler): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.host = kwargs.get('host', 'test.mosquitto.org') - self.port = kwargs.get('port', 1883) - self.keepalive = kwargs.get('keepalive', 60) - - self.client = MQTTClient() - - self.topic: str = kwargs.get('topic', 'RTsys') - - self.event_parser: Callable[[str, Any], str] = kwargs.get('event_parser', - lambda port, msg: (f'{self.topic}/Output/{port}', msg)) - - def initialize(self): - self.client.connect(self.host, self.port, self.keepalive) - print('MQTT connected') - - def run(self): - print('MQTT running...') - while True: - topic, payload = self.pop_event() - self.client.publish(topic, payload) - print(f'MQTT sends ') #: {topic} : {payload} > {datetime.datetime.now()}') - - - if __name__ == '__main__': - def inject_msg(): - print(f'Thread active') - for i in range(20): - OUT.queue.put(('Port', f' msg: {i} ')) - print(f'Msg in q and i = {i}') - if i == 3: - time.sleep(15) - else: - time.sleep(2.5) - print('Closing...') - - - OUT = MQTTOutputHandler() - OUT.initialize() - t = threading.Thread(target=inject_msg, daemon=True) - t.start() - OUT.run() - -except ImportError: - from xdevs.plugins.output_handlers.bad_dependencies import BadDependenciesHandler - - - class MQTTOutputHandler(BadDependenciesHandler): - def __init__(self, **kwargs): - super().__init__(handler_type='mqtt', **kwargs) diff --git a/xdevs/plugins/output_handlers/tcp_output_handler.py b/xdevs/plugins/output_handlers/tcp.py similarity index 86% rename from xdevs/plugins/output_handlers/tcp_output_handler.py rename to xdevs/plugins/output_handlers/tcp.py index 380ea4e..1b78f66 100644 --- a/xdevs/plugins/output_handlers/tcp_output_handler.py +++ b/xdevs/plugins/output_handlers/tcp.py @@ -1,10 +1,8 @@ -import socket import time -import threading from typing import Any, Callable from xdevs.plugins.util.socket_server import SocketServer -from xdevs.rt_sim.output_handler import OutputHandler +from xdevs.abc.handler import OutputHandler class TCPOutputHandler(OutputHandler): # TODO cambiar a SocketClientOutputHandler (más generico que TCP, abre la puerta a SocketServerOutputHandler) @@ -82,24 +80,3 @@ def run(self): # If a system error occurred when connecting, we assume that the server has been shut down. print(f'Error while connecting to server: {e}') break - - -if __name__ == '__main__': - - def inject_msg(): - print(f'Thread active') - for i in range(20): - TCP.queue.put(('Port', f' msg: {i} ')) - print(f'Msg in q and i = {i}') - if i == 3: - time.sleep(15) - else: - time.sleep(2.5) - TCP.exit() - print('Closing...') - - - TCP = TCPOutputHandler(host='LocalHost', port=4321) - t = threading.Thread(target=inject_msg, daemon=True) - t.start() - TCP.run() diff --git a/xdevs/plugins/transducers/bad_dependencies_transducer.py b/xdevs/plugins/transducers/bad_dependencies.py similarity index 74% rename from xdevs/plugins/transducers/bad_dependencies_transducer.py rename to xdevs/plugins/transducers/bad_dependencies.py index 6981433..d03cbf2 100644 --- a/xdevs/plugins/transducers/bad_dependencies_transducer.py +++ b/xdevs/plugins/transducers/bad_dependencies.py @@ -1,5 +1,5 @@ from abc import ABC -from xdevs.transducers import Transducer +from xdevs.abc.transducer import Transducer class BadDependenciesTransducer(Transducer, ABC): @@ -9,7 +9,7 @@ def __init__(self, **kwargs): :param str transducer_type: transducer type. """ super().__init__(**kwargs) - raise ImportError('{} transducer specific dependencies are not imported'.format(kwargs.get('transducer_type'))) + raise ImportError(f'{kwargs.get('transducer_type')} transducer specific dependencies are not imported') def create_known_data_types_map(self): pass diff --git a/xdevs/plugins/transducers/csv_transducer.py b/xdevs/plugins/transducers/csv.py similarity index 86% rename from xdevs/plugins/transducers/csv_transducer.py rename to xdevs/plugins/transducers/csv.py index f93ba46..aa2bdc2 100644 --- a/xdevs/plugins/transducers/csv_transducer.py +++ b/xdevs/plugins/transducers/csv.py @@ -2,7 +2,7 @@ import csv import os from typing import Any, Iterable, Type -from xdevs.transducers import Transducer +from xdevs.abc.transducer import Transducer class CSVTransducer(Transducer): @@ -54,12 +54,6 @@ def bulk_data(self, sim_time: float): self.event_csv_writer.writerow([event_insert[field] for field in self.event_header]) def _create_csv_file(self, filename: str, header: list[str]): - # 1. If output file already exist, we ask the use if he/she wants to overwrite it. - # if os.path.exists(filename): - # print('Transducer output file {} already exists.'.format(filename)) - # if input('Do you want to overwrite it? [Y/n] >').lower() in ['n', 'no']: - # raise FileExistsError('File already exists and user does not want to overwrite it') - # 2. If directory that will contain the file does not exist, we create it. os.makedirs(os.path.dirname(filename), exist_ok=True) diff --git a/xdevs/plugins/transducers/elasticsearch_transducer.py b/xdevs/plugins/transducers/elasticsearch.py similarity index 97% rename from xdevs/plugins/transducers/elasticsearch_transducer.py rename to xdevs/plugins/transducers/elasticsearch.py index 0e971e7..b0cb412 100644 --- a/xdevs/plugins/transducers/elasticsearch_transducer.py +++ b/xdevs/plugins/transducers/elasticsearch.py @@ -1,6 +1,6 @@ from __future__ import annotations import logging -from xdevs.transducers import Transducer +from xdevs.abc.transducer import Transducer try: from elasticsearch import Elasticsearch @@ -81,7 +81,7 @@ def create_index(self, index_name: str, field_properties: dict[str, dict[str, st except ModuleNotFoundError: - from .bad_dependencies_transducer import BadDependenciesTransducer + from .bad_dependencies import BadDependenciesTransducer class ElasticsearchTransducer(BadDependenciesTransducer): diff --git a/xdevs/plugins/transducers/sql_transducer.py b/xdevs/plugins/transducers/sql.py similarity index 97% rename from xdevs/plugins/transducers/sql_transducer.py rename to xdevs/plugins/transducers/sql.py index b751ca2..e8cd62c 100644 --- a/xdevs/plugins/transducers/sql_transducer.py +++ b/xdevs/plugins/transducers/sql.py @@ -1,5 +1,5 @@ from __future__ import annotations -from xdevs.transducers import Transducer +from xdevs.abc.transducer import Transducer try: from sqlalchemy import create_engine, text, Column, Float, Integer, MetaData, String, Table @@ -85,7 +85,7 @@ def create_table(self, table_name: str, columns: list[Column], except ModuleNotFoundError: - from .bad_dependencies_transducer import BadDependenciesTransducer + from .bad_dependencies import BadDependenciesTransducer class SQLTransducer(BadDependenciesTransducer): diff --git a/xdevs/plugins/util/socket_server.py b/xdevs/plugins/util/socket_server.py index 6883eea..502200d 100644 --- a/xdevs/plugins/util/socket_server.py +++ b/xdevs/plugins/util/socket_server.py @@ -92,5 +92,3 @@ def start_oh(self): c_thread = threading.Thread(target=output_client_handler, daemon=True, args=(self.server_socket, self.server_address, self.output_queue)) c_thread.start() - - diff --git a/xdevs/plugins/wrappers/bad_dependencies.py b/xdevs/plugins/wrappers/bad_dependencies.py new file mode 100644 index 0000000..d37c58e --- /dev/null +++ b/xdevs/plugins/wrappers/bad_dependencies.py @@ -0,0 +1,27 @@ +from abc import ABC +from xdevs.models import Atomic + + +class BadDependenciesWrapper(Atomic, ABC): + def __init__(self, **kwargs): + """ + Template wrapper for using when dependencies are not installed. + :param str wrapper_type: wrapper type. + """ + super().__init__(**kwargs) + raise ImportError(f'{kwargs['wrapper_type']} wrapper specific dependencies are not installed') + + def deltint(self): + pass + + def deltext(self, e: float): + pass + + def lambdaf(self): + pass + + def initialize(self): + pass + + def exit(self): + pass diff --git a/xdevs/plugins/wrappers/pypdevs.py b/xdevs/plugins/wrappers/pypdevs.py index 224f1f8..01eb3d2 100644 --- a/xdevs/plugins/wrappers/pypdevs.py +++ b/xdevs/plugins/wrappers/pypdevs.py @@ -1,71 +1,76 @@ -import logging -from xdevs.models import Atomic, Port - try: from pypdevs.DEVS import AtomicDEVS from pypdevs.minimal import AtomicDEVS as AtomicDEVSMin -except ImportError: - logging.warning("pypdevs module not installed.") + from xdevs.models import Atomic, Port + + + def update_sigma_on_state_change(delt_func): + def inner(self, *args, **kwargs): + prev_state = self.phase + delt_func(self, *args, **kwargs) + if prev_state != self.phase: + self.sigma = self.atomic.timeAdvance() + return inner -def update_sigma_on_state_change(delt_func): - def inner(self, *args, **kwargs): - prev_state = self.phase - delt_func(self, *args, **kwargs) - if prev_state != self.phase: - self.sigma = self.atomic.timeAdvance() - return inner + class PyPDEVSWrapper(Atomic): + def __init__(self, atomic: AtomicDEVS or AtomicDEVSMin): + super().__init__() + self.atomic: AtomicDEVS or AtomicDEVSMin = atomic + self.pypdevs_in_ports = {} # IO ports dictionaries to efficiently manage pypdevs ports + self.xdevs_out_ports = {} -class PyPDEVSWrapper(Atomic): + for pypdevs_in_port in self.atomic.IPorts: + xdevs_in_port = Port(None, pypdevs_in_port.name) + self.add_in_port(xdevs_in_port) + self.pypdevs_in_ports[pypdevs_in_port.name] = pypdevs_in_port + setattr(self, pypdevs_in_port.name, xdevs_in_port) - def __init__(self, atomic: AtomicDEVS or AtomicDEVSMin): - super().__init__() - self.atomic: AtomicDEVS or AtomicDEVSMin = atomic - self.pypdevs_in_ports = {} # IO ports dictionaries to efficiently manage pypdevs ports - self.xdevs_out_ports = {} + for pypdevs_out_port in self.atomic.OPorts: + xdevs_out_port = Port(None, pypdevs_out_port.name) + self.add_out_port(xdevs_out_port) + self.xdevs_out_ports[pypdevs_out_port.name] = xdevs_out_port + setattr(self, pypdevs_out_port.name, xdevs_out_port) - for pypdevs_in_port in self.atomic.IPorts: - xdevs_in_port = Port(None, pypdevs_in_port.name) - self.add_in_port(xdevs_in_port) - self.pypdevs_in_ports[pypdevs_in_port.name] = pypdevs_in_port - setattr(self, pypdevs_in_port.name, xdevs_in_port) + @update_sigma_on_state_change + def deltint(self): + self.phase = self.atomic.state = self.atomic.intTransition() - for pypdevs_out_port in self.atomic.OPorts: - xdevs_out_port = Port(None, pypdevs_out_port.name) - self.add_out_port(xdevs_out_port) - self.xdevs_out_ports[pypdevs_out_port.name] = xdevs_out_port - setattr(self, pypdevs_out_port.name, xdevs_out_port) + @update_sigma_on_state_change + def deltext(self, e: float): + self.phase = self.atomic.state = self.atomic.extTransition(self._inputs_to_dict()) + self.continuef(e) - @update_sigma_on_state_change - def deltint(self): - self.phase = self.atomic.state = self.atomic.intTransition() + def lambdaf(self) -> None: + outputs = self.atomic.outputFnc() - @update_sigma_on_state_change - def deltext(self, e: float): - self.phase = self.atomic.state = self.atomic.extTransition(self._inputs_to_dict()) - self.continuef(e) + for pypdevs_out_port, values in outputs.items(): + if len(values) > 0: + xdevs_out_port = self.xdevs_out_ports[pypdevs_out_port.name] + xdevs_out_port.extend(values) - def lambdaf(self) -> None: - outputs = self.atomic.outputFnc() + def initialize(self) -> None: + pass - for pypdevs_out_port, values in outputs.items(): - if len(values) > 0: - xdevs_out_port = self.xdevs_out_ports[pypdevs_out_port.name] - xdevs_out_port.extend(values) + def exit(self) -> None: + pass - def initialize(self) -> None: - pass + def _inputs_to_dict(self): + in_values = {} - def exit(self) -> None: - pass + for in_port in self.in_ports: + in_port_values = list(in_port.values) + in_values[self.pypdevs_in_ports[in_port.name]] = in_port_values - def _inputs_to_dict(self): - in_values = {} + return in_values + + +except ImportError: + from .bad_dependencies import BadDependenciesWrapper - for in_port in self.in_ports: - in_port_values = list(in_port.values) - in_values[self.pypdevs_in_ports[in_port.name]] = in_port_values - return in_values + class PyPDEVSWrapper(BadDependenciesWrapper): + def __init__(self, **kwargs): + super().__init__(wrapper_type='pypdevs') diff --git a/xdevs/rt_sim/rt_manager.py b/xdevs/rt.py similarity index 66% rename from xdevs/rt_sim/rt_manager.py rename to xdevs/rt.py index aa622aa..e800e97 100644 --- a/xdevs/rt_sim/rt_manager.py +++ b/xdevs/rt.py @@ -4,9 +4,9 @@ import threading import time from typing import Any -from xdevs.models import Port -from xdevs.rt_sim.input_handler import InputHandler, InputHandlers -from xdevs.rt_sim.output_handler import OutputHandler, OutputHandlers +from xdevs.factory import InputHandler, InputHandlers, OutputHandler, OutputHandlers +from xdevs.models import Coupled, Port +from xdevs.sim import Coordinator def run_handler(handler: InputHandler | OutputHandler): @@ -48,24 +48,24 @@ def __init__(self, max_jitter: float = None, time_scale: float = 1, event_window self.input_handlers: list[InputHandler] = list() self.output_handlers: list[OutputHandler] = list() - def add_input_handler(self, handler_id: str, **kwargs): + def add_input_handler(self, handler_id: str, *args, **kwargs): """ Add a new InputHandler to the system. :param handler_id: unique ID of the input handler to be created. :param kwargs: any additional configuration parameter needed for creating the input handler. """ - i_handler = InputHandlers.create_input_handler(handler_id, **kwargs, queue=self.input_queue) + i_handler = InputHandlers.create_input_handler(handler_id, *args, **kwargs, queue=self.input_queue) self.input_handlers.append(i_handler) - def add_output_handler(self, handler_id: str, **kwargs): + def add_output_handler(self, handler_id: str, *args, **kwargs): """ Add a new OutputHandler to the system. :param handler_id: unique ID of the output handler to be created. :param kwargs: any additional configuration parameter needed for creating the output handler. """ - o_handler = OutputHandlers.create_output_handler(handler_id, **kwargs) + o_handler = OutputHandlers.create_output_handler(handler_id, *args, **kwargs) self.output_handlers.append(o_handler) def initialize(self, initial_t: float): @@ -87,12 +87,12 @@ def initialize(self, initial_t: float): def exit(self, final_t: float): self.last_v_time = final_t - def sleep(self, next_v_time: float) -> tuple[float, list[tuple[Any, Any]]]: + def wait_until(self, next_v_time: float) -> tuple[float, list[tuple[str, Any]]]: """ Function that implements the real time specification by waiting for ingoing events to the system. - :param next_v_time: simulation time of the next cycle. - :return: a tuple of: actual simulation time when function returned and list of ingoing events. + :param next_v_time: simulation time of the next internal event in the simulation. + :return: a tuple of: actual simulation time when function returned and list of input events. """ next_r_time = self.last_r_time + (next_v_time - self.last_v_time) * self.time_scale events: list[tuple[str, Any]] = list() @@ -107,20 +107,18 @@ def sleep(self, next_v_time: float) -> tuple[float, list[tuple[Any, Any]]]: except queue.Empty: break # event window timeout, we are done with messages # Finally, we compute the current time. Must be between last_r_time and next_r_time - r_time = min(next_r_time, time.time()) - v_time = (r_time - self.initial_r_time) / self.time_scale - self.last_v_time = v_time - self.last_r_time = r_time + self.last_r_time = min(next_r_time, time.time()) + self.last_v_time = min(next_v_time, (self.last_r_time - self.initial_r_time) / self.time_scale) except queue.Empty: # we did not receive any message, just update the time - self.last_v_time = next_v_time self.last_r_time = next_r_time + self.last_v_time = next_v_time # If needed, we check that the jitter is not too big if self.max_jitter is not None and abs(time.time() - self.last_r_time) > self.max_jitter: raise RuntimeError('maximum jitter exceeded.') return self.last_v_time, events - def output_messages(self, port: Port): + def propagate_output(self, port: Port): """ An outgoing event is inserted in the queues of all OutputHandlers. @@ -129,3 +127,49 @@ def output_messages(self, port: Port): for o_handler in self.output_handlers: for msg in port.values: o_handler.queue.put((port.name, msg)) + + +class RealTimeCoordinator(Coordinator): + def __init__(self, model: Coupled, manager: RealTimeManager): + super().__init__(model) + self.manager: RealTimeManager = manager + + def initialize(self): + super().initialize() + self.manager.initialize(self.clock.time) + + def exit(self): + self.manager.exit(self.clock.time) + super().exit() + + def simulate_iters(self, time_interv: float = float("inf")): + self.initialize() + while self.clock.time < time_interv: + if self.time_next == float("inf") and not self.manager.input_handlers: + break + # SLEEP UNTIL NEXT STATE TRANSITION + t, msgs = self.manager.wait_until(min(time_interv, self.time_next)) + # INJECT EXTERNAL EVENTS (if any) + for port_id, msg in msgs: + port = self.model.get_in_port(port_id) + if port is not None: + try: + port.add(msg) + except TypeError as e: + print(f'invalid message type: {e}', file=sys.stderr) + else: + print(f'input port "{port_id}" does not exit', file=sys.stderr) + # UPDATE SIMULATION CLOCK + self.clock.time = t + # EXECUTE NEXT CYCLE (if applies) + if self.clock.time == self.time_next: + self.lambdaf() + self.deltfcn() + # EXECUTE TRANSDUCERS (if any) + self._execute_transducers() + # EJECT NEW OUTPUT EVENTS + for port in self.model.out_ports: + self.manager.propagate_output(port) + # CLEAR THE PORTS OF THE MODEL + self.clear() + self.exit() diff --git a/xdevs/rt_sim/__init__.py b/xdevs/rt_sim/__init__.py deleted file mode 100644 index 4e4f02e..0000000 --- a/xdevs/rt_sim/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .rt_coord import RealTimeCoordinator -from .rt_manager import RealTimeManager diff --git a/xdevs/rt_sim/input_handler.py b/xdevs/rt_sim/input_handler.py deleted file mode 100644 index 37cf22d..0000000 --- a/xdevs/rt_sim/input_handler.py +++ /dev/null @@ -1,106 +0,0 @@ -from __future__ import annotations - -import datetime -from abc import ABC, abstractmethod -from typing import ClassVar, Type, Callable, Any -import sys -import pkg_resources - -from xdevs.rt_sim.mqtt_connector import Connector - - -class InputHandler(ABC): - def __init__(self, **kwargs): - """ - Handler interface for injecting external events to the system. - - :param queue: used to collect and inject all external events joining the system. - :param Callable[[Any], tuple[str, str]] event_parser: event parser function. It transforms incoming events - into tuples (port, message). Note that both are represented as strings. Messages need further parsing. - :param dict[str, Callable[[str], Any]] msg_parsers: message parsers. Keys are port names, and values are - functions that take a string and returns an object of the corresponding port type. If a parser is not - defined, the input handler assumes that the port type is str and forward the message as is. By default, all - the ports are assumed to accept str objects. - """ - self.queue = kwargs.get('queue') - if self.queue is None: - raise ValueError('queue is mandatory') - self.event_parser: Callable[[Any], tuple[str, str]] | None = kwargs.get('event_parser') - self.msg_parsers: dict[str, Callable[[str], Any]] = kwargs.get('msg_parsers', dict()) - - self.connections: dict[str, str] = kwargs.get('connections', None) - self.connector = Connector(conections=self.connections) - - def initialize(self): - """Performs any task before calling the run method. It is implementation-specific. By default, it is empty.""" - pass - - def exit(self): - """Performs any task after the run method. It is implementation-specific. By default, it is empty.""" - pass - - @abstractmethod - def run(self): - """Execution of the input handler. It is implementation-specific""" - pass - - def push_event(self, event: Any): - """Parses event as tuple port-message and pushes it to the queue.""" - try: - port, msg = self.event_parser(event) - # AQUI IRIA EL CONECTOR MQTT; para corregir el puerto en cuestion - port = self.connector.input_handler(port) - except Exception as e: - # if an exception is triggered while parsing the event, we ignore it - print(f'error parsing input event ("{event}"): {e}. Event will be ignored', file=sys.stderr) - return - #print(f'HAGO PUSH MSG DE {port},{msg}') - self.push_msg(port, msg) - - def push_msg(self, port: str, msg: str): - """Parses the message as the proper object and pushes it to the queue.""" - #print(f'Entro en push_msg con port ->{port}') - try: - # if parser is not defined, we forward the message as is (i.e., in string format) - msg = self.msg_parsers.get(port, lambda x: x)(msg) - #print(f'EL msg = {msg}') - except Exception as e: - # if an exception is triggered while parsing the message, we ignore it - print(f'error parsing input msg ("{msg}") in port {port}: {e}. Message will be ignored', file=sys.stderr) - return - #print('###') - #print(f'EVENTO ENTRA EN LA COLA EN:{datetime.datetime.time()}') - self.queue.put((port, msg)) - #print(f'COla = {self.queue}, puse en {port}:{msg}') - - -class InputHandlers: - _plugins: ClassVar[dict[str, Type[InputHandler]]] = { - ep.name: ep.load() for ep in pkg_resources.iter_entry_points('xdevs.plugins.input_handlers') - } - - @staticmethod - def add_plugin(name: str, plugin: Type[InputHandler]): - """ - Registers a custom input handler to the plugin system. - - :param name: name used to identify the custom input handler. It must be unique. - :param plugin: custom input handler type. Note that it must not be an object, just the class. - """ - if name in InputHandlers._plugins: - raise ValueError('xDEVS input_handler plugin with name "{}" already exists'.format(name)) - InputHandlers._plugins[name] = plugin - - @staticmethod - def create_input_handler(name: str, **kwargs) -> InputHandler: - """ - Creates a new input handler. Note that this is done by the real-time manager. - Users do not directly create input handlers using this method. - - :param name: unique ID of the input handler to be created. - :param kwargs: any additional configuration parameter needed for creating the input handler. - :return: an instance of the InputHandler class. - """ - if name not in InputHandlers._plugins: - raise ValueError('xDEVS input_handler plugin with name "{}" not found'.format(name)) - return InputHandlers._plugins[name](**kwargs) diff --git a/xdevs/rt_sim/mqtt_connector.py b/xdevs/rt_sim/mqtt_connector.py deleted file mode 100644 index 029af14..0000000 --- a/xdevs/rt_sim/mqtt_connector.py +++ /dev/null @@ -1,23 +0,0 @@ -class Connector(): - def __init__(self, conections: dict[str, str]): - - """ - Función para conectar de forma correcta los puertos (que usen protocolo MQTT) - - :param conections: dict[key: str, value: str]. Donde la key es el puerto de al que me quiero conectar y el - value es el puerto de mi acoplado. - """ - - self.connections: dict[str, str] = conections - - def input_handler(self, port: str): - #print(f'Le paso port = {port}') - #print(self.connections) - if self.connections is not None: - for key, value in self.connections.items(): - if port == key: - return value - #print(f'Devuelvo port = {port}') - return port - - diff --git a/xdevs/rt_sim/output_handler.py b/xdevs/rt_sim/output_handler.py deleted file mode 100644 index ee51864..0000000 --- a/xdevs/rt_sim/output_handler.py +++ /dev/null @@ -1,97 +0,0 @@ -from __future__ import annotations -import queue -from abc import ABC, abstractmethod -import sys -from typing import Any, Callable, ClassVar, Type - -import pkg_resources - - -class OutputHandler(ABC): - def __init__(self, **kwargs): - """ - Handler interface for ejecting internal events from the system. - - :param queue.SimpleQueue() queue: is the queue where all the desired events to be ejected are putted. - :param Callable[[str, str], Any] event_parser: event parser function. It transforms incoming tuples - (port, message) into events. Note that both are represented as strings. - :param dict[str, Callable[[Any], str]] msg_parser: message parsers. Keys are port names, and values are - functions that take a string and returns an object of the corresponding port type. If a parser is not - defined, the output handler assumes that the port type is str and forward the message as is. By default, all - the ports are assumed to accept str objects. - - TODO documentation - """ - self.queue = queue.SimpleQueue() - self.event_parser: Callable[[str, str], Any] | None = kwargs.get('event_parser') - self.msg_parsers: dict[str, Callable[[Any], str]] = kwargs.get('msg_parsers', dict()) - - def initialize(self): - """Performs any task before calling the run method. It is implementation-specific. By default, it is empty.""" - pass - - def exit(self): - """Performs any task before calling the run method. It is implementation-specific. By default, it is empty.""" - pass - - @abstractmethod - def run(self): - """Execution of the output handler. It is implementation-specific""" - pass - - def pop_event(self) -> Any: - """Waits until it receives an outgoing event and parses it with the desired format.""" - while True: - port, msg = self.pop_msg() - # print(f'POP_EVENT: recibo port = {port} y msg = {msg}') - try: - event = self.event_parser(port, msg) - except Exception as e: - print(f'error parsing output event ("{port}","{msg}"): {e}. Event will be ignored', file=sys.stderr) - continue - return event - - def pop_msg(self) -> tuple[str, str]: - """Waits until it receives an outgoing message and returns the port and message in string format.""" - while True: - port, msg = self.queue.get() - # print(f'POP_MSG: recibo port = {port} y msg = {msg}') - try: - msg = self.msg_parsers.get(port, lambda x: str(x))(msg) - except Exception as e: - print(f'error parsing output msg ("{msg}"): {e}. Message will be ignored', file=sys.stderr) - continue - return port, msg - - -class OutputHandlers: - _plugins: ClassVar[dict[str, Type[OutputHandler]]] = { - ep.name: ep.load() for ep in pkg_resources.iter_entry_points('xdevs.plugins.output_handlers') - } - - @staticmethod - def add_plugin(name: str, plugin: Type[OutputHandler]): - """ - Registers a custom output handler to the plugin system. - - :param name: name used to identify the custom input handler. It must be unique. - :param plugin: custom input handler type. Note that it must not be an object, just the class. - """ - if name in OutputHandlers._plugins: - raise ValueError('xDEVS output_handler plugin with name "{}" already exists'.format(name)) - OutputHandlers._plugins[name] = plugin - - @staticmethod - def create_output_handler(name: str, **kwargs) -> OutputHandler: - """ - - Creates a new output handler. Note that this is done by the real-time manager. - Users do not directly create output handlers using this method. - - :param name: unique ID of the output handler to be created. - :param kwargs: any additional configuration parameter needed for creating the output handler. - :return: an instance of the OutputHandler class. - """ - if name not in OutputHandlers._plugins: - raise ValueError('xDEVS output_handler plugin with name "{}" not found'.format(name)) - return OutputHandlers._plugins[name](**kwargs) diff --git a/xdevs/rt_sim/rt_coord.py b/xdevs/rt_sim/rt_coord.py deleted file mode 100644 index fb55401..0000000 --- a/xdevs/rt_sim/rt_coord.py +++ /dev/null @@ -1,51 +0,0 @@ -import sys -from xdevs.models import Coupled -from xdevs.rt_sim.rt_manager import RealTimeManager -from xdevs.sim import Coordinator - - -class RealTimeCoordinator(Coordinator): - def __init__(self, model: Coupled, manager: RealTimeManager): - super().__init__(model) - self.manager: RealTimeManager = manager - - def initialize(self): - super().initialize() - self.manager.initialize(self.clock.time) - - def exit(self): - self.manager.exit(self.clock.time) - super().exit() - - def simulate(self, time_interv: float = 10000): - self.initialize() - while self.clock.time < time_interv: - if self.time_next == float("inf") and not self.manager.input_handlers: - print('infinity reached') - break - # SLEEP UNTIL NEXT STATE TRANSITION - t, msgs = self.manager.sleep(min(time_interv, self.time_next)) - # INJECT EXTERNAL EVENTS (if any) - for port_id, msg in msgs: - port = self.model.get_in_port(port_id) - if port is not None: - try: - port.add(msg) - except TypeError as e: - print(f'invalid message type: {e}', file=sys.stderr) - else: - print(f'input port "{port_id}" does not exit', file=sys.stderr) - # UPDATE SIMULATION CLOCK - self.clock.time = t - # EXECUTE NEXT CYCLE (if applies) - if self.clock.time == self.time_next: - self.lambdaf() - self.deltfcn() - # EJECT NEW OUTPUT EVENTS - for port in self.model.out_ports: - self.manager.output_messages(port) - # EXECUTE TRANSDUCERS (if any) - self._execute_transducers() - # CLEAR THE PORTS OF THE MODEL - self.clear() - self.exit() diff --git a/xdevs/sim.py b/xdevs/sim.py index 123409d..3e5fa77 100644 --- a/xdevs/sim.py +++ b/xdevs/sim.py @@ -4,16 +4,16 @@ import itertools import pickle import logging +import warnings from abc import ABC, abstractmethod from collections import defaultdict -from concurrent import futures from typing import Generator, Optional from xmlrpc.server import SimpleXMLRPCServer -from xdevs import INFINITY -from xdevs.models import Atomic, Coupled, Component, Port, T -from xdevs.transducers import Transducer +from xdevs import INFINITY, T +from xdevs.models import Atomic, Coupled, Component, Port +from xdevs.abc import Transducer class SimulationClock: @@ -88,22 +88,22 @@ def __init__(self, model: Atomic, clock: SimulationClock, @property def ta(self) -> float: - return self.model.ta + return self.model.ta() def initialize(self): self.model.initialize() self.time_last = self.clock.time - self.time_next = self.time_last + self.model.ta + self.time_next = self.time_last + self.model.ta() def exit(self): self.model.exit() def deltfcn(self) -> Simulator | None: # TODO if not self.model.in_empty(): - e = self.clock.time - self.time_last if self.clock.time == self.time_next: - self.model.deltcon(e) + self.model.deltcon() else: + e = self.clock.time - self.time_last self.model.deltext(e) elif self.clock.time == self.time_next: self.model.deltint() @@ -117,7 +117,7 @@ def deltfcn(self) -> Simulator | None: # TODO self.trigger_event_transducers() self.time_last = self.clock.time - self.time_next = self.time_last + self.model.ta + self.time_next = self.time_last + self.model.ta() return self def lambdaf(self): @@ -154,21 +154,6 @@ def __init__(self, model: Coupled, clock: Optional[SimulationClock] = None, flat self.event_transducers_mapping = event_transducers_mapping self.state_transducers_mapping = state_transducers_mapping - # A log named logger is created. Logs will be sent to the file SimulationsLogs.log. The file can be find in - # xdevs/examples/basic/ - - self.logger = logging.getLogger("Simulation_Coordinator") - - fh = logging.FileHandler("SimulationLogs.log", mode='w') # for overwrite the file add mode='w' - - self.logger.setLevel(logging.DEBUG) - - formatter = logging.Formatter('%(asctime)s %(name)s - %(levelname)s - %(message)s') - - fh.setFormatter(formatter) - - self.logger.addHandler(fh) - @property def root_coordinator(self) -> bool: return self.model.parent is None @@ -325,7 +310,6 @@ def inject(self, port: str | Port[T], values: T | list[T], e: float = 0) -> bool def simulate(self, num_iters: int = 10000): self.clock.time = self.time_next cont = 0 - while cont < num_iters and self.clock.time < INFINITY: self.lambdaf() self.deltfcn() @@ -334,7 +318,7 @@ def simulate(self, num_iters: int = 10000): self.clock.time = self.time_next cont += 1 - def simulate_time(self, time_interv: float = 10000): + def simulate_time(self, time_interv: float = INFINITY): self.clock.time = self.time_next tf = self.clock.time + time_interv @@ -345,14 +329,6 @@ def simulate_time(self, time_interv: float = 10000): self.clear() self.clock.time = self.time_next - def simulate_inf(self): - while True: - self.lambdaf() - self.deltfcn() - self._execute_transducers() - self.clear() - self.clock.time = self.time_next - def _execute_transducers(self): for transducer in self._transducers: transducer.trigger(self.clock.time) diff --git a/xdevs/tests/test_csv_transducer.py b/xdevs/tests/test_csv_transducer.py index 2093045..795166a 100644 --- a/xdevs/tests/test_csv_transducer.py +++ b/xdevs/tests/test_csv_transducer.py @@ -6,15 +6,15 @@ from xdevs.sim import Coordinator from xdevs.examples.devstone.devstone import LI, DelayedAtomic, HI -from xdevs.transducers import Transducers -from xdevs.examples.basic.basic import Job, Processor, Gpt +from xdevs.factory import Transducers +from xdevs.examples.gpt.gpt import Job, Processor, Gpt class TestCsvTransducer(TestCase): def test_component_filtering_by_type(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) # csv.add_target_port() # csv.add_target_port_by_components(gpt, component_filter=[Coupled, "coupled_.*"], port_filter=OutPort) @@ -29,7 +29,7 @@ def test_component_filtering_by_type(self): def test_component_filtering_by_regex(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) csv.filter_components(".*or") self.assertEqual(len(csv.target_components), 2) @@ -42,45 +42,45 @@ def test_component_filtering_by_regex(self): def test_component_filtering_by_callable(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) - csv.filter_components(lambda comp: hasattr(comp, "proc_time")) + csv.filter_components(lambda comp: hasattr(comp, "proc_t")) self.assertEqual(1, len(csv.target_components)) csv = Transducers.create_transducer('csv', transducer_id='tt') li = LI("LI_root", depth=10, width=10, int_delay=0, ext_delay=0) csv.add_target_component(li) - csv.filter_components(lambda comp: isinstance(comp, DelayedAtomic) and comp.name[-1] == "0") + csv.filter_components(lambda comp: isinstance(comp, DelayedAtomic) and comp.name[-3] == "0") self.assertEqual(10, len(csv.target_components)) def test_ports_filtering_by_type(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) csv.add_target_ports_by_component(gpt, port_filters=Port) - self.assertEqual(8, len(csv.target_ports)) + self.assertEqual(7, len(csv.target_ports)) def test_ports_filtering_by_regex(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) csv.add_target_ports_by_component(gpt, port_filters="i_.*") - self.assertEqual(5, len(csv.target_ports)) + self.assertEqual(4, len(csv.target_ports)) csv.target_ports.clear() csv.add_target_ports_by_component(gpt, port_filters="[io]_.{3,5}ed") self.assertEqual(2, len(csv.target_ports)) csv.target_ports.clear() - csv.add_target_ports_by_component(gpt, port_filters=".*start.*") + csv.add_target_ports_by_component(gpt, port_filters=".*stop.*") self.assertEqual(1, len(csv.target_ports)) csv.target_ports.clear() def test_ports_filtering_by_callable(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) filter_func = lambda port: port.name.startswith("i_") and port.name.endswith("ed") @@ -89,11 +89,11 @@ def test_ports_filtering_by_callable(self): def test_ports_filtering_mixed(self): csv = Transducers.create_transducer('csv', transducer_id='tt') - gpt = Gpt("gpt", 3, 100) + gpt = Gpt("gpt", 3, 9, 100) csv.add_target_component(gpt) csv.add_target_ports_by_component(gpt) - self.assertEqual(8, len(csv.target_ports)) + self.assertEqual(7, len(csv.target_ports)) csv.target_ports.clear() input_ports_filter = lambda port: port.name.startswith("i_") @@ -106,7 +106,7 @@ def test_ports_filtering_mixed2(self): csv = Transducers.create_transducer('csv', transducer_id='tt') hi = HI("HI_root", depth=10, width=10, int_delay=0, ext_delay=0) - comp_filters = (lambda comp: isinstance(comp, DelayedAtomic), ".*[0-2]$") + comp_filters = (lambda comp: isinstance(comp, DelayedAtomic), ".*[0-2]_.*$") csv.add_target_component(hi, *comp_filters) print(csv.target_components) @@ -173,13 +173,13 @@ def test_behavior(self): trans_id = "%s_test_behavior" % self.__class__.__name__ csv_transducer = Transducers.create_transducer('csv', transducer_id=trans_id, exhaustive=True) - gpt = Gpt("gpt", 3, 1000) + gpt = Gpt("gpt", 3, 9, 1000) csv_transducer.add_target_component(gpt) coord = Coordinator(gpt, flatten=False) coord.add_transducer(csv_transducer) coord.initialize() - coord.simulate_time() + coord.simulate() coord.exit() self.assertTrue(os.path.exists(csv_transducer.state_filename)) @@ -187,3 +187,8 @@ def test_behavior(self): # TODO: continue when state changes are register appropiately # TODO: def test_pause_resume(self): + + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/xdevs/tests/test_devstone.py b/xdevs/tests/test_devstone.py index d3f8f1d..026168b 100644 --- a/xdevs/tests/test_devstone.py +++ b/xdevs/tests/test_devstone.py @@ -1,4 +1,5 @@ import unittest +from xdevs import INFINITY from xdevs.sim import Coordinator from xdevs.examples.devstone.devstone import DEVStone, LI, HI, HO, HOmod import random @@ -66,7 +67,7 @@ def test_behavior(self): coord = Coordinator(root) coord.initialize() # coord.inject(li_root.i_in, 0) - coord.simulate() + coord.simulate_time(INFINITY) self.assertEqual(root.n_internals, (params["width"] - 1) * (params["depth"] - 1) + 1) self.assertEqual(root.n_externals, (params["width"] - 1) * (params["depth"] - 1) + 1) @@ -116,7 +117,7 @@ def test_behavior(self): root = DEVStone("HI_root", **params) coord = Coordinator(root) coord.initialize() - coord.simulate() + coord.simulate_time(INFINITY) self.assertEqual(root.n_internals, (((params["width"] - 1) * params["width"]) / 2) * (params["depth"] - 1) + 1) self.assertEqual(root.n_externals, (((params["width"] - 1) * params["width"]) / 2) * (params["depth"] - 1) + 1) @@ -165,7 +166,7 @@ def test_behavior(self): coord = Coordinator(root) coord.initialize() # TODO aqui n_externals debería ser igual a n_atomics (pero no lo es...) - coord.simulate() + coord.simulate_time(INFINITY) self.assertEqual(root.n_internals, (params["width"] - 1) * params["width"] / 2 * (params["depth"] - 1) + 1) self.assertEqual(root.n_externals, (params["width"] - 1) * params["width"] / 2 * (params["depth"] - 1) + 1) @@ -221,7 +222,7 @@ def test_behavior(self): root = DEVStone("HOmod_root", **params) coord = Coordinator(root) coord.initialize() - coord.simulate() + coord.simulate_time(INFINITY) calc_in = lambda x, w: 1 + (x - 1)*(w - 1) exp_trans = 1 diff --git a/xdevs/tests/test_elasticsearch_transducer.py b/xdevs/tests/test_elasticsearch_transducer.py deleted file mode 100644 index 66a4973..0000000 --- a/xdevs/tests/test_elasticsearch_transducer.py +++ /dev/null @@ -1,34 +0,0 @@ -from xdevs.transducers import Transducers -from xdevs.examples.basic.basic import Job, Processor - - -if __name__ == '__main__': - es = Transducers.create_transducer('elasticsearch', - transducer_id='transducer_test', - exhaustive=True, - url='http://localhost:9200') - - model = Processor('processor', 100) - es.add_target_component(model) - es.add_target_port(model.o_out) - - # Try to comment and uncomment the mapper lines to see the effect on the output file - # csv.state_mapper = {'current_job': (str, lambda x: str(x.current_job))} - es.state_mapper['current_job'] = (str, lambda x: str(x.current_job)) - es.event_mapper = {'name': (int, lambda x: x.name), 'time': (int, lambda x: x.time)} - - es.initialize() - clock = 0 - es.bulk_data(clock) - - model.i_in.add(Job(0)) - model.deltext(1) - clock += 1 - model.i_in.clear() - es.bulk_data(1) - clock += model.sigma - model.lambdaf() - model.deltint() - es.bulk_data(clock) - - print('done') diff --git a/xdevs/tests/test_sql_transducer.py b/xdevs/tests/test_sql_transducer.py deleted file mode 100644 index e12d087..0000000 --- a/xdevs/tests/test_sql_transducer.py +++ /dev/null @@ -1,36 +0,0 @@ -from xdevs.transducers import Transducers -from xdevs.examples.basic.basic import Job, Processor - - -if __name__ == '__main__': - sql = Transducers.create_transducer('sql', - transducer_id='transducer_test', - sim_time_id='time', - include_names=True, - exhaustive=True, - url='mysql+pymysql://root@localhost/test') - - model = Processor('processor', 100) - sql.add_target_component(model) - sql.add_target_port(model.o_out) - - # Try to comment and uncomment the mapper lines to see the effect on the output file - # csv.state_mapper = {'current_job': (str, lambda x: str(x.current_job))} - sql.state_mapper['current_job'] = (Job, lambda x: x.current_job) - sql.event_mapper = {'name': (int, lambda x: x.name), 'time': (int, lambda x: x.time)} - - sql.initialize() - clock = 0 - sql.bulk_data(clock) - - model.i_in.add(Job(0)) - model.deltext(1) - clock += 1 - model.i_in.clear() - sql.bulk_data(1) - clock += model.sigma - model.lambdaf() - model.deltint() - sql.bulk_data(clock) - - print('done') diff --git a/xdevs/tests/test_transducible.py b/xdevs/tests/test_transducible.py index cc2f905..0a94a30 100644 --- a/xdevs/tests/test_transducible.py +++ b/xdevs/tests/test_transducible.py @@ -1,7 +1,9 @@ from __future__ import annotations import unittest from typing import Dict, Tuple, Type, Callable -from xdevs.transducers import Transducer, Transducers, Transducible, T +from xdevs import T +from xdevs.abc.transducer import Transducer, Transducible +from xdevs.factory import Transducers class NonTransducibleClass: diff --git a/xdevs/utils.py b/xdevs/utils.py deleted file mode 100644 index fd48800..0000000 --- a/xdevs/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -from xdevs.models import Atomic, Port - - -class Generator(Atomic): - def __init__(self, name: str, num_outputs: int = 1, period: float = None): - super(Generator, self).__init__(name=name) - self.num_outputs: int = num_outputs - self.period: float = period - - self.o_out: Port[int] = Port(int, 'o_out') - self.add_out_port(self.o_out) - - def deltint(self): - self.hold_in('active', self.period) if self.period else self.passivate() - - def deltext(self, e: float): - pass - - def lambdaf(self): - self.o_out.extend(range(self.num_outputs)) - - def initialize(self): - self.activate() - - def exit(self): - pass diff --git a/xdevs/wrappers.py b/xdevs/wrappers.py deleted file mode 100644 index 10f655d..0000000 --- a/xdevs/wrappers.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations -import pkg_resources -from typing import ClassVar, Type -from xdevs.models import Atomic - - -class Wrappers: - _plugins: ClassVar[dict[str, Type[Atomic]]] = { - ep.name: ep.load() for ep in pkg_resources.iter_entry_points('xdevs.plugins.wrappers') - } - - @staticmethod - def add_plugin(name: str, plugin: Type[Atomic]): - if name in Wrappers._plugins: - raise ValueError(f'xDEVS wrapper plugin with name "{name}" already exists') - Wrappers._plugins[name] = plugin - - @staticmethod - def create_wrapper(name: str) -> Type[Atomic]: - if name not in Wrappers._plugins: - raise ValueError(f'xDEVS wrapper plugin with name "{name}" not found') - return Wrappers._plugins[name]