Se stai cercando informazioni sul Coronavirus (COVID-19) ti raccomando di visitare i seguenti siti ufficiali:
Informazioni sul Coronavirus in Italia (Ministero della Salute)
Informazioni sul Coronavirus nel mondo (Organizzazione Mondiale della Sanità)
Panoramica delle condizioni del Coronavirus (Organizzazione Mondiale della Sanità)
Domande e risposte sul Coronavirus (Organizzazione Mondiale della Sanità)
Io non sono un medico e il presente articolo è stato ispirato da quello pubblicato sul The Whashington Post: Why outbreaks like coronavirus spread exponentially, and how to “flatten the curve”
Quello che mi interessa in questa sede è dare uno spunto di programmazione a tutti quelli che, come me, ora sono chiusi in casa ed hanno voglia di ingannare il tempo con qualcosa di diverso dal solito (insomma, buttandosi a fare un po’ di programmazione!).
Spero che questo sia anche un ottimo esempio del perché, in questo momento, dobbiamo fare di tutto per restare il più possibile a casa ed evitare il contatto con gli altri, per la nostra e la loro salute, anche se sono nostri familiari o amici.
Fatte queste premesse arriviamo a noi: quello che vogliamo fare è costruire un simulatore, similmente a quanto dimostrato dal The Whashington Post, su come si possa espandere un’epidemia e quanto sia utile ridurre al minimo l’interazione tra i vari membri della popolazione.
Il simulatore è costruito interamente in JavaScript utilizzando l’elemento canvas del HTML5.
La versione interattiva del simulatore, realizzato come illustrato nell’articolo, si trova in fondo all’articolo |
Quello che vogliamo costruire è un simulatore come questo di seguito:
I parametri in input che vogliamo passare al simulatore saranno:
- la popolazione totale (800 nel mio caso)
- il numero di malati iniziali (ne basta anche solo 1)
- il numero di quelli che restano fermi
- il tempo di guarigione in millisecondi (3000 nel mio caso)
Giocando un po’ con il simulatore ci accorgeremo subito del perché restare a casa è in questo momento fondamentale.
Partendo dai parametri precedenti e impostando a 0 il numero di quelli fermi otterremo una curva simile alla seguente:
In questo caso il contagio ha raggiunto un picco massimo del 81% della popolazione, infettando tutti quanti alla fine della simulazione. Nella realtà questo picco rappresenta il massimo impatto sul sistema sanitario, che nella realtà sarebbe al collasso se dovesse avvenire qualcosa di simile.
Se aumentiamo il numero della popolazione che sta ferma (200 individui, ovvero il 25%) abbiamo una curva simile alla seguente:
Notiamo che il picco massimo è sceso leggermente, arrivando al 68%, ma restando comunque molto alto.
Attuando misure più restrittive e fermando il 75% della popolazione, ossia 600 individui su 800, avremo una curva come la seguente:
Il picco si è dimezzato in questo caso, arrivando ad un massimo del 39%. Con misure ancora più restrittive, arrivando a fermare 700 individui, quindi l’87,5% della popolazione, si arriva a picchi ancora inferiori, ma non inesistenti (si parla del 21%):
Questo modello rappresenta, sebbene in linea di massima, molto bene la situazione attuale. Le misure restrittive servono principalmente per evitare il collasso del sistema sanitario e dare la possibilità ai medici di occuparsi di tutti quanti i pazienti più gravi, cosa che in caso contrario sarebbe impossibile. Il contagio allungherà così la sua presenza nel tempo, ma verrà anche abbattuto il suo impatto sulla società.
Inutile dire come questa simulazione sia molto approssimativa e non tiene conto di altri fattori, tra il quale anche il decesso dei malati.
Detto tutto questo arriviamo al codice vero e proprio. Anzitutto creiamo l’interfaccia in HTML nel modo seguente:
1 2 3 4 5 6 7 8 9 10 |
<div> Popolazione <input type="text" value="800" id="npallini" size="4"> Malati iniziali <input type="text" value="1" id="nmalati" size="4"> Fermi <input type="text" value="600" id="nfermi" size="4"> T. guarigione <input type="text" value="2000" id="tempoguarigione" size="4"> <input type="button" value="INIZIA" id="inizia" onclick="simulazione()"> </div> <canvas id="particelle" width="800px" height="600px"></canvas> <canvas id="grafico" width="800px" height="120px"></canvas> <div id="info"></div> |
Procediamo adesso con il codice in JavaScript, che riporto di seguito con i commenti all’interno del codice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
// registriamo l'id dell'animazione fornito da requestAnimationFrame // in partenza questo valore sarà null idSimulazione = null; // vogliamo dare la possibilità di interrompere l'animazione // di predefinito non vi è alcuna chiamata stop ovviamente stop = false; // questa funzione viene chiamata quando si clicca sul pulsante INIZIA function simulazione() { /* GESTIONE DEL PULSANTE - INIZIO */ // leggiamo il valore del pulsante var pulsante = document.getElementById("inizia").value; // se il testo è FERMA, dobbiamo fermare l'animazione if( pulsante == "FERMA" ) { // cancelliamo la chiamata al browser per effettuare l'animazione window.cancelAnimationFrame( idSimulazione ); // impostiamo stop su true, in modo da fermare i cicli di seguito stop = true; // cambiamo il valore del pulsante su inizia document.getElementById("inizia").value = "INIZIA"; // altrimenti la iniziamo } else { stop = false; // cambiamo il valore del pulsante su ferma document.getElementById("inizia").value = "FERMA"; } /* GESTIONE DEL PULSANTE - FINE */ // preleviamo il riferimento al canvas delle particelle var canvas = document.getElementById("particelle"); var ctx = canvas.getContext("2d"); // preleviamo il riferimento al canvas del grafico var grafico = document.getElementById("grafico"); var gctx = grafico.getContext("2d"); // ATTENZIONE! questi due passaggi sono importanti per entrambi i canvas gctx.beginPath(); // questo ci serve per evitare lo slowdown, in caso contrario il canvas terrà conto di tutte le precedenti scritture, rallentando visibilmente l'esecuzione dell'animazione // in questo modo puliamo il canvas gctx.clearRect(0, 0, grafico.width, grafico.height); // metodo per disegnare una riga verticale gctx.drawVerticalLine = function(left, top, width, color){ this.fillStyle=color; this.fillRect(left, top, 1, width); }; // queste sono le pareti del nostro spazio var pareti = [ [5,5], [795,5], [795,595], [5,595] ]; // preimpostiamo tutti i valori var pallini = []; // popolazione var npallini = document.getElementById("npallini").value ? document.getElementById("npallini").value : 800; // n malati var nmalati = document.getElementById("nmalati").value ? document.getElementById("nmalati").value : 1; // n di quelli fermi var nfermi = document.getElementById("nfermi").value ? document.getElementById("nfermi").value : 600; // tempo di guarigione var tempoguarigione = document.getElementById("tempoguarigione").value ? document.getElementById("tempoguarigione").value : 2000; // altre variabili per il funzionamento del sistema var maxmalati = 0; var frame = 0; var start = new Date().getTime() / 1000; // predisponiamo tutte le palline in modo casuale // ogni pallino rappresenta un membro della popolazione var pallinicalc = []; for( i = 0; i < npallini; i++ ) { var x = Math.floor(Math.random() * (canvas.width-20)) + 10; var y = Math.floor(Math.random() * (canvas.height-20)) + 10; var pos = [x, y]; // verifichiamo che la posizione non sia già esistente, in tal caso non // aggiungiamo il pallino al numero totale var trovato = false; for( j = 0; j < pallinicalc.length; j++ ) { if(JSON.stringify(pos)==JSON.stringify(pallinicalc[j])) { trovato = true; } } if( !trovato ) { pallinicalc.push(pos); } else { i--; // se la posizione esiste di già, torniamo di uno indietro e riproviamo } } // con questa classe gestiamo ogni membro della popolazione class Pallino { constructor(x, y, stato, direzione, velocita) { this.x = x; this.y = y; // 0 = sano, 1 = malato, 2 = guarito this.stato = stato; // valore in pi-greco this.direzione = direzione; this.velocita = velocita; this.raggio = 5; // il tempo ci servirà per valure la guarigione this.tempo = stato ? new Date().getTime() : 0; } // calcoliamo l'urto sulle pareti // nel mio caso ho considerato solamente pareti verticali ed orizzontali urtoParete() { var nPareti = pareti.length; for( vi = 0; vi < nPareti; vi++ ) { var vf = vi < nPareti - 1 ? vi + 1 : 0; // calcoliamo il coefficiente angolare della parete var a = (pareti[vf][1]-pareti[vi][1])/(pareti[vf][0]-pareti[vi][0]); // parete orizzontale if( isFinite( a ) ) { var b = -1; var c = -a * pareti[vi][0] + pareti[vi][1]; var d = Math.abs(a * this.x + b * this.y + c) / Math.sqrt(a*a+b*b); // invertiamo velocità e direzione if( d <= this.raggio ) { this.direzione = -this.direzione; this.velocita = this.velocita; } // parete verticale } else { // invertiamo velocità e direzione if( Math.abs( this.x - pareti[vf][0] ) <= this.raggio ) { this.direzione = -this.direzione; this.velocita = -this.velocita; } } } } // controlliamo se i pallini si toccano tra di loro urtoPallino(pallino) { // se il pallino non è se stesso if( this.x != pallino.x && this.y != pallino.y ) { // la massima distanza tra due pallini var d = Math.sqrt(Math.pow(this.x-pallino.x,2)+Math.pow(this.y-pallino.y,2)); // la confrontiamo con la somma dei raggi di ciascun pallino if( d <= this.raggio + pallino.raggio ) { // questa è una semplificazione dell'urto elastico bidimensionale // visto che entrambi i pallini hanno la medesima massa // calcoliamo l'angolo di impatto e sommiamolo alla direzione var phi = Math.atan((this.y-pallino.y)/(this.x-pallino.x)); this.direzione += phi / 2; if( this.stato < 1 && pallino.stato == 1 ) { this.stato = 1; this.tempo = new Date().getTime(); } } } } // restituiamo il colore in base allo stato: sano, malato, guarito getColore() { var colori = ["#0095DD","#ed1c24","#3cb878"]; return colori[this.stato]; } // controlliamo le interazioni da eseguire su tutti controlla() { for(var j = 0; j < pallini.length; j++ ) { this.urtoPallino(pallini[j]); } this.urtoParete(); if( new Date().getTime() - this.tempo > tempoguarigione && this.tempo > 0) { this.stato = 2; } } // poi effettuiamo il movimento, derivato dai controlli precedenti muovi() { this.x += this.velocita * Math.cos(this.direzione); this.y += this.velocita * Math.sin(this.direzione); } } // creiamo tutti i pallini preimpostati in cima, distribuendo malati e sani for( i = 0; i < pallinicalc.length; i++ ) { var stato = nmalati-- > 0 ? 1 : 0; var velocita = nfermi-- > 0 ? 0 : 2; pallini.push(new Pallino(pallinicalc[i][0],pallinicalc[i][1],stato,2*Math.PI*Math.random(),velocita)); } // avviamo il processo di disegno function disegna() { // ripuliamo il canvas come prima per il grafico ctx.beginPath(); // questo ci serve per evitare lo slowdown ctx.clearRect(0, 0, canvas.width, canvas.height); var sani = 0; var malati = 0; var curati = 0; // disegniamo le pareti function disegnaPareti() { var nPareti = pareti.length; for( vi = 0; vi < nPareti; vi++ ) { var vf = vi < nPareti - 1 ? vi + 1 : 0; ctx.moveTo(pareti[vi][0], pareti[vi][1]); ctx.lineTo(pareti[vf][0], pareti[vf][1]); ctx.stroke(); } } // disegniamo un pallino // NB sto usando Path2D per evitare che venga disegnato // il bordo di controno per ciascun pallino function disegnaPallino(x, y, colore, raggio) { var pallino = new Path2D(); pallino.arc(x, y, raggio, 0, Math.PI*2, false); ctx.fillStyle = colore; ctx.fill(pallino); } // disegniamo tutti i pallini chiamando la funzione precedente function disegnaPallini() { for(i = 0; i < pallini.length; i++ ) { disegnaPallino(pallini[i].x,pallini[i].y,pallini[i].getColore(),pallini[i].raggio); // già che ci siamo contiamo lo stato attuale di sani e malati switch( pallini[i].stato ) { case 0: sani++; break; case 1: malati++; break; case 2: curati++; break; } } } // chiamiamo i controlli per il movimento dei pallini function muoviPallini() { for(i = 0; i < pallini.length; i++ ) { pallini[i].controlla(); } for(i = 0; i < pallini.length; i++ ) { pallini[i].muovi(); } } // disegniamo il grafico nel tempo e scriviamo tutti i parametri function disegnaGrafico() { var colori = ["#0095DD","#ed1c24","#3cb878"]; maxmalati = malati > maxmalati ? malati : maxmalati; gctx.drawVerticalLine(frame, 0, grafico.height * curati / npallini, colori[2]); gctx.drawVerticalLine(frame, grafico.height * curati / npallini, grafico.height * sani / npallini, colori[0]); gctx.drawVerticalLine(frame, grafico.height * (curati+sani) / npallini, grafico.height * malati / npallini, colori[1]); document.getElementById("info").innerHTML = 'Sani: <span style="color:'+colori[0]+'">'+sani+'</span> • Malati: <span style="color:'+colori[1]+'">'+malati+'</span> • Guariti: <span style="color:'+colori[2]+'">'+curati+'</span> • Picco malati: <span style="color:'+colori[1]+'">'+maxmalati+' ('+Math.round(100*maxmalati/npallini,2)+'% del totale)</span>'; } disegnaPareti(); disegnaPallini(); muoviPallini(); disegnaGrafico(); // controlliamo se l'animazione prosegue o meno if( !stop ) idSimulazione = window.requestAnimationFrame(disegna); console.log("id: " + idSimulazione ); var now = new Date().getTime() / 1000; // avanziamo di frame frame++; } function init() { idSimulazione = window.requestAnimationFrame(disegna); } init(); } |
Infine riporto di seguito l’esempio funzionante, per chiunque lo volesse testare al volo (inserisci i parametri e premi INIZIA):
Con un po’ di pazienza e qualche altro accorgimento (per esempio che i pallini non possano essere generati fuori dalle pareti) si può implementare la simulazione per avere anche diverse zone isolate.In questo caso notiamo come una zona isolata aiuti ulteriormente a ridurre il numero di contagiati e mantenere anche parte della popolazione sana, benché nessuno stia fermo.
Per costruire la zona isolata possiamo predisporre le pareti nel modo seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var pareti = [ [5,5], [200,5], [200,300], [210,300], [210,5], [795,5], [795,595], [210,595], [210,350], [200,350], [200,595], [5,595] ]; |
Dobbiamo inoltre prendere qualche altro accorgimento, ma questo lo lascio per chi ha voglia di provarci per conto proprio. 🙂