Aliquot Code Walkthrough

This walkthrough is intended for readers who have an intermediate or expert level of coding knowledge. For an overview on how to use Aliquot in the workflow editor, see this guide.

About this tutorial

This walkthrough will go through each of the 10 sections within an Antha element and describe what each part does. To follow along, you can either copy each code block into your editor of choice or download the final file from here.

Things you'll need

  • A Basic knowledge of golang

What you'll learn

After completing this lesson, you will learn the following:

  • The different sections within an Antha element
  • Why some sections are empty
  • The difference between the types of information which comes into and goes out of an element
  • Basic functions used in an Aliquot element

The basics

Antha element files are comprised of 11 essential parts.

  • protocol (the element name)
  • import
  • Parameters
  • Data
  • Inputs
  • Outputs
  • Requirements
  • Setup
  • Steps
  • Analysis
  • Validation

The element name

Every element in the Antha environment needs a unique element name. For this element, the name is "Aliquot".

Good names are simple, descriptive, and must begin with a capital letter. An element name must also be an unbroken sequence of characters. To designate the name of an element, we use the word "protocol" immediately before it.

1
protocol Aliquot

import

This element imports five different libraries or packages. They are the Antha search package, wtype, wutil, and mixer libraries and the go package strconv.

Element imports can be either Antha Standard Libraries, Antha Packages or Go packages.

1
2
3
4
5
6
7
import (
  "github.com/antha-lang/antha/antha/AnthaStandardLibrary/Packages/search"
  "github.com/antha-lang/antha/antha/anthalib/mixer"
  "github.com/antha-lang/antha/antha/anthalib/wtype"
  "github.com/antha-lang/antha/antha/anthalib/wutil"
  "strconv"
)

Parameters

The Aliquot element takes nine pieces of data as its Parameters. Parameters are the non-physical inputs for an element, like temperature, duration, or volume. The physical inputs for an element are described in the Inputs block.

Parameter types can be either basic go types, such as bool, int, or string, or Antha types, like Volume, Concentration or *LHComponent. Basic go types will be lower case, Antha types will be upper case. A full list of valid Antha-specific parameter types can be found in the wunit library.

All names in the Parameters block must begin with a capital letter as these are public variables that are visible and can be used by other external elements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Parameters (
  // This parameter represents the volume of solution that you have in the lab available to be aliquoted. It does not represent the total volume to be aliquoted or the volume of liquid that will be used.
  SolutionVolume Volume
  // This parameter dictates the final volume each aliquot will have.
  VolumePerAliquot Volume
  // This parameter states the number of aliquots that will be made from the input Solution.
  NumberofAliquots int
  // This parameter states whether the aliquots should be made by row or column.
  ByRow bool
  // This parameter sets the number of replicate plates to perform aliquots to. Default number of plates is 1.
  NumberOfReplicatePlates int
  // This parameter is an optional field. If the solution to be aliquoted has components that may sink to the bottom of the solution then select this option for the solution to be premixed prior to transfer.
  PreMix bool
  // This parameter is an optional field. If you want to change the name of the input Solution for traceability then do so. If not the default name will be given as the chosen input Solution LHComponent name.
  ChangeSolutionName string
  // This parameter is an optional field. It states the number of wells that have already been used in the output plate and will start making aliquots from this position onwards. If there is more than one replicate plate all plates would have the same number of wells already used.
  WellsAlreadyUsed map[string][]string
  // This name will be used as the identifier for the specific plate you are making your aliquots to appended with the replicate plate number. If left blank it will default to AliquotPlate. If running more than one instance of the Aliquot element in parallel and you want the dilutions to all be made on the same plate be sure this name is the same across all instance parameter sets, and be sure to wire WellsAlreadyUsed into WellsUsed between the different instances.
  AliquotPlateName string
)

Note: Each parameter has a comment associated with it defined by the preceding // notation. It is important that all your parameters have a comment as these comments are used to populate the info boxes in the Antha OS to guide the user to their physical purpose. They are also important for any future developer to understand the purpose of this variable within your code.

Data

