7 Design Tables
Here we use the term Design Table to refer to a data table that is designed for an experiment. It should contain all relevant factors for later analyzing the experimental results.
Experiments on microwell plates can easily involve hundreds of unique liquid combinations, so specifying them manually can be tedious and error-prone. Here we present a short-hand for creating design tables.
WARNING: This is very abstract. Don’t bother with it if you’re looking for something easy. That said, it makes complex experiments much, much easier to design, trouble-shoot, and analyze.
NOTE: In this chapter, we’ll uses the following terms interchangeably: column, factor, variable.
7.1 First examples
Let’s create a design table with a single row like this:
plate | source | destination | volume |
---|---|---|---|
plate1 | water | A01 | 25 ul |
This table could be used to specify a single pipetting operation that dispenses 25 ul of water into well A01
of plate1
. We will specify this in Roboliq as follows:
roboliq: v1
objects:
data1:
type: Data
description: First example
design:
plate: plate1
source: water
destination: A01
volume: 25 ul
---
In a Roboliq protocol, designs are placed under the objects
property. In this case, its name is data1
, its type is Data
, and it has a description. The design
property is where factors are specified.
Let’s expand the design table a bit to dispenses 25 ul of water into several destinations.
plate | source | destination | volume |
---|---|---|---|
plate1 | water | A01 | 25 ul |
plate1 | water | B01 | 25 ul |
plate1 | water | C01 | 25 ul |
To specify this in Roboliq, we change the destination
property to a branching factor:
roboliq: v1
objects:
data1:
type: Data
description: First example
design:
plate: plate1
source: water
destination*: [A01, B01, C01]
volume: 25 ul
Notice the asterisk in destination*
. An asterisk at the end of a factor name indicates branching. Branching factors require an array of values, and for each value in the array, the existing rows are first replicated and then the value is assigned to the factor in that row.
Now let’s assign a different volume to each row:
plate | source | destination | volume |
---|---|---|---|
plate1 | water | A01 | 25 ul |
plate1 | water | B01 | 50 ul |
plate1 | water | C01 | 75 ul |
To specify this in Roboliq, we change the volume
property to an array:
roboliq: v1
objects:
data1:
type: Data
description: First example
design:
plate: plate1
source: water
destination*: [A01, B01, C01]
volume: [25 ul, 50 ul, 75 ul]
When a factor value is an array, a new column is added with those values.
Try it. Copy the above code to a new file in Roboliq’s root directory named data1Test.yaml
. Open a terminal and change directory to the Roboliq root, and run the following command:
npm run design -- --path objects.data1 data1Test.yaml
It should produce this output:
plate source destination volume
====== ====== =========== ======
plate1 water A01 25 ul
plate1 water B01 50 ul
plate1 water C01 75 ul
====== ====== =========== ======
Branches can also be specified as integers. If you specify an integer, it creates that many branches which are each given an integer index, as follows:
roboliq: v1
objects:
designInteger:
type: Data
design:
a*: 3
b*: 3
Which creates the following table:
a | b |
---|---|
1 | 1 |
1 | 2 |
1 | 3 |
2 | 1 |
2 | 2 |
2 | 3 |
3 | 1 |
3 | 2 |
3 | 3 |
7.2 Nested branching
Nested branching provides a lot of power to the design specification, but it is also where the complexity starts. Consider this table where two sources are nested in each destination, and each source has its own volume:
plate | destination | source | volume | liquidClass |
---|---|---|---|---|
plate1 | A01 | water | 50 ul | Roboliq_Water_Air_1000 |
plate1 | A01 | dye | 25 ul | Roboliq_Water_Air_1000 |
plate1 | B01 | water | 50 ul | Roboliq_Water_Air_1000 |
plate1 | B01 | dye | 25 ul | Roboliq_Water_Air_1000 |
plate1 | C01 | water | 50 ul | Roboliq_Water_Air_1000 |
plate1 | C01 | dye | 25 ul | Roboliq_Water_Air_1000 |
This can be described in Roboliq as follows:
roboliq: v1
objects:
data2:
type: Data
description: Nested example
design:
plate: plate1
destination*: [A01, B01, C01]
source*:
water:
volume: 50 ul
dye:
volume: 25 ul
liquidClass: Roboliq_Water_Air_1000
Create a file named data2Test.yaml
with those contents and run:
npm run design -- --path objects.data2 data2Test.yaml
Let’s walk through this example step-by-step to see how the desired table is achieved.
Step 0: A design starts as a single empty row.
Step 1: plate: plate1
This assigns the value plate1
to the property plate
in the first row:
plate |
---|
plate1 |
Step 2: destination*: [A01, B01, C01]
Three copies of the previous row are created, and a column for destination
is added to each row, each with its own value:
plate | destination |
---|---|
plate1 | A01 |
plate1 | B01 |
plate1 | C01 |
Step 3: source*
This branch has two keys: water and dye. So to start with, two copies are made of each of the previous three rows. The first copy is updated according to the first key, giving us:
plate | destination | source |
---|---|---|
plate1 | A01 | water |
plate1 | B01 | water |
plate1 | C01 | water |
Then the conditions embedded under water:
are applied to those rows – in this case, volume = 50 ul
:
plate | destination | source | volume |
---|---|---|---|
plate1 | A01 | water | 50 ul |
plate1 | B01 | water | 50 ul |
plate1 | C01 | water | 50 ul |
For the second table copy, an analogous process sets source = dye
and volume = 25 ul
:
plate | destination | source | volume |
---|---|---|---|
plate1 | A01 | dye | 25 ul |
plate1 | B01 | dye | 25 ul |
plate1 | C01 | dye | 25 ul |
Next those two tables are concatenated, giving us:
plate | destination | source | volume |
---|---|---|---|
plate1 | A01 | water | 50 ul |
plate1 | A01 | dye | 25 ul |
plate1 | B01 | water | 50 ul |
plate1 | B01 | dye | 25 ul |
plate1 | C01 | water | 50 ul |
plate1 | C01 | dye | 25 ul |
Step 4: liquidClass: Roboliq_Water_Air_1000
Finally, liquidClass = Roboliq_Water_Air_1000
is assigned to all rows:
plate | destination | source | volume | liquidClass |
---|---|---|---|---|
plate1 | A01 | water | 50 ul | Roboliq_Water_Air_1000 |
plate1 | A01 | dye | 25 ul | Roboliq_Water_Air_1000 |
plate1 | B01 | water | 50 ul | Roboliq_Water_Air_1000 |
plate1 | B01 | dye | 25 ul | Roboliq_Water_Air_1000 |
plate1 | C01 | water | 50 ul | Roboliq_Water_Air_1000 |
plate1 | C01 | dye | 25 ul | Roboliq_Water_Air_1000 |
7.4 Actions
Roboliq provides various designs “actions” that can be used for more sophisticated values. The most important ones are:
- allocateWells
- range
- calculate
- case
7.4.1 allocateWells
Let’s take a look at an example:
replicate*: 2
well=allocateWells:
rows: 8
columns: 12
An action is indicated with the “=”-infix. So in the case of well=allocateWells
, the factor name is well
, the action is allocateWells
, and the properties are the arguments to the action. In this case, rows
and columns
tells the plate dimension we want to get wells for, and 2 wells will be allocated since the table has two rows:
{replicate: 1, well: A01}
{replicate: 2, well: B01}
7.4.2 range
The range
action gives you an integer sequence. It accepts these arguments:
from
: the integer to start at (optional)till
: the integer to end at (optional)step
: the distance between generated integers (default = 1)
Here’s an example:
a*: 2
b*: 2
c=range: {}
d=range: {from: 10, step: 10}
Which produces this result:
a | b | c | d |
---|---|---|---|
1 | 1 | 1 | 10 |
1 | 2 | 2 | 20 |
2 | 1 | 3 | 30 |
2 | 2 | 4 | 40 |
The first range, c
, just numbers all the rows starting with 1. The second range, d
, starts numbering at 10 and procedes in steps of 10.
7.4.3 calculate
The calculate
action takes a string to be parsed by mathjs. The calculation will be made for each row individually. Here’s an example:
a*: 3
volume=calculate: '(a * 10) ul'
more=calculate: '(50 ul) - volume'
And here is the result:
a | volume | more |
---|---|---|
1 | 10 ul | 40 ul |
2 | 20 ul | 30 ul |
3 | 30 ul | 20 ul |
Alternatively, the calculate
action can accept parameters:
value
: the string to parseunits
: the units of the final output
a*: 3
volume=calculate:
value: '(a * 10)'
units: ul
With this output:
a | volume |
---|---|
1 | 10 ul |
2 | 20 ul |
3 | 30 ul |
7.4.4 case
A case
action takes an array of cases and tests them against each row of the table. The first case whose where
statement is missing or evaluates to true
will be applied. The individual case items take these arguments:
where
- an optional mathjs statement that will be evaluated on each rowdesign
- a design specification that will be applied to the matching rows
Here’s an example:
a*: 3
volumeCase=case:
- where: a < 2
design:
volume: 10 ul
- design:
volume: 12354 ul
a | volumeCase | volume |
---|---|---|
1 | 1 | 10 ul |
2 | 2 | 12345 ul |
3 | 2 | 12345 ul |
7.5 Step and data nesting
You can only load one design per step, but you can nest steps and load another design in the sub-step. Consider these two excerpts of designs:
data1:
design:
a: Alice
b: *3
c: Charles
d: Daniel
data2:
design:
d: David
Let’s use them in these steps:
1:
data: {source: data1}
description: "`{{a}} {{c}} {{d}}`"
1:
data: {source: data2}
description: "`{{a}} {{c}} {{d}}`"
The descriptions should be expanded as follows:
1.description: “Alice Charles Daniel” 1.1.description: “Alice Charles David”
In 1.1, “$$b” does not exist, but “$a” and “$c” still do. That is to say: column data from a previous data
directive are not carried into sub-steps with a new data
directive, but the values that were the same for all columns remain in scope.