Laboratorio di Calcolo 2

proff. A. Andreazza, D. Galli, E. Spoletini, G. Tiana, A. Vairo

Universita' degli Studi di Milano

Anno Accademico 2004/2005

Lezione 3

Introduzione

Scopo di questa sessione di laboratorio e' imparare ad utilizzare il programma ROOT per l'analisi dei dati, simile al programma PAW usato al primo anno. La principale differenza e' che ROOT usa il C++ come linguaggio per l'esecuzione dei comandi.
Oltre ad avere un'interfaccia grafica molto piu' flessibile, ROOT ci fornisce oggetti utili per l'istogrammazione e la generazione di numeri casuali.
 

Oggetti e strutture

A lezione avete visto come il C++ permetta di definire degli aggregati complessi di dati sotto forma di strutture.

Un esempio di struttura puo' essere un punto bidimensionale:

struct punto {
   float x;
   float y;
};
Dopo aver definito questa struttura, noi possiamo definire delle variabili di tipo punto ed accedere alla loro struttura interna, ad esempio:
struct punto XY1, *XY2; // dichiarazione una variabile e di
                        // un puntatore alla struttura
XY1.x=1.;               // inizializzazione di XY1
XY1.y=2.;
XY2 = new struct punto;
// L'operatore new funziona anche sulle struture.
*XY2 = XY1;
// esiste l'operatore di assegnazione di strutture
// (copia membro a membro)
cout << "Le coordinate del punto 2 sono: " << XY2->x << XY2->y << endl;
// per i puntatori l'operatore per accedere alle variabili
// e' "->" invece di "."
Il tutto e' molto piu' snello se usiamo un typedef per abbreviare la definizione:
typedef struct punto PUNTO;
In tal caso le istruzioni diventano semplicemente
PUNTO XY1, *XY2; // dichiarazione una variabile e di
                        // un puntatore alla struttura
XY1.x=1.;               // inizializzazione di XY1
XY1.y=2.;
XY2 = new PUNTO;
// L'operatore new funziona anche sulle struture.
*XY2 = XY1;
// esiste l'operatore di assegnazione di strutture
// (copia membro a membro)
cout << "Le coordinate del punto 2 sono: " << XY2->x << XY2->y << endl;
// per i puntatori l'operatore per accedere alle variabili
// e' "->" invece di "."

Le strutture permettono solamente di raccogliere un insieme di dati, lasciando agli utenti la massima liberta' (ovvero, nessuna protezione) nella loro gestione. I linguaggi di programmazione piu' recenti (C++, Java...) tendono ad enfatizzare delle altre entita' dette oggetti. Mentre una struttura pone l'enfasi sui dati, gli oggetti pongono l'enfasi sul comportamento: un oggetto e' caratterizzato da un insieme di funzioni, dette metodi, attraverso le quali puo' operare. La sua struttura interna contiene dei dati che pero' non sono accessibili all'utente: quello che importa sono solo le azioni che l'oggetto puo' compiere. In C++ si possono definire delle classi, ovvero dei tipi di oggetti. Una volta definita una classe si possono istanziare oggetti di tale classe, sia usando dichiarazioni di tali oggetti oppure usando l'operatore new. Una volta dichiarato un oggetto si possono chiamare i sui metodi allo stesso modo con cui si accede alle componeni di una struttura, ovvero

Un particolare metodo e` il costruttore, che puo` venire chiamato al momento della dichiarazione dell'oggetto per crearlo opportunamente inizializzato. In questo corso non creeremo delle classi, ma ci limiteremo ad usare un certo numero di classi predefinite.

Un primo oggetto: un file stream

