Repository: Foloso/MixSimulator (language: python)

Issue:
Mas simulation
- use of "init" params to choose the initial time_index to use in some functions
- keep the last stock (get_stock) for hydropowerplant when simulating
- make functionnal _notify_is_down 


Discussion / hints:

Fix patch:
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..0e91fc5
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
diff --git a/Evaluation.py b/Evaluation.py
old mode 100644
new mode 100755
index 49a6889..f01dd9c
--- a/Evaluation.py
+++ b/Evaluation.py
@@ -9,7 +9,7 @@
 from typing import List
 import warnings
 from .nevergradBased import Optimizer as opt
-from . import Demand as de
+from .demand.classic import Demand as de
 from datetime import datetime
 #from matplotlib import ticker
 
diff --git a/Experiments/Scenario_type.py b/Experiments/Scenario_type.py
new file mode 100755
index 0000000..2b8969a
--- /dev/null
+++ b/Experiments/Scenario_type.py
@@ -0,0 +1,275 @@
+#!/usr/bin/python3
+import threading
+from MixSimulator import ElectricityMix
+from MixSimulator.Evaluation import EvaluationBudget
+from MixSimulator.demand.classic.Demand import Demand
+import MixSimulator.nevergradBased.Optimizer as opt
+import time
+from datetime import datetime
+from math import ceil
+from MixSimulator.agents.Moderator import StoppableThread
+import matplotlib.pyplot as plt
+import numpy as np
+import sys
+import copy
+import os
+import warnings
+from typing import List, Dict
+import random
+import pickle
+
+"""
+    (0) Functions : Check the thread running in background, plot loss results and generate scenarios
+"""
+def generate_random_scenario(centrals: List, time_index: int) -> Dict:
+    scenario = {}
+    for central in centrals:
+        tmp = {"down":[], "up":[]}
+        default_proba = random.uniform(0, 0.07)
+        
+        for i in range(time_index):
+            tmp[np.random.choice(["up", "down"], p=[1-default_proba,default_proba])].append(i)
+
+        up = []
+        down = []
+        for i in range(time_index):
+            if i in tmp["down"] and (i-1 not in tmp["down"] or i==0):
+                down.append(i)
+            if i-1 in tmp["down"] and i in tmp["up"]: 
+                up.append(i)
+        tmp["up"] = up
+        tmp["down"] = down
+
+        scenario.update({central:tmp.copy()})
+    
+    print("scenario: ", scenario)
+    event_stack = {}
+    for i in range(time_index):
+        for central in scenario.keys():
+            if i in scenario[central]["down"]:
+                try:
+                    event_stack[i].append((central._notify_is_down,central,"down"))
+                except:
+                    event_stack.update({i:[(central._notify_is_down,central,"down")]})
+            elif i in scenario[central]["up"]:
+                try:
+                    event_stack[i].append((central._notify_is_up,central,"up"))
+                except:
+                    event_stack.update({i:[(central._notify_is_up,central,"up")]})
+
+
+    # print(numpy.arange(0, 2))
+    #print("scenario: ", event_stack)
+    return event_stack
+
+def check_thread_running():
+    list_ = []
+    while True:
+        tmp = threading.enumerate().copy()
+        if tmp != list_:
+            list_ = tmp
+            for thread in list_:
+                if thread.is_alive():
+                    print("THREAD:  " + thread.name)
+        time.sleep(10)
+
+thread_checker = StoppableThread(target=check_thread_running, name="thread_checker")
+thread_checker.start()
+
+  
+def moving_average(x, w):
+    return np.convolve(x, np.ones(w), 'valid') / w
+
+def plot_loss(optimum, mode : str = "default", average_wide : int = 0, step : int = 1):
+        #set the moving average wide
+        if average_wide == 0 :
+            average_wide = ceil(len(optimum[-1]["coef"])/4)
+    
+        if mode == "default" or mode == "save" or mode == "best":
+            #set Y
+            Y_: Dict[str,List[float]] ={} 
+            Y_["lost"] = []
+            Y_["demand_gap"] = []
+            for t_i in range(0,len(optimum)):
+                Y_["lost"].append(optimum[t_i]["loss"])
+                Y_["demand_gap"].append(optimum[t_i]["unsatisfied demand"])
+
+            fig, axs = plt.subplots(1, 2, figsize=(10, 10))        
+        
+            # data integration        
+            X = [(i+1)*step for i in range(0,len(optimum))]  
+            #smooth_value = moving_average(Y_["lost"],average_wide)
+            for n_axs in range(0,2) :
+                if n_axs == 0:
+                    #axs[n_axs].plot(X[(average_wide - 1):], smooth_value, '.-' ,alpha=0.5, lw=2, label="lost")
+                    axs[n_axs].plot(X,Y_["lost"],'.-' ,alpha=0.5, lw=2, label="lost")
+                else:
+                    axs[n_axs].plot(X,Y_["demand_gap"],'-*' ,alpha=0.5, lw=2, label="demand gap")
+              
+            # Add execution_time and loss information
+            try :  
+                info = "Final loss: "+ "{:.3f}".format(optimum[-1]["loss"])+" ; run_time: "+"{:.3f}".format(optimum[-1]["elapsed_time"])
+            except :
+                info = "production_cost: "+ "{:.3f}".format(optimum[-1]["loss"])
+            info += ";Final demand gap: "+"{:.3f}".format(optimum[-1]["unsatisfied demand"])                   
+            plt.annotate(info,
+                xy=(0.5, 0), xytext=(0, 10),
+                xycoords=('axes fraction', 'figure fraction'),
+                textcoords='offset points',
+                size=10, ha='center', va='bottom')
+                
+            # plots parametrizations
+            for n_axs in range(0,2) :  
+                axs[n_axs].grid()
+                axs[n_axs].yaxis.set_tick_params(length=0)
+                axs[n_axs].xaxis.set_tick_params(length=0)
+                axs[n_axs].set_xlabel('budgets')
+                #axs[n_axs].yaxis.set_major_formatter(StrMethodFormatter("{x}"+units[0]))
+                if n_axs == 0 :
+                    axs[n_axs].set_ylabel('Loss')
+                elif n_axs == 1 :
+                    axs[n_axs].set_ylabel('Demand gap')
+                axs[n_axs].legend()
+                
+            fig.tight_layout()
+            try :
+                path = "Loss_results_"+datetime.now().strftime("%d_%m_%Y")
+                os.makedirs(path)
+                name = path+"/"+"opt_"+str(datetime.now().strftime("%H%M%S"))+".png"
+                fig.savefig(name)
+                if mode == "default" :
+                    plt.show()
+                
+            except OSError:
+                warnings.warn("Can't create the directory "+path+" or already exists")
+                try : 
+                    name = path+"/"+"opt_"+str(datetime.now().strftime("%H%M%S"))+".png"
+                    fig.savefig(name)
+                    if mode == "default" :
+                        plt.show()
+                except FileNotFoundError:
+                    name = "opt_"+str(datetime.now().strftime("%H%M%S"))+".png"
+                    fig.savefig(name)
+                    if mode == "default" :
+                        plt.show()
+
+        elif mode == "None" :
+            pass
+        else :
+            warnings.warn("Choose an available option : default, save or None")
+            #plt.show()
+    
+
+""" 
+(1) INITIALIZATION  PARAMS: 
+        - Configure nevergrad optimizers : argument[1] is opt_name, arg[2] is budget, arg[3] num_worker
+        - time index as duration (arg[4]),
+        - step of evaluation as step_budget (arg[5]), 
+        - time_interval as t_i (arg[6]).
+"""
+opt_name = "OnePlusOne"
+budget = 50
+num_worker = 1
+duration = 12
+step_budget = 25
+t_i = 1 #time_interval
+log_filename = "log.pickle"
+
+for i in range(1,len(sys.argv)):
+    if i == 1:
+        opt_name = str(sys.argv[i])
+    elif i == 2:
+        budget = int(sys.argv[i])
+    elif i == 3:
+        num_worker = int(sys.argv[i])
+    elif i == 4:
+        duration = int(sys.argv[i])
+    elif i == 5:
+        step_budget = int(sys.argv[i])
+    elif i == 6:
+        t_i = int(sys.argv[i])
+    elif i == 7:
+        log_filename = str(sys.argv[i])+".pickle"
+
+### optimizer 
+opt = opt.Optimizer(opt = [opt_name], budget = [budget], num_worker = num_worker)
+""" 
+(2) Init MixSimulator instance :
+    Case one [Default] : "classic" method (see test_classic.py for more use case)
+    Case two : "MAS" or Multi Agent System method
+
+    Default parameters :
+    ------------------------
+    method : string = "classic",    --> method explain above
+    carbon_cost : float = 0         --> cost of the CO2 production 
+    penalisation_cost: float = 1e7  --> penalisation cost when production is more or less than the demand #NEED VERIFICATION
+"""
+
+classic_mix = ElectricityMix.mix(method="classic",carbon_cost=0,penalisation_cost=100) 
+mas_mix = ElectricityMix.mix(method="MAS",carbon_cost=0,penalisation_cost=100)
+
+centrals = mas_mix.get_moderator().get_observable()
+scenario = generate_random_scenario(centrals, duration)
+
+""" 
+(3) ONE SHOT optimization by calling the classic approach
+
+"""
+classic_mix.set_data_csv("data/RIToamasina/dataset_RI_Toamasina_v2.csv",delimiter=";")
+demand = Demand()
+data_demand = demand.set_data_csv("data/RIToamasina/DIR-TOAMASINA_concat.csv", delimiter = ",")
+classic_mix.set_demand(demand)
+classic_result = classic_mix.optimizeMix(1e10,optimizer = opt, step = step_budget, penalisation = 100, carbon_cost = 0, time_index = duration, plot = "save").copy()
+###
+### MODIFY RESULTS BASED ON EVENTS
+backup_results =  copy.deepcopy(classic_result)
+for t in scenario.keys():
+    for event in scenario[t]:
+        for position, central in enumerate(classic_mix.get_centrals()):
+            if central.get_id() == event[1].get_name():
+                if event[2] == "down":
+                    for step_, value in enumerate(classic_result):
+                        for ti in range(t,duration):
+                            classic_result[step_]["coef"][ti][position] = 0.0
+                elif event[2] == "up":
+                    for step_, value in enumerate(classic_result):
+                        for ti in range(t,duration):
+                            classic_result[step_]["coef"][ti][position] = backup_results[step_]["coef"][ti][position]
+
+for step_,value in enumerate(classic_result):
+    classic_result[step_]["loss"] = classic_mix.loss_function(classic_result[step_]["coef"], time_interval=t_i, no_arrange=True)
+###
+### print(classic_result)
+### PLEASE Check this specific plot in the folder "Loss_result_....."
+plot_loss(classic_result,step = step_budget, mode="save")
+
+"""
+(4) Simulating the mas platform (Manually)
+        1 - First, set params by using set_params method
+        2 - Launch the run_optimization method to initiate the simulation
+        3 - Add events
+        4 - Get the result after all threads done
+"""
+mas_mix.get_moderator().set_params(1e10,optimizer = opt, step = step_budget, penalisation = 100, carbon_cost = 0, time_index = duration, plot = "None")
+mas_mix.get_moderator().run_optimization()
+
+for t in scenario.keys():
+    for event in scenario[t]:
+        event[0](t)
+
+### Waiting method
+while True:
+    if len(threading.enumerate()) == 2:
+        thread_checker.stop()
+        break
+print("SIMULATION DONE")
+
+mas_mix.get_moderator().plotResults(mas_mix.get_moderator().get_results(),mode="save")
+
+### PLEASE Check this specific plot in the folder "Loss_result_....."
+plot_loss(mas_mix.get_moderator().get_results(),step = step_budget, mode="save")
+
+log = {}
+log.update({"events": scenario, "opt_params":classic_mix.get_params(), "mas_results": mas_mix.get_moderator().get_results(), "classic_results":classic_result})
+
+print(log,file=open(log_filename,"a"))
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
diff --git a/MANIFEST.in b/MANIFEST.in
old mode 100644
new mode 100755
diff --git a/Mas_platform.py b/Mas_platform.py
new file mode 100755
index 0000000..db22d2d
--- /dev/null
+++ b/Mas_platform.py
@@ -0,0 +1,136 @@
+from .agents.Moderator import Moderator
+from .power_plants.mas.Hydropowerplant import Hydropowerplant
+from .power_plants.mas.Thermalpowerplant import Thermalpowerplant
+from .demand.mas.Demand import Demand
+import nevergrad as ng
+from .nevergradBased.Optimizer import Optimizer
+import numpy as np # type: ignore
+import pandas as pd # type: ignore
+import pkgutil
+import csv
+import os
+import warnings
+from math import ceil
+#import time
+from typing import List, Any, Type, Dict
+from datetime import datetime
+import matplotlib.pyplot as plt # type: ignore
+
+class Mas_platform():
+    """
+        Platform of a multi agent system :
+            - Initialisation of the agents from original dataset
+            - Get and initiate the optimizer
+            - Data visualization
+    """
+
+    def __init__(self, demand = None, carbon_cost: float = 0, penalisation_cost: float = 1e7, bind = None, raw_data = None, variation_data = None, delimiter: str=";", variation_delimiter: str=";",) -> None:
+        self.__moderator = Moderator(carbon_cost, penalisation_cost)
+        if demand is not None :
+            self.__moderator.set_demand(demand)
+        if (bind is not None) and (raw_data is not None) :
+            self.set_data_csv(bind = bind, raw_data = raw_data, delimiter = delimiter)
+        else : #Load default data
+            self.set_data_to("Toamasina")
+        if variation_data is not None :
+            self.set_variation_csv(bind = variation_data, delimiter = variation_delimiter)
+
+    def get_moderator(self) -> Moderator:
+        return self.__moderator
+
+    def set_moderator(self, new_moderator) -> None :
+        self.__moderator = new_moderator
+
+    def set_variation_csv(self, bind = None, delimiter: str=";") -> None:
+        if self.__moderator.get_observable() == []:
+             warnings.warn("Please load a original dataset by using MixSimulator.set_data_csv(...)")
+             raise 
+        else :
+            try :
+                data = pd.DataFrame(pd.read_csv(bind,delimiter=delimiter))
+            except FileNotFoundError as e :
+                    print("Error occured on pandas.read_csv : ",e)
+                    print("Please check your file")
+                    raise           
+            except Exception as e:
+                    print("Error occured on pandas.read_csv : ",e)
+                    raise
+                    
+            for powerplant_index in range(len(self.__moderator.get_agents())):
+                if self.__moderator.get_observable()[powerplant_index].is_tuneable():
+                    for i in range (0,data.shape[0]):
+                        if self.__moderator.get_observable()[powerplant_index].get_id() == data["centrals"][i]:
+                            tmp_list=[]
+                            upper = str(data["upper"][i]).split(":")
+                            upper = [float(numeric_string) for numeric_string in upper]
+                            lower = str(data["lower"][i]).split(":")
+                            lower = [float(numeric_string) for numeric_string in lower]
+                            discrete = str(data["discrete"][i]).split(":")
+                            discrete = [float(numeric_string) for numeric_string in discrete]
+                            self.__moderator.get_observable()[powerplant_index].set_variation_params(lower = lower, upper = upper, choices = discrete)
+        
+    def set_data_csv(self, bind = None, raw_data = None, delimiter: str=";"):
+        if raw_data is not None :
+            data = pd.DataFrame(raw_data)
+            #set columns & index           
+            header = data.iloc[0]
+            data = data[1:]
+            data.columns = header
+            data = data.reset_index(drop=True)
+            for column in data.columns.tolist():
+                try:
+                    # convert numeric values
+                    data[column] = pd.to_numeric(data[column])
+                except:
+                    pass
+        else :
+            try :
+                data = pd.DataFrame(pd.read_csv(bind,delimiter=delimiter))
+            except FileNotFoundError as e :
+                print("Error occured on pandas.read_csv : ",e)
+                print("Please check your file")
+                raise           
+            except Exception as e:
+                print("Error occured on pandas.read_csv : ",e)
+                raise
+            
+        #self.__reset_centrals()
+        #powerplant = pc.PowerCentral()
+        try :
+            self.__moderator.get_demand().set_mean_demand(data["Demand"][0])
+            self.__moderator.set_constant_lost(data["lost"][0])
+            for i in range (0,data.shape[0]):
+                isHydro = data["hydro"][i]
+                if isHydro.lower() == "true" :
+                    powerplant = Hydropowerplant(data["height"][i],data["flow"][i],data["capacity"][i],data["stock_available"][i],0.1,0.8)
+                else :
+                    powerplant = Thermalpowerplant()
+                powerplant.set_tuneable(data["tuneable"][i])
+                powerplant.set_name(str(data["centrals"][i]))
+                powerplant.set_fuel_consumption(data["fuel_consumption"][i])
+                powerplant.set_availability(data["availability"][i])
+                powerplant.set_fuel_cost(data["fuel_cost"][i])
+                powerplant.set_initial_value(data["init_value"][i])
+                powerplant.set_lifetime(data["lifetime"][i])
+                powerplant.set_carbon_prod(data["carbon_production"][i])
+                powerplant.set_raw_power(data["raw_power"][i])
+                powerplant.set_nb_employees(data["nb_employees"][i])
+                powerplant.set_mean_employees_salary(data["mean_salary"][i])
+                powerplant.set_max_var(data["max_var"][i])
+                ### Register in each agent the observer (moderator) and that allows moderator getting each agent
+                powerplant.register_observer([self.__moderator])
+        except KeyError:
+            print("Columns must be in: tuneable, centrals, fuel_consumption, availability, fuel_cost, init_value, lifetime, carbon_cost, raw_power, nb_employees, mean_salary, demand, lost, height, flow, capacity, stock_available")
+            raise
+            
+    def set_data_to(self, dataset, delimiter: str=";"):
+        if dataset == "Toamasina":
+            #by defaut we keep it "Toamasina"
+            data = pkgutil.get_data('mixsimulator', '/data/RIToamasina/dataset_RI_Toamasina_v2.csv')
+            data = csv.reader(data.decode('utf-8').splitlines(), delimiter = delimiter)
+            self.set_data_csv(raw_data=data)
+        else :
+            #by defaut we keep it "Toamasina"
+            data = pkgutil.get_data('mixsimulator', '/data/RIToamasina/dataset_RI_Toamasina_v2.csv')
+            data = csv.reader(data.decode('utf-8').splitlines(), delimiter = delimiter)
+            self.set_data_csv(raw_data=data)
\ No newline at end of file
diff --git a/MixSimulator.py b/MixSimulator.py
old mode 100644
new mode 100755
index f0580aa..6cdb951
--- a/MixSimulator.py
+++ b/MixSimulator.py
@@ -1,6 +1,6 @@
-from .centrals import PowerCentral as pc
-from .centrals import HydroCentral as hc 
-from .Demand import Demand
+from .power_plants.classic import PowerCentral as pc
+from .power_plants.classic import HydroCentral as hc 
+from .demand.classic.Demand import Demand
 import nevergrad as ng
 from .nevergradBased.Optimizer import Optimizer
 import numpy as np # type: ignore
