Interpolation von Funktionswerten

Manche Simulationen sind numerisch so aufwändig, dass sich nur wenige Punkte berechnen lassen, wenn man eine flüssig ablaufende Animation erreichen will. Schwankt die Ergebnisfunktion nicht zu stark, kann man zusätzliche, für eine graphische Darstellung benötigte Punkte schneller durch Interpolation erhalten. Zu diesem Zweck wird in diesem Abschnitt ein Funktions-Bean Interpolation1DFunction konstruiert, das die Koordinaten der Datenpunkte in zwei Vektoren dataX und dataY abspeichert und zu einem Eingangsvektor von x-Werten einen Ausgangsvektor mit y-Werten erzeugt, die entsprechend der Daten interpoliert sind. Außerdem soll noch die Interpolationsmethode (linear, polynomial, Spline) gewählt werden können.

Funktions-Beans sind in der Regel einfach zu programmieren, weil der größte Teil der benötigten Funktionalität durch Vererbung zur Verfügung gestellt wird. Um die interne Struktur eines solchen Beans zu verstehen, ist daher zunächst ein Blick auf den »Stammbaum« nötig:

12982

Der Grundgedanke ist, dass ein Funktions-Bean von anderen Beans Eingangswerte bekommt, daraufhin mit Hilfe seiner compute()-Funktion neue Ausgangswerte berechnet und diese als Nachricht an nachfolgende Beans weitergibt. Alternativ kann das automatische Berechnen und Weiterschicken auch ausgeschaltet werden, z.B. wenn man Zwischenberechnungen durchführen möchte, die nicht an Ausgabe-Beans weitergeleitet werden sollen. In diesem Fall werden neue Ausgabewerte erst durch einen expliziten Aufruf der Funktion trigger() erzeugt. Schließlich können durch einen Aufruf von inform() andere Beans unabhängig vom Eintreffen neuer Eingangswerte von generellen Änderungen - etwa neuen Werten für interne Parameter - informiert werden.

Diese Infrastruktur wird von der Basisklasse GenericFunction bereitgestellt. Ihre Kinder unterscheiden sich nach Typ und Anzahl der Ein- und Ausgabewerte; für zwei wichtige Speziallfälle gibt es weitere Oberklassen:

Ein Spezialfall der GenericVectorFunction ist die OneParameterFunction, die einer eindimensionalen Funktion $ x = f(t)$ entspricht, im Code als double evaluate(double t) bezeichnet. Ein Beispiel dafür ist die HarmonicFunction, definiert durch die Funktion

$\displaystyle x = A \cos(\omega t + \alpha) + x_0 $

Um sie zu implementieren, muss man im Wesentlichen nur die Funktion selbst angeben:

  public double evaluate(double t) {
    double x = a * cos(omega * t + alpha) + offset;
    return x;
  }

Dazu müssen die Felder für die zusätzlichen Parameter sowie die üblichen Get- und Set-Funktionen angelegt werden, etwa

  protected double a;

  public double getAmplitude() {
    return this.a;
  }

  public void setAmplitude(double a) {
    this.a = a;
    inform();
  }

Schließlich fehlt nur noch ein einfacher Standard-Konstruktor:

  public HarmonicFunction() {
    super();
    a = 1.0;
    omega = 1.0;
    alpha = 0.0;
    offset = 0.0;
  }

Die Interpolation1DFunction lässt sich in analoger Weise programmieren, wobei hier das Verfahren zur Berechnung der Funktionswerte das eigentliche Problem darstellt. Statt sich nun sofort mit den entsprechenden mathematischen Verfahren zu beschäftigen, 1 sollte man das Rad nicht neu erfinden, sondern nach vorhandenen Lösungen suchen. Fündig wird man z.B. in [3], das fertige Klassen für Interpolationsberechnungen zur Verfügung stellt. Diese wurden leicht an die PhysBeans-Umgebung angepasst, unter eine abstrakte Oberklasse physbeans.math.Interpolator1D eingeordnet und um eine Klasse für die lineare Interpolation erweitert. Sie werden in folgender Weise verwendet:

Für die Implementierung der Interpolation1DFunction werden folgende Felder gebraucht:

Der Konstruktor setzt alle Felder auf sinnvolle Anfangswerte:

  public Interpolation1DFunction() {
    super();
    dataX = new DVector(1.0, 2.0, 3.0);
    dataY = new DVector(1.0, 4.0, 9.0);
    type = SPLINE_INTERPOLATOR;
    createInterpolator();
  }

Dabei wurde das Erzeugen des Interpolators herausgezogen, damit es auch von anderen Methoden verwendet werden kann:

  protected void createInterpolator() {
    Point2dVector points = new Point2dVector(dataX, dataY);
    switch (type) {
      case  LINEAR_INTERPOLATOR:
        interpolator = new LinearInterpolator(points);
        break;
      case  POLYNOMIAL_INTERPOLATOR:
        interpolator = new NewtonInterpolator(points);
        break;
      case  SPLINE_INTERPOLATOR:
        interpolator = new SplineInterpolator(points);
        break;
      default:
        interpolator = new SplineInterpolator(points);
    }
  }

