Tutorial

The purpose of the sequencer is to execute Sequencer scripts, either for engineering or science purposes.

The unit of execution is a single step which is just a normal python function or method with no input parameters.

Sequences are modeled as Directed Acyclic Graph (DAG). Each node in the graph can either be a simple action which just invokes a single sequencer step or a more complex node which contains a complete sequencer script.

This allows sequences to be grouped and nested freely. Ultimately they will execute steps.

The Sequencer API allows to create these graphs.

Building Sequences

Sequencer scripts are modeled as DAGs, the Sequencer API allows to create nodes in the DAG. There are different node types that determine the way its children are scheduled for execution (e.g. Parallel, Sequential), you are free to select, mix and match the node type(s) that suits better to your needs.

Each node in the DAG depends, for execution purposes, from its parent nodes. Meaning that a node will not be started until every node that precedes it has finished its own execution.

The Sequencer Engine expects to find either a module method or a specific class in a module in order to construct a Sequencer script from it. The conventions are the following.

  1. A module level create_sequence() function. See Tutorial 1.

  2. A class named Tpl which must provide a static method create_sequence. See Parallel tasks sample.

In either case, the return value is the root node of the graph being implemented.

The first convention is tried first, if no create_sequence() function is found, the second convention is attempted.

For very simple scripts, following the first convention is perfectly fine. For more complex scripts, e.g. one is creating a class and methods to control .e.g. a detector, and it is going to be instantiated many times to control many detectors in parallel, the class approach is advised.

Node Dependencies

Node dependencies determines the execution order of the sequence steps. A node wont’t run until all its dependencies have finished. In the graph, a node dependency is seen as an icoming edge.

The basic node types Sequence and Parallel defines a dependency hierarchy.

Sequence nodes are executed one after the other, therefore each node depends on its predecessor. On the other hand, Parallel nodes indicates that all nodes it contains shall be executed together. By combining and chaining this two basic node types it is possible to express any dependency graph.

Very simple sequences

Let’s start with a simple sequence.

As mentioned before the simplest step is a python coroutine with no input parameters which is used to create an Action node.

Note

The no input parameter rule can be bypassed with strategies shows in Passing Arguments to Actions

In this case, we define a sequence that executes two steps, one after the other, namely do_a() and do_b(). Source codes is shown below.

Tutorial 1
""" Sequencer tutorial 01.

The simplest sequence.
Execute one step after the other.
"""
import asyncio
from seqlib.nodes import Sequence, Action


async def do_a():
    print("A")


async def do_b():
    await asyncio.sleep(1)
    print("B")


def create_sequence(*args, **kw):
    """Creates a simple sequence"""
    return Sequence.create(
        Action(do_a, name="A"), do_b, name="Tut_01"
    )

Important

The Sequencer Engine is based on asyncio library, therefore it is biased towards coroutines, but they are not mandatory as shown in Loop example.

There are some simple rules to create sequences:
  • A step (Action) is a python coroutine with no input parameters. See Passing Arguments to Actions to break this rule.

  • In this case, the sequence is created using the Sequence.create (Sequence) constructor, which receives the steps to be executed (in the given order).

  • The python module which contains the sequence must define a create_sequence function as shown in the example. It returns a Sequence node that holds the nodes (Action (s) in this case) that it will execute.

Note

The Sequence.create constructor provides syntax sugar in order to support passing coroutines as the sequence’s grapn nodes (do_b in the above example). In such a case a node of type seq.nodes.Action is automagically created and inserted in the graph.

This simple sequence 01 is graphically shown below.

