Je reprends le clavier pour cette nouvelle version du régulateur. Le principe reste le même, à savoir :
- Le chauffage est arrêté (ou mis hors-gel) ET le régulateur stoppé en cas d’absence prolongée (par exemple : journée de travail, la nuit au rez-de-chaussée inoccupé d’une maison dont les chambres sont à l’étage etc…).
- Le régulateur est lancé AVANT le début d’occupation (prévoir le temps nécessaire pour atteindre la température de consigne), il est ensuite exécuté chaque minute.
- J’ai supprimé la détection de dégivrage, mais conservé la ré-initialisation sur changement de consigne (À ÉVITER pendant la phase d’auto-régulation décrite ci-après).
Le script PID est maintenant multi-pièces (paramètre interne « $nb_rooms ») et multi-splits (paramètre interne « $nb_splits »). - On aura toujours un seul capteur de température par pièce (faisant office de thermostat), mais il peut y avoir plusieurs splits pour une même pièce.
- On utilise toujours des télécommandes IR z-wave (en principe une par split), avec leurs périphériques de contrôle créés à l’inclusion …
- Comme précédemment, on devra créer manuellement un périphérique « état » de type
« liste de valeurs » pour stocker la consigne de référence « $idcons ».
Les paramètres d’appel du script sont modifiés comme suit :- &start=1 (initialisation au démarrage du régulateur) ou 0 (par la suite) => inchangé
- &idtemp1=xxxxxx …&idtempn=xxxxxx (codes API des capteurs de température)
- &aw=0.20&lag=0.75&tol=0.3 => inchangés
- &stallpso=x (par exemple 20) => limite le nombre de combinaisons PID à tester (ci-après)
Le script est maintenant « auto-régulé », c'est-à-dire que, pendant la phase initiale de réglage, différentes combinaisons des paramètres ‘kp’, ‘ki’, ‘kd’ sont automatiquement testées et évaluées par le programme (*). Le nombre de tests infructueux autorisé est inférieur ou égal à «stallpso » et le compteur est réinitialisé à chaque succès (voir « itae »). On devra créer manuellement DEUX PÉRIPHÉRIQUES « état » de type « texte » pour chaque pièce (n) :- « PSO(n) » stocke des combinaisons PID semi-aléatoires calculées par un nouveau script (algorithme de type PSO « Particle Swarm Optimisation »), qui seront testées à l’exécution suivante du script principal. Il s’agit donc d’un processus en 2 temps.
- « PID(n) » stocke les meilleures combinaisons testées au format « kp;ki;kd;itae ». La dernière valeur « itae » est un indice de performance calculé automatiquement par le script. Après un nombre pré-défini d’essais d’optimisation exécutés SANS DIMINUTION notable (>10%) de l’indice, le script considère l’optimisation comme terminée et stoppe toute nouvelle tentative ; on pourra cependant à tout moment relancer le processus en augmentant la valeur &stallpso.
(*) Ces paramètres sont fondamentaux pour le bon fonctionnement du régulateur, mais ils sont délicats à obtenir …SCRIPT PID :
- Code : Tout sélectionner
<?
/*************************************************************************************************************************************
Ce script doit être lancé à intervalles constants (1 minute) une première fois au démarrage avec le paramètre &start=1, et ensuite
avec le paramètre &start=0. Tous les autres paramètres sont de type "Float"
*************************************************************************************************************************************/
function sdk_pid($seterr,$lag,$int,$delT,$Tx,$Ty) {
//function sdk_pid($seterr,$chg,$int,$delT,$Tx,$der,$Ty) {
/****** Réponse Proportionnelle ********/
$pidout1 = $seterr ;
/****** Réponse Intégrale ********/
if($Tx != 0.0) {
$int1 = $int * $delT ;
$int2 = $int1 / $Tx ;
} else {
$int2 = 0.0 ; // Pour la mise au point des paramètres
}
$pidout2 = $pidout1 + $int2 ;
/****** Réponse Dérivée ********/
// $der1 = $chg * $Ty ; // ou bien Single pole filter
$der1 = $lag * $Ty ; // Single pole filter
$der2 = $der1 / $delT ;
$pidout3 = $pidout2 + $der2 ;
return $pidout3 ;
}
function sdk_val($idper) {
// Lecture Valeur périphérique
$per = getValue($idper) ;
$id_val = $per["value"] ;
return $id_val ;
}
function sdk_chkmaj($idper) {
$arr1 = getValue($idper) ;
$maj = $arr1["change"] ; // Date-heure de la dernière mise à jour
$upd = strtotime($maj) / 60 ; // Au format UNIX (minutes)
$ref = strtotime("now") / 60 ; // Au format UNIX (minutes)
$delta = round($ref - $upd) ; // Temps écoulé en minutes depuis la mise à jour
return $delta ;
}
function sdk_prop($temp,$cons,$seuil) {
/****** Réponse Proportionnelle ********/
if($temp <= $cons + $seuil) {
$seterror = $temp - $cons ;
} else {
$seterror = $seuil ; // On limite l'erreur si la pièce chauffe toute seule par les vitrages
}
return $seterror ;
}
function sdk_spf($temp,$deriv,$lagcut,$lagstate) {
/****** Réponse dérivée : Single pole filter ****************/
//$lagstate = (1.0-$lagcut)*$lagstate + $lagcut*$chg ;
//$chg = $seterr - $deriv ; // Si la consigne est constante ou évolue régulièrement
$chg = $temp - $deriv ; // Si la consigne change rapidement
$x_lagcut = 1.0 - $lagcut ;
$lag = $x_lagcut * $lagstate ;
$cut = $lagcut * $chg ;
$state = $lag + $cut ; // $lagstate
return $state ;
}
function sdk_int($out,$max,$min,$integral,$seterr,$anti) {
//Soft Integrator Anti-Windup
if($out >= $max or $out <= $min) {
$seterr_2 = $anti * $seterr ;
$int = $integral + $seterr_2 ;
} else {
$int = $integral + $seterr ;
}
return $int ;
}
function sdk_out($out,$max,$min) {
//Ajustements aux limites
if($out >= $max) {
$out = $max ;
} else if ($out <= $min) {
$out = $min ;
} else {
$out = $out ;
}
return $out ;
}
function sdk_cut($chaine) {
// Parse PID coefficients (UNNORMALIZED)
$pd = explode(";", $chaine) ;
$p = $pd[0] ; // P
$it = $pd[1] ; // I
$d = $pd[2] ; // D
$fit = $pd[3] ; // ITAE index
return array(1=>$p, 2=>$it, 3=>$d, 4=>$fit) ;
}
function sdk_fitness($itae1, $d, $e) { // ITAE : Integral of time * absolute value of error
$itae2 = $itae1 + ($d * abs($e)) ;
return $itae2 ;
}
/***************************** Paramètres *******************************/
$init = getArg('start') ; // Indicateur d'initialisation de l'intégration
/* http://www.mstarlabs.com/apeng/techniques/pidsoftw.html
Soft Integrator Anti-Windup : As it cures the windup problem, the integral clamping strategy sometimes delays desirable
integrator action. The soft anti-windup strategy reduces integrator changes rather than completely eliminating them. Select a
reduction factor in the range 0.05 to 0.25.
A reduction factor of 0 is the same as the clamping anti-windup strategy. A reduction factor of 1 is the same as no anti-windup correction.*/
$anti_w = getArg('aw') ; // Anti-windup
/* Improving Derivative Response : Single pole filter. This requires an additional lag variable for filter state,
plus an additional parameter between 0.0 and 1.0 to configure the cutoff frequency.
A lag parameter value of 0.15 to 0.35 usually works well. The lower this cutoff level, the better the high frequency
noise rejection but the more likely that effectiveness of the derivative term is reduced.*/
$lagcut = getArg('lag') ; // Single pole filter 0.75
$thresh = getArg('tol') ; // Seuil de filtrage de l'erreur positive
$stall = getArg('stallpso') ; // Nombre maximum de combinaisons PID "PSO" testées sans amélioration notable ITAE (20)
/*************************** Périphériques ******************************/
$idtemp1 = getArg('idtemp1') ; // A remplacer par le code API de la température OREGON Pièce n°1
$idtemp2 = getArg('idtemp2') ; // A remplacer par le code API de la température OREGON Pièce n°2
// $idtempx = getArg('idtempx') ; // A remplacer par le code API de la température OREGON Pièce n°x
$idtemp = array(1=>$idtemp1, 2=>$idtemp2, ...... x=>$idtempx) ; // API températures OREGON
$idtemp_ext = xxxxxx ; // A remplacer par le code API du (des) capteur(s) OREGON (Température Extérieure)
$idsetpt1 = xxxxxx ; // A remplacer par le code API de la consigne REMOTEC ZXT-120 n°1
$idsetpt2 = xxxxxx ; // A remplacer par le code API de la consigne REMOTEC ZXT-120 n°2
// $idsetptx = xxxxxx ; // A remplacer par le code API de la consigne REMOTEC ZXT-120 n°x
$idsetpt = array(1=>$idsetpt1, 2=>$idsetpt2, ..... x=>$idsetptx) ; // API consignes REMOTEC ZXT-120
$idpid1 = xxxxxxx ; // API PID A RENSEIGNER : pièce n°1
$idpid2 = xxxxxxx ; // API PID A RENSEIGNER : pièce n°2
// $idpidx = xxxxxxx ; // A COMPLéTER
$idpso1 = xxxxxxx ; // API PSO A RENSEIGNER : pièce n°1
$idpso2 = xxxxxxx ; // API PSO A RENSEIGNER : pièce n°2
// $idpsox = xxxxxx ; // A COMPLéTER
$idcons_pil= xxxxxx ; // A remplacer par le code API de la consigne Pilotage (Float)
$setpoint = sdk_val($idcons_pil) * 1.0 ; // Consigne Pilotage (Float)
$nb_rooms = x ; // Nombre de pièces
$nb_splits = x ; // Nombre de splits (>= Nombre de pièces)
for ($i = 1; $i <= $nb_splits ; $i++) {
$maxi[$i] = x ; // Température maxi des splits
$mini[$i] = x ; // Température mini des splits
}
switch ($init) {
case 1 :
/************** Initialisation des variables ************/
for ($i = 1; $i <= $nb_splits ; $i++) { // Index des splits
$int = 'int'.$i.'' ;
$der = 'der'.$i.'' ;
$state = 'state'.$i.'' ;
saveVariable($int, 0.0) ; // Intégrale
saveVariable($der, 0.0) ; // Dérivée
saveVariable($state, 0.0) ; // Lagstate pour Single pole filter
}
saveVariable('previous', time()) ;
/******** Température extérieure (calculer la moyenne si plusieurs capteurs) **********/
$temp_ext = sdk_val($idtemp_ext) ; // Température extérieure moyenne
// Si température extérieure < 0, réajuster la consigne pilotage -1°C
// ATTENTION : cette valeur doit être réinitialisée quotidiennement
// (par script ou bien avec une variable interne ...)
if ($temp_ext < 0.0) {
if ($setpoint >= max($mini) + 1) {
$setpoint = $setpoint - 1.0 ;
setValue($idcons_pil, round($setpoint)) ; // Consigne Pilotage (Integer)
} else {
$setpoint = max($mini) ;
setValue($idcons_pil, round($setpoint)) ; // Consigne Pilotage (Integer)
}
}
saveVariable('setpil',$setpoint) ; // Sauvegarde valeur pour détecter les changements
/*************************** INIT PSO ********************************/
$itae_start = strtotime("now") / 60 ; // Au format UNIX (minutes)
savevariable('debut', $itae_start) ;
$idpid = array(1=>$idpid1, 2=>$idpid2, ...... x=>$idpidx) ; // API PID : A COMPLéTER JUSQU'à x ...
$idpso = array(1=>$idpso1, 2=>$idpso2, ...... x=>$idpsox) ; // API PSO : A COMPLéTER JUSQU'à x ...
// Ecrire les paramètres PID testés la veille
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
$str_pid[$i] = sdk_val($idpid[$i]) ; // Lecture des dernières valeurs PID stockées pour chaque pièce
$pid[$i] = sdk_cut($str_pid[$i]) ; // Parse PID coefficients
$var6 = 'pbtest'.$i.'' ;
$psotest = loadVariable($var6) ; // Nombre de combinaisons PID "PSO" déjà testées pour cette pièce
$str_pso[$i] = sdk_val($idpso[$i]) ; // Lecture des dernières valeurs PSO stockées pour chaque pièce
if ($psotest <= $stall) {
$var2 = 'itae'.$i.'' ;
$var3 = 'kp'.$i.'' ;
$var4 = 'ti'.$i.'' ;
$var5 = 'td'.$i.'' ;
$itae = loadVariable($var2) ; // ITAE index précédemment calculé pour la pièce i
if ($itae == 0.0) { $itae = 999999999999.0 ; } // Première exécution du programme
if ($str_pso[$i] == "STOP PSO") { // Relance si nécessaire l'exécution du programme PSO
$var8 = 'lastpso'.$i.'' ;
$lastpso = loadVariable($var8) ;
setValue($idpso[$i], $lastpso) ;
}
$tolerance = 1.10 ; // Tolerance 10 %
if ($itae * $tolerance < $pid[$i][4]) { // L'indice ITAE calculé la veille est meilleur que celui stocké (à durées identiques)
$kp[$i] = loadVariable($var3) ; // Proportionnel testé la veille
$ti[$i] = loadVariable($var4) ; // Intégrale testé la veille
$td[$i] = loadVariable($var5) ; // Dérivée testé le veille
$pid_new = array($kp[$i], $ti[$i], $td[$i], round($itae, 3)) ;
$str_gb = implode(";",$pid_new) ;
setValue($idpid[$i], $str_gb) ; // On écrit les nouveaux paramètres
savevariable($var6, 0) ; // On réinitialise le compteur
usleep(3000000) ; // On attend 3 secondes
} else { // L'indice ITAE testé la veille est moins bon : On stocke le nombre de tests sans changements
savevariable($var6, $psotest + 1) ; // On incrémente le compteur
}
} else if ($str_pso[$i] != "STOP PSO") {
$var8 = 'lastpso'.$i.'' ;
savevariable($var8, $str_pso[$i]) ; // Sauvegarde de la dernière combinaison PSO
setValue($idpso[$i], "STOP PSO") ; // Bloque l'exécution du programme PSO
}
}
// Calculs du jour
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
$var3 = 'kp'.$i.'' ;
$var4 = 'ti'.$i.'' ;
$var5 = 'td'.$i.'' ;
$str_pid[$i] = sdk_val($idpid[$i]) ; // Lecture des dernières valeurs PID stockées pour chaque pièce
$pid[$i] = sdk_cut($str_pid[$i]) ; // Parse PID coefficients
$var6 = 'pbtest'.$i.'' ;
$psotest = loadVariable($var6) ; // Nombre de combinaisons PID "PSO" déjà testées pour cette pièce
$maj_pso = sdk_chkmaj($idpso[$i]) ; // Temps écoulé en minutes depuis la mise à jour PSO pour cette pièce
// Erreur initiale normée
$var1 = 'norme'.$i.'' ;
$norme[$i] = $setpoint - sdk_val($idtemp[$i]) ; // Erreur initiale algébrique (norme) pour la pièce i
if ($norme[$i] == 0.0) { $norme[$i] = 0.001 ; }
savevariable($var1, $norme[$i]) ;
$setpt_err = ($setpoint - sdk_val($idtemp[$i])) / $norme[$i] ; // Erreur normée pour la pièce i
$fitness = sdk_fitness(0.0, 1.0, $setpt_err) ; // Initialise ITAE index
$var2 = 'itae'.$i.'' ;
savevariable($var2, $fitness) ;
// On lit les paramètres PID
$var7 = 'pbprev'.$i.'' ;
$psoprev[$i] = loadVariable($var7) ; // PSO précédemment testé pour la pièce i
if ($psotest <= $stall and $psoprev[$i] != $str_pso[$i]) { // On teste les PSO
savevariable($var7, $str_pso[$i]) ; // On sauvegarde le dernier PSO testé
$pso[$i] = sdk_cut($str_pso[$i]) ; // Parse PID coefficients
savevariable($var3, $pso[$i][1]) ; // Proportionnel
savevariable($var4, $pso[$i][2]) ; // Intégrale
savevariable($var5, $pso[$i][3]) ; // Dérivée
// echo "Pour la pièce n°".$i.", Kp(pso) = ".$pso[$i][1].", Ti(pso) = ".$pso[$i][2].", Td(pso) = ".$pso[$i][3]."<br>" ;
} else { // On utilise les PID
savevariable($var3, $pid[$i][1]) ; // Proportionnel
savevariable($var4, $pid[$i][2]) ; // Intégrale
savevariable($var5, $pid[$i][3]) ; // Dérivée
// echo "Pour la pièce n°".$i.", Kp(pid) = ".$pid[$i][1].", Ti(pid) = ".$pid[$i][2].", Td(pid) = ".$pid[$i][3]."<br>" ;
}
}
break ;
case 0 :
/*************** On recharge les derniers paramètres PID ****************/
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
$var3 = 'kp'.$i.'' ;
$var4 = 'ti'.$i.'' ;
$var5 = 'td'.$i.'' ;
$kp[$i] = loadVariable($var3) ;
$ti[$i] = loadVariable($var4) ;
$td[$i] = loadVariable($var5) ;
}
/*********** Réinitialisation des variables en cas de changement de consigne pilotage *********/
$prev_setpt = loadVariable('setpil') ;
saveVariable('setpil',$setpoint) ; // Sauvegarde valeur pour détecter les changements
if ($setpoint == $prev_setpt) {
// setValue(xxxxxx, 0) ; // Pas de reset (OPTIONNEL : créer un périphérique "état" de type texte pour visualiser le reset)
} else {
// setValue(xxxxxx, 1) ; // Reset sur changement de consigne (OPTIONNEL)
for ($i = 1; $i <= $nb_splits ; $i++) { // Indice des splits
$int = 'int'.$i.'' ;
$der = 'der'.$i.'' ;
$state = 'state'.$i.'' ;
saveVariable($int, 0.0) ; // Intégrale
saveVariable($der, 0.0) ; // Dérivée
saveVariable($state, 0.0) ; // Lagstate pour Single pole filter
}
saveVariable('previous', time()) ; // Heure du reset
}
/************************* TRAITEMENT ********************************/
$prev = loadVariable('previous') ; // Heure du précédent calcul
$sec = time() ; // Heure courante, mesurée en secondes depuis le début de l'époque UNIX
$dur_sec = $sec - $prev ; // Durée en secondes depuis le précédent calcul
$dur = $dur_sec / 60.0 ; // Durée en minutes (float)
if ($dur == 0) { $dur = 1.0 ; }
/***************************** PSO ***********************************/
$itae_start = loadVariable('debut') ; // Début du calcul ITAE
$itae_now = strtotime("now") / 60 ; // Au format UNIX (minutes)
$itae_dur = $itae_now - $itae_start ; // Durée du calcul ITAE
$itae_max = 6 * 60 ; // En minutes
if ($itae_dur <= $itae_max) {
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
$temp[$i] = sdk_val($idtemp[$i]) ; // Température de la pièce
$var1 = 'norme'.$i.'' ;
$norme = loadVariable($var1) ; // Erreur initiale algébrique (norme) pour la pièce i
$setpt_err = ($setpoint - sdk_val($idtemp[$i])) / $norme ; // Erreur normée pour la pièce i
$var2 = 'itae'.$i.'' ;
$itae_old = loadVariable($var2) ; // ITAE index précédent pour la pièce i
$fitness = sdk_fitness($itae_old, $dur, $setpt_err) ; // Nouvel ITAE index pour la pièce i
savevariable($var2, $fitness) ;
}
}
/***************************** PID ***********************************/
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice des pièces
$temp[$i] = sdk_val($idtemp[$i]) ; // Température des pièces
// On recharge les valeurs précédentes
$int = 'int'.$i.'' ;
$der = 'der'.$i.'' ;
$state = 'state'.$i.'' ;
$integral[$i] = loadVariable($int) ; // Précédente intégration
$deriv[$i] = loadVariable($der) ; // Erreur Précédente
$lagstate[$i] = loadVariable($state) ; // Single pole filter
/* u = -Kp * ( err + integral(err)/Ti + deriv(err)*Td )
When approximating the integral using rectangular rule integration, the integral adjustment at each time step becomes the integrand
(setpoint error) times the update time interval. When approximating the derivative using a first difference, the derivative estimate is the
difference in successive setpoint error values divided by the update time interval.*/
/****************** Réponse Proportionnelle *****************/
$seterr[$i] = sdk_prop($temp[$i],$setpoint,$thresh) ;
/*********** Réponse dérivée : Single pole filter ***********/
$state[$i] = sdk_spf($temp[$i],$deriv[$i],$lagcut,$lagstate[$i]) ;
saveVariable($state, $state[$i]) ;
/************************ Résultats *************************/
$pidout[$i] = round(-$kp[$i] * sdk_pid($seterr[$i],$state[$i],$integral[$i],$dur,$ti[$i],$td[$i])) ;
// $pidout_liv = round($pidout_l) ;
saveVariable($der, $temp[$i]) ; // to avoid the setpoint level spikes
/****** Soft Integrator Anti-Windup (if outside limits) ******/
$new_int[$i] = sdk_int($pidout[$i],$maxi[$i],$mini[$i],$integral[$i],$seterr[$i],$anti_w) ;
saveVariable($int, $new_int[$i]) ;
/****** Ajustements aux limites ******/
$pidout[$i] = sdk_out($pidout[$i],$maxi[$i],$mini[$i]) ;
// echo "PIDOUT ".$i." = ".$pidout[$i]."<br>" ;
}
saveVariable('previous', $sec) ; // Heure du calcul
/********************* Réglages périphériques *************************/
$j = 1 ;
$repeat = 3 ; // On répète l'opération au cas où le split ne l'ait pas enregistrée
while ($j <= $repeat):
for ($i = 1; $i <= $nb_splits ; $i++) { // Indice des splits
if ($i == 1) { $k = $i ; } else { $k = $i - 1 ; } // Dans ce exemple il y a 2 splits pour la pièce n°1 (ADAPTER A CHAQUE CONFIGURATION !!!)
$IR[$i] = sdk_val($idsetpt[$i]) ; // Consignes des splits
if($IR[$i] != $pidout[$k]) {
setValue($idsetpt[$i], $pidout[$k]) ; // Consigne
}
}
// echo " Consigne pièce n°1 = ".$IR[1].", Consigne pièce n°2 = ".$IR[2].", Consigne pièce n°3 = ".$IR[3]."....."<br>" ;
if ($j < $repeat) { usleep(5000000) ; } // Attend 5 secondes
// for ($i = 1; $i <= $nb_splits ; $i++) { // Indices des splits
// $IR[$i] = sdk_val($idsetpt[$i]) ; // Consignes des splits
// }
$j++ ;
endwhile ;
break;
}
?>
Le script PSO ci-après calcule par itérations de nouvelles combinaisons (semi-aléatoires) des paramètres ‘kp’, ‘ki’, et ‘kd’, à l’intérieur d’un domaine régi par une fonction mathématique d’optimisation et des bornes fournies au programme (**).
Ce script doit être lancé avec une fréquence suffisante pour pouvoir générer des combinaisons acceptables, mais en évitant de saturer la box (1/2 heure à 1 heure semblent corrects). Les nouvelles combinaisons obtenues ne sont renouvelées qu’après 24 heures.
Ces combinaisons « PSO(n) » sont stockées pour être ensuite évaluées (sur une durée CONSTANTE de 6 heures) par le script principal (PID) : si leur indice « itae » est notablement inférieur à celui des valeurs « PID(n) » précédemment enregistrées, elles remplacent ces dernières à l’initialisation suivante du script PID.
Lorsque les valeurs sont stabilisées, on peut suspendre l’exécution du script PSO.
(**) Les six variables d’appel &mini[1-2-3] et &maxi[1-2-3] s’appliquent respectivement aux 3 paramètres PID. On devra ajuster leurs valeurs pour obtenir des combinaisons adaptées (essayer par exemple : 50<kp<100 et 50<ki<150 et 0.1<kd<1.0, que l’on pourra affiner par la suite en jouant sur &stallpso du script principal).SCRIPT PSO :
- Code : Tout sélectionner
<?
// ###########################################################################
// # Ce programme calcule des variantes des paramètres PID pour chaque pièce.
// # Il doit être lancé AVANT le programme de régulation PID du chauffage.
// # Le programme PID lui-même évaluera les nouveaux paramètres en les comparant
// # avec les anciens, qui seront remplacés si les variantes sont meilleures.
// ###########################################################################
// &maxi1=100&maxi2=150&maxi3=1&mini1=50&mini2=50&mini3=0.05
// https://www.researchgate.net/publication/46165785_A_particle_swarm_optimization_approach_for_tuning_of_SISO_PID_control_loops
// Méthode de Ziegler-Nichols
// ku = gain maximal; tu = période d'oscillation du signal
// PID : 0.6 * ku; tu / 2; tu / 8
// PIR (Pessen Integral Rule) : 0.7 * ku; tu / 2.5; 3 * tu / 20
// Léger dépassement : 0.33 * ku; tu / 2; tu / 3
// Aucun dépassement : 0.2 * ku; tu / 2; tu / 3
function sdk_val($idper) {
//Lecture Valeur périphérique
$per = getValue($idper) ;
$id_val = $per["value"] ;
return $id_val ;
}
function sdk_chkmaj($idper) {
$arr1 = getValue($idper) ;
$maj = $arr1["change"] ; // Date-heure de la dernière mise à jour
$upd = strtotime($maj) / 60 ; // Au format UNIX (minutes)
$ref = strtotime("now") / 60 ; // Au format UNIX (minutes)
$delta = round($ref - $upd) ; // Temps écoulé en minutes depuis la mise à jour
return $delta ;
}
function sdk_div($x, $max) { // Retourne le diviseur de x <= max
$n = count($x) ;
for ($i = 1 ; $i <= $n ; $i++) {
$div[$i] = 1 ;
while ($x[$i] > $max) {
$x[$i] = $x[$i] / 10 ;
$div[$i] = $div[$i] * 10 ;
}
}
return $div ;
}
function sdk_speed($const, $vit1, $cog, $pb, $pos1, $soc, $gb, $vm) {
// v[j,n](iter+1) = chi * [v[j,n](iter) + c1*rand1*(pbest[j,n] - s[j,n](iter)) + c2*rand2*(gbest[j,n] - s[j,n](iter))] Equation (6.1)
$rnd1 = $cog * rand(-$vm * 100.0, $vm * 100.0) / 100.0 ;
$rnd2 = $soc * rand(-$vm * 100.0, $vm * 100.0) / 100.0 ;
$fact1 = $rnd1 * ($pb - $pos1) ;
$fact2 = $rnd2 * ($gb - $pos1) ;
$vit2 = $const * ($vit1 + $fact1 + $fact2) ;
return $vit2 ;
}
function sdk_pos($pos1, $vit2) {
// s[j,n](iter+1) = S[j,n](iter) + v[j,n](iter+1) Equation (6.2)*/
$pos2 = $pos1 + $vit2 ;
return $pos2 ;
}
// ######################## FITNESS FUNCTIONS ###################################
// http://www-optima.amp.i.kyoto-u.ac.jp/member/student/hedar/Hedar_files/go.htm
function sdk_michalewicz($x) { // FITNESS FUNCTION
// https://gist.github.com/denis-bz/da697d8bc74fae4598bf
// limites = [0, pi]
$michalewicz = 0.5 ; // orig 10: ^20 => underflow
$fact = 2 * $michalewicz ;
$n = count($x) ;
$s = 0.0 ;
for ($i = 1 ; $i < $n + 1 ; $i++) {
$j = $i * 1.0 ; // De 1.0 à $n.0
$s = $s + (sin($x[$j]) * sin($j * pow($x[$j], 2) / pi())) ;
}
return - pow($s, $fact ) ;
}
// ######################## END OF FITNESS FUNCTIONS #############################
function sdk_display_array($arr, $level = 0) {
if(gettype($arr) == 'array') {
foreach($arr as $key => $value) {
$chaine = str_repeat(". .", $level) ;
echo "<i>$chaine</i> Niveau <i>$level</i> Clé <i>$key</i> : valeur = <i>$arr</i><br>" ;
sdk_display_array($value, $level + 1) ;
}
} else {
$chaine = str_repeat(". .", $level) ;
echo "<i>$chaine</i> Niveau <i>$level</i> : valeur = <i>$arr</i><br><br>" ;
}
}
function sdk_contraintes($par, $min, $max) {
// Contraintes
$n = count($par) ;
for ($k = 1 ; $k <= $n + 1 ; $k++) { $test[$k] = 0.0 ; }
for ($k = 1 ; $k <= $n ; $k++) {
if ($par[$k] < $min[$k] or $par[$k] > $max[$k]) { $test[$n+1] = $test[$n+1] + 1 ; } // Echec
}
return $test ;
}
// Paramètres
$maxi1 = getArg('maxi1') ; // Valeur maximum inclusive coefficient proportionnel kp
$maxi2 = getArg('maxi2') ; // Valeur maximum inclusive coefficient intégrale ti
$maxi3 = getArg('maxi3') ; // Valeur maximum inclusive coefficient dérivée td
$maxi = array(1=>$maxi1, 2=>$maxi2, 3=>$maxi3) ;
$mini1 = getArg('mini1') ; // Valeur minimum inclusive coefficient proportionnel
$mini2 = getArg('mini2') ; // Valeur minimum inclusive coefficient intégrale ti
$mini3 = getArg('mini3') ; // Valeur minimum inclusive coefficient dérivée td
$mini = array(1=>$mini1, 2=>$mini2, 3=>$mini3) ;
$nb_rooms = x ; // Nombre de pièces A RENSEIGNER
$swarm = array() ; // Swarm
$gbest = array() ; // Global Best
$pbest = array() ; // Particle Best
$dimensions = 3 ; // PID
$swarm_size = 20 ; // Nombre de particules
$chi = 0.73 ; // Constriction factor [2*kappa/ABS(2-phi-SQRT(phi^2-4*phi)) avec kappa=1; phi=ph1+phi2=2.05+2.05]
$c1 = 2.05 ; // Cognitive acceleration
$c2 = 2.05 ; // Social acceleration
$vmax = 1.0 ;
$max_iter = 1000 ; // Durée de l'algorithme
$stall = $max_iter / 2 ; // Si les résultats de l'algorithme PSO n'évoluent plus, il est interrompu
$mval = pi() ; // Valeur maximum de la fonction d'optimisation michalewicz (3D) -> minimum = 0
// Le programme ne tourne que si nécessaire pour une pièce déterminée
$idpso1 = xxxxxxx ; // API GBEST A RENSEIGNER : pièce n°1
$idpso2 = xxxxxxx ; // API GBEST A RENSEIGNER : pièce n°2
// $idpsox = xxxxxx ; // A COMPLéTER
$idpso = array(1=>$idpso1, 2=>$idpso2, ..... ) ; // A COMPLéTER JUSQU'à x ...
$step = 24 * 60 ; // Update possible après 24 heures
$pso = array() ;
$pso_old = array() ;
$maj_pso = array() ;
$req = array() ; // Test si mise à jour requise
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
$var = 'pso'.$i."" ;
$pso_old[$i] = loadVariable($var) ; // PSO précédent
$pso[$i] = sdk_val($idpso[$i]) ;
saveVariable($var, $pso[$i]) ; // PSO actuel
$maj_pso[$i] = sdk_chkmaj($idpso[$i]) ; // Temps écoulé en minutes depuis la mise à jour
if ($pso[$i] == $pso_old[$i] and $maj_pso[$i] >= $step and $pso[$i] != "STOP PSO") {$req[$i] = 1 ; } // Mise à jour requise
}
//for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
//echo "Req de ".$i." = ".$req[$i]."<br>" ;
// if (array_key_exists($i, $req)) { echo "Mise à jour requise pièce n°".$i."<br>" ;}
//}
// PSO
$norm = sdk_div($maxi, $mval) ; // Multiplicateurs des paramètres PID calculés
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
if (array_key_exists($i, $req)) {
// Initialisation des particules
for ($j = 1; $j <= $swarm_size ; $j++) { // Indice de la particule
for ($k = 1; $k <= $dimensions ; $k++) { // Indice du paramètre
$velocity = rand(-$vmax * 100.0, $vmax * 100.0) / 100.0 ; // Soit un nombre aléatoire entre -1 et +1
$position = rand(0.0, $mval * 100.0) / 100.0 ; // Paramètre aléatoire (positif) entre 0 et maximum de la fonction d'optimisation
$swarm[$i][$j]["VELOCITY"][$k] = $velocity ;
$swarm[$i][$j]["POSITION"][$k] = $position ;
}
// Evaluation de la particule courante
$particle[$i] = $swarm[$i][$j] ;
$test = $particle[$i]["POSITION"] ; // $test["POSITION"][paramètre] = gbest
$fitness = sdk_michalewicz($test) ;
// On remplace "best fitness", "particle best" et "global best"
$bestfit[$i] = $fitness ;
$bestfit2[$i] = $bestfit[$i] ;
$swarm[$i][$j]["P_BEST"] = $swarm[$i][$j]["POSITION"] ;
$grp_best[$i] = $particle[$i] ;
}
}
}
$iter = 1 ;
while ($iter != $max_iter) {
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
if (array_key_exists($i, $req)) {
// Boucle principale des particules
$fail = 0 ; // Réinitialisé à zéro à chaque succés
for ($j = 1; $j <= $swarm_size ; $j++) { // Indice de la particule
for ($k = 1; $k <= $dimensions ; $k++) { // Indice du paramètre PID
//Calculs de "vitesse"
$vel1 = $swarm[$i][$j]["VELOCITY"][$k] ;
$pbest = $swarm[$i][$j]["P_BEST"][$k] ;
$pos1 = $swarm[$i][$j]["POSITION"][$k] ;
$gbest = $grp_best[$i]["POSITION"][$k] ;
$vit2 = sdk_speed($chi, $vel1, $c1, $pbest, $pos1, $c2, $gbest, $vmax) ;
$swarm[$i][$j]["VELOCITY"][$k] = $vit2 ;
//Calculs de "position"
$pos2 = sdk_pos($pos1, $vit2) ;
If ($pos2 > $mval) {
$pos2 = $mval ;
} else if ($pos2 < 0.0) {
$pos2 = 0.0 ;
}
$swarm[$i][$j]["POSITION"][$k] = $pos2 ;
}
// Evaluation de la particule courante
$particle[$i] = $swarm[$i][$j] ;
$test = $particle[$i]["POSITION"] ;
$fitness = sdk_michalewicz($test) ;
// On teste la cohérence de la particule
$pid[$i] = $swarm[$i][$j]["POSITION"] ;
$kp[$i] = $pid[$i][1] * $norm[1] ;
$ti[$i] = $pid[$i][2] * $norm[2] ;
$td[$i] = $pid[$i][3] * $norm[3] ;
$params = array(1=>$kp[$i], 2=>$ti[$i], 3=>$td[$i]) ;
$good = sdk_contraintes($params, $mini, $maxi) ; // Teste si les contraintes sont satisfaites
// Si la particule courante est la meilleure
$m = count($params) ;
if ($fitness < $bestfit[$i] and $good[$m+1] == 0) {
// On remplace "best fitness", "particle best" et "global best"
$bestfit[$i] = $fitness ;
$swarm[$i][$j]["P_BEST"] = $swarm[$i][$j]["POSITION"] ;
$grp_best[$i] = $swarm[$i][$j]["P_BEST"] ;
}
if ($fitness < $bestfit2[$i]) { // Particules hors limites
$bestfit2[$i] = $fitness ;
$pro[$i] = $kp[$i] ;
$int[$i] = $ti[$i] ;
$der[$i] = $td[$i] ;
}
$fail++ ;
// Si les résultats n'évoluent plus
if ($fail >= $stall) {
$j = $swarm_size + 1 ; // On sort de la boucle des particules
echo "Pour la pièce n°<i>$i<i> et la particule n°<i>$j<i> : sortie de la boucle après <i>$iter<i> itérations<br>" ;
}
}
}
}
$iter++ ;
}
// Sauvegarde des GBESTS ..
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
if (array_key_exists($i, $req)) {
// On teste la cohérence de la particule
$kp[$i] = round($grp_best[$i][1] * $norm[1] , 0) ;
$ti[$i] = round($grp_best[$i][2] * $norm[2] , 0) ;
$td[$i] = round($grp_best[$i][3] * $norm[3] , 3) ;
$params = array(1=>$kp[$i], 2=>$ti[$i], 3=>$td[$i]) ;
$good = sdk_contraintes($params, $mini, $maxi) ; // Teste si les contraintes sont satisfaites
$m = count($params) ;
if ($good[$m+1] == 0) { // Succès
$arr_gbest = array($kp[$i], $ti[$i], $td[$i]) ;
$str_gbest = implode(";", $arr_gbest) ;
setValue($idpso[$i], $str_gbest) ;
$code[$i] = 0 ; // La valeur obtenue est correcte
} else { // On vérifie si le programme a tourné, et on enregistre les résultats => OPTIONNEL(*)
$params = array(1=>$pro[$i], 2=>$int[$i], 3=>$der[$i]) ;
$good = sdk_contraintes($params, $mini, $maxi) ;
$echecs = " = ".strval(round($good[$m+1], 0))." Echecs" ;
$arr_test = array($i, round($pro[$i], 0), round($int[$i], 0), round($der[$i], 3), $echecs) ;
$str_test = implode(";", $arr_test) ;
// setValue(xxxxxxx, $str_test) ; // (*) Créer un périphérique "état" de type texte. RENSEIGNER SON API
$code[$i] = 1 ; // La valeur obtenue est incorrecte
}
}
}
$result = 0 ;
for ($i = 1; $i <= $nb_rooms ; $i++) { // Indice de la pièce
$result = $result + $code[$i] ; // Nombre de calculs incorrects
}
// ****************************** Pour relancer le programme PSO si nécessaire (** RECOMMANDé) **********************************
// if ($result == 0) { setValue(xxxxxxx, 0) ; } else { setValue(xxxxxxx, $result) ; } //
// (**) Créer un périphérique "état" de type texte, et une règle : réenclenchement autorisé après 5 minutes, horaire = toutes les
// minutes, critère = différent de 0, action = lancer le programme PSO (via macro avec délai 5 minutes). RENSEIGNER SON API
// ***************************************************** OPTIONNEL *************************************************************
?>