config.trash_control = True stock_volume = 630 ######################################################################################################################## # Labware ######################################################################################################################## # Configure the tips tips10 = labware_manager.load('opentrons_96_tiprack_10ul', 1, label='tips10') tips300a = labware_manager.load('opentrons_96_tiprack_300ul', 4, label='tips300a') # Configure the pipettes. p10 = instruments_manager.P10_Single(mount='left', tip_racks=[tips10]) p50 = instruments_manager.P50_Single(mount='right', tip_racks=[tips300a]) # Control tip usage p10.start_at_tip(tips10[p10_start_tip]) p50.start_at_tip(tips300a[p50_start_tip]) # All the labware containers eppendorf_1_5_rack = labware_manager.load( 'opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 5, label='eppendorf_1_5_rack') plateA = labware_manager.load('biorad_96_wellplate_200ul_pcr', 6, label='plateA') plateB = labware_manager.load('biorad_96_wellplate_200ul_pcr',
def run(protocol_context: protocol_api.ProtocolContext): config.protocol_context = protocol_context #################################################################################################################### ## Well volumes #################################################################################################################### strand_volume_min = 0 strand_volume_max = 28 strand_a_volume_count = 6 strand_b_volume_count = 5 # historic : strand_volumes = [0, 2, 5, 8, 12, 16, 20, 28] strand_a_volumes = list( float_range(strand_volume_min, strand_volume_max, strand_a_volume_count)) strand_b_volumes = list( float_range(strand_volume_min, strand_volume_max, strand_b_volume_count)) replica_groups = list( pair_up_strand_volumes(strand_a_volumes, strand_b_volumes)) replica_groups.append( make_pair(mean(strand_a_volumes[-2], strand_a_volumes[-3]), mean(strand_b_volumes[-2], strand_b_volumes[-3]))) replica_groups.append( make_pair(mean(strand_a_volumes[-1], strand_a_volumes[-2]), mean(strand_b_volumes[-1], strand_b_volumes[-2]))) #################################################################################################################### ## Accounting #################################################################################################################### num_columns = 12 num_rows = 8 num_wells = num_rows * num_columns num_replicates = 3 num_replica_groups = num_wells // num_replicates num_replica_groups_per_row = num_columns // num_replicates max_dna_working_concentration = Concentration(0.5, "uM") nominal_source_strand_concentration = Concentration(10, "uM") buffer_source_concentration = Concentration(5, "x") evagreen_source_concentration = Concentration( 25, "uM") # per https://biotium.com/product/evagreen-dye-20x-in-water/ evagreen_target_concentration = max_dna_working_concentration * 2 # we figure we should always have more evagreen than reagents total_well_volume = 84 buffer_per_well = total_well_volume / buffer_source_concentration.x evagreen_per_well = total_well_volume / evagreen_source_concentration.molar * evagreen_target_concentration.molar dna_and_water_per_well = total_well_volume - buffer_per_well - evagreen_per_well assert len(replica_groups) == num_replica_groups #################################################################################################################### ## Strand Dilution #################################################################################################################### # Diluting each strand strand_dilution_vol = 1200 strand_dilution_factor = strand_volume_max / total_well_volume * nominal_source_strand_concentration.molar / max_dna_working_concentration.molar strand_dilution_source_vol = strand_dilution_vol / strand_dilution_factor strand_dilution_water_vol = strand_dilution_vol - strand_dilution_source_vol #################################################################################################################### ## Plating Volumes #################################################################################################################### # Figure out the strand a, strand b, and per-well-water volumes def make_empty_plate(): row = [0] * num_columns result = [] for i in range(num_rows): result.append(row.copy()) return result strand_a_plate = make_empty_plate() strand_b_plate = make_empty_plate() for i, replica_group in enumerate(replica_groups): row = i // num_replica_groups_per_row col_first = (i % num_replica_groups_per_row) * num_replicates for j in range(num_replicates): col = col_first + j strand_a_plate[row][col] = replica_group['StrandA'] strand_b_plate[row][col] = replica_group['StrandB'] total_water_plate = make_empty_plate() common_water_per_well = infinity for row in range(num_rows): for col in range(num_columns): total_water_plate[row][ col] = total_well_volume - buffer_per_well - evagreen_per_well - strand_a_plate[ row][col] - strand_b_plate[row][col] common_water_per_well = min(common_water_per_well, total_water_plate[row][col]) master_mix_per_well = buffer_per_well + evagreen_per_well + common_water_per_well master_mix_plate = make_empty_plate() for row in range(num_rows): for col in range(num_columns): master_mix_plate[row][col] = master_mix_per_well per_well_water_plate = make_empty_plate() for row in range(num_rows): for col in range(num_columns): per_well_water_plate[row][ col] = dna_and_water_per_well - common_water_per_well - strand_a_plate[ row][col] - strand_b_plate[row][col] #################################################################################################################### ## Master Mix #################################################################################################################### master_mix_buffer_nominal_vol = num_wells * buffer_per_well master_mix_evagreen_nominal_vol = num_wells * evagreen_per_well mm_overhead_factor = 1.1 master_mix_buffer_vol = master_mix_buffer_nominal_vol * mm_overhead_factor master_mix_evagreen_vol = master_mix_evagreen_nominal_vol * mm_overhead_factor master_mix_common_water_vol = num_wells * common_water_per_well * mm_overhead_factor master_mix_vol = master_mix_buffer_vol + master_mix_evagreen_vol + master_mix_common_water_vol #################################################################################################################### # Labware #################################################################################################################### # Configure the tips tips300a = labware_manager.load('opentrons_96_tiprack_300ul', slot=1, label='tips300a') tips10 = labware_manager.load('opentrons_96_tiprack_10ul', slot=4, label='tips10') tips300b = labware_manager.load('opentrons_96_tiprack_300ul', slot=7, label='tips300b') # Configure the pipettes. p10 = instruments_manager.P10_Single(mount='left', tip_racks=[tips10]) p50 = instruments_manager.P50_Single(mount='right', tip_racks=[tips300a, tips300b]) # Blow out faster than default in an attempt to avoid hanging droplets on the pipettes after blowout (probably not needed any more) p10.set_flow_rate(blow_out=p10.get_flow_rates()['blow_out'] * config.blow_out_rate_factor) p50.set_flow_rate(blow_out=p50.get_flow_rates()['blow_out'] * config.blow_out_rate_factor) # Control tip usage p10.start_at_tip(tips10[p10_start_tip]) p50.start_at_tip(tips300a[p50_start_tip]) # Labware containers eppendorf_1_5_rack = labware_manager.load( 'opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', slot=5, label='eppendorf_1_5_rack') plate = labware_manager.load('biorad_96_wellplate_200ul_pcr', slot=6, label='plate') # Name specific places in the labware containers diluted_strand_a = eppendorf_1_5_rack['A6'] diluted_strand_b = eppendorf_1_5_rack['B6'] eppendorf_5_0_rack = labware_manager.load( 'Atkinson_15_tuberack_5ml_eppendorf', slot=8, label='eppendorf_5_0_rack') master_mix = eppendorf_5_0_rack['A1'] waterA = eppendorf_5_0_rack['C4'] waterB = eppendorf_5_0_rack['C5'] note_liquid(location=waterA, name='Water', initially=waterA_initial_volume) note_liquid(location=waterB, name='Water', initially=waterB_initial_volume) #################################################################################################################### # Well & Pipettes #################################################################################################################### # Figuring out what pipettes should pipette what volumes p10_max_vol = 10 p50_min_vol = 5 def usesP10(volume, allow_zero=False): return (allow_zero or 0 < volume) and (volume < p50_min_vol and volume <= p10_max_vol) #################################################################################################################### # Making master mix and diluting strands #################################################################################################################### def diluteStrands(): if manually_dilute_strands: note_liquid(location=diluted_strand_a, name='Diluted StrandA', initially=strand_dilution_vol) note_liquid(location=diluted_strand_b, name='Diluted StrandB', initially=strand_dilution_vol) log('Diluting Strands') info( pretty.format( 'Diluted Strand A recipe: water={0:n} strandA={1:n} vol={2:n}', strand_dilution_water_vol, strand_dilution_source_vol, strand_dilution_vol)) info( pretty.format( 'Diluted Strand B recipe: water={0:n} strandB={1:n} vol={2:n}', strand_dilution_water_vol, strand_dilution_source_vol, strand_dilution_vol)) user_prompt('Ensure diluted strands manually present and mixed') else: strand_a = eppendorf_1_5_rack['A1'] strand_b = eppendorf_1_5_rack['B1'] assert strand_a_min_vol >= strand_dilution_source_vol + strand_a.geometry.min_aspiratable_volume assert strand_b_min_vol >= strand_dilution_source_vol + strand_b.geometry.min_aspiratable_volume note_liquid(location=strand_a, name='StrandA', concentration=strand_a_conc, initially_at_least=strand_a_min_vol ) # i.e.: we have enough, just not specified how much note_liquid(location=strand_b, name='StrandB', concentration=strand_b_conc, initially_at_least=strand_b_min_vol) # ditto note_liquid(location=diluted_strand_a, name='Diluted StrandA') note_liquid(location=diluted_strand_b, name='Diluted StrandB') # We used to auto-mix, but now, even when auto-diluting, we rely on user to have mixed on the vortexer # p50.layered_mix([strand_a]) # p50.layered_mix([strand_b]) # Create dilutions of strands log('Moving water for diluting Strands A and B') p50.transfer( strand_dilution_water_vol, waterA, [diluted_strand_a, diluted_strand_b], new_tip= 'once', # can reuse for all diluent dispensing since dest tubes are initially empty trash=config.trash_control) log('Diluting Strand A') p50.transfer(strand_dilution_source_vol, strand_a, diluted_strand_a, trash=config.trash_control, keep_last_tip=True) p50.layered_mix([diluted_strand_a]) log('Diluting Strand B') p50.transfer(strand_dilution_source_vol, strand_b, diluted_strand_b, trash=config.trash_control, keep_last_tip=True) p50.layered_mix([diluted_strand_b]) def createMasterMix(): if manually_make_master_mix: note_liquid(location=master_mix, name='Master Mix', initially=master_mix_vol) log('Creating Master Mix') info( pretty.format( 'Master Mix recipe: water={0:n} buffer={1:n} EvaGreen={2:n} total={3:n} (extra={4}%)', master_mix_common_water_vol, master_mix_buffer_vol, master_mix_evagreen_vol, master_mix_vol, 100.0 * (mm_overhead_factor - 1))) user_prompt('Ensure master mix manually present and mixed') else: # Mostly just for fun, we put the ingredients for the master mix in a nice warm place to help them melt temp_slot = 11 temp_module = modules_manager.load('tempdeck', slot=temp_slot) screwcap_rack = labware_manager.load( 'opentrons_24_aluminumblock_generic_2ml_screwcap', slot=temp_slot, label='screwcap_rack', share=True, well_geometry=IdtTubeWellGeometry) buffers = list(zip(screwcap_rack.rows(0), buffer_volumes)) evagreens = list(zip(screwcap_rack.rows(1), evagreen_volumes)) for buffer in buffers: note_liquid(location=buffer[0], name='Buffer', initially=buffer[1], concentration=buffer_source_concentration) for evagreen in evagreens: note_liquid(location=evagreen[0], name='Evagreen', initially=evagreen[1], concentration=evagreen_source_concentration) note_liquid(location=master_mix, name='Master Mix') # Buffer was just unfrozen. Mix to ensure uniformity. EvaGreen doesn't freeze, no need to mix p50.layered_mix([buffer for buffer, __ in buffers], incr=2) # transfer from multiple source wells, each with a current defined volume def transfer_multiple(msg, xfer_vol_remaining, tubes, dest, new_tip, *args, **kwargs): tube_index = 0 cur_well = None cur_vol = 0 min_vol = 0 while xfer_vol_remaining > 0: if xfer_vol_remaining < p50_min_vol: warn( "remaining transfer volume of %f too small; ignored" % xfer_vol_remaining) return # advance to next tube if there's not enough in this tube while cur_well is None or cur_vol <= min_vol: if tube_index >= len(tubes): fatal('%s: more reagent needed' % msg) cur_well = tubes[tube_index][0] cur_vol = tubes[tube_index][1] min_vol = max( p50_min_vol, cur_vol / config. min_aspirate_factor_hack, # tolerance is proportional to specification of volume. can probably make better guess cur_well.geometry.min_aspiratable_volume) tube_index = tube_index + 1 this_vol = min(xfer_vol_remaining, cur_vol - min_vol) assert this_vol >= p50_min_vol # TODO: is this always the case? log('%s: xfer %f from %s in %s to %s in %s' % (msg, this_vol, cur_well, cur_well.parent, dest, dest.parent)) p50.transfer(this_vol, cur_well, dest, trash=config.trash_control, new_tip=new_tip, **kwargs) xfer_vol_remaining -= this_vol cur_vol -= this_vol def mix_master_mix(): log('Mixing Master Mix') p50.layered_mix( [master_mix], incr=2, initial_turnover=master_mix_evagreen_vol * 1.2, max_tip_cycles=config.layered_mix.max_tip_cycles_large) log('Creating Master Mix: Water') p50.transfer(master_mix_common_water_vol, waterB, master_mix, trash=config.trash_control) log('Creating Master Mix: Buffer') transfer_multiple( 'Creating Master Mix: Buffer', master_mix_buffer_vol, buffers, master_mix, new_tip='once', keep_last_tip=True ) # 'once' because we've only got water & buffer in context p50.done_tip() # EvaGreen needs a new tip log('Creating Master Mix: EvaGreen') transfer_multiple( 'Creating Master Mix: EvaGreen', master_mix_evagreen_vol, evagreens, master_mix, new_tip='always', keep_last_tip=True ) # 'always' to avoid contaminating the Evagreen source w/ buffer mix_master_mix() ######################################################################################################################## # Plating ######################################################################################################################## def plateMasterMix(): log('Plating Master Mix') for row in range(num_rows): for col in range(num_columns): volume = master_mix_plate[row][col] if volume == 0: continue p: EnhancedPipetteV1 = p10 if usesP10(volume) else p50 if not p.tip_attached: p.pick_up_tip() well = plate.rows(row).wells(col) p.transfer(volume, master_mix, well, new_tip='never', trash=config.trash_control, full_dispense=True) p10.done_tip() p50.done_tip() def platePerWellWater(): log('Plating per-well water') # Plate per-well water. We save tips by being happy to pollute our water trough with a bit of master mix. for row in range(num_rows): for col in range(num_columns): volume = per_well_water_plate[row][col] if volume == 0: continue p: EnhancedPipetteV1 = p10 if usesP10(volume) else p50 if not p.tip_attached: p.pick_up_tip() well = plate.rows(row).wells(col) p.transfer(volume, waterB, well, new_tip='never', trash=config.trash_control, full_dispense=True) p10.done_tip() p50.done_tip() def plateStrandA(): # Plate strand A # All plate wells at this point only have water and master mix, so we can't get cross-plate-well # contamination. We only need to worry about contaminating the Strand A source, which we accomplish # by using new_tip='always'. Update: we don't worry about that pollution, that source is disposable. # So we can minimize tip usage. log('Plating Strand A') for row in range(num_rows): for col in range(num_columns): volume = strand_a_plate[row][col] if volume == 0: continue p: EnhancedPipetteV1 = p10 if usesP10(volume) else p50 if not p.tip_attached: p.pick_up_tip() well = plate.rows(row).wells(col) log('Plating Strand A: volume %d with %s' % (volume, p.name)) p.transfer(volume, diluted_strand_a, well, new_tip='never', trash=config.trash_control, full_dispense=True) p10.done_tip() p50.done_tip() def mix_plate_well(well, keep_last_tip=False): p50.layered_mix([well], incr=0.75, keep_last_tip=keep_last_tip) def plateStrandBAndMix(): # Plate strand B and mix # Mixing always needs the p50, but plating may need either; optimize tip usage log('Plating Strand B') mixed_wells = set() for row in range(num_rows): for col in range(num_columns): volume = strand_b_plate[row][col] if volume == 0: continue p: EnhancedPipetteV1 = p10 if usesP10(volume, allow_zero=True) else p50 well = plate.rows(row).wells(col) log("Plating Strand B: well='%s' vol=%d pipette=%s" % (well.get_name(), volume, p.name)) if not p.tip_attached: p.pick_up_tip() p.transfer(volume, diluted_strand_b, well, new_tip='never', trash=config.trash_control, full_dispense=True) if p is p50: mix_plate_well(well, keep_last_tip=True) mixed_wells.add(well) p.done_tip() for well in plate.wells(): if well not in mixed_wells: mix_plate_well(well) def plateEverythingAndMix(): plateMasterMix() platePerWellWater() plateStrandA() plateStrandBAndMix() #################################################################################################################### # Off to the races #################################################################################################################### diluteStrands() createMasterMix() plateEverythingAndMix()