Biuletyn nr 11

Biuletyn KDM
1 | 2 | 3 | 4 | 5
6 | 7 | 8 | 9 | 10
11 | 12 | 13 | 14
15 | 16 | 17 | 18
19 | 20 | 21 | 22
23 | 24 | 25 | 26
27 | 28 | 29 | 30
31 | 32
Lista biuletynów

Biuletyn nr 11 (4 maja 2006).

Spis treści

Precyzja obliczeń: float czy double

Autor: Maciej Szpindler

Wykonując obliczenia z uzyciem liczb rzeczywistych, korzystając z komputerów, należy mieć świadomość z ograniczeń stających przed nami. Komputery reprezentują liczby rzeczywiste korzystając z arytmetyki zmiennopozycyjnej o skończonej precyzji. Na współczesnych maszynach najczęściej wykorzystywane są liczby zmiennoprzecinkowe pojedynczej precyzji float i podwójnej precyzji double (odpowiednie nazwy w języku C). Pisząc własny program obliczeniowy dobrze jest zastanowić się jakich reprezentacji będziemy używać i jakie to może mieć konsekwencje dla wyników naszych obliczeń.

Reprezentacje liczb zmiennoprzecinkowych

Liczba zmiennoprzecinkowa x może być reprezentowana za pomocą 2 liczb m i e w następujący sposób:

x = m \cdot b^{e}, gdzie

  • b oznacza podstawę systemu liczbowego, w przypadku komputerów jest to zwykle 2,
  • m nazywana mantysą jest liczbą z przedziału [0,b),
  • e nazywane jest wykładnikiem,
  • dodatkowo jedna cyfra opisuje znak liczby zmiennoprzecinowej.

Zauważmy że, ustalając m i zmieniając e uzyskujemy zmianę położenia przecinka w zapisie dzięsiętnym liczby - stąd nazwa. Oczywiście przy ustalonej liczbie cyfr dla liczb m i e można dowolną liczbę rzeczywistą przybliżyć tylko ze skończoną dokładnością.

Rozmiar
liczby
Znak Mantysa Wykładnik Nazwa
Float 32 bity 1 bit 23 bity 8 bitów float (C), real*4 (Fortran)
Double 64 bity 1 bit 52 bity 11 bitów double (C), real*8 (Fortran)


Posługując się liczbami zmiennoprzecinkowymi należy mieć świadomość, że w szczególności:

  • w arytmetyce zmiennoprzecinkowej nie ma łączności i rozdzielności, stąd kolejność wykonywania działań ma znaczenie dla ostatecznego wyniku,
  • może wystąpić problem przełnienia,
  • występują błędy zaokrągleń, które się kumulują.

Przykład

Często nie jest obojętne jakiej reprezentacji liczb rzeczywistych użyjemy. Można się o tym przekonać na prostym przykładzie liczenia sumy liczb od 1.0 do miliona (tzn. sumy ciągu arytmetycznego dla a_1 = 1 i a_n = 1000000 oraz a_1 = 1000000 i a_n = 1 z różnicą 1 i -1 odpowiednio). Wykonamy to samo sumowanie w liczbach pojedynczej precyzji (float) i podwójnej precyzji (double). Jak zobaczymy nawet tak, wydawałoby się, nieskomplikowany problem doprowadzi nas do wymienionych wyżej kłopotów.

int main() {
 
       double d1, d2;
       float f1, f2, f3, u, p, v, s;
       int i;
       int n = 1000000;
 
       d1 = 0.0; d2 = 0.0; f1 = 0.0; f2 = 0.0;
       for (i = 1; i <= n; i++) {
               d1 += (double) i;
               d2 += (double) (n + 1 - i);
               f1 += (float) i;
               f2 += (float) (n + 1 - i);
       }
 
       printf("precyzja        a_1     a_n     suma    blad \n");
       printf("double          1       %d      %f      %f \n", n, d1, d1-d1);
       printf("double          %d      1       %f      %f \n", n, d2, d1-d2);
       printf("float           1       %d      %f      %f \n", n, f1, d1-f1);
       printf("float           %d      1       %f      %f \n", n, f2, d1-f2);
              
       return 0;
}
precyzja        a_1     a_n     suma                    blad
double          1       1000000 500000500000.000000     0.000000
double          1000000 1       500000500000.000000     0.000000
float           1       1000000 499941376000.000000     59124000.000000
float           1000000 1       499872694272.000000     127805728.000000

