Week 3 HW: lab automation

Python Script for Opentrons Artwork

Bacteriophase Bacteriophase
from opentrons import types
import math

metadata = {    # see https://docs.opentrons.com/v2/tutorial.html#tutorial-metadata
    'author': 'Saba Saeed',
    'protocolName': 'Opentrons Bacteriophage Structural Artwork',
    'description': '',
    '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' : 'Yellow',
    'C1' : 'Green',
    'D1' : 'Cyan',
    'E1' : 'Blue'
}

def run(protocol):

    ##############################################################################
    ###   Load labware, modules and pipettes
    ##############################################################################

    tips_20ul = protocol.load_labware(
        'opentrons_96_tiprack_20ul',
        TIP_RACK_DECK_SLOT,
        'Opentrons 20uL Tips'
    )

    pipette_20ul = protocol.load_instrument(
        "p20_single_gen2",
        "right",
        [tips_20ul]
    )

    temperature_module = protocol.load_module(
        'temperature module gen2',
        COLORS_DECK_SLOT
    )

    temperature_plate = temperature_module.load_labware(
        'opentrons_96_aluminumblock_generic_pcr_strip_200ul',
        'Cold Plate'
    )

    color_plate = temperature_plate

    agar_plate = protocol.load_labware(
        'htgaa_agar_plate',
        AGAR_DECK_SLOT,
        'Agar Plate'
    )

    center_location = agar_plate['A1'].top()

    pipette_20ul.starting_tip = tips_20ul.well(PIPETTE_STARTING_TIP_WELL)

    ##############################################################################
    ###   Helper functions for this lab
    ##############################################################################

    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}")

    def dispense_and_detach(pipette, volume, location):
        above_location = location.move(types.Point(z=location.point.z + 5))
        pipette.move_to(above_location)
        pipette.dispense(volume, location)
        pipette.move_to(above_location)

    ##############################################################################
    ###   Patterning & Auto-Scaling
    ##############################################################################

    # Raw GUI coordinate arrays
    azurite_points = [
        (-2.25,20.25),(2.25,20.25),(2.25,18.75),(2.25,15.75),(2.25,6.75),
        (-8.25,-3.75),(6.75,-3.75),(8.25,-3.75),(-9.75,-6.75),(-6.75,-6.75),
        (2.25,-6.75),(-6.75,-8.25),(-2.25,-8.25),(2.25,-9.75),(3.75,-9.75),
        (5.25,-9.75),(8.25,-9.75),(-12.75,-11.25),(9.75,-11.25),
        (-12.75,-12.75),(-11.25,-12.75),(5.25,-14.25),(11.25,-14.25),
        (12.75,-14.25),(-5.25,-15.75),(12.75,-15.75)
    ]

    mclover3_points = [
        (-0.75,20.25),(0.75,20.25),(3.75,18.75),(-6.75,17.25),(-3.75,17.25),
        (3.75,17.25),(-5.25,15.75),(-2.25,15.75),(-3.75,14.25),(3.75,14.25),
        (2.25,12.75),(-0.75,11.25),(0.75,11.25),(2.25,9.75),(-6.75,8.25),
        (3.75,8.25),(-2.25,6.75),(-0.75,6.75),(-2.25,5.25),(-2.25,3.75),
        (-2.25,2.25),(-2.25,0.75),(-0.75,0.75),(-2.25,-0.75),(-2.25,-2.25),
        (-9.75,-3.75),(-6.75,-3.75),(5.25,-3.75),(9.75,-3.75),
        (-11.25,-5.25),(-0.75,-5.25),(3.75,-5.25),(-5.25,-6.75),
        (-2.25,-6.75),(-0.75,-6.75),(5.25,-6.75),(-9.75,-8.25),
        (2.25,-8.25),(6.75,-8.25),(9.75,-8.25),(-5.25,-9.75),
        (-3.75,-9.75),(9.75,-9.75),(-5.25,-11.25),(5.25,-11.25),
        (-5.25,-12.75),(5.25,-12.75),(-12.75,-14.25),(-5.25,-14.25),
        (-12.75,-15.75),(-3.75,-17.25)
    ]

    mrfp1_points = [
        (-3.75,18.75),(-5.25,17.25),(5.25,17.25),(-6.75,15.75),
        (-0.75,15.75),(0.75,15.75),(5.25,15.75),(-6.75,14.25),
        (-6.75,12.75),(-2.25,12.75),(-6.75,11.25),(-6.75,9.75),
        (-3.75,9.75),(-2.25,9.75),(-0.75,9.75),(0.75,9.75),
        (-5.25,8.25),(5.25,8.25),(0.75,6.75),(-0.75,5.25),
        (0.75,5.25),(0.75,3.75),(0.75,2.25),(0.75,0.75),
        (0.75,-0.75),(-0.75,-2.25),(0.75,-2.25),(-5.25,-3.75),
        (-3.75,-3.75),(-2.25,-3.75),(-0.75,-3.75),(0.75,-3.75),
        (2.25,-3.75),(3.75,-3.75),(-9.75,-5.25),(-8.25,-5.25),
        (-6.75,-5.25),(-5.25,-5.25),(-3.75,-5.25),(-2.25,-5.25),
        (0.75,-5.25),(2.25,-5.25),(5.25,-5.25),(6.75,-5.25),
        (8.25,-5.25),(9.75,-5.25),(-11.25,-6.75),(-3.75,-6.75),
        (0.75,-6.75),(3.75,-6.75),(9.75,-6.75),(-8.25,-8.25),
        (-3.75,-8.25),(3.75,-8.25),(8.25,-8.25),(-11.25,-9.75),
        (-9.75,-9.75),(11.25,-9.75),(-11.25,-11.25),(-3.75,-11.25),
        (3.75,-11.25),(11.25,-11.25),(-3.75,-12.75),(3.75,-12.75),
        (11.25,-12.75),(-3.75,-14.25),(3.75,-14.25),
        (-3.75,-15.75),(3.75,-15.75),(3.75,-17.25)
    ]

    sfgfp_points = [
        (5.25,14.25),(5.25,12.75),(5.25,11.25),(5.25,9.75),
        (-3.75,6.75),(3.75,6.75),(-12.75,-8.25),
        (-11.25,-8.25),(-12.75,-9.75)
    ]

    # Color grouping
    color_groups = [
        ('Blue', azurite_points),
        ('Green', mclover3_points),
        ('Red', mrfp1_points),
        ('Yellow', sfgfp_points)
    ]

    # Auto-scaling logic
    all_points = azurite_points + mclover3_points + mrfp1_points + sfgfp_points
    max_r = 0

    for x, y in all_points:
        r = math.sqrt(x**2 + y**2)
        if r > max_r:
            max_r = r

    scale_factor = 38.0 / max_r if max_r > 38.0 else 1.0

    # Physical compilation loop
    for color_name, points in color_groups:

        if not points:
            continue

        pipette_20ul.pick_up_tip()

        for i, (x, y) in enumerate(points):

            if i % 20 == 0:
                drops_remaining = len(points) - i
                volume_to_aspirate = min(20, drops_remaining)

                pipette_20ul.aspirate(
                    volume_to_aspirate,
                    location_of_color(color_name)
                )

            scaled_x = x * scale_factor
            scaled_y = y * scale_factor

            adjusted_location = center_location.move(
                types.Point(x=scaled_x, y=scaled_y)
            )

            dispense_and_detach(
                pipette_20ul,
                1,
                adjusted_location
            )

        pipette_20ul.drop_tip()
Bacteriophase Bacteriophase