Nun fehlen nur noch die Zugriffsfunktionen, etwa

  public DVector getDataX() {
    return dataX;
  }

  public void setDataX(DVector x) {
    dataX = x;
    createInterpolator();
    inform();
  }

und die »eigentliche« Rechenroutine

  public double evaluate(double t) {
    return interpolator.evaluate(t);
  }

Wie eingangs versprochen reduziert sich also das Erstellen eines Funktions-Beans dank der Vererbungshierarchie tatsächlich auf einige Buchhaltungs-Operationen - abgesehen natürlich von der Implementierung der eigentlichen Funktion selbst. Zwar wurden hier nicht immer die letzten objektorientierten Feinheiten ausgenutzt 2 , aber der Aufwand stünde in keinem Verhältnis zum Ergebnis.

Nun muss noch die BeanInfo-Klasse erzeugt werden: Malen Sie ein schönes Icon mit dem weißem Hintergrund der Funktions-Beans (vielleicht inspiriert von dem Icon der PolynomialFitFunction), tragen Sie es im BeanInfo-Editor ein und aktivieren Sie die Trigger- und Vector-Listener sowie die Methoden trigger und inform. Als Properties werden natürlich dataX, dataY und interpolatorType gebraucht, aber auch autoTriggered und outputSize sind manchmal nützlich. Vergessen Sie auch inputVector und outputVector nicht; sie werden nur für die »Verdrahtung« verwendet und sollten deswegen als hidden gekennzeichnet werden.

Wenn Sie das neue Bean testweise in ein Applet einbauen und sich das Properties-Fenster ansehen, stellen Sie fest, dass bei dataX und dataY nur der lapidare Hinweis [DVector] steht, die aktuellen Werte sind nicht sichtbar und lassen sich auch nicht ändern. Beim Nachdenken ist das nicht verwunderlich - schließlich kann der Property-Editor nicht wissen, wie er mit dem PhysBeans-eigenen Datentyp DVector umgehen soll. Um ihm auf die Sprünge zu helfen, muss man einen eigenen Property-Editor für DVector-Variablen schreiben und ihn im BeanInfo-File angeben.

Der größte Teil dieser Arbeit ist bereits erledigt: Unter physbeans.editors.DVectorEditor befindet sich der benötigte Editor. Öffnen Sie noch einmal den BeanInfo-Editor für Interpolation1DFunction und tragen Sie bei dataX und dataY als Property Editor Class den Wert physbeans.editors.DVectorEditor.class ein. Wenn Sie sich nun - nach Abspeichern und Übersetzen der geänderten Klassen - das Testapplet im Form-Editor ansehen, hat sich nichts geändert! Das liegt daran, dass NetBeans zur Geschwindigkeitserhöhung die Eigenschaften der verwendeten Beans intern zwischenspeichert (»cached«). Also beenden Sie NetBeans und starten es neu, danach sollte bei dataX statt des Typ-Hinweises ein editierbares Textfeld mit dem aktuellen Wert stehen. Darüber hinaus kann man auch einen Custom-Editor öffnen, der die Eingabe von DVectoren sehr erleichtert.

Es gibt aber noch ein weiteres Problem beim Property-Editor: Für den interpolatorType kann jede beliebige ganze Zahl eingetragen werden! Man könnte auf die Idee kommen, das dadurch abzufangen, dass man in der entsprechenden Set-Funktion bei Eingabe falscher Werte entweder gar nichts tut oder einen Standardwert wählt. Aber das führt beim armen Benutzer des Beans nur dazu, dass er im Integer-Nebel stochern muss, bis er das gewünschte Interpolationsverfahren erhält!

Stattdessen kommt man an dieser Stelle nicht darum herum, einen eigenen PropertyEditor zu schreiben, der statt eines Feldes zur direkten Eingabe von Zahlenwerten eine Auswahlbox mit sinnvollen Beschriftungstexten anzeigt. Glücklicherweise gibt es dafür in PhysBeans schon eine Klasse physbeans.editors.TaggedEditor, die nur noch erweitert werden muss, um die richtigen Beschriftungen zu erhalten. Erzeugen Sie also eine Klasse physbeans.editors.InterpolatorTypeEditor, die von TaggedEditor abgeleitet ist und nur aus dem folgenden Konstruktor besteht:

  public InterpolatorTypeEditor() {
    types = new String[] {"linear interpolator",
      "polynomial interpolator", "spline interpolator"};
  }

Tragen Sie physbeans.editors.InterpolatorTypeEditor.class als Property Editor Class für den interpolatorType ein und starten Sie NetBeans neu, anschließend ist beim interpolatorType statt des Textfeldes die gewünschte Auswahlbox zu sehen.

Anmerkungen:

1 was nichtsdestotrotz nichts schaden kann, etwa mit [37,11]

2 etwa ein eigener Aufzählungstyp (»Enum«) für den Interpolationstyp oder eine polymorphe Erzeugungsfunktion (»Factory method«) statt des switch in createInterpolator

ZurückWeiter