Fortran 2

Da CdM_unimore.

Lezione 14

Dichiarazioni alternative di matrici e vettori



Il linguaggio di programmazione FORTRAN consente di dichiarare vettori e matrici con elementi che non cominciano necessariamente da 1, in tal caso per un vettore si opera:

<tipo> <nome vettore> (<pos1> : <pos2>)

mentre per una matrice si procede:

<tipo> <nome matrice> (<pos1> : <pos2>, <pos3> : <pos4>)

Esempio di programmi

Si riporta un programma che dichiara un vettore di elementi reali che vanno da -9 a +9 e ne stampa a video i valori:


1.Dichiarazione vettore.PNG


L' output a video sarà:


2.output.PNG


Nel caso di matrice si poteva avere ad esempio:


3.Dichiarazione matrice.PNG


È possibile dichiarare anche matrici multi-dimensionali, fino a 7 dimensioni, la sintassi per il richiamo di ogni singolo elemento è equivalente a quella utilizzata per le matrici bi-dimensionali.

Si riporta un esempio di matrice quadri-dimensionale, questa occupa uno spazio in memoria pari al prodotto tra il numero totale di elementi e lo spazio occupato da ciascun elemento ovvero 4 byte:


4. Multidim.PNG

Inserimento valori di una matrice o un vettore tramite file di testo


Quando si scrive un programma che prevede l'inserimento dei valori di una matrice o di un vettore è necessario inserirli, una volta fatto girare il programma, ad uno ad uno nel terminale; per evitare questa operazione alquanto noiosa è possibile inserirli direttamente tramite un file di testo. Per prima cosa è necessario aprire nel nostro file editor un nuovo file di testo e salvarlo ad esempio input.txt. Successivamente si inseriscono tutti i valori che compongono la matrice o il vettore che devono essere letti dal programma rispettando l'ordine delle righe e delle colonne. Ecco un esempio:

Input.png

In questo caso le prime 5 righe costituiscono la matrice mentre l'ultima contiene i termini del vettore. Una volta inseriti tutti i termini salviamo il file. Infine per fare in modo che il programma legga tutti i valori da questo file, quando vado sul terminale scrivo questo comando:

gfortran (nomedelfile).for && ./a.out <input.txt>

In generale a.out si aspetta un input da tastiera da parte dell'utente ma sarà direttamente il computer a inondare il programma con i termini contenuti nel nostro file di testo. Se volessi invece raccogliere i risultati del programma in un file di testo utilizzo la variante >output.txt. In questo caso dal terminale sembra che non succeda niente e che il programma sia fallito ma se dall'editor apro il file output.txt trovo nel file di testo tutti i risultati del programma.

Elevamento a potenza



Nel programma FORTRAN l’elevamento a potenza è effettuato con base**esponente. Si tratta dell’unica operazione che è calcolata con precedenza da destra a sinistra, ovvero il compilatore prima legge l’esponente a destra dei due asterischi e poi eleva la base, a sinistra, a tale valore.2

Segue un esempio di programma con elevamento a potenza degli elementi di un vettore:


5.potenze.PNG


Utilizzo di elementi di vettori o matrici non dichiarati



Si riporta un programma in cui si mostra cosa succederebbe qualora si utilizzasse un numero maggiore di elementi rispetto a quelli dichiarati nella dimensione di un vettore o di una matrice:


6. array.PNG


Nel programma si dichiara un vettore di 8 elementi tuttavia, attraverso l’istruzione go to, si incrementa la variabile di conteggio i un numero illimitato di volte fino al momento in cui il programma si blocca. Superato il numero di elementi dichiarato, il FORTRAN comincerà a stampare numeri presi dalla memoria ad esso dedicata, pari a 4 GB, fino al suo esaurimento e terminerà con la stampa dell'errore: "Segmentation fault" che indica che il programma sta accedendo a una memoria non assegnata. A differenza dell’output che è gestito dal programma, l’errore è invece gestito dal sistema operativo (nello specifico è il kernel del sistema operativo che interviene), che blocca tutto quando il programma tenta l'accesso a una memoria superiore a quella ad esso assegnata.

