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.3 Hidden factors

There are generally many ways to achieve the same results. As an example, an alternative way of achieving the same result as above is:

roboliq: v1
objects:
  data2:
    type: Data
    description: Nested example
    design:
      plate: plate1
      destination*: [A01, B01, C01]
      .sourceId*:
      - source: water
        volume: 50 ul
      - source: dye
        volume: 25 ul
      liquidClass: Roboliq_Water_Air_1000

In this case, the branching factor is .sourceId* and it’s an array. The period prefix hides that column, and the final results are the same as above.

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 parse
  • units: 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 row
  • design - 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.