/*
Program     : Percobaan Kontroler NN 2 menggunakan Arduino Mega
DiBuat Oleh : EruP-RMK
Keluaran    : v1.00
Tanggal     : 01 Juli 2005 - NN Versi MCS52
              05 Desember 2008 - NN Versi AVR ATmega8
              30 Oktober 2019 - NN Versi Arduino
              12 September 2018 - 8 Agustus 2020 - Observasi Plant
              17 Nopember 2020 - 19 Nopember 2020 - NN + Observasi Plant
Untuk       : Praktikum Kontrol Cerdas 2 POLITEKNIK ELEKTRONIKA NEGERI SURABAYA (PENS)

Perangkat   : Arduino Mega, Plant Emulasi, Koneksi ADC Emulasi dan Pin Port I/O (sebagai PWM) Emulasi
*/

const int MOTOR=3;                                // Nomor PIN untuk output PWM (emulasi PWM oleh Arduino)
const int SENSOR=A0;                              // Nomot PIN ADC

//float Kecepatan=0, SettingPoint=0, PWM=0, adc;
//float out_plant=0;
//float alfa=0.2;                                   // semakin kecil Alfa, TM semakin besar
//float Load=0;                                     // Pembebanan, 0% = tanpa beban, 100% = beban maksimal
//int   Periode=200;                                // panjang periode (Setting Point on/off) dalam satuan sampling, Periode=0 -> tidak periodik
//int   Random=1;                                   // nilai Setting Point acak
//int   Index=0;                                    // penghitung sampling (Loop)
//float SetP=0;                                     // BackUp Setting Point
//float VinMin=0.25;                                // Tegangan plant minimal, antara 0 volt sampai 5 volt
//int   RpmMax=2000;                                // Display Max
float Error, dE, eL, MSE=2e5;
/*
float plant(float vin)                            // Plant Emulasi
{
  static float v1=0, v2=0;
  vin-=VinMin; if(vin<0) vin=0;                   // dilengkapi dengan titik nyala minimal
  v1=vin*.8+v1*.2;
  v2=v1*.5+v2*.5;
  return v2*alfa+out_plant*(1-alfa);
}

int _analogRead(int ch)                           // ADC Emulasi
{
  return out_plant*1023/5*(1.0-Load);             // VRef 5 Volt resolusi 10 bit
}

void _analogWrite(int pin, int PWM)               // DAC Emulasi
{
  out_plant = plant(PWM*5.0/255);                 // VRef 5 Volt resolusi 8 bit
}
*/

/*
Adaptive Polinomial
y=ax5+bx4+cx3+dx2+ex+f
*/

#define ORDE 6
float K[ORDE+1]={0,0,0,0,0,0,0};

float Poly(float x)
(
  float Z=1;
  float P=0;
  for(i=0;i<=ORDE;i++)
  {
    P+=K[i]*Z;
    Z*=Z;
  }
)

void UpdatePoly(float x, float e)
{
  #define LR 0.01
  float Z=1;
  float P=0;
  for(i=0;i<=ORDE;i++)
  {
    K[i]+=LR*e*Z;
    Z*=Z;
  }
}

/*
Demo NN, SImple NN dengan
- 1 Input Node
- 1 LApis Hidden Layer, 5-beberapa Node
- 1 Output Neuron
- Domain Data [0,1]
*/

#define nInput          2
#define MaxHidden       50                              // Max Jumlah Node/Neuron pada Hidden Layer
float LearningRate    = 0.06;// jangan 5, terlalu besar    // dapat diubah dengan perintah "R0.5;"
unsigned char nHidden = 25;                                  // dapat diubah dengan perintah "H10;"

