Jdi na navigaci předmětu

1. Domácí úkol: Gauss

Úvod

Prvním domácím úkolem se vrátíme k studentům velmi dobře známému lineárně-algebraickému algoritmu, ke Gaussově eliminační metodě (GEM). Obsahem tohoto úkolu cílíme zejména na následující body:

  1. procvičení základů Julia (podmínky, cykly, typy, metody, práce s poli/maticemi),
  2. oživení některých lineárně-algebraických znalostí (řešení soustav lineárních rovnic, inverze matice, GEM),
  3. názorná demonstrace nedostatků Gaussovy eliminační metody (viz gauss.ipynb).

Carl Friedrich Gauss byl jedním z největších matematiků a na jeho jméno narazíte ve spoustě matematických partií. V případě GEM nelze tvrdit, že by byl jejím prvním objevitelem. Jisté verze GEM existovaly již dříve a stopy vedou až k čínským matematikům do doby před naším letopočtem. Pravdou ovšem zůstává, že Gauss se touto metodou zabýval systematicky a to včetně důkazů. V literatuře občas také narazíte na alternativní název "Gauss–Jordanova eliminace."

Gauss
Obrázek 1. Carl Friedrich Gauss (Zdroj: commons.wikimedia.org. Autor: Siegfried Detlev Bendixen, 1828. Public domain.)

Teoretický background

Implementační cíle úkolu jsou tři:

  1. funkce echelon_form! převádějící zadanou matici na redukovaný horní stupňovitý tvar (reduced echelon form),
  2. funkce inverse invertující zadanou čtvercovou matici,
  3. funkce solve hledající řešení zadané soustavy lineárních rovnic pomocí GEM.

Všechny z uvedených metod budou moci pracovat s různými číselnými datovými typy (strojová čísla, komplexní čísla, racionální čísla, modulární aritmetika). Podrobně jsou tyto metody popsány dále v textu. V implementaci funkcí inverse a solve půjde přirozeně využít funkci echelon_form!.

Svůj kód umístěte do souboru gauss.jl, kde je i předpřipravena nekompletní šablona. V tomto souboru můžete (a je to velmi vhodné) definovat i další pomocné metody. Případně můžete přidat i další testy do adresáře test, viz sekci Lokální spouštění testů.

Než se pustíme do formálního popisu zadání, rychle připomeneme/představíme matematické pozadí problému.

Redukovaný horní stupňovitý tvar (HST)

Matice je v horním stupňovitém tvaru, právě když

  1. Zcela nulové řádky jsou jenom v dolní části matice.
  2. První (zleva doprava) nenulový prvek (pivot) v daném řádku je vždy v sloupcovém pořadí striktně za prvním nenulovým prvkem v předchozím řádku.
  3. Ve sloupci, kde je pivot (první nenulový prvek v daném řádku), jsou všechny prvky rovné nule vyjma řádku s pivotem, který je roven jedné.

Například následující matice je v redukovaném HST:

[
  0 1 0 0 3 0
  0 0 1 0 2 0
  0 0 0 1 1 0
  0 0 0 0 0 1
  0 0 0 0 0 0
]

Gaussova eliminační metoda (GEM)

Ekvivalentní úpravy matice v Gaussově eliminační metodě jsou následující:

  1. prohození dvou řádků matice,
  2. vynásobení řádku matice nenulovým číslem,
  3. přičtení násobku jednoho řádku k jinému řádku.

Tyto úpravy nemění množinu všech řešení dané soustavy, proto se jim říká ekvivalentní úpravy.

GEM převede matici do redukovaného HST následujícím způsobem. Nejprve proveďme první krok:

  1. V prvním sloupci vyber pivot (viz níže) a prohoď řádek s pivotem a první řádek. Pokud je celý první sloupec nulový, tak se posuň do druhého kroku.
  2. Vynásob první řádek vhodným číslem tak, aby v prvním řádku a prvním sloupci (na místě pivotu) vznikla 1.
  3. Přičítáním vhodných násobků prvního řádku ke všem ostatním řádkům vytvoř v prvním sloupci pod pivotem nuly.

