Note de ce sujet :
  • Moyenne : 0 (0 vote(s))
  • 1
  • 2
  • 3
  • 4
  • 5
[v16.03] Mode forçage Demi-sinus
#11
Sauf que ça scintille plus lol
De mon coté j ai bien superposé un algorithme de Bresenham pour répartir les ON sur le mode multisinus…
Bref chacun fait comme il veut
Répondre

#12
j ai repris les courbes graphiques sur F1ATB des différents mode pour comparer visuellement

https://59jag59.github.io/demi-sinus/
Répondre

#13
Très intéressantes ces courbes
On voit du 20 hz ce qui peut expliquer le scintillement
Et à 4 % et 96 % ça donne quoi ?
Répondre

#14
(20-11-2025, 09:00 PM)59jag a écrit : j ai repris les courbes graphiques sur F1ATB des différents mode pour comparer visuellement
Bonjour,

  bravo pour cet outil de comparaison  

  j'ai apporté quelques retouches :

  élargi la plage visible à 2 secondes pour voir l'enchainement de cycles et justifier l'équilibre éléc quand les séquences identiques se suivent
  + ajout d'un peu d'information sur les durées des séquences et % réellement commandé 
  + correction pour la séquence demi sinus basé sur 1000ms et pas 990
  + correction pour la séquence train de sinus basé sur 990ms et pas 1000ms
  + différentiation multi sinus tel quel programmé dans RMS (la table codé en dur est écrasé lors de l'exécution du setup) et la retouche de répartition (revue grâce a cette appli / merci) [c'est dans le code JS en PJ]


Pièces jointes
.html   59jag_2s.html (Taille : 13.55 Ko / Téléchargements : 13)
Merci André Smile ,
Routeur V16.04 (since V2.01) / 1xESP32 (IP fixe) / Source UxI / 5 actions
Panneaux 1680Wc
1 Triac : ECS 2000W
1 SSR (multi) : ECS 1800W
1 SSR (On-Off) : Circulateur plancher chauffant eau 50W
1 SSR (multi) : circuit d'eau 1500W
1 SSR (multi) : Ultime 2000W
Répondre

#15
Chapeau bas pour ce JS Michy, je vais l enrichir du mode multisinus reparti , pour mieux comprendre les différences

j ai donc ajouté dans le code de JS "mon mode mulitsinus" et génial ce qu' a realisée Michy car ca permet de faire une analyse qui me laisse perplexe lol
En comparant demi sinus et multi sinus on constate
globalement le demi sinus avec des trames sont globalement plus courtes , donc avantage demi sinus
mais finalement l'avantage du demi sinus sur une repartition parfaite fait que visuellement sur les effets d'une ampoule cette régularité la rend plus perceptible par l'oeil, alors que le mutli sinus ( qui est finalement moins reparti) à un meilleur effet de remanence sur la retine, ce qui rend le scintillement moins visible bien que présent
cette régularité "parfaite" augmente les chances de synchronisation avec les modes de mesures "lentes" et donc probabilité plus grande que la mesure soit un peu plus erronée sur un shelly

Si je résume avantage au demi-sinus , mode multi sinus reparti pour les cas ou le scintillement induit sur les ampoules est génant