float InNN[nInput];                                       // Data Masukan NN, 1 input
float OutNN;                                      // Data Keluaran NN, Output Neouron, setelah fungsi Aktifasi, 1 output
float NH[MaxHidden];                              // Keluaran Hidden Neuron, setelah Aktifasi
float eH[MaxHidden];                              // Error Propagasi Hidden Neuron, setelah Aktifasi
float BH[MaxHidden];                              // Bias dari Hidden Neuron
float WH[MaxHidden][nInput];                      // Bobot Masukan Hidden Neuron
float WO[MaxHidden];                              // Bobot Masukan dari Output Neuron
float BO;                                         // Bias Output Neuron
float ErrNN;                                      // Error untuk sistem update NN

float SkalaInput  = 1.0/200.0;
float SkalaOutput = 100;

void Init_NN(void)
{
  unsigned char i,h;
  for(h=0;h<nHidden;h++)
  {
    for(i=0;i<nInput;i++)
    {
      WH[h][i]=(float)random(-10000,10001)/10000.0;  // Bangkitkan random untuk tiap bobot, [-1,1]
//Serial.print(" W="); Serial.print((float)random(-1000,1001)/1000);
//Serial.print(" WH[h][i]="); Serial.print(WH[h][i]);
    }
    BH[h]=0;        // Nilai Awal Bias=0
    WO[h]=(float)random(-10000,10001)/10000.0;  // Bangkitkan random untuk tiap bobot, [-1,1]
//Serial.print(" WO[h]="); Serial.print(WO[h]);
  }
  BO=0;
}

float sigmoid(float x)
{
  if(x>5) return 1;                               // Untuk menghindarkan OverFlow,
  else if(x<-5) return 0;                         // Jika x>5 set y=1, Jika x<5, y=0
  else return (float)1/(1+exp(-x));               // Diantara itu, hitung normal
}

void HitungMaju(void)
{
  float zo,zh;
  unsigned char i,h;
  zo=0;
  for(h=0;h<nHidden;h++)
  {
    zh=0;

//Serial.print(" InNN[0]="); Serial.print(InNN[0]);
//Serial.print(" InNN[1]="); Serial.print(InNN[1]);

    for(i=0;i<nInput;i++)
    {
      zh+=InNN[i]*WH[h][i];

//Serial.print(" InNN[0]="); Serial.print(InNN[i]);
//Serial.print(" WH[h][0]="); Serial.print(WH[h][i]);
//Serial.print(" zh="); Serial.print(zh);
    }

    zh+=BH[h];



    NH[h]=sigmoid(zh);                  // Hitung keluaran masing-masing Hidden Neuron  
    zo+=NH[h]*WO[h];            // Sekaligus hitung masukan Sigma dari Output Neuron


  }

//Serial.print(" zo"); Serial.print(zo);

  OutNN=sigmoid(zo+BO);     // Hitung keluaran dari Output Neuron

//Serial.print(" OutNN"); Serial.print(OutNN);

}

void UpdateNN(void)
{
  unsigned char i,h;
  float eo, eh;
  eo=ErrNN*OutNN*(1-OutNN);             // temporary, biar cepat
  for(h=0;h<nHidden;h++)
  {
    eH[h]=WO[h]*eo;                     // selesaikan sebelum update WO
    WO[h]+=LearningRate*eo*NH[h];       // update bobot output
    eh=eH[h]*NH[h]*(1-NH[h]);           // temporary, biar cepat
    for(i=0;i<nInput;i++)
      WH[h][i]+=LearningRate*eh*InNN[i];
    BH[h]+=LearningRate*eh;
  }  
  BO+=LearningRate*eo;
}

void setup()                                      // Bagian yang pertama dikerjakan oleh Arduino, hanya satu kali
{
  Serial.begin(115200);                           // sesuaikan dengan serial pada program Monitor
  Serial.println(" ");
  randomSeed(analogRead(0));                      // atur nilai awal bilangan random, dari ADC
  //Init_NN();
}

#define NTabel 25

