poniedziałek, 15 maja 2017

Początki programowania obiektowego

Dawno się do was nie odzywałem... Cóż, "taki life" :-) Zwłaszcza że ten rozdział będzie trudny i nie mogłem się zebrać do jego napisania...
Ale wiosna przyszła i pora wrócić do nauki.

Wchodzimy dziś w zupełnie nowy obszar, albo jak mówią informatycy "zmieniamy paradygmat". Poprzednio trzymaliśmy się programowania strukturalnego programowania proceduralnego, choć tak naprawdę było w tym drobne oszustwo wynikające z głębokiej semantyki języka Processingu.

Teraz zaczniemy jawnie korzystać z możliwości programowania obiektowego.

Co to oznacza? Otóż zaczniemy rozumieć, że używamy predefiniowane obiekty,  a także nauczymy się je sami definiować. 

Czym są obiekty? Pewnie jeszcze nie skorzystaliście z linków do Wikipedii to wam ją przytoczę :-)


  • "Obiekt to podstawowe pojęcie wchodzące w skład paradygmatu programowania obiektowego w analizie i projektowaniu oprogramowania oraz w programowaniu. W tym paradygmacie programy definiuje się za pomocą obiektów – elementów łączących stan (czyli dane, nazywane najczęściej polami) i zachowanie (czyli procedury, tu: metody). Obiektowy program komputerowy wyrażony jest jako zbiór takich obiektów, komunikujących się pomiędzy sobą w celu wykonywania zadań."

Nie będę już dalej cytował teorii - ale zanim przejdziemy dalej zapoznajcie się z nią używając Wiki lub "wujka Google".
Przejdźmy do naszego nowego przykładu. Od razu mamy w nim obiekty
 

Mamy w nim od razu deklaracje dwóch obiektów. World jest tablicą, a tablice w Processingu są obiektami. Podobnie output ,  który jest narzędziem do zapisywania pliku tekstowego na dysku.
Mamy też tajemnicze RGB - "klasę" czyli "typ",  o którym napisano że dopiero zostanie zdefiniowany.  Tablica i PrintWriter są obiektami predefiniowanymi, które dostajemy w ramach języka Processing. RGB to będzie właśnie nasz własny typ obiektowy służący do przechowywania koloru rozbitego na poszczególne składowe. Zdefiniujemy go za chwilę.

Po czym to poznajemy że jakiś typ jest obiektem? Najłatwiej po sposobie użycia. Żebyśmy mogli używać tablicy World musimy najpierw użyć operatora new. Czyli w przeciwieństwie do int czy float samo zadeklarowanie nie wystarcza.
Podobnie będzie z typem elementu tablicy World. W przeciwieństwie do poprzednich naszych programów, gdzie elementy tablicy były albo typu int albo float, RGB nie jest typem prostym, ale klasą, czyli typem obiektowym. A skoro go sami wymyśliliśmy, to musimy zdefiniować (czyli powiedzieć dokładnie) czym jest:


Składnia takiej deklaracji polega na zamknięciu w strukturę class Name {   }
deklaracji danych i deklaracji funkcji/procedur. Te dane opisują stan obiektu, procedury opisują możliwe sposoby zmiany tego stanu. Dane nazywane są też atrybutami, a funkcje i procedury  metodami.
W naszym przykładzie mamy trzy atrybuty każdego obiektu RGB, trzy liczby typu int nazwane R, G i B. Mogłyby się nazywać też Red, Green, Blue, albo jeszcze inaczej (np. SkladowaCzerwona, SkladowaZielona, SkladowaNiebieska), ale po co komplikować sobie zapis?
Poza atrybutami mamy też kilka metod: Visualise(), isEmpty(), Set() oraz RGB().
Procedura Visualise() ma za zadanie narysowanie graficznej reprezentacji koloru w postaci punktu lub kwadratu w miejscu ustalonym przez współrzędne XY. Współrzędne odnoszą się jednak do położenia w tablicy World , a nie do okna, bo okno może być większe od tablicy dzięki "mnożnikowi" czyli współczynnikowi skali W.
Wyświetlanie odbywa się jednak tylko wtedy gdy obiekt RGB nie jest pusty, co jest sprawdzane specjalną funkcją zwracającą wartość logiczną (boolean).
Funkcja isEmpty() sprawdza czy obiektowi RGB nadano jakiś kolor inny niż czarny - czyli czy któraś ze składowych różni się od 0
Do zmiany wszystkich składowych "za jednym zamachem" służy procedura Set().
A do czego służy procedura czy też funkcja o nazwie RGB czyli takiej samej jak cały typ?
To jest tzw. konstruktor. Jest używany przez operator new do zainicjowania obiektu. Powinien zatem nadawać mu jakiś ściśle zdefiniowany stan. Nasz używa instrukcji R=G=B=0; żeby wszystkim składowym nadać stan 0. Konstruktor inkrementuje też zmienną RGB_Counter, dzięki czemu wiemy ile stworzyliśmy już obiektów typu RGB.
Jeszcze jedno. Definiując konstruktor nie podajemy typu jaki zwraca. Taka jest konwencja we wszystkich językach obiektowych jakie znam.  