Code :
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta name="author" content="André Buhart - F1ATB">
        <meta name="copyright" content="F1ATB">
        <title>FFT Routeur - Comparaison</title>
        <style>
            body {
                text-align: center;
                font-size: 150%;
                background-color: #222;
                color: white;
            }
            .Oscillo {
                border: 4px grey inset;
                height: 150px;
                width: 100%;
                background-color: black;
                margin-bottom: 5px;
                margin-top: 0;
            }
            
            h4 {
                margin-bottom: 2px;
                margin-top: 2px;
            }
            h4 span {
                color: #8cf;
                font-size: 0.5em;
            }
            svg {
                font-size: 14px;
            }
            
            .slider-container {
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 10px;
                margin: 10px auto;
                max-width: 95%;
            }
            
            .slider-container input[type="range"] {
                flex: 1;
                max-width: 70%;
            }
            
            .slider-container button {
                font-size: 20px;
                width: 30px;
                height: 30px;
                border: 2px solid #666;
                background: #444;
                color: white;
                cursor: pointer;
                border-radius: 5px;
            }
            
            .slider-container button:hover {
                background: #666;
            }
            
            .slider-container button:active {
                background: #888;
            }
            
            .nouveau {
                border-color: #0f0;
            }
        </style>
    </head>
    <body onload="remakeTableMS();">
        <h4>Découpe Sinus <span id="trameDecoupe"></span></h4>
        <div class="Oscillo" id="Decoupe"></div>
        <h4>Demi-Sinus <span id="trameDemi"></span></h4>
        <div class="Oscillo" id="Demi"></div>
        <!-- ===== AJOUT: Multi-Sinus Réparti ===== -->
        <h4>Multi-Sinus Réparti <span id="trameReparti"></span> <span style="color:#0f0;">★ NOUVEAU</span></h4>
        <div class="Oscillo nouveau" id="Reparti"></div>
        <!-- ====================================== -->
    <h4>Multi-Sinus <span id="trameMulti"></span></h4>
    <span><input type="checkbox" id="natif" name="natif" onchange="remakeTableMS();" checked ><label for="natif">Répartition d'origine</label></span>
        <div class="Oscillo" id="Multi"></div>
        <h4>Train de Sinus <span id="trameTrain"></span></h4>
        <div class="Oscillo" id="Train"></div>
        <div class="slider-container">
            <button id="btnOuvertureMinus">−</button>
            <input type="range" min="0" max="100" value="50" oninput="trace();" id="curseurOuverture" step="1">
            <button id="btnOuverturePlus">+</button>
        </div>
        <div><span>Ouverture : </span><span id="percentOuverture">50</span><span> %</span></div>
        <div class="slider-container">
            <button id="btnDureeMinus">−</button>
            <input type="range" min="100" max="2000" value="1000" oninput="trace();" id="curseurDuree" step="50">
            <button id="btnDureePlus">+</button>
        </div>
        <div><span>Durée : </span><span id="valueDuree">2000</span><span> ms</span></div>
        <script>
        // Tables pour le mode multi-sinus, retouchés avec l'aide cet outil
        const tabPulseSinusTotal = [ 2,
            73,43,31,25,40,17,15,25,11,20,     9,25,15, 7,40,25,12,11,21, 5,
            19, 9,13,25, 8,19,11,25,31,20,    13,25, 3,35,40,25,19,21,23, 5,
            17,19, 7,25,40,13,15,25,37, 4,    37,25,15,13,40,25, 7,19,17, 5,
            23,21,19,25,40,35, 3,25,13,20,    31,25,11,19, 8,25,13, 9,19, 5,
            21,11,12,25,40, 7,15,25, 9,20,    11,25,15,17,40,25,31,43,73, 2
        ];
        const tabPulseSinusOn = [ 0,
             1, 1, 1, 1, 2, 1, 1, 2, 1, 2,     1, 3, 2, 1, 6, 4, 2, 2, 4, 1,
             4, 2, 3, 6, 2, 5, 3, 7, 9, 6,     4, 8, 1,12,14, 9, 7, 8, 9, 2,
             7, 8, 3,11,18, 6, 7,12,18, 2,    19,13, 8, 7,22,14, 4,11,10, 3,
            14,13,12,16,26,23, 2,17, 9,14,    22,18, 8,14, 6,19,10, 7,15, 4,
            17, 9,10,21,34, 6,13,22, 8,18,    10,23,14,16,38,24,30,42,72, 2
        ];
        // ===== AJOUT: Fonction Bresenham =====
        function genererRepartitionBresenham(pulseOn, pulseTotal) {
            const repartition = new Array(pulseTotal).fill(false);
            
            if (pulseOn === 0) return repartition;
            if (pulseOn >= pulseTotal) {
                repartition.fill(true);
                return repartition;
            }
            
            // Pour l'équilibre électrique, on travaille par paires de demi-sinusoïdes
            // Si pulseOn est impair, on arrondit au pair supérieur pour la répartition
            let pulseOnPaires = Math.floor(pulseOn / 2);
            let pulseTotalPaires = Math.floor(pulseTotal / 2);
            
            // Algorithme de Bresenham pour répartir les PAIRES
            let erreur = pulseTotalPaires / 2;
            let compteurPaires = 0;
            
            for (let i = 0; i < pulseTotalPaires && compteurPaires < pulseOnPaires; i++) {
                erreur += pulseOnPaires;
                if (erreur >= pulseTotalPaires) {
                    erreur -= pulseTotalPaires;
                    // Marquer la paire (positive ET négative)
                    repartition[i * 2] = true;
                    if (i * 2 + 1 < pulseTotal) {
                        repartition[i * 2 + 1] = true;
                    }
                    compteurPaires++;
                }
            }
            
            // Si pulseOn est impair, ajouter une dernière demi-sinusoïde
            if (pulseOn % 2 === 1 && pulseOn < pulseTotal) {
                // Trouver le premier emplacement libre pour ajouter la demi-sinusoïde impaire
                for (let i = 0; i < pulseTotal; i++) {
                    if (!repartition[i]) {
                        repartition[i] = true;
                        break;
                    }
                }
            }
            
            return repartition;
        }
        // =====================================
        function remakeTableMS() { // table avec tolérance 0,4%, [interval max = 730ms (1%+0.4% = 1/73 = 1.3698%]
            let natif = document.getElementById('natif').checked;
            let minimalDuration = natif ? 20 : 2; // en natif le mini est a 20 demi sinus soit 200ms
            let erreur = 0.0;
            let vrai = 0.0;
            let target = 0.0;
            for (let I = 0; I < 101; I++) {
                tabPulseSinusTotal[I] = -1;
                tabPulseSinusOn[I] = -1;
                target = I / 100.0;
                for (let T = minimalDuration; T < 101; T++) {
                    for (let N = 0; N <= T; N++) {
                        if (T % 2 == 1 || N % 2 == 0) { // Valeurs impaires du total ou pulses pairs pour éviter courant continu
                            vrai = N / T;
                            erreur = Math.abs(vrai - target);
                            if (erreur < 0.004) {
                                tabPulseSinusTotal[I] = T;
                                tabPulseSinusOn[I] = N;
                                N = 101;
                                T = 101;
                            }
                        }
                    }
                }
            }
            if (natif) {trace();return;}
            // les tables sont prêtes, on refait une passe pour affiner la précision avec une durée de cycle modifiée
            for (let I = 0; I <= 100; I++) {
                if ((10000000.0 * tabPulseSinusOn[I] / tabPulseSinusTotal[I]) == (100000 * I)) continue; // on ne fera pas mieux arrondi 5 chiffres apres virgule identique
                if ((40.0 * I / 100.0) == Math.floor(40.0 * (I / 100.0))) { // avec 400ms // corrige 5%, 15%, 35%, 45%, 55%, 65%, 85%, 95%
                        tabPulseSinusTotal[I] = 40;
                        tabPulseSinusOn[I] = 40.0 * (I / 100.0);                // pulses pairs Ok! (multiplie par 0.4) avec 5% comme premiere modif
                }
                if ((25.0 * I / 100.0) == Math.floor(25.0 * (I / 100.0))) { // avec 250ms // corrige 4%, 8%, 12%, 16%, 24%, 36%, 48%, 52%, 64%, 76, 84%, 92%, 96%
                        tabPulseSinusTotal[I] = 25;                             // total impaires 25 Ok!
                        tabPulseSinusOn[I] = 25 * (I / 100.0);
                }
            }
            // saut de 17% a 18% est precedé par 0.6667 suivi 1.5152 suivi de 0.8658 ==> meilleur regularité avec  7 / 39 passe a 0.6667 => 1.2821 => 1.0989
            tabPulseSinusTotal[18] = 39; tabPulseSinusOn[18] = 7;  // total impaires Ok!
            // saut de 46% a 47% est precedé par 1.1538 suivi 0.5128 suivi de 1.3333 ==> meilleur regularité avec  8 / 17 passe a 1.1538 => 0.9050 => 0.9412
            tabPulseSinusTotal[47] = 17; tabPulseSinusOn[47] = 8;  // total impaires Ok!
            // saut de 53% a 54% est precedé par 1.3333 suivi 0.5128 suivi de 1.1538 ==> meilleur regularité avec 20 / 37 passe a 1.3333 => 0.7207 => 0.9459
            tabPulseSinusTotal[54] = 37; tabPulseSinusOn[54] = 20; // total impaires Ok!
            // saut de 82% a 83% est precedé par 0.8658 suivi 1.5152 suivi de 0.6667 ==> meilleur regularité avec 34 / 41 passe a 0.8658 => 1.1086 => 1.0732
            tabPulseSinusTotal[83] = 41; tabPulseSinusOn[83] = 34; // total impaires Ok!
            trace();
        }
        function calculerLongueurTrame(mode, pourcentage) { // retouche pour affichage % reel qui differe de l'ouverture souhaitée
            if (mode === "decoupe") {
                let invPI = 1.0 / Math.PI;
                let real = 100 * 0.5 * (invPI + invPI * Math.cos(Math.PI * ((100-pourcentage)/100))) / invPI;
                let msg = "";
                return msg.concat(real.toPrecision(4), "% -> 20"); // reglage sur période d'une demi sinus, l'equilibre électrique se fait sur 2 demi sinus
            }
            
            if (mode === "demi") {
                if (pourcentage === 0) return 20;
                if (pourcentage === 100) return 20;
                // cycle basé sur 2000ms mais motif repetitif au mieux
                // la decoupe se fait par multiple de 10ms entier
                let R = 2000 / pourcentage;
                if (pourcentage > 50) R = 2000 / (100 - pourcentage);
                let approx = "";
                if ( R/10 != Math.floor(R/10)) {
                    R = Math.floor(Math.floor(R+10)/10)*10;
                    approx = " Moy ~";
                }
                return approx + R;
            }
            
            if (mode === "multi") {
                if (pourcentage === 0) return 20;
                if (pourcentage === 100) return 20;
                let PulseTotal = tabPulseSinusTotal[pourcentage];
                if (tabPulseSinusTotal[pourcentage] % 2 == 1) PulseTotal *= 2; // quand impaire, l'équilibre se fait sur 2 sequences
                let real = 100.0 * tabPulseSinusOn[pourcentage]/tabPulseSinusTotal[pourcentage];
                let msg = "";
                return msg.concat(tabPulseSinusOn[pourcentage], "/", tabPulseSinusTotal[pourcentage], " = ", real.toPrecision(4), "% -> ", PulseTotal * 10); // Nombre de périodes * 10ms
            }
            
            // ===== AJOUT: Mode Réparti =====
            if (mode === "reparti") {
                if (pourcentage === 0) return 20;
                if (pourcentage === 100) return 20;
                let PulseTotal = tabPulseSinusTotal[pourcentage];
                if (tabPulseSinusTotal[pourcentage] % 2 == 1) PulseTotal *= 2;
                let real = 100.0 * tabPulseSinusOn[pourcentage]/tabPulseSinusTotal[pourcentage];
                let msg = "";
                return msg.concat(tabPulseSinusOn[pourcentage], "/", tabPulseSinusTotal[pourcentage], " Bresenham = ", real.toPrecision(4), "% -> ", PulseTotal * 10);
            }
            // ================================
            
            if (mode === "train") {
                let real = Math.min((100.0 * pourcentage / 99.0), 100.0);
                let msg = "";
                return msg.concat(" Moy ", real.toPrecision(4), "% / 1980");  // équilibre éléctrique après deux cycle de 990ms
            }
            
            return 0;
        }
        function genererSignal(mode, pourcentage, nbPoints) {
            const signal = [];
            if (mode === "decoupe") {
                const retard = 20 - pourcentage / 5;
                for (let t = 0; t < 4000; t++) { // courbe sur 2 secondes
                    if (t % 20 < retard) {
                        signal[t] = 0;
                    } else {
                        const phi = Math.PI * 2 * 50 * t /2000;
                        signal[t] = Math.sin(phi);
                    }
                }
            }
            if (mode === "demi") {
                let Phase = 0;
                let Plot = false;
                let last_signe = false;
                for (let t = 0; t < 4000; t++) { // courbe sur 2 secondes
                    const phi = Math.PI * 2 * 50 * t / 2000;
                    if (t % 20 === 0) {
                        const phi2 = Math.PI * 2 * 50 * (t + 10) / 2000;
                        const signe = Math.sin(phi2) > 0;
                        Phase += pourcentage;
                        if (Phase >= 100 && last_signe !== signe) { // en demi sinus, pas de notion de 990ms
                            Plot = true;
                            Phase -= 100;
                            last_signe = signe;
                        } else {
                            Plot = false;
                        }
                    }
                    signal[t] = Plot ? Math.sin(phi) : 0;
                }
            }
            if (mode === "multi") {
                let PulseComptage = -1; // pour demarrer la sequence au bord gauche
                const PulseOn = tabPulseSinusOn[pourcentage];
                const PulseTotal = tabPulseSinusTotal[pourcentage];
                for (let t = 0; t < 4000; t++) { // courbe sur 2 secondes
                    if (PulseComptage < PulseOn) {
                        const phi = Math.PI * 2 * 50 * t / 2000;
                        signal[t] = Math.sin(phi);
                    } else {
                        signal[t] = 0;
                    }
                    if (t % 20 === 0) {
                        PulseComptage++;
                        if (PulseComptage >= PulseTotal) PulseComptage = 0;
                    }
                }
            }
            // ===== AJOUT: Mode Réparti avec Bresenham =====
            if (mode === "reparti") {
                const PulseOn = tabPulseSinusOn[pourcentage];
                const PulseTotal = tabPulseSinusTotal[pourcentage];
                const repartition = genererRepartitionBresenham(PulseOn, PulseTotal);
                
                let PulseComptage = -1; // pour demarrer la sequence au bord gauche
                for (let t = 0; t < 4000; t++) {
                    // Test la condition AVANT d'incrémenter (comme dans multi, mais PulseComptage < PulseOn devient repartition[PulseComptage])
                    const indexActuel = PulseComptage < 0 ? 0 : PulseComptage;
                    if (repartition[indexActuel]) {
                        const phi = Math.PI * 2 * 50 * t / 2000;
                        signal[t] = Math.sin(phi);
                    } else {
                        signal[t] = 0;
                    }
                    // Incrément APRÈS, exactement comme dans multi
                    if (t % 20 === 0) {
                        PulseComptage++;
                        if (PulseComptage >= PulseTotal) PulseComptage = 0;
                    }
                }
            }
            // ==============================================
            if (mode === "train") {
                for (let t = 0; t < 4000; t++) { // courbe sur 2 secondes
                    if ((t % 1980) > pourcentage * 20) {  // prise en compte du seuil 990ms pour avoir un total impair
                        signal[t] = 0;
                    } else {
                        const phi = Math.PI * 2 * 50 * t / 2000;
                        signal[t] = Math.sin(phi);
                    }
                }
            }
            return signal;
        }
        function tracerGraphe(divId, signal, duree, nbPoints) {
            const div = document.querySelector(divId);
            const H = div.clientHeight;
            const W = div.clientWidth;
            let svg = `<svg height="${H}" width="${W}">`;
            svg += `<line x1="0" y1="${H * 0.9}" x2="${W}" y2="${H * 0.9}" style="stroke:white;stroke-width:1" />`;
            svg += `<polyline points="`;
            
            for (let t = 0; t < nbPoints; t++) {
                const Y = H * (0.45 - 0.43 * signal[t]);
                const X = W * t / nbPoints;
                svg += `${X},${Y} `;
            }
            svg += `" style="fill:none;stroke:yellow;stroke-width:1" />`;
            // Graduation temporelle
            const intervalleGrad = duree >= 500 ? 100 : 50;
            const pasGrad = intervalleGrad * nbPoints / duree;
            
            for (let t = 0; t < nbPoints; t += pasGrad) {
                const X = W * t / nbPoints;
                const X3 = W * (t + 4) / nbPoints;
                const Y = H * 0.9;
                const Y2 = H * 0.95;
                const Y3 = H * 0.99;
                const T = Math.round(t * duree / nbPoints);
                svg += `<line x1="${X}" y1="${Y2}" x2="${X}" y2="${Y}" style="stroke:white;stroke-width:1" />`;
                svg += `<text x="${X3}" y="${Y3}" fill="white">${T}</text>`;
            }
            
            const X3 = W * 980 / 1000;
            const Y3 = H * 0.99;
            svg += `<text x="${X3}" y="${Y3}" fill="white">ms</text></svg>`;
            div.innerHTML = svg;
        }
        function trace() {
            const curseurOuverture = document.querySelector("#curseurOuverture");
            const curseurDuree = document.querySelector("#curseurDuree");
            const percentOuverture = document.querySelector("#percentOuverture");
            const valueDuree = document.querySelector("#valueDuree");
            const pourcentage = parseInt(curseurOuverture.value);
            const duree = parseInt(curseurDuree.value);
            
            percentOuverture.textContent = pourcentage;
            valueDuree.textContent = duree;
            // Nombre de points proportionnel à la durée (2 points par ms pour 50Hz)
            const nbPoints = duree * 2;
            // Générer et tracer les 4 graphiques
            const modes = [
                { nom: "decoupe", div: "#Decoupe", trame: "#trameDecoupe" },
                { nom: "demi", div: "#Demi", trame: "#trameDemi" },
                { nom: "multi", div: "#Multi", trame: "#trameMulti" },
                { nom: "reparti", div: "#Reparti", trame: "#trameReparti" }, // AJOUT
                { nom: "train", div: "#Train", trame: "#trameTrain" }
            ];
            modes.forEach(mode => {
                const signal = genererSignal(mode.nom, pourcentage, nbPoints);
                tracerGraphe(mode.div, signal, duree, nbPoints);
                
                // Afficher la longueur de trame
                const longueurTrame = calculerLongueurTrame(mode.nom, pourcentage);
                const trameSpan = document.querySelector(mode.trame);
                trameSpan.textContent = `(Trame: ${longueurTrame} ms)`;
            });
        }
        // Gestionnaires des boutons +/-
        document.addEventListener('DOMContentLoaded', function() {
            const curseurOuverture = document.querySelector("#curseurOuverture");
            const curseurDuree = document.querySelector("#curseurDuree");
            
            // Boutons Ouverture
            document.getElementById('btnOuverturePlus').addEventListener('click', function() {
                let val = parseInt(curseurOuverture.value);
                if (val < 100) {
                    curseurOuverture.value = val + 1;
                    trace();
                }
            });
            
            document.getElementById('btnOuvertureMinus').addEventListener('click', function() {
                let val = parseInt(curseurOuverture.value);
                if (val > 0) {
                    curseurOuverture.value = val - 1;
                    trace();
                }
            });
            
            // Boutons Durée
            document.getElementById('btnDureePlus').addEventListener('click', function() {
                let val = parseInt(curseurDuree.value);
                if (val < 2000) { // courbe sur 2 secondes
                    curseurDuree.value = val + 50;
                    trace();
                }
            });
            
            document.getElementById('btnDureeMinus').addEventListener('click', function() {
                let val = parseInt(curseurDuree.value);
                if (val > 100) {
                    curseurDuree.value = val - 50;
                    trace();
                }
            });
        });
        </script>
    </body>
</html>

explications ici
  1. Smile
   
Répondre

#16
Le demi-sinus à 990ms, c'est pour équilibrer les pulses positifs et négatifs. Si vous avez un nombre impair et vous avez une trame de 1000ms, vous aurez une composante continue sur le long terme.

André
Répondre



Atteindre :


Utilisateur(s) parcourant ce sujet :
2 visiteur(s)

Moteur MyBB, © 2002-2025 Melroy van den Berg.