The Data block of an Antha element defines the information produced by the element as a data output. These include things like final sample volume, number of aliquots performed, or thaw-time required.

All names in the Data block must begin with a capital letter.

For this Aliquot element, there is a single data output WellsUsed. The WellsUsed data output is as the name suggests a list of wells in a specific plate that have been used by this element. The map structure is used here to associate the list of well coordinates that will contain an aliquot to the specified output plate name.

1
2
3
Data (
  // This data output is a count of how many wells have been used in the output plate.
  WellsUsed map[string][]string)

You may see a few elements that has this data output and a corresponding Parameter input called WellsAlreadyUsed. These are compatible parameters to be wired together between elements such that the downstream element will look through this map and be sure not to use the wells that have already been used.

Inputs

The Inputs block of an element file defines the physical materials required by the element. These include things like solution sample, DNA part, or multi-well plate.

As with the Parameters and Data blocks all names in the Inputs block must begin with a capital letter.

1
2
3
4
5
6
Inputs (
  // This Physical input will have associated properties to determine how the liquid should be handled, e.g. is your Solution water or is it Glycerol. If your physical liquid does not exist in the Antha LHComponent library then create a new one on the fly with the Add_Solution element and wire the output into this input. Alternatively wire a solution made by another element into this input to be aliquoted.
    Solution *LHComponent
    // This parameter allows you to specify the type of plate you are aliquoting your Solution into. Choose from one of the available plate options from the Antha plate library.
    OutPlate *LHPlate
)

Outputs

The Outputs section lists the physical things that are generated by an element.

All names in the Outputs block must begin with a capital letter. In this instance the output of the Aliquot element is a list of Aliquots.

1
2
3
4
Outputs (
  // This is a list of the resulting aliquots that have been made by the element.
Aliquots []*LHComponent
)

Requirements

The Requirements block describes checks that the user wants to be in place before the element runs. For instance, if you only want to allow plates from a certain manufacturer, you would put that requirement here. Currently the Requirements section is not supported in Antha but will be soon. Therefore the Requirements block here is empty.

1
2
3
Requirements {

}

Setup

The Setup block is performed the first time that an element is executed. This can be used to perform any configuration that is needed globally for the element, and is also used to define any special setup that may be needed for groups of concurrent tasks that might be executed at the same time. Any variables that need to be accessed by the Steps function globally can be defined here as well, but need to be handled with care to avoid concurrency problems.

At this current time, the Setup block is not supported by Antha, but will be in future releases.

1
2
3
4
// Conditions to run on startup
Setup {

}

Steps

The heart of an Antha element is the Steps block, which defines the actual steps taken to transform a set of Parameters and Inputs into Data and Outputs. The Steps block is a kernel function, meaning it shares no information for every concurrent sample that is processed, and defines the workflow to transform a single block of inputs and samples into a single set of outputs, even if the element is operating on an entire array (such as micro-titre plate of samples at once).

Typically the Steps block is the longest block of the entire element.

Breaking down the Steps block

The Steps block is always introduced with the word "Steps" and a curly brace.