Skoro typ RGB już mamy zdefiniowany, pora na jego użycie. 





Że RGB jest obiektem, widać już w procedurze setup. Żeby zacząć używać komórki tablicy World nusimy w niej najpierw utworzyć obiekt typu RGB za pomocą operatora new (linia 51).

Potem za pomocą metody Set() nadajemy mu jakiś kolor (linia 54, albo alternatywne 52 i 53 jeśli zdecydujemy się inaczej zakomentować), a za pomocą metody Visualise() rysujemy w środku okna (linia 55), jako że jest to akurat środkowy element tablicy.
Zwróćcie uwagę jak dostajemy się do tych metod.
Indeksowanie tablicy pozwala nam wybrać element tablicy. Na razie bezpiecznie możemy wybrać tylko obiekt w położeniu [Side/2][Side/2] bo tylko on został utworzony. Ale co ma zrobić obiekt? Do tego wyboru służy "kropka" (czyli operator wyboru składowej).
Po kropce możemy w Processingu napisać nazwę dowolnej metody, a także dowolnego atrybutu obiektu (w innych językach obiektowych sprawa jest nieco bardziej skomplikowana).


Nieco podobnie ma się sprawa z typem PrintWriter, który też występuje powyżej. Jest co prawda tworzony nie za pomocą operatora new ale specjalnej funkcji bibliotecznej createWriter() , ale operator new musi zapewne znajdować się wewnątrz tej funkcji.
Natomiast użycie odbywa się podobnie do pisania na konsolę tekstową Processingu, jednak właśnie z użyciem "operatora kropki". Pisząc output.println() wybieramy spośród różnych metod możliwych dla obiektu output typy PrintWriter operację println().


No to teraz pora na deser. Użyjemy sobie naszego typu do jakiejś ładnej symulacji definiując procedurę draw():




Procedura draw() wrzuca najpierw do pliku (linia 70) numer kroku i liczbę istniejących obiektów RGB. Pozwoli nam to wykonać w arkuszu kalkulacyjnym albo naszym własnymgramie wykres wzrostu. Właściwe działanie procedury odbywa się zaś w bloku dla warunku if(!Stop) - czyli dopóki nie nadamy zmiennej Stop wartości true.


W obrębie tego bloku powinniście rozpoznać znany już początek pętli kroku Monte-Carlo (Linie 75-79). Reszta działań odbywa się tylko wtedy gdy wylosowany element tablicy zawiera obiekt typu RGB co sprawdzamy za pomocą warunku World[Y][X] != null . Dla takiej sytuacji losujemy współrzędne Xt i Yt jakiejś sąsiedniej komórki w obrębie sąsiedztwa Moora (linie 82-83) i jeśli jest ona realnie w tablicy i jest pusta (warunek w liniach 84-85) dokonujemy jej zasiedlenia przez nowy obiekt, który będzie "potomkiem" obiektu znajdującego się w komórce Y X.

