Symulacje i modele


W tym artykule napiszemy symulację zachowania wielu oddziaływujących na siebie obiektów. Zostanie zdefiniowana klasa opisująca zachowanie i właściwości tych obiektów. Pokażemy jak nimi sterować przy pomocy stopera i sygnałów oraz jak przekazać wartości zmiennych pomiędzy obiektami.

Jest to średnio zaawansowany artykuł. Przed przeczytaniem warto zajrzeć do podręcznika, do rozdziału o klasach w POOL.

1. Pierwszy model.

Na początek napiszemy bardzo prostą klasę, którą rozwiniemy w następnych rozdziałach.
Klasa w POOL jest zdefiniowana identycznie jak funkcja. W rzeczy samej - nowy obiekt (żółw) danej klasy wykonuje opisującą go funkcję podczas utworzenia. Wszystkie funkcje i zmienne utworzone w tym czasie stają się składowymi obiektu. Także parametry funkcji tworzącej zostają zachowane na czas życia obiektu jako zmienne lokalne.

W pierwszej wersji programu klasa model pozwala ustawić proste właściwości żółwia: początkową pozycję i zwrot oraz prędkość, z jaką będzie się poruszał. Wartości te można przekazać jako argumenty funkcji tworzącej żółwia - nazwijmy ją konstruktorem. Konstruktor ustawia także kolor pisaka i promień żółwia (używany teraz przy odbiciu od ściany). Na koniec konstruktor wypisuje w oknie tekstowym informacje o parametrach nowego żółwia.
Klasa model zawiera jedną funkcję składową: krok dt. Funkcja ta pozwala przesunąć żółwia do przodu o odległość wynikającą z jego prędkości (podanej jako parametr konstruktora) i przedziału czasu dt podanego jako argument funkcji.

