├── examples ├── __init__.py ├── grid_analysis │ ├── __init__.py │ ├── requirements.txt │ ├── policies │ │ ├── __init__.py │ │ ├── random_kill.py │ │ └── degrade.py │ ├── applications │ │ ├── __init__.py │ │ └── vas.py │ ├── metrics.py │ └── infrastructure.py ├── user_distribution │ ├── requirements.txt │ ├── dataset.parquet │ ├── main.py │ ├── infrastructure.py │ ├── update_policy.py │ └── metric.py ├── image_prediction │ ├── requirements.txt │ ├── services │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── model.py │ │ ├── end_service.py │ │ ├── predictor.py │ │ └── trainer.py │ ├── application.py │ ├── main.py │ ├── update_policy.py │ └── metrics.py ├── echo │ ├── main.py │ ├── infrastructure.py │ ├── update_policy.py │ ├── echo.py │ └── application.py └── sock_shop │ ├── mpi │ ├── main.py │ └── update_policy.py │ └── rest │ ├── main.py │ └── update_policy.py ├── docs ├── _static │ ├── js │ │ └── custom.js │ ├── images │ │ ├── dark.png │ │ ├── light.png │ │ └── full_bg.png │ ├── landing │ │ ├── logo.png │ │ ├── name.png │ │ ├── clouds.png │ │ ├── stars.png │ │ ├── twinkling.png │ │ └── github.svg │ ├── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── site.webmanifest │ └── css │ │ ├── custom.css │ │ ├── buttons.css │ │ └── landing.css.map ├── _templates │ ├── autosummary │ │ ├── method.rst │ │ ├── attribute.rst │ │ ├── function.rst │ │ ├── property.rst │ │ ├── class.rst │ │ └── module.rst │ └── index.html ├── requirements.txt ├── source │ ├── api │ │ └── index.rst │ └── overview │ │ ├── install.rst │ │ ├── advanced │ │ ├── emulation │ │ │ └── index.rst │ │ └── index.rst │ │ ├── index.rst │ │ ├── examples │ │ ├── index.rst │ │ └── echo.rst │ │ └── getting-started │ │ ├── update-policy.rst │ │ └── index.rst ├── README.md ├── make.bat ├── Makefile └── index.rst ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md └── workflows │ └── build_and_deploy.yaml ├── eclypse ├── workflow │ ├── __init__.py │ ├── event │ │ ├── __init__.py │ │ └── defaults.py │ └── trigger │ │ └── __init__.py ├── __init__.py ├── remote │ ├── _node │ │ └── __init__.py │ ├── utils │ │ ├── __init__.py │ │ ├── response_code.py │ │ ├── ops.py │ │ └── ray_interface.py │ ├── service │ │ ├── __init__.py │ │ └── rest.py │ ├── bootstrap │ │ ├── __init__.py │ │ └── options_factory.py │ ├── __init__.py │ └── communication │ │ ├── __init__.py │ │ ├── mpi │ │ ├── requests │ │ │ ├── __init__.py │ │ │ ├── broadcast.py │ │ │ ├── multicast.py │ │ │ └── unicast.py │ │ ├── __init__.py │ │ └── response.py │ │ └── rest │ │ ├── __init__.py │ │ ├── methods.py │ │ ├── codes.py │ │ └── http_request.py ├── utils │ ├── __init__.py │ ├── constants.py │ ├── types.py │ └── tools.py ├── builders │ ├── __init__.py │ ├── application │ │ ├── __init__.py │ │ └── sock_shop │ │ │ ├── __init__.py │ │ │ ├── mpi_services │ │ │ ├── __init__.py │ │ │ ├── cart.py │ │ │ ├── user.py │ │ │ ├── payment.py │ │ │ ├── catalog.py │ │ │ ├── shipping.py │ │ │ └── frontend.py │ │ │ └── rest_services │ │ │ ├── __init__.py │ │ │ ├── cart.py │ │ │ ├── catalog.py │ │ │ ├── user.py │ │ │ ├── shipping.py │ │ │ ├── payment.py │ │ │ ├── frontend.py │ │ │ └── order.py │ └── infrastructure │ │ ├── __init__.py │ │ └── generators │ │ └── __init__.py ├── simulation │ ├── _simulator │ │ └── __init__.py │ └── __init__.py ├── report │ ├── __init__.py │ ├── metrics │ │ └── __init__.py │ ├── reporters │ │ ├── __init__.py │ │ ├── gml.py │ │ ├── json.py │ │ ├── csv.py │ │ └── tensorboard.py │ └── reporter.py ├── graph │ ├── __init__.py │ └── assets │ │ ├── __init__.py │ │ ├── symbolic.py │ │ └── additive.py └── placement │ ├── __init__.py │ └── strategies │ ├── __init__.py │ ├── static.py │ ├── round_robin.py │ ├── strategy.py │ ├── random.py │ ├── first_fit.py │ └── best_fit.py ├── .readthedocs.yaml ├── CITATION.cff ├── Makefile ├── LICENSE ├── .ruff.toml ├── .pre-commit-config.yaml ├── .pylintrc ├── CHANGELOG.md ├── pyproject.toml └── README.md /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/js/custom.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/grid_analysis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @jacopo-massa 2 | @vdecaro 3 | -------------------------------------------------------------------------------- /examples/grid_analysis/requirements.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | ray[train] 3 | scipy 4 | -------------------------------------------------------------------------------- /examples/user_distribution/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | psutil 3 | pyarrow 4 | -------------------------------------------------------------------------------- /examples/image_prediction/requirements.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | torch 3 | torchvision 4 | -------------------------------------------------------------------------------- /eclypse/workflow/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for workflow management, including events and triggers.""" 2 | -------------------------------------------------------------------------------- /docs/_static/images/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/images/dark.png -------------------------------------------------------------------------------- /docs/_static/images/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/images/light.png -------------------------------------------------------------------------------- /docs/_static/landing/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/landing/logo.png -------------------------------------------------------------------------------- /docs/_static/landing/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/landing/name.png -------------------------------------------------------------------------------- /docs/_static/images/full_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/images/full_bg.png -------------------------------------------------------------------------------- /docs/_static/landing/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/landing/clouds.png -------------------------------------------------------------------------------- /docs/_static/landing/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/landing/stars.png -------------------------------------------------------------------------------- /docs/_static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/favicon/favicon.ico -------------------------------------------------------------------------------- /docs/_static/landing/twinkling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/landing/twinkling.png -------------------------------------------------------------------------------- /docs/_static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /docs/_static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /docs/_static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /examples/user_distribution/dataset.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/examples/user_distribution/dataset.parquet -------------------------------------------------------------------------------- /docs/_static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/_static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclypse-org/eclypse/HEAD/docs/_static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /eclypse/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | __version__ = "0.8.1" 4 | 5 | os.environ["RAY_DEDUP_LOGS"] = "0" 6 | os.environ["RAY_COLOR_PREFIX"] = "1" 7 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/method.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. automethod:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/attribute.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoattribute:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/function.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autofunction:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/property.rst: -------------------------------------------------------------------------------- 1 | {{ objname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoproperty:: {{ objname }} 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | enum-tools[sphinx]==0.11.0 2 | jinja2>=3.1.5 3 | pydata-sphinx-theme==0.16.0 4 | sphinx==7.2.6 5 | sphinx-copybutton==0.5.2 6 | sphinx-icon==0.2.2 7 | -------------------------------------------------------------------------------- /eclypse/remote/_node/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for the remote node and operations it can perform.""" 2 | 3 | from .node import RemoteNode 4 | 5 | __all__ = ["RemoteNode"] 6 | -------------------------------------------------------------------------------- /eclypse/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for utility functions. 2 | 3 | It comprises logging, constants used in ECLYPSE, and helper 4 | functions for the other modules. 5 | """ 6 | -------------------------------------------------------------------------------- /examples/grid_analysis/policies/__init__.py: -------------------------------------------------------------------------------- 1 | from .degrade import degrade_policy 2 | from .random_kill import kill_policy 3 | 4 | __all__ = ["degrade_policy", "kill_policy"] 5 | -------------------------------------------------------------------------------- /eclypse/builders/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for the application and infrastructure builders.""" 2 | 3 | from . import infrastructure 4 | from . import application 5 | 6 | __all__ = ["application", "infrastructure"] 7 | -------------------------------------------------------------------------------- /eclypse/simulation/_simulator/__init__.py: -------------------------------------------------------------------------------- 1 | """Package containing the (local and remote) simulator classes.""" 2 | 3 | from .local import Simulator 4 | from .remote import RemoteSimulator 5 | 6 | __all__ = ["RemoteSimulator", "Simulator"] 7 | -------------------------------------------------------------------------------- /examples/image_prediction/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .end_service import EndService 2 | from .predictor import PredictorService 3 | from .trainer import TrainerService 4 | 5 | __all__ = ["EndService", "PredictorService", "TrainerService"] 6 | -------------------------------------------------------------------------------- /eclypse/remote/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for utilities used in the remote module.""" 2 | 3 | from .ops import RemoteOps 4 | from .response_code import ResponseCode 5 | 6 | __all__ = [ 7 | "RemoteOps", 8 | "ResponseCode", 9 | ] 10 | -------------------------------------------------------------------------------- /eclypse/remote/service/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for classes allowing the definition of services logic in ECLYPSE remote simulations.""" 2 | 3 | from .service import Service 4 | from .rest import RESTService 5 | 6 | __all__ = ["RESTService", "Service"] 7 | -------------------------------------------------------------------------------- /eclypse/remote/bootstrap/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for remote node configuration and bootstrapping.""" 2 | 3 | from .options_factory import RayOptionsFactory 4 | from .bootstrap import RemoteBootstrap 5 | 6 | __all__ = ["RayOptionsFactory", "RemoteBootstrap"] 7 | -------------------------------------------------------------------------------- /eclypse/simulation/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for simulation configuration and engine.""" 2 | 3 | from .simulation import Simulation 4 | from .config import SimulationConfig 5 | 6 | __all__ = [ 7 | "Simulation", 8 | "SimulationConfig", 9 | ] 10 | -------------------------------------------------------------------------------- /eclypse/report/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for reporting and metrics.""" 2 | 3 | from .report import Report 4 | from .reporter import Reporter 5 | from .metrics import metric 6 | 7 | 8 | __all__ = [ 9 | "Report", 10 | "Reporter", 11 | "metric", 12 | ] 13 | -------------------------------------------------------------------------------- /eclypse/remote/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for every functionality related to a remote simulation. 2 | 3 | It includes Remote nodes, services, communicaiton interfaces, and some utilities. 4 | """ 5 | 6 | from .utils.ray_interface import ray_backend 7 | 8 | __all__ = ["ray_backend"] 9 | -------------------------------------------------------------------------------- /docs/_static/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} 2 | -------------------------------------------------------------------------------- /docs/source/api/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. currentmodule:: eclypse 5 | 6 | .. autosummary:: 7 | :recursive: 8 | :toctree: reference/public 9 | :caption: Public 10 | 11 | builders 12 | graph 13 | placement 14 | remote 15 | report 16 | simulation 17 | workflow 18 | utils 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.11" 7 | jobs: 8 | post_create_environment: 9 | - pip install poetry 10 | post_install: 11 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs 12 | 13 | sphinx: 14 | configuration: docs/conf.py 15 | -------------------------------------------------------------------------------- /eclypse/builders/application/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for the application builders. 2 | 3 | It has the following builders: 4 | - SockShop: A microservice-based application that simulates an e-commerce platform, \ 5 | made of 7 microservices. 6 | """ 7 | 8 | from .sock_shop.application import get_sock_shop 9 | 10 | __all__ = ["get_sock_shop"] 11 | -------------------------------------------------------------------------------- /eclypse/graph/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for modelling the infrastructure and the applications in an ECLYPSE simulation.""" 2 | 3 | from .asset_graph import AssetGraph 4 | from .application import Application 5 | from .infrastructure import Infrastructure 6 | 7 | __all__ = [ 8 | "Application", 9 | "AssetGraph", 10 | "Infrastructure", 11 | ] 12 | -------------------------------------------------------------------------------- /eclypse/remote/communication/__init__.py: -------------------------------------------------------------------------------- 1 | """Package collecting interfaces for communication among services into an application.""" 2 | 3 | from .interface import EclypseCommunicationInterface 4 | from .route import Route 5 | from .request import EclypseRequest 6 | 7 | __all__ = [ 8 | "EclypseCommunicationInterface", 9 | "EclypseRequest", 10 | "Route", 11 | ] 12 | -------------------------------------------------------------------------------- /examples/grid_analysis/applications/__init__.py: -------------------------------------------------------------------------------- 1 | from .vas import get_vas 2 | from .assembly_platform import get_assembly_platform 3 | from .health_guardian import get_health_guardian 4 | 5 | 6 | def get_apps(seed: int = None): 7 | return [ 8 | get_vas(seed=seed), 9 | get_health_guardian(seed=seed), 10 | get_assembly_platform(seed=seed), 11 | ] 12 | -------------------------------------------------------------------------------- /eclypse/remote/communication/mpi/requests/__init__.py: -------------------------------------------------------------------------------- 1 | """Package collecting requests that can be sent to the remote nodes, using MPI protocol.""" 2 | 3 | from .multicast import MulticastRequest 4 | from .broadcast import BroadcastRequest 5 | from .unicast import UnicastRequest 6 | 7 | __all__ = [ 8 | "BroadcastRequest", 9 | "MulticastRequest", 10 | "UnicastRequest", 11 | ] 12 | -------------------------------------------------------------------------------- /eclypse/workflow/event/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for managing events in the Eclypse framework. 2 | 3 | It provides a decorator to define events for the simulation. 4 | """ 5 | 6 | from .event import EclypseEvent 7 | from .decorator import event 8 | from .defaults import get_default_events 9 | 10 | __all__ = [ 11 | "EclypseEvent", 12 | "event", 13 | "get_default_events", 14 | ] 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Documentation is built with sphinx. 4 | 5 | Build the documentation (it must be executed in the `docs` folder): 6 | ``` 7 | sphinx-build . _build 8 | ``` 9 | 10 | ### Doc coverage 11 | it is possible to check the class coverage to find classes that are missing from the documentation using the command: 12 | ``` 13 | sphinx-build -b coverage . _build 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .bd-main .bd-content .bd-article-container { 2 | max-width: 100%; 3 | /* default is 60em */ 4 | } 5 | 6 | .bd-page-width { 7 | max-width: 100%; 8 | /* default is 88rem */ 9 | } 10 | 11 | .bd-sidebar-primary { 12 | max-width: 20%; 13 | } 14 | 15 | #rtd-footer-container { 16 | margin-top: 0px !important; 17 | margin-right: 0px !important; 18 | } 19 | -------------------------------------------------------------------------------- /eclypse/placement/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for placement, placement views and management.""" 2 | 3 | from .placement import Placement 4 | from .view import PlacementView 5 | from ._manager import PlacementManager 6 | from .strategies.strategy import PlacementStrategy 7 | 8 | __all__ = [ 9 | "Placement", 10 | "PlacementManager", 11 | "PlacementStrategy", 12 | "PlacementView", 13 | ] 14 | -------------------------------------------------------------------------------- /eclypse/remote/communication/mpi/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for the MPI communication interface. 2 | 3 | Based on the `Message Passing Interface (MPI) protocol `_. 4 | """ 5 | 6 | from .response import Response 7 | from .interface import EclypseMPI, exchange 8 | 9 | __all__ = [ 10 | "EclypseMPI", 11 | "Response", 12 | "exchange", 13 | ] 14 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for the Sock Shop application builder. 2 | 3 | It provides a getter function to create a Sock Shop application in 3 4 | different configurations, by choosing the `communication interface` in the getter: 5 | 6 | - "None": The application graph without any remote Service configuration. 7 | - "rest": The application graph with REST Services. 8 | - "mpi": The application graph with MPI Services. 9 | """ 10 | -------------------------------------------------------------------------------- /eclypse/remote/communication/rest/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for the REST communication interface. 2 | 3 | Based on the 4 | `Representational State Transfer (REST) protocol `_. 5 | """ 6 | 7 | from .interface import EclypseREST, register_endpoint as endpoint 8 | from .codes import HTTPStatusCode 9 | from .methods import HTTPMethod 10 | 11 | __all__ = ["EclypseREST", "HTTPMethod", "HTTPStatusCode", "endpoint"] 12 | -------------------------------------------------------------------------------- /eclypse/remote/utils/response_code.py: -------------------------------------------------------------------------------- 1 | """Module for the ResponseCode enumeration. 2 | 3 | It defines the possible responses to an EclypseRequest. 4 | """ 5 | 6 | from enum import ( 7 | Enum, 8 | auto, 9 | ) 10 | 11 | 12 | class ResponseCode(Enum): 13 | """Enum class, denoting possible responses to an `EclypseRequest`. 14 | 15 | Attributes: 16 | OK: The request was processed successfully. 17 | ERROR: An error occurred while processing the request. 18 | """ 19 | 20 | OK = auto() 21 | ERROR = auto() 22 | -------------------------------------------------------------------------------- /eclypse/report/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for managing reportable metrics in an ECLYPSE simulation. 2 | 3 | It provides a set of decorators to define metrics at different levels of the simulation. 4 | """ 5 | 6 | from .metric import ( 7 | simulation, 8 | application, 9 | infrastructure, 10 | service, 11 | interaction, 12 | node, 13 | link, 14 | ) 15 | 16 | __all__ = [ 17 | "application", 18 | "infrastructure", 19 | "interaction", 20 | "link", 21 | "node", 22 | "service", 23 | # DECORATORS 24 | "simulation", 25 | ] 26 | -------------------------------------------------------------------------------- /docs/source/overview/install.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Install ECLYPSE 3 | =============== 4 | 5 | ECLYPSE can be installed using `pip `_. 6 | 7 | .. note:: 8 | 9 | Do not use the global environment to install ECLYPSE. 10 | It is recommended to create a `virtual environment `_ first. 11 | 12 | To install ECLYPSE, run the following command: 13 | 14 | .. code-block:: shell 15 | 16 | pip install eclypse 17 | 18 | Now you are ready to run :doc:`your first ECLYPSE simulation `! 19 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/__init__.py: -------------------------------------------------------------------------------- 1 | """MPI implementation for the Sock Shop application services.""" 2 | 3 | from .catalog import CatalogService 4 | from .user import UserService 5 | from .cart import CartService 6 | from .order import OrderService 7 | from .payment import PaymentService 8 | from .shipping import ShippingService 9 | from .frontend import FrontendService 10 | 11 | __all__ = [ 12 | "CartService", 13 | "CatalogService", 14 | "FrontendService", 15 | "OrderService", 16 | "PaymentService", 17 | "ShippingService", 18 | "UserService", 19 | ] 20 | -------------------------------------------------------------------------------- /eclypse/builders/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for the infrastructure builders. 2 | 3 | Default builders include: `b-cube`, `fat_tree`, `hierarchical`, `random`, and `star`. 4 | 5 | We provide also a getter function for the Orion CEV infrastructure: `get_orion_cev`. 6 | """ 7 | 8 | from .generators import ( 9 | b_cube, 10 | fat_tree, 11 | hierarchical, 12 | random, 13 | star, 14 | ) 15 | from .orion_cev import get_orion_cev 16 | 17 | __all__ = [ 18 | "b_cube", 19 | "fat_tree", 20 | "get_orion_cev", 21 | "hierarchical", 22 | "random", 23 | "star", 24 | ] 25 | -------------------------------------------------------------------------------- /eclypse/remote/communication/rest/methods.py: -------------------------------------------------------------------------------- 1 | """Module for the HTTPMethod class. 2 | 3 | It defines the http metohds supported by the `EclypseREST` communication interface. 4 | """ 5 | 6 | from enum import Enum 7 | 8 | 9 | class HTTPMethod(str, Enum): 10 | """HTTP methods supported by the `EclypseREST` communication interface. 11 | 12 | Attributes: 13 | GET: The GET HTTP method. 14 | POST: The POST HTTP method. 15 | PUT: The PUT HTTP method. 16 | DELETE: The DELETE HTTP method. 17 | """ 18 | 19 | GET = "GET" 20 | POST = "POST" 21 | PUT = "PUT" 22 | DELETE = "DELETE" 23 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use ECLYPSE in your work or research project, please cite it as below." 3 | title: "ECLYPSE: an Edge-Cloud Python Platform for Simulated runtime Environments" 4 | version: 0.8.1 5 | date-released: 2024-11-15 6 | url: "https://github.com/eclypse-org/eclypse" 7 | repository-code: "https://github.com/eclypse-org/eclypse" 8 | license: "MIT" 9 | authors: 10 | - family-names: "Massa" 11 | given-names: "Jacopo" 12 | orcid: "https://orcid.org/0000-0002-5255-537X" 13 | - family-names: "De Caro" 14 | given-names: "Valerio" 15 | orcid: "https://orcid.org/0000-0002-5267-6614" 16 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | """Package collecting placement strategies. 2 | 3 | They can be used to place services of an application on infrastructure nodes. 4 | """ 5 | 6 | from .strategy import PlacementStrategy 7 | from .round_robin import RoundRobinStrategy 8 | from .random import RandomStrategy 9 | from .static import StaticStrategy 10 | from .first_fit import FirstFitStrategy 11 | from .best_fit import BestFitStrategy 12 | 13 | __all__ = [ 14 | "BestFitStrategy", 15 | "FirstFitStrategy", 16 | "PlacementStrategy", 17 | "RandomStrategy", 18 | "RoundRobinStrategy", 19 | "StaticStrategy", 20 | ] 21 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/__init__.py: -------------------------------------------------------------------------------- 1 | """MPI implementation for the Sock Shop application services.""" 2 | 3 | # pylint: disable=duplicate-code 4 | 5 | from .catalog import CatalogService 6 | from .user import UserService 7 | from .cart import CartService 8 | from .order import OrderService 9 | from .payment import PaymentService 10 | from .shipping import ShippingService 11 | from .frontend import FrontendService 12 | 13 | __all__ = [ 14 | "CartService", 15 | "CatalogService", 16 | "FrontendService", 17 | "OrderService", 18 | "PaymentService", 19 | "ShippingService", 20 | "UserService", 21 | ] 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to extend ECLYPSE 4 | title: "[FEAT] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /examples/grid_analysis/policies/random_kill.py: -------------------------------------------------------------------------------- 1 | import random as rnd 2 | 3 | from networkx.classes.reportviews import ( 4 | EdgeView, 5 | NodeView, 6 | ) 7 | 8 | 9 | def kill_policy(kill_probability: float): 10 | revive_probability = kill_probability / 2 11 | 12 | def node_update_wrapper(nodes: NodeView): 13 | for _, resources in nodes.data(): 14 | if rnd.random() < kill_probability: 15 | resources["availability"] = 0 16 | elif rnd.random() < revive_probability: 17 | resources["availability"] = 0.99 18 | 19 | def edge_update_wrapper(_: EdgeView): 20 | pass 21 | 22 | return node_update_wrapper, edge_update_wrapper 23 | -------------------------------------------------------------------------------- /eclypse/remote/utils/ops.py: -------------------------------------------------------------------------------- 1 | """Module for the RemoteOps enumeration. 2 | 3 | It defines the operations that can be performed on a Service. 4 | """ 5 | 6 | from enum import Enum 7 | 8 | 9 | class RemoteOps(str, Enum): 10 | """Enum class for the operations that can be performed on a service. 11 | 12 | The operations are executed via the `ops_entrypoint` method of the RemoteEngine class. 13 | 14 | Attributes: 15 | DEPLOY: Deploy the service. 16 | UNDEPLOY: Undeploy the service. 17 | START: Start the service. 18 | STOP: Stop the service. 19 | """ 20 | 21 | DEPLOY = "deploy" 22 | UNDEPLOY = "undeploy" 23 | START = "start_service" 24 | STOP = "stop_service" 25 | -------------------------------------------------------------------------------- /eclypse/workflow/trigger/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for trigger classes. 2 | 3 | This module provides various trigger classes that can be used to control the 4 | execution of workflows. 5 | """ 6 | 7 | from .trigger import ( 8 | Trigger, 9 | RandomTrigger, 10 | PeriodicTrigger, 11 | ScheduledTrigger, 12 | ) 13 | 14 | from .cascade import ( 15 | CascadeTrigger, 16 | RandomCascadeTrigger, 17 | PeriodicCascadeTrigger, 18 | ScheduledCascadeTrigger, 19 | ) 20 | 21 | __all__ = [ 22 | "CascadeTrigger", 23 | "PeriodicCascadeTrigger", 24 | "PeriodicTrigger", 25 | "RandomCascadeTrigger", 26 | "RandomTrigger", 27 | "ScheduledCascadeTrigger", 28 | "ScheduledTrigger", 29 | "Trigger", 30 | ] 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve ECLYPSE 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## Environment 14 | - OS: Ubuntu/MacOS 15 | - Python version: 3.10/3.11/3.12 16 | - ECLYPSE version: 17 | - ECLYPSE-core version: 18 | 19 | ## To Reproduce 20 | **Numbered** steps to reproduce the behaviour. 21 | 22 | ## Expected behaviour 23 | A clear and concise description of what you expected to happen. 24 | 25 | ## Screenshots 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | ## Additional context 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /docs/source/overview/advanced/emulation/index.rst: -------------------------------------------------------------------------------- 1 | Emulation 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | :hidden: 7 | 8 | emulation 9 | messaging 10 | 11 | .. grid:: 2 12 | 13 | .. grid-item:: 14 | 15 | .. card:: :octicon:`codespaces;1em;info` **Emulation** 16 | :link-type: doc 17 | :link: emulation 18 | 19 | Define the **logics** of your **application services** and run them on a *physical* machine 20 | or a *cluster* of machines. 21 | 22 | .. grid-item:: 23 | 24 | .. card:: :octicon:`comment-discussion;1em;info` **Messaging** 25 | :link-type: doc 26 | :link: messaging 27 | 28 | Exchange **messages between services** in emulation mode using *MPI*-like or *REST*-style interfaces. 29 | -------------------------------------------------------------------------------- /eclypse/builders/infrastructure/generators/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for the infrastructure builders. 2 | 3 | It has the following builders: 4 | 5 | - b_cube: A BCube infrastructure with switches and hosts. 6 | - fat_tree: A Fat-Tree infrastructure with switches and hosts. 7 | - hierarchical: A hierarchical infrastructure made of nodes partitioned into groups. 8 | - star: A star infrastructure with clients connected to a central node. 9 | - random: A random infrastructure with nodes connected with a given probability. 10 | """ 11 | 12 | from .b_cube import b_cube 13 | from .fat_tree import fat_tree 14 | from .hierarchical import hierarchical 15 | from .random import random 16 | from .star import star 17 | 18 | __all__ = [ 19 | "b_cube", 20 | "fat_tree", 21 | "hierarchical", 22 | "random", 23 | "star", 24 | ] 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := check 2 | 3 | check: 4 | pre-commit run -a 5 | 6 | changelog: 7 | cz bump --changelog 8 | 9 | patch: 10 | cz bump --increment patch 11 | 12 | setup: 13 | python -m pip install --upgrade pip 14 | pip install poetry 15 | poetry config virtualenvs.create false 16 | poetry install --with=dev,deploy --no-root 17 | 18 | format: 19 | # docformatter --config pyproject.toml --in-place eclypse 20 | # black --config=pyproject.toml eclypse 21 | # pycln --config=pyproject.toml eclypse 22 | isort eclypse 23 | ruff check 24 | ruff format 25 | 26 | build: format 27 | poetry build -v --no-cache --format wheel 28 | 29 | verify: 30 | twine check --strict dist/* 31 | 32 | publish-test: build verify 33 | poetry publish -r test-pypi --skip-existing -v 34 | 35 | publish: build verify 36 | poetry publish --skip-existing -v 37 | -------------------------------------------------------------------------------- /examples/image_prediction/services/utils.py: -------------------------------------------------------------------------------- 1 | import torchvision 2 | from torchvision.datasets import MNIST 3 | 4 | from eclypse.utils.constants import DEFAULT_SIM_PATH 5 | 6 | BASE_PATH = DEFAULT_SIM_PATH / "image_prediction" 7 | RUNTIME_PATH = BASE_PATH / "runtime" 8 | LEARNING_RATE = 0.001 9 | EPOCHS = 100 10 | BATCH_SIZE = 1024 11 | STEPS = 600 12 | STEP_EVERY_MS = 1000 13 | 14 | 15 | def load_data(train: bool = True) -> MNIST: 16 | """Loads the MNIST dataset from torchvision datasets.""" 17 | return MNIST( 18 | BASE_PATH / "dataset", 19 | download=True, 20 | train=train, 21 | transform=torchvision.transforms.Compose( 22 | [ 23 | torchvision.transforms.ToTensor(), 24 | torchvision.transforms.Normalize((0.1307,), (0.3081,)), 25 | ] 26 | ), 27 | ) 28 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ name }} 6 | :show-inheritance: 7 | :members: 8 | 9 | {% block methods %} 10 | {% if methods %} 11 | .. rubric:: {{ _('Methods') }} 12 | 13 | .. autosummary:: 14 | :toctree: {{ name }} 15 | 16 | {% for item in methods %} 17 | {{ item | filter_out_parent_class_members(name, module) }} 18 | {%- endfor %} 19 | 20 | {% endif %} 21 | {% endblock %} 22 | 23 | 24 | {% block attributes %} 25 | {% if attributes %} 26 | .. rubric:: {{ _('Attributes') }} 27 | 28 | .. autosummary:: 29 | :nosignatures: 30 | :toctree: {{ name }} 31 | 32 | {% for item in attributes %} 33 | {{ item | filter_out_parent_class_members(name, module) }} 34 | {%- endfor %} 35 | 36 | {% endif %} 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /docs/_static/css/buttons.css: -------------------------------------------------------------------------------- 1 | /* custom.css */ 2 | .start-button { 3 | background-color: transparent; 4 | /* Transparent background */ 5 | border: 2px solid #007bff; 6 | /* Blue border */ 7 | color: #007bff; 8 | /* Blue text color */ 9 | padding: 12px 24px; 10 | /* Adjust padding as needed */ 11 | text-align: center; 12 | text-decoration: none; 13 | display: inline-block; 14 | font-size: 20px; 15 | /* Adjust font size as needed */ 16 | margin: 4px 2px; 17 | cursor: pointer; 18 | border-radius: 4px; 19 | transition: background-color 0.3s ease; 20 | /* Smooth transition */ 21 | } 22 | 23 | .start-button:hover { 24 | background-color: rgba(0, 123, 255, 0.1); 25 | /* Slightly transparent blue on hover */ 26 | border-color: #0056b3; 27 | /* Darker blue border on hover */ 28 | color: #0056b3; 29 | /* Darker blue text color on hover */ 30 | } 31 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/image_prediction/services/model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | 6 | class MNISTModel(nn.Module): 7 | def __init__(self): 8 | super(MNISTModel, self).__init__() 9 | self.conv1 = nn.Conv2d(1, 32, 3, 1) 10 | self.conv2 = nn.Conv2d(32, 64, 3, 1) 11 | self.dropout1 = nn.Dropout2d(0.25) 12 | self.dropout2 = nn.Dropout2d(0.5) 13 | self.fc1 = nn.Linear(9216, 128) 14 | self.fc2 = nn.Linear(128, 10) 15 | 16 | def forward(self, x): 17 | x = self.conv1(x) 18 | x = F.relu(x) 19 | x = self.conv2(x) 20 | x = F.relu(x) 21 | x = F.max_pool2d(x, 2) 22 | x = self.dropout1(x) 23 | x = torch.flatten(x, 1) 24 | x = self.fc1(x) 25 | x = F.relu(x) 26 | x = self.dropout2(x) 27 | x = self.fc2(x) 28 | output = F.log_softmax(x, dim=1) 29 | return output 30 | -------------------------------------------------------------------------------- /examples/echo/main.py: -------------------------------------------------------------------------------- 1 | from application import echo_app as app 2 | from infrastructure import get_infrastructure 3 | 4 | from eclypse.placement.strategies import RandomStrategy 5 | from eclypse.simulation import ( 6 | Simulation, 7 | SimulationConfig, 8 | ) 9 | from eclypse.utils.constants import DEFAULT_SIM_PATH 10 | 11 | if __name__ == "__main__": 12 | 13 | seed = 2 14 | sim_config = SimulationConfig( 15 | seed=seed, 16 | max_steps=30, 17 | step_every_ms=500, 18 | log_to_file=True, 19 | path=DEFAULT_SIM_PATH / "EchoApp", 20 | # remote=True, 21 | # log_level="TRACE", 22 | include_default_metrics=True, 23 | ) 24 | 25 | sim = Simulation( 26 | get_infrastructure(seed=seed), 27 | simulation_config=sim_config, 28 | ) 29 | 30 | sim.register(app, RandomStrategy(seed=seed)) 31 | sim.start() 32 | sim.wait() 33 | print(sim.report.application()) 34 | -------------------------------------------------------------------------------- /docs/_static/landing/github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/user_distribution/main.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | from infrastructure import get_infrastructure 4 | from metric import get_metrics 5 | 6 | from eclypse.builders.application import get_sock_shop 7 | from eclypse.placement.strategies import BestFitStrategy 8 | from eclypse.simulation import ( 9 | Simulation, 10 | SimulationConfig, 11 | ) 12 | from eclypse.utils.constants import DEFAULT_SIM_PATH 13 | 14 | SEED = 42 15 | STEPS = 4167 16 | 17 | app = get_sock_shop(seed=SEED) 18 | strategy = BestFitStrategy() 19 | 20 | sim_config = SimulationConfig( 21 | step_every_ms="auto", 22 | seed=SEED, 23 | max_steps=STEPS, 24 | path=DEFAULT_SIM_PATH / "user-distribution", 25 | events=get_metrics(), 26 | log_to_file=True, 27 | ) 28 | infrastructure = get_infrastructure(SEED) 29 | 30 | sim = Simulation(infrastructure, simulation_config=sim_config) 31 | sim.register(app, strategy) 32 | 33 | start_time = time() 34 | sim.start() 35 | sim.wait() 36 | print("Elapsed time: ", time() - start_time) 37 | -------------------------------------------------------------------------------- /.github/workflows/build_and_deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to PyPI 2 | on: [workflow_dispatch] 3 | 4 | jobs: 5 | build_and_deploy: 6 | strategy: 7 | matrix: 8 | os: [ubuntu-22.04, ubuntu-latest, macos-13, macos-latest] 9 | python-version: ["3.11", "3.12", "3.13"] 10 | runs-on: ${{ matrix.os }} 11 | continue-on-error: true 12 | 13 | steps: 14 | - name: Checkout Source Code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Install Dependencies 23 | run: make setup 24 | 25 | - name: Build the Package 26 | run: make build 27 | 28 | - name: Verify the distribution 29 | run: make verify 30 | 31 | - name: Upload to PyPI 32 | run: | 33 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN_PUBLIC }} 34 | poetry publish --skip-existing -vv --no-interaction 35 | -------------------------------------------------------------------------------- /examples/user_distribution/infrastructure.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | from metric import user_count_asset 3 | from update_policy import ( 4 | EdgeUpdatePolicy, 5 | UserDistributionPolicy, 6 | kill_policy, 7 | ) 8 | 9 | from eclypse.builders.infrastructure import hierarchical 10 | 11 | 12 | def get_infrastructure(seed: int): 13 | kill_probability = 0.1 14 | node_policy = kill_policy(kill_probability=kill_probability) 15 | edge_policy = EdgeUpdatePolicy(kill_probability=kill_probability) 16 | 17 | i = hierarchical( 18 | node_assets={"user_count": user_count_asset()}, 19 | infrastructure_id="hierarchical", 20 | n=187, 21 | node_update_policy=[node_policy, UserDistributionPolicy()], 22 | include_default_assets=True, 23 | link_update_policy=edge_policy, 24 | symmetric=True, 25 | seed=seed, 26 | ) 27 | 28 | mapping = {old_name: new_name for new_name, old_name in enumerate(i.nodes())} 29 | i = nx.relabel_nodes(i, mapping, copy=False) 30 | return i 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | clean: 12 | rm -rf $(BUILDDIR) 13 | rm -rf source/api/reference 14 | 15 | # Put it first so that "make" without argument is like "make help". 16 | help: 17 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 18 | 19 | livehtml: 20 | sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)"/html $(SPHINXOPTS) $(O) 21 | 22 | # check docstrings using pylint in ../eclypse module 23 | check: 24 | # pylint --rcfile=../.pylintrc --enable=C0114,C0115,C0116 --disable=R0801,E0611 ../eclypse 25 | ruff check --select D,E501 ../eclypse 26 | 27 | .PHONY: help Makefile 28 | 29 | # Catch-all target: route all unknown targets to Sphinx using the new 30 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 31 | %: Makefile clean 32 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 33 | -------------------------------------------------------------------------------- /eclypse/graph/assets/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for defining asset classes. 2 | 3 | Available classes are: 4 | 5 | - `Asset`: The base and extendable class for all assets. 6 | - `Additive`: Represents a numeric asset where the aggregation is additive. 7 | - `Multiplicative`: Represents a numeric asset where the aggregation is multiplicative. 8 | - `Concave`: Represents a numeric asset where the aggregation is concave. 9 | - `Convex`: Represents a numeric asset where the aggregation is convex. 10 | - `Symbolic`: Represents a symbolic asset (set of values with no order relation). 11 | - `AssetBucket`: Represents a collection of assets. 12 | """ 13 | 14 | from .asset import Asset 15 | from .additive import Additive 16 | from .multiplicative import Multiplicative 17 | from .concave import Concave 18 | from .convex import Convex 19 | from .symbolic import Symbolic 20 | from .bucket import AssetBucket 21 | from .space import AssetSpace 22 | 23 | __all__ = [ 24 | "Additive", 25 | "Asset", 26 | "AssetBucket", 27 | "AssetSpace", 28 | "Concave", 29 | "Convex", 30 | "Multiplicative", 31 | "Symbolic", 32 | ] 33 | -------------------------------------------------------------------------------- /docs/source/overview/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 3 3 | :hidden: 4 | 5 | install 6 | getting-started/index 7 | advanced/index 8 | examples/index 9 | 10 | 11 | Overview 12 | -------- 13 | 14 | .. grid:: 1 15 | 16 | .. grid-item:: 17 | 18 | .. card:: :octicon:`desktop-download;1em;info` **Installation** 19 | :link-type: doc 20 | :link: install 21 | 22 | Learn how to install ECLYPSE. 23 | 24 | .. grid-item:: 25 | 26 | .. card:: :octicon:`repo;1em;info` **Getting Started** 27 | :link-type: doc 28 | :link: getting-started/index 29 | 30 | Start your first simulation with ECLYPSE. 31 | 32 | .. grid-item:: 33 | 34 | .. card:: :octicon:`terminal;1em;info` **Advanced** 35 | :link-type: doc 36 | :link: advanced/index 37 | 38 | Learn more about advanced features of ECLYPSE. 39 | 40 | .. grid-item:: 41 | 42 | .. card:: :octicon:`code;1em;info` **Examples** 43 | :link-type: doc 44 | :link: examples/index 45 | 46 | Explore example simulations to use ECLYPSE effectively. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Valerio De Caro & Jacopo Massa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/cart.py: -------------------------------------------------------------------------------- 1 | """The `CartService` handles the shopping cart functionality. 2 | 3 | - Key Responsibilities: 4 | - Manages the user's shopping cart by adding, removing, or updating items. 5 | - Stores cart data temporarily for guest users or long-term for registered users. 6 | """ 7 | 8 | from eclypse.remote.communication import rest 9 | from eclypse.remote.service import RESTService 10 | 11 | 12 | class CartService(RESTService): 13 | """REST endpoints for the Cart service.""" 14 | 15 | @rest.endpoint("/cart", "GET") 16 | def get_cart(self, **_): 17 | """Get the user's shopping cart. 18 | 19 | Returns: 20 | int: The HTTP status code. 21 | dict: The response body. 22 | 23 | Example: 24 | (200, { 25 | "items": [ 26 | {"id": "1", "quantity": 2}, 27 | {"id": "2", "quantity": 1}, 28 | ], 29 | }) 30 | """ 31 | return 200, { 32 | "items": [ 33 | {"id": "1", "quantity": 2}, 34 | {"id": "2", "quantity": 1}, 35 | ], 36 | } 37 | -------------------------------------------------------------------------------- /examples/grid_analysis/policies/degrade.py: -------------------------------------------------------------------------------- 1 | from networkx.classes.reportviews import ( 2 | EdgeView, 3 | NodeView, 4 | ) 5 | 6 | 7 | def degrade_value(value, degradation_rate): 8 | return max(0, value * (1 - degradation_rate)) 9 | 10 | 11 | def degrade_policy(target_degradation: float, epochs: int): 12 | degradation_rate = 1 - (target_degradation ** (1 / epochs)) 13 | 14 | def node_update_wrapper(nodes: NodeView): 15 | for _, resources in nodes.data(): 16 | for key in resources: 17 | if key in ["cpu", "gpu", "ram", "storage", "availability"]: 18 | resources[key] = degrade_value(resources[key], degradation_rate) 19 | 20 | def edge_update_wrapper(edges: EdgeView): 21 | for _, _, resources in edges.data(): 22 | for key in resources: 23 | resources[key] = degrade_value(resources[key], degradation_rate) 24 | 25 | resources["latency"] = resources["latency"] * (1 + degradation_rate) 26 | 27 | resources["bandwidth"] = degrade_value( 28 | resources["bandwidth"], degradation_rate 29 | ) 30 | 31 | return node_update_wrapper, edge_update_wrapper 32 | -------------------------------------------------------------------------------- /examples/image_prediction/application.py: -------------------------------------------------------------------------------- 1 | from services import ( 2 | EndService, 3 | PredictorService, 4 | TrainerService, 5 | ) 6 | 7 | from eclypse.graph import Application 8 | from eclypse.utils.constants import MAX_LATENCY 9 | 10 | image_app = Application("ImagePrediction", include_default_assets=True) 11 | 12 | image_app.add_service( 13 | EndService("EndService"), 14 | cpu=1, 15 | gpu=0, 16 | ram=0.5, 17 | storage=0.5, 18 | availability=0.9, 19 | processing_time=5, 20 | ) 21 | 22 | image_app.add_service( 23 | PredictorService("PredictorService"), 24 | cpu=1, 25 | gpu=1, 26 | ram=16.0, 27 | storage=64.0, 28 | availability=0.9, 29 | ) 30 | 31 | image_app.add_service( 32 | TrainerService("TrainerService"), 33 | cpu=16, 34 | gpu=4, 35 | ram=16.0, 36 | storage=2.0, 37 | availability=0.9, 38 | ) 39 | 40 | 41 | image_app.add_edge( 42 | "EndService", 43 | "PredictorService", 44 | latency=MAX_LATENCY, 45 | bandwidth=20.0, 46 | symmetric=True, 47 | ) 48 | 49 | image_app.add_edge( 50 | "PredictorService", 51 | "TrainerService", 52 | latency=MAX_LATENCY, 53 | bandwidth=100.0, 54 | symmetric=True, 55 | ) 56 | -------------------------------------------------------------------------------- /docs/source/overview/examples/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :hidden: 8 | 9 | echo 10 | sock_shop 11 | 12 | We have implemented several examples to demonstrate the capabilities of ECLYPSE. 13 | 14 | The code of the examples is available at the :icon:`fa-brands fa-github` `eclypse/examples `_ subdirectory of the GitHub repository. 15 | 16 | .. grid:: 1 17 | 18 | .. grid-item:: 19 | 20 | .. card:: :octicon:`megaphone;1em;info` **Echo** 21 | :link-type: doc 22 | :link: echo 23 | 24 | An application made of 5 services that simply echo the messages they receive, unicasting and broadcasting them, to compare the waiting times of the different communication patterns. 25 | 26 | 27 | .. grid-item:: 28 | 29 | .. card:: :octicon:`package-dependents;1em;info` **SockShop** 30 | :link-type: doc 31 | :link: sock_shop 32 | 33 | A microservices application that simulates an online shop, with 7 services that interact with each other to simulate the purchase of socks. 34 | This example is provided in two versions that differ for the communication interface used by the services: MPI and REST. 35 | -------------------------------------------------------------------------------- /examples/echo/infrastructure.py: -------------------------------------------------------------------------------- 1 | from update_policy import ( 2 | edge_random_update, 3 | node_random_update, 4 | ) 5 | 6 | from eclypse.graph import Infrastructure 7 | 8 | 9 | # Creating an instance of the Infrastructure class 10 | def get_infrastructure(seed: int = 2) -> Infrastructure: 11 | echo_infra = Infrastructure( 12 | "EchoInfrastructure", 13 | node_update_policy=node_random_update, 14 | edge_update_policy=edge_random_update, 15 | include_default_assets=True, 16 | seed=seed, 17 | ) 18 | echo_infra.add_node("CloudServer") 19 | echo_infra.add_node("EdgeGateway") 20 | echo_infra.add_node("IoTDevice") 21 | echo_infra.add_node("CloudStorage") 22 | echo_infra.add_node("EdgeSensor") 23 | 24 | echo_infra.add_edge( 25 | "CloudServer", "EdgeGateway", latency=5.0, bandwidth=80.0, symmetric=True 26 | ) 27 | echo_infra.add_edge( 28 | "EdgeGateway", "IoTDevice", latency=8.0, bandwidth=50.0, symmetric=True 29 | ) 30 | echo_infra.add_edge( 31 | "IoTDevice", "CloudStorage", latency=15.0, bandwidth=100.0, symmetric=True 32 | ) 33 | echo_infra.add_edge( 34 | "CloudStorage", "EdgeSensor", latency=9.0, bandwidth=70.0, symmetric=True 35 | ) 36 | 37 | return echo_infra 38 | -------------------------------------------------------------------------------- /examples/sock_shop/mpi/main.py: -------------------------------------------------------------------------------- 1 | from update_policy import ( 2 | edge_random_update, 3 | node_random_update, 4 | ) 5 | 6 | from eclypse.builders.application import get_sock_shop 7 | from eclypse.builders.infrastructure import hierarchical 8 | from eclypse.placement.strategies import RandomStrategy 9 | from eclypse.simulation import Simulation 10 | from eclypse.simulation.config import SimulationConfig 11 | from eclypse.utils.constants import DEFAULT_SIM_PATH 12 | 13 | if __name__ == "__main__": 14 | seed = 22 15 | infrastructure = hierarchical( 16 | n=30, 17 | node_partitioning=[0.6, 0.1, 0.15, 0.15], 18 | node_update_policy=node_random_update, 19 | link_update_policy=edge_random_update, 20 | include_default_assets=True, 21 | symmetric=True, 22 | seed=seed, 23 | ) 24 | 25 | sim_config = SimulationConfig( 26 | seed=seed, 27 | step_every_ms=500, 28 | max_steps=100, 29 | path=DEFAULT_SIM_PATH / "SockShopMPI", 30 | remote=True, 31 | include_default_metrics=True, 32 | ) 33 | 34 | sim = Simulation(infrastructure, simulation_config=sim_config) 35 | 36 | app = get_sock_shop(communication_interface="mpi", include_default_assets=True) 37 | 38 | sim.register(app, RandomStrategy(seed=seed)) 39 | sim.start() 40 | sim.wait() 41 | -------------------------------------------------------------------------------- /examples/echo/update_policy.py: -------------------------------------------------------------------------------- 1 | import random as rnd 2 | 3 | from networkx.classes.reportviews import ( 4 | EdgeView, 5 | NodeView, 6 | ) 7 | 8 | 9 | def node_random_update(nodes: NodeView): 10 | for _, resources in nodes.data(): 11 | if rnd.random() < 0.02: 12 | resources["availability"] = 0 13 | elif rnd.random() < 0.5 and resources["availability"] == 0: 14 | resources["availability"] = 1 15 | else: 16 | resources["cpu"] = round(max(0, resources["cpu"] * rnd.uniform(0.95, 1.05))) 17 | resources["gpu"] = round(max(0, resources["gpu"] * rnd.uniform(0.9, 1.1))) 18 | resources["ram"] = round(max(0, resources["ram"] * rnd.uniform(0.8, 1.2))) 19 | resources["storage"] = round( 20 | max(0, resources["storage"] * rnd.uniform(0.9, 1.1)) 21 | ) 22 | resources["availability"] = min( 23 | 1, max(0, resources["availability"] * rnd.uniform(0.995, 1.005)) 24 | ) 25 | 26 | 27 | def edge_random_update(edges: EdgeView): 28 | for _, _, resources in edges.data(): 29 | resources["latency"] = round( 30 | max(0, resources["latency"] * rnd.uniform(0.9, 1.1)) 31 | ) 32 | resources["bandwidth"] = round( 33 | max(0, resources["bandwidth"] * rnd.uniform(0.95, 1.05)) 34 | ) 35 | -------------------------------------------------------------------------------- /eclypse/report/reporters/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for managing simulation reporters, including the off-the-shelf ones. 2 | 3 | If you are interested in creating HTML reports, use the 4 | `eclypse-html-report ` CLI tool. 5 | """ 6 | 7 | from typing import Callable, Dict, List, Optional 8 | 9 | from eclypse.report import Reporter 10 | 11 | from .csv import CSVReporter 12 | from .gml import GMLReporter 13 | from .json import JSONReporter 14 | from .tensorboard import TensorBoardReporter 15 | 16 | 17 | def get_default_reporters( 18 | requested_reporters: Optional[List[str]], 19 | ) -> Dict[str, Callable[..., Reporter]]: 20 | """Get the default reporters, comprising CSV, GML, JSON, and TensorBoard. 21 | 22 | Returns: 23 | Dict[str, Type[Reporter]]: The default reporters. 24 | """ 25 | default_reporters = { 26 | "csv": CSVReporter, 27 | "gml": GMLReporter, 28 | "json": JSONReporter, 29 | "tensorboard": TensorBoardReporter, 30 | } 31 | 32 | return ( 33 | {k: v for k, v in default_reporters.items() if k in requested_reporters} 34 | if requested_reporters 35 | else {} 36 | ) 37 | 38 | 39 | __all__ = [ 40 | "CSVReporter", 41 | "GMLReporter", 42 | "JSONReporter", 43 | "Reporter", 44 | "TensorBoardReporter", 45 | "get_default_reporters", 46 | ] 47 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/catalog.py: -------------------------------------------------------------------------------- 1 | """The `CatalogService` is responsible for managing and serving product information. 2 | 3 | - Key Responsibilities: 4 | - 5 | - Supports operations such as searching for products and listing available items \ 6 | in the store. 7 | - Interfaces with the underlying data store to fetch product data. 8 | """ 9 | 10 | from eclypse.remote.communication import rest 11 | from eclypse.remote.service import RESTService 12 | 13 | 14 | class CatalogService(RESTService): 15 | """REST endpoints for the Catalog service.""" 16 | 17 | @rest.endpoint("/catalog", "GET") 18 | def get_catalog(self, **_): 19 | """Get the catalog, retrieving product details such as name, price, and description. 20 | 21 | Returns: 22 | int: The HTTP status code. 23 | dict: The response body. 24 | 25 | Example: 26 | (200, { 27 | "products": [ 28 | {"id": "1", "name": "Product 1", "price": 19.99}, 29 | {"id": "2", "name": "Product 2", "price": 29.99}, 30 | ], 31 | }) 32 | """ 33 | return 200, { 34 | "products": [ 35 | {"id": "1", "name": "Product 1", "price": 19.99}, 36 | {"id": "2", "name": "Product 2", "price": 29.99}, 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /eclypse/remote/communication/rest/codes.py: -------------------------------------------------------------------------------- 1 | """Module for an integer enumeration for HTTP status codes.""" 2 | 3 | from enum import IntEnum 4 | 5 | 6 | class HTTPStatusCode(IntEnum): 7 | """HTTP status codes used by the `EclypseREST` communication interface. 8 | 9 | Attributes: 10 | OK: 200 - OK status code. 11 | CREATED: 201 - Created status code. 12 | NO_CONTENT: 204 - No Content status code. 13 | BAD_REQUEST: 400 - Bad Request status code. 14 | UNAUTHORIZED: 401 - Unauthorized status code. 15 | FORBIDDEN: 403 - Forbidden status code. 16 | NOT_FOUND: 404 - Not Found status code. 17 | METHOD_NOT_ALLOWED: 405 - Method Not Allowed status code. 18 | CONFLICT: 409 - Conflict status code. 19 | INTERNAL_SERVER_ERROR: 500 - Internal Server Error status code. 20 | NOT_IMPLEMENTED: 501 - Not Implemented status code. 21 | SERVICE_UNAVAILABLE: 503 - Service Unavailable status code. 22 | GATEWAY_TIMEOUT: 504 - Gateway Timeout status code. 23 | """ 24 | 25 | OK = 200 26 | CREATED = 201 27 | NO_CONTENT = 204 28 | BAD_REQUEST = 400 29 | UNAUTHORIZED = 401 30 | FORBIDDEN = 403 31 | NOT_FOUND = 404 32 | METHOD_NOT_ALLOWED = 405 33 | CONFLICT = 409 34 | INTERNAL_SERVER_ERROR = 500 35 | NOT_IMPLEMENTED = 501 36 | SERVICE_UNAVAILABLE = 503 37 | GATEWAY_TIMEOUT = 504 38 | -------------------------------------------------------------------------------- /docs/source/overview/advanced/index.rst: -------------------------------------------------------------------------------- 1 | Advanced Features 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :hidden: 7 | 8 | triggers 9 | emulation/index 10 | reporting 11 | 12 | .. grid:: 2 13 | 14 | .. grid-item:: 15 | 16 | .. card:: :octicon:`workflow;1em;info` **Triggers** 17 | :link-type: doc 18 | :link: triggers 19 | 20 | Learn how to define and use **triggers** to activate complex workflows in your simulation. 21 | Explore built-in triggers or create your own custom triggers to suit your simulation needs. 22 | 23 | .. grid-item:: 24 | 25 | .. card:: :octicon:`codespaces;1em;info` **Emulation** 26 | :link-type: doc 27 | :link: emulation/index 28 | 29 | Learn how to move from a simulation to a real-world application by defining the **logics** of your **application services** and running them on a *physical* machine or a *cluster* of machines. 30 | 31 | .. grid-item:: 32 | 33 | .. card:: :octicon:`log;1em;info` **Reporting** 34 | :link-type: doc 35 | :link: reporting 36 | 37 | Discover how to generate and manage **reports** in your simulation, including **custom reports** and **report templates**. 38 | Learn how to use and customise the **reporting engine** to create and manage simulation 39 | metrics. 40 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/user.py: -------------------------------------------------------------------------------- 1 | """The `UserService` class. 2 | 3 | It manages all user-related functionality, including registration,\ 4 | authentication, and profile management. 5 | 6 | - Key Responsibilities: 7 | - Handles user sign-up, login, and logout processes. 8 | - Manages user data, including credentials, personal information, and addresses. 9 | """ 10 | 11 | from eclypse.remote.communication import rest 12 | from eclypse.remote.service import RESTService 13 | 14 | 15 | class UserService(RESTService): 16 | """REST endpoints for the User service.""" 17 | 18 | @rest.endpoint("/user", "GET") 19 | def get_catalog(self, user_id: int, **_): 20 | """Get the user's profile information. 21 | 22 | Args: 23 | user_id (int): The user's ID. 24 | 25 | Returns: 26 | int: The HTTP status code. 27 | dict: The response body. 28 | 29 | Example: 30 | (200, { 31 | "user_id": 12345, 32 | "name": "John Doe", 33 | "email": " 34 | "address": "123 Main St", 35 | "phone": "555-1234", 36 | }) 37 | """ 38 | return 200, { 39 | "user_id": user_id, 40 | "name": "John Doe", 41 | "email": "john@example.com", 42 | "address": "123 Main St", 43 | "phone": "555-1234", 44 | } 45 | -------------------------------------------------------------------------------- /examples/image_prediction/services/end_service.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random as rnd 4 | from typing import ( 5 | TYPE_CHECKING, 6 | Optional, 7 | ) 8 | 9 | from eclypse.remote.service import Service 10 | 11 | from .utils import load_data 12 | 13 | if TYPE_CHECKING: 14 | from torchvision.datasets import MNIST 15 | 16 | 17 | class EndService(Service): 18 | def __init__(self, name: str): 19 | super().__init__(name, comm_interface="rest") 20 | self.data: Optional[MNIST] = None 21 | self.img_counter = 0 22 | self.correct = 0 23 | 24 | async def step(self): 25 | image_idx = rnd.randint(0, len(self.data) - 1) 26 | image, label = self.data[image_idx] 27 | 28 | image_pred = await self.rest.get("PredictorService/predict", image=image) 29 | image_pred = image_pred.body 30 | if image_pred["predictions"] == label: 31 | self.correct += 1 32 | self.logger.success(f"Prediction on image {image_idx} is correct.") 33 | else: 34 | self.logger.error(f"Prediction on image {image_idx} is incorrect.") 35 | self.img_counter += 1 36 | 37 | def on_deploy(self): 38 | self.logger.info(f"{self.id} deployed. Loading dataset...") 39 | self.data = load_data(train=False) 40 | self.logger.info(f"Dataset loaded successfully.") 41 | 42 | def on_undeploy(self): 43 | self.data = None 44 | -------------------------------------------------------------------------------- /examples/sock_shop/rest/main.py: -------------------------------------------------------------------------------- 1 | from update_policy import ( 2 | edge_random_update, 3 | node_random_update, 4 | ) 5 | 6 | from eclypse.builders.application import get_sock_shop 7 | from eclypse.builders.infrastructure import hierarchical 8 | from eclypse.placement.strategies import RandomStrategy 9 | from eclypse.placement.strategies.random import RandomStrategy 10 | from eclypse.simulation import Simulation 11 | from eclypse.simulation.config import SimulationConfig 12 | from eclypse.utils.constants import DEFAULT_SIM_PATH 13 | 14 | if __name__ == "__main__": 15 | seed = 22 16 | infrastructure = hierarchical( 17 | n=30, 18 | node_partitioning=[0.6, 0.2, 0.1, 0.1], 19 | node_update_policy=node_random_update, 20 | link_update_policy=edge_random_update, 21 | include_default_assets=True, 22 | symmetric=True, 23 | seed=seed, 24 | ) 25 | 26 | sim_config = SimulationConfig( 27 | seed=seed, 28 | step_every_ms=500, 29 | max_steps=100, 30 | path=DEFAULT_SIM_PATH / "SockShopREST", 31 | include_default_metrics=True, 32 | remote=True, 33 | ) 34 | 35 | sim = Simulation( 36 | infrastructure, 37 | simulation_config=sim_config, 38 | ) 39 | 40 | app = get_sock_shop(communication_interface="rest", include_default_assets=True) 41 | 42 | sim.register(app, RandomStrategy(seed=seed)) 43 | sim.start() 44 | sim.wait() 45 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block attributes %} 6 | {% if attributes %} 7 | .. rubric:: {{ _('Module Attributes') }} 8 | 9 | .. autosummary:: 10 | :toctree: {{ name }} 11 | {% for item in attributes %} 12 | {{ item }} 13 | {%- endfor %} 14 | {% endif %} 15 | {% endblock %} 16 | 17 | {% block functions %} 18 | {% if functions %} 19 | .. rubric:: {{ _('Functions') }} 20 | 21 | .. autosummary:: 22 | :toctree: {{ name }} 23 | {% for item in functions %} 24 | {{ item }} 25 | {%- endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block classes %} 30 | {% if classes %} 31 | .. rubric:: {{ _('Classes') }} 32 | 33 | .. autosummary:: 34 | :toctree: {{ name }} 35 | {% for item in classes %} 36 | {{ item }} 37 | {%- endfor %} 38 | {% endif %} 39 | {% endblock %} 40 | 41 | {% block exceptions %} 42 | {% if exceptions %} 43 | .. rubric:: {{ _('Exceptions') }} 44 | 45 | .. autosummary:: 46 | :toctree: {{ name }} 47 | {% for item in exceptions %} 48 | {{ item }} 49 | {%- endfor %} 50 | {% endif %} 51 | {% endblock %} 52 | 53 | {% block modules %} 54 | {% if modules %} 55 | .. rubric:: Modules 56 | 57 | .. autosummary:: 58 | :toctree: {{ name }} 59 | :recursive: 60 | {% for item in modules %} 61 | {{ item.split('.')[-1] }} 62 | {%- endfor %} 63 | {% endif %} 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /examples/sock_shop/mpi/update_policy.py: -------------------------------------------------------------------------------- 1 | import random as rnd 2 | 3 | from networkx.classes.reportviews import ( 4 | EdgeView, 5 | NodeView, 6 | ) 7 | 8 | 9 | # update edges 10 | def node_random_update(nodes: NodeView): 11 | for _, resources in nodes.data(): 12 | if rnd.random() < 0.02: 13 | resources["availability"] = 0 14 | elif rnd.random() < 0.5 and resources["availability"] == 0: 15 | resources["availability"] = 1 16 | else: 17 | # Randomly update resources with different ranges 18 | resources["cpu"] = round(max(0, resources["cpu"] * rnd.uniform(0.95, 1.05))) 19 | resources["gpu"] = round(max(0, resources["gpu"] * rnd.uniform(0.9, 1.1))) 20 | resources["ram"] = round(max(0, resources["ram"] * rnd.uniform(0.8, 1.2))) 21 | resources["storage"] = round( 22 | max(0, resources["storage"] * rnd.uniform(0.9, 1.1)) 23 | ) 24 | resources["availability"] = min( 25 | 1, max(0, resources["availability"] * rnd.uniform(0.995, 1.005)) 26 | ) 27 | 28 | 29 | def edge_random_update(edges: EdgeView): 30 | for _, _, resources in edges.data(): 31 | # Randomly update resources with different ranges 32 | resources["latency"] = round( 33 | max(0, resources["latency"] * rnd.uniform(0.9, 1.1)) 34 | ) 35 | resources["bandwidth"] = round( 36 | max(0, resources["bandwidth"] * rnd.uniform(0.95, 1.05)) 37 | ) 38 | -------------------------------------------------------------------------------- /examples/sock_shop/rest/update_policy.py: -------------------------------------------------------------------------------- 1 | import random as rnd 2 | 3 | from networkx.classes.reportviews import ( 4 | EdgeView, 5 | NodeView, 6 | ) 7 | 8 | 9 | # update edges 10 | def node_random_update(nodes: NodeView): 11 | for _, resources in nodes.data(): 12 | if rnd.random() < 0.02: 13 | resources["availability"] = 0 14 | elif rnd.random() < 0.5 and resources["availability"] == 0: 15 | resources["availability"] = 1 16 | else: 17 | # Randomly update resources with different ranges 18 | resources["cpu"] = round(max(0, resources["cpu"] * rnd.uniform(0.95, 1.05))) 19 | resources["gpu"] = round(max(0, resources["gpu"] * rnd.uniform(0.9, 1.1))) 20 | resources["ram"] = round(max(0, resources["ram"] * rnd.uniform(0.8, 1.2))) 21 | resources["storage"] = round( 22 | max(0, resources["storage"] * rnd.uniform(0.9, 1.1)) 23 | ) 24 | resources["availability"] = min( 25 | 1, max(0, resources["availability"] * rnd.uniform(0.995, 1.005)) 26 | ) 27 | 28 | 29 | def edge_random_update(edges: EdgeView): 30 | for _, _, resources in edges.data(): 31 | # Randomly update resources with different ranges 32 | resources["latency"] = round( 33 | max(0, resources["latency"] * rnd.uniform(0.9, 1.1)) 34 | ) 35 | resources["bandwidth"] = round( 36 | max(0, resources["bandwidth"] * rnd.uniform(0.95, 1.05)) 37 | ) 38 | -------------------------------------------------------------------------------- /eclypse/remote/communication/mpi/response.py: -------------------------------------------------------------------------------- 1 | """Module for the Response class. 2 | 3 | It is used to acknowledge the processing of a message exchange within an MPIRequest. 4 | """ 5 | 6 | from datetime import datetime 7 | from typing import Optional 8 | 9 | from eclypse.remote.utils import ResponseCode 10 | 11 | 12 | class Response: 13 | """Response class. 14 | 15 | A Response is a data structure for acknowledging the processing of a message 16 | exchange within an `MPIRequest`. 17 | """ 18 | 19 | def __init__( 20 | self, 21 | code: ResponseCode = ResponseCode.OK, 22 | timestamp: Optional[datetime] = None, 23 | ): 24 | """Initializes a Response object. 25 | 26 | Args: 27 | code (ResponseCode): The response code. 28 | timestamp (datetime.datetime): The timestamp of the response. 29 | """ 30 | self.code = code 31 | self.timestamp = timestamp if timestamp is not None else datetime.now() 32 | 33 | def __str__(self) -> str: 34 | """Returns a string representation of the response. 35 | 36 | Returns: 37 | str: The string representation of the response, in the format: 38 | - 39 | """ 40 | return f"{self.timestamp} - {self.code}" 41 | 42 | def __repr__(self) -> str: 43 | """Returns the official string representation of the response. 44 | 45 | Returns: 46 | str: The string representation of the response, same as __str__. 47 | """ 48 | return self.__str__() 49 | -------------------------------------------------------------------------------- /examples/image_prediction/main.py: -------------------------------------------------------------------------------- 1 | from application import image_app as app 2 | from metrics import get_metrics 3 | from services.utils import ( 4 | BASE_PATH, 5 | STEP_EVERY_MS, 6 | STEPS, 7 | ) 8 | from update_policy import DegradePolicy 9 | 10 | from eclypse.builders.infrastructure import star 11 | from eclypse.placement.strategies import RandomStrategy 12 | from eclypse.remote.bootstrap import ( 13 | RayOptionsFactory, 14 | RemoteBootstrap, 15 | ) 16 | from eclypse.simulation import ( 17 | Simulation, 18 | SimulationConfig, 19 | ) 20 | 21 | if __name__ == "__main__": 22 | 23 | seed = 2 24 | with_gpus = RemoteBootstrap(ray_options_factory=RayOptionsFactory(num_gpus=0.1)) 25 | 26 | sim_config = SimulationConfig( 27 | seed=seed, 28 | max_steps=STEPS, 29 | step_every_ms=STEP_EVERY_MS, 30 | include_default_metrics=False, 31 | events=get_metrics(), 32 | log_to_file=True, 33 | path=BASE_PATH, 34 | remote=True, # use "with_gpus" instead of "True" if you have available GPUs 35 | ) 36 | 37 | sim = Simulation( 38 | star( 39 | infrastructure_id="IPInfr", 40 | n_clients=5, 41 | seed=seed, 42 | link_update_policy=DegradePolicy(epochs=STEPS), 43 | include_default_assets=True, 44 | resource_init="max", 45 | symmetric=True, 46 | ), 47 | simulation_config=sim_config, 48 | ) 49 | strategy = RandomStrategy(spread=True) 50 | 51 | sim.register(app, strategy) 52 | sim.start() 53 | sim.wait() 54 | -------------------------------------------------------------------------------- /examples/echo/echo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from eclypse.remote.service import Service 5 | 6 | 7 | class EchoService(Service): 8 | def __init__(self, id: str): 9 | super().__init__(id, store_step=True) 10 | self.i = 0 11 | 12 | async def step(self): 13 | message = {"message": f"Hello from {self.id}!"} 14 | 15 | neigh = await self.mpi.get_neighbors() 16 | expected_wait_unicast = 0 17 | t_init_unicast = time.time() 18 | for n in neigh: 19 | req = await self.mpi.send(n, message) 20 | expected_wait_unicast += req.route.cost(message) if req.route else 0 21 | t_final_unicast = time.time() 22 | t_unicast = t_final_unicast - t_init_unicast 23 | self.logger.info( 24 | f"Service {self.id}, {self.i} - Unicasts in: {t_unicast}, expected = {expected_wait_unicast}" 25 | ) 26 | t_init_broadcast = time.time() 27 | req = await self.mpi.send(neigh, message) 28 | expected_wait_broadcast = max( 29 | [r.cost(message) for r in req.routes if r], default=0 30 | ) 31 | t_final_broadcast = time.time() 32 | t_broadcast = t_final_broadcast - t_init_broadcast 33 | self.logger.info( 34 | f"Service {self.id}, {self.i} - Broadcasts in: {t_broadcast}, expected = {expected_wait_broadcast}" 35 | ) 36 | self.i += 1 37 | await asyncio.sleep(1) 38 | return ( 39 | self.i, 40 | t_unicast, 41 | expected_wait_unicast, 42 | t_broadcast, 43 | expected_wait_broadcast, 44 | ) 45 | -------------------------------------------------------------------------------- /examples/image_prediction/update_policy.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import numpy as np 4 | from networkx.classes.reportviews import EdgeView 5 | 6 | 7 | def exponential_decay(init, target, N, decay_rate=None): 8 | 9 | # Compute decay rate if not provided 10 | if decay_rate is None: 11 | decay_rate = np.log(init / target) / N 12 | 13 | # Calculate values for each step n 14 | steps = np.arange(0, N + 1) 15 | values = target + (init - target) * np.exp(-decay_rate * steps) 16 | 17 | return values 18 | 19 | 20 | class DegradePolicy: 21 | 22 | def __init__(self, epochs: int): 23 | self.epochs = epochs 24 | self.starting_decay_epoch = int(0.75 * epochs) 25 | self.init_latency = defaultdict(lambda: None) 26 | self.values = defaultdict(lambda: None) 27 | self.i = 0 28 | self.target_latency = 1000 29 | 30 | def __call__(self, edges: EdgeView): 31 | if self.i > self.starting_decay_epoch: 32 | for s, d, resources in edges.data(): 33 | if self.init_latency[(s, d)] is None: 34 | self.init_latency[(s, d)] = resources["latency"] 35 | self.values[(s, d)] = exponential_decay( 36 | self.init_latency[(s, d)], 37 | self.target_latency, 38 | self.epochs - self.starting_decay_epoch, 39 | decay_rate=0.005, 40 | ) 41 | 42 | else: 43 | resources["latency"] = self.values[(s, d)][ 44 | self.i - self.starting_decay_epoch 45 | ] 46 | self.i += 1 47 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/shipping.py: -------------------------------------------------------------------------------- 1 | """The `ShippingService` class. 2 | 3 | It manages the logistics and shipment of orders, ensuring items reach customers. 4 | 5 | - Key Responsibilities: 6 | - Handles the shipping of completed orders. 7 | - Calculates shipping costs, delivery times, and tracks shipment status. 8 | - Coordinates with third-party shipping providers for physical delivery. 9 | """ 10 | 11 | from eclypse.remote.communication import rest 12 | from eclypse.remote.service import RESTService 13 | 14 | 15 | class ShippingService(RESTService): 16 | """REST endpoints for the Shipping service.""" 17 | 18 | @rest.endpoint("/details", "GET") 19 | def get_shipping_detils(self, order_id, **_): 20 | """Get the shipping details for an order. 21 | 22 | Args: 23 | order_id (str): The order ID. 24 | 25 | Returns: 26 | int: The HTTP status code. 27 | dict: The response body. 28 | 29 | Example: 30 | (200, { 31 | "order_id": "12345", 32 | "status": "success", 33 | "shipping_details": { 34 | "carrier": "UPS", 35 | "tracking_number": "1234567890", 36 | "estimated_delivery_date": "2024-04-09", 37 | }, 38 | }) 39 | """ 40 | return 200, { 41 | "order_id": order_id, 42 | "status": "success", 43 | "shipping_details": { 44 | "carrier": "UPS", 45 | "tracking_number": "1234567890", 46 | "estimated_delivery_date": "2024-04-09", 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /examples/grid_analysis/metrics.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from typing import TYPE_CHECKING 5 | 6 | import psutil 7 | 8 | from eclypse.report.metrics import metric 9 | from eclypse.report.metrics.defaults import ( 10 | SimulationTime, 11 | StepNumber, 12 | alive_nodes, 13 | response_time, 14 | seed, 15 | ) 16 | 17 | if TYPE_CHECKING: 18 | from eclypse.graph import ( 19 | Application, 20 | Infrastructure, 21 | ) 22 | from eclypse.placement import Placement 23 | 24 | 25 | @metric.application 26 | def used_nodes(_: Application, placement: Placement, __: Infrastructure): 27 | return len(set(placement.mapping.values())) 28 | 29 | 30 | @metric.application 31 | def is_placed(app: Application, placement: Placement, _: Infrastructure): 32 | return len(placement.mapping) == len(app.nodes) 33 | 34 | 35 | @metric.simulation(name="cpu_usage", activates_on=["step", "stop"]) 36 | class CPUMonitor: 37 | 38 | def __init__(self): 39 | self.process = psutil.Process(os.getpid()) 40 | 41 | def __call__(self, event): 42 | return self.process.cpu_percent(interval=0.1) 43 | 44 | 45 | @metric.simulation(name="memory_usage", activates_on=["step", "stop"]) 46 | class MemoryMonitor: 47 | 48 | def __init__(self): 49 | self.process = psutil.Process(os.getpid()) 50 | 51 | def __call__(self, event): 52 | memory_usage = self.process.memory_info().rss 53 | return memory_usage / (1024 * 1024) # Convert to MB 54 | 55 | 56 | def get_metrics(): 57 | return [ 58 | response_time, 59 | used_nodes, 60 | is_placed, 61 | SimulationTime(), 62 | StepNumber(), 63 | alive_nodes, 64 | seed, 65 | ] 66 | -------------------------------------------------------------------------------- /eclypse/remote/communication/mpi/requests/broadcast.py: -------------------------------------------------------------------------------- 1 | """Module for the BroadcastRequest class, subclassing MulticastRequest. 2 | 3 | It represents a request to broadcast a message to all neighbor services in the network. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import ( 9 | TYPE_CHECKING, 10 | Any, 11 | Dict, 12 | Generator, 13 | Optional, 14 | ) 15 | 16 | from eclypse.remote import ray_backend 17 | 18 | from .multicast import MulticastRequest 19 | 20 | if TYPE_CHECKING: 21 | from datetime import datetime 22 | 23 | from eclypse.remote.communication.mpi import EclypseMPI 24 | 25 | 26 | class BroadcastRequest(MulticastRequest): 27 | """Request for broadcasting a message to all neighbor services in the network.""" 28 | 29 | def __init__( 30 | self, 31 | body: Dict[str, Any], 32 | _mpi: EclypseMPI, 33 | timestamp: Optional[datetime] = None, 34 | ): 35 | """Initializes a BroadcastRequest object. 36 | 37 | Args: 38 | body (Dict[str, Any]): The body of the request. 39 | _mpi (EclypseMPI): The MPI interface. 40 | timestamp (Optional[datetime], optional): The timestamp of the request. 41 | Defaults to None. 42 | """ 43 | super().__init__( 44 | recipient_ids=ray_backend.get(_mpi.get_neighbors()), 45 | body=body, 46 | _mpi=_mpi, 47 | timestamp=timestamp, 48 | ) 49 | 50 | def __await__(self) -> Generator[Any, None, BroadcastRequest]: 51 | """Await the request to complete. 52 | 53 | Returns: 54 | Awaitable: The result of the request. 55 | """ 56 | return super().__await__() # type: ignore[return-value] 57 | -------------------------------------------------------------------------------- /eclypse/remote/communication/mpi/requests/multicast.py: -------------------------------------------------------------------------------- 1 | """Module for the MulticastRequest class, subclassing MPIRequest. 2 | 3 | It represents a request to send a message to multiple recipients. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import ( 9 | TYPE_CHECKING, 10 | Any, 11 | Dict, 12 | Generator, 13 | List, 14 | Optional, 15 | ) 16 | 17 | from eclypse.remote.communication import EclypseRequest 18 | 19 | if TYPE_CHECKING: 20 | from datetime import datetime 21 | 22 | from eclypse.remote.communication.mpi import EclypseMPI 23 | 24 | 25 | class MulticastRequest(EclypseRequest): 26 | """A request to send a message to multiple recipients.""" 27 | 28 | def __init__( 29 | self, 30 | recipient_ids: List[str], 31 | body: Dict[str, Any], 32 | _mpi: EclypseMPI, 33 | timestamp: Optional[datetime] = None, 34 | ): 35 | """Initializes a MulticastRequest object. 36 | 37 | Args: 38 | recipient_ids (List[str]): The IDs of the recipient nodes. 39 | body (Dict[str, Any]): The body of the request. 40 | _mpi (EclypseMPI): The MPI interface. 41 | timestamp (Optional[datetime], optional): The timestamp of the request. 42 | Defaults to None. 43 | """ 44 | super().__init__( 45 | recipient_ids=recipient_ids, 46 | data=body, 47 | _comm=_mpi, 48 | timestamp=timestamp, 49 | ) 50 | 51 | def __await__(self) -> Generator[Any, None, MulticastRequest]: 52 | """Await the request to complete. 53 | 54 | Returns: 55 | Awaitable: The result of the request. 56 | """ 57 | return super().__await__() # type: ignore[return-value] 58 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | target-version = "py312" 2 | 3 | include = ["eclypse/**/*.py"] 4 | line-length = 88 5 | indent-width = 4 6 | 7 | exclude = [ 8 | ".git", 9 | ".hg", 10 | ".mypy_cache", 11 | ".ruff_cache", 12 | ".tox", 13 | ".venv", 14 | "build", 15 | "dist", 16 | "tests", 17 | "examples", 18 | "docs", 19 | ] 20 | 21 | [format] 22 | quote-style = "double" 23 | indent-style = "space" 24 | docstring-code-format = true 25 | skip-magic-trailing-comma = false 26 | 27 | [lint] 28 | select = [ 29 | "E", 30 | "F", 31 | "W", 32 | # "I", # isort works better so far 33 | "B", 34 | "UP", 35 | "SIM", 36 | "C90", 37 | "RUF", 38 | "TCH", 39 | "PL", 40 | "ARG", 41 | "D", 42 | ] 43 | 44 | ignore = [ 45 | "D203", 46 | "D213", 47 | "D100", 48 | "D104", 49 | "E501", 50 | "PLC0415", 51 | "UP006", 52 | "UP007", 53 | "UP008", 54 | "UP035", 55 | "UP045", 56 | ] 57 | 58 | fixable = ["ALL"] 59 | unfixable = ["UP045", "UP035"] 60 | 61 | [lint.mccabe] 62 | max-complexity = 10 63 | 64 | [lint.pylint] 65 | max-args = 20 66 | max-locals = 30 67 | max-branches = 15 68 | max-positional-args = 20 69 | 70 | # [lint.isort] 71 | # known-first-party = ["eclypse"] 72 | # combine-as-imports = true 73 | # force-sort-within-sections = false 74 | # force-wrap-aliases = true 75 | # split-on-trailing-comma = true 76 | 77 | [lint.pycodestyle] 78 | max-doc-length = 100 79 | max-line-length = 88 80 | 81 | [lint.pydocstyle] 82 | convention = "google" 83 | 84 | [lint.flake8-type-checking] 85 | strict = true 86 | 87 | [lint.flake8-tidy-imports] 88 | banned-module-level-imports = ["ray", "tensorboardX"] 89 | 90 | [lint.per-file-ignores] 91 | "**/__init__.py" = ["I001"] # allow re-exported symbols 92 | "tests/**" = ["D"] 93 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v6.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: detect-private-key 10 | - id: requirements-txt-fixer 11 | 12 | - repo: https://github.com/pycqa/isort 13 | rev: 7.0.0 14 | hooks: 15 | - id: isort 16 | 17 | - repo: https://github.com/commitizen-tools/commitizen 18 | rev: v4.9.1 19 | hooks: 20 | - id: commitizen 21 | - id: commitizen-branch 22 | stages: 23 | - pre-push 24 | 25 | - repo: https://github.com/astral-sh/ruff-pre-commit 26 | rev: v0.14.4 27 | hooks: 28 | - id: ruff-check 29 | name: ruff pycln 30 | args: [--fix, --select, "F401"] 31 | 32 | - repo: https://github.com/astral-sh/ruff-pre-commit 33 | rev: v0.14.8 34 | hooks: 35 | - id: ruff-check 36 | - id: ruff-format 37 | 38 | # - repo: https://github.com/psf/black 39 | # rev: 24.2.0 40 | # hooks: 41 | # - id: black 42 | 43 | # - repo: https://github.com/PyCQA/docformatter 44 | # rev: v1.7.7 45 | # hooks: 46 | # - id: docformatter 47 | # args: [--in-place, --config, ./pyproject.toml] 48 | 49 | # - repo: https://github.com/hadialqattan/pycln 50 | # rev: v2.4.0 51 | # hooks: 52 | # - id: pycln 53 | # args: [--config, ./pyproject.toml] 54 | 55 | - repo: https://github.com/pre-commit/mirrors-mypy 56 | rev: v1.18.2 57 | hooks: 58 | - id: mypy 59 | exclude: ^docs/ 60 | 61 | # - repo: local 62 | # hooks: 63 | # - id: pylint 64 | # name: pylint 65 | # entry: pylint 66 | # language: system 67 | # types: [python] 68 | 69 | exclude: "examples/.*" 70 | -------------------------------------------------------------------------------- /eclypse/remote/service/rest.py: -------------------------------------------------------------------------------- 1 | """Module for the RESTService class. 2 | 3 | It uses the REST interface to communicate with other services in the same application. 4 | 5 | It differs from a base Service, since it runs its own loop forever, handling the 6 | communication with other services through HTTP requests. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from .service import Service 12 | 13 | 14 | class RESTService(Service): 15 | """Base class for services in ECLYPSE remote applications.""" 16 | 17 | def __init__( 18 | self, 19 | service_id: str, 20 | ): 21 | """Initializes a Service object. 22 | 23 | Args: 24 | service_id (str): The name of the service. 25 | """ 26 | super().__init__(service_id=service_id, comm_interface="rest") 27 | 28 | async def step(self): 29 | """The service's main loop. 30 | 31 | This method must be overridden by the user. 32 | 33 | Returns: 34 | Any: The result of the step (if any). 35 | """ 36 | 37 | def _init_thread(self): 38 | self._run_task_fn = lambda: None 39 | super()._init_thread() 40 | 41 | def _stop(self): 42 | """Stops the service.""" 43 | if not self.deployed: 44 | raise RuntimeError(f"Service {self.id} is not deployed on any node.") 45 | if self.running: 46 | self._running = False 47 | self._loop.call_soon_threadsafe(self._loop.stop) 48 | self._thread.join() 49 | 50 | @property 51 | def mpi(self): 52 | """Raises an error since the service is not an MPI service. 53 | 54 | Raises: 55 | RuntimeError: The service is not an MPI service. 56 | """ 57 | raise RuntimeError( 58 | f"Service {self.id} implements {self._comm_interface}, not mpi." 59 | ) 60 | -------------------------------------------------------------------------------- /examples/echo/application.py: -------------------------------------------------------------------------------- 1 | from echo import EchoService 2 | 3 | from eclypse.graph import Application 4 | 5 | # Creating an instance of the EchoApp class 6 | echo_app = Application("EchoApp", include_default_assets=True) 7 | 8 | echo_app.add_service( 9 | EchoService("Gateway"), 10 | cpu=1, 11 | gpu=0, 12 | ram=0.5, 13 | storage=0.5, 14 | availability=0.9, 15 | processing_time=0.1, 16 | ) 17 | 18 | echo_app.add_service( 19 | EchoService("SecurityService"), 20 | cpu=2, 21 | gpu=0, 22 | ram=4.0, 23 | storage=2.0, 24 | availability=0.8, 25 | processing_time=2.0, 26 | ) 27 | 28 | echo_app.add_service( 29 | EchoService("LightingService"), 30 | cpu=1, 31 | gpu=0, 32 | ram=2.0, 33 | storage=5.0, 34 | availability=0.8, 35 | processing_time=1.0, 36 | ) 37 | 38 | echo_app.add_service( 39 | EchoService("ClimateControlService"), 40 | cpu=2, 41 | gpu=0, 42 | ram=3.0, 43 | storage=8.0, 44 | availability=0.85, 45 | processing_time=1.5, 46 | ) 47 | 48 | 49 | echo_app.add_service( 50 | EchoService("EntertainmentService"), 51 | cpu=3, 52 | gpu=1, 53 | ram=4.0, 54 | storage=10.0, 55 | availability=0.9, 56 | processing_time=5.0, 57 | ) 58 | 59 | echo_app.add_edge( 60 | "Gateway", 61 | "LightingService", 62 | latency=100.0, 63 | bandwidth=20.0, 64 | symmetric=True, 65 | ) 66 | 67 | echo_app.add_edge( 68 | "Gateway", 69 | "ClimateControlService", 70 | latency=100.0, 71 | bandwidth=10.0, 72 | symmetric=True, 73 | ) 74 | 75 | echo_app.add_edge( 76 | "Gateway", 77 | "SecurityService", 78 | latency=50.0, 79 | bandwidth=5.0, 80 | symmetric=True, 81 | ) 82 | 83 | echo_app.add_edge( 84 | "SecurityService", 85 | "EntertainmentService", 86 | latency=50.0, 87 | bandwidth=10.0, 88 | symmetric=True, 89 | ) 90 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/cart.py: -------------------------------------------------------------------------------- 1 | """The `CartService` handles the shopping cart functionality. 2 | 3 | - Key Responsibilities: 4 | - Manages the user's shopping cart by adding, removing, or updating items. 5 | - Stores cart data temporarily for guest users or long-term for registered users. 6 | """ 7 | 8 | from eclypse.remote.communication import mpi 9 | from eclypse.remote.service import Service 10 | 11 | 12 | class CartService(Service): 13 | """MPI workflow of the Cart service.""" 14 | 15 | async def step(self): 16 | """Example workflow of the Cart service. 17 | 18 | It starts with fetching the user's cart data. 19 | """ 20 | await self.frontend_request() # pylint: disable=no-value-for-parameter 21 | 22 | @mpi.exchange(receive=True, send=True) 23 | def frontend_request(self, sender_id, body): 24 | """Process the frontend request and send the response to the `FrontendService`. 25 | 26 | Args: 27 | sender_id (str): The ID of the sender. 28 | body (dict): The request body. 29 | 30 | Returns: 31 | str: The ID of the recipient. 32 | dict: The response body. 33 | """ 34 | self.logger.info(f"{self.id} - {body}") 35 | 36 | # Send response to FrontendService 37 | if body.get("request_type") == "cart_data": 38 | frontend_response = { 39 | "response_type": "cart_response", 40 | "items": [ 41 | {"product_id": "1", "quantity": 2}, 42 | {"product_id": "2", "quantity": 1}, 43 | ], 44 | } 45 | else: 46 | frontend_response = { 47 | "response_type": "cart_response", 48 | "status": "Invalid request", 49 | } 50 | 51 | return sender_id, frontend_response 52 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/payment.py: -------------------------------------------------------------------------------- 1 | """The `PaymentService` class. 2 | 3 | It is responsible for handling all payment-related transactions in the SockShop. 4 | 5 | - Key Responsibilities: 6 | - Processes payment details and initiates transactions for placed orders. 7 | - Communicates with external payment providers and returns transaction statuses \ 8 | (e.g., success, failure). 9 | """ 10 | 11 | import os 12 | import random as rnd 13 | 14 | from eclypse.remote.communication import rest 15 | from eclypse.remote.service import RESTService 16 | from eclypse.utils.constants import RND_SEED 17 | 18 | 19 | class PaymentService(RESTService): 20 | """REST service for payment processing.""" 21 | 22 | def __init__(self, service_id: str): 23 | """Initialize the PaymentService with a random number generator. 24 | 25 | Args: 26 | service_id (str): The ID of the service. 27 | """ 28 | super().__init__(service_id) 29 | self.rnd = rnd.Random(os.getenv(RND_SEED)) 30 | 31 | @rest.endpoint("/pay", "POST") 32 | def execute_payment(self, order_id: int, amount: float, **_): 33 | """Process the payment for the order. 34 | 35 | Args: 36 | order_id (int): The order ID. 37 | amount (float): The total amount to be paid. 38 | 39 | Returns: 40 | int: The HTTP status code. 41 | dict: The response body. 42 | 43 | Example: 44 | (200, { 45 | "order_id": 12345, 46 | "transaction_id": 54321, 47 | "status": "success", 48 | }) 49 | """ 50 | return 200, { 51 | "order_id": order_id, 52 | "amount": amount + self.rnd.randint(1, 10), 53 | "transaction_id": self.rnd.randint(1000, 9999), 54 | "status": "success", 55 | } 56 | -------------------------------------------------------------------------------- /eclypse/remote/bootstrap/options_factory.py: -------------------------------------------------------------------------------- 1 | """Module for RayOptionsFactory class. 2 | 3 | It incapsulates several option for Ray remote nodes. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import ( 9 | TYPE_CHECKING, 10 | Any, 11 | Dict, 12 | Optional, 13 | ) 14 | 15 | if TYPE_CHECKING: 16 | from eclypse.graph import Infrastructure 17 | 18 | 19 | class RayOptionsFactory: 20 | """Factory for creating Ray options for remote nodes.""" 21 | 22 | def __init__(self, detached: bool = False, **ray_options): 23 | """Create a new RayOptionsFactory. 24 | 25 | Args: 26 | detached (bool, optional): Whether to run the actor detached. Defaults to False. 27 | **ray_options: The options for Ray. See the documentation \ 28 | `here `_ \ 29 | for more information. 30 | """ # noqa E501 31 | self.detached = detached 32 | self.ray_options = ray_options 33 | self._infrastructure: Optional[Infrastructure] = None 34 | 35 | def _attach_infrastructure(self, infrastructure: Infrastructure): 36 | """Attach an infrastructure to the factory. 37 | 38 | Args: 39 | infrastructure (Infrastructure): The infrastructure to attach. 40 | """ 41 | self._infrastructure = infrastructure 42 | 43 | def __call__(self, name: str) -> Dict[str, Any]: 44 | """Create the options for the actor. 45 | 46 | Args: 47 | name (str): The name of the actor. 48 | 49 | Returns: 50 | Dict[str, Any]: The options for the actor. 51 | """ 52 | to_return: Dict[str, Any] = {"name": name} 53 | if self.detached: 54 | to_return["detached"] = True 55 | to_return.update(self.ray_options) 56 | return to_return 57 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/user.py: -------------------------------------------------------------------------------- 1 | """The `UserService` class. 2 | 3 | It manages all user-related functionality, including registration,\ 4 | authentication, and profile management. 5 | 6 | - Key Responsibilities: 7 | - Handles user sign-up, login, and logout processes. 8 | - Manages user data, including credentials, personal information, and addresses. 9 | """ 10 | 11 | from eclypse.remote.communication import mpi 12 | from eclypse.remote.service import Service 13 | 14 | 15 | class UserService(Service): 16 | """MPI workflow of the User service.""" 17 | 18 | async def step(self): 19 | """Example workflow of the `UserService` class. 20 | 21 | It starts with fetching the user's profile information. 22 | """ 23 | await self.frontend_request() # pylint: disable=no-value-for-parameter 24 | 25 | @mpi.exchange(receive=True, send=True) 26 | def frontend_request(self, sender_id, body): 27 | """Process the frontend request and send the response to the `FrontendService`. 28 | 29 | Args: 30 | sender_id (str): The ID of the sender. 31 | body (dict): The request body. 32 | 33 | Returns: 34 | str: The ID of the recipient. 35 | dict: The response body. 36 | """ 37 | self.logger.info(f"{self.id} - {body}") 38 | 39 | # Send response to FrontendService 40 | if body.get("request_type") == "user_data": 41 | frontend_response = { 42 | "response_type": "user_response", 43 | "name": "John Doe", 44 | "email": "john@example.com", 45 | "address": "123 Main St", 46 | "phone": "555-1234", 47 | } 48 | else: 49 | frontend_response = { 50 | "response_type": "user_response", 51 | "status": "Invalid request", 52 | } 53 | 54 | return sender_id, frontend_response 55 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/payment.py: -------------------------------------------------------------------------------- 1 | """The `PaymentService` class. 2 | 3 | It is responsible for handling all payment-related transactions in the SockShop. 4 | 5 | - Key Responsibilities: 6 | - Processes payment details and initiates transactions for placed orders. 7 | - Communicates with external payment providers and returns transaction statuses \ 8 | (e.g., success, failure). 9 | """ 10 | 11 | import random as rnd 12 | 13 | from eclypse.remote.communication import mpi 14 | from eclypse.remote.service import Service 15 | 16 | 17 | class PaymentService(Service): 18 | """MPI workflow of the Payment service.""" 19 | 20 | async def step(self): 21 | """Example workflow of the `PaymentService` class. 22 | 23 | It consists of processing payment requests. 24 | """ 25 | await self.order_request() # pylint: disable=no-value-for-parameter 26 | 27 | @mpi.exchange(receive=True, send=True) 28 | def order_request(self, sender_id, body): 29 | """Process the order request and send the response to the `OrderService`. 30 | 31 | Args: 32 | sender_id (str): The ID of the sender. 33 | body (dict): The request body. 34 | 35 | Returns: 36 | str: The ID of the recipient. 37 | dict: The response body. 38 | """ 39 | self.logger.info(f"{self.id} - {body}") 40 | 41 | # Send response to OrderService 42 | if body.get("request_type") == "payment_request": 43 | payment_response = { 44 | "response_type": "payment_response", 45 | "order_id": body.get("order_id"), 46 | "status": "success" if rnd.choice([True, False]) else "failure", 47 | } 48 | else: 49 | payment_response = { 50 | "response_type": "payment_response", 51 | "status": "Invalid request", 52 | } 53 | 54 | return sender_id, payment_response 55 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Add files or directories matching the regex patterns to the ignore-list. The 4 | # regex matches against paths and can be in Posix or Windows format. 5 | ignore-paths=tests,examples,docs 6 | 7 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 8 | # number of processors available to use. 9 | jobs=0 10 | 11 | # Minimum supported python version 12 | py-version=3.11 13 | 14 | # Specify a score threshold under which the program will exit with error. 15 | fail-under=10.0 16 | 17 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint in 18 | # a server-like mode. 19 | clear-cache-post-run=no 20 | 21 | [MESSAGES CONTROL] 22 | 23 | disable=C0114,C0115,C0116,R0401,W0718,E0401 24 | 25 | [REPORTS] 26 | 27 | # Tells whether to display a full report or only the messages 28 | reports=no 29 | 30 | # Activate the evaluation score. 31 | score=no 32 | 33 | [SIMILARITIES] 34 | 35 | # Minimum lines number of a similarity. 36 | min-similarity-lines=20 37 | 38 | # Ignore comments when computing similarities. 39 | ignore-comments=yes 40 | 41 | # Ignore docstrings when computing similarities. 42 | ignore-docstrings=yes 43 | 44 | # Ignore imports when computing similarities. 45 | ignore-imports=yes 46 | 47 | [FORMAT] 48 | 49 | # Maximum number of characters on a single line. 50 | max-line-length=120 51 | 52 | # Regexp for a line that is allowed to be longer than the limit. 53 | ignore-long-lines="^\s*(# )??$" 54 | 55 | [DESIGN] 56 | 57 | # Maximum number of arguments for function / method (see R0913). 58 | max-args=20 59 | 60 | # Maximum number of locals for function / method body (see R0914). 61 | max-locals=30 62 | 63 | # Maximum number of attributes for a class (see R0902). 64 | max-attributes=15 65 | 66 | # Maximum number of positional arguments for function / method (see R0917). 67 | max-positional-arguments=20 68 | 69 | # Minimum number of public methods for a class (see R0903). 70 | min-public-methods=1 71 | 72 | # R0912 73 | max-branches=15 74 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/catalog.py: -------------------------------------------------------------------------------- 1 | """The `CatalogService` is responsible for managing and serving product information. 2 | 3 | - Key Responsibilities: 4 | - 5 | - Supports operations such as searching for products and listing available items \ 6 | in the store. 7 | - Interfaces with the underlying data store to fetch product data. 8 | """ 9 | 10 | from eclypse.remote.communication import mpi 11 | from eclypse.remote.service import Service 12 | 13 | 14 | class CatalogService(Service): 15 | """MPI workflows for the Catalog service.""" 16 | 17 | async def step(self): 18 | """Example workflow of the `Catalog` service. 19 | 20 | It starts with receiving a request from the `FrontendService` 21 | and sending a response containing product information. 22 | """ 23 | await self.frontend_request() # pylint: disable=no-value-for-parameter 24 | 25 | @mpi.exchange(receive=True, send=True) 26 | def frontend_request(self, sender_id, body): 27 | """Process requests from the FrontendService and send responses back. 28 | 29 | Args: 30 | sender_id (str): The ID of the sender. 31 | body (dict): The request body. 32 | 33 | Returns: 34 | str: The ID of the recipient. 35 | dict: The response body. 36 | """ 37 | self.logger.info(f"{self.id} - {body}") 38 | 39 | # Send response to FrontendService 40 | if body.get("request_type") == "catalog_data": 41 | frontend_response = { 42 | "response_type": "catalog_response", 43 | "products": [ 44 | {"id": "1", "name": "Product 1", "price": 19.99}, 45 | {"id": "2", "name": "Product 2", "price": 29.99}, 46 | ], 47 | } 48 | else: 49 | frontend_response = { 50 | "response_type": "catalog_response", 51 | "status": "Invalid request", 52 | } 53 | 54 | return sender_id, frontend_response 55 | -------------------------------------------------------------------------------- /examples/image_prediction/services/predictor.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import numpy as np 4 | import torch 5 | 6 | from eclypse.remote.communication import rest 7 | from eclypse.remote.service import RESTService 8 | 9 | from .model import MNISTModel 10 | from .utils import ( 11 | EPOCHS, 12 | STEP_EVERY_MS, 13 | STEPS, 14 | ) 15 | 16 | 17 | class PredictorService(RESTService): 18 | def __init__(self, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | self.model = None 21 | self.img_counter = 0 22 | self.last_model_time = None 23 | self.new_model = False 24 | self.new_model_signal = True 25 | 26 | @rest.endpoint("/predict", method="GET") 27 | async def predict(self, image: np.ndarray): 28 | last_time, req_time = self.last_model_time, time.time() 29 | if self.last_model_time is None or (req_time - self.last_model_time) > ( 30 | STEPS / EPOCHS 31 | ) * (STEP_EVERY_MS / 1000): 32 | self.last_model_time = req_time 33 | self.new_model = await self.poll_model() 34 | if not self.new_model: 35 | self.last_model_time = last_time 36 | else: 37 | self.new_model_signal = True 38 | 39 | self.model.eval() 40 | image = np.expand_dims(image, axis=0) 41 | image = torch.tensor(image) 42 | with torch.no_grad(): 43 | prediction = torch.argmax(self.model(image), dim=-1) 44 | 45 | self.img_counter += 1 46 | return 200, {"predictions": prediction} 47 | 48 | async def poll_model(self): 49 | req = await self.rest.get("TrainerService/get_model") 50 | if req.status_code == 200: 51 | self.model.load_state_dict(req.body["model"]) 52 | self.logger.warning("Model updated.") 53 | return True 54 | return False 55 | 56 | def on_deploy(self): 57 | self.logger.info(f"{self.id} deployed. Loading model...") 58 | self.model = MNISTModel() 59 | self.logger.info(f"Model loaded successfully.") 60 | 61 | def on_undeploy(self): 62 | self.model = None 63 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/shipping.py: -------------------------------------------------------------------------------- 1 | """The `ShippingService` class. 2 | 3 | It manages the logistics and shipment of orders, ensuring items reach customers. 4 | 5 | - Key Responsibilities: 6 | - Handles the shipping of completed orders. 7 | - Calculates shipping costs, delivery times, and tracks shipment status. 8 | - Coordinates with third-party shipping providers for physical delivery. 9 | """ 10 | 11 | from eclypse.remote.communication import mpi 12 | from eclypse.remote.service import Service 13 | 14 | 15 | class ShippingService(Service): 16 | """MPI workflow of the Shipping service.""" 17 | 18 | async def step(self): 19 | """Example workflow of the `ShippingService` class. 20 | 21 | It consists of processing shipping requests. 22 | """ 23 | await self.order_request() # pylint: disable=no-value-for-parameter 24 | 25 | @mpi.exchange(receive=True, send=True) 26 | def order_request(self, sender_id, body): 27 | """Process the order request and send the response to the `OrderService`. 28 | 29 | Args: 30 | sender_id (str): The ID of the sender. 31 | body (dict): The request body. 32 | 33 | Returns: 34 | str: The ID of the recipient. 35 | dict: The response body. 36 | """ 37 | self.logger.info(f"{self.id} - {body}") 38 | 39 | # Send response to OrderService 40 | if body.get("request_type") == "shipping_request": 41 | shipping_response = { 42 | "response_type": "shipping_response", 43 | "order_id": body.get("order_id"), 44 | "status": "success", 45 | "shipping_details": { 46 | "carrier": "UPS", 47 | "tracking_number": "1234567890", 48 | "estimated_delivery_date": "2023-05-01", 49 | }, 50 | } 51 | else: 52 | shipping_response = { 53 | "response_type": "shipping_response", 54 | "status": "Invalid request", 55 | } 56 | 57 | return sender_id, shipping_response 58 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/frontend.py: -------------------------------------------------------------------------------- 1 | """The `FrontendService` class. 2 | 3 | It serves as the user interface for the SockShop application, 4 | providing the user-facing components of the store. 5 | 6 | - Key Responsibilities: 7 | - Displays product catalogs, shopping carts, and order information to users. 8 | - Interacts with backend services \ 9 | (e.g., `CatalogService`, `UserService`, `OrderService`) to display real-time data. 10 | - Manages user input and interactions such as product searches, \ 11 | cart updates, and order placements. 12 | """ 13 | 14 | from eclypse.remote.service import Service 15 | 16 | 17 | class FrontendService(Service): 18 | """Example workflow of the Frontend service.""" 19 | 20 | def __init__(self, name): 21 | """Initialize the Frontend service, setting the communication interface to REST.""" 22 | super().__init__(name, comm_interface="rest") 23 | self.user_id = 12345 24 | 25 | async def step(self): 26 | """Example workflow of the `Frontend` service. 27 | 28 | It starts with fetching the catalog, user data, and cart items, then placing an order. 29 | """ 30 | catalog_r = await self.rest.get("CatalogService/catalog") 31 | user_r = await self.rest.get("UserService/user", user_id=self.user_id) 32 | cart_r = await self.rest.get("CartService/cart") 33 | 34 | products = catalog_r.data.get("products", []) 35 | items = cart_r.data.get("items", []) 36 | user_data = user_r.body 37 | self.logger.info(f"{self.id} - {user_data}") 38 | 39 | order_items = [ 40 | { 41 | "id": item["id"], 42 | "amount": next( 43 | ( 44 | product["price"] * item["quantity"] 45 | for product in products 46 | if product["id"] == item["id"] 47 | ), 48 | None, 49 | ), 50 | } 51 | for item in items 52 | ] 53 | 54 | order_r = await self.rest.post("OrderService/order", items=order_items) 55 | self.logger.info(f"{order_r.body}") 56 | -------------------------------------------------------------------------------- /eclypse/report/reporters/gml.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-argument 2 | """Module for GMLReporter class. 3 | 4 | It is used to report the simulation metrics in GML format. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from typing import ( 10 | TYPE_CHECKING, 11 | List, 12 | ) 13 | 14 | import networkx as nx 15 | 16 | from eclypse.report.reporter import Reporter 17 | 18 | if TYPE_CHECKING: 19 | from eclypse.workflow.event import EclypseEvent 20 | 21 | 22 | class GMLReporter(Reporter): 23 | """Class to report simulation metrics in GML format using NetworkX.""" 24 | 25 | def __init__(self, *args, **kwargs): 26 | """Initialize the GML reporter.""" 27 | super().__init__(*args, **kwargs) 28 | self.report_path = self.report_path / "gml" 29 | 30 | def report( 31 | self, 32 | _: str, 33 | __: int, 34 | callback: EclypseEvent, 35 | ) -> List[tuple[str, nx.DiGraph]]: 36 | """Extract graph data from callback and prepare it for writing. 37 | 38 | Args: 39 | _ (str): The name of the event. 40 | __ (int): The index of the event trigger (step). 41 | callback (EclypseEvent): The executed callback containing the data to report. 42 | 43 | Returns: 44 | List of (graph_name, graph_object) tuples. 45 | """ 46 | entries = [] 47 | for d in self.dfs_data(callback.data): 48 | if not d or d[-1] is None: 49 | continue 50 | graph = d[-1] 51 | if not isinstance(graph, nx.DiGraph): 52 | continue 53 | name = f"{callback.name}{'-' + graph.id if hasattr(graph, 'id') else ''}" 54 | entries.append((name, graph)) 55 | return entries 56 | 57 | async def write(self, _: str, data: List[tuple[str, nx.DiGraph]]): 58 | """Write graphs in GML format. 59 | 60 | Args: 61 | callback_type (str): The type of the callback. 62 | data (List[Tuple[str, nx.DiGraph]]): The graphs to write. 63 | """ 64 | for name, graph in data: 65 | path = self.report_path / f"{name}.gml" 66 | nx.write_gml(graph, path, stringizer=str) 67 | -------------------------------------------------------------------------------- /examples/user_distribution/update_policy.py: -------------------------------------------------------------------------------- 1 | import random as rnd 2 | from pathlib import Path 3 | 4 | import pandas as pd 5 | from networkx.classes.reportviews import ( 6 | EdgeView, 7 | NodeView, 8 | ) 9 | 10 | 11 | def kill_policy(kill_probability: float): 12 | revive_probability = kill_probability / 2 13 | 14 | def node_update_wrapper(nodes: NodeView): 15 | for _, resources in nodes.data(): 16 | if rnd.random() < kill_probability: 17 | resources["availability"] = 0 18 | elif rnd.random() < revive_probability: 19 | resources["availability"] = 0.99 20 | 21 | return node_update_wrapper 22 | 23 | 24 | class EdgeUpdatePolicy: 25 | def __init__(self, kill_probability: float): 26 | self.initial_latencies = None 27 | self.kill_probability = kill_probability 28 | self.revive_probability = kill_probability / 2 29 | 30 | def __call__(self, edges: EdgeView): 31 | if self.initial_latencies is None: 32 | self.initial_latencies = { 33 | (u, v): data["latency"] for u, v, data in edges.data() 34 | } 35 | 36 | for u, v, data in edges.data(): 37 | if rnd.random() < self.kill_probability: 38 | data["latency"] += rnd.randint(1, 5) 39 | elif rnd.random() < self.revive_probability: 40 | data["latency"] = self.initial_latencies[(u, v)] 41 | 42 | 43 | class UserDistributionPolicy: 44 | def __init__(self): 45 | self.df = pd.read_parquet(Path(__file__).parent / "dataset.parquet") 46 | self.df = self.df.astype({"node_id": int, "time": int, "user_count": int}) 47 | 48 | self.step = self.df["time"].min() 49 | self.factor = 1 50 | 51 | def __call__(self, nodes: NodeView): 52 | 53 | if self.step == 1000 or self.step == 3000: 54 | self.factor += 2 55 | elif self.step == 2000 or self.step == 4000: 56 | self.factor -= 2 57 | 58 | current_data = self.df[self.df["time"] == self.step] 59 | for _, row in current_data.iterrows(): 60 | user_count = int(row["user_count"]) * self.factor 61 | nodes[row["node_id"]]["user_count"] = user_count 62 | 63 | self.step += 1 64 | -------------------------------------------------------------------------------- /docs/_static/css/landing.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["../scss/landing.scss"],"names":[],"mappings":"AAyCA;EACI;EACA;EACA;EACA;EACA,kBA3CS;;;AA8Cb;AAAA;EAEI;EACA;EACA;EACA;EACA,OAjDE;EAkDF,QAlDE;EAmDF;EACA;;;AAGJ;EACI;EACA,WA7BO;;;AAgCX;EACI,OA5DO;EA6DP,QA7DO;EA8DP;EACA;EACA;EACA;EACA,WArCM;;;AAwCV;EACI;EACA;EACA;EACA;EACA;EACA,OAzES;EA0ET,QAzEU;EA0EV;EACA;EACA;EACA;;;AAGJ;AAAA;AAAA;EAGI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAnFW;EAoFX,QAnFY;EAoFZ;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EAEI;EACA;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA,oBA5IiB;EA6IjB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OA7JW;EA8JX;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI,iBAtKS;;;AAyKb;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAIJ;EAOI;IACI,OAHW;IAIX;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;AAAA;IAEI,OAjPC;IAkPD,QAlPC;IAmPD;IACA;IACA;IACA;;EAGJ;IACI,OAzPM;IA0PN,QA1PM;IA2PN;IACA;;EAGJ;IACI,OA/PQ;IAgQR,QA/PS;IAgQT;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA,OA5DW;IA6DX;IACA;;;AAIR;EAOI;IACI,OAHW;IAIX;IACA;IACA;;EAGJ;IACI;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;;EAGJ;IACI,OAnUC;IAoUD,QApUC;IAqUD;IACA;IACA;IACA;;EAGJ;IACI,OA3UM;IA4UN;IACA;IACA;;EAGJ;IACI,OAjVQ;IAkVR,QAjVS;IAkVT;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;IACA,OAxEW;;;AA4EnB;EAOI;AAAA;IAEI,OAhXC;IAiXD,QAjXC;IAkXD;IACA;;EAGJ;IACI,OAtXM;IAuXN,QAvXM;IAwXN;IACA;;EAGJ;IACI,OA5XQ;IA6XR,QA5XS;IA6XT;IACA;;EAGJ;IACI,OApYM;IAqYN;IACA;IACA;;EAGJ;IACI;IACA;IACA,OA7YM;;;AAiZd;EACI;EACA;;;AAGJ;EACI;EACA;EAEA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EAEA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;IACA;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;;;AAIR;EACI;IACI;IACA;;EAGJ;IACI;;EAGJ;IACI;IACA;;;AAIR;EACI;IACI;IACA;;EAGJ;IACI;;EAGJ;IACI;IACA;;;AAIR;EACI;IACI,kBA/fC;IAggBD;;EAGJ;IACI,kBAlgBK;IAmgBL;;;AAIR;EACI;IACI;;EAGJ;IACI;;EAGJ;IACI;;EAGJ;IACI;;;AAIR;EAEI;IAEI;IACA;;EAGJ;IAEI;IACA;;EAGJ;IAEI;IACA;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI;;;AAIR;EACI;IACI;;EAGJ;IACI","file":"landing.css"} 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 6 3 | :hidden: 4 | 5 | Overview 6 | Reference 7 | 8 | 9 | 10 | .. image:: _static/images/light.png 11 | :align: center 12 | :width: 20% 13 | :class: only-light 14 | 15 | .. image:: _static/images/dark.png 16 | :align: center 17 | :width: 20% 18 | :class: only-dark 19 | 20 | ===================== 21 | ECLYPSE documentation 22 | ===================== 23 | **ECLYPSE** (Edge-Cloud raY-based Platform for Simulated Environments) stands as a groundbreaking simulation library, crafted entirely in Python. 24 | It offers a practical interface for experimenting with deployment strategies across different infrastructure settings. 25 | 26 | One of its key strengths lies in its ability to simulate the deployment of service-based applications in environments that closely mimic real-world conditions, with or without actual application implementation. 27 | This flexibility allows users to explore various deployment strategies comprehensively, testing placement and deployment scenarios with precision. 28 | 29 | ECLYPSE empowers developers and researchers to gain valuable insights into the nuances of deployment in diverse infrastructure scenarios, fostering informed decision-making and driving advancements in Cloud and Edge computing. 30 | 31 | Key features include: 32 | 33 | - **Entirely written in Python:** Accessible and adaptable for a wide range of users. 34 | - **Easy to use:** Intuitive interface for seamless experimentation and analysis. 35 | - **Actual implementation of services:** Simulate real-world scenarios with precise deployment strategies. 36 | - **User-defined placement strategies and application/infrastructure update policies:** Tailor simulations to specific research or development needs, allowing for comprehensive testing and analysis. 37 | - **Reporting of key metrics:** Provides insights into application, infrastructure, and simulation performance through various formats, aiding in comprehensive analysis and decision-making. 38 | - **Logging capabilities:** Allows for detailed tracking and analysis of simulation activities, facilitating troubleshooting and optimization efforts. 39 | 40 | .. button-ref:: source/overview/index 41 | :ref-type: myst 42 | :outline: 43 | :color: secondary 44 | :expand: 45 | :align: center 46 | :shadow: 47 | 48 | :octicon:`play;1em;info` Start using ECLYPSE 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.8.0 (2025-11-03) 2 | 3 | ### BREAKING CHANGE 4 | 5 | - Import paths and dependencies have changed. 6 | 7 | ### Feat 8 | 9 | - **core**: Integrate eclypse_core into eclypse and remove the external core dependency 10 | 11 | ### Fix 12 | 13 | - Reintroduce shield_interrupt decorator to catch CTRL-C 14 | - Move nx import to make random generator working correctly 15 | 16 | ### Refactor 17 | 18 | - Merge core files into public code 19 | - Remove placement view residual usage 20 | 21 | ## 0.7.4 (2025-06-27) 22 | 23 | ## 0.7.3 (2025-06-26) 24 | 25 | ## 0.7.2 (2025-06-25) 26 | 27 | ### Fix 28 | 29 | - Add activates_on to event decorator 30 | 31 | ## 0.7.1 (2025-06-10) 32 | 33 | ## 0.7.0 (2025-06-10) 34 | 35 | ### Feat 36 | 37 | - Add "strict" flag to infrastructure builders 38 | - Add fat_tree generator 39 | - Add Orion CEV infrastructure builder 40 | - Remove group as default asset 41 | 42 | ### Fix 43 | 44 | - Apply dfs_data generalisation to csv reporter 45 | - JSON reporter now keeps callback.data structures untouched 46 | - Remove remote node metric 47 | - Correct default value for bandwidth asset 48 | - Prune edge assets in sock_shop builder 49 | - Add default values to off-the-shelf assets 50 | - Correct merge of user-defined and default path assets aggregators 51 | - Rename "dispatch" into "step" method, in examples 52 | 53 | ### Refactor 54 | 55 | - Change examples according to new interface 56 | - Use new DRIVING_EVENT constant 57 | - Copy core utils module structure 58 | - Remove EclypseCallback and change EclypseEvent management 59 | - Redefine imports in examples after package structure changes 60 | - Copy same core import structure for communication package 61 | - Rewrite examples removing node group 62 | - Rename report and step parameters to avoid pylint warning 63 | - Merge echo example notebooks into a single one 64 | 65 | ## 0.6.16 (2024-11-25) 66 | 67 | ### Feat 68 | 69 | - Include default metric for service step result 70 | 71 | ### Fix 72 | 73 | - Add missing return in Report class 74 | 75 | ### Refactor 76 | 77 | - Change default gml metrics name 78 | - Add Report class, remove html report 79 | 80 | ## 0.6.12 (2024-11-21) 81 | 82 | ## 0.6.10 (2024-11-16) 83 | 84 | ### Fix 85 | 86 | - Add missing await in SockShop mpi CatalogService 87 | - Move report class to eclypse-core 88 | 89 | ### Refactor 90 | 91 | - Adjust imports from core 92 | - Add wrapper for SimulationConfig core class 93 | -------------------------------------------------------------------------------- /eclypse/utils/constants.py: -------------------------------------------------------------------------------- 1 | """Module containing constants used throughout the ECLYPSE package. 2 | 3 | Attributes: 4 | MIN_FLOAT (float): Minimum float value. Default is ``0.0`` 5 | MAX_FLOAT (float): Maximum float value. Default is ``1e9`` 6 | FLOAT_EPSILON (float): Smallest positive float (machine epsilon). 7 | Default is ``sys.float_info.min`` 8 | 9 | MIN_BANDWIDTH (float): Minimum bandwidth value. Default is ``0.0`` 10 | MAX_BANDWIDTH (float): Maximum bandwidth value. Default is ``1e9`` 11 | 12 | MIN_LATENCY (float): Minimum latency value. Default is ``0.0`` 13 | MAX_LATENCY (float): Maximum latency value. Default is ``1e9`` 14 | 15 | MIN_AVAILABILITY (float): Minimum availability value. Default is ``0.0`` 16 | MAX_AVAILABILITY (float): Maximum availability value. Default is ``1.0`` 17 | 18 | DEFAULT_SIM_PATH (Path): Default path to the simulation folder. 19 | Default is ``~/eclypse-sim`` 20 | 21 | DEFAULT_REPORT_TYPE (str): Default report type used in simulations. 22 | Default is ``csv`` 23 | 24 | RND_SEED (str): Environment variable key to configure the random seed. 25 | Value is ``ECLYPSE_RND_SEED`` 26 | LOG_LEVEL (str): Environment variable key to configure the logging level. 27 | Value is ``ECLYPSE_LOG_LEVEL`` 28 | LOG_FILE (str): Environment variable key to configure the log file path. 29 | Value is ``ECLYPSE_LOG_FILE`` 30 | """ 31 | 32 | import sys 33 | from pathlib import Path 34 | 35 | MIN_FLOAT, MAX_FLOAT = 0.0, 1e9 36 | FLOAT_EPSILON = sys.float_info.min 37 | 38 | RND_SEED = "ECLYPSE_RND_SEED" 39 | LOG_LEVEL = "ECLYPSE_LOG_LEVEL" 40 | LOG_FILE = "ECLYPSE_LOG_FILE" 41 | 42 | ### Application requirements / Infrastructure resources constants 43 | 44 | MIN_BANDWIDTH, MAX_BANDWIDTH = MIN_FLOAT, MAX_FLOAT 45 | MIN_LATENCY, MAX_LATENCY = MIN_FLOAT, MAX_FLOAT 46 | MIN_AVAILABILITY, MAX_AVAILABILITY = 0.0, 1.0 47 | COST_RECOMPUTATION_THRESHOLD = 0.05 # 5% threshold to consider recomputation cost 48 | 49 | ### Simulation metrics 50 | 51 | DEFAULT_SIM_PATH = Path.home() / "eclypse-sim" 52 | DEFAULT_REPORT_TYPE = "csv" 53 | DRIVING_EVENT = "enact" 54 | 55 | __all__ = [ 56 | "DEFAULT_REPORT_TYPE", 57 | "DEFAULT_SIM_PATH", 58 | "FLOAT_EPSILON", 59 | "MAX_AVAILABILITY", 60 | "MAX_BANDWIDTH", 61 | "MAX_FLOAT", 62 | "MAX_LATENCY", 63 | "MIN_AVAILABILITY", 64 | "MIN_BANDWIDTH", 65 | "MIN_FLOAT", 66 | "MIN_LATENCY", 67 | ] 68 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/static.py: -------------------------------------------------------------------------------- 1 | """Module for the Static placement strategy. 2 | 3 | It overrides the `place` method of the 4 | PlacementStrategy class to place services of an application on infrastructure nodes 5 | based on a predefined mapping of services to nodes in the form of a dictionary. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from typing import ( 11 | TYPE_CHECKING, 12 | Any, 13 | Dict, 14 | ) 15 | 16 | from .strategy import PlacementStrategy 17 | 18 | if TYPE_CHECKING: 19 | from eclypse.graph import ( 20 | Application, 21 | Infrastructure, 22 | ) 23 | from eclypse.placement import ( 24 | Placement, 25 | PlacementView, 26 | ) 27 | 28 | 29 | class StaticStrategy(PlacementStrategy): 30 | """StaticStrategy class. 31 | 32 | Static placement strategy based on a predefined mapping of services 33 | to nodes in the form of a dictionary. 34 | """ 35 | 36 | def __init__(self, mapping: Dict[str, str]): 37 | """Initializes the StaticPlacementStrategy object. 38 | 39 | Args: 40 | mapping (Optional[Dict[str, str]]): A dictionary mapping service IDs to node IDs. 41 | """ 42 | if not mapping: 43 | raise ValueError("Please provide a valid mapping of services to nodes.") 44 | 45 | self.mapping = mapping 46 | super().__init__() 47 | 48 | def place( 49 | self, 50 | infrastructure: Infrastructure, 51 | application: Application, 52 | _: Dict[str, Placement], 53 | __: PlacementView, 54 | ) -> Dict[Any, Any]: 55 | """Returns the static mapping of services to nodes, given at initialization. 56 | 57 | Returns: 58 | Dict[str, str]: the static mapping. 59 | """ 60 | if not self.is_feasible(infrastructure, application): 61 | return {} 62 | return self.mapping 63 | 64 | def is_feasible(self, infrastructure: Infrastructure, _: Application) -> bool: 65 | """Check if the application can be placed on the infrastructure. 66 | 67 | It checks if all the nodes in the mapping are available in the infrastructure. 68 | """ 69 | for node in self.mapping.values(): 70 | if node not in infrastructure.nodes: 71 | infrastructure.logger.error( 72 | f"Node {node} not found or not available in the infrastructure." 73 | ) 74 | return False 75 | return True 76 | -------------------------------------------------------------------------------- /docs/source/overview/getting-started/update-policy.rst: -------------------------------------------------------------------------------- 1 | Update Policy 2 | ============= 3 | 4 | In ECLYPSE, an ``UpdatePolicy`` is a function that defines how the state of the infrastructure evolves over time. It enables dynamic simulations by modifying node or edge assets at each simulation step. 5 | 6 | Unlike assets, update policies are not classes. Instead, they are simple functions with a fixed signature, depending on whether they operate on nodes or edges. 7 | 8 | Function Signature 9 | ------------------ 10 | 11 | There are two kinds of update policies: 12 | 13 | - **Node update policies**: 14 | 15 | .. code-block:: python 16 | 17 | def my_node_policy(nodes: NodeView): 18 | ... 19 | 20 | - **Edge update policies**: 21 | 22 | .. code-block:: python 23 | 24 | def my_edge_policy(edges: EdgeView): 25 | ... 26 | 27 | Both `NodeView` and `EdgeView` are provided by the `networkx` library and behave like dictionaries over the graph structure. Each node or edge has an associated data dictionary containing asset instances. 28 | In particular a node is a tuple of the form ``(node_id, node_data)``, where `node_id` is the node identifier and `node_data` is a dictionary containing the asset instances. 29 | On the other hand, an edge is a tuple of the form ``(source_node_id, target_node_id, edge_data)``, where `source_node_id` and `target_node_id` are the identifiers of the source and target nodes, respectively, and `edge_data` is a dictionary containing the asset instances. 30 | 31 | Writing Custom Policies 32 | ----------------------- 33 | 34 | You can define your own update policies by modifying the relevant asset values within each node or edge. 35 | 36 | .. code-block:: python 37 | :caption: **Example:** A node policy that caps CPU to a fixed maximum 38 | 39 | def cap_cpu(nodes: NodeView): 40 | for _, data in nodes.items(): 41 | if "cpu" in data: 42 | data["cpu"].value = min(data["cpu"].value, 2.0) 43 | 44 | .. code-block:: python 45 | :caption: **Example:** An edge policy that increases latency: 46 | 47 | def increase_latency(edges: EdgeView): 48 | for _, _, data in edges: 49 | if "latency" in data: 50 | data["latency"].value += 1.0 51 | 52 | .. important:: 53 | 54 | Update policies must always ensure that modified asset values remain consistent. 55 | Use the asset's :py:meth:`~eclypse.graph.assets.asset.Asset.is_consistent()` method if needed. Otherwise, placement and simulation logic may occur on inconsistent data. 56 | -------------------------------------------------------------------------------- /examples/image_prediction/services/trainer.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import ( 3 | Any, 4 | List, 5 | ) 6 | 7 | import torch 8 | from torch.utils.data import DataLoader 9 | 10 | from eclypse.remote.communication import rest 11 | from eclypse.remote.service import RESTService 12 | 13 | from .model import MNISTModel 14 | from .utils import ( 15 | BATCH_SIZE, 16 | EPOCHS, 17 | LEARNING_RATE, 18 | load_data, 19 | ) 20 | 21 | 22 | class TrainerService(RESTService): 23 | def __init__(self, *args, **kwargs): 24 | super().__init__(*args, **kwargs) 25 | self.train_thread: threading.Thread = None 26 | self.data = None 27 | self.model: torch.nn.Module = None 28 | self.model_queue: List[Any] = None 29 | self.epoch: int = 0 30 | 31 | @rest.endpoint("/get_model", method="GET") 32 | def get_model(self): 33 | if len(self.model_queue) > 0: 34 | return 200, { 35 | "model": self.model_queue.pop(0), 36 | } 37 | return 404, {"error": "No model available."} 38 | 39 | def train(self): 40 | self.logger.info("Training started.") 41 | loader = DataLoader( 42 | self.data, 43 | batch_size=BATCH_SIZE, 44 | shuffle=True, 45 | ) 46 | criterion = torch.nn.CrossEntropyLoss() 47 | optimizer = torch.optim.SGD(self.model.parameters(), lr=LEARNING_RATE) 48 | self.model.train() 49 | self.model.to("cuda") 50 | for self.epoch in range(EPOCHS): 51 | for x, y in loader: 52 | x, y = x.to("cuda"), y.to("cuda") 53 | optimizer.zero_grad() 54 | outputs = self.model(x) 55 | loss = criterion(outputs, y) 56 | loss.backward() 57 | optimizer.step() 58 | self.model_queue.append( 59 | {k: v.detach().cpu() for k, v in self.model.state_dict().items()} 60 | ) 61 | 62 | def on_deploy(self): 63 | self.logger.info(f"{self.id} deployed. Loading model...") 64 | self.model_queue = [] 65 | self.model = MNISTModel() 66 | self.data = load_data() 67 | 68 | self.train_thread = threading.Thread(target=self.train, daemon=True) 69 | self.train_thread.start() 70 | 71 | def on_undeploy(self): 72 | self.model_queue = None 73 | self.train_thread = None 74 | self.data = None 75 | self.model = None 76 | 77 | @property 78 | def is_training(self): 79 | return self.train_thread.is_alive() if self.train_thread else False 80 | -------------------------------------------------------------------------------- /eclypse/graph/assets/symbolic.py: -------------------------------------------------------------------------------- 1 | """Module for a Symbolic Asset class. 2 | 3 | It represents an asset defined as a list of symbolic variables, 4 | such as a location or security taxonomy. The logic of the asset is 5 | defined in terms of a set of constraints: 6 | 7 | - `aggregate`: Aggregate the assets into a single asset via intersection. 8 | - `satisfies`: Check if all the elements in the constraint are present in the asset. 9 | - `is_consistent`: Check if the asset belongs to the interval [lower_bound, upper_bound]. 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from typing import ( 15 | Any, 16 | Set, 17 | ) 18 | 19 | from .asset import Asset 20 | 21 | 22 | class Symbolic(Asset): 23 | """SymbolicAsset represents an asset defined as a list of symbolic variables. 24 | 25 | The logic of the asset is defined in terms of a set of constraints. 26 | """ 27 | 28 | def aggregate(self, *assets: Any) -> Any: 29 | """Aggregate the assets into a single asset via union. 30 | 31 | Args: 32 | assets (Iterable[SymbolicAsset]): The assets to aggregate. 33 | 34 | Returns: 35 | SymbolicAsset: The aggregated asset. 36 | """ 37 | # return list(set().union(*assets)) 38 | 39 | uniques: Set[Any] = set() 40 | for asset in assets: 41 | _asset = [asset] if isinstance(asset, str) else asset 42 | uniques.update(_asset) 43 | return list(uniques) 44 | 45 | def satisfies(self, asset: Any, constraint: Any) -> bool: 46 | """Check if `asset` contains `constraint`. 47 | 48 | In a symbolic asset, `asset` contains `constraint` 49 | if all the variables in `constraint` are present in `asset`. 50 | 51 | Args: 52 | asset (SymbolicAsset): The "container" asset. 53 | constraint (SymbolicAsset): The "contained" asset. 54 | 55 | Returns: 56 | bool: True if asset >= constraint, False otherwise. 57 | """ 58 | return all(x in asset for x in constraint) 59 | 60 | def is_consistent(self, asset: Any) -> bool: 61 | """Consistency check for `asset`. 62 | 63 | Checks if all the lower bound variables are present in the asset and all the 64 | variables in the asset are present in the upper bound. 65 | 66 | Args: 67 | asset (SymbolicAsset): The asset to be checked. 68 | 69 | Returns: True if lower_bound <= asset <= upper_bound, False otherwise. 70 | """ 71 | return all(lower in asset for lower in self.lower_bound) and all( 72 | x in self.upper_bound for x in asset 73 | ) 74 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/round_robin.py: -------------------------------------------------------------------------------- 1 | """Module for the RoundRobin placement strategy. 2 | 3 | A `PlacementStrategy` that attempts to distribute services across nodes, 4 | in a round-robin fashion. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from typing import ( 10 | TYPE_CHECKING, 11 | Any, 12 | Callable, 13 | Dict, 14 | Optional, 15 | ) 16 | 17 | from .strategy import PlacementStrategy 18 | 19 | if TYPE_CHECKING: 20 | from eclypse.graph import ( 21 | Application, 22 | Infrastructure, 23 | ) 24 | from eclypse.placement import ( 25 | Placement, 26 | PlacementView, 27 | ) 28 | 29 | 30 | class RoundRobinStrategy(PlacementStrategy): 31 | """RoundRobin class. 32 | 33 | A `PlacementStrategy` that attempts to distribute services across nodes, in a 34 | round-robin fashion. 35 | """ 36 | 37 | def __init__(self, sort_fn: Optional[Callable[[Any], Any]] = None): 38 | """Initializes the `RoundRobin` placement strategy. 39 | 40 | Args: 41 | sort_fn (Optional[Callable[[Any], Any]], optional): A function to sort the 42 | infrastructure nodes. Defaults to None. 43 | """ 44 | self.sort_fn = sort_fn 45 | super().__init__() 46 | 47 | def place( 48 | self, 49 | _: Infrastructure, 50 | application: Application, 51 | __: Dict[str, Placement], 52 | placement_view: PlacementView, 53 | ) -> Dict[Any, Any]: 54 | """Performs the placement according to a round-robin logic. 55 | 56 | Places the services of an application on the infrastructure nodes, attempting 57 | to distribute them evenly. 58 | 59 | Args: 60 | _ (Infrastructure): The infrastructure to place the application on. 61 | application (Application): The application to place on the infrastructure. 62 | __ (Dict[str, Placement]): The placement of all the applications in the simulations. 63 | placement_view (PlacementView): The snapshot of the current state of the infrastructure. 64 | 65 | Returns: 66 | Dict[str, str]: A mapping of services to infrastructure nodes. 67 | """ 68 | mapping = {} 69 | infrastructure_nodes = list(placement_view.residual.nodes(data=True)) 70 | if self.sort_fn: 71 | infrastructure_nodes.sort(key=self.sort_fn) 72 | 73 | for service in application.nodes: 74 | selected_node, _ = infrastructure_nodes.pop(0) 75 | mapping[service] = selected_node 76 | 77 | infrastructure_nodes.append(selected_node) 78 | 79 | return mapping 80 | -------------------------------------------------------------------------------- /eclypse/utils/types.py: -------------------------------------------------------------------------------- 1 | """Module containing type aliases used throughout the ECLYPSE package. 2 | 3 | Attributes: 4 | PrimitiveType (Union): Type alias for primitive types.\ 5 | Possible values are ``int``, ``float``, ``str``, ``bool``, ``list``,\ 6 | ``tuple``, ``dict``, ``set``. 7 | CascadeTriggerType (Union): Type alias for cascade trigger types.\ 8 | Possible values are: 9 | - ``str``: CascadeTrigger 10 | - ``Tuple[str, int]``: PeriodicCascadeTrigger 11 | - ``Tuple[str, List[int]]``: ScheduledCascadeTrigger 12 | - ``Tuple[str, float]``: RandomCascadeTrigger 13 | ActivatesOnType (Union): Type alias for the activates on types.\ 14 | It can be a single `CascadeTriggerType` or a list of them. 15 | HTTPMethodLiteral (Literal): Literal type for HTTP methods.\ 16 | Possible values are ``"GET"``, ``"POST"``, ``"PUT"``, ``"DELETE"``. 17 | ConnectivityFn (Callable): Type alias for the connectivity function.\ 18 | It takes two lists of strings and returns a generator of tuples of strings. 19 | EventType (Literal): Literal type for the event types.\ 20 | Possible values are ``"application"``, ``"infrastructure"``, ``"service"``,\ 21 | ``"interaction"``, ``"node"``, ``"link"``, ``"simulation"``. 22 | LogLevel (Literal): Literal type for the log levels.\ 23 | Possible values are ``"TRACE"``, ``"DEBUG"``, ``"ECLYPSE"``, ``"INFO"``,\ 24 | ``"SUCCESS"``, ``"WARNING"``, ``"ERROR"``, ``"CRITICAL"``. 25 | """ 26 | 27 | from __future__ import annotations 28 | 29 | from typing import ( 30 | Callable, 31 | Generator, 32 | List, 33 | Literal, 34 | Tuple, 35 | Union, 36 | ) 37 | 38 | PrimitiveType = Union[int, float, str, bool, list, tuple, dict, set] 39 | 40 | CascadeTriggerType = Union[ 41 | str, # CascadeTrigger 42 | Tuple[str, int], # PeriodicCascadeTrigger 43 | Tuple[str, List[int]], # ScheduledCascadeTrigger 44 | Tuple[str, float], # RandomCascadeTrigger 45 | ] 46 | ActivatesOnType = Union[CascadeTriggerType, List[CascadeTriggerType]] 47 | 48 | HTTPMethodLiteral = Literal[ 49 | "GET", 50 | "POST", 51 | "PUT", 52 | "DELETE", 53 | ] 54 | 55 | ConnectivityFn = Callable[ 56 | [List[str], List[str]], Generator[Tuple[str, str], None, None] 57 | ] 58 | 59 | EventType = Literal[ 60 | "application", 61 | "infrastructure", 62 | "service", 63 | "interaction", 64 | "node", 65 | "link", 66 | "simulation", 67 | ] 68 | 69 | LogLevel = Literal[ 70 | "TRACE", 71 | "DEBUG", 72 | "ECLYPSE", 73 | "INFO", 74 | "SUCCESS", 75 | "WARNING", 76 | "ERROR", 77 | "CRITICAL", 78 | ] 79 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/rest_services/order.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-value-for-parameter 2 | """The `OrderService` class. 3 | 4 | It processes user orders, ensuring the coordination between\ 5 | different services like payment, inventory, and shipping. 6 | 7 | - Key Responsibilities: 8 | - Creates, updates, and manages customer orders. 9 | - Interacts with the `PaymentService` and `ShippingService` to complete the order transaction. 10 | - Tracks the status of placed orders (e.g., pending, confirmed, shipped). 11 | """ 12 | 13 | from eclypse.remote.communication import rest 14 | from eclypse.remote.communication.rest import HTTPStatusCode 15 | from eclypse.remote.service import RESTService 16 | 17 | 18 | class OrderService(RESTService): 19 | """REST endpoints for the Order service.""" 20 | 21 | def __init__(self, name): 22 | """Initialize the OrderService with an order ID. 23 | 24 | Args: 25 | name (str): The name of the service. 26 | """ 27 | super().__init__(name) 28 | self.order_id = 54321 29 | 30 | @rest.endpoint("/order", "POST") 31 | async def create_order(self, items, **_): 32 | """Create a new order for the user. 33 | 34 | Args: 35 | items (list): The list of items in the order. 36 | 37 | Returns: 38 | int: The HTTP status code. 39 | dict: The response body. 40 | 41 | Example: 42 | (201, { 43 | "order_id": "54321", 44 | "transaction_id": "12345", 45 | "shipping_details": { 46 | "carrier": "UPS", 47 | "tracking_number": "1234567890", 48 | "estimated_delivery_date": "2024-04-09", 49 | }, 50 | "status": "success", 51 | }) 52 | """ 53 | amount = sum(item["amount"] for item in items) 54 | payment_r = self.rest.post( 55 | "PaymentService/pay", 56 | order_id=self.order_id, 57 | amount=amount, 58 | ) 59 | shipping_r = self.rest.get("ShippingService/details", order_id=self.order_id) 60 | 61 | payment_r = await payment_r 62 | shipping_r = await shipping_r 63 | 64 | shipping_details = shipping_r.body.get("shipping_details") 65 | transaction_id = payment_r.body.get("transaction_id") 66 | 67 | self.logger.info(f"{transaction_id}") 68 | 69 | return HTTPStatusCode.CREATED, { 70 | "order_id": self.order_id, 71 | "transaction_id": transaction_id, 72 | "shipping_details": shipping_details, 73 | "status": "success", 74 | } 75 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/strategy.py: -------------------------------------------------------------------------------- 1 | """Module for defining a global placement strategy. 2 | 3 | It provides an abstract class that must be implemented by the user to define a global 4 | placement strategy for the entire infrastructure. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from abc import ( 10 | ABC, 11 | abstractmethod, 12 | ) 13 | from typing import ( 14 | TYPE_CHECKING, 15 | Any, 16 | Dict, 17 | ) 18 | 19 | if TYPE_CHECKING: 20 | from eclypse.graph import ( 21 | Application, 22 | Infrastructure, 23 | ) 24 | from eclypse.placement import ( 25 | Placement, 26 | PlacementView, 27 | ) 28 | 29 | 30 | class PlacementStrategy(ABC): 31 | """PlacementStrategy abstract class. 32 | 33 | A global placement strategy that places services of an application on infrastructure nodes. 34 | """ 35 | 36 | @abstractmethod 37 | def place( 38 | self, 39 | infrastructure: Infrastructure, 40 | application: Application, 41 | placements: Dict[str, Placement], 42 | placement_view: PlacementView, 43 | ) -> Dict[Any, Any]: 44 | """Defines the placement logic. 45 | 46 | Given an infrastructure, an application, a dictionary of placements, and a 47 | placement view, return a mapping of services IDs to node IDs, for the 48 | application. 49 | 50 | This method must be overridden by the user. 51 | 52 | Args: 53 | infrastructure (Infrastructure): The infrastructure to place the application onto. 54 | application (Application): The application to place onto the infrastructure. 55 | placements (Dict[str, Placement]): A dictionary of placements. 56 | placement_view (PlacementView): The placement view to use for the placement. 57 | 58 | Returns: 59 | Dict[Any, Any]: A dictionary mapping service IDs to node IDs, or None if the \ 60 | application cannot be placed onto the infrastructure. 61 | """ 62 | 63 | def is_feasible( 64 | self, 65 | infrastructure: Infrastructure, 66 | _: Application, # pylint: disable=unused-argument 67 | ) -> bool: 68 | """Check if the application can be placed on the infrastructure. 69 | 70 | Args: 71 | infrastructure (Infrastructure): The infrastructure to place the application onto. 72 | application (Application): The application to place onto the infrastructure. 73 | 74 | Returns: 75 | bool: True if the application can be placed on the infrastructure, False \ 76 | otherwise. 77 | """ 78 | return len(list(infrastructure.available.nodes)) > 0 79 | -------------------------------------------------------------------------------- /eclypse/report/reporters/json.py: -------------------------------------------------------------------------------- 1 | """Module for the JSON reporter, used to report simulation metrics in JSON format.""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | from datetime import datetime as dt 7 | from typing import ( 8 | TYPE_CHECKING, 9 | Any, 10 | List, 11 | ) 12 | 13 | import aiofiles # type: ignore[import-untyped] 14 | 15 | from eclypse.report.reporter import Reporter 16 | 17 | if TYPE_CHECKING: 18 | from eclypse.workflow.event import EclypseEvent 19 | 20 | 21 | class JSONReporter(Reporter): 22 | """Class to report the simulation metrics in JSON lines format.""" 23 | 24 | def __init__(self, *args, **kwargs): 25 | """Initialize the JSON reporter.""" 26 | super().__init__(*args, **kwargs) 27 | self.report_path = self.report_path / "json" 28 | 29 | def report( 30 | self, 31 | event_name: str, 32 | event_idx: int, 33 | callback: EclypseEvent, 34 | ) -> List[Any]: 35 | """Reports the callback values in JSON lines format. 36 | 37 | Args: 38 | event_name (str): The name of the event. 39 | event_idx (int): The index of the event trigger (step). 40 | callback (EclypseEvent): The executed callback containing the data to report. 41 | 42 | Returns: 43 | List[Any]: A list of dictionaries representing the JSON lines to report. 44 | """ 45 | return ( 46 | [ 47 | { 48 | "timestamp": dt.now().isoformat(), 49 | "event_name": event_name, 50 | "event_idx": event_idx, 51 | "callback_name": callback.name, 52 | "data": callback.data, 53 | } 54 | ] 55 | if callback.data 56 | else [] 57 | ) 58 | 59 | async def write(self, callback_type: str, data: List[dict]): 60 | """Write the JSON lines report to a file. 61 | 62 | Args: 63 | callback_type (str): The type of the callback (used for file naming). 64 | data (List[dict]): The list of dictionaries to write as JSON lines. 65 | """ 66 | path = self.report_path / f"{callback_type}.jsonl" 67 | async with aiofiles.open(path, "a", encoding="utf-8") as f: 68 | for item in data: 69 | line = json.dumps(item, ensure_ascii=False, cls=_SafeJSONEncoder) 70 | await f.write(f"{line}\n") 71 | 72 | 73 | class _SafeJSONEncoder(json.JSONEncoder): 74 | def default(self, o: Any) -> Any: 75 | if hasattr(o, "isoformat"): 76 | return o.isoformat() 77 | if isinstance(o, (set, tuple)): 78 | return list(o) 79 | return super().default(o) 80 | -------------------------------------------------------------------------------- /eclypse/report/reporter.py: -------------------------------------------------------------------------------- 1 | """Module for the Reporter abstract class. 2 | 3 | It defines the basic structure of a reporter, which is used to generate reports during 4 | the simulation. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from abc import ( 10 | ABC, 11 | abstractmethod, 12 | ) 13 | from itertools import product 14 | from pathlib import Path 15 | from typing import ( 16 | TYPE_CHECKING, 17 | Any, 18 | List, 19 | Union, 20 | ) 21 | 22 | if TYPE_CHECKING: 23 | from eclypse.workflow.event.event import EclypseEvent 24 | 25 | 26 | class Reporter(ABC): 27 | """Abstract class to report the simulation metrics. 28 | 29 | It provides the interface for the simulation reporters. 30 | """ 31 | 32 | def __init__( 33 | self, 34 | report_path: Union[str, Path], 35 | ): 36 | """Create a new Reporter. 37 | 38 | Args: 39 | report_path (Union[str, Path]): The path to save the reports. 40 | """ 41 | self.report_path = Path(report_path) 42 | 43 | async def init(self): 44 | """Perform any preparation logic (file creation, folder setup, headers, etc).""" 45 | self.report_path.mkdir(parents=True, exist_ok=True) 46 | 47 | @abstractmethod 48 | async def write(self, callback_type: str, data: Any): 49 | """Write a batch of buffered data to the destination (file, db, etc).""" 50 | 51 | @abstractmethod 52 | def report( 53 | self, 54 | event_name: str, 55 | event_idx: int, 56 | callback: EclypseEvent, 57 | ) -> List[Any]: 58 | """Report the simulation reportable callbacks. 59 | 60 | Args: 61 | event_name (str): The name of the event. 62 | event_idx (int): The index of the event trigger (step). 63 | callback (EclypseEvent): The executed event. 64 | 65 | Returns: 66 | List[Any]: The list of entries to be written. 67 | """ 68 | 69 | def dfs_data(self, data: Any) -> List: 70 | """Perform DFS on the nested dictionary and build paths (concatenated keys) as strings. 71 | 72 | Args: 73 | data (Any): The data to traverse. 74 | 75 | Returns: 76 | List: The list of paths. 77 | """ 78 | 79 | def dfs(d): 80 | if isinstance(d, dict): 81 | for key, value in d.items(): 82 | for key_path, value_path in product(dfs(key), dfs(value)): 83 | yield key_path + value_path 84 | elif isinstance(d, tuple): 85 | for path in product(*map(dfs, d)): 86 | yield [item for subpath in path for item in subpath] 87 | else: 88 | yield [d] 89 | 90 | # Start the DFS from the root of the dictionary 91 | return list(dfs(data)) 92 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/random.py: -------------------------------------------------------------------------------- 1 | """Module for the Random placement strategy. 2 | 3 | It overrides the `place` method of the 4 | PlacementStrategy class to place services of an application on infrastructure nodes 5 | randomly. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import os 11 | import random as rnd 12 | from typing import ( 13 | TYPE_CHECKING, 14 | Any, 15 | Dict, 16 | Optional, 17 | ) 18 | 19 | from eclypse.utils.constants import RND_SEED 20 | 21 | from .strategy import PlacementStrategy 22 | 23 | if TYPE_CHECKING: 24 | from eclypse.graph import ( 25 | Application, 26 | Infrastructure, 27 | ) 28 | from eclypse.placement import ( 29 | Placement, 30 | PlacementView, 31 | ) 32 | 33 | 34 | class RandomStrategy(PlacementStrategy): 35 | """A placement strategy that places services randomly onto nodes.""" 36 | 37 | def __init__(self, spread: bool = False, seed: Optional[int] = None): 38 | """Initializes the Random placement strategy. 39 | 40 | Args: 41 | spread (bool, optional): Whether to spread the services across different nodes. \ 42 | Defaults to False. 43 | seed (Optional[int], optional): The seed for the random number generator. \ 44 | Defaults to None. 45 | """ 46 | self._rnd = rnd.Random(seed if seed is not None else os.environ[RND_SEED]) 47 | self.spread = spread 48 | super().__init__() 49 | 50 | def place( 51 | self, 52 | _: Infrastructure, 53 | application: Application, 54 | __: Dict[str, Placement], 55 | placement_view: PlacementView, 56 | ) -> Dict[Any, Any]: 57 | """Places the services of an application on the infrastructure nodes, randomly. 58 | 59 | Args: 60 | _ (Infrastructure): The infrastructure to place the application on. 61 | application (Application): The application to place on the infrastructure. 62 | __ (Dict[str, Placement]): The placement of all the applications in the simulations. 63 | placement_view (PlacementView): The snapshot of the current state of the \ 64 | infrastructure. 65 | 66 | Returns: 67 | Dict[str, str]: A mapping of services to infrastructure nodes. 68 | """ 69 | infrastructure_nodes = list(placement_view.residual.nodes()) 70 | if not infrastructure_nodes: 71 | return {} 72 | 73 | self._rnd.shuffle(infrastructure_nodes) 74 | 75 | if self.spread: 76 | return { 77 | service: infrastructure_nodes[i % len(infrastructure_nodes)] 78 | for i, service in enumerate(application.nodes) 79 | } 80 | 81 | return { 82 | service: self._rnd.choice(infrastructure_nodes) 83 | for service in application.nodes 84 | } 85 | -------------------------------------------------------------------------------- /docs/source/overview/getting-started/index.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting started 3 | =============== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :hidden: 8 | 9 | assets 10 | update-policy 11 | placement-strategy 12 | events 13 | topology 14 | simulation 15 | 16 | Let's get started with ECLYPSE! This guide will walk you through the steps to set up a simulation using the framework. 17 | ECLYPSE is designed to be flexible and extensible, allowing you to model complex scenarios in Cloud-Edge computing environments. 18 | 19 | The following steps outline the full workflow for setting up a simulation: 20 | 21 | .. grid:: 2 22 | 23 | .. grid-item:: 24 | 25 | .. card:: 1. Define assets 26 | :link: assets 27 | :link-type: doc 28 | 29 | **Assets** describe resource-related properties used in the simulation, including infrastructure *capabilities* and application service *requirements*. 30 | They enable compatibility matching between applications and infrastructure. 31 | 32 | .. grid-item:: 33 | 34 | .. card:: 2. Define update policies 35 | :link: update-policy 36 | :link-type: doc 37 | 38 | **Update policies** model how assets, network and applications' topologies evolve over time, reflecting changes in infrastructure capabilities or application requirements. They allow the simulation of dynamic behaviours. 39 | 40 | .. grid-item:: 41 | 42 | .. card:: 3. Define a placement strategy 43 | :link: placement-strategy 44 | :link-type: doc 45 | 46 | A **placement strategy** defines how services are allocated across the infrastructure. It can be defined per application or globally, and can reflect performance, locality, or optimisation goals. 47 | 48 | .. grid-item:: 49 | 50 | .. card:: 4. Define simulation workflow through events 51 | :link: events 52 | :link-type: doc 53 | 54 | **Events** allow custom actions during the simulation, such as metric collection, event logging, or injecting logic. 55 | They provide extensibility and observability to the simulation process. 56 | 57 | .. grid-item:: 58 | 59 | .. card:: 5. Define Infrastructure and Application(s) 60 | :link: topology 61 | :link-type: doc 62 | 63 | Define the network **infrastructure** (nodes and links) and the **applications** (services and interactions). Assets and update policies are linked to them at this stage. 64 | 65 | .. grid-item:: 66 | 67 | .. card:: 6. Create, configure and run the simulation 68 | :link: simulation 69 | :link-type: doc 70 | 71 | **Instantiate** and **configure** the simulation by setting its parameters 72 | and registering applications with their placement strategies. 73 | Then, **run** the simulation to start the event loop. 74 | 75 | .. tip:: 76 | 77 | If you have not yet installed ECLYPSE, refer to the :doc:`Installation <../install>` page. 78 | -------------------------------------------------------------------------------- /examples/user_distribution/metric.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | import os 5 | from typing import ( 6 | TYPE_CHECKING, 7 | Any, 8 | Dict, 9 | ) 10 | 11 | import psutil 12 | 13 | from eclypse.graph.assets import Additive 14 | from eclypse.report.metrics import metric 15 | from eclypse.report.metrics.defaults import ( 16 | SimulationTime, 17 | response_time, 18 | ) 19 | 20 | if TYPE_CHECKING: 21 | from eclypse.graph import ( 22 | Application, 23 | Infrastructure, 24 | ) 25 | from eclypse.placement import Placement 26 | 27 | # Node asset for the infrastructure 28 | 29 | 30 | def user_count_asset( 31 | lower_bound: float = 0.0, 32 | upper_bound: float = float("inf"), 33 | init_value: int = 0, 34 | ) -> Additive: 35 | return Additive(lower_bound, upper_bound, init_value, functional=False) 36 | 37 | 38 | # Metrics for the simulation 39 | 40 | 41 | @metric.node(name="user_count") 42 | def user_count_metric(_: str, resources: Dict[str, Any], __, ___, ____) -> float: 43 | return resources.get("user_count", 0) 44 | 45 | 46 | @metric.node(name="user_delay") 47 | def user_delay( 48 | node: str, 49 | resources: Dict[str, Any], 50 | placements: Dict[str, Placement], 51 | infr: Infrastructure, 52 | __, 53 | ) -> float: 54 | 55 | placement = placements.get("SockShop") 56 | 57 | # If the application is not placed, return infinity 58 | if placement is None or placement.is_partial: 59 | return float("inf") 60 | 61 | frontend_node = placement.service_placement("FrontendService") 62 | 63 | latency = infr.path_resources(node, frontend_node)["latency"] 64 | user_count = resources.get("user_count", 0) 65 | return ( 66 | latency + (user_count * math.log(1 + user_count)) 67 | if latency is not None 68 | else float("inf") 69 | ) 70 | 71 | 72 | @metric.application(name="used_nodes") 73 | def used_nodes(_: Application, placement: Placement, __: Infrastructure) -> Application: 74 | return len(set(placement.mapping.values())) 75 | 76 | 77 | @metric.simulation(name="cpu_usage", activates_on=["enact", "stop"]) 78 | class CPUMonitor: 79 | 80 | def __init__(self): 81 | self.process = psutil.Process(os.getpid()) 82 | 83 | def __call__(self, event): 84 | return self.process.cpu_percent(interval=0.1) 85 | 86 | 87 | @metric.simulation(name="memory_usage", activates_on=["enact", "stop"]) 88 | class MemoryMonitor: 89 | 90 | def __init__(self): 91 | self.process = psutil.Process(os.getpid()) 92 | 93 | def __call__(self, event): 94 | memory_usage = self.process.memory_info().rss 95 | return memory_usage / (1024 * 1024) # Convert to MB 96 | 97 | 98 | def get_metrics(): 99 | return [ 100 | user_count_metric, 101 | user_delay, 102 | response_time, 103 | CPUMonitor(), 104 | MemoryMonitor(), 105 | SimulationTime(), 106 | ] 107 | -------------------------------------------------------------------------------- /eclypse/builders/application/sock_shop/mpi_services/frontend.py: -------------------------------------------------------------------------------- 1 | """The `FrontendService` class. 2 | 3 | It serves as the user interface for the SockShop application, 4 | providing the user-facing components of the store. 5 | 6 | - Key Responsibilities: 7 | - Displays product catalogs, shopping carts, and order information to users. 8 | - Interacts with backend services \ 9 | (e.g., `CatalogService`, `UserService`, `OrderService`) to display real-time data. 10 | - Manages user input and interactions such as product searches, \ 11 | cart updates, and order placements. 12 | """ 13 | 14 | from eclypse.remote.communication import mpi 15 | from eclypse.remote.service import Service 16 | 17 | 18 | class FrontendService(Service): 19 | """MPI workflow of the Frontend service.""" 20 | 21 | def __init__(self, name): 22 | """Initialize the FrontendService with a user ID. 23 | 24 | Args: 25 | name (str): The name of the service. 26 | """ 27 | super().__init__(name) 28 | self.user_id = 12345 29 | 30 | async def step(self): 31 | """Example workflow of the `Frontend` service. 32 | 33 | It starts with fetching the catalog, user data, and cart items, then placing an order. 34 | """ 35 | # Send request to CatalogService 36 | await self.catalog_request() 37 | 38 | # Receive response from CatalogService 39 | catalog_response = await self.mpi.recv() 40 | 41 | self.logger.info(f"{self.id} - {catalog_response}") 42 | 43 | # Send request to UserService 44 | user_request = {"request_type": "user_data", "user_id": self.user_id} 45 | self.mpi.send("UserService", user_request) 46 | 47 | # Receive response from UserService 48 | user_response = await self.mpi.recv() 49 | self.logger.info(f"{self.id} - {user_response}") 50 | 51 | # Send request to CartService 52 | cart_request = {"request_type": "cart_data", "user_id": self.user_id} 53 | self.mpi.send("CartService", cart_request) 54 | 55 | # Receive response from CartService 56 | cart_response = await self.mpi.recv() 57 | self.logger.info(f"{self.id} - {cart_response}") 58 | 59 | cart_items = cart_response.get("items", []) 60 | 61 | # Send request to OrderService 62 | order_request = { 63 | "request_type": "order_request", 64 | "user_id": self.user_id, 65 | "items": cart_items, 66 | } 67 | self.mpi.send("OrderService", order_request) 68 | 69 | # Receive response from OrderService 70 | order_response = await self.mpi.recv() 71 | self.logger.info(f"{self.id} - {order_response}") 72 | 73 | @mpi.exchange(send=True) 74 | def catalog_request(self): 75 | """Send a request to the CatalogService for product data. 76 | 77 | Returns: 78 | str: The recipient service name. 79 | dict: The request body. 80 | """ 81 | return "CatalogService", {"request_type": "catalog_data"} 82 | -------------------------------------------------------------------------------- /docs/source/overview/examples/echo.rst: -------------------------------------------------------------------------------- 1 | Echo 2 | ==== 3 | 4 | The Echo Application showcases a simple microservices architecture where messages are echoed back and forth among a set of identical services. 5 | This example provides insights into the basic structure and interaction patterns of microservices within a distributed system. 6 | 7 | The whole code for this example can be found in the `examples/echo `_ directory of the ECLYPSE Github repository. 8 | 9 | Application 10 | ----------- 11 | 12 | The Echo Application consists of several identical services, each of which receives a message and echoes it back to all of its neighbors. This symmetrical architecture ensures that each service behaves identically. 13 | 14 | As each service echoes messages both by broadcasting to all neighbors and unicasting individually to each neighbour, it is useful to compare the expected results of these two communication methods. 15 | Broadcasting is expected to be faster than unicasting, as it involves sending messages to all neighbors simultaneously. Unicasting, on the other hand, requires sending a separate message to each neighbor, which can result in a longer total communication time. 16 | 17 | .. dropdown:: Application code 18 | 19 | .. literalinclude:: ../../../../examples/echo/application.py 20 | :language: python 21 | 22 | 23 | Echo Service 24 | ------------ 25 | 26 | The `EchoService` class is the core component responsible for echoing messages within the Echo Example application. Below is the code for the EchoService along with an explanation: 27 | 28 | .. dropdown:: Service code 29 | 30 | .. literalinclude:: ../../../../examples/echo/echo.py 31 | :language: python 32 | :linenos: 33 | 34 | We defined the `EchoService` class, which inherits from the :class:`~eclypse.remote.service.service.Service` class provided in ECLYPSE. The `dispatch` implements the logic of the `EchoService`, thus it is responsible for sending messages to neighbors and logging the communication statistics. 35 | 36 | 37 | Infrastructure 38 | -------------- 39 | 40 | The EchoApplication is deployed on a network of 4 heterogeneous nodes interconnected via 4 links, named *EchoInfrastructure*. 41 | 42 | .. dropdown:: Infrastructure code 43 | 44 | .. literalinclude:: ../../../../examples/echo/infrastructure.py 45 | :language: python 46 | 47 | The *EchoInfrastructure* is also updated at each iteration to simulate the ever chaning nature of real-world networks. 48 | To do so, we used a **random** node/edge udpate policy. 49 | 50 | .. dropdown:: Update policy code 51 | 52 | .. literalinclude:: ../../../../examples/echo/update_policy.py 53 | :language: python 54 | 55 | Simulation 56 | ---------- 57 | 58 | The simulation is run *remotely* for 20 iterations, each lasting 0.5 seconds. 59 | Logs are enabled, as is the reporting, in a folder named as the application. 60 | A random seed is set to ensure reproducibility. 61 | 62 | .. dropdown:: Simulation code 63 | 64 | .. literalinclude:: ../../../../examples/echo/main.py 65 | :language: python 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "eclypse" 3 | version = "0.8.1" 4 | description = "an Edge-CLoud pYthon Platform for Simulated runtime Environments" 5 | authors = [ 6 | "Jacopo Massa ", 7 | "Valerio De Caro ", 8 | ] 9 | maintainers = [ 10 | "Jacopo Massa ", 11 | "Valerio De Caro ", 12 | ] 13 | license = "MIT" 14 | readme = "README.md" 15 | homepage = "https://github.com/eclypse-org/eclypse" 16 | repository = "https://github.com/eclypse-org/eclypse" 17 | 18 | [tool.poetry.dependencies] 19 | python = ">=3.11, <3.14" 20 | networkx = "^3.6" 21 | loguru = "^0.7.3" 22 | pandas = "^2.3.3" 23 | aiofiles = "^25.1.0" 24 | tensorboardx = { version = "^2.6.4", optional = true } 25 | ray = { extras = ["default"], version = "^2.52.1", optional = true } 26 | 27 | [tool.poetry.extras] 28 | remote = ["ray"] 29 | tboard = ["tensorboardX"] 30 | 31 | [tool.poetry.group.dev] 32 | optional = true 33 | 34 | [tool.poetry.group.dev.dependencies] 35 | black = "^25.11.0" 36 | commitizen = "^4.10.0" 37 | pre-commit = "^4.5.0" 38 | mypy = "^1.19.0" 39 | isort = "^7.0.0" 40 | ruff = "^0.14.8" 41 | # pycln = "^2.6.0" 42 | # pylint = "^4.0.2" 43 | # docformatter = { extras = ["tomli"], version = "^1.7.7" } 44 | 45 | [tool.poetry.group.deploy] 46 | optional = true 47 | 48 | [tool.poetry.group.deploy.dependencies] 49 | wheel = "^0.45.1" 50 | setuptools = "^80.9.0" 51 | twine = "^6.2.0" 52 | 53 | [tool.poetry.group.test] 54 | optional = true 55 | 56 | [tool.poetry.group.test.dependencies] 57 | pytest = "^8.4.2" 58 | pytest-cov = "^7.0.0" 59 | pytest-xdist = "^3.8.0" 60 | 61 | [tool.poetry.group.docs] 62 | optional = true 63 | 64 | # To build the docs, at least python3.10 is required 65 | [tool.poetry.group.docs.dependencies] 66 | myst-parser = "^4.0.1" 67 | sphinx-copybutton = "^0.5.2" 68 | jinja2 = "^3.1.6" 69 | docformatter = { extras = ["tomli"], version = "^1.7.7" } 70 | sphinx-autobuild = "^2025.8.25" 71 | enum-tools = "^0.13.0" 72 | sphinx-favicon = "^1.0.1" 73 | sphinx-design = "^0.6.1" 74 | sphinx-icon = "^0.2.2" 75 | pydata-sphinx-theme = "^0.16.1" 76 | sphinx = "^8.2.3" 77 | 78 | [build-system] 79 | requires = ["poetry-core", "setuptools", "wheel"] 80 | build-backend = "poetry.core.masonry.api" 81 | 82 | [tool.commitizen] 83 | name = "cz_conventional_commits" 84 | version = "0.8.1" 85 | tag_format = "$version" 86 | version_files = [ 87 | "pyproject.toml:version", 88 | "eclypse/__init__.py:__version__", 89 | "CITATION.cff:version", 90 | ] 91 | 92 | [tool.isort] 93 | multi_line_output = 3 94 | include_trailing_comma = true 95 | force_grid_wrap = 2 96 | filter_files = true 97 | skip = ["__init__.py"] 98 | 99 | # [tool.docformatter] 100 | # recursive = true 101 | # black = true 102 | # diff = true 103 | # tab-width = 4 104 | 105 | # [tool.pycln] 106 | # all = true 107 | 108 | [tool.pytest.ini_options] 109 | testpaths = "tests" 110 | addopts = "--cov-report=xml --cov-report=term-missing --cov" 111 | filterwarnings = ["ignore::DeprecationWarning"] 112 | -------------------------------------------------------------------------------- /eclypse/remote/communication/mpi/requests/unicast.py: -------------------------------------------------------------------------------- 1 | """Module for UnicastRequest class, subclassing MPIRequest. 2 | 3 | It represents a request to send a message to a single recipient. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from typing import ( 9 | TYPE_CHECKING, 10 | Any, 11 | Dict, 12 | Generator, 13 | Optional, 14 | ) 15 | 16 | from eclypse.remote.communication.mpi.requests import MulticastRequest 17 | 18 | if TYPE_CHECKING: 19 | from datetime import ( 20 | datetime, 21 | timedelta, 22 | ) 23 | 24 | from eclypse.remote.communication.mpi import ( 25 | EclypseMPI, 26 | Response, 27 | ) 28 | from eclypse.remote.communication.route import Route 29 | 30 | 31 | class UnicastRequest(MulticastRequest): 32 | """A request to send a message to a single recipient.""" 33 | 34 | def __init__( 35 | self, 36 | recipient_id: str, 37 | body: Dict[str, Any], 38 | _mpi: EclypseMPI, 39 | timestamp: Optional[datetime] = None, 40 | ): 41 | """Initializes a UnicastRequest object. 42 | 43 | Args: 44 | recipient_id (str): The ID of the recipient node. 45 | body (Dict[str, Any]): The body of the request. 46 | _mpi (EclypseMPI): The MPI interface. 47 | timestamp (Optional[datetime], optional): The timestamp of the request. 48 | Defaults to None. 49 | """ 50 | super().__init__( 51 | recipient_ids=[recipient_id], 52 | body=body, 53 | _mpi=_mpi, 54 | timestamp=timestamp, 55 | ) 56 | 57 | def __await__(self) -> Generator[Any, None, UnicastRequest]: 58 | """Await the request to complete. 59 | 60 | Returns: 61 | Awaitable: The result of the request. 62 | """ 63 | return super().__await__() # type: ignore[return-value] 64 | 65 | @property 66 | def recipient_id(self) -> str: 67 | """The ID of the recipient. 68 | 69 | Returns: 70 | str: The ID. 71 | """ 72 | return self._recipient_ids[0] 73 | 74 | @property 75 | def response(self) -> Optional[Response]: 76 | """The response to the request. 77 | 78 | Returns: 79 | Optional[Response]: The response to the request if available, None otherwise. 80 | """ 81 | return self.responses[0] 82 | 83 | @property 84 | def route(self) -> Optional[Route]: 85 | """The route to the recipient. 86 | 87 | Returns: 88 | Optional[Route]: The route to the recipient if available, None otherwise. 89 | """ 90 | return self.routes[0] 91 | 92 | @property 93 | def elapsed_time(self) -> Optional[timedelta]: 94 | """The elapsed time until the response was received. 95 | 96 | Returns: 97 | Optional[timedelta]: The elapsed time until the response was received, 98 | or None if the response is not yet available. 99 | """ 100 | return self.elapsed_times[0] 101 | -------------------------------------------------------------------------------- /eclypse/report/reporters/csv.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-argument 2 | """Module for the CSVReporter class. 3 | 4 | It is used to report the simulation metrics in a CSV format. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from datetime import datetime as dt 10 | from typing import ( 11 | TYPE_CHECKING, 12 | Any, 13 | List, 14 | ) 15 | 16 | import aiofiles # type: ignore[import-untyped] 17 | 18 | from eclypse.report.reporter import Reporter 19 | 20 | if TYPE_CHECKING: 21 | from eclypse.workflow.event import EclypseEvent 22 | 23 | CSV_DELIMITER = "," 24 | DEFAULT_IDX_HEADER = ["timestamp", "event_id", "n_event", "callback_id"] 25 | 26 | DEFAULT_CSV_HEADERS = { 27 | "simulation": [*DEFAULT_IDX_HEADER, "value"], 28 | "application": [*DEFAULT_IDX_HEADER, "application_id", "value"], 29 | "service": [*DEFAULT_IDX_HEADER, "application_id", "service_id", "value"], 30 | "interaction": [*DEFAULT_IDX_HEADER, "application_id", "source", "target", "value"], 31 | "infrastructure": [*DEFAULT_IDX_HEADER, "value"], 32 | "node": [*DEFAULT_IDX_HEADER, "node_id", "value"], 33 | "link": [*DEFAULT_IDX_HEADER, "source", "target", "value"], 34 | } 35 | 36 | 37 | class CSVReporter(Reporter): 38 | """Class to report the simulation metrics in CSV format. 39 | 40 | It prints an header with the format of the rows and then the values of the 41 | reportable. 42 | """ 43 | 44 | def __init__(self, *args, **kwargs): 45 | """Initialize the CSV reporter.""" 46 | super().__init__(*args, **kwargs) 47 | self.report_path = self.report_path / "csv" 48 | 49 | def report( 50 | self, 51 | event_name: str, 52 | event_idx: int, 53 | callback: EclypseEvent, 54 | ) -> List[str]: 55 | """Reports the callback values in a CSV file, one per line. 56 | 57 | Args: 58 | event_name (str): The name of the event. 59 | event_idx (int): The index of the event trigger (step). 60 | callback (EclypseEvent): The executed callback containing the data to report. 61 | """ 62 | lines = [] 63 | for line in self.dfs_data(callback.data): 64 | if line[-1] is None: 65 | continue 66 | 67 | fields = [dt.now().isoformat(), event_name, event_idx, callback.name, *line] 68 | 69 | fields = [str(f) for f in fields] 70 | lines.append(CSV_DELIMITER.join(fields)) 71 | 72 | return lines 73 | 74 | async def write(self, callback_type: str, data: Any): 75 | """Writes the data to a CSV file based on the callback type. 76 | 77 | Args: 78 | callback_type (str): The type of the callback. 79 | data (Any): The data to write to the CSV file. 80 | """ 81 | path = self.report_path / f"{callback_type}.csv" 82 | if not path.exists(): 83 | async with aiofiles.open(path, "a", encoding="utf-8") as f: 84 | await f.write( 85 | f"{CSV_DELIMITER.join(DEFAULT_CSV_HEADERS[callback_type])}\n" 86 | ) 87 | 88 | async with aiofiles.open(path, "a", encoding="utf-8") as f: 89 | await f.writelines([f"{line}\n" for line in data]) 90 | -------------------------------------------------------------------------------- /examples/image_prediction/metrics.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | 5 | from eclypse.report.metrics import metric 6 | from eclypse.report.metrics.defaults import featured_latency 7 | from eclypse.utils.constants import DRIVING_EVENT 8 | 9 | 10 | @metric.service(name="img_counter", remote=True) 11 | def get_img_counter(self): 12 | if self.id == "EndService": 13 | if self.img_counter == 0: 14 | self.logger.warning("No images were processed") 15 | return 0 16 | return self.img_counter 17 | return None 18 | 19 | 20 | @metric.service(name="accuracy", remote=True) 21 | def get_acc(self): 22 | if self.id == "EndService" and self.img_counter != 0: 23 | acc = self.correct / self.img_counter 24 | self.correct = 0 25 | self.img_counter = 0 26 | return acc 27 | return None 28 | 29 | 30 | @metric.service(name="model_change", remote=True) 31 | def is_changed(self): 32 | if self.id == "PredictorService": 33 | if self.new_model_signal: 34 | self.new_model_signal = False 35 | return 1 36 | return None 37 | 38 | 39 | @metric.simulation(name="cpu_usage", activates_on=[DRIVING_EVENT, "stop"]) 40 | class CPUMonitor: 41 | 42 | def __init__(self): 43 | self.process = None 44 | 45 | def __call__(self, event): 46 | if self.process is None: 47 | self.process = psutil.Process(os.getpid()) 48 | return self.process.cpu_percent(interval=0.1) 49 | 50 | 51 | @metric.service( 52 | name="remote_cpu_usage", 53 | activates_on=[DRIVING_EVENT, "stop"], 54 | remote=True, 55 | ) 56 | class RemoteCPUMonitor: 57 | 58 | def __init__(self): 59 | self.process = None 60 | 61 | def __call__(self, event): 62 | if self.process is None: 63 | self.process = psutil.Process(os.getpid()) 64 | return self.process.cpu_percent(interval=0.1) 65 | 66 | 67 | @metric.simulation( 68 | name="memory_usage", 69 | activates_on=[DRIVING_EVENT, "stop"], 70 | ) 71 | class MemoryMonitor: 72 | 73 | def __init__(self): 74 | self.process = None 75 | 76 | def __call__(self, event): 77 | if self.process is None: 78 | self.process = psutil.Process(os.getpid()) 79 | memory_usage = self.process.memory_info().rss 80 | return memory_usage / (1024 * 1024) # Convert to MB 81 | 82 | 83 | @metric.service( 84 | name="remote_memory_usage", 85 | activates_on=[DRIVING_EVENT, "stop"], 86 | remote=True, 87 | ) 88 | class RemoteMemoryMonitor: 89 | 90 | def __init__(self): 91 | self.process = None 92 | 93 | def __call__(self, event): 94 | if self.process is None: 95 | self.process = psutil.Process(os.getpid()) 96 | memory_usage = self.process.memory_info().rss 97 | return memory_usage / (1024 * 1024) 98 | 99 | 100 | def get_metrics(): 101 | return [ 102 | get_img_counter, 103 | get_acc, 104 | is_changed, 105 | featured_latency, 106 | CPUMonitor(), 107 | MemoryMonitor(), 108 | RemoteCPUMonitor(), 109 | RemoteMemoryMonitor(), 110 | ] 111 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/first_fit.py: -------------------------------------------------------------------------------- 1 | """Module for a First Fit placement strategy. 2 | 3 | It overrides the `place` method of the 4 | PlacementStrategy class to place services of an application on infrastructure nodes 5 | based on the first node that satisfies the requirements of the service. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import random as rnd 11 | from typing import ( 12 | TYPE_CHECKING, 13 | Any, 14 | Callable, 15 | Dict, 16 | Optional, 17 | ) 18 | 19 | from .strategy import PlacementStrategy 20 | 21 | if TYPE_CHECKING: 22 | from eclypse.graph import ( 23 | Application, 24 | Infrastructure, 25 | ) 26 | from eclypse.placement import ( 27 | Placement, 28 | PlacementView, 29 | ) 30 | 31 | 32 | class FirstFitStrategy(PlacementStrategy): 33 | """FirstFitStrategy class. 34 | 35 | A placement strategy that places services onto the first node that satisfies the 36 | requirements. 37 | """ 38 | 39 | def __init__(self, sort_fn: Optional[Callable[[Any], Any]] = None): 40 | """Initializes the FirstFit placement strategy. 41 | 42 | Args: 43 | sort_fn (Optional[Callable[[Any], Any]], optional): A function to sort \ 44 | the infrastructure nodes. Defaults to None. 45 | """ 46 | self.sort_fn = sort_fn 47 | super().__init__() 48 | 49 | def place( 50 | self, 51 | infrastructure: Infrastructure, 52 | application: Application, 53 | _: Dict[str, Placement], 54 | placement_view: PlacementView, 55 | ) -> Dict[str, str]: 56 | """Performs the placement according to a first-fit logic. 57 | 58 | Places the services of an application on the infrastructure nodes based on 59 | the first node that satisfies the requirements of the service. 60 | 61 | Args: 62 | infrastructure (Infrastructure): The infrastructure to place the application on. 63 | application (Application): The application to place on the infrastructure. 64 | _ (Dict[str, Placement]): The placement of all the applications in the simulations. 65 | placement_view (PlacementView): The snapshot of the current state of the infrastructure. 66 | 67 | Returns: 68 | Dict[str, str]: A mapping of services to infrastructure nodes. 69 | """ 70 | if not self.is_feasible(infrastructure, application): 71 | return {} 72 | 73 | mapping = {} 74 | infrastructure_nodes = list(placement_view.residual.nodes(data=True)) 75 | if self.sort_fn: 76 | infrastructure_nodes.sort(key=self.sort_fn) 77 | else: 78 | rnd.shuffle(infrastructure_nodes) 79 | 80 | for service, sattr in application.nodes(data=True): 81 | for node, nattr in infrastructure_nodes: 82 | if infrastructure.node_assets.satisfies(nattr, sattr): 83 | mapping[service] = node 84 | new_res = infrastructure.node_assets.consume(nattr, sattr) 85 | infrastructure_nodes.remove((node, nattr)) 86 | infrastructure_nodes.append((node, new_res)) 87 | break 88 | return mapping 89 | -------------------------------------------------------------------------------- /eclypse/placement/strategies/best_fit.py: -------------------------------------------------------------------------------- 1 | """Module for a Best Fit placement strategy. 2 | 3 | It overrides the `place` method of the 4 | PlacementStrategy class to place services of an application on infrastructure nodes 5 | based on the node that best fits the requirements of the service (i.e., the node that 6 | satisfies the requirements and has the least amount of resources left after the placement). 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import random as rnd 12 | from typing import ( 13 | TYPE_CHECKING, 14 | Any, 15 | Dict, 16 | Optional, 17 | ) 18 | 19 | from .strategy import PlacementStrategy 20 | 21 | if TYPE_CHECKING: 22 | from eclypse.graph import ( 23 | Application, 24 | Infrastructure, 25 | ) 26 | from eclypse.placement import ( 27 | Placement, 28 | PlacementView, 29 | ) 30 | 31 | 32 | class BestFitStrategy(PlacementStrategy): 33 | """BestFitStrategy class. 34 | 35 | A placement strategy that places services onto the node that best fits the 36 | requirements. 37 | """ 38 | 39 | def place( 40 | self, 41 | infrastructure: Infrastructure, 42 | application: Application, 43 | _: Dict[str, Placement], 44 | placement_view: PlacementView, 45 | ) -> Dict[Any, Any]: 46 | """Performs the placement according to a best-fit logic. 47 | 48 | Places the services of an application on the infrastructure nodes based on 49 | the node that best fits the requirements of the service. 50 | 51 | Args: 52 | infrastructure (Infrastructure): The infrastructure to place the application on. 53 | application (Application): The application to place on the infrastructure. 54 | _ (Dict[str, Placement]): The placement of all the applications in the simulations. 55 | placement_view (PlacementView): The snapshot of the current state of the \ 56 | infrastructure. 57 | 58 | Returns: 59 | Dict[str, str]: A mapping of services to infrastructure nodes. 60 | """ 61 | if not self.is_feasible(infrastructure, application): 62 | return {} 63 | 64 | mapping = {} 65 | infrastructure_nodes = list(placement_view.residual.nodes(data=True)) 66 | rnd.shuffle(infrastructure_nodes) 67 | 68 | for service, sattr in application.nodes(data=True): 69 | best_fit: Optional[str] = None 70 | best_nattr: Optional[Dict[str, Any]] = None 71 | for node, nattr in infrastructure_nodes: 72 | if infrastructure.node_assets.satisfies(nattr, sattr) and ( 73 | best_fit is None 74 | or infrastructure.node_assets.satisfies( 75 | placement_view.residual.nodes[best_fit], nattr 76 | ) 77 | ): 78 | best_fit = node 79 | best_nattr = nattr 80 | mapping[service] = best_fit 81 | if best_fit is None or best_nattr is None: 82 | continue 83 | 84 | new_res = infrastructure.node_assets.consume(best_nattr, sattr) 85 | infrastructure_nodes.remove((best_fit, best_nattr)) 86 | infrastructure_nodes.append((best_fit, new_res)) 87 | return mapping 88 | -------------------------------------------------------------------------------- /eclypse/report/reporters/tensorboard.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=no-member, unused-argument 2 | """Module for TensorBoardReporter class. 3 | 4 | It is used to report the simulation metrics on a TensorBoard file, using the 5 | TensorBoardX library. It creates a separate plot for each callback, where the x-axis is 6 | the combination of 'event_name' and 'event_idx', and the y-axis is the value. Each plot 7 | contains multiple lines, one for each unique path in the data dictionary. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from typing import ( 13 | TYPE_CHECKING, 14 | Any, 15 | List, 16 | ) 17 | 18 | from eclypse.report.reporter import Reporter 19 | 20 | if TYPE_CHECKING: 21 | from tensorboardX import SummaryWriter 22 | 23 | from eclypse.workflow.event import EclypseEvent 24 | 25 | 26 | class TensorBoardReporter(Reporter): 27 | """Asynchronous reporter for simulation metrics in TensorBoardX format.""" 28 | 29 | def __init__(self, *args, **kwargs): 30 | """Initialize the TensorBoard reporter.""" 31 | super().__init__(*args, **kwargs) 32 | self.report_path = self.report_path / "tensorboard" 33 | self._writer = None 34 | 35 | async def init(self): 36 | """Initialize the TensorBoard reporter.""" 37 | from tensorboardX import ( # pylint: disable=import-outside-toplevel 38 | SummaryWriter, 39 | ) 40 | 41 | self._writer = SummaryWriter(log_dir=self.report_path) 42 | 43 | def report( 44 | self, 45 | _: str, 46 | event_idx: int, 47 | callback: EclypseEvent, 48 | ) -> List[Any]: 49 | """Generate TensorBoard-compatible metric tuples. 50 | 51 | Returns a list of (callback_name, metric_dict, event_idx) to be written. 52 | 53 | Args: 54 | _ (str): The name of the event. 55 | event_idx (int): The index of the event trigger (step). 56 | callback (EclypseEvent): The executed callback containing the data to report. 57 | 58 | Returns: 59 | List[Any]: A list of tuples with (callback_name, metric_dict, event_idx). 60 | """ 61 | if callback.type is None: 62 | return [] 63 | 64 | entries = [] 65 | for line in self.dfs_data(callback.data): 66 | if line[-1] is None: 67 | continue 68 | metric_name = "/".join(line[:-1]) or "value" 69 | entries.append((callback.name, {metric_name: line[-1]}, event_idx)) 70 | 71 | return entries 72 | 73 | async def write( 74 | self, callback_type: str, data: list[tuple[str, dict[str, float], int]] 75 | ): 76 | """Write the collected metrics to TensorBoard. 77 | 78 | Args: 79 | callback_type (str): The type of the callback (used for organizing plots). 80 | data (list[tuple[str, dict[str, float], int]]): List of tuples 81 | containing (callback_name, metric_dict, event_idx). 82 | """ 83 | for cb_name, metric_dict, step in data: 84 | self.writer.add_scalars(f"{callback_type}/{cb_name}", metric_dict, step) 85 | 86 | @property 87 | def writer(self) -> SummaryWriter: 88 | """Get the TensorBoardX SummaryWriter.""" 89 | if self._writer is None: 90 | raise RuntimeError("TensorBoard reporter is not initialised.") 91 | return self._writer 92 | -------------------------------------------------------------------------------- /eclypse/remote/utils/ray_interface.py: -------------------------------------------------------------------------------- 1 | """Module for RayInterface class. 2 | 3 | It provides a simple interface to customise and configure the Ray backend used by 4 | Eclypse. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import os 10 | from contextlib import redirect_stderr 11 | from typing import ( 12 | TYPE_CHECKING, 13 | Any, 14 | Dict, 15 | ) 16 | 17 | if TYPE_CHECKING: 18 | from ray import ObjectRef 19 | from ray.actor import ActorHandle 20 | 21 | 22 | class RayInterface: 23 | """A simple interface to customise and configure the Ray backend used by Eclypse.""" 24 | 25 | def __init__(self): 26 | """Initialize the RayInterface.""" 27 | self._backend = None 28 | 29 | def init(self, runtime_env: Dict[str, Any]): 30 | """Initialize the Ray backend with the given runtime environment. 31 | 32 | Args: 33 | runtime_env (Dict[str, Any]): The runtime environment to use for Ray. 34 | """ 35 | self.backend.init(runtime_env=runtime_env) 36 | 37 | def get(self, obj: ObjectRef) -> Any: 38 | """Get the result of a Ray task or a list of Ray tasks. 39 | 40 | Ignores any output to stderr. 41 | 42 | Args: 43 | obj (ObjectRef): The Ray task or list of Ray tasks. 44 | 45 | Returns: 46 | Union[Any, List[Any]]: The result of the Ray task or list of Ray tasks. 47 | """ 48 | with ( 49 | open(os.devnull, "w", encoding="utf-8") as devnull, 50 | redirect_stderr(devnull), 51 | ): 52 | return self.backend.get(obj) 53 | 54 | def put(self, obj: Any) -> ObjectRef: 55 | """Put an object into the Ray object store. 56 | 57 | Args: 58 | obj (Any): The object to put into the Ray object store. 59 | 60 | Returns: 61 | ObjectRef: A reference to the object in the Ray object store. 62 | """ 63 | return self.backend.put(obj) 64 | 65 | def get_actor(self, name: str) -> ActorHandle: 66 | """Get a Ray actor by its name. 67 | 68 | Args: 69 | name (str): The name of the Ray actor. 70 | 71 | Returns: 72 | ActorHandle: The Ray actor handle. 73 | """ 74 | return self.backend.get_actor(name) 75 | 76 | def remote(self, fn_or_class): 77 | """Handle the remote execution of a function or class. 78 | 79 | Args: 80 | fn_or_class: The function or class to execute remotely. 81 | 82 | Returns: 83 | ObjectRef: A reference to the remote execution result. 84 | """ 85 | return self.backend.remote(fn_or_class) 86 | 87 | @property 88 | def backend(self): 89 | """Get the Ray backend. 90 | 91 | If the backend is not initialised, it will attempt to import Ray and set it as the backend. 92 | 93 | Returns: 94 | Any: The Ray backend. 95 | 96 | Raises: 97 | ImportError: If Ray cannot be imported, indicating that 98 | the required dependencies are missing. 99 | """ 100 | if self._backend is None: 101 | import ray # pylint: disable=import-outside-toplevel 102 | 103 | self._backend = ray 104 | return self._backend 105 | 106 | 107 | ray_backend = RayInterface() 108 | -------------------------------------------------------------------------------- /eclypse/graph/assets/additive.py: -------------------------------------------------------------------------------- 1 | """Module for the AdditiveAsset class. 2 | 3 | It represents a numeric asset where the aggregation is the sum of the assets. 4 | It provides the interface for the basic algebraic functions between assets: 5 | 6 | - `aggregate`: Aggregate the assets into a single asset via summation. 7 | - `satisfies`: Check if the asset contains another asset. 8 | - `is_consistent`: Check if the asset belongs to the interval [lower_bound, upper_bound]. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | from typing import ( 14 | TYPE_CHECKING, 15 | Any, 16 | Callable, 17 | Optional, 18 | Union, 19 | ) 20 | 21 | from .asset import Asset 22 | 23 | if TYPE_CHECKING: 24 | from eclypse.utils.types import PrimitiveType 25 | 26 | from .space import AssetSpace 27 | 28 | 29 | class Additive(Asset): 30 | """AdditiveAsset represents a numeric asset where the aggregation is additive.""" 31 | 32 | def __init__( 33 | self, 34 | lower_bound: float, 35 | upper_bound: float, 36 | init_fn_or_value: Optional[ 37 | Union[PrimitiveType, AssetSpace, Callable[[], Any]] 38 | ] = None, 39 | functional: bool = True, 40 | ): 41 | """Create a new Additive asset. 42 | 43 | Args: 44 | lower_bound (float): The lower bound of the asset. 45 | upper_bound (float): The upper bound of the asset. 46 | init_fn_or_value (Optional[Union[PrimitiveType, AssetSpace, Callable[[], Any]]]): 47 | The function to initialize the asset. It can be a primitive type, a 48 | callable with no arguments or an `AssetSpace` object. If it is not 49 | provided, the asset will be initialized with the lower bound. 50 | Defaults to None. 51 | functional (bool, optional): If True, the asset is functional. Defaults to 52 | True. 53 | 54 | Raises: 55 | ValueError: If $lower_bound > upper_bound$. 56 | """ 57 | super().__init__( 58 | lower_bound=lower_bound, 59 | upper_bound=upper_bound, 60 | init_fn_or_value=init_fn_or_value, 61 | functional=functional, 62 | ) 63 | 64 | def aggregate(self, *assets: float) -> float: 65 | """Aggregate the assets into a single asset via summation. 66 | 67 | Args: 68 | assets (Iterable[float]): The assets to aggregate. 69 | 70 | Returns: 71 | float: The aggregated asset. 72 | """ 73 | return sum(assets, start=self.lower_bound) 74 | 75 | def satisfies(self, asset: float, constraint: float) -> bool: 76 | """Check `asset` contains `constraint`. 77 | 78 | In an additive asset, the higher value contains the lower value. 79 | 80 | Args: 81 | asset (float): The "container" asset. 82 | constraint (float): The "contained" asset. 83 | 84 | Returns: 85 | True if asset >= constraint, False otherwise. 86 | """ 87 | return asset >= constraint 88 | 89 | def is_consistent(self, asset: float) -> bool: 90 | """Check if the asset belongs to the interval [lower_bound, upper_bound]. 91 | 92 | Args: 93 | asset (float): The asset to be checked. 94 | 95 | Returns: 96 | True if lower_bound <= asset <= upper_bound, False otherwise. 97 | """ 98 | return self.lower_bound <= asset <= self.upper_bound 99 | -------------------------------------------------------------------------------- /eclypse/remote/communication/rest/http_request.py: -------------------------------------------------------------------------------- 1 | """Module for the HTTPRequest class. 2 | 3 | It is used to send and receive data between services that 4 | communicate using the REST communication interface. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from typing import ( 10 | TYPE_CHECKING, 11 | Any, 12 | Dict, 13 | Generator, 14 | Optional, 15 | Tuple, 16 | ) 17 | 18 | from eclypse.remote.communication import EclypseRequest 19 | 20 | if TYPE_CHECKING: 21 | from eclypse.remote.communication import Route 22 | 23 | from .codes import HTTPStatusCode 24 | from .interface import EclypseREST 25 | from .methods import HTTPMethod 26 | 27 | 28 | class HTTPRequest(EclypseRequest): 29 | """HTTPRequest class. 30 | 31 | An HTTP request is used to send and receive data between services in the 32 | same application, using the REST communication protocol. 33 | """ 34 | 35 | def __init__( 36 | self, 37 | url: str, 38 | method: HTTPMethod, 39 | data: Dict[Any, Any], 40 | _rest: EclypseREST, 41 | ): 42 | """Initializes an HTTPRequest object. 43 | 44 | Args: 45 | url (str): The URL of the request. 46 | method (HTTPMethod): The HTTP method of the request. 47 | data (Dict[Any, Any]): The data to send in the request. 48 | _rest (EclypseREST): The REST interface used to send the request. 49 | """ 50 | recipient_id = url.split("/")[0] 51 | data["url"] = url 52 | data["method"] = method 53 | 54 | super().__init__( 55 | recipient_ids=[recipient_id], 56 | data=data, 57 | _comm=_rest, 58 | ) 59 | 60 | def __await__(self) -> Generator[Any, None, HTTPRequest]: 61 | """Await the request to complete. 62 | 63 | Returns: 64 | Awaitable: The result of the request. 65 | """ 66 | return super().__await__() # type: ignore[return-value] 67 | 68 | @property 69 | def route(self) -> Optional[Route]: 70 | """Get the route of the request. 71 | 72 | Returns: 73 | Optional[Route]: The route of the request. 74 | """ 75 | return self.routes[0] 76 | 77 | @property 78 | def response(self) -> Optional[Tuple[HTTPStatusCode, Dict[str, Any]]]: 79 | """Get the response of the request. 80 | 81 | Returns: 82 | Tuple[HTTPStatusCode, Dict[str, Any]]: The response of the request. 83 | """ 84 | return self.responses[0] 85 | 86 | @property 87 | def status_code(self) -> HTTPStatusCode: 88 | """Get the status code of the response. 89 | 90 | Returns: 91 | HTTPStatusCode: The status code of the response. 92 | 93 | Raises: 94 | RuntimeError: If the request is not completed yet. 95 | """ 96 | if self.response is None: 97 | raise RuntimeError("Request not completed yet") 98 | return self.response[0] 99 | 100 | @property 101 | def body(self) -> Dict[str, Any]: 102 | """Get the body of the response. 103 | 104 | Returns: 105 | Dict[str, Any]: The body of the response. 106 | 107 | Raises: 108 | RuntimeError: If the request is not completed yet. 109 | """ 110 | if self.response is None: 111 | raise RuntimeError("Request not completed yet") 112 | return self.response[1] 113 | -------------------------------------------------------------------------------- /examples/grid_analysis/infrastructure.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import ( 4 | Any, 5 | Callable, 6 | Optional, 7 | Union, 8 | ) 9 | 10 | from policies import ( 11 | degrade_policy, 12 | kill_policy, 13 | ) 14 | 15 | from eclypse.builders.infrastructure import ( 16 | hierarchical, 17 | random, 18 | star, 19 | ) 20 | from eclypse.graph import Infrastructure 21 | from eclypse.graph.assets import Concave 22 | from eclypse.graph.assets.space import ( 23 | AssetSpace, 24 | Choice, 25 | ) 26 | from eclypse.utils.constants import ( 27 | MAX_FLOAT, 28 | MIN_FLOAT, 29 | ) 30 | from eclypse.utils.types import PrimitiveType 31 | 32 | 33 | def get_infrastructure(config) -> Infrastructure: 34 | node_update_policy, link_update_policy = get_policy(config) 35 | common_config = { 36 | "node_update_policy": node_update_policy, 37 | "link_update_policy": link_update_policy, 38 | "resource_init": "max", 39 | "symmetric": True, 40 | "seed": config["seed"], 41 | "node_assets": {"energy": idle_energy_consumption()}, 42 | "include_default_assets": True, 43 | } 44 | if config["topology"][0] == "star": 45 | infr = star( 46 | infrastructure_id="star", 47 | n_clients=config["nodes"], 48 | **common_config, 49 | ) 50 | elif config["topology"][0] == "random": 51 | infr = random( 52 | infrastructure_id="random", 53 | n=config["nodes"], 54 | p=config["topology"][1], 55 | **common_config, 56 | ) 57 | elif config["topology"][0] == "hierarchical": 58 | infr = hierarchical( 59 | infrastructure_id="hierarchical", 60 | n=config["nodes"], 61 | **common_config, 62 | ) 63 | else: 64 | raise ValueError(f"Unknown topology {config['topology']}") 65 | 66 | apply_load(infr, config["load"]) 67 | return infr 68 | 69 | 70 | def get_policy(config): 71 | if config["policy"][0] == "degrade": 72 | return degrade_policy(config["policy"][1], config["max_steps"]) 73 | return kill_policy(config["policy"][1]) 74 | 75 | 76 | def apply_load(infr: Infrastructure, load: float): 77 | if load != 0: 78 | for _, attr in infr.nodes(data=True): 79 | for key in ["cpu", "gpu", "ram", "storage"]: 80 | attr[key] = int(attr[key] * (1 - load)) 81 | 82 | for _, _, attr in infr.edges(data=True): 83 | for key in ["bandwidth"]: 84 | attr[key] = attr[key] * (1 - load) 85 | 86 | 87 | def idle_energy_consumption( 88 | lower_bound: float = MAX_FLOAT, 89 | upper_bound: float = MIN_FLOAT, 90 | init_fn_or_value: Optional[ 91 | Union[PrimitiveType, AssetSpace, Callable[[], Any]] 92 | ] = None, 93 | ) -> Concave: 94 | """Create a new additive asset for idle energy consumption. 95 | Args: 96 | lower_bound (float): The lower bound of the asset. 97 | upper_bound (float): The upper bound of the asset. 98 | init_fn_or_value (Optional[Union[PrimitiveType, AssetSpace, Callable[[], Any]]]): 99 | The function/scalar to initialize the idle energy consumption value. 100 | 101 | Returns: 102 | Concave: The idle energy consumption asset. 103 | """ 104 | _init_fn = ( 105 | Choice([20, 50, 80, 150]) if init_fn_or_value is None else init_fn_or_value 106 | ) 107 | return Concave(lower_bound, upper_bound, _init_fn, functional=False) 108 | -------------------------------------------------------------------------------- /examples/grid_analysis/applications/vas.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Callable, 3 | Dict, 4 | Optional, 5 | ) 6 | 7 | from networkx.classes.reportviews import ( 8 | EdgeView, 9 | NodeView, 10 | ) 11 | 12 | from eclypse.graph import Application 13 | from eclypse.graph.assets import Asset 14 | 15 | 16 | def get_vas( 17 | application_id: str = "VAS", 18 | node_update_policy: Optional[Callable[[NodeView], None]] = None, 19 | edge_update_policy: Optional[Callable[[EdgeView], None]] = None, 20 | node_assets: Optional[Dict[str, Asset]] = None, 21 | edge_assets: Optional[Dict[str, Asset]] = None, 22 | include_default_assets: bool = True, 23 | requirement_init: str = "min", 24 | seed: Optional[int] = None, 25 | ) -> Application: 26 | """Get the Video Analytics Serving (VAS) application.""" 27 | 28 | flows = [ 29 | [ 30 | "ObjectDetectionService", 31 | "ObjectTrackingService", 32 | "ObjectClassificationService", 33 | ], # Object processing flow 34 | [ 35 | "ObjectDetectionService", 36 | "ObjectCountingService", 37 | ], # Detection and Counting 38 | [ 39 | "AudioDetectionService", 40 | "ObjectClassificationService", 41 | ], # Audio-visual processing 42 | ] 43 | 44 | app = Application( 45 | application_id=application_id, 46 | node_update_policy=node_update_policy, 47 | edge_update_policy=edge_update_policy, 48 | node_assets=node_assets, 49 | edge_assets=edge_assets, 50 | include_default_assets=include_default_assets, 51 | requirement_init=requirement_init, 52 | flows=flows, 53 | seed=seed, 54 | ) 55 | 56 | app.add_node( 57 | "ObjectDetectionService", 58 | cpu=2, 59 | ram=4.0, 60 | storage=2.0, 61 | gpu=2, 62 | availability=0.95, 63 | processing_time=15, 64 | ) 65 | app.add_node( 66 | "ObjectTrackingService", 67 | cpu=2, 68 | ram=3.5, 69 | storage=1.5, 70 | gpu=1.5, 71 | availability=0.94, 72 | processing_time=20, 73 | ) 74 | app.add_node( 75 | "ObjectClassificationService", 76 | cpu=2, 77 | ram=4.0, 78 | storage=2.0, 79 | gpu=2, 80 | availability=0.95, 81 | processing_time=25, 82 | ) 83 | app.add_node( 84 | "ObjectCountingService", 85 | cpu=1, 86 | ram=2.0, 87 | storage=1.0, 88 | gpu=1, 89 | availability=0.90, 90 | processing_time=10, 91 | ) 92 | app.add_node( 93 | "AudioDetectionService", 94 | cpu=1, 95 | ram=1.5, 96 | storage=0.75, 97 | gpu=0.5, 98 | availability=0.90, 99 | processing_time=8, 100 | ) 101 | 102 | app.add_edge( 103 | "ObjectDetectionService", 104 | "ObjectTrackingService", 105 | symmetric=True, 106 | latency=30, 107 | bandwidth=10, 108 | ) 109 | app.add_edge( 110 | "ObjectTrackingService", 111 | "ObjectClassificationService", 112 | symmetric=True, 113 | latency=50, 114 | bandwidth=15, 115 | ) 116 | app.add_edge( 117 | "ObjectDetectionService", 118 | "ObjectCountingService", 119 | symmetric=True, 120 | latency=30, 121 | bandwidth=8, 122 | ) 123 | app.add_edge( 124 | "AudioDetectionService", 125 | "ObjectClassificationService", 126 | symmetric=True, 127 | latency=40, 128 | bandwidth=10, 129 | ) 130 | 131 | return app 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | eclypse-logo 4 | 5 |