strict digraph "" {
	graph [bb="0,0,297.59,66",
		label="seq.samples.tut.tut01",
		lheight=0.19,
		lp="148.79,11",
		lwidth=1.64,
		rankdir=LR
	];
	node [label="\N"];
	start_Tut_01_7K99X2B	[color=black,
		height=0.5,
		label="",
		pos="18,44",
		shape=circle,
		style=filled,
		width=0.5];
	A_oAJLK	[height=0.5,
		label=A,
		pos="99,44",
		width=0.75];
	start_Tut_01_7K99X2B -> A_oAJLK	[pos="e,71.874,44 36.142,44 43.644,44 52.75,44 61.642,44"];
	do_b_Bg3N	[height=0.5,
		label=do_b,
		pos="189.79,44",
		width=0.77205];
	A_oAJLK -> do_b_Bg3N	[pos="e,161.9,44 126.16,44 134.16,44 143.12,44 151.7,44"];
	end_Tut_01_7K99X2B	[color=black,
		height=0.61111,
		label="",
		pos="275.59,44",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	do_b_Bg3N -> end_Tut_01_7K99X2B	[pos="e,253.51,44 217.77,44 225.91,44 234.91,44 243.31,44"];
}

simple sequence 01

Notice that despite that in the create_sequence() function only two nodes were specified, the simple sequence 01 figure shows 4 nodes. The sequencer engine adds a start node (black circle) and end node (double black circle) to every node container type, i.e. those nodes that have children: Parallel, Sequence and Loop.

Note

The start and end node, among other things, makes easy to chain nodes together by linking the end node of a container with the start of the next.

Executing Tasks in Parallel

One is not limited to create just linear sequences. Parallel activities (pseudo parallel) can be created using the Parallel.create() constructor. It receives the same parameters as the Sequence node constructor. When executed, the sequencer engine processes the :class:’Parallel’ nodes children in parallel.

Parallel tasks sample
"""
Defines a sequence where activities are executed in (pseudo) Parallel.
"""
import asyncio
from seqlib.nodes import Action, ActionInThread, Parallel


class Tpl:
    def do_a(self):
        """I don't need to be a coroutine"""
        print("A")

    async def do_b(self):
        await asyncio.sleep(1)
        print("B")

    @staticmethod
    def create(*args, **kw):
        t = Tpl()
        return Parallel.create(
            ActionInThread(t.do_a, name="one"),
            Action(t.do_b, name="two"),
            name="Tut_02",
        )

Which is represented graphically as follows.

strict digraph "" {
	graph [bb="0,0,206,112",
		label="seq.samples.tut.tut02",
		lheight=0.19,
		lp="103,11",
		lwidth=1.64,
		rankdir=LR
	];
	node [label="\N"];
	start_Tut_02_xA3NN4J	[color=black,
		height=0.5,
		label="",
		pos="18,67",
		shape=circle,
		style=filled,
		width=0.5];
	one_X6ZBW	[height=0.5,
		label=one,
		pos="99,94",
		width=0.75];
	start_Tut_02_xA3NN4J -> one_X6ZBW	[pos="e,74.761,86.058 35.369,72.594 44,75.544 54.905,79.271 65.188,82.786"];
	two_D1pWx	[height=0.5,
		label=two,
		pos="99,40",
		width=0.75];
	start_Tut_02_xA3NN4J -> two_D1pWx	[pos="e,74.761,47.942 35.369,61.406 44,58.456 54.905,54.729 65.188,51.214"];
	end_Tut_02_xA3NN4J	[color=black,
		height=0.61111,
		label="",
		pos="184,67",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	one_X6ZBW -> end_Tut_02_xA3NN4J	[pos="e,162.76,73.584 123.56,86.335 132.74,83.35 143.37,79.893 153.07,76.735"];
	two_D1pWx -> end_Tut_02_xA3NN4J	[pos="e,162.76,60.416 123.56,47.665 132.74,50.65 143.37,54.107 153.07,57.265"];
}

Parallel Sequence

Points to notice:
  1. In this case the Sequencer Engine discover a class named Tpl and calls its create method (@staticmethod as the convention mandates).

  2. The example Parallel Sequence also shows that steps are not limited to coroutines. Just wrap it in ActionInThread node.

In order to avoid normal methods or functions to potentially block the asyncio loop (by holding th e CPU) they must be executed on their own Thread.

This is achieved with the ActionInThread node. In the example the do_a() method is wrapped in such node. Otherwise the Sequence constructor will complain and raise an exception.

Executing Tasks in a Loop

The Loop node allows to repeat a set of steps while a condition is True.

Loop example
"""
Defines a Loop
"""
import asyncio
from seqlib.nodes import Action, ActionInThread, Loop


def do_a():
    """I don't need to be a coroutine"""
    print("A")


async def do_b():
    await asyncio.sleep(1)
    print("B")


async def do_c():
    print("C")


async def my_condition():
    """Loop.index is a contex variable. Use get() to access it"""
    return Loop.index.get() < 5


def create_sequence(*args, **kw):
    """Creates a simple sequence"""

    return Loop.create(
        ActionInThread(do_a, name="one"),
        Action(do_b, name="two"),
        Action(do_c, name="three"),
        condition=my_condition,
        name="Tut_Loop",
    )

Which is represented graphically as follows.

strict digraph "" {
	graph [bb="0,0,761.97,158",
		label="seq.samples.tut.tut03",
		lheight=0.19,
		lp="380.99,11",
		lwidth=1.64,
		rankdir=LR
	];
	node [label="\N"];
	start_Tut_Loop_gjPPgDY	[color=black,
		height=0.5,
		label="",
		pos="18,78",
		shape=circle,
		style=filled,
		width=0.5];
	"Loop._init_loop_JP2Xl"	[height=0.5,
		label="Loop init",
		pos="115.78,78",
		width=1.1883];
	start_Tut_Loop_gjPPgDY -> "Loop._init_loop_JP2Xl"	[pos="e,72.709,78 36.123,78 43.711,78 53.085,78 62.687,78"];
	my_condition_14Nkq	[height=0.5,
		label=condition,
		pos="257.09,78",
		shape=diamond,
		width=1.7093];
	"Loop._init_loop_JP2Xl" -> my_condition_14Nkq	[pos="e,195.52,78 158.58,78 167.02,78 176.12,78 185.24,78"];
	end_Tut_Loop_gjPPgDY	[color=black,
		height=0.61111,
		label="",
		pos="384.41,136",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	my_condition_14Nkq -> end_Tut_Loop_gjPPgDY	[label=F,
		lp="340.52,124",
		pos="e,364.05,127.04 281.66,88.909 302.41,98.514 332.69,112.53 354.82,122.77"];
	start_block_Tut_Loop_gjPPgDY	[color=black,
		height=0.5,
		label="",
		pos="384.41,78",
		shape=circle,
		style=filled,
		width=0.5];
	my_condition_14Nkq -> start_block_Tut_Loop_gjPPgDY	[pos="e,366.17,78 318.91,78 331.92,78 345.03,78 356,78"];
	one_4Lqjg	[height=0.5,
		label=one,
		pos="470.41,78",
		width=0.75];
	start_block_Tut_Loop_gjPPgDY -> one_4Lqjg	[pos="e,443.09,78 402.82,78 411.54,78 422.44,78 432.89,78"];
	two_7XZo8	[height=0.5,
		label=two,
		pos="561.41,78",
		width=0.75];
	one_4Lqjg -> two_7XZo8	[pos="e,534.38,78 497.63,78 505.96,78 515.32,78 524.23,78"];
	three_21PxW	[height=0.5,
		label=three,
		pos="653.19,78",
		width=0.77169];
	two_7XZo8 -> three_21PxW	[pos="e,625.31,78 588.86,78 597.12,78 606.38,78 615.21,78"];
	end_block_Tut_Loop_gjPPgDY	[color=black,
		height=0.61111,
		label="",
		pos="739.97,44",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	three_21PxW -> end_block_Tut_Loop_gjPPgDY	[pos="e,719.28,51.896 677.36,68.709 687.42,64.675 699.31,59.906 709.95,55.639"];
	end_block_Tut_Loop_gjPPgDY -> my_condition_14Nkq	[pos="e,277.68,65.914 718.18,39.395 701.09,36.011 676.19,32 654.19,32 383.41,32 383.41,32 383.41,32 348.47,32 311.23,47.95 286.51,61.065"];
}

Loop example

As the code in Loop example shows, the Loop’s constructor takes a variable number of arguments as the Loop’s body, the part that is repeated, do_a(), do_b() and do_c() in this case. The condition node is specified with the condition keyword.

The Loop’s index is kept in the context variable index, meaning it can be accessed as Loop.index.get() as the my_condition() function shows.

In order to separate the index value of the different loops that might be occurring at the same time the Loop’s index is implemented as a context variable. Therefore to get its value one has to call its get() method as the my_condition() function shows.

Embedding Sequencer Scripts

Sequences can be reused or embedded in order to produce more complex activities. The following example uses the sequences “Tut_01” and “Tut_02” to create a new sequence and just for the kicks it puts each one in a different Loop, combined with a couple of local functions.

Important

Embedding a Sequence entails to import the module and instantiate its Sequencer script (either with create_sequence() or by Tpl.create(). But how do you know which one to call? You don’t have to know. You let the OB object do it for you as:

from seqlib.ob import OB
from seq.samples.tut import tut01

mynode = OB.create_sequence(tut01)
Sequence embedding example
""" Tut03 - Sequence embedding

Defines a sequence reusing other sequencer scripts.
"""
from seqlib.nodes import Loop, Parallel
from seq.samples.tut import tut01, tut02
from seqlib.ob import OB


async def do_something():
    print("something")


async def a_condition():
    return Loop.index < 5


async def b_condition():
    return Loop.index < 3


def create_sequence():
    a = OB.create_sequence(tut01)
    # .create_sequence()
    b = OB.create_sequence(tut02)
    # .create_sequence()

    loop_a = Loop.create(
        a, do_something, condition=a_condition
    )
    loop_b = Loop.create(
        b, do_something, condition=b_condition
    )

    return Parallel.create(
        loop_a, loop_b, name="EMBED_SAMPLE"
    )
Some points to note:
  • Use seqlib.ob.OB.create_sequence() to select the right method to instantiate a predefined sequencer script, so it can be reused.

  • There are two loops. Each condition node has its own copy of Loop.index.

The graphical representation of Sequence embedding example is shown at embedded sequence, it has many levels due to the two loops running in parallel.

strict digraph "" {
	graph [bb="0,0,1139.9,366",
		label="seq.samples.tut.tut_embed01",
		lheight=0.19,
		lp="569.94,11",
		lwidth=2.25,
		rankdir=LR
	];
	node [label="\N"];
	start_EMBED_SAMPLE_RmzJPwz	[color=black,
		height=0.5,
		label="",
		pos="18,209",
		shape=circle,
		style=filled,
		width=0.5];
	start_Loop_qq34R4r	[color=black,
		height=0.5,
		label="",
		pos="91,237",
		shape=circle,
		style=filled,
		width=0.5];
	start_EMBED_SAMPLE_RmzJPwz -> start_Loop_qq34R4r	[pos="e,73.953,230.67 35.111,215.35 43.826,218.79 54.789,223.11 64.622,226.99"];
	start_Loop_RmzJPPR	[color=black,
		height=0.5,
		label="",
		pos="91,183",
		shape=circle,
		style=filled,
		width=0.5];
	start_EMBED_SAMPLE_RmzJPwz -> start_Loop_RmzJPPR	[pos="e,73.953,188.88 35.111,203.1 43.735,199.94 54.561,195.98 64.314,192.41"];
	"Loop._init_loop_zpEJy"	[height=0.5,
		label="Loop init",
		pos="188.78,237",
		width=1.1883];
	start_Loop_qq34R4r -> "Loop._init_loop_zpEJy"	[pos="e,145.71,237 109.12,237 116.71,237 126.09,237 135.69,237"];
	"Loop._init_loop_z6Jjq"	[height=0.5,
		label="Loop init",
		pos="188.78,183",
		width=1.1883];
	start_Loop_RmzJPPR -> "Loop._init_loop_z6Jjq"	[pos="e,145.71,183 109.12,183 116.71,183 126.09,183 135.69,183"];
	end_EMBED_SAMPLE_RmzJPwz	[color=black,
		height=0.61111,
		label="",
		pos="538.41,190",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	a_condition_mExP0	[height=0.5,
		label=condition,
		pos="330.09,244",
		shape=diamond,
		width=1.7093];
	"Loop._init_loop_zpEJy" -> a_condition_mExP0	[pos="e,277.2,241.39 231.58,239.1 242.78,239.66 255.14,240.28 267.19,240.89"];
	end_Loop_qq34R4r	[color=black,
		height=0.61111,
		label="",
		pos="457.41,290",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	end_Loop_qq34R4r -> end_EMBED_SAMPLE_RmzJPwz	[pos="e,523.94,207.05 471.81,273.04 484.41,257.09 503.42,233.03 517.74,214.91"];
	a_condition_mExP0 -> end_Loop_qq34R4r	[label=F,
		lp="413.52,282",
		pos="e,436.55,282.71 357.97,253.87 378.25,261.31 406.03,271.51 426.92,279.17"];
	start_block_Loop_qq34R4r	[color=black,
		height=0.5,
		label="",
		pos="457.41,348",
		shape=circle,
		style=filled,
		width=0.5];
	a_condition_mExP0 -> start_block_Loop_qq34R4r	[pos="e,442.96,336.84 346.96,257.17 369.33,275.73 410.18,309.63 435.15,330.36"];
	start_Tut_01_qED0N9r	[color=black,
		height=0.5,
		label="",
		pos="538.41,308",
		shape=circle,
		style=filled,
		width=0.5];
	start_block_Loop_qq34R4r -> start_Tut_01_qED0N9r	[pos="e,522.1,315.75 474.02,340.1 485.13,334.47 500.27,326.8 512.97,320.37"];
	A_OPnQE	[height=0.5,
		label=A,
		pos="624.41,290",
		width=0.75];
	start_Tut_01_qED0N9r -> A_OPnQE	[pos="e,598.47,295.34 556.41,304.36 565.66,302.38 577.45,299.85 588.56,297.47"];
	do_something_vmAJr	[height=0.5,
		label=do_something,
		pos="998.65,287",
		width=1.6727];
	end_block_Loop_qq34R4r	[color=black,
		height=0.61111,
		label="",
		pos="1117.9,252",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	do_something_vmAJr -> end_block_Loop_qq34R4r	[pos="e,1096.6,258.05 1042,274.36 1056.8,269.93 1073.2,265.05 1086.8,260.98"];
	end_block_Loop_qq34R4r -> a_condition_mExP0	[pos="e,383.52,241.58 1095.8,248.53 1072.3,244.99 1033.4,240 999.65,240 456.41,240 456.41,240 456.41,240 436.02,240 413.77,240.54 393.76,\
241.21"];
	do_b_rEPOK	[height=0.5,
		label=do_b,
		pos="716.2,290",
		width=0.77205];
	A_OPnQE -> do_b_rEPOK	[pos="e,688.32,290 651.87,290 660.12,290 669.38,290 678.22,290"];
	end_Tut_01_qED0N9r	[color=black,
		height=0.61111,
		label="",
		pos="841.22,290",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	do_b_rEPOK -> end_Tut_01_qED0N9r	[pos="e,818.99,290 744.2,290 763.26,290 788.87,290 808.84,290"];
	end_Tut_01_qED0N9r -> do_something_vmAJr	[pos="e,938.48,288.14 863.5,289.59 880.31,289.26 904.75,288.79 928.17,288.34"];
	b_condition_L5mnW	[height=0.5,
		label=condition,
		pos="330.09,183",
		shape=diamond,
		width=1.7093];
	"Loop._init_loop_z6Jjq" -> b_condition_L5mnW	[pos="e,268.52,183 231.58,183 240.02,183 249.12,183 258.24,183"];
	end_Loop_RmzJPPR	[color=black,
		height=0.61111,
		label="",
		pos="457.41,190",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	end_Loop_RmzJPPR -> end_EMBED_SAMPLE_RmzJPwz	[pos="e,516.18,190 479.58,190 487.73,190 497.21,190 506.11,190"];
	b_condition_L5mnW -> end_Loop_RmzJPPR	[label=F,
		lp="413.52,195",
		pos="e,435.19,188.81 382.06,185.85 396.57,186.66 411.94,187.52 424.92,188.24"];
	start_block_Loop_RmzJPPR	[color=black,
		height=0.5,
		label="",
		pos="457.41,94",
		shape=circle,
		style=filled,
		width=0.5];
	b_condition_L5mnW -> start_block_Loop_RmzJPPR	[pos="e,442.01,103.7 348.46,170.18 364.3,158.54 388.42,140.94 409.62,126 417.37,120.54 425.91,114.66 433.55,109.45"];
	start_Tut_02_xA34qLP	[color=black,
		height=0.5,
		label="",
		pos="538.41,94",
		shape=circle,
		style=filled,
		width=0.5];
	start_block_Loop_RmzJPPR -> start_Tut_02_xA34qLP	[pos="e,520.3,94 475.55,94 485.7,94 498.78,94 510.29,94"];
	one_XLNW8	[height=0.5,
		label=one,
		pos="624.41,40",
		width=0.75];
	start_Tut_02_xA34qLP -> one_XLNW8	[pos="e,604.22,52.338 554.04,84.594 565.46,77.257 581.65,66.849 595.51,57.934"];
	two_vljOM	[height=0.5,
		label=two,
		pos="624.41,94",
		width=0.75];
	start_Tut_02_xA34qLP -> two_vljOM	[pos="e,597.09,94 556.82,94 565.54,94 576.44,94 586.89,94"];
	do_something_XM7G8	[height=0.5,
		label=do_something,
		pos="841.22,93",
		width=1.6727];
	end_block_Loop_RmzJPPR	[color=black,
		height=0.61111,
		label="",
		pos="998.65,128",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	do_something_XM7G8 -> end_block_Loop_RmzJPPR	[pos="e,976.79,123.3 889.7,103.69 914.82,109.35 944.94,116.13 966.98,121.09"];
	end_block_Loop_RmzJPPR -> b_condition_L5mnW	[pos="e,351.64,171.25 976.57,130.66 946.72,134.2 890.39,140 842.22,140 456.41,140 456.41,140 456.41,140 422.3,140 385.65,154.43 360.85,\
166.57"];
	end_Tut_02_xA34qLP	[color=black,
		height=0.61111,
		label="",
		pos="716.2,90",
		shape=doublecircle,
		style=filled,
		width=0.61111];
	one_XLNW8 -> end_Tut_02_xA34qLP	[pos="e,696.74,79.72 645.79,51.346 658.23,58.275 674.3,67.224 687.84,74.765"];
	two_vljOM -> end_Tut_02_xA34qLP	[pos="e,694.12,90.939 651.38,92.843 661.56,92.39 673.33,91.866 683.96,91.392"];
	end_Tut_02_xA34qLP -> do_something_XM7G8	[pos="e,781.01,91.556 738.35,90.516 747.64,90.742 759.12,91.022 770.99,91.312"];
}

embedded sequence

Conclusion

This finishes the basic tutorial. One can create any type of flow using the node types show. Mor details about node’s attributes and context are given in A Deeper Look.