Program głównego żółwia (#first) tworzy dwa żółwie klasy model. Opis użytej do tego instrukcji newturtle znajdziesz w podręczniku. Aby poruszać żółwiami, pętla repeat wywołuje funkcję krok dla każdego z nich. Funkcja, którą mają wykonać obiekty wywoływana jest w POOL przy pomocy operatora @.
Opóźnienie w pętli pozwala zachować stałe tempo wykonania programu, odpowiednie do ustawień prędkości żółwi.

;funkcja-konstruktor żółwi o argumentach:
; p - pozycja początkowa {x y}
; h - początkowy kierunek w stopniach
; v - prędkość w pikselach na sekundę
to model :p :h [:v 60]
  setpos :p
  setheading :h
  setpencolor random 1000
  pendown
  showturtle
  setradius 10

  ;funkcja składowa przesuwająca żółwia do przodu,
  ;długość kroku zależy od zmiennej v żółwia oraz
  ;czasu w milisekundach, podanego jako parametr dt
  to krok :dt
    right (runif -20 20)     ;losowy obrót
    forward 0.001 * :dt * :v ;krok w przód
  end

  (print who "|p =| :p "|h =| :h "|v =| :v)
end

;konfiguracja:
; -żółwie odbijają się od ścian
bounce
; -żółwia #first jest ukryty i nie rysuje
hideturtle penup

;dwa żółwie klasy "model" gotowe na polecenia:
"m1 := (newturtle $model {-50 0} 30 15) ;lewy, nieco powolniejszy
"m2 := (newturtle $model {50 0} (-50))  ;prawy, domyślna prędkość

;krok co 0.1s - możesz zmniejszyć/zwiększyć tą wartość
"dt := 100
repeat 500 [
  sync :dt ;opóźnienie
  (krok :dt) @ :m1
  (krok :dt) @ :m2
]

2. Więcej samodzielności.

W poprzednim rozdziale żółw #first sterował żółwiami :m1 i :m2 wydając im polecenia w pętli. Lepszym sposobem regularnego wywoływania instrukcji jest użycie stopera. Stoper nie wymaga programowania opóźnień (sync lub wait) i nie blokuje dostępu do żółwia #first z linii poleceń. W kolejnej wersji programu skorzystamy ze stopera.
W podręczniku znajdziesz opis użycia stopera. Teraz skorzystamy z wariantu, w którym stoper regularnie wysyła sygnał do wszystkich żółwi. W klasie opisującej nasz model możemy odebrać taki sygnał przy pomocy odpowiedniej funkcji obsługi - zastąpi ona funkcję krok z poprzedniego rozdziału. Wartość czasu potrzebną do obliczenia długości kroku żółwia odczytamy z informacji, które zawiera sygnał wysyłany przez stoper.

Na tym etapie zmieniamy także sposób tworzenia nowych żółwi. Do programu żółwia #first dodajemy funkcję obsługi zdarzenia - kliknięcia myszą (zobacz opis w podręczniku). Teraz przy każdym kliknięciu w oknie grafiki pojawi się nowy żółw, który od razu przystępuje do obsługi sygnałów wysyłanych przez stoper.

Po uruchomieniu programu i utworzeniu kilku żółwi przełącz się do linii poleceń. Możesz wydawać instrukcje - żółw #first jest zajęty wykonywaniem funkcji stopera tylko przez ułamek milisekundy. Możesz np. sprawdzić, ile żółwi zostało już utworzonych: print count children.

;funkcja-konstruktor żółwi
to model :p :h [:v 60]
  setpos :p
  setheading :h
  (setpencolor random 1000 75)
  pendown
  showturtle
  setradius 10

  ;funkcja obsługi sygnału "krok"
  to onsignalkrok :turtle :data
    right (runif -20 20)
    let "dt :data,2 ;czas od ostatniej iteracji stopera
    forward 0.001 * :dt * :v
  end

  (print who "|p =| :p "|h =| :h "|v =| :v)
end

;nowy żółw na każde kliknięcie myszą
to onmouseclick :mousepos
  let "m (newt $model :mousepos random 360 20 + random 100)
end

;konfiguracja żółwia #first
bounce ht pu

;co 0.5s sygnał "krok" do wszystkich żółwi,
;spróbuj zwiększyć częstotliwość stopera:
;użyj wartości 100, 50, 20
"t := timer "krok 500

3. Więcej współpracy.

W poprzednich programach żółwie chodziły własnymi ścieżkami, nie reagując na siebie nawzajem. W tym rozdziale umożliwimy im komunikację. Zamiast wykonywać losowe skręty obliczymy w każdym kroku nowy kierunek żółwia - nieco w stronę pozostałych żółwi.
Zmianie ulega funkcja obsługi onsignalkrok - zamiast obrotu right (runif -20 20) użyjemy instrukcji ustawiającej żółwia zgodnie z podanym wektorem: setheaddir zwrot; zwrot jest nową funkcją składową klasy model. Zawiera ona kilka nowych elementów:

all - instrukcja zwracająca listę wszystkich żółwi istniejących w programie;

this - instrukcja zwracająca żółwia, który ją wywołuje; w tym przypadku umożliwia ona uniknięcie obliczania kierunku "do samego siebie": :m <> this;

pos @ :m - odczytanie pozycji kolejnych żółwi.

;funkcja-konstruktor żółwi
to model :p :h [:v 60]
  setpos :p
  setheading :h
  (setpencolor random 1000 75)
  pendown
  showturtle
  setradius 10

  ;funkcja obliczająca nowy kierunek
  to zwrot
    ;tylko jeśli są co najmniej 3 zółwie
    if (count all) > 2 [
      let "sx 0 let "sy 0
      foreach "m all [
        ;nie licz kierunku do samego siebie
        ;oraz kierunku do żółwia głównego
        if and (:m <> this) (:m <> #first) [
          let "pm pos @ :m ;pozycja żółwia :m
          let "dx :pm,1 - xcor
          let "dy :pm,2 - ycor
          let "norm sqrt :dx^2 + :dy^2
          "sx := :sx + :dx / :norm
          "sy := :sy + :dy / :norm
        ]
      ]
      "sx := 0.1 * :sx + 0.9 * headdir,1
      "sy := 0.1 * :sy + 0.9 * headdir,2
      output array :sx :sy ;nowy kierunek żółwia
    ]
    output headdir ;bieżący kierunek, bez zmian
  end

  ;funkcja obsługi sygnału "krok"
  to onsignalkrok :turtle :data
    setheaddir zwrot ;obrót w kierunku pozostałych żółwi
    fd 0.001 * :data,2 * :v
  end

  (print who "|p =| :p "|h =| :h "|v =| :v)
end

;nowy żółw na każde kliknięcie myszą
to onmouseclick :mousepos
  let "m (newt $model :mousepos random 360 50 + random 100)
end

;konfiguracja żółwia #first
bounce ht pu
setpalette "hot ;inna paleta, dla urozmaicenia

;co 0.05s sygnał "krok" do wszystkich żółwi
"t := timer "krok 50

Powyższy program wygląda rozsądnie i działa jak tego oczekujemy. Jednak do przygotowania ciekawszej i bardziej rozbudowanej symulacji potrzebujemy jeszcze większego porządku - podziału na dwa etapy:

obliczenie zmian - przygotowanie nowych wartości kierunku, prędkości, itd.; stan wszystkich obiektów jest "zamrożony" i obiekty mogą go wzajemnie odczytywać;

wykonanie zmian - stan obiektów zmienia się zgodnie z wartościami obliczonymi w poprzednim etapie; obiekty nie komunikują się ze sobą.

Dzięki temu podziałowi obliczenia w każdym etapie mogą być wykonywane równolegle, a wyniki są niezależne od kolejności, w jakiej zostały uruchomione obliczenia. Aby zrealizować ten algorytm trzeba zmodyfikować działanie stopera, tak by przy każdym "cyknięciu" wysłał dwa sygnały: sygnał "zwrot", którego funkcja obsługi w klasie model obliczy nowy kierunek żółwia na podstawie położenia innych żółwi oraz sygnał "krok", który wykona obrót i krok do przodu każdego żółwia. W nowym programie stopera sygnały są "blokujące" (zobacz opis w podręczniku), tzn. kolejne instrukcje w programie wysyłającym sygnał są wstrzymane do czasu zakończenia obsługi wysłanego sygnału przez wszystkie oczekujące go obiekty.

Kolejnym usprawnieniem w nowej wersji programu jest zapamiętanie tworzonych żółwi klasy model w zmiennej współdzielonej (jedna kopia zmiennej dostępna dla wszystkich obiektów): shared "żółwie. Upraszcza to przeszukiwanie listy żółwi podczas obliczenia nowego kierunku.

shared "żółwie ;lista żółwi klasy model
"żółwie := []

to model :p :h [:v 60]
  setpos :p
  setheading :h
  (setpencolor random 1000 75)
  pendown
  showturtle
  setradius 10

  let "kierunek headdir
  ;funkcja obsługi sygnału "zwrot"
  to onsignalzwrot
    if (count :żółwie) < 2 [
      "kierunek := headdir stop
    ]
    let "sx 0 let "sy 0
    foreach "m :żółwie [
      if :m <> this [
        let "pm pos @ :m
        let "dx :pm,1 - xcor
        let "dy :pm,2 - ycor
        let "norm sqrt :dx^2 + :dy^2
        "sx := :sx + :dx / :norm
        "sy := :sy + :dy / :norm
      ]
    ]
    "sx := 0.1 * :sx + 0.9 * headdir,1
    "sy := 0.1 * :sy + 0.9 * headdir,2
    "kierunek := array :sx :sy
  end

  ;funkcja obsługi sygnału "krok"
  to onsignalkrok :turtle :data
    setheaddir :kierunek
    fd 0.001 * :data * :v
  end

  (print who "|p =| :p "|h =| :h "|v =| :v)
end

;nowy żółw na każde kliknięcie myszą
to onmouseclick :mousepos
  queue :żółwie (newt $model :mousepos random 360 50 + random 100)
end

;konfiguracja żółwia #first
bounce ht pu
setpalette "blue2red

;co 0.05s:
"t := timer [
  signalw "zwrot      ;blokujący sygnał "zwrot"
  (signalw "krok :dt;blokujący sygnał "krok"
] 50

4. Pełna symulacja.

Mamy już wszystkie elementy potrzebne do zaprogramowania symulacji. Będzie to model kulek-cząstek posiadających masę nadającą im bezwładność i ładunek, dzięki któremu mogą się przyciągać lub odpychać. Suwakami możesz zmieniać stałą oddziaływania (suwak "siła") i proporcję mas (suwak "asymetria").

shared "cząstki
"cząstki := []

to model :q :masa [:v 0] [:h 0] [:ft 0.001] [:c 90]
  to ustaw_siłę :g   ;ustaw nową stałą oddziaływania:
    "c := -(:q * :g;ładunek * stała oddziaływania
  end

  to ustaw_masę :m   ;ustaw nową masę cząstki
    ;od pierwiastka z masy zależy też promień:
    setr 10 * sqrt :m
    "masa := :m
  end

  "ładunek := :q ;wartość ładunku dostępna publicznie
  ustaw_masę :masa
  ustaw_siłę :c
  setpos pos @ parent
  seth :h
  st

  let "fx 0 ;suma sił w kierunku X
  let "fy 0 ;suma sił w kierunku Y
  to onsignalf  ;oblicz sumę sił na sygnał "f"
    "fx := 0 "fy := 0
    foreach "m :cząstki [
      if :m <> this [
        let "p pos @ :m
        let "r radius + radius @ :m
        let "d distance :p
        ifelse :d > :r
        [let "fm (:c * :ładunek @ :m) / :d^3]
        [let "fm (:c * :ładunek @ :m) / :r^3]
        "fx += :fm * (:p,1 - xcor)
        "fy += :fm * (:p,2 - ycor)
      ]
    ]
  end

  to onsignalkrok :turtle :data
    let "dt 0.01 * :data
    if :dt > 0.8 ["dt := 0.8]

    let "hdir headdir
    :hdir,1 := :v * :hdir,1 + :fx * :dt / :masa
    :hdir,2 := :v * :hdir,2 + :fy * :dt / :masa
    "v := sqrt :hdir,1^2 + :hdir,2^2
    if :v > 30 ["v := 30]
    setheaddir :hdir

    let "ds :v * :dt
    "v -= :ds * :ft / :masa
    if :v < 0 ["v := 0]
    fd :ds
  end
end

bounce ht pu

repeat 30 [ ;czerwone cząstki "dodatnie", cięższe
  setxy (rnorm -30 30) (rnorm -10 30)
  "m := (newt $model 1 0.7)
  (setturtleimg "red_light_small) @ :m
  queue :cząstki :m
]

repeat 30 [ ;niebieskie cząstki "ujemne", lżejsze
  setxy (rnorm 30 30) (rnorm -10 30)
  "m := (newt $model (-1) 0.3)
  (setturtleimg "blue_light_small) @ :m
  queue :cząstki :m
]

home
;jedna żółta ciężka cząstka:
"m := (newt $model 3 2 4 45 0)
(setturtleimg "yellow_light) @ :m
queue :cząstki :m

;suwak zmieniający "stałą oddziaływania"
"gs := (slider "siła {10 4} {10 100 5} 90)
setonchange :gs [
  foreach "m :cząstki [(ustaw_siłę getvalue :gs) @ :m]
]

;suwak zmieniający proporcje mas cząstek
"sym := (slider "asymetria {10 60} {-0.9 0.9 0.1} 0.4)
setonchange :sym [
  let "s 0.5 * (1 + getvalue :sym)
  foreach "m :cząstki [
    if :ładunek @ :m = 1 [(ustaw_masę :s) @ :m]
    if :ładunek @ :m = -1 [(ustaw_masę 1 - :s) @ :m]
  ]
]

"t := timer [
  signalw "f
  (signalw "krok :dt)
] 25

5. Podsumowanie.

Artykuł ilustruje najbardziej podstawowe techniki przygotowania symulacji złożonej z wielu komunikujących się ze sobą obiektów: opis właściwości i zachowania obiektu przy pomocy klasy, użycie stopera do regularnego wykonania kroków symulacji i użycie sygnałów do wyzwalania równoległych zadań.
Symulacja, jaką przygotowaliśmy opiera się o bardzo proste zasady - bezwładność i siłę podobną do elektrostatycznej. Nawet tak prosty opis pozwala zaobserwować ciekawe zachowania grupy obiektów. Program może być podstawą do zbudowania bardziej realistycznego opisu, np. uzupełnionego o inne oddziaływania lub o inne modele obiektów. Warto też dodać interaktywne elementy, reagujące na działania użytkownika.