Per chiarire questi concetti consideriamo le classi ifstream (input file stream) e ofstream (output file stream) che il C++ utilizza per la lettura e scrittura su file. Alcuni metodi di queste classi sono:
void open(char *nomefile) per aprire un file;
void close() per chiudere un file;
int precision(int NumeroDiCifreSignificative) per modificare il numero di cifre significative da stampare per i numeri in virgola mobile;
bool good() restituisce true se il file e` funzionante;
bool bad() restituisce true se c'e` una condizione di errore;
bool eof() restituisce true se si e` tentato di leggere oltre la fine del file.

Ad esempio il seguente programma scrive con precisione sempre crescente il valore di π sul file pigreco.dat:

 
#include <fstream>
#include <cmath>    
// in questo caso il compilatore chiede di inserire la libreria matematica

using namespace std;

int main() {
  ofstream f;                    // dichiarazione di f come oggetto
                                 // di tipo output file stream
  f.open("pigreco.dat");         // Apertura del file pigreco.dat
  for (int i=3; i<20; i++) {
    f.precision(i);              // Cambio di precisione della stampa
    f << 2.*acos(0.) << endl;    // Utilizzo l'operatore di uscita <<
  }
  f.close();                     // Al termine delle operazioni si chiude il file
  return 0;
}
Una volta compilato ed eseguito, il file viene riempito con il seguente contenuto:
3.14
3.142
3.1416
3.14159
3.141593
3.1415927
3.14159265
3.141592654
3.1415926536
3.14159265359
3.14159265359
3.1415926535898
3.14159265358979
3.141592653589793
3.1415926535897931
3.14159265358979312
3.141592653589793116

In questo esempio f e`stato prima dichiarato e poi inizializzato invocando il metodo open. L'inizializzazione puo` anche avvenire direttamente al momento della dichiarazione, invocando un costruttore. Una classe puo` avere anche piu` di un costruttore. Nel nostro caso, un costruttore per gli oggetti ifstream e ofstream e`una funzione che ha come argomento il nome del file da aprire. Il costruttore viene invocato al momento della dichiarazione usando la seguente sintassi:

ofstream f("pigreco.dat")
oppure, nel caso si usino puntatori a oggetti allocati dinamicamente:
ofstream *f= new ofstream("pigreco.dat")

Il programma precedente, puo` essere riscritto in maniera equivalente:

 
#include <fstream>
#include <cmath>

using namespace std;

int main() {
  ofstream f("pigreco.dat");     // dichiarazione di f come oggetto
                                 // di tipo output file stream
                                 // ed invocazione del costruttore
  for (int i=3; i<20; i++) {
    f.precision(i);              // Cambio di precisione della stampa
    f << 2.*acos(0.) << endl;    // Utilizzo l'operatore di uscita <<
  }
  f.close();                     // Al termine delle operazioni si chiude il file
  return 0;
}
 

ROOT

Il programma ROOT (http://root.cern.ch) si propone come un evoluzione di PAW scritta in C++, quindi orientata verso gli oggetti.

Per accedere al programma bisogna configurare alcune variabili d'ambiente, definento ROOTSYS ed aggiungendo la directory dove si trova l'eseguibile di ROOT al PATH.
Probabilmente tali variabili saranno gia` state configurate a livello di sistema, ma per sicurezza potete controllare il loro contenuto dando i comandi:
echo $ROOTSYS
e
echo $PATH
e vedere se in entrambi compare la directory /tools/root.
Nel caso questa non apparisse, si possono definire tali variabili da riga di comando:

export ROOTSYS=/tools/root
export PATH=${PATH}:${ROOTSYS}/bin
Per comodita` potete aggiungere anche questi comandi al file .zshrc.

Se le variabili sono configurate correttamente, sara` sufficiente dare da riga di comando
root
Per invocare il programma.

ROOT interpreta tutto cio' che viene scritto sulla riga di comando attraverso CINT, un interprete C++ interattivo. Quindi dal prompt di ROOT e' possibile dare istruzioni in C++, creare variabili, effettuare operazioni con esse...

Per bypassare l'interprete C++, ci sono un insieme di comandi speciali che iniziano con un '.'.

Il piu' semplice e' quello per terminare la sessione di ROOT:
.q

Classe TRandom

Per fare un esempio di utilizzo di ROOT e delle sue classi, iniziamo a provare ad usare un oggetto di tipo TRandom per la generazione di numeri casuali.
Questi oggetti hanno un insieme di metodi:
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).
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.