float Tabel[25][4]={
  //L   C   Perkiraan Banjir  Bahaya
  {175,175,24,16},  //1
  {175,150,23,12},  //1
  {175,100,22,8},   //1
  {175,50,21,4},    //1
  {175,25,20,0},    //1
  {150,175,19,12},  //1
  {150,150,18,9},   //1
  {150,100,17,6},   //1
  {150,50,16,3},    //1
  {150,25,15,0},    //1
  {100,175,14,8},   //5
  {100,150,13,6},   //5
  {100,100,12,4},   //5
  {100,50,11,2},    //5
  {100,25,10,0},    //5
  {50,175,9,4},     //9
  {50,150,8,3},     //9
  {50,100,7,2},     //9
  {50,50,6,1},      //9
  {50,25,5,0},      //9
  {25,175,4,0},     //9
  {25,150,3,0},     //9
  {25,100,2,0},     //9
  {25,50,1,0},      //9
  {25,25,0,0}};     //16

void Learning()
{
  #define TOL 0.1
  float MSE=1e30;
  float tMSE=1;
  int iterasi=0;
  //Init_NN();
  float LR=0.05;
  LearningRate=LR;
  while(tMSE>TOL&&iterasi<10000)
  {
    iterasi++;
    Serial.print("#");
    Serial.print(iterasi);
    MSE=0;
    for(int i=0;i<25;i++)
    {
      float L=Tabel[i][0];
      float C=Tabel[i][1];
      float K=Tabel[i][2];
      float B=Tabel[i][3];
      InNN[0]=L*SkalaInput;
      InNN[1]=C*SkalaInput;
      //Serial.print("L:"); Serial.print(L); Serial.print(" ");
      //Serial.print("C:"); Serial.print(C); Serial.print(" ");
      //Serial.print("K:"); Serial.print(K); Serial.print(" ");
      //Serial.print("B:"); Serial.print(B); Serial.print(" ");
      HitungMaju();
      //Serial.print("NN:"); Serial.print(OutNN*SkalaOutput); Serial.print(" ");
      ErrNN=B-OutNN*SkalaOutput;
      //Serial.print("e:"); Serial.print(ErrNN); Serial.println();
      MSE+=(ErrNN*ErrNN);
      //ErrNN=ErrNN*1/tMSE;
      UpdateNN();
    }
    //Serial.print(" MSEz=");
    //Serial.print(MSE,6);
    MSE=sqrt(MSE)/25;
    tMSE=0.9*tMSE+0.1*MSE;
    LearningRate=0.01/tMSE;
    Serial.print(" MSE=");
    Serial.println(MSE,6);
    //Serial.print(" tMSE=");
    //Serial.println(tMSE,6);
    //Serial.println();
  }
}

void Running()
{
  float MSE=0;
  MSE=0;
  for(int i=0;i<25;i++)
  {
    float L=Tabel[i][0];
    float C=Tabel[i][1];
    float K=Tabel[i][2];
    float B=Tabel[i][3];
    InNN[0]=L*SkalaInput;
    InNN[1]=C*SkalaInput;
    Serial.print("#:"); Serial.print(i+1); Serial.print(" ");
    Serial.print("L:"); Serial.print(L); Serial.print(" ");
    Serial.print("C:"); Serial.print(C); Serial.print(" ");
    Serial.print("K:"); Serial.print(K); Serial.print(" ");
    Serial.print("B:"); Serial.print(B); Serial.print(" ");
    HitungMaju();
    Serial.print("NN:"); Serial.print(OutNN*SkalaOutput); Serial.print(" ");
    ErrNN=B-OutNN*SkalaOutput;
    Serial.print("e:"); Serial.print(ErrNN); Serial.println();
    MSE+=(ErrNN*ErrNN);
  }
  Serial.print("MSEz=");
  Serial.print(MSE,6);
  MSE=sqrt(MSE)/25;
  Serial.print(" MSE=");
  Serial.println(MSE,6);
  Serial.println();
}

