MNwyklad01: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przykry (dyskusja | edycje)
mNie podano opisu zmian
 
Przykry (dyskusja | edycje)
mNie podano opisu zmian
Linia 13: Linia 13:
Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw  
Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw  
* określić ''dane problemu'' i ''cel obliczeń'', czyli dokładnie  
* określić ''dane problemu'' i ''cel obliczeń'', czyli dokładnie  
      sformułować zadanie w języku matematyki,  
sformułować zadanie w języku matematyki,  
* określić ''środki obliczeniowe'' dzięki którym chcemy  osiągnąć cel,
* określić ''środki obliczeniowe'' 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ć
''klasę rozpatrywanych danych'' oraz ''model obliczeniowy'' w obrębie
''klasę rozpatrywanych danych'' oraz ''model obliczeniowy'' w obrębie
którego będą działać nasze algorytmy.
którego będą działać nasze algorytmy.
 
 
Wbrew dość powszechnej opinii ''nie jest prawdą'', że głównym celem metod
Wbrew dość powszechnej opinii ''nie jest prawdą'', że głównym celem 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
Linia 32: Linia 32:


; metody numeryczne
; metody numeryczne
:  --- główny nacisk idzie na aspekty algorytmiczne, ale
:  --- główny nacisk idzie na aspekty algorytmiczne, ale przy uwzględnieniu pozostałych;
przy uwzględnieniu pozostałych;


; analiza numeryczna
; analiza numeryczna
:  --- przede wszystkim badanie właściwości algorytmów,
:  --- przede wszystkim badanie właściwości algorytmów, ich optymalności oraz wpływu arytmetyki zmiennopozycyjnej na jakość uzyskanych wyników;
ich optymalności oraz wpływu arytmetyki zmiennopozycyjnej na jakość uzyskanych
wyników;


; matematyka obliczeniowa
; matematyka obliczeniowa
:  --- głównie teoretyczna analiza możliwości taniej i
:  --- głównie teoretyczna analiza możliwości taniej i dokładnej aproksymacji rozwiązań zadań matematycznych;
dokładnej aproksymacji rozwiązań zadań matematycznych;


; obliczenia naukowe
; obliczenia naukowe
:  --- nacisk na praktyczne zastosowania metod
:  --- nacisk na praktyczne zastosowania metod numerycznych, symulacje, realizacje na komputerach o dużej mocy obliczeniowej.
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
Oczywiście, granice podziału nie są ostre i najczęściej typowy wykład z tego
Linia 80: Linia 75:
modelowego języka.
modelowego języka.


====\sc Podstawienie====
====Podstawienie====


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Linia 96: Linia 91:
; arytmetyczno--arytmetyczne:
; arytmetyczno--arytmetyczne:
:  <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//y, y\ne 0</math>, gdzie <math>\displaystyle x,y</math> są stałymi lub  
<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,


; arytmetyczno--logiczne:
; arytmetyczno--logiczne:
:  <math>\displaystyle (x,y)\mapsto x<y</math>,  
:  <math>\displaystyle (x,y)\mapsto x<y</math>,  
  <math>\displaystyle (x,y)\mapsto x\le y</math>, <math>\displaystyle (x,y)\mapsto x=y</math>, <math>\displaystyle (x,y)\mapsto x\ne y</math>,  
<math>\displaystyle (x,y)\mapsto x\le y</math>, <math>\displaystyle (x,y)\mapsto x=y</math>, <math>\displaystyle (x,y)\mapsto x\ne y</math>,  
  gdzie <math>\displaystyle x,y</math> są stałymi lub zmiennymi liczbowymi,
gdzie <math>\displaystyle x,y</math> są stałymi lub zmiennymi liczbowymi,


; logiczno--logiczne:
; logiczno--logiczne:
:  <math>\displaystyle p\mapsto\,not\,p</math>,  
:  <math>\displaystyle p\mapsto\,\tilde{}\,p</math>,  
  <math>\displaystyle (p,q)\mapsto p\,and\,q</math>, <math>\displaystyle (p,q)\mapsto p\,or\,q</math>,  
<math>\displaystyle (p,q)\mapsto p\,and\,q</math>, <math>\displaystyle (p,q)\mapsto p\,or\,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 koniecznie)  
Dla niektórych zadań wygodnie jest (a czasem koniecznie)  
Linia 115: Linia 110:
(<math>\displaystyle \sqrt{\;}, \cos(), \sin(), \exp(), \log(),</math> itp.),  
(<math>\displaystyle \sqrt{\;}, \cos(), \sin(), \exp(), \log(),</math> itp.),  
czy nawet funkcji bardziej skomplikowanych. Na przykład,  
czy nawet funkcji bardziej skomplikowanych. Na przykład,  
zastosowanie [[trojmian|Uzupe�nij: "szkolnych" wzorów na obliczanie pierwiatków  
zastosowanie "szkolnych" wzorów na obliczanie pierwiatków  
równania kwadratowego]] byłoby  
równania kwadratowego byłoby  
niemożliwe, gdyby pierwiastkowanie było niemożliwe.
niemożliwe, gdyby pierwiastkowanie było niemożliwe.
   
   
Linia 137: Linia 132:
w nim uwzględnić fakt, że działania matematyczne (ani tym bardziej
w nim uwzględnić fakt, że działania matematyczne (ani tym bardziej
obliczanie wartości funkcji matematycznych) ''nie są'' wykonywane dokładnie.
obliczanie wartości funkcji matematycznych) ''nie są'' wykonywane dokładnie.
Temu zagadnieniu poświęcamy rozdział dotyczący [[sec:fl|Uzupe�nij: arytmetyki
Czasem
zmiennoprzecinkowej]]. Jednak już w tym miejscu zwracamy uwagę, że 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, ''kiedy'' i ''jak'' zrobić to tak, by wciąż wyciągać prawidłowe wnioski odnośnie faktycznej
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
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.
Linia 148: Linia 143:
Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane.  
Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane.  