Jak widać w wyniku nie koniecznie musimy dostać to co chcielibyśmy, mimo że rozważany problem jest bardzo prosty. Od razu wnioskujemy, że najprostszym sposobem uniknięcia błędów jest zwiększenie precyzji obliczeń, co powoduje zwiększenie zużycia pamięci (czym dziś się specjalnie nie przejmuje) i nieco dłuższy czas obliczeń.

Nie jest to jednak jedyna skuteczna metoda. Modyfikując nieco nasz algorytm sumowania możemy znacząco poprawić wynik pozostając przy pojedynczej precyzji. Policzmy jeszcze raz naszą sumę stosując algorytm Gilla-Möllera sumowania z poprawkami.


int main() {
       double d1;
       float f1;
       float f3, u, p, v, s;
       int i;
       int n = 1000000;

       d1 = 0.0;
       u = 0.0; p = 0.0;
       for (i = 1; i <= n; i++) {
               d1 += (double) i;
               f1 += (float) i;
               
               v = (float) i;
               s = u + v;
               p += u - s + v;
               u = s;
       }

       f3 = s + p;

       printf("precyzja        a_1     a_n     suma    blad \n");
       printf("float           1       %d      %f      %f \n", n, f1, d1-f1);
       printf("float [GM]      1       %d      %f      %f \n", n, f3, d1-f3);
 
       return 0;
}
precyzja        a_1     a_n     suma                    blad
float           1       1000000 499941376000.000000     59124000.000000
float [GM]      1       1000000 500000358400.000000     141600.000000

Jak widać udało się zmniejszyć błąd o kilka rzedów stosując jedynie sprytną modyfikację sposobu sumowania!

Bliskie spotkania z Gnuplotem

Autor: Michał Łopuszyński

Wstęp

Bardzo często przy wykonywaniu obliczeń stajemy przed problemem szybkiego zwizualizowania i przeanalizowania otrzymanych wyników. Zwykle chcemy sobie odpowiedzieć na pytania typu: czy moje obliczenia zbiegły się, czy są zgodne ze sobą, jakie trendy są widoczne itp. Typowe metody stosowane w takiej analizie to kreślenie wykresów z danych zawartych w plikach wynikowych (najczęściej tekstowych), porównywanie ich ze znanymi funkcjami, a także dopasowywanie krzywych parametrycznych do otrzymanych rezultatów.

Bardzo dobrze do tego typu zadań nadaje się program gnuplot. Jego przydatnymi cechami są duża przenośność (dostępny na platformach: GNU/Linux, Windows, Solaris, Unicos/mp, ...), jak również możliwość zautomatyzowania powtarzających się zadań przy pomocy prostych skryptów. Podstawom obsługi tego narzędzia będzie poświęcony poniższy artykuł.

Spotkanie 1: Kreślimy wykresy funkcji

Z gnuplotem można pracować w sposób interatkywny i wsadowy. My wybierzemy ten drugi sposób, czyli będziemy przygotowywać proste skrypty, a potem przetwarzać je programem gnuplot na obrazki.

Zacznijmy od prostego przykładu. Przygotowanie pliku skrypt.gnu zawierającego:

plot sin(x), cos(x)
pause -1  # poczekaj na naciśnięcie klawisza  (# oznacza komentarz)

i uruchomienie go komendą gnuplot skrypt.gnu da w wyniku obrazek:

R1sin1.png