1
Steps {

First we're going to perform a number of checks on the user input parameter values to see if they are compatible before creating any of the liquid handling instructions.

Firstly, we will check that the number of Aliquots required is possible given the required aliquot volume and the specified volume of solution you have access to. If there isn't enough solution for the number of aliquots at the specified aliquot volume, the system will display an error in the Antha OS console during simulation and terminate the run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// We need to make sure that we have enough solution after subtracting the residual volume of solution left in the input plate.
// In future this will be calculated explicitly, but here we are estimating it as 10% extra for simplicity.
residualVolAllowance := 0.10
residualVol := wunit.MultiplyVolume(SolutionVolume, residualVolAllowance)

// Calculate the volume needed based on the number of aliquots, number of replicate plates, and aliquot amount specified. This is only used for error messages.
minVolume := wunit.MultiplyVolume(VolumePerAliquot, float64(NumberofAliquots*NumberOfReplicatePlates))
volumeNeeded := wunit.MultiplyVolume(minVolume, (1 / (1 - residualVolAllowance)))

// Here we're doing some maths to work out what the possible number of aliquots is that we can make given the volume specified and the volume of solution we have.
// We round this number down to the nearest number of aliquots.
number := (SolutionVolume.SIValue() - residualVol.SIValue()) / VolumePerAliquot.SIValue()
possiblenumberofAliquots, _ := wutil.RoundDown(number)

// The total number of aliquots to be made is the number specified by the user for each of the Replicate Plates being made.
if possiblenumberofAliquots < (NumberofAliquots * NumberOfReplicatePlates) {
Errorf("Not enough solution for this many aliquots. You have specified %s, but %s is required based on the parameters you have specified and a 10 percent allowance for residual volume left in the input plate.", SolutionVolume.ToString(), volumeNeeded.ToString())
}

Note: you will see that a number of arithmetic functions have been used here that can be applied to the Antha type Volume. In particular the MultiplyVolume function from the wunit library. Functions in this library allow us to perform basic arithmetic on Antha types rather than standard float64 or int types. These functions return the result as the Antha type, e.g. in this case the results of the multiplications will be of type Volume. We have also used the SIValue() function to convert our volumes to SI values to calculate a float64, which we have then rounded down to the nearest whole number with the RoundDown function from the wutil library.

Next we will check that the plate type that has been selected has a large enough volume capacity per well for the aliquot volume that is defined by the user. If it isn't, an error message will be displayed in the Antha OS console during simulation and the run will be terminated.

1
2
3
4
//check if maxvolume of outplate is higher than specified aliquot volume
if OutPlate.Welltype.MaxVolume().LessThanRounded(VolumePerAliquot, 5) {
      Errorf("Aliquot volume specified (%s) too high for well capacity (%s) of current plate (%s)", VolumePerAliquot.ToString(), OutPlate.Welltype.MaxVolume(), OutPlate.Name())
}

Note: Here, we can access the MaxVolume() of a well type of a specific plate type and check if it is less than the rounded value of our aliquot volume using the LessThanRounded function. If the well volume is less than the rounded aliquot volume the error message will be returned.

Next we're going to check our optional parameters PreMix and ChangeSolutionName and perform actions as appropriate.

1
2
3
4
5
6
7
8
9
  // if PreMix is selected change liquid type accordingly
  if PreMix {
    Solution.Type = wtype.LTPreMix
  }

  // if a solution name is given change the name
  if ChangeSolutionName != "" {
    Solution.CName = ChangeSolutionName
  }

Note: If the user has selected the PreMix option in the parameters then the liquid handling policy of the solution will be changed to the PreMix liquid type (wtype.LTPreMix). If not then the liquid handling policy associated with the liquid handling component for our solution will be used as default. Different LHPolicies can be applied to liquid handling components using the Add_Solution element when a more complex LHPolicy is required over PreMix or the default. Likewise, if the ChangeSolutionName parameter is not empty (!= "") then the Component name (CName) field of the LHComponent will be overwritten with the user specified name.

We will also check our optional ByRow parameter to specify well positions by row or by column.

1
2
  // This code allows the user to specify how the aliquots should be made, by row or by column.
     allwellpositions := OutPlate.AllWellPositions(ByRow)

Note: Here we are initialisng a variable called all well positions and storing all the well coordinates from our specific plate type using the AllWellPositions function. Here the function requires a boolean argument, if true it will return all the well coordinates in order by row, if false it will return them in order by column. This variable of well coordinates will be used later to direct the order of the wells that should be used for the aliquots.

We then declare a variable, which will store the liquid handling component information about our aliquots. We do so with the make command specifying an empty array of pointers to the LHComponent type.

1
    aliquots := make([]*LHComponent,0)

Now that we have a place to store the LHComponent information about the aliquots as we make them.

We will now perform another check of one of our parameters to make sure that no mistakes were made. Our NumberOfReplicatePlates parameter specifies the number of plates that you will end up with after running this element. Therefore if the user specifies this value to be 0 there would be no plates to make aliquots to. It is therefore important to check if a 0 has been entered and if so report an error message to the Antha OS console, also terminating the simulation.

1
2
3
4
// This code checks to make sure the number of replicate plates is greater than 0.
    if NumberOfReplicatePlates < 1 {
        Errorf("Number of replicate plates must be greater than 0")
    }

We next create a map for storing all the well locations that will be used when running this element. We do so again with the make command as before. The structure of the map is a key of type string that will represent the plate name and will link to an array of strings which will be the used well coordinates.

1
2
// Make a map of platenames to wells used in those plates
    WellsUsed = make(map[string][]string)

Next we start a loop that will contain all the information for producing the liquid handling instructions for our aliquots. The loop will repeat itself as many times as there are replicate plates specified by the user. This way the same number of aliquots will be made per plate to the same well locations with each round of the loop specifying a new plate. In order to specify a new plate with each cycle of the loop we need to rename our aliquot plate each iteration.

1
2
// This loop allows the user to specify the number of replicate plates of aliquots they want.
for platenumber := 1; platenumber < (NumberOfReplicatePlates + 1); platenumber++ {

Here we have started our replicate plate loop. Then we specify the plate names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  // If the user has not specified a plate name then set it to the default "AliquotPlate"
  if AliquotPlateName == "" {
    AliquotPlateName = "AliquotPlate"
  }

  // Here we are creating a variable to store our new renamed plate names in, depending on the number of replicates specified by the user.
  var platename string

  // If more than one plate rename including platenumber
  if platenumber > 1 {
    platename = AliquotPlateName + strconv.Itoa(platenumber)
  } else {
    platename = AliquotPlateName
  }

Note: Notice that if our platenumber is greater than 1 that the plate name is renamed to be the original plate name (either user specified or the default, AliquotPlate) appended with the plate number. If our plate number is just 1 then we don't rename the plate but just maintain the user specified plate name or default plate name.

Next we start to populate our WellsUsed map with the various plate names that have just been generated as the keys to the map and assign a list of well coordinates from previous runs if they have been wired up. Alternatively if this parameter has been left blank by the user and hasn't been wired up from an upstream element that the list of well coordinates will be blank, indicating that no wells have been used in those plates.

1
2
// Populate the WellsUsed map with platenames and the wells already used from plates with the same name from other runs, or left blank when not wired from upstream or specified by user inputs.
WellsUsed[platename] = append(WellsUsed[platename], WellsAlreadyUsed[platename]...)

Note: Here we are appending to our WellsUsed map for a specific plate name (key) our values (well coordinates) from the WellsAlreadyUsed map. If a platename in WellsAlreadyUsed exists then its contents (well coordinates) will be applied to the WellsUsed map for that specific plate name if it exists here also.

We now have everything in place to start creating the liquid handling instructions for the aliquots. Firstly we will start a loop that will iterate through the commands for generating the aliquots as many times as the user has specified in the NumberofAliquots parameter.

1
    for i := 0; i < NumberofAliquots; i++ {

For each aliquot we want to make, we will do a check and perform an operation. The first thing we want to do is to check whether the solution being aliquoted is DNA. If it is, we want to add a line of code which tells the system not to reuse tips unless from the same solution to avoid cross contamination of DNA samples. We do this by setting the solution type to wtype.LTDoNotMix. If the user has specified that the parameter PreMix be true (as described earlier in the code), then this line of code will overwrite that specification. This line of code is specific to DNA and will avoid any contamination between samples through tip reuse.

1
2
3
        if Solution.TypeName() == "dna"{
            Solution.Type = wtype.LTDoNotMix
        }

Then we make a new aliquot from the sample solution by calling the Sample function from the mixer library, specifying the physical "Solution" and matching it to a user specified volume, "VolumePerAliquot".

1
        aliquotSample := mixer.Sample(Solution, VolumePerAliquot)

Note: This line of code is creating the liquid handling component information for one of our aliquots by specifying the physical solution it will be made from and the volume of that solution that will be used. It is important to remember that this alone is not enough to generate the physical mixing instructions for this sample, continue below to see how this is done.

Firstly we create a variable that will store the physical mix instructions for our aliquot.

1
2
// Create a variable to contain all the liquid handling instructions for each aliquot
var aliquot *LHComponent

Next we will identify what the well coordinate is for the next empty well in our output plate.

1
well, _ := search.NextFreeWell(allwellpositions, WellsUsed[platename])

Note: Here we are using an Antha function NextFreeWell from the search library that searches through the allwellpositions slice that we created at the beginning of our code specific to our output plate type. We compare the well coordinates fromour WellsUsed map entry for a specific platename against our allwellpositions slice. If there are matching entries, e.g. A1 in WellsUsed and A1 in allwellpositions then antha will return the next free well coordinate and store it in the variable well, if ByRow is set to true that would be A2, if set to false it will be B1.

We next use all the information we have generated so far to dictate where our aliquot will be mixed and generate those physical mix instructions.

1
2
3
4
5
6
7
8
// The MixNamed command here cycles through the well positions of the chosen plate type and plate name for each aliquot.
// the MixNamed command is used instead of Mix to specify the plate type (e.g. "greiner384" or "pcrplate_skirted")
// the four input fields to the MixNamed command represent
// 1. the platetype as a string: commonly the input to the Antha element will actually be an LHPlate rather than a string so the type field can be accessed with OutPlate.Type
// 2. well location as a  string e.g. "A1" (in this instance determined by a counter and the plate type or leaving it blank "" will leave the well location up to the scheduler),
// 3. the plate name as a string,
// 4. the sample or array of samples to be mixed; in the case of an array you'd normally feed this in as samples...
aliquot = MixNamed(OutPlate.Type, well, platename, aliquotSample)

Note: Take note that we are using the MixNamed command that specifies the output plate type, the next free well position that we just identified and stored in the variable well, the plate name stored in the variable plate name that may or may not have been renamed based on the number of replicate plates specified by the user and finally the aliquot sample information we generated using the Sample function earlier.

Now we have used up another well coordinate in our output plate so we need to update our WellsUsed map to reflect this, we do so by appending our well coordinate to the slice of well coordinates for this specific plate, identified by its platename.

1
2
// store all of the used wells in the WellsUsed slice.
WellsUsed[platename] = append(WellsUsed[platename], well)

As long as everything has gone to plan and there have been no errors in generating our mixing instructions then we will append those mixing instructions for that specific aliquot to a list of aliquots. We also need to close our aliquot loop followed by closing our replicate plate loop. Before closing the Steps section for this element we also need to make our temporary list of aliquots a public variable so that it can be ouput and is visible to elements external to this one. We do so by capitalising the variable name.

1
2
3
4
5
6
7
8
// If the liquid handling instructions for a single aliquot were made with no errors then add to the aliquots slice.
if aliquot != nil {
  aliquots = append(aliquots, aliquot)
              }
          }
      }
Aliquots = aliquots
}