====\sc Warunkowe====
====Warunkowe====


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Linia 162: Linia 157:
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.


====\sc Powtarzane====
====Powtarzane====


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Linia 173: Linia 168:
jest poleceniem.  
jest poleceniem.  


====\sc Kombinowane====
====Kombinowane====


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Linia 190: Linia 185:
za "wejście" i "wyjście".  
za "wejście" i "wyjście".  


====\sc Wprowadzanie danych====
====Wprowadzanie danych====


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Linia 214: Linia 209:
w punkcie <math>\displaystyle t</math>.  
w punkcie <math>\displaystyle t</math>.  


====\sc Wyprowadzanie wyników====
====Wyprowadzanie wyników====


<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
Linia 238: Linia 233:


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
* wykorzystanie standardowych języków programowania (C, Fortran, być może ze wstawkami w asemblerze) oraz specjalizowanych bibliotek
wstawkami w asemblerze) oraz specjalizowanych 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
 
====Języki programowania: C i Fortran====
====Języki programowania: C i Fortran====


Linia 307: Linia 300:
for(i <nowiki>=</nowiki> 1; i <<nowiki>=</nowiki> N; i++)  
for(i <nowiki>=</nowiki> 1; i <<nowiki>=</nowiki> N; i++)  
x <nowiki>=</nowiki> x + 1/i;
x <nowiki>=</nowiki> x + 1/i;
printf("Wartość sumy x <nowiki>=</nowiki> 1 + 1/2 + ... + 1/
printf("Wartość sumy x <nowiki>=</nowiki> 1 + 1/2 + ... + 1/\%d jest równa
return(0);
return(0);
}
}
Linia 355: Linia 348:
for(i <nowiki>=</nowiki> 1; i <<nowiki>=</nowiki> N; i++)  
for(i <nowiki>=</nowiki> 1; i <<nowiki>=</nowiki> N; i++)  
x <nowiki>=</nowiki> x + 1.0/i;
x <nowiki>=</nowiki> x + 1.0/i;
printf("Wartość sumy x <nowiki>=</nowiki> 1 + 1/2 + ... + 1/
printf("Wartość sumy x <nowiki>=</nowiki> 1 + 1/2 + ... + 1/\%d jest równa
return(0);
return(0);
}
}
Linia 361: Linia 354:
   
   
W języku C mamy dostępnych sześć typów numerycznych:
W języku C mamy dostępnych sześć typów numerycznych:
* stałopozycyjne, 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,
** <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  
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
** <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>.
<math>\displaystyle -9.2\cdot 10^{18}\ldots +9.2\cdot 10^{18}</math>.
* zmiennopprzecinkowe, dla reprezentacji liczb rzeczywistych
* zmiennopozycyjne, dla reprezentacji liczb rzeczywistych
** <code>float</code>, pojedynczej precyzji, 32-bitowy, gwarantuje precyzję około <math>\displaystyle 10^{-7}</math> i zakres liczb reprezentowalnych w przybliżeniu <math>\displaystyle 10^{-38}\ldots 10^{38}</math>;
** <code>float</code>, pojedynczej precyzji, 32-bitowy, gwarantuje precyzję około <math>\displaystyle 10^{-7}</math> i
** <code>double</code>, podwójnej precyzji, 64-bitowy, ma precyzję na poziomie <math>\displaystyle 10^{-16}</math> przy orientacyjnym zakresie <math>\displaystyle 10^{-308}\ldots 10^{308}</math>;
zakres liczb reprezentowalnych w przybliżeniu <math>\displaystyle 10^{-38}\ldots 10^{38}</math>;
** <code>long double</code>, rozszerzonej podwójnej precyzji, na pecetach 80-bitowy, ale w pamięci zajmuje on 12 bajtów) o precyzji rzędu <math>\displaystyle 10^{-20}</math> i odpowiadający standardowi double extended IEEE 754.
** <code>double</code>, podwójnej precyzji, 64-bitowy, ma precyzję na poziomie <math>\displaystyle 10^{-16}</math> przy
orientacyjnym zakresie <math>\displaystyle 10^{-308}\ldots 10^{308}</math>;
** <code>long double</code>, rozszerzonej podwójnej precyzji, na pecetach 80-bitowy, ale w pamięci zajmuje
on 12 bajtów) o precyzji rzędu <math>\displaystyle 10^{-20}</math> i odpowiadający standardowi double
extended IEEE 754.
   
   
Powyższe typy zmiennoprzecinkowe w realizacji na PC odpowiadają
Powyższe typy zmiennoprzecinkowe w realizacji na PC odpowiadają
[[Uzupe�nij: standardowi IEEE 754]]. Szczegóły można wyczytać w podstawowej
[http://www.cs.berkeley.edu/&nbsp;wkahan/ieee754status/754story.html  standardowi IEEE 754].
literaturze numerycznej, np. . Standardowo, operacje arytmetyczne
Standardowo, operacje arytmetyczne
na obu typach <code>float</code> i <code>double</code> są tak samo pracochłonne, gdyż
na obu typach <code>float</code> i <code>double</code> są tak samo pracochłonne, gdyż
wszystkie obliczenia w C wykonywane są z maksymalną dostępną precyzją (czyli, na
wszystkie obliczenia w C wykonywane są z maksymalną dostępną precyzją (czyli, na
Linia 401: Linia 384:
funkcjami matematycznymi.  Podstawowe funkcje matematyczne (<math>\displaystyle \sin, \cos, \exp,
funkcjami matematycznymi.  Podstawowe funkcje matematyczne (<math>\displaystyle \sin, \cos, \exp,
\ldots</math>, itp.) nie są składnikami języka C, lecz w zamian są zaimplementowane w
\ldots</math>, itp.) nie są składnikami języka C, lecz w zamian są zaimplementowane w
tzw. standardowej bibliotece matematycznej \lstux!libm.a!; prototypy tych
tzw. standardowej bibliotece matematycznej <code>libm.a</code>; prototypy tych
funkcji oraz definicje rozmaitych stałych matematycznych: <math>\displaystyle \pi, e, \ldots</math> itp.
funkcji oraz definicje rozmaitych stałych matematycznych: <math>\displaystyle \pi, e, \ldots</math> itp.
znajdują się w pliku nagłówkowym \lstux!math.h!. Aby więc skorzystać z tych
znajdują się w pliku nagłówkowym <code>math.h</code>. Aby więc skorzystać z tych
funkcji w programie, należy   
funkcji w programie, należy   
* w nagłówku pliku, w którym korzystamy z funkcji lub stałych matematycznych,  
* w nagłówku pliku, w którym korzystamy z funkcji lub stałych matematycznych,  
umieścić linię <code>#include <math.h></code>
umieścić linię <code>#include <math.h></code>
* przy linkowaniu dołączyć bibliotekę matematyczną za pomocą opcji \lstux!-lm!
* przy linkowaniu dołączyć bibliotekę matematyczną za pomocą opcji <code>-lm</code>


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
Linia 434: Linia 417:
/* oczywiście, wystarczyłoby x <nowiki>=</nowiki>(2.0*M_PI*rand())/RAND_MAX; */
/* oczywiście, wystarczyłoby x <nowiki>=</nowiki>(2.0*M_PI*rand())/RAND_MAX; */
y <nowiki>=</nowiki> sin(x);
y <nowiki>=</nowiki> sin(x);
fprintf(stderr, "(}
fprintf(stderr, "(\%3d) x <nowiki>=</nowiki> \%10.5e sin(x) <nowiki>=</nowiki> \%10.5e\n", i, x, y);
}
return(0);
return(0);
}
}
Linia 474: Linia 458:
stworzenia prostego interfejsu do ówcześnie istniejących bibliotek numerycznych
stworzenia prostego interfejsu do ówcześnie istniejących bibliotek numerycznych
algebry liniowej: pakietów LINPACK  i EISPACK
algebry liniowej: pakietów LINPACK  i EISPACK
  (których był współautorem). Stworzone przez niego:
   
(których był współautorem). Stworzone przez niego:
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, zakładając wspólnie z J.&nbsp;Little'm firmę MathWorks
dzieło, zakładając wspólnie z J.&nbsp;Little'm firmę MathWorks
. Jej główny produkt nazwano MATLAB (od:
.  
Jej główny produkt nazwano [http://www.mathworks.com  MATLAB] (od:
"MATrix LABoratory").  
"MATrix LABoratory").  


Linia 500: Linia 486:
sukcesie MATLABa, John W.&nbsp;Eaton z Wydziału Inżynierii Chemicznej Uniwersytetu
sukcesie MATLABa, John W.&nbsp;Eaton z Wydziału Inżynierii Chemicznej Uniwersytetu
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, zob. ) oprogramowanie o funkcjonalności maksymalnie
licencji GPL) oprogramowanie o funkcjonalności maksymalnie
zbliżonej do MATLABa: Octave. Jak wyjaśnia twórca Octave,
zbliżonej do MATLABa: [http://www.octave.org  Octave]. Jak wyjaśnia twórca Octave,
, nazwa nie ma nic wspólnego z muzyką, ale z
, nazwa nie ma nic wspólnego z muzyką, ale z
pewną anegdotą.  Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest
pewną anegdotą.  Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest
Linia 508: Linia 494:
[[Image:MNscilab-screenshot.png|thumb|400px||Screenshot Scilaba]]
[[Image:MNscilab-screenshot.png|thumb|400px||Screenshot Scilaba]]


Drugim udanym klonem MATLABa jest francuski 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 autora niniejszych notatek, nie dorównuje on elegancji i
subiektywnej ocenie autora 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.
Linia 587: Linia 573:
====Pierwsze kroki w Octave====
====Pierwsze kroki w Octave====


Octave uruchamiamy poleceniem \lstux!octave!,  
Octave uruchamiamy poleceniem <code>octave</code>,


<div class="output" style="background-color:#e0e8e8; padding:1em"><pre>
<div class="output" style="background-color:#e0e8e8; padding:1em"><pre>
   
   
[przykry@bit MN]<math>\displaystyle  octave
[przykry@bit MN] octave
GNU Octave, version 2.9.5 (i686-redhat-linux-gnu).
GNU Octave, version 2.9.5 (i686-redhat-linux-gnu).
Copyright (C) 2006 John W. Eaton.
Copyright (C) 2006 John W. Eaton.
Linia 616: Linia 602:
   
   
octave:1> 1900/2000
octave:1> 1900/2000
ans = 0.95000
ans <nowiki>=</nowiki> 0.95000
octave:2> sin(pi)
octave:2> sin(pi)
ans =  1.2246e-16
ans <nowiki>=</nowiki> 1.2246e-16
octave:3> e^(i*pi)
octave:3> e^(i*pi)
ans = -1.0000e+00 + 1.2246e-16i
ans <nowiki>=</nowiki> -1.0000e+00 + 1.2246e-16i
</pre></div>
</pre></div>
   
   
Ostatnie dwa wyniki dają nam namacalny dowód, że obliczenia wykonywane są {\em
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
''numerycznie'', ze skończoną precyzją: w Octave niektóre tożsamości matematyczne
są spełnione {\em jedynie w przybliżeniu}, np. </math>\sin(\pi) \approx 0<math>\displaystyle  oraz
są spełnione ''jedynie w przybliżeniu'', np. <math>\displaystyle \sin(\pi) \approx 0</math> oraz
</math>e^{i\pi} \approx -1<math>\displaystyle . 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\footnote{Oczywiście, są to także wartości przybliżone!}:
stałymi matematycznymi (Oczywiście, są to także wartości przybliżone!):
\lstoct{e </math>\approx<math>\displaystyle  2.71828182845905}\lstoct{pi </math>\approx<math>\displaystyle  3.14159265358979}
<code>e <math>\displaystyle \approx</math> 2.71828182845905</code><code>pi <math>\displaystyle \approx</math> 3.14159265358979</code>
oraz jednostką urojoną \lstoct{i = </math>\sqrt{-1}<math>\displaystyle }
oraz jednostką urojoną <code>i</code> <math>\displaystyle = \sqrt{-1}</math>.


====Dokumentacja Octave====
====Dokumentacja Octave====
Linia 635: Linia 621:
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 \lstoct{help}.
użyciu polecenia <code>help</code>.


====Macierz: postawowy obiekt w Octave====
====Macierz: postawowy obiekt w Octave====


Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz
Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz
dwuwymiarowa </math>n\times m<math>\displaystyle , o elementach rzeczywistych lub zespolonych,
dwuwymiarowa <math>\displaystyle n\times m</math>, o elementach rzeczywistych lub zespolonych,
<center><math>\displaystyle
\begin{pmatrix}
a_{11} & \cdots & a_{1m}\\
\vdots &        & \vdots\\
a_{n1} & \cdots & a_{nm}
\end{pmatrix} .
</math></center>
</math></center>


\beginpmatrix
W szczególności, biorąc <math>\displaystyle m=1</math> albo <math>\displaystyle n=1</math>, dostajemy wektor (kolumnowy lub
a_{11} & \cdots & a_{1m}<br>
wierszowy
\vdots &        & \vdots<br>
a_{n1} & \cdots & a_{nm}
\endpmatrix .
<center><math>\displaystyle  
<center><math>\displaystyle  
W szczególności, biorąc </math>m<nowiki>=</nowiki>1<math>\displaystyle  albo </math>n<nowiki>=</nowiki>1<math>\displaystyle , dostajemy wektor (kolumnowy lub
\begin{pmatrix}
wierszowy
a_{1} \\
\vdots\\
a_{n}
\end{pmatrix} , \qquad\text{lub}\qquad
\begin{pmatrix}
a_{1} & \cdots & a_{m}
\end{pmatrix} .
</math></center>
</math></center>


\beginpmatrix
Z kolei dla <math>\displaystyle m=n=1</math> mamy do czynienia ze "zwykłymi" liczbami rzeczywistymi lub
a_{1} <br>
\vdots<br>
a_{n}
\endpmatrix , \qquad\text{lub}\qquad
\beginpmatrix
a_{1} & \cdots & a_{m}
\endpmatrix .
<center><math>\displaystyle
Z kolei dla </math>m<nowiki>=</nowiki>n<nowiki>=</nowiki>1<math>\displaystyle  mamy do czynienia ze ``zwykłymi'' liczbami rzeczywistymi lub
zespolonymi.
zespolonymi.


Linia 674: Linia 660:
<div class="output" style="background-color:#e0e8e8; padding:1em"><pre>
<div class="output" style="background-color:#e0e8e8; padding:1em"><pre>
   
   
octave:6> A = [2 -1 0; -1 3 -2; 2 2.71828 3.14]   
octave:6> A <nowiki>=</nowiki> [2 -1 0; -1 3 -2; 2 2.71828 3.14]   
A =
A <nowiki>=</nowiki>


   2.00000  -1.00000  0.00000
   2.00000  -1.00000  0.00000
Linia 683: Linia 669:
</pre></div>
</pre></div>
   
   
definiuje macierz kwadratową </math>3\times 3<math>\displaystyle  o elementach równych  
definiuje macierz kwadratową <math>\displaystyle 3\times 3</math> o elementach równych  
<center><math>\displaystyle
\begin{pmatrix}
2 & -1 & 0\\ -1 & 3 & -2\\ 2 & 2.71828 & 3.14
\end{pmatrix} .
</math></center>
</math></center>
\beginpmatrix
2 & -1 & 0<br> -1 & 3 & -2<br> 2 & 2.71828 & 3.14
\endpmatrix .
<center><math>\displaystyle


Tak
Tak
Linia 701: Linia 686:
<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) = 4
octave:3> B(1,1) <nowiki>=</nowiki> 4
B = 4
B <nowiki>=</nowiki> 4


octave:4> B(2,1) = 3 + B(1,1)
octave:4> B(2,1) <nowiki>=</nowiki> 3 + B(1,1)
B =
B <nowiki>=</nowiki>


   4
   4
   7
   7


octave:5> B(3,2) = 28
octave:5> B(3,2) <nowiki>=</nowiki> 28
B =
B <nowiki>=</nowiki>


   4  0
   4  0
Linia 718: Linia 703:
</pre></div>
</pre></div>
   
   
Pierwsza komenda określa \lstoct{B} jako macierz </math>1\times 1<math>\displaystyle , czyli zwykłą
Pierwsza komenda określa <code>B</code> jako macierz <math>\displaystyle 1\times 1</math>, czyli zwykłą
liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia --
liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia --
powoduje, że \lstoct{B} zostaje rozszerzony do macierzy </math>3\times 2<math>\displaystyle . Zauważmy, że
powoduje, że <code>B</code> zostaje rozszerzony do macierzy <math>\displaystyle 3\times 2</math>. Zauważmy, że
elementom \lstoct{B} o nieokreślonych przez nas wartościach zostaje przypisana domyślna
elementom <code>B</code> o nieokreślonych przez nas wartościach zostaje przypisana domyślna
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 {\em docelowego wymiaru} (u nas to była
początku wszystkie elementy macierzy ''docelowego wymiaru'' (u nas to była
macierz </math>3\times 2<math>\displaystyle ) 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 = zeros(3,2)
B <nowiki>=</nowiki> zeros(3,2)
B(1,1) = 3 + B(1,1)
B(1,1) <nowiki>=</nowiki> 3 + B(1,1)
B(2,1) = pi
B(2,1) <nowiki>=</nowiki> pi
B(3,2) = 28
B(3,2) <nowiki>=</nowiki> 28
</pre></div>
</pre></div>
   
   
Jeśli nie chcemy za każdym razem obserwować echa prowadzonych przez nas działań
Jeśli nie chcemy za każdym razem obserwować echa prowadzonych przez nas działań
na macierzy \lstoct{B}, komendy powinniśmy kończyć średnikiem: średnik po
na macierzy <code>B</code>, komendy powinniśmy kończyć średnikiem: średnik po
komendzie Octave blokuje echo wyniku jej wykonania. Tak więc, znacznie
komendzie Octave blokuje echo wyniku jej wykonania. Tak więc, znacznie
wygodniej będzie nam napisać
wygodniej będzie nam napisać
Linia 744: Linia 729:
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
B = zeros(3,2);
B <nowiki>=</nowiki> zeros(3,2);
B(1,1) = 3 + B(1,1);
B(1,1) <nowiki>=</nowiki> 3 + B(1,1);
B(2,1) = pi;
B(2,1) <nowiki>=</nowiki> pi;
B(3,2) = 28;
B(3,2) <nowiki>=</nowiki> 28;
B
B
</pre></div>
</pre></div>
   
   
Ostatnie polecenie -- \lstoct{B} {\em bez} średnika na końcu -- spowoduje
Ostatnie polecenie -- <code>B</code> ''bez'' średnika na końcu -- spowoduje
wypisanie zawartości \lstoct{B} na ekran.  
wypisanie zawartości <code>B</code> na ekran.  


Tak utworzoną macierz możemy zapisać do pliku, wykorzystując jeden z kilku  
Tak utworzoną macierz możemy zapisać do pliku, wykorzystując jeden z kilku  
Linia 801: Linia 786:
</pre></div>
</pre></div>
   
   
Sesję Octave kończymy poleceniem \lstoct{quit}.
Sesję Octave kończymy poleceniem <code>quit</code>.


====Wektoryzacja====
====Wektoryzacja====
Linia 807: Linia 792:
Ponieważ podstawowym obiektem w Octave jest macierz, predefiniowane operacje matematyczne
Ponieważ podstawowym obiektem w Octave jest macierz, predefiniowane operacje matematyczne
wykonują się od razu na całej macierzy.
wykonują się od razu na całej macierzy.
Bez żadnej  przesady możena stwierdzić, że umiejętność  {\em
Bez żadnej  przesady możena stwierdzić, że umiejętność  ''wektoryzacji'' i ''blokowania'' algorytmów jest podstawą pisania efektywnych
wektoryzacji} i {\em blokowania} algorytmów jest podstawą pisania efektywnych
implementacji algorytmów w Octave.
implementacji algorytmów w Octave.


Linia 817: Linia 801:
zadanie można wykonać korzystając z operatorów lub funkcji macierzowych.
zadanie można wykonać korzystając z operatorów lub funkcji macierzowych.


\begintabular {||l||m{0.65\columnwidth}|m{0.25\columnwidth}||}
{| border=1
\hline
|+ <span style="font-variant:small-caps"> </span>
& Tradycyjny kod w Octave, używający pętli & Efektywny kod wektorowy (macierzowy) w Octave\\
|-
\hline
|  
\hline
|| Tradycyjny kod w Octave, używający pętli ||  Efektywny kod wektorowy (macierzowy) w Octave
1 &
|-
|
 
1 ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
s = 0;
s <nowiki>=</nowiki> 0;
for i = 1:size(x,1)
for i <nowiki>=</nowiki> 1:size(x,1)
s = s + abs(x(i));
s <nowiki>=</nowiki> s + abs(x(i));
end
end
</pre></div>
</pre></div>
   
   
&
||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
s = sum(abs(x));
s <nowiki>=</nowiki> sum(abs(x));
</pre></div>
</pre></div>
  \\
   
|-
|


\hline
2 ||
2 &
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
N = 500; h = (b-a)/(N-1);
N <nowiki>=</nowiki> 500; h <nowiki>=</nowiki> (b-a)/(N-1);
for i = 1:N
for i <nowiki>=</nowiki> 1:N
x(i) = a + (i-1)*h;
x(i) <nowiki>=</nowiki> a + (i-1)*h;
y(i) = sin(x(i));
y(i) <nowiki>=</nowiki> sin(x(i));
end
end
plot(x,y);
plot(x,y);
</pre></div>
</pre></div>
   
   
&
||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
N = 500;
N <nowiki>=</nowiki> 500;
x = linspace(a,b,N);
x <nowiki>=</nowiki> linspace(a,b,N);
y = sin(x);
y <nowiki>=</nowiki> sin(x);
plot(x,y);
plot(x,y);
</pre></div>
</pre></div>
  \\
   
 
|-
\hline
|
3a &
3a ||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
for i = 1:size(C,1),
for i <nowiki>=</nowiki> 1:size(C,1),
for j = 1:size(C,2),
for j <nowiki>=</nowiki> 1:size(C,2),
for k = 1:size(A,2),
for k <nowiki>=</nowiki> 1:size(A,2),
C(i,j) = C(i,j) + A(i,k)*B(k,j);
C(i,j) <nowiki>=</nowiki> C(i,j) + A(i,k)*B(k,j);
end
end
end
end
Linia 873: Linia 861:
</pre></div>
</pre></div>
   
   
&
||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
C = C + A*B;
C <nowiki>=</nowiki> C + A*B;
</pre></div>
</pre></div>
  \\
   
|-
|


\hline
3b ||
3b &
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
for i = 1:size(C,1),
for i <nowiki>=</nowiki> 1:size(C,1),
for j = 1:size(C,2),
for j <nowiki>=</nowiki> 1:size(C,2),
C(i,j) = C(i,j) + A(i,:)*B(:,j);
C(i,j) <nowiki>=</nowiki> C(i,j) + A(i,:)*B(:,j);
end
end
end
end
</pre></div>
</pre></div>
   
   
&
||
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
<div class="code" style="background-color:#e8e8e8; padding:1em"><pre>
   
   
C = C + A*B;
C <nowiki>=</nowiki> C + A*B;
</pre></div>
</pre></div>
  \\
   
|-
|


\hline
|}
\endtabular


Zwróćmy uwagę na to, że kod wektorowy Octave oraz, w jeszcze większym stopniu,  
Zwróćmy uwagę na to, że kod wektorowy Octave oraz, w jeszcze większym stopniu,  
Linia 906: Linia 896:


Pierwszy przykład pokazuje też, że kod macierzowy jest elastyczniejszy: albowiem
Pierwszy przykład pokazuje też, że kod macierzowy jest elastyczniejszy: albowiem
obliczy sumę modułów wszystkich elementów \lstoct{x} nie tylko gdy \lstoct{x}
obliczy sumę modułów wszystkich elementów <code>x</code> nie tylko gdy <code>x</code>
jest wektorem (co musieliśmy milcząco założyć w kodzie w lewej kolumnie), ale
jest wektorem (co musieliśmy milcząco założyć w kodzie w lewej kolumnie), ale
tak samo dobrze zadziała, gdy \lstoct{x} jest macierzą!
tak samo dobrze zadziała, gdy <code>x</code> jest macierzą!


Szczególnie wiele na czytelności i wygodzie zyskujemy w drugim przykładzie,
Szczególnie wiele na czytelności i wygodzie zyskujemy w drugim przykładzie,
gdzie najpierw funkcja \lstoct{linspace} pozwala nam uniknąć żmudnego
gdzie najpierw funkcja <code>linspace</code> pozwala nam uniknąć żmudnego
wyznaczania </math>N<math>\displaystyle  równoodległych węzłów </math>x_i<math>\displaystyle  w odcinku </math>[a,b]<math>\displaystyle , a następnie
wyznaczania <math>\displaystyle N</math> równoodległych węzłów <math>\displaystyle x_i</math> w odcinku <math>\displaystyle [a,b]</math>, a następnie
funkcja \lstoct{sin} zaaplikowana do całego wektora </math>x<math>\displaystyle  daje wartość sinusa w
funkcja <code>sin</code> zaaplikowana do całego wektora <math>\displaystyle x</math> daje wartość sinusa w
zadanych przez nas węzłach.
zadanych przez nas węzłach.


Linia 920: Linia 910:
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 trzeci w wersji z potrójną pętlą
operacji mnożenia dwóch macierzy. Przykład trzeci w wersji z potrójną pętlą
\lstoct{for} 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 czwarty zdaje się być napisany odrobinę w duchu wektorowym
natomiast przykład czwarty 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>i<math>\displaystyle -tego wiersza </math>A<math>\displaystyle  i </math>j<math>\displaystyle -tej kolumny </math>B<math>\displaystyle ). 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>64\times 64<math>\displaystyle
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 \lstoct{C(i,j)=C(i,j)+A(i,k)*B(k,j)} uzyskano czas
* Dla pętli postaci <code>C(i,j)<nowiki>=</nowiki>C(i,j)+A(i,k)*B(k,j)</code> uzyskano czas
21.6s,
21.6s,
* Dla pętli postaci  \lstoct{C(i,j)=C(i,j)+A(i,:)*B(:,j)} --- 0.371s
* Dla pętli postaci  <code>C(i,j)<nowiki>=</nowiki>C(i,j)+A(i,:)*B(:,j)</code> --- 0.371s
* Dla pętli postaci \lstoct{C=C+A*B} kod działał jedynie 0.00288s!
* Dla pętli postaci <code>C<nowiki>=</nowiki>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
\lstoct{C = C + A*B}. Po wektoryzacji wewnętrznej pętli, program doznaje
<code>C <nowiki>=</nowiki> 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!</math></pre>
wolniejszy od kodu macierzowego!

Wersja z 18:37, 29 sie 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 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 celem 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.

Uwaga

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, ale przy uwzględnieniu pozostałych;
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 inne okazje.

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 = <math>\displaystyle {\cal W}</math>;

gdzie z jest zmienną, a 𝒲 jest wyrażeniem o wartościach tego samego typu co z. 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
xx, (x,y)x+y,

(x,y)xy, (x,y)x*y, (x,y)x//y,y0, gdzie x,y są stałymi lub zmiennymi liczbowymi,

arytmetyczno--logiczne
(x,y)x<y,

(x,y)xy, (x,y)x=y, (x,y)xy, gdzie x,y są stałymi lub zmiennymi liczbowymi,

logiczno--logiczne
p~p,

(p,q)pandq, (p,q)porq, gdzie p,q są stałymi lub zmiennymi logicznymi.

Dla niektórych zadań wygodnie jest (a czasem koniecznie) uzupełnić ten zbiór o dodatkowe operacje, takie jak obliczanie wartości niektórych standardowych funkcji matematycznych (,cos(),sin(),exp(),log(), 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.

Uwaga

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 (,cos(),sin(),exp(),log(), 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 (ani 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 𝒜1 i 𝒜2 są poleceniami, przy czym dopuszczamy polecenia puste.

Powtarzane

 
while(<math>\displaystyle {\cal W}</math>)
	<math>\displaystyle {\cal A}</math>;

gdzie W 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 𝒜j 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 x jest zmienną rzeczywistą, a t "adresem" pewnego funkcjonału Parser nie mógł rozpoznać (nieznana funkcja „\toR”): {\displaystyle \displaystyle L:F\toR} należącym to pewnego zbioru T. W wyniku wykonania tego polecenia w zmiennej x zostaje umieszczona wartość Lt(f).

Polecenie to pozwala zdobyć informację o danej f. Jeśli F=Rn to zwykle mamy T={1,2,,n} i Li(f)=fi, co w praktyce odpowiada wczytaniu i-tej współrzędnej wektora danych. W szczególności, ciąg poleceń 𝒩(x[i],i), i=1,2,,n, pozwala uzyskać pełną informację o f. Jeśli zaś F jest pewną klasą funkcji Parser nie mógł rozpoznać (nieznana funkcja „\toR”): {\displaystyle \displaystyle f:[a,b]\toR} , to możemy mieć np. T=[a,b] i Lt(f)=f(t). W tym przypadku, wykonanie polecenia 𝒩(x,t) odpowiada w praktyce skorzystaniu ze specjalnej procedury (albo urządzenia zewnętrznego) obliczającej (mierzącego) wartość funkcji f w punkcie t.

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 puste), 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 specjalizowanych bibliotek
  • użycie gotowego środowiska obliczeń numerycznych będącego wygodnym interfejsem do specjalizowanych bibliotek numerycznych

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 programowania opartymi na prostych konstrukcjach 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: Java, Pascal (ten język, zdaje się, jest popularny już tylko w obrębie wydziału MIM UW...), VisualBasic nie są zbyt odpowiednie dla obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala: real nie jest zgodny z powszechnym standardem 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.

Prosty program numeryczny

Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) N-tą sumę częściową szeregu harmonicznego

x=i=1N1i.

Przyjmijmy, że parametr N 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 = 0.0;
for(i = 1; i <= N; i++) 
	x = x + 1/i;
printf("Wartość sumy x = 1 + 1/2 + ... + 1/\%d jest równa 
return(0);
}

Sęk w tym, że ten program nie działa! To znaczy, kompiluje się i uruchamia, ale twierdzi uparcie, że nasza suma wynosi... 1.

Winę za to ponosi linijka

 
	x = 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 i>1, 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 = x + 1.0/i;

albo bardziej uniwersalnie, rzutując choć jedną z liczb na odpowiedni typ:

 
	x = 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 = 0.0;
for(i = 1; i <= N; i++) 
	x = x + 1.0/i;
printf("Wartość sumy x = 1 + 1/2 + ... + 1/\%d jest równa 
return(0);
}

W języku C mamy dostępnych sześć typów numerycznych:

  • stałoprzecinkowe, dla reprezentacji liczb całkowitych
    • int oraz long int. W realizacji GCC na komputery klasy PC, oba typy: int i long int są identyczne (32-bitowe) i ich zakres wynosi około 2.1109+2.1109. Typ int 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. zakresem unsigned int będzie w przybliżeniu

0+4.2109.

    • long long int (64-bitowy) o zakresie w przybliżeniu 9.21018+9.21018.
  • zmiennopprzecinkowe, dla reprezentacji liczb rzeczywistych
    • float, pojedynczej precyzji, 32-bitowy, gwarantuje precyzję około 107 i zakres liczb reprezentowalnych w przybliżeniu 10381038;
    • double, podwójnej precyzji, 64-bitowy, ma precyzję na poziomie 1016 przy orientacyjnym zakresie 1030810308;
    • long double, rozszerzonej podwójnej precyzji, na pecetach 80-bitowy, ale w pamięci zajmuje on 12 bajtów) o precyzji rzędu 1020 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 (sin,cos,exp,, 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: π,e, 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 [0,2π].

 [{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 = 0; i < N; i++)
	{
		x = rand()/(double)RAND_MAX;
		x *= 2.0*M_PI;
		/* oczywiście, wystarczyłoby x =(2.0*M_PI*rand())/RAND_MAX; */
		y = sin(x);
		fprintf(stderr, "(\%3d) x = \%10.5e sin(x) = \%10.5e\n", i, x, y);
	}
	return(0);
}

Zwróćmy uwagę na linię x = 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 [0,1]. Dla prawidłowego uzyskania losowej liczby z przedziału [0,1] kluczowe jest jednak zrzutowanie jednej z dzielonych liczb na typ double! Gdyby tego nie zrobić, uzyskalibyśmy zawsze x=0 lub sporadycznie x=1, zgodnie z regułą C typ wyniku jest zgodny z typem argumentów. Rzeczywiście, w naszym wypadku, (błędna) linia x = 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 x, 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

Typowa sesja MATLABa. Zwróć uwagę na edytor kodu źródłowego na bieżąco interpretujący go i wychwytujący potencjalne błędy

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, zakładając wspólnie z J. Little'm firmę MathWorks . Jej główny produkt nazwano 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.

Screenshot Octave. W terminalu otwarta sesja w ascetycznym trybie tekstowym, grafika wyświetlana z wykorzystaniem Gnuplota

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. Jak wyjaśnia twórca Octave, , nazwa nie ma nic wspólnego z muzyką, ale z pewną anegdotą. Wersja 1.0 pakietu Octave ukazała się w 1996 roku i jest intensywnie rozwijana do dziś.

Screenshot Scilaba

Drugim udanym klonem MATLABa jest francuski Scilab , opracowany w laboratoriach INRIA i wciąż doskonalony. W subiektywnej ocenie autora 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ę.

Porównanie MATLABa, Octave i Scilaba

Korzystając 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żynierkich, 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.

Wybór pomiędzy darmowymi klonami MATLABa: Octave i Scilabem, jest właściwie kwestią osobistych preferencji. W ninijeszym kursie będziemy posłygiwać 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: przykładowo, w Octave zarówno 'Newton' jak i "Newton" oznacza łańcuch znakowy (string) --- w MATLABie możliwa jest tylko pierwsza wersja; w Octave możemy pisać pętle for ... endfor jak i for ... end --- w MATLABie możliwa jest tylko druga postać; w Octave możemy pisać zarówno z = z+5 jak i, wzorem języka C, z += 5 --- MATLAB nie dopuszcza drugiego z wariantów; znakiem komentarza w Octave może być # oraz używany w tym celu MATLABie znak definiowanie funkcji w trybie interaktywnym, natomiast funkcje MATLABa muszą być zapisane w zewnętrznym pliku tekstowym.

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.

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 = 0.95000
octave:2> sin(pi)
ans =  1.2246e-16
octave:3> e^(i*pi)
ans = -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. sin(π)0 oraz eiπ1. 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 =1.

Dokumentacja Octave

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.

Macierz: postawowy obiekt w Octave

Podstawowym obiektem, z jakim mamy do czynienia w Octave, jest macierz dwuwymiarowa n×m, o elementach rzeczywistych lub zespolonych,

(a11a1man1anm).

W szczególności, biorąc m=1 albo n=1, dostajemy wektor (kolumnowy lub wierszowy

(a1an),lub(a1am).

Z kolei dla m=n=1 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 = [2 -1 0; -1 3 -2; 2 2.71828 3.14]  
A =

   2.00000  -1.00000   0.00000
  -1.00000   3.00000  -2.00000
   2.00000   2.71828   3.14000

definiuje macierz kwadratową 3×3 o elementach równych

(21013222.718283.14).

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) = 4
B = 4

octave:4> B(2,1) = 3 + B(1,1)
B =

  4
  7

octave:5> B(3,2) = 28
B =

   4   0
   7   0
   0  28

Pierwsza komenda określa B jako macierz 1×1, czyli zwykłą liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia -- powoduje, że B zostaje rozszerzony do macierzy 3×2. Zauważmy, że elementom B o nieokreślonych przez nas wartościach zostaje przypisana domyślna wartość zero.

W przypadku, gdy wypełniana macierz jest duża, znacznie korzystniej jest prealokować macierz, ustawiając na samym początku wszystkie elementy macierzy docelowego wymiaru (u nas to była macierz 3×2) na zero; potem możemy już działać jak poprzednio:

 
B = zeros(3,2)
B(1,1) = 3 + B(1,1)
B(2,1) = pi
B(3,2) = 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 = zeros(3,2);
B(1,1) = 3 + B(1,1);
B(2,1) = pi;
B(3,2) = 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
Zapis do formatu ASCII. Ze względu na późniejszą konwersję z zapisu w

postaci dzisiętnej do dwójkowej, zob. \link{xxx}, 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
binarny
Po prostu zrzut stanu pamięci
 
save -binary  macierzb.dat B
HDF5
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.

Wektoryzacja

Ponieważ podstawowym obiektem w Octave jest macierz, 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

1 ||

 
s = 0;
for i = 1:size(x,1)
	s = s + abs(x(i));
end
 
s = sum(abs(x));

2 ||

 
N = 500; h = (b-a)/(N-1);
for i = 1:N
	x(i) = a + (i-1)*h;
	y(i) = sin(x(i));
end
plot(x,y);
 
N = 500;
x = linspace(a,b,N);
y = sin(x);
plot(x,y);

3a ||

 
for i = 1:size(C,1),
	for j = 1:size(C,2),
		for k = 1:size(A,2),
			C(i,j) = C(i,j) + A(i,k)*B(k,j);
		end
	end
end
 
C = C + A*B;

3b ||

 
for i = 1:size(C,1),
	for j = 1:size(C,2),
		C(i,j) = C(i,j) + A(i,:)*B(:,j);
	end
end
 
C = 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 N równoodległych węzłów xi w odcinku [a,b], a następnie funkcja sin zaaplikowana do całego wektora x 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: trzeci i czwarty, 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 trzeci w wersji z potrójną pętlą for naśladuje sposób programowania znany nam z C lub Pascala, natomiast przykład czwarty zdaje się być napisany odrobinę w duchu wektorowym (brak trzeciej, wewnętrznej pętli, zastąpionej operacją wektorową: iloczynem skalarnym i-tego wiersza A i j-tej kolumny B). Poniżej porównanie czasów działania tych trzech implementacji w przypadku macierzy 64×64 (czasy dla PC z procesorem Celeron 1GHz):

  • Dla pętli postaci C(i,j)=C(i,j)+A(i,k)*B(k,j) uzyskano czas

21.6s,

  • Dla pętli postaci C(i,j)=C(i,j)+A(i,:)*B(:,j) --- 0.371s
  • Dla pętli postaci C=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 = C + A*B. Po wektoryzacji wewnętrznej pętli, program doznaje kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie wolniejszy od kodu macierzowego!