Jak na pierwsze spotkanie z nowym narzędziem efekt nie jest zły, ale spróbujmy jeszcze nasz wykres uczynić trochę czytelniejszym:

set xlabel "x"  # Etykieta osi X
set ylabel "y"  # Etykieta osi Y
plot [0:6.28][-1.1:1.3] sin(x) title "y=sin(x)" linewidth 3, cos(x) title "y=cos(x)" linetype 4 linewidth 3
pause -1

Użyliśmy tu możliwości podania explicite zakresu kreślenia ( plot [xmin:xmax][ymin:ymax] ...), ponadto wykorzystaliśmy modyfikatory linetype (określający kolor linii) i linewidth (grubość linii). Dla przejrzystości dodaliśmy też opisy osi i własną legendę. Oto wynik:

R1sin2.png

Prawda, że dużo lepiej?

Kruczki i sztuczki:

  • Zamiast na ekranie gnuplot możne tworzyć dla nas wykresy bezpośrednio w plikach graficznych (np. w formacie png lub postscript). Przykładowo dodanie na początku skryptu:
set terminal png size 640,480
set output "wykres.png"

spowoduje wygenerowanie obrazka wykres.png w rozdzielczości 640x480. Taka możliwość pozwala generować proste wykresy kontrolne bezpośrednio na serwerach obliczeniowych, bez potrzeby kopiowania często ogromnych plików z danymi na lokalną maszynę użytkownika.

  • Style punktów, linii itp. dostępne dla danego typu terminala (ekran, obrazek png, postscript itp.) możemy obejrzeć wydając gnuplotowi polecenie test.

Spotkanie 2: Podglądamy dane z pliku

Równie dobrze jak z funkcjami gnuplot radzi sobie również z kreśleniem danych z plików tekstowych w formacie kolumn liczbowych oddzielonych białymi znakami. Załóżmy, że nasz plik data.txt zawiera kolumnę z argumentami i dwie serie danych. Do jego wizualizacji przygotujmy następujący skrypt:

plot "data.txt" using ($1):($2) title "seria 1" pointtype 5, \
     "data.txt" using ($1):($3) title "seria 2" pointtype 5
pause -1

Nowością jest tu podanie nazwy pliku (zamiast wzoru funkcji), a następnie klauzuli using, która wskazuje gnuplotowi numery kolumn, jakie mają być użyte przy kreśleniu odpowiednio jako x i y. Ze względu na składnię numery te musimy poprzedzić znakiem $. Dodatkowo pokazaliśmy też jak można wybrać styl punktu z danymi (modyfikator pointtype 5, co w naszym przypadku odpowiada kwadracikom). Znak \ oznacza przejście do następnej linii.

Po uruchomieniu skryptu dostajemy:

R2plt1.png

A gdybyśmy chcieli wykreślić sumę obu serii danych? Nic prostszego - pomoże nam skrypt:

plot "data.txt"  using ($1):( ($2) + ($3) ) t "serie 1 + 2" with boxes linetype 3 linewidth 3
pause -1

który jako wynik wygeneruje:

R2plt2.png

Przy okazji zobaczyliśmy jak można zmienić styl wykresu z punktowego na "pudełkowy" (modyfikator with boxes).

Kruczki i sztuczki:

  • Dzięki modyfikatorowi using możemy dane z zewnętrznego pliku poddawać praktycznie dowolnym transformacjom np.
 plot "data.txt"  using ($1):( log($2) + 5*sin($3) )
  • Więcej o innych możliwych stylach (oprócz punktowego i "pudełkowego") dowiemy się zaglądając do pomocy poleceniem help plot with.

Spotkanie 3: Dopasowujemy krzywe

Na zakończenie zmierzymy się z problemem dopasowywania funkcji do wyników obliczeń. Spróbujemy opisać nasze dane, które wykreśliliśmy w poprzednim paragrafie, przy pomocy krzywej Lorentza:

f(x)=\frac{a}{b^2 + (x-c)^2}

Zobaczmy jak można zrobić to programem gnuplot:

# Definicja funkcji do dopasowania:
f(x)=a/(b**2.0+(x-c)**2.0)   
# Wywolanie funkcji dopasowujacej
fit f(x) "data.txt" using ($1):($2) via a,b,c
# Co nam wyszlo - rysunek
plot "data.txt" using ($1):($2) title "seria 1" linetype 3 pointtype 5, \ 
    f(x) title "dopasowanie" linetype 10      
pause -1

Najważniejszym elementem jest tu zdefiniowanie funkcji z odpowiednimi parametrami do dopasowania, a następnie użycie polecenia fit <funkcja> <plik_danych> via <lista_parametrów>. Przy określaniu serii danych z zewnętrznego pliku obowiązują identyczne zasady, jak w przypadku polecenia plot.

Po wykonaniu skryptu otrzymujemy następujący obrazek:

R3fit1.png

dodatkowym efektem będą również wartości dopasowanych parametrów razem z błędami:

Final set of parameters            Asymptotic Standard Error
=======================            ==========================
a               = 10.2613          +/- 0.1364       (1.329%)
b               = -0.307363        +/- 0.002584     (0.8408%)
c               = 1.49856          +/- 0.001811     (0.1208%)

Kruczki i sztuczki:

  • Czasami zdarza się, że procedura dopasowująca ma kłopoty ze znalezieniem właściwych wartości parametrów. Wtedy, przed wywołaniem fit, warto nadać im możliwie rozsądne wartości (np. rzędem wielkości odpowiadające spodziewanym wynikom).
  • Informacja na temat wyników działania komendy fit trafia nie tylko na ekran, ale także do pliku fit.log.

Podsumowanie

W powyższym artykule przedstawione zostały podstawy obsługi programu gnuplot. Oczywiście w tym krótkim wprowadzeniu nie starczyło już miejsca na wiele ciekawych i bardziej zaawansowanych możliwości tego programu takich, jak np. rysowanie wykresów trójwymiarowych, wizualizacja krzywych danych równaniami parametrycznymi, kreślenie zależności w biegunowym układzie współrzędnych itd. Czytelników zainteresowanych dodatkowymi informacjami na temat pakietu odsyłamy na jego stronę domową.

Wszystkie skrypty i dane wejściowe wykorzystane w artykule znajdują się tutaj.


Powłoki: tcsh kontra bash

Autor: Łukasz Bolikowski

Tło

Wśród powłok systemowych (ang. shell) w systemach UNIX wyróżnić można dwie popularne rodziny, wywodzące się od dwóch ważnych powłok stworzonych na przełomie lat 70-tych i 80-tych.

Pierwsza rodzina, to powłoki wywodzące się z powłoki Bourne'a. Współcześnie, najpopularniejszym przedstawicielem tej rodziny jest powłoka bash, dobrze znana użytkownikom systemów GNU/Linux i *BSD.

Druga rodzina, to powłoki oparte na powłoce C. Obecnie rodzina ta reprezentowana jest przede wszystkim przez powłokę tcsh, którą spotkać można na większych komputerach obliczeniowych.

Oba typy powłok mogą realizować z grubsza te same zadania, a różnią się przede wszystkim składnią wydawanych poleceń. Wybór pomiędzy jedną a drugą rodziną jest więc, w dużej mierze, kwestią smaku.

W ICM utarł się zwyczaj korzystania z rodziny powłok C. Wybór uzasadnić można tym, że historycznie nie na wszystkich komputerach dużej mocy dostępna była powłoka bash, za to w systemie GNU/Linux bez żadnego problemu zainstalować można tcsh.

Niniejszy krótki artykuł podsumowuje różnice pomiędzy powłokami bash i tcsh, a skierowany jest do średniozaawansowanych użytkowników GNU/Linuksa, którzy znają bash, a chcieliby poznać tcsh.

