This week, we programmed the Opentrons liquid handling robot to create fluorescent protein masterpieces. I was really looking forward to this lab and even did last week homework about expressing GFP in E.Coli. Rather than using the GFP, I found we used a variety of different colors of superfluorescent proteins. Ronan’s webtool [1] made it really easy to visualize a design, and we could even upload images to serve as a template for our designs. I decided to go all in on turtles and make a turtles all the way down image featuring a turtle with a globe for its shell. This was the original image, from my collection of Turtle CADS:
I took the coordinates from this image for each different color and put them into the Colab Python Script. I had to add and change colors to match the ones I used. I originally ran into a problem where I’d tell the robot to fill up the pipette, empty it, then continue attempting to dispense I was able to see this error in the simulation and correct it by adding a loop where, after the pipette was empty (20 dots in my case using a p20 and 1ul droplets) I’d pick up more before continuing. I used the built-in Gemini chatbot to help with this assignment, after explaining the functions I had access to I asked it to use these functions to deposit blue droplets at every coordinate in a list. From there, I used the same functions and procedures to do the rest of my colors. After some troubleshooting, the simulation finally showed me the result I wanted, and the verification also went through. I’ll attach my Python code at the end of this webpage.
It was really straightforward to go from my design to the Opentron, at least from my point of view as a student (not sure what magic the TAs and staff work behind the scenes). The machine itself was incredibly precise, not only in movement but also in dispensing.
First color dispensing, machine goes back to refill when out of fluid, but this is not because of sensing but instead code we input. It also doesn’t know where the top of the gel is so we calibrated and retuned to find the perfect height to dispense at and the right height to move at to clear the dish walls.
Before I knew it, the dispensing part was finished, but the cells still needed to be cultured. We dispensed E.Coli capable of producing fluorescent proteins but they still had to incubate overnight so the proteins could be produced
from opentrons import types
metadata = { # see https://docs.opentrons.com/v2/tutorial.html#tutorial-metadata
'author': 'Alayah Hines',
'protocolName': 'HGTAA Lab3 Turtle',
'description': 'Makes a cute turtle',
'source': 'HTGAA 2026 Opentrons Lab',
'apiLevel': '2.20'
}
##############################################################################
### Robot deck setup constants - don't change these
##############################################################################
TIP_RACK_DECK_SLOT = 9
COLORS_DECK_SLOT = 6
AGAR_DECK_SLOT = 5
PIPETTE_STARTING_TIP_WELL = 'A1'
well_colors = {
'A1' : 'darkgreen', #swapped
'B1' : 'Green',
'C1' : 'Orange',
'D1' : 'Cyan', # Added
'E1' : 'Blue', # Added
'F1' : 'Yellow'
}
def run(protocol):
##############################################################################
### Load labware, modules and pipettes
##############################################################################
# Tips
tips_20ul = protocol.load_labware('opentrons_96_tiprack_20ul', TIP_RACK_DECK_SLOT, 'Opentrons 20uL Tips')
# Pipettes
pipette_20ul = protocol.load_instrument("p20_single_gen2", "right", [tips_20ul])
# Modules
temperature_module = protocol.load_module('temperature module gen2', COLORS_DECK_SLOT)
# Temperature Module Plate
temperature_plate = temperature_module.load_labware('opentrons_96_aluminumblock_generic_pcr_strip_200ul',
'Cold Plate')
# Choose where to take the colors from
color_plate = temperature_plate
# Agar Plate
agar_plate = protocol.load_labware('htgaa_agar_plate', AGAR_DECK_SLOT, 'Agar Plate') ## TA MUST CALIBRATE EACH PLATE!
# Get the top-center of the plate, make sure the plate was calibrated before running this
center_location = agar_plate['A1'].top()
pipette_20ul.starting_tip = tips_20ul.well(PIPETTE_STARTING_TIP_WELL)
##############################################################################
### Patterning
##############################################################################
###
### Helper functions for this lab
###
# pass this e.g. 'Red' and get back a Location which can be passed to aspirate()
def location_of_color(color_string):
for well,color in well_colors.items():
if color.lower() == color_string.lower():
return color_plate[well]
raise ValueError(f"No well found with color {color_string}")
# For this lab, instead of calling pipette.dispense(1, loc) use this: dispense_and_detach(pipette, 1, loc)
def dispense_and_detach(pipette, volume, location):
"""
Move laterally 5mm above the plate (to avoid smearing a drop); then drop down to the plate,
dispense, move back up 5mm to detach drop, and stay high to be ready for next lateral move.
5mm because a 4uL drop is 2mm diameter; and a 2deg tilt in the agar pour is >3mm difference across a plate.
"""
assert(isinstance(volume, (int, float)))
above_location = location.move(types.Point(z=location.point.z + 5)) # 5mm above
pipette.move_to(above_location) # Go to 5mm above the dispensing location
pipette.dispense(volume, location) # Go straight downwards and dispense
pipette.move_to(above_location) # Go straight up to detach drop and stay high
###
### Turtle Code Below
###
# Each tuple is (x_offset_mm, y_offset_mm)
green_coords = [
(-34.8, 8.4),(20.4, 8.4),(22.8, 8.4),(25.2, 8.4),(27.6, 8.4),(30, 8.4),(32.4, 8.4),(-34.8, 6),(18, 6),(27.6, 6),(30, 6),(32.4, 6),(34.8, 6),(15.6, 3.6),(18, 3.6),(27.6, 3.6),(30, 3.6),(32.4, 3.6),(34.8, 3.6),(37.2, 3.6),(13.2, 1.2),(15.6, 1.2),(18, 1.2),(27.6, 1.2),(30, 1.2),(32.4, 1.2),(34.8, 1.2),(37.2, 1.2),(13.2, -1.2),(15.6, -1.2),(18, -1.2),(20.4, -1.2),(22.8, -1.2),(25.2, -1.2),(27.6, -1.2),(30, -1.2),(32.4, -1.2),(34.8, -1.2),(37.2, -1.2),(13.2, -3.6),(15.6, -3.6),(18, -3.6),(20.4, -3.6),(22.8, -3.6),(25.2, -3.6),(27.6, -3.6),(30, -3.6),(32.4, -3.6),(34.8, -3.6),(37.2, -3.6),(13.2, -6),(15.6, -6),(18, -6),(20.4, -6),(22.8, -6),(25.2, -6),(27.6, -6),(30, -6),(32.4, -6),(34.8, -6),(-32.4, -8.4),(-32.4, -10.8),(-30, -10.8),(-32.4, -13.2),(-30, -13.2),(-27.6, -13.2),(-25.2, -13.2),(-32.4, -15.6),(-30, -15.6),(-27.6, -15.6),(-25.2, -15.6),(-6, -20.4),(-3.6, -20.4),(-1.2, -20.4),(1.2, -20.4),(3.6, -20.4),(6, -20.4),(-6, -22.8),(-3.6, -22.8),(-1.2, -22.8),(1.2, -22.8),(3.6, -22.8),(6, -22.8),(-6, -25.2),(-3.6, -25.2),(-1.2, -25.2),(1.2, -25.2),(3.6, -25.2),(6, -25.2)
]
# Pick up a tip for green color
pipette_20ul.pick_up_tip()
# Dispense each green dot
for i, (x_offset, y_offset) in enumerate(green_coords):
if i % 20 == 0:
# Aspirate 20uL or the remaining number of dispenses, whichever is smaller
pipette_20ul.aspirate(min(20, len(green_coords) - i), location_of_color('Green'))
adjusted_location = center_location.move(types.Point(x=x_offset, y=y_offset))
dispense_and_detach(pipette_20ul, 1, adjusted_location)
# After all green drops are dispensed, drop the tip
pipette_20ul.drop_tip()
# --- End of green ---
# --- Begin Cyan Patterning ---
cyan_coords = [
(-20.4, 30),(-18, 30),(-15.6, 30),(-13.2, 30),(-10.8, 30),(-8.4, 30),(-6, 30),(-3.6, 30),(-1.2, 30),(1.2, 30),(3.6, 30),(6, 30),(8.4, 30),(-22.8, 27.6),(10.8, 27.6),(13.2, 27.6),(-25.2, 25.2),(15.6, 25.2),(-27.6, 22.8),(18, 22.8),(-30, 20.4),(20.4, 20.4),(-32.4, 18),(22.8, 18),(-32.4, 15.6),(22.8, 15.6),(-32.4, 13.2),(22.8, 13.2),(-37.2, 10.8),(-34.8, 10.8),(-32.4, 10.8),(20.4, 10.8),(22.8, 10.8),(25.2, 10.8),(27.6, 10.8),(30, 10.8),(32.4, 10.8),(-37.2, 8.4),(-32.4, 8.4),(18, 8.4),(34.8, 8.4),(-37.2, 6),(-32.4, 6),(15.6, 6),(37.2, 6),(-37.2, 3.6),(-34.8, 3.6),(-32.4, 3.6),(13.2, 3.6),(39.6, 3.6),(-34.8, 1.2),(10.8, 1.2),(39.6, 1.2),(-34.8, -1.2),(-30, -1.2),(10.8, -1.2),(39.6, -1.2),(-34.8, -3.6),(-27.6, -3.6),(10.8, -3.6),(39.6, -3.6),(-34.8, -6),(-32.4, -6),(-25.2, -6),(-22.8, -6),(-20.4, -6),(10.8, -6),(37.2, -6),(-34.8, -8.4),(-30, -8.4),(-18, -8.4),(-15.6, -8.4),(-13.2, -8.4),(-10.8, -8.4),(6, -8.4),(8.4, -8.4),(13.2, -8.4),(15.6, -8.4),(18, -8.4),(20.4, -8.4),(22.8, -8.4),(25.2, -8.4),(27.6, -8.4),(30, -8.4),(32.4, -8.4),(34.8, -8.4),(-34.8, -10.8),(-27.6, -10.8),(-25.2, -10.8),(-8.4, -10.8),(-6, -10.8),(-3.6, -10.8),(-1.2, -10.8),(1.2, -10.8),(3.6, -10.8),(13.2, -10.8),(27.6, -10.8),(-34.8, -13.2),(-22.8, -13.2),(-20.4, -13.2),(13.2, -13.2),(27.6, -13.2),(-34.8, -15.6),(-22.8, -15.6),(-20.4, -15.6),(-18, -15.6),(-15.6, -15.6),(-13.2, -15.6),(-10.8, -15.6),(-8.4, -15.6),(8.4, -15.6),(10.8, -15.6),(13.2, -15.6),(27.6, -15.6),(-32.4, -18),(-30, -18),(-27.6, -18),(-25.2, -18),(-8.4, -18),(-6, -18),(-3.6, -18),(-1.2, -18),(1.2, -18),(3.6, -18),(6, -18),(8.4, -18),(15.6, -18),(18, -18),(20.4, -18),(22.8, -18),(25.2, -18),(-8.4, -20.4),(8.4, -20.4),(-8.4, -22.8),(8.4, -22.8),(-8.4, -25.2),(8.4, -25.2),(-6, -27.6),(-3.6, -27.6),(-1.2, -27.6),(1.2, -27.6),(3.6, -27.6),(6, -27.6)
]
# Pick up a tip for Cyan color
pipette_20ul.pick_up_tip()
# Dispense each Cyan dot
for i, (x_offset, y_offset) in enumerate(cyan_coords):
if i % 20 == 0:
# Aspirate 20uL or the remaining number of dispenses, whichever is smaller
pipette_20ul.aspirate(min(20, len(cyan_coords) - i), location_of_color('Cyan'))
adjusted_location = center_location.move(types.Point(x=x_offset, y=y_offset))
dispense_and_detach(pipette_20ul, 1, adjusted_location)
# After all Cyan drops are dispensed, drop the tip
pipette_20ul.drop_tip()
# --- End Cyan Patterning ---
# --- Begin Blue Patterning ---
blue_coords = [
(-18, 27.6),(-15.6, 27.6),(-13.2, 27.6),(-10.8, 27.6),(-8.4, 27.6),(-6, 27.6),(-3.6, 27.6),(-1.2, 27.6),(1.2, 27.6),(3.6, 27.6),(6, 27.6),(8.4, 27.6),(-18, 25.2),(-15.6, 25.2),(-13.2, 25.2),(-10.8, 25.2),(-8.4, 25.2),(-6, 25.2),(-3.6, 25.2),(-1.2, 25.2),(1.2, 25.2),(3.6, 25.2),(6, 25.2),(8.4, 25.2),(-18, 22.8),(-15.6, 22.8),(-13.2, 22.8),(-10.8, 22.8),(-8.4, 22.8),(-6, 22.8),(-3.6, 22.8),(-1.2, 22.8),(1.2, 22.8),(3.6, 22.8),(6, 22.8),(-18, 20.4),(-15.6, 20.4),(-13.2, 20.4),(-10.8, 20.4),(-8.4, 20.4),(-6, 20.4),(-3.6, 20.4),(-1.2, 20.4),(1.2, 20.4),(3.6, 20.4),(-15.6, 18),(-13.2, 18),(-10.8, 18),(-8.4, 18),(-6, 18),(3.6, 18),(-15.6, 15.6),(-13.2, 15.6),(-10.8, 15.6),(3.6, 15.6),(6, 15.6),(8.4, 15.6),(-30, 13.2),(-27.6, 13.2),(-25.2, 13.2),(-22.8, 13.2),(-20.4, 13.2),(6, 13.2),(8.4, 13.2),(10.8, 13.2),(13.2, 13.2),(-30, 10.8),(-27.6, 10.8),(-25.2, 10.8),(-22.8, 10.8),(-20.4, 10.8),(-18, 10.8),(6, 10.8),(8.4, 10.8),(10.8, 10.8),(13.2, 10.8),(15.6, 10.8),(-30, 8.4),(-27.6, 8.4),(-25.2, 8.4),(-22.8, 8.4),(-20.4, 8.4),(-18, 8.4),(6, 8.4),(8.4, 8.4),(10.8, 8.4),(13.2, 8.4),(15.6, 8.4),(-30, 6),(-27.6, 6),(-25.2, 6),(-22.8, 6),(-20.4, 6),(-18, 6),(-1.2, 6),(1.2, 6),(3.6, 6),(6, 6),(8.4, 6),(10.8, 6),(13.2, 6),(20.4, 6),(22.8, 6),(25.2, 6),(-30, 3.6),(-27.6, 3.6),(-25.2, 3.6),(-22.8, 3.6),(-20.4, 3.6),(-18, 3.6),(-15.6, 3.6),(-3.6, 3.6),(-1.2, 3.6),(1.2, 3.6),(3.6, 3.6),(6, 3.6),(8.4, 3.6),(10.8, 3.6),(20.4, 3.6),(-30, 1.2),(-27.6, 1.2),(-25.2, 1.2),(-22.8, 1.2),(-20.4, 1.2),(-18, 1.2),(-15.6, 1.2),(-3.6, 1.2),(-1.2, 1.2),(1.2, 1.2),(3.6, 1.2),(6, 1.2),(8.4, 1.2),(20.4, 1.2),(-27.6, -1.2),(-25.2, -1.2),(-22.8, -1.2),(-20.4, -1.2),(-18, -1.2),(-15.6, -1.2),(-3.6, -1.2),(-1.2, -1.2),(1.2, -1.2),(3.6, -1.2),(6, -1.2),(8.4, -1.2),(-25.2, -3.6),(-22.8, -3.6),(-20.4, -3.6),(-18, -3.6),(-15.6, -3.6),(-13.2, -3.6),(-3.6, -3.6),(-1.2, -3.6),(1.2, -3.6),(3.6, -3.6),(6, -3.6),(8.4, -3.6),(-18, -6),(-15.6, -6),(-13.2, -6),(-3.6, -6),(-1.2, -6),(1.2, -6),(3.6, -6),(6, -6),(8.4, -6),(-3.6, -8.4),(-1.2, -8.4),(1.2, -8.4),(3.6, -8.4)
]
# Pick up a tip for Blue color
pipette_20ul.pick_up_tip()
# Dispense each Blue dot
for i, (x_offset, y_offset) in enumerate(blue_coords):
if i % 20 == 0:
# Aspirate 20uL or the remaining number of dispenses, whichever is smaller
pipette_20ul.aspirate(min(20, len(blue_coords) - i), location_of_color('Blue'))
adjusted_location = center_location.move(types.Point(x=x_offset, y=y_offset))
dispense_and_detach(pipette_20ul, 1, adjusted_location)
# After all Blue drops are dispensed, drop the tip
pipette_20ul.drop_tip()
# --- End Blue Patterning ---
# --- Begin Dark green Patterning ---
dark_green_coords = [
(-20.4, 27.6),(-22.8, 25.2),(-20.4, 25.2),(10.8, 25.2),(13.2, 25.2),(-25.2, 22.8),(-22.8, 22.8),(-20.4, 22.8),(8.4, 22.8),(10.8, 22.8),(13.2, 22.8),(15.6, 22.8),(-27.6, 20.4),(-25.2, 20.4),(-22.8, 20.4),(-20.4, 20.4),(6, 20.4),(8.4, 20.4),(10.8, 20.4),(13.2, 20.4),(15.6, 20.4),(18, 20.4),(-30, 18),(-27.6, 18),(-25.2, 18),(-22.8, 18),(-20.4, 18),(-18, 18),(-3.6, 18),(-1.2, 18),(1.2, 18),(6, 18),(8.4, 18),(10.8, 18),(13.2, 18),(15.6, 18),(18, 18),(20.4, 18),(-30, 15.6),(-27.6, 15.6),(-25.2, 15.6),(-22.8, 15.6),(-20.4, 15.6),(-18, 15.6),(-8.4, 15.6),(-6, 15.6),(-3.6, 15.6),(-1.2, 15.6),(1.2, 15.6),(10.8, 15.6),(13.2, 15.6),(15.6, 15.6),(18, 15.6),(20.4, 15.6),(-18, 13.2),(-15.6, 13.2),(-13.2, 13.2),(-10.8, 13.2),(-8.4, 13.2),(-6, 13.2),(-3.6, 13.2),(-1.2, 13.2),(1.2, 13.2),(3.6, 13.2),(15.6, 13.2),(18, 13.2),(20.4, 13.2),(-15.6, 10.8),(-13.2, 10.8),(-10.8, 10.8),(-8.4, 10.8),(-6, 10.8),(-3.6, 10.8),(-1.2, 10.8),(1.2, 10.8),(3.6, 10.8),(18, 10.8),(-15.6, 8.4),(-13.2, 8.4),(-10.8, 8.4),(-8.4, 8.4),(-6, 8.4),(-3.6, 8.4),(-1.2, 8.4),(1.2, 8.4),(3.6, 8.4),(-15.6, 6),(-13.2, 6),(-10.8, 6),(-8.4, 6),(-6, 6),(-3.6, 6),(-13.2, 3.6),(-10.8, 3.6),(-8.4, 3.6),(-6, 3.6),(-13.2, 1.2),(-10.8, 1.2),(-8.4, 1.2),(-6, 1.2),(-13.2, -1.2),(-10.8, -1.2),(-8.4, -1.2),(-6, -1.2),(-10.8, -3.6),(-8.4, -3.6),(-6, -3.6),(-10.8, -6),(-8.4, -6),(-6, -6),(-8.4, -8.4),(-6, -8.4),(15.6, -10.8),(18, -10.8),(20.4, -10.8),(22.8, -10.8),(25.2, -10.8),(15.6, -13.2),(18, -13.2),(20.4, -13.2),(22.8, -13.2),(25.2, -13.2),(15.6, -15.6),(18, -15.6),(20.4, -15.6),(22.8, -15.6),(25.2, -15.6)
]
# Pick up a tip for Dark green color
pipette_20ul.pick_up_tip()
# Dispense each Dark Green dot
for i, (x_offset, y_offset) in enumerate(dark_green_coords):
if i % 20 == 0:
# Aspirate 20uL or the remaining number of dispenses, whichever is smaller
pipette_20ul.aspirate(min(20, len(dark_green_coords) - i), location_of_color('darkgreen'))
adjusted_location = center_location.move(types.Point(x=x_offset, y=y_offset))
dispense_and_detach(pipette_20ul, 1, adjusted_location)
# After all Dark Green drops are dispensed, drop the tip
pipette_20ul.drop_tip()
# --- End Dark Green Patterning ---
# --- Begin Yellow Patterning ---
yellow_points = [
(22.8, 3.6),(25.2, 3.6),(-32.4, 1.2),(22.8, 1.2),(25.2, 1.2),(-32.4, -1.2),(-32.4, -3.6),(-30, -3.6),(-30, -6),(-27.6, -6),(-27.6, -8.4),(-25.2, -8.4),(-22.8, -8.4),(-20.4, -8.4),(10.8, -8.4),(-22.8, -10.8),(-20.4, -10.8),(-18, -10.8),(-15.6, -10.8),(-13.2, -10.8),(-10.8, -10.8),(6, -10.8),(8.4, -10.8),(10.8, -10.8),(-18, -13.2),(-15.6, -13.2),(-13.2, -13.2),(-10.8, -13.2),(-8.4, -13.2),(-6, -13.2),(-3.6, -13.2),(-1.2, -13.2),(1.2, -13.2),(3.6, -13.2),(6, -13.2),(8.4, -13.2),(10.8, -13.2),(-6, -15.6),(-3.6, -15.6),(-1.2, -15.6),(1.2, -15.6),(3.6, -15.6),(6, -15.6)
]
# Pick up a tip for Yellow color
pipette_20ul.pick_up_tip()
# Dispense each Yellow dot
for i, (x_offset, y_offset) in enumerate(yellow_points):
if i % 20 == 0:
# Aspirate 20uL or the remaining number of dispenses, whichever is smaller
pipette_20ul.aspirate(min(20, len(yellow_points) - i), location_of_color('Yellow'))
adjusted_location = center_location.move(types.Point(x=x_offset, y=y_offset))
dispense_and_detach(pipette_20ul, 1, adjusted_location)
# After all Yellow drops are dispensed, drop the tip
pipette_20ul.drop_tip()
# --- End Yellow Patterning (yellow_points) ---
# Don't forget to end with a drop_tip()