diff --git a/.github/workflows/odd-memory-usage-test.yaml b/.github/workflows/odd-memory-usage-test.yaml index ce79fb4dc32..3302ec0203a 100644 --- a/.github/workflows/odd-memory-usage-test.yaml +++ b/.github/workflows/odd-memory-usage-test.yaml @@ -20,5 +20,5 @@ jobs: with: mixpanel-user: ${{ secrets.MIXPANEL_INGEST_USER }} mixpanel-secret: ${{ secrets.MIXPANEL_INGEST_SECRET }} - mixpanel-project-id: 12345 + mixpanel-project-id: ${{ secrets.OT_APP_MIXPANEL_ID }} previous-version-count: '2' \ No newline at end of file diff --git a/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py b/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py index 79414e13765..75658f11438 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py +++ b/abr-testing/abr_testing/protocols/active_protocols/12_KAPA HyperPlus Library Prep.py @@ -14,7 +14,7 @@ MagneticBlockContext, ThermocyclerContext, ) -from typing import List, Tuple, Optional +from typing import List, Tuple, Dict metadata = { "protocolName": "KAPA HyperPlus Library Preparation", @@ -23,17 +23,6 @@ requirements = {"robotType": "Flex", "apiLevel": "2.21"} -tt_50 = 0 -tt_200 = 0 -p50_rack_count = 0 -p200_rack_count = 0 -tip50 = 50 -tip200 = 200 -p50_racks_ondeck = [] -p200_racks_ondeck = [] -p50_racks_offdeck = [] -p200_racks_offdeck = [] - def add_parameters(parameters: ParameterContext) -> None: """Parameters.""" @@ -56,7 +45,7 @@ def add_parameters(parameters: ParameterContext) -> None: variable_name="num_samples", display_name="number of samples", description="How many samples to be perform for library prep", - default=8, + default=48, minimum=8, maximum=48, ) @@ -73,7 +62,7 @@ def add_parameters(parameters: ParameterContext) -> None: variable_name="Fragmentation_time", display_name="time on thermocycler", description="Fragmentation time in thermocycler", - default=10, + default=30, minimum=10, maximum=30, ) @@ -104,7 +93,7 @@ def run(ctx: ProtocolContext) -> None: num_cols = math.ceil(num_samples / 8) # Pre-set parameters - sample_vol = 35.0 + # sample_vol = 35.0 frag_vol = 15.0 end_repair_vol = 10.0 adapter_vol = 5.0 @@ -112,14 +101,14 @@ def run(ctx: ProtocolContext) -> None: amplification_vol = 30.0 bead_vol_1 = 88.0 bead_vol_2 = 50.0 - bead_vol = bead_vol_1 + bead_vol_2 + # bead_vol = bead_vol_1 + bead_vol_2 bead_inc = 2.0 rsb_vol_1 = 25.0 rsb_vol_2 = 20.0 - rsb_vol = rsb_vol_1 + rsb_vol_2 + # rsb_vol = rsb_vol_1 + rsb_vol_2 elution_vol = 20.0 elution_vol_2 = 17.0 - etoh_vol = 400.0 + # etoh_vol = 400.0 # Importing Labware, Modules and Instruments magblock: MagneticBlockContext = ctx.load_module( @@ -146,63 +135,47 @@ def run(ctx: ProtocolContext) -> None: samples_flp = FLP_plate.rows()[0][:num_cols] sample_plate = ctx.load_labware( - "armadillo_96_wellplate_200ul_pcr_full_skirt", "D1", "Sample Pate" + "armadillo_96_wellplate_200ul_pcr_full_skirt", "D1", "Sample Plate 1" ) sample_plate_2 = ctx.load_labware( - "armadillo_96_wellplate_200ul_pcr_full_skirt", "B2", "Sample Pate" + "armadillo_96_wellplate_200ul_pcr_full_skirt", "B2", "Sample Plate 2" ) samples_2 = sample_plate_2.rows()[0][:num_cols] samples = sample_plate.rows()[0][:num_cols] - reservoir = ctx.load_labware("nest_96_wellplate_2ml_deep", "C2") + reservoir = ctx.load_labware( + "nest_96_wellplate_2ml_deep", "C2", "Beads + Buffer + Ethanol" + ) + # Load tipracks + tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "A3") + tiprack_50_2 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "A2") + + tiprack_200_1 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "C1") + tiprack_200_2 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "C3") + + if trash_tips: + ctx.load_waste_chute() - trash = ctx.load_waste_chute() unused_lids: List[Labware] = [] # Load TC Lids if disposable_lid: - unused_lids = helpers.load_disposable_lids(ctx, 5, ["C3"], deck_riser) + unused_lids = helpers.load_disposable_lids(ctx, 5, ["C4"], deck_riser) # Import Global Variables global tip50 global tip200 global p50_rack_count global p200_rack_count - global tt_50 - global tt_200 - - p200 = ctx.load_instrument("flex_8channel_1000", pipette_1000_mount) - p50 = ctx.load_instrument("flex_8channel_50", pipette_50_mount) - - Available_on_deck_slots = ["A2", "A3", "B3"] - Available_off_deck_slots = ["A4", "B4"] - p50_racks_to_dump = [] - p200_racks_to_dump = [] - - if REUSE_RSB_TIPS: - Available_on_deck_slots.remove("A3") - tip50_reuse = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "A3") - RSB_tip = [] - p50_rack_count += 1 - tt_50 += 12 - p50.tip_racks.append(tip50_reuse) - ctx.comment(f"Adding 50 ul tip rack #{p50_rack_count}") - for x in range(num_cols): - RSB_tip.append(tip50_reuse.wells()[8 * x]) - tt_50 -= 1 - p50.starting_tip = tip50_reuse.wells()[(len(RSB_tip)) * 8] - - if REUSE_REMOVE_TIPS: - Available_on_deck_slots.remove("A2") - tip200_reuse = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "A2") - RemoveSup_tip = [] - p200_rack_count += 1 - tt_200 += 12 - p200.tip_racks.append(tip200_reuse) - ctx.comment(f"Adding 200 ul tip rack #{p200_rack_count}") - for x in range(num_cols): - RemoveSup_tip.append(tip200_reuse.wells()[8 * x]) - tt_200 -= 1 - p200.starting_tip = tip200_reuse.wells()[(len(RemoveSup_tip)) * 8] + tip_count = {1000: 0, 50: 0} + + p200 = ctx.load_instrument( + "flex_8channel_1000", + pipette_1000_mount, + tip_racks=[tiprack_200_1, tiprack_200_2], + ) + p50 = ctx.load_instrument( + "flex_8channel_50", pipette_50_mount, tip_racks=[tiprack_50_1, tiprack_50_2] + ) # Load Reagent Locations in Reservoirs lib_amplification_wells: List[Well] = temp_plate.columns()[num_cols + 3] @@ -226,53 +199,23 @@ def run(ctx: ProtocolContext) -> None: etoh2 = reservoir.columns()[5] etoh2_res = etoh2[0] - liquid_vols_and_wells = { - "Samples": [ - {"well": sample_plate.wells()[: 8 * num_cols], "volume": sample_vol} - ], + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Samples": [{"well": sample_plate.wells()[: 8 * num_cols], "volume": 35.0}], "Final Library": [ - {"well": sample_plate_2.wells()[: 8 * num_cols], "volume": elution_vol_2} + {"well": sample_plate_2.wells()[: 8 * num_cols], "volume": 17.0} ], - "Adapters": [{"well": adapters, "volume": adapter_vol * 2.0}], + "Adapters": [{"well": adapters, "volume": 10.0}], "End Repair Mix": [ - { - "well": end_repair_cols, - "volume": (end_repair_vol * num_cols) + (0.1 * end_repair_vol), - } - ], - "Fragmentation Mix": [ - {"well": frag, "volume": (frag_vol * num_cols) + (0.1 * frag_vol)} - ], - "Ligation Mix": [ - { - "well": ligation, - "volume": (ligation_vol * num_cols) + (0.1 * ligation_vol), - } - ], - "Amplification Mix": [ - { - "well": lib_amplification_wells, - "volume": (amplification_vol * num_cols) + (0.1 * amplification_vol), - } - ], - "Ampure Beads": [ - { - "well": bead, - "volume": (bead_vol * num_cols) + (0.1 * bead_vol * num_cols), - } - ], - "Resuspension Buffer": [ - {"well": rsb, "volume": (rsb_vol * num_cols) + (0.1 * rsb_vol * num_cols)} + {"well": temp_plate.wells()[: 8 * num_cols], "volume": 61.0} ], + "Fragmentation Mix": [{"well": frag, "volume": 91.5}], + "Ligation Mix": [{"well": ligation, "volume": 200.0}], + "Amplification Mix": [{"well": lib_amplification_wells, "volume": 183.0}], + "Ampure Beads": [{"well": bead, "volume": 910.8}], + "Resuspension Buffer": [{"well": rsb, "volume": 297.0}], "Ethanol 80%": [ - { - "well": etoh1, - "volume": (etoh_vol * num_cols) + (0.1 * etoh_vol * num_cols), - }, - { - "well": etoh2, - "volume": (etoh_vol * num_cols) + (0.1 * etoh_vol * num_cols), - }, + {"well": etoh1, "volume": 2000.0}, + {"well": etoh2, "volume": 2000.0}, ], } waste1 = reservoir.columns()[6] @@ -281,213 +224,24 @@ def run(ctx: ProtocolContext) -> None: waste2 = reservoir.columns()[7] waste2_res = waste2[0] - def tiptrack(rack: int, reuse_col: Optional[int], reuse: bool = False) -> None: - """Tip Track.""" - global tt_50 - global tt_200 - global p50_racks_ondeck - global p200_racks_ondeck - global p50_racks_offdeck - global p200_racks_offdeck - global p50_rack_count - global p200_rack_count - - if rack == tip50: - if ( - tt_50 == 0 and not reuse - ): # If this is the first column of tip box and these aren't reused tips - ctx.comment("Troubleshoot") - if len(Available_on_deck_slots) > 0: - avail_slot = Available_on_deck_slots[0] - p50_rack_count += 1 - tt_50 += 12 - addtiprack = ctx.load_labware( - "opentrons_flex_96_tiprack_50ul", - avail_slot, - f"50 ul Tip Rack #{p50_rack_count}", - ) - ctx.comment( - f"Add 50 ul tip rack #{p50_rack_count} to slot {avail_slot}." - ) - Available_on_deck_slots.pop(0) - p50_racks_ondeck.append(addtiprack) - p50_racks_to_dump.append(addtiprack) - p50.tip_racks.append(addtiprack) - elif ( - len(Available_on_deck_slots) == 0 - and len(Available_off_deck_slots) > 0 - ): - p50_rack_count += 1 - tt_50 += 12 - addtiprack = ctx.load_labware( - "opentrons_flex_96_tiprack_50ul", - Available_off_deck_slots[0], - f"50 ul Tip Rack #{p50_rack_count}", - ) - Available_off_deck_slots.pop( - 0 - ) # Load rack into staging area slot to be moved on deck - ctx.comment(f"Adding 50 ul tip rack #{p50_rack_count}") - p50_racks_offdeck.append( - addtiprack - ) # used in TipSwap then deleted once it is moved - p50.tip_racks.append( - addtiprack - ) # lets pipette know it can use this rack now - TipSwap( - 50 - ) # Throw first tip box out and replace with a box from staging area - elif ( - len(Available_on_deck_slots) == 0 - and len(Available_off_deck_slots) == 0 - ): # If there are no tip racks on deck or in staging area to use - ctx.pause("Please place a new 50ul Tip Rack in slot A4") - p50_rack_count += 1 - tt_50 += 12 - addtiprack = ctx.load_labware( - "opentrons_flex_96_tiprack_50ul", - "A4", - f"50 ul Tip Rack #{p50_rack_count}", - ) - ctx.comment(f"Adding 50 ul tip rack #{p50_rack_count}") - p50_racks_offdeck.append( - addtiprack - ) # used in TipSwap, then deleted once it is moved - p50.tip_racks.append( - addtiprack - ) # lets pipette know it can use this rack now - TipSwap( - 50 - ) # Throw first tip box out and replace with a box from staging area - # Call where tips will actually be picked up - if reuse and REUSE_RSB_TIPS and reuse_col: - p50.pick_up_tip(tip50_reuse.wells()[8 * reuse_col]) - else: - tt_50 -= 1 - ctx.comment("Column " + str(12 - tt_50)) - ctx.comment( - "Available On Deck Slots:" + str(len(Available_on_deck_slots)) - ) - ctx.comment( - "Available Off Deck Slots:" + str(len(Available_off_deck_slots)) - ) - p50.pick_up_tip() - - if rack == tip200: - if ( - tt_200 == 0 and not reuse - ): # If this is the first column of tip box and these aren't reused tips - if len(Available_on_deck_slots) > 0: - avail_slot = Available_on_deck_slots[0] - p200_rack_count += 1 - tt_200 += 12 - addtiprack = ctx.load_labware( - "opentrons_flex_96_tiprack_200ul", - avail_slot, - f"200 ul Tip Rack #{p200_rack_count}", - ) - ctx.comment( - f"Adding 200 ul tip rack #{p200_rack_count} to slot {avail_slot}" - ) - Available_on_deck_slots.pop(0) - p200_racks_ondeck.append(addtiprack) - p200_racks_to_dump.append(addtiprack) - p200.tip_racks.append(addtiprack) - elif ( - len(Available_on_deck_slots) == 0 - and len(Available_off_deck_slots) > 0 - ): - p200_rack_count += 1 - tt_200 += 12 - addtiprack = ctx.load_labware( - "opentrons_flex_96_tiprack_200ul", - Available_off_deck_slots[0], - f"200 ul Tip Rack #{p200_rack_count}", - ) - Available_off_deck_slots.pop( - 0 - ) # Load rack into staging area slot to be moved on deck - ctx.comment(f"Adding 200 ul tip rack #{p200_rack_count}") - p200_racks_offdeck.append( - addtiprack - ) # used in TipSwap then deleted once it is moved - p200.tip_racks.append( - addtiprack - ) # lets pipette know it can use this rack now - TipSwap( - 200 - ) # Throw first tip box out and replace with a box from staging area - elif ( - len(Available_on_deck_slots) == 0 - and len(Available_off_deck_slots) == 0 - ): # If there are no tip racks on deck or in staging area to use - ctx.pause("Please place a new 200ul Tip Rack in slot B4") - p200_rack_count += 1 - tt_200 += 12 - addtiprack = ctx.load_labware( - "opentrons_flex_96_tiprack_200ul", - "B4", - f"200 ul Tip Rack #{p200_rack_count}", - ) - ctx.comment(f"Adding 200 ul tip rack #{p200_rack_count}") - p200_racks_offdeck.append( - addtiprack - ) # used in TipSwap, then deleted once it is moved - p200.tip_racks.append( - addtiprack - ) # lets pipette know it can use this rack now - TipSwap( - 200 - ) # Throw first tip box out and replace with a box from staging area - # Call where tips will actually be picked up - if reuse and REUSE_REMOVE_TIPS and reuse_col: - p200.pick_up_tip(tip200_reuse.wells()[8 * reuse_col]) - else: - tt_200 -= 1 - ctx.comment("Column " + str(12 - tt_200)) - ctx.comment( - "Available On Deck Slots:" + str(len(Available_on_deck_slots)) - ) - ctx.comment( - "Available Off Deck Slots:" + str(len(Available_off_deck_slots)) - ) - p200.pick_up_tip() - - tiptrack(tip50, None, reuse=False) - p50.return_tip() helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, p50) - def TipSwap(tipvol: int) -> None: - """Tip swap.""" - if tipvol == 50: - rack_to_dispose = p50_racks_to_dump[0] - rack_to_add = p50_racks_offdeck[0] - deck_slot = p50_racks_to_dump[0].parent - p50_racks_ondeck.append(rack_to_add) - p50_racks_to_dump.pop(0) - p50_racks_to_dump.append(rack_to_add) - p50_racks_ondeck.pop(0) - p50_racks_offdeck.pop(0) - - if tipvol == 200: - rack_to_dispose = p200_racks_to_dump[0] - rack_to_add = p200_racks_offdeck[0] - deck_slot = p200_racks_to_dump[0].parent - p200_racks_ondeck.append(rack_to_add) - p200_racks_to_dump.pop(0) - p200_racks_to_dump.append(rack_to_add) - p200_racks_ondeck.pop(0) - p200_racks_offdeck.pop(0) - - ctx.move_labware( - labware=rack_to_dispose, new_location=trash, use_gripper=USE_GRIPPER - ) - ctx.move_labware( - labware=rack_to_add, new_location=deck_slot, use_gripper=USE_GRIPPER - ) - ctx.comment( - f"Threw out: {rack_to_dispose} and placed {rack_to_add} to {deck_slot}" - ) + def tip_track(pipette: InstrumentContext, tip_count: Dict) -> None: + """Track tip usage.""" + # Get the current tip count for the pipette + current_tips = tip_count[pipette.max_volume] + + # Check if tip count exceeds the maximum tips per rack + if current_tips >= (96 * 2): + tip_count[pipette.max_volume] = 0 + pipette.reset_tipracks() + + # Pick up a new tip and update the count + if not pipette._has_tip: + pipette.pick_up_tip() + tip_count[ + pipette.max_volume + ] += 8 # Adjust increment based on multi-channel pipette def run_tag_profile( unused_lids: List[Labware], used_lids: List[Labware] @@ -517,7 +271,7 @@ def run_tag_profile( if disposable_lid: if len(used_lids) <= 1: - ctx.move_labware(lid_on_plate, "C4", use_gripper=True) + ctx.move_labware(lid_on_plate, "D4", use_gripper=True) else: ctx.move_labware(lid_on_plate, used_lids[-2], use_gripper=True) # #Move Plate to H-S @@ -691,7 +445,8 @@ def mix_beads( loc8 = res.bottom().move(types.Point(x=move / 2, y=-move / 2, z=5)) loc = [loc_center_d, loc1, loc5, loc2, loc6, loc3, loc7, loc4, loc8] - + if not pip._has_tip: + tip_track(pip, tip_count) pip.aspirate( mix_vol, res.bottom().move(types.Point(x=0, y=0, z=10)) ) # Blow bubbles to start @@ -712,8 +467,8 @@ def remove_supernatant(well: Well, vol: float, waste_: Well, column: int) -> Non p200.flow_rate.aspirate = 15 num_trans = math.ceil(vol / 190) vol_per_trans = vol / num_trans + tip_track(p200, tip_count) for x in range(num_trans): - tiptrack(tip200, column, reuse=True if REUSE_REMOVE_TIPS else False) p200.aspirate(vol_per_trans / 2, well.bottom(0.2)) ctx.delay(seconds=1) p200.aspirate(vol_per_trans / 2, well.bottom(0.2)) @@ -742,8 +497,8 @@ def Fragmentation( ctx.comment("Mixing and Transfering beads to column " + str(i + 1)) - tiptrack(tip50, None, reuse=False) p50.flow_rate.dispense = 15 + tip_track(p50, tip_count) p50.aspirate(frag_vol, frag_res) p50.dispense(p50.current_volume, samples[i]) p50.flow_rate.dispense = 150 @@ -779,8 +534,8 @@ def end_repair( "**** Mixing and Transfering beads to column " + str(i + 1) + " ****" ) - tiptrack(tip50, None, reuse=False) p50.flow_rate.dispense = 15 + tip_track(p50, tip_count) p50.aspirate(end_repair_vol, er_res) p50.dispense(p50.current_volume, samples[i]) p50.flow_rate.dispense = 150 @@ -813,7 +568,7 @@ def index_ligation( ctx.comment("-------Ligating Indexes-------") ctx.comment("-------Adding and Mixing ELM-------") for i in samples: - tiptrack(tip50, None, reuse=False) + tip_track(p50, tip_count) p50.aspirate(ligation_vol, ligation_res) p50.dispense(p50.current_volume, i) for x in range(10 if not dry_run else 1): @@ -834,7 +589,7 @@ def index_ligation( # Add and mix adapters ctx.comment("-------Adding and Mixing Adapters-------") for i_well, x_well in zip(samples, adapters): - tiptrack(tip50, None, reuse=False) + tip_track(p50, tip_count) p50.aspirate(adapter_vol, x_well) p50.dispense(p50.current_volume, i_well) for y in range(10 if not dry_run else 1): @@ -867,7 +622,6 @@ def lib_cleanup() -> None: ctx.move_labware(FLP_plate, tc_mod, use_gripper=USE_GRIPPER) for x, i in enumerate(samples): - tiptrack(tip200, None, reuse=False) mix_beads(p200, bead_res, bead_vol_1, 7 if x == 0 else 2, x) p200.aspirate(bead_vol_1, bead_res) p200.dispense(bead_vol_1, i) @@ -914,11 +668,7 @@ def lib_cleanup() -> None: else: # Second Wash this_res = etoh2_res this_waste_res = waste2_res - if REUSE_ETOH_TIPS: - tiptrack(tip200, None, reuse=False) - for i in samp_list: - if not REUSE_ETOH_TIPS: - tiptrack(tip200, None, reuse=False) + tip_track(p200, tip_count) p200.aspirate(150, this_res) p200.air_gap(10) p200.dispense(p200.current_volume, i.top()) @@ -930,11 +680,7 @@ def lib_cleanup() -> None: ctx.delay(seconds=10) # Remove the ethanol wash for x, i in enumerate(samp_list): - if REUSE_ETOH_TIPS: - if x != 0: - tiptrack(tip200, None, reuse=False) - if not REUSE_ETOH_TIPS: - tiptrack(tip200, None, reuse=False) + tip_track(p200, tip_count) p200.aspirate(155, i) p200.air_gap(10) p200.dispense(p200.current_volume, this_waste_res) @@ -962,7 +708,7 @@ def lib_cleanup() -> None: for col, i in enumerate(samp_list): ctx.comment(f"****Adding RSB to Columns: {col+1}****") - tiptrack(tip50, col, reuse=True if REUSE_RSB_TIPS else False) + tip_track(p50, tip_count) p50.aspirate(rsb_vol_1, rsb_res) p50.air_gap(5) p50.dispense(p50.current_volume, i) @@ -997,7 +743,7 @@ def lib_cleanup() -> None: p200.flow_rate.aspirate = 10 for i_int, (s, e) in enumerate(zip(samp_list, samples_2)): - tiptrack(tip50, i_int, reuse=True if REUSE_RSB_TIPS else False) + tip_track(p50, tip_count) p50.aspirate(elution_vol, s) p50.air_gap(5) p50.dispense(p50.current_volume, e.bottom(1), push_out=3) @@ -1031,12 +777,11 @@ def lib_amplification( ctx.comment( "**** Mixing and Transfering beads to column " + str(i + 1) + " ****" ) - - tiptrack(tip50, None, reuse=False) mix_beads( p50, amplification_res, amplification_vol, 7 if i == 0 else 2, i ) # 5 reps for first mix in reservoir p50.flow_rate.dispense = 15 + tip_track(p50, tip_count) p50.aspirate(amplification_vol, amplification_res) p50.dispense(p50.current_volume, samples_2[i]) p50.flow_rate.dispense = 150 @@ -1065,10 +810,11 @@ def lib_cleanup_2() -> None: ctx.comment("-------Starting Cleanup-------") ctx.comment("-------Adding and Mixing Cleanup Beads-------") for x, i in enumerate(samples_2): - tiptrack(tip200, None, reuse=False) mix_beads(p200, bead_res, bead_vol_2, 7 if x == 0 else 2, x) + tip_track(p200, tip_count) p200.aspirate(bead_vol_2, bead_res) p200.dispense(bead_vol_2, i) + p200.return_tip() mix_beads(p200, i, bead_vol_2, 7 if not dry_run else 1, num_cols - 1) for x in range(10 if not dry_run else 1): if x == 9: @@ -1112,11 +858,7 @@ def lib_cleanup_2() -> None: else: # Second Wash this_res = etoh2_res this_waste_res = waste2_res - if REUSE_ETOH_TIPS: - tiptrack(tip200, None, reuse=False) - for i in samp_list_2: - if not REUSE_ETOH_TIPS: - tiptrack(tip200, None, reuse=False) + tip_track(p200, tip_count) p200.aspirate(150, this_res) p200.air_gap(10) p200.dispense(p200.current_volume, i.top()) @@ -1128,11 +870,7 @@ def lib_cleanup_2() -> None: ctx.delay(seconds=10) # Remove the ethanol wash for x, i in enumerate(samp_list_2): - if REUSE_ETOH_TIPS: - if x != 0: - tiptrack(tip200, None, reuse=False) - if not REUSE_ETOH_TIPS: - tiptrack(tip200, None, reuse=False) + tip_track(p200, tip_count) p200.aspirate(155, i) p200.air_gap(10) p200.dispense(p200.current_volume, this_waste_res) @@ -1159,7 +897,7 @@ def lib_cleanup_2() -> None: for col, i in enumerate(samp_list_2): ctx.comment(f"****Adding RSB to Columns: {col+1}****") - tiptrack(tip50, col, reuse=True if REUSE_RSB_TIPS else False) + tip_track(p50, tip_count) p50.aspirate(rsb_vol_2, rsb_res) p50.air_gap(5) p50.dispense(p50.current_volume, i) @@ -1194,7 +932,7 @@ def lib_cleanup_2() -> None: p200.flow_rate.aspirate = 10 for i_int, (s, e) in enumerate(zip(samp_list_2, samples_flp)): - tiptrack(tip50, i_int, reuse=True if REUSE_RSB_TIPS else False) + tip_track(p50, tip_count) p50.aspirate(elution_vol_2, s) p50.air_gap(5) p50.dispense(p50.current_volume, e.bottom(1), push_out=3) @@ -1215,5 +953,14 @@ def lib_cleanup_2() -> None: lib_cleanup() unused_lids, used_lids = lib_amplification(unused_lids, used_lids) lib_cleanup_2() + + # Probe liquid waste + reservoir.label = "Liquid Waste" # type: ignore[attr-defined] + waste1 = reservoir.columns()[6] + waste1_res = waste1[0] + + waste2 = reservoir.columns()[7] + waste2_res = waste2[0] + end_probed_wells = [waste1_res, waste2_res] helpers.find_liquid_height_of_all_wells(ctx, p50, end_probed_wells) diff --git a/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py new file mode 100644 index 00000000000..89f643729fb --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/6_Omega_HDQ_DNA_Cells-Flex_96_channel.py @@ -0,0 +1,351 @@ +"""Omega Bio-tek Mag-Bind Blood & Tissue DNA HDQ - Bacteria.""" +from typing import List, Dict +from abr_testing.protocols import helpers +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + InstrumentContext, +) +from opentrons.protocol_api.module_contexts import ( + HeaterShakerContext, + MagneticBlockContext, + TemperatureModuleContext, +) +from opentrons import types +import numpy as np + +metadata = { + "author": "Zach Galluzzo ", + "protocolName": "Omega Bio-tek Mag-Bind Blood & Tissue DNA HDQ - Bacteria", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_dot_bottom_parameter(parameters) + + +# Start protocol +def run(ctx: ProtocolContext) -> None: + """Protocol.""" + dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] + + dry_run = False + tip_mixing = False + + wash_vol = 600.0 + AL_vol = 230.0 + bind_vol = 320.0 + sample_vol = 180.0 + elution_vol = 100.0 + + # Same for all HDQ Extractions + deepwell_type = "nest_96_wellplate_2ml_deep" + if not dry_run: + settling_time = 2.0 + num_washes = 3 + if dry_run: + settling_time = 0.5 + num_washes = 1 + bead_vol = PK_vol = 20.0 + inc_temp = 55.0 + AL_total_vol = AL_vol + PK_vol + binding_buffer_vol = bead_vol + bind_vol + starting_vol = AL_total_vol + sample_vol + + h_s: HeaterShakerContext = ctx.load_module(helpers.hs_str, "D1") # type: ignore[assignment] + sample_plate, h_s_adapter = helpers.load_hs_adapter_and_labware( + deepwell_type, h_s, "Sample Plate" + ) + h_s.close_labware_latch() + samples_m = sample_plate.wells()[0] + + # NOTE: MAG BLOCK will be on slot 6 + + temp: TemperatureModuleContext = ctx.load_module( + helpers.temp_str, "A3" + ) # type: ignore[assignment] + elutionplate, tempblock = helpers.load_temp_adapter_and_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", temp, "Elution Plate/Reservoir" + ) + + magblock: MagneticBlockContext = ctx.load_module( + "magneticBlockV1", "C1" + ) # type: ignore[assignment] + liquid_waste = ctx.load_labware("nest_1_reservoir_195ml", "B3", "Liquid Waste") + waste = liquid_waste.wells()[0].top() + + lysis_reservoir = ctx.load_labware(deepwell_type, "D2", "Lysis reservoir") + lysis_res = lysis_reservoir.wells()[0] + bind_reservoir = ctx.load_labware( + deepwell_type, "C2", "Beads and binding reservoir" + ) + bind_res = bind_reservoir.wells()[0] + wash1_reservoir = ctx.load_labware(deepwell_type, "C3", "Wash 1 reservoir") + wash1_res = wash1_reservoir.wells()[0] + wash2_reservoir = ctx.load_labware(deepwell_type, "B1", "Wash 2 reservoir") + wash2_res = wash2_reservoir.wells()[0] + elution_res = elutionplate.wells()[0] + # Load Pipette and tip racks + # Load tips + tiprack_1 = ctx.load_labware( + "opentrons_flex_96_tiprack_1000ul", + "A1", + adapter="opentrons_flex_96_tiprack_adapter", + ) + tips = tiprack_1.wells()[0] + + tiprack_2 = ctx.load_labware( + "opentrons_flex_96_tiprack_1000ul", + "A2", + adapter="opentrons_flex_96_tiprack_adapter", + ) + tips1 = tiprack_2.wells()[0] + # load 96 channel pipette + pip: InstrumentContext = ctx.load_instrument( + "flex_96channel_1000", mount="left", tip_racks=[tiprack_1, tiprack_2] + ) + # Load Liquids and probe + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Lysis Buffer": [{"well": lysis_reservoir.wells(), "volume": AL_vol + 92.0}], + "PK Buffer": [{"well": lysis_reservoir.wells(), "volume": PK_vol + 8.0}], + "Binding Buffer": [{"well": bind_reservoir.wells(), "volume": bind_vol + 91.5}], + "Magnetic Beads": [{"well": bind_reservoir.wells(), "volume": bead_vol + 8.5}], + "Wash 1 and 2 Buffer": [ + {"well": wash1_reservoir.wells(), "volume": (wash_vol * 2.0) + 100.0} + ], + "Wash 3 Buffer": [ + {"well": wash2_reservoir.wells(), "volume": wash_vol + 100.0} + ], + "Elution Buffer": [{"well": elutionplate.wells(), "volume": elution_vol + 5}], + "Samples": [{"well": sample_plate.wells(), "volume": sample_vol}], + } + + helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, pip) + + pip.flow_rate.aspirate = 50 + pip.flow_rate.dispense = 150 + pip.flow_rate.blow_out = 300 + + def resuspend_pellet(vol: float, plate: Well, reps: int = 3) -> None: + """Re-suspend pellets.""" + pip.flow_rate.aspirate = 200 + pip.flow_rate.dispense = 300 + + loc1 = plate.bottom().move(types.Point(x=1, y=0, z=1)) + loc2 = plate.bottom().move(types.Point(x=0.75, y=0.75, z=1)) + loc3 = plate.bottom().move(types.Point(x=0, y=1, z=1)) + loc4 = plate.bottom().move(types.Point(x=-0.75, y=0.75, z=1)) + loc5 = plate.bottom().move(types.Point(x=-1, y=0, z=1)) + loc6 = plate.bottom().move(types.Point(x=-0.75, y=0 - 0.75, z=1)) + loc7 = plate.bottom().move(types.Point(x=0, y=-1, z=1)) + loc8 = plate.bottom().move(types.Point(x=0.75, y=-0.75, z=1)) + + if vol > 1000: + vol = 1000 + + mixvol = vol * 0.9 + + for _ in range(reps): + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc1) + pip.aspirate(mixvol, loc2) + pip.dispense(mixvol, loc2) + pip.aspirate(mixvol, loc3) + pip.dispense(mixvol, loc3) + pip.aspirate(mixvol, loc4) + pip.dispense(mixvol, loc4) + pip.aspirate(mixvol, loc5) + pip.dispense(mixvol, loc5) + pip.aspirate(mixvol, loc6) + pip.dispense(mixvol, loc6) + pip.aspirate(mixvol, loc7) + pip.dispense(mixvol, loc7) + pip.aspirate(mixvol, loc8) + pip.dispense(mixvol, loc8) + if _ == reps - 1: + pip.flow_rate.aspirate = 50 + pip.flow_rate.dispense = 30 + pip.aspirate(mixvol, loc8) + pip.dispense(mixvol, loc8) + + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 200 + + def bead_mix(vol: float, plate: Well, reps: int = 5) -> None: + """Bead mix.""" + pip.flow_rate.aspirate = 200 + pip.flow_rate.dispense = 300 + + loc1 = plate.bottom().move(types.Point(x=0, y=0, z=1)) + loc2 = plate.bottom().move(types.Point(x=0, y=0, z=8)) + loc3 = plate.bottom().move(types.Point(x=0, y=0, z=16)) + loc4 = plate.bottom().move(types.Point(x=0, y=0, z=24)) + + if vol > 1000: + vol = 1000 + + mixvol = vol * 0.9 + + for _ in range(reps): + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc1) + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc2) + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc3) + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc4) + if _ == reps - 1: + pip.flow_rate.aspirate = 50 + pip.flow_rate.dispense = 30 + pip.aspirate(mixvol, loc1) + pip.dispense(mixvol, loc1) + + pip.flow_rate.aspirate = 150 + pip.flow_rate.dispense = 200 + + # Start Protocol + temp.set_temperature(inc_temp) + # Transfer and mix lysis + pip.pick_up_tip(tips) + pip.aspirate(AL_total_vol, lysis_res) + pip.dispense(AL_total_vol, samples_m) + resuspend_pellet(400, samples_m, reps=4 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() + + # Mix, then heat + ctx.comment("Lysis Mixing") + helpers.set_hs_speed(ctx, h_s, 1800, 10, False) + if not dry_run: + h_s.set_and_wait_for_temperature(55) + ctx.delay( + minutes=10 if not dry_run else 0.25, + msg="Please allow another 10 minutes of 55C incubation to complete lysis.", + ) + h_s.deactivate_shaker() + + # Transfer and mix bind&beads + pip.pick_up_tip(tips) + bead_mix(binding_buffer_vol, bind_res, reps=4 if not dry_run else 1) + pip.aspirate(binding_buffer_vol, bind_res) + pip.dispense(binding_buffer_vol, samples_m) + bead_mix(binding_buffer_vol + starting_vol, samples_m, reps=4 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() + pip.home() + + # Shake for binding incubation + ctx.comment("Binding incubation") + helpers.set_hs_speed(ctx, h_s, 1800, 10, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + + ctx.delay( + minutes=settling_time, + msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", + ) + + # Remove Supernatant and move off magnet + pip.pick_up_tip(tips) + pip.aspirate(1000, samples_m.bottom(dot_bottom)) + pip.dispense(1000, waste) + if starting_vol + binding_buffer_vol > 1000: + pip.aspirate(1000, samples_m.bottom(dot_bottom)) + pip.dispense(1000, waste) + pip.return_tip() + + # Transfer plate from magnet to H/S + helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) + + # Washes + for i in range(num_washes if not dry_run else 1): + if i == 0 or i == 1: + wash_res = wash1_res + else: + wash_res = wash2_res + + pip.pick_up_tip(tips) + pip.aspirate(wash_vol, wash_res) + pip.dispense(wash_vol, samples_m) + if not tip_mixing: + pip.return_tip() + helpers.set_hs_speed(ctx, h_s, 1800, 5, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + + ctx.delay( + minutes=settling_time, + msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", + ) + + # Remove Supernatant and move off magnet + pip.pick_up_tip(tips) + pip.aspirate(1000, samples_m.bottom(dot_bottom)) + pip.dispense(1000, bind_res.top()) + if wash_vol > 1000: + pip.aspirate(1000, samples_m.bottom(dot_bottom)) + pip.dispense(1000, bind_res.top()) + pip.return_tip() + + # Transfer plate from magnet to H/S + helpers.move_labware_to_hs(ctx, sample_plate, h_s, h_s_adapter) + + # Dry beads + if dry_run: + drybeads = 0.5 + else: + drybeads = 10 + # Number of minutes you want to dry for + for beaddry in np.arange(drybeads, 0, -0.5): + ctx.delay( + minutes=0.5, + msg="There are " + str(beaddry) + " minutes left in the drying step.", + ) + + # Elution + pip.pick_up_tip(tips1) + pip.aspirate(elution_vol, elution_res) + pip.dispense(elution_vol, samples_m) + resuspend_pellet(elution_vol, samples_m, reps=3 if not dry_run else 1) + if not tip_mixing: + pip.return_tip() + pip.home() + + helpers.set_hs_speed(ctx, h_s, 2000, 5, True) + + # Transfer plate to magnet + helpers.move_labware_from_hs_to_destination(ctx, sample_plate, h_s, magblock) + + ctx.delay( + minutes=settling_time, + msg="Please wait " + str(settling_time) + " minute(s) for beads to pellet.", + ) + + pip.pick_up_tip(tips1) + pip.aspirate(elution_vol, samples_m) + pip.dispense(elution_vol, elutionplate.wells()[0]) + pip.return_tip() + + pip.home() + pip.reset_tipracks() + + # Empty Plates + pip.pick_up_tip() + pip.aspirate(1000, samples_m) + pip.dispense(1000, liquid_waste["A1"].top()) + pip.aspirate(1000, wash1_res) + pip.dispense(1000, liquid_waste["A1"].top()) + pip.aspirate(1000, wash2_res) + pip.dispense(1000, liquid_waste["A1"].top()) + pip.return_tip() + helpers.find_liquid_height_of_all_wells(ctx, pip, [liquid_waste["A1"]]) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py index b4282397baf..2575caecf6e 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/12_KAPA HyperPlus Library Prep Liquid Setup.py @@ -25,59 +25,94 @@ def run(protocol: protocol_api.ProtocolContext) -> None: p1000, ) = load_common_liquid_setup_labware_and_instruments(protocol) - reservoir = protocol.load_labware("nest_96_wellplate_2ml_deep", "D2") # Reservoir - temp_module_res = protocol.load_labware( - "opentrons_96_wellplate_200ul_pcr_full_skirt", "B3" + reservoir = protocol.load_labware( + "nest_96_wellplate_2ml_deep", "D2", "Beads + Buffer + Ethanol" + ) # Reservoir + temp_plate = protocol.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", + "B3", + "Temp Module Reservoir Plate", ) sample_plate_1 = protocol.load_labware( - "opentrons_96_wellplate_200ul_pcr_full_skirt", "D3" + "opentrons_96_wellplate_200ul_pcr_full_skirt", "D3", "Sample Plate 1" ) # Sample Plate sample_plate_2 = protocol.load_labware( - "opentrons_96_wellplate_200ul_pcr_full_skirt", "C3" + "opentrons_96_wellplate_200ul_pcr_full_skirt", "C3", "Sample Plate 2" ) # Sample Plate # Sample Plate 1 Prep: dispense 17 ul into column 1 total 136 ul p1000.transfer( - volume=136, - source=source_reservoir["A1"].bottom(z=0.5), - dest=sample_plate_1["A1"], + volume=[35, 35, 35, 35, 35, 35], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + sample_plate_1["A1"].top(), + sample_plate_1["A2"].top(), + sample_plate_1["A3"].top(), + sample_plate_1["A4"].top(), + sample_plate_1["A5"].top(), + sample_plate_1["A6"].top(), + ], blow_out=True, blowout_location="source well", + new_tip="once", trash=False, ) # Sample Plate 2 Prep: dispense 17 ul into column 1 total 136 ul p1000.transfer( - volume=136, - source=source_reservoir["A1"].bottom(z=0.5), - dest=sample_plate_2["A1"], + volume=[17, 17, 17, 17, 17, 17], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + sample_plate_2["A1"].top(), + sample_plate_2["A2"].top(), + sample_plate_2["A3"].top(), + sample_plate_2["A4"].top(), + sample_plate_2["A5"].top(), + sample_plate_2["A6"].top(), + ], blow_out=True, blowout_location="source well", + new_tip="once", trash=False, ) # Reservoir Plate Prep: p1000.transfer( - volume=[1214.4, 396, 352, 352], - source=source_reservoir["A1"].bottom(z=0.5), - dest=[reservoir["A1"], reservoir["A4"], reservoir["A5"], reservoir["A6"]], + volume=[910.8, 297, 2000, 2000], + source=source_reservoir["A1"].bottom(z=2), + dest=[ + reservoir["A1"].top(), + reservoir["A4"].top(), + reservoir["A5"].top(), + reservoir["A6"].top(), + ], blow_out=True, blowout_location="source well", + new_tip="once", trash=False, ) # Temp Module Res Prep: dispense 30 and 200 ul into columns 1 and 3 - total 1840 ul + # adapters + + # Rest of liquids p1000.transfer( - volume=[80, 88, 132, 200, 200], - source=source_reservoir["A1"].bottom(z=0.5), + volume=[10, 10, 10, 10, 10, 10, 61, 91.5, 200, 183], + source=source_reservoir["A1"].bottom(z=2), dest=[ - temp_module_res["A1"], - temp_module_res["A2"], - temp_module_res["A3"], - temp_module_res["A4"], - temp_module_res["A5"], + temp_plate["A1"].top(), + temp_plate["A2"].top(), + temp_plate["A3"].top(), + temp_plate["A4"].top(), + temp_plate["A5"].top(), + temp_plate["A6"].top(), + temp_plate["A7"].top(), + temp_plate["A8"].top(), + temp_plate["A9"].top(), + temp_plate["A10"].top(), ], blow_out=True, blowout_location="source well", + new_tip="once", trash=False, ) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/6_Omega_HDQ_DNA_Cells Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/6_Omega_HDQ_DNA_Cells Liquid Setup.py new file mode 100644 index 00000000000..da40b983a1d --- /dev/null +++ b/abr-testing/abr_testing/protocols/liquid_setups/6_Omega_HDQ_DNA_Cells Liquid Setup.py @@ -0,0 +1,92 @@ +"""Plate Filler Protocol for Omega HDQ DNA Cell Protocol.""" +from opentrons import protocol_api +from abr_testing.protocols.helpers import ( + load_common_liquid_setup_labware_and_instruments, +) + +metadata = { + "protocolName": "DVT2ABR6: Omega HDQ DNA Cells Protocol", + "author": "Rhyann clarke ", + "source": "Protocol Library", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.21", +} + + +def run(protocol: protocol_api.ProtocolContext) -> None: + """Protocol.""" + # Initiate Labware + ( + source_reservoir, + tip_rack, + p1000, + ) = load_common_liquid_setup_labware_and_instruments(protocol) + deepwell_type = "nest_96_wellplate_2ml_deep" + + lysis_reservoir = protocol.load_labware(deepwell_type, "D2", "Lysis reservoir") + bind_reservoir = protocol.load_labware( + deepwell_type, "D3", "Beads and binding reservoir" + ) + wash1_reservoir = protocol.load_labware(deepwell_type, "C3", "Wash 1 reservoir") + wash2_reservoir = protocol.load_labware(deepwell_type, "B3", "Wash 2 reservoir") + sample_plate = protocol.load_labware(deepwell_type, "B2", "Sample Plate") + elution_plate = protocol.load_labware( + "armadillo_96_wellplate_200ul_pcr_full_skirt", "B1", "Elution Plate/ reservoir" + ) + p1000.transfer( + volume=350, + source=source_reservoir["A1"].bottom(z=2), + dest=lysis_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 440, + source=source_reservoir["A1"].bottom(z=2), + dest=bind_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 1300, + source_reservoir["A1"].bottom(z=2), + wash1_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 700, + source_reservoir["A1"].bottom(z=2), + wash2_reservoir.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 180, + source_reservoir["A1"].bottom(z=2), + sample_plate.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) + p1000.transfer( + 100, + source_reservoir["A1"].bottom(z=2), + elution_plate.wells(), + blow_out=True, + blowout_location="source well", + new_tip="once", + trash=False, + ) diff --git a/abr-testing/abr_testing/tools/abr_setup.py b/abr-testing/abr_testing/tools/abr_setup.py index a0fca76b6e4..224ba5bf120 100644 --- a/abr-testing/abr_testing/tools/abr_setup.py +++ b/abr-testing/abr_testing/tools/abr_setup.py @@ -4,7 +4,10 @@ import configparser import traceback import sys +from datetime import datetime, timedelta +from typing import Any from hardware_testing.scripts import ABRAsairScript # type: ignore +from abr_testing.automation import google_sheets_tool from abr_testing.data_collection import ( get_run_logs, abr_google_drive, @@ -13,6 +16,49 @@ from abr_testing.tools import sync_abr_sheet +def clean_sheet(sheet_name: str, credentials: str) -> Any: + """Remove data older than 60 days from sheet.""" + sheet = google_sheets_tool.google_sheet( + credentials=credentials, file_name=sheet_name, tab_number=0 + ) + date_columns = sheet.get_column(3) + curr_date = datetime.now() + cutoff_days = 60 # Cutoff period in days + cutoff_date = curr_date - timedelta(days=cutoff_days) + + rem_rows = [] + for row_id, date in enumerate(date_columns): + # Convert to datetime if needed + formatted_date = None + if isinstance(date, str): # Assuming dates might be strings + try: + formatted_date = datetime.strptime(date, "%m/%d/%Y") + except ValueError: + try: + formatted_date = datetime.strptime(date, "%Y-%m-%d") + except ValueError: + continue + + # Check if the date is older than the cutoff + if formatted_date < cutoff_date: + rem_rows.append(row_id) + if len(rem_rows) > 2000: + break + if len(rem_rows) == 0: + # No more rows to remove + print("Nothing to remove") + return + print(f"Rows to be removed: {rem_rows}") + try: + sheet.batch_delete_rows(rem_rows) + print("deleted rows") + except Exception: + print("could not delete rows") + traceback.print_exc() + sys.exit(1) + clean_sheet(sheet_name, credentials) + + def run_sync_abr_sheet( storage_directory: str, abr_data_sheet: str, room_conditions_sheet: str ) -> None: @@ -20,8 +66,10 @@ def run_sync_abr_sheet( sync_abr_sheet.run(storage_directory, abr_data_sheet, room_conditions_sheet) -def run_temp_sensor() -> None: +def run_temp_sensor(ambient_conditions_sheet: str, credentials: str) -> None: """Run temperature sensors on all robots.""" + # Remove entries > 60 days + clean_sheet(ambient_conditions_sheet, credentials) processes = ABRAsairScript.run() for process in processes: process.start() @@ -71,34 +119,27 @@ def main(configurations: configparser.ConfigParser) -> None: ambient_conditions_sheet = None sheet_url = None - has_defaults = False # If default is not specified get all values default = configurations["DEFAULT"] - if len(default) > 0: - has_defaults = True - try: - if has_defaults: - storage_directory = default["Storage"] - email = default["Email"] - drive_folder = default["Drive_Folder"] - sheet_name = default["Sheet_Name"] - sheet_url = default["Sheet_Url"] - except KeyError as e: - print("Cannot read config file\n" + str(e)) + credentials = "" + if default: + try: + credentials = default["Credentials"] + except KeyError as e: + print("Cannot read config file\n" + str(e)) # Run Temperature Sensors - if not has_defaults: - ambient_conditions_sheet = configurations["TEMP-SENSOR"]["Sheet_Url"] + ambient_conditions_sheet = configurations["TEMP-SENSOR"]["Sheet_Url"] + ambient_conditions_sheet_name = configurations["TEMP-SENSOR"]["Sheet_Name"] print("Starting temp sensors...") - run_temp_sensor() + run_temp_sensor(ambient_conditions_sheet_name, credentials) print("Temp Sensors Started") # Get Run Logs and Record - if not has_defaults: - storage_directory = configurations["RUN-LOG"]["Storage"] - email = configurations["RUN-LOG"]["Email"] - drive_folder = configurations["RUN-LOG"]["Drive_Folder"] - sheet_name = configurations["RUN-LOG"]["Sheet_Name"] - sheet_url = configurations["RUN-LOG"]["Sheet_Url"] + storage_directory = configurations["RUN-LOG"]["Storage"] + email = configurations["RUN-LOG"]["Email"] + drive_folder = configurations["RUN-LOG"]["Drive_Folder"] + sheet_name = configurations["RUN-LOG"]["Sheet_Name"] + sheet_url = configurations["RUN-LOG"]["Sheet_Url"] print(sheet_name) if storage_directory and drive_folder and sheet_name and email: print("Retrieving robot run logs...") @@ -113,11 +154,10 @@ def main(configurations: configparser.ConfigParser) -> None: if storage_directory and sheet_url and ambient_conditions_sheet: run_sync_abr_sheet(storage_directory, sheet_url, ambient_conditions_sheet) # Collect calibration data - if not has_defaults: - storage_directory = configurations["CALIBRATION"]["Storage"] - email = configurations["CALIBRATION"]["Email"] - drive_folder = configurations["CALIBRATION"]["Drive_Folder"] - sheet_name = configurations["CALIBRATION"]["Sheet_Name"] + storage_directory = configurations["CALIBRATION"]["Storage"] + email = configurations["CALIBRATION"]["Email"] + drive_folder = configurations["CALIBRATION"]["Drive_Folder"] + sheet_name = configurations["CALIBRATION"]["Sheet_Name"] if storage_directory and drive_folder and sheet_name and email: print("Retrieving and recording robot calibration data...") get_calibration_data(storage_directory, drive_folder, sheet_name, email) diff --git a/api-client/src/runs/getRunLoadedLabwareDefintions.ts b/api-client/src/runs/getRunLoadedLabwareDefintions.ts new file mode 100644 index 00000000000..d96e7facf8f --- /dev/null +++ b/api-client/src/runs/getRunLoadedLabwareDefintions.ts @@ -0,0 +1,17 @@ +import { GET, request } from '../request' + +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RunLoadedLabwareDefinitions } from './types' + +export function getRunLoadedLabwareDefintions( + config: HostConfig, + runId: string +): ResponsePromise { + return request( + GET, + `runs/${runId}/loaded_labware_definitions`, + null, + config + ) +} diff --git a/api-client/src/runs/index.ts b/api-client/src/runs/index.ts index fff1f303543..cbbe54999fa 100644 --- a/api-client/src/runs/index.ts +++ b/api-client/src/runs/index.ts @@ -16,6 +16,7 @@ export * from './createLabwareDefinition' export * from './constants' export * from './updateErrorRecoveryPolicy' export * from './getErrorRecoveryPolicy' +export * from './getRunLoadedLabwareDefintions' export * from './types' export type { CreateRunData } from './createRun' diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index e41626b6448..ea24c040ebc 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -9,6 +9,9 @@ import type { RunTimeParameter, NozzleLayoutConfig, OnDeckLabwareLocation, + LabwareDefinition1, + LabwareDefinition2, + LabwareDefinition3, } from '@opentrons/shared-data' import type { ResourceLink, ErrorDetails } from '../types' export * from './commands/types' @@ -86,6 +89,10 @@ export interface LabwareOffset { vector: VectorOffset } +export interface RunLoadedLabwareDefinitions { + data: Array +} + export interface Run { data: RunData } diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 761f1f604f3..ac762aebec4 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,10 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + ## Internal Release 2.3.0-alpha.0 This internal release, pulled from the `edge` branch, contains features being developed for evo tip functionality. It's for internal testing only. diff --git a/api/src/opentrons/drivers/heater_shaker/driver.py b/api/src/opentrons/drivers/heater_shaker/driver.py index 624e81d51d5..cb9afe39758 100644 --- a/api/src/opentrons/drivers/heater_shaker/driver.py +++ b/api/src/opentrons/drivers/heater_shaker/driver.py @@ -23,6 +23,7 @@ class GCODE(str, Enum): CLOSE_LABWARE_LATCH = "M243" GET_LABWARE_LATCH_STATE = "M241" DEACTIVATE_HEATER = "M106" + GET_RESET_REASON = "M114" HS_BAUDRATE = 115200 @@ -166,12 +167,20 @@ async def home(self) -> None: async def get_device_info(self) -> Dict[str, str]: """Send get-device-info command""" - c = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode( gcode=GCODE.GET_VERSION ) response = await self._connection.send_command( - command=c, retries=DEFAULT_COMMAND_RETRIES + command=device_info, retries=DEFAULT_COMMAND_RETRIES + ) + + reset_reason = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode( + gcode=GCODE.GET_RESET_REASON + ) + await self._connection.send_command( + command=reset_reason, retries=DEFAULT_COMMAND_RETRIES ) + return utils.parse_hs_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: diff --git a/api/src/opentrons/drivers/temp_deck/driver.py b/api/src/opentrons/drivers/temp_deck/driver.py index 6cb385f460f..93a41a08cb7 100644 --- a/api/src/opentrons/drivers/temp_deck/driver.py +++ b/api/src/opentrons/drivers/temp_deck/driver.py @@ -31,6 +31,7 @@ class GCODE(str, Enum): GET_TEMP = "M105" SET_TEMP = "M104" DEVICE_INFO = "M115" + GET_RESET_REASON = "M114" DISENGAGE = "M18" PROGRAMMING_MODE = "dfu" @@ -154,10 +155,16 @@ async def get_device_info(self) -> Dict[str, str]: Example input from Temp-Deck's serial response: "serial:aa11bb22 model:aa11bb22 version:aa11bb22" """ - c = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( gcode=GCODE.DEVICE_INFO ) - response = await self._send_command(command=c) + response = await self._send_command(command=device_info) + + reset_reason = CommandBuilder( + terminator=TEMP_DECK_COMMAND_TERMINATOR + ).add_gcode(gcode=GCODE.GET_RESET_REASON) + await self._send_command(command=reset_reason) + return utils.parse_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: diff --git a/api/src/opentrons/drivers/thermocycler/driver.py b/api/src/opentrons/drivers/thermocycler/driver.py index a3e6340c4f5..6d25564fa5a 100644 --- a/api/src/opentrons/drivers/thermocycler/driver.py +++ b/api/src/opentrons/drivers/thermocycler/driver.py @@ -33,6 +33,7 @@ class GCODE(str, Enum): DEACTIVATE_LID = "M108" DEACTIVATE_BLOCK = "M14" DEVICE_INFO = "M115" + GET_RESET_REASON = "M114" ENTER_PROGRAMMING = "dfu" @@ -292,12 +293,20 @@ async def deactivate_block(self) -> None: async def get_device_info(self) -> Dict[str, str]: """Send get device info command""" - c = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( gcode=GCODE.DEVICE_INFO ) response = await self._connection.send_command( - command=c, retries=DEFAULT_COMMAND_RETRIES + command=device_info, retries=DEFAULT_COMMAND_RETRIES + ) + + reset_reason = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( + gcode=GCODE.GET_RESET_REASON + ) + await self._connection.send_command( + command=reset_reason, retries=DEFAULT_COMMAND_RETRIES ) + return utils.parse_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: @@ -353,6 +362,14 @@ async def get_device_info(self) -> Dict[str, str]: response = await self._connection.send_command( command=c, retries=DEFAULT_COMMAND_RETRIES ) + + reset_reason = CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode( + gcode=GCODE.GET_RESET_REASON + ) + await self._connection.send_command( + command=reset_reason, retries=DEFAULT_COMMAND_RETRIES + ) + return utils.parse_hs_device_information(device_info_string=response) async def enter_programming_mode(self) -> None: diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index 3862981b20c..64f82a751ef 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -36,10 +36,9 @@ HepaFanState, HepaUVState, StatusBarState, + PipetteSensorResponseQueue, ) from opentrons.hardware_control.module_control import AttachedModulesControl -from opentrons_hardware.firmware_bindings.constants import SensorId -from opentrons_hardware.sensors.types import SensorDataType from ..dev_types import OT3AttachedInstruments from .types import HWStopCondition @@ -168,9 +167,7 @@ async def liquid_probe( num_baseline_reads: int, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: ... diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 0ff3c033f44..f710eb405ac 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -104,7 +104,6 @@ ErrorCode, SensorId, ) -from opentrons_hardware.sensors.types import SensorDataType from opentrons_hardware.firmware_bindings.messages.message_definitions import ( StopRequest, ) @@ -142,6 +141,10 @@ EstopState, HardwareEventHandler, HardwareEventUnsubscriber, + PipetteSensorId, + PipetteSensorType, + PipetteSensorData, + PipetteSensorResponseQueue, ) from opentrons.hardware_control.errors import ( InvalidPipetteName, @@ -212,6 +215,7 @@ from .types import HWStopCondition from .flex_protocol import FlexBackend from .status_bar_state import StatusBarStateController +from opentrons_hardware.sensors.types import SensorDataType log = logging.getLogger(__name__) @@ -967,7 +971,6 @@ def _build_tip_action_group( async def tip_action( self, origin: float, targets: List[Tuple[float, float]] ) -> None: - move_group = self._build_tip_action_group(origin, targets) runner = MoveGroupRunner( move_groups=[move_group], @@ -1492,9 +1495,7 @@ async def liquid_probe( num_baseline_reads: int, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: head_node = axis_to_node(Axis.by_mount(mount)) tool = sensor_node_for_pipette(OT3Mount(mount.value)) @@ -1503,6 +1504,27 @@ async def liquid_probe( "Liquid Presence Detection not available on this pipette." ) + if response_queue is None: + response_capture: Optional[ + Callable[[Dict[SensorId, List[SensorDataType]]], None] + ] = None + else: + + def response_capture(data: Dict[SensorId, List[SensorDataType]]) -> None: + response_queue.put_nowait( + { + PipetteSensorId(sensor_id.value): [ + PipetteSensorData( + sensor_type=PipetteSensorType(packet.sensor_type.value), + _as_int=packet.to_int, + _as_float=packet.to_float(), + ) + for packet in packets + ] + for sensor_id, packets in data.items() + } + ) + positions = await liquid_probe( messenger=self._messenger, tool=tool, @@ -1515,7 +1537,7 @@ async def liquid_probe( num_baseline_reads=num_baseline_reads, sensor_id=sensor_id_for_instrument(probe), force_both_sensors=force_both_sensors, - response_queue=response_queue, + emplace_data=response_capture, ) for node, point in positions.items(): self._position.update({node: point.motor_position}) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 377397ba597..e85b51ad53f 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -45,6 +45,7 @@ EstopPhysicalStatus, HardwareEventHandler, HardwareEventUnsubscriber, + PipetteSensorResponseQueue, ) from opentrons_shared_data.pipette.types import PipetteName, PipetteModel @@ -62,9 +63,9 @@ ) from opentrons.util.async_helpers import ensure_yield from .types import HWStopCondition -from .flex_protocol import FlexBackend -from opentrons_hardware.firmware_bindings.constants import SensorId -from opentrons_hardware.sensors.types import SensorDataType +from .flex_protocol import ( + FlexBackend, +) log = logging.getLogger(__name__) @@ -354,9 +355,7 @@ async def liquid_probe( num_baseline_reads: int, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: z_axis = Axis.by_mount(mount) pos = self._position diff --git a/api/src/opentrons/hardware_control/emulation/heater_shaker.py b/api/src/opentrons/hardware_control/emulation/heater_shaker.py index a465de86312..b172cb9ee16 100644 --- a/api/src/opentrons/hardware_control/emulation/heater_shaker.py +++ b/api/src/opentrons/hardware_control/emulation/heater_shaker.py @@ -45,6 +45,7 @@ def __init__(self, parser: Parser, settings: HeaterShakerSettings) -> None: GCODE.HOME.value: self._home, GCODE.ENTER_BOOTLOADER.value: self._enter_bootloader, GCODE.GET_VERSION.value: self._get_version, + GCODE.GET_RESET_REASON.value: self._get_reset_reason, GCODE.OPEN_LABWARE_LATCH.value: self._open_labware_latch, GCODE.CLOSE_LABWARE_LATCH.value: self._close_labware_latch, GCODE.GET_LABWARE_LATCH_STATE.value: self._get_labware_latch_state, @@ -126,6 +127,9 @@ def _get_version(self, command: Command) -> str: f"SerialNo:{self._settings.serial_number}" ) + def _get_reset_reason(self, command: Command) -> str: + return "M114 Last Reset Reason: 01" + def _open_labware_latch(self, command: Command) -> str: self._latch_status = HeaterShakerLabwareLatchStatus.IDLE_OPEN return "M242" diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 6c4b4f291bc..7bb5e05f47b 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -98,6 +98,7 @@ EstopState, HardwareFeatureFlags, FailedTipStateCheck, + PipetteSensorResponseQueue, ) from .errors import ( UpdateOngoingError, @@ -143,8 +144,6 @@ from .backends.flex_protocol import FlexBackend from .backends.ot3simulator import OT3Simulator from .backends.errors import SubsystemUpdating -from opentrons_hardware.firmware_bindings.constants import SensorId -from opentrons_hardware.sensors.types import SensorDataType mod_log = logging.getLogger(__name__) @@ -2679,9 +2678,7 @@ async def _liquid_probe_pass( probe: InstrumentProbeType, p_travel: float, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: plunger_direction = -1 if probe_settings.aspirate_while_sensing else 1 end_z = await self._backend.liquid_probe( @@ -2715,9 +2712,7 @@ async def liquid_probe( # noqa: C901 probe_settings: Optional[LiquidProbeSettings] = None, probe: Optional[InstrumentProbeType] = None, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] - ] = None, + response_queue: Optional[PipetteSensorResponseQueue] = None, ) -> float: """Search for and return liquid level height. diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index bc32431d2a5..4e3bb875498 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -1,3 +1,4 @@ +from asyncio import Queue import enum import logging from dataclasses import dataclass @@ -712,3 +713,63 @@ def __init__( super().__init__( f"Expected tip state {expected_state}, but received {actual_state}." ) + + +@enum.unique +class PipetteSensorId(int, enum.Enum): + """Sensor IDs available. + + Not to be confused with SensorType. This is the ID value that separate + two or more of the same type of sensor within a system. + + Note that this is a copy of an enum defined in opentrons_hardware.firmware_bindings.constants. That version + is authoritative; this version is here because this data is exposed above the hardware control layer and + therefore needs a typing source here so that we don't create a dependency on the internal hardware package. + """ + + S0 = 0x0 + S1 = 0x1 + UNUSED = 0x2 + BOTH = 0x3 + + +@enum.unique +class PipetteSensorType(int, enum.Enum): + """Sensor types available. + + Note that this is a copy of an enum defined in opentrons_hardware.firmware_bindings.constants. That version + is authoritative; this version is here because this data is exposed above the hardware control layer and + therefore needs a typing source here so that we don't create a dependency on the internal hardware package. + """ + + tip = 0x00 + capacitive = 0x01 + environment = 0x02 + pressure = 0x03 + pressure_temperature = 0x04 + humidity = 0x05 + temperature = 0x06 + + +@dataclass(frozen=True) +class PipetteSensorData: + """Sensor data from a monitored sensor. + + Note that this is a copy of an enum defined in opentrons_hardware.firmware_bindings.constants. That version + is authoritative; this version is here because this data is exposed above the hardware control layer and + therefore needs a typing source here so that we don't create a dependency on the internal hardware package. + """ + + sensor_type: PipetteSensorType + _as_int: int + _as_float: float + + def to_float(self) -> float: + return self._as_float + + @property + def to_int(self) -> int: + return self._as_int + + +PipetteSensorResponseQueue = Queue[Dict[PipetteSensorId, List[PipetteSensorData]]] diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index f25293f85fb..4774a45c475 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -341,6 +341,14 @@ VerifyTipPresenceCommandType, ) +from .get_next_tip import ( + GetNextTip, + GetNextTipCreate, + GetNextTipParams, + GetNextTipResult, + GetNextTipCommandType, +) + from .liquid_probe import ( LiquidProbe, LiquidProbeParams, @@ -611,6 +619,12 @@ "VerifyTipPresenceParams", "VerifyTipPresenceResult", "VerifyTipPresenceCommandType", + # get next tip command bundle + "GetNextTip", + "GetNextTipCreate", + "GetNextTipParams", + "GetNextTipResult", + "GetNextTipCommandType", # liquid probe command bundle "LiquidProbe", "LiquidProbeParams", diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 9c548fa8045..16663bf6df6 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -323,6 +323,14 @@ GetTipPresenceCommandType, ) +from .get_next_tip import ( + GetNextTip, + GetNextTipCreate, + GetNextTipParams, + GetNextTipResult, + GetNextTipCommandType, +) + from .liquid_probe import ( LiquidProbe, LiquidProbeParams, @@ -375,6 +383,7 @@ SetStatusBar, VerifyTipPresence, GetTipPresence, + GetNextTip, LiquidProbe, TryLiquidProbe, heater_shaker.WaitForTemperature, @@ -460,6 +469,7 @@ SetStatusBarParams, VerifyTipPresenceParams, GetTipPresenceParams, + GetNextTipParams, LiquidProbeParams, TryLiquidProbeParams, heater_shaker.WaitForTemperatureParams, @@ -543,6 +553,7 @@ SetStatusBarCommandType, VerifyTipPresenceCommandType, GetTipPresenceCommandType, + GetNextTipCommandType, LiquidProbeCommandType, TryLiquidProbeCommandType, heater_shaker.WaitForTemperatureCommandType, @@ -627,6 +638,7 @@ SetStatusBarCreate, VerifyTipPresenceCreate, GetTipPresenceCreate, + GetNextTipCreate, LiquidProbeCreate, TryLiquidProbeCreate, heater_shaker.WaitForTemperatureCreate, @@ -712,6 +724,7 @@ SetStatusBarResult, VerifyTipPresenceResult, GetTipPresenceResult, + GetNextTipResult, LiquidProbeResult, TryLiquidProbeResult, heater_shaker.WaitForTemperatureResult, diff --git a/api/src/opentrons/protocol_engine/commands/get_next_tip.py b/api/src/opentrons/protocol_engine/commands/get_next_tip.py new file mode 100644 index 00000000000..7ff10681bfb --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/get_next_tip.py @@ -0,0 +1,127 @@ +"""Get next tip command request, result, and implementation models.""" + +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, Type, List, Literal, Union + +from opentrons.types import NozzleConfigurationType + +from ..errors import ErrorOccurrence +from ..types import NextTipInfo, NoTipAvailable, NoTipReason +from .pipetting_common import PipetteIdMixin + +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) + +if TYPE_CHECKING: + from ..state.state import StateView + + +GetNextTipCommandType = Literal["getNextTip"] + + +class GetNextTipParams(PipetteIdMixin): + """Payload needed to resolve the next available tip.""" + + labwareIds: List[str] = Field( + ..., + description="Labware ID(s) of tip racks to resolve next available tip(s) from" + " Labware IDs will be resolved sequentially", + ) + startingTipWell: Optional[str] = Field( + None, + description="Name of starting tip rack 'well'." + " This only applies to the first tip rack in the list provided in labwareIDs", + ) + + +class GetNextTipResult(BaseModel): + """Result data from the execution of a GetNextTip.""" + + nextTipInfo: Union[NextTipInfo, NoTipAvailable] = Field( + ..., + description="Labware ID and well name of next available tip for a pipette," + " or information why no tip could be resolved.", + ) + + +class GetNextTipImplementation( + AbstractCommandImpl[GetNextTipParams, SuccessData[GetNextTipResult]] +): + """Get next tip command implementation.""" + + def __init__( + self, + state_view: StateView, + **kwargs: object, + ) -> None: + self._state_view = state_view + + async def execute(self, params: GetNextTipParams) -> SuccessData[GetNextTipResult]: + """Get the next available tip for the requested pipette.""" + pipette_id = params.pipetteId + starting_tip_name = params.startingTipWell + + num_tips = self._state_view.tips.get_pipette_active_channels(pipette_id) + nozzle_map = self._state_view.tips.get_pipette_nozzle_map(pipette_id) + + if ( + starting_tip_name is not None + and nozzle_map.configuration != NozzleConfigurationType.FULL + ): + # This is to match the behavior found in PAPI, but also because we don't have logic to automatically find + # the next tip with partial configuration and a starting tip. This will never work for a 96-channel due to + # x-axis overlap, but could eventually work with 8-channel if we better define starting tip USED or CLEAN + # state when starting a protocol to prevent accidental tip pick-up with starting non-full tip racks. + return SuccessData( + public=GetNextTipResult( + nextTipInfo=NoTipAvailable( + noTipReason=NoTipReason.STARTING_TIP_WITH_PARTIAL, + message="Cannot automatically resolve next tip with starting tip and partial tip configuration.", + ) + ) + ) + + next_tip: Union[NextTipInfo, NoTipAvailable] + for labware_id in params.labwareIds: + well_name = self._state_view.tips.get_next_tip( + labware_id=labware_id, + num_tips=num_tips, + starting_tip_name=starting_tip_name, + nozzle_map=nozzle_map, + ) + if well_name is not None: + next_tip = NextTipInfo(labwareId=labware_id, tipStartingWell=well_name) + break + # After the first tip rack is exhausted, starting tip no longer applies + starting_tip_name = None + else: + next_tip = NoTipAvailable( + noTipReason=NoTipReason.NO_AVAILABLE_TIPS, + message="No available tips for given pipette, nozzle configuration and provided tip racks.", + ) + + return SuccessData(public=GetNextTipResult(nextTipInfo=next_tip)) + + +class GetNextTip(BaseCommand[GetNextTipParams, GetNextTipResult, ErrorOccurrence]): + """Get next tip command model.""" + + commandType: GetNextTipCommandType = "getNextTip" + params: GetNextTipParams + result: Optional[GetNextTipResult] + + _ImplementationCls: Type[GetNextTipImplementation] = GetNextTipImplementation + + +class GetNextTipCreate(BaseCommandCreate[GetNextTipParams]): + """Get next tip command creation request model.""" + + commandType: GetNextTipCommandType = "getNextTip" + params: GetNextTipParams + + _CommandCls: Type[GetNextTip] = GetNextTip diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index a34178e2a00..11f1972e105 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -1116,6 +1116,37 @@ def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": }[state] +class NextTipInfo(BaseModel): + """Next available tip labware and well name data.""" + + labwareId: str = Field( + ..., + description="The labware ID of the tip rack where the next available tip(s) are located.", + ) + tipStartingWell: str = Field( + ..., description="The (starting) well name of the next available tip(s)." + ) + + +class NoTipReason(Enum): + """The cause of no tip being available for a pipette and tip rack(s).""" + + NO_AVAILABLE_TIPS = "noAvailableTips" + STARTING_TIP_WITH_PARTIAL = "startingTipWithPartial" + INCOMPATIBLE_CONFIGURATION = "incompatibleConfiguration" + + +class NoTipAvailable(BaseModel): + """No available next tip data.""" + + noTipReason: NoTipReason = Field( + ..., description="The reason why no next available tip could be provided." + ) + message: Optional[str] = Field( + None, description="Optional message explaining why a tip wasn't available." + ) + + class BaseCommandAnnotation(BaseModel): """Optional annotations for protocol engine commands.""" diff --git a/api/tests/opentrons/drivers/heater_shaker/test_driver.py b/api/tests/opentrons/drivers/heater_shaker/test_driver.py index a1fadc34446..a3f5e1151b3 100644 --- a/api/tests/opentrons/drivers/heater_shaker/test_driver.py +++ b/api/tests/opentrons/drivers/heater_shaker/test_driver.py @@ -137,10 +137,14 @@ async def test_get_device_info( ) response = await subject.get_device_info() assert response == {"serial": "TC2101010A2", "model": "A", "version": "21.2.1"} - expected = CommandBuilder(terminator=driver.HS_COMMAND_TERMINATOR).add_gcode( + device_info = CommandBuilder(terminator=driver.HS_COMMAND_TERMINATOR).add_gcode( gcode="M115" ) - connection.send_command.assert_called_once_with(command=expected, retries=0) + reset_reason = CommandBuilder(terminator=driver.HS_COMMAND_TERMINATOR).add_gcode( + gcode="M114" + ) + connection.send_command.assert_any_call(command=device_info, retries=0) + connection.send_command.assert_called_with(command=reset_reason, retries=0) async def test_enter_bootloader( diff --git a/api/tests/opentrons/drivers/temp_deck/test_driver.py b/api/tests/opentrons/drivers/temp_deck/test_driver.py index df424aa397c..60d5cc271d4 100644 --- a/api/tests/opentrons/drivers/temp_deck/test_driver.py +++ b/api/tests/opentrons/drivers/temp_deck/test_driver.py @@ -65,9 +65,15 @@ async def test_get_device_info(driver: TempDeckDriver, connection: AsyncMock) -> response = await driver.get_device_info() - expected = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode("M115") + device_info = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + "M115" + ) + reset_reason = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode( + "M114" + ) - connection.send_command.assert_called_once_with(command=expected, retries=3) + connection.send_command.assert_any_call(command=device_info, retries=3) + connection.send_command.assert_called_with(command=reset_reason, retries=3) assert response == {"serial": "s", "model": "m", "version": "v"} diff --git a/api/tests/opentrons/drivers/thermocycler/test_driver.py b/api/tests/opentrons/drivers/thermocycler/test_driver.py index 0198e4e623f..610d87fa753 100644 --- a/api/tests/opentrons/drivers/thermocycler/test_driver.py +++ b/api/tests/opentrons/drivers/thermocycler/test_driver.py @@ -237,10 +237,13 @@ async def test_device_info( device_info = await subject.get_device_info() - expected = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + get_device_info = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( gcode="M115" ) + reset_reason = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + gcode="M114" + ) - connection.send_command.assert_called_once_with(command=expected, retries=3) - + connection.send_command.assert_any_call(command=get_device_info, retries=3) + connection.send_command.assert_called_with(command=reset_reason, retries=3) assert device_info == {"serial": "s", "model": "m", "version": "v"} diff --git a/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py b/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py index 47cf6d4ebe3..da5388c558e 100644 --- a/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py +++ b/api/tests/opentrons/drivers/thermocycler/test_gen2_driver.py @@ -225,11 +225,15 @@ async def test_device_info( device_info = await subject.get_device_info() - expected = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + get_device_info = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( gcode="M115" ) + reset_reason = CommandBuilder(terminator=driver.TC_COMMAND_TERMINATOR).add_gcode( + gcode="M114" + ) - connection.send_command.assert_called_once_with(command=expected, retries=3) + connection.send_command.assert_any_call(command=get_device_info, retries=3) + connection.send_command.assert_called_with(command=reset_reason, retries=3) assert device_info == { "serial": "EMPTYSN", diff --git a/api/tests/opentrons/protocol_engine/commands/test_get_next_tip.py b/api/tests/opentrons/protocol_engine/commands/test_get_next_tip.py new file mode 100644 index 00000000000..4221cae864d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_get_next_tip.py @@ -0,0 +1,142 @@ +"""Test get next tip in place commands.""" +from decoy import Decoy + +from opentrons.types import NozzleConfigurationType +from opentrons.protocol_engine import StateView +from opentrons.protocol_engine.types import NextTipInfo, NoTipAvailable, NoTipReason +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.get_next_tip import ( + GetNextTipParams, + GetNextTipResult, + GetNextTipImplementation, +) + +from opentrons.hardware_control.nozzle_manager import NozzleMap + + +async def test_get_next_tip_implementation( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command should have an execution implementation.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.FULL) + + decoy.when( + state_view.tips.get_next_tip( + labware_id="123", + num_tips=42, + starting_tip_name="xyz", + nozzle_map=mock_nozzle_map, + ) + ).then_return("foo") + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NextTipInfo(labwareId="123", tipStartingWell="foo") + ), + ) + + +async def test_get_next_tip_implementation_multiple_tip_racks( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command with multiple tip racks should not apply starting tip to the following ones.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123", "456"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.FULL) + + decoy.when( + state_view.tips.get_next_tip( + labware_id="456", + num_tips=42, + starting_tip_name=None, + nozzle_map=mock_nozzle_map, + ) + ).then_return("foo") + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NextTipInfo(labwareId="456", tipStartingWell="foo") + ), + ) + + +async def test_get_next_tip_implementation_no_tips( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command should return with NoTipAvailable if there are no available tips.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123", "456"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.FULL) + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NoTipAvailable( + noTipReason=NoTipReason.NO_AVAILABLE_TIPS, + message="No available tips for given pipette, nozzle configuration and provided tip racks.", + ) + ), + ) + + +async def test_get_next_tip_implementation_partial_with_starting_tip( + decoy: Decoy, + state_view: StateView, +) -> None: + """A GetNextTip command should return with NoTipAvailable if there's a starting tip and a partial config.""" + subject = GetNextTipImplementation(state_view=state_view) + params = GetNextTipParams( + pipetteId="abc", labwareIds=["123", "456"], startingTipWell="xyz" + ) + mock_nozzle_map = decoy.mock(cls=NozzleMap) + + decoy.when(state_view.tips.get_pipette_active_channels("abc")).then_return(42) + decoy.when(state_view.tips.get_pipette_nozzle_map("abc")).then_return( + mock_nozzle_map + ) + decoy.when(mock_nozzle_map.configuration).then_return(NozzleConfigurationType.ROW) + + result = await subject.execute(params) + + assert result == SuccessData( + public=GetNextTipResult( + nextTipInfo=NoTipAvailable( + noTipReason=NoTipReason.STARTING_TIP_WITH_PARTIAL, + message="Cannot automatically resolve next tip with starting tip and partial tip configuration.", + ) + ), + ) diff --git a/app-shell-odd/package.json b/app-shell-odd/package.json index 9835b30df79..6ced69031f9 100644 --- a/app-shell-odd/package.json +++ b/app-shell-odd/package.json @@ -42,6 +42,7 @@ "dateformat": "3.0.3", "electron-devtools-installer": "3.2.0", "electron-store": "5.1.1", + "electron-updater": "6.3.9", "execa": "4.0.0", "form-data": "2.5.0", "fs-extra": "10.0.0", diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index 2a2b46ebb7c..5459cc2593e 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,10 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.3.0. It's for internal testing only. + ## Internal Release 2.3.0-alpha.0 This internal release, pulled from the `edge` branch, contains features being developed for evo tip functionality. It's for internal testing only. diff --git a/app-shell/package.json b/app-shell/package.json index 8c474d7247c..99dab77203d 100644 --- a/app-shell/package.json +++ b/app-shell/package.json @@ -50,6 +50,7 @@ "electron-localshortcut": "3.2.1", "electron-devtools-installer": "3.2.0", "electron-store": "5.1.1", + "electron-updater": "6.3.9", "execa": "4.0.0", "form-data": "2.5.0", "fs-extra": "10.0.0", diff --git a/app/package.json b/app/package.json index e87da02e4b0..33ae6252d1a 100644 --- a/app/package.json +++ b/app/package.json @@ -77,6 +77,7 @@ "@types/node-fetch": "2.6.11", "@types/styled-components": "^5.1.26", "axios": "^0.21.1", + "electron-updater": "6.3.9", "postcss-apply": "0.12.0", "postcss-color-mod-function": "3.0.3", "postcss-import": "16.0.0", diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index f73ac990fc6..1cdc29feab0 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,8 +1,8 @@ { "__dev_internal__enableLabwareCreator": "Enable App Labware Creator", - "__dev_internal__enableLocalization": "Enable App Localization", "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "__dev_internal__forceHttpPolling": "Poll all network requests instead of using MQTT", + "__dev_internal__lpcRedesign": "LPC Redesign", "__dev_internal__protocolStats": "Protocol Stats", "__dev_internal__protocolTimeline": "Protocol Timeline", "__dev_internal__reactQueryDevtools": "Enable React Query Devtools", diff --git a/app/src/assets/localization/zh/app_settings.json b/app/src/assets/localization/zh/app_settings.json index 3405d5edbfd..ace4ca5fb4f 100644 --- a/app/src/assets/localization/zh/app_settings.json +++ b/app/src/assets/localization/zh/app_settings.json @@ -1,6 +1,5 @@ { "__dev_internal__enableLabwareCreator": "启用应用实验耗材创建器", - "__dev_internal__enableLocalization": "Enable App Localization", "__dev_internal__forceHttpPolling": "强制轮询所有网络请求,而不是使用MQTT", "__dev_internal__enableRunNotes": "在协议运行期间显示备注", "__dev_internal__protocolStats": "协议统计", diff --git a/app/src/local-resources/instruments/__tests__/hooks.test.ts b/app/src/local-resources/instruments/__tests__/hooks.test.ts index 94b6043a125..468c2da5e0d 100644 --- a/app/src/local-resources/instruments/__tests__/hooks.test.ts +++ b/app/src/local-resources/instruments/__tests__/hooks.test.ts @@ -37,9 +37,11 @@ const mockP1000V2Specs = { 'opentrons/opentrons_flex_96_tiprack_1000ul/1', 'opentrons/opentrons_flex_96_tiprack_200ul/1', 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_tiprack_20ul/1', 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_20ul/1', ], minVolume: 5, maxVolume: 1000, diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx index 8ed93c1cb81..104085fcb15 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx @@ -2,7 +2,6 @@ import { useNavigate } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, vi, afterEach, beforeEach, expect } from 'vitest' -import { when } from 'vitest-when' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' @@ -10,7 +9,6 @@ import { getAppLanguage, getStoredSystemLanguage, updateConfigValue, - useFeatureFlag, } from '/app/redux/config' import { getSystemLanguage } from '/app/redux/shell' import { SystemLanguagePreferenceModal } from '..' @@ -34,9 +32,6 @@ describe('SystemLanguagePreferenceModal', () => { vi.mocked(getAppLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) vi.mocked(getSystemLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) vi.mocked(getStoredSystemLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) - when(vi.mocked(useFeatureFlag)) - .calledWith('enableLocalization') - .thenReturn(true) vi.mocked(useNavigate).mockReturnValue(mockNavigate) }) afterEach(() => { diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx index b4bf54c0d17..d3b04f19061 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx @@ -20,7 +20,6 @@ import { getAppLanguage, getStoredSystemLanguage, updateConfigValue, - useFeatureFlag, } from '/app/redux/config' import { getSystemLanguage } from '/app/redux/shell' @@ -33,7 +32,6 @@ type ArrayElement< export function SystemLanguagePreferenceModal(): JSX.Element | null { const { i18n, t } = useTranslation(['app_settings', 'shared', 'branded']) - const enableLocalization = useFeatureFlag('enableLocalization') const [currentOption, setCurrentOption] = useState( LANGUAGES[0] @@ -126,7 +124,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { } }, [i18n, systemLanguage, showBootModal]) - return enableLocalization && (showBootModal || showUpdateModal) ? ( + return showBootModal || showUpdateModal ? ( diff --git a/app/src/organisms/LabwareOffsetTabs/index.tsx b/app/src/organisms/LabwareOffsetTabs/index.tsx index 3ad81c51d01..0e32c8e5bd4 100644 --- a/app/src/organisms/LabwareOffsetTabs/index.tsx +++ b/app/src/organisms/LabwareOffsetTabs/index.tsx @@ -73,7 +73,6 @@ export function LabwareOffsetTabs({ (false) const updateAvailable = Boolean(useSelector(getAvailableShellUpdate)) - const enableLocalization = useFeatureFlag('enableLocalization') const appLanguage = useSelector(getAppLanguage) const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) @@ -277,7 +272,7 @@ export function GeneralSettings(): JSX.Element { - {enableLocalization && currentLanguageOption != null ? ( + {currentLanguageOption != null ? ( <> { vi.mocked(Shell.getAvailableShellUpdate).mockReturnValue(null) vi.mocked(getAlertIsPermanentlyIgnored).mockReturnValue(false) vi.mocked(getAppLanguage).mockReturnValue(US_ENGLISH) - when(vi.mocked(useFeatureFlag)) - .calledWith('enableLocalization') - .thenReturn(true) }) afterEach(() => { vi.resetAllMocks() diff --git a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx index cbc0d68e353..b44cc721e8a 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx @@ -33,7 +33,6 @@ import { toggleDevInternalFlag, toggleDevtools, toggleHistoricOffsets, - useFeatureFlag, } from '/app/redux/config' import { InlineNotification } from '/app/atoms/InlineNotification' import { getRobotSettings, updateSetting } from '/app/redux/robot-settings' @@ -90,7 +89,6 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { const appLanguage = useSelector(getAppLanguage) const currentLanguageOption = LANGUAGES.find(lng => lng.value === appLanguage) - const enableLocalization = useFeatureFlag('enableLocalization') return ( @@ -143,18 +141,16 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { } /> - {enableLocalization ? ( - { - setCurrentOption('LanguageSetting') - }} - iconName="language" - /> - ) : null} + { + setCurrentOption('LanguageSetting') + }} + iconName="language" + /> { toggleERSettings: mockToggleER, }) vi.mocked(getAppLanguage).mockReturnValue(MOCK_DEFAULT_LANGUAGE) - when(vi.mocked(useFeatureFlag)) - .calledWith('enableLocalization') - .thenReturn(true) }) afterEach(() => { diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index cc39206a191..1af99bc3733 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -4,9 +4,9 @@ export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'forceHttpPolling', 'protocolStats', 'enableRunNotes', + 'lpcRedesign', 'protocolTimeline', 'enableLabwareCreator', - 'enableLocalization', 'reactQueryDevtools', ] diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index ac87cd07576..8084ef14198 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -12,9 +12,9 @@ export type DevInternalFlag = | 'forceHttpPolling' | 'protocolStats' | 'enableRunNotes' + | 'lpcRedesign' | 'protocolTimeline' | 'enableLabwareCreator' - | 'enableLocalization' | 'reactQueryDevtools' export type FeatureFlags = Partial> diff --git a/app/src/redux/config/selectors.ts b/app/src/redux/config/selectors.ts index dbac2cd3c05..dea9cc3f171 100644 --- a/app/src/redux/config/selectors.ts +++ b/app/src/redux/config/selectors.ts @@ -148,7 +148,7 @@ export const getOnDeviceDisplaySettings: ( sleepMs: SLEEP_NEVER_MS, brightness: 4, textSize: 1, - unfinishedUnboxingFlowRoute: '/welcome', + unfinishedUnboxingFlowRoute: '/choose-language', } }) diff --git a/components/README.md b/components/README.md index 03680fccf37..be6918d11ae 100644 --- a/components/README.md +++ b/components/README.md @@ -18,7 +18,7 @@ export default function CowButton(props) { Usage requirements for dependent projects: -- Node v18 and yarn +- Node v22.11.0+ and yarn - The following `dependencies` (peer dependencies of `@opentrons/components`) - `react`: `17.0.1`, - `react-router-dom`: `^4.2.2`, diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx index 3064469f247..89cfffd983b 100644 --- a/components/src/organisms/Toolbox/index.tsx +++ b/components/src/organisms/Toolbox/index.tsx @@ -5,31 +5,27 @@ import { DIRECTION_COLUMN, JUSTIFY_SPACE_BETWEEN, NO_WRAP, - POSITION_FIXED, + POSITION_RELATIVE, } from '../../styles' import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING } from '../../ui-style-constants' import { PrimaryButton } from '../../atoms' import { textDecorationUnderline } from '../../ui-style-constants/typography' +import type { StyleProps } from '../../primitives' -export interface ToolboxProps { +export interface ToolboxProps extends StyleProps { title: JSX.Element children: React.ReactNode disableCloseButton?: boolean - width?: string - height?: string confirmButtonText?: string onConfirmClick?: () => void confirmButton?: JSX.Element onCloseClick?: () => void closeButton?: JSX.Element - side?: 'left' | 'right' - horizontalSide?: 'top' | 'bottom' titlePadding?: string childrenPadding?: string subHeader?: JSX.Element | null secondaryHeaderButton?: JSX.Element - position?: string } export function Toolbox(props: ToolboxProps): JSX.Element { @@ -43,14 +39,13 @@ export function Toolbox(props: ToolboxProps): JSX.Element { height = '100%', disableCloseButton = false, width = '19.5rem', - side = 'right', - horizontalSide = 'bottom', confirmButton, titlePadding = SPACING.spacing16, childrenPadding = SPACING.spacing16, subHeader, secondaryHeaderButton, - position = POSITION_FIXED, + position = POSITION_RELATIVE, + ...styleProps } = props const slideOutRef = useRef(null) @@ -69,26 +64,17 @@ export function Toolbox(props: ToolboxProps): JSX.Element { handleScroll() }, [slideOutRef]) - const positionStyles = - position === POSITION_FIXED - ? { - ...(side === 'right' && { right: '0' }), - ...(side === 'left' && { left: '0' }), - ...(horizontalSide === 'bottom' && { bottom: '0' }), - ...(horizontalSide === 'top' && { top: '5rem' }), - zIndex: 10, - } - : {} return ( - {subHeader != null ? subHeader : null} + {subHeader ?? null} {title} - {secondaryHeaderButton != null ? secondaryHeaderButton : null} + {secondaryHeaderButton ?? null} {onCloseClick != null && closeButton != null ? ( ) : null} - {confirmButton != null ? confirmButton : null} + {confirmButton ?? null} ) : null} diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index f6c70f087bf..e9475de8b95 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -271,8 +271,8 @@ async def liquid_probe( num_baseline_reads: int, sensor_id: SensorId = SensorId.S0, force_both_sensors: bool = False, - response_queue: Optional[ - asyncio.Queue[Dict[SensorId, List[SensorDataType]]] + emplace_data: Optional[ + Callable[[Dict[SensorId, List[SensorDataType]]], None] ] = None, ) -> Dict[NodeId, MotorPositionStatus]: """Move the mount and pipette simultaneously while reading from the pressure sensor.""" @@ -360,12 +360,12 @@ async def liquid_probe( await finalize_logs(messenger, tool, listeners, pressure_sensors) # give response data to any consumer that wants it - if response_queue: + if emplace_data: for s_id in listeners.keys(): data = listeners[s_id].get_data() if data: for d in data: - response_queue.put_nowait({s_id: data}) + emplace_data({s_id: data}) return positions diff --git a/opentrons-ai-client/src/assets/localization/en/create_protocol.json b/opentrons-ai-client/src/assets/localization/en/create_protocol.json index 6f744df2297..cc0f79bd011 100644 --- a/opentrons-ai-client/src/assets/localization/en/create_protocol.json +++ b/opentrons-ai-client/src/assets/localization/en/create_protocol.json @@ -4,6 +4,7 @@ "application_scientific_dropdown_placeholder": "Select an option", "basic_aliquoting": "Basic aliquoting", "pcr": "PCR", + "serial_dilution": "Serial dilution", "other": "Other", "application_other_title": "Other application", "application_other_caption": "Example: “cherrypicking” or “serial dilution”", diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx index 7226aa6a1a9..dffdcaa46d3 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx @@ -93,6 +93,12 @@ describe('ChatDisplay', () => { }) it('should call trackEvent when download button is clicked', () => { + props.chat = { + ...props.chat, + role: 'assistant', + reply: + '```python\ndef run(protocol):\n print("hello")\n print("protocol")\n return True\n```', + } URL.createObjectURL = vi.fn() window.URL.revokeObjectURL = vi.fn() HTMLAnchorElement.prototype.click = vi.fn() @@ -110,6 +116,24 @@ describe('ChatDisplay', () => { }) }) + it('should not call trackEvent when download button is clicked', () => { + URL.createObjectURL = vi.fn() + window.URL.revokeObjectURL = vi.fn() + HTMLAnchorElement.prototype.click = vi.fn() + + render(props) + // eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style + const downloadPath = document.querySelector( + '[aria-roledescription="download"]' + ) as Element + fireEvent.click(downloadPath) + + expect(mockUseTrackEvent).not.toHaveBeenCalledWith({ + name: 'download-protocol', + properties: {}, + }) + }) + it('should call trackEvent when copy button is clicked', async () => { Object.defineProperty(navigator, 'clipboard', { value: { diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 961fa93b445..b1b1e9df98a 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -106,7 +106,18 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const handleFileDownload = (): void => { const lastCodeBlock = document.querySelector(`#${chatId}`) - const code = lastCodeBlock?.textContent ?? '' + const code = lastCodeBlock?.textContent?.trim() ?? '' + // Don't proceed if code is empty, no need to download as a python file + if (!code) { + return + } + // Make sure python protocol is valid + const hasRunFunction = code.includes('def run(') + const numberOfLines = code.split('\n').length + if (!hasRunFunction || numberOfLines <= 3) { + return + } + const blobParts: BlobPart[] = [code] const file = new File(blobParts, 'OpentronsAI.py', { type: 'text/python' }) @@ -238,6 +249,7 @@ function ParagraphText(props: JSX.IntrinsicAttributes): JSX.Element { {...props} fontSize={TYPOGRAPHY.fontSize20} lineHeight={TYPOGRAPHY.lineHeight24} + css="white-space: pre-wrap;" /> ) } diff --git a/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx b/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx index 54819e99a3a..98cb7792020 100644 --- a/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx +++ b/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx @@ -6,6 +6,7 @@ import { ControlledInputField } from '../../atoms/ControlledInputField' export const BASIC_ALIQUOTING = 'basic_aliquoting' export const PCR = 'pcr' +export const SERIAL_DILUTION = 'serial_dilution' export const OTHER = 'other' export const APPLICATION_SCIENTIFIC_APPLICATION = 'application.scientificApplication' @@ -19,6 +20,7 @@ export function ApplicationSection(): JSX.Element | null { const options = [ { name: t(BASIC_ALIQUOTING), value: BASIC_ALIQUOTING }, { name: t(PCR), value: PCR }, + { name: t(SERIAL_DILUTION), value: SERIAL_DILUTION }, { name: t(OTHER), value: OTHER }, ] diff --git a/opentrons-ai-client/src/resources/constants.ts b/opentrons-ai-client/src/resources/constants.ts index 1b2620df1bf..2bd2701f5d1 100644 --- a/opentrons-ai-client/src/resources/constants.ts +++ b/opentrons-ai-client/src/resources/constants.ts @@ -8,12 +8,13 @@ export const STAGING_CREATE_PROTOCOL_END_POINT = export const STAGING_UPDATE_PROTOCOL_END_POINT = 'https://staging.opentrons.ai/api/chat/updateProtocol' -export const PROD_END_POINT = 'https://opentrons.ai/api/chat/completion' -export const PROD_FEEDBACK_END_POINT = 'https://opentrons.ai/api/chat/feedback' +export const PROD_END_POINT = 'https://ai.opentrons.com/api/chat/completion' +export const PROD_FEEDBACK_END_POINT = + 'https://ai.opentrons.com/api/chat/feedback' export const PROD_CREATE_PROTOCOL_END_POINT = - 'https://opentrons.ai/api/chat/createProtocol' + 'https://ai.opentrons.com/api/chat/createProtocol' export const PROD_UPDATE_PROTOCOL_END_POINT = - 'https://opentrons.ai/api/chat/updateProtocol' + 'https://ai.opentrons.com/api/chat/updateProtocol' // auth0 domain export const AUTH0_DOMAIN = 'identity.auth.opentrons.com' diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index b1510aaf89c..ae2369d6bd6 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -162,8 +162,10 @@ export function generateChatPrompt( const defs = getOnlyLatestDefs() const robotType = t(values.instruments.robot) - const scientificApplication = t(values.application.scientificApplication) - const description = values.application.description + const scientificApplication = `- ${t( + values.application.scientificApplication + )}` + const description = `- ${values.application.description}` const pipetteMounts = values.instruments.pipettes === TWO_PIPETTES ? [ @@ -209,9 +211,7 @@ export function generateChatPrompt( const prompt = `${t('create_protocol_prompt_robot', { robotType })}\n${t( 'application_title' - )}: \n${scientificApplication}\n\n${t( - 'description' - )}: \n${description}\n\n${t( + )}:\n${scientificApplication}\n\n${t('description')}:\n${description}\n\n${t( 'pipette_mounts' )}:\n\n${pipetteMounts}${flexGripper}\n\n${t( 'modules_title' diff --git a/opentrons-ai-server/README.md b/opentrons-ai-server/README.md index 041328a5c99..51fd4a2218c 100644 --- a/opentrons-ai-server/README.md +++ b/opentrons-ai-server/README.md @@ -9,9 +9,7 @@ The Opentrons AI server is a FastAPI server that handles complex tasks like runn Currently we have 2 environments: `staging` and `prod`. - staging: -- prod: - -If your browser blocks cross site cookies, use instead. +- prod: ### Environment Variables and Secrets @@ -33,7 +31,7 @@ The opentrons-ai-server/api/settings.py file manages environment variables and s 1. This allows formatting of of `.md` and `.json` files. 1. select the python version `pyenv local 3.12.6`. 1. This will create a `.python-version` file in this directory. -1. select the node version with `nvs` or `nvm` currently 18.19\*. +1. select the node version with `nvs` or `nvm` currently 22.11\*. 1. Install pipenv and python dependencies using `make setup`. 1. Install docker if you plan to run and build the docker container locally. 1. `make teardown` will remove the virtual environment but requires pipenv to be installed. diff --git a/package.json b/package.json index f439433535a..cde33247dd4 100755 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "download": "8.0.0", "electron": "33.2.1", "electron-builder": "25.1.8", - "electron-updater": "6.3.9", "eslint": "^8.56.0", "eslint-config-prettier": "^8.1.0", "eslint-config-standard": "^16.0.2", diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index b7509372dcd..4ec0c63f13f 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -239,7 +239,7 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { onClose() }} onCloseClick={handleClearSelectedWells} - height="calc(100vh - 64px)" + height="100%" closeButton={ {t('clear_wells')} diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx index e32a3e2d65d..a7c891e7c3c 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/index.tsx @@ -21,6 +21,7 @@ import { getSelectedWells } from '../../well-selection/selectors' import { SelectableLabware } from '../Labware/SelectableLabware' import { wellFillFromWellContents } from '../LabwareOnDeck/utils' import { deselectWells, selectWells } from '../../well-selection/actions' +import { PROTOCOL_NAV_BAR_HEIGHT_REM } from '../ProtocolNavBar' import { LiquidToolbox } from './LiquidToolbox' import type { WellGroup } from '@opentrons/components' @@ -51,18 +52,19 @@ export function AssignLiquidsModal(): JSX.Element | null { return ( ` z-index: ${props => (props.showShadow === true ? 11 : 0)}; padding: ${SPACING.spacing12}; + height: ${PROTOCOL_NAV_BAR_HEIGHT_REM}rem; width: 100%; justify-content: ${JUSTIFY_SPACE_BETWEEN}; align-items: ${ALIGN_CENTER}; diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx index 91e793f5005..53874bb8dc0 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx @@ -1,5 +1,6 @@ import { useMemo, useState, Fragment } from 'react' import { useDispatch, useSelector } from 'react-redux' +import round from 'lodash/round' import { ALIGN_CENTER, BORDERS, @@ -35,13 +36,13 @@ import { SlotDetailsContainer } from '../../../organisms' import { selectZoomedIntoSlot } from '../../../labware-ingred/actions' import { selectors } from '../../../labware-ingred/selectors' import { DeckSetupDetails } from './DeckSetupDetails' +import { DECK_SETUP_TOOLS_WIDTH_REM, DeckSetupTools } from './DeckSetupTools' import { animateZoom, getCutoutIdForAddressableArea, useDeckSetupWindowBreakPoint, zoomInOnCoordinate, } from './utils' -import { DeckSetupTools } from './DeckSetupTools' import type { StagingAreaLocation, TrashCutoutId } from '@opentrons/components' import type { @@ -122,19 +123,37 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { const hasWasteChute = wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0 + const windowInnerWidthRem = window.innerWidth / 16 + const deckMapRatio = round( + (windowInnerWidthRem - DECK_SETUP_TOOLS_WIDTH_REM) / windowInnerWidthRem, + 2 + ) + const viewBoxX = deckDef.cornerOffsetFromOrigin[0] const viewBoxY = hasWasteChute ? deckDef.cornerOffsetFromOrigin[1] - WASTE_CHUTE_SPACE - DETAILS_HOVER_SPACE : deckDef.cornerOffsetFromOrigin[1] - const viewBoxWidth = deckDef.dimensions[0] + const viewBoxWidth = deckDef.dimensions[0] / deckMapRatio const viewBoxHeight = deckDef.dimensions[1] + DETAILS_HOVER_SPACE const initialViewBox = `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}` const [viewBox, setViewBox] = useState(initialViewBox) + const isZoomed = Object.values(zoomIn).some(val => val != null) + const viewBoxNumerical = viewBox?.split(' ').map(val => Number(val)) ?? [] + const viewBoxAdjustedNumerical = [ + ...viewBoxNumerical.slice(0, 2), + (viewBoxNumerical[2] - viewBoxNumerical[0]) / deckMapRatio + + viewBoxNumerical[0], + viewBoxNumerical[3], + ] + const viewBoxAdjusted = viewBoxAdjustedNumerical.reduce((acc, num, i) => { + return i < viewBoxNumerical.length - 1 ? acc + `${num} ` : acc + `${num}` + }, '') + const [hoveredLabware, setHoveredLabware] = useState(null) const [hoveredModule, setHoveredModule] = useState(null) const [hoveredFixture, setHoveredFixture] = useState(null) @@ -202,7 +221,7 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { width="100%" height={tab === 'protocolSteps' ? '65.75vh' : '100%'} flexDirection={DIRECTION_COLUMN} - padding={SPACING.spacing24} + padding={isZoomed ? '0' : SPACING.spacing24} > void setHoveredFixture: (fixture: Fixture | null) => void } | null + position?: string } export type CategoryExpand = Record +export const DECK_SETUP_TOOLS_WIDTH_REM = 21.875 export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { - const { onCloseClick, setHoveredLabware, onDeckProps } = props + const { + onCloseClick, + setHoveredLabware, + onDeckProps, + position = POSITION_FIXED, + } = props const { t, i18n } = useTranslation(['starting_deck_state', 'shared']) const { makeSnackbar } = useKitchen() const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) @@ -329,10 +340,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { dispatch( createContainer({ slot, - labwareDefURI: - selectedNestedLabwareDefUri == null - ? selectedLabwareDefUri - : selectedNestedLabwareDefUri, + labwareDefURI: selectedNestedLabwareDefUri ?? selectedLabwareDefUri, adapterUnderLabwareDefURI: selectedNestedLabwareDefUri == null ? undefined @@ -358,6 +366,13 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) onCloseClick() } + const positionStyles = + position === POSITION_FIXED + ? { + right: SPACING.spacing12, + top: `calc(${PROTOCOL_NAV_BAR_HEIGHT_REM}rem + ${SPACING.spacing12})`, + } + : {} return ( <> {showDeleteLabwareModal != null ? ( @@ -382,8 +397,10 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { ) : null} {changeModuleWarning} {liquids.map(({ name, displayColor, ingredientId }) => { return ( diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index f9062b4b3c7..3759aabf4d5 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -62,7 +62,7 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { {selectedSlot.slot === 'offDeck' ? ( - + { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index fc452103584..b37b1eaebce 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -17,7 +17,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { stepIconsByType } from '../../../../form-types' -import { FormAlerts } from '../../../../organisms' +import { FormAlerts, PROTOCOL_NAV_BAR_HEIGHT_REM } from '../../../../organisms' import { useKitchen } from '../../../../organisms/Kitchen/hooks' import { RenameStepModal } from '../../../../organisms/RenameStepModal' import { getFormWarningsForSelectedStep } from '../../../../dismiss/selectors' @@ -272,7 +272,8 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { /> ) : null} { return ( @@ -69,7 +72,6 @@ export const TimelineToolbox = (): JSX.Element => { titlePadding={SPACING.spacing12} childrenPadding={SPACING.spacing12} confirmButton={formData != null ? undefined : } - height="calc(100vh - 6rem)" > = {}, + hostOverride?: HostConfig +): UseQueryResult { + const contextHost = useHost() + const host = + hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + + return useQuery( + [host, 'runs', runId, 'loaded_labware_definitions'], + () => + getRunLoadedLabwareDefintions(host as HostConfig, runId as string).then( + response => response.data + ), + { + enabled: host != null && runId != null && options.enabled !== false, + ...options, + } + ) +} diff --git a/robot-server/robot_server/labware_offsets/__init__.py b/robot-server/robot_server/labware_offsets/__init__.py new file mode 100644 index 00000000000..6d94b35d49a --- /dev/null +++ b/robot-server/robot_server/labware_offsets/__init__.py @@ -0,0 +1 @@ +"""Endpoints for storing and retrieving labware offsets.""" diff --git a/robot-server/robot_server/labware_offsets/fastapi_dependencies.py b/robot-server/robot_server/labware_offsets/fastapi_dependencies.py new file mode 100644 index 00000000000..f472dc96a64 --- /dev/null +++ b/robot-server/robot_server/labware_offsets/fastapi_dependencies.py @@ -0,0 +1,29 @@ +"""FastAPI dependencies for the `/labwareOffsets` endpoints.""" + + +from typing import Annotated + +from fastapi import Depends + +from server_utils.fastapi_utils.app_state import ( + AppState, + AppStateAccessor, + get_app_state, +) +from .store import LabwareOffsetStore + + +_labware_offset_store_accessor = AppStateAccessor[LabwareOffsetStore]( + "labware_offset_store" +) + + +async def get_labware_offset_store( + app_state: Annotated[AppState, Depends(get_app_state)], +) -> LabwareOffsetStore: + """Get the server's singleton LabwareOffsetStore.""" + labware_offset_store = _labware_offset_store_accessor.get_from(app_state) + if labware_offset_store is None: + labware_offset_store = LabwareOffsetStore() + _labware_offset_store_accessor.set_on(app_state, labware_offset_store) + return labware_offset_store diff --git a/robot-server/robot_server/labware_offsets/models.py b/robot-server/robot_server/labware_offsets/models.py new file mode 100644 index 00000000000..8c6dd5760f6 --- /dev/null +++ b/robot-server/robot_server/labware_offsets/models.py @@ -0,0 +1,19 @@ +"""Request/response models for the `/labwareOffsets` endpoints.""" + + +from typing import Literal, Type +from typing_extensions import Self + +from robot_server.errors.error_responses import ErrorDetails + + +class LabwareOffsetNotFound(ErrorDetails): + """An error returned when a requested labware offset does not exist.""" + + id: Literal["LabwareOffsetNotFound"] = "LabwareOffsetNotFound" + title: str = "Labware Offset Not Found" + + @classmethod + def build(cls: Type[Self], bad_offset_id: str) -> Self: + """Return an error with a standard message.""" + return cls.construct(detail=f'No offset found with ID "{bad_offset_id}".') diff --git a/robot-server/robot_server/labware_offsets/router.py b/robot-server/robot_server/labware_offsets/router.py new file mode 100644 index 00000000000..fb017fc1457 --- /dev/null +++ b/robot-server/robot_server/labware_offsets/router.py @@ -0,0 +1,189 @@ +"""FastAPI endpoint functions for the `/labwareOffsets` endpoints.""" + + +from datetime import datetime +import textwrap +from typing import Annotated, Literal + +import fastapi +from opentrons.protocol_engine import LabwareOffset, LabwareOffsetCreate, ModuleModel +from opentrons.types import DeckSlotName + +from robot_server.labware_offsets.models import LabwareOffsetNotFound +from robot_server.service.dependencies import get_current_time, get_unique_id +from robot_server.service.json_api.request import RequestModel +from robot_server.service.json_api.response import ( + MultiBodyMeta, + PydanticResponse, + SimpleBody, + SimpleEmptyBody, + SimpleMultiBody, +) + +from .store import LabwareOffsetNotFoundError, LabwareOffsetStore +from .fastapi_dependencies import get_labware_offset_store + + +router = fastapi.APIRouter(prefix="/labwareOffsets") + + +@PydanticResponse.wrap_route( + router.post, + path="", + summary="Store a labware offset", + description=textwrap.dedent( + """\ + Store a labware offset for later retrieval through `GET /labwareOffsets`. + + On its own, this does not affect robot motion. + To do that, you must add the offset to a run, through the `/runs` endpoints. + """ + ), + status_code=201, +) +async def post_labware_offset( # noqa: D103 + store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)], + new_offset_id: Annotated[str, fastapi.Depends(get_unique_id)], + new_offset_created_at: Annotated[datetime, fastapi.Depends(get_current_time)], + request_body: Annotated[RequestModel[LabwareOffsetCreate], fastapi.Body()], +) -> PydanticResponse[SimpleBody[LabwareOffset]]: + new_offset = LabwareOffset.construct( + id=new_offset_id, + createdAt=new_offset_created_at, + definitionUri=request_body.data.definitionUri, + location=request_body.data.location, + vector=request_body.data.vector, + ) + store.add(new_offset) + return await PydanticResponse.create( + content=SimpleBody.construct(data=new_offset), + status_code=201, + ) + + +@PydanticResponse.wrap_route( + router.get, + path="", + summary="Search for labware offsets", + description=( + "Get a filtered list of all the labware offsets currently stored on the robot." + " Filters are ANDed together." + " Results are returned in order from oldest to newest." + ), +) +async def get_labware_offsets( # noqa: D103 + store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)], + id: Annotated[ + str | None, + fastapi.Query(description="Filter for exact matches on the `id` field."), + ] = None, + definition_uri: Annotated[ + str | None, + fastapi.Query( + alias="definitionUri", + description=( + "Filter for exact matches on the `definitionUri` field." + " (Not to be confused with `location.definitionUri`.)" + ), + ), + ] = None, + location_slot_name: Annotated[ + DeckSlotName | None, + fastapi.Query( + alias="locationSlotName", + description="Filter for exact matches on the `location.slotName` field.", + ), + ] = None, + location_module_model: Annotated[ + ModuleModel | None, + fastapi.Query( + alias="locationModuleModel", + description="Filter for exact matches on the `location.moduleModel` field.", + ), + ] = None, + location_definition_uri: Annotated[ + str | None, + fastapi.Query( + alias="locationDefinitionUri", + description=( + "Filter for exact matches on the `location.definitionUri` field." + " (Not to be confused with just `definitionUri`.)" + ), + ), + ] = None, + cursor: Annotated[ + int | None, + fastapi.Query( + description=( + "The first index to return out of the overall filtered result list." + " If unspecified, defaults to returning `pageLength` elements from" + " the end of the list." + ) + ), + ] = None, + page_length: Annotated[ + int | Literal["unlimited"], + fastapi.Query( + alias="pageLength", description="The maximum number of entries to return." + ), + ] = "unlimited", +) -> PydanticResponse[SimpleMultiBody[LabwareOffset]]: + if cursor not in (0, None) or page_length != "unlimited": + # todo(mm, 2024-12-06): Support this when LabwareOffsetStore supports it. + raise NotImplementedError( + "Pagination not currently supported on this endpoint." + ) + + result_data = store.search( + id_filter=id, + definition_uri_filter=definition_uri, + location_slot_name_filter=location_slot_name, + location_definition_uri_filter=location_definition_uri, + location_module_model_filter=location_module_model, + ) + + meta = MultiBodyMeta.construct( + # todo(mm, 2024-12-06): Update this when pagination is supported. + cursor=0, + totalLength=len(result_data), + ) + + return await PydanticResponse.create( + SimpleMultiBody[LabwareOffset].construct( + data=result_data, + meta=meta, + ) + ) + + +@PydanticResponse.wrap_route( + router.delete, + path="/{id}", + summary="Delete a single labware offset", + description="Delete a single labware offset. The deleted offset is returned.", +) +async def delete_labware_offset( # noqa: D103 + store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)], + id: Annotated[ + str, + fastapi.Path(description="The `id` field of the offset to delete."), + ], +) -> PydanticResponse[SimpleBody[LabwareOffset]]: + try: + deleted_offset = store.delete(offset_id=id) + except LabwareOffsetNotFoundError as e: + raise LabwareOffsetNotFound.build(bad_offset_id=e.bad_offset_id).as_error(404) + else: + return await PydanticResponse.create(SimpleBody.construct(data=deleted_offset)) + + +@PydanticResponse.wrap_route( + router.delete, + path="", + summary="Delete all labware offsets", +) +async def delete_all_labware_offsets( # noqa: D103 + store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)] +) -> PydanticResponse[SimpleEmptyBody]: + store.delete_all() + return await PydanticResponse.create(SimpleEmptyBody.construct()) diff --git a/robot-server/robot_server/labware_offsets/store.py b/robot-server/robot_server/labware_offsets/store.py new file mode 100644 index 00000000000..9f7e76cfd57 --- /dev/null +++ b/robot-server/robot_server/labware_offsets/store.py @@ -0,0 +1,68 @@ +# noqa: D100 + +from opentrons.protocol_engine import LabwareOffset, ModuleModel +from opentrons.types import DeckSlotName + + +# todo(mm, 2024-12-06): Convert to be SQL-based and persistent instead of in-memory. +# https://opentrons.atlassian.net/browse/EXEC-1015 +class LabwareOffsetStore: + """A persistent store for labware offsets, to support the `/labwareOffsets` endpoints.""" + + def __init__(self) -> None: + self._offsets_by_id: dict[str, LabwareOffset] = {} + + def add(self, offset: LabwareOffset) -> None: + """Store a new labware offset.""" + assert offset.id not in self._offsets_by_id + self._offsets_by_id[offset.id] = offset + + def search( + self, + id_filter: str | None, + definition_uri_filter: str | None, + location_slot_name_filter: DeckSlotName | None, + location_module_model_filter: ModuleModel | None, + location_definition_uri_filter: str | None, + # todo(mm, 2024-12-06): Support pagination (cursor & pageLength query params). + # The logic for that is currently duplicated across several places in + # robot-server and api. We should try to clean that up, or at least avoid + # making it worse. + ) -> list[LabwareOffset]: + """Return all matching labware offsets in order from oldest-added to newest.""" + + def is_match(candidate: LabwareOffset) -> bool: + return ( + id_filter in (None, candidate.id) + and definition_uri_filter in (None, candidate.definitionUri) + and location_slot_name_filter in (None, candidate.location.slotName) + and location_module_model_filter + in (None, candidate.location.moduleModel) + and location_definition_uri_filter + in (None, candidate.location.definitionUri) + ) + + return [ + candidate + for candidate in self._offsets_by_id.values() + if is_match(candidate) + ] + + def delete(self, offset_id: str) -> LabwareOffset: + """Delete a labware offset by its ID. Return what was just deleted.""" + try: + return self._offsets_by_id.pop(offset_id) + except KeyError: + raise LabwareOffsetNotFoundError(bad_offset_id=offset_id) from None + + def delete_all(self) -> None: + """Delete all labware offsets.""" + self._offsets_by_id.clear() + + +class LabwareOffsetNotFoundError(KeyError): + """Raised when trying to access a labware offset that doesn't exist.""" + + def __init__(self, bad_offset_id: str) -> None: + super().__init__(bad_offset_id) + self.bad_offset_id = bad_offset_id diff --git a/robot-server/robot_server/router.py b/robot-server/robot_server/router.py index 63ae4e5ab43..49d835d7eb9 100644 --- a/robot-server/robot_server/router.py +++ b/robot-server/robot_server/router.py @@ -7,14 +7,15 @@ from .client_data.router import router as client_data_router from .commands.router import commands_router +from .data_files.router import datafiles_router from .deck_configuration.router import router as deck_configuration_router from .error_recovery.settings.router import router as error_recovery_settings_router from .health.router import health_router from .instruments.router import instruments_router +from .labware_offsets.router import router as labware_offset_router from .maintenance_runs.router import maintenance_runs_router from .modules.router import modules_router from .protocols.router import protocols_router -from .data_files.router import datafiles_router from .robot.router import robot_router from .runs.router import runs_router from .service.labware.router import router as labware_router @@ -55,6 +56,12 @@ dependencies=[Depends(check_version_header)], ) +router.include_router( + router=labware_offset_router, + tags=["Labware Offset Management"], + dependencies=[Depends(check_version_header)], +) + router.include_router( router=runs_router, tags=["Run Management"], @@ -78,6 +85,7 @@ tags=["Data files Management"], dependencies=[Depends(check_version_header)], ) + router.include_router( router=commands_router, tags=["Simple Commands"], diff --git a/robot-server/tests/integration/conftest.py b/robot-server/tests/integration/conftest.py index 0f96913a8f4..33e8b6c3472 100644 --- a/robot-server/tests/integration/conftest.py +++ b/robot-server/tests/integration/conftest.py @@ -132,8 +132,8 @@ async def _clean_server_state_async() -> None: await _reset_deck_configuration(robot_client) await _reset_error_recovery_settings(robot_client) - await _delete_client_data(robot_client) + await _delete_labware_offsets(robot_client) asyncio.run(_clean_server_state_async()) @@ -179,3 +179,7 @@ async def _reset_deck_configuration(robot_client: RobotClient) -> None: async def _reset_error_recovery_settings(robot_client: RobotClient) -> None: await robot_client.delete_error_recovery_settings() + + +async def _delete_labware_offsets(robot_client: RobotClient) -> None: + await robot_client.delete_all_labware_offsets() diff --git a/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml b/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml new file mode 100644 index 00000000000..d9ff35d7136 --- /dev/null +++ b/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml @@ -0,0 +1,131 @@ +test_name: Test /labwareOffsets CRUD operations. + +marks: + - usefixtures: + - ot3_server_base_url + +stages: + - name: Add a labware offset and check the response + request: + url: '{ot3_server_base_url}/labwareOffsets' + method: POST + json: + data: + definitionUri: definitionUri1 + location: + slotName: A1 + definitionUri: testNamespace/testLoadName/123 + moduleModel: thermocyclerModuleV2 + vector: { x: 1, y: 1, z: 1 } + response: + status_code: 201 + json: + data: + id: !anystr + createdAt: !anystr + definitionUri: definitionUri1 + location: + slotName: A1 + definitionUri: testNamespace/testLoadName/123 + moduleModel: thermocyclerModuleV2 + vector: { x: 1, y: 1, z: 1 } + save: + json: + offset_1_data: data + offset_1_id: data.id + + - name: Add another labware offset to add more testing data + request: + url: '{ot3_server_base_url}/labwareOffsets' + method: POST + json: + data: + definitionUri: definitionUri2 + location: + slotName: A2 + vector: { x: 2, y: 2, z: 2 } + response: + status_code: 201 + save: + json: + offset_2_data: data + + - name: Add another labware offset to add more testing data + request: + url: '{ot3_server_base_url}/labwareOffsets' + method: POST + json: + data: + definitionUri: definitionUri3 + location: + slotName: A3 + vector: { x: 3, y: 3, z: 3 } + response: + status_code: 201 + save: + json: + offset_3_data: data + + - name: Test getting all labware offsets + request: + url: '{ot3_server_base_url}/labwareOffsets' + method: GET + response: + json: + data: + - !force_format_include '{offset_1_data}' + - !force_format_include '{offset_2_data}' + - !force_format_include '{offset_3_data}' + meta: + cursor: 0 + totalLength: 3 + + # Just a basic test here. More complicated tests for the filters belong in the unit tests. + - name: Test getting labware offsets with a filter + request: + url: '{ot3_server_base_url}/labwareOffsets?locationSlotName=A2' + method: GET + response: + json: + data: + - !force_format_include '{offset_2_data}' + meta: + cursor: 0 + totalLength: 1 + + - name: Delete a labware offset + request: + url: '{ot3_server_base_url}/labwareOffsets/{offset_1_id}' + method: DELETE + response: + json: + data: !force_format_include '{offset_1_data}' + + - name: Make sure it got deleted + request: + url: '{ot3_server_base_url}/labwareOffsets' + response: + json: + data: + - !force_format_include '{offset_2_data}' + - !force_format_include '{offset_3_data}' + meta: + cursor: 0 + totalLength: 2 + + - name: Delete all labware offsets + request: + url: '{ot3_server_base_url}/labwareOffsets' + method: DELETE + response: + json: {} + + - name: Make sure they all got deleted + request: + url: '{ot3_server_base_url}/labwareOffsets' + response: + json: + data: [] + meta: + cursor: 0 + totalLength: 0 diff --git a/robot-server/tests/integration/robot_client.py b/robot-server/tests/integration/robot_client.py index 9db9409d90a..7e6b70c09f6 100644 --- a/robot-server/tests/integration/robot_client.py +++ b/robot-server/tests/integration/robot_client.py @@ -384,6 +384,11 @@ async def delete_error_recovery_settings(self) -> Response: response.raise_for_status() return response + async def delete_all_labware_offsets(self) -> Response: + response = await self.httpx_client.delete(url=f"{self.base_url}/labwareOffsets") + response.raise_for_status() + return response + async def poll_until_run_completes( robot_client: RobotClient, run_id: str, poll_interval: float = _RUN_POLL_INTERVAL diff --git a/robot-server/tests/labware_offsets/test_store.py b/robot-server/tests/labware_offsets/test_store.py new file mode 100644 index 00000000000..0f28f2e6825 --- /dev/null +++ b/robot-server/tests/labware_offsets/test_store.py @@ -0,0 +1,124 @@ +# noqa: D100 + + +from datetime import datetime +from opentrons.protocol_engine import ( + LabwareOffset, + LabwareOffsetLocation, + LabwareOffsetVector, +) +from opentrons.types import DeckSlotName +import pytest +from robot_server.labware_offsets.store import ( + LabwareOffsetStore, + LabwareOffsetNotFoundError, +) + + +def _get_all(store: LabwareOffsetStore) -> list[LabwareOffset]: + return store.search( + id_filter=None, + definition_uri_filter=None, + location_definition_uri_filter=None, + location_module_model_filter=None, + location_slot_name_filter=None, + ) + + +def test_filters() -> None: + """Test that the `.search()` method applies filters correctly.""" + ids_and_definition_uris = [ + ("id-1", "definition-uri-a"), + ("id-2", "definition-uri-b"), + ("id-3", "definition-uri-a"), + ("id-4", "definition-uri-b"), + ("id-5", "definition-uri-a"), + ("id-6", "definition-uri-b"), + ] + labware_offsets = [ + LabwareOffset( + id=id, + createdAt=datetime.now(), + definitionUri=definition_uri, + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + for (id, definition_uri) in ids_and_definition_uris + ] + + subject = LabwareOffsetStore() + + for labware_offset in labware_offsets: + subject.add(labware_offset) + + # No filters: + assert ( + subject.search( + id_filter=None, + definition_uri_filter=None, + location_definition_uri_filter=None, + location_module_model_filter=None, + location_slot_name_filter=None, + ) + == labware_offsets + ) + + # Filter on one thing: + result = subject.search( + id_filter=None, + definition_uri_filter="definition-uri-b", + location_definition_uri_filter=None, + location_module_model_filter=None, + location_slot_name_filter=None, + ) + assert len(result) == 3 + assert result == [ + entry for entry in labware_offsets if entry.definitionUri == "definition-uri-b" + ] + + # Filter on two things: + result = subject.search( + id_filter="id-2", + definition_uri_filter="definition-uri-b", + location_definition_uri_filter=None, + location_module_model_filter=None, + location_slot_name_filter=None, + ) + assert result == [labware_offsets[1]] + + # Filters should be ANDed, not ORed, together: + result = subject.search( + id_filter="id-1", + definition_uri_filter="definition-uri-b", + location_definition_uri_filter=None, + location_module_model_filter=None, + location_slot_name_filter=None, + ) + assert result == [] + + +def test_delete() -> None: + """Test the `delete()` method.""" + a, b, c = [ + LabwareOffset( + id=id, + createdAt=datetime.now(), + definitionUri="", + location=LabwareOffsetLocation(slotName=DeckSlotName.SLOT_A1), + vector=LabwareOffsetVector(x=1, y=2, z=3), + ) + for id in ["id-a", "id-b", "id-c"] + ] + + subject = LabwareOffsetStore() + + with pytest.raises(LabwareOffsetNotFoundError): + subject.delete("b") + + subject.add(a) + subject.add(b) + subject.add(c) + assert subject.delete(b.id) == b + assert _get_all(subject) == [a, c] + with pytest.raises(LabwareOffsetNotFoundError): + subject.delete(b.id) diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 2df16125574..955dff87690 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -42,6 +42,7 @@ "setStatusBar": "#/definitions/SetStatusBarCreate", "verifyTipPresence": "#/definitions/VerifyTipPresenceCreate", "getTipPresence": "#/definitions/GetTipPresenceCreate", + "getNextTip": "#/definitions/GetNextTipCreate", "liquidProbe": "#/definitions/LiquidProbeCreate", "tryLiquidProbe": "#/definitions/TryLiquidProbeCreate", "heaterShaker/waitForTemperature": "#/definitions/opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate", @@ -199,6 +200,9 @@ { "$ref": "#/definitions/GetTipPresenceCreate" }, + { + "$ref": "#/definitions/GetNextTipCreate" + }, { "$ref": "#/definitions/LiquidProbeCreate" }, @@ -4123,6 +4127,62 @@ }, "required": ["params"] }, + "GetNextTipParams": { + "title": "GetNextTipParams", + "description": "Payload needed to resolve the next available tip.", + "type": "object", + "properties": { + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, + "labwareIds": { + "title": "Labwareids", + "description": "Labware ID(s) of tip racks to resolve next available tip(s) from Labware IDs will be resolved sequentially", + "type": "array", + "items": { + "type": "string" + } + }, + "startingTipWell": { + "title": "Startingtipwell", + "description": "Name of starting tip rack 'well'. This only applies to the first tip rack in the list provided in labwareIDs", + "type": "string" + } + }, + "required": ["pipetteId", "labwareIds"] + }, + "GetNextTipCreate": { + "title": "GetNextTipCreate", + "description": "Get next tip command creation request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "getNextTip", + "enum": ["getNextTip"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/GetNextTipParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "LiquidProbeParams": { "title": "LiquidProbeParams", "description": "Parameters required for a `liquidProbe` command.", diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index a30ad6dfe18..99895132440 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -86,9 +86,11 @@ describe('pipette data accessors', () => { 'opentrons/opentrons_flex_96_tiprack_1000ul/1', 'opentrons/opentrons_flex_96_tiprack_200ul/1', 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_tiprack_20ul/1', 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_20ul/1', ], minVolume: 5, maxVolume: 1000, @@ -170,15 +172,52 @@ describe('pipette data accessors', () => { }) }) it('returns the correct liquid info for a p50 pipette model version with default and lowVolume', () => { - const tiprack50uL = 'opentrons/opentrons_flex_96_tiprack_50ul/1' - const tiprackFilter50uL = 'opentrons/opentrons_flex_96_filtertiprack_50ul/1' - const mockLiquidDefault = { $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', - defaultTipracks: [tiprack50uL, tiprackFilter50uL], + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_tiprack_20ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_20ul/1', + ], maxVolume: 50, minVolume: 5, supportedTips: { + t20: { + uiMaxFlowRate: 57, + aspirate: { + default: { + 1: expect.anything(), + }, + }, + defaultAspirateFlowRate: { + default: 35, + valuesByApiLevel: { + '2.14': 35, + }, + }, + defaultBlowOutFlowRate: { + default: 57, + valuesByApiLevel: { + '2.14': 57, + }, + }, + defaultDispenseFlowRate: { + default: 57, + valuesByApiLevel: { + '2.14': 57, + }, + }, + defaultFlowAcceleration: 1200, + defaultPushOutVolume: 2, + defaultReturnTipHeight: 0.71, + defaultTipLength: 52.0, + dispense: { + default: { + 1: expect.anything(), + }, + }, + }, t50: { uiMaxFlowRate: 57, aspirate: { @@ -218,10 +257,50 @@ describe('pipette data accessors', () => { } as PipetteV2LiquidSpecs const mockLiquidLowVolume = { $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', - defaultTipracks: [tiprack50uL, tiprackFilter50uL], + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_tiprack_20ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_20ul/1', + ], maxVolume: 30, minVolume: 1, supportedTips: { + t20: { + uiMaxFlowRate: 26.7, + aspirate: { + default: { + 1: expect.anything(), + }, + }, + defaultAspirateFlowRate: { + default: 26.7, + valuesByApiLevel: { + '2.14': 26.7, + }, + }, + defaultBlowOutFlowRate: { + default: 26.7, + valuesByApiLevel: { + '2.14': 26.7, + }, + }, + defaultDispenseFlowRate: { + default: 26.7, + valuesByApiLevel: { + '2.14': 26.7, + }, + }, + defaultFlowAcceleration: 1200, + defaultPushOutVolume: 7, + defaultReturnTipHeight: 0.71, + defaultTipLength: 52.0, + dispense: { + default: { + 1: expect.anything(), + }, + }, + }, t50: { uiMaxFlowRate: 26.7, aspirate: { diff --git a/shared-data/labware/definitions/2/opentrons_flex_96_filtertiprack_20ul/1.json b/shared-data/labware/definitions/2/opentrons_flex_96_filtertiprack_20ul/1.json new file mode 100644 index 00000000000..019410060f7 --- /dev/null +++ b/shared-data/labware/definitions/2/opentrons_flex_96_filtertiprack_20ul/1.json @@ -0,0 +1,1026 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "metadata": { + "displayName": "Opentrons Flex 96 Filter Tip Rack 20 µL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "wells": { + "A1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 11.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 11.38, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 52.0, + "tipOverlap": 10.5, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_flex_96_filtertiprack_20ul" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + } +} diff --git a/shared-data/labware/definitions/2/opentrons_flex_96_tiprack_20ul/1.json b/shared-data/labware/definitions/2/opentrons_flex_96_tiprack_20ul/1.json new file mode 100644 index 00000000000..106f414aa2f --- /dev/null +++ b/shared-data/labware/definitions/2/opentrons_flex_96_tiprack_20ul/1.json @@ -0,0 +1,1026 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "metadata": { + "displayName": "Opentrons Flex 96 Tip Rack 20 µL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "wells": { + "A1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 86.38, + "y": 11.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 20, + "x": 113.38, + "y": 11.38, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 52.0, + "tipOverlap": 10.5, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_flex_96_tiprack_20ul" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + } +} diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json index 95292a3f98b..1b734fe1011 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 802.9, "defaultAspirateFlowRate": { @@ -229,8 +280,10 @@ "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", "opentrons/opentrons_flex_96_filtertiprack_200ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json index caa2de5cb48..ff9676e70eb 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json @@ -1,6 +1,71 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.462, 0.5646, 0.0415], + [0.648, 0.3716, 0.1307], + [1.032, 0.2742, 0.1938], + [1.37, 0.1499, 0.3221], + [2.014, 0.1044, 0.3845], + [2.772, 0.0432, 0.5076], + [3.05, -0.0809, 0.8517], + [3.4, 0.0256, 0.5268], + [3.962, 0.0612, 0.4057], + [4.438, 0.0572, 0.4217], + [5.164, 0.018, 0.5955], + [5.966, 0.0095, 0.6393], + [7.38, 0.0075, 0.6514], + [9.128, 0.0049, 0.6705], + [10.16, 0.0033, 0.6854], + [13.812, 0.0024, 0.6948], + [21, 0.0024, 0.6948] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.462, 0.5646, 0.0415], + [0.648, 0.3716, 0.1307], + [1.032, 0.2742, 0.1938], + [1.37, 0.1499, 0.3221], + [2.014, 0.1044, 0.3845], + [2.772, 0.0432, 0.5076], + [3.05, -0.0809, 0.8517], + [3.4, 0.0256, 0.5268], + [3.962, 0.0612, 0.4057], + [4.438, 0.0572, 0.4217], + [5.164, 0.018, 0.5955], + [5.966, 0.0095, 0.6393], + [7.38, 0.0075, 0.6514], + [9.128, 0.0049, 0.6705], + [10.16, 0.0033, 0.6854], + [13.812, 0.0024, 0.6948], + [21, 0.0024, 0.6948] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 57, "defaultAspirateFlowRate": { @@ -85,6 +150,8 @@ "minVolume": 5, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_20ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json index acffd367df2..4d59ad3d7e4 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json @@ -1,6 +1,69 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 26.7, + "defaultAspirateFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultDispenseFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultBlowOutFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.11, 0.207815, 0.040201], + [0.65, 0.43933, 0.014735], + [1.04, 0.256666, 0.133466], + [1.67, 0.147126, 0.247388], + [2.45, 0.078774, 0.361536], + [2.89, 0.042387, 0.450684], + [3.2, 0.014781, 0.530464], + [3.79, 0.071819, 0.347944], + [4.22, 0.051592, 0.424605], + [4.93, 0.021219, 0.552775], + [5.81, 0.023461, 0.541725], + [7.21, 0.008959, 0.625982], + [8.93, 0.005456, 0.651235], + [10.0, 0.007108, 0.636489], + [13.61, 0.002591, 0.681656], + [21.61, 0.002591, 0.681656] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.11, 0.207815, 0.040201], + [0.65, 0.43933, 0.014735], + [1.04, 0.256666, 0.133466], + [1.67, 0.147126, 0.247388], + [2.45, 0.078774, 0.361536], + [2.89, 0.042387, 0.450684], + [3.2, 0.014781, 0.530464], + [3.79, 0.071819, 0.347944], + [4.22, 0.051592, 0.424605], + [4.93, 0.021219, 0.552775], + [5.81, 0.023461, 0.541725], + [7.21, 0.008959, 0.625982], + [8.93, 0.005456, 0.651235], + [10.0, 0.007108, 0.636489], + [13.61, 0.002591, 0.681656], + [21.61, 0.002591, 0.681656] + ] + } + }, + "defaultPushOutVolume": 7 + }, "t50": { "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { @@ -83,6 +146,8 @@ "minVolume": 1, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_20ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json index 6c490c82962..997a7785422 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 189.1, + "defaultAspirateFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultDispenseFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.6, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { @@ -178,6 +229,7 @@ "maxVolume": 1000, "minVolume": 5, "defaultTipracks": [ + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json index 43e7cb88798..95f555dff89 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 189.1, + "defaultAspirateFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultDispenseFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.6, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 194, "defaultAspirateFlowRate": { @@ -181,6 +232,7 @@ "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", "opentrons/opentrons_flex_96_filtertiprack_200ul/1", "opentrons/opentrons_flex_96_filtertiprack_50ul/1" diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json index ef067186a90..6a371944682 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p200/default/3_0.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 189.1, + "defaultAspirateFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultDispenseFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.6, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 194, "defaultAspirateFlowRate": { @@ -115,7 +166,9 @@ "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_filtertiprack_200ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json index a1f0a806fa2..4f0ee9103b2 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json @@ -16,7 +16,7 @@ "valuesByApiLevel": { "2.14": 478 } }, "defaultFlowAcceleration": 24000.0, - "defaultTipLength": 57.9, + "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { "default": { diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json index 8a322ace79b..c70853beff3 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { @@ -197,8 +248,10 @@ "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", "opentrons/opentrons_flex_96_filtertiprack_200ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json index 5f49411d926..c8c3a02b398 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { @@ -197,8 +248,10 @@ "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", "opentrons/opentrons_flex_96_filtertiprack_200ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json index 5f49411d926..c8c3a02b398 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_7.json @@ -1,6 +1,57 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { @@ -197,8 +248,10 @@ "opentrons/opentrons_flex_96_tiprack_1000ul/1", "opentrons/opentrons_flex_96_tiprack_200ul/1", "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_tiprack_20ul/1", "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", "opentrons/opentrons_flex_96_filtertiprack_200ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json index 7fe702bc97d..91bcb28e628 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json @@ -1,7 +1,7 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { - "t50": { + "t20": { "uiMaxFlowRate": 57, "defaultAspirateFlowRate": { "default": 35, @@ -16,7 +16,7 @@ "valuesByApiLevel": { "2.14": 57 } }, "defaultFlowAcceleration": 1200.0, - "defaultTipLength": 57.9, + "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { "default": { @@ -37,9 +37,7 @@ [9.128, 0.0049, 0.6705], [10.16, 0.0033, 0.6854], [13.812, 0.0024, 0.6948], - [27.204, 0.0008, 0.7165], - [50.614, 0.0002, 0.7328], - [53.046, -0.0005, 0.7676] + [21, 0.0024, 0.6948] ] } }, @@ -62,9 +60,76 @@ [9.128, 0.0049, 0.6705], [10.16, 0.0033, 0.6854], [13.812, 0.0024, 0.6948], - [27.204, 0.0008, 0.7165], - [50.614, 0.0002, 0.7328], - [53.046, -0.0005, 0.7676] + [21, 0.0024, 0.6948] + ] + } + }, + "defaultPushOutVolume": 2 + }, + "t50": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.410909, 0.493556, 0.066133], + [0.596364, 0.393336, 0.107314], + [0.977273, 0.287768, 0.170271], + [1.332727, 0.173299, 0.282139], + [1.943636, 0.099498, 0.380495], + [2.704545, 0.050285, 0.476146], + [3.024545, -0.038371, 0.715922], + [3.313636, -0.005484, 0.616454], + [3.926364, 0.072085, 0.359418], + [4.257273, 0.016758, 0.576653], + [4.992727, 0.024144, 0.545207], + [5.83, 0.017456, 0.5786], + [7.226364, 0.008433, 0.631202], + [9.033636, 0.00882, 0.628403], + [10.071818, 0.004396, 0.668375], + [13.759091, 0.003288, 0.679527], + [27.283636, 0.001159, 0.708828], + [50.902727, 0.000289, 0.732565], + [53.340909, -0.000532, 0.77433] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.410909, 0.493556, 0.066133], + [0.596364, 0.393336, 0.107314], + [0.977273, 0.287768, 0.170271], + [1.332727, 0.173299, 0.282139], + [1.943636, 0.099498, 0.380495], + [2.704545, 0.050285, 0.476146], + [3.024545, -0.038371, 0.715922], + [3.313636, -0.005484, 0.616454], + [3.926364, 0.072085, 0.359418], + [4.257273, 0.016758, 0.576653], + [4.992727, 0.024144, 0.545207], + [5.83, 0.017456, 0.5786], + [7.226364, 0.008433, 0.631202], + [9.033636, 0.00882, 0.628403], + [10.071818, 0.004396, 0.668375], + [13.759091, 0.003288, 0.679527], + [27.283636, 0.001159, 0.708828], + [50.902727, 0.000289, 0.732565], + [53.340909, -0.000532, 0.77433] ] } }, @@ -75,6 +140,8 @@ "minVolume": 5, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_20ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json index 474bfca8df5..e2071093e13 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_6.json @@ -1,6 +1,71 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 57, + "defaultAspirateFlowRate": { + "default": 35, + "valuesByApiLevel": { "2.14": 35 } + }, + "defaultDispenseFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultBlowOutFlowRate": { + "default": 57, + "valuesByApiLevel": { "2.14": 57 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.462, 0.5646, 0.0415], + [0.648, 0.3716, 0.1307], + [1.032, 0.2742, 0.1938], + [1.37, 0.1499, 0.3221], + [2.014, 0.1044, 0.3845], + [2.772, 0.0432, 0.5076], + [3.05, -0.0809, 0.8517], + [3.4, 0.0256, 0.5268], + [3.962, 0.0612, 0.4057], + [4.438, 0.0572, 0.4217], + [5.164, 0.018, 0.5955], + [5.966, 0.0095, 0.6393], + [7.38, 0.0075, 0.6514], + [9.128, 0.0049, 0.6705], + [10.16, 0.0033, 0.6854], + [13.812, 0.0024, 0.6948], + [21, 0.0024, 0.6948] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.462, 0.5646, 0.0415], + [0.648, 0.3716, 0.1307], + [1.032, 0.2742, 0.1938], + [1.37, 0.1499, 0.3221], + [2.014, 0.1044, 0.3845], + [2.772, 0.0432, 0.5076], + [3.05, -0.0809, 0.8517], + [3.4, 0.0256, 0.5268], + [3.962, 0.0612, 0.4057], + [4.438, 0.0572, 0.4217], + [5.164, 0.018, 0.5955], + [5.966, 0.0095, 0.6393], + [7.38, 0.0075, 0.6514], + [9.128, 0.0049, 0.6705], + [10.16, 0.0033, 0.6854], + [13.812, 0.0024, 0.6948], + [21, 0.0024, 0.6948] + ] + } + }, + "defaultPushOutVolume": 2 + }, "t50": { "uiMaxFlowRate": 57, "defaultAspirateFlowRate": { @@ -75,6 +140,8 @@ "minVolume": 5, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_20ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json index 33e1410ce99..d5c3e11c7ba 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json @@ -1,7 +1,7 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { - "t50": { + "t20": { "uiMaxFlowRate": 26.7, "defaultAspirateFlowRate": { "default": 26.7, @@ -16,7 +16,7 @@ "valuesByApiLevel": { "2.14": 26.7 } }, "defaultFlowAcceleration": 1200.0, - "defaultTipLength": 57.9, + "defaultTipLength": 52.0, "defaultReturnTipHeight": 0.71, "aspirate": { "default": { @@ -36,8 +36,7 @@ [8.93, 0.005456, 0.651235], [10.0, 0.007108, 0.636489], [13.61, 0.002591, 0.681656], - [26.99, 0.001163, 0.701094], - [45.25, 0.000207, 0.726887] + [21.61, 0.002591, 0.681656] ] } }, @@ -59,8 +58,74 @@ [8.93, 0.005456, 0.651235], [10.0, 0.007108, 0.636489], [13.61, 0.002591, 0.681656], - [26.99, 0.001163, 0.701094], - [45.25, 0.000207, 0.726887] + [21.61, 0.002591, 0.681656] + ] + } + }, + "defaultPushOutVolume": 7 + }, + "t50": { + "uiMaxFlowRate": 26.7, + "defaultAspirateFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultDispenseFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultBlowOutFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.4, 0.507042, 0.058982], + [0.593, 0.404962, 0.099815], + [0.966, 0.285078, 0.170906], + [1.326, 0.178383, 0.273973], + [1.947, 0.103651, 0.373068], + [2.72, 0.052739, 0.472193], + [3.045, -0.036061, 0.71373], + [3.369, 0.013437, 0.563006], + [3.972, 0.069054, 0.375634], + [4.386, 0.042685, 0.480373], + [5.089, 0.015649, 0.598954], + [5.944, 0.01764, 0.588822], + [7.342, 0.006829, 0.653077], + [9.094, 0.005478, 0.663002], + [10.145, 0.004767, 0.669464], + [13.84, 0.003034, 0.687049], + [27.343, 0.000964, 0.715687], + [45.875, 0.000236, 0.735607] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.4, 0.507042, 0.058982], + [0.593, 0.404962, 0.099815], + [0.966, 0.285078, 0.170906], + [1.326, 0.178383, 0.273973], + [1.947, 0.103651, 0.373068], + [2.72, 0.052739, 0.472193], + [3.045, -0.036061, 0.71373], + [3.369, 0.013437, 0.563006], + [3.972, 0.069054, 0.375634], + [4.386, 0.042685, 0.480373], + [5.089, 0.015649, 0.598954], + [5.944, 0.01764, 0.588822], + [7.342, 0.006829, 0.653077], + [9.094, 0.005478, 0.663002], + [10.145, 0.004767, 0.669464], + [13.84, 0.003034, 0.687049], + [27.343, 0.000964, 0.715687], + [45.875, 0.000236, 0.735607] ] } }, @@ -71,6 +136,8 @@ "minVolume": 1, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_20ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json index e27cb962b70..a178f6cc780 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_6.json @@ -1,6 +1,69 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { + "t20": { + "uiMaxFlowRate": 26.7, + "defaultAspirateFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultDispenseFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultBlowOutFlowRate": { + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } + }, + "defaultFlowAcceleration": 1200.0, + "defaultTipLength": 52.0, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.11, 0.207815, 0.040201], + [0.65, 0.43933, 0.014735], + [1.04, 0.256666, 0.133466], + [1.67, 0.147126, 0.247388], + [2.45, 0.078774, 0.361536], + [2.89, 0.042387, 0.450684], + [3.2, 0.014781, 0.530464], + [3.79, 0.071819, 0.347944], + [4.22, 0.051592, 0.424605], + [4.93, 0.021219, 0.552775], + [5.81, 0.023461, 0.541725], + [7.21, 0.008959, 0.625982], + [8.93, 0.005456, 0.651235], + [10.0, 0.007108, 0.636489], + [13.61, 0.002591, 0.681656], + [21.61, 0.002591, 0.681656] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.11, 0.207815, 0.040201], + [0.65, 0.43933, 0.014735], + [1.04, 0.256666, 0.133466], + [1.67, 0.147126, 0.247388], + [2.45, 0.078774, 0.361536], + [2.89, 0.042387, 0.450684], + [3.2, 0.014781, 0.530464], + [3.79, 0.071819, 0.347944], + [4.22, 0.051592, 0.424605], + [4.93, 0.021219, 0.552775], + [5.81, 0.023461, 0.541725], + [7.21, 0.008959, 0.625982], + [8.93, 0.005456, 0.651235], + [10.0, 0.007108, 0.636489], + [13.61, 0.002591, 0.681656], + [21.61, 0.002591, 0.681656] + ] + } + }, + "defaultPushOutVolume": 7 + }, "t50": { "uiMaxFlowRate": 26.7, "defaultAspirateFlowRate": { @@ -73,6 +136,8 @@ "minVolume": 1, "defaultTipracks": [ "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + "opentrons/opentrons_flex_96_tiprack_20ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_20ul/1" ] }