Potomek nie jest identyczną kopią, ponieważ z dużym prawdopodobieństwem może zajść mutacja jego składowych koloru (w liniach 89-94) dokonana metodą błądzenia losowego i dopiero takie zmutowane składowe są przypisywane na potomka (linia 96).
Na koniec sprawdzamy czy nie zasiedliliśmy właśnie jednego z czterech rogów macierzy, czyli punktów najbardziej odległych od środka (w programie sprawdzamy tylko jeden róg, ale nic nie stoi na przeszkodzie żebyście ten warunek uzupełnili). Jeśli doszliśmy do rogu to dalsze działanie modelu uznajemy za niecelowe i ustawiamy zmienną Stop na true.

A teraz małe wyjaśnienie w ramach post scriptum. Napisałem na początku że z paradygmat u obiektowego korzystaliśmy właściwie cały czas, choć o tym nie wiedzieliśmy. To fakt. Processing jest "nakładką" na język Java, a ten jest w 100% obiektowy. Cała aplikacja w Processingu jest więc obiektem języka Java, którego jedną, dwie lub trzy metody redefiniujemy (czy też  nadpisujemy). Te metody to draw(), setup() i ewentualnie exit(). Rozwiniemy tą kwestię przy okazji "dziedziczenia".


niedziela, 2 kwietnia 2017

Model wpływu czy model Isinga

Prezentowany poprzednio dynamiczny model wpływu społecznego Nowaka-Latane bardzo przypomina komórkową implementacje bardzo słynnego w fizyce statystycznej modelu Isinga.
Możemy go jeszcze bardziej zbliżyć do wersji fizyków dodając szum (Noise), czyli zupełnie spontaniczne zmiany "poglądu", a także możliwość zamiennego stosowania "reguły większości" ("majority rule") i odwrotnej "reguły mniejszości" ("minority rule"), w której agent zawsze chce mieć pogląd mniejszościowy (np. dlatego, że to wyróżnia z tłumu). Polega to na odwróceniu znaków wyjścia reguły za pomocą zmiennej MajorityRule równej 1 lub -1, pod warunkiem że poglądy są zdefiniowane także jako 1 lub -1 (patrz. setup() )


Ponadto użyjemy też zapisu statystyki symulacji do pliku tekstowego rozdzielanego tabulacjami, co pozwoli wykonać wykres zmian w czasie w Excelu, Calcu lub innym programie czytającym takie dane.
Ponieważ dane do takiego pliku są przez program najpierw wpisywane do buforu w pamięci, a na dysk dopiero jak zgromadzi się ich odpowiednio dużo, plik musi zostać prawidłowo zamknięty w momencie kończenia programu. Dlatego definiujemy też procedurę exit(), wywoływaną automatycznie przez system przy każdym poprawnym zakończeniu w której właśnie znajdują się wywołania sprzątające i zamykające plik wyjściowy (flush() i close() );

Pozostała nam procedura Count() wywoływana w każdym draw() w celu zliczenia liczby "czerwonych" agentów, czyli tych których stan jest równy 1, oraz także wywoływana z draw() właściwa procedura symulacji DoMonteCarloStep().
W porównaniu z wyjściowym programem modelującym tylko wpływ mamy dwie ważne modyfikacje. Po pierwsze sprawdzamy czy "nie wdał nam się szum" (linia 81) losując liczbę z zakresu 0..1 i sprawdzając czy jest mniejsza od zadanego poziomu szumu. Jeśli tak, to po prostu zmieniamy stan agenta na przeciwny (linia 83), a właściwą regułę modelu stosujemy tylko wtedy gdy dany agent w tym kroku nie został trafiony szumem (linie 87-99).
W tej regule po zsumowaniu wpływów ustalamy odpowiedź agenta, ale mnożąc 1 i -1 przez zmienną MajorityRule. Jeśli jest ona równa 1 to wynik się nie zmienia, ale jeśli jest równa -1, o efekt działania reguły zostaje odwrócony.
Poniżej wynik działania programu dla małych kilkuset kroków "reguły większości". Wynik dla reguły mniejszości was zaskoczy :-)



środa, 1 marca 2017

Wpływ społeczny - reguła większości