void CetakTabel()
{
  for(int i=0;i<25;i++)
  {
    Serial.print(i+1);
    Serial.print(" ");
    Serial.print(Tabel[i][0]);
    Serial.print(" ");
    Serial.print(Tabel[i][1]);
    Serial.print(" ");
    Serial.print(Tabel[i][2]);
    Serial.print(" ");
    Serial.print(Tabel[i][3]);
    Serial.println();

  }
}
/*
Perintah Serial
T           - Lihat Tabel
Tn L C K B  - Mengisi Tabel Prediksi, contoh T1 20 50 10 20
Ln          - Kalibrasi Level Ketinggian Air Sungai
P           - Perintah memulai Pelatihan NN
R           - Uji Running
R L C       - Uji Running NN, contoh R 20 30
I           - Inisialisasi
H           - Help
*/
void loop()                                 // Bagian yang dikerjakan secara terus-menerus oleh Arduino
{
  if(Serial.available())                    // Apakah ada penerimaan data serial dari PC ?
  {
    float L, C, K, B; 
    int n;
    switch(Serial.read())                   // akhiri setiap perintah dengan ';' contoh "s1000;"
    {
      case 't':                             // Tabel Presiksi
      case 'T':                             // Jika Ya, apakah data dimulai dengan huruf depan 's' ?
        n=Serial.parseInt();                // nomor baris Tabel
        if(n==0)
        {
          CetakTabel();
          break;
        }
        L=Serial.parseFloat();              // Level Ketinggian Sungai
        C=Serial.parseFloat();              // Curah Hujan
        K=Serial.parseFloat();              // Kemungkinan Banjir
        B=Serial.parseFloat();              // Bahaya
        Tabel[n][0]=L;
        Tabel[n][1]=C;
        Tabel[n][2]=K;
        Tabel[n][3]=B;
        CetakTabel();
        break;
      case 'L':
      case 'l':
        L=Serial.parseFloat();
        // S = Baca Sensor Ketinggian
        // Tinggi = Konstanta - S
        // Hitung, Konstanta = L + S
        break;
      case 'p':
      case 'P':                                   // Ubah Timing plant, "a0.0;" - "a1.0;" alfa 0.00 sampai alfa 1.00
        Learning();
        break;
      case 'r':
      case 'R':                                   // Ubah beban Plant, "l0;" - "l100;" load mati (0%) sampai load 100%
        L=Serial.parseFloat();
        if(L==0)
          Running();
        else
        {
          C=Serial.parseFloat();
          InNN[0]=L*SkalaInput;
          InNN[1]=C*SkalaInput;
          HitungMaju();
          Serial.println(OutNN);
        }
        break;
      case 'I':
      case 'i':
        Init_NN();
        break;
      case 'h':
      case 'H':                                   // Ubah Timing plant, "a0.0;" - "a1.0;" alfa 0.00 sampai alfa 1.00
        Serial.println("Perintah Serial");
        Serial.println("Tn L C K B  - Mengisi Tabel Prediksi, contoh T1 20 50 10 20");
        Serial.println("Ln          - Kalibrasi Level Ketinggian Air Sungai");
        Serial.println("P           - Perintah memulai Pelatian NN");
        Serial.println("R L C       - Uji Running NN, contoh R 20 30");
        Serial.println("I           - Inisialisasi/Hapus Pelatian");
        Serial.println("H           - Help");
        break;
    }
  }                                               // Misal data yang diterima, "s1000"<CR>
                                                  // Set Carriage Return pada terminal, agar saat enter, lansung dikirimkan CR  
  //if(Index==0)
  //  SettingPoint=(Random==0?SetP:random(0,RpmMax));
  //if(Periode>0&&Index*2==Periode)
  //  SettingPoint=(Random==0?0:random(0,RpmMax));

  //adc=_analogRead(SENSOR);                        // Baca tegangan sensor dari PORT ADC, tegangan sensor sebanding dengan kecepatan/keluaran PLANT
  //Kecepatan=adc*RpmMax/1023;                      // Konversi dari ADC ke Kecepatan (dari data biner ADC ke RPM)
  
  //Error=SettingPoint-Kecepatan;
  //dE=Error-eL;
  //eL=Error;
  //MSE=Error*Error*0.0002+MSE*0.9998;
  
  // ubah jumlah input (nInput) sesuai dengan yang diinginkan
  // contoh, hanya input error dan feedback kecepatan
  
  //satu input
  //InNN[0]=SettingPoint/RpmMax;
  //InNN[0]=Error/(2*RpmMax)+0.5;           // InNN=[0,1] normalisasi
  //InNN[0]=dE/(2*RpmMax)+0.5;              // InNN=[0,1] normalisasi
  //InNN[0]=Kecepatan/RpmMax;
  
  //dua input
  //InNN[0]=SettingPoint/RpmMax;
  //InNN[0]=Error/(2*RpmMax)+0.5;
  //InNN[1]=dE/(2*RpmMax)+0.5;              // InNN=[0,1] normalisasi
  //InNN[1]=Kecepatan/RpmMax;
  
  //tiga input
  //InNN[2]=SettingPoint/RpmMax;
  //InNN[0]=Error/(2*RpmMax)+0.5;           // InNN=[0,1] normalisasi
  //InNN[1]=dE/(2*RpmMax)+0.5;              // InNN=[0,1] normalisasi
  //InNN[1]=Kecepatan/RpmMax;
  
  // empat input
  //InNN[2]=SettingPoint/RpmMax;
  //InNN[0]=Error/(2*RpmMax)+0.5;           // InNN=[0,1] normalisasi
  //InNN[1]=dE/(2*RpmMax)+0.5;              // InNN=[0,1] normalisasi
  //InNN[3]=Kecepatan/RpmMax;

  
  //HitungMaju();                                   // Hitung dari InNN[0,1] ke OutNN[0,1]
  
  //ErrNN=Error/RpmMax;                             // Error untuk Update NN, nilai [-1,1], dari Error[-2000,2000]
  //UpdateNN();                                     // Dari ErrNN, lakukan Update secara OnLine

  //PWM=OutNN*255;                                  // Tentukan nilai PWM (data biner) berdasarkan Setting Point yang telah ditentukan
  //if(PWM<0) PWM=0; if(PWM>255) PWM=255;
  //_analogWrite(MOTOR,PWM);                        // Kirim ke PLANT melalui PORT I/O (sinyal PWM yang di-emulasi dalam bentuk program dalam Arduino)
 /*
  Serial.print("SetPoint=");                      // Tampilkan SetPoint ke Keterangan, SetPoint=1000
  Serial.print(SettingPoint,0);
  Serial.print(" Kecepatan=");                    // Tampilkan Kecepatan
  Serial.print(Kecepatan,0);
  Serial.print(" PWM=");
  Serial.print(PWM,0);
  Serial.print(" Error=");
  Serial.print(Error,0);
  Serial.print(" MSE=");
  Serial.print(MSE,0);
  Serial.print(" Max=");
  Serial.print(RpmMax);
  Serial.print(" Hidden=");
  Serial.print(nHidden);
  Serial.print(" LR=");
  Serial.println(LearningRate);

  Serial.print(SettingPoint,0);                   // Kirim data ke program monitor melalui komunikasi serial
  Serial.write(' ');
  Serial.print(Kecepatan,0);                      // Format Protokol: "1000 850 87 2000"<CR>
  Serial.write(' ');
  Serial.print(PWM,0);                            // tanda 0 artinya tidak ada nilai pecahan
  Serial.write(' ');                            
  Serial.print(Error>RpmMax?RpmMax:Error<0?0:Error,0);
  Serial.write(' ');                            
  Serial.print(MSE/100>RpmMax?RpmMax:MSE/100,0);
  Serial.write(' ');                            
  Serial.println(RpmMax);                         // akhir data serial, dengan karakter <LF-CR>
*/
  //if(Periode>0)                                   // Jika setting point dibuat periodik
  //{
  //  Index++;
  //  if(Index>=Periode) Index=0;
  //}
  
  delay(10);                                      // Atur waktu sampling
}