Week 3 HW: Lab Automation

Focus on Lab Automation research, with creative examples of OpenTrans instruction sets using Python. Final project slide to be included in Node deck.


Opentrons Art

This week started witn an exploration of the Opentrons Art web app found at https://opentrons-art.rcdonovan.com

I was able to quickly upload an image and randomize the colors, to generate a point paired data set. I really like the bitmap rasterization and creative expression found in the gallery.

automationart automationart

My investigation is based on my background in high resolution digital imaging. I wanted to better understand the pixel to microliter (uL) relationship. I see that with a 200 uL maximum quantity and a 90-100 mm Petrie Dish, it would seem that there are some basic constraints.

I look at that as an opportunity and design challenge to maximize resolution for the purpose of future scientific discovery. Similar to Moore’s law of exponential growth, the imaging industry has experienced the same trends, given today’’s 8K resolution and greater camera sensors.

Another reference point is with Twist labs, who have discovered how to overcome scale and quality limitations through in-silica transformation of a defined lab scale.

My approach was to explore how vector based graphics, defined by a series of points and splines, could be leveraged to create what is considered “infinite resolution” or at the very least, scalable and adjustable to meet the target output.

SVG, or “Scalable Vector Graphics” are the source of my BioArt for this activity. The entire library of icons we use in this Markdown format is a good example of what’s possible!

SVG SVG

I used Claude Ai to explore a web-friendly code base that would allow me to generate the key value pairs needed to script a Python function in the Opentrons protocol. The React/JS framework made it possible to design a User Interface (Ui) that allows for selection of any SVG, to render a resolution independent sample to the screen.

Dynamic features include assignment of a Color from an available list, increase in “Pitch” which is the number of points that are spaced along the computed line segments. Most importantly, is “Radius” which includes a value for uL, which relates to the size of a droplet in OpenTrons. The output is a PNG for a quick visual reference, and a JSON file or Text file for future parsing. I chose a simple Copy/Paste Text field to obtain the list of x,y point pairs, for use in Python for Opentrons.

App App Screenshot of SVG-to-Opentrons Converter web app by Eric Schneider

I processed several sample images and ran into a slight issue with how SVG segments are deemed continuous, so I refined the parser to handle each line segment individually. I also introduced GitHub to maintain a sense of version control as a web application can quickly grow, or become corrupt, by Ai agents.

I then focused on ensuring the web application could appear inside of our preferred Colab environment, using Python and iFrame libraries. However, that is “sandboxed” and can’t share data directly. (Which is why the copy-paste is important to expedite). I tried to replicate the solution in Colab, but most things broke.

I moved on to the Opentron Simulator in Colab, with my new Data Set. I have an intermediate understanding of coding, and with the help of Claude Ai, I was able to articulate my need for a recursive list that would not only plot the points needed for pipetting, but also manage aspiration in batches of 20, not exceeding 200 uL.

After some basic Python formatting errors, I was able to preview the results via the Simulation module, and it was a very close match to my design intent.

Protocol Sim Protocol Sim

See Appendix - Python Code

Reflection: I noticed that I was able to control the results of Vector for a high quality line that uses the full range of X, Y to the 10th of a millimeter (1 decimal point). Of course there is still the limitation of 80 mm diameter and 200 uL saturation, but I am encouraged that this technique can be refined for the purpose of high resolution design intent. I’m thinking about:

  • BioCircuits that follow continual line traces for current
  • BioSensors with defined sizes and shapes that are scalable
  • BioArt that mirrors iconography and symbols, with dot-pitch resolution controls.
  • BioPhotos that strive for incremental bitmap resolution at the microscopic level.

Imaging App- Future enhancement ideas:

  • Z depth may impact Radius.
  • Multiple SVG Layers, for multi-color assignments.
  • Save/load to a repository
  • Data sharing with Colab workspaces.
  • Integration of JSON for data sharing
  • Replicate application in Python in Colab natively.
  • Integrate color selection into color location.
  • Branching existing Automation Art code and exploring how to contribute to codebase.

