Säkra summor i Fortran 95
För (rätt många) år sedan var det populärt med
så kallad intervallaritmetik. Ett flyttal representeras då som
ett intervall med hjälp av två flyttal. Genom att systematiskt
avrunda nedåt respektive uppåt kan vi se till att ett beräknat
resultat hela tiden ligger i intervallet. IEEE-standarden har stöd för
sådana avrundningar vilket göra att det inte är så
svårt att implementera intervallaritmetik. Det finns flera nackdelar,
som förklarar varför intevallaritmetik aldrig slog igenom. Det
krävs ju dubbelt så mycket minne och dubbelt så många
räkneoperationer. Om man inte har speciella algoritmer så kan
intervallen bli väldigt stora. Det är t.ex. inte så intressant
att få veta att ett resultat som borde vara omkring 10 ligger i intervallet
[-10000, 20000] till exempel.
Här följer några exempel. Jag har utnyttjat Suns
Fortran95-kompilator som har stöd för intervallaritmetik (det är
dock inte en del av standarden för Fortran95).
% cat interval1.f95 program interval_ex interval :: a, b ! a och b är två variabler av typen interval a = [2.0, 3.0] ! två intervallkonstanter b = [-1.0, 3.0] print*, a + b, a - b, a * b, a / b, b / a ! räkna lite a = [0.1] ! kort för [0.1, 0.1] print*, a print*, a * 10 end program interval_ex % f95 -xia interval1.f95 xia slår på intervallaritmetiken % a.out [1.0,6.0] [-1.0,4.0] [-3.0,9.0] [-Inf,Inf] [-0.5,1.5] [0.099999999999999991,0.10000000000000001] OBS: avrundat åt två håll [0.99999999999999988,1.0000000000000003]
Man kan notera att a + b
etc. fungerar som man tror. Notera
att a / b
ger division med noll eftersom nollan tillhör
intervallet b
. I dessa enkla exempel räknar vi exakt, men
det är inte fallet med a = [0.1]
. Vi vet att
0.1
inte kan lagras exakt i enkel- eller dubbel precision
så efter tilldelningen kommer a
att innehålla
0.1
avrundat nedåt och uppåt.
Här följer ett summationsprogram:
% cat summor.f95 program summor implicit none integer :: k interval (kind = 4) :: summa ! summa är en intervallvariabel i enkel prec. summa = [0.0] do k = 1, 1000000 summa = summa + [1.0] / k end do print*, "1 + 1/2 + ... + 1/1000000 = ", summa, wid(summa) ! wid = intervallets längd summa = [0.0] do k = 1000000, 1, -1 summa = summa + [1.0] / k end do print*, "1/1000000 + ... + 1/2 + 1 = ", summa, wid(summa) end program summor % f95 -xia summor.f95 % a.out 1 + 1/2 + ... + 1/1000000 = [13.971489,14.924348] 0.952858 1/1000000 + ... + 1/2 + 1 = [14.350338,14.436089] 0.085749626
Notera bredden på intervallen! Det finns några andra
intervallfunktioner förutom wid
. inf
ger
intervallets minsta värde och sup
dess största.
mid
ger mittpunkten.
För att se hur väl intervallaritmetik fungerar för en riktig tillämpning så konverterade jag ett program för lösandet av linjära ekvationssystem. Det är en rätt enkel process. Alla typdeklarationer för flyttalstyperna får konverteras till motsvarande intervalltyp. En del flyttalskonstanter får också skrivas om. Det enda problem jag hade var att konvertera jämförelseoperationen vid den partiella pivoteringen.
En if-sats av typen if ( a < b ) then ...
kommer att ge
kompileringsfel om a
och b
är intervall. Detta
är ju inte så konstigt ty vad menar vi med "mindre än" om
t.ex. t.ex. a = [1, 3]
och b = [2, 4]
?
Sun har implementerat tre varianter av "mindre än":
a
-intervallet är mindre än alla element i
b
-intervallet. Dvs. sup(a) < inf(b)
.a
-intervallet är mindre än något element i
b
-intervallet. Dvs. inf(a) < sup(b)
.inf(a) < inf(b)
och
sup(a) < sup(b)
.
Relationsoperatorerna har namnen .clt.
(certainly less than),
.plt
och .slt.
.
I en test med A x = b där A hade dimension 500 och konditionstal 172 så varierade vidden på x-intervallen mellan 10-5 och 1.69 fastän maxfelet i lösningen var så litet som 6 10-15. Beräkningen gjordes i dubbel precision. Även om det kan vara enkelt att konvertera programmen så är således resultatet inte alltid så användbart.
Om man inte har tillgång till Suns Fortran 95 så kan man implementera intervallaritmetik på egen hand. Det förenklas givetvis om man har språk som stödjer operatoröverlagring, som C++ och Fortran (i viss utsträckning), eftersom syntaxen blir så mycket trevligare och då det gör konverterandet av gamla program rätt så enkel. Java stödjer inte överlagring, vilket är en stor brist i fall som detta.
Här följer ett exempel där jag har beräknat våra
två summor i enkel precision och med de fyra avrundningsmoderna. Dessa
är avrundning till närmaste, avrundning mot +oändligheten
(alltid uppåt), avrundning mot minus oändligheten samt avrundning
mot noll (nedåt för positiva tal och uppåt för negativa).
I tabellen nedan har vi summorna s1
och s2
(som
i labben) och avrundningsriktningarna (positive
=
+oändligheten, negative
= -oändligheten). Programmet,
round.f90
, är skrivet i Fortran
90 och utnyttjar inte överlagring. error
är skillnaden
mellan enkelprecisionsvärdena och det exakta (avrundat till dubbel
precision).
type s1 error s2 error nearest : 14.3573580 -0.0354 14.3926516 -0.0001 tozero : 13.9714899 -0.4212 14.3503389 -0.0423 positive : 14.9243479 0.5317 14.4360886 0.0434 negative : 13.9714899 -0.4213 14.3503389 -0.0424
Notera att avrundning till närmaste ger ett mycket bättre resultat. Jämför också med summationsprogrammet ovan.
Slutligen ett litet problem. Kan Du förklara följande?
% cat interval3.f95 program sin_ex print*, wid([1e20]), sin([1e20]), wid(sin([1e20])) print*, wid([1e21]), sin([1e21]), wid(sin([1e21])) print*, wid([1e22]), sin([1e22]), wid(sin([1e22])) print*, wid([1e23]), sin([1e23]), wid(sin([1e23])) end program sin_ex % f95 -xia interval3.f95 % a.out 0.0E+0 [-0.64525128526578091,-0.64525128526578079] 1.1102230246251565E-16 0.0E+0 [-0.66712017707180494,-0.66712017707180482] 1.1102230246251565E-16 0.0E+0 [-0.85220084976718891,-0.85220084976718879] 1.1102230246251565E-16 16777216.0 [-1.0,1.0] 2.0