6 Advanced Protocols
Advanced protocols can contain many more elements than the simple protocols described in the previous chapter. In this chapter we’ll discuss parameters
, objects
, data
properties, variable scope, substitution, directives, and template functions.
6.1 Parameters
Parameters are named values that you define at the beginning of a protocol. You can then use the parameter name instead of the value later in the protocol. In this partial example, a VOLUME
parameter is defined which is used in the pipetter.pipette
step:
parameters:
VOLUME:
description: "amount of water to dispense"
value: 200 ul
...
steps:
1:
command: pipetter.pipette
sources: water
destinations: plate1(all)
volumes: $#VOLUME
A parameter should be given a description
and a value
. Notice that $#
prefixes VOLUME
in the step; $#
tells Roboliq to substitute in a parameter value. Substitution is discussed in more detail later in this chapter.
6.2 Objects
There are several more object types you might want to use in advanced protocols:
Data
: for defining a data table. The Data
type facilitates complex experimental designs, and you can read more about it in the next section and in the chapter on Design Tables.
Variable
: For defining references to other variables. Variables are not particularly useful in Roboliq, but they could potentially be used to easily switch between objects in case you have, for example, two water sources water1
and water2
. In that case you could have a water
variable whose value to set to the source you want to use:
objects:
water1: ...
water2: ...
water:
description: "water source"
type: Variable
value: water1
steps:
1:
command: pipetter.pipette
sources: water
destinations: plate1(all)
volumes: 50 ul
Template
: You can use a template to define re-usable steps. Here is a toy example for a template named dispenseToPlate1
which creates a pipetter.pipette
command that transfers a volume of water to all wells on plate1
:
objects:
plate1:
type: Plate
...
dispenseToPlate1:
description: "template to dispense `volume` ul of water to all wells on plate1"
type: Template
template:
command: pipetter.pipette
sources: water
destinations: plate1(all)
volumes: "{{volume}}"
steps:
1:
command: system.call
name: dispenseToPlate1
params:
volume: 10 ul
2:
command: system.call
name: dispenseToPlate1
params:
volume: 50 ul
This protocol will first dispense 10ul to all wells, then another 50ul to all wells – not actually a useful protocol, but it illustrates the point. In the system.call
command, the name of the template is specified along with the parameters template parameters. Templates are expanded using the Handlebars template engine, which is the reason for the “{{” and “}}” delimiters in the line volumes: "{{volume}}"
.
6.3 Data
Roboliq supports data tables to enable complex experiments. Conceptually, a data table is like a spread sheet of rows and named columns, where each row represents some “thing”, a each column represents a property. In Roboliq, a data table is an array of JSON objects: each object is a row, and each property is a column. Normally all the objects will have the same set of properties (but this is not required).
Data tables are supported by a Data
type, a data
property, a data()
directive, and a set of data.*
commands.
6.3.1 Data
type
You can define a data table using the Data
object type. Here’s an example where each row has a well, a liquid source, and a volume:
objects:
data1:
type: Data
value:
- {well: A01, volume: 10 ul, source: liquid1}
- {well: B01, volume: 10 ul, source: liquid2}
- {well: A02, volume: 20 ul, source: liquid1}
- {well: B02, volume: 20 ul, source: liquid2}
You can define as many data tables as you want in a protocol.
6.3.2 data
property
After defining a data table, you need to “activate” it for usage. This is done using the data
property, which understands the following parameters:
source
: this is the name of aData
object.where
: this is an optional boolean mathjs expression that is evaluated for each data row – only rows for which the expression evaluates to true are activated.orderBy
: an optional array of column names for ordering rows. The ordering behavior is the same as the_.sortBy
function in lodash.
Any step can be given a data
property to make a table available in that step and its sub-steps. Here’s an example application:
steps:
1:
data: {source: data1}
command: pipetter.pipette
sources: $$source
destinationPlate: plate1
destinations: $$well
volumes: $$volume
The data
property activates our data table data1
. The command pipetter.pipette
can now access the data columns by using the $$
-prefix along with the column name. So the above example is essentially equivalent to this:
steps:
1:
command: pipetter.pipette
sources: [liquid1, liquid2, liquid1, liquid2]
destinationPlate: plate1
destinations: [A01, B01, A02, B02]
volumes: [10 ul, 10 ul, 20 ul, 20 ul]
6.3.3 data.*
commands
Roboliq’s data.*
commands provide two commands for more sophisticated handling of data tables: data.forEachRow
and data.forEachGroup
.
data.forEachRow
lets you run a series of steps on each row of the data table. For each row, the command activates a new data table containing only that row, and it runs its sub-steps
using that new table. Here’s a toy example:
steps:
1:
data: {source: data1}
command: data.forEachRow
steps:
1:
command: pipetter.pipette
sources: $source
destinationPlate: plate1
destinations: $well
volumes: $volume
2:
command: fluorescenceReader.measurePlate
object: plate1
output:
joinKey: well
In this case, the sub-steps will be repeated 4 times, once for each row. That mean each well will be dispensed into and measured before moving onto the next well. Notice that here only a single $-prefix was used rather than the double $$-prefix for the column variables. When a data table is activated, Roboliq will check if any of the columns have all the same value; if so, that property and value will be automatically added to the current scope (see the next section about Scope). Scope variables are accessible via the $-prefix. Since the data.forEachRow
command activates each row individually, all of its columns will be added to the scope.
data.forEachGroup
lets you operate on groups of rows at a time. You provide a groupBy
property for it to group by, and then for each group it activates a new data table with those rows and runs its sub-steps
using that new table. Here’s another toy example:
steps:
1:
data: {source: data1}
command: data.forEachGroup
groupBy: source
steps:
1:
command: pipetter.pipette
sources: $source
destinationPlate: plate1
destinations: $$well
volumes: $$volume
2:
command: fluorescenceReader.measurePlate
object: plate1
output:
joinKey: well
Since there are two unique source
values in data1
, the data.forEachGroup
command will create two new data tables for it sub-steps:
First table:
- {well: A01, volume: 10 ul, source: liquid1}
- {well: A02, volume: 20 ul, source: liquid1}
Second table:
- {well: B01, volume: 10 ul, source: liquid2}
- {well: B02, volume: 20 ul, source: liquid2}
So the sub-steps will be repeated twice, once for new data table. Notice that here we used the single $-prefix for source
, but the double $$-prefix for well
and volume
. Because the values of the well and volume columns are not the same in all rows, they are not automatically added to the scape, and we can’t use the single $-prefix.
6.3.4 data()
directive
The data()
directive lets you assign a modified version of your data table to a property value. It will be easier to explain this in the section on Substitution, so we’ll postpone the discussion till the end of this chapter.
6.4 Scope
Scope is the set of currently active variables in a step. These usually come from one of two sources: 1) the data
directive and commands, as discussed above, or 2) a loop command like system.repeat
, which lets you add an index variable to the scope of the sub-steps.
The scope is a kind of stacked-tower structure. When a step pushes variables into scope, they are available to that step’s command and all substeps; however, they are not available to sibling or parent steps.
6.5 Substitution
Substitution lets you work with parameters and data tables by inserting their values into the protocol. Roboliq supports three forms: template substitution, scope substitution, and directive substitution.
6.5.1 Scope $ substitution
In scope substition, an expression starting with $ is replaced with a value from the scope. There are various forms of replacement, which we’ll dive into now.
$#...
: pre-scope substitution for parameter values
Parameters are not actually part of the scope, and they are accessible outside of steps as well. This means that they can be used in other parameter values and in object definitions, which is not the case for normal scope variables. You can substitute the value of a parameter named MYPARAM
by with $#MYPARAM
.
${...}
: javascript expression
Roboliq will substitute in the result of a JavaScript expression. The JavaScript expression has access to:
- The scope variables
_
: the lodash module and its many functions.math
: the mathjs module and its many functions.
Note that any value JSON value may be returned, whether it’s a string, number, boolean, array, or object.
$(...)
: mathjs calculation
The mathjs module provides a fairly broad range of math operations and is able to handle of units, such as volume. The mathjs expression has access to the current scope variables.
$...
: scope value substitution
Here you just name the scope variable, and Roboliq will substitute in its value.
If you have activated a data table using the data
property, then you can use $colName
to get an array of all the values in the column named colName
. Furthermore, if any of the data columns are filled with the same value, then that value is added to the scope as $colName_ONE
, where colName
is the actual name of the column. For example, if the active data table has a column named plate
whose entries are all plate1
, then $plate1_ONE = "plate1"
.
NOTE: Scope substitution can only be used as a parameter value, but not as a parameter name or part of a longer string. The following uses are invalid:
text: "Hello, $name"
: Roboliq only supports scope substitution for an entire value, so thename
value will not be substituted into this text.
You can use template substitution for this purpose instead.$myparam: 4
: Roboliq does not support scope substitution for property names. You can use template substitution for this purpose instead.
Examples
Let’s look at examples of $-substitutions. Consider this protocol:
roboliq: v1
parameters:
TEXT: { value: "Hello, World" }
objects:
data1:
type: Data
value:
- {a: 1, b: 1}
- {a: 1, b: 2}
steps:
1:
data: data1
command: system.echo
value:
javascript: "${`${TEXT} ${a} ${__step.command}`}"
math: "$(a * 10)"
scopeParameter: $TEXT
scopeColumn: $b
scopeOne: $a_ONE
scopeData: $__data[0].b
scopeObjects: $__objects.data1.type
scopeParameters: $__parameters.TEXT.value
scopeStep: $__step.command
The system.echo
command will output the object described in its value
parameter. The resulting value is this:
javascript: "Hello, World 1 system.echo"
math: 10
scopeParameter: "Hello, World"
scopeColumn: [1, 2]
scopeOne: 1
scopeData: 1
scopeObjects: "Data"
scopeParameters: "Hello, World"
scopeStep: "system.echo"
6.5.2 Template ` substitution
Template substitution uses the Handlebars template engine to manipulate text. Template substitution occurs on strings that start and end with a tick (`). Here’s a simple example that produces a new string:
text: "`Hello, {{name}}`"
If the name
in the current scope is “John”, then this will set text: "Hello, John"
.
If the template substitution result is enclosed by braces or brackets, Roboliq will attempt to parse it as a JSON object. Here’s a trivial example that turns a template substitution into a command, assuming that name
is currently in scope:
1: `{command: "system._echo", text: "Hello, {{name}}"}`
6.5.3 Directive () substitution
Directives are substitution functions. The main one is the data()
directive, which was briefly mentioned above in the Data section. The other directives are also closely related to Data
objects, and they are discussed more in the chapter on Design Tables.
The data()
directive lets you assign a modified version of your data table to a property value. The directive can take several properties:
where
: same as for thedata
property, this lets you select a subset of rows in the active data table.map
: each row in the data table will be mapped to this value. This is how you can transform your rows.summarize
: likemap
, but for summarizing all the rows into a single row. Summarize has a particularity: all column names are pushed into the current scope as arrays, and they are not overwritten by a single common value even if the column only contains a single value.join
: a string separator that will be used to join all elements of the array (seeArray#join
in some JavaScript documentation).head
: if set totrue
,
Let’s consider some examples using the data1
table from above. Here is the table:
- {well: A01, volume: 10 ul, source: liquid1}
- {well: B01, volume: 10 ul, source: liquid2}
- {well: A02, volume: 20 ul, source: liquid1}
- {well: B02, volume: 20 ul, source: liquid2}
and here are the examples:
Directive:
data(): {where: 'source == "liquid1"'}}
Result:
- {well: "A01", volume: "10 ul", source: "liquid1"}
- {well: "A02", volume: "20 ul", source: "liquid1"}
Directive:
data(): {map: '$volume'}
Result:
["10 ul", "10 ul", "20 ul", "20 ul"]
Directive:
data(): {where: 'source == "liquid1"', map: '$(volume * 2)'}
Result:
["20 ul", "40 ul"]
Directive:
data(): {map: {well: "$well"}}
Result:
- {well: "A01"}
- {well: "B01"}
- {well: "A02"}
- {well: "B02"}
Directive:
data(): {map: "$well", join: ","}
Result:
"A01,B01,A02,B02"
Directive:
data(): {summarize: {totalVolume: '$(sum(volume))'}}
Result:
- {totalVolume: "60 ul"}
Directive:
data(): {groupBy: "source", summarize: {source: '${source[0]}', totalVolume: '$(sum(volume))'}}
Result:
- {source: "liquid1", totalVolume: "30 ul"}
- {source: "liquid2", totalVolume: "30 ul"}