6 | 7 | ![PyPI - Version](https://img.shields.io/pypi/v/eclypse) 8 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/eclypse) 9 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&)](https://github.com/pre-commit/pre-commit) 10 | 11 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 12 | [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) 13 | [![Checked with pylint](https://img.shields.io/badge/pylint-10/10-green)](https://pylint.pycqa.org/en/latest/) 14 | [![Import sorted with isort](https://img.shields.io/badge/isort-checked-brightgreen)](https://pycqa.github.io/isort/) 15 | [![Import cleaned with pycln](https://img.shields.io/badge/pycln-checked-brightgreen)](https://github.com/hadialqattan/pycln) 16 | [![Code style: black](https://img.shields.io/badge/code%20style-black-black)](https://github.com/psf/black) 17 | [![Doc style: docformatter](https://img.shields.io/badge/doc%20style-docformatter-black)](https://github.com/PyCQA/docformatter) 18 | 19 | **ECLYPSE** (**E**dge-**CL**oud p**Y**thon **P**latform for **S**imulated runtime **E**nvironments) is the first simulation library entirely written in Python, for experimenting with deployment strategies in varying infrastructure conditions. It provides an interface to simulate deployments of service-based applications onto life-like infrastructures, without and with an actual application implementation to be deployed. 20 | 21 | ## Installation 22 | 23 | To install ECLYPSE and all its dependencies, you can run the following commands: 24 | ```bash 25 | 26 | pip install eclypse 27 | 28 | ``` 29 | 30 | **N.B.** We **strongly** suggest the installation of ECLYPSE in a virtual environment. 31 | 32 | ## Documentation 33 | 34 | The documentation for ECLYPSE can be found [here](https://eclypse.readthedocs.io/en/latest/). 35 | 36 | ## Publication 37 | 38 | ECLYPSE is described and assessed in: 39 | 40 | > Jacopo Massa, Valerio De Caro, Stefano Forti, Patrizio Dazzi, Davide Bacciu, Antonio Brogi
41 | > [**ECLYPSE: a Python Framework for Simulation and Emulation of the Cloud-Edge Continuum**](https://arxiv.org/abs/2501.17126),
42 | > arXiv preprint arXiv:2501.17126, 2025. 43 | 44 | If you want to cite ECLYPSE in your work, you can use the following BibTeX entry: 45 | ```bibtex 46 | @misc{massa2025eclypse, 47 | title = {{ECLYPSE: a Python Framework for Simulation and Emulation of the Cloud-Edge Continuum}}, 48 | author = {Jacopo Massa and Valerio De Caro and Stefano Forti and Patrizio Dazzi and Davide Bacciu and Antonio Brogi}, 49 | year = {2025}, 50 | eprint = {2501.17126}, 51 | archivePrefix = {arXiv}, 52 | primaryClass = {cs.NI}, 53 | url = {https://arxiv.org/abs/2501.17126}, 54 | } 55 | ``` 56 | 57 | ## Authors 58 | 59 | - **Jacopo Massa** 60 | - Website: [https://pages.di.unipi.it/massa](https://pages.di.unipi.it/massa) 61 | - GitHub: [jacopo-massa](https://github.com/jacopo-massa) 62 | - **Valerio De Caro** 63 | - Website: [https://vdecaro.github.io](https://vdecaro.github.io) 64 | - GitHub: [vdecaro](https://github.com/vdecaro) 65 | 66 | ## Contact Us 67 | If you want to get in touch with us, [drop us an e-mail](mailto:jacopo.massa@di.unipi.it,valerio.decaro@di.unipi.it?subject=[ECLYPSE]%20Request%20for%20information)! 68 | -------------------------------------------------------------------------------- /eclypse/workflow/event/defaults.py: -------------------------------------------------------------------------------- 1 | """Default events to be managed by the ECLYPSE simulator.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import ( 6 | List, 7 | Optional, 8 | ) 9 | 10 | from eclypse.workflow.event import EclypseEvent 11 | from eclypse.workflow.trigger import CascadeTrigger 12 | 13 | 14 | class StartEvent(EclypseEvent): 15 | """The start is the beginning of the simulation.""" 16 | 17 | def __init__(self): 18 | """Initialize the start event.""" 19 | super().__init__( 20 | name="start", 21 | verbose=True, 22 | ) 23 | 24 | def __call__(self): 25 | """Empty by default.""" 26 | 27 | 28 | class EnactEvent(EclypseEvent): 29 | """The EnactEvent represents the enactment phase of the simulation. 30 | 31 | The enact is the actuation of the placement decisions made by the placement 32 | algorithms. 33 | """ 34 | 35 | def __init__(self): 36 | """Initialize the enact event.""" 37 | super().__init__( 38 | name="enact", 39 | verbose=True, 40 | ) 41 | 42 | def __call__(self, _: Optional[EclypseEvent] = None): 43 | """Enact placement decisions. 44 | 45 | It calls the audit and enact methods of the simulator. 46 | """ 47 | self.simulator.audit() 48 | self.simulator.enact() 49 | 50 | 51 | class StepEvent(EclypseEvent): 52 | """The StepEvent represents a simulation step. 53 | 54 | The step is the phase of the simulation where the applications and infrastructure 55 | are updated, according to the given update policies. 56 | """ 57 | 58 | def __init__(self): 59 | """Initialize the step event.""" 60 | super().__init__( 61 | name="step", 62 | triggers=[CascadeTrigger("enact")], 63 | verbose=True, 64 | ) 65 | 66 | def __call__(self, _: EclypseEvent): 67 | """Update applications and infrastructure.""" 68 | for app in self.simulator.applications.values(): 69 | if app.is_dynamic: 70 | app.evolve() 71 | 72 | if self.simulator.infrastructure.is_dynamic: 73 | self.simulator.infrastructure.evolve() 74 | 75 | 76 | class StopEvent(EclypseEvent): 77 | """The stop is the end of the simulation. 78 | 79 | Its triggers are set after the SimulationConfig is created, so it can be triggered 80 | by the 'StepEvent', according to the parameters defined in the configuration. 81 | """ 82 | 83 | def __init__(self): 84 | """Initialize the stop event.""" 85 | super().__init__( 86 | name="stop", 87 | verbose=True, 88 | ) 89 | 90 | def __call__(self, _: Optional[EclypseEvent] = None): 91 | """Empty by default.""" 92 | 93 | 94 | def get_default_events(user_events: List[EclypseEvent]) -> List[EclypseEvent]: 95 | """Returns the default events to be managed by the ECLYPSE simulator. 96 | 97 | Events are: 98 | 'start', 'stop', 'step', and 'enact'. If the user has defined an event with the same 99 | name as one of the default events, the default event is overridden. 100 | 101 | Args: 102 | user_events (List[EclypseEvent]): The user-defined events. 103 | 104 | Returns: 105 | List[EclypseEvent]: The default events. 106 | """ 107 | user_event_names = [event.name for event in user_events] 108 | return list( 109 | filter( 110 | lambda x: x.name not in user_event_names, 111 | [ 112 | StartEvent(), 113 | EnactEvent(), 114 | StepEvent(), 115 | StopEvent(), 116 | ], 117 | ) 118 | ) 119 | -------------------------------------------------------------------------------- /docs/_templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ECLYPSE Documentation 5 | 6 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 |
27 | 28 | 50 | 51 |
52 |
53 | Entirely written in Python 54 | Accessible and adaptable for a wide range of users. 55 |
56 |
57 | Easy to use 58 | Intuitive interface for seamless experimentation and analysis. 59 |
60 |
61 | Actual implementation of services 62 | Simulate real-world scenarios with precise deployment strategies. 63 |
64 |
65 | Placement Strategies & Update Policies 66 | Tailor context-aware simulations for specific needs. 67 |
68 |
69 | Metrics & Reporting 70 | Gain insights into performance through various formats. 71 |
72 |
73 | Logging capabilities 74 | Track and analyze activities for troubleshooting and optimization. 75 |
76 |
77 | 78 | 79 | -------------------------------------------------------------------------------- /eclypse/utils/tools.py: -------------------------------------------------------------------------------- 1 | """Module containing utility functions used throughout the ECLYPSE package.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | from sys import getsizeof 7 | from typing import ( 8 | TYPE_CHECKING, 9 | Any, 10 | Callable, 11 | Optional, 12 | ) 13 | 14 | from . import constants 15 | 16 | if TYPE_CHECKING: 17 | from eclypse.graph.assets import AssetBucket 18 | from eclypse.simulation import Simulation 19 | 20 | 21 | def get_bytes_size(d: Any) -> int: 22 | """Returns the size of an object in bytes. 23 | 24 | The size is computed according to the following rules: 25 | 26 | - int, float, str, bool: the size is the size of the object itself. 27 | - list, tuple, set: the size is the sum of the sizes of the elements in the collection. 28 | - dict: the size is the sum of the sizes of the keys and values in the dictionary. 29 | - objects with a __dict__ attribute: the size is the size of the __dict__ attribute. 30 | - other objects: the size is the size of the object itself, computed using `sys.getsizeof`. 31 | 32 | Args: 33 | d (Any): The object to be measured. 34 | 35 | Returns: 36 | int: The size of the object in bytes. 37 | """ 38 | if isinstance(d, (int, float, str, bool)): 39 | return getsizeof(d) 40 | if isinstance(d, (list, tuple, set)): 41 | return sum(get_bytes_size(e) for e in d) 42 | if isinstance(d, dict): 43 | return sum(get_bytes_size(k) + get_bytes_size(v) for k, v in d.items()) 44 | if hasattr(d, "__dict__"): 45 | return get_bytes_size(d.__dict__) 46 | return getsizeof(d) 47 | 48 | 49 | def get_constant(name: str) -> Any: 50 | """Get the value of a constant in the `constants` module, given its name. 51 | 52 | Args: 53 | name (str): The name of the constant to retrieve 54 | 55 | Returns: 56 | Any: The value of the constant 57 | """ 58 | return getattr(constants, name) 59 | 60 | 61 | def camel_to_snake(s: str) -> str: 62 | """Convert a CamelCase string to a snake_case string. 63 | 64 | .. code-block:: python 65 | 66 | s = "MyCamelCaseSentence" 67 | print(camel_to_snake(s)) # my_camel_case_sentence 68 | 69 | Args: 70 | s (str): The CamelCase string to convert. 71 | 72 | Returns: 73 | str: The snake_case string. 74 | """ 75 | s = re.sub(r"(? Optional[Callable]: 109 | simulation: Simulation = args[0] 110 | try: 111 | return func(*args, **kwargs) 112 | except KeyboardInterrupt: 113 | simulation.logger.warning("SIMULATION INTERRUPTED.") 114 | simulation.stop() 115 | return None 116 | 117 | return wrapper 118 | 119 | 120 | __all__ = [ 121 | "camel_to_snake", 122 | "get_bytes_size", 123 | "get_constant", 124 | "shield_interrupt", 125 | ] 126 | --------------------------------------------------------------------------------