Per definire un oggetto di tipo TRandom, basta dichiararlo:
TRandom nomeOggetto;
per usare i metodi ad esso associati, si usa una sintassi simile a quella delle strutture:
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;
Come vedete noi possiamo utilizzare gen per fare quello che vogliamo (generare numeri casuali di diverso tipo) senza sapere niente della sua struttura interna (enfasi sul comportamento e non sui dati).

E' possibile anche costruire puntatori ad oggetti, ma la questione e' un po' sottile ed in ogni caso non saranno necessari per gli esercizi di oggi.

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:

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. Un esempio e' il file randfile.C. Per eseguire una macro di questo tipo e' sufficiente dare il comando
.x nomefile
Variabili definite all'interno di questo tipo di macro, rimangono accessibili anche dopo la sua conclusione.

Una macro puo' anche contenere la definizione di una nuova funzione, come la macro randread.C che definisce una funzione TH1F randread(char*). 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.

Se ci si accorge che la funzione ha degli errori e si vuole modificare la macro, nelle versioni piu' recenti di ROOT, 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

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.

Grafica

Buona parte dei tipi di oggetti (istogramma, grafici...) che useremo sono dotati di un metodo
void Draw()
che disegna l'oggetto all'interno di un pannello.
Una volta disegnato, si possono fare diverse operazioni sull'oggetto utilizzando l'interfaccia grafica.

Per aggiungere testi, frecce, disegni... sul grafico, e' sufficiente selezionare Editor dal menu Edit. Per visualizzare le coordinate del puntatore del mouse, selezionare Options -> Event Status.

Per modificare la presentazione grafica di un oggetto e' possibile selezionarlo posizionandoci sopra il cursore  e schiacciando il bottone destro del mouse. Cio' fa comparire un menu che dipende dal tipo di oggetto selezionato ed indica le opzioni possibili. Particolarmente utili saranno il FitPanel o il DrawPanel per cambiare la rappresentazione degli istogrammi. Per spostare o ridimenzionare un oggetto, basta selezionarlo con il bottone sinistro del mouse.

Per i grafici (ad esempio quelli creati con grafico.C) il DrawPanel non funziona, ma e' possibile cambiare interattivamente le caratteristiche delle linee e dei punti disegnati selezionando SetLineAttributes e SetMarkerAttributes.

Il puntatore del mouse cambia forma a seconda dell'oggetto che si sta selezionando, e questa caratteristica permette di facilitare molto la selezione. Puo' essere utile ricordare che il puntatore a croce di solito indica che si sta selezionando un'area di sfondo, la freccia un grafico o un istogramma.

Infine, se avete fatto un bel grafico e volete salvarlo, dal menu File selezionate Save As.... E` importante indicare esplicitamente il suffisso del file quando si dichiara il nome.
Per rivedere i file salvati potete usare i seguenti programmi:

L'esercizio

Proviamo a verificare il teorema del limite centrale.

Tale teorema 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 r.m.s. σ. Allora, per N tendente all'infinito la quantita' (x1+x2+...+xN)/N tendera' ad avere una distribuzione gaussiana con valor medio Xm e r.m.s. σ/sqrt(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 una precisione migliore delle singole misure.

Verifichiamo empiricamente che questo succeda.

Passo 1: costruire una macro per ROOT che scriva su di un file 25200 numeri distribuiti uniformemente tra 0 e 1.

Passo 2: leggere il file e mettere i valori ottenuti in un istogramma, verificare che la distribuzione sia corretta, il suo valor medio ed rms.

Passo 3: leggere i numeri del file a gruppi di due, fare un istogramma delle medie dei due numeri di ciascun gruppo.

Passo 4: ripetere il passo 3 leggendo i numeri in gruppi di 3, 4,...9, fare gli istogrammi delle medie e verificare l'evoluzione della forma della distribuzione e della sua rms.

Passo 5: fare un grafico del valore della rms in funzione del numero di punti mediati.

Consultando la tabella per gli oggetti di tipo TF1, dovreste essere in grado di tracciare la funzione che descrive i dati osservati.

Ancora sull'integrazione

Per verificare il funzionamento del metodo di integrazione con numeri casuali, provate a creare delle funzioni di ROOT che calcolino: verificando che la convergenza migliora con l'aumentare del numero di punti.

Alla fine di tutto, compilare la relazione Relazione