Przyszła pora zaimplementować pierwszy prawdziwy model symulacyjny z psychologii społecznej. Będzie to najprostsza forma modelu dynamicznego wpływu społecznego w postaci probabilistycznego automatu komórkowego 2D.
Komórki automatu (w modelach psychologicznych częściej zwane agentami, ale w tym przypadku byłoby to ewidentnie "na wyrost") posługują się bardzo prostą regułą:
"Dopasowujesz swój stan -1 lub 1 do stanu WIĘKSZOŚCI twoich sąsiadów w sąsiedztwie Moora" co jest implementacją psychologicznego prawa konformizmu:
"Jednostka dopasowuje swoją postawę do postawy dominującej w jej otoczeniu".
Jak widać na powyższym obrazku symulacja ma cyklicznie (w procedurze draw() ) do wykonania trzy zadania: wizualizację, obliczanie statystyk (w Count() ) oraz właściwy model (DoMonteCarloStep()).

  • Wizualizacja przegląda wszystkie komórki i te w stanie 1 rysuje w kolorze czerwonym, a inne, zakładamy że w stanie -1 w kolorze białym. Tym razem nie posługujemy się jednak pojedynczymi pikselami, ale prostokątami (rect(x,y,w,h)), a właściwie kwadratami bo oznaczające szerokości i wysokość parametry trzeci i czwarty są równe S. Także pozycjonując komórki posługujemy się tym parametrem, dzięki czemu ich "rogi startowe" oddalone są o wielokrotności S.
  • Statystyka jest na razie bardzo prosta - jedynie zliczamy liczbę agentów w stanie 1 czyli czerwonych.
  • Wreszcie sam krok modelu polega na wykonaniu tzw. "kroku Monte Carlo" czyli wykonaniu tylu losowań ile jest komórek. Dla każdej komórki sprawdzamy w podwójnej pętli stan jej i jej sąsiedztwa Moora sumując stany komórek. Ponieważ jest to 9 komórek,  to w sumie wychodzi nam albo liczba dodatnia albo ujemna i znak tej liczby staje się nowym stanem wylosowanej komórki centralnej. Warto zwrócić uwagę, że przy obliczaniu prawdziwych współrzędnych komórki zakładamy że świat symulacji zamknięty jest w torus (temu służą operacje modulo %)
Oczywiście program jest jeszcze niekompletny. Potrzebujemy tradycyjnie ;-) deklaracji i inicjalizacji zmiennych w procedurze setup():
Główne zadanie tej części programu polega na utworzeniu tablicy świata (linia 12) i jej zainicjowaniu liczbami -1 i 1 w proporcji zadanej parametrem Ones.
Oczywiście musimy ustalić też rozmiar okna, ale ze względu na sposób wizualizacji nie musi być on równy rozmiarowi tablicy. Wręcz przeciwnie - lepiej żeby był większy. Do wpasowania wizualizacji tablicy świata w okno służy współczynnik S, który obliczamy dzieląc szerokość okna przed długość wiersza tablicy (w linii 18).
Na koniec wprowadzamy jeszcze funkcję exit() obsługująca zakończenie działania programu. Na razie nie robi ona nic istotnego, poza wywołaniem swojego super odpowiednika i powiedzeniem "Do widzenia", ale w dalszych częściach kursu będzie nam się przydawać.
Nie pokaże na razie zrzutu ekranu - sami się przekonajcie co wychodzi :-)
I pomyślcie jak przerobić ten program, żeby dopuszczalny był też stan 0 - "komórka jeszcze nic nie wie, więc nie nie ma zdania".
Więcej o teorii związanej z tym modelem w poblikacjach naukowych profesora Nowaka, ale jest też przystępna wersja polska: WPŁYW SPOŁECZNY JAKO MODEL ROZPRZESTRZENIANIA SIĘ MEMÓW

piątek, 24 lutego 2017

Processing vs. Java

