Laboratorio di Calcolo 2

proff. A. Andreazza, D. Galli, E. Spoletini

Universita' degli Studi di Milano

Anno Accademico 2007/2008

Lezione 4

Introduzione

Scopo di questa sessione di laboratorio e' sviluppare delle applicazioni dei numeri pseudocasuali. Per fare questi esercizi utilizzeremo principalmente gli oggetti TRandom di ROOT. Per impratichirci con questi oggetti quindi, anziche' utilizzare programmi compilati, scriveremo principalmente delle macro.

Vedremo come utilizzare tali oggetti all'interno di un programma compilato.

Classe TRandom

La classe TRandom permette di costruire oggetti in grado di generare numeri pseudocasuali.

Il generatore di numeri casuali che sta all'interno di TRandom utilizza il classico metodo della congruenza visto a lezione. Se vi ricordate questo metodo ha il problema che, se si guarda in dettaglio, esiste una correlazione tra un numero ed il suo successivo, che si dispongono in realta` su una serie di rette parallele.

Mentre continua la spiegazione, scricate e fate eseguire la macro
correlazione.C:
questa macro usa un oggetto TRandom per generare numeri uniformemente nell'intervallo (0,1) e crea un istogramma di xN in funzione di xN-1 Siccome le rette su cui si dispongono i numeri sono molto dense, per vedere l'effetto bisogna ingrandire molto una regione dello spazio che contiene i punti, ma allora solo una piccola frazione di questi punti cadra` nella regione selezionata ed il tempo di esecuzione sara` quindi molto grande, perche' bisogna generare molti piu` punti di quelli utili.

L'esecuzione della macro dovrebbe dare un grafico simile al seguente, anche se con meno statistica:

ma richiede parecchio tempo di CPU.

Esercizio

Mentre la macro e' in escuzione provate a trasformarla in un programma compilato. Seguendo i consigli della lezione precedente: Usare le classi di ROOT in un programma.

Potrete verificare che il programma compilato gira molto piu` velocemente della macro. Questa e' una caratteristica generale da tenere a mente: i linguaggi interpretati, come le macro di ROOT che vengono tradotte in istruzioni per il calcolatore da un interprete nel momento stesso in cui devono venire eseguite, sono spesso molto piu` flessibili e permissivi dei linguaggi compilati, ma molto meno efficienti nell'esecuzione delle operazioni.

