Week 3 HW: Lab Automation

Week 3: Lab Automation & Opentrons Art

Introduction

This week’s focus is on the intersection of biology, robotics, and creative coding. As part of the HTGAA 2026* cohort based in Zambia, I am exploring how liquid-handling automation (specifically the Opentrons OT-2) can streamline laboratory workflows. Beyond the technical utility, this assignment challenged us to use the robot as a canvas, translating digital coordinates into physical biological art.

Lab automation isn’t just about efficiency; it’s about precision in environments where resources must be used optimally. My work this week involves a Python-based protocol that instructs the robot to “paint” a design using colored liquids in a 96-well plate.

AI Documentation (Opentrons Python Script)

Model used: Gemini 3 Flash (Free Tier)

Description of AI Contribution: AI was utilized to translate the artistic concept from the Opentrons GUI into a functional Python script using the Opentrons API v2.13. Specifically, the AI assisted in:

  • Optimization Logic: Implementing a conditional loop (if spots_drawn % 8 == 0) to handle bulk aspiration, which reduces the number of trips the pipette makes to the source reservoir.
  • Spatial Mapping: Calculating relative coordinates using types.Point for precise deposition on an agar plate or flat-bottom well plate.
  • Troubleshooting: Ensuring proper tip handling (e.g., including drop_tip() commands) to prevent cross-contamination and robot errors.
  • Metadata Structure: Properly formatting the protocol metadata and labware loading sequences required for the robot to recognize the script.

The final art concept and the selection of the specific visual ID (zjiq3p93t07ee2n) were directed by the student, while the AI served as a technical co-pilot for the Python implementation.


The Artwork Design

I used the Opentrons Art GUI to map out the coordinates for my design. The visual representation and the specific well-mapping for this protocol can be viewed at the link below:

View my design here: Opentrons Art Design - x8zh29jmvm87u3v


Opentrons Python Protocol

Below is the Python script generated to execute the design. This script defines the labware (tips, reservoir, and plate) and the specific pipetting movements required to recreate the art.

from opentrons import types