OpenTrons Lab:

I was able to coordinate a working session with an OpenTrons OT-2, with Karen Ingram at the Charlotte Makerspace “BioArt Studio” which is an emerging destination for bioscience and art.

We attempted to load my protocol with vectorized points, but we encountered errors partially due to some code bugs which were quickly resolved. However, my Labware profiles were not defined for this platform configuration.

We deferred additional debugging in favor of using a known working Protocol for this session, which led to the output shown here. This is a good test since it shows the current state of functionality.

AutomatedPaint AutomatedPaint

I learned how to launch and calibrate the equipment for an automated production run. I also observed an opportunity to 3D print a calibration target that would make centering the gantry over a printable art medium like watercolor paper inside of a petrie dish. We discussed a custom hold-down to keep the paper flat for more control over quality.

Our BioArt Studio session concluded with a request for a copy of a working Protocol file, so I could “reverse engineer” and configure my Protocol with the correct Labware settings. I installed a local copy of OpenTrons controller app, and was able to edit the script to include available Labware, as well as suppress the Thermal plate as it is not used in this model, and required adjustments to handling of the Z axis. Our next working session will fine-tune and test the Automation & Design protocol.

Protocol Protocol

Update: 4/25/26 - The Protocol file was updated with the reassigned Labware, and was able to run the following design at 0.5uL with success:

Rendering Rendering

Research Paper

I am sharing a link to an essay written by Karen Ingram, that illustrates the influence of automation on BioArt, including OpenTrons Ot-2 renderings.

https://biogeneticblooms.substack.com/p/the-blue-rose-of-metaphor-and-mystery

Be sure to browse the essays and artwork in this collection!

To learn more about the intersection of Biotechnology & Art , I am citing a research topic published by Cambridge Press. https://www.cambridge.org/engage/coe/article-details/660b2e409138d23161e8ebdf?show=item

I am excited about the field of synthetic Bioscience and Art as a result of our recent collaboration. I am grateful for the knowledge sharing and access to the BioLab.


Final Project

My Final project has been positively influenced by this week’s automation activity, as it validates that I can strive to achieve some specific lab results using the automated OpenTrons OT-2 as a tool in the process.

The path I will take for my final project starts with the identification of a Protein that can be synthesized to ensure my work is based on biotechnology best practices. The use of TWIST as a provider of automated creation of a Plasmid is the 1st step in the automation workflow.

Once I have a product, I expect to use the OpenTrons automation platform to construct a series of experiments in a host medium that will Grow into Art.

I plan on 3D printing supporting assemblies that will allow me to grow a photographic “film negative” plate, which could be a modified petrie dish that acts as a film back on a customized camera body and lighting rig.

I plan on creating a unique “exposure calibration” plate that will assist in lab test cases.

My long-range goal is to achieve a sustainable, repeatable solution that leverages automation and can scale up based on future demand for a BioPhoto “Lab” experience. I believe we are at pivotal moment in science and automation similar to when George Eastman revolutionized the photography industry through film and camera development for mass consumption. Many other industrial design solutions surround this theme.

My Final Project will reflect (and develop) artifacts of biotechnology and photography.

final project final project

Checklist:

  • Review this week’s recitation and this week’s lab for details on the Opentrons and programming it.
  • Generate an artistic design using the GUI at https://opentrons-art.rcdonovan.com
  • Write your own Python script which draws your design using the Opentrons.
  • If you use AI to help complete this homework or lab, document how you used AI and which models made contributions.
  • Sign up for a robot time slot if you are at MIT/Harvard/Wellesley or at a Node offering Opentrons automation.(Alt:MakerspaceCharlotte)
  • Find and describe a published paper that utilizes the Opentrons or an automation tool to achieve novel biological applications.
  • Write a description about what you intend to do with automation tools for your final project.
  • Final Project Ideas - Submit one slide to Node

