Create a Pipeline#

This guide addresses how to connect multiple panels into a Pipeline to express complex multi-page workflows where the output of one stage feeds into the next stage.

Prerequisites

  1. The Param with Panel How-to Guides describe how to set up classes that declare parameters and link them to some computation or visualization.


To start, lets instantiate an empty Pipeline. We will use the ‘katex’ extension to render mathematical notation in this example.

import param
import panel as pn
pn.extension('katex')

pipeline = pn.pipeline.Pipeline()

Now let’s populate the pipeline with our first stage which takes two inputs (a and b) and produces two outputs (c, computed by multiplying the inputs, and d, computed by raising a to the power b).

To create this stage, let’s:

  1. Declare a Parameterized class with some input parameters (e.g. a = param.Integer)

  2. Decorate a method with @param.output to declare outputs (we’ll discuss this more later)

  3. Declare a panel method that returns a view of this stage’s object

class Stage1(param.Parameterized):

    a = param.Integer(default=2, bounds=(0, 10))
    b = param.Integer(default=3, bounds=(0, 10))

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

    @param.depends('a', 'b')
    def view(self):
        c, d = self.output()
        c_out = pn.pane.LaTeX('${a} * {b} = {c}$'.format(
            a=self.a, b=self.b, c=c), styles={'font-size': '2em'})
        d_out = pn.pane.LaTeX('${a}^{{{b}}} = {d}$'.format(
            a=self.a, b=self.b, d=d), styles={'font-size': '2em'})
        return pn.Column(
		    c_out, d_out,  margin=(40, 10), styles={'background': '#f0f0f0'}
		)

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

We can now render this stage on its own:

stage1 = Stage1()
stage1.panel()

Before we create a second stage let’s briefly discuss the some details about the system of outputs that links the stages. To declare the output for our first stage, we decorated one of its methods with @param.output(('c', param.Integer), ('d', param.Integer)). However, there are multiple ways to declare outputs with this decorator:

  • param.output(): Declaring an output without arguments will declare that the method returns an output that will inherit the name of the method and does not make any specific type declarations.

  • param.output(param.Integer): Declaring an output with a specific Parameter or Python type also declares an output with the name of the method but declares that the output will be of a specific type.

  • param.output(c=param.Integer): Declaring an output using a keyword argument allows overriding the method name as the name of the output and declares the type.

It is also possible to declare multiple outputs, either as keywords or tuples:

  • param.output(c=param.Integer, d=param.String) or

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

Importantly, in addition to passing along the outputs designated with param.output(), the Pipeline will also pass along the values of any input parameters whose names match input parameters on the next stage (unless inherit_params is set to False).

Ok, enough explanation, let’s take a look at the outputs of our first stage:

stage1.param.outputs()
{'c': (<param.parameters.Integer object at 0x121b0d0c0>, <bound method Stage1.output of Stage1(a=2, b=3, name='Stage100974')>, 0), 'd': (<param.parameters.Integer object at 0x121b0d180>, <bound method Stage1.output of Stage1(a=2, b=3, name='Stage100974')>, 1)}

Our Pipeline will use this information to determine what outputs are available to be fed into the next stage of the workflow.

Now let’s set up a second stage that will also declare a c input Parameter to consume the c output of the first stage. Note, the second stage does not have to consume all parameters, and here we will ignore the first stage’s output d. Otherwise, the second stage below is very similar to the first one; it declares both a view method that depends on the parameters of the class, and a panel method that returns a view of the object. As this is our last stage, we don’t need to define any further outputs.

class Stage2(param.Parameterized):

    c = param.Integer(default=6, bounds=(0, None))
    exp = param.Number(default=0.1, bounds=(0, 3))

    @param.depends('c', 'exp')
    def view(self):
        out = pn.pane.LaTeX('${%s}^{%s}={%.3f}$' % (self.c, self.exp, self.c**self.exp),
                      styles={'font-size': '2em'})
        return pn.Column(out, margin=(40, 10), styles={'background': '#f0f0f0'})

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

Now let’s add our stages to our Pipeline using the add_stage method

pipeline.add_stage('Stage 1', Stage1)
pipeline.add_stage('Stage 2', Stage2)

Finally, to display the pipeline UI we simply let it render itself:

pipeline

As you can see the Pipeline renders a diagram displaying the available stages in the workflow along with previous and next buttons to move between each stage. Note also when progressing to Stage 2, the c parameter widget is not rendered because its value has been provided by the previous stage.

Here is the complete code for this section in case you want to easily copy it:

import param
import panel as pn
pn.extension('katex')

pipeline = pn.pipeline.Pipeline()

class Stage1(param.Parameterized):

    a = param.Integer(default=2, bounds=(0, 10))
    b = param.Integer(default=3, bounds=(0, 10))

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

    @param.depends('a', 'b')
    def view(self):
        c, d = self.output()
        c_out = pn.pane.LaTeX('${a} * {b} = {c}$'.format(
            a=self.a, b=self.b, c=c), styles={'font-size': '2em'})
        d_out = pn.pane.LaTeX('${a}^{{{b}}} = {d}$'.format(
            a=self.a, b=self.b, d=d), styles={'font-size': '2em'})
        return pn.Column(c_out, d_out,  margin=(40, 10), styles={'background': '#f0f0f0'})

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

class Stage2(param.Parameterized):

    c = param.Integer(default=6, bounds=(0, None))
    exp = param.Number(default=0.1, bounds=(0, 3))

    @param.depends('c', 'exp')
    def view(self):
        out = pn.pane.LaTeX('${%s}^{%s}={%.3f}$' % (self.c, self.exp, self.c**self.exp),
                      styles={'font-size': '2em'})
        return pn.Column(out, margin=(40, 10), styles={'background': '#f0f0f0'})

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

pipeline.add_stage('Stage 1', Stage1)
pipeline.add_stage('Stage 2', Stage2)

pipeline