środa, 15 kwietnia 2020

Zbiór Cantora

Zbiór Cantora – podzbiór prostej rzeczywistej opisany w 1883[1] przez niemieckiego matematyka Georga Cantora. Zbiór ten odkrył w 1875 Henry John Stephen Smith.
Zbiór Cantora jest najprostszym przykładem fraktala.

Klasyczny zbiór Cantora (zwany także trójkowym zbiorem Cantora) to podzbiór przedziału domkniętego [0,1]. Jego konstrukcja polega na usuwaniu z odcinka jego środkowej 1/3 i aplikowaniu tej samej zasady rekurencyjnie do dwu pozostałych pod-odcinków. W świecie idealnym, po nieskończonej liczbie iteracji ;-) powstaje nam bardzo rozproszony podzbiór punktów z zadanego zakresu. W świecie grafiki komputerowej nie ma oczywiście sensu zajmować się czymś co jest poniżej rozdzielczości ekranu, musimy więc konstrukcję naszego zbioru zatrzymać na rozmiarze pojedynczego piksela.
Organizacja programu jest podobna do poprzedniego (testującego rekurencyjną procedurę rysowania linii kropkowanej) . Procedura setup() ustawia parametry okna i wywołuje testowaną procedurę rekurencyjną. Tyle że w tym wypadku dwie nieco odmienne.

Pokazana procedura cantorSetHor1() każdą iterację zbioru rysuje na innej linii ekranu, posługując się wartością d czyli długością odcinka dzielonego na danym etapie. Zbiór jest zdefiniowany w zakresie liczb rzeczywistych, więc posługujemy się ich przybliżeniem - typem float. Będziemy mieć z tym pewien problem, bo całość mapujemy na CAŁKOWITE współrzędne pikseli okna. Do tego dochodzi "skłonność" procedury line() do włączania końcowych punktów, co przy granicznych długościach powodowałoby asymetryczne nakładanie się rysowanych linii. Stąd jawnie zabieramy obu bocznym liniom punkty graniczące z linią środkową, czyli ta formalnie wycinaną, a praktycznie kolorowaną na zielono. Na wszelki wypadek kolorujemy też punkt środkowy odcinka. Będzie to widoczne tylko wtedy gdy linia zrobi się bardzo, bardzo krótka ;-)

Po wykonaniu rysowania (linie 26-33) wywołujemy znowu funkcję cantorSetHor1() dla prawego i lewego pod-odcinka (linia 35).

Alternatywna procedura cantorSetHor2() zbudowana jest niemal identycznie. Jedyna różnica to współrzędna Y okna używana w rysowaniu. Zawsze jest to height/2 , co powoduje że wszystkie efekty rysowania trafiają w tą samą linie. No i kolory zielony został zamieniony na 'cyan', a 'magenta' na czerwony.


Możemy się temu lepiej przyjrzeć, jeśli zmienimy w setup'ie grubość linii np. na 5. MUSIMY TEŻ WTEDY ZMIENIĆ  SPOSÓB KOŃCZENIA LINI!

strokeWeight(5); strokeCap(SQUARE);


Rekurencja

"Rekurencja, zwana także rekursją (ang. recursion, z łac. recurrere, przybiec z powrotem) – odwoływanie się np. funkcji lub definicji do samej siebie.
W logice wnioskowanie rekurencyjne opiera się na założeniu istnienia pewnego stanu początkowego oraz zdania (lub zdań) stanowiącego podstawę wnioskowania (przy czym, aby cały dowód był poprawny, zarówno reguła, jak i stan początkowy muszą być prawdziwe). Istotą rekurencji jest tożsamość dziedziny i przeciwdziedziny reguły wnioskowania, wskutek czego wynik wnioskowania może podlegać tej samej regule zastosowanej ponownie."
"Rekurencja jest podstawową techniką wykorzystywaną w funkcyjnych językach programowania. Należy jednak zachować ostrożność przy używaniu rekurencji w rzeczywistych programach. Ryzyko istnieje szczególnie przy przetwarzaniu dużej ilości głęboko zagnieżdżonych danych."
Z Wikipedii
W praktyce rekurencja sprowadza się do wielokrotnego wywoływania funkcji przez samą siebie, coraz bardziej "w głąb stosu", dla coraz mniej skomplikowanego zadania, aż dochodzimy do trywialnej jego wersji i dalsze zagłębianie się nie ma już sensu. 
Rekurencja jest przez teoretyków programowania uznawana za samo sedno inteligencji algorytmicznej. Wg. nich nie jest programistą ten kto rekurencji nie rozumie ;-)
Ja nie byłbym tak ortodoksyjny, ale jednak uważam, że warto tą ideę wytłumaczyć, bo bez tego trudno o zrozumienie grafik fraktalnych, które są po prostu ładne :-)

Temat rekurencji wprowadza się zwykle za pomocą funkcji silni czy jakiś ciągów - np. Fibbonacciego. Ale to czysta matematyka, a ja w tym kursie staram się używać raczej grafiki, jako trafiającej do większej liczby odbiorców.
W grafice komputerowej raczej unika się algorytmów rekurencyjnych, jako mało efektywnych, ale istnieje jeden, historycznie istotny. To "wypełnianie przez sianie" czy też, wg. bardziej popularnej nazwy angielskiej "flood fill".
Niestety algorytm ten wymaga możliwości odczytania koloru punktu, który już jest na ekranie (czy w oknie), której jednak w Processingu nie znalazłem. Jest taka, która pozwala czytać kolor punktu załadowanego obrazka, co może kiedyś jeszcze wykorzystamy.

Dla ilustracji prostej graficznej procedury rekurencyjnej użyjemy procedury rysującej linię przerywaną. Jest ona oczywiście bardzo nieefektywna i do wymagających obliczeniowo zadań się nie nadaje, ale dydaktycznie powinna być wystarczająca.

Wynikiem działania programu będą czarne kropki naniesione wzdłuż dowolnego odcinka prostej. Ich gęstość może być różna. Na obrazki limit gęstości wynosi 10. Żółte tło pod kropkami stanowi "kontrolę" narysowaną zwykłą funkcją line()  Processingu.




Moglibyśmy te kropki oczywiście narysować też za pomocą pętli. Większość algorytmów rekurencyjnych da się zmienić na klasyczne, choć zapis rekurencyjny jest zazwyczaj dużo prostszy.
Oto kompletny program:


Funkcja setup() służy nam jak zwykle do ustawień, oraz do pierwszego wywołania naszej rekurencyjnej funkcji bline() (linia 12.) i narysowania kontrolnej linii klasycznym algorytmem (linia 11.). Funkcji draw() nie implementujemy bo jest w tym teście niepotrzebna.

Sama funkcja jest dosyć prosta. Najpierw sprawdzamy czy zadany odcinek jest wystarczająco długi żebyśmy chcieli postawić w nim kropkę (linie 17-20). Długość liczymy po prostu jako odległość Euklidesa między punktami.
Potem obliczamy współrzędne punktu leżącego pośrodku odcinka wyznaczonego przez parametry wywołania. Kolejność dalszych operacji jest dowolna. U mnie najpierw następują rekurencyjne wywołania dla obu połówek odcinka, a potem dopiero zaznaczenie punktu środkowego za pomocą point(), ale równie dobrze można zrobić odwrotnie.
Każde zatem wywołanie rysuje jeden punkt i próbuje narysować kolejne używając wywołania tej samej funkcji. Za zakończenie tego procesu odpowiada zmienna limit (linia 4.)

Jeśli zrozumiecie jak to działa, kolejne programy, rysujące znane fraktale będą dla was dużo bardziej zrozumiałe.