RAPORT Z BADAŃ WŁASNYCH OPRACOWANIE BIBLIOTEKI DOKŁADNYCH OBLICZEŃ NUMERYCZNYCH W JĘZYKU PYTHON MARCIN CIURA Streszczenie. W ramach badań opracowano bibliotekę procedur w języku Python, która realizuje podstawowe operacje arytmetyczne i funkcje matematyczne. Dzięki zastosowaniu leniwie wartościowanych ułamków łańcuchowych wyniki działań można podawać z dowolną dokładnością. Autor zaimplementował znane z literatury algorytmy wykonywania czterech działań arytmetycznych na ułamkach łańcuchowych oraz opracował nowe algorytmy obliczania wartości funkcji wykładniczej, logarytmów oraz funkcji trygonometrycznych i cyklometrycznych ułamków łańcuchowych. 1. Wprowadzenie W języku programowania Python dostępny jest standardowy zmiennoprzecinkowy typ danych float i biblioteka funkcji matematycznych math. Ponadto od wersji 2.4 języka wprowadzono doń bibliotekę stałoprzecinkową decimal operującą na liczbach dziesiętnych [1]. Do profesjonalnych obliczeń numerycznych można używać dodatkowej biblioteki gmpy, stanowiącej interfejs do biblioteki obliczeń wielokrotnej precyzji GNU Multiple Precision (GMP) [2]. Wspólną cechą tych bibliotek jest to, że wykonywane za ich pomocą obliczenia, choć szybkie, są obarczone błędami zaokrągleń. Cała dziedzina nauki – analiza numeryczna – powstała, by szacować wielkość tych błędów, nawarstwiających się przy stosowaniu algorytmów numerycznych. Idea niniejszej pracy jest odmienna: biblioteka będąca jej przedmiotem służy dokładnym obliczeniom. Przy jej stosowaniu nie występują błędy zaokrągleń. W literaturze przedmiotu opisano wiele podejść do dokładnej arytmetyki liczb rzeczywistych, z zastosowaniem ułamków łańcuchowych [3, 4], skalowanych liczb całkowitych [5, 6], przekształceń Möbiusa [7], i innych sposobów, które nie doczekały się praktycznych implementacji, jak układy pozycyjne o niecałkowitej lub ujemnej podstawie, zagnieżdżone ciągi przedziałów o końcach wymiernych, czy ciągi Cauchy’ego [8]. Niniejsza praca przedstawia użycie do dokładnych obliczeń ułamków łańcuchowych, co po raz pierwszy zaproponował Gosper [3]. 2. Ułamki łańcuchowe Skończony ułamek łańcuchowy to wyrażenie postaci (1) 1 a0 + 1 a1 + 1 a2 + a3 + 1 .. . + ak Słowa kluczowe. Dokładna arytmetyka liczb rzeczywistych, ułamki łańcuchowe, arytmetyka Gospera, szereg Ostrogradskiego-Sierpińskiego, leniwe wartościowanie, język programowania Python. 1 gdzie ogniwo a0 jest liczbą całkowitą, a kolejne ogniwa ai dla i > 0 to liczby naturalne dodatnie, przy czym ostatnie ogniwo ak jest większe od 1 [9, rozdz. XII]. Dla zaoszczędzenia miejsca zamiast wyrażenia (1) pisze się często [a0 ; a1 , a2 , a3 , . . . , ak ]. Nieskończony ułamek łańcuchowy to granica ciągu ułamków skończonych [a0 ; a1 , a2 , a3 , . . .] = lim [a0 ; a1 , a2 , a3 , . . . , ak ]. k→∞ Każdą liczbę rzeczywistą można jednoznacznie przedstawić w postaci ułamka łańcuchowego, przy czym liczbom wymiernym odpowiadają skończone ułamki łańcuchowe, a liczbom niewymiernym – nieskończone. Kolejne ogniwa ułamka łańcuchowego odpowiadającego danej liczbie wymiernej łatwo obliczać, stosując algorytm Euklidesa wyznaczania największego wspólnego dzielnika, np. 96 = 1 · 65 + 31 65 = 2 · 31 + 3 31 3 = = 10 · 3 + 1 3 · 1, a zatem 96 = [1; 2, 10, 3]. 65 Oto rozwinięcia niektórych liczb niewymiernych w ułamki łańcuchowe: √ 2 = [1; 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, . . .] √ − 2 = [−2; 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, . . .] √ 3 = [1; 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, . . .] e = [2; 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8, . . .] e1/2 = [1; 1, 1, 1, 5, 1, 1, 9, 1, 1, 13, 1, . . .] tg(1/2) = [0; 1, 1, 4, 1, 8, 1, 12, 1, 16, 1, 20, . . .] π = [3; 7, 15, 1, 292, 1, 1, 1, 2, 1, 3, 1, . . .] √ 3 2 = [1; 3, 1, 5, 1, 1, 4, 1, 1, 8, 1, 14, . . .]. Atrakcyjną własnością ułamków łańcuchowych jest to, że obcięte ułamki łańcuchowe (redukty) stanowią najlepsze możliwe przybliżenia liczb rzeczywistych. Po obliczeniu wartości początkowych ogniw rozwinięcia danej liczby rzeczywistej znana jest dokładność jej przybliżenia przez to rozwinięcie. Jest to spostrzeżenie kluczowe dla dalszej części pracy, w której przybliżane liczby są wynikami działań na innych ułamkach łańcuchowych. 3. Wymagane cechy języka programowania Do obliczeń na ułamkach łańcuchowych zgodnych z algorytmami przedstawionymi poniżej potrzebne są tylko liczby całkowite, jednak ich wartość bezwzględna może być dowolnie duża, większa niż pojemność słowa maszynowego. Aby zatem móc w jakimś języku programowania wygodnie zaimplementować podane algorytmy, powinien być w nim dostępny typ całkowity o nieograniczonym zbiorze dopuszczalnych wartości. Wymaganie to spełnia język Python – od zawsze można w nim było korzystać z typu wbudowanego long, a od wersji 2.3, wydanej w 2003 roku, został on w pełni zunifikowany z typem int, odpowiadającym słowu maszynowemu. Oznacza to, że wyrażenia całkowite, których wartość wykracza poza zakres typu int, są automatycznie konwertowane do typu long. 2 Sposób wykonywania obliczeń w opracowanej bibliotece różni się od zwykłego, gorliwego wartościowania (ang. eager evaluation). Opiera się on mianowicie na leniwym wartościowaniu (ang. lazy evaluation): przy tworzeniu wyrażenia arytmetycznego nie są wykonywane żadne obliczenia na liczbach. Zamiast tego powstaje skierowany acykliczny graf podwyrażeń. Od wierzchołka tego grafu, odpowiadającego całemu wyrażeniu, można następnie kolejno żądać tyle ogniw bądź cyfr dziesiętnych, ile zachodzi potrzeba. Wierzchołek ten z kolei żąda ogniw z wierzchołków z nim połączonych, które odpowiadają jego bezpośrednim podwyrażeniom, i tak dalej, aż do wierzchołków wejściowych, odpowiadających stałym liczbom. Dzięki leniwemu wartościowaniu wykonuje się tylko tyle obliczeń, ile jest w danej chwili potrzebne, a podwyrażenia dynamicznie dopasowują swoją dokładność, aby cyfry otrzymywane z wartościowania całego wyrażenia zawsze były prawidłowe. Istnieją języki programowania, np. Haskell [10], oparte w całości na koncepcji leniwego wartościowania, ale i w języku Python można z niego wygodnie korzystać dzięki generatorom, wprowadzonym do niego eksperymentalnie w wersji 2.2 i na stałe w wersji 2.3. Generatory są podobne do funkcji, zwracających tablice, lecz w odróżnieniu od nich przy kolejnych wywołaniach zwracają wynik element po elemencie. Dzięki temu funkcja wywołująca generator może rozpocząć przetwarzanie wyniku nie czekając, aż zostanie on w całości wygenerowany, co zresztą byłoby w ogóle niemożliwe, jeśli generator jest nieskończony. Ponieważ tworzenie wyrażeń arytmetycznych wiąże się z powstaniem grafu powiązanych zależnościami obiektów, konieczne jest zwalnianie pamięci, zajmowanej przez obiekt, gdy przestanie on być potrzebny, czyli przestaną być potrzebne wszystkie obiekty, które korzystały z wyników przezeń generowanych. Zarządzanie pamięcią znacznie się upraszcza przy zastosowaniu odśmiecania pamięci (ang. garbage collection), czyli automatycznego zwalniania niepotrzebnych obszarów pamięci. Język Python jest od swej pierwszej wersji wyposażony w automatyczny odśmiecacz pamięci. 4. Działania arytmetyczne Algorytmy czterech działań arytmetycznych na leniwie obliczanych ułamkach łańcuchowych podał Gosper [3, pkt. 101]. Polegają one na sprowadzeniu sum, różnic, iloczynów i ilorazów do ogólnej funkcji bihomograficznej, której kolejne ogniwa mogą być generowane na podstawie kolejnych – generowanych na żądanie – ogniw obu operandów. Każdy system dokładnej arytmetyki liczb rzeczywistych napotyka problem tego, że równość liczb niewymiernych jest nierozstrzygalna w skończonej liczbie kroków. Dotyczy to też opisywanego w tej pracy systemu: jeśli dowolnie wiele początkowych ogniw dwóch nieskończonych ułamków łańcuchowych jest parami równe, to nie wiadomo, czy kolejne ogniwa będą dalej parami równe. Problem nierostrzygalny występuje nie tylko przy porównywaniu liczb, ale i wtedy, gdy argumenty operacji są niewymierne, a jej wynik prawdopodobnie wymierny. Możliwe jest idealistyczne rozwiązanie tego problemu: udokumentowanie faktu, że takie obliczenia nigdy się nie skończą. Autor umożliwił taką wersję działania opracowanej biblioteki – osiąga się ją przez ustawienie parametru kmax , dostępnego dla programisty, na wartość ujemną. Domyślne ustawienie tego parametru jest jednak inne; autor sądzi, że bardziej praktyczne. Otóż jeśli algorytm Gospera wykona kmax iteracji z jednakowymi wartościami dolnego i górnego ograniczenia na następne generowane ogniwo, to generuje owo górne ograniczenie, a po nim symbol None, oznaczający nieskończone ogniwo, czyli koniec ułamka łańcuchowego. Podobnie jeśli funkcja służąca do porównywania liczb pobierze po kmax /2 parami równych ogniw z obu swoich argumentów, to decyduje, że są one równe. 3 Algorytm Gospera potrzebuje 81 iteracji, aby odkryć, że √ (math.sqrt(5) − 1)/2 − ( 5 − 1)/2 > 0, gdzie math.sqrt() to funkcja obliczająca pierwiastek jako liczbę zmiennoprzecinkową, a funkcji porównującej liczby potrzeba 39 = 78/2 iteracji, by odkryć, że powyższa odjemna i odjemnik są nierówne. √ Jest to przypadek pesymistyczny: każde ogniwo liczby ( 5 − 1)/2 odpowiada za √ przyrost dokładności o log10 ( 5 + 1)/2 ≈ 0,21 cyfr dziesiętnych, jednak twierdzenie Lochsa [11] głosi, że w prawie każdym (w sensie Lebesgue’a) ułamku łańcuchowym każde kolejne ogniwo powoduje przyrost dokładności średnio o π 2 /(6 ln 2 ln 10) ≈ 1,03 cyfry dziesiętnej. Badania empiryczne [12] wskazują, że wariancja liczby cyfr dziesiętnych przypadających na jedno ogniwo wynosi w przybliżeniu 0,62. Po kmax = 100 iteracjach z niezmienionymi wartościami dolnego i górnego ograniczenia na kolejne ogniwo liczba poprawnych cyfr po przecinku w wartości nie wygenerowanego jeszcze ogona ułamka łańcuchowego ma rozkład w przybliżeniu normalny z wartością oczekiwaną 1,03 × 100 i wariancją 0,62 × 100. Zatem różnica między dokładną wartością ogona a wygenerowaną liczbą całkowitą ma rozkład w przybliżeniu logarytmiczno-normalny o wartości oczekiwanej 10−1,03×100+(0,62×100 ln 10)/2 ≈ 2,07−100 , czyli z grubsza pomiędzy 10−32 a 10−31 . Algorytm generuje wtedy parę ogniw (a, ∞), podczas gdy poprawny wynik to ciąg (a, a0 , . . .) lub (a − 1, 1, a00 , . . .), gdzie a0 ­ 1031 , a00 ­ 1031 − 1. Zgodnie z twierdzeniem Gaussa-Kuźmina [13, 14] dowolne ogniwo w rozwinięciu prawie każdej liczby na ułamek łańcuchowy jest większe od n0 = 1031 z prawdopodobieństwem log2 (1 + 1/n0 ) = log2 (1 + 10−31 ) ≈ 10−31 / ln 2 ≈ 1,5 × 10−31 . Zatem przy kmax = 100 prawdopodobieństwo, że algorytm błędnie obetnie rozwinięcie losowej liczby w ułamek łańcuchowy nie przekracza wartości 1,5 × 10−31 pomnożonej przez liczbę obliczonych ogniw. Podobny problem występuje, gdy funkcja o argumentach niewymiernych ma wymierny wynik, np. eln 5 , czy cos(π/3). Został on podobnie rozwiązany w opracowanej bibliotece. Na rysunku 1 przedstawiono sprawdzenie kilku przykładowych tożsamości, wykonane w trybie interaktywnym interpretera Pythona z zastosowaniem opracowanej biblioteki, a możliwe właśnie dzięki decyzji projektowej objaśnionej powyżej. W przedostatnim przykładzie porównywane liczby są nierówne, gdyż liczby zmiennoprzecinkowe to w gruncie rzeczy liczby wymierne o dużym mianowniku, a zatem skończone ułamki łańcuchowe, które mają mniej niż kmax ogniw. W ostatnim przykładzie biblioteka sygnalizuje równość porównywanych liczb, ponieważ reprezentują je nieskończone ułamki łańcuchowe, z których porównano tylko kmax początkowych ogniw. 4 >>> (sqrt(2) - 1)*(sqrt(2) + 1) 1. >>> sqrt(5 + 2*sqrt(6)) == sqrt(2) + sqrt(3) True >>> [sin(x)**2 + cos(x)**2 for x in ... [pi*random(), pi*random(), pi*random()]] [1., 1., 1.] >>> [cos(3*x)/cos(x) == cos(x)**2 - 3*sin(x)**2 for x in ... [pi*random(), pi*random(), pi*random()]] [True, True, True] >>> [x == x + cf(10)**-1000 for x in ... [cf(random()), cf(random()), cf(random())]] [False, False, False] >>> sqrt(2) == sqrt(2) + cf(10)**-1000 True Rysunek 1. Przykładowe tożsamości i wyniki ich sprawdzenia 5. Funkcje przestępne i π Do funkcji przestępnych zalicza się funkcję wykładniczą i logarytmiczną oraz funkcje trygonometryczne i cyklometryczne. Nie wszystkie z nich trzeba było bezpośrednio implementować w opracowanej bibliotece, a to dzięki poniższym tożsamościom: x . x sin x = 2 tg 1 + tg2 2 2 . x 2 x cos x = 1 − tg 1 + tg2 2 2 x arc sin x = arc tg √ 1 − x2 arc cos x = π/2 − arc sin x xy = ey ln x log10 x = ln x/ ln 10. Po uwzględnieniu tych tożsamości, aby uzyskać zastępnik standardowej biblioteki math, pozostaje zdefiniować funkcje sqrt(), exp(), tan(), log(), atan() oraz liczbę pi. Leniwy algorytm obliczania pierwiastka kwadratowego oparto na metodzie Newtona (stycznych), dzięki czemu jest on szybszy, niż przy zastosowaniu tożsamości x1/2 = e(ln x)/2 . Algorytm rozpoczyna się od obliczenia wartości a, równej całko√ witoliczbowemu przybliżeniu x. Tak długo, jak ogniwa liczb a i x/a są równe, należy je emitować; w przeciwnym wypadku należy obliczyć następne przybliżenie a := (a + x/a)/2. Zbieżność tego algorytmu jest kwadratowa, co oznacza, że po każdym kolejnym wyznaczeniu wartości a liczba poprawnych cyfr dziesiętnych wyniku w przybliżeniu się podwaja. Algorytmy obliczania wartości pozostałych funkcji przestępnych autor oparł na rozwijaniu argumentu w szereg Ostrogradskiego drugiego rodzaju [15], który poprawniej powinien się zwać szeregiem Ostrogradskiego-Sierpińskiego [16]: x = bxc + 1/q1 − 1/q2 + 1/q3 − 1/q4 + . . . , gdzie kolejne mianowniki qk wybiera się zachłannie największe z możliwych, przy zachowaniu tego, by kolejne sumy częściowe szeregu przybliżały x na przemian z dołu i z góry. 5 Po składniku ±1/q kolejny składnik jest co do wartości bezwzględnej równy co najwyżej 1/(q 2 + q), zatem liczba poprawnych cyfr w przybliżeniu co najmniej się podwaja z każdym składnikiem. Przypadkiem pesymistycznym jest rozwijanie w szereg Ostrogradskiego-Sierpińskiego liczb o części ułamkowej równej stałej Cahena 1/1 − 1/2 + 1/6 − 1/42 + 1/1806 − 1/3263442 + 1/10650056950806 − . . . Leniwe obliczanie wartości funkcji ex polega na leniwym rozwijaniu x w szereg Ostrogradskiego-Sierpińskiego z pamiętaniem dwóch ostatnich sum częściowych sk−1 i sk oraz zastosowaniu poniższych zależności do obliczania esk−1 i esk : e1/q = [1; q − 1, 1, 1, 3q − 1, 1, 1, 5q − 1, . . .], ex+y = ex ey , ex−y = ex /ey . Tak długo, jak ogniwa liczb esk−1 i esk są równe, należy je emitować; w przeciwnym przypadku należy obliczyć następną sumę częściową sk+1 . Algorytm leniwego obliczania wartości tg x jest analogiczny, z tym, że korzysta z zależności tg(1/q) = [0; q − 1, 1, 3q − 2, 1, 5q − 2, 1, 7q − 2, . . .], tg x ± tg y . 1 ∓ tg x tg y Wartość ln x oblicza się leniwie, leniwie znajdując sumy częściowe szeregu Ostrogradskiego-Sierpińskiego tak, żeby tg(x ± y) = esk−1 < x ¬ esk lub esk−1 > x ­ esk . Tak długo, jak ogniwa liczb sk−1 i sk są równe, należy je emitować; w przypadku ich nierówności – obliczyć następną sumę częściową sk+1 . Wartości funkcji arc tg x oblicza się tak samo, jak logarytmu naturalnego, tylko z tg sk−1 i tg sk zamiast esk−1 i esk . Kwadratowa zbieżność powyższych algorytmów wynika z kwadratowej zbieżności szeregu Ostrogradskiego-Sierpińskiego. Liczbę π oblicza się leniwie na podstawie uogólnionego ułamka łańcuchowego 1 π =3+ 9 6+ 6+ 25 49 6+ 81 6+ .. . Algorytm ten ma zbieżność liniową, ale okazał się w praktyce szybszy od algorytmów o wyższym rzędzie zbieżności. 6. Podsumowanie Autor opracował bibliotekę dokładnych obliczeń na liczbach rzeczywistych w języku programowania Python, opartą na leniwie wartościowanych ułamkach łańcuchowych. Z adresu WWW http://sun.aei.polsl.pl/~mciura/cf.py można pobrać kod źródłowy biblioteki. Działania arytmetyczne w opracowanej bibliotece opierają się na algorytmie Gospera, przy czym autor wprowadził doń heurystykę, przerywającą pętle nieskończone. Wartości funkcji przestępnych są obliczane przez autorskie algorytmy, oparte na rozwijaniu liczb w szereg Ostrogradskiego-Sierpińskiego. 6 Opracowana biblioteka może znaleźć zastosowanie przy badaniu zjawisk chaotycznych, w geometrii obliczeniowej i numerycznej teorii liczb, a także w programach edukacyjnych. Szybkość wykonywania działań arytmetycznych jest zadowalająca, natomiast czas potrzebny do obliczenia wyników bardziej skomplikowanych wyrażeń zawierających funkcje przestępne można liczyć nawet w sekundach. Literatura [1] G. van Rossum, F.L. Drake, Jr. (ed.), The Python Language Reference Manual (version 2.5), Network Theory Ltd, 2006. [2] The GMP Team, The GNU Multiple Precision Arithmetic Library, Free Software Foundation, 2007. [3] M. Minsky, B. Gosper, M. Beeler et al., Artificial Intelligence Memo No. 239, Massachusetts Institute of Technology, A.I. Laboratory, 1972. [4] J. Vuillemin, Exact real computer arithmetic with continued fractions, Proceedings of the 1988 ACM conference on LISP and functional programming, ACM, 1988, 14–27. [5] H.-J. Boehm, R. Cartwright, M. Riggle, M.J. O’Donnell, Exact real arithmetic: a case study in higher order programming, Proceedings of the 1986 ACM conference on LISP and functional programming, ACM, 1986, 162–173. [6] K. Briggs, Implementing exact real arithmetic in Python, C++ and C, Theoretical Computer Science 351 (2006), 74–81. [7] P. Potts, Exact real arithmetic using Möbius transformations, rozprawa doktorska, Department of Computing, Imperial College of Science, Technology and Medicine, University of London, 1999. [8] M. Escardó, Introduction to Exact Numerical Computation, School of Computer Science, St. Andrews University, 2000. [9] W. Sierpiński, Teoria liczb, PWN, 1950. [10] S. Peyton Jones (red.), Haskell 98 language and libraries: the Revised Report, Cambridge University Press, 2003. [11] G. Lochs, Vergleich der Genauigkeit von Dezimalbruch und Kettenbruch, Abhandlungen aus dem Mathematischen Seminar der Universität Hamburg 27 (1964), 142–144. [12] W. Bosma, K. Dajani, C. Kraaikamp, Entropy and counting correct digits, Raport nr 9925, Department of Mathematics, University of Nijmegen, 1999. [13] C.F. Gauß, Werke, tom 101 , 552–556. [14] R.O. Kuzmin, Sur un problème de Gauss, Atti del Congresso Internazionale dei Matematici, Bologna, 1928, 83–89. [15] J.J. Remez, O zakonomiernych riadach, kotoryje mogut byt’ swiazany s dwumia ałgoritmami M. W. Ostrogradskogo dla pribliżenija irracyonalnych czisieł, Uspiechi matiematiczeskich nauk 6 (1951), 33–42. [16] W. Sierpiński, O kilku algorytmach dla rozwijania liczb rzeczywistych na szeregi, Sprawozdania z posiedzeń Towarzystwa Naukowego Warszawskiego, Wydział III 4 (1911), 56–77. Instytut Informatyki, Politechnika Śląska, 44-100 Gliwice Adres poczty elektronicznej: [email protected] 7