© Jiří Macur, 2006
Při demonstraci chaotického chování dynamického systému se obvykle používá tzv. rotátor, což je rovinné kyvadlo s hmotností m v homogenním gravitačním poli g s nehmotným, avšak tuhým závěsem délky l. Pohyb kyvadla pak není omezen na malé kmity, ale může vykonávat i kruhový pohyb s proměnnou úhlovou rychlostí w. Rotátor pak může vykazovat chaotické chování v případě působení vnější vynucující síly. Nejprve však vytvoříme nástroje pro simulaci volného rotátoru.
Stav volného rotátoru je plně určen dvěmi veličinami: úhlovou výchylkou f a úhlovou rychlostí w, přičemž platí:
(3.1)
Kanonický tvar dynamického systému je pak
(3.2)
Pro simulaci budeme používat objektový přístup v jazyce Java, pro grafický výstup použijeme nejjednodušší grafickou knihovnu AWT. Předpokládáme, že čtenář je obeznámen se základy objektového programování, všechny konstrukce budeme vytvářet s důrazem spíše na ilustrativnost než na efektivitu algoritmů tak, aby byl patrný přímočarý vztah mezi modelovaným dynamickým systémem a jeho objektovou implementací.
Objektu rotátor přiřadíme následující atributy:
Typ atributu |
Název |
Význam |
Parametry systému |
m |
hmotnost |
l |
délka závěsu |
|
g |
gravitační zrychlení |
|
Stavové proměnné |
fi |
úhlová výchylka |
omega |
úhlová rychlost |
|
Pomocné proměnné pro vizualizaci pohybu |
oldx, oldy |
stav rotátoru v rast. souřadnicích |
width, height |
rozměry zobrazované plochy |
|
x0,y0 |
souřadnice upevnění závěsu |
|
fg, bg |
barva popředí a pozadí |
|
r, d |
poloměr a průměr závaží rotátoru |
Jednoduchá implementace animovaného pohybu rotátoru pak může být následující:
Program 1
import java.applet.*;
import java.awt.*;
public class simul extends Applet
{
private double l = 0.3, m = 1; //parametry
private double fi = 3, omega = 0; //stavové proměnné
private int oldx,oldy; //uložení stavu v rastru
private int x0,y0; //souřadnice upevnění závěsu
private int width,height; //rozměry zobraz. plochy
private Color fg = Color.black; //barva popředí
private Color bg = Color.white; //barva pozadí
private int r = 5, d = 10; //poloměr a průměr závaží
private double g = 9.81; //gravitační zrychlení
private double h = 0.005; //krok numerické metody
private boolean konec; //příznak zastavení výpočtu
public void init() //inicializace appletu
{
Dimension d = this.size(); //zjištění rozměru plochy
width = d.width; //nastavení měřítka;
height = d.height;
x0 = width/2; y0 = height/2; //závěs do středu plochy
setBackground(bg);
konec = false; //povolení cyklu výpočtu
}
public void stop() //zastaveni appletu
{
konec = true; //zastaví výpočet
super.stop(); //metoda mateřské třídy
}
public void paint(Graphics g) //ovladač překreslení
{
g.setColor(fg); //nastavení barvy kreslení
g.drawString("Animace rotátoru 1",10,15);
while (!konec) //cyklus vlastního výpočtu
{
step(); //novy stav rotátoru
show(g); //animace
try //zpomalení výpočtu
{
Thread.sleep(5); //pozastav thread na x ms
} catch(InterruptedException e){};
}
}
public double f1(double fi,double omega) //definice dyn. systému
{
return omega; //první složka vekt. funkce
}
public double f2(double fi,double omega)
{
return -Math.sin(fi)*g/l; //druhá složka vekt. funkce
}
public void step() //výpočet nového stavu
{
double h2 = 0.5*h;
double k11 = f1(fi,omega); //algoritmus Runge-Kutta
double k12 = f2(fi,omega);
double k21 = f1(fi+h2*k11,omega+h2*k12);
double k22 = f2(fi+h2*k11,omega+h2*k12);
double k31 = f1(fi+h2*k21,omega+h2*k22);
double k32 = f2(fi+h2*k21,omega+h2*k22);
double k41 = f1(fi+h*k31,omega+h*k32);
double k42 = f2(fi+h*k31,omega+h*k32);
fi += h/6*(k11+2*k21+2*k31+k41); //nastavení nového stavu
omega += h/6*(k12+2*k22+2*k32+k42);
}
public int x(double fi) //transf. z úhlové souřadnice
{ //do kartézských rastru
return (int)(x0+width*l*Math.sin(fi));
}
public int y(double fi)
{
return (int)(y0+height*l*Math.cos(fi));
}
public void show(Graphics g) //zobrazení stavu
{
g.setColor(bg); //skryj původní stav
g.drawOval(oldx-r,oldy-r,d,d);
g.drawLine(x0,y0,oldx,oldy);
oldx = x(fi); oldy = y(fi); //zapamatuj si stav
g.setColor(fg); //zobraz nový stav
g.drawOval(oldx-r,oldy-r,d,d);
g.drawLine(x0,y0,oldx,oldy);
}
} // konec appletu simul
Uvedený program je velmi jednoduchý, přesto k němu uvedeme několik poznámek:
Třída simul je odvozena od třídy applet, která obsahuje potřebnou funkcionalitu pro vložení programu do prostředí prohlížeče Internetu. Tato funkcionalita je zabezpečena zejména množinou metod, z nichž některé slouží jako ovladače událostí – tj. jsou volány automaticky systémovým prostředím podle událostí, k nimž v systému došlo. Typickým reprezentantem je metoda init(), která je automaticky vyvolána při inicializaci třídy (zastupuje konstruktor při vytvoření instance). Metodu paint(Graphics) vyvolá systém při potřebě obnovy obsahu okna (např. při jeho odkrytí). Parametr metody (objekt třídy Graphics) systém naplní hodnotami grafického kontextu, platnými pro okno, v němž se applet prezentuje. Metoda stop() je automaticky vyvolána při zastavení appletu (např. při zavření okna prohlížeče). Pro správnou činnost naší třídy bychom měli alespoň tyto metody mateřské třídy nově definovat.
V metodě init tedy zjistíme rozměry plochy appletu, kterou definuje implicitně prostředí (tj. prohlížeč) a nastavíme měřítko tak, aby se naše animace do vymezeného prostoru správně umístila. Dále zde nastavíme barvu pozadí. Nastavíme také logickou proměnnou konec, která určuje trvání výpočtu.
V metodě stop pouze zakážeme další pokračování výpočtu, který se odehrává v následující metodě paint. Pak pokračujeme v zastavování appletu tak, jak je definováno v jeho mateřské třídě.
V metodě paint vykreslíme popředí okna, tj. popisný text a spustíme cyklus výpočtu nového stavu a jeho animované vykreslování. Tento postup není, jak dále uvidíme, zcela správný, avšak pro první pokus postačí. Pro výpočet nového stavu je volána metoda step naší třídy simul a metoda show pro animované zobrazování. Na většině dnešních počítačů by však byl pohyb systému příliš rychlý, proto ho zpomalíme tak, že při každém průchodu cyklem "uspíme" applet na 5ms. Tento postup je výhodný z několika důvodů:
1. animace bude probíhat stejně rychle na různě výkonných počítačích
2. při uspání vždy dojde k předání řízení ve prospěch jiných procesů v počítači – bude tedy možné i nadále rozumně pracovat s jinými aplikacemi a zejména bude snadné náš applet ukončit.
Mechanizmus předání řízení Thread.sleep může vyvolat výjimku (jedná se o systémovou záležitost), kterou musíme ošetřit – uvedený způsob, kdy na výjimku nijak nereagujeme, sice není příliš doporučován, ale pro naše účely kvůli jednoduchosti postačí.
Metoda step obsahuje implementaci klasické metody Runge-Kutta – z původního stavu vypočítá nový stav dynamického systému. Ten je definován vektorovou funkcí (3.2), přičemž její složky jsou implementovány funkcemi f1 a f2.
Metoda show smaže zobrazení původního stavu a vykreslí náš dynamický systém v novém stavu. K tomu však potřebuje přístup ke grafickým zdrojům, což jí zajistí parametr třídy Graphics, který byl naplněn systémem při automatickém volání metody paint(Graphics). K vykreslení nového stavu je zapotřebí transformace z polárních do kartézských souřadnic, kterou provádějí funkce x a y. Protože je zapotřebí si při animaci zapamatovat starý stav (aby ho bylo možné smazat), uloží metoda show již transformované hodnoty pro vykreslení stavu v rastrových souřadnicích do atributů xold a yold tak, aby je měla rychle k dispozici a nemusela znovu provádět transformaci.
Poznámka: Mazání stavu překreslením jednoduchého obrázku rotátoru barvou pozadí je z hlediska rychlosti daleko efektivnější než celkové smazání celé zobrazované plochy.
Uvedený text uložíme do souboru simul.java a přeložíme do p-kódu příkazem
> javac simul.java
Při bezchybném překladu vytvoří překladač vytvoří ve stejném adresáři soubor s přeloženou třídou simul.class.
Applet je závislý na prostředí prohlížeče – velmi jednoduchá stránka v jazyce HTML, která při interpretaci prohlížečem applet vyvolá, může vypadat např. takto:
<HTML>
<HEAD>
<TITLE>Simulace</TITLE>
</HEAD>
<BODY bgcolor="gray">
<H1 align="center">Rotátor</H1>
<P align='center'>
<APPLET CODE='simul.class' width='300' height='300'></APPLET>
</P>
</BODY>
</HTML>
Text uložíme do souboru simul.htm do stejného adresáře, v němž je nachystán náš applet simul.class. Stránku s appletem lze vyvolat prohlížečem lokálně nebo ji umístit na server WWW – pak bude přístupná v Internetu.
Celkově je patrné, že program je velmi přímočarý a jednoduchý, provádí simulaci rychle a z hlediska očekávaného chování – nepříliš dobře. Rychle totiž zjistíme, že prohlížeč je činností appletu zcela zablokován, při překrytí animace se původní obsah pozadí neobnoví, při pokusu o obnovení obsahu okna může dojít dokonce k havárii prohlížeče a je třeba ho zastavit systémovými prostředky. Celý problém je v nesprávném použití metody paint pro řízení běhu appletu. Metoda pro nás sice spustí výpočet, ale už se nezastaví a události, které by metodu paint měly opět vyvolat, ji nemohou reentrantně spustit.
![]() |
Obr. 3.1 První program simulující netlumený rotátor bez buzení
Náš program tedy upravíme tak, aby byl celý mechanizmus řízení procesů zjevnější, i když poněkud složitější. Budeme při tom vycházet z následujících požadavků, které nám jazyk Java umožňuje pohodlně splnit:
• nechť je objekt rotátoru co nejvíce izolován od prostředí tak, aby bylo možné spouštět a zastavovat paralelně s ním jiné procesy podle potřeby
• případné změny jeho atributů (nastavení nových počátečních podmínek) nechť provádí jiný objekt, který je schopen komunikovat s uživatelem
• jiný "vizualizační" objekt bude objektu rotátoru také poskytovat prostředky pro zobrazování stavu.
Pomocí správce plochy (layout manager) typu border rozdělíme oblast na dva nestejně velké díly. Do prvního (severního) vložíme panel s ovládacími prvky (textová pole pro nastavení počátečních podmínek dynamického systému), do druhého (centrálního) větší oblast pro vykreslování.
Poznámka: I v našem předcházejícím příkladě byl implicitně použit nejjednodušší správce plochy, který ji však nijak nedělil. Použití správce bylo zděděno z třídy applet.
Ovládací komponenty umožní uživateli vložit do běžícího appletu nové výchozí hodnoty. V jazyce Java je interakce mezi programem a uživatelem (nebo jinými procesy) řešena obecně na základě událostí (events). Prvky, které události generují (tlačítka, textová pole apod.), si zároveň registrují své možné posluchače (objekty, které události zachytí a zpracují). Posluchači však musejí být na tuto činnost připraveni – implementují patřičné rozhraní pro zachycení události. Schematicky je celý mechanizmus následující:
Ke každé interaktivní komponentě je přiřazeno spektrum událostí, které může generovat (např. komponenta tlačítko – button, může generovat události Action, Focus, Mouse, …). Písmeno E v obrázku je pak třeba nahradit konkrétní událostí, s níž chceme pracovat. Pro událost Action (stisk tlačítka) například:
1. registrujeme posluchače metodou addActionListener(<posluchač>)
2. posluchač musí implementovat rozhraní ActionListener
3. definuje tedy metodu actionPerformed(ActionEvent e)
Které události může prvek generovat, jak se nazývají potřebné metody (ovladače) pro zpracování těchto událostí, to vše nalezneme snadno v dokumentaci JDK.
Z následujícího programu jsou podrobnosti použití události zcela zřejmé.
Samotný objekt rotátoru umístíme do samostatné třídy, která bude mít na starosti pouze výpočet nových stavů rotátoru. Starost o jejich zobrazování, resp. nastavování nových počátečních stavů svěříme jiným třídám. Navíc třídu rotátoru umístíme do separátního procesního vlákna, což lze v prostředí Javy provést velmi snadno – stačí naši třídu učinit potomkem třídy Thread, která zajistí potřebnou funkcionalitu. Výhodou umístění rotátoru do procesu odlišného od celého appletu je možnost zvýšení priority, pozastavování nebo přednostního přidělení procesu jinému procesoru (pokud je přítomen). Hlavní metodou (tělem vlákna) je metoda run(), kterou musíme nově definovat. Obsah této metody je tvořen obvykle nějakým delším výpočtem, v našem případě sem umístíme cyklus výpočtu nových stavů rotátoru. Spuštění těla se neprovádí přímo, o spuštění se postará metoda start(), kterou zavoláme po vytvoření instance vlákna. Vlákno zanikne automaticky s koncem metody run(), který musíme ošetřit tak, abychom se vyvarovali dalšího běhu vlákna i v případě, že ostatní části programu již skončily.
Struktura našeho programu bude tvořena čtyřmi třídami:
Třída |
Zděděna od |
Popis činnosti |
simul |
Applet |
Rozdělí plochu na dvě části, do první umístí panel Vstup s ovládacími prvky, do druhé zobrazovací třídu Anim. Vytvoří instanci třídy Rotator a nastartuje její vlákno. Zachycuje události z ovládacího prvku panelu a předává nová data do objektu třídy Rotator. |
Vstup |
Panel |
Definuje v kontejneru panelu dvě vstupní pole pro nové počáteční hodnoty stavu rotátoru a tlačítko, které generuje událost. Ovladač události je ve třídě simul, která předává referenci na sebe při volání konstruktoru třídy Vstup. Ovladač po zachycení události přečte obsah vstupních polí v panelu a po konverzi je předá rotátoru. |
Anim |
Canvas |
Vytváří univerzální kreslící plochu, v níž se prezentuje stav rotátoru. Hlavní metodou třídy je metoda show(double fi), kterou volá objekt rotátoru, po vypočtení nového stavu. Veškeré transformace zobrazení jsou umístěny v třídě Anim. Aby mohl rotátor třídu využívat, dostává po vytvoření odkaz na instanci třídy Anim. |
Rotator |
Thread |
Definuje vlastní dynamický systém a simuluje jeho chování. V simulačním cyklu testuje proměnnou zmena, která slouží k synchronizaci s vnucenou změnou stavu z vnějšího prostředí. Cyklus simulace končí nastavením příznaku konec, což zajistí obdoba destruktoru appletu. |
Z uvedené tabulky je patrné, že applet simul má určitou řídící roli – vytváří instance ostatních tříd, předává jim potřebná data, zachytává jejích události. Panel vstup soustřeďuje komponenty pro interakci s uživatelem, zatímco Anim se stará o zobrazování. Je třeba mít na zřeteli, že strukturální návrh aplikace by mohl vypadat i zcela odlišně a byl by rovněž funkční. V této oblasti neexistuje žádná předem stanovená metodika a vše závisí na preferencích programátora.
Poznámka: Z hlediska principů OOP si trochu ulehčujeme situaci, když přistupujeme z appletu přímo k proměnným jiných tříd. Nemá však smysl snažit se o objektový purismus – naše programy se nerozrostou do té míry, aby porušení zapouzdření stavu objektů mohlo vést ke zmatkům a přehlednost a rychlost výsledné aplikace je pro nás důležitější než objektová "čistota".
Náš vylepšený druhý program tedy může vypadat například takto:
Program 2
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
public class simul extends Applet implements ActionListener
//třída implementuje rozhraní zpracování události od tlač. v panelu vstup
{
private Vstup vstup; //obsahuje tři podřízené třídy
private Anim anim;
private Rotator rot;
public void init() //inicializace appletu ("konstruktor")
{
setLayout(new BorderLayout()); //zavedení správce plochy
vstup = new Vstup(this); //při vytvoření instance panelu přidáme odkaz
add(vstup,"North"); //zařazení panelu do správy plochy nahoru
anim = new Anim(300,300); //vytvoření instance zobrazovací třídy
add(anim,"Center"); //zařazení do správy plochy doprostřed
anim.repaint(); //vynucení překreslení po umístění
rot = new Rotator(); //vytvoření instance objektu rotátoru
rot.anim = this.anim; //předání odkazu na zobrazovací třídu
rot.start(); //spuštění těla vlákna rotátoru
}
public void stop() //konec appletu ("destruktor")
{
rot.konec = true; //konec vlákna rotátoru
super.stop(); //dokončení destrukce mateřskou třídou
}
public void actionPerformed(ActionEvent e)
//obsluha události od tlačítka panelu
{
//do poč. hodnot rotátoru vlož načtené údaje
rot.fi0 = (Double.valueOf(vstup.fi0.getText())).doubleValue();
rot.omega0 = (Double.valueOf(vstup.omega0.getText())).doubleValue();
rot.zmena = true; //informuj rotátor o změně
}
} //konec třídy simul
class Vstup extends Panel
{
public TextField fi0,omega0; //třída obsahuje dvě veřejná vstupní pole
private Button odeslat; //a jedno tlačítko
Vstup(simul posluchac) //konstruktor dostane odkaz na posluchače
{ //aby ho mohl zaregistrovat
setLayout(new FlowLayout()); //definuje správce své plochy
add(new Label("Fi:")); //do plochy přidá popisný text
fi0 = new TextField("0",4); //vytvoří instanci vstního pole
add(fi0); //s parametry impl. hodnota a délka pole
add(new Label("Omega:"));
omega0 = new TextField("0",4);
add(omega0);
odeslat = new Button("Odeslat"); //vytvoření instance tlačítka
add(odeslat);
//tlačítko si registruje ovladač své události
odeslat.addActionListener(posluchac);
}
} //konec třídy Vstup
class Anim extends Canvas //třída Canvas ("plátno") se používá
{ //k zobrazení rastrové grafiky
private int width,height; //atributy geometrie oblasti
private int x0,y0; //bod závěsu rotátoru
private double l=0.3; //délka rotátoru
private int oldx=0,oldy=0; //zapamatovaný stav v rastrových souřadnicích
private int r=5,d=10; //poloměr a průměr závaží rotátoru
private Color fg=Color.black; //barva popředí
private Color bg=Color.white; //barva pozadí
private Graphics g; //uložení grafického kontextu
Anim(int height0, int width0) //konstruktor
{
super(); //použijeme funkcionalitu mateřské třídy
width = width0; //uložíme geometrii plochy
height = height0;
x0 = width/2; y0 = height/2; //umístíme závěs do středu
this.setSize(width,height); //nastavíme požadované rozměry
}
public void paint(Graphics g) //ovladač překreslení
{
g.setColor(fg);
setBackground(bg);
g.drawString("Animace rotátoru 1",10,15);
}
public int x(double fi) //transf. z úhlové souřadnice
{ //do kartézských ratrových
return (int)(x0+width*l*Math.sin(fi));
}
public int y(double fi)
{
return (int)(y0+width*l*Math.cos(fi));
}
public void showState(double fi) //zobrazení stavu
{
g = this.getGraphics();
g.setColor(bg); //skryj původní stav
g.drawOval(oldx-r,oldy-r,d,d);
g.drawLine(x0,y0,oldx,oldy);
oldx=x(fi);oldy=y(fi); //vypočítej reprezentaci nového stavu
g.setColor(fg); //zobraz nový stav
g.drawOval(oldx-r,oldy-r,d,d);
g.drawLine(x0,y0,oldx,oldy);
}
} //konec třídy Anim
class Rotator extends Thread //dědíme funkcionalitu třídy vlákna
{
public double l=0.3, m=1; //parametry dyn. systému
public double fi,omega; //stavové proměnné
public double fi0 = 3, omega0 = 0; //počáteční hodnoty
public boolean zmena = false; //indikátor změny z vnějšku
public boolean konec = false; //indikátor ukončení výpočtu
private double g = 9.81; //grav. zrychlení
private double h = 0.005; //diskretizační krok
public Anim anim = null; //reference na zobrazovací třídu
public Rotator() //konstruktor
{
fi = fi0; omega = omega0; //počáteční podmínky
}
public void setState() //nastavení nespojitého nového stavu
{
if (zmena)
{
fi = fi0; omega = omega0;
zmena = false;
}
}
public double f1(double x1,double x2) //definice dynamického systému
{
return x2;
}
public double f2(double x1,double x2)
{
return -Math.sin(x1)*g/l;
}
public void step() //výpočet nového stavu
{
double h2 = 0.5*h;
double k11 = f1(fi,omega); //algoritmus Runge-Kutta
double k12 = f2(fi,omega);
double k21 = f1(fi+h2*k11,omega+h2*k12);
double k22 = f2(fi+h2*k11,omega+h2*k12);
double k31 = f1(fi+h2*k21,omega+h2*k22);
double k32 = f2(fi+h2*k21,omega+h2*k22);
double k41 = f1(fi+h*k31,omega+h*k32);
double k42 = f2(fi+h*k31,omega+h*k32);
fi += h/6*(k11+2*k21+2*k31+k41); //nový stav
omega += h/6*(k12+2*k22+2*k32+k42);
}
public void run() //tělo vlákna
{
while(!konec)
{
setState(); //je-li požadována změna, proveď ji
step(); //nový stav
anim.showState(fi); //zobrazení stavu
try //prodleva (předání řízení)
{
Thread.sleep(5);
} catch(InterruptedException e) {};
}
}
} //konec třídy Rotator
Poznámky k programu 2:
• V sekci pro import balíků musíme zařadit java.awt.event.*
• Applet simul musí implementovat rozhraní ActionListener, aby mohl zpracovat událost od tlačítka. Deklarovaná implementace spočívá v definici metody actionPerformed(ActionEvent e), která bude po události automaticky vyvolána. Název metody a typ parametru události nalezneme v dokumentaci JDK.
• V ovladači události přečteme obsah textových polí jiné třídy – proto musí být odpovídající komponenty v panelu Vstup veřejné. Konverze řetězců, které tyto komponenty poskytují pomocí metody getText(), na potřebný typ double se může zdát poněkud složitá, je však často používaná. Ke konverzi použijeme statickou metodu valueOf(string) třídy Double, která však nevrací skalární hodnotu ale objekt. Z objektu pak vyjmeme potřebný atribut metodou doubleValue().
• Při předání nového stavu do objektu rotátor musíme zajistit správnou synchronizaci (rotátor běží nezávisle na procesu appletu). Proto nastavíme nové hodnoty stavu do veřejných atributů rotátoru a příznakem změny informujeme vlákno rotátoru, aby si při první vhodné příležitosti nastavilo nový počáteční stav objektu. K synchronizaci vláken existují v třídě Thread i lepší nástroje, avšak jejich použití v našem programu by bylo zbytečně robustní a komplikované.
Obr. 3.2. Výstup simulace pohybu rotátoru se zadáváním počátečních podmínek