Przykre testy: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przykry (dyskusja | edycje)
mNie podano opisu zmian
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 35 wersji utworzonych przez 2 użytkowników)
Linia 1: Linia 1:
<!--  
<!--  
Konwertowane  z pliku LaTeX przez latex2mediawiki, zob. http://www.ii.uj.edu.pl/&nbsp;pawlik1/latex2mediawiki.php.
Konwertowane  z pliku LaTeX przez latex2mediawiki, zob. http://www.ii.uj.edu.pl/&nbsp;pawlik1/latex2mediawiki.php.
Linia 6: Linia 5:
-->
-->
   
   
=Wprowadzenie do metod numerycznych=
<div style="background-color:#fff; border: 1em #eee solid; padding:1em; margin-bottom:1em;">
[http://www.mimuw.edu.pl/~przykry/Zamawiane/przedmiot.pdf Notatki w formacie PDF]
[http://www.mimuw.edu.pl/~przykry/Zamawiane/przedmiot-beamer.pdf Slajdy w formacie PDF]
[http://usosweb.mimuw.edu.pl/kontroler.php?_action=actionx:katalog2/przedmioty/pokazPrzedmiot(prz_kod:1000-5D96MN) Strona przedmiotu w USOSie]
</div>
 
=Wielkie układy równań liniowych=
{{powrot |Metody numeryczne | do strony głównej
przedmiotu <strong>Metody numeryczne</strong>}}
 
[[MN08LAB | Ćwiczenia do wykładu >>>]]
 


<strong>Metody numeryczne</strong> to dziedzina wiedzy zajmująca się  
==Wstęp==
problemami obliczeniowymi i konstrukcją algorytmów rozwiązywania
Wraz z coraz większymi modelami pojawiającymi się w praktyce obliczeniowej,
zadań matematycznych. Najczęściej, zadania obliczeniowe postawione są w
coraz częściej zachodzi potrzeba rozwiązywania zadań algebry liniowej, w której
dziedzinie rzeczywistej (lub zespolonej) i dlatego mówimy o zadaniach
macierze są co prawda wielkiego wymiaru, ale najczęściej <strong>rozrzedzone</strong>, to
obliczeniowych <strong>matematyki ciągłej</strong> (w odróżnieniu od
znaczy jest w nich bardzo dużo zer. Bardzo często zdarza się, że macierz
[[Matematyka_dyskretna|matematyki dyskretnej]]).
wymiaru <math>N</math> ma tylko <math>O(N)</math> niezerowych elementów. Wykorzytanie tej specyficznej
własności macierzy nie tylko prowadzi do algorytmów istotnie szybszych od ich
analogów dla macierzy gęstych (to znaczy takich, które (w założeniu) mają <math>N^2</math>
elementów), ale wręcz są jedynym sposobem na to, by niektóre zadania w ogóle
stały się rozwiązywalne przy obecnym stanie techniki obliczeniowej!


==Zadania metod numerycznych==
Jednym ze szczególnie ważnych źródeł układów równań z macierzami rozrzedzonymi są np. <strong>równania różniczkowe cząstkowe</strong> (a więc np. modele pogody, naprężeń w konstrukcji samochodu, przenikania kosmetyków do głębszych warstw skóry, itp.).


Aby w ogóle mówić w problemie obliczeniowym, musimy najpierw
Modele wielostanowych systemów kolejkowych (np. routera obsługującego wiele komputerów) także prowadzą do gigantycznych układów równań z macierzami rozrzedzonymi o specyficznej strukturze.
* określić <strong>dane problemu</strong> i <strong>cel obliczeń</strong>, czyli dokładnie
 
sformułować zadanie w języku matematyki,
Z reguły zadania liniowe ''wielkiego wymiaru'' będą miały strukturę macierzy rozrzedzonej, gdyż najczęściej związki pomiędzy niewiadomymi w równaniu nie dotyczą wszystkich, tylko wybranej grupy.
* 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ć
<strong>klasę rozpatrywanych danych</strong> oraz <strong>model obliczeniowy</strong> w obrębie
którego będą działać nasze algorytmy.
 
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
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.


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Uwaga</span>  
<span  style="font-variant:small-caps;">Przykład: Macierz z kolekcji Boeinga</span>  
<div class="solution" style="margin-left,margin-right:3em;">
<div class="solution" style="margin-left,margin-right:3em;">


Nasz przedmiot ma różne wcielenia i z tego powodu czasem nosi inne nazwy, w
Spójrzmy na macierz sztywności dla modelu silnika lotniczego, wygenerowaną swego czasu w zakładach Boeinga i pochodzącą z dyskretyzacji pewnego równania różniczkowego cząstkowego. Pochodzi z [http://www.cise.ufl.edu/research/sparse/matrices/Boeing  kolekcji Tima Davisa]. Jest to mała macierz, wymiaru 8032 (w kolekcji spotkasz równania z milionem i więcej niewiadomych).
zależności od tego, na jaki aspekt metod obliczeniowych jest położony największy
 
nacisk.
[[Image:MNsparseA.png|thumb|550px|center|Struktura niezerowych elementów macierzy bcsstk38.]]
* '''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.


Jej <strong>współczynnik wypełnienia</strong> (to znaczy, stosunek liczby niezerowych do wszystkich elementów macierzy) wynosi jedynie <math>0.006</math>, a więc macierz jest bardzo rozrzedzona: w każdym wierszu są średnio tylko 44 niezerowe elementy.
</div></div>
</div></div>


==Model obliczeniowy==
<!--


Tworząc i analizując algorytmy, jakie będą pojawiać w naszym wykładzie, będziemy
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
posługiwać się pewnym uproszczonym modelem obliczeń, dzięki czemu będziemy mogli
<span  style="font-variant:small-caps;">Przykład: Macierz MBEACXC</span>  
skoncentrować się na <strong>esencji</strong> algorytmu, bez detali implementacyjnych ---
<div class="solution" style="margin-left,margin-right:3em;">
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ę
Dane pochodzą z serwisu
pojęciem <strong>programu</strong>. Zastosujemy przy tym notację
[http://math.nist.gov/MatrixMarket  MatrixMarket]. Jest to niezbyt duża macierz, wymiaru <math>496 \times 496</math>, niesymetryczna, o elementach rzeczywistych. Źródłem tej macierzy jest pewien model ekonomiczny.
podobną do tej z języka programowania <strong>C</strong>.


Program składa się z <strong>deklaracji</strong>, czyli opisu obiektów,
[[Image:MNmbeacxc|thumb|550px|center|Struktura niezerowych elementów macierzy MBEACXC]]
których będziemy używać, oraz z <strong>poleceń</strong> (<strong>instrukcji</strong>),
czyli opisu akcji, które będziemy wykonywać.


Dostępnymi obiektami są <strong>stałe</strong> i <strong>zmienne</strong> typu
Tylko pozornie macierz ta wydaje się dość gęsta, w rzeczywistości jej współczynnik wypełnienia wynosi około 20 procent.
całkowitego (<code>int</code>),  
</div></div>
rzeczywistego (<code>float</code> i <code>double</code>). Typ logiczny symulujemy tak jak w
 
C wartościami zero-jedynkowymi typu całkowitego.
-->
   
   
Zmienne jednego typu mogą być grupowane w wektory albo tablice.
==Reprezentacja macierzy rzadkich==


Widzimy więc, że podstawowe algorytmy numeryczne będą bazować na mało
Zacznijmy od sposobu przechowywania macierzy rozrzedzonych. Naturalnie, nie ma
skomplikowanych typach danych. Również nieskomplikowane będą instrukcje naszego
sensu przechowywać wszystkich <strong>zerowych</strong> jej elementów: wystarczy
modelowego języka. Dążenie do prostoty (ale nie za wszelką cenę) jest charakterystyczne dla numeryki. Typowe jest zastępowanie skomplikowanych tablic rekordów prostszymi macierzami, eleganckich rekurencji --- zwykłymi pętlami działającymi na danych w miejscu. Jedną z myśli przewodnich numeryki jest bowiem <strong>szybkość</strong>, a rezygnacja z barokowych konstrukcji językowych jest tu analogiczna do sposobu konstrukcji architektury procesora RISC, pod hasłem: ''efektywność przez prostotę''.
ograniczyć się do zachowania tych niezerowych! W ten sposób zmniejszamy zarówno
wymagania pamięciowe, jak i liczbę operacji zmiennoprzecinkowych potrzebnych do
prowadzenia działań na macierzy (np. w przypadku mnożenia macierzy przez
wektor, nie będziemy mnożyć przez zera!).  


(Na pewno zastanawiasz się teraz, jaka jest ''druga'' myśl przewodnia numeryki. To <strong>dokładność</strong>.)
====Format współrzędnych====


===Podstawienie===
Do zapamiętania macierzy <math>A</math> wymiaru <math>N\times M</math> o <math>NNZ</math> niezerowych elementów wykorzystujemy trzy wektory: <code>I</code>,
<code>J</code> --- oba typu <code>int</code> --- oraz <code>V</code>, typu <code>double</code>,
wszystkie o długości <math>NNZ</math>, przy czym


Najprostsza z instrukcji, bez której nie można się obejść:
<center><math>
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>  
A_{I[k], J[k]} = V[k], \qquad\forall k = 0,\ldots, NNZ-1</math></center>
z = <math>\displaystyle {\cal W}</math>;
</nowiki></div>
   
   
Tutaj <math>\displaystyle z</math> jest zmienną, a <math>\displaystyle {\cal W}</math> jest <strong>wyrażeniem</strong>
[[Image:MNaij-matrix-format.png|thumb|550px|center|Format AIJ]]
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
skończonej liczby <strong>operacji elementarnych</strong> na wyrażeniach.
Operacje elementarne to:


; arytmetyczno--arytmetyczne:
W tym formacie wprowadzane są macierze rzadkie do Octave'a i MATLABa:
:  <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, y\ne 0</math>, gdzie <math>\displaystyle x,y</math> są stałymi lub
zmiennymi liczbowymi,


; arytmetyczno--logiczne:
<div style="margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><pre>A = sparse(I,J,V,N,N);
: <math>\displaystyle (x,y)\mapsto x<y</math>,
</pre></div>  
<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,
 
; logiczno--logiczne:
:  <math>\displaystyle p\mapsto\,\sim \,p</math>,
<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.
   
   
Dla niektórych zadań wygodnie jest (a czasem konieczne)
Jednak wewnętrznie przechowywane są w innym formacie, o którym [[#Format spakowanych kolumn (wierszy)|poniżej]].
uzupełnić ten zbiór o dodatkowe operacje, takie jak
obliczanie wartości niektórych standardowych funkcji matematycznych
(<math>\displaystyle \sqrt{\;}, \cos(), \sin(), \exp(), \log(),</math> 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.
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Uwaga</span>
<div class="solution" style="margin-left,margin-right:3em;">
 
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 <math>\displaystyle +,\,-,\,\times,\,\div</math>, 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 (<math>\displaystyle \sqrt{\;}, \cos(), \sin(),
\exp(), \log(),</math> itp.), jednak wykonanie takiej instrukcji wymaga mniej więcej
kilkadziesiąt razy więcej czasu
niż wykonanie operacji dodawania.
</div></div>


<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Uwaga</span>  
<span  style="font-variant:small-caps;">Przykład</span>  
<div class="solution" style="margin-left,margin-right:3em;">
<div class="solution" style="margin-left,margin-right:3em;">


Niestety, aby nasz  model obliczeniowy wiernie odpowiadał rzeczywistości, musimy
Pokażemy jak w Octave wprowadzić macierz rozrzedzoną.
w nim uwzględnić fakt, że działania matematyczne (i, tym bardziej,
obliczanie wartości funkcji matematycznych) <strong>nie są</strong> 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, <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
intuicji numerycznej, popartej doświadczeniem.
</div></div>


Mamy trzy podstawowe polecenia złożone: warunkowe, powtarzania i kombinowane.
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>octave:10> I = [1, 1, 1, 2, 3, 1, 4];
octave:11> J = [1, 3, 2, 2, 3, 4, 4];
octave:12> V = [1, 2, 3, 4, 5, 6, 7];
octave:13> N = 4;
octave:14> A = sparse(I,J,V,N,N)
A =


===Warunkowe===
Compressed Column Sparse (rows = 4, cols = 4, nnz = 7)


  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>  
  (1, 1) -> 1
if(<math>\displaystyle \cal W</math>)  
  (1, 2) ->  3
<math>\displaystyle {\cal A}_1</math>;
  (2, 2) ->  4
else
  (1, 3) ->  2
<math>\displaystyle {\cal A}_2</math>;
  (3, 3) ->  5
  (1, 4) -> 6
  (4, 4) -> 7
 
octave:15> spy(A);
</nowiki></div>
</nowiki></div>
   
   
gdzie <math>\displaystyle {\cal W}</math> jest wyrażeniem o wartościach całkowitych (0 odpowiada
Strukturę jej niezerowych elementów ukaże nam polecenie <code style="color: #006">spy(A)</code>. Tak właśnie zostały wygenerowane obrazki macierzy w niniejszym wykładzie.
logicznemu fałszowi, inne wartości --- logicznej prawdzie), a <math>\displaystyle {\cal A}_1</math>
i <math>\displaystyle {\cal A}_2</math> są poleceniami, przy czym dopuszczamy polecenia puste.


===Powtarzane===


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>  
</div></div>
while(<math>\displaystyle {\cal W}</math>)
 
<math>\displaystyle {\cal A}</math>;
====Format spakowanych kolumn (wierszy)====
</nowiki></div>
 
Format współrzędnych nie narzucał żadnego uporządkowania elementów macierzy
gdzie <math>\displaystyle W</math> jest wyrażeniem o wartościach logicznych, a <math>\displaystyle \cal A</math>  
--- można było je umieszczać w dowolnej kolejności. Narzucenie sensownego
jest poleceniem.  
porządku mogłoby wspomóc realizację wybranych istotnych operacji na macierzy,
na przykład, aby wygodnie było realizować działanie (prawostronnego) mnożenia
macierzy przez wektor, dobrze byłoby przechowywać elementy macierzy ''wierszami''. Tak właśnie jest zorganizowany format spakowanych wierszy (''Compressed Sparse Row, CSR''). Analogicznie jest zdefiniowany format
spakowanych kolumn (''Compressed Sparse Column, CSC''), którym zajmiemy się bliżej.
 
Podobnie jak w przypadku formatu współrzędnych, macierz w formacie CSC jest
przechowywana w postaci trzech wektorów: <code>AV</code> jest wektorem typu
<code>double</code> o długości <math>NNZ</math>, zawierającym <strong>kolejne</strong> niezerowe elementy
macierzy <math>A</math> wpisywane <strong>kolumnami</strong>, <code>AI</code> jest wektorem typu <code>int</code>
o długości <math>NNZ</math>, zawierającym numery wierszy macierzy <math>A</math> odpowiadających
elementom z <code>AV</code>. Natomiast zamiast tablicy <code>J</code>, jak to było w
formacie współrzędnych, mamy krótszy wektor typu <code>int</code>, <code>AP</code>, o
długości <math>M</math>, zawierający na <math>j</math>-tym miejscu indeks pozycji w <code>AV</code>, od
którego rozpoczynają się w <code>AV</code> elementy <math>j</math>-tej kolumny macierzy <math>A</math>.
 
[[Image:MNcsc-matrix-format.png|thumb|550px|center|Format CSC]]


===Kombinowane===
Mamy więc zależność, przy założeniu, że <math>AP[0]=0</math>,
<center><math>
A_{AI[AP[j]+k],j+1} = AV[AP[j]+k], \qquad j = 0,\ldots,M-1, \, k =
0,\ldots,AP[j+1]-1</math></center>


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>
Taki (z drobnymi modyfikacjami) format macierzy wykorzystują np. pakiety ARPACK
{
i UMFPACK (a także, wewnętrznie, Octave i MATLAB).
<math>\displaystyle {\cal A}_1;\displaystyle {\cal A}_2;\displaystyle \ldots\displaystyle {\cal A}_n;</math>
}
</nowiki></div>
gdzie <math>\displaystyle {\cal A}_j</math> są poleceniami.  


Na podstawie tych trzech poleceń można tworzyć inne, takie
====Format diagonalny====
jak pętle <code>for()</code>, czy <code>switch()</code>, itd.


Mamy też dwa szczególne polecenia, które odpowiadają
Znacznie mniej uniwersalny niż poprzednie i dlatego rzadziej spotykany. Kolejne
za "wejście" i "wyjście".  
diagonale macierzy przechowujemy w kolejnych wierszach macierzy <math>P\times N</math>,
gdzie <math>P</math> jest liczbą niezerowych diagonal w <math>A\in R^{N\times N}</math>.


===Wprowadzanie danych===
Szczególnie wygodny do reprezentacji macierzy taśmowych. Wykorzystywany m.in.
przez funkcję LAPACKa <code style="color: #903">DGBSV</code> służącą rozwiązywaniu równań z macierzami
taśmowymi.


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>
====Uwagi praktyczne====
<math>\displaystyle {\cal IN}</math>(x,t);
</nowiki></div>
gdzie <math>\displaystyle x</math> jest zmienną rzeczywistą, a <math>\displaystyle t</math> "adresem" pewnego
funkcjonału <math>\displaystyle L:F\toR</math> należącym to pewnego zbioru <math>\displaystyle T</math>.
W wyniku wykonania tego polecenia w zmiennej <math>\displaystyle x</math> zostaje
umieszczona wartość <math>\displaystyle L_t(f)</math>.


Polecenie to pozwala zdobyć <strong>informację</strong> o danej <math>\displaystyle f</math>.
Mnożenie macierzy w formacie AIJ przez wektor jest kilka razy wolniejsze w porównaniu do macierzy w formacie CSC/CSR (z tych dwóch oczywiście (dlaczego?) CSR jest najszybszy). Co więcej, okazuje się, że w typowych implementacjach, mnożenie macierzy rozrzedzonej (reprezentowanej np. w formacie CSC) przez wektor jest mało efektywne, mniej więcej na poziomie dziesięciu procent możliwości obliczeniowych procesora.  
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
współrzędnej wektora danych. W szczególności, ciąg poleceń
<math>\displaystyle {\cal IN}(x[i],i)</math>, <math>\displaystyle i=1,2,\ldots,n</math>, pozwala uzyskać pełną
informację o <math>\displaystyle f</math>. Jeśli zaś <math>\displaystyle F</math> jest pewną klasą
funkcji <math>\displaystyle f:[a,b]\toR</math>, to możemy mieć np. <math>\displaystyle T=[a,b]</math> i <math>\displaystyle L_t(f)=f(t)</math>.  
W tym przypadku, wykonanie polecenia <math>\displaystyle {\cal IN}(x,t)</math> odpowiada
w praktyce skorzystaniu ze specjalnej procedury (albo urządzenia
zewnętrznego) obliczającej (mierzącego) wartość funkcji <math>\displaystyle f</math>
w punkcie <math>\displaystyle t</math>.  


===Wyprowadzanie wyników===
Jeśli już ''poważnie'' myśleć o przyspieszeniu mnożenia macierzy przez wektor (np. gdy chcemy stosować [[#Stacjonarne metody iteracyjne|iteracyjną metodę rozwiązywania układu równań]] z tą macierzą), warto rozważyć [http://bebop.cs.berkeley.edu/pubs/vuduc2003-dissertation.pdf  inne formaty] --- łączące w sobie podział macierzy na bloki (tak, jak w algorytmach BLAS) i przechowywanie (w zasadzie) tylko niezerowych elementów macierzy.


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>
==Macierze specjalne==
<math>\displaystyle {\cal OUT}</math>(<math>\displaystyle {\cal W}</math>);
</nowiki></div>
gdzie <math>\displaystyle {\cal W}</math> 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
Zajmiemy się teraz zadaniem rozwiązywania układu równań liniowych
wszystkich zmiennych są nieokreślone, oraz że dla dowolnych
<center><math>
danych wykonanie programu wymaga wykonania skończonej liczby
Ax = b</math>,</center>
operacji elementarnych. Wynikiem obliczeń jest skończony ciąg
liczb rzeczywistych (albo <math>\displaystyle puste</math>), którego kolejne współrzędne
pokazywane są poleceniem <math>\displaystyle {\cal OUT}</math>.


==Środowisko obliczeniowe==
ale w sytuacji, gdy macierz <math>A</math> jest rozrzedzona i dużego wymiaru. Dokonamy przeglądu kilku rodzajów algorytmów mających na celu wykorzystanie rozrzedzenia macierzy dla obniżenia kosztu wyznaczenia rozwiązania układu.


Ze względu na swój utylitarny charakter, w metodach numerycznych niemal równie
Należy pamiętać, że z reguły najlepsze wyniki uzyskuje się, gdy konkretny
ważna jak dobór optymalnego algorytmu jest jego efektywna <strong>implementacja</strong> na
algorytm dobierze się do konkretnej macierzy. W zastosowaniach pojawiają się
konkretnej architekturze.
m.in. macierze rzadkie o bardzo szczególnej strukturze, dla nich warto stosować
wyspecjalizowane algorytmy.


W praktyce mamy dwie możliwości:
====Macierze taśmowe====
* 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====
Macierz <math>A</math> taka, że dla <math>|i-j| \geq d</math>, <math>a_{ij} = 0</math>, nazywamy macierzą taśmową
z rozstępem <math>d</math>, o szerokości pasma <math>2d+1</math>.


Programy numeryczne (a przynajmniej ich jądra obliczeniowe) są zazwyczaj niezbyt
Łatwo sprawdzić, że algorytm eliminacji Gaussa (bez wyboru elementu głównego)
wymagające jeśli chodzi o struktury danych, co więcej, prostota struktur danych
nie spowoduje dodatkowego wypełnienia w takiej macierzy (a więc da się wykonać w
szybko rewanżuje się efektywniejszym kodem. Dlatego, trawestując Einsteina, w
miejscu). W przypadku konieczności wyboru elementu głównego, pesymistyczne
dobrym programie numerycznym należy
oszacowanie rozstępu macierzy rozkładu LU jest równe <math>\max\{2d,N\}</math> --- tak
więc, dla niezbyt dużych <math>d</math> wciąż wynikowa macierz jest taśmowa. 


<blockquote  style="background-color: #fefeee; padding:1em;  margin-left,margin-right:2em;  margin-top,margin-bottom: 1em;">
W szczególności, gdy macierz jest taśmowa z pasmem o rozstępie <math>d</math> i jednocześnie
używać tak prostych struktur danych, jak to
diagonalnie dominująca, wtedy rozkład LU takiej macierzy da się wykonać w
możliwe (ale nie prostszych!...)
miejscu kosztem <math>O(d\,N)</math>, czyli liniowym.
</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
hegemonia Fortranu była nie do podważenia, o tyle teraz coraz więcej
oprogramowania numerycznego powstaje w C.


W naszym wykładzie wybieramy C ze względu na jego uniwersalność,
W [http://www.netlib.org/lapack  LAPACKu] zaimplementowano szybki ''solver'' równań z macierzami taśmowymi,
doskonałą przenośność i całkiem dojrzałe kompilatory. Dodajmy,
<code style="color: #903">DGBSV</code>, ale wymagający specjalnego sposobu
że funkcje w C można mieszać np. z gotowymi bibliotekami napisanymi w
przechowywania macierzy, wykorzystującego format diagonalny.
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: [[Programowanie_obiektowe|Java]], Pascal (ten język, zdaje się, jest popularny już
====Macierze trójdiagonalne====
tylko w obrębie [http://www.mimuw.edu.pl  wydziału MIM UW]...), VisualBasic i inne,  [http://www.cs.berkeley.edu/&nbsp;wkahan/JAVAhurt.pdf  nie są zbyt odpowiednie] dla
obliczeń numerycznych. Mało tego, np. podstawowy typ numeryczny Pascala:
<code>real</code> nie jest zgodny z powszechnym standardem [[MN03|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
Szczególnym przypadkiem macierzy taśmowych są macierze trójdiagonalne, tzn. taśmowe o
procesorów firmy [http://developer.intel.com  Intel] i [http://developer.amd.com  AMD], najczęściej spotykanej w obecnie używanych
rozstępie <math>d=1</math>:
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 [http://gcc.gnu.org  GCC], który jest omówiony w wykładzie [[Środowisko_programisty|Środowisko programisty]].


====Prosty program numeryczny w C====
<center><math>
A =  
\begin{pmatrix}
a_1 & c_1 &  &  & \\
b_2 & a_2 & c_2      &  & \\
  &  b_3 & a_3  & \ddots &  \\
  & & \ddots & \ddots  & c_{N-1}\\
  &  &      & b_N  & a_N
\end{pmatrix}
</math></center>


Napiszemy teraz program obliczający (w niezbyt wyrafinowany sposób) <math>\displaystyle N</math>-tą sumę
Zadanie rozwiązywania równań z taką macierzą
częściową szeregu harmonicznego
<center><math>
<center><math>\displaystyle
Ax = e
x = \sum_{i=1}^N \frac{1}{i}.
</math></center>
</math></center>


Przyjmijmy, że parametr <math>\displaystyle N</math> będzie miał wartość równą 2006. W pierwszym odruchu,
jest bardzo  często spotykane, więc warto przytoczyć algorytm w całej okazałości --- popularnie zwany <strong>algorytmem przeganiania</strong> (niektóre źródła nazywają go ''algorytmem Thomasa'').
prawie każdy początkujący student pisze program w rodzaju:


  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>
Zacznijmy jednak od stwierdzenia, kiedy macierz trójdiagonalna nie wymaga wyboru elementu głównego:
#include <stdio.h>
#define N 2006


int main(void)
{{stwierdzenie|||
Jeśli macierz <math>A</math> ma <strong>słabo dominującą diagonalę</strong>, tzn.
 
<center><math>|a_i|\,\ge\,|b_i|\,+\,|c_i|,\qquad 1\le i\le N</math>,</center>
 
(<math>b_1=0=c_N</math>) i przynajmniej dla jednego indeksu "<math>i</math>" mamy powyżej
ostrą nierówność "<math>></math>", to algorytm przeganiania jest wykonalny bez
przestawień wierszy. Ponadto wymaga on <math>7N</math> operacji arytmetycznych,
a więc jest prawie optymalny.
}}
 
{{algorytm|Metoda przeganiania|Metoda przeganiania|
<pre><math>d_1</math> = <math>a_1</math>;
<math>f_1</math> = <math>e_1</math>;
for (i = 2; i <= N; i++)
{
{
float x;
<math>l</math> = <math>b_i</math>/<math>a_{i-1}</math>;
unsigned int i;
<math>d_i</math> = <math>a_i</math> - <math>l</math> * <math>c_{i-1}</math>;
<math>f_i</math> = <math>e_i</math> - <math>l</math> * <math>f_{i-1}</math>;
}
<math>x_1</math> = <math>f_N</math>;
for (i = N-1; i >= 1; i--)
<math>x_i</math> = <math>f_i</math> - <math>c_i</math> * <math>x_{i+1}</math>;
</pre>}}


x = 0.0;
==Metody bezpośrednie==
for(i = 1; i <= N; i++)
x = x + 1/i;
printf("Wartość sumy x = 1 + 1/2 + ... + 1/&#37;d jest równa &#37;g\n",  N, x);


return(0);
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
}
<span style="font-variant:small-caps;">Przykład: Strzałka Wilkinsona</span>  
</nowiki></div>
<div class="solution" style="margin-left,margin-right:3em;">
   
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.


Winę za to ponosi linijka
Rozważmy układ równań z macierzą diagonalnie dominującą postaci
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>
x = x + 1/i;
</nowiki></div>
w której wykonujemy dzielenie <code>1/i</code>. Obie liczby są typu <code>int</code> 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 <math>\displaystyle i >
1</math>, po prostu zero.


Prawidłowy program musi więc wymusić potraktowanie choć jednej z tych liczb jako
<center><math>A = \begin{pmatrix}
liczby zmiennoprzecinkowej, co najprościej uzyskać albo przez
* & * & * & \cdots & * \\
* & * &        &  & \\
* &  & *      &  &  \\
\vdots & & & \ddots  & \\
* &  &      &  & *
\end{pmatrix}
</math></center>


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>  
gdzie <math>*</math> oznacza jakiś niezerowy element.
x = x + 1.0/i;
Łatwo sprawdzić, że chociaż wyjściowa macierz jest rozrzedzona, to zastosowanie do niej eliminacji Gaussa powoduje, że w wyniku
</nowiki></div>
dostajemy <strong>gęste</strong> czynniki rozkładu.
albo bardziej uniwersalnie, rzutując choć jedną z liczb na odpowiedni typ:


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>
Tymczasem wystarczy odwrócić kolejność równań i numerację niewiadomych (co dla
x = x + 1/((float) i);
macierzy jest równoznaczne z odwróceniem porządku wierszy i kolumn, korzystając
</nowiki></div>
z pewnej (jakiej?) macierzy permutacji <math>P</math>):
Poprawny kod miałby więc postać


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>  
<center><math>\widetilde{A} = PAP^T = \begin{pmatrix}
#include <stdio.h>
* &        &  & & *\\
#define N 2006
  &  \ddots  & & & \vdots\\
  &      & * &  & * \\
    &        &  &  * & * \\
* & \cdots & * & * & *
\end{pmatrix}
</math></center>


int main(void)
Wtedy okazuje się, że rozkład naszej macierzy nie powoduje już ''wypełnienia'' czynników rozkładu!
{
</div></div>
float x;
unsigned int i;


x = 0.0;
Właśnie na tym polega główny problem w rozwiązywaniu układów z macierzami
for(i = 1; i <= N; i++)
rzadkimi metodami bezpośrednimi: jak maksymalnie wykorzystać rozrzedzenie macierzy tak, by czynniki rozkładu były możliwie mało wypełnione. Albowiem wiedząc to będziemy mogli ograniczyć się jedynie do fizycznego wyznaczenia wartości <strong>niezerowych</strong> elementów macierzy rozkładu. Ponadto wymagania pamięciowe algorytmu nie będą istotnie wykraczać ponad ilość pamięci potrzebnej na przechowanie danych (tzn. macierzy).
x = x + 1.0/i;
printf("Wartość sumy x = 1 + 1/2 + ... + 1/&#37;d jest równa %g\n",  N, x);


return(0);
W ogólnym przypadku rozwiązanie takiego zadania jest trudne i większość
}
algorytmów opiera się na pewnych heurystykach, które jednak w praktyce warto wspomóc wcześniejszą analizą konkretnego układu równań jaki mamy rozwiązać. Najczęściej dąży się do takiego przenumerowania równań i niewiadomych, by w efekcie z góry przewidzieć, gdzie wystąpią zera w macierzach rozkładu --- i, by takich zer było jak najwięcej (by wypełnienie było jak najmniejsze)! Na architekturach z pamięcią hierarchiczną dąży się także do tego, by w trakcie rozkładu można było korzystać z BLAS Level 3, a więc permutacje wierszy i kolumn macierzy muszą to także brać pod uwagę.
</nowiki></div>
====Typy numeryczne w C====


W języku C mamy dostępnych sześć typów numerycznych:
Stosuje się kilka strategii wyznaczania korzystnych permutacji
* stałoprzecinkowe, dla reprezentacji liczb całkowitych
(''reorderingu''), z których warto wymienić
** <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>.
* przybliżone algorytmy minimalnego stopnia (''approximate minimum degree, AMD''), np. [http://www.cise.ufl.edu/research/sparse/amd AMD];
** <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>.
* techniki podziału grafów na (prawie) rozłączne składowe ''nested dissection, ND'', np. [http://glaros.dtc.umn.edu/gkhome/views/metis  METIS].
* zmiennopprzecinkowe, 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>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ą
Używają ich biblioteki takie jak [http://www.cise.ufl.edu/research/sparse/umfpack UMFPACK], czy [http://www.cse.clrc.ac.uk/nag/hsl HSL].
[http://www.cs.berkeley.edu/&nbsp;wkahan/ieee754status/754story.html standardowi IEEE 754]. Standardowo, operacje arytmetyczne 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 procesorach architektury IA-32 Intela i AMD: w precyzji oferowanej przez typ <code>long double</code>), a następnie dopiero wynik zapisywany do zmiennej reprezentowany jest w stosownym typie . Jednakże typ pojedynczej precyzji <code>float</code> oferuje znacznie większe możliwości 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 wykorzystania pamięci podręcznej (''cache'') i przetwarzania wektorowego.


====Stałe matematyczne i podstawowa biblioteka matematyczna====
W Octave mamy do dyspozycji także kilka procedur generujących takie permutacje, w tym: <code style="color: #006">colamd</code> (AMD dla macierzy niesymetrycznych) oraz <code style="color: #006">symamd</code> (AMD dla macierzy symetrycznych). Większy wybór oferuje MATLAB, jednak należy bezwzględnie pamiętać o jednym: nie ma uniwersalnej metody ''reorderingu'' i dla konkretnej macierzy może istnieć specjalna metoda, która da oszałamiające rezultaty, podczas gdy standardowe podejścia nie dadzą efektu.


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 (<math>\displaystyle \sin, \cos, \exp,
\ldots</math>, itp.) nie są składnikami języka C, lecz w zamian są zaimplementowane w
tzw. standardowej bibliotece matematycznej <code style="color: #666">libm.a</code>; prototypy tych
funkcji oraz definicje rozmaitych stałych matematycznych: <math>\displaystyle \pi, e, \ldots</math> itp.
znajdują się w pliku nagłówkowym <code style="color: #666">math.h</code>. 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ę <code>#include <math.h></code>
* przy linkowaniu dołączyć bibliotekę matematyczną za pomocą opcji <code style="color: #666">-lm</code>
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Przykład</span>  
<span  style="font-variant:small-caps;">Przykład</span>  
<div class="solution" style="margin-left,margin-right:3em;">
<div class="solution" style="margin-left,margin-right:3em;">


Oto przykładowy prosty program numeryczny w C; drukuje on tablicę wartości
Najprostszy sposób na wykorzystanie metody bezpośredniego rozwiązywania układu z macierzą rzadką to zastosowanie znanego nam operatora do macierzy typu rozrzedzonego:
sinusów losowo wybranych liczb z przedziału <math>\displaystyle [0,2\pi]</math>.


  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki> [{Tablica losowych sinusów}]
  <div style="margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><pre>A = sparse(...);
#include <math.h>
x = A \ b;
#include <stdio.h>  
</pre></div>  
#include <stdlib.h> /* zawiera definicję funkcji rand() i stałej RAND_MAX */
#define N 15 /* ile liczb wydrukować */
Octave domyślnie przyłoży do <math>A</math> reordering <code style="color: #006">colamd</code> i następnie skorzysta z biblioteki UMFPACK by rozwiązać taki układ. Dodatkowo, badane jest wcześniej, czy macierz nie jest przypadkiem taśmowa o wąskiej wstędze: jeśli jest, to korzysta się z LAPACKa.


int main(void)
(Jak widzisz, operator rozwiązywania układu równań jest mocno przeciążony: działa i na macierzach kwadratowych, i na prostokątnych, i na rozrzedzonych --- na każdym rodzaju w inny sposób. Nie dziwi Cię chyba, dlaczego wygodnie było napisać Octave w C++?)
{
 
int i;
</div></div>
double x,y;
 
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Przykład: Wypełnienie pewnej macierzy w zależności od użytego reorderingu</span>
<div class="solution" style="margin-left,margin-right:3em;">


for( i = 0; i < N; i++)
Rozważmy ponownie macierz pochodzącą z kolekcji [http://www.cise.ufl.edu/research/sparse/matrices/Boeing  Tima Davisa]. Jest to macierz symetryczna, dodatnio określona, wymiaru 8032, o 355460 elementach niezerowych i, w konsekwencji, o wypełnieniu około 0.6 procent.  
{
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, "(&#37;3d) x = &#37;10.5e sin(x) = &#37;10.5e\n", i, x, y);
}
return(0);
}
</nowiki></div>
Zwróćmy uwagę na linię <code>x = 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
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
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: "<strong>typ
wyniku jest zgodny z typem argumentów</strong>". Rzeczywiście, w naszym wypadku,
(błędna) linia <code>x = 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
<code>int</code>, to wynik ma być również typu <code>int</code> -- zostanie więc wykonane dzielenie
całkowite; ponieważ mamy <code>rand() <math>\displaystyle \leq</math> RAND_MAX</code>, to wynikiem będzie
albo 0, albo 1, i taki rezultat, po zamianie na typ <code>double</code>, zostanie przypisany
zmiennej <math>\displaystyle x</math>, co oczywiście nie jest naszym zamiarem. Natomiast, gdy
przynajmniej jedna z dzielonych liczb jest typu <code>double</code>, to oczywiście wynikiem
też musi być liczba typu <code>double</code>, zostanie więc wykonane zwyczajne dzielenie
dwóch liczb zmiennoprzecinkowych (wynik <code>rand()</code> automatycznie zostanie
zrzutowany na typ <code>double</code>).


Kompilujemy ten program, zgodnie z uwagami uczynionymi na początku, poleceniem
[[Image:MNsparseA.png|thumb|550px|center|Struktura niezerowych elementów macierzy.]]


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fcfcfc;"><nowiki>
Zobaczymy, jak w zależności od użytego algorytmu permutacji kolumn i
gcc -o sinusy sinusy.c -lm
wierszy poradzi sobie algorytm rozkładu Cholesky'ego.
</nowiki></div>
</div></div>


====Środowisko obliczeń numerycznych: MATLAB i jego klony====
[[Image:MNsparsechol.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> wykonanego
standardowym algorytmem. Czas rozkładu: 0.892013]]


[[Image:MNmatlab-screenshot.png|thumb|550px|center|Typowa sesja MATLABa. Zwróć uwagę na edytor kodu źródłowego na
[[Image:MNsparsecholcolamd.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z reorderingiem
bieżąco interpretujący go i wychwytujący potencjalne błędy]]
COLAMD. Czas rozkładu: 0.813038]]


W końcu lat siedemdziesiątych ubiegłego wieku, Cleve Moler wpadł na pomysł
[[Image:MNsparsecholsymamd.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z reorderingiem
stworzenia prostego interfejsu do ówcześnie istniejących bibliotek numerycznych
SYMAMD. Czas rozkładu: 0.487683s. Prawie dwa razy
algebry liniowej: pakietów LINPACK  i EISPACK
szybciej niż bez reorderingu, chociaż i tak wskutek wzrostu wypełnienia macierzy
w dolnym trójkącie mamy aż 2 procent niezerowych elementów.]]
(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ą [http://www.mathworks.com  MATLAB] (od:
"MATrix LABoratory").


MATLAB wkrótce rozrósł się potężnie, implementując (lub wykorzystując) wiele
[[Image:MNsparsecholsymrcm.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z odwrotnym reorderingiem
najlepszych z istniejących algorytmów numerycznych, a także oferując bogate
Cuthill-McKee. Czas rozkładu: 0.845928]]
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
[[Image:MNsparsecholcolperm.png|thumb|550px|center|Czynnik rozkładu Cholesky'ego <math>L</math> z jeszcze
efektywnych kodów w MATLABie jest maksymalne wykorzystanie gotowych
innym (bardzo tanim, ale jak widać czasem zupełnie złym) reorderingiem. Czas rozkładu: 5.947936]]
(prekompilowanych) funkcji MATLABa oraz --- zredukowanie do minimum
obecności takich struktur programistycznych jak pętle.  


[[Image:MNoctave-screenshot.png|thumb|550px|center|Screenshot Octave. W terminalu otwarta sesja w
Na zakończenie popatrzmy, jak ważne jest spostrzeżenie symetrii macierzy:
ascetycznym
trybie tekstowym, grafika wyświetlana z wykorzystaniem [http://www.gnuplot.info  Gnuplota]]]


Kierując się podobnymi przesłankami co C.&nbsp;Moler, oraz bazując na wielkim
[[Image:MNsparseLlu.png|thumb|550px|center|Rozkład LU, czynnik <math>L</math> (bez reorderingu). Czas rozkładu LU: 1.696018s. Nieakceptowalny, podobnie
sukcesie MATLABa, John W.&nbsp;Eaton z Wydziału Inżynierii Chemicznej Uniwersytetu
jak drastyczne wypełnienie macierzy.]]
Wisconsin w USA, zaczął w 1994 roku opracowywać darmowe (udostępniane na tzw.
licencji GPL) oprogramowanie o funkcjonalności maksymalnie
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ś.


[[Image:MNscilab-screenshot.png|thumb|550px|center|Screenshot Scilaba.]]
[[Image:MNsparseUlu.png|thumb|550px|center|Rozkład LU, czynnik <math>U</math> (bez reorderingu)]]


Drugim udanym klonem MATLABa jest francuski [http://www.scilab.org  Scilab],
Jak widać, w naszym przypadku standardowe algorytmy (COLAMD i SYMAMD) poradziły sobie całkiem nieźle, chociaż wypełnienie i tak znacząco wzrosło. Zapewne, dla tej macierzy, która jest specyficznego typu --- pochodzi z dyskretyzacji równania różniczkowego --- algorytm ND mógłby tu jeszcze lepiej zadziałać.
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:
</div></div>
* 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
==Stacjonarne metody iteracyjne==
--- acz profesjonalnych --- zewnętrznych bibliotekach numerycznych (BLAS, LAPACK, FFTW, itp.)


MATLAB jest wciąż niedościgniony jeśli chodzi o narzędzia programistyczne (np.
Gdy macierz jest rozrzedzona, mnożenie takiej macierzy przez wektor jest bardzo tanie (koszt jest proporcjonalny do liczby niezerowych elementów macierzy).
ma wbudowany --- działający <strong>na bieżąco</strong> w czasie edycji kodu źródłowego! --- debugger, a także kompilator funkcji JIT) oraz możliwości wizualizacji danych i wyników obliczeń. Jeśli zaniedbać koszt 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
Dlatego, jeśli możemy zadowolić się <strong>rozwiązaniem przybliżonym</strong> układu, a w zamian osiągnąć je tanim kosztem, warto rozważyć metody iteracyjne.
strony są często 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ą
Najprostsze metody iteracyjne (najprostsze w analizie i implementacji, ale ---
prekompilowane, dając tym samym znaczące przyspieszenie działania. Najnowsze wersje MATLABa (od wersji 6.5) wykorzystują zresztą domyślnie kompilację JIT --- w trakcie pisania kodu źródłowego funkcji.
jak można się domyślić --- w praktyce najmniej efektywne) polegają na rozkładzie
macierzy na część "łatwo odwracalną", <math>M</math>, i "resztę", <math>Z</math>. Dokładniej,
jeśli <math>M</math> jest nieosobliwa, to równanie <math>Ax = b</math> można zapisać jako
zadanie punktu stałego


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ę
<center><math>Mx = Nx + b</math>,</center>
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
gdzie <math>Z=M-A</math>. Inaczej:
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====
<center><math>x = M^{-1}(Zx + b)</math>,</center>


Octave uruchamiamy poleceniem <code style="color: #666">octave</code>,
i zastosować doń [[MN02#Iteracja prosta Banacha|metodę iteracji prostej Banacha]]:


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>  
<center><math>x_{k+1} = M^{-1}(Zx_k + b)</math></center>
[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.
Takie metody nazywamy stacjonarnymi metodami iteracyjnymi. Aby przeanalizować zbieżność takiej metody, warto rozpatrzyć przypadek ogólniejszy


Please contribute if you find this software useful.
<center><math>
For more information, visit http://www.octave.org/help-wanted.html
  x_k\,=\,B x_{k-1}\,+\, c</math>,</center>


Report bugs to <bug@octave.org> (but first, please read
dla pewnej macierzy <math>B</math>  oraz wektora <math>c</math>. (Dla stacjonarnej metody iteracyjnej, <math>B=M^{-1}Z</math> oraz <math>c=M^{-1}b</math>.)
http://www.octave.org/bugs.html to learn how to write a helpful report).


octave:1>
W  tym przypadku
</nowiki></div>
 
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:


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>
<center><math>x_k- x^*\,=\,B^k( x_0- x^*),  
octave:1> 1900/2000
</math></center>
ans = 0.95000
octave:2> sin(pi)
ans =  1.2246e-16
octave:3> e^(i*pi)
ans = -1.0000e+00 + 1.2246e-16i
</nowiki></div>
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 <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
stałymi matematycznymi (oczywiście, są to także wartości przybliżone!):
<code style="color: #006">e <math>\displaystyle \approx</math> 2.71828182845905</code>, <code style="color: #006">pi <math>\displaystyle \approx</math> 3.14159265358979</code>
oraz jednostką urojoną <code style="color: #006">i</code> <math>\displaystyle = \sqrt{-1}</math>.


Octave ma dobrą dokumentację, dostępną w trakcie sesji
a stąd i z nierówności <math>\|B^k\|\le\|B\|^k</math>, mamy
Octave; dla każdej funkcji, stałej itp.  można uzyskać jej dobry opis przy
użyciu polecenia <code style="color: #006">help</code>.


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


W szczególności, biorąc <math>\displaystyle m=1</math> albo <math>\displaystyle n=1</math>, dostajemy wektor (kolumnowy lub
Warunkiem dostatecznym zbieżności iteracji prostych
wierszowy
jest więc <math>\|B\|<1</math>. Okazuje się, że warunkiem koniecznym i dostatecznym
<center><math>\displaystyle
zbieżności tej iteracji dla dowolnego wektora startowego <math>x_0</math> jest
\begin{pmatrix}
 
a_{1} \\
<center><math>\rho = \max\{ |\lambda| : \lambda  \mbox{ jest wartością własną } B\} < 1</math></center>
\vdots\\
 
a_{n}
Tak więc, metody oparte na iteracji prostej będą zbieżne liniowo z ilorazem
\end{pmatrix} , \qquad\text{lub}\qquad
<math>\rho</math>.
\begin{pmatrix}
 
a_{1} & \cdots & a_{m}
Zaletą stacjonarnych metod iteracyjnych jest również ich prostota,
\end{pmatrix} .
przez co są one łatwe do zaprogramowania, co łatwo zobaczyć na przykładach metod: [[#Metoda Jacobiego|Jacobiego]] i [[#Metoda Gaussa-Seidela|Gaussa--Seidela]], które teraz omówimy.
 
====Metoda Jacobiego====
 
Biorąc <math>M =  \mbox{diag} (A)</math>, gdzie <math>\mbox{diag} (A)</math> jest macierzą diagonalną składającą się
z wyrazów stojących na głównej przekątnej macierzy
<math>A</math>, układ <math>A x= b</math> jest równoważny układowi
 
<center><math>M x = Z x +  b</math>,</center>
 
a stąd (o ile na przekątnej macierzy <math>A</math> nie mamy zera)
otrzymujemy metodę iteracyjną
 
<center><math>x_k\,=\,B x_{k-1}\,+\, c,
</math></center>
</math></center>


Z kolei dla <math>\displaystyle m=n=1</math> mamy do czynienia ze "zwykłymi" liczbami rzeczywistymi lub
gdzie <math>B=-M^{-1}Z</math> i <math>c=M^{-1} b</math>, zwaną
zespolonymi.
<strong>metodą Jacobiego</strong>.
 
Rozpisując ją po współrzędnych dostajemy (numer iteracji wyjątkowo zaznaczamy w postaci ''górnego'' indeksu) układ rozszczepionych równań:
 
<center><math>x^{(k)}_i = \frac{1}{a_{ii}} \left( b_i - \sum_{j\neq i} a_{ij}x^{(k-1)}_j \right)</math>,</center>
 
co znaczy dokładnie tyle, że w <math>i</math>-tym równaniu wyjściowego układu przyjmujemy za współrzędne <math>x</math> wartości z poprzedniej iteracji i na tej podstawie wyznaczamy wartość <math>x_i</math>.
 
{{twierdzenie|O zbieżności metody Jacobiego|O zbieżności metody Jacobiego|
 
W metodzie Jacobiego warunek dostateczny zbieżności,
<math>\|B\|<1</math>, jest spełniony np. wtedy, gdy macierz <math>A</math> ma
dominującą przekątną, tzn. gdy
 
<center><math>
  |a_{i,i}|\,>\,\sum_{j\neq i}|a_{i,j}|,\qquad \forall i = 1,\ldots, N</math></center>


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
{{dowod|||
kilka sposobów, w zależności od potrzeb. Można robić to interakcyjnie, podając
Rzeczywiście, ponieważ wyraz <math>(i,j)</math> macierzy <math>M^{-1}Z</math>
elementy macierzy z linii komend, na przykład:
wynosi <math>0</math> dla <math>i=j</math> oraz <math>a_{i,j}/a_{i,i}</math> dla <math>i\neq j</math>, a więc


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>
<center><math>\begin{align} \lefteqn{\|M^{-1}Z\|_\infty \;=\; \max_{1\le i\le N}
octave:6> A = [2 -1 0; -1 3 -2; 2 2.71828 3.14] 
  \sum_{j=1,j\ne i}^N{|a_{i,j}|}/{|a_{i,i}|} } \\
A =
    && =\;\max_{1\le i\le N}
      \sum_{j=1}^N{|a_{i,j}|}/{|a_{i,i}|}\,-\,1\;<\;1,
\end{align}</math></center>


  2.00000  -1.00000  0.00000
przy czym ostatnia nierówność wynika z warunku diagonalnej dominacji.  
  -1.00000  3.00000  -2.00000
}}
  2.00000  2.71828  3.14000


</nowiki></div>
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Przykład: Macierz laplasjanu</span>  
definiuje macierz kwadratową <math>\displaystyle 3\times 3</math> o elementach równych
<div class="solution" style="margin-left,margin-right:3em;">
<center><math>\displaystyle
 
\begin{pmatrix}  
Macierz <math>N\times N</math>, zwana <strong>macierzą jednowymiarowego laplasjanu</strong>
2 & -1 & 0\\ -1 & 3 & -2\\ 2 & 2.71828 & 3.14
<center><math>
\end{pmatrix} .
L = \begin{pmatrix}  
2 & -1 &       &  \\
-1 & 2 & \ddots & \\
    & \ddots & \ddots & -1 \\
    &      &  -1 & 2  
\end{pmatrix}  
</math></center>
</math></center>


Tak
pojawia się w bardzo wielu zastosowaniach, także jako podzadanie w algorytmach
więc, macierz w Octave definiujemy przez podanie jej elementów. Elementy
numerycznych. Ta macierz jest macierzą taśmową, symetryczną i dodatnio
wprowadzamy kolejno wierszami, elementy w wierszu oddzielamy spacjami (lub
określoną,  więc układ równań z tą macierzą można bez trudu rozwiązać metodami
ewentualnie przecinkami), natomiast kolejne wiersze oddzielamy średnikami.
bezpośrednimi, kosztem <math>O(N)</math>. Stosując do niej metodę Jacobiego mamy <math>M = 2I</math> oraz <math>Z = L - M</math>. Obliczając
normę macierzy iteracji Jacobiego dostajemy <math>||M^{-1}Z||_\infty
= 1</math>, co nie rozstrzyga jeszcze o jej zbieżności lub niezbieżności.


Macierz możemy także tworzyć stopniowo, wprowadzając jeden za drugim jej
Potrzebna będzie bardziej subtelna analiza, Okazuje się, że są znane wzory na wartości własne macierzy <math>L</math>:  
kolejne elementy:


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>  
<center><math>\lambda_j = 4\sin^2 \left(\frac{j \pi }{2(N+1)} \right)</math>,</center>
octave:3> B(1,1) = 4
B = 4


octave:4> B(2,1) = 3 + B(1,1)
dla <math>1 \leq j \leq N</math>.
B =


  4
W konsekwencji, wartościami własnymi <math>M^{-1}Z = \frac{1}{2}Z = \frac{1}{2}L -
  7
I</math> są liczby <math>\mu_i = \frac{1}{2} \lambda_i - 1</math>. Ponieważ <math>0 < \mu_i < 1</math>, znaczy to, że metoda Jacobiego jest zbieżna dla macierzy <math>L</math>.


octave:5> B(3,2) = 28
Z drugiej strony, nie dajmy się zwieść optymizmowi matematyka (''"nie martw się, jest zbieżny..."''): nietrudno sprawdzić, że <math>||Z||_2 = 1 - O(N^{-2}) < 1</math>, co oznacza, że metoda Jacobiego --- choć zbieżna --- dla dużych <math>N</math> staje się
B =
zbieżna tak wolno, że w praktyce bezużyteczna!
</div></div>


  4  0
====Metoda Gaussa-Seidela====
  7  0
 
  0  28
<!--
</nowiki></div>
Ciekawostką jest, że Gauss nie miał z nią nic wspólnego, a Seidel był podobno jej przeciwnikiem...
-->
   
   
Pierwsza komenda określa <code style="color: #006">B</code> jako macierz <math>\displaystyle 1\times 1</math>, czyli zwykłą
Heurystyka tej metody opiera się na zmodyfikowaniu metody Jacobiego tak, by w każdym momencie iteracji korzystać z najbardziej "aktualnych" współrzędnych przybliżenia rozwiązania <math>x</math>.
liczbę, druga -- rozszerza nasz obiekt do wektora, natomiast trzecia --
 
powoduje, że <code style="color: #006">B</code> zostaje rozszerzony do macierzy <math>\displaystyle 3\times 2</math>. Zauważmy, że
Rzeczywiście, przecież rozwiązując równanie metody Jacobiego:
elementom <code style="color: #006">B</code> o nieokreślonych przez nas wartościach zostaje przypisana domyślna
 
wartość zero.
<center><math>x^{(k)}_i = \frac{1}{a_{ii}} \left( b_i - \sum_{j\neq i} a_{ij}x^{(k-1)}_j \right)</math>,</center>


<nowiki>
nietrudno zauważyć, że w części sumy moglibyśmy odwoływać się do "dokładniejszych" wartości <math>x^{(k)}_j</math>: dla <math>j < i</math>, tzn.
W przypadku, gdy wypełniana macierz jest
duża, znacznie korzystniej jest prealokować macierz, ustawiając na samym
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:
</nowiki>


  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>  
<center><math>x^{(k)}_i = \frac{1}{a_{ii}} \left( b_i - \sum_{j< i} a_{ij}x^{(k)}_j - \sum_{j>i} a_{ij}x^{(k-1)}_j \right)</math></center>
B = zeros(3,2)
 
B(1,1) = 3 + B(1,1)
W języku rozkładu macierzy <math>A = M - Z</math> i iteracji <math>x_{k+1} = M^{-1}(Zx_k + b)</math> mamy więc <math>M = \mbox{tril} (A)</math> (dolny trójkąt macierzy <math>A</math>).
B(2,1) = pi
 
B(3,2) = 28
{{twierdzenie|O zbieżności metody Gaussa-Seidela|O zbieżności metody Gaussa-Seidela|
</nowiki></div>  
 
Jeśli macierz <math>A</math> jest diagonalnie dominująca, to metoda Gaussa--Seidela jest zbieżna dla dowolnego wektora startowego <math>x_0</math>.
Jeśli nie chcemy za każdym razem obserwować echa prowadzonych przez nas działań
}}
na macierzy <code style="color: #006">B</code>, komendy powinniśmy kończyć średnikiem: średnik po
 
komendzie Octave blokuje echo wyniku jej wykonania. Tak więc, znacznie
Metoda Gaussa--Seidela jest w wielu przypadkach rzeczywiście szybciej zbieżna od metody Jacobiego, np. tak jest w przypadku macierzy jednowymiarowego Laplasjanu. Wciąż jednak, dodajmy, dla zadań bardzo źle uwarunkowanych jej zbieżność jest zbyt wolna by ją stosować jako samodzielną metodę.
wygodniej będzie nam napisać
 
<div style="margin-top:1em; padding-top,padding-bottom:1em;">
<span  style="font-variant:small-caps;">Uwaga</span>  
<div class="solution" style="margin-left,margin-right:3em;">
 
Obie metody, Jacobiego i (zwłaszcza) Gaussa--Seidela stosuje się także czasem w prostych algorytmach rozwiązywania <strong>układów równań nieliniowych</strong>: ich zaletą jest to, że głównym składnikiem iteracji jest rozwiązywanie [[MN02|<strong>skalarnego</strong> równania nieliniowego]] na każdym kroku metody.
</div></div>
 
====Złożoność stacjonarnych metod iteracyjnych====
 
Zastanówmy się teraz nad złożonością metod iteracyjnych. Ponieważ możemy jedynie znaleźć pewne przybliżenie rozwiązania dokładnego <math>x^*</math>, przez złożoność metody będziemy rozumieli koszt kombinatoryczny obliczenia <math>x_k</math> z zadaną dokładnością <math>\epsilon>0</math>. Dla uproszczenia założymy, że medoda jest zbieżna liniowo z ilorazem <math>\rho</math>. Zauważmy, że aby zredukować błąd początkowy do  <math>\epsilon>0</math>, wystarczy wykonać <math>k=k(\epsilon)</math> iteracji, gdzie <math>k</math> spełnia
 
<center><math>\rho^k\| x_0- x^*\|\,\le\,\epsilon</math>,</center>


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
czyli
B = zeros(3,2);
B(1,1) = 3 + B(1,1);
B(2,1) = pi;
B(3,2) = 28;
B
</nowiki></div>
Ostatnie polecenie -- <code style="color: #006">B</code> <strong>bez</strong> średnika na końcu -- spowoduje
wypisanie zawartości <code style="color: #006">B</code> na ekran.


Tak utworzoną macierz możemy zapisać do pliku, wykorzystując jeden z kilku
<center><math>k\,\ge\,\frac{\log(1/\epsilon) - \log(1/\| x_0- x^*\|)}{\log(1/\rho)}</math></center>
formatów:
* tekstowy
* binarny
* HDF5
Zapis w formacie tekstowym to po prostu zapis do formatu ASCII. Ze względu na późniejszą [[WDP_Reprezentacja_liczb|konwersję z zapisu w
postaci dziesiętnej do dwójkowej]], wiąże się z drobnymi
niedokładnościami w odczycie.
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>  
save macierzb.dat B
</nowiki></div>
W wyniku dostajemy
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>
# 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
</nowiki></div>
Zapis binarny to prostu zrzut stanu pamięci
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
save -binary  macierzb.dat B
</nowiki></div>  
Format [http://hdf.ncsa.uiuc.edu/HDF5  HDF5] to format gwarantujący pełną przenośność danych pomiędzy różnymi
architekturami, akceptowany przez wiele zewnętrznych aplikacji, m.in.
[http://www.opendx.org  OpenDX].


  <div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>  
Liczba ta zależy więc w istotny sposób od błędu początkowego i (przede wszystkim) od współczynnika redukcji błędu <math>\rho</math>, natomiast zależność od dokładności <math>\epsilon</math> i wymiaru <math>N</math> układu jest dużo mniej istotna (w zadaniach praktycznych -- takich jak jednowymiarowy laplasjan --- jednak często
save -hdf5  macierzb.dat B
okazuje się, że... <math>\rho</math> zależy od <math>N</math>!).
</nowiki></div>  
Dane z pliku wczytujemy z powrotem poleceniem


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>  
Zakładając, że koszt jednej iteracji wynosi <math>c=c(N)</math> (<math>c(N)</math> jest tym mniejszy, im mniejsza jest liczba niezerowych elementów macierzy <math>A</math>), złożoność  metody jest proporcjonalna do
load macierzb.dat
</nowiki></div>  
Sesję Octave kończymy poleceniem <code style="color: #006">quit</code>.


====Notacja dwukropkowa Molera====
<center><math>c(N)\,\frac{\log(1/\epsilon)}{\log(1/\rho)}.
</math></center>


Do istniejącej macierzy możemy odwoływać się tradycyjnie, czyli do pojedynczych
Stąd oczywisty wniosek (prawdziwy nie tylko dla metod stacjonarnych), że metody iteracyjne warto stosować zamiast metod bezpośrednich w przypadku, gdy
elementów, np. <code style="color: #006">alfa = D(2,2)</code>, lub do jej większych bloków, używając popularnej tzw. <strong>notacji dwukropkowej</strong> Molera.  
* wymiar <math>N</math> układu <math>A x= b</math> jest "duży", oraz
* macierz <math>A</math> układu jest "rozrzedzona", tzn. ma stosunkowo niewielką liczbę elementów  niezerowych, np. proporcjonalną do <math>N</math>.
 
Układy o tych własnościach powstają często przy numerycznym rozwiązywaniu równań różniczkowych cząstkowych.  


[[Image:MNcolon-matrix-1-color.png|thumb|550px|center|Wybór bloku w macierzy <code style="color: #006">D</code> przy użyciu notacji dwukropkowej]]
==Metody przestrzeni Kryłowa==


Jest ona
Zupełnie inny pomysł na realizację metody iteracyjnej przedstawiają metody przestrzeni Kryłowa, gdzie kolejne przybliżenie <math>x_k</math> dobiera się w taki sposób, by minimalizowało pewną miarę błędu na podprzestrzeni Kryłowa
szalenie intuicyjna, a mianowicie: pisząc na przykład <code style="color: #006">v = D(2:5,2)</code>
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 <code style="color: #006">W = D(2:3,5:7)</code> 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|thumb|550px|center|Wybór zestawu kolumn w macierzy <code style="color: #006">D</code>]]
<center><math>K_k = \mbox{span} \{r,A_r,\ldots, A^{k-1}r \}</math>,</center>


Dodatkowo, aby odwołać się do całego 4. wiersza (odpowiednio: 5. kolumny)
gdzie <math>r = b-Ax_0</math> jest ''residuum'' na początku iteracji. (Zwróć uwagę, że przestrzeń Kryłowa jest rozpięta przez kolejne wektory [[MN13#Metoda potęgowa|metody potęgowej]]] --- to nie przypadek!). W zależności od wyboru sposobu miary błędu, dostajemy inną metodę iteracyjną, takie jak CG, GMRES, PCR, BiCG, i inne. Tutaj omówimy pokrótce tylko najpopularniejszą: CG.
macierzy <math>\displaystyle D</math>, można użyć skrótowej notacji <code style="color: #006">D(4,:)</code> (odpowiednio:
<code style="color: #006">D(:,5)</code>).  


Odwołanie się do całej macierzy jest także możliwe (przez użycie jej nazwy, np.
====CG====
<code style="color: #006">A = 2*D</code> lub <code style="color: #006">G = log(abs(D))</code>).


Notację dwukropkową można także wykorzystać do wygenerowania samodzielnych zbiorów indeksów:
Metoda <strong>gradientów sprzężonych</strong>, w skrócie CG (''conjugate gradients''), działa przy założeniu, że <math>A</math> jest <strong>symetryczna i dodatnio określona</strong>.


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #444; background-color:#fdfdfd;"><nowiki>  
Kolejne przybliżenie <math>x_k</math> ma minimalizować błąd w normie energetycznej
octave:1> N = 10;
indukowanej przez <math>A</math>,
octave:2> idx = 1:N
idx =
  1  2  3  4  5  6  7  8  9  10


octave:3> idx2 = 1:2:N
<center><math>||x_k -x||_A = \sqrt{(x_k -x)^TA(x_k -x)}
idx2 =
</math></center>
  1  3  5  7  9


octave:4> nidx = N:-1:1
na przestrzeni afinicznej <math>x_0 + K_k</math>. Okazuje się (co ''nie jest'' oczywiste --- trzeba skorzystać z rozmaitych własności ortogonalności generowanych wektorów), że takie zadanie minimalizacji daje się bardzo efektywnie rozwiązać, skąd dostajemy bardzo zwarty algorytm:
nidx =
  10  9  8  7  6  5  4  3  2  1
</nowiki></div>
i dlatego pętle, które w C zapisywalibyśmy


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #000; background-color:#fcfcfc;"><nowiki>  
{{algorytm|Metoda CG|Metoda CG|
for(i=0; i<=N; i++)
<pre>
r = b-A*x;
<math>\rho_0</math> = <math>||r||_2^2</math>; <math>\beta</math> = 0; k = 1;
while (!stop)
{
{
...instrukcje...
p = r + <math>\beta</math>*p;
}
w = A*p;
for(i=N; i>=1; i--)
<math>\alpha</math> = <math>\rho_{k-1}</math>/<math>p^Tw</math>;
{
x = x + <math>\alpha</math>*p;
...instrukcje...
r = r - <math>\alpha</math>*w;
<math>\rho_k</math> = <math>||r||_2^2</math>;
<math>\beta</math> = <math>\frac{\rho_{k}}{\rho_{k-1}}</math>;
k++;
}
}
</nowiki></div>
</pre>
}}
 
Jak widać, całą iterację da się wykonać przechowując w pamięci tylko kilka wektorów (a nie, jak możnaby się obawiać, całą przestrzeń <math>K_k</math>), a najdroższym jej elementem jest mnożenie macierzy przez wektor.
 
{{twierdzenie|O zbieżności CG jako metody bezpośredniej|O zbieżności CG jako metody bezpośredniej|
 
Niech <math>A\in R^{N\times N}</math> będzie symetryczna i dodatnio określona. Algorytm CG znajdzie dokładne rozwiązanie po co najwyżej <math>N</math> iteracjach.
}}
 
Powyższe twierdzenie, choć teoretycznie interesujące, ma małą wartość praktyczną z dwóch powodów:
* dla bardzo dużych <math>N</math>, wykonanie <math>N</math> iteracji może być wciąż zbyt kosztownym zadaniem;
* ponieważ w arytmetyce skończonej precyzji ortogonalność --- z której w bardzo istotny sposób korzysta się przy wyprowadzeniu algorytmu --- pogarsza się z iteracji na iterację i w konsekwencji, po wielu iteracjach, jakość <math>x_k</math> przestaje się poprawiać.
   
   
w Octave zapiszemy
Dlatego wygodniej potraktować CG jako metodę iteracyjną. Zachodzi bowiem


<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
{{twierdzenie|Zbieżność CG jako metody iteracyjnej|Zbieżność CG jako metody iteracyjnej|
for i=0:N
...instrukcje...
end
for i=N:-1:1
...instrukcje...
end
</nowiki></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====
Po <math>k</math> iteracjach metody CG,


Ponieważ podstawowymi obiektami w Octave są wektory i macierze, predefiniowane
<center><math>||x_k - x||_A \leq 2 \, \left( \frac{\sqrt{ \mbox{cond} (A)} - 1}{\sqrt{ \mbox{cond} (A)} +1}\right)^k \, ||x_0 - x||_A</math>,</center>
operacje matematyczne wykonują się od razu na całej macierzy. 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
gdzie <math>\mbox{cond} (A) = ||A||_2||A^{-1}||_2</math>.
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.


{| border=1
====GMRES====
|+ <span style="font-variant:small-caps"> </span>
|-
|
  ||  Tradycyjny kod w Octave, używający pętli  ||  Efektywny kod wektorowy (macierzowy) w Octave
|-
|


Alg 1  ||
Metoda GMRES (''Generalized Minimum RESidual'') nie wymaga ani symetrii, ani dodatniej określoności macierzy, jest więc bardziej uniwersalna, choć też bardziej kosztowna od CG. Jej szczegółowe omówienie, w tym --- oszacowania szybkości zbieżności --- wykracza niestety poza ramy niniejszego wykładu.
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
s = 0;
for i = 1:size(x,1)
s = s + abs(x(i));
end
</nowiki></div>
||
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
s = sum(abs(x));
</nowiki></div>
|-
|


Alg 2  ||
====Ściskanie macierzy====
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
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);
</nowiki></div>
||
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
N = 500;
x = linspace(a,b,N);
y = sin(x);
plot(x,y);
</nowiki></div>
|-
|
Alg 3a  ||
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
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
</nowiki></div>
||
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
C = C + A*B;
</nowiki></div>
|-
|


Alg 3b  ||
Zbieżność wszystkich poznanych metod iteracyjnych zależy od własności
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
spektralnych macierzy układu. Pojawiające się w zastosowaniach macierze
for i = 1:size(C,1),
często mają niekorzystne własności spektralne (np. bardzo duży wskaźnik uwarunkowania), przez co metody iteracyjne zbiegają na nich bardzo wolno.
for j = 1:size(C,2),
C(i,j) = C(i,j) + A(i,:)*B(:,j);
end
end
</nowiki></div>
||
<div style="font-family: monospace; white-space: pre; border-style: dashed; border-width: thin; border-color: black; margin: 1em; padding:1em; color: #006; background-color:#fcfcfc;"><nowiki>
C = C + A*B;
</nowiki></div>
|-
|


|}
Dlatego bardzo korzystne może być wstępne przetransformowanie układu


Zwróćmy uwagę na to, że kod wektorowy Octave oraz, w jeszcze większym stopniu,
<center><math>Ax = b
kod macierzowy jest znacznie bardziej elegancki i czytelniejszy od
</math></center>
tradycyjnego. Widać to dobrze na wszystkich powyższych przykładach.


Pierwszy przykład pokazuje też, że kod macierzowy jest elastyczniejszy: albowiem
z macierzą o niekorzystnych własnościach, do układu
obliczy sumę modułów wszystkich elementów <code style="color: #006">x</code> nie tylko gdy <code style="color: #006">x</code>
jest wektorem (co musieliśmy milcząco założyć w kodzie w lewej kolumnie), ale
tak samo dobrze zadziała, gdy <code style="color: #006">x</code> jest macierzą!


Szczególnie wiele na czytelności i wygodzie zyskujemy w drugim przykładzie,
<center><math>MAx = Mb</math>,</center>
gdzie najpierw funkcja <code style="color: #006">linspace</code> pozwala nam uniknąć żmudnego
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 <code style="color: #006">sin</code> zaaplikowana do całego wektora <math>\displaystyle x</math> daje wartość sinusa w
zadanych przez nas węzłach.


Kod wektorowy lub (zwłaszcza) macierzowy jest też znacznie szybszy. Spójrzmy
gdzie macierz <math>MA</math> ma znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej. W przypadku macierzy symetrycznych widzieliśmy, że kluczowe znaczenie dla zbieżności metody miał rozkład wartości własnych: jeśli wartości własne były bardzo rozrzucone po prostej, to uwarunkowanie było bardzo duże i w konsekwencji zbieżność powolna. Aby zbieżność była szybsza, kluczowe jest, by:
teraz na przykłady: (3a) i (3b), które pokażą nam prawdziwą moc funkcji
* wartości własne <math>MA</math> były upakowane w klastrach
macierzowych, unikających wielokrotnie zagnieżdżonych pętli. Oba dotyczą
* najlepiej wszystkie w (małym) otoczeniu wartości 1
operacji mnożenia dwóch macierzy. Przykład (3a) w wersji z potrójną pętlą
<code style="color: #006">for</code> 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 <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>
(czasy dla PC z procesorem Celeron 1GHz):
* Dla pętli postaci <code style="color: #006">C(i,j)=C(i,j)+A(i,k)*B(k,j)</code> uzyskano czas 21.6s,
* Dla pętli postaci  <code style="color: #006">C(i,j)=C(i,j)+A(i,:)*B(:,j)</code> --- 0.371s
* Dla pętli postaci <code style="color: #006">C=C+A*B</code> kod działał jedynie 0.00288s!
   
   
Widzimy, jak beznadziejnie wolny jest kod oparty na trzech zagnieżdżonych
Jeśli więc chcielibyśmy przekształcić macierz tak, by metoda iteracyjna dla <math>MA</math> zbiegała szybko, musimy w jakiś sposób "ścisnąć" spektrum macierzy <math>A</math> w okolice jedności. Taką operację nazywamy <strong>ściskaniem</strong> (''preconditioning''), a macierz <math>M</math> --- <strong>imadłem</strong>.
pętlach: jest on kilka tysięcy razy wolniejszy od implementacji macierzowej
<code style="color: #006">C = C + A*B</code>. Po wektoryzacji wewnętrznej pętli, program doznaje
kilkudziesięciokrotnego przyspieszenia, lecz nadal jest ponadstukrotnie
wolniejszy od kodu macierzowego!


=Literatura=
Aby całość miała sens, macierz ściskająca <math>M</math> powinna:
* być łatwa w konstrukcji,
* być tania w mnożeniu przez wektor (głównym elementem każdej metody iteracyjnej jest mnożenie macierzy przez wektor: <math>M\cdot (A \cdot x)</math>),
* macierz <math>MA</math> powinna mieć znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej.
Kilka ekstremalnych propozycji na macierz imadła to <math>M = I</math> (łatwa w
konstrukcji i tania w mnożeniu, ale niestety nic nie polepsza...) oraz
<math>M=A^{-1}</math> (rewelacyjnie poprawia zbieżność metody iteracyjnej, dając zbieżność
w jednej iteracji, ale bardzo droga w konstrukcji i mnożeniu). Widać więc, że
należy poszukiwać czegoś pośredniego, co niskim kosztem przybliża działanie
macierzy odwrotnej.


Materiały zgromadzone w kolejnych wykładach przewodnikiem po zagadnieniach w nich omawianych. Prawdziwe studia wymagają wszelako <strong>studiowania</strong> --- także dobrej literatury fachowej! Dlatego pod koniec każdego wykładu umieszczamy informację o tym, gdzie szukać pogłębionych informacji na temat omawianego na wykładzie materiału.  
Dlatego jednym z powszechniej stosowanych (aczkolwiek wciąż nie najbardziej skutecznych) sposobów ściskania te oparte na zastosowaniu jednego kroku klasycznej metody iteracyjnej.


Naszym podstawowym źródłem będzie nowoczesny podręcznik
[[Image:MNpcgconv.png|thumb|550px|center|Zbieżność metody CG bez żadnego  ściskania oraz ściśniętej
#<span style="font-variant:small-caps">D. Kincaid, W. Cheney</span>, <cite>Analiza numeryczna</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa, 2006.
imadłem opartym na jednej iteracji (blokowej) metody Jacobiego.]]


Oprócz niego, w języku polskim było wydanych kilka innych podręczników i monografii (niestety, większość z nich dość dawno temu)
Inne sposoby ściśnięcia macierzy wykorzystują np. techniki tzw. <strong>niepełnego
#<span style="font-variant:small-caps">M. Dryja, J. i M. Jankowscy</span>, <cite>Przegląd metod i algorytmów numerycznych</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa, 1988,
rozkładu</strong> macierzy, albo --- w specyficznych przypadkach --- tzw.<strong> metody
#<span style="font-variant:small-caps">A. Bjorck, G. Dahlquist</span>, <cite>Metody numeryczne</cite>, PWN, Warszawa, XXXX,
wielosiatkowe</strong>.
#<span style="font-variant:small-caps">Stoer, Bulirsch</span>, <cite>Wstęp do analizy numerycznej</cite>, PWN, Warszawa, XXXX,
#<span style="font-variant:small-caps">A.Kiełbasiński, H. Schwetlick</span>, <cite>Numeryczna algebra liniowa</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa, 1992.
#<span style="font-variant:small-caps">A. Ralston</span>, <cite>XXXX</cite>, PWN, Warszawa, XXXX,
#<span style="font-variant:small-caps">Fortuna, Macukow, </span>, <cite></cite>,


Do tego warto wspomnieć o kilku wartościowych podręcznikach i monografiach w języku angielskim
Okazuje się, że zarówno CG jak i GMRES da się zaimplementować tak, by w jednej
#<span style="font-variant:small-caps">A. Quarteroni, Valli, Saleri</span>, <cite></cite>, ,
iteracji było konieczne tylko jedno mnożenie przez macierz imadła.
#<span style="font-variant:small-caps">P. Deuflhard, A. Hohmann</span>, <cite>Numerical Analysis in Modern Scientific Computing. An Introduction</cite>, Springer, 2003, ISBN 0-387-95410-4,
#<span style="font-variant:small-caps">K. Atkinson</span>, <cite>An Introduction to Numerical Analysis</cite>, Wiley, 1989,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,
#<span style="font-variant:small-caps"></span>, <cite></cite>, ,


Zachęcamy gorąco do zapoznania się z nimi i sztuką obliczeń numerycznych, gdyż jej prawdziwy urok, niuanse i tajemnice --- podobnie jak w przypadku każdej innej ''sztuki'' --- może docenić jedynie ''wyrobiony'' odbiorca.
==Literatura==


Jeśli chodzi o programowanie w Octave lub w MATLABie, warto skorzystać z   [http://www.octave.org/manual internetowego podręcznika Octave] lub [  co jeszcze].
W celu dogłębnego zapoznania się z omawianym na wykładzie materiałem, przeczytaj <b>rozdział 4.6 i 4.7</b> w
* D. Kincaid, W. Cheney <cite>Analiza numeryczna</cite>, Wydawnictwa Naukowo-Techniczne, Warszawa 2006, ISBN 83-204-3078-X.
Tym razem wymienione rozdziały nie wystarczą do dogłębnego zrozumienia omawianych treści. Warto więc sięgnąć po jeden z podręczników
* <span style="font-variant:small-caps">C. T. Kelley</span>, <cite>Iterative Solution of Systems of Linear and Nonlinear Equations</cite>, SIAM, 1995,
* <span style="font-variant:small-caps">Y. Saad</span>, <cite>[http://www-users.cs.umn.edu/&nbsp;saad/books.html Iterative Methods for Sparse Linear Systems]</cite>, PWS, 1996.

Aktualna wersja na dzień 22:13, 11 wrz 2023


Wielkie układy równań liniowych

<<< Powrót do strony głównej przedmiotu Metody numeryczne

Ćwiczenia do wykładu >>>


Wstęp

Wraz z coraz większymi modelami pojawiającymi się w praktyce obliczeniowej, coraz częściej zachodzi potrzeba rozwiązywania zadań algebry liniowej, w której macierze są co prawda wielkiego wymiaru, ale najczęściej rozrzedzone, to znaczy jest w nich bardzo dużo zer. Bardzo często zdarza się, że macierz wymiaru N ma tylko O(N) niezerowych elementów. Wykorzytanie tej specyficznej własności macierzy nie tylko prowadzi do algorytmów istotnie szybszych od ich analogów dla macierzy gęstych (to znaczy takich, które (w założeniu) mają N2 elementów), ale wręcz są jedynym sposobem na to, by niektóre zadania w ogóle stały się rozwiązywalne przy obecnym stanie techniki obliczeniowej!

Jednym ze szczególnie ważnych źródeł układów równań z macierzami rozrzedzonymi są np. równania różniczkowe cząstkowe (a więc np. modele pogody, naprężeń w konstrukcji samochodu, przenikania kosmetyków do głębszych warstw skóry, itp.).

Modele wielostanowych systemów kolejkowych (np. routera obsługującego wiele komputerów) także prowadzą do gigantycznych układów równań z macierzami rozrzedzonymi o specyficznej strukturze.

Z reguły zadania liniowe wielkiego wymiaru będą miały strukturę macierzy rozrzedzonej, gdyż najczęściej związki pomiędzy niewiadomymi w równaniu nie dotyczą wszystkich, tylko wybranej grupy.

Przykład: Macierz z kolekcji Boeinga

Spójrzmy na macierz sztywności dla modelu silnika lotniczego, wygenerowaną swego czasu w zakładach Boeinga i pochodzącą z dyskretyzacji pewnego równania różniczkowego cząstkowego. Pochodzi z kolekcji Tima Davisa. Jest to mała macierz, wymiaru 8032 (w kolekcji spotkasz równania z milionem i więcej niewiadomych).

Struktura niezerowych elementów macierzy bcsstk38.

Jej współczynnik wypełnienia (to znaczy, stosunek liczby niezerowych do wszystkich elementów macierzy) wynosi jedynie 0.006, a więc macierz jest bardzo rozrzedzona: w każdym wierszu są średnio tylko 44 niezerowe elementy.


Reprezentacja macierzy rzadkich

Zacznijmy od sposobu przechowywania macierzy rozrzedzonych. Naturalnie, nie ma sensu przechowywać wszystkich zerowych jej elementów: wystarczy ograniczyć się do zachowania tych niezerowych! W ten sposób zmniejszamy zarówno wymagania pamięciowe, jak i liczbę operacji zmiennoprzecinkowych potrzebnych do prowadzenia działań na macierzy (np. w przypadku mnożenia macierzy przez wektor, nie będziemy mnożyć przez zera!).

Format współrzędnych

Do zapamiętania macierzy A wymiaru N×M o NNZ niezerowych elementów wykorzystujemy trzy wektory: I, J --- oba typu int --- oraz V, typu double, wszystkie o długości NNZ, przy czym

AI[k],J[k]=V[k],k=0,,NNZ1
Format AIJ

W tym formacie wprowadzane są macierze rzadkie do Octave'a i MATLABa:

A = sparse(I,J,V,N,N);

Jednak wewnętrznie przechowywane są w innym formacie, o którym poniżej.

Przykład

Pokażemy jak w Octave wprowadzić macierz rozrzedzoną.

octave:10> I = [1, 1, 1, 2, 3, 1, 4]; octave:11> J = [1, 3, 2, 2, 3, 4, 4]; octave:12> V = [1, 2, 3, 4, 5, 6, 7]; octave:13> N = 4; octave:14> A = sparse(I,J,V,N,N) A = Compressed Column Sparse (rows = 4, cols = 4, nnz = 7) (1, 1) -> 1 (1, 2) -> 3 (2, 2) -> 4 (1, 3) -> 2 (3, 3) -> 5 (1, 4) -> 6 (4, 4) -> 7 octave:15> spy(A);

Strukturę jej niezerowych elementów ukaże nam polecenie spy(A). Tak właśnie zostały wygenerowane obrazki macierzy w niniejszym wykładzie.


Format spakowanych kolumn (wierszy)

Format współrzędnych nie narzucał żadnego uporządkowania elementów macierzy --- można było je umieszczać w dowolnej kolejności. Narzucenie sensownego porządku mogłoby wspomóc realizację wybranych istotnych operacji na macierzy, na przykład, aby wygodnie było realizować działanie (prawostronnego) mnożenia macierzy przez wektor, dobrze byłoby przechowywać elementy macierzy wierszami. Tak właśnie jest zorganizowany format spakowanych wierszy (Compressed Sparse Row, CSR). Analogicznie jest zdefiniowany format spakowanych kolumn (Compressed Sparse Column, CSC), którym zajmiemy się bliżej.

Podobnie jak w przypadku formatu współrzędnych, macierz w formacie CSC jest przechowywana w postaci trzech wektorów: AV jest wektorem typu double o długości NNZ, zawierającym kolejne niezerowe elementy macierzy A wpisywane kolumnami, AI jest wektorem typu int o długości NNZ, zawierającym numery wierszy macierzy A odpowiadających elementom z AV. Natomiast zamiast tablicy J, jak to było w formacie współrzędnych, mamy krótszy wektor typu int, AP, o długości M, zawierający na j-tym miejscu indeks pozycji w AV, od którego rozpoczynają się w AV elementy j-tej kolumny macierzy A.

Format CSC

Mamy więc zależność, przy założeniu, że AP[0]=0,

AAI[AP[j]+k],j+1=AV[AP[j]+k],j=0,,M1,k=0,,AP[j+1]1

Taki (z drobnymi modyfikacjami) format macierzy wykorzystują np. pakiety ARPACK i UMFPACK (a także, wewnętrznie, Octave i MATLAB).

Format diagonalny

Znacznie mniej uniwersalny niż poprzednie i dlatego rzadziej spotykany. Kolejne diagonale macierzy przechowujemy w kolejnych wierszach macierzy P×N, gdzie P jest liczbą niezerowych diagonal w ARN×N.

Szczególnie wygodny do reprezentacji macierzy taśmowych. Wykorzystywany m.in. przez funkcję LAPACKa DGBSV służącą rozwiązywaniu równań z macierzami taśmowymi.

Uwagi praktyczne

Mnożenie macierzy w formacie AIJ przez wektor jest kilka razy wolniejsze w porównaniu do macierzy w formacie CSC/CSR (z tych dwóch oczywiście (dlaczego?) CSR jest najszybszy). Co więcej, okazuje się, że w typowych implementacjach, mnożenie macierzy rozrzedzonej (reprezentowanej np. w formacie CSC) przez wektor jest mało efektywne, mniej więcej na poziomie dziesięciu procent możliwości obliczeniowych procesora.

Jeśli już poważnie myśleć o przyspieszeniu mnożenia macierzy przez wektor (np. gdy chcemy stosować iteracyjną metodę rozwiązywania układu równań z tą macierzą), warto rozważyć inne formaty --- łączące w sobie podział macierzy na bloki (tak, jak w algorytmach BLAS) i przechowywanie (w zasadzie) tylko niezerowych elementów macierzy.

Macierze specjalne

Zajmiemy się teraz zadaniem rozwiązywania układu równań liniowych

Ax=b,

ale w sytuacji, gdy macierz A jest rozrzedzona i dużego wymiaru. Dokonamy przeglądu kilku rodzajów algorytmów mających na celu wykorzystanie rozrzedzenia macierzy dla obniżenia kosztu wyznaczenia rozwiązania układu.

Należy pamiętać, że z reguły najlepsze wyniki uzyskuje się, gdy konkretny algorytm dobierze się do konkretnej macierzy. W zastosowaniach pojawiają się m.in. macierze rzadkie o bardzo szczególnej strukturze, dla nich warto stosować wyspecjalizowane algorytmy.

Macierze taśmowe

Macierz A taka, że dla |ij|d, aij=0, nazywamy macierzą taśmową z rozstępem d, o szerokości pasma 2d+1.

Łatwo sprawdzić, że algorytm eliminacji Gaussa (bez wyboru elementu głównego) nie spowoduje dodatkowego wypełnienia w takiej macierzy (a więc da się wykonać w miejscu). W przypadku konieczności wyboru elementu głównego, pesymistyczne oszacowanie rozstępu macierzy rozkładu LU jest równe max{2d,N} --- tak więc, dla niezbyt dużych d wciąż wynikowa macierz jest taśmowa.

W szczególności, gdy macierz jest taśmowa z pasmem o rozstępie d i jednocześnie diagonalnie dominująca, wtedy rozkład LU takiej macierzy da się wykonać w miejscu kosztem O(dN), czyli liniowym.

W LAPACKu zaimplementowano szybki solver równań z macierzami taśmowymi, DGBSV, ale wymagający specjalnego sposobu przechowywania macierzy, wykorzystującego format diagonalny.

Macierze trójdiagonalne

Szczególnym przypadkiem macierzy taśmowych są macierze trójdiagonalne, tzn. taśmowe o rozstępie d=1:

A=(a1c1b2a2c2b3a3cN1bNaN)

Zadanie rozwiązywania równań z taką macierzą

Ax=e

jest bardzo często spotykane, więc warto przytoczyć algorytm w całej okazałości --- popularnie zwany algorytmem przeganiania (niektóre źródła nazywają go algorytmem Thomasa).

Zacznijmy jednak od stwierdzenia, kiedy macierz trójdiagonalna nie wymaga wyboru elementu głównego:

Stwierdzenie

Jeśli macierz A ma słabo dominującą diagonalę, tzn.

|ai||bi|+|ci|,1iN,

(b1=0=cN) i przynajmniej dla jednego indeksu "i" mamy powyżej ostrą nierówność ">", to algorytm przeganiania jest wykonalny bez przestawień wierszy. Ponadto wymaga on 7N operacji arytmetycznych, a więc jest prawie optymalny.

Algorytm Metoda przeganiania


<math>d_1</math> = <math>a_1</math>; 
<math>f_1</math> = <math>e_1</math>;
for (i = 2; i <= N; i++)
{
	<math>l</math> = <math>b_i</math>/<math>a_{i-1}</math>;
	<math>d_i</math> = <math>a_i</math> - <math>l</math> * <math>c_{i-1}</math>;
	<math>f_i</math> = <math>e_i</math> - <math>l</math> * <math>f_{i-1}</math>;
} 
<math>x_1</math> = <math>f_N</math>;
for (i = N-1; i >= 1; i--)
	<math>x_i</math> = <math>f_i</math> - <math>c_i</math> * <math>x_{i+1}</math>;

Metody bezpośrednie

Przykład: Strzałka Wilkinsona

Rozważmy układ równań z macierzą diagonalnie dominującą postaci

A=(**********)

gdzie * oznacza jakiś niezerowy element. Łatwo sprawdzić, że chociaż wyjściowa macierz jest rozrzedzona, to zastosowanie do niej eliminacji Gaussa powoduje, że w wyniku dostajemy gęste czynniki rozkładu.

Tymczasem wystarczy odwrócić kolejność równań i numerację niewiadomych (co dla macierzy jest równoznaczne z odwróceniem porządku wierszy i kolumn, korzystając z pewnej (jakiej?) macierzy permutacji P):

A~=PAPT=(**********)

Wtedy okazuje się, że rozkład naszej macierzy nie powoduje już wypełnienia czynników rozkładu!

Właśnie na tym polega główny problem w rozwiązywaniu układów z macierzami rzadkimi metodami bezpośrednimi: jak maksymalnie wykorzystać rozrzedzenie macierzy tak, by czynniki rozkładu były możliwie mało wypełnione. Albowiem wiedząc to będziemy mogli ograniczyć się jedynie do fizycznego wyznaczenia wartości niezerowych elementów macierzy rozkładu. Ponadto wymagania pamięciowe algorytmu nie będą istotnie wykraczać ponad ilość pamięci potrzebnej na przechowanie danych (tzn. macierzy).

W ogólnym przypadku rozwiązanie takiego zadania jest trudne i większość algorytmów opiera się na pewnych heurystykach, które jednak w praktyce warto wspomóc wcześniejszą analizą konkretnego układu równań jaki mamy rozwiązać. Najczęściej dąży się do takiego przenumerowania równań i niewiadomych, by w efekcie z góry przewidzieć, gdzie wystąpią zera w macierzach rozkładu --- i, by takich zer było jak najwięcej (by wypełnienie było jak najmniejsze)! Na architekturach z pamięcią hierarchiczną dąży się także do tego, by w trakcie rozkładu można było korzystać z BLAS Level 3, a więc permutacje wierszy i kolumn macierzy muszą to także brać pod uwagę.

Stosuje się kilka strategii wyznaczania korzystnych permutacji (reorderingu), z których warto wymienić

  • przybliżone algorytmy minimalnego stopnia (approximate minimum degree, AMD), np. AMD;
  • techniki podziału grafów na (prawie) rozłączne składowe nested dissection, ND, np. METIS.

Używają ich biblioteki takie jak UMFPACK, czy HSL.

W Octave mamy do dyspozycji także kilka procedur generujących takie permutacje, w tym: colamd (AMD dla macierzy niesymetrycznych) oraz symamd (AMD dla macierzy symetrycznych). Większy wybór oferuje MATLAB, jednak należy bezwzględnie pamiętać o jednym: nie ma uniwersalnej metody reorderingu i dla konkretnej macierzy może istnieć specjalna metoda, która da oszałamiające rezultaty, podczas gdy standardowe podejścia nie dadzą efektu.

Przykład

Najprostszy sposób na wykorzystanie metody bezpośredniego rozwiązywania układu z macierzą rzadką to zastosowanie znanego nam operatora do macierzy typu rozrzedzonego:

A = sparse(...);
x = A \ b;

Octave domyślnie przyłoży do A reordering colamd i następnie skorzysta z biblioteki UMFPACK by rozwiązać taki układ. Dodatkowo, badane jest wcześniej, czy macierz nie jest przypadkiem taśmowa o wąskiej wstędze: jeśli jest, to korzysta się z LAPACKa.

(Jak widzisz, operator rozwiązywania układu równań jest mocno przeciążony: działa i na macierzach kwadratowych, i na prostokątnych, i na rozrzedzonych --- na każdym rodzaju w inny sposób. Nie dziwi Cię chyba, dlaczego wygodnie było napisać Octave w C++?)

Przykład: Wypełnienie pewnej macierzy w zależności od użytego reorderingu

Rozważmy ponownie macierz pochodzącą z kolekcji Tima Davisa. Jest to macierz symetryczna, dodatnio określona, wymiaru 8032, o 355460 elementach niezerowych i, w konsekwencji, o wypełnieniu około 0.6 procent.

Struktura niezerowych elementów macierzy.

Zobaczymy, jak w zależności od użytego algorytmu permutacji kolumn i wierszy poradzi sobie algorytm rozkładu Cholesky'ego.

Czynnik rozkładu Cholesky'ego L wykonanego standardowym algorytmem. Czas rozkładu: 0.892013
Czynnik rozkładu Cholesky'ego L z reorderingiem COLAMD. Czas rozkładu: 0.813038
Czynnik rozkładu Cholesky'ego L z reorderingiem SYMAMD. Czas rozkładu: 0.487683s. Prawie dwa razy szybciej niż bez reorderingu, chociaż i tak wskutek wzrostu wypełnienia macierzy w dolnym trójkącie mamy aż 2 procent niezerowych elementów.
Czynnik rozkładu Cholesky'ego L z odwrotnym reorderingiem Cuthill-McKee. Czas rozkładu: 0.845928
Czynnik rozkładu Cholesky'ego L z jeszcze innym (bardzo tanim, ale jak widać czasem zupełnie złym) reorderingiem. Czas rozkładu: 5.947936

Na zakończenie popatrzmy, jak ważne jest spostrzeżenie symetrii macierzy:

Rozkład LU, czynnik L (bez reorderingu). Czas rozkładu LU: 1.696018s. Nieakceptowalny, podobnie jak drastyczne wypełnienie macierzy.
Rozkład LU, czynnik U (bez reorderingu)

Jak widać, w naszym przypadku standardowe algorytmy (COLAMD i SYMAMD) poradziły sobie całkiem nieźle, chociaż wypełnienie i tak znacząco wzrosło. Zapewne, dla tej macierzy, która jest specyficznego typu --- pochodzi z dyskretyzacji równania różniczkowego --- algorytm ND mógłby tu jeszcze lepiej zadziałać.

Stacjonarne metody iteracyjne

Gdy macierz jest rozrzedzona, mnożenie takiej macierzy przez wektor jest bardzo tanie (koszt jest proporcjonalny do liczby niezerowych elementów macierzy). Dlatego, jeśli możemy zadowolić się rozwiązaniem przybliżonym układu, a w zamian osiągnąć je tanim kosztem, warto rozważyć metody iteracyjne.

Najprostsze metody iteracyjne (najprostsze w analizie i implementacji, ale --- jak można się domyślić --- w praktyce najmniej efektywne) polegają na rozkładzie macierzy na część "łatwo odwracalną", M, i "resztę", Z. Dokładniej, jeśli M jest nieosobliwa, to równanie Ax=b można zapisać jako zadanie punktu stałego

Mx=Nx+b,

gdzie Z=MA. Inaczej:

x=M1(Zx+b),

i zastosować doń metodę iteracji prostej Banacha:

xk+1=M1(Zxk+b)

Takie metody nazywamy stacjonarnymi metodami iteracyjnymi. Aby przeanalizować zbieżność takiej metody, warto rozpatrzyć przypadek ogólniejszy

xk=Bxk1+c,

dla pewnej macierzy B oraz wektora c. (Dla stacjonarnej metody iteracyjnej, B=M1Z oraz c=M1b.)

W tym przypadku

xkx*=Bk(x0x*),

a stąd i z nierówności BkBk, mamy

xkx*Bkx0x*.

Warunkiem dostatecznym zbieżności iteracji prostych jest więc B<1. Okazuje się, że warunkiem koniecznym i dostatecznym zbieżności tej iteracji dla dowolnego wektora startowego x0 jest

ρ=max{|λ|:λ jest wartością własną B}<1

Tak więc, metody oparte na iteracji prostej będą zbieżne liniowo z ilorazem ρ.

Zaletą stacjonarnych metod iteracyjnych jest również ich prostota, przez co są one łatwe do zaprogramowania, co łatwo zobaczyć na przykładach metod: Jacobiego i Gaussa--Seidela, które teraz omówimy.

Metoda Jacobiego

Biorąc M=diag(A), gdzie diag(A) jest macierzą diagonalną składającą się z wyrazów stojących na głównej przekątnej macierzy A, układ Ax=b jest równoważny układowi

Mx=Zx+b,

a stąd (o ile na przekątnej macierzy A nie mamy zera) otrzymujemy metodę iteracyjną

xk=Bxk1+c,

gdzie B=M1Z i c=M1b, zwaną metodą Jacobiego.

Rozpisując ją po współrzędnych dostajemy (numer iteracji wyjątkowo zaznaczamy w postaci górnego indeksu) układ rozszczepionych równań:

xi(k)=1aii(bijiaijxj(k1)),

co znaczy dokładnie tyle, że w i-tym równaniu wyjściowego układu przyjmujemy za współrzędne x wartości z poprzedniej iteracji i na tej podstawie wyznaczamy wartość xi.

Twierdzenie O zbieżności metody Jacobiego

W metodzie Jacobiego warunek dostateczny zbieżności, B<1, jest spełniony np. wtedy, gdy macierz A ma dominującą przekątną, tzn. gdy

|ai,i|>ji|ai,j|,i=1,,N

Dowód

Rzeczywiście, ponieważ wyraz (i,j) macierzy M1Z wynosi 0 dla i=j oraz ai,j/ai,i dla ij, a więc

Parser nie mógł rozpoznać (nieznana funkcja „\lefteqn”): {\displaystyle \begin{align} \lefteqn{\|M^{-1}Z\|_\infty \;=\; \max_{1\le i\le N} \sum_{j=1,j\ne i}^N{|a_{i,j}|}/{|a_{i,i}|} } \\ && =\;\max_{1\le i\le N} \sum_{j=1}^N{|a_{i,j}|}/{|a_{i,i}|}\,-\,1\;<\;1, \end{align}}

przy czym ostatnia nierówność wynika z warunku diagonalnej dominacji.

Przykład: Macierz laplasjanu

Macierz N×N, zwana macierzą jednowymiarowego laplasjanu

L=(2112112)

pojawia się w bardzo wielu zastosowaniach, także jako podzadanie w algorytmach numerycznych. Ta macierz jest macierzą taśmową, symetryczną i dodatnio określoną, więc układ równań z tą macierzą można bez trudu rozwiązać metodami bezpośrednimi, kosztem O(N). Stosując do niej metodę Jacobiego mamy M=2I oraz Z=LM. Obliczając normę macierzy iteracji Jacobiego dostajemy ||M1Z||=1, co nie rozstrzyga jeszcze o jej zbieżności lub niezbieżności.

Potrzebna będzie bardziej subtelna analiza, Okazuje się, że są znane wzory na wartości własne macierzy L:

λj=4sin2(jπ2(N+1)),

dla 1jN.

W konsekwencji, wartościami własnymi M1Z=12Z=12LI są liczby μi=12λi1. Ponieważ 0<μi<1, znaczy to, że metoda Jacobiego jest zbieżna dla macierzy L.

Z drugiej strony, nie dajmy się zwieść optymizmowi matematyka ("nie martw się, jest zbieżny..."): nietrudno sprawdzić, że ||Z||2=1O(N2)<1, co oznacza, że metoda Jacobiego --- choć zbieżna --- dla dużych N staje się zbieżna tak wolno, że w praktyce bezużyteczna!

Metoda Gaussa-Seidela

Heurystyka tej metody opiera się na zmodyfikowaniu metody Jacobiego tak, by w każdym momencie iteracji korzystać z najbardziej "aktualnych" współrzędnych przybliżenia rozwiązania x.

Rzeczywiście, przecież rozwiązując równanie metody Jacobiego:

xi(k)=1aii(bijiaijxj(k1)),

nietrudno zauważyć, że w części sumy moglibyśmy odwoływać się do "dokładniejszych" wartości xj(k): dla j<i, tzn.

xi(k)=1aii(bij<iaijxj(k)j>iaijxj(k1))

W języku rozkładu macierzy A=MZ i iteracji xk+1=M1(Zxk+b) mamy więc M=tril(A) (dolny trójkąt macierzy A).

Twierdzenie O zbieżności metody Gaussa-Seidela

Jeśli macierz A jest diagonalnie dominująca, to metoda Gaussa--Seidela jest zbieżna dla dowolnego wektora startowego x0.

Metoda Gaussa--Seidela jest w wielu przypadkach rzeczywiście szybciej zbieżna od metody Jacobiego, np. tak jest w przypadku macierzy jednowymiarowego Laplasjanu. Wciąż jednak, dodajmy, dla zadań bardzo źle uwarunkowanych jej zbieżność jest zbyt wolna by ją stosować jako samodzielną metodę.

Uwaga

Obie metody, Jacobiego i (zwłaszcza) Gaussa--Seidela stosuje się także czasem w prostych algorytmach rozwiązywania układów równań nieliniowych: ich zaletą jest to, że głównym składnikiem iteracji jest rozwiązywanie skalarnego równania nieliniowego na każdym kroku metody.

Złożoność stacjonarnych metod iteracyjnych

Zastanówmy się teraz nad złożonością metod iteracyjnych. Ponieważ możemy jedynie znaleźć pewne przybliżenie rozwiązania dokładnego x*, przez złożoność metody będziemy rozumieli koszt kombinatoryczny obliczenia xk z zadaną dokładnością ϵ>0. Dla uproszczenia założymy, że medoda jest zbieżna liniowo z ilorazem ρ. Zauważmy, że aby zredukować błąd początkowy do ϵ>0, wystarczy wykonać k=k(ϵ) iteracji, gdzie k spełnia

ρkx0x*ϵ,

czyli

klog(1/ϵ)log(1/x0x*)log(1/ρ)

Liczba ta zależy więc w istotny sposób od błędu początkowego i (przede wszystkim) od współczynnika redukcji błędu ρ, natomiast zależność od dokładności ϵ i wymiaru N układu jest dużo mniej istotna (w zadaniach praktycznych -- takich jak jednowymiarowy laplasjan --- jednak często okazuje się, że... ρ zależy od N!).

Zakładając, że koszt jednej iteracji wynosi c=c(N) (c(N) jest tym mniejszy, im mniejsza jest liczba niezerowych elementów macierzy A), złożoność metody jest proporcjonalna do

c(N)log(1/ϵ)log(1/ρ).

Stąd oczywisty wniosek (prawdziwy nie tylko dla metod stacjonarnych), że metody iteracyjne warto stosować zamiast metod bezpośrednich w przypadku, gdy

  • wymiar N układu Ax=b jest "duży", oraz
  • macierz A układu jest "rozrzedzona", tzn. ma stosunkowo niewielką liczbę elementów niezerowych, np. proporcjonalną do N.

Układy o tych własnościach powstają często przy numerycznym rozwiązywaniu równań różniczkowych cząstkowych.

Metody przestrzeni Kryłowa

Zupełnie inny pomysł na realizację metody iteracyjnej przedstawiają metody przestrzeni Kryłowa, gdzie kolejne przybliżenie xk dobiera się w taki sposób, by minimalizowało pewną miarę błędu na podprzestrzeni Kryłowa

Kk=span{r,Ar,,Ak1r},

gdzie r=bAx0 jest residuum na początku iteracji. (Zwróć uwagę, że przestrzeń Kryłowa jest rozpięta przez kolejne wektory metody potęgowej] --- to nie przypadek!). W zależności od wyboru sposobu miary błędu, dostajemy inną metodę iteracyjną, takie jak CG, GMRES, PCR, BiCG, i inne. Tutaj omówimy pokrótce tylko najpopularniejszą: CG.

CG

Metoda gradientów sprzężonych, w skrócie CG (conjugate gradients), działa przy założeniu, że A jest symetryczna i dodatnio określona.

Kolejne przybliżenie xk ma minimalizować błąd w normie energetycznej indukowanej przez A,

||xkx||A=(xkx)TA(xkx)

na przestrzeni afinicznej x0+Kk. Okazuje się (co nie jest oczywiste --- trzeba skorzystać z rozmaitych własności ortogonalności generowanych wektorów), że takie zadanie minimalizacji daje się bardzo efektywnie rozwiązać, skąd dostajemy bardzo zwarty algorytm:

Algorytm Metoda CG


r = b-A*x;
<math>\rho_0</math> = <math>||r||_2^2</math>; <math>\beta</math> = 0; k = 1;
while (!stop)
{
	p = r + <math>\beta</math>*p;
	w = A*p;
	<math>\alpha</math> = <math>\rho_{k-1}</math>/<math>p^Tw</math>;
	x = x + <math>\alpha</math>*p;
	r = r - <math>\alpha</math>*w;
	<math>\rho_k</math> = <math>||r||_2^2</math>;
	<math>\beta</math> = <math>\frac{\rho_{k}}{\rho_{k-1}}</math>;
	k++;	
}

Jak widać, całą iterację da się wykonać przechowując w pamięci tylko kilka wektorów (a nie, jak możnaby się obawiać, całą przestrzeń Kk), a najdroższym jej elementem jest mnożenie macierzy przez wektor.

Twierdzenie O zbieżności CG jako metody bezpośredniej

Niech ARN×N będzie symetryczna i dodatnio określona. Algorytm CG znajdzie dokładne rozwiązanie po co najwyżej N iteracjach.

Powyższe twierdzenie, choć teoretycznie interesujące, ma małą wartość praktyczną z dwóch powodów:

  • dla bardzo dużych N, wykonanie N iteracji może być wciąż zbyt kosztownym zadaniem;
  • ponieważ w arytmetyce skończonej precyzji ortogonalność --- z której w bardzo istotny sposób korzysta się przy wyprowadzeniu algorytmu --- pogarsza się z iteracji na iterację i w konsekwencji, po wielu iteracjach, jakość xk przestaje się poprawiać.

Dlatego wygodniej potraktować CG jako metodę iteracyjną. Zachodzi bowiem

Twierdzenie Zbieżność CG jako metody iteracyjnej

Po k iteracjach metody CG,

||xkx||A2(cond(A)1cond(A)+1)k||x0x||A,

gdzie cond(A)=||A||2||A1||2.

GMRES

Metoda GMRES (Generalized Minimum RESidual) nie wymaga ani symetrii, ani dodatniej określoności macierzy, jest więc bardziej uniwersalna, choć też bardziej kosztowna od CG. Jej szczegółowe omówienie, w tym --- oszacowania szybkości zbieżności --- wykracza niestety poza ramy niniejszego wykładu.

Ściskanie macierzy

Zbieżność wszystkich poznanych metod iteracyjnych zależy od własności spektralnych macierzy układu. Pojawiające się w zastosowaniach macierze często mają niekorzystne własności spektralne (np. bardzo duży wskaźnik uwarunkowania), przez co metody iteracyjne zbiegają na nich bardzo wolno.

Dlatego bardzo korzystne może być wstępne przetransformowanie układu

Ax=b

z macierzą o niekorzystnych własnościach, do układu

MAx=Mb,

gdzie macierz MA ma znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej. W przypadku macierzy symetrycznych widzieliśmy, że kluczowe znaczenie dla zbieżności metody miał rozkład wartości własnych: jeśli wartości własne były bardzo rozrzucone po prostej, to uwarunkowanie było bardzo duże i w konsekwencji zbieżność powolna. Aby zbieżność była szybsza, kluczowe jest, by:

  • wartości własne MA były upakowane w klastrach
  • najlepiej wszystkie w (małym) otoczeniu wartości 1

Jeśli więc chcielibyśmy przekształcić macierz tak, by metoda iteracyjna dla MA zbiegała szybko, musimy w jakiś sposób "ścisnąć" spektrum macierzy A w okolice jedności. Taką operację nazywamy ściskaniem (preconditioning), a macierz M --- imadłem.

Aby całość miała sens, macierz ściskająca M powinna:

  • być łatwa w konstrukcji,
  • być tania w mnożeniu przez wektor (głównym elementem każdej metody iteracyjnej jest mnożenie macierzy przez wektor: M(Ax)),
  • macierz MA powinna mieć znacznie korzystniejsze własności z punktu widzenia używanej metody iteracyjnej.

Kilka ekstremalnych propozycji na macierz imadła to M=I (łatwa w konstrukcji i tania w mnożeniu, ale niestety nic nie polepsza...) oraz M=A1 (rewelacyjnie poprawia zbieżność metody iteracyjnej, dając zbieżność w jednej iteracji, ale bardzo droga w konstrukcji i mnożeniu). Widać więc, że należy poszukiwać czegoś pośredniego, co niskim kosztem przybliża działanie macierzy odwrotnej.

Dlatego jednym z powszechniej stosowanych (aczkolwiek wciąż nie najbardziej skutecznych) sposobów ściskania są te oparte na zastosowaniu jednego kroku klasycznej metody iteracyjnej.

Zbieżność metody CG bez żadnego ściskania oraz ściśniętej imadłem opartym na jednej iteracji (blokowej) metody Jacobiego.

Inne sposoby ściśnięcia macierzy wykorzystują np. techniki tzw. niepełnego rozkładu macierzy, albo --- w specyficznych przypadkach --- tzw. metody wielosiatkowe.

Okazuje się, że zarówno CG jak i GMRES da się zaimplementować tak, by w jednej iteracji było konieczne tylko jedno mnożenie przez macierz imadła.

Literatura

W celu dogłębnego zapoznania się z omawianym na wykładzie materiałem, przeczytaj rozdział 4.6 i 4.7 w

  • D. Kincaid, W. Cheney Analiza numeryczna, Wydawnictwa Naukowo-Techniczne, Warszawa 2006, ISBN 83-204-3078-X.

Tym razem wymienione rozdziały nie wystarczą do dogłębnego zrozumienia omawianych treści. Warto więc sięgnąć po jeden z podręczników