Quale versione del fortran utilizzi?
A cura di Giuseppe Ciaburro
Pubblicato il 08/06/2002
Una evoluzione del linguaggio Fortran 90
Il Fortran il cui nome deriva dal termine FORmula
TRANslation è stato introdotto nel 1957 e rimane il linguaggio preferito
da molti programmatori scientifici. Il nuovo fortran detto Fortran 90, è
stato realizzato con una struttura simile al linguaggio C. Le novità
essenziali del Fortran 90 sono: subroutines ricorsive, allocazione dinamica,
puntatori, dati strutturati definiti dall’utente, moduli, e la notevole abilità
di manipolare interi array. Il Fortran 90 è totalmente compatibile
con il Fortran 77 questo implica che sia ancora presente in esso della sintassi
considerata ormai obsoleta. Il linguaggio F invece è una
evoluzione del linguaggio Fortran 90 infatti include solo le caratteristiche
moderne è compatto, e facile da imparare. Di seguito si supporrà
che il lettore abbia una certa familiarità con i concetti di base della
programmazione, inizialmente forniamo un esempio di un programma F.
program prodotto real :: m, a, force m = 2.0 ! massa in Kg a = 4.0 ! accelerazione in unità mks force = m*a ! forza in Newton print *, force end program prodotto
Le novità del linguaggio F possono essere riassunte nei seguenti punti:
Di seguito introduciamo la sintassi necessarie che ci permette di associare alle variabili m e a il valore da noi desiderato ed introdotto da tastiera. Da notare l’uso dello statement read non formattato.
program prodotto2 real :: m, a, force ! SI units print *, "mass m = ?" read *, m print *, "acceleration a = ?" read *, a force = m*a print *, "force (in newtons) =", force end program prodotto2
Il linguaggio F utilizza il costrutto do
per fare in modo che il computer esegua gli stessi statements più di
una volta. Un esempio di un costrutto do è di seguito riportato:
program serie
integer :: n
real :: sum_serie ! sum è una keyword
sum_serie = 0.0
! aggiunge i primi 100 termini di una serie semplice
do n = 1, 100
sum_serie = sum_serie + 1.0/real(n)**2
print *, n,sum_serie
end do
end program serie
Da notare che n è una variabile integer. In questo caso lo statement do specifica il primo e l’ultimo valore di n; n si incrementa di una unità per defaultì. Il blocco degli statements interni al loop si indenta per chiarezza.
Poiché il prodotto n*n è ottenuto utilizzando un’aritmetica intera, iè meglio convertire n in variabile reale prima della moltiplicazione. Inoltre si può notare che l’esponente è ottenuto utilizzando l’operatore **.
Nel prossimo esempio l’uscita dal loop Do è vincolata dal soddisfacimento di un test.
program serie_test
! Illustrazione dell’uso del costrutto do
integer :: n
real :: sum_series, newterm, relative_change
n = 0
sum_series = 0.0
do
n = n + 1
newterm = 1.0/(n*n)
sum_series = sum_series + newterm
relative_change = newterm/sum_series
if (relative_change < 0.0001) then
exit
end if
print *, n,relative_change,sum_series
end do
end program serie_test
Le novità introdotte dal linguaggio F presenti nell’esempio precedente sono:
Tabella 1. Sommario degli operatori relazionali.
|
relazione |
Operatore |
|
Minore |
< |
|
Minore uguale |
<= |
|
Uguale |
== |
|
Non uguale |
/= |
|
Maggiore |
> |
|
Maqgiore uguale |
>= |
Il programma seguente illustra l’uso del parametro kind e del costrutto
do con nome:
program series_example
! illustra l’uso del parametro kind e del loop do con nome
integer, parameter :: double = 8
integer :: n
real (kind = double) :: sum_series, newterm, relative_change
n = 0
sum_series = 0.0
print_change: do
n = n + 1
newterm = 1.0/real(n, kind = double)**2
sum_series = sum_series + newterm
relative_change = newterm/sum_series
if (relative_change < 0.0001) then
exit print_change
end if
print *, n,relative_change,sum_series
end do print_change
end program series_example
double = 8
Inoltre la doppia precisione in F su macchine SGI è ottenuta ponendo:
double = 2
I Subprograms sono chiamati dal main program o da altri subprograms. Ad esempio, nel programma seguente si aggiunge e si moltiplica due numeri introdotti da tastiera. Da notare che le variabili x e y sono public e sono disponibile dal main program.
module common public :: initial,add,multiply integer, parameter, public :: double = 8 real (kind = double), public :: x,y contains subroutine initial() print *, "x = ?" read *,x print *, "y = ?" read *,y end subroutine initial subroutine add(sum2) real (kind = double), intent (in out) :: sum2 sum2 = x + y end subroutine add subroutine multiply(product2) real (kind = double), intent (in out) :: product2 product2 = x*y end subroutine multiply end module common program tasks ! illustra l’uso dei module e delle subroutines ! notare quante variabikli sono passate use common real (kind = double) :: sum2, product2 call initial() ! initializza le variabili call add(sum2) ! aggiunge due variabili call multiply(product2) print *, "sum =", sum2, "product =", product2 end program tasks
Cerchiamo ora di anlizzare nel dettaglio quali direttive impartire per ottenere un output formattato. Abbiamo detto che l’asterisco * denota il formato di default, per ottenere invece un fortato specifico bisogna associare al comando di output una lista di descrittori di formato. Un esempio di direttiva di output formattato è la seguente:
print "(t7,a,t16,a,t28,a)", "tempo","T_caffè","T_caffè - T_stanza"
Il descrittore t (tab) è usato per saltare ad una specifica posizione di una linea di output. Il descrittore a (alfanumerico) è usato per le stringhe di caratteri. Un esempio di descrittore f (floating point) è dato da:
print "(f10.2,2f13.4)",t,T_caffè,T_caffè - T_stanza
Il descrittore f13.4 sta ad indicare che delle prossime 13 posizioni utilizzate per stampare un valore reale, 4 posti sono riservati per stampare le cifre decimali dopo la virgola. (Il punto decimale ed il segno meno occupano 2 posizioni delle 13 anzidette.) Il descrittore 2f13.4 sta ad indicare che il descrittore f13.4 è utilizzato 2 volte. Un altro descrittore è i (integer).
Un’altra novità nella sintassi del linguaggio F è nell’uso dello
statement parameter:
real (kind = double), public, parameter :: g = 9.8
Un parameter è una costante con nome. Il suo valore è fissato atraverso la sua dichiarazione e non può essere modificato durante l’esecuzione del programma.
Il programma seguente mostra come aprire un nuovo file, scriverci all’interno,
chiudere lostesso ed infine come leggere dei dati da un file già esistente:
program save_data
! illustra come scrivere e leggere su file
integer :: i,j,x
character(len = 32) :: file_name
print *, "name of file?"
read *, file_name
open (unit=5,file=file_name,action="write",status="new")
do i = 1,4
x = i*i
write (unit=5,fmt=*) i,x
end do
close(unit=5)
open (unit=1,file=file_name,action="read",status="old")
do i = 1,4
read (unit=1,fmt = *) j,x
print *, j,x
end do
close(unit=1)
end program save_data
Gli statements Input/output si riferiscono a particolari file attraverso l’impiego delle cosiddette unità (unit). Gli statements read e write non si riferiscono direttamente ad un file, ma si riferiscono ad un numero di un file che deve essere connesso al file in questione. Ci sono molti modi di utilizzare lo statement open, ma l’esempio precedente è tipico. Tra le parentesi tonde bisogna specificare l’unità alla quale associare il file, il nome del file, il valore dello specificatore action che può essere read, write, e readwrite (di default) ed infine status che può assumere i seguenti valori old, new, replace, oppure scratch.
Se si suppone di riutilizzare i dati ottenuti sullo stesso sistema su cui è stato compilato il programma si possono usare operazioni di input/output non formattate per salvare overhead, extra space, e roundoff error associati alla conversione dei valori tra rappresentazione interna ed esterna. Naturalmante l’uso di dati non formattati è strettamente dipendente dalla macchina e dal compilatore utilizzati. L’accesso non formattato è molto utile quando i dato sono generati da un programma e quindi analizzati da un altro programma residenti entrambi sulla stessa macchina. Per generare file non formattati basta omettere lo specificatore di formato.
Le definizioni e l’uso degli array sono indicate nel programma seguente:
module common
public :: initial,cross
contains
subroutine initial(a,b)
real, dimension (:), intent(out) :: a,b
a(1:3) = (/ 2.0, -3.0, -4.0 /)
b(1:3) = (/ 6.0, 5.0, 1.0 /)
end subroutine initial
subroutine cross(r,s)
real, dimension (:), intent(in) :: r,s
real, dimension (3) :: cross_product
! note use of dummy variables
integer :: component,i,j
do component = 1,3
i = modulo(component,3) + 1
j = modulo(i,3) + 1
cross_product(component) = r(i)*s(j) - s(i)*r(j)
end do
print *, ""
print *, "three components of the vector product:"
print "(a,t10,a,t16,a)", "x","y","z"
print *, cross_product
end subroutine cross
end module common
program vector ! illustra l’uso degli array
use common
real, dimension (3) :: a,b
real :: dot
call initial(a,b)
dot = dot_product(a,b)
print *, "dot product = ", dot
call cross(a,b)
end program vector
Le novità nell’uso degli arrays sono:
real, dimension (10) :: x,y
real, dimension (1:10) :: x,y
integer, dimension (-10:10) :: prob
integer, dimension (10,10) :: spin
a(1:3) = (/ 2.0, -3.0, -4.0 /)
che è equivalente alle tre istruzioni seguenti:
a(1) = 2.0 a(2) = -3.0 a(3) = -4.0
print *, cross_product
Il linguaggio F ha molte funzioni per la moltiplicazione di array e matrici. Per esempio, la funzione dot_function che opera con due vettori e fornisce il prodotto scalare. Alcune funzioni per la manipolazione di array sono: maxval, minval, product, e sum.
Una delle più importanti novità introdotte con il Fortran 90 è
l’allocazione dinamica, attraverso la quale la grandezza degli array può
essere modificata durante l’esecuzione del programma. L’uso degli statement
allocate e deallocate è di seguito illustrata. E’ da notare
l’uso del ciclo implied do.
program dynamic_array ! semplice esempio di array dinamico real, dimension (:), allocatable :: x integer :: i,N N = 2 allocate(x(N:2*N)) ! ciclo implied do x(N:2*N) = (/ (i*i, i = N, 2*N) /) print *, x deallocate(x) allocate(x(N:3*N)) x = (/ (i*i, i = N, 3*N) /) print *, x end program dynamic_array
Un esempio del modo in cui è possibile passare gli array è il seguente:
module param integer, public, parameter :: double = 8 end module param module common use param private public :: initial integer, public :: N contains subroutine initial(x) real (kind = double), intent(inout), dimension(:) :: x N = 100 x(1) = 1.0 end subroutine initial end module common program test use param use common real (kind = double), allocatable,dimension (:) :: x N = 10 allocate(x(N)) call initial(x) end program test
Il Fortran 90 include diverse procedure di costruzione che si rivelano molto
utili. Una delle più utili è la subroutine random_number.
Sebbene sia una buona idea scrivere una routine per la generazione casuale di
numeri utilizzando un algoritmo già testato su un particolare problema
di interesse, è d’altra parte conveniente utilizzare la subroutine random_number
nella fase di debugging o nel caso l’accuratezza dei dati non sia così
importante. Il programma che segue illustra diversi usi della subroutine random_number
e random_seed. E’ da notare che gli argomenti rnd della random_number
devono necessariamente essere real, con intent out, e possono essere scalari
oppure degli array.
program random_example
real :: rnd
real, dimension (:), allocatable :: x
integer, dimension(2) :: seed, seed_old
integer :: L,i,n_min,n_max,ran_int,sizer
! genera interi random tra n_min e n_max
! la dimensione di seed è 1 in F e 2 in Fortran 90.
call random_seed(sizer)
print *, sizer
! illustra l’uso di put e get
seed(1) = 1239
seed(2) = 9863 ! necessaria in Fortran 90
call random_seed(put=seed)
call random_seed(get=seed_old)
! conferma del valore di seed
print *, "seed = ", seed_old
L = 100 ! length of sequence
n_min = 1
n_max = 10
do i = 1,L
call random_number(rnd)
ran_int = (n_max - n_min + 1)*rnd + n_min
print *,ran_int
end do
! assegna i numeri random all’array x
allocate(x(L))
call random_number(x)
print "(4f13.6)", x
call random_seed(get=seed_old)
print *, "seed = ", seed_old
end program random_example
E’ da notare come la subroutine random_seed sia utilizzata per specificare seed. La specificazione è utile quando la stessa sequenza di numeri random è utilizzata per testare il programma.
Un semplice esempio di definizione ricorsiva è fornito dalla funzione
fattoriale:
factorial(n) = n! = n(n-1)(n-2) ... 1
La definizione ricorsiva del fattoriale è:
factorial(1) = 1 factorial(n) = n factorial(n-1)
Vediamo come è possibile introdurre tale definizione in un programma:
module fact
public :: f
contains
recursive function f(n) result (factorial_result)
integer, intent (in) :: n
integer :: factorial_result
if (n <= 0) then
factorial_result = 1
else
factorial_result = n*f(n-1)
end if
end function f
end module fact
program test_factorial
use fact
integer :: n
print *, "integer n?"
read *, n
print "(i4, a, i10)", n, "! = ", f(n)
end program test_factorial
Nel programma greatest che segue, (ricavato dal The Fun of Computing,John G. Kemeny, True BASIC (1990)) dati due interi, n e m, si ricava il massimo comun divisore. Ad esempio, se n = 1000 e m = 32, allora il massimo comun divisore (gcd) è gcd = 8.
Un metodo per la ricerca del gcd è quello di dividere n per m. Si scrive n = q m + r, dove q è il quoziente e r è il resto. Se r = 0, allora m divide n e m è il gcd. Altrimenti, un qualsiasi divisore tra m ed r divide anche n, allora risulta gcd(n,m) = gcd(m,r). Poichè r < m, abbiamo reso la ricerca più facile. Ad esempio, sia n = 1024 e m = 24. Allora q = 42 e r = 16. La nostra ricerca si riduce dunque a gcd(24,16). Essendo q = 1 e r = 8 si calcola il gcd(16,8). In definitiva q = 2, e r = 0 cosicchè gcd = 8. L’esempio seguente implementa tale algoritmo.
module gcd_def
public :: gcd
contains
recursive function gcd(n,m) result (gcd_result)
integer, intent (in) :: n,m
integer :: gcd_result
integer :: remainder
remainder = modulo(n,m)
if (remainder == 0) then
gcd_result = m
else
gcd_result = gcd(m,remainder)
end if
end function gcd
end module gcd_def
program greatest
use gcd_def
integer :: n,m
print *, "enter two integers n, m"
read *, n,m
print "(a,i6,a,i6,a ,i6)", "gcd of",n," and",m,"=",gcd(n,m)
end program greatest
Un ulteriore esempio di ricorsività è fornito dalla cosiddetta torre di Hanoi.
Il volume di una ipersfera d-dimensionale per unità di raggio può essere relazionato all’area di una ipersfera (d - 1)-dimensionale. Il programma seguente utilizza una subroutine ricorsiva per sfruttare tale proprietà e calcolare quindi il volume per unità di area di una ipersfera d-dimensionale:
module common
public :: initialize,integrate
integer, parameter, public :: double = 8
real (kind = double), parameter, public :: zero = 0.0
real (kind = double), public :: h, volume
integer, public :: d
contains
subroutine initialize()
print *, "dimension d?"
read *, d ! spatial dimension
print *, "integration interval h?"
read *, h
volume = 0.0
end subroutine initialize
recursive subroutine integrate(lower_r2, remaining_d)
! lower_r2 is contribution to r^2 from lower dimensions
real(kind = double),intent (in) :: lower_r2
integer, intent (in) :: remaining_d ! # dimensions to integrate
real (kind = double) :: x
x = 0.5*h ! mid-point approximation
if (remaining_d > 1) then
lower_d: do
call integrate(lower_r2 + x**2, remaining_d - 1)
x = x + h
if (x > 1) then
exit lower_d
end if
end do lower_d
else
last_d: do
if (x**2 + lower_r2 <= 1) then
volume = volume + h**(d - 1)*(1 - lower_r2 - x**2)**0.5
end if
x = x + h
if (x > 1) then
exit last_d
end if
end do last_d
end if
end subroutine integrate
end module common
program hypersphere
! programma originale di Jon Goldstein
use common
call initialize()
call integrate(zero, d - 1)
volume = (2**d)*volume
print *, volume
end program hypersphere
L’unico operatiore intinseco per le espressioni contenenti caratteri è
l’operatore di concatenazione //. Per esempio, la concatenazione tra
le costanti di carattere string e beans si scrive come:
"string"//"beans"
Il risultato, stringbeans, può essere
assegnato ad una variabile di tipo carattere. Un esempio di concatenazione utile
è dato dal seguente programma.
program write_files
! Programma test per l’apertura di un file e la scrittura di dati
integer :: i,n
character(len = 15) :: file_name
n = 11
do i = 1,n
! assegna number.dat a file_name utilizzando lo statement write
write(unit=file_name,fmt="(i2.2,a)") i,".dat"
! // è l’operatore di concatenazione
file_name = "config"//file_name
open (unit=1,file=file_name,action="write",status="replace")
write (unit=1, fmt=*) i*i,file_name
close(unit=1)
end do
end program write_files
In tale programma è da notare l’uso dello statement write per costruire una stringa di caratteri costituita da numeri e caratteri.
Il programma che segue mostra come il Fortran 90 definisce ed utilizza le variabili
complesse.
program complex_example integer, parameter :: double = 2 real (kind = double), parameter :: pi = 3.141592654 complex (kind = double) :: b,bstar,f,arg real (kind = double) :: c complex :: a integer :: d ! Una costante complessa è scritta come due numeri reali separati ! da una virgola e racchiusi in parentesi. a = (2,-3) ! Se una parte ha un kind, l’altra parte deve avere lo stesso kind b = (0.5_double,0.8_double) print *, "a =", a ! notare che a ha una minore precisione rispetto b print *, "a*a =", a*a print *, "b =", b print *, "a*b =", a*b c = real(b) ! real part of b print *, "real part of b =", c c = aimag(b) ! parte immaginaria di b print *, "imaginary part of b =", c d = int(a) print *, "real part of a (converted to integer) =", d arg = cmplx(0.0,pi) b = exp(arg) ! disposto su due linee per una più agevole lettura bstar = conjg(b) ! complesso coniugato di b f = abs(b) ! valore assoluto di b print *, "properties of b =", b,bstar,b*bstar,f end program complex_example