Skip to content

Processes

Laboratory workflows are expressed as processes and use the PythonLab package to express the steps. A process defines the labware that is used and what the interactions should be. pythonLab processes are denoted in a python like syntax, but they are not directly executed by a python interpreter. They are rather parsed into a workflow graph, which can be used by a Scheduler to calculate an optimal schedule (=order of execution). This order of execution might be different from the initial notation. An Orchestrator executes then the schedule and supervises the device communication, e.g. to SiLA servers/devices.

pythonLab Architecture

Basic process

If you are planning to define multiple processes within the same lab it is advised to start with a basic process that subsequent processes can inherit from. This is the basis that other processes can build on.

BasicProcess
from abc import ABC

from pythonlab.resources.services.moving import MoverServiceResource
from pythonlab.resources.services.human import HumanServiceResource
from pythonlab.resources.services.hello_world import GreeterServiceResource
from pythonlab.resources.services.labware_storage import LabwareStorageResource
from pythonlab.resource import LabwareResource

# TODO: add whatever resources you need
from pythonlab.process import PLProcess


class BasicProcess(PLProcess, ABC):
    def __init__(
        self, process_name: str, num_plates: int = 0, priority=7
    ):  # 0 has highest priority
        self.num_mw_plates = num_plates
        self.name = process_name

        super().__init__(priority=priority)

    def create_resources(self):
        # the device names should match the ones in the platform_config
        self.hotel1 = LabwareStorageResource(proc=self, name="Hotel1")
        self.hotel2 = LabwareStorageResource(proc=self, name="Hotel2")
        self.hotel3 = LabwareStorageResource(proc=self, name="Hotel3")
        self.robot_arm = MoverServiceResource(proc=self, name="GenericArm")
        self.human = HumanServiceResource(proc=self, name="Human")
        self.greeter = GreeterServiceResource(proc=self, name="Greeter")
        # TODO: add your resources here with names matching the platform_config

        # the continers are automatically named/enumerated. You can change the naming without causing problems
        self.containers = [
            LabwareResource(
                proc=self, name=f"{self.name}_cont_{cont}", lidded=True, filled=False
            )
            for cont in range(self.num_mw_plates)
        ]

    def process(self):
        raise NotImplementedError

In this case, BasicProcess defines a number of resources that are present in this setup. There are 3 hotels for storage, a robot arm to move plates, a human being and a "greeter" entity. It also initialized an arbitrary amount of plates. The names in your BasicProcess need to match with the names defined in your platform_config.

Simple one-step process

The process GreeterTest defines a simple process that only includes one step. The process function implements the actual steps that should be executed. In this case, it is only one call to the Greeter SiLA server

GreeterTest
"""
Duplicate this file and add/modify the missing parts to create new processes
"""

from lab_adaption.processes.basic_process import BasicProcess


class GreeterTest(BasicProcess):
    def __init__(self):
        super().__init__(num_plates=1, process_name="GreeterTest")

    def init_service_resources(self):
        # setting start position of containers
        super().init_service_resources()
        for i, cont in enumerate(self.containers):
            cont.set_start_position(self.hotel1, i)

    def process(self):
        self.greeter.wave(self.containers[0])

A more elaborate process

InterestingExample is a more elaborate process with multiple interactions.

InterestingExample
"""
Duplicate this file and add/modify the missing parts to create new processes
"""

from lab_adaption.processes.basic_process import BasicProcess


class InterestingExample(BasicProcess):
    def __init__(self):
        super().__init__(priority=3, num_plates=3, process_name="InterestingExample")

    def init_service_resources(self):
        # setting start position of containers
        super().init_service_resources()
        for i, cont in enumerate(self.containers):
            cont.set_start_position(self.hotel1, i)

    def judge_answer(self, answer) -> bool:
        # extract the number from the response
        number = answer.Response.value
        # return whether the number is even
        return number % 2 == 0

    def process(self):
        # loop through all containers
        for cont in self.containers:
            # move all containers to hotel2 and read their barcodes
            self.robot_arm.move(cont, self.hotel2, read_barcode=True)
            # move all containers to the human for inspection (it can hold up to two)
            self.robot_arm.move(cont, self.human)
            # have the human assign a number to each container
            answer = self.human.request_number(
                cont, message=f"assign number to {cont.name}!"
            )
            # do some computation on the result
            judgement = self.judge_answer(answer)
            # depending on the result, put the labware in hotel1 or hotel 3
            if judgement:
                self.robot_arm.move(cont, self.hotel3)
            else:
                self.robot_arm.move(cont, self.hotel1)

