MNwyklad01: Różnice pomiędzy wersjami
mNie podano opisu zmian |
|||
Linia 1: | Linia 1: | ||
=Wprowadzenie do metod numerycznych= | |||
<strong>Metody numeryczne</strong> to dziedzina wiedzy zajmująca się | |||
problemami obliczeniowymi i konstrukcją algorytmów rozwiązywania | problemami obliczeniowymi i konstrukcją algorytmów rozwiązywania | ||
zadań matematycznych. Najczęściej, zadania obliczeniowe postawione są w | zadań matematycznych. Najczęściej, zadania obliczeniowe postawione są w | ||
dziedzinie rzeczywistej (lub zespolonej) i dlatego mówimy o zadaniach | dziedzinie rzeczywistej (lub zespolonej) i dlatego mówimy o zadaniach | ||
obliczeniowych | obliczeniowych <strong>matematyki ciągłej</strong> (w odróżnieniu od [[|Dodaj WIKIlink: matematyki | ||
dyskretnej). | dyskretnej]]). | ||
===Zadania metod numerycznych=== | ===Zadania metod numerycznych=== | ||
Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw | Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw | ||
* określić | * określić <strong>dane problemu</strong> i <strong>cel obliczeń</strong>, czyli dokładnie | ||
sformułować zadanie w języku matematyki, | sformułować zadanie w języku matematyki, | ||
* określić | * określić <strong>środki obliczeniowe</strong> dzięki którym chcemy osiągnąć cel, | ||
* dla analizy zadania i sposobów jego rozwiązania wygodnie jest zdefiniować | * dla analizy zadania i sposobów jego rozwiązania wygodnie jest zdefiniować | ||
<strong>klasę rozpatrywanych danych</strong> oraz <strong>model obliczeniowy</strong> w obrębie | |||
którego będą działać nasze algorytmy. | którego będą działać nasze algorytmy. | ||
Wbrew dość powszechnej opinii | Wbrew dość powszechnej opinii <strong>nie jest prawdą</strong>, że głównym przedmiotem metod | ||
numerycznych jest badanie wpływu błędów zaokrągleń na wynik. Raczej, głównym | numerycznych jest badanie wpływu błędów zaokrągleń na wynik. Raczej, głównym | ||
celem metod numerycznych jest konstrukcja optymalnych (w jasno określonym | celem metod numerycznych jest konstrukcja optymalnych (w jasno określonym | ||
Linia 38: | Linia 32: | ||
; metody numeryczne | ; metody numeryczne | ||
: --- główny nacisk idzie na aspekty algorytmiczne | : --- główny nacisk idzie na aspekty algorytmiczne; | ||
; analiza numeryczna | ; analiza numeryczna | ||
Linia 59: | Linia 53: | ||
Tworząc i analizując algorytmy, jakie będą pojawiać w naszym wykładzie, będziemy | Tworząc i analizując algorytmy, jakie będą pojawiać w naszym wykładzie, będziemy | ||
posługiwać się pewnym uproszczonym modelem obliczeń, dzięki czemu będziemy mogli | posługiwać się pewnym uproszczonym modelem obliczeń, dzięki czemu będziemy mogli | ||
skoncentrować się na | skoncentrować się na <strong>esencji</strong> algorytmu, bez detali implementacyjnych --- | ||
zostawiając je na | zostawiając je na inną okazję (dobra implementacja konkretnego algorytmu może być | ||
sama w sobie interesującym wyzwaniem programistycznym; często bywa, że dobre | |||
implementacje, nawet prostych algorytmów numerycznych, są mało czytelne). | |||
Aby zdefiniować nasz model obliczeniowy, posłużymy się | Aby zdefiniować nasz model obliczeniowy, posłużymy się | ||
pojęciem | pojęciem <strong>programu</strong>. Zastosujemy przy tym notację | ||
podobną do tej z języka programowania | podobną do tej z języka programowania <strong>C</strong>. | ||
Program składa się z | Program składa się z <strong>deklaracji</strong>, czyli opisu obiektów, | ||
których będziemy używać, oraz z | których będziemy używać, oraz z <strong>poleceń</strong> (<strong>instrukcji</strong>), | ||
czyli opisu akcji, które będziemy wykonywać. | czyli opisu akcji, które będziemy wykonywać. | ||
Dostępnymi obiektami są | Dostępnymi obiektami są <strong>stałe</strong> i <strong>zmienne</strong> typu | ||
całkowitego (<code>int</code>), | całkowitego (<code>int</code>), | ||
rzeczywistego (<code>float</code> i <code>double</code>). Typ logiczny symulujemy tak jak w | rzeczywistego (<code>float</code> i <code>double</code>). Typ logiczny symulujemy tak jak w | ||
Linia 81: | Linia 77: | ||
modelowego języka. | modelowego języka. | ||
==Podstawienie== | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
z | z \EQUALREAD <math>\displaystyle {\cal W}</math>; | ||
</pre></div> | </pre></div> | ||
gdzie <math>\displaystyle z</math> jest zmienną, a <math>\displaystyle {\cal W}</math> jest | gdzie <math>\displaystyle z</math> jest zmienną, a <math>\displaystyle {\cal W}</math> jest <strong>wyrażeniem</strong> | ||
o wartościach tego samego typu co <math>\displaystyle z</math>. Jest to polecenie proste. | o wartościach tego samego typu co <math>\displaystyle z</math>. Jest to polecenie proste. | ||
Wyrażeniem jest pojedyncza stała lub zmienna, albo złożenie | Wyrażeniem jest pojedyncza stała lub zmienna, albo złożenie | ||
skończonej liczby | skończonej liczby <strong>operacji elementarnych</strong> na wyrażeniach. | ||
Operacje elementarne to: | Operacje elementarne to: | ||
Linia 98: | Linia 94: | ||
: <math>\displaystyle x\mapsto -x</math>, <math>\displaystyle (x,y)\mapsto x+y</math>, | : <math>\displaystyle x\mapsto -x</math>, <math>\displaystyle (x,y)\mapsto x+y</math>, | ||
<math>\displaystyle (x,y)\mapsto x-y</math>, <math>\displaystyle (x,y)\mapsto x*y</math>, | <math>\displaystyle (x,y)\mapsto x-y</math>, <math>\displaystyle (x,y)\mapsto x*y</math>, | ||
<math>\displaystyle (x,y)\mapsto x | <math>\displaystyle (x,y)\mapsto x/y, y\ne 0</math>, gdzie <math>\displaystyle x,y</math> są stałymi lub | ||
zmiennymi liczbowymi, | zmiennymi liczbowymi, | ||
Linia 107: | Linia 103: | ||
; logiczno--logiczne: | ; logiczno--logiczne: | ||
: <math>\displaystyle p\mapsto\,\ | : <math>\displaystyle p\mapsto\,\sim \,p</math>, | ||
<math>\displaystyle (p,q)\mapsto p\, | <math>\displaystyle (p,q)\mapsto p\, \wedge \,q</math>, <math>\displaystyle (p,q)\mapsto p\, \vee \,q</math>, | ||
gdzie <math>\displaystyle p,q</math> są stałymi lub zmiennymi logicznymi. | gdzie <math>\displaystyle p,q</math> są stałymi lub zmiennymi logicznymi. | ||
Dla niektórych zadań wygodnie jest (a czasem | Dla niektórych zadań wygodnie jest (a czasem konieczne) | ||
uzupełnić ten zbiór o dodatkowe operacje, takie jak | uzupełnić ten zbiór o dodatkowe operacje, takie jak | ||
obliczanie wartości niektórych standardowych funkcji matematycznych | obliczanie wartości niektórych standardowych funkcji matematycznych | ||
Linia 135: | Linia 131: | ||
}} | }} | ||
{{uwaga||| | |||
Niestety, aby nasz model obliczeniowy wiernie odpowiadał rzeczywistości, musimy | Niestety, aby nasz model obliczeniowy wiernie odpowiadał rzeczywistości, musimy | ||
w nim uwzględnić fakt, że działania matematyczne ( | w nim uwzględnić fakt, że działania matematyczne (i, tym bardziej, | ||
obliczanie wartości funkcji matematycznych) | obliczanie wartości funkcji matematycznych) <strong>nie są</strong> wykonywane dokładnie. | ||
Czasem | Czasem | ||
uwzględnienie tego faktu wiąże się ze znaczącym wzrostem komplikacji analizy | uwzględnienie tego faktu wiąże się ze znaczącym wzrostem komplikacji analizy | ||
algorytmu i dlatego "w pierwszym przybliżeniu" często pomija się to | algorytmu i dlatego "w pierwszym przybliżeniu" często pomija się to | ||
ograniczenie przyjmując model w którym wszystkie (lub prawie wszystkie) | ograniczenie przyjmując model w którym wszystkie (lub prawie wszystkie) | ||
działania arytmetyczne wykonują się dokładnie. Wiedza o tym, | działania arytmetyczne wykonują się dokładnie. Wiedza o tym, <strong>kiedy</strong> i | ||
<strong>jak</strong> zrobić to tak, by wciąż wyciągać prawidłowe wnioski odnośnie faktycznej | |||
realizacji algorytmów w obecności błędów zaokrągleń jest częścią sztuki i wymaga | realizacji algorytmów w obecności błędów zaokrągleń jest częścią sztuki i wymaga | ||
intuicji numerycznej, popartej doświadczeniem. | intuicji numerycznej, popartej doświadczeniem. | ||
}} | |||
Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane. | Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane. | ||
==Warunkowe== | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
Linia 163: | Linia 161: | ||
i <math>\displaystyle {\cal A}_2</math> są poleceniami, przy czym dopuszczamy polecenia puste. | i <math>\displaystyle {\cal A}_2</math> są poleceniami, przy czym dopuszczamy polecenia puste. | ||
==Powtarzane== | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
Linia 174: | Linia 172: | ||
jest poleceniem. | jest poleceniem. | ||
==Kombinowane== | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
Linia 191: | Linia 189: | ||
za "wejście" i "wyjście". | za "wejście" i "wyjście". | ||
==Wprowadzanie danych== | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
Linia 203: | Linia 201: | ||
umieszczona wartość <math>\displaystyle L_t(f)</math>. | umieszczona wartość <math>\displaystyle L_t(f)</math>. | ||
Polecenie to pozwala zdobyć | Polecenie to pozwala zdobyć <strong>informację</strong> o danej <math>\displaystyle f</math>. | ||
Jeśli <math>\displaystyle F=R^n</math> to zwykle mamy <math>\displaystyle T=\{1,2,\ldots,n\}</math> i | Jeśli <math>\displaystyle F=R^n</math> to zwykle mamy <math>\displaystyle T=\{1,2,\ldots,n\}</math> i | ||
<math>\displaystyle L_i(f)=f_i</math>, co w praktyce odpowiada wczytaniu <math>\displaystyle i</math>-tej | <math>\displaystyle L_i(f)=f_i</math>, co w praktyce odpowiada wczytaniu <math>\displaystyle i</math>-tej | ||
Linia 215: | Linia 213: | ||
w punkcie <math>\displaystyle t</math>. | w punkcie <math>\displaystyle t</math>. | ||
==Wyprowadzanie wyników== | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
Linia 235: | Linia 233: | ||
Ze względu na swój utylitarny charakter, w metodach numerycznych niemal równie | Ze względu na swój utylitarny charakter, w metodach numerycznych niemal równie | ||
ważna jak dobór optymalnego algorytmu jest jego efektywna | ważna jak dobór optymalnego algorytmu jest jego efektywna <strong>implementacja</strong> na | ||
konkretnej architekturze. | konkretnej architekturze. | ||
W praktyce mamy dwie możliwości: | W praktyce mamy dwie możliwości: | ||
* wykorzystanie standardowych języków programowania (C, Fortran, być może ze wstawkami w asemblerze) oraz | * wykorzystanie standardowych języków programowania (C, Fortran, być może ze wstawkami w asemblerze) oraz wyspecjalizowanych bibliotek | ||
* użycie gotowego środowiska obliczeń numerycznych będącego wygodnym interfejsem do specjalizowanych bibliotek numerycznych | * użycie gotowego środowiska obliczeń numerycznych będącego wygodnym interfejsem do specjalizowanych bibliotek numerycznych | ||
Zaleta pierwszego podejścia to (zazwyczaj) szybko działający kod wynikowy, ale kosztem długotrwałego i żmudnego programowania. W drugim przypadku jest na odwrót: developerka i testowanie --- wyjątkowo ważne w przypadku programu numerycznego --- postępują bardzo szybko, ale czasem kosztem ogólnej efektywności uzyskanego produktu. | |||
====Języki programowania: C i Fortran==== | ====Języki programowania: C i Fortran==== | ||
Linia 247: | Linia 247: | ||
wymagające jeśli chodzi o struktury danych, co więcej, prostota struktur danych | wymagające jeśli chodzi o struktury danych, co więcej, prostota struktur danych | ||
szybko rewanżuje się efektywniejszym kodem. Dlatego, trawestując Einsteina, w | szybko rewanżuje się efektywniejszym kodem. Dlatego, trawestując Einsteina, w | ||
dobrym programie numerycznym należy | dobrym programie numerycznym należy | ||
Językami | <blockquote style="background-color:#fefeee"> | ||
używać tak prostych struktur danych, jak to | |||
możliwe (ale nie prostszych!...) | |||
</blockquote> | |||
Językami opartymi na prostych konstrukcjach programistycznych są: Fortran i C. Dlatego właśnie są to języki | |||
dominujące współcześnie pisane programy numeryczne. O ile w przeszłości | dominujące współcześnie pisane programy numeryczne. O ile w przeszłości | ||
hegemonia Fortranu była nie do podważenia, o tyle w chwili obecnej coraz więcej | hegemonia Fortranu była nie do podważenia, o tyle w chwili obecnej coraz więcej | ||
Linia 256: | Linia 260: | ||
W naszym wykładzie wybieramy C ze względu na jego uniwersalność, | W naszym wykładzie wybieramy C ze względu na jego uniwersalność, | ||
doskonałą przenośność i w chwili obecnej całkiem dojrzałe kompilatory. Dodajmy, | doskonałą przenośność i (w chwili obecnej) całkiem dojrzałe kompilatory. Dodajmy, | ||
że funkcje w C można mieszać z np. z gotowymi bibliotekami napisanymi w | że funkcje w C można mieszać z np. z gotowymi bibliotekami napisanymi w | ||
Fortranie. Fortran, | Fortranie. Fortran, | ||
Linia 268: | Linia 272: | ||
kompilatora. | kompilatora. | ||
Inne popularne języki: Java, Pascal (ten język, zdaje się, jest popularny już | Inne popularne języki: [[Dodaj WIKIlink: Java]], Pascal (ten język, zdaje się, jest popularny już | ||
tylko w obrębie wydziału MIM UW...), VisualBasic nie są zbyt odpowiednie dla | tylko w obrębie [http://www.mimuw.edu.pl wydziału MIM UW]...), VisualBasic i inne, nie są zbyt odpowiednie dla | ||
obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala: | obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala: | ||
<code>real</code> nie jest zgodny z powszechnym standardem IEEE 754. Jednak, ze względu na coraz większą | <code>real</code> nie jest zgodny z powszechnym standardem [[Dodaj WIKIlink: IEEE 754]]. Jednak, ze względu na coraz większą | ||
komplikację kodów numerycznych służących np. do prowadzenia zaawansowanych | komplikację kodów numerycznych służących np. do prowadzenia zaawansowanych | ||
symulacji metodą elementu skończonego, coraz więcej kodów wykorzystuje | symulacji metodą elementu skończonego, coraz więcej kodów wykorzystuje | ||
Linia 277: | Linia 281: | ||
W przykładach będziemy najczęściej odnosić się do architektury x86, tzn. 32-bitowej IA-32 | W przykładach będziemy najczęściej odnosić się do architektury x86, tzn. 32-bitowej IA-32 | ||
procesorów firmy Intel i AMD, najczęściej spotykanej w obecnie używanych | procesorów firmy [http://developer.intel.com Intel] i [http://developer.amd.com AMD], najczęściej spotykanej w obecnie używanych | ||
komputerach. Należy jednak pamiętać, że obecnie następuje przejście na | komputerach. Należy jednak pamiętać, że obecnie następuje przejście na | ||
architekturę 64-bitową. Ze względu jednak na brak pewności co do ostatecznie | architekturę 64-bitową. Ze względu jednak na brak pewności co do ostatecznie | ||
przyjętych standardów w tym obszarze, ograniczymy się do procesorów | przyjętych standardów w tym obszarze, ograniczymy się do procesorów | ||
32-bitowych. | 32-bitowych. W przykładach będziemy korzystać z kompilatora [http://gcc.gnu.org GCC], który jest omówiony w wykładzie [[Dodaj WIKIlink: Środowisko programistyczne]]. | ||
====Prosty program numeryczny==== | ====Prosty program numeryczny w C==== | ||
Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) <math>\displaystyle N</math>-tą sumę | Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) <math>\displaystyle N</math>-tą sumę | ||
Linia 303: | Linia 307: | ||
unsigned int i; | unsigned int i; | ||
x | x \EQUALREAD 0.0; | ||
for(i | for(i \EQUALREAD 1; i <\EQUALREAD N; i++) | ||
x | x \EQUALREAD x + 1/i; | ||
printf("Wartość sumy x | printf("Wartość sumy x \EQUALREAD 1 + 1/2 + ... + 1/ | ||
return(0); | return(0); | ||
} | } | ||
</pre></div> | </pre></div> | ||
Sęk w tym, że ten program | Sęk w tym, że ten program <strong>nie działa!</strong> To znaczy: owszem, kompiluje się i | ||
uruchamia, ale twierdzi uparcie, że nasza suma wynosi... 1. | uruchamia, ale twierdzi uparcie, że nasza suma wynosi... 1. | ||
Linia 317: | Linia 321: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
x | x \EQUALREAD x + 1/i; | ||
</pre></div> | </pre></div> | ||
Linia 330: | Linia 334: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
x | x \EQUALREAD x + 1.0/i; | ||
</pre></div> | </pre></div> | ||
Linia 337: | Linia 341: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
x | x \EQUALREAD x + 1/((float) i); | ||
</pre></div> | </pre></div> | ||
Linia 351: | Linia 355: | ||
unsigned int i; | unsigned int i; | ||
x | x \EQUALREAD 0.0; | ||
for(i | for(i \EQUALREAD 1; i <\EQUALREAD N; i++) | ||
x | x \EQUALREAD x + 1.0/i; | ||
printf("Wartość sumy x | printf("Wartość sumy x \EQUALREAD 1 + 1/2 + ... + 1/ | ||
return(0); | return(0); | ||
} | } | ||
</pre></div> | </pre></div> | ||
====Typy numeryczne w C==== | |||
W języku C mamy dostępnych sześć typów numerycznych: | W języku C mamy dostępnych sześć typów numerycznych: | ||
* stałoprzecinkowe, dla reprezentacji liczb całkowitych | * stałoprzecinkowe, dla reprezentacji liczb całkowitych | ||
** <code>int</code> oraz <code>long int</code>. W realizacji GCC na komputery klasy PC, oba typy: <code>int</code> i <code>long int</code> są identyczne (32-bitowe) i ich zakres wynosi około <math>\displaystyle -2.1\cdot 10^9\ldots +2.1\cdot 10^9</math>. Typ <code>int</code> i jemu pokrewne odnoszą się do liczb całkowitych ze znakiem (dodatnich lub ujemnych). Ich warianty bez znaku: <code>unsigned int</code>, itp. odnoszą się do liczb bez znaku (nieujemnych), dlatego np. zakresem <code>unsigned int</code> będzie w przybliżeniu | ** <code>int</code> oraz <code>long int</code>. W realizacji [http://gcc.gnu.org GCC] na komputery klasy PC, oba typy: <code>int</code> i <code>long int</code> są identyczne (32-bitowe) i ich zakres wynosi około <math>\displaystyle -2.1\cdot 10^9\ldots +2.1\cdot 10^9</math>. Typ <code>int</code> i jemu pokrewne odnoszą się do liczb całkowitych ze znakiem (dodatnich lub ujemnych). Ich warianty bez znaku: <code>unsigned int</code>, itp. odnoszą się do liczb bez znaku (nieujemnych), dlatego np. zakresem <code>unsigned int</code> będzie w przybliżeniu | ||
<math>\displaystyle 0\ldots +4.2\cdot 10^9</math>. | <math>\displaystyle 0\ldots +4.2\cdot 10^9</math>. | ||
** <code>long long int</code> (64-bitowy) o zakresie w przybliżeniu <math>\displaystyle -9.2\cdot 10^{18}\ldots +9.2\cdot 10^{18}</math>. | ** <code>long long int</code> (64-bitowy) o zakresie w przybliżeniu <math>\displaystyle -9.2\cdot 10^{18}\ldots +9.2\cdot 10^{18}</math>. | ||
Linia 380: | Linia 386: | ||
optymalizacji uzyskanego kodu a ponadto, obiekty typu <code>float</code> zajmują | optymalizacji uzyskanego kodu a ponadto, obiekty typu <code>float</code> zajmują | ||
dwukrotnie mniej miejsca w pamięci niż <code>double</code>, dając możliwość lepszego | dwukrotnie mniej miejsca w pamięci niż <code>double</code>, dając możliwość lepszego | ||
wykorzystania pamięci podręcznej | wykorzystania pamięci podręcznej ''cache '' i przetwarzania wektorowego. | ||
====Stałe matematyczne i podstawowa biblioteka matematyczna==== | ====Stałe matematyczne i podstawowa biblioteka matematyczna==== | ||
Linia 417: | Linia 423: | ||
double x,y; | double x,y; | ||
for( i | for( i \EQUALREAD 0; i < N; i++) | ||
{ | { | ||
x | x \EQUALREAD rand()/(double)RAND_MAX; | ||
x * | x *\EQUALREAD 2.0*M_PI; | ||
/* oczywiście, wystarczyłoby x | /* oczywiście, wystarczyłoby x \EQUALREAD (2.0*M_PI*rand())/RAND_MAX; */ | ||
y | y \EQUALREAD sin(x); | ||
fprintf(stderr, "( | fprintf(stderr, "(} | ||
return(0); | return(0); | ||
} | } | ||
</pre></div> | </pre></div> | ||
Zwróćmy uwagę na linię <code>x | Zwróćmy uwagę na linię <code>x \EQUALREAD rand()/(double)RAND_MAX;</code> Funkcja <code>rand()</code> | ||
zwraca losową liczbę całkowitą z przedziału [0,<code>RAND_MAX</code>], więc iloraz z | zwraca losową liczbę całkowitą z przedziału [0,<code>RAND_MAX</code>], więc iloraz z | ||
predefiniowaną stałą <code>RAND_MAX</code> będzie liczbą z przedziału <math>\displaystyle [0,1]</math>. Dla | predefiniowaną stałą <code>RAND_MAX</code> będzie liczbą z przedziału <math>\displaystyle [0,1]</math>. Dla | ||
prawidłowego uzyskania losowej liczby z przedziału <math>\displaystyle [0,1]</math> kluczowe jest jednak | prawidłowego uzyskania losowej liczby z przedziału <math>\displaystyle [0,1]</math> kluczowe jest jednak | ||
zrzutowanie jednej z dzielonych liczb na typ <code>double</code>! Gdyby tego nie zrobić, | zrzutowanie jednej z dzielonych liczb na typ <code>double</code>! Gdyby tego nie zrobić, | ||
uzyskalibyśmy zawsze <math>\displaystyle x=0</math> lub sporadycznie <math>\displaystyle x=1</math>, zgodnie z regułą C | uzyskalibyśmy zawsze <math>\displaystyle x=0</math> lub sporadycznie <math>\displaystyle x=1</math>, zgodnie z regułą C <strong>typ | ||
wyniku jest zgodny z typem argumentów | wyniku jest zgodny z typem argumentów</strong>. Rzeczywiście, w naszym wypadku, | ||
(błędna) linia <code>x | (błędna) linia <code>x \EQUALREAD rand()/RAND_MAX;</code> zostałaby wykonana tak: ponieważ | ||
wynikiem funkcji <code>rand()</code> jest <code>int</code> i stała <code>RAND_MAX</code> jest także typu | wynikiem funkcji <code>rand()</code> jest <code>int</code> i stała <code>RAND_MAX</code> jest także typu | ||
<code>int</code>, to wynik ma być również typu <code>int</code> -- zostanie więc wykonane dzielenie | <code>int</code>, to wynik ma być również typu <code>int</code> -- zostanie więc wykonane dzielenie | ||
Linia 458: | Linia 463: | ||
====Środowisko obliczeń numerycznych: MATLAB i jego klony==== | ====Środowisko obliczeń numerycznych: MATLAB i jego klony==== | ||
[[Image:MNmatlab-screenshot.png| | [[Image:MNmatlab-screenshot.png|frame|400px|center|Typowa sesja MATLABa. Zwróć uwagę na edytor kodu źródłowego na | ||
bieżąco interpretujący go i wychwytujący potencjalne błędy]] | bieżąco interpretujący go i wychwytujący potencjalne błędy]] | ||
Linia 468: | Linia 473: | ||
język skryptów i łatwe w użyciu narzędzia manipulacji macierzami, zdobyły | język skryptów i łatwe w użyciu narzędzia manipulacji macierzami, zdobyły | ||
wielką popularność w środowisku naukowym. W 1984 roku Moler skomercjalizował swe | wielką popularność w środowisku naukowym. W 1984 roku Moler skomercjalizował swe | ||
dzieło, | dzieło, pod nazwą [http://www.mathworks.com MATLAB] (od: | ||
"MATrix LABoratory"). | "MATrix LABoratory"). | ||
Linia 485: | Linia 488: | ||
obecności takich struktur programistycznych jak pętle. | obecności takich struktur programistycznych jak pętle. | ||
[[Image:MNoctave-screenshot.png| | [[Image:MNoctave-screenshot.png|frame|400px|center|Screenshot Octave. W terminalu otwarta sesja w | ||
ascetycznym | ascetycznym | ||
trybie tekstowym, grafika wyświetlana z wykorzystaniem Gnuplota]] | trybie tekstowym, grafika wyświetlana z wykorzystaniem [http://www.gnuplot.info Gnuplota]]] | ||
Kierując się podobnymi przesłankami co C. Moler, oraz bazując na wielkim | Kierując się podobnymi przesłankami co C. Moler, oraz bazując na wielkim | ||
Linia 493: | Linia 496: | ||
Wisconsin w USA, zaczął w 1994 roku opracowywać darmowe (udostępniane na tzw. | Wisconsin w USA, zaczął w 1994 roku opracowywać darmowe (udostępniane na tzw. | ||
licencji GPL) oprogramowanie o funkcjonalności maksymalnie | licencji GPL) oprogramowanie o funkcjonalności maksymalnie | ||
zbliżonej do MATLABa: [http://www.octave.org Octave]. | zbliżonej do MATLABa: [http://www.octave.org Octave]. Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest | ||
intensywnie rozwijana do dziś. | intensywnie rozwijana do dziś. | ||
[[Image:MNscilab-screenshot.png| | [[Image:MNscilab-screenshot.png|frame|400px|center|Screenshot Scilaba.]] | ||
Drugim udanym klonem MATLABa jest francuski [http://www.scilab.org Scilab] , | Drugim udanym klonem MATLABa jest francuski [http://www.scilab.org Scilab] , | ||
opracowany w laboratoriach INRIA i wciąż doskonalony. W | opracowany w laboratoriach INRIA i wciąż doskonalony. W | ||
subiektywnej ocenie | subiektywnej ocenie autorów niniejszych notatek, nie dorównuje on elegancji i | ||
szlachetnej prostocie Octave. Na plus tego pakietu należy zaliczyć m.in. | szlachetnej prostocie Octave. Na plus tego pakietu należy zaliczyć m.in. | ||
znacznie bardziej rozbudowany zestaw funkcji podstawowych, na minus -- przede | znacznie bardziej rozbudowany zestaw funkcji podstawowych, na minus --- przede | ||
wszystkim znacznie mniejszy stopień zgodności z MATLABem, a poza tym: niewygodny | wszystkim znacznie mniejszy stopień zgodności z MATLABem, a poza tym: niewygodny | ||
system pomocy oraz "toporną" (choć o dużym potencjale) grafikę. | system pomocy oraz "toporną" (choć o dużym potencjale) grafikę. | ||
Korzystając z dowolnego z omówionych powyżej pakietów, otrzymujemy: | |||
Korzystając z omówionych powyżej pakietów, otrzymujemy: | |||
* możliwość obliczania funkcji matematycznych (nawet dość egzotycznych), | * możliwość obliczania funkcji matematycznych (nawet dość egzotycznych), | ||
finansowych, analizy sygnałów, itp.; | finansowych, analizy sygnałów, itp.; | ||
Linia 524: | Linia 523: | ||
wejścia-wyjścia, to narzędzie robi się rzeczywiście intrygujące, jako środowisko | wejścia-wyjścia, to narzędzie robi się rzeczywiście intrygujące, jako środowisko | ||
obliczeń numerycznych "dla każdego" (nie tylko dla profesjonalnego numeryka). | obliczeń numerycznych "dla każdego" (nie tylko dla profesjonalnego numeryka). | ||
Stąd wielka popularność MATLABa w środowiskach | Stąd wielka popularność MATLABa w środowiskach inżynierskich, gdyż umożliwia | ||
szybką implementację rozmaitych --- zwłaszcza testowych --- wersji algorytmu np. | szybką implementację rozmaitych --- zwłaszcza testowych --- wersji algorytmu np. | ||
przeprowadzania skomplikowanej symulacji. Po zweryfikowaniu wyników, można | przeprowadzania skomplikowanej symulacji. Po zweryfikowaniu wyników, można | ||
Linia 537: | Linia 536: | ||
MATLAB jest wciąż niedościgniony jeśli chodzi o narzędzia programistyczne (np. | MATLAB jest wciąż niedościgniony jeśli chodzi o narzędzia programistyczne (np. | ||
ma wbudowany --- działający | ma wbudowany --- działający <strong>na bieżąco</strong> w czasie edycji kodu źródłowego! --- | ||
debugger) oraz możliwości wizualizacji danych i wyników obliczeń. Jeśli nie dbać | debugger) oraz możliwości wizualizacji danych i wyników obliczeń. Jeśli nie dbać | ||
o koszta zakupu dodatkowych modułów, ma także najbogatszy zestaw funkcji | o koszta zakupu dodatkowych modułów, ma także najbogatszy zestaw funkcji | ||
Linia 544: | Linia 543: | ||
strony są powszechnie dostępne (nawet w wersji profesjonalnej) w komputerowych | strony są powszechnie dostępne (nawet w wersji profesjonalnej) w komputerowych | ||
laboratoriach akademickich w Polsce. | laboratoriach akademickich w Polsce. | ||
Zarówno w MATLABie, jak i w Octave, jest możliwe pisanie funkcji, które będą | |||
prekompilowane, dając znaczące przyspieszenie działania. | |||
Wybór pomiędzy darmowymi klonami MATLABa: Octave i Scilabem, jest właściwie kwestią osobistych preferencji. | Wybór pomiędzy darmowymi klonami MATLABa: Octave i Scilabem, jest właściwie kwestią osobistych preferencji. | ||
W ninijeszym kursie będziemy | W ninijeszym kursie będziemy posługiwać się | ||
Octave, który jest bardzo wiernym klonem MATLABa, przez co daje zaskakująco wysoki | Octave, który jest bardzo wiernym klonem MATLABa, przez co daje zaskakująco wysoki | ||
stopień kompatybilności z programami napisanymi w MATLABie. To jest bardzo | stopień kompatybilności z programami napisanymi w MATLABie. To jest bardzo | ||
Linia 553: | Linia 555: | ||
się Octave, bez trudu można "przesiąść się" na MATLABa i korzystać z jego | się Octave, bez trudu można "przesiąść się" na MATLABa i korzystać z jego | ||
bogatszych możliwości, droga w przeciwną stronę też jest nietrudna. | bogatszych możliwości, droga w przeciwną stronę też jest nietrudna. | ||
Inną zaletą Octave jest swobodniejsza niż w MATLABie składnia. | |||
Inną zaletą Octave jest swobodniejsza niż w MATLABie składnia | |||
Octave jest dołączany do większości popularnych dystrybucji Linuxa, najczęściej | Octave jest dołączany do większości popularnych dystrybucji Linuxa, najczęściej | ||
Linia 609: | Linia 598: | ||
octave:1> 1900/2000 | octave:1> 1900/2000 | ||
ans | ans \EQUALREAD 0.95000 | ||
octave:2> sin(pi) | octave:2> sin(pi) | ||
ans | ans \EQUALREAD 1.2246e-16 | ||
octave:3> e^(i*pi) | octave:3> e^(i*pi) | ||
ans | ans \EQUALREAD -1.0000e+00 + 1.2246e-16i | ||
</pre></div> | </pre></div> | ||
Ostatnie dwa wyniki dają nam namacalny dowód, że obliczenia wykonywane są | Ostatnie dwa wyniki dają nam namacalny dowód, że obliczenia wykonywane są | ||
<strong>numerycznie</strong>, ze skończoną precyzją: w Octave niektóre tożsamości matematyczne | |||
są spełnione | są spełnione <strong>jedynie w przybliżeniu</strong>, np. <math>\displaystyle \sin(\pi) \approx 0</math> oraz | ||
<math>\displaystyle e^{i\pi} \approx -1</math>. Przy okazji widzimy, że Octave dysponuje podstawowymi | <math>\displaystyle e^{i\pi} \approx -1</math>. Przy okazji widzimy, że Octave dysponuje podstawowymi | ||
stałymi matematycznymi ( | stałymi matematycznymi (oczywiście, są to także wartości przybliżone!): | ||
<code>e <math>\displaystyle \approx</math> 2.71828182845905</code>, <code>pi <math>\displaystyle \approx</math> 3.14159265358979</code> | <code>e <math>\displaystyle \approx</math> 2.71828182845905</code>, <code>pi <math>\displaystyle \approx</math> 3.14159265358979</code> | ||
oraz jednostką urojoną <code>i</code> <math>\displaystyle = \sqrt{-1}</math>. | oraz jednostką urojoną <code>i</code> <math>\displaystyle = \sqrt{-1}</math>. | ||
Octave ma dobrą dokumentację, dostępną w trakcie sesji | Octave ma dobrą dokumentację, dostępną w trakcie sesji | ||
Octave; dla każdej funkcji, stałej itp. można uzyskać jej dobry opis przy | Octave; dla każdej funkcji, stałej itp. można uzyskać jej dobry opis przy | ||
użyciu polecenia <code>help</code>. | użyciu polecenia <code>help</code>. | ||
Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz | Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz | ||
Linia 667: | Linia 652: | ||
<div class="output" style="background-color:#e0e8e8; padding:1em"><pre> | <div class="output" style="background-color:#e0e8e8; padding:1em"><pre> | ||
octave:6> A | octave:6> A \EQUALREAD [2 -1 0; -1 3 -2; 2 2.71828 3.14] | ||
A | A \EQUALREAD | ||
2.00000 -1.00000 0.00000 | 2.00000 -1.00000 0.00000 | ||
Linia 693: | Linia 678: | ||
<div class="output" style="background-color:#e0e8e8; padding:1em"><pre> | <div class="output" style="background-color:#e0e8e8; padding:1em"><pre> | ||
octave:3> B(1,1) | octave:3> B(1,1) \EQUALREAD 4 | ||
B | B \EQUALREAD 4 | ||
octave:4> B(2,1) | octave:4> B(2,1) \EQUALREAD 3 + B(1,1) | ||
B | B \EQUALREAD | ||
4 | 4 | ||
7 | 7 | ||
octave:5> B(3,2) | octave:5> B(3,2) \EQUALREAD 28 | ||
B | B \EQUALREAD | ||
4 0 | 4 0 | ||
Linia 716: | Linia 701: | ||
wartość zero. | wartość zero. | ||
<!-- | |||
W przypadku, gdy wypełniana macierz jest | W przypadku, gdy wypełniana macierz jest | ||
duża, znacznie korzystniej jest prealokować macierz, ustawiając na samym | duża, znacznie korzystniej jest prealokować macierz, ustawiając na samym | ||
początku wszystkie elementy macierzy | początku wszystkie elementy macierzy <strong>docelowego wymiaru</strong> (u nas to była | ||
macierz <math>\displaystyle 3\times 2</math>) na zero; potem możemy już działać jak poprzednio: | macierz <math>\displaystyle 3\times 2</math>) na zero; potem możemy już działać jak poprzednio: | ||
--> | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
B | B \EQUALREAD zeros(3,2) | ||
B(1,1) | B(1,1) \EQUALREAD 3 + B(1,1) | ||
B(2,1) | B(2,1) \EQUALREAD pi | ||
B(3,2) | B(3,2) \EQUALREAD 28 | ||
</pre></div> | </pre></div> | ||
Linia 736: | Linia 723: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
B | B \EQUALREAD zeros(3,2); | ||
B(1,1) | B(1,1) \EQUALREAD 3 + B(1,1); | ||
B(2,1) | B(2,1) \EQUALREAD pi; | ||
B(3,2) | B(3,2) \EQUALREAD 28; | ||
B | B | ||
</pre></div> | </pre></div> | ||
Ostatnie polecenie -- <code>B</code> | Ostatnie polecenie -- <code>B</code> <strong>bez</strong> średnika na końcu -- spowoduje | ||
wypisanie zawartości <code>B</code> na ekran. | wypisanie zawartości <code>B</code> na ekran. | ||
Linia 752: | Linia 739: | ||
* HDF5 | * HDF5 | ||
Zapis w formacie tekstowym to po | Zapis w formacie tekstowym to po prostu zapis do formatu ASCII. Ze względu na późniejszą [[|Dodaj WIKIlink: konwersję z zapisu w | ||
postaci | postaci dziesiętnej do dwójkowej]], wiąże się z drobnymi | ||
niedokładnościami w odczycie. | niedokładnościami w odczycie. | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
Linia 796: | Linia 783: | ||
Sesję Octave kończymy poleceniem <code>quit</code>. | Sesję Octave kończymy poleceniem <code>quit</code>. | ||
====Wektoryzacja==== | ====Notacja dwukropkowa Molera==== | ||
Do istniejącej macierzy możemy odwoływać się tradycyjnie, czyli do pojedynczych | |||
elementów, np. \lstoct!alfa \EQUALREAD D(2,2)!, lub do jej większych bloków, używając popularnej tzw. <strong>notacji dwukropkowej</strong> Molera. | |||
[[Image:MNcolon-matrix-1-color.png|frame|400px|center|Wybór bloku w macierzy <code>D</code> przy użyciu notacji dwukropkowej]] | |||
Jest ona | |||
szalenie intuicyjna, a mianowicie: pisząc na przykład \lstoct!v \EQUALREAD D(2:5,2)! | |||
definiujemy (wektor) <math>\displaystyle v</math>, który zawiera wiersze macierzy <math>\displaystyle D</math> od 2 do 5 wybrane z | |||
drugiej kolumny. Podobnie, pisząc \lstoct!W \EQUALREAD D(2:3,5:7)! definiujemy macierz | |||
<math>\displaystyle W</math> wymiaru | |||
<math>\displaystyle 2\times 3</math>, o elementach, które zostały wybrane z przecięcia wierszy od 2 do 3 z | |||
kolumnami od 5 do 7 macierzy <math>\displaystyle D</math>. | |||
[[Image:MNcolon-matrix-2-color.png|frame|400px|center|Wybór zestawu kolumn w macierzy <code>D</code>]] | |||
Dodatkowo, aby odwołać się do całego 4. wiersza (odpowiednio: 5. kolumny) | |||
macierzy <math>\displaystyle D</math>, można użyć skrótowej notacji \lstoct!D(4,:)! (odpowiednio: | |||
\lstoct!D(:,5)!). | |||
Odwołanie się do całej macierzy jest także możliwe (przez użycie jej nazwy, np. | |||
\lstoct!A \EQUALREAD 2*D! lub \lstoct!G \EQUALREAD log(abs(D))!). | |||
Notację dwukropkową można także wykorzystać do wygenerowania samodzielnych zbiorów indeksów: | |||
<div class="output" style="background-color:#e0e8e8; padding:1em"><pre> | |||
octave:1> N \EQUALREAD 10; | |||
octave:2> idx \EQUALREAD 1:N | |||
idx \EQUALREAD | |||
1 2 3 4 5 6 7 8 9 10 | |||
octave:3> idx2 \EQUALREAD 1:2:N | |||
idx2 \EQUALREAD | |||
1 3 5 7 9 | |||
octave:4> nidx \EQUALREAD N:-1:1 | |||
nidx \EQUALREAD | |||
10 9 8 7 6 5 4 3 2 1 | |||
</pre></div> | |||
i dlatego pętle, które w C zapisywalibyśmy | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | |||
for(i\EQUALREAD 0; i<\EQUALREAD N; i++) | |||
{ | |||
...instrukcje... | |||
} | |||
for(i\EQUALREAD N; i>\EQUALREAD 1; i--) | |||
{ | |||
...instrukcje... | |||
} | |||
</pre></div> | |||
w Octave zapiszemy | |||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | |||
for i\EQUALREAD 0:N | |||
...instrukcje... | |||
end | |||
for i\EQUALREAD N:-1:1 | |||
...instrukcje... | |||
end | |||
</pre></div> | |||
Za chwilę przekonamy się, jak można w ogóle pozbyć się potrzeby stosowania większości pętli w kodach Octave i MATLABa. | |||
====Wektoryzacja w Octave==== | |||
Ponieważ | Ponieważ podstawowymi obiektami w Octave są wektory i macierze, predefiniowane | ||
wykonują się od razu na całej macierzy. | operacje matematyczne wykonują się od razu na całej macierzy. Bez żadnej | ||
Bez żadnej | przesady możena stwierdzić, że umiejętność <strong>wektoryzacji</strong> i | ||
<strong>blokowania</strong> algorytmów jest podstawą pisania efektywnych implementacji | |||
algorytmów w Octave. | |||
Zobaczmy kilka prostych przykładów zawartych w tabeli poniżej. W | Zobaczmy kilka prostych przykładów zawartych w tabeli poniżej. W | ||
Linia 813: | Linia 871: | ||
|- | |- | ||
| | | | ||
|| Tradycyjny kod w Octave, używający pętli || Efektywny kod wektorowy (macierzowy) w Octave | |||
|- | |- | ||
| | | | ||
1 || | Alg 1 || | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
s | s \EQUALREAD 0; | ||
for i | for i \EQUALREAD 1:size(x,1) | ||
s | s \EQUALREAD s + abs(x(i)); | ||
end | end | ||
</pre></div> | </pre></div> | ||
Linia 829: | Linia 887: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
s | s \EQUALREAD sum(abs(x)); | ||
</pre></div> | </pre></div> | ||
Linia 835: | Linia 893: | ||
| | | | ||
2 || | Alg 2 || | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
N | N \EQUALREAD 500; h \EQUALREAD (b-a)/(N-1); | ||
for i | for i \EQUALREAD 1:N | ||
x(i) | x(i) \EQUALREAD a + (i-1)*h; | ||
y(i) | y(i) \EQUALREAD sin(x(i)); | ||
end | end | ||
plot(x,y); | plot(x,y); | ||
Linia 849: | Linia 907: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
N | N \EQUALREAD 500; | ||
x | x \EQUALREAD linspace(a,b,N); | ||
y | y \EQUALREAD sin(x); | ||
plot(x,y); | plot(x,y); | ||
</pre></div> | </pre></div> | ||
Linia 857: | Linia 915: | ||
|- | |- | ||
| | | | ||
3a || | Alg 3a || | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
for i | for i \EQUALREAD 1:size(C,1), | ||
for j | for j \EQUALREAD 1:size(C,2), | ||
for k | for k \EQUALREAD 1:size(A,2), | ||
C(i,j) | C(i,j) \EQUALREAD C(i,j) + A(i,k)*B(k,j); | ||
end | end | ||
end | end | ||
Linia 872: | Linia 930: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
C | C \EQUALREAD C + A*B; | ||
</pre></div> | </pre></div> | ||
Linia 878: | Linia 936: | ||
| | | | ||
3b || | Alg 3b || | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
for i | for i \EQUALREAD 1:size(C,1), | ||
for j | for j \EQUALREAD 1:size(C,2), | ||
C(i,j) | C(i,j) \EQUALREAD C(i,j) + A(i,:)*B(:,j); | ||
end | end | ||
end | end | ||
Linia 891: | Linia 949: | ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | <div class="code" style="background-color:#e8e8e8; padding:1em"><pre> | ||
C | C \EQUALREAD C + A*B; | ||
</pre></div> | </pre></div> | ||
Linia 915: | Linia 973: | ||
Kod wektorowy lub (zwłaszcza) macierzowy jest też znacznie szybszy. Spójrzmy | Kod wektorowy lub (zwłaszcza) macierzowy jest też znacznie szybszy. Spójrzmy | ||
teraz na przykłady: | teraz na przykłady: (3a) i (3b), które pokażą nam prawdziwą moc funkcji | ||
macierzowych, unikających wielokrotnie zagnieżdżonych pętli. Oba dotyczą | macierzowych, unikających wielokrotnie zagnieżdżonych pętli. Oba dotyczą | ||
operacji mnożenia dwóch macierzy. Przykład | operacji mnożenia dwóch macierzy. Przykład (3a) w wersji z potrójną pętlą | ||
<code>for</code> naśladuje sposób programowania znany nam z C lub Pascala, | <code>for</code> naśladuje sposób programowania znany nam z C lub Pascala, | ||
natomiast przykład | natomiast przykład (3b) zdaje się być napisany odrobinę w duchu wektorowym | ||
(brak trzeciej, wewnętrznej pętli, zastąpionej operacją wektorową: iloczynem | (brak trzeciej, wewnętrznej pętli, zastąpionej operacją wektorową: iloczynem | ||
skalarnym <math>\displaystyle i</math>-tego wiersza <math>\displaystyle A</math> i <math>\displaystyle j</math>-tej kolumny <math>\displaystyle B</math>). Poniżej porównanie | skalarnym <math>\displaystyle i</math>-tego wiersza <math>\displaystyle A</math> i <math>\displaystyle j</math>-tej kolumny <math>\displaystyle B</math>). Poniżej porównanie | ||
czasów działania tych trzech implementacji w przypadku macierzy <math>\displaystyle 64\times 64</math> | czasów działania tych trzech implementacji w przypadku macierzy <math>\displaystyle 64\times 64</math> | ||
(czasy dla PC z procesorem Celeron 1GHz): | (czasy dla PC z procesorem Celeron 1GHz): | ||
* Dla pętli postaci <code>C(i,j) | * Dla pętli postaci <code>C(i,j)\EQUALREAD C(i,j)+A(i,k)*B(k,j)</code> uzyskano czas | ||
21.6s, | 21.6s, | ||
* Dla pętli postaci <code>C(i,j) | * Dla pętli postaci <code>C(i,j)\EQUALREAD C(i,j)+A(i,:)*B(:,j)</code> --- 0.371s | ||
* Dla pętli postaci <code>C | * Dla pętli postaci <code>C\EQUALREAD C+A*B</code> kod działał jedynie 0.00288s! | ||
Widzimy, jak beznadziejnie wolny jest kod oparty na trzech zagnieżdżonych | Widzimy, jak beznadziejnie wolny jest kod oparty na trzech zagnieżdżonych | ||
pętlach: jest on kilka tysięcy razy wolniejszy od implementacji macierzowej | pętlach: jest on kilka tysięcy razy wolniejszy od implementacji macierzowej | ||
<code>C | <code>C \EQUALREAD C + A*B</code>. Po wektoryzacji wewnętrznej pętli, program doznaje | ||
kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie | kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie | ||
wolniejszy od kodu macierzowego! | wolniejszy od kodu macierzowego! |
Wersja z 16:56, 1 wrz 2006
Wprowadzenie do metod numerycznych
Metody numeryczne to dziedzina wiedzy zajmująca się problemami obliczeniowymi i konstrukcją algorytmów rozwiązywania zadań matematycznych. Najczęściej, zadania obliczeniowe postawione są w dziedzinie rzeczywistej (lub zespolonej) i dlatego mówimy o zadaniach obliczeniowych matematyki ciągłej (w odróżnieniu od [[|Dodaj WIKIlink: matematyki dyskretnej]]).
Zadania metod numerycznych
Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw
- określić dane problemu i cel obliczeń, czyli dokładnie
sformułować zadanie w języku matematyki,
- określić środki obliczeniowe dzięki którym chcemy osiągnąć cel,
- dla analizy zadania i sposobów jego rozwiązania wygodnie jest zdefiniować
klasę rozpatrywanych danych oraz model obliczeniowy w obrębie którego będą działać nasze algorytmy.
Wbrew dość powszechnej opinii nie jest prawdą, że głównym przedmiotem metod numerycznych jest badanie wpływu błędów zaokrągleń na wynik. Raczej, głównym celem metod numerycznych jest konstrukcja optymalnych (w jasno określonym sensie, np. pod względem wymaganej liczby operacji, lub pod względem ilości niezbędnej informacji, czy też pod względem dokładności uzyskiwanego wyniku) algorytmów rozwiązywania konkretnych zadań matematycznych.
Nasz przedmiot ma różne wcielenia i z tego powodu czasem nosi inne nazwy, w zależności od tego, na jaki aspekt metod obliczeniowych jest położony największy nacisk.
- metody numeryczne
- --- główny nacisk idzie na aspekty algorytmiczne;
- analiza numeryczna
- --- przede wszystkim badanie właściwości algorytmów, ich optymalności oraz wpływu arytmetyki zmiennopozycyjnej na jakość uzyskanych wyników;
- matematyka obliczeniowa
- --- głównie teoretyczna analiza możliwości taniej i dokładnej aproksymacji rozwiązań zadań matematycznych;
- obliczenia naukowe
- --- nacisk na praktyczne zastosowania metod numerycznych, symulacje, realizacje na komputerach o dużej mocy obliczeniowej.
Oczywiście, granice podziału nie są ostre i najczęściej typowy wykład z tego przedmiotu stara się pokazać pełne spektrum zagadnień z nim związanych. Tak będzie również i w naszym przypadku.
Model obliczeniowy
Tworząc i analizując algorytmy, jakie będą pojawiać w naszym wykładzie, będziemy posługiwać się pewnym uproszczonym modelem obliczeń, dzięki czemu będziemy mogli skoncentrować się na esencji algorytmu, bez detali implementacyjnych --- zostawiając je na inną okazję (dobra implementacja konkretnego algorytmu może być sama w sobie interesującym wyzwaniem programistycznym; często bywa, że dobre implementacje, nawet prostych algorytmów numerycznych, są mało czytelne).
Aby zdefiniować nasz model obliczeniowy, posłużymy się pojęciem programu. Zastosujemy przy tym notację podobną do tej z języka programowania C.
Program składa się z deklaracji, czyli opisu obiektów, których będziemy używać, oraz z poleceń (instrukcji), czyli opisu akcji, które będziemy wykonywać.
Dostępnymi obiektami są stałe i zmienne typu
całkowitego (int
),
rzeczywistego (float
i double
). Typ logiczny symulujemy tak jak w
C wartościami zero-jedynkowymi typu całkowitego.
Zmienne jednego typu mogą być grupowane w wektory albo tablice.
Widzimy więc, że podstawowe algorytmy numeryczne będą bazować na mało skomplikowanych typach danych. Również nieskomplikowane będą instrukcje naszego modelowego języka.
Podstawienie
z \EQUALREAD <math>\displaystyle {\cal W}</math>;
gdzie jest zmienną, a jest wyrażeniem o wartościach tego samego typu co . Jest to polecenie proste.
Wyrażeniem jest pojedyncza stała lub zmienna, albo złożenie skończonej liczby operacji elementarnych na wyrażeniach. Operacje elementarne to:
- arytmetyczno--arytmetyczne
- , ,
, , , gdzie są stałymi lub zmiennymi liczbowymi,
- arytmetyczno--logiczne
- ,
, , , gdzie są stałymi lub zmiennymi liczbowymi,
- logiczno--logiczne
- ,
, , gdzie są stałymi lub zmiennymi logicznymi.
Dla niektórych zadań wygodnie jest (a czasem konieczne) uzupełnić ten zbiór o dodatkowe operacje, takie jak obliczanie wartości niektórych standardowych funkcji matematycznych ( itp.), czy nawet funkcji bardziej skomplikowanych. Na przykład, zastosowanie "szkolnych" wzorów na obliczanie pierwiatków równania kwadratowego byłoby niemożliwe, gdyby pierwiastkowanie było niemożliwe.
Należy pamiętać, że w praktyce funkcje standardowe (o ile są dopuszczalne) są obliczane używając czterech podstawowych operacji arytmetycznych. Dokładniej, jednostka arytmetyczna procesora potrafi wykonywać jedynie operacje , przy czym dzielenie zajmuje kilka razy więcej czasu niż pozostałe operacje arytmetyczne. Niektóre procesory (np. Itanium 2) są w stanie wykonywać niejako jednocześnie operację dodawania i mnożenia (tzw. FMADD, fused multiply and add). Praktycznie wszystkie współczesne procesory mają także wbudowany koprocesor matematyczny, realizujący asemblerowe polecenia wyznaczenia wartości standardowych funkcji matematycznych ( itp.), jednak wykonanie takiej instrukcji wymaga mniej więcej kilkadziesiąt, a czasem nawet kilkaset razy więcej czasu niż wykonanie operacji dodawania.
Niestety, aby nasz model obliczeniowy wiernie odpowiadał rzeczywistości, musimy w nim uwzględnić fakt, że działania matematyczne (i, tym bardziej, obliczanie wartości funkcji matematycznych) nie są wykonywane dokładnie. Czasem uwzględnienie tego faktu wiąże się ze znaczącym wzrostem komplikacji analizy algorytmu i dlatego "w pierwszym przybliżeniu" często pomija się to ograniczenie przyjmując model w którym wszystkie (lub prawie wszystkie) działania arytmetyczne wykonują się dokładnie. Wiedza o tym, kiedy i jak zrobić to tak, by wciąż wyciągać prawidłowe wnioski odnośnie faktycznej realizacji algorytmów w obecności błędów zaokrągleń jest częścią sztuki i wymaga intuicji numerycznej, popartej doświadczeniem.
Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane.
Warunkowe
if(<math>\displaystyle \cal W</math>) <math>\displaystyle {\cal A}_1</math>; else <math>\displaystyle {\cal A}_2</math>;
gdzie jest wyrażeniem o wartościach całkowitych (0 odpowiada logicznemu fałszowi, inne wartości --- logicznej prawdzie), a i są poleceniami, przy czym dopuszczamy polecenia puste.
Powtarzane
while(<math>\displaystyle {\cal W}</math>) <math>\displaystyle {\cal A}</math>;
gdzie jest wyrażeniem o wartościach logicznych, a jest poleceniem.
Kombinowane
{ <math>\displaystyle {\cal A}_1;\displaystyle {\cal A}_2;\displaystyle \ldots\displaystyle {\cal A}_n;</math> }
gdzie są poleceniami.
Na podstawie tych trzech poleceń można tworzyć inne, takie
jak pętle for()
, czy switch()
, itd.
Mamy też dwa szczególne polecenia, które odpowiadają za "wejście" i "wyjście".
Wprowadzanie danych
<math>\displaystyle {\cal IN}</math>(x,t);
gdzie jest zmienną rzeczywistą, a "adresem" pewnego funkcjonału Parser nie mógł rozpoznać (nieznana funkcja „\toR”): {\displaystyle \displaystyle L:F\toR} należącym to pewnego zbioru . W wyniku wykonania tego polecenia w zmiennej zostaje umieszczona wartość .
Polecenie to pozwala zdobyć informację o danej . Jeśli to zwykle mamy i , co w praktyce odpowiada wczytaniu -tej współrzędnej wektora danych. W szczególności, ciąg poleceń , , pozwala uzyskać pełną informację o . Jeśli zaś jest pewną klasą funkcji Parser nie mógł rozpoznać (nieznana funkcja „\toR”): {\displaystyle \displaystyle f:[a,b]\toR} , to możemy mieć np. i . W tym przypadku, wykonanie polecenia odpowiada w praktyce skorzystaniu ze specjalnej procedury (albo urządzenia zewnętrznego) obliczającej (mierzącego) wartość funkcji w punkcie .
Wyprowadzanie wyników
<math>\displaystyle {\cal OUT}</math>(<math>\displaystyle {\cal W}</math>);
gdzie jest wyrażeniem o wartościach rzeczywistych. Polecenie to pozwala "wskazać" kolejną współrzędną wyniku.
Zakładamy, że na początku procesu obliczeniowego wartości wszystkich zmiennych są nieokreślone, oraz że dla dowolnych danych wykonanie programu wymaga wykonania skończonej liczby operacji elementarnych. Wynikiem obliczeń jest skończony ciąg liczb rzeczywistych (albo ), którego kolejne współrzędne pokazywane są poleceniem .
Środowisko obliczeniowe
Ze względu na swój utylitarny charakter, w metodach numerycznych niemal równie ważna jak dobór optymalnego algorytmu jest jego efektywna implementacja na konkretnej architekturze.
W praktyce mamy dwie możliwości:
- wykorzystanie standardowych języków programowania (C, Fortran, być może ze wstawkami w asemblerze) oraz wyspecjalizowanych bibliotek
- użycie gotowego środowiska obliczeń numerycznych będącego wygodnym interfejsem do specjalizowanych bibliotek numerycznych
Zaleta pierwszego podejścia to (zazwyczaj) szybko działający kod wynikowy, ale kosztem długotrwałego i żmudnego programowania. W drugim przypadku jest na odwrót: developerka i testowanie --- wyjątkowo ważne w przypadku programu numerycznego --- postępują bardzo szybko, ale czasem kosztem ogólnej efektywności uzyskanego produktu.
Języki programowania: C i Fortran
Programy numeryczne (a przynajmniej ich jądra obliczeniowe) są zazwyczaj niezbyt wymagające jeśli chodzi o struktury danych, co więcej, prostota struktur danych szybko rewanżuje się efektywniejszym kodem. Dlatego, trawestując Einsteina, w dobrym programie numerycznym należy
używać tak prostych struktur danych, jak to możliwe (ale nie prostszych!...)
Językami opartymi na prostych konstrukcjach programistycznych są: Fortran i C. Dlatego właśnie są to języki dominujące współcześnie pisane programy numeryczne. O ile w przeszłości hegemonia Fortranu była nie do podważenia, o tyle w chwili obecnej coraz więcej oprogramowania numerycznego powstaje w C.
W naszym wykładzie wybieramy C ze względu na jego uniwersalność, doskonałą przenośność i (w chwili obecnej) całkiem dojrzałe kompilatory. Dodajmy, że funkcje w C można mieszać z np. z gotowymi bibliotekami napisanymi w Fortranie. Fortran, język o bardzo długiej tradycji, wciąż żywy i mający grono wiernych fanów, jest nadal wybierany przez numeryków na całym świecie między innymi ze względu na jego dopasowanie do zadań obliczeniowych (właśnie w tym celu powstał), a także ze względu na doskonałe kompilatory dostępne na superkomputerach, będące efektem wieloletniej ewolucji i coraz lepszego nie tylko dopasowania kompilatora do spotykanych konstrukcji językowych, ale także na odwrót --- coraz lepszego zrozumienia programistów, jak pisać programy, by wycisnąć jak najwięcej z kompilatora.
Inne popularne języki: Dodaj WIKIlink: Java, Pascal (ten język, zdaje się, jest popularny już
tylko w obrębie wydziału MIM UW...), VisualBasic i inne, nie są zbyt odpowiednie dla
obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala:
real
nie jest zgodny z powszechnym standardem Dodaj WIKIlink: IEEE 754. Jednak, ze względu na coraz większą
komplikację kodów numerycznych służących np. do prowadzenia zaawansowanych
symulacji metodą elementu skończonego, coraz więcej kodów wykorzystuje
możliwości obiektowych języków C++ i Fortran90.
W przykładach będziemy najczęściej odnosić się do architektury x86, tzn. 32-bitowej IA-32 procesorów firmy Intel i AMD, najczęściej spotykanej w obecnie używanych komputerach. Należy jednak pamiętać, że obecnie następuje przejście na architekturę 64-bitową. Ze względu jednak na brak pewności co do ostatecznie przyjętych standardów w tym obszarze, ograniczymy się do procesorów 32-bitowych. W przykładach będziemy korzystać z kompilatora GCC, który jest omówiony w wykładzie Dodaj WIKIlink: Środowisko programistyczne.
Prosty program numeryczny w C
Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) -tą sumę częściową szeregu harmonicznego
Przyjmijmy, że parametr będzie miał wartość równą 2006. W pierwszym odruchu, prawie każdy początkujący student pisze program w rodzaju:
#include <stdio.h> #define N 2006 int main(void) { float x; unsigned int i; x \EQUALREAD 0.0; for(i \EQUALREAD 1; i <\EQUALREAD N; i++) x \EQUALREAD x + 1/i; printf("Wartość sumy x \EQUALREAD 1 + 1/2 + ... + 1/ return(0); }
Sęk w tym, że ten program nie działa! To znaczy: owszem, kompiluje się i uruchamia, ale twierdzi uparcie, że nasza suma wynosi... 1.
Winę za to ponosi linijka
x \EQUALREAD x + 1/i;
w której wykonujemy dzielenie 1/i
. Obie liczby są typu int
i
dlatego, zgodnie z regułami C, wynik ich dzielenia także jest całkowity:
dostajemy część całkowitą z dzielenia tych dwóch liczb, czyli, gdy tylko , po prostu zero.
Prawidłowy program musi więc wymusić potraktowanie choć jednej z tych liczb jako liczby zmiennoprzecinkowej, co najprościej uzyskać albo przez
x \EQUALREAD x + 1.0/i;
albo bardziej uniwersalnie, rzutując choć jedną z liczb na odpowiedni typ:
x \EQUALREAD x + 1/((float) i);
Poprawny kod miałby więc postać
#include <stdio.h> #define N 2006 int main(void) { float x; unsigned int i; x \EQUALREAD 0.0; for(i \EQUALREAD 1; i <\EQUALREAD N; i++) x \EQUALREAD x + 1.0/i; printf("Wartość sumy x \EQUALREAD 1 + 1/2 + ... + 1/ return(0); }
Typy numeryczne w C
W języku C mamy dostępnych sześć typów numerycznych:
- stałoprzecinkowe, dla reprezentacji liczb całkowitych
int
orazlong int
. W realizacji GCC na komputery klasy PC, oba typy:int
ilong int
są identyczne (32-bitowe) i ich zakres wynosi około . Typint
i jemu pokrewne odnoszą się do liczb całkowitych ze znakiem (dodatnich lub ujemnych). Ich warianty bez znaku:unsigned int
, itp. odnoszą się do liczb bez znaku (nieujemnych), dlatego np. zakresemunsigned int
będzie w przybliżeniu
.
long long int
(64-bitowy) o zakresie w przybliżeniu .
- zmiennopprzecinkowe, dla reprezentacji liczb rzeczywistych
float
, pojedynczej precyzji, 32-bitowy, gwarantuje precyzję około i zakres liczb reprezentowalnych w przybliżeniu ;double
, podwójnej precyzji, 64-bitowy, ma precyzję na poziomie przy orientacyjnym zakresie ;long double
, rozszerzonej podwójnej precyzji, na pecetach 80-bitowy, ale w pamięci zajmuje on 12 bajtów) o precyzji rzędu i odpowiadający standardowi double extended IEEE 754.
Powyższe typy zmiennoprzecinkowe w realizacji na PC odpowiadają
standardowi IEEE 754.
Standardowo, operacje arytmetyczne
na obu typach float
i double
są tak samo pracochłonne, gdyż
wszystkie obliczenia w C wykonywane są z maksymalną dostępną precyzją (czyli, na
procesorach architektury IA-32 Intela i AMD: w precyzji oferowanej przez typ
long double
), a następnie dopiero wynik zapisywany do zmiennej
reprezentowany jest w stosownym typie . Jednakże typ
pojedynczej precyzji float
oferuje znacznie większe możliwości
optymalizacji uzyskanego kodu a ponadto, obiekty typu float
zajmują
dwukrotnie mniej miejsca w pamięci niż double
, dając możliwość lepszego
wykorzystania pamięci podręcznej cache i przetwarzania wektorowego.
Stałe matematyczne i podstawowa biblioteka matematyczna
Język C jest językiem małym i, jak wiadomo, nawet proste operacje
wejścia-wyjścia są w istocie nie częścią języka, ale funkcjami (makrami?)
bibliotecznymi. Z drugiej strony jednak, jak zorientowaliśmy się, nie stwarza to
programiście żadnych specjalnych niedogodności. Podobnie rzecz ma się z
funkcjami matematycznymi. Podstawowe funkcje matematyczne (, itp.) nie są składnikami języka C, lecz w zamian są zaimplementowane w
tzw. standardowej bibliotece matematycznej libm.a
; prototypy tych
funkcji oraz definicje rozmaitych stałych matematycznych: itp.
znajdują się w pliku nagłówkowym math.h
. Aby więc skorzystać z tych
funkcji w programie, należy
- w nagłówku pliku, w którym korzystamy z funkcji lub stałych matematycznych,
umieścić linię #include <math.h>
- przy linkowaniu dołączyć bibliotekę matematyczną za pomocą opcji
-lm
Przykład
Oto przykładowy prosty program numeryczny w C; drukuje on tablicę wartości sinusów losowo wybranych liczb z przedziału .
[{Tablica losowych sinusów}] #include <math.h> #include <stdio.h> #include <stdlib.h> /* zawiera definicję funkcji rand() i stałej RAND_MAX */ #define N 15 /* ile liczb wydrukować */ int main(void) { int i; double x,y; for( i \EQUALREAD 0; i < N; i++) { x \EQUALREAD rand()/(double)RAND_MAX; x *\EQUALREAD 2.0*M_PI; /* oczywiście, wystarczyłoby x \EQUALREAD (2.0*M_PI*rand())/RAND_MAX; */ y \EQUALREAD sin(x); fprintf(stderr, "(} return(0); }
Zwróćmy uwagę na linię x \EQUALREAD rand()/(double)RAND_MAX;
Funkcja rand()
zwraca losową liczbę całkowitą z przedziału [0,RAND_MAX
], więc iloraz z
predefiniowaną stałą RAND_MAX
będzie liczbą z przedziału . Dla
prawidłowego uzyskania losowej liczby z przedziału kluczowe jest jednak
zrzutowanie jednej z dzielonych liczb na typ double
! Gdyby tego nie zrobić,
uzyskalibyśmy zawsze lub sporadycznie , zgodnie z regułą C typ
wyniku jest zgodny z typem argumentów. Rzeczywiście, w naszym wypadku,
(błędna) linia x \EQUALREAD rand()/RAND_MAX;
zostałaby wykonana tak: ponieważ
wynikiem funkcji rand()
jest int
i stała RAND_MAX
jest także typu
int
, to wynik ma być również typu int
-- zostanie więc wykonane dzielenie
całkowite; ponieważ mamy rand() RAND_MAX
, to wynikiem będzie
albo 0, albo 1, i taki rezultat, po zamianie na typ double
, zostanie przypisany
zmiennej , co oczywiście nie jest naszym zamiarem. Natomiast, gdy
przynajmniej jedna z dzielonych liczb jest typu double
, to oczywiście wynikiem
też musi być liczba typu double
, zostanie więc wykonane zwyczajne dzielenie
dwóch liczb zmiennoprzecinkowych (wynik rand()
automatycznie zostanie
zrzutowany na typ double
).
Kompilujemy ten program, zgodnie z uwagami uczynionymi na początku, poleceniem
gcc -o sinusy sinusy.c -lm
Środowisko obliczeń numerycznych: MATLAB i jego klony

W końcu lat siedemdziesiątych ubiegłego wieku, Cleve Moler wpadł na pomysł stworzenia prostego interfejsu do ówcześnie istniejących bibliotek numerycznych algebry liniowej: pakietów LINPACK i EISPACK
(których był współautorem). Stworzone przez niego: język skryptów i łatwe w użyciu narzędzia manipulacji macierzami, zdobyły wielką popularność w środowisku naukowym. W 1984 roku Moler skomercjalizował swe dzieło, pod nazwą MATLAB (od: "MATrix LABoratory").
MATLAB wkrótce rozrósł się potężnie, implementując (lub wykorzystując) wiele najlepszych z istniejących algorytmów numerycznych, a także oferując bogate możliwości wizualizacji wyników. Dzięki swemu interfejsowi, składającemu się z prostych, niemal intuicyjnych funkcji, oraz ogromnym możliwościom jest jednym z powszechniej używanych pakietów do prowadzenia symulacji komputerowych w naukach przyrodniczych (i nie tylko).
Interpreter MATLABa jest dosyć wolny, dlatego podstawową regułą pisania efektywnych kodów w MATLABie jest maksymalne wykorzystanie gotowych (prekompilowanych) funkcji MATLABa oraz --- zredukowanie do minimum obecności takich struktur programistycznych jak pętle.

Kierując się podobnymi przesłankami co C. Moler, oraz bazując na wielkim sukcesie MATLABa, John W. Eaton z Wydziału Inżynierii Chemicznej Uniwersytetu Wisconsin w USA, zaczął w 1994 roku opracowywać darmowe (udostępniane na tzw. licencji GPL) oprogramowanie o funkcjonalności maksymalnie zbliżonej do MATLABa: Octave. Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest intensywnie rozwijana do dziś.

Drugim udanym klonem MATLABa jest francuski Scilab , opracowany w laboratoriach INRIA i wciąż doskonalony. W subiektywnej ocenie autorów niniejszych notatek, nie dorównuje on elegancji i szlachetnej prostocie Octave. Na plus tego pakietu należy zaliczyć m.in. znacznie bardziej rozbudowany zestaw funkcji podstawowych, na minus --- przede wszystkim znacznie mniejszy stopień zgodności z MATLABem, a poza tym: niewygodny system pomocy oraz "toporną" (choć o dużym potencjale) grafikę.
Korzystając z dowolnego z omówionych powyżej pakietów, otrzymujemy:
- możliwość obliczania funkcji matematycznych (nawet dość egzotycznych),
finansowych, analizy sygnałów, itp.;
- bardzo szeroki zakres nowoczesnych narzędzi umożliwiających
wykonywanie podstawowych zadań numerycznych, takich jak: rozwiązywanie równań, obliczanie całek, itd.;
- efektowną wizualizację wyników w postaci wykresów dwu- i trójwymiarowych,
a także funkcje do manipulacji obrazem i dźwiękiem;
- możliwość nieograniczonych rozszerzeń przy użyciu funkcji i skryptów
tworzonych przez osoby trzecie (lub własnych).
Jeśli uwzględnić dodatki wielce ułatwiające życie, w tym rozbudowane funkcje wejścia-wyjścia, to narzędzie robi się rzeczywiście intrygujące, jako środowisko obliczeń numerycznych "dla każdego" (nie tylko dla profesjonalnego numeryka). Stąd wielka popularność MATLABa w środowiskach inżynierskich, gdyż umożliwia szybką implementację rozmaitych --- zwłaszcza testowych --- wersji algorytmu np. przeprowadzania skomplikowanej symulacji. Po zweryfikowaniu wyników, można ewentualnie rozpocząć implementację w docelowym środowisku (np. masywnie równoległym komputerze, w Fortranie 95 z wykorzystaniem komercyjnych bibliotek i specyficznymi optymalizacjami kodu), choć bardzo często wyniki uzyskane w MATLABie w zupełności wystarczą.
Zarówno MATLAB, jak Octave i Scilab, opierają się w dużym zakresie na darmowych --- acz profesjonalnych --- zewnętrznych bibliotekach numerycznych (LAPACK, ATLAS, ARPACK, itp.)
MATLAB jest wciąż niedościgniony jeśli chodzi o narzędzia programistyczne (np. ma wbudowany --- działający na bieżąco w czasie edycji kodu źródłowego! --- debugger) oraz możliwości wizualizacji danych i wyników obliczeń. Jeśli nie dbać o koszta zakupu dodatkowych modułów, ma także najbogatszy zestaw funkcji numerycznych. W chwili pisania niniejszego tekstu, (stosunkowo tanie) wersje studenckie MATLABa, nie są oferowane w Polsce przez producenta, choć z drugiej strony są powszechnie dostępne (nawet w wersji profesjonalnej) w komputerowych laboratoriach akademickich w Polsce.
Zarówno w MATLABie, jak i w Octave, jest możliwe pisanie funkcji, które będą prekompilowane, dając znaczące przyspieszenie działania.
Wybór pomiędzy darmowymi klonami MATLABa: Octave i Scilabem, jest właściwie kwestią osobistych preferencji. W ninijeszym kursie będziemy posługiwać się Octave, który jest bardzo wiernym klonem MATLABa, przez co daje zaskakująco wysoki stopień kompatybilności z programami napisanymi w MATLABie. To jest bardzo ważne w praktyce, ponieważ jest dostępnych wiele (nawet darmowych) pakietów numerycznych opracowanych właśnie w języku poleceń MATLABa. Umiejąc posługiwać się Octave, bez trudu można "przesiąść się" na MATLABa i korzystać z jego bogatszych możliwości, droga w przeciwną stronę też jest nietrudna. Inną zaletą Octave jest swobodniejsza niż w MATLABie składnia.
Octave jest dołączany do większości popularnych dystrybucji Linuxa, najczęściej jednak użytkownik musi samodzielnie doinstalować go z płytki instalacyjnej. Ponadto, kody źródłowe najświeższej wersji Octave są dostępne na stronie http://www.octave.org. Dodatkowo, pod http://octave.sourceforge.net znajdziemy pakiet rozszerzeń do Octave, pod nazwą Octave-forge. Wymaga on od użytkowników Linuxa samodzielnej (przebiegającej bezproblemowo) instalacji. Octave można także zainstalować pod Windows, korzystając z programu instalacyjnego dostępnego pod adresem internetowym http://octave.sourceforge.net. W Windowsowej wersji Octave, pakiet Octave-forge jest standardowo dołączony.
Pierwsze kroki w Octave
Octave uruchamiamy poleceniem octave
,
[przykry@bit MN] octave GNU Octave, version 2.9.5 (i686-redhat-linux-gnu). Copyright (C) 2006 John W. Eaton. This is free software; see the source code for copying conditions. There is ABSOLUTELY NO WARRANTY; not even for MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, type `warranty'. Additional information about Octave is available at http://www.octave.org. Please contribute if you find this software useful. For more information, visit http://www.octave.org/help-wanted.html Report bugs to <bug@octave.org> (but first, please read http://www.octave.org/bugs.html to learn how to write a helpful report). octave:1>
Naturalnie, Octave zachowuje wszystkie możliwości kalkulatora naukowego, wykonując podstawowe operacje arytmetyczne i obliczając wartości wielu funkcji matematycznych; oto zapis takiej sesji Octave:
octave:1> 1900/2000 ans \EQUALREAD 0.95000 octave:2> sin(pi) ans \EQUALREAD 1.2246e-16 octave:3> e^(i*pi) ans \EQUALREAD -1.0000e+00 + 1.2246e-16i
Ostatnie dwa wyniki dają nam namacalny dowód, że obliczenia wykonywane są
numerycznie, ze skończoną precyzją: w Octave niektóre tożsamości matematyczne
są spełnione jedynie w przybliżeniu, np. oraz
. Przy okazji widzimy, że Octave dysponuje podstawowymi
stałymi matematycznymi (oczywiście, są to także wartości przybliżone!):
e 2.71828182845905
, pi 3.14159265358979
oraz jednostką urojoną i
.
Octave ma dobrą dokumentację, dostępną w trakcie sesji
Octave; dla każdej funkcji, stałej itp. można uzyskać jej dobry opis przy
użyciu polecenia help
.
Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz dwuwymiarowa , o elementach rzeczywistych lub zespolonych,
W szczególności, biorąc albo , dostajemy wektor (kolumnowy lub wierszowy
Z kolei dla mamy do czynienia ze "zwykłymi" liczbami rzeczywistymi lub zespolonymi.
Octave umożliwia, za swoim pierwowzorem --- MATLABem, bardzo wygodną pracę z macierzami.
Wprowadzanie macierzy do Octave jest bardzo intuicyjne i możemy zrobić to na kilka sposobów, w zależności od potrzeb. Można robić to interakcyjnie, podając elementy macierzy z linii komend, na przykład:
octave:6> A \EQUALREAD [2 -1 0; -1 3 -2; 2 2.71828 3.14] A \EQUALREAD 2.00000 -1.00000 0.00000 -1.00000 3.00000 -2.00000 2.00000 2.71828 3.14000
definiuje macierz kwadratową o elementach równych
Tak więc, macierz w Octave definiujemy przez podanie jej elementów. Elementy wprowadzamy kolejno wierszami, elementy w wierszu oddzielamy spacjami (lub ewentualnie przecinkami), natomiast kolejne wiersze oddzielamy średnikami.
Macierz możemy także tworzyć stopniowo, wprowadzając jeden za drugim jej kolejne elementy:
octave:3> B(1,1) \EQUALREAD 4 B \EQUALREAD 4 octave:4> B(2,1) \EQUALREAD 3 + B(1,1) B \EQUALREAD 4 7 octave:5> B(3,2) \EQUALREAD 28 B \EQUALREAD 4 0 7 0 0 28
Pierwsza komenda określa B
jako macierz , czyli zwykłą
liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia --
powoduje, że B
zostaje rozszerzony do macierzy . Zauważmy, że
elementom B
o nieokreślonych przez nas wartościach zostaje przypisana domyślna
wartość zero.
B \EQUALREAD zeros(3,2) B(1,1) \EQUALREAD 3 + B(1,1) B(2,1) \EQUALREAD pi B(3,2) \EQUALREAD 28
Jeśli nie chcemy za każdym razem obserwować echa prowadzonych przez nas działań
na macierzy B
, komendy powinniśmy kończyć średnikiem: średnik po
komendzie Octave blokuje echo wyniku jej wykonania. Tak więc, znacznie
wygodniej będzie nam napisać
B \EQUALREAD zeros(3,2); B(1,1) \EQUALREAD 3 + B(1,1); B(2,1) \EQUALREAD pi; B(3,2) \EQUALREAD 28; B
Ostatnie polecenie -- B
bez średnika na końcu -- spowoduje
wypisanie zawartości B
na ekran.
Tak utworzoną macierz możemy zapisać do pliku, wykorzystując jeden z kilku formatów:
- tekstowy
- binarny
- HDF5
Zapis w formacie tekstowym to po prostu zapis do formatu ASCII. Ze względu na późniejszą [[|Dodaj WIKIlink: konwersję z zapisu w postaci dziesiętnej do dwójkowej]], wiąże się z drobnymi niedokładnościami w odczycie.
save macierzb.dat B
W wyniku dostajemy
# Created by Octave 2.9.5, Mon Jul 31 17:15:25 2006 CEST <przykry@bit> # name: B # type: matrix # rows: 3 # columns: 2 3 0 3.14159265358979 0 0 28
Zapis binarny to prostu zrzut stanu pamięci
save -binary macierzb.dat B
Format HDF5 to format gwarantujący pełną przenośność danych pomiędzy różnymi architekturami, akceptowany przez wiele zewnętrznych aplikacji, m.in. OpenDX.
save -hdf5 macierzb.dat B
Dane z pliku wczytujemy z powrotem poleceniem
load macierzb.dat
Sesję Octave kończymy poleceniem quit
.
Notacja dwukropkowa Molera
Do istniejącej macierzy możemy odwoływać się tradycyjnie, czyli do pojedynczych elementów, np. \lstoct!alfa \EQUALREAD D(2,2)!, lub do jej większych bloków, używając popularnej tzw. notacji dwukropkowej Molera.

D
przy użyciu notacji dwukropkowejJest ona szalenie intuicyjna, a mianowicie: pisząc na przykład \lstoct!v \EQUALREAD D(2:5,2)! definiujemy (wektor) , który zawiera wiersze macierzy od 2 do 5 wybrane z drugiej kolumny. Podobnie, pisząc \lstoct!W \EQUALREAD D(2:3,5:7)! definiujemy macierz wymiaru , o elementach, które zostały wybrane z przecięcia wierszy od 2 do 3 z kolumnami od 5 do 7 macierzy .

D
Dodatkowo, aby odwołać się do całego 4. wiersza (odpowiednio: 5. kolumny) macierzy , można użyć skrótowej notacji \lstoct!D(4,:)! (odpowiednio: \lstoct!D(:,5)!).
Odwołanie się do całej macierzy jest także możliwe (przez użycie jej nazwy, np. \lstoct!A \EQUALREAD 2*D! lub \lstoct!G \EQUALREAD log(abs(D))!).
Notację dwukropkową można także wykorzystać do wygenerowania samodzielnych zbiorów indeksów:
octave:1> N \EQUALREAD 10; octave:2> idx \EQUALREAD 1:N idx \EQUALREAD 1 2 3 4 5 6 7 8 9 10 octave:3> idx2 \EQUALREAD 1:2:N idx2 \EQUALREAD 1 3 5 7 9 octave:4> nidx \EQUALREAD N:-1:1 nidx \EQUALREAD 10 9 8 7 6 5 4 3 2 1
i dlatego pętle, które w C zapisywalibyśmy
for(i\EQUALREAD 0; i<\EQUALREAD N; i++) { ...instrukcje... } for(i\EQUALREAD N; i>\EQUALREAD 1; i--) { ...instrukcje... }
w Octave zapiszemy
for i\EQUALREAD 0:N ...instrukcje... end for i\EQUALREAD N:-1:1 ...instrukcje... end
Za chwilę przekonamy się, jak można w ogóle pozbyć się potrzeby stosowania większości pętli w kodach Octave i MATLABa.
Wektoryzacja w Octave
Ponieważ podstawowymi obiektami w Octave są wektory i macierze, predefiniowane operacje matematyczne wykonują się od razu na całej macierzy. Bez żadnej przesady możena stwierdzić, że umiejętność wektoryzacji i blokowania algorytmów jest podstawą pisania efektywnych implementacji algorytmów w Octave.
Zobaczmy kilka prostych przykładów zawartych w tabeli poniżej. W pierwszej kolumnie tabeli, dane zadanie zaimplementujemy w Octave przy użyciu pętli (zwróćmy przy okazji uwagę na to, jak programuje się pętle w Octave, one nie są zupełnie bezużyteczne...). W drugiej kolumnie zobaczymy, jak to samo zadanie można wykonać korzystając z operatorów lub funkcji macierzowych.
Tradycyjny kod w Octave, używający pętli | Efektywny kod wektorowy (macierzowy) w Octave | |
Alg 1 || s \EQUALREAD 0; for i \EQUALREAD 1:size(x,1) s \EQUALREAD s + abs(x(i)); end |
s \EQUALREAD sum(abs(x)); | |
Alg 2 || N \EQUALREAD 500; h \EQUALREAD (b-a)/(N-1); for i \EQUALREAD 1:N x(i) \EQUALREAD a + (i-1)*h; y(i) \EQUALREAD sin(x(i)); end plot(x,y); |
N \EQUALREAD 500; x \EQUALREAD linspace(a,b,N); y \EQUALREAD sin(x); plot(x,y); | |
Alg 3a || for i \EQUALREAD 1:size(C,1), for j \EQUALREAD 1:size(C,2), for k \EQUALREAD 1:size(A,2), C(i,j) \EQUALREAD C(i,j) + A(i,k)*B(k,j); end end end |
C \EQUALREAD C + A*B; | |
Alg 3b || for i \EQUALREAD 1:size(C,1), for j \EQUALREAD 1:size(C,2), C(i,j) \EQUALREAD C(i,j) + A(i,:)*B(:,j); end end |
C \EQUALREAD C + A*B; | |
Zwróćmy uwagę na to, że kod wektorowy Octave oraz, w jeszcze większym stopniu, kod macierzowy jest znacznie bardziej elegancki i czytelniejszy od tradycyjnego. Widać to dobrze na wszystkich powyższych przykładach.
Pierwszy przykład pokazuje też, że kod macierzowy jest elastyczniejszy: albowiem
obliczy sumę modułów wszystkich elementów x
nie tylko gdy x
jest wektorem (co musieliśmy milcząco założyć w kodzie w lewej kolumnie), ale
tak samo dobrze zadziała, gdy x
jest macierzą!
Szczególnie wiele na czytelności i wygodzie zyskujemy w drugim przykładzie,
gdzie najpierw funkcja linspace
pozwala nam uniknąć żmudnego
wyznaczania równoodległych węzłów w odcinku , a następnie
funkcja sin
zaaplikowana do całego wektora daje wartość sinusa w
zadanych przez nas węzłach.
Kod wektorowy lub (zwłaszcza) macierzowy jest też znacznie szybszy. Spójrzmy
teraz na przykłady: (3a) i (3b), które pokażą nam prawdziwą moc funkcji
macierzowych, unikających wielokrotnie zagnieżdżonych pętli. Oba dotyczą
operacji mnożenia dwóch macierzy. Przykład (3a) w wersji z potrójną pętlą
for
naśladuje sposób programowania znany nam z C lub Pascala,
natomiast przykład (3b) zdaje się być napisany odrobinę w duchu wektorowym
(brak trzeciej, wewnętrznej pętli, zastąpionej operacją wektorową: iloczynem
skalarnym -tego wiersza i -tej kolumny ). Poniżej porównanie
czasów działania tych trzech implementacji w przypadku macierzy
(czasy dla PC z procesorem Celeron 1GHz):
- Dla pętli postaci
C(i,j)\EQUALREAD C(i,j)+A(i,k)*B(k,j)
uzyskano czas
21.6s,
- Dla pętli postaci
C(i,j)\EQUALREAD C(i,j)+A(i,:)*B(:,j)
--- 0.371s - Dla pętli postaci
C\EQUALREAD C+A*B
kod działał jedynie 0.00288s!
Widzimy, jak beznadziejnie wolny jest kod oparty na trzech zagnieżdżonych
pętlach: jest on kilka tysięcy razy wolniejszy od implementacji macierzowej
C \EQUALREAD C + A*B
. Po wektoryzacji wewnętrznej pętli, program doznaje
kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie
wolniejszy od kodu macierzowego!