from opentrons import types
import subprocess, sys
import numpy as np
metadata = { # see https://docs.opentrons.com/v2/tutorial.html#tutorial-metadata
'author': 'Vivien',
'protocolName': 'Gray-Scott Reaction-Diffusion Pattern',
'description': 'Generates a reaction-diffusion pattern on an agar plate using the Gray-Scott model, pipetting where the B concentration is high.',
'source': 'HTGAA 2026 Opentrons Lab',
'apiLevel': '2.20'
}
##############################################################################
### Gray-Scott Model Global Parameters and Functions
##############################################################################
# Gray-Scott Model Parameters
GS_DA = 1.0
GS_DB = 0.5
GS_F = 0.014 # Feed rate. Experiment with values like 0.035 (spots), 0.014 (worms), 0.062 (moving spots)
GS_K = 0.045 # Kill rate. Experiment with values like 0.065 (spots), 0.045 (worms), 0.061 (moving spots)
GS_DT = 1.0
#1. Mitosis / spots : f=0.0367, k=0.0649
#2. Solitions / worms : f=0.030, k=0.062
#3. Coral / dense : f=0.0545, k=0.062
#4. Vibe : f = 0.029, K = 0.057
#5. labyrythn : f = 0.060, K = 0.063
def laplace(Z):
"""Weighted 3x3 Laplacian (Karl Sims style) using numpy.roll."""
return (
-1.0 * Z +
0.2 * (np.roll(Z, 1, axis=0) + np.roll(Z, -1, axis=0) +
np.roll(Z, 1, axis=1) + np.roll(Z, -1, axis=1)) +
0.05 * (np.roll(np.roll(Z, 1, axis=0), 1, axis=1) +
np.roll(np.roll(Z, 1, axis=0), -1, axis=1) +
np.roll(np.roll(Z, -1, axis=0), 1, axis=1) +
np.roll(np.roll(Z, -1, axis=0), -1, axis=1))
)
def simulate_gray_scott(A, B, num_iterations):
"""
Runs the Gray-Scott simulation for a given number of iterations.
Modifies A and B arrays in place.
"""
for _ in range(num_iterations):
lapA = laplace(A)
lapB = laplace(B)
reaction = A * B * B
A += (GS_DA * lapA - reaction + GS_F * (1 - A)) * GS_DT
B += (GS_DB * lapB + reaction - (GS_K + GS_F) * B) * GS_DT
return A, B # Return A and B for clarity, though they are modified in-place
##############################################################################
### 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 (Gray-Scott Reaction-Diffusion Model)
###
# Grid size for simulation
size = 100 # Reduced size to make simulation faster and denser pattern on plate
A = np.ones((size, size))
B = np.zeros((size, size))
# Create a central square seed for B
r = 5
A[size//2-r:size//2+r, size//2-r:size//2+r] = 0.5
B[size//2-r:size//2+r, size//2-r:size//2+r] = 0.25
# Add small random noise to the entire grid to introduce variation
# This helps break symmetry and encourages diverse pattern formation
A += np.random.rand(size, size) * 0.05 # Add noise up to 0.05
B += np.random.rand(size, size) * 0.05 # Add noise up to 0.05
B_min = float(B.min()); B_max = float(B.max());
B_mean = float(B.mean())
print("B stats:", B_min, B_mean, B_max)
# Ensure values remain within bounds [0, 1] after adding noise
A = np.clip(A, 0.0, 1.0)
B = np.clip(B, 0.0, 1.0)
print("Running Gray-Scott simulation...")
# Define the number of simulation iterations here
SIMULATION_ITERATIONS = 8000 # Initial run for 400 iterations
A, B = simulate_gray_scott(A, B, SIMULATION_ITERATIONS)
print(f"Gray-Scott simulation complete after {SIMULATION_ITERATIONS} iterations. Starting patterning.")
print(f"To run more iterations, change the 'SIMULATION_ITERATIONS' variable in the code and re-run this cell and the visualization cell below.")
# Patterning Parameters for Opentrons
PIPETTE_VOLUME = 1 # 1uL per dot
ASPIRATE_VOLUME = 16 # Aspirate up to this much at a time
MAX_DOTS_PER_COLOR = 300 # Maximum number of dots to dispense per color
# Colors to use for B components
COLOR_FOR_PRIMARY_PATTERN = 'Green' # For the 'thick black lines' pattern
COLOR_FOR_SECONDARY_PATTERN = 'Orange' # For other B concentrations
COLOR_FOR_TERTIARY_PATTERN = 'Red' # For the highest B concentrations
# Thresholds for B values
# For 'thick black lines' (Green color)
PRIMARY_PATTERN_B_LOWER_THRESHOLD = 0.40 * B_max
PRIMARY_PATTERN_B_UPPER_THRESHOLD = 0.70 * B_max # This value will also define the lower bound for Red
# For the secondary pattern (Orange color)
# This will catch 'B' values that are above this, but not in the primary or tertiary pattern bands
SECONDARY_PATTERN_B_THRESHOLD = 0.20 * B_max # Adjust this percentage as needed
print("Using Red Pattern (Highest B) threshold: B >=", PRIMARY_PATTERN_B_UPPER_THRESHOLD)
print("Using Green Pattern (Mid B - 'thick lines') B thresholds: (", PRIMARY_PATTERN_B_LOWER_THRESHOLD, ",", PRIMARY_PATTERN_B_UPPER_THRESHOLD, ")")
print("Using Orange Pattern (Lower B) threshold: (", SECONDARY_PATTERN_B_THRESHOLD, ",", PRIMARY_PATTERN_B_LOWER_THRESHOLD, "]")
# Agar plate dimensions (estimated for a standard 90mm round agar plate,
# patterning in a 70x70mm square area roughly) to fit the pattern
PATTERN_AREA_WIDTH_MM = 55 # Reduced from 85mm to fit within 40mm radius safe area
PATTERN_AREA_HEIGHT_MM = 55 # Reduced from 85mm to fit within 40mm radius safe area
# Calculate scaling factor to map grid coordinates to millimeters on the plate
scale_x = PATTERN_AREA_WIDTH_MM / size
scale_y = PATTERN_AREA_HEIGHT_MM / size
# Add sampling step for clearer dots
SAMPLING_STEP = 4 # Pipette every Nth pixel to create distinct dots
DOT_SPACING_MM = SAMPLING_STEP * scale_x # Actual physical spacing between dot centers
print(f"Desired dot spacing for distinct patterns: {DOT_SPACING_MM:.2f} mm (approx. 2mm drop diameter).")
pipetted_count_primary = 0 # Green
pipetted_count_secondary = 0 # Orange
pipetted_count_tertiary = 0 # Red
# --- Pipetting for Tertiary Pattern (Red component - highest B concentration) ---
pipette_20ul.pick_up_tip()
pipette_20ul.aspirate(ASPIRATE_VOLUME, location_of_color(COLOR_FOR_TERTIARY_PATTERN))
current_pipette_volume = ASPIRATE_VOLUME
print(f"Starting patterning for Tertiary Pattern with {COLOR_FOR_TERTIARY_PATTERN}.")
for y in range(0, size, SAMPLING_STEP):
for x in range(0, size, SAMPLING_STEP):
if B[y, x] >= PRIMARY_PATTERN_B_UPPER_THRESHOLD:
if current_pipette_volume < PIPETTE_VOLUME:
pipette_20ul.aspirate(ASPIRATE_VOLUME, location_of_color(COLOR_FOR_TERTIARY_PATTERN))
current_pipette_volume += ASPIRATE_VOLUME
x_offset_mm = (x - size / 2) * scale_x
y_offset_mm = (y - size / 2) * scale_y
adjusted_location = center_location.move(types.Point(x=x_offset_mm, y=y_offset_mm))
dispense_and_detach(pipette_20ul, PIPETTE_VOLUME, adjusted_location)
current_pipette_volume -= PIPETTE_VOLUME
pipetted_count_tertiary += 1
if pipetted_count_tertiary >= MAX_DOTS_PER_COLOR:
break
if pipetted_count_tertiary >= MAX_DOTS_PER_COLOR:
break
pipette_20ul.drop_tip()
print(f"Total {pipetted_count_tertiary} drops pipetted for Tertiary Pattern using {COLOR_FOR_TERTIARY_PATTERN}.")
# --- Pipetting for Primary Pattern (Green component - 'thick lines') ---
pipette_20ul.pick_up_tip()
pipette_20ul.aspirate(ASPIRATE_VOLUME, location_of_color(COLOR_FOR_PRIMARY_PATTERN))
current_pipette_volume = ASPIRATE_VOLUME
print(f"Starting patterning for Primary Pattern with {COLOR_FOR_PRIMARY_PATTERN}.")
for y in range(0, size, SAMPLING_STEP):
for x in range(0, size, SAMPLING_STEP):
if (B[y, x] > PRIMARY_PATTERN_B_LOWER_THRESHOLD) and (B[y, x] < PRIMARY_PATTERN_B_UPPER_THRESHOLD):
if current_pipette_volume < PIPETTE_VOLUME:
pipette_20ul.aspirate(ASPIRATE_VOLUME, location_of_color(COLOR_FOR_PRIMARY_PATTERN))
current_pipette_volume += ASPIRATE_VOLUME
x_offset_mm = (x - size / 2) * scale_x
y_offset_mm = (y - size / 2) * scale_y
adjusted_location = center_location.move(types.Point(x=x_offset_mm, y=y_offset_mm))
dispense_and_detach(pipette_20ul, PIPETTE_VOLUME, adjusted_location)
current_pipette_volume -= PIPETTE_VOLUME
pipetted_count_primary += 1
if pipetted_count_primary >= MAX_DOTS_PER_COLOR:
break
if pipetted_count_primary >= MAX_DOTS_PER_COLOR:
break
pipette_20ul.drop_tip()
print(f"Total {pipetted_count_primary} drops pipetted for Primary Pattern using {COLOR_FOR_PRIMARY_PATTERN}.")
# --- Pipetting for Secondary Pattern (Orange component) ---
pipette_20ul.pick_up_tip()
pipette_20ul.aspirate(ASPIRATE_VOLUME, location_of_color(COLOR_FOR_SECONDARY_PATTERN))
current_pipette_volume = ASPIRATE_VOLUME
print(f"Starting patterning for Secondary Pattern with {COLOR_FOR_SECONDARY_PATTERN}.")
for y in range(0, size, SAMPLING_STEP):
for x in range(0, size, SAMPLING_STEP):
# Dispense Orange if B is above its threshold, AND NOT in the Green or Red band
if (B[y, x] > SECONDARY_PATTERN_B_THRESHOLD) and (B[y, x] <= PRIMARY_PATTERN_B_LOWER_THRESHOLD):
if current_pipette_volume < PIPETTE_VOLUME:
pipette_20ul.aspirate(ASPIRATE_VOLUME, location_of_color(COLOR_FOR_SECONDARY_PATTERN))
current_pipette_volume += ASPIRATE_VOLUME
x_offset_mm = (x - size / 2) * scale_x
y_offset_mm = (y - size / 2) * scale_y
adjusted_location = center_location.move(types.Point(x=x_offset_mm, y=y_offset_mm))
dispense_and_detach(pipette_20ul, PIPETTE_VOLUME, adjusted_location)
current_pipette_volume -= PIPETTE_VOLUME
pipetted_count_secondary += 1
if pipetted_count_secondary >= MAX_DOTS_PER_COLOR:
break
if pipetted_count_secondary >= MAX_DOTS_PER_COLOR:
break
pipette_20ul.drop_tip()
print(f"Total {pipetted_count_secondary} drops pipetted for Secondary Pattern using {COLOR_FOR_SECONDARY_PATTERN}.")