4. Domácí úkol: Intervalová aritmetika
Úvod
Během svých programovacích dobrodružstvích jste již určitě narazili na problémy s počítáním se strojovými čísly (tak jak je definuje standard IEEE 754).
Jejich výhodou je rychlost, operace s nimi jsou prováděny na hardwarové úrovni (přesněji v Julia jde o typy Float64
, Float32
a Float16
, BigFloat
do této efektivní kategorie už nepatří).
Mezi nevýhody patří ztráta přesnosti při zaokrouhlování, "katastrofické krácení", jejich nerovnoměrné rozložení na reálné ose, atd.
V důsledku těchto problémů je nutné při práci s nimi postupovat opatrně a snažit se aktivně vyhýbat situacím, kde by mohla vzniknout fatální chyba. Například výrazy, které jsou z matematického pohledu totožné nemusí být ekvivalentní či vhodné k použití v implementaci (například pro různá a ). Některé numerické algoritmy, které jsou správné při ideálních operacích s reálnými čísly selhávají při implementaci ve strojových číslech (nejkřiklavějším případem je asi Gaussova eliminace).
Existují různé další přístupy modelující reálná čísla na počítačích. V tomto úkolu se podíváme na tzv. Intervalovou aritmetiku. Dalším velmi zajímavým přístupem je třeba číselný formát Unum. Například Mathematica k problému přistupuje rozlišováním dvou typů "přesnosti": accuracy a precision.
Intervalová aritmetika
Tento přístup k nepřesné aproximaci tělesa reálných čísel spočívá ve snaze hlídat velikost možné chyby akumulované během výpočtu a zároveň v možnosti práce s vrozenou neurčitostí hodnot, s kterými pracujeme.
Místo čísel budeme pracovat s uzavřenými intervaly , . Intervalovou hodnotou reprezentujeme fakt, že může mít libovolnou hodnotu z daného intervalu. Připouštíme rovnost krajních bodů intervalu (tím popíšeme situaci, kdy danou hodnotu známe přesně) a množina všech intervalových hodnot tak přirozeně i obsahuje všechna "stará" reálná čísla (jakožto ).
Algebraické operace
Dále definujeme algebraické operace sčítání
odčítání
a násobení
Definice každé z těchto operací je poměrně přirozená. Pokud hodnoty operandů příslušely uvedeným intervalům, pak libovolná výsledná hodnota padne do uvedeného výsledného intervalu a zároveň je tento výsledný interval nejmenší možný s touto vlastností.
Operace dělení je komplikovanější kvůli ne/možnosti dělení nulou, definujeme ji předpisem
kde
Situaci, kdy je v děliteli obsažena nula uvažovat nebudeme (lze to, ale situace by se poměrně komplikovala), vyvoláme výjimku dělení nulou.
Obyčejná reálná čísla jsou v tomto číselném systému přirozeně obsažena jako "intervaly" .
Zadání
Cílem tohoto úkolu je vytvořit Julia balíček IntervalArithmetic.jl
, který bude implementovat datový typ modelující intervaly a dále bude implementovat metody pro práci s objekty tohoto typu, zejména algebraické operace.
Kód musí mít náležitou strukturu, aby se s repozitářem dalo pracovat jako s Julia balíčkem (viz sekci Julia balíček).
Zadání připravené na větvi assignment/04-interval
je záměrně prakticky prázdné, abyste nebyli nijak omezeni nebo tlačeni v jistém směru.
Ohledně struktury Julia balíčku se můžete inspirovat ve výukových materiálech, balíčku PkgTemplates.jl a našeho ukázkového balíčku WLS.jl.
Otázky
Dále pomocí své implementace prozkoumejte jeden ze známých klasických ukázek tzv. katastrofického krácení.
Mějme dvě čísla exaktně reprezentovatelná v 64 bitové přesnosti, a .
Vypočtěte přesnou hodnotu výrazu .
- Jak se tato přesná hodnota liší od hodnoty získané pomocí standardních operací s čísly typu
Float64
? - Jak se tato přesná hodnota liší od hodnoty (z matematicky identického) výrazu opět vyhodnoceného v aritmetice strojových čísel?
- Která z hodnot získaná počítáním se strojovými čísly je přesnější (to bez znalosti exaktní hodnoty nelze určit)?
Nyní oba výpočte proveďte v intervalové aritmetice, tj. s čísly Interval(x) + Interval(-eps(Float64), eps(Float64))
a Interval(y) + Interval(-eps(Float64), eps(Float64))
.
Získáme z příslušných výsledků nějakou informaci, kterou jsme v předchozích výpočtech s obyčejnými strojovými čísly neměli?
Odpovědi na tyto otázky uveďte v textu merge requestu, nebo přidejte do notebooku v repozitáři.
Interval{T}
Typ Modul IntervalArithmetic
bude exportovat parametrický typ Interval{T}
, který bude podtypem typu Real
, a kde T
je podtyp typu Real
.
Každá instance Interval{T}
bude mít atributy x1
a x2
typu T
udávající počáteční a koncový bod intervalu (situace, kdy x2
je menší než x1
nastat nesmí).
Obě hodnoty mohou být shodné.
Instance typu Interval{T}
půjde vytvořit pomocí standardního konstruktoru
Interval(1.0, 2.0)
Dále chceme pracovat i s intervaly nulové délky a tak musíme definovat konstruktory
Interval(x::T)
, který vytvoří instanci typuInterval{T}
se shodnými krajními bodyx
,Interval{T}(x::S)
, který vytvoří instanci typuInterval{T}
s krajními bodyT(x)
. Tento konstruktor je potřeba k tomu, aby nám dobře fungovaly algebraické operace s původními reálnými čísly (včetně celých čísel atp.), viz níže.
Algebraické operace
Podle popisu výše implementujte algebraické metody +
, -
, \*
a /
mezi objekty typu Interval{T}
se stejným typem T
.
Dále zajistěte, aby se tento nový typ choval přátelsky k již existujícím reálně-číselným typům a intervalům s jiným číselným typem.
Přesněji, například součet 1 + Interval(2, 3)
má velmi dobrý smysl jako Interval(3, 4)
a součet Interval(2, 3) + Interval(-1.0, -1.0)
jako Interval(1.0, 2.0)
.
Toho docílíme zadefinováním dvou pomocných metod promote_rule
a convert
:
import Base.promote_rule, Base.convert
promote_rule(::Type{Interval{T}}, ::Type{Interval{S}}) where { T <: Real, S <: Real } = Interval{promote_type(S, T)}
promote_rule(::Type{Interval{T}}, ::Type{S}) where { T <: Real, S <: Real } = Interval{promote_type(S, T)}
function convert(::Type{Interval{T}}, x::Interval{S}) where { T <: Real, S <: Real }
return Interval(T(x.x1), T(x.x2))
end
function convert(::Type{Interval{T}}, x::S) where { T <: Real, S <: Real }
return Interval(T(x), T(x))
end
Zájemci mohou více detailů nalézt v dokumentaci, ale výše uvedený snippet by měl být dostatečný.
show
Funkce Rozšiřte funkci show
o metodu umožňující pěkný výpis intervalu, k znázornění uzavřeného intervalu můžete využít hranatých závorek: [1, 2]
nebo vhodného unicode symbolu.
Ukázka použití
K otestování a ukázce použití je v repozitáři připraven notebook notebooks/demo.ipynb
, který obsahuje
- ukázky základního použití balíčku,
- demonstrace chování chyby u některých algoritmů.
Implementační poznámky
Julia balíček
Julia balíček bude vybaven automatizovanými unit testy a generátorem dokumentace pomocí Documenter.jl
.
Dokumentaci nemusíte vystavovat veřejně přístupnou na webu.
Stačí, když půjde lokálně vytvořit.
Inspirovat se můžete v balíčku, který jsme vytvářeli v semestru WLS.jl
Strukturu balíčku vytvořte ve svém repozitáři ve větvi solution/04-interval
.
Uživatel/opravující ho pak bude moci instalovat v Pkg
módu příkazem
] add https://gitlab.fit.cvut.cz/BI-JUL/B221/USERNAME#solution/04-interval
případně
] dev https://gitlab.fit.cvut.cz/BI-JUL/B221/USERNAME#solution/04-interval
Jak úkol vyřeším, odevzdám?
Snadno.
Při řešení zadání na větvi assignment/XY-TITLE
vytvořte vlastní
větev solution/XY-TITLE
vycházející z větve se zadáním.
Nyní pracujte, upravujte, dle libosti, větev solution/XY-TITLE
.
Pokud jste se stavem spokojeni, vytvořte merge request (MR) větve solution/XY-TITLE
do větve assignment/XY-TITLE
a přiřaďte mě (Tomáš Kalvoda) k tomuto (MR).
Při vytvoření MR také dojde k zobrazení stavu případných testů, které zadání obsahuje.
Detailní pokyny k tomuto procesu budou také vždy uvedeny přímo ve větvi se zadáním.