benmiles.xyz Biotech 🦠, startups 🚀, tech 👨‍💻

My first attempt at working with Autoprotocol

Recently I wrote about my first experience of running a simple experiment on Transcriptic’s cloud biology platform. It was a simple bacterial growth curve. The protocol for running this experiment was produced by interacting with a GUI to enter parameters and the actual knitty-gritty of liquid handling and spectroscopic measurements was already predefined.

The growth curve protocol had been written as a package, which is simply a package of code that when connected to the Transcriptic web application generates a user interface that can parametrically generate the commands that are executed by the platform. This is a great way of handling protocols as it allows easy execution of experiments by users that have no experience with code.

Transcriptic accepts protocols defined by the Autoprotocol standard (also designed by Transcriptic). So another method of executing experiments on Transcriptic is to write a protocol in the Autoprotocol standard and submit this to Transcriptic via the API for execution. This is what I had a quick go with.

autoprotocol brand

Example protocol, the burden assay

First I needed a protocol to turn into the Autoprotocol standard JSON format. I picked a protocol from ‘Quantifying cellular capacity identifies gene expression designs with reduced burden’ a paper from the Ellis group and co. The specific protocol is the spectroscopic analysis of a fluorescent reporter recombinant DNA system transformed into cells. The experiment is designed to assess the burden of the gene cassette on the host.

Writing the protocol

Autoprotocol protocols can be quite lengthy due to the granularity of specifying each liquid handling step etc. When working with 96-well or 384-well plates one could end up with a lot of repetition. To make the construction of protocols simpler Autoprotocol provides a python package that can programmatically output Autoprotocol JSON.

Containers

I first started by defining references in the protocol, these are essentially the containers that are used in the experiment, be they existing containers with reagents or containers to be created where for instance the assay will take place.

When instantiating a container, you need to supply a few arguments mainly ID, container type and container destiny (where the container ends up at the end of the run).

import json
from autoprotocol.protocol import Protocol

#instantiate new Protocol object
p = Protocol()

# Add the containers to the the protocol, unfortunately I had to pick slightly different containers to the ones used in the paper.

# The protocol assumes I already have a stock of already transformed bacteria and of arabinose

bacteria_stock = p.ref("bacteria_stock", cont_type="micro-2.0", storage="cold_4")
bacteria_overgrow = p.ref("bacteria_overgrow", cont_type="96-deep", discard=True)

inducer_arabinose = p.ref("inducer_arabinose", cont_type="micro-1.5", storage="cold_4")
reaction_plate = p.ref("reaction_plate", cont_type="96-flat", storage="cold_4")
bacteria_prep = p.ref("bacteria_prep", cont_type="96-deep", discard=True)

After adding all the containers the protocol actions kick off with growing a fresh liquid culture from a bacterial stock. To achieve a culture of bacteria in an exponential phase of growth.

Liquid handling and culturing bacteria

# Should be dispensing M9, but M9 media isn't a standard reagent at Transcriptic
# Dispense fills the container with standard reagents from Transcriptic
p.dispense(bacteria_prep,
            "lb-broth-100ug-ml-amp",
            [{"column": 0, "volume": "1500:microliter"}])

# Add bacteria from stock container to fresh media
p.transfer(bacteria_stock.well(0).set_volume("1000:microliter"),
           bacteria_prep.well(0),
           "5:microliter")

# Cover the plate prior to shaking incubation
p.cover(bacteria_prep, lid="universal")

# 16hr incubation
p.incubate(bacteria_prep,
           "warm_37",
           "16:hour",
           shaking=True)

# Prep media for overgrowing bacteria sample
p.dispense(bacteria_overgrow,
            "lb-broth-100ug-ml-amp",
            [
              {"column": 0, "volume": "1000:microliter"}
            ]
          )

p.uncover(bacteria_prep)

# Innoculate overgrowth sample
p.transfer(bacteria_prep.well("A1"),
           bacteria_overgrow.well("A1"),
           "20:microliter",
           mix_after=True)

p.cover(bacteria_overgrow, lid="universal")

# Incubate bacteria to guarantee exponential phase
p.incubate(bacteria_overgrow,
           "warm_37",
           "1:hour",
           shaking=True)

p.uncover(bacteria_overgrow)

# Transfer exponential phase bacteria to microplate for the assay
p.distribute(bacteria_overgrow.well("A1").set_volume("1000:microliter"),
             reaction_plate.wells_from(0,4),
             "200:microliter"
             )