One by one, it reads the barcodes of the containers and holds it up for a human being to inspect. Based on their answer, the container is moved either to hotel 1 or 3.

Inspecting processes

Pythonlab converts process descriptions to a graph-based workflow. Quickstart shows how you can access the UI to inspect the workflow graph.

More Examples

Below are some more condensed examples on how to describe workflows in pythonLab

Simple linear workflow

protocol_path = "protocols/evacuation_speroids.lhc"
self.robot_arm.move(cont, self.dispenser)
self.dispenser.run_protocol(labware=cont, protocol=protocol_path)

This process is parsed into the following workflow graph:

Simple linear workflow

Implicit movements for reagents and multi labware steps

bravo_positions = [4, 6, 7, 8]
for i in range(len(growth_plates)):
    self.robot_arm.move(cont, target_loc=self.pipetter, lidded=False, position=bravo_positions[i])
self.pipetter.executeProtocol(growth_plates[0], protocol=sup_rem_protocol, duration=200,
                              reagents=growth_plates[1:],
self.pipetter.executeProtocol(growth_plates, protocol=lysis_protocol, duration=240,
                            reagents=[self.lysis_buffer],
                            reagent_pos=[3])          
for cont in growth_plates:
    self.robot_arm.move(cont, self.incubator2, lidded=True)                           

The reagent plates in reagents=[...] are moved to and from the liquid handler implicitly:

Implicit reagent movement workflow

Simple for-loop

for plate in self.target_plates:
    self.robot_arm.move(plate, self.echo, role="destination", read_barcode=True, lidded=False)
    self.echo.execute_transfer_protocol(self.source_plate, plate, protocol)
    self.robot_arm.move(plate, self.hotel2, lidded=True)

This process is parsed into the following workflow graph:

Simple for-loop workflow

Single conditional

def is_acceptable(answer) -> bool:
    return answer.Response.value >= 5

cont = self.containers[0]
self.robot_arm.move(cont, self.human)
answer = self.human.request_number(cont, message="assign quality score")
acceptable = self.is_acceptable(answer)
if acceptable:
    self.robot_arm.move(cont, self.incubator1)
else:
    self.robot_arm.move(cont, self.hotel1)

This process is parsed into the following workflow graph:

Single conditional workflow

Nested for-loop and timing comstraints

meas_time = 65
meas_points = [0, 5, 15, 30]
for cont in self.containers:
    self.robot_arm.move(cont, self.reader)
    self.reader.single_read(cont, method="protocol", label=f"read_{cont.name}_{0}")
    for i in range(1, len(meas_points)):
        self.robot_arm.move(cont, self.incubator1)
        self.incubator1.incubate(cont, duration=10, temperature=295, shaking_frequency=400)
        self.robot_arm.move(cont, self.reader)
        wait = 60*(meas_points[i] - meas_points[i-1]) - meas_time
        self.reader.single_read(cont, method="protocol", label=f"read_{cont.name}_{i}",
                                relations=[("min_wait", f"read_{cont.name}_{i-1}", [wait]),
                                            ("max_wait", f"read_{cont.name}_{i-1}", [wait+20])
                                            ]
        )
    self.robot_arm.move(cont, self.hotel)

This process is parsed into the following workflow graph:

Nested for-loop workflow

Nested for-loop with conditional and break

# cultured plates in incubator 37°, 200rpm, 45-90 min until aver_OD is >= 0.4.
for cont in cultured_plates:
    for j in range(3):
        self.robot_arm.move(cont, target_loc=self.reader2, lidded=False)
        od = self.reader2.single_read(cont, method=od_600)
        if j < max_cult_intervals - 1:
            aver_od = self.compute_average(od)
            if aver_od > 0.4:
                break
            else:
                # incubate for another interval
                self.robot_arm.move(cont, target_loc=self.incubator2, lidded=True)
                self.incubator2.incubate(cont, duration=cult_time_interval, temperature=cult_temp,
                                         shaking_frequency=cult_shaking_freq)    
    self.robot_arm.move(cont, target_loc=self.pipetter, lidded=False, position=3)   

This process is parsed into the following workflow graph:

Nested for-loop with conditional and break workflow