Pipelines

In Parametrisierung wurde beschrieben, wie Klassen erstellt werden, die Parameter deklarieren und mit Berechnungen oder Visualisierungen verknüpfen. In diesem Abschnitt erfahrt ihr, wie ihr mehrere solcher Panels mit einer Pipeline verbinden könnt um komplexe Workflows auszudrücken, bei denen die Ausgabe einer Stufe in die nächste Stufe eingespeist wird.

[1]:
import panel as pn
import param


pn.extension("katex")
[2]:
pipeline = pn.pipeline.Pipeline()

Während wir früher bereits gesehen haben, wie Methoden mit dem param.depends-Decorator verknüpft werden, verwenden Pipelines einen anderen Decorator und eine Konvention zum Anzeigen der Objekte. Der param.output-Decorator bietet eine Möglichkeit, die Methoden einer Klasse mit Anmerkungen zu versehen, indem seine Ausgaben deklariert werden. Pipeline verwendet diese Informationen, um zu bestimmen, welche Ausgaben verfügbar sind, um in die nächste Stufe des Workflows eingespeist zu werden. Im folgenden Beispiel hat die Klasse Stage1 zwei Parameter (a und b) und eine Ausgabe c. Die Signatur des Decorators ermöglicht eine Reihe von verschiedenen Möglichkeiten, die Ausgaben zu deklarieren:

  • param.output(): Wenn eine Ausgabe ohne Argumente deklariert wird, gibt die Methode eine Ausgabe zurück, die den Namen der Methode erbt und keine spezifischen Typ-Deklarationen vornimmt.

  • param.output(param.Number): Beim Deklarieren einer Ausgabe mit einem bestimmten Parameter oder einem Python-Typ wird die Ausgabe mit einem bestimmten Typ deklariert.

  • param.output(c=param.Number): Wenn eine Ausgabe mit einem Keyword-Argument deklariert wird, könnt ihr damit den Methodennamen als Namen der Ausgabe überschreiben und den Typ deklarieren.

Es ist auch möglich, mehrere Parameter als Keywords oder als Tupel zu deklarieren:

  • param.output(c=param.Number, d=param.String)

  • param.output(('c', param.Number), ('d', param.String))

Im folgenden Beispiel ist die Ausgabe einfach das Ergebnis der Multiplikation der beiden Eingaben (a und b), die die Ausgabe c erzeugen. Zusätzlich deklarieren wir eine view-Methode, die einen LaTeX-Pane zurückgibt. Schließlich gibt eine panel-Methode ein Panel-Objekt zurück, das sowohl die Parameter als auch den View rendern.

[3]:
class Stage1(param.Parameterized):
    a = param.Number(default=5, bounds=(0, 10))

    b = param.Number(default=5, bounds=(0, 10))

    @param.output(("c", param.Number), ("d", param.Number))
    def output(self):
        return self.a * self.b, self.a**self.b

    @param.depends("a", "b")
    def view(self):
        c, d = self.output()
        return pn.pane.LaTeX(
            "${a} * {b} = {c}$\n${a}^{{{b}}} = {d}$".format(
                a=self.a, b=self.b, c=c, d=d
            )
        )

    def panel(self):
        return pn.Row(self.param, self.view)


stage1 = Stage1()
stage1.panel()
[3]:

Zusammenfassend haben wir einige Konventionen befolgt, um diese Phase unserer Pipeline zu erstellen:

  1. Deklarieren einer parametrisierten Klasse mit einigen Eingabeparametern

  2. Deklarieren und Benennen einer oder mehrerer Ausgabemethoden

  3. Deklarieren einer panel-Methode, die einen View des Objekts zurückgibt, das von der Pipeline gerendert werden kann.

Nachdem das Objekt nun instanziiert wurde, können wir es auch nach seinen Ausgaben befragen:

[4]:
stage1.param.outputs()
[4]:
{'c': (<param.Number at 0x13f5fb7c0>,
  <bound method Stage1.output of Stage1(a=5, b=5, name='Stage100954')>,
  0),
 'd': (<param.Number at 0x13f5fb640>,
  <bound method Stage1.output of Stage1(a=5, b=5, name='Stage100954')>,
  1)}

Wir können sehen, dass Stage1 eine Ausgabe mit dem Namen c und dem Typ Number deklariert, auf die mit der output-Methode zugegriffen werden kann. Nun fügen wir stage1 mit add_stage unserer Pipeline hinzu:

[5]:
pipeline.add_stage("Stage 1", stage1)

Für eine Pipeline benötigen wir jedoch noch mindestens eine stage2, das das Ergebnis von stage1 weiterverarbeitet. Daher sollte ein Parameter c aus dem Ergebnis von stage1 deklariert werden. Als weiteren Parameter definieren wir exp und erneut eine view-Methode, die von den beiden Parametern und der panel-Methode abhängt.

[6]:
class Stage2(param.Parameterized):
    c = param.Number(default=5, precedence=-1, bounds=(0, None))

    exp = param.Number(default=0.1, bounds=(0, 3))

    @param.depends("c", "exp")
    def view(self):
        return pn.pane.LaTeX(
            "${%s}^{%s}={%.3f}$" % (self.c, self.exp, self.c**self.exp)
        )

    def panel(self):
        return pn.Row(self.param, self.view)


stage2 = Stage2(c=stage1.output()[0])
stage2.panel()
[6]:

Auch stage2 fügen wir nun dem pipeline-Objekt hinzu:

[7]:
pipeline.add_stage("Stage 2", stage2)

Wir haben nun eine zweistufige Pipeline, bei der der Output c von stage1 an stage2 übergeben wird. Nun können wir uns die Pipeline anzeigen lassen mit pipeline.layout:

[8]:
pipeline.layout
[8]:

Das Rendering der Pipeline zeigt ein kleines Diagramm mit den verfügbaren Workflow-Stufen sowie die Schaltflächen Previous und Next, um zwischen den einzelnen Phasen wechseln zu können. Dies ermöglicht die Navigation auch in komplexeren Workflows mit sehr viel mehr Phasen.

Oben haben wir jede Stufe einzeln instanziiert. Wenn die Pipeline jedoch als Server-App deployed werden soll, können die Stufen jedoch auch als Teil des Konstruktors deklariert werden:

[9]:
stages = [("Stage 1", Stage1), ("Stage 2", Stage2)]

pipeline = pn.pipeline.Pipeline(stages)
pipeline.layout
[9]:

Dabei können die Pipeline-Stufen entweder Parameterized-Instanzen oder Parameterized-Klassen sein. Bei Instanzen müsst ihr jedoch darauf achten, dass die Aktualisierung der Parameter der Klasse auch den aktuellen Status der Klasse aktualisiert.