From e0be72b1fde3d6bc0360bd1a1b30730072275566 Mon Sep 17 00:00:00 2001 From: shinedday <shinedday@gmail.com> Date: Tue, 31 May 2022 10:25:33 +0200 Subject: [PATCH] Version : IoTAMAK 0.0.5 Amas : * Rework the way to add agents to give more option to user, can now choose multiple agents and choose agent destination From : add_agent(self, experience_name: str, args: List = None) To : add_agent(self, experience_name: str, client_ip: str = None, agent_name: str = "agent.py", args: List = None) param experience_name: name of the experience folder param client_ip: if the agent should be created in a specific device, you can specify an ip address, otherwise the Amas will try to share the work between the devices param agent_name: if using multiple kind of agent, you can specify the relative path in the experiment directory to the agent file to use param args: if any argument is needed to initiate the new agent Added Mail class, that can be used to communicate between agents, a mail contain the sender id, the cycle of the mail creation as well as a payload that can be anything serializable. Added CommunicatingAgent class, that can be used instead of agent * A mailbox that work like a stack of mail * A method to send mails * And a method de receaive them Scheduler : * Fixed a bug where the mqtt client would lock himself making communication with the scheduler impossible --- .gitignore | 1 + README.md | 480 +++++++++++++++++++--------- dist/iotAmak-0.0.5-py3-none-any.whl | Bin 0 -> 12870 bytes iotAmak/amas.py | 44 ++- iotAmak/communicating_agent.py | 37 +++ iotAmak/scheduler.py | 1 - iotAmak/tool/mail.py | 10 + setup.py | 2 +- 8 files changed, 408 insertions(+), 167 deletions(-) create mode 100644 dist/iotAmak-0.0.5-py3-none-any.whl create mode 100644 iotAmak/communicating_agent.py create mode 100644 iotAmak/tool/mail.py diff --git a/.gitignore b/.gitignore index af6a8da..67da076 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ venv __pycache__/ build/ iotAmak.egg-info/ +README.html diff --git a/README.md b/README.md index 4847251..ac081d1 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,92 @@ -# IoTAMAK-core +# **IoTAMAK : a framework for distributed MAS** -- [IoTAMAK-core](#iotamak-core) -- [Reseau](#reseau) -- [Random note](#random-note) -- [Agent Cycle](#agent-cycle) -- [Shared work](#shared-work) -- [Wait function](#wait-function) -- [Experience format](#experience-format) +**Name :** Sebastien GOYON +**Group :** M1 CSA 2021-2022, UT3 Paul Sabatier +**Internship tutor :** Guilhem MARCILLAUD, IRIT SMAC team. -# Reseau +- [**IoTAMAK : a framework for distributed MAS**](#iotamak--a-framework-for-distributed-mas) + - [**Definition**](#definition) + - [**Goal**](#goal) + - [**Subject**](#subject) + - [**Context**](#context) + - [**Constraint**](#constraint) + - [**Solution**](#solution) + - [**Project managment**](#project-managment) + - [**Development**](#development) + - [**IoTAMAK core**](#iotamak-core) + - [**Code structure** :](#code-structure-) + - [**MAS behavious**](#mas-behavious) + - [**Version**](#version) + - [**IoTAMAK UI**](#iotamak-ui) + - [**Network**](#network) + - [**Raspberry network**](#raspberry-network) + - [**Server network**](#server-network) + - [**Bibliography**](#bibliography) -Port : - * Mqtt : 1883 - * SSH : 22 -**Mqtt:** - * Broker : Mosquitto (Windows) - * Client : python paho-mqtt +## **Definition** +MAS (multi-agent system): +> A multi-agent system (MAS or "self-organized system") is a computerized system composed of multiple interacting intelligent agents. +> Multi-agent systems can solve problems that are difficult or impossible for an individual agent or a monolithic system to solve. +> Intelligence may include methodic, functional, procedural approaches, algorithmic search or reinforcement learning. -**Configuration windows :** -WSL (pour pouvoir démarer l'amas) +## **Goal** +### **Subject** -# Random note +> The goal of this project is to produce a wab-app to supervise remotly experiments distributed on multiple device for researcher in MAS of the IRIT. The application need to give in real time information to the user of the state of each devise / agent (activity, result..). The general architecture of the system is in 3 part : +> 1. Some devises that can work (Raspberry pi 3b) +> 2. A control device collecting information (server) +> 3. An application that can show the server information according to the user need. +> +> This project will be focused on the devedloppement of a system that can distribute a SMA defnie by AMAK on multiple raspberry PI. Once distributed, the server can start and stop the experiment. The 2nd goal of this project is to build a network architecure between those raspberry so each Agent can communicate between them and with the manager. -ihm : - 1. Loading (check if good format) -> Warning -> can't "start" - 2. Ping (all in config) -> Warning -> can't start, can't agent - 3. Agent -> Warning -> already running -> kill ? - 4. Whatever (start) +### **Context** +Last year I participated in the conception and development of Py-AMAK (Python) during my internship, an extention of AMAK (Java) a framework that helps developper to produce MAS system and visualize the result. -Start procédure : - 1. start broker - 2. start scheduler - 3. Start amas/env +This year the goal of the intership is to developp IoTAMAK, that will extend the application of Py-AMAK to use network and multiple device. -```mermaid -flowchart TD - - subgraph server - amas - scheduler - end - - client - - amas <-->|mqtt| client - scheduler <-->|mqtt| client -``` - -```mermaid -flowchart TD - - server -->|create n agent| remote_servers - -``` - -Agent neighboor: - -add neighboor : --> subscribe to his metrics - -remove neighboor : --> unsuscribe to his metrics - -on_perceive -> update neighbors -then wait -then send metrics -then phase 2 - -# Agent Cycle -```mermaid -flowchart TD - - on_cycle_begin - - subgraph on_perceive - find_neighbor - subscribe_neighbor_metric - find_neighbor --> subscribe_neighbor_metric - end - - on_cycle_begin --> on_perceive - - subgraph twophase - wait - end - - subgraph onephasee - nothing - end - - on_perceive -.-> twophase - on_perceive -.-> onephasee - - twophase -.-> on_decide - onephasee -.-> on_decide - - on_cycle_en[on_cycle_end] - - on_decide --> on_act - on_act --> publish_metric - publish_metric --> on_cycle_en - -``` -`find_neighbor` : -> pas d'info de base ? Comment trouver les voisins - -Solution : - * Tout le monde accepte toute les metrics mais ne regarde que ces voisins - * L'amas sait tout en distribue les données - * ? +Notable diference between IoTAMAK and Py-AMAK on the interface : + * The structure need to be done in a more pythonic way (no getter/setter, no public/private attribute) + * The UI need to be a web app powered by a server +### **Constraint** -Amas sait tout, il s'abonne aux metrics, calcules les voisins, et les envois aux agents. -Les agents stock les données et les lisent dans on_perceive +**Language :** Python -# Shared work +The system need to follow MVC design patern, this mean that the core could work on his own, and a UI is not require. - 1. Python multiprocessing - 2. Envoie du fichier par ssh - 3. execution +## **Solution** -Probleme : - * 1 session ssh par agent - * Souci de dépendance - * Pas élégant +The project will be splited into 2 parts : + * IoTAMAK-core : a python module (that can be easily installed with pip) that provide all the basic method required to build a MAS experiment + * IoTAMAK UI : a django based server that can interact with any experiment developped with IoTAMAK-core. -Utiliser fabrique pour partager les fichier +## **Project managment** -Communication : pexpect -> linux only (wsl work) +Meeting : weekly -# Wait function +Tool : + * Gitlab + * Trello + * Discord -```py -while not condition: - pass -``` -15 agents -> 43 sec/cycle +## **Development** +### **IoTAMAK core** -```py -while not condition: - sleep(0.01) -``` -15 agents -> 0.09 sec/cycle +The classes that the developper will interact with (Agent / Amas / Environment) need to be very similar to the Py-AMAK one, this is why the class looks very similar. -Solution a trouvé ! +#### **Code structure** : -# Experience format - -``` -/experienceNameFolder/ - /agent.py - /amas.py - /env.py - /... -``` - -Les noms amas, agent et env sont obligatoire et ne peuvent pas etre modifier - -Ils doivent contenir un code de la sorte : -```py -if __name__ == '__main__': - a = MyAgent(int(sys.argv[1]), ...) #MyAmas() MyEnv() - a.run() -``` - -Pour ajouter des attributs a l'initialisation aux agents il suffit de rajouter des `type(sys.argv[x])`. - -**A AJOUTER** -un .config dans les experience avec la version du coeur - -# diagrame de classe +In AMAK a SMA is composed of 4 main strucure : + * Scheduler : it's role is to make sure that everything is working in the right order, it's also play a huge role with the interface between the UI and the experiment, being able to pause it, or closing everyting. + * Agent : an agent can have various comportment that need to be define + * Amas : it's a superstructure, above the agents that provide conviniant way to add, remove or make communication between agents + * Environment : a place where the agent live. ```mermaid classDiagram @@ -245,4 +156,267 @@ classDiagram Schedulable <|-- Env Schedulable <|-- Amas SSHClient <|-- Amas -``` \ No newline at end of file +``` + +#### **MAS behavious** + +```mermaid +graph TD + + subgraph first_part: + amas_metrics(Amas : publish all stored metrics to database) + amas_begin(Amas : on_cycle_begin) + amas_not(Amas : notify scheduler) + env_begin(Env : on_cycle_begin) + env_not(Env : notify scheduler) + + amas_metrics --> amas_begin + amas_begin --> amas_not + env_begin --> env_not + end + + subgraph main_part: + agent1_begin(Agent 1 : on_cycle_begin) + agent1_perceive(Agent 1 : on_perceive) + agent1_decide(Agent 1 : on_decide) + agent1_act(Agent 1 : on_act) + agent1_finish(Agent 1 : on_cycle_end) + agent1_metric(Agent 1 : send metrics) + agent1_sche(Agent 1 : notify scheduler) + + agent1_begin --> agent1_perceive + agent1_perceive --> agent1_decide + agent1_decide --> agent1_act + agent1_act --> agent1_finish + agent1_finish --> agent1_metric + agent1_metric --> agent1_sche + + agent_mid(...) + + agentn_begin(Agent n : on_cycle_begin) + agentn_perceive(Agent n : on_perceive) + agentn_decide(Agent n : on_decide) + agentn_act(Agent n : on_act) + agentn_finish(Agent n : on_cycle_end) + agentn_metric(Agent n : send metrics) + agentn_sche(Agent n : notify scheduler) + + agentn_begin --> agentn_perceive + agentn_perceive --> agentn_decide + agentn_decide --> agentn_act + agentn_act --> agentn_finish + agentn_finish --> agentn_metric + agentn_metric --> agentn_sche + + end + + subgraph last_part: + amas_finish(Amas : on_cycle_end) + amas_noti(Amas : notify scheduler) + env_finish(Env : on_cycle_end) + env_noti(Env : notify scheduler) + + amas_finish --> amas_noti + env_finish --> env_noti + end + + scheduler_not_first(Scheduler : Notify Amas/env) + scheduler_wait_first(Scheduler : Wait Amas/env) + scheduler_not_main(Scheduler : Notify all Agents) + scheduler_wait_main(Scheduler : Wait all agents) + scheduler_not_last(Scheduler : Notify Amas/env) + scheduler_wait_last(Scheduler : Wait Amas/env) + + + scheduler_not_first --> amas_metrics + scheduler_not_first --> env_begin + amas_not --> scheduler_wait_first + env_not --> scheduler_wait_first + + scheduler_wait_first --> scheduler_not_main + + scheduler_not_main --> agent1_begin + scheduler_not_main --> agent_mid + scheduler_not_main --> agentn_begin + agent1_sche --> scheduler_wait_main + agent_mid --> scheduler_wait_main + agentn_sche --> scheduler_wait_main + + scheduler_wait_main --> scheduler_not_last + + scheduler_not_last --> amas_finish + scheduler_not_last --> env_finish + env_noti --> scheduler_wait_last + amas_noti --> scheduler_wait_last +``` + +#### **Version** + +**0.0.1 :** + * Basic implementation of the system + +**0.0.2 :** + +Clean code : + * Provide way better interface for the developper + +Amas : + * Optimize metric publish to the database + +SSH Client : + * will try to connect once again if it have failed + +**0.0.3 :** +Amas : + * Add : method agent_neighbour that publish the metric of agent2 to agent 1 + +Schedulable & Scheduler : + * Greatly improve the wait model, now using threading semaphore + +**0.0.4** +Feature : + * Amas, Env and Agent now take only 1 argument to simplify arg managment for the user. (Not compatible with 0.0.3) + * It's now possible to seed the experiment. + * Scheduler will print exec time + +Requirements.txt / Setup.py : + * add requirement version + +Known bug : + * Scheduler : auto mode pause seem to lock the scheduler in a state where it's not possible to interact anymore with it. + +### **IoTAMAK UI** + +The Web application will be powered by a django server. + + +## **Network** + +In order for the system to work a network architecture need to be conceptualize. + +The communication between agents need to be MQTT. + +There are 3 actore in this system : a server, some user and somre device(raspberry) that execute agents. + +```mermaid + +graph LR + +raspberry_1 +raspberry_n +client((User)) + +git[(Gitlab : iotamak-core)] + +subgraph server + django + broker +end + +broker-- MQTT 1883---raspberry_1 +django-- SSH/SFTP: 22---raspberry_1 +django-- ping---raspberry_1 +broker-- MQTT 1883---raspberry_n +django-- SSH/SFTP: 22---raspberry_n +django-- ping---raspberry_n + +django-- Git : pull --- git +raspberry_n-- Git : pull ---git +raspberry_1-- Git : pull ---git + +client --> django +``` + +### **Raspberry network** +From the point of view of a raspberry the network look like this : +```mermaid +graph LR + +subgraph raspberry + direction TB + os(OS) + client_1 + client_2 + client_n + + os-- nohup/kill---client_1 + os-- nohup/kill---client_2 + os-- nohup/kill---client_n +end + +server((Server)) + +git((Gitlab : iotamak-core)) + +server-- MQTT : 1883---client_1 +server-- MQTT : 1883---client_2 +server-- MQTT : 1883---client_n +server-- ping--- os +server-- SSH/SFTP: 22--- os +git-- Git : pull--- os +``` + +We can see that beside the agent communicate not only with MQTT to the server but also with ssh and sftp in order to share the experiment and control the experiment in case of abnormal beharvior(kill agents for example, or see the error stack). + +### **Server network** + +```mermaid +graph LR + +subgraph Server + direction TB + + subgraph DjangoApp + ihm[Mqtt : IHM] + django[Django server] + + end + + subgraph Experiment + amas[Amas] + env[Environment] + scheduler[Scheduler] + end + + cache((Cache server : Redis)) + broker((Broker : Mosquitto)) + database[(Database : PostgreSQL)] + + django-- Port : 6379 ---cache + + django-- popen / kill ---amas + django-- popen / kill ---env + django-- popen / kill ---scheduler + django-- Port : 5432 ---database + + amas-- MQTT : 1883---broker + env-- MQTT : 1883---broker + scheduler-- MQTT : 1883---broker + ihm-- MQTT : 1883---broker + + +end + +raspberry +git((Gitlab : iotamak-core)) +User + +django-- SSH/SFTP: 22 / ping ---raspberry +django-- Git : pull ---git +amas-- SSH: 22 ---raspberry +broker-- MQTT : 1883---raspberry +User --> django +``` + +The server is composed of multiple componant : + * A MQTT broker (Mosquitto is used here) : to hold the communication between the agent, amas, scheduler, and env. + * A Django application composed of the main application and a MQTT client to ineteract with the experiment + * An experiment, composed of a Scheduler, an environment and an amas. + * A database : since the base Django database (Sqlite3) didn't meet the requirement for the system and external database is require, PoqstgreSQL. + * A cache server Redis : in order to have real time graph and canvas, a cahe server was needed. + +## **Bibliography** + + 1. Smac : https://www.irit.fr/en/departement/dep-interaction-collective-intelligence/smac-team/ + 2. Irit : https://www.irit.fr/en/home/ + 3. MAS definition : https://en.wikipedia.org/wiki/Multi-agent_system diff --git a/dist/iotAmak-0.0.5-py3-none-any.whl b/dist/iotAmak-0.0.5-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..7f2cb73d9fc5e1064103c9bf8b0a4a26fefd98e6 GIT binary patch literal 12870 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 A2LJ#7 literal 0 HcmV?d00001 diff --git a/iotAmak/amas.py b/iotAmak/amas.py index f0e82f4..8d81578 100644 --- a/iotAmak/amas.py +++ b/iotAmak/amas.py @@ -37,14 +37,14 @@ class Amas(Schedulable, SSHClient): SSHClient.__init__(self, true_client) - self.seed = seed + self.seed: int = seed self.broker_ip: str = broker_ip self.subscribe("scheduler/schedulable/wakeup", self.wake_up) self.next_id: int = 0 - self.agents_cmd: List[Cmd] = [] + self.agents_cmd: List[List[Cmd]] = [[] for _ in range(len(self.clients))] self.on_initialization() self.on_initial_agents_creation() @@ -58,10 +58,20 @@ class Amas(Schedulable, SSHClient): """ pass - def add_agent(self, experience_name: str, args: List = None) -> None: + def add_agent( + self, + experience_name: str, + client_ip: str = None, + agent_name: str = "agent.py", + args: List = None + ) -> None: """ Function that need to be called to create a new agent :param experience_name: name of the experience folder + :param client_ip: if the agent should be created in a specific device, you can specify an ip address, + otherwise the Amas will try to share the work between the devices + :param agent_name: if using multiple kind of agent, you can specify the relative path in the experiment + directory to the agent file to use :param args: if any argument is needed to initiate the new agent :return: None """ @@ -71,13 +81,28 @@ class Amas(Schedulable, SSHClient): arg_dict = {"broker_ip": str(self.broker_ip), "seed": self.seed, "identifier": self.next_id} command = "nohup python " - command += "\'Desktop/mqtt_goyon/example/" + experience_name + "/agent.py\' \'" + command += "\'Desktop/mqtt_goyon/example/" + experience_name + "/"+agent_name+"\' \'" command += json.dumps(arg_dict) + "\' " for arg in args: command += str(arg) + " " command += "&" - self.agents_cmd.append(Cmd(command)) + if client_ip is None: + # find the most suitable pi + i_min = 0 + for elem in range(len(self.clients)): + if len(self.agents_cmd[i_min]) > len(self.agents_cmd[elem]): + i_min = elem + self.agents_cmd[i_min].append(Cmd(command)) + else: + have_found = False + for i_client in range(len(self.clients)): + if self.clients[i_client].hostname == i_client: + self.agents_cmd[i_client].append(Cmd(command)) + have_found = True + break + if not have_found: + self.agents_cmd[0].append(Cmd(command)) self.subscribe("agent/" + str(self.next_id) + "/metric", self.agent_metric) self.subscribe("agent/" + str(self.next_id) + "/log", self.agent_log) @@ -89,13 +114,8 @@ class Amas(Schedulable, SSHClient): """ Method used to start new agent trough ssh """ - total_pi = len(self.clients) - for client in range(total_pi): - self.run_cmd(client, list(self.agents_cmd[ - client * len(self.agents_cmd) // total_pi:(client + 1) * len( - self.agents_cmd) // total_pi - ]) - ) + for i_client in range(len(self.clients)): + self.run_cmd(i_client, self.agents_cmd[i_client]) print("Amas, push agent done") def agent_neighbour(self, id_agent1: int, id_agent2: int) -> None: diff --git a/iotAmak/communicating_agent.py b/iotAmak/communicating_agent.py new file mode 100644 index 0000000..17521a2 --- /dev/null +++ b/iotAmak/communicating_agent.py @@ -0,0 +1,37 @@ +import json +import pathlib +import sys +from typing import List, Any + +sys.path.insert(0, str(pathlib.Path(__file__).parent)) + +from iotAmak.agent import Agent +from iotAmak.tool.mail import Mail + + +class CommunicatingAgent(Agent): + """ + Agent class that can communicate + """ + + def __init__(self, arguments: str) -> None: + self.mailbox: List[Mail] = [] + Agent.__init__(self, arguments) + self.subscribe("mail", self.receive_mail) + + def receive_mail(self, client, userdata, message) -> None: + """ + Called when the agent receive a new message + """ + raw = json.loads(message) + self.mailbox.append(Mail(raw.get("id"), raw.get("cycle"), raw.get("payload"))) + + def send_mail(self, agent_id: int, payload: Any) -> None: + """ + Send a mail to agent id + :param agent_id: id of the receiver + :param payload: anything, must be serializable with json.dumps(). + """ + + new_mail = {"id": self.id, "cycle": self.nbr_cycle, "payload": payload} + self.publish("agent/" + str(agent_id) + "/mail", json.dumps(new_mail)) diff --git a/iotAmak/scheduler.py b/iotAmak/scheduler.py index 3f6a54f..9f89941 100644 --- a/iotAmak/scheduler.py +++ b/iotAmak/scheduler.py @@ -41,7 +41,6 @@ class Scheduler(Schedulable): Function called when the IHM pause the scheduler """ self.paused = True - self.ihm_semaphore.acquire() def unpause(self, client, userdata, message) -> None: """ diff --git a/iotAmak/tool/mail.py b/iotAmak/tool/mail.py new file mode 100644 index 0000000..cda8892 --- /dev/null +++ b/iotAmak/tool/mail.py @@ -0,0 +1,10 @@ +from typing import Any + + +class Mail: + + def __init__(self, sender_id: int, date: int, payload: Any) -> None: + + self.sender_id: int = sender_id + self.date: int = date + self.payload: Any = payload diff --git a/setup.py b/setup.py index c62ced9..ee51db1 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup( name='iotAmak', packages=find_packages(), - version='0.0.4', + version='0.0.5', description='AmakFramework in python', author='SMAC - GOYON Sebastien', install_requires=[ -- GitLab