@@ -138,6 +138,9 @@ def set_demand(self, demand: Demand):
         
     def get_demand(self):
         return self.__demand
+    
+    def get_centrals(self):
+        return self.__centrals
         
     def set_lost(self, lost: float):
         self.__lost = lost
@@ -214,9 +217,12 @@ def get_weighted_coef(self, usage_coef, time_interval):
                 self.__centrals[central_index].back_propagate(weighted_coef[t][central_index], t, time_interval)
         return weighted_coef
 
-    def loss_function(self, usage_coef, time_interval : int = 1) -> float :
-        usage_coef = self.arrange_coef_as_array_of_array(usage_coef)
-        weighted_coef = self.get_weighted_coef(usage_coef, time_interval=time_interval)
+    def loss_function(self, usage_coef, time_interval : int = 1, no_arrange = False, init : int = 0) -> float :
+        if no_arrange is False:
+            usage_coef = self.arrange_coef_as_array_of_array(usage_coef)
+            weighted_coef = self.get_weighted_coef(usage_coef, time_interval=time_interval)
+        else :
+            weighted_coef = usage_coef
         loss = 0
         for t in range(0, len(weighted_coef)):
             loss += self.get_production_cost_at_t(weighted_coef[t], t, time_interval) + ( self.get_penalisation_cost() * np.abs( self.get_unsatisfied_demand_at_t(weighted_coef[t], t, time_interval)) )
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
diff --git a/__init__.py b/__init__.py
old mode 100644
new mode 100755
index e69de29..b741cd0
--- a/__init__.py
+++ b/__init__.py
@@ -0,0 +1,15 @@
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+from .base import ElectricityMix
+from .Mas_platform import Mas_platform
+from .MixSimulator import MixSimulator
+# from .Demand import Demand
+from .nevergradBased import Optimizer	
+from .Evaluation import EvaluationBudget
+
+# __all__ = ["ElectricityMix","Mas_platform","MixSimulator","Demand","Optimizer","EvaluationBudget"]
+__all__ = ["ElectricityMix","Mas_platform","MixSimulator","Optimizer","EvaluationBudget"]
+
+
+__version__ = "0.4"
\ No newline at end of file
diff --git a/agents/Agent.py b/agents/Agent.py
new file mode 100755
index 0000000..9208071
--- /dev/null
+++ b/agents/Agent.py
@@ -0,0 +1,89 @@
+from typing import List, Dict
+import json
+import uuid
+from .Interfaces import Observable
+from threading import Timer, Lock
+
+
+class Periodic(object):
+    """
+    A periodic task running in threading.Timers
+    """
+    def __init__(self):
+        self._lock = Lock()
+        self._timer = None
+        self._stopped = True
+
+    def set_function(self, function, *args, **kwargs):
+        self.__function = function
+        self.args = args
+        self.kwargs = kwargs
+
+    def set_interval(self, interval):
+        self.__interval = interval
+
+    def start(self, from_run=False):
+        self._lock.acquire()
+        if from_run or self._stopped:
+            self._stopped = False
+            self._timer = Timer(self.__interval, self._run)
+            self._timer.start()
+        self._lock.release()
+
+    def _run(self):
+        self.start(from_run=True)
+        self.__function(*self.args, **self.kwargs)
+
+    def stop(self):
+        self._lock.acquire()
+        self._stopped = True
+        self._timer.cancel()
+        self._lock.release()
+
+
+
+class Agent(Observable):
+  
+    def __init__(self) -> None:
+        ### Temporary disable id
+        super().__init__()
+        self._code_files = "params_files/exchange_code.json" ### TEMPORARY CHANGE FILE PATH
+        self.__type = "empty"
+        self.__name = ""
+        self._scheduled_actions = {}
+        self.set_id(uuid.uuid4())
+
+    def __repr__(self):
+        return self.get_name()+"["+str(self.get_id())+"] ("+self.__type+")"
+
+    def _schedule_action(self, actions: Dict) -> None:
+        ## action is a dict {function, interval}
+        for action in actions.keys():
+            if action not in self._scheduled_actions.keys():
+                ## self._schedule_actions is a dict {function, Periodic --the class above--)}
+                periodic = Periodic()
+                periodic.set_function(action)
+                periodic.set_interval(actions[action])
+                self._scheduled_actions.update({action: periodic})
+                self._scheduled_actions[action].start()
+      
+    def register_observer(self, moderators: List, t_from=0) -> None:
+        super().register_observer(moderators)
+        register_signal = json.load(open(self._code_files))["100"]
+        register_signal["id"] = self.get_id()
+        register_signal["t_from"] = t_from
+        self._notify_observers(register_signal)
+
+    def set_type(self, ntype):
+        self.__type = ntype
+
+    def get_type(self):
+        return self.__type
+
+    def set_name(self, name):
+        self.__name = name
+
+    def get_name(self):
+        return self.__name
+
+
diff --git a/agents/Interfaces.py b/agents/Interfaces.py
new file mode 100755
index 0000000..a5b8dca
--- /dev/null
+++ b/agents/Interfaces.py
@@ -0,0 +1,31 @@
+from typing import List
+
+class Observable:
+    def __init__(self):
+        self.__observers = []
+        self.__id = None
+
+    def get_id(self) -> str:
+        return self.__id
+
+    def set_id(self, id) -> None:
+        self.__id = id
+
+    def get_observers(self) -> List :
+        return self.__observers
+
+    def register_observer(self, observers: List) -> None:
+        for observer in observers:
+            if observer not in self.__observers:
+                self.__observers.append(observer)
+
+    def _notify_observers(self, signal) -> None:
+        for obs in self.get_observers():
+            obs.update(self, signal)
+
+
+
+class Observer:
+  
+    def update(self, observable, *args, **kwargs):
+        pass
\ No newline at end of file
diff --git a/agents/Moderator.py b/agents/Moderator.py
new file mode 100755
index 0000000..bf6e230
--- /dev/null
+++ b/agents/Moderator.py
@@ -0,0 +1,502 @@
+from mimetypes import init
+from .Interfaces import Observer, Observable
+from ..power_plants.mas.PowerPlant import PowerPlant
+from ..demand.mas.Demand import Demand
+import nevergrad as ng
+from ..nevergradBased.Optimizer import Optimizer
+import numpy as np # type: ignore
+import pandas as pd # type: ignore
+import pkgutil
+import copy
+import csv
+import os
+import warnings
+from math import ceil
+#import time
+from typing import List, Any, Type, Dict
+from datetime import datetime
+import matplotlib.pyplot as plt # type: ignore
+import threading
+import ctypes
+
+class StoppableThread(threading.Thread):
+    """
+        Thread class with a stop() method. The thread itself has to check
+        regularly for the stopped() condition.
+    """
+
+    def __init__(self,  *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+    def run(self):
+        try:
+            super().run()
+        finally:
+            # print('stopped thread: ' + self.name)
+            pass
+
+    def get_id(self):
+        if hasattr(self, '_thread_id'):
+            return self._thread_id
+        for id, thread in threading._active.items():
+            if thread is self:
+                return id
+
+    def stop(self):
+        thread_id = self.get_id()
+        resu = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread_id),
+                                                     ctypes.py_object(SystemExit))
+        if resu > 1:
+            ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
+
+
+class Moderator(Observer):
+    def __init__(self,carbon_cost, penalisation_cost) -> None:
+        super().__init__()
+        self.__log_file = open("optimisation.log", "w")
+        self.__observable : List[PowerPlant] = []
+        self.__powerplants_down : List[PowerPlant] = []
+        dm = Demand(demand= 20, var_per_day= 0.2, var_per_season= 0.2)
+        dm.set_data_to("Toamasina",delimiter=",")
+        self.__params_state = None
+        self.__demand = dm
+        self.__cst_lost = 0.
+        self.__penalisation_cost = penalisation_cost
+        self.__optimizer =  Optimizer()
+        self.__carbon_cost = carbon_cost
+        self.__carbon_quota = 800139. # (g/MWh)
+        self.__results = ...
+        self.__latest_results = ...
+        
+        # for reuse and get_params()
+        self.time_index = 24*7
+        self.step = 1
+        self.time_interval = 1
+        self.plot = "default"
+        self.average_wide = 0
+
+        self.__cur_optimization : StoppableThread = None
+
+    def __reset_powerplant(self):
+        self.__observable : List[PowerPlant] = []
+
+    def get_observable(self) -> List[Observable]:
+        return self.__observable
+
+    def set_observable(self, observables: List[Observable]) -> None:
+        self.__observable = observables
+      
+    def __add_observable(self, observable: Observable) -> None:
+        if observable not in self.__observable:
+            self.__observable.append(observable)
+
+    def __shutdown_observable(self, observable, id) -> None:
+        observable.shutdown()
+
+    def __power_on_observable(self, observable: Observable) -> None:
+        observable.power_on()
+    
+    ### COMMUNICATION
+    def __update_self(self, observable, signal):
+        print(observable, "sends signal code ", signal["code"])
+        if signal["code"] == 100:
+            self.__add_observable(observable)
+        elif signal["code"] == 400:
+            self.__shutdown_observable(observable,signal["id"])
+        elif signal["code"] == 200:  
+            self.__power_on_observable(observable)  
+
+    def update(self, observable, signal, *args, **kwargs) -> None:
+        if self.__params_state is None:
+            self.__update_self(observable, signal)
+        else:
+            ## relaunching optimization
+            _kwargs = {"carbon_quota": self.__carbon_quota , "demand": self.get_demand(), "lost": self.__cst_lost, 
+                    "optimizer": self.__optimizer, "step" : self.step,
+                    "time_index": self.time_index, "time_interval": self.time_interval, 
+                    "penalisation" : self.__penalisation_cost, "carbon_cost" : self.get_carbon_cost(), 
+                    "plot" : self.plot, "average_wide" : self.average_wide}
+            if self.__cur_optimization is None:
+                pass
+            elif signal["t_from"] == self.__cur:
+                _kwargs.update({"init": signal["t_from"], "time_index": _kwargs["time_index"]-signal["t_from"]})
+                self.__cur_optimization.stop()
+                self.__update_self(observable, signal)
+                self.__cur_optimization = StoppableThread(target=self.optimizeMix, kwargs=_kwargs, name=str(observable)+str(signal["code"]))
+                self.__cur = signal["t_from"]
+                self.__cur_optimization.start()
+            elif signal["t_from"] > self.__cur:
+                _kwargs.update({"init": signal["t_from"], "time_index": _kwargs["time_index"]-signal["t_from"]})
+                print("wainting for current optimization to finish".upper())
+                self.__cur_optimization.join()
+                self.__update_self(observable, signal)
+                self.__cur_optimization = StoppableThread(target=self.optimizeMix, kwargs=_kwargs, name=str(observable)+str(signal["code"]))
+                self.__cur = signal["t_from"]
+                self.__cur_optimization.start()
+
+
+    ### PARAMETRIZATION
+    def set_demand(self, demand_agent) -> None:
+        self.__demand = demand_agent
+
+    def get_demand(self):
+        return self.__demand
+
+    def set_mean_demand(self, mean_demand) -> None:
+        self.__demand = mean_demand
+
+    def set_constant_lost(self, lost) -> None:
+        self.__cst_lost = lost
+
+    def set_optimizer(self, optimizer: Optimizer):
+        self.__optimizer = optimizer
+        
+    def get_optimizer(self) -> Optimizer:
+        return self.__optimizer
+
+    def set_carbon_quota(self, cb_quota: float ):
+        self.__carbon_quota = cb_quota
+
+    def set_penalisation_cost(self, k):
+        self.__penalisation_cost = k
+
+    def get_penalisation_cost(self):
+        return self.__penalisation_cost
+        
+    def set_carbon_cost(self, carbon_cost):
+        self.__carbon_cost = carbon_cost
+
+    def get_carbon_cost(self):
+        return self.__carbon_cost
+
+    def get_results(self):
+        return self.__results
+
+    ### EVALUATION FUNCTIONS
+    def get_production_cost_at_t(self, usage_coef, time_index, time_interval):
+        production_cost = 0
+        for powerplant_index in range (0, len(self.__observable)):
+            if self.__observable[powerplant_index].get_type() != "Demand":
+                powerplant = self.__observable[powerplant_index]
+                production_cost += ( (powerplant.get_fuel_cost() * powerplant.get_fuel_consumption() 
+                * powerplant.get_raw_power() * usage_coef[powerplant_index]) + ( powerplant.get_employees_salary()
+                + powerplant.get_amortized_cost(time_index) ) ) * time_interval
+        return production_cost
+
+    def get_production_at_t(self, usage_coef, time_interval):
+        production = 0
+        for powerplant_index in range (0, len(self.__observable)):
+            if self.__observable[powerplant_index].get_type() != "Demand":
+                powerplant = self.__observable[powerplant_index]
+                production += (powerplant.get_raw_power() * usage_coef[powerplant_index]) * time_interval
+        return production
+
+    def get_unsatisfied_demand_at_t(self, usage_coef, time_index, time_interval, init : int = 0):
+        #return ( self.__demand.get_demand_approxima(time_index, time_interval) - self.get_production_at_t(usage_coef, time_interval))
+        return ( self.__demand.get_demand_monthly(time_index, time_interval, init = init) - self.get_production_at_t(usage_coef, time_interval))
+        
+    def get_carbon_production_at_t(self, usage_coef, time_interval):
+        carbon_prod = 0
+        for powerplant_index in range (0, len(self.__observable)):
+            if self.__observable[powerplant_index].get_type() != "Demand":
+                powerplant = self.__observable[powerplant_index]
+                carbon_prod += (powerplant.get_carbon_production() * usage_coef[powerplant_index] * powerplant.get_raw_power()) * time_interval
+        return carbon_prod
+        
+    def get_carbon_over_production(self, usage_coef, time_interval):
+        emited_carbon = 0 # (g)
+        total_production = 1
+        for t in range(0, len(usage_coef)):
+            emited_carbon += self.get_carbon_production_at_t(usage_coef[t], time_interval)
+            total_production += self.get_production_at_t(usage_coef[t], time_interval)
+        carbon_production = emited_carbon/total_production
+        return max(0, carbon_production - self.__carbon_quota) # (g/MWh)
+
+    def get_weighted_coef(self, usage_coef, time_interval, init : int = 0):
+        if init == 0 :
+            for powerplant in self.__observable:                                       
+                if powerplant.get_type() not in ["Demand"] :  
+                    powerplant.reset_powerplant()  
+        else :
+            for powerplant in self.__observable:                                ###########################################
+                if powerplant.get_type() not in ["Demand","Hydropowerplant"] :  ## Keep previous history stock for hydro ##
+                    powerplant.reset_powerplant()                               ###########################################
+
+        weighted_coef = usage_coef.copy()
+        for t in range(0, len(weighted_coef)):
+            for powerplant_index, powerplant in enumerate(self.__observable):
+                if powerplant.get_type() != "Demand":
+                    min_av = powerplant.get_min_availability()
+                    max_av = powerplant.get_max_availability(t, init = init)
+                    if max_av < min_av:
+                        min_av = 0 # a verifier 
+                    weighted_coef[t][powerplant_index] = min_av + weighted_coef[t][powerplant_index]*(max_av-min_av)
+                    powerplant.back_propagate(weighted_coef[t][powerplant_index], t, time_interval)
+        return weighted_coef
+
+    def loss_function(self, usage_coef, time_interval : int = 1, no_arrange = False, init : int = 0) -> float :
+        if no_arrange is False:
+            usage_coef = self.arrange_coef_as_array_of_array(usage_coef)
+            weighted_coef = self.get_weighted_coef(usage_coef, time_interval=time_interval, init = init)
+        else :
+            weighted_coef = usage_coef
+        loss = 0
+        for t in range(0, len(weighted_coef)):
+            loss += self.get_production_cost_at_t(weighted_coef[t], t, time_interval) + ( self.get_penalisation_cost() * np.abs( self.get_unsatisfied_demand_at_t(weighted_coef[t], t, time_interval, init = init)) )
+        loss +=  self.get_carbon_cost() * (self.get_carbon_over_production(weighted_coef, time_interval) )
+        return loss
+
+    def arrange_coef_as_array_of_array(self, raw_usage_coef):
+        ordered_coef = []
+        cur_time_coef = []
+        for coef_index in range(len(raw_usage_coef)):
+            cur_time_coef.append(raw_usage_coef[coef_index])
+            ## indice de la premiere powerplant a t+1
+            if (coef_index+1)%len(self.__observable) == 0:
+                ordered_coef.append(cur_time_coef)
+                cur_time_coef = []
+        return ordered_coef
+
+
+    ### OPTiMiZATION
+    def __opt_params(self, time_index):
+        self.__optimizer.set_parametrization(self.get_opt_params(time_index))
+        
+    def get_opt_params(self, time_index):
+        variable_parametrization = []
+        for _ in range(time_index):
+            for powerplant_index in range(len(self.__observable)):
+                if self.__observable[powerplant_index].get_type() != "Demand":
+                    if not self.__observable[powerplant_index].is_tuneable():
+                        variable_parametrization += [ng.p.Choice([0.,1.])]
+                    else:
+                        #check the params by 
+                        variable_parametrization += [self.__observable[powerplant_index].get_variation_params()]
+        return ng.p.Tuple(*variable_parametrization)
+
+    def __reshape_results(self, results, time_interval):
+        for tmp in results:
+            usage_coef = self.arrange_coef_as_array_of_array(tmp['coef'])
+            tmp.update({"coef":self.get_weighted_coef(usage_coef, time_interval)})
+
+        # for powerplant_index in range(0, len(self.__observable)):
+        #     try:
+        #         print(self.__observable[powerplant_index].get_stock_evolution())
+        #     # Not a hydro power plant, so the methode does not exist
+        #     except:
+        #         pass
+        return results
+
+    def optimizeMix(self, carbon_quota: float = None, demand: Demand = None, lost: float = None, 
+                    optimizer: Optimizer = None, step : int = 1,
+                    time_index: int = 24*7, time_interval: float = 1,
+                    penalisation : float = None, carbon_cost : float = None, plot : str = "default", average_wide : int = 0, init : int = 0):
+
+        # init params    
+        print("OPTIMIZATION IS RUNNING:\n time_index = "+str(time_index)+" hours\n from init="+str(init)+" to "+str(self.time_index))
+        # step is the step of opt budget
+        if time_index < time_interval:
+            time_interval = time_index
+        else:
+            time_interval = time_interval
+        if demand is not None : self.set_demand(demand)
+        if lost is not None : self.set_constant_lost(lost)
+        if penalisation is not None : self.set_penalisation_cost(penalisation)
+        if carbon_cost is not None : self.set_carbon_cost(carbon_cost)
+        if carbon_quota is not None : self.set_carbon_quota(carbon_quota)
+        if optimizer is not None : self.set_optimizer(optimizer)
+        
+        # tune optimizer parametrization
+        self.__opt_params(time_index)
+        
+        # init constraints
+        constraints = {}
+        constraints.update({"time_interval":time_interval})
+        
+        # let's optimize
+        self.__latest_results = self.__optimizer.optimize(mix = self , func_to_optimize = self.loss_function, constraints=constraints, step = step, init = init)
+        
+        self.__latest_results = self.__reshape_results(self.__latest_results, time_interval)
+
+        self.plotResults(self.__latest_results, mode = "save" , time_interval = time_interval, average_wide = average_wide)
+
+        # update results when init is different of 0
+        if init == 0:
+            self.__results = copy.deepcopy(self.__latest_results)
+        else :
+            self.__results = self.__update_results(self.__results, self.__latest_results, init)
+        
+        print("optimization is done".upper())
+        return self.__results
+
+    def get_params(self) -> dict:
+        return {"agents" : self.__observable, "optimizer" : self.get_optimizer(),
+                "penalisation_cost" : self.get_penalisation_cost(), "carbon_cost" : self.get_carbon_cost(),
+                "demand" : self.__demand, "lost" : self.__lost, "carbon_quota" : self.__carbon_quota,
+                "step" : self.step, "time_interval" : self.time_interval, "time_index" : self.time_index,
+                "plot" : self.plot, "moving average_wide" : self.average_wide, "initial time_index" : self.__init}
+
+    def set_params(self, carbon_quota: float = None, demand: Demand = None, lost: float = None, 
+                    optimizer: Optimizer = None, step : int = 1,
+                    time_index: int = 24*7, time_interval: float = 1,
+                    penalisation : float = None, carbon_cost : float = None, plot : str = "default", average_wide : int = 0, init : int = 0):
+
+        # init params
+        self.__params_state = -1                
+        self.time_index = time_index
+        self.__init = init
+        # step is the step of opt budget
+        self.step = step
+        self.time_interval = time_interval
+        self.plot = plot
+        self.average_wide = average_wide
+        if demand is not None : self.set_demand(demand)
+        if lost is not None : self.set_constant_lost(lost)
+        if penalisation is not None : self.set_penalisation_cost(penalisation)
+        if carbon_cost is not None : self.set_carbon_cost(carbon_cost)
+        if carbon_quota is not None : self.set_carbon_quota(carbon_quota)
+        if optimizer is not None : self.set_optimizer(optimizer)
+
+    def run_optimization(self):
+        """
+            Initial run of the simulation (must be run at first)
+        """
+        _kwargs = {"carbon_quota": self.__carbon_quota , "demand": self.get_demand(), "lost": self.__cst_lost, 
+            "optimizer": self.__optimizer, "step" : self.step, "time_index": self.time_index, 
+            "time_interval": self.time_interval, "penalisation" : self.__penalisation_cost, 
+            "carbon_cost" : self.get_carbon_cost(), "plot" : self.plot, "average_wide" : self.average_wide}
+        self.__cur = 0
+        self.__cur_optimization = StoppableThread(target=self.optimizeMix, kwargs=_kwargs, name="Initial run")
+        self.__cur_optimization.start()
+        
+
+    def __update_results(self, original, new, init):
+        output = []
+        for step_ in range(0,len(original)):
+            previous_coef = original[step_]["coef"][:init]
+            next_coef = new[step_]["coef"]
+            
+            previous_loss = self.loss_function(previous_coef, self.time_interval, no_arrange = True)
+            next_loss = self.loss_function(next_coef, self.time_interval, no_arrange = True)
+
+            production = 0
+            u_demand = 0
+            carbon_prod = 0
+            for i, weighted_coef in enumerate([previous_coef,next_coef]):
+                for t in range(0, len(weighted_coef)):
+                    production +=  self.get_production_cost_at_t(weighted_coef[t], t, self.time_interval)
+                    if i == 1:
+                        u_demand += np.abs(self.get_unsatisfied_demand_at_t(weighted_coef[t], t, self.time_interval, init = init))
+                    else :
+                        u_demand += np.abs(self.get_unsatisfied_demand_at_t(weighted_coef[t], t, self.time_interval))
+                carbon_prod += self.get_carbon_production_at_t(weighted_coef[t], self.time_interval)
+            output.append({"loss":previous_loss+next_loss, "coef":previous_coef+next_coef, "production":production, "unsatisfied demand":u_demand, "carbon production":carbon_prod})
+        
+        return output
+
+    
+    ###########
+    ## PLOTS ##
+    ###########    
+    def moving_average(self, x, w):
+        return np.convolve(x, np.ones(w), 'valid') / w
+        
+    def plotResults(self, optimum : List = [], mode : str = "default", time_interval : float = 1, average_wide : int = 0):
+        #set the moving average wide
+        if average_wide == 0 :
+            average_wide = ceil(len(optimum[-1]["coef"])/4)
+    
+        if mode == "default" or mode == "save" or mode == "best":
+            #set Y of axe 1
+            Y: Dict[str,List[float]] ={}
+            label_y: List[str]=[]
+            sum_prod = []
+            for c in self.__observable :
+                label_y.append(c.get_name())
+                Y.update({c.get_name():[]})
+            if mode != "best":
+                for array in optimum[-1]["coef"] :
+                    sum_t = 0.
+                    for enum, value in enumerate(array) :
+                        Y[label_y[enum]].append(value)
+                        sum_t = sum_t + (value*self.get_observable()[enum].get_raw_power())
+                    sum_prod.append(sum_t)
+            else :
+                ##TODO get best loss
+                pass
+
+            
+            #set Y of axe 2
+            Y_: Dict[str,List[float]] ={} 
+            Y_["demand"] = []
+            Y_["production"] = sum_prod
+            for t_i in range(0,len(optimum[-1]["coef"])):
+                Y_["demand"].append(self.__demand.get_demand_monthly(t_i, time_interval))
+
+            fig, axs = plt.subplots(1, 2, figsize=(12, 6))        
+        
+            # data integration        
+            X = [i for i in range(0,len(optimum[-1]["coef"]))]  
+            for n_axs in range(0,2) :
+                if n_axs == 0 :
+                    for central, value in Y.items():
+                        smooth_value = self.moving_average(value,average_wide)
+                        axs[n_axs].plot(X[(average_wide - 1):], smooth_value, '.-' ,alpha=0.5, lw=2, label=central)
+                elif n_axs == 1 :
+                    axs[n_axs].plot(X,Y_["demand"],label="demand")
+                    smooth_value = self.moving_average(Y_["production"],average_wide)
+                    axs[n_axs].plot(X[(average_wide - 1):], smooth_value, '.-' ,alpha=0.5, lw=2, label="production")
+            
+              
+            # Add execution_time and loss information
+            try :  
+                info = "production_cost: "+ "{:.3f}".format(optimum[-1]["loss"])+" ; execution_time: "+"{:.3f}".format(optimum[-1]["elapsed_time"])
+            except :
+                info = "production_cost: "+ "{:.3f}".format(optimum[-1]["loss"])
+            info += "; demand gap: "+"{:.3f}".format(optimum[-1]["unsatisfied demand"])                   
+            plt.annotate(info,
+                xy=(0.5, 0), xytext=(0, 10),
+                xycoords=('axes fraction', 'figure fraction'),
+                textcoords='offset points',
+                size=10, ha='center', va='bottom')
+                
+            # plots parametrizations
+            for n_axs in range(0,2) :  
+                axs[n_axs].grid()
+                axs[n_axs].yaxis.set_tick_params(length=0)
+                axs[n_axs].xaxis.set_tick_params(length=0)
+                axs[n_axs].set_xlabel('hours')
+                #axs[n_axs].yaxis.set_major_formatter(StrMethodFormatter("{x}"+units[0]))
+                if n_axs == 0 :
+                    axs[n_axs].set_ylabel('coef (%)')
+                elif n_axs == 1 :
+                    axs[n_axs].set_ylabel('MWh')
+                axs[n_axs].legend()
+                
+            fig.tight_layout()
+            try :
+                path = "results_"+datetime.now().strftime("%d_%m_%Y")
+                os.makedirs(path)
+                name = path+"/"+"opt_"+str(self.get_optimizer().get_optimizers())+"_"+datetime.now().strftime("%H%M%S")+".png"
+                fig.savefig(name)
+                if mode == "default" :
+                    plt.show()
+                
+            except OSError:
+                warnings.warn("Can't create the directory "+path+" or already exists")
+                try : 
+                    name = path+"/"+"opt_"+str(self.get_optimizer().get_optimizers())+"_"+datetime.now().strftime("%H%M%S")+".png"
+                    fig.savefig(name)
+                    if mode == "default" :
+                        plt.show()
+                except FileNotFoundError:
+                    name = "opt_"+str(self.get_optimizer().get_optimizers())+"_"+datetime.now().strftime("%H%M%S")+".png"
+                    fig.savefig(name)
+                    if mode == "default" :
+                        plt.show()
+
+        elif mode == "None" :
+            pass
+        else :
+            warnings.warn("Choose an available option : default, save or None")
+            #plt.show()
\ No newline at end of file
diff --git a/centrals/__init__.py b/agents/__init__.py
old mode 100644
new mode 100755
similarity index 100%
rename from centrals/__init__.py
rename to agents/__init__.py
diff --git a/base.py b/base.py
new file mode 100755
index 0000000..5fb9a5a
--- /dev/null
+++ b/base.py
@@ -0,0 +1,20 @@
+from . import MixSimulator as ms
+from . import Mas_platform as mp
+import warnings
+
+class ElectricityMix:
+    """
+        Regroup all methods to simulate the electricity mix :
+            - classic
+            - Multi Agent System (MAS)
+    """
+    def __init__(self):
+        pass
+
+    def mix(method: str = "classic", carbon_cost: float = 0, penalisation_cost: float = 1000000000000):
+        if method == "classic":
+            return ms.MixSimulator(carbon_cost = carbon_cost,penalisation_cost = penalisation_cost)
+        elif method == "MAS":
+            return mp.Mas_platform()
+        else:
+            warnings.warn("Choose an available option : classic or MAS")
\ No newline at end of file
diff --git a/centrals/__init__.pyc b/centrals/__init__.pyc
deleted file mode 100644
index febd5b9..0000000
Binary files a/centrals/__init__.pyc and /dev/null differ
diff --git a/data/.DS_Store b/data/.DS_Store
new file mode 100755
index 0000000..5008ddf
Binary files /dev/null and b/data/.DS_Store differ
diff --git a/data/RIToamasina/DIR-TOAMASINA_concat.csv b/data/RIToamasina/DIR-TOAMASINA_concat.csv
old mode 100644
new mode 100755
diff --git a/data/RIToamasina/dataset_RI_Toamasina.csv b/data/RIToamasina/dataset_RI_Toamasina.csv
old mode 100644
new mode 100755
diff --git a/data/RIToamasina/dataset_RI_Toamasina_v2.csv b/data/RIToamasina/dataset_RI_Toamasina_v2.csv
old mode 100644
new mode 100755
diff --git a/data/RIToamasina/dataset_RI_Toamasina_variation_template.csv b/data/RIToamasina/dataset_RI_Toamasina_variation_template.csv
old mode 100644
new mode 100755
diff --git a/data/RIToamasina/demand/toamasina_demand.csv b/data/RIToamasina/demand/toamasina_demand.csv
old mode 100644
new mode 100755
diff --git a/data/dataset_CANVAS_fr.xlsx b/data/dataset_CANVAS_fr.xlsx
old mode 100644
new mode 100755
diff --git a/data/dataset_demand_CANVAS_fr.xlsx b/data/dataset_demand_CANVAS_fr.xlsx
old mode 100644
new mode 100755
diff --git a/data/dataset_variation_CANVAS_fr.xlsx b/data/dataset_variation_CANVAS_fr.xlsx
old mode 100644
new mode 100755
diff --git a/demand/__init__.py b/demand/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/Demand.py b/demand/classic/Demand.py
old mode 100644
new mode 100755
similarity index 100%
rename from Demand.py
rename to demand/classic/Demand.py
diff --git a/demand/classic/__init__.py b/demand/classic/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/demand/mas/Demand.py b/demand/mas/Demand.py
new file mode 100755
index 0000000..46f7785
--- /dev/null
+++ b/demand/mas/Demand.py
@@ -0,0 +1,131 @@
+import pandas as pd
+import json
+from typing import List
+from ...agents.Agent import Agent
+from prophet import Prophet 
+from math import pi
+from math import cos, floor
+import pkgutil
+import csv
+
+
+def massive_divergence(demande, demande2):
+    return True
+
+class Demand(Agent):
+
+    def __init__(self, model=None, demand: float = 20, var_per_day: float = 0.1 , var_per_season: float = 0.1) -> None:
+        super().__init__()
+        if model is not None:
+            self.set_model(model)
+        self.set_type("Demand")
+
+        ### from old demand implementation
+        self.__var_per_day = var_per_day
+        self.__var_per_season = var_per_season
+        self.__mean_demand = demand
+        self.__pt = Prophet(seasonality_mode='multiplicative')
+        self.__periods = 12*20
+        self.data_demand = None
+    
+    ### COMMUNICATION
+    def __notify_demand_value_change(self):
+        signal = json.loads(open(self._code_files))["8080"]
+        signal["values"] = self.__demand
+        signal["t_from"] = 3
+        self._notify_observers(signal)
+
+    def set_demande_change(self, demand: List[float]) -> None:
+        errored_demand = self.__demand
+        self.__demand = demand
+        if massive_divergence(errored_demand, self.__demand):
+            self.__notify_demand_value_change()
+
+    ### PARAMETRIZATION
+    def set_model(self, model) -> None:
+        self.__model = model
+
+    def predict_demand(self, time_series: pd.DataFrame) -> List[float]:
+        self.__demand =  list(self.__model.predict(time_series))
+
+
+    ### TMP WITH PROPHET
+    def set_forecast_periods(self, periods) -> None:
+        self.__periods = periods
+    
+    def forecast_with_prophet(self, bind = None, raw_data = None, init_date: str = "2017-01-01", delimiter: str = ";", column: str = "Total Ventes"):
+        """
+            The method must get a dataset with at least 3 columns
+            - month : int, 
+            - year : int,
+            - the monthly demand in kwh (determinated by the parameter "column")
+            
+            The method also use a forcast model from prophet to predict future demand.
+            The periods can be set by set_forcast_periods.
+        """
+        if raw_data is not None :
+            data = pd.DataFrame(raw_data)
+            #set columns & index           
+            header = data.iloc[0]
+            data = data[1:]
+            data.columns = header
+            data = data.reset_index(drop=True)
+            for col in data.columns.tolist():
+                try:
+                    # convert numeric values
+                    data[col] = pd.to_numeric(data[col])
+                except:
+                    pass
+        else :
+            data = pd.read_csv(bind, delimiter = delimiter)
+
+        data["date"]= data["month"].astype("str") + "-" + data ["year"].astype("str")
+        data["datetime"] = pd.to_datetime(data["date"])
+        data_to_use = pd.DataFrame()
+        data_to_use[["datetime","demands"]] = data.loc[data["datetime"] >= init_date][["datetime",column]]
+        
+        #let's forecast
+        train = pd.DataFrame()
+        train[["ds","y"]] = data[["datetime",column]]
+        self.__pt.fit(train)
+        future = self.__pt.make_future_dataframe(periods = self.__periods, freq='MS')
+        fcst = self.__pt.predict(future)
+        fcst = fcst.loc[fcst["ds"] > data["datetime"].tail(1).item()]
+        fcst[["datetime","demands"]] = fcst[["ds","yhat"]]
+        self.data_demand = data_to_use.append(fcst,ignore_index=True)
+        
+        return self.data_demand
+	
+    def set_data_to(self, dataset, delimiter: str=";"):
+        if dataset == "Toamasina":
+            #by defaut we keep it "Toamasina"
+            data = pkgutil.get_data('mixsimulator', '/data/RIToamasina/DIR-TOAMASINA_concat.csv')
+            data = csv.reader(data.decode('utf-8').splitlines(), delimiter = delimiter)
+            self.forecast_with_prophet(raw_data=data)
+        else :
+            #by defaut we keep it "Toamasina"
+            data = pkgutil.get_data('mixsimulator', '/data/RIToamasina/DIR-TOAMASINA_concat.csv')
+            data = csv.reader(data.decode('utf-8').splitlines(), delimiter = delimiter)
+            self.forecast_with_prophet(raw_data=data)
+       
+    def get_demand(self, t):
+        self.data_demand.reset_index()
+        try :
+            return self.data_demand.iloc[t]["demands"]/1000 #cause data units in kwh
+        except IndexError :
+            print("Please check init_date or change the forcasting params : increase periods with Demand.set_forcast_periods")
+            raise
+
+    def set_mean_demand(self, demand: float):
+        self.__mean_demand = demand
+
+    def get_demand_approxima(self, t, interval):
+        demande = self.__mean_demand * (1 + cos(4 * pi * ( t * interval )/ 24)*self.__var_per_day + cos(2 * pi * ( t * interval ) / (24*365))* self.__var_per_season)
+        return demande
+        
+    def get_demand_monthly(self, t, interval, init : int = 0):
+        m = (t + init)/(24*30)
+        m = floor(m)
+        # for now we divide it by 30*24 (better approximation TO DO)
+        demande = (self.get_demand(m)/(30*24)) * (1 + cos(4 * pi * ( (t + init) * interval )/ 24)*self.__var_per_day)
+        return demande
\ No newline at end of file
diff --git a/demand/mas/__init__.py b/demand/mas/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/demand_simulator/demand_simulator.py b/demand_simulator/demand_simulator.py
deleted file mode 100644
index 2d1a138..0000000
--- a/demand_simulator/demand_simulator.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import pandas as pd
-import numpy as np
-from sklearn.linear_model import LinearRegression
-
-
-def get_accuracy(original_score, pred):
-    return ((original_score-pred)**2).sum()
-
-
-data = pd.read_csv("../data/RIToamasina/demand/toamasina_demand.csv")
-columns = ['MT/HT Abonnes', 'BT Abonnes', 'Total Abonnes', 'year', 'month']
-train = data.loc[data["year"]!=2017]
-test = data.loc[data["year"]==2017]
-
-X_train = train[columns] 
-y_1_train = train["Production Brute Totale"]
-y_2_train = train["Total Ventes"]
-
-X_test = test[columns]
-y_1_test = test["Production Brute Totale"]
-y_2_test = test["Total Ventes"]
-
-
-reg = LinearRegression().fit(X_train, y_1_train)
-
-pred = reg.predict(X_test)
-
-print(get_accuracy(pred, y_1_test))
\ No newline at end of file
diff --git a/documentation/Project_Documentation.pdf b/documentation/Project_Documentation.pdf
old mode 100644
new mode 100755
diff --git a/documentation/images/Evolution_coef_72h.png b/documentation/images/Evolution_coef_72h.png
old mode 100644
new mode 100755
diff --git a/get_branches.sh b/get_branches.sh
new file mode 100755
index 0000000..ff81ae4
--- /dev/null
+++ b/get_branches.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+for remote in `git branch -r`; do git branch --track ${remote#origin/} $remote; done
+
diff --git a/nevergradBased/Optimizer.py b/nevergradBased/Optimizer.py
old mode 100644
new mode 100755
index 146fbf3..8fca800
--- a/nevergradBased/Optimizer.py
+++ b/nevergradBased/Optimizer.py
@@ -76,7 +76,7 @@ def get_params(self) -> dict:
         return {"optimizer(s)" : self.__optimizers, "budget(s)" : self.get_budget(),
                 "parametrization" : self.get_parametrization(), "num_worker" : self.get_num_worker()}
     
-    def optimize(self, mix = None , func_to_optimize = None, constraints = None, step : int = 1):
+    def optimize(self, mix = None , func_to_optimize = None, constraints = None, step : int = 1, init : int = 0):
         
         #setting budgets
         budgets = self.get_budget()
@@ -97,7 +97,7 @@ def optimize(self, mix = None , func_to_optimize = None, constraints = None, ste
         #let's minimize
         for tmp_budget in range(0, total_budget):
             x = optimizer.ask()
-            loss = func_to_optimize(*x.args, constraints["time_interval"])
+            loss = func_to_optimize(*x.args, constraints["time_interval"],init = init)
             optimizer.tell(x, loss)
             if (tmp_budget+1)%step == 0:
                 result_per_budget = {}
@@ -108,15 +108,23 @@ def optimize(self, mix = None , func_to_optimize = None, constraints = None, ste
                 #Readjustment
                 if mix is not None :
                     usage_coef = mix.arrange_coef_as_array_of_array(recommendation.value)
-                    weighted_coef = mix.get_weighted_coef(usage_coef, time_interval=constraints["time_interval"])
+                    try :
+                        weighted_coef = mix.get_weighted_coef(usage_coef, time_interval=constraints["time_interval"], init = init)
+                    except:
+                        weighted_coef = mix.get_weighted_coef(usage_coef, time_interval=constraints["time_interval"])
                     production = 0
                     u_demand = 0
+                    carbon_prod = 0
                     for t in range(0, len(weighted_coef)):
                         production +=  mix.get_production_cost_at_t(weighted_coef[t], t, constraints["time_interval"])
-                        u_demand += mix.get_unsatisfied_demand_at_t(weighted_coef[t], t, constraints["time_interval"])
+                        try:
+                            u_demand += np.abs(mix.get_unsatisfied_demand_at_t(weighted_coef[t], t, constraints["time_interval"], init = init))
+                        except:
+                            u_demand += np.abs(mix.get_unsatisfied_demand_at_t(weighted_coef[t], t, constraints["time_interval"]))
+                        carbon_prod += mix.get_carbon_production_at_t(weighted_coef[t], constraints["time_interval"])
                     result_per_budget.update({"production": production})
                     result_per_budget.update({"unsatisfied demand": u_demand})
-                    result_per_budget.update({"carbon production": mix.get_carbon_production_at_t(weighted_coef[t], constraints["time_interval"])})
+                    result_per_budget.update({"carbon production": carbon_prod})
                 result_per_budget.update({"elapsed_time": time.time() - start_time})
                 result.append(result_per_budget)
                 
diff --git a/nevergradBased/__init__.py b/nevergradBased/__init__.py
old mode 100644
new mode 100755
diff --git a/params_files/exchange_code.json b/params_files/exchange_code.json
new file mode 100755
index 0000000..df1e7af
--- /dev/null
+++ b/params_files/exchange_code.json
@@ -0,0 +1,36 @@
+{
+    "100" : {
+        "type" : "register",
+        "id" : null,
+        "t_from": null,
+        "code": 100
+    },
+    "200" : {
+        "status" : "OK",
+        "type" : "health",
+        "id" : null,
+        "t_from": null,
+        "code" : 200
+    },
+    "400" : {
+        "status" : "DOWN",
+        "type" : "health",
+        "id" : null,
+        "t_from": null,
+        "code" : 400
+    },
+
+    "101" : {
+        "type" : "disponibility",
+        "value" : null,
+        "id" : null,
+        "t_from": null,
+        "code" : 101
+    },
+
+    "8080" : {
+        "type" : "demand_change",
+        "t_from" : null,
+        "values" : null
+    }
+}
\ No newline at end of file
diff --git a/params_files/settings.json b/params_files/settings.json
new file mode 100755
index 0000000..fdb14dc
--- /dev/null
+++ b/params_files/settings.json
@@ -0,0 +1,12 @@
+{
+    "power_plant_environement":
+    {
+        "simple_weather": {
+            "url": "https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid=3159336af6f662a719a184f1bb722347"
+        },
+        "precipitation": {
+            "url": "https://maps.openweathermap.org/maps/2.0/weather/1h/PR0/{zoom}/{x_tile}/{y_tile}?appid=3159336af6f662a719a184f1bb722347"
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/power_plants/__init__.py b/power_plants/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/centrals/HydroCentral.py b/power_plants/classic/HydroCentral.py
old mode 100644
new mode 100755
similarity index 100%
rename from centrals/HydroCentral.py
rename to power_plants/classic/HydroCentral.py
diff --git a/centrals/PowerCentral.py b/power_plants/classic/PowerCentral.py
old mode 100644
new mode 100755
similarity index 100%
rename from centrals/PowerCentral.py
rename to power_plants/classic/PowerCentral.py
diff --git a/power_plants/classic/__init__.py b/power_plants/classic/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/power_plants/mas/Hydropowerplant.py b/power_plants/mas/Hydropowerplant.py
new file mode 100755
index 0000000..4e1c144
--- /dev/null
+++ b/power_plants/mas/Hydropowerplant.py
@@ -0,0 +1,73 @@
+from .PowerPlant import PowerPlant
+from math import cos, pi
+
+class Hydropowerplant(PowerPlant):
+    """
+        Agent simulating a Hydroelectic facility
+    """
+    def __init__(self, hauteur: float, moyenne_apport: float, capacity: float, available_stock: float, var_per_day: float, var_per_season: float ) -> None:
+        super().__init__()
+        self.set_type("Hydropowerplant")
+        self.__hauteur = hauteur
+        self.__moyenne_apport = moyenne_apport # m3/s
+        self.__capacity = capacity # m3
+        self.__init_stock = available_stock
+        self.__available_stock =  [available_stock]# m3
+        self.__tuneable = True
+        self.__var_per_day = var_per_day
+        self.__var_per_season = var_per_season
+
+    def isHydro(self) -> bool:
+        return True
+
+    def __get_natural_availability(self, t, init : int = 0) -> float:
+        debit_t = self.__moyenne_apport * (1 + (cos(2 * pi * ( t + init )/ 24))* self.__var_per_day + (cos(2 * pi * ( t + init ) / (24*365)))* self.__var_per_season)
+        power = (self.__hauteur * debit_t * 9.8 * 0.9 * 997)/1000000 # unit is MW
+        if self._raw_power == 0.:
+            return 0.
+        else :
+            return power/self._raw_power
+
+    def __get__artificial_availability(self, t, init : int = 0) ->float:
+        debit_t_max = self.__get_available_stock(t + init)/(3600)
+        power = (self.__hauteur * debit_t_max * 9.8 * 0.9 * 997) /1000000 # unit is MW
+        if self._raw_power == 0.:
+            return 0.
+        else :
+            return power/self._raw_power
+
+    def get_availability(self, t, init : int = 0) -> float:
+        dummy_availability = self.__get__artificial_availability(t, init = init) + self.__get_natural_availability(t, init = init)
+        if dummy_availability > 1:
+            dummy_availability = 1
+        return dummy_availability
+
+    def __get_available_stock(self, t):
+        return self.__available_stock[t]
+
+    def back_propagate(self, usage_coef, t, interval, init : int = 0):
+        super().back_propagate(usage_coef, t, interval, init = init)
+        diff = usage_coef - self.__get_natural_availability(t, init = init)
+        diff_power = diff * self._raw_power
+        # back to m3/s so diff_power has to be in W not in MW ===> *1000
+        bandwidth = (diff_power* 1000000) / (9.8*self.__hauteur* 0.9 * 997)  
+        self.__update_stock(bandwidth, interval, t, init = init)
+
+    def __update_stock(self, bandwidth, interval, t, init : int = 0):
+        # bandwidth can be either positive or negative
+        #### if positiv, we have used stocked water
+        #### if not the stock has been increased
+        current_availability = self.__available_stock[t + init]
+        current_availability += -bandwidth*(interval*3600)
+        if current_availability > self.__capacity:
+            current_availability = self.__capacity
+        if init == 0:
+            self.__available_stock.append(current_availability)
+        else :
+            self.__available_stock[t+init] = current_availability
+
+    def get_stock_evolution(self):
+        return self.__available_stock
+
+    ### TODO : add/improve prediction function, data monitoring and signal system
+
diff --git a/power_plants/mas/PowerPlant.py b/power_plants/mas/PowerPlant.py
new file mode 100755
index 0000000..d5ca9a7
--- /dev/null
+++ b/power_plants/mas/PowerPlant.py
@@ -0,0 +1,202 @@
+import pandas as pd
+import sklearn.linear_model as linear_model
+import json, math
+from typing import List, Dict
+from ...agents.Agent import Agent
+import nevergrad as ng
+
+class PowerPlant(Agent):
+    """
+        Agent simulating all the common functions of a power plant
+    """
+    def __init__(self, tuneable:bool=False) -> None:
+        super().__init__()
+        self.__api_setting = json.load(open("params_files/settings.json"))
+        self.__changeRate = 0. #(percent)
+        self.__initial_value = 0.
+        self.__lifetime = 0 #in hour
+        self.__carbon_prod = 0. #g/MWh
+        self._raw_power = 0. #MW
+        self._original_raw_power = 0.
+        self.__availability = 1.  #of the source
+        self.__nb_employes = 1
+        self.__mean_salary = 0. #per month
+        self.__tuneable = tuneable
+        self.__fuel_cost = 0. #$/g
+        self.__fuel_consumption = 0. #g/MWh
+        self.__init_cur_usage = 0
+        self.__cur_usage = 0
+        self.__max_var = 1
+        self.__lower = 0.
+        self.__upper = 1.
+        self.__choices = None
+        self.__lat = None
+        self.__long = None
+        self.__x_tile = None
+        self.__y_tile = None
+        self.__zoom = None
+
+    ### ENVIRONEMENT MONITORING & MODELISATION
+    def set_location(self, location: Dict, zoom: int = 2) -> None:
+        self.__lat = location["lat"]
+        self.__long = location["long"]
+        lat_rad = math.radians(self.__lat)
+        n = 2.0 ** zoom
+        self.__x_tile = int((self.__long + 180.0) / 360.0 * n)
+        self.__y_tile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
+        self.__zoom = zoom
+        self._schedule_action({self.get_environement, 60})
+
+    def get_location(self) -> Dict:
+        return {"lat": self.__lat, "long": self.__long, "x_tile": self.__x_tile, "y_tile": self.__y_tile, "zoom": self.__zoom}
+
+    def get_environement(self):
+        url = self.__api_setting["power_plant_environement"]["simple_weather"]["url"]
+        ### TO DO
+        # FETCH FROM API
+        print("getting env")
+
+    ### COMMUNICATION
+    def _notify_is_up(self, t_from=0) -> None:
+        signal = json.load(open(self._code_files))["200"]
+        signal["id"] = self.get_id()
+        signal["t_from"] = t_from
+        self._notify_observers(signal)
+
+    def _notify_is_down(self, t_from=0) -> None:
+        signal = json.load(open(self._code_files))["400"]
+        signal["id"] = self.get_id()
+        signal["t_from"] = t_from
+        self._notify_observers(signal)
+
+    def _notify_disponibility(self) -> None:
+        pass
+
+    ### PARAMETRIZATION
+    def set_init_cur_usage(self, init_usage):
+        self.__init_cur_usage = init_usage
+        self.__cur_usage = init_usage
+
+    def set_max_var(self, max_var):
+        self.__max_var = max_var
+
+    def set_nb_employees(self, nb_employees):
+        self.__nb_employes = nb_employees
+
+    def set_initial_value(self, initial_value):
+        self.__initial_value = initial_value
+
+    def set_lifetime(self, lifetime):
+        self.__lifetime = lifetime
+
+    def set_fuel_consumption(self, fuel_consumption):
+        self.__fuel_consumption = fuel_consumption
+
+    def get_fuel_consumption(self):
+        return self.__fuel_consumption
+
+    def set_fuel_cost(self, fuel_cost):
+        self.__fuel_cost = fuel_cost
+
+    def get_fuel_cost(self):
+        return self.__fuel_cost
+
+    def get_amortized_cost(self, time_index):
+        if time_index > self.__lifetime * 365 * 24:
+            return 0
+        else:
+            return ( self.__initial_value / ( self.__lifetime * 365 * 24 ) ) 
+
+    def is_tuneable(self) -> bool:
+        #is controlled or not
+        return self.__tuneable
+
+    def get_carbon_production(self) -> float: # g/MWh
+        return self.__carbon_prod
+
+    def set_carbon_prod(self, carbonCost: float=0) -> None:
+        self.__carbon_prod = carbonCost
+
+    def set_raw_power(self, rawPower):
+        self._raw_power = rawPower
+
+    def get_raw_power(self) -> float: # MW
+        return self._raw_power
+
+    def get_availability(self, time_index, init : int = 0) -> float: # percent
+        return self.__availability
+
+    def reset_powerplant(self):
+        self.__cur_usage = self.__init_cur_usage
+
+    def get_max_availability(self, time_index, init : int = 0) -> float:
+        theorical_availability = self.get_availability(time_index, init = init)
+        if self.__cur_usage + self.__max_var <= theorical_availability:
+            theorical_availability =  self.__cur_usage + self.__max_var
+        return theorical_availability
+
+    def get_min_availability(self) -> float:
+        theorical_availability = 0
+        if self.__cur_usage - self.__max_var >= theorical_availability:
+            theorical_availability =  self.__cur_usage - self.__max_var
+        return theorical_availability
+
+    def back_propagate(self, usage_coef, t, time_interval, init : int = 0):
+        self.__cur_usage = usage_coef
+        
+    def set_availability(self, availability: float):
+        self.__availability = availability
+
+    def get_employees_salary(self, total_working_time_per_day=8) ->float:
+        #mean salary per hour
+        perHourMeanSalary = self.__mean_salary/(31*total_working_time_per_day)
+        return perHourMeanSalary * self.__nb_employes
+
+    def set_mean_employees_salary(self, mean_salary):
+        self.__mean_salary = mean_salary
+
+    def set_tuneable(self, tuneable: bool) -> None:
+        self.__tuneable = tuneable
+
+    def shutdown(self) -> None:
+        self._original_raw_power = self._raw_power
+        self._raw_power = 0.
+
+    def power_on(self) -> None:
+        self._raw_power = self._original_raw_power
+
+    def __getUsageCoef(self, usage_coef: float) -> None:
+        if(self.__tuneable):
+            usage_coef = min(self.__availability, usage_coef)
+        else:
+            usage_coef = self.__availability
+            
+    def set_variation_params(self, lower: float, upper : float, choices : List[float] = None) -> None:
+        self.__lower = lower
+        self.__upper = upper
+        self.__choices = choices
+        
+    def get_variation_params(self) -> ng.p.Choice:
+        final_params = []        
+        if self.__choices != None :
+            if self.__lower == self._upper :
+                return ng.p.Choice(self.__choices)
+            else :
+                for low, up in zip(self.__lower, self.__upper):
+                    if low == up : continue
+                    final_params.append(ng.p.Scalar(lower=low ,upper=up))
+                discret = ng.p.Choice(self.__choices)
+                final_params.append(discret)
+                params = ng.p.Choice(final_params)
+                return params
+            
+        else :
+            if self.__lower != 0. and self.__upper != 1. :
+                for low, up in zip(self.__lower, self.__upper):
+                    final_params.append(ng.p.Scalar(lower=low ,upper=up))
+                params = ng.p.Choice(final_params)
+                return params 
+            else :
+                final_params.append(ng.p.Scalar(lower=self.__lower ,upper=self.__upper))
+                params = ng.p.Choice(final_params)
+                return params
diff --git a/power_plants/mas/Thermalpowerplant.py b/power_plants/mas/Thermalpowerplant.py
new file mode 100755
index 0000000..188959c
--- /dev/null
+++ b/power_plants/mas/Thermalpowerplant.py
@@ -0,0 +1,15 @@
+from .PowerPlant import PowerPlant
+
+class Thermalpowerplant(PowerPlant):
+    """
+        Agent simulating a fuel, nuclear or solar thermal power plant
+    """
+
+    def __init__(self):
+        super().__init__()
+        self.set_type("Thermalpowerplant")
+
+    def __get_water_availability(self):
+        pass
+    
+    ### TODO : add/improve prediction function, data monitoring and signal system
\ No newline at end of file
diff --git a/power_plants/mas/__init__.py b/power_plants/mas/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/try_mas.sh b/try_mas.sh
new file mode 100755
index 0000000..9f7fdc3
--- /dev/null
+++ b/try_mas.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+cp test_mas.py ../
+python3.9 ../test_mas.py
\ No newline at end of file


Test patch:
diff --git a/test.py b/test_classic.py
similarity index 87%
rename from test.py
rename to test_classic.py
index db9889b..57ace03 100755
--- a/test.py
+++ b/test_classic.py
@@ -1,7 +1,7 @@
 import mixsimulator.MixSimulator as ms 
 import mixsimulator.nevergradBased.Optimizer as opt	
 from mixsimulator.Evaluation import EvaluationBudget
-import mixsimulator.Demand as dm
+from .demand.classic.Demand import Demand
 
 """ 
 (1) Configure nevergrad optimizers 
@@ -56,7 +56,7 @@
         https://github.com/Foloso/MixSimulator/tree/master/data/RIToamasina/demand
 
 """
-demand = dm.Demand()
+demand = Demand()
 data_demand = demand.set_data_csv("data/RIToamasina/DIR-TOAMASINA_concat.csv", delimiter = ",")
 """
     The method must get a dataset with at least 3 columns
@@ -98,8 +98,8 @@
     --> each result is a dict of "loss", "coef", "production", "unsatisfied demand", "carbon production" and "elapsed_time"
 
 """
-print(mix.optimizeMix(99999999999999999999,optimizer = opt_CMA, step = 20, penalisation = 100, carbon_cost = 0, time_index = 168, plot = "None"),"num_worker <------------ 1")
-print(mix.optimizeMix(99999999999999999999,optimizer = opt_CMA_30, step = 20, penalisation = 100, carbon_cost = 0, time_index = 168, plot = "None"),"num_worker <------------ 30")
+print(mix.optimizeMix(1e10,optimizer = opt_CMA, step = 20, penalisation = 100, carbon_cost = 0, time_index = 168, plot = "None"),"num_worker <------------ 1")
+print(mix.optimizeMix(1e10,optimizer = opt_CMA_30, step = 20, penalisation = 100, carbon_cost = 0, time_index = 168, plot = "None"),"num_worker <------------ 30")
 ### Get all parameters used by the mix 
 print(mix.get_params())
 
@@ -122,4 +122,4 @@
     carbon_cost: float = 0
 """
 eva=EvaluationBudget()
-eva.evaluate(mix,10,1000,optimizer_list = ["OnePlusOne","DE","CMA","PSO","NGOpt"], indicator_list = ["loss","elapsed_time","production","unsatisfied demand","carbon production"],carbonProdLimit = 9999999999999, time_index = 12, penalisation = 100, carbon_cost = 10)
+eva.evaluate(mix,10,100,optimizer_list = ["OnePlusOne","DE","CMA","PSO","NGOpt"], indicator_list = ["loss","elapsed_time","production","unsatisfied demand","carbon production"],carbonProdLimit = 1e10, time_index = 24, penalisation = 100, carbon_cost = 10)
diff --git a/test_mas.py b/test_mas.py
new file mode 100755
index 0000000..3a7f089
--- /dev/null
+++ b/test_mas.py
@@ -0,0 +1,171 @@
+import threading
+from MixSimulator import ElectricityMix
+from MixSimulator.Evaluation import EvaluationBudget
+import MixSimulator.nevergradBased.Optimizer as opt
+import time
+from MixSimulator.agents.Moderator import StoppableThread
+from typing import List, Dict
+import random
+import numpy
+
+"""
+    (0) Check the thread running in background
+"""
+def generate_random_scenario(centrals: List, time_index: int) -> Dict:
+    scenario = {}
+    for central in centrals:
+        tmp = {"down":[], "up":[]}
+        default_proba = random.uniform(0, 0.2)
+        
+        for i in range(time_index):
+            tmp[numpy.random.choice(["up", "down"], p=[1-default_proba,default_proba])].append(i)
+
+        up = []
+        down = []
+        for i in range(time_index):
+            if i in tmp["down"] and (i-1 not in tmp["down"] or i==0):
+                down.append(i)
+            if i-1 in tmp["down"] and i in tmp["up"]: 
+                up.append(i)
+        tmp["up"] = up
+        tmp["down"] = down
+
+        scenario.update({central:tmp.copy()})
+    
+    event_stack = {}
+    for i in range(time_index):
+        for central in scenario.keys():
+            if i in scenario[central]["down"]:
+                try:
+                    event_stack[i].append(central._notify_is_down)
+                except:
+                    event_stack.update({i:[central._notify_is_down]})
+            elif i in scenario[central]["up"]:
+                try:
+                    event_stack[i].append(central._notify_is_up)
+                except:
+                    event_stack.update({i:[central._notify_is_up]})
+
+
+    # print(numpy.arange(0, 2))
+    print("scenario: ", event_stack)
+    return event_stack
+
+def check_thread_running():
+    list_ = []
+    while True:
+        tmp = threading.enumerate().copy()
+        if tmp != list_:
+            list_ = tmp
+            for thread in list_:
+                if thread.is_alive():
+                    print("THREAD:  " + thread.name)
+        time.sleep(10)
+
+thread_checker = StoppableThread(target=check_thread_running, name="thread_checker")
+thread_checker.start()
+
+""" 
+(1) Configure nevergrad optimizers 
+
+    Default Parameters :
+    ----------
+    opt = [ng.optimizers.OnePlusOne], 
+    budget: List[int] = [100], 
+    num_worker: int = 1, 
+    instrum = ng.p.Array(shape=(2,))
+"""
+# opt_OPO = opt.Optimizer(opt = ["OnePlusOne"], budget = [20], num_worker = 1) 
+opt_OPO_20 = opt.Optimizer(opt = ["OnePlusOne"], budget = [20], num_worker = 30)
+
+""" 
+(2) Init MixSimulator instance :
+    Case one [Default] : "classic" method (see test_classic.py for more use case)
+    Case two : "MAS" or Multi Agent System method
+
+    Default parameters :
+    ------------------------
+    method : string = "classic",    --> method explain above
+    carbon_cost : float = 0         --> cost of the CO2 production 
+    penalisation_cost: float = 1e7  --> penalisation cost when production is more or less than the demand #NEED VERIFICATION
+"""
+
+# classic_mix = ElectricityMix.mix(method="classic",carbon_cost=0,penalisation_cost=100) 
+mas_mix = ElectricityMix.mix(method="MAS",carbon_cost=0,penalisation_cost=100)
+
+""" 
+(7) ONE SHOT optimization by calling the moderator of the MAS platform
+    
+    Default parameters :
+    ---------------------------
+    carbon_quota: float = None,     --> carbon limitation (example : 99999999999999999999)
+    demand: Demand = None,          --> you can use it to fix a constant demand for each time step (eliminates (5))
+    lost: float = None,             --> you can use it to fix a constant lost for each time step
+    optimizer: Optimizer = None,    --> nevergrad optimizer initiate in (1)
+    step : int = 1,                 --> the number of step of budget where each time the candidates are registered
+    time_index: int = 24*7,         --> duration over which we optimize (in hour)
+    time_interval: float = 1,       --> the number of step of time where we evaluate the loss function (by default each 1 hour)
+    penalisation : float = None,    --> penalisation cost 
+    carbon_cost : float = None,     --> penalisation cost of carbon_quota
+    plot : str = "default",         --> write "None" for no plot
+    average_wide : int = 0          --> average_wide of moving average plot parameter
+    
+    Output: 
+    -------
+    List of results in each step of budget (here step = 20, that means only one step cause budget is set to 20)
+    --> each result is a dict of "loss", "coef", "production", "unsatisfied demand", "carbon production" and "elapsed_time"
+
+"""
+# print(mas_mix.get_moderator().optimizeMix(1e10,optimizer = opt_OPO, step = 20, penalisation = 100, carbon_cost = 0, time_index = 168, plot = "default"))
+
+
+""" 
+(8) Evaluation of results by budget for each selected optimizer
+    Default parameters :
+    --------------------
+    mix (or moderator),                     --> the mix or moderator to evaluate
+    sequence,                               --> each budget to evaluate
+    max_budgets, 
+    optimizer_list: List['str'],    
+    indicator_list: List['str'],            --> indicators are ["loss","elapsed_time","production","unsatisfied demand","carbon production"]
+    num_worker: int = 1, 
+    bind: str = None,                       --> path to dataset
+    time_index: int = 24, 
+    carbonProdLimit: float = 39500000000,   --> equal to carbon_quota
+    time_interval : float = 1, 
+    average_wide : int = 0, 
+    penalisation : float = 1000000000000,   --> equal to penalisation cost
+    carbon_cost: float = 0
+"""
+# eva=EvaluationBudget()
+# eva.evaluate(mas_mix.get_moderator(),10,100,optimizer_list = ["OnePlusOne","DE","CMA","PSO","NGOpt"], indicator_list = ["loss","elapsed_time","production","unsatisfied demand","carbon production"],carbonProdLimit = 1e10, time_index = 24, penalisation = 100, carbon_cost = 10)
+
+"""
+(9) Simulating the mas platform (Manually)
+        1 - First, set params by using set_params method
+        2 - Run the run_optimization method to initiate the simulation
+        3 - Add events
+"""
+mas_mix.get_moderator().set_params(1e10,optimizer = opt_OPO_20, step = 20, penalisation = 100, carbon_cost = 0, time_index = 4, plot = "save")
+mas_mix.get_moderator().run_optimization()
+
+# centrale1 = mas_mix.get_moderator().get_observable()[0]
+# centrale2 = mas_mix.get_moderator().get_observable()[1]
+
+# centrale1._notify_is_down(6)
+# centrale1._notify_is_up(12)
+centrals = mas_mix.get_moderator().get_observable()
+scenario = generate_random_scenario(centrals, 4)
+for t in scenario.keys():
+    for event in scenario[t]:
+        event(t)
+
+while True:
+    if len(threading.enumerate()) == 2:
+        thread_checker.stop()
+        break
+print("SIMULATION DONE")
+
+print("FINAL RESULT: ", mas_mix.get_moderator().get_results())
+mas_mix.get_moderator().plotResults(mas_mix.get_moderator().get_results())
+