The full Steps block

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
Steps {

    // We need to make sure that we have enough solution after subtracting the residual volume of solution left in the input plate.
    // In future this will be calculated explicitly, but here we are estimating it as 10% extra for simplicity.
    residualVolAllowance := 0.10
    residualVol := wunit.MultiplyVolume(SolutionVolume, residualVolAllowance)

    // Calculate the volume needed based on the number of aliquots, number of replicate plates, and aliquot amount specified. This is only used for error messages.
    minVolume := wunit.MultiplyVolume(VolumePerAliquot, float64(NumberofAliquots*NumberOfReplicatePlates))
    volumeNeeded := wunit.MultiplyVolume(minVolume, (1 / (1 - residualVolAllowance)))

    // Here we're doing some maths to work out what the possible number of aliquots is that we can make given the volume specified and the volume of solution we have.
    // We round this number down to the nearest number of aliquots.
    number := (SolutionVolume.SIValue() - residualVol.SIValue()) / VolumePerAliquot.SIValue()
    possiblenumberofAliquots, _ := wutil.RoundDown(number)

    // The total number of aliquots to be made is the number specified by the user for each of the Replicate Plates being made.
    if possiblenumberofAliquots < (NumberofAliquots * NumberOfReplicatePlates) {
        Errorf("Not enough solution for this many aliquots. You have specified %s, but %s is required based on the parameters you have specified and a 10 percent allowance for residual volume left in the input plate.", SolutionVolume.ToString(), volumeNeeded.ToString())
    }

    //check if maxvolume of outplate is higher than specified aliquot volume
    if OutPlate.Welltype.MaxVolume().LessThanRounded(VolumePerAliquot, 5) {
        Errorf("Aliquot volume specified (%s) too high for well capacity (%s) of current plate (%s)", VolumePerAliquot.ToString(), OutPlate.Welltype.MaxVolume(), OutPlate.Name())
    }

    // if PreMix is selected change liquid type accordingly
    if PreMix {
        Solution.Type = wtype.LTPreMix
    }

    // if a solution name is given change the name
    if ChangeSolutionName != "" {
        Solution.CName = ChangeSolutionName
    }

    // This code allows the user to specify how the aliquots should be made, by row or by column.
    allwellpositions := OutPlate.AllWellPositions(ByRow)

    // Make a slice to contain all the liquid handling instructions for all of the aliquots.
    aliquots := make([]*LHComponent, 0)

    // This code checks to make sure the number of replicate plates is greater than 0.
    if NumberOfReplicatePlates < 1 {
        Errorf("Number of replicate plates must be greater than 0")
    }

    // Make a map of platenames to wells used in those plates
    WellsUsed = make(map[string][]string)

    // This loop allows the user to specify the number of replicate plates of aliquots they want.
    for platenumber := 1; platenumber < (NumberOfReplicatePlates + 1); platenumber++ {

        // Here we are setting the AliquotPlateName to default to AliquotPlate if left blank by the user
        var platename string

        // If the user has not specified a plate name then set it to the default "AliquotPlate"
        if AliquotPlateName == "" {
            AliquotPlateName = "AliquotPlate"
        }

        // If more than one plate rename including platenumber
        if platenumber > 1 {
            platename = AliquotPlateName + strconv.Itoa(platenumber)
        } else {
            platename = AliquotPlateName
        }

        // Populate the WellsUsed map with platenames and the wells already used from plates with the same name from other runs
        WellsUsed[platename] = append(WellsUsed[platename], WellsAlreadyUsed[platename]...)

        // This loop cycles through the number of aliquots required
        for i := 0; i < NumberofAliquots; i++ {

            // This statement changes the liquid handling policy if the solution being aliquoted is DNA to avoid cross contamination.
            if Solution.TypeName() == "dna" {
                Solution.Type = wtype.LTDoNotMix
            }

            // This line is using the Sample function from the mixer library to create a liquid handling component from a specified physical solution and the user defined aliquot volume
            aliquotSample := mixer.Sample(Solution, VolumePerAliquot)

            // Create a variable to contain all the liquid handling instructions for each aliquot
            var aliquot *LHComponent

            well, _ := search.NextFreeWell(allwellpositions, WellsUsed[platename])

            // The MixNamed command here cycles through the well positions of the chosen plate type and plate name for each aliquot.
            // the MixNamed command is used instead of Mix to specify the plate type (e.g. "greiner384" or "pcrplate_skirted")
            // the four input fields to the MixNamed command represent
            // 1. the platetype as a string: commonly the input to the Antha element will actually be an LHPlate rather than a string so the type field can be accessed with OutPlate.Type
            // 2. well location as a  string e.g. "A1" (in this instance determined by a counter and the plate type or leaving it blank "" will leave the well location up to the scheduler),
            // 3. the plate name as a string,
            // 4. the sample or array of samples to be mixed; in the case of an array you'd normally feed this in as samples...
            aliquot = MixNamed(OutPlate.Type, well, platename, aliquotSample)

            // store all of the used wells in the WellsUsed slice.
            WellsUsed[platename] = append(WellsUsed[platename], well)

            // If the liquid handling instructions for a single aliquot were made with no errors then add to the aliquots slice.
            if aliquot != nil {
                aliquots = append(aliquots, aliquot)
            }

        }

    }
    Aliquots = aliquots
}