Appendix - Python Code

from opentrons import types

metadata = {
    'author': 'Eric Schneider',
    'protocolName': 'Rasterizr',
    'description': 'SVG to OT',
    'source': 'HTGAA 2026 Opentrons Lab',
    'apiLevel': '2.20'
    # 2.7
}

##############################################################################
###   Robot deck setup constants - don't change these
##############################################################################
#original HTGAA: 
#TIP_RACK_DECK_SLOT = 9 #HTGAA
#COLORS_DECK_SLOT = 6 #HTGAA
#AGAR_DECK_SLOT = 5 #HTGAA
#PIPETTE_STARTING_TIP_WELL = 'A1'

#Makerspace Charlotte: 
TIP_RACK_DECK_SLOT = 6 #MSC
COLORS_DECK_SLOT = 3 #MSC
AGAR_DECK_SLOT = 1 #MSC
PIPETTE_STARTING_TIP_WELL = 'A1'  # *****TO BE CONFIRMED****

# TO DO: update these colors and wells to match your actual color plate layout
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])   #HTGAA same

    # Modules
    #   temperature_module = protocol.load_module('temperature module gen2', COLORS_DECK_SLOT) #HTGAA temp module only, (not MSC)

    # Temperature Module Plate
    #temperature_plate = temperature_module.load_labware(
       # 'opentrons_96_aluminumblock_generic_pcr_strip_200ul',    #HTGAA
       # 'opentrons_6_tuberack_nest_50ml_conical'
        #'Cold Plate'
   # )

    # Choose where to take the colors from
    #color_plate = temperature_plate


    #new no temperature module that adds Z height issue
    color_plate = protocol.load_labware(
    'opentrons_6_tuberack_nest_50ml_conical', COLORS_DECK_SLOT)

    # Agar Plate
    # agar_plate = protocol.load_labware('htgaa_agar_plate', AGAR_DECK_SLOT, 'Agar Plate'). #HTGAA
    #Makerspace Charlotte CUSTOM AGAR PLATE 3D PRINTED WITH PETRIE DISH HOLDER
    agar_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', AGAR_DECK_SLOT, 'Agar 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.
        """
        assert(isinstance(volume, (int, float)))
        #above_location = location.move(types.Point(z=location.point.z + 5)) #original HTGAA
        above_location = location.move(types.Point(z=5))
        pipette.move_to(above_location)
        pipette.dispense(volume, location)
        pipette.move_to(above_location)

    ###
    ### YOUR CODE HERE to create your design
    ###

    ## reminder set Z
    agar_plate.set_offset(x=0.00, y=0.00, z=0.00)

    # start by picking up tip
    pipette_20ul.pick_up_tip()

    # PASTE a list of Current Coordinates (will be dynamic load once integrated or automated)
    currentCoords = [
        [-6.1, 26.8], [-7.9, 25.7], [-8.6, 23.8], [-9.9, 22.6], [-11.3, 21.5],
        [-12.1, 19.7], [-14.2, 19.7], [-15.3, 21.4], [-17.2, 22.3], [-19.3, 22],
        [-20.8, 20.7], [-21.4, 18.7], [-20.8, 16.7], [-19.2, 15.4], [-17.2, 15.1],
        [-15.3, 16.1], [-14.2, 17.8], [-12.1, 17.8], [-11.3, 15.9], [-9.9, 14.8],
        [-8.6, 13.6], [-7.3, 12.3], [-5.9, 11.1], [-6.4, 9.6], [-8.5, 9.6],
        [-10.6, 9.5], [-12.1, 8.1], [-12.3, 6], [-13.3, 4.9], [-15.4, 4.4],
        [-17.3, 3.6], [-19.1, 2.5], [-20.6, 1], [-21.8, -0.7], [-22.7, -2.6],
        [-23.4, -4.6], [-23.8, -6.6], [-24.1, -8.7], [-24.6, -10.6], [-26, -12.1],
        [-26.8, -14.1], [-26.7, -16.2], [-25.9, -18.1], [-24.4, -19.5], [-23.4, -18.3],
        [-24.7, -16.6], [-25, -14.6], [-24.2, -12.7], [-22.5, -11.6], [-20.4, -11.5],
        [-18.7, -12.7], [-17.8, -14.6], [-18.1, -16.6], [-19.4, -18.2], [-18.5, -19.5],
        [-16.9, -18.1], [-16.1, -16.2], [-16.1, -14.1], [-16.8, -12.2], [-18.2, -10.6],
        [-18.6, -8.8], [-18.3, -6.7], [-17.7, -4.7], [-16.7, -2.9], [-15.1, -1.5],
        [-13.2, -0.6], [-12.3, -1.9], [-12.3, -4], [-12.3, -6.1], [-12.1, -8.2],
        [-10.6, -9.4], [-11.2, -11.3], [-13.1, -11.9], [-13.2, -14], [-13.2, -16.1],
        [-12.4, -17.7], [-10.5, -18.5], [-11.2, -20.5], [-13.1, -21.1], [-14.1, -22.9],
        [-14, -25], [-12.7, -26.6], [-10.7, -26.9], [-8.6, -26.9], [-6.5, -26.9],
        [-4.4, -26.9], [-2.3, -26.8], [-2.3, -24.7], [-2.3, -22.6], [-2.3, -20.5],
        [-2.3, -18.4], [-2.3, -16.3], [-2.3, -14.2], [-2.3, -12.1], [-2.3, -10],
        [-0.7, -9.5], [1.4, -9.5], [2.3, -11.3], [2.3, -13.4], [2.3, -15.5],
        [2.3, -17.6], [2.3, -19.7], [2.3, -21.8], [2.3, -23.9], [2.3, -26],
        [4, -26.9], [6.1, -26.9], [8.2, -26.9], [10.3, -26.9], [12.4, -26.7],
        [13.9, -25.3], [14.1, -23.2], [13.4, -21.3], [11.5, -20.5], [10.5, -19.3],
        [11, -17.7], [13, -17.4], [13.2, -15.4], [13.2, -13.3], [12.6, -11.4],
        [10.5, -11.3], [10.8, -9.4], [12.2, -7.9], [12.3, -5.8], [12.3, -3.7],
        [12.3, -1.6], [13.4, -0.7], [15.3, -1.6], [16.8, -3], [17.8, -4.9],
        [18.4, -6.9], [18.6, -9], [18, -10.8], [16.7, -12.4], [16, -14.3],
        [16.2, -16.4], [17.1, -18.3], [18.7, -19.6], [19.3, -18.1], [18, -16.4],
        [17.9, -14.3], [18.8, -12.5], [20.6, -11.5], [22.7, -11.6], [24.3, -12.9],
        [25.1, -14.8], [24.6, -16.8], [23.3, -18.5], [24.6, -19.4], [26.1, -17.9],
        [26.8, -15.9], [26.7, -13.8], [25.9, -11.9], [24.4, -10.5], [24.1, -8.5],
        [23.8, -6.4], [23.3, -4.3], [22.6, -2.4], [21.7, -0.5], [20.4, 1.2],
        [18.9, 2.6], [17.1, 3.7], [15.1, 4.5], [13.1, 4.9], [12.3, 6.8],
        [11.7, 8.8], [9.9, 9.5], [7.8, 9.6], [5.9, 10.3], [6, 12.3],
        [8.1, 12.3], [8.6, 13.9], [10.1, 14.9], [11.3, 16.1], [11.8, 17.8],
        [13.9, 17.8], [15.1, 16.3], [16.9, 15.2], [18.9, 15.2], [20.6, 16.5],
        [21.4, 18.4], [21, 20.4], [19.5, 21.9], [17.5, 22.3], [15.6, 21.6],
        [14.3, 19.9], [12.4, 19.7], [11.3, 20.7], [10.7, 22.3], [8.7, 23],
        [8.3, 25], [6.9, 26.5], [4.9, 26.9], [2.8, 26.9], [0.7, 26.9],
        [-1.4, 26.9], [-3.5, 26.9], [-5.6, 26.8], [-2.6, 23], [-3.1, 21.4],
        [-4, 22.8], [3.8, 23], [3.3, 21.5], [2.5, 22.8], [-8.6, 18.7],
        [-8.7, 16.7], [-9.6, 17.5], [-9.6, 19.5], [-8.9, 21], [-8.6, 19.1],
        [9.2, 20.9], [9.6, 19.1], [9.6, 17.1], [8.6, 17.1], [8.6, 19.1],
        [8.9, 21], [-2.5, 19.3], [-1.8, 17.5], [0.1, 16.9], [1.9, 17.6],
        [2.7, 19.4], [4.1, 18.7], [3.5, 16.8], [2, 15.5], [0, 15.1],
        [-1.9, 15.5], [-3.5, 16.7], [-4.1, 18.6], [-2.7, 19.5], [4.1, 10.9],
        [3.4, 9.6], [1.4, 9.6], [-0.6, 9.6], [-2.6, 9.6], [-4.1, 10.1],
        [-4.1, 12.1], [-2.2, 12.3], [-0.2, 12.3], [1.8, 12.3], [3.8, 12.3],
        [-4, 4.9], [-2.2, 4], [-0.9, 2.4], [-0.5, 0.5], [-0.9, -1.5],
        [-2.2, -3.1], [-4, -4], [-6, -4], [-7.8, -3.1], [-9.1, -1.5],
        [-9.6, 0.4], [-9.1, 2.4], [-7.9, 4], [-6.1, 4.9], [-4.1, 4.9],
        [9.3, 4.7], [8.8, 3.2], [6.8, 3.2], [4.8, 3.2], [2.8, 3.3],
        [2.8, 4.9], [4.8, 5], [6.8, 5], [8.8, 5], [9.1, 1.3],
        [9.1, -0.4], [7.1, -0.4], [5, -0.4], [3, -0.4], [1.4, 0.3],
        [2.7, 1.4], [4.7, 1.4], [6.7, 1.4], [8.7, 1.3], [9.3, -2.6],
        [8.7, -4], [6.7, -4.1], [4.7, -4.1], [2.7, -4.1], [0.8, -3.9],
        [1.2, -2.3], [3.2, -2.3], [5.2, -2.3], [7.2, -2.3], [9.2, -2.4],
        [-7.8, -14.6], [-8.4, -16], [-10.4, -16], [-11.3, -14.9], [-11, -13.2],
        [-9, -13.2], [-7.8, -14.1], [11.3, -14.6], [10.7, -16], [8.7, -16],
        [7.8, -14.9], [8.2, -13.2], [10.2, -13.2], [11.3, -14.1], [-6, 3],
        [-7.4, 1.7], [-7.6, -0.3], [-6.5, -1.8], [-4.5, -2.2], [-2.9, -1.2],
        [-2.3, 0.7], [-3.2, 2.4], [-5, 3.2]
    ]

    batch_size = 20
    total = 0

    for i in range(0, len(currentCoords), batch_size):
        batch = currentCoords[i: i + batch_size]
        coordCount = len(batch)

        print(f"\nBatch {i//batch_size + 1}: aspirating {coordCount} units")
        pipette_20ul.aspirate(coordCount, location_of_color('Green'))

        for x, y in batch:
            adjusted_location = center_location.move(types.Point(x, y))
            dispense_and_detach(pipette_20ul, 1, adjusted_location)
            total += 1
            print(f"  Dispensed at ({x}, {y}) — running total: {total}")

    print(f"\nTotal objects processed: {total}")

    pipette_20ul.drop_tip()