Se in una seconda variante si prova a eliminare l’espressione implicit none e si sostituisce a(i) con a(j), il programma non stampa alcun valore ma solo l’errore.

Per evitare questi errori, in fase di compilazione, prima di inserire il nome del file, si può aggiungere la specifica -fbounds-check che compila controllando i limiti delle matrici o dei vettori e avverte qualora ci siano questi tipi di problemi, tuttavia funziona solo se è stata dichiarata la dimensione della matrice o del vettore.

Ad esempio per il programma salvato con riprovaarray.for si può operare:

$ gfortran -fbounds-check riprovaarray.for && ./a.exe


Ecco un altro esempio per mettere in crisi il programma:


Pocciamemoria.png


In questo caso se facciamo in modo che i valori del vettore b scorrano da 1 a 100 il programma si arresta immediatamente e restituisce l'errore di segmentation fault; invece se li facciamo scorrere da 1 a 12, cioè poco oltre il valore rispetto al quale è stato definito il vettore, vediamo che il programma non restituisce l'errore. Questo avviene perché il linguaggio Fortran è stato progettato per essere veloce e fare molti controlli rallenta le operazioni computazionali.

Questa tipologia di errori nella scrittura del codice è molto pericolosa perché nel migliore dei casi il programma restituisce un errore quindi ci si accorge dell'errore commesso, mentre la situazione peggiore che può capitare è che il programma non restituisca nessun tipo di errore e quindi si prendano per buoni risultati che in realtà non lo sono.

Stoccaggio di matrici in memoria



La memoria interna di un calcolatore è costituita da una sequenza di celle, le minime unità accessibili direttamente, costituite da una quantità fissa di bit legata alla dimensione del bus dati, minimo un byte e sono identificate univocamente da un indirizzo numerico. Nei linguaggi di programmazione gli elementi di una matrice sono immagazzinati in celle di memoria contigue. Il FORTRAN è l’unico programma in cui le matrici sono stoccate per colonna, a differenza ad esempio del linguaggio C dove sono stoccate per righe. Le colonne di ogni matrice sono scritte in sequenza in memoria, perciò è indispensabile sapere da quante righe è formata la matrice, “leading dimension”, per poter sapere quanto è lunga ogni colonna e quindi quando comincia ogni nuova sequenza in memoria. Segue una schematizzazione dell'allocazione delle colonne di una matrice in memoria:


Memoria matrix.PNG


Sottoprogammi



Il linguaggio di programmazione FORTRAN consente di suddividere le operazioni tramite sottoprogrammi che ritornano un risultato al programma chiamante, sviluppando un approccio top-down. L'utilizzo dei sottoprogrammi è molto utile quando si deve ripetere una stessa operazione più volte, evitando così tutte le volte di copiare e incollare nel programma principale la stessa operazione. Questi sottoprogrammi possono essere del tipo FUNZIONI oppure SUBROUTINE. Funzioni e subroutine devono avere nome univoco e per il resto sono indipendenti dal programma che le ha richiamate. Il costrutto FUNZIONE restituisce un singolo valore scalare, motivo per cui spesso si ricorre al costrutto SUBROUTINE. A differenza dei programmi che terminano con “ stop end”, i sottoprogrammi terminano con “return end”. Nel sottoprogramma le etichette sono autonome perciò per dichiarare le variabili locali, si possono utilizzare etichette aventi stesso nome e tipologia di quelle già dichiarate nel programma principale (MAIN). Programma e sottoprogramma comunicano inviandosi indirizzi di memoria perciò è fondamentale che comunichino con la stessa tipologia di elementi: in particolare il programma invia i parametri passati al sottoprogramma che restituisce un valore.

Funzioni



E' indispensabile dichiarare la natura delle FUNZIONI in base al risultato che riportano al programma chiamante e consistente in una delle 6 tipologie utilizzate per dichiarare le variabili nel programma principale quindi : integer, real, double precision, complex, logical e character. La dichiarazione del tipo di funzione è seguita da un nome assegnato e dai parametri di comunicazione col programma principale.

Si riporta un esempio di struttura di una funzione:

<tipo risultato> <nome funzione> (<parametri passati>)


<comandi>


return

end


Subroutines



La dichiarazione delle SUBROUTINES avviene inserendo il termine subroutine, seguito dal nome assegnato al sottoprogramma e infine dai parametri di comunicazione col programma principale. I parametri sono passati per posizione, perciò è fondamentale il loro ordine.

Segue un esempio di struttura di una subroutine:

SUBROUTINE <nome subroutine> (<parametri passati>)


<comandi>


return

end


Chiamata dei sottoprogrammi



Per chiamare una FUNZIONE o una SUBROUTINE dal programma principale è necessario utilizzare il comando call. Ecco un esempio generico:

call <nome function> (<parametri passati>)

call <nome subroutine> (<parametri passati>)

Esempio di programma chiamante e relativi sottoprogrammi: Integrazione per Trapezi



Obiettivo: "utilizzo di una subroutine per il campionamento della funzione seno entro un dato intervallo, concludendo con una successiva subroutine il quale, sulla base di tali campionamenti, mi calcoli l'integrale con la regola dei trapezi"

Flusso logico: per prima cosa si definirà la funzione, l'intervallo di integrazione e si campionerà questa funzione su "n" punti equispaziati lungo tale intervallo. Successivamente con quei campionamenti si calcolerà l'integrale utilizzando delle somme di aree e infine si restituirà il risultato a video.

In teoria, tale flusso logico potrebbe essere inserito interamente nel programma principale, ottenendo i medesimi risultati. Nonostante ciò, è comunque preferibile suddividere tali entità logiche in sottoprogrammi; in tal modo il programma, oltre a essere più leggibile, avrà in esso una serie di subroutine utilizzabili in futuro per altri programmi.

Function f(x)



Funzione 1.png


Con la compilazione di questo programma, si è chiesto al personal computer memoria pari a 1000 campionamenti, lasciando però la scelta del suo effettivo numero all'utente (il quale sarà ovviamente minore di 1000). Lo stoccaggio maggiore di memoria dedicata ai campionamenti potrà essere oltremodo utile, ad esempio, nell'individuazione dell' errore di calcolo dovuto all'utilizzo di un determinato numero di campionamenti, questo senza però modificare il programma ogni qual volta decidessi di cambiar numero d'intervalli.

A questo punto, si dovrà definire una funzione che mi prenda in input un numero di parametri variabili indipendenti, restituendo come output un solo numero reale:



Funzione 2.png



Inoltre si dovrà richiamare tale funzione nel programma principale:



Funzione 3.png



Come test, si provi a far scrivere a video il valore della funzione f per X pari a 3.5:



Funzione 4.png



Il programma ultimato sarà:



Funzione 5.png



Risultato del run del programma:



Funzione 6.png



Subroutine CAMPFUN


A questo punto, usufruendo già della funzione integranda, si potrà chiedere all'utente l'inserimento del numero di campionamenti voluti:



Campfun 1.png



Per il campionamento dei valori della funzione in esame, non si potrà utilizzare il costrutto FUNCTION, e questo perchè tramite quest'ultima si otterrebbe un numero in formato stadard (real, integer etc.). In realtà l'intenzione del programmatore sarà quella di stoccare due vettori di massimo 1000 elementi, contenenti uno le coordinate di x, l'altro i corrispondenti valori della funzione.

In tal caso, Fortran prevedrebbe l'utilizzo della SUBROUTINE, ossia di un sottoprogramma:



Campfun 2.png



Osservazione 1: ogni qual volta si dovesse ricorrere a un sottoprogramma, occorrerà ricordarsi che esso utilizza parametri INDIPENDENTI da quelle del programma principale; ciò vuol dire che si dovrà rinominarne dei nuovi, utilizzabili esclusivamente all'interno del sottoprogramma stesso. Il collegamento tra variabili della Subroutine e quelle del programma principale avverà al momento della chiamata, sarà quindi indispensabile che l'ordine delle variabili (tra le parentesi) sia uguale tra Subroutine e Call, così che queste possano essere trasferite correttamente.