V druhém kroku se přesuneme do druhého sloupce:

  1. Pivot hledáme v druhém sloupci v řádcích pod řádkem s předchozím pivotem (pokud jsme ještě žádný nenalezli, tak ve všech řádcích). Pokud pivot nenajdeme, posouváme se do dalšího sloupce. Řádek s novým pivotem opět přesuneme do řádku hned pod řádkem s předchozím pivotem.
  2. Vynásobíme řádek s aktuálním pivotem vhodným číslem tak, aby na jeho místě vznikla 1.
  3. Přičítáním vhodných násobků vytvoříme nuly v druhém sloupci v řádcích pod i nad řádkem s pivotem.

Tento krok opakujeme v dalších sloupcích, dokud všechny neproběhneme. Na konci běhu algoritmu dostáváme matici v redukovaném HST. Algoritmus jistě skončí, protože matice má konečný počet sloupců.

Implementační pokyny

Naše implementace musí zvládnout pracovat s maticemi s prvky minimálně následujících typů

  • Float64
  • Complex{Float64}
  • BigFloat
  • Complex{BigFloat}
  • Int64 (zde viz poznámku níže)
  • Rational{Int64}
  • AbstractGaloisField z GaloisFields balíčku

Pokud to bude možné, tak se budeme snažit počítat v exaktní aritmetice.

Vaše implementace by ale měla fungovat i s dalšími podobnými typy. Nezapomínejte, že máte k dispozici abstraktní typy jako AbstractFloat nebo Integer.

Volba pivotu

Z popisu algoritmu v předchozích odstavcích je patrná nutnost umět rozhodnout o nenulovosti prvku matice, resp. čísla daného typu. Rozlišíme dvě situace:

  1. V případě čísel typů s nimiž počítáme v exaktní aritmetice (bez numerických chyb; tj. Int64, Rational{Int64} a AbstractGaloisField) za nenulové číslo bereme libovolné číslo daného typu různé od 0 (ve smyslu !=).
  2. U čísel typů založených na číslech s pohyblivou desetinnou čárkou (tj. Float64, Complex{Float64}, BigFloat a Complex{BigFloat}) za nulová budeme považovat všechna čísla v absolutní hodnotě menší nebo rovno 2eps(T), kde T je buď přímo nějaký float nebo vystupuje jako float v Complex{T}.

Volba pivotu není úplně triviální, existuje více strategií. My opět chování rozdělíme podle typu prvků matice s kterou počítáme.

  1. Pokud počítáme v exaktní aritmetice (Int64, Rational{Int64} a AbstractGaloisField), pak jako pivot volíme první nenulové číslo, které připadá v úvahu.
  2. Pokud počítáme se čísly založenými na strojových číslech (Float64, Complex{Float64}, BigFloat a Complex{BigFloat}), pak jako pivot volíme číslo s největší možnou absolutní hodnotou.

echelon_form!

Funkce echelon_form! převede zadanou matici A do redukovaného HST. Funkce modifikuje vstup (pokud to lze) a současně i vrátí příslušnou matici v redukovaném HST.

Pokud je na vstupu matice, jejíž prvky jsou typu T, v kterém nejsme vždy schopni provádět dělení (podtypy abstraktního typu Integer), pak funkce vytvoří kopii zadané matice a přetypuje její prvky na Rational{T}. Tímto způsobem budeme stále počítat v exaktní aritmetice.

Jednoduchý příklad použití (další naleznete ve složce test):

julia> A = [1 2 3; 4 5 6];

julia> echelon_form!(A)
2×3 Matrix{Rational{Int64}}:
 1//1  0//1  -1//1
 0//1  1//1   2//1

HINT: Funkci echelon_form! můžete přidat i vhodné nepovinné argumenty, pomocí kterých můžete předávat informaci například metodě solve…​

inverse

Funkce inverse pro čtvercovou matici A vrátí její inverzní matici. Pokud matice A není čtvercová, nebo není regulární, pak vyhodí výjimku ErrorException (viz metodu error).

K výpočtu inverzní matice použijeme metodu echelon_form! a standardní algoritmus známý z BI-LIN (převod matice (A|E) na matici (E|A^(-1))).

Například:

julia> A = [1.0 1.0 -1.0; 1.0 -1.0 1.0; -1.0 1.0 1.0];

julia> inverse(A)
3×3 Matrix{Float64}:
 0.5  0.5  0.0
 0.5  0.0  0.5
 0.0  0.5  0.5

solve