Processing jest ściśle związany z Javą, podobnie jak C++ jest związane z C.
Można dzięki temu w miarę łagodnie i etapami przejść od programowania w Processingu do programowanie w Javie.
Można najpierw programować Processing w Eclipse - ulubionym IDE starych wyjadaczy Javy, potem w Eclipse programować w Javie, ale z użyciem bibliotek Procesingu, potem mieszając...
Aż wreszcie zostać z samą Javą...
A wtedy ofert pracy nie zabraknie.

http://www.instructables.com/id/Proclipsing-Using-the-Eclipse-IDE-for-Processing-p/

https://youtu.be/0cqSjyvA8EY

środa, 15 lutego 2017

Walentynkowy wykres funkcji 2D

Z okazji walentynek zrobiliśmy wariację na temat dwuwymiarowej funkcji ( f(x,y) ).

Korzystamy z ogólnej idei krzywych "sercokształtnych", jednak zamiast rysować krzywą, badamy cały szeroki zakres x, y kolorując wyniki.
Program oparty jest na poprzedniej wizualizacji funkcji 2D więc nie prezentuje go w całości - brakujące elementy są takie jak poprzednio:


Główna różnica polega na rozbudowanej instrukcji wybierającej kolor, która może posługiwać się skalą, albo też tylko kolorować jakościowo różne obszary (większe od zera, zero i mniejsze od zera). Można śmiało powalczyć z kolorystyką, no i z innymi niż zaimplementowana funkcjami.

środa, 8 lutego 2017

Szkocka krata i gra w życie - czyli sąsiedztwo Moora

Dotychczasowe przykłady automatu dwuwymiarowego stosowały tzw. sąsiedztwo von Neumana. Alternatywą jest sąsiedztwo Moore, które teraz zaimplementujemy w naszym automacie z regułą modulo:
Różnica polega na tym, że sąsiadami komórki aktualnej, poza sąsiadującymi "przez ścianę", są też komórki sąsiadujące "po rogach". Czyli zamiast 4 sąsiadów komórka ma ich 8.
Reszta programu pozostaje bez zmian, ale efekt mniej już przypomina arabskie mozaiki, a bardziej jakieś tkaniny - np. momentami serwetki, a czasem szkocką kratę...

Mając już taką wersję automatu komórkowego jesteśmy dosłownie o krok od zaimplementowania najsławniejszego z automatów - "Life" czyli "gry w życie" Johna Conwaya.
Po pierwsze deklarujemy trzy zmienne (linie 12-14), które są parametrami modelu - minim to najmniejsza liczba sąsiadów przy których komórka przeżywa, czyli jej stan pozostaje równy 1, maxim to maksymalna liczba sąsiadów przy których komórka przeżywa, a birth  to liczba sąsiadów, przy której komórka "martwa" (o stanie 0) może stać się żywa, czyli posiąść stan 1. Wartości domyślne tych zmiennych odpowiadają parametrom Life Conwaya, ale modyfikując je możecie zbadać też niektóre inne automaty z tej rodziny - czasem są zaskakujące.
Po drugie, ze względów kosmetycznych staramy się żeby szerokość i wysokość świata automatu była wielokrotnością 3 (linia 6) i  wyświetlamy sobie kontrolnie na konsoli realny rozmiar świata i okna, żeby mieć pewność że okno ma wystarczający rozmiar.
Wreszcie zmieniamy inicjalizację automatu w taki sposób żeby był zasiewany wyłącznie jedynkami - bo takie są reguły tego modelu.
Po modyfikacji deklaracji i procedury setup() możemy przejść do implementacji reguły automatu w procedurze draw():
Sama reguła (linie 87-98) zależna jest obowiązkowo od stanu komórki aktualnej. 
  • Jeśli jest ona martwa to może ożyć wtedy i tylko wtedy gdy ma birth sąsiadów. 
  • Gdy jest już żywa, to może przeżyć jeśli jej liczba sąsiadów mieści się w zakresie od minim do maxim.