OD600 and fluorescence measurements with an arabinose induction step

After the bacteria has been cultured to be in an exponential phase of growth the culture is transfered to another plate where the spectroscopic assay will take place.

During the assay, measurements are made of the OD600, fluorescent emission at 528nm and fluorescent emission at 645nm. These measurements occur twice prior to the expression system being induced by arabinose. Then following induction the 3 spectroscopic measurements are taken every 30 minutes, 8 times.

As an aside I’m pretty sure this code can be cleaned up a lot to remove so much of the repetition.

## Assay time!

# Incubate bacteria at 37 degrees for 3 hours
p.cover(reaction_plate, lid="universal")
p.incubate(reaction_plate, "warm_37", "3:hour",shaking=True)

# Read the first four wells on the reaction plate.
p.absorbance(reaction_plate, reaction_plate.wells_from(0,4).indices(), "600:nanometer",
    "OD600_reading_post3hr")
p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="485:nanometer", emission= "528:nanometer", dataref=
        "528_reading_post3hr")
p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="590:nanometer", emission= "645:nanometer", dataref=
        "645_reading_post3hr")

# Incubate bacteria at 37 degrees for 30 mins
p.incubate(reaction_plate, "warm_37", "30:minute",shaking=True)

# Another measurement
p.absorbance(reaction_plate, reaction_plate.wells_from(0,4).indices(), "600:nanometer",
    "OD600_reading_post3hr2")
p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="485:nanometer", emission= "528:nanometer", dataref=
        "528_reading_post3hr2")
p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="590:nanometer", emission= "645:nanometer", dataref=
        "645_reading_post3hr2")

# Incubate
p.incubate(reaction_plate, "warm_37", "30:minute",shaking=True)

# Measurement
p.absorbance(reaction_plate, reaction_plate.wells_from(0,4).indices(), "600:nanometer",
    "OD600_reading_preinduce")
p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="485:nanometer", emission= "528:nanometer", dataref=
        "528_reading_preinduce")
p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="590:nanometer", emission= "645:nanometer", dataref=
        "645_reading_preinduce")

p.uncover(reaction_plate)

# Induce the expression system with arabinose
p.distribute(inducer_arabinose.well(0).set_volume("1000:microliter"),
             reaction_plate.wells_from(0,4),
             "100:microliter")

p.cover(reaction_plate, lid="universal")

# Note that creating the 8 time series measurements from here can be done with a single while loop. The count variable is used in the dataref assignment
count = 0
while count < 9:
    # Incubate
    p.incubate(reaction_plate, "warm_37", "30:minute",shaking=True)

    # Measure
    p.absorbance(reaction_plate, reaction_plate.wells_from(0,4).indices(), "600:nanometer",
        "OD600_reading_" + str(count))
    p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="485:nanometer", emission= "528:nanometer", dataref=
            "528_reading_" + str(count))
    p.fluorescence(reaction_plate, reaction_plate.wells_from(0,4).indices(), excitation="590:nanometer", emission= "645:nanometer", dataref=
            "645_reading_" + str(count))
    count +=1

Once the run finishes all the containers execute their ‘destiny’ which is either being discarded or returned to storage.

Getting the protocol onto Transcriptic

After the protocol has been written this can all be ‘built’ to JSON in the Autoprotocol format with this line in the python protocol:

# Builds the Autoprotocol JSON
print json.dumps(p.as_dict(), indent=2)

Executing the file with python burden.py will dump the JSON into STDOUT however this output can be piped into other functions.

I used the Transcriptic Runner package to first validate the protocol then create a test run via the API with the following command from the docs:

$ python burden.py | transcriptic submit --project ":project_id" --title "Burden Assay" --test

If the PUT request on the API works the run appears with all of the actions and containers interpreted into the UI:

burden screenshot

Once the run is logged against the project it is easier to go through the UI to check the run than going through the JSON from python file.

I haven’t tested the run on any materials as I don’t have any of the strains or the plasmids in my inventory, but it would be cool try and replicate some of the results from the paper.

I’ll try and work on wrapping the burden assay protocol in a harness which makes the protocol flexible by accepting parameters via UI.

The documentation between Transcriptic and Autoprotocol is really decent and helpful. In particular analysis and validation with the Transcriptic Runner was a big helper in eliminating small errors in the protocol. One common thing was setting ‘virtual volumes’ where containers will have a volume of liquid in sometime in the future.

Looking forward to trying to grab a quick word with the people from the Transcriptic team at SynBioBeta next week at Imperial College.

Ben