metadata = {    # see https://docs.opentrons.com/v2/tutorial.html#tutorial-metadata
    'author': 'ELSA MULEYA',
    'protocolName': 'HTGAA Agar Art - Full Set',
    'description': 'FLORAL ART',
    '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' : 'Red',
    'B1' : 'Green',
    'C1' : 'Orange'
}


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

  ###
  ### YOUR CODE HERE to create your design
  mrfp1_points = [(-8.8, 24.2),(-6.6, 24.2),(6.6, 24.2),(8.8, 24.2),(-11, 22),(-8.8, 22),(-6.6, 22),(-4.4, 22),(4.4, 22),(6.6, 22),(8.8, 22),(11, 22),(-11, 19.8),(-8.8, 19.8),(-6.6, 19.8),(-4.4, 19.8),(-2.2, 19.8),(2.2, 19.8),(4.4, 19.8),(6.6, 19.8),(8.8, 19.8),(11, 19.8),(-11, 17.6),(-8.8, 17.6),(-6.6, 17.6),(-4.4, 17.6),(-2.2, 17.6),(2.2, 17.6),(4.4, 17.6),(6.6, 17.6),(8.8, 17.6),(11, 17.6),(-11, 15.4),(-8.8, 15.4),(-4.4, 15.4),(-2.2, 15.4),(2.2, 15.4),(6.6, 15.4),(8.8, 15.4),(11, 15.4),(-11, 13.2),(-8.8, 13.2),(-2.2, 13.2),(2.2, 13.2),(8.8, 13.2),(11, 13.2),(-22, 11),(-19.8, 11),(-17.6, 11),(-15.4, 11),(-8.8, 11),(-6.6, 11),(6.6, 11),(8.8, 11),(15.4, 11),(17.6, 11),(19.8, 11),(22, 11),(-24.2, 8.8),(-22, 8.8),(-19.8, 8.8),(-17.6, 8.8),(-15.4, 8.8),(-13.2, 8.8),(13.2, 8.8),(15.4, 8.8),(17.6, 8.8),(19.8, 8.8),(22, 8.8),(24.2, 8.8),(-24.2, 6.6),(-22, 6.6),(-19.8, 6.6),(-17.6, 6.6),(-13.2, 6.6),(-11, 6.6),(0, 6.6),(4.4, 6.6),(11, 6.6),(17.6, 6.6),(19.8, 6.6),(22, 6.6),(24.2, 6.6),(-22, 4.4),(-19.8, 4.4),(-17.6, 4.4),(-6.6, 4.4),(6.6, 4.4),(15.4, 4.4),(17.6, 4.4),(19.8, 4.4),(22, 4.4),(24.2, 4.4),(-19.8, 2.2),(-17.6, 2.2),(-15.4, 2.2),(-13.2, 2.2),(13.2, 2.2),(15.4, 2.2),(17.6, 2.2),(19.8, 2.2),(22, 2.2),(-8.8, 0),(0, 0),(8.8, 0),(-19.8, -2.2),(-17.6, -2.2),(-15.4, -2.2),(-13.2, -2.2),(-6.6, -2.2),(0, -2.2),(2.2, -2.2),(6.6, -2.2),(13.2, -2.2),(15.4, -2.2),(17.6, -2.2),(19.8, -2.2),(-22, -4.4),(-19.8, -4.4),(-17.6, -4.4),(-13.2, -4.4),(-6.6, -4.4),(-2.2, -4.4),(6.6, -4.4),(17.6, -4.4),(19.8, -4.4),(22, -4.4),(-24.2, -6.6),(-22, -6.6),(-19.8, -6.6),(-17.6, -6.6),(-11, -6.6),(-4.4, -6.6),(4.4, -6.6),(11, -6.6),(13.2, -6.6),(17.6, -6.6),(19.8, -6.6),(22, -6.6),(24.2, -6.6),(-24.2, -8.8),(-22, -8.8),(-19.8, -8.8),(-17.6, -8.8),(-15.4, -8.8),(-13.2, -8.8),(-11, -8.8),(0, -8.8),(11, -8.8),(13.2, -8.8),(15.4, -8.8),(17.6, -8.8),(19.8, -8.8),(22, -8.8),(24.2, -8.8),(-22, -11),(-19.8, -11),(-17.6, -11),(-15.4, -11),(-13.2, -11),(-8.8, -11),(-6.6, -11),(6.6, -11),(8.8, -11),(13.2, -11),(15.4, -11),(17.6, -11),(19.8, -11),(22, -11),(-11, -13.2),(-8.8, -13.2),(-2.2, -13.2),(2.2, -13.2),(8.8, -13.2),(11, -13.2),(-11, -15.4),(-8.8, -15.4),(-6.6, -15.4),(-2.2, -15.4),(2.2, -15.4),(4.4, -15.4),(8.8, -15.4),(11, -15.4),(-11, -17.6),(-8.8, -17.6),(-6.6, -17.6),(-4.4, -17.6),(-2.2, -17.6),(2.2, -17.6),(4.4, -17.6),(6.6, -17.6),(8.8, -17.6),(11, -17.6),(-11, -19.8),(-8.8, -19.8),(-6.6, -19.8),(-4.4, -19.8),(-2.2, -19.8),(2.2, -19.8),(4.4, -19.8),(6.6, -19.8),(8.8, -19.8),(11, -19.8),(-11, -22),(-8.8, -22),(-6.6, -22),(-4.4, -22),(4.4, -22),(6.6, -22),(8.8, -22),(11, -22),(-8.8, -24.2),(-6.6, -24.2),(6.6, -24.2),(8.8, -24.2)]
  sfgfp_points = [(-11, 28.6),(11, 28.6),(-13.2, 26.4),(-11, 26.4),(-8.8, 26.4),(8.8, 26.4),(11, 26.4),(13.2, 26.4),(-13.2, 24.2),(-11, 24.2),(11, 24.2),(13.2, 24.2),(-13.2, 22),(13.2, 22),(-13.2, 19.8),(13.2, 19.8),(-13.2, 17.6),(13.2, 17.6),(-13.2, 15.4),(-6.6, 15.4),(4.4, 15.4),(13.2, 15.4),(-26.4, 13.2),(-24.2, 13.2),(-22, 13.2),(-19.8, 13.2),(-17.6, 13.2),(-6.6, 13.2),(-4.4, 13.2),(4.4, 13.2),(6.6, 13.2),(17.6, 13.2),(19.8, 13.2),(22, 13.2),(24.2, 13.2),(26.4, 13.2),(28.6, 13.2),(-28.6, 11),(-26.4, 11),(-24.2, 11),(24.2, 11),(26.4, 11),(28.6, 11),(30.8, 11),(-26.4, 8.8),(-2.2, 8.8),(0, 8.8),(2.2, 8.8),(26.4, 8.8),(28.6, 8.8),(-15.4, 6.6),(-4.4, 6.6),(-2.2, 6.6),(2.2, 6.6),(13.2, 6.6),(15.4, 6.6),(26.4, 6.6),(-15.4, 4.4),(-13.2, 4.4),(-8.8, 4.4),(-2.2, 4.4),(2.2, 4.4),(8.8, 4.4),(13.2, 4.4),(-8.8, 2.2),(-6.6, 2.2),(-4.4, 2.2),(0, 2.2),(4.4, 2.2),(6.6, 2.2),(8.8, 2.2),(-6.6, 0),(6.6, 0),(-8.8, -2.2),(-4.4, -2.2),(4.4, -2.2),(8.8, -2.2),(-15.4, -4.4),(-8.8, -4.4),(2.2, -4.4),(8.8, -4.4),(13.2, -4.4),(15.4, -4.4),(-15.4, -6.6),(-13.2, -6.6),(-2.2, -6.6),(0, -6.6),(2.2, -6.6),(15.4, -6.6),(-26.4, -8.8),(-2.2, -8.8),(2.2, -8.8),(26.4, -8.8),(-28.6, -11),(-26.4, -11),(-24.2, -11),(24.2, -11),(26.4, -11),(28.6, -11),(-26.4, -13.2),(-24.2, -13.2),(-22, -13.2),(-19.8, -13.2),(-17.6, -13.2),(-6.6, -13.2),(-4.4, -13.2),(4.4, -13.2),(6.6, -13.2),(17.6, -13.2),(19.8, -13.2),(22, -13.2),(24.2, -13.2),(26.4, -13.2),(-13.2, -15.4),(-4.4, -15.4),(6.6, -15.4),(13.2, -15.4),(-13.2, -17.6),(13.2, -17.6),(-13.2, -19.8),(13.2, -19.8),(-13.2, -22),(13.2, -22),(-13.2, -24.2),(-11, -24.2),(11, -24.2),(13.2, -24.2),(-13.2, -26.4),(-11, -26.4),(-8.8, -26.4),(8.8, -26.4),(11, -26.4),(13.2, -26.4),(-11, -28.6),(11, -28.6)]

  # Combine the point data with their corresponding well colors into an art_data dictionary
  art_data = {
      'Red': {
          'well': 'A1',
          'points': mrfp1_points
      },
      'Green': {
          'well': 'B1',
          'points': sfgfp_points
      },
      'Orange': {
          'well': 'C1',
          'points': [] # Add points for Orange if needed, otherwise leave empty
      }
  }

  # --- EXECUTION LOGIC ---
  # Center spot of the agar (adjust based on plate size)
  center_well = agar_plate['D6'] # Fixed: Use dictionary-like access instead of wells_by_name()

  for color_name, data in art_data.items():
      source = color_plate[data["well"]] # Fixed: source_plate should be color_plate
      pipette_20ul.pick_up_tip()

      spots_drawn = 0
      for x, y in data["points"]:
          # Aspirate enough liquid for up to 8 spots, or less if fewer spots remain.
          # Each spot is 2uL, so 8 spots is 16uL.
          # The 'min' ensures we don't aspirate more than 16uL at a time or more than what's needed.
          if spots_drawn % 8 == 0:
              pipette_20ul.aspirate(min(16, (len(data["points"])-spots_drawn) * 2), source)

          # Create the relative coordinate on the agar plate
          target = center_well.top().move(types.Point(x=x, y=y, z=0))

          # Use the helper function to dispense and detach the tip
          dispense_and_detach(pipette_20ul, 2, target)

          spots_drawn += 1

      pipette_20ul.drop_tip()

3. Final Project Ideas


Idea 1: Zambia Mineral-Waste Bioremediation Predictor

  • Technical Problem: Mining tailings IN Zambia contain high levels of $Cu$ and $Zn$. Traditional cleaning is too expensive. We need “extremophiles” to stabilize these metals.
  • The Project: A computational pipeline to analyze the genomes of Bacillus and Pseudomonas from Zambian sites. I will search for protein sequences (Metallothioneins) that bind heavy metals.
  • Data Source: NCBI SRA data for “Zambian Mine Tailings,” specifically searching for the pbr (lead) and mer (mercury) operons.

Idea 2: Maize Lethal Necrosis (MLN) Genomic Tracker

  • Technical Problem: MLN is a double infection (MCMV + SCMV) devastating maize. It’s hard to distinguish strains visually.
  • The Project: A Comparative Genomics study comparing RNA sequences of MCMV from East Africa vs. South Africa to see if a unique “Zambian strain” is emerging.
  • Data Source: Nextstrain.org and GenBank, focusing on mutations in the Coat Protein (CP) gene.

Idea 3: Maize Yield “Climate-Window” Predictor

  • Technical Problem: Maize is highly vulnerable to moisture stress during the 2-week silking stage. Climate change has shifted Zambia’s rainy season.
  • The Project: An automated Predictive Model using “Agro-Meteorological” data to calculate Growing Degree Days (GDD) for Zambian hybrids (SeedCo/MRI) against 20 years of rainfall patterns.
  • Data Source: CHIRPS rainfall data for Zambia.