Si pone comunque il problema di visualizzare i grafici prodotti, dato che il programma fatto partireda linea di comando non ha la possibilita' di farlo. Ci sono due modi per affrontare il problema:

  1. utilizzare i TFile, come indicato nella precedente lezione, salvando in un file gli istogrammi e poi visualizzandoli con un TBrowser;
  2. utilizzare un oggetto di tipo TApplication. La ricetta da seguire in questo caso e':
    1. includere il file header di TApplication:
      #include <TApplication.h>
    2. all'inizio del programma creare un oggetto TApplication:
      TApplication myApp("nome",0,0)
      (gli zeri sono necessari per far partire l'applicazione senza passarle parametri, "nome" puo` essere una stringa qualunque);
    3. come ultima operazione nel programma principale, trasferire il controllo all'oggetto TApplication creato in precedenza:
      myApp.Run();
      da questo punto in poi, il controllo passerà all'oggetto attraverso l'interfaccia grafica di ROOT, che si occupera` di disegnare tutti i canvas ed istrogrammi di cui e` stato fatto il Draw() nel programma principale, ma non potranno essere svolte ulteriori istruzioni nel programma C++.

Esercizio facoltativo

Per ovviare ai problemi di correlazione dell'algoritmo di congruenza lineare, ROOT fornisce altri oggetti, TRandom2 e TRandom3, che hanno le stesse proprieta` degli oggetti TRandom, salvo dei generatori che adottano particolari accorgimenti per ridurre le correlazioni: se siete riusciti a costruire il programma compilato, provate a farlo rigirare usando TRandom2 o TRandom3 al posto del semplice TRandom.

Metodi degli oggetti TRandom

TRandom(unsigned int seed); (costruttore)
costruisce un oggetto TRandom, inizializzando il seme del generatore di numeri casuali a seed.
Se seed=0, allora il seme viene generato a partire dall'orologio del sistema
(con granularita` di un secondo, per cui fare attenzione: se si dichiarano insieme diversi oggetti TRandom,
molto probabilmente questi genereranno la stessa sequenza di numeri).
void SetSeed(unsigned int seed);
definisce il seme del generatore di numeri casuali, se invocato con argomento 0,
lo costruisce a partire dall'orologio di sistema.
double Rndm();
restituisce un numero distribuito uniformemente nell'intervallo [0,1]
int Poisson(double m);
restituisce un numero intero secondo la distribuzione di Poisson con valor medio m
double Gaus(double media, double sigma);
restituisce un numero secondo una distribuzione gaussiana con i valori dati di valor medio e sigma.
double Uniform(double min, double max);
restituisce un numero secondo una distribuzione uniforme nell'intervallo corrispondente ai valori dati.
double Exp(double tau);
restituisce un numero secondo una distribuzione esponenziale (1/τ)exp(-x/τ) con il valore dato del parametro τ.
double BreitWigner(double media, double gamma);
restituisce un numero secondo una distribuzione di Breit-Wigner (tipica dei fenomeni di risonanza: (2/π) Γ/((x-xmedio)22)
con i valori dati dei parametri xmedio e Γ.

Utilizzo degli oggetti Trandom

Per definire un oggetto di tipo TRandom, basta dichiararlo:
TRandom nomeOggetto;
eventualmente utilizzando un costruttore per inizializzare il seme del generatore:
TRandom nomeOggetto(0);.

Per usare i metodi ad esso associati, si usa la solita sintassi degli oggetti:
nomeOggetto.nomeMetodo(argomenti);
Per fare un esempio, per stampare 100 numeri interi estratti da una distribuzione di Poisson con valor medio 3, potremmo scrivere il seguente frammento di codice in RooT:


TRandom gen(0);
for (int i=0; i<100; i++) cout << gen.Poisson(3.) << endl;

E' possibile anche costruire puntatori ad oggetti, utilizzanto l'operatore new abbinato ad un costruttore:
TRandom *gen = new TRandom(0);
ma a questo punto per accedere ai metodi, bisogna usare l'operatore freccia:
gen->Poisson(3.)

Potete trovare una tabella riassuntiva con la selezione dei tipi di oggetti e dei relativi metodi che saranno utili per questa lezione e per la parte di simulazione. Tali tipi di oggetti sono accessibili solo all'interno di ROOT (vedremo in seguito come utilizzarli all'interno dei nostri programmi).

Oltre alla tabella riassuntiva, potete trovare:

Ripasso sulle macro

Per eseguire compiti complessi o ripetitivi e' possibile utilizzare delle macro. Siccome il linguaggio di ROOT e' il C++, le macro non sono altro che file contenenti frammenti di programmi in C++ (o piu' semplicemente in C, nel nostro caso).
Ci sono due tipi di macro: Le prime contengono un blocco di istruzioni delimitato da parentesi graffe. La maggior parte delle macro che avete eseguito fino ad adesso cadono in questa categoria. Per eseguire una macro di questo tipo e' sufficiente dare il comando
.x nomefile
e le variabili definite all'interno di questo tipo di macro, rimangono accessibili anche dopo la sua conclusione (se non ci sono stati errori di esecuzione).

Una macro puo' anche contenere la definizione di una nuova funzione, come la macro medialetture.C che definisce una funzione TH1F medialetture(char* nomefile,int N) che legge dei numeri da un file e ne fa la media di N alla volta e li mette in un istogramma.

In tal caso non ha senso eseguire direttamente la macro (come facciamo a passare i parametri giusti alla funzione?). Si puo' pero' caricarla in memoria con il comando:
.L nomefile
A questo punto la nuova funzione e' nota a ROOT e puo' essere utilizzata da linea di comando o da altre macro. Ad esempio al prompt di ROOT si puo` dare le seguente lista di comandi:

.L medialetture.c
TRandom gen(0);
ofstream g("miofile.dat");
for (int i=0; i<10000; i++) g << gen.Rndm() << endl;
g.close();
TH1F isto=medialetture("miofile.dat",1);
isto.Draw(); 

Se ci si accorge che la funzione ha degli errori e si vuole modificare la macro, si puo':

  1. scaricare la macro dalla memoria con il comando .U nomefile
  2. modifica il file contenente la macro
  3. caricarlo di nuovo con .L nomefile

N.B.: poiche' ROOT esegue automaticamente l'inclusione di molti degli include file di sistema, spesso non c'e' bisogno delle dichiarazioni di #include nel file contentente la macro.

Teorema del limite centrale

Il teorema del limite centrale, detto anche "legge dei grandi numeri" e' la base di tutto il trattamento statistico degli errori e grosso modo puo' essere formulato:
Siano x1, x2, x3, ...xN numeri casuali tutti estratti da una stessa distribuzione con valor medio Xm e varianza σ2. Allora, per N tendente all'infinito la quantita' (x1+x2+...+xN)/N (la media degli N numeri) tendera' ad avere una distribuzione gaussiana con valor medio Xm e varianza σ2/N.

Il teorema di fatto dice che, se si fanno tante misure, anche con errori non gaussiani, la media alla fine avra' una distribuzione gaussiana ed un'incertezza (r.m.s. ovvero la radice quadrata della varianza) che scala con la radice quadrata del numero di misure mediate.

Esercizio

Verificare empiricamente il teorema del limite centrale:

  1. costruire una macro per ROOT che scriva su di un file dei numeri distribuiti uniformemente tra 0 e 1.
  2. leggere il file e mettere i valori ottenuti in un istogramma, verificare che la distribuzione sia corretta, ben contenuta nell'istogramma e segnare il suo valor medio ed r.m.s.
  3. leggere i numeri del file facendone le medie a due a due, fare un istogramma delle medie.
  4. ripetere il punto 3 facendo le media a gruppi di 4 e 8: verificare che la distribuzione diventi sempre piu` gaussiana come forma e che l'rms scali nel modo indicato sopra.

Esercizio facoltativo

Provare a ripetere l'esercizio precedente con altre distribuzioni di partenza:

Nel caso della Breit-Wigner si vede che il teorema del limite centrale non funziona e la r.m.s. non dimisuisce facendo la media. Quale delle ipotesi del teorema non e' verificata per questa distribuzione?

Esercizio facoltativo

Per distribuzioni come la Breit-Wigner, per avere una stima del punto centrale della distribuzione, aziche' il valor medio, conviene prendere la mediana: Si dovrebbe vedere che, mentre la varianza della media non cambia, quella della mediana scende rapidamente con il numero di misure.

Integrazione con metodi Monte Carlo

Una forma di integrazione possibile e' quella di estrarre delle numeri a caso in un area che contiene il grafico della funzione da integrale. La frazione dei punti che cadono all'interno del grafico della funzione moltiplicata per l'area totale su cui si estraggono i punti, da` il valore dell'integrale.

Questo metodo puo` essere utile nel caso di funzioni rapidamente oscillanti o nel caso di integrali multidimensionali.

Una funzione oscillante

Come esercizio, provare a calcolare l'integrale tra 0 e 1 della funzione sin2(1/x) estraendo punti a caso nel quadrato (0,1)x(0,1). Farsi stampare il valore dell'integrale aggiornato ogni 100 estrazioni: facendo un grafico di questo numero dovreste vedere un fenomeno del tipo di quello presentato nella figura di seguito, in cui, dopo alcune oscillazioni iniziali, l'integrale converge:

Integrali multidimensionali

Proviamo ad applicare lo stesso metodo al calcolo di volumi di oggetti bidimensionali. Provare a scrivere una macro di ROOT che calcoli il volume di una sfera n-dimensionale di raggio 1: fate un grafico del valore ottenuto in funzione del numero di punti estratti, come nel caso dell'integrale precedente.

Per comodita` sarebbe meglio fare il grafico della differenza tra il valore calcolato del volume della sfera e quello vero:

Si noti che in ROOT si puo` accedere ai valori di π ed alla funzione Γ attraverso le chiamate alle funzioni:
double TMath::Pi();
double TMath::Gamma(double);

Andando su qualcosa di piu` complicato, la seguente macro calcola il potenziale gravitazionale della terra, assumendo che sia una sfera di densita` uniforme, effettuando con il metodo Monte Carlo l'integrale:


double potenziale(double x, double y, double z) {
  int Npunti=1000000;
  TRandom generatore(0);
  double G =    6.6742E-11 ;  // costante di gravitazione universale
  double massa= 5.9723E+24 ;  // massa della terra
  double raggio=6.5781E+06 ;  // raggio della terra
  double potenziale=0;
  double volume=0;
  for ( int i=0; i<Npunti; i++ ) {
    double x1 = generatore.Uniform(-raggio,raggio); 
    double y1 = generatore.Uniform(-raggio,raggio); 
    double z1 = generatore.Uniform(-raggio,raggio); 
    if ( (x1*x1+y1*y1+z1*z1) < (raggio*raggio) ) {
      volume+=1.;
      double r=sqrt( pow(x-x1,2)+pow(y-y1,2)+pow(z-z1,2) );
      potenziale-= G*massa/r;
    }
  }
  return potenziale/volume;
}

Costruire un grafico del potenziale gravitazionale dal centro della sfera, fino ad una distanza pari a 3 volte il raggio della sfera:

Si noti che ora la normalizzazione deve essere fatta in modo diverso per tenere conto del numero di punti effettivamente utilizzati.

Provare ad adattare la funzione di cui sopra al caso in cui la massa fosse in realta` concentrata in una crosta sferica di raggio 1000 km.

In questo caso e' forse piu` utile passare direttamente in coordinate polari, generando numeri uniformemente negli intervalli:

  1. RTerra-1000 km < r < RTerra
  2. 0 < θ < π
  3. 0 < φ < 2π
e ricordandosi che la funzione integranda in coordinate polari cambia forma e diventa:

Alla fine di tutto, compilare la relazione