Osservazione 2: riguardo la definizione della grandezza dei vettori X_ e Y_ , si dovrebbe in tal caso passare la variabile nmax alla subroutine in questione, e questo perchè, essendo quest'ultima "blindata", l'unica interfaccia con il programma principale sarà la serie di parametri definiti nel richiamo della subroutine stessa. Per ovviare a ciò, si potrebbe semplicemente definirlo come vettore, non specificando però la grandezza dello stesso; questo lo si fa utilizzando l'asterisco. In caso di matrice, si potrà utilizzare l'asterisco solamente nella definizione del numero di colonne: questo perchè se ciò non fosse, il programma non saprebbe come "affettare" la memoria.

A questo punto occorrerà inserire un ciclo do il quale, utilizzando una variabile intera i, possa campionare la funzione f per tutti i campionamenti:



Campfun 3.png



Adesso occorrerà richiamare la subroutine nel programma principale e stampare a video i valori della funzione ottenuti:



Campfun 4.png



IMPORTANTE: aggiornare sempre la definizione delle variabili:



Campfun 5.png



Il programma principale ottenuto sarà il seguente:



Campfun 7.png



Se si dovesse lanciare il programma definendo 10 campionamenti (equidistanti) nell'intervallo compreso tra -3.14 a 3.14, si otterrebbero a video questi risultati:



Campfun 6.png

Subroutine TRAPEZISUB


Si passerà adesso al calcolo dell'integrale tramite il metodo dei trapezi. Per prima cosa, si eliminerà la scrittura a video dei campionamenti precedentemente inserita (righe 32-35); successivamente si definira una nuova subroutine chiamata TRAPEZISUB (si è utilizzato un nome diverso da quello del programma principale per evitare problemi di conflitto), la quale svolgerà il passaggio logico dal campionamento della funzione alla somma delle aree dei trapezi, rappresentando appunto l'approssimazione dell'integrale con tal metodo.

Richiamo della subroutine nel programma principale:


Trapezisub 1.png


1° parametro: numero campionamenti (il rispettivo numero degli intervalli sarà n-1). e 3° parametro: campionamenti in termini di coordinate x e y. 4°parametro: output integrazione, in questo caso sarà la sommatoria delle aree dei trapezi costruiti sui singoli intervalli. Tale parametro sarà un valore di natura real e dovrà essere dichiarato anch'esso tra le variabili iniziali del programma principale.

Compilazione subroutine:


Trapezisub 2.png


La riga 102 rappresenterà lo scorrimento dei contributi delle aree dei singoli intervalli, avente come secondo membro dell'equazione d'assegnazione una moltiplicazione: il primo fattore sarà caratterizzato dal valor medio della funzione nei due estremi dell'intervallo locale in esame, mentre il secondo rappresenterà la dimensione dell'intervallo stesso.

Infine, si mostrerà il risultato dell'integrazione con una semplice stampa a video, collocata sotto il richiamo della subroutine nel programma principale:


Trapezisub 3.png


Se si dovesse lanciare il programma definendo 10 campionamenti nell'intervallo compreso tra -3 a 3, si otterrebbe a video: << 10 1.97181308 >>

Osservazione: nel caso si volesse operare per affinamenti successivi, anzichè compiere il campionamento e l'integrazione per un singolo numero di campionamenti n, si potrebbe inserire un ciclo do inglobante il richiamo delle subroutine. In tal modo il numero di campionamenti si incrementerebbe di un valore dopo ogni calcolo dell'integrale e stampa a video, fino a quando non si raggiungerebbe il valore di campionamenti deciso dall'utente:


Trapezisub 4.png


Con questo metodo si potrebbe quindi avere un'idea sull'intensità della convergenza di questo metodo d'integrazione al valor teorico.

Riferimenti bibliografici



1: Introduction to Fortran: http://http://www.stanford.edu/class/me200c/tutorial_77/10_arrays.html

2: Introduction to Fortran: http://http://www.stanford.edu/class/me200c/tutorial_77/06_expressions.html

3: Introduction to Fortran: http://http://www.stanford.edu/class/me200c/tutorial_77/11_subprograms.html