Zmienne środowiskowe

Przykład: przypisanie zmiennej środowiskowej TERM wartości vt100. W bash przypisujemy tak:

 export TERM=vt100

w tcsh tak:

 setenv TERM vt100

Przykład: przypisanie zmiennej lokalnej f wartości foo.txt. W bash przypisujemy tak:

 f=foo.txt

w tcsh tak:

 set f = foo.txt

Przekierowanie wyjścia

W bash można przekierować do osobnych plików strumienie standard output i standard error. Uruchommy następujący program (język C):

 #include <stdio.h>
 
 main() {
         fprintf(stdout, "OUT\n");
         fprintf(stderr, "ERR\n");
 }

Przekierowanie stdout do pliku o.txt a stderr do e.txt realizujemy tak:

 ./test.x > o.txt 2> e.txt

Przekierowanie obu strumieni do jednego pliku to:

 ./test.x > oe.txt 2>&1

Zauważmy, że domyślnie potok (znak |) przekazuje tylko stdout:

 # ./test.x | wc -l
 ERR
 1

Można łączyć strumienie i przekazywać dalej potokiem:

 # ./test.x 2>&1 | wc -l
 2


Oto zapisane w tcsh odpowiedniki powyższych przykładów. Przekierowanie obu strumieni do jednego pliku realizuje się tak:

./test.x >& oe.txt

Aby przekierować osobno strumienie stderr i stdout piszemy (dziękuję p. Maciejowi Maciejczykowi za wskazanie tego rozwiązania):

( ./test.x > o.txt ) >& e.txt

Sprawdzenie, że potok przekazuje tylko stdout:

 # ./test.x | wc -l
 ERR
 1

Łączenie strumieni i przekazanie potokiem:

 # ./test.x |& wc -l
 2

Pętle, instrukcje warunkowe, inne drobiazgi

Dla zilustrowania różnic w składni pętli for, przeanalizujmy przykład przemianowujący hurtowo pliki. W bash:

 for i in `seq 1 9` a b c
 do
   mv foo.$i.test bar-$i.txt
 done

W tcsh:

 foreach i ( `seq 1 9` a b c )
   mv foo.$i.test bar-$i.txt
 end


Oto inny przykład: sprawdzamy, czy skrypt istnieje, jeśli tak, to uruchamiamy. Proszę zwrócić uwagę na sposób wykonywania skryptów. W bash:

 if [ -f /opt/modules/modules/init/bash ]
 then
   . /opt/modules/modules/init/bash
 fi

W tcsh:

 if ( -f /opt/modules/modules/init/tcsh ) then
   source /opt/modules/modules/init/tcsh
 endif


A tu co minutę wypisujemy "znak" dzwonka (ang. bell, kod ASCII 7). Oprócz składni pętli while, proszę zwrócić uwagę na różnicę w sprawdzaniu kodu wyjścia wykonywanego programu true, oraz na różnicę we wprowadzaniu liczbowego kodu znaku bell. W bash:

 while /bin/true
 do
   echo -n $'\007'
   sleep 60
 done

W tcsh:

 while ( { /bin/true } )
   echo -n '\007'
   sleep 60
 end

Autouzupełnianie

Zarówno bash, jak i tcsh po naciśnięciu klawisza <TAB> "domyślają się" reszty wpisywanej nazwy. Obie powłoki umożliwiają zaprogramowanie dodatkowych "sztuczek" pozwalających domyślać się więcej i lepiej, służy do tego polecenie complete.

Programowanie autouzupełniania to temat sam w sobie, o autouzupełnianiu w tcsh pisaliśmy w Biuletynie nr 7.

Szczegółowe informacje można też znaleźć w manualu man bash, man tcsh. W bash powyższe udogodnienie funkcjonuje pod nazwą programmable completion, w tcsh jako autocompletion.

Dalsze informacje

Cennym źródłem informacji są strony manuala: man bash, man tcsh.