Funkce solve bere dva argumenty, matici A a vektor b, a vrátí nějaké řešení soustavy s maticí A a pravou stranou b. Pokud matice A a vektor b nemají správné rozměry, nebo soustava nemá řešení, pak opět dostaneme výjimku ErrorException.

K výpočtu lze zase velmi přirozeně použít metodu echelon_form!.

Například:

julia> A = [1.0 1.0 -1.0; 1.0 -1.0 1.0; -1.0 1.0 1.0];

julia> b = [1.0, 1.0, 1.0];

julia> solve(A, b)
3-element Vector{Float64}:
 1.0
 1.0

Lokální spouštění testů

Toto zadání je úmyslně vytvořeno lehce "na koleni" a nevyužívá například moduly nebo Julia projekty ke správě závislostí (látka, ke které se dostaneme později během semestru).

K lokálnímu spuštění budete proto pravděpodobně potřebovat doinstalovat balíčky Glob a GaloisFields.

import Pkg; Pkg.add("Glob"); Pkg.add("GaloisFields")

Poté v kořenovém adresáři tohoto zadání stačí z příkazové řádky spustit

$ julia --color=yes test/runtests.jl

Tímto příkazem spustíte všechny test v souborech test/test_*.jl. Případně tak do adresáře test můžete snadno přidat i další pomocné vlastní testy vlastních pomocných metod, jen je musíte pojmenovat ve tvaru test_*.jl.

Pokud vše dopadne dobře, měli byste vidět následující standardní výstup:

$ julia test/runtests.jl
┌ Info: Running tests in test_echelon_form.jl...
└ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Test Summary:                                                | Pass  Total
echelon_form!: integer and trivial examples                  |    8      8
Test Summary:                                                | Pass  Total
echelon_form!: over rational numbers                         |    4      4
Test Summary:                                                | Pass  Total
echelon_form!: GaloisField, GF(5)                            |    4      4
Test Summary:                                                | Pass  Total
echelon_form!: Float64                                       |    3      3
Test Summary:                                                | Pass  Total
echelon_form!: BigFloat                                      |    3      3
┌ Info: Running tests in test_inverse.jl...
└ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Test Summary:                                                | Pass  Total
inverse: non-invertible matrices                             |    3      3
Test Summary:                                                | Pass  Total
inverse: computation over rational field, small examples     |    3      3
Test Summary:                                                | Pass  Total
inverse: computation in floating point arithmetic            |    2      2
┌ Info: Running tests in test_other.jl...
└ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
...
...
┌ Info: Running tests in test_solve.jl...
└ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Test Summary:                                                | Pass  Total
solve: non-solvable examples                                 |    2      2
Test Summary:                                                | Pass  Total
solve: simple solvable examples                              |    4      4
Test Summary:                                                | Pass  Total
solve: a badly conditioned system                            |    1      1
Test Summary:                                                | Pass  Total
solve: randomized tests                                      |    3      3

V opačném případě dostanete od Julia vynadáno!

Řešení a odevzdání

Opět vytvořte větev odvozenou z větve assignment/01-gauss a nezvěte ji například solution/01-gauss. Do solution/01-gauss vložte své řešení editací souboru gauss.jl případně přidáním testů do složky test. Až budete se svým řešením spokojeni, vytvořte MR (to můžete i dříve, aspoň uvidíte výsledek testů, pokud je nespouštíte lokálně) a přiřaďte mě k němu jako assignee. Tímto aktem úkol odevzdáte.

Poznámky

GaloisFields

Pro práci s konečnými tělesy zvolíme balíček GaloisFields. Nebudeme z něj potřebovat nic složitého, jen počítání v konečných tělesech (sčítání a násobení modulo zadané prvočíslo). Práce s ním je jednoduchá:

julia> using GaloisFields; import GaloisFields: AbstractGaloisField

julia> Z5 = GaloisField(5)
𝔽₅

julia> m1 = Z5[1 2; 3 4]
2×2 Matrix{𝔽₅}:
 1  2
 3  4

julia> m1 * m1
2×2 Matrix{𝔽₅}:
 2  0
 0  2

julia> m1 + m1
2×2 Matrix{𝔽₅}:
 2  4
 1  3

julia> Z5 <: AbstractGaloisField
true

Import abstraktního typu je už proveden v šabloně pro řešení. Další příklady lze nalézt v testech.