W obu przypadkach zapisujemy obie wartości do tablicy WorldNew, bo nie zawiera ona stanu poprzedniego tylko poprzedni od poprzedniego. Można by ten fragment zoptymalizować, ale chyba nie warto...
Można też zredukować liczbę kolorów w pętli wyświetlania co nieco przyśpieszy wykonanie instrukcji switch. Zrobiłem to (linie 54-58), ale nie sprawdzałem jaki jest zysk, bo i tak wąskim gardłem tego programu jest przede wszystkim wyświetlanie.
Poniżej efekt działania po dosyć dużej liczbie kroków, gdzie praktycznie całość świata zajmują już struktury niezmienne "martwe natury" i proste oscylatory...
Ale i tak najciekawsze jest obserwowanie dynamiki - żadem obrazek tego nie odda. Obserwując uważne uda wam się na pewno zaobserwować tzw. szybowce, ale może też inne gatunki "statków kosmicznych"



poniedziałek, 6 lutego 2017

Przyśpieszanie automatu 2D

Automat prezentowany poprzednio jest wystarczająco szybki, żeby go obserwować, ale gdybyśmy chcieli robić jakieś eksperymenty - np. sprawdzać okresowość, albo gdybyśmy zwiększyli rozmiar świata do 1000x1000 to byłoby prawdopodobnie już zbyt wolno. Przynajmniej na moim komputerze robiło się nużąco ;-)
W jaki sposób możemy ten program przyśpieszyć?
Prawdziwi programiści dokonali by tzw. profilowania kodu i ustalili, w których liniach program "siedzi" najdłużej. Wy na razie musicie mi uwierzyć na słowo - w tym przypadku najkosztowniejsze czasowo jest po prostu rysowanie na ekranie, czyli wywołania procedury point().
Co możemy na to poradzić skoro za każdym razem musimy odrysować stan całego automatu?
Ale czy na pewno?
Zauważcie że jeśli startujemy od pojedynczej komórki (co jak ustaliliśmy jest znacznie ciekawsze niż start z losowego wypełnienia większą ich liczbą) to widzimy wyraźną okresowość działania automatu. Bardzo rzadko faktycznie zmienia się stan wszystkich komórek - zazwyczaj spora cześć, zwłaszcza czarnych pozostaje w tym samym kolorze...
Gdybyśmy wiedzieli czy komórka na ekranie ma już właściwy kolor moglibyśmy rysować tylko to co się zmieniło, zmniejszając liczbę wywołań kosztownej operacji point().
Ale przecież wiemy! Potrzebna informacja jest już w programie - wystarczy porównać komórkę tablicy WorldOld z tablica WorldNew.
Pojawia się jednak problem kosmetyczny. Napis który dotychczas pojawiał się na co krok odświeżanym tle jest teraz na skraju pola, który odświeżany jest stosunkowo rzadko, więc wyniki działania kolejnych wywołań text() nakładają się na siebie.
Musimy więc przenieść napis gdzieś poza obszar wyświetlania świata i samemu zadbać o odświeżanie tła. To właśnie wykonywane jest w liniach 93-96.
Wszystko w porządku?
Nie?
Oczywiście że nie - chyba że sami wpadliście na konieczne modyfikacje setupu.
Po pierwsze okno musi mieć dodatkowe miejsce na wyświetlanie napisu. Będzie więc trochę prostokątne (linia 15).
Ale jest jeszcze jedna, poważniejsza sprawa. Musimy zadbać o to, żeby w pierwszym kroku CAŁA tablica New różniła się od tablicy Old.  Gwarantuje to wypełnienie tablicy New wartościami -1. Tablica Old jest przez system wypełniona zerami z pewną domieszką naszych wylosowanych stanów. Dzięki temu wszystko zadziała...
Czyli udaje nam się przynajmniej okresowo osiągnąć prędkość ok. 70 klatek na sekundę. Na waszych nowych komputerach może być nawet szybciej.

Ale bez przesady z tymi zachwytami ;-) Cudów nie ma i ta optymalizacja nie zawsze zadziała, a wręcz może być niepotrzebnym obciążeniem. Kiedy?
Wtedy gdy wystartujemy z dosyć gęstego zasiewu, np. Dens=0.1
W takiej sytuacji dosyć szybko osiągamy stan, w którym każda komórka zmienia się w każdym kroku i znowu trzeba odrysować każdą.