Week 3 Homework: Lab Automation

Part 1: Python Script for Opentrons Artwork

Using GUI opentrons-art.rcdonovan.com, I created two designs, one using a whole range of colors and the other using just red and green (https://opentrons-art.rcdonovan.com/?id=zk5r154b9r6q995).

full
mplum_points = [(-4.4, 24.2),(-2.2, 24.2),(0, 24.2),(-6.6, 22),(-4.4, 22),(2.2, 22),(-6.6, 19.8),(-4.4, 19.8),(2.2, 19.8),(-6.6, 17.6),(-4.4, 17.6),(-2.2, 17.6),(0, 17.6),(2.2, 17.6),(-6.6, 15.4),(-4.4, 15.4),(-2.2, 15.4),(-6.6, 13.2),(-4.4, 13.2),(-6.6, 11),(-4.4, 11),(-6.6, 8.8),(-4.4, 8.8),(-6.6, 6.6),(-4.4, 6.6),(-2.2, 6.6),(-6.6, 4.4),(-4.4, 4.4),(-2.2, 4.4),(-6.6, 2.2),(-4.4, 2.2),(-2.2, 2.2),(-6.6, 0),(-4.4, 0),(-2.2, 0),(-6.6, -2.2),(-4.4, -2.2),(-2.2, -2.2),(-6.6, -4.4),(-4.4, -4.4),(-2.2, -4.4),(-6.6, -6.6),(-4.4, -6.6),(-2.2, -6.6),(-6.6, -8.8),(-4.4, -8.8),(-2.2, -8.8),(-6.6, -11),(-4.4, -11),(-2.2, -11),(-6.6, -13.2),(-4.4, -13.2),(-2.2, -13.2),(-6.6, -15.4),(-4.4, -15.4),(-2.2, -15.4),(-4.4, -17.6)] 
mko2_points = [(0, 15.4),(2.2, 15.4),(-2.2, 13.2),(0, 13.2),(2.2, 13.2),(4.4, 13.2),(-2.2, 11),(0, 11),(2.2, 11),(4.4, 11),(-2.2, 8.8),(0, 8.8),(2.2, 8.8),(4.4, 8.8),(0, 6.6),(2.2, 6.6)] 
ultramarine_points = [(-2.2, 22),(0, 22),(-2.2, 19.8),(0, 19.8)] 
electra2_points = [(4.4, 2.2),(6.6, 2.2),(8.8, 2.2),(11, 2.2),(13.2, 2.2),(15.4, 2.2),(17.6, 2.2),(8.8, 0),(13.2, 0),(6.6, -2.2),(13.2, -2.2),(15.4, -2.2),(17.6, -2.2),(4.4, -4.4),(13.2, -4.4),(2.2, -6.6),(4.4, -6.6),(6.6, -6.6),(8.8, -6.6),(13.2, -6.6)]
redgreen
megfp_points = [(-4.4, 24.2),(-2.2, 24.2),(0, 24.2),(-6.6, 22),(-4.4, 22),(2.2, 22),(-6.6, 19.8),(-4.4, 19.8),(2.2, 19.8),(-6.6, 17.6),(-4.4, 17.6),(-2.2, 17.6),(0, 17.6),(2.2, 17.6),(-6.6, 15.4),(-4.4, 15.4),(-2.2, 15.4),(-6.6, 13.2),(-4.4, 13.2),(-6.6, 11),(-4.4, 11),(-6.6, 8.8),(-4.4, 8.8),(-6.6, 6.6),(-4.4, 6.6),(-2.2, 6.6),(-6.6, 4.4),(-4.4, 4.4),(-2.2, 4.4),(-6.6, 2.2),(-4.4, 2.2),(-2.2, 2.2),(-6.6, 0),(-4.4, 0),(-2.2, 0),(-6.6, -2.2),(-4.4, -2.2),(-2.2, -2.2),(-6.6, -4.4),(-4.4, -4.4),(-2.2, -4.4),(-6.6, -6.6),(-4.4, -6.6),(-2.2, -6.6),(-6.6, -8.8),(-4.4, -8.8),(-2.2, -8.8),(-6.6, -11),(-4.4, -11),(-2.2, -11),(-6.6, -13.2),(-4.4, -13.2),(-2.2, -13.2),(-6.6, -15.4),(-4.4, -15.4),(-2.2, -15.4),(-4.4, -17.6)] 
mrfp1_points = [(-2.2, 22),(0, 22),(-2.2, 19.8),(0, 19.8),(0, 15.4),(2.2, 15.4),(-2.2, 13.2),(0, 13.2),(2.2, 13.2),(4.4, 13.2),(-2.2, 11),(0, 11),(2.2, 11),(4.4, 11),(-2.2, 8.8),(0, 8.8),(2.2, 8.8),(4.4, 8.8),(0, 6.6),(2.2, 6.6)]
response

I submitted my design but I still have some inquiries about the python file.

from opentrons import types

import string

metadata = {
    'protocolName': 'Sarah Jimenez - Opentrons Art - HTGAA',
    'author': 'HTGAA',
    'source': 'HTGAA 2026',
    'apiLevel': '2.20'
}

Z_VALUE_AGAR = 2.0
POINT_SIZE = 1

megfp_points = [(-4.4,24.2), (-2.2,24.2), (0,24.2), (-6.6,22), (-4.4,22), (2.2,22), (-6.6,19.8), (-4.4,19.8), (2.2,19.8), (-6.6,17.6), (-4.4,17.6), (-2.2,17.6), (0,17.6), (2.2,17.6), (-6.6,15.4), (-4.4,15.4), (-2.2,15.4), (-6.6,13.2), (-4.4,13.2), (-6.6,11), (-4.4,11), (-6.6,8.8), (-4.4,8.8), (-6.6,6.6), (-4.4,6.6), (-2.2,6.6), (-6.6,4.4), (-4.4,4.4), (-2.2,4.4), (-6.6,2.2), (-4.4,2.2), (-2.2,2.2), (-6.6,0), (-4.4,0), (-2.2,0), (-6.6,-2.2), (-4.4,-2.2), (-2.2,-2.2), (-6.6,-4.4), (-4.4,-4.4), (-2.2,-4.4), (-6.6,-6.6), (-4.4,-6.6), (-2.2,-6.6), (-6.6,-8.8), (-4.4,-8.8), (-2.2,-8.8), (-6.6,-11), (-4.4,-11), (-2.2,-11), (-6.6,-13.2), (-4.4,-13.2), (-2.2,-13.2), (-6.6,-15.4), (-4.4,-15.4), (-2.2,-15.4), (-4.4,-17.6)]
mrfp1_points = [(-2.2,22), (0,22), (-2.2,19.8), (0,19.8), (0,15.4), (2.2,15.4), (-2.2,13.2), (0,13.2), (2.2,13.2), (4.4,13.2), (-2.2,11), (0,11), (2.2,11), (4.4,11), (-2.2,8.8), (0,8.8), (2.2,8.8), (4.4,8.8), (0,6.6), (2.2,6.6)]

point_name_pairing = [("megfp", megfp_points),("mrfp1", mrfp1_points)]

# Robot deck setup constants
TIP_RACK_DECK_SLOT = 9
COLORS_DECK_SLOT = 6
AGAR_DECK_SLOT = 5
PIPETTE_STARTING_TIP_WELL = 'A1'

# Place the PCR tubes in this order
well_colors = {
    'A1': 'sfGFP',
    'A2': 'mRFP1',
    'A3': 'mKO2',
    'A4': 'Venus',
    'A5': 'mKate2_TF',
    'A6': 'Azurite',
    'A7': 'mCerulean3',
    'A8': 'mClover3',
    'A9': 'mJuniper',
    'A10': 'mTurquoise2',
    'A11': 'mBanana',
    'A12': 'mPlum',
    'B1': 'Electra2',
    'B2': 'mWasabi',
    'B3': 'mScarlet_I',
    'B4': 'mPapaya',
    'B5': 'eqFP578',
    'B6': 'tdTomato',
    'B7': 'DsRed',
    'B8': 'mKate2',
    'B9': 'EGFP',
    'B10': 'mRuby2',
    'B11': 'TagBFP',
    'B12': 'mChartreuse_TF',
    'C1': 'mLychee_TF',
    'C2': 'mTagBFP2',
    'C3': 'mEGFP',
    'C4': 'mNeonGreen',
    'C5': 'mAzamiGreen',
    'C6': 'mWatermelon',
    'C7': 'avGFP',
    'C8': 'mCitrine',
    'C9': 'mVenus',
    'C10': 'mCherry',
    'C11': 'mHoneydew',
    'C12': 'TagRFP',
    'D1': 'mTFP1',
    'D2': 'Ultramarine',
    'D3': 'ZsGreen1',
    'D4': 'mMiCy',
    'D5': 'mStayGold2',
    'D6': 'PA_GFP'
}

volume_used = {
    'megfp': 0,
    'mrfp1': 0
}

def update_volume_remaining(current_color, quantity_to_aspirate):
    rows = string.ascii_uppercase
    for well, color in list(well_colors.items()):
        if color == current_color:
            if (volume_used[current_color] + quantity_to_aspirate) > 250:
                # Move to next well horizontally by advancing row letter, keeping column number
                row = well[0]
                col = well[1:]
                
                # Find next row letter
                next_row = rows[rows.index(row) + 1]
                next_well = f"{next_row}{col}"
                
                del well_colors[well]
                well_colors[next_well] = current_color
                volume_used[current_color] = quantity_to_aspirate
            else:
                volume_used[current_color] += quantity_to_aspirate
            break

def run(protocol):
    # Load labware, modules and pipettes
    protocol.home()

    # 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])

    # Deep Well Plate
    temperature_plate = protocol.load_labware('nest_96_wellplate_2ml_deep', 6)

    # Agar Plate
    agar_plate = protocol.load_labware('htgaa_agar_plate', AGAR_DECK_SLOT, 'Agar Plate')
    agar_plate.set_offset(x=0.00, y=0.00, z=Z_VALUE_AGAR)

    # 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)
    
    # Helper function (dispensing)
    def dispense_and_jog(pipette, volume, location):
        assert(isinstance(volume, (int, float)))
        # Go above the location
        above_location = location.move(types.Point(z=location.point.z + 2))
        pipette.move_to(above_location)
        # Go downwards and dispense
        pipette.dispense(volume, location)
        # Go upwards to avoid smearing
        pipette.move_to(above_location)

    # Helper function (color location)
    def location_of_color(color_string):
        for well,color in well_colors.items():
            if color.lower() == color_string.lower():
                return temperature_plate[well]
        raise ValueError(f"No well found with color {color_string}")

    # Print pattern by iterating over lists
    for i, (current_color, point_list) in enumerate(point_name_pairing):
        # Skip the rest of the loop if the list is empty
        if not point_list:
            continue

        # Get the tip for this run, set the bacteria color, and the aspirate bacteria of choice
        pipette_20ul.pick_up_tip()
        max_aspirate = int(18 // POINT_SIZE) * POINT_SIZE
        quantity_to_aspirate = min(len(point_list)*POINT_SIZE, max_aspirate)
        update_volume_remaining(current_color, quantity_to_aspirate)
        pipette_20ul.aspirate(quantity_to_aspirate, location_of_color(current_color))

        # Iterate over the current points list and dispense them, refilling along the way
        for i in range(len(point_list)):
            x, y = point_list[i]
            adjusted_location = center_location.move(types.Point(x, y))

            dispense_and_jog(pipette_20ul, POINT_SIZE, adjusted_location)
            
            if pipette_20ul.current_volume == 0 and len(point_list[i+1:]) > 0:
                quantity_to_aspirate = min(len(point_list[i:])*POINT_SIZE, max_aspirate)
                update_volume_remaining(current_color, quantity_to_aspirate)
                pipette_20ul.aspirate(quantity_to_aspirate, location_of_color(current_color))

        # Drop tip between each color
        pipette_20ul.drop_tip()

Part 2: Opentrons on published science and project

2.1. Automated, high-throughput in-situ hybridization of Lytechinus pictus embryos

response

This study presents the development of an automated, high-throughput hybridization chain reaction (HT-HCR) pipeline optimized for whole-mount embryos of Lytechinus pictus, addressing a central technical limitation in developmental biology: the low scalability of conventional in situ hybridization. The authors engineered a miniaturized, robotics-based workflow capable of processing large probe sets in 96-well format with minimal reagent volumes and without manual intervention, followed by automated confocal imaging. Using this platform, they generated spatial expression data for over one hundred genes across multiple embryonic stages, encompassing transcription factors, signaling components, and physiological regulators. Importantly, the method preserves spatial resolution while dramatically increasing throughput, thereby enabling systematic construction of spatial gene expression atlases and facilitating integration with perturbation-based studies. Overall, this work represents a methodological advance that bridges classical embryological approaches with scalable spatial transcriptomics, significantly expanding the experimental capacity for gene regulatory network analysis in early development.

response response

2.2. What about the project?

An Opentrons platform could be strategically integrated into the “exchangeable Fabs” workflow to automate and standardize the Design–Build–Test cycle of recombinant Fab production. At the molecular cloning stage, Opentrons could perform high-throughput Golden Gate or Gibson assemblies to systematically combine variable Fab regions with a standardized Fc backbone plasmid, followed by automated bacterial transformation, colony PCR setup, and plasmid prep normalization in 96-well format. For protein production, the system could prepare transfection mixes (e.g., HEK293 or CHO systems), seed cells, and manage media exchanges in small-scale expression screens to optimize Fab yield and stability. Downstream, Opentrons could automate affinity purification workflows (Protein A/G or Ni-NTA for tagged constructs) using magnetic bead–based protocols, ensuring reproducible wash and elution conditions while minimizing reagent waste—particularly relevant for resource-limited laboratories. Finally, in the validation phase, the robot could execute ELISA plate coating, blocking, serial dilutions, and detection antibody incubations to quantitatively screen binding affinity and specificity across multiple designed Fabs in parallel. Overall, implementing Opentrons would increase reproducibility, reduce hands-on time and antibody wastage, and enable scalable, semi-automated screening of in silico–designed Fab variants prior to functional characterization.

Part 3: Project ideas!!

full full full