From 20548100a997c37dcac987aaba55dce1c045fa18 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Fri, 26 Jul 2024 08:03:53 -0700 Subject: [PATCH] test(analysis-snapshot-testing): add 2.19 smoke test protocols --- ...S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py | 654 ++++++++++++++++++ ...S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3.py | 605 ++++++++++++++++ 2 files changed, 1259 insertions(+) create mode 100644 analyses-snapshot-testing/files/protocols/Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py create mode 100644 analyses-snapshot-testing/files/protocols/OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3.py diff --git a/analyses-snapshot-testing/files/protocols/Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py b/analyses-snapshot-testing/files/protocols/Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py new file mode 100644 index 00000000000..0702bca32cb --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke.py @@ -0,0 +1,654 @@ +############# +# CHANGELOG # +############# + +# ---- +# 2.19 +# ---- + +# NOTHING NEW +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# The only change was changing pipette overlap values, which is not anything that can be validated by the smoke test +# Just make sure the protocol runs normally + +# ---- +# 2.18 +# ---- + +# - labware.set_offset +# - Runtime Parameters added +# - TrashContainer.top() and Well.top() now return objects of the same type +# - pipette.drop_tip() if location argument not specified the tips will be dropped at different locations in the bin +# - pipette.drop_tip() if location is specified, the tips will be dropped in the same place every time + +# ---- +# 2.17 +# ---- + +# NOTHING NEW +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# There were no new positive test cases for 2.17 +# The negative test cases are captured in the 2.17 dispense changes protocol + +# ---- +# 2.16 +# ---- + +# - prepare_to_aspirate added +# - fixed_trash property changed +# - instrument_context.trash_container property changed + +# ---- +# 2.15 +# ---- + +# - move_labware added - Manual Deck State Modification +# - ProtocolContext.load_adapter added +# - OFF_DECK location added + +from opentrons import protocol_api, types +import dataclasses +import typing + +metadata = { + "protocolName": "Flex Smoke Test - v2.19", + "author": "Derek Maggio ", +} + +requirements = { + "robotType": "OT-3", + "apiLevel": "2.19", +} + +DeckSlots = typing.Literal[ + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", +] +ValidModuleLocations = typing.List[ + typing.Union[ + protocol_api.ThermocyclerContext, + protocol_api.MagneticBlockContext, + protocol_api.Labware, # H/S Adapter or Temp Module Adapter + ] +] + +TestConfigurationChoices = typing.Literal["qa", "dev"] + + +@dataclasses.dataclass +class MoveSequence: + """A sequence of moves for a given labware.""" + + move_tos: typing.List[DeckSlots | ValidModuleLocations] + starting_location: DeckSlots | ValidModuleLocations + reset_to_start_after_each_move: bool + + def do_moves(self, ctx: protocol_api.ProtocolContext, labware: protocol_api.Labware): + + if labware.parent is not self.starting_location: + ctx.move_labware(labware, self.starting_location, use_gripper=True) + + for location in self.move_tos: + ctx.move_labware(labware, location, use_gripper=True) + + if self.reset_to_start_after_each_move: + ctx.move_labware(labware, self.starting_location, use_gripper=True) + + +@dataclasses.dataclass +class AllMoveSequences: + """All move sequences for the gripper.""" + + moves: typing.List[MoveSequence] + + @classmethod + def abbreviated_moves(cls, all_modules: typing.List[ValidModuleLocations]) -> "AllMoveSequences": + module_to_move_to = all_modules[0] + return cls( + [MoveSequence(move_tos=["B2", module_to_move_to, "D4", "C3"], starting_location="C2", reset_to_start_after_each_move=False)], + ) + + @classmethod + def all_moves(cls, all_modules: typing.List[ValidModuleLocations]) -> "AllMoveSequences": + return cls( + [ + # Covers + # Deck -> Deck + # Deck -> Staging Area Slot 3 + # Deck -> Staging Area Slot 4 + # Deck -> All modules + # Staging Area Slot 3 -> Deck + # Staging Area Slot 4 -> Deck + # All modules -> Deck + MoveSequence(move_tos=["B2", "C3", "D4"] + all_modules, starting_location="C2", reset_to_start_after_each_move=True), + # Covers + # Staging Area Slot 3 -> Staging Area Slot 4 + # Staging Area Slot 3 -> All modules + # Staging Area Slot 4 -> Staging Area Slot 3 + # All modules -> Staging Area Slot 3 + # Note: cannot cover staging area slot 3 -> staging area slot 3. Not enough room on deck + MoveSequence(move_tos=["D4"] + all_modules, starting_location="C3", reset_to_start_after_each_move=True), + # Covers + # Staging Area Slot 4 -> Staging Area Slot 4 + # Staging Area Slot 4 -> All modules + # All modules -> Staging Area Slot 4 + MoveSequence(move_tos=["C4"] + all_modules, starting_location="D4", reset_to_start_after_each_move=True), + ] + + + # Covers + # module -> module + [ + MoveSequence( + move_tos=[module_location for module_location in all_modules if module_location != starting_location], + starting_location=starting_location, + reset_to_start_after_each_move=True, + ) + for starting_location in all_modules + ], + ) + + def do_moves( + self, ctx: protocol_api.ProtocolContext, labware: protocol_api.Labware, original_labware_location: DeckSlots | ValidModuleLocations + ): + for move_sequence in self.moves: + move_sequence.do_moves(ctx, labware) + + if labware.parent is not original_labware_location: + ctx.move_labware(labware, original_labware_location, use_gripper=True) + + +@dataclasses.dataclass +class ModuleTemperatureConfiguration: + thermocycler_block: float + thermocycler_lid: float + heater_shaker: float + temperature_module: float + + @classmethod + def qa_configuration(cls) -> "ModuleTemperatureConfiguration": + return cls( + thermocycler_block=60.0, + thermocycler_lid=80.0, + heater_shaker=50.0, + temperature_module=50.0, + ) + + @classmethod + def dev_configuration(cls) -> "ModuleTemperatureConfiguration": + return cls( + thermocycler_block=50.0, + thermocycler_lid=50.0, + heater_shaker=45.0, + temperature_module=40.0, + ) + + +@dataclasses.dataclass +class TestConfiguration: + # Don't default these, they are set by runtime parameters + configuration_name: TestConfigurationChoices + reservoir_name: str + well_plate_name: str + prefer_gripper_disposal: bool + + test_set_offset: bool + run_abbreviated_pipetting_test: bool + + # Make this greater than or equal to 2, and less than or equal to 12 + partial_tip_pickup_column_count: int + + module_temps: ModuleTemperatureConfiguration + gripper_moves: AllMoveSequences + + @property + def is_qa(self) -> bool: + return self.configuration_name == "qa" + + @property + def is_dev(self) -> bool: + return self.configuration_name == "dev" + + @classmethod + def _get_qa_config( + cls, prefer_gripper_disposal: bool, reservoir_name: str, well_plate_name: str, all_modules: typing.List[ValidModuleLocations] + ) -> "TestConfiguration": + return cls( + configuration_name="qa", + reservoir_name=reservoir_name, + well_plate_name=well_plate_name, + test_set_offset=True, + run_abbreviated_pipetting_test=False, + partial_tip_pickup_column_count=12, + prefer_gripper_disposal=prefer_gripper_disposal, + module_temps=ModuleTemperatureConfiguration.qa_configuration(), + gripper_moves=AllMoveSequences.all_moves(all_modules), + ) + + @classmethod + def _get_dev_config( + cls, prefer_gripper_disposal: bool, reservoir_name: str, well_plate_name: str, all_modules: typing.List[ValidModuleLocations] + ) -> "TestConfiguration": + return cls( + configuration_name="dev", + reservoir_name=reservoir_name, + well_plate_name=well_plate_name, + test_set_offset=False, + run_abbreviated_pipetting_test=True, + partial_tip_pickup_column_count=2, + prefer_gripper_disposal=prefer_gripper_disposal, + module_temps=ModuleTemperatureConfiguration.dev_configuration(), + gripper_moves=AllMoveSequences.abbreviated_moves(all_modules), + ) + + @classmethod + def get_configuration( + cls, + parameters: protocol_api.Parameters, + where_to_put_labware_on_modules: typing.List[ + protocol_api.ThermocyclerContext | protocol_api.MagneticBlockContext | protocol_api.Labware + ], + ) -> "TestConfiguration": + test_configuration = parameters.test_configuration + prefer_gripper_disposal = parameters.prefer_gripper_disposal + reservoir_name = parameters.reservoir_name + well_plate_name = parameters.well_plate_name + + if test_configuration == "qa": + return cls._get_qa_config( + prefer_gripper_disposal=prefer_gripper_disposal, + reservoir_name=reservoir_name, + well_plate_name=well_plate_name, + all_modules=where_to_put_labware_on_modules, + ) + elif test_configuration == "dev": + return cls._get_dev_config( + prefer_gripper_disposal=prefer_gripper_disposal, + reservoir_name=reservoir_name, + well_plate_name=well_plate_name, + all_modules=where_to_put_labware_on_modules, + ) + else: + raise ValueError(f"Invalid test configuration: {test_configuration}") + + +################# +### CONSTANTS ### +################# + +HEATER_SHAKER_ADAPTER_NAME = "opentrons_96_pcr_adapter" +HEATER_SHAKER_NAME = "heaterShakerModuleV1" +MAGNETIC_BLOCK_NAME = "magneticBlockV1" +TEMPERATURE_MODULE_ADAPTER_NAME = "opentrons_96_well_aluminum_block" +TEMPERATURE_MODULE_NAME = "temperature module gen2" +THERMOCYCLER_NAME = "thermocycler module gen2" + +TIPRACK_96_ADAPTER_NAME = "opentrons_flex_96_tiprack_adapter" +TIPRACK_96_NAME = "opentrons_flex_96_tiprack_1000ul" + +PIPETTE_96_CHANNEL_NAME = "flex_96channel_1000" + +############################## +# Runtime Parameters Support # +############################## + +# -------------------------- # +# Added in API version: 2.18 # +# -------------------------- # + + +def add_parameters(parameters: protocol_api.Parameters): + + test_configuration_choices = [ + {"display_name": "QA Smoke Test", "value": "qa"}, + {"display_name": "Developer Validation", "value": "dev"}, + ] + + reservoir_choices = [ + {"display_name": "Agilent 1 Well 290 mL", "value": "agilent_1_reservoir_290ml"}, + {"display_name": "Nest 1 Well 290 mL", "value": "nest_1_reservoir_290ml"}, + ] + + well_plate_choices = [ + {"display_name": "Nest 96 Well 100 µL", "value": "nest_96_wellplate_100ul_pcr_full_skirt"}, + {"display_name": "Corning 96 Well 360 µL", "value": "corning_96_wellplate_360ul_flat"}, + {"display_name": "Opentrons Tough 96 Well 200 µL", "value": "opentrons_96_wellplate_200ul_pcr_full_skirt"}, + ] + + parameters.add_str( + variable_name="test_configuration", + display_name="Test Configuration", + description="Configuration of QA test to perform", + default="qa", + choices=test_configuration_choices, + ) + + parameters.add_str( + variable_name="reservoir_name", + display_name="Reservoir Name", + description="Name of the reservoir", + default="nest_1_reservoir_290ml", + choices=reservoir_choices, + ) + + parameters.add_str( + variable_name="well_plate_name", + display_name="Well Plate Name", + description="Name of the well plate", + default="nest_96_wellplate_100ul_pcr_full_skirt", + choices=well_plate_choices, + ) + + parameters.add_bool( + variable_name="prefer_gripper_disposal", + display_name="I LOVE TO REFILL TIP RACKS", + description="Prefer to use the gripper to dispose of labware, instead of manual moves off deck", + default=False, + ) + + +def run(ctx: protocol_api.ProtocolContext) -> None: + ################ + ### FIXTURES ### + ################ + + trash_bin = ctx.load_trash_bin("B3") + waste_chute = ctx.load_waste_chute() + + ############### + ### MODULES ### + ############### + thermocycler = ctx.load_module(THERMOCYCLER_NAME) # A1 & B1 + magnetic_block = ctx.load_module(MAGNETIC_BLOCK_NAME, "C1") + heater_shaker = ctx.load_module(HEATER_SHAKER_NAME, "A3") + temperature_module = ctx.load_module(TEMPERATURE_MODULE_NAME, "D1") + + thermocycler.open_lid() + heater_shaker.open_labware_latch() + + ####################### + ### MODULE ADAPTERS ### + ####################### + + temperature_module_adapter = temperature_module.load_adapter(TEMPERATURE_MODULE_ADAPTER_NAME) + heater_shaker_adapter = heater_shaker.load_adapter(HEATER_SHAKER_ADAPTER_NAME) + adapters = [temperature_module_adapter, heater_shaker_adapter] + + ########################## + ### TEST CONFIGURATION ### + ########################## + + test_config: TestConfiguration = TestConfiguration.get_configuration( + ctx.params, [thermocycler, magnetic_block, temperature_module_adapter, heater_shaker_adapter] + ) + + ############### + ### LABWARE ### + ############### + + source_reservoir = ctx.load_labware(test_config.reservoir_name, "D2") + dest_pcr_plate = ctx.load_labware(test_config.well_plate_name, "C2") + + tip_rack_1 = ctx.load_labware(TIPRACK_96_NAME, "A2", adapter=TIPRACK_96_ADAPTER_NAME) + tip_rack_adapter = tip_rack_1.parent + + tip_rack_2 = ctx.load_labware(TIPRACK_96_NAME, "C3") + tip_rack_3 = ctx.load_labware(TIPRACK_96_NAME, "C4") + + tip_racks = [tip_rack_1, tip_rack_2, tip_rack_3] + + ########################## + ### PIPETTE DEFINITION ### + ########################## + + pipette_96_channel = ctx.load_instrument(PIPETTE_96_CHANNEL_NAME, mount="left", tip_racks=tip_racks) + pipette_96_channel.trash_container = trash_bin + + assert isinstance(pipette_96_channel.trash_container, protocol_api.TrashBin) + + ######################## + ### LOAD SOME LIQUID ### + ######################## + + water = ctx.define_liquid(name="water", description="High Quality H₂O", display_color="#42AB2D") + source_reservoir.wells_by_name()["A1"].load_liquid(liquid=water, volume=29000) + + ################################ + ### GRIPPER LABWARE MOVEMENT ### + ################################ + + def dispose_with_preferred_method(labware: protocol_api.Labware): + """ + Get the disposal preference based on the PREFER_MOVE_OFF_DECK flag. + + Returns: + tuple: A tuple containing the disposal preference. The first element is the location preference, + either `protocol_api.OFF_DECK` or `waste_chute`. The second element is a boolean indicating + whether the gripper is being used or not. + """ + if test_config.prefer_gripper_disposal: + ctx.move_labware(labware, waste_chute, use_gripper=True) + else: + ctx.move_labware(labware, protocol_api.OFF_DECK, use_gripper=False) + + def test_manual_moves(): + # In C4 currently + ctx.move_labware(source_reservoir, "D4", use_gripper=False) + + def test_pipetting(): + def test_partial_tip_pickup_usage(): + pipette_96_channel.configure_nozzle_layout(style=protocol_api.COLUMN, start="A12") + + for i in range(1, test_config.partial_tip_pickup_column_count + 1): + + pipette_96_channel.pick_up_tip(tip_rack_2[f"A{i}"]) + + pipette_96_channel.aspirate(5, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + + pipette_96_channel.dispense(5, dest_pcr_plate[f"A{i}"]) + + if test_config.is_qa: + if i == 1: + ctx.pause( + "Watch this next tip drop in the waste chute. We are going to compare it against the next drop in the waste chute." + ) + + if i == 2: + ctx.pause( + "Watch this next tip drop in the waste chute. It should drop in a different location than the previous drop." + ) + + if i == 1: + pipette_96_channel.drop_tip(waste_chute) + elif i == 2: + pipette_96_channel.drop_tip() + else: + pipette_96_channel.drop_tip(trash_bin) + + dispose_with_preferred_method(tip_rack_2) + + def test_full_tip_rack_usage(): + pipette_96_channel.configure_nozzle_layout(style=protocol_api.ALL, start="A1") + pipette_96_channel.pick_up_tip(tip_rack_1["A1"]) + + pipette_96_channel.aspirate(10, source_reservoir["A1"]) + pipette_96_channel.touch_tip() + + pipette_96_channel.air_gap(height=30) + + pipette_96_channel.dispense(10, dest_pcr_plate["A1"]) + + pipette_96_channel.blow_out(waste_chute) + + pipette_96_channel.return_tip() + dispose_with_preferred_method(tip_rack_1) + ctx.move_labware(tip_rack_3, tip_rack_adapter, use_gripper=True) + + if not test_config.run_abbreviated_pipetting_test: + pipette_96_channel.pick_up_tip(tip_rack_3["A1"]) + + pipette_96_channel.transfer( + volume=10, + source=source_reservoir["A1"], + dest=dest_pcr_plate["A1"], + new_tip="never", + touch_tip=True, + blow_out=True, + blowout_location="trash", + mix_before=(3, 5), + mix_after=(5, 15), + ) + pipette_96_channel.return_tip() + + test_partial_tip_pickup_usage() + test_full_tip_rack_usage() + + def test_module_usage(): + + def test_thermocycler(): + thermocycler.close_lid() + + thermocycler.set_block_temperature(test_config.module_temps.thermocycler_block, hold_time_seconds=5.0) + thermocycler.set_lid_temperature(test_config.module_temps.thermocycler_lid) + thermocycler.deactivate() + + def test_heater_shaker(): + heater_shaker.open_labware_latch() + heater_shaker.close_labware_latch() + + heater_shaker.set_target_temperature(test_config.module_temps.heater_shaker) + heater_shaker.set_and_wait_for_shake_speed(1000) + heater_shaker.wait_for_temperature() + + heater_shaker.deactivate_heater() + heater_shaker.deactivate_shaker() + + def test_temperature_module(): + temperature_module.set_temperature(test_config.module_temps.temperature_module) + temperature_module.deactivate() + + def test_magnetic_block(): + pass + + test_thermocycler() + test_heater_shaker() + test_temperature_module() + test_magnetic_block() + + def test_labware_waste_chute_disposal_with_gripper(): + ctx.move_labware(source_reservoir, waste_chute, use_gripper=True) + ctx.move_labware(dest_pcr_plate, waste_chute, use_gripper=True) + + def test_labware_set_offset(): + """Test the labware.set_offset method.""" + ###################### + # labware.set_offset # + ###################### + + # -------------------------- # + # Added in API version: 2.18 # + # -------------------------- # + + SET_OFFSET_AMOUNT = 10.0 + ctx.move_labware(labware=source_reservoir, new_location=protocol_api.OFF_DECK, use_gripper=False) + pipette_96_channel.pick_up_tip(tip_rack_3["A1"]) + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of the PCR Plate, well A1, in slot C2? It should be at the LPC calibrated height.") + + dest_pcr_plate.set_offset( + x=0.0, + y=0.0, + z=SET_OFFSET_AMOUNT, + ) + + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + ctx.pause( + "Is the pipette tip in the middle of the PCR Plate, well A1, in slot C2? It should be 10mm higher than the LPC calibrated height." + ) + + ctx.move_labware(labware=dest_pcr_plate, new_location="D2", use_gripper=False) + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of the PCR Plate, well A1, in slot D2? It should be at the LPC calibrated height.") + + dest_pcr_plate.set_offset( + x=0.0, + y=0.0, + z=SET_OFFSET_AMOUNT, + ) + + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + ctx.pause( + "Is the pipette tip in the middle of the PCR Plate, well A1, in slot D2? It should be 10mm higher than the LPC calibrated height." + ) + + ctx.move_labware(labware=dest_pcr_plate, new_location="C2", use_gripper=False) + pipette_96_channel.move_to(dest_pcr_plate.wells_by_name()["A1"].top()) + + ctx.pause( + "Is the pipette tip in the middle of the PCR Plate, well A1, in slot C2? It should be 10mm higher than the LPC calibrated height." + ) + + ctx.move_labware(labware=source_reservoir, new_location="D2", use_gripper=False) + pipette_96_channel.move_to(source_reservoir.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of the reservoir , well A1, in slot D2? It should be at the LPC calibrated height.") + + pipette_96_channel.return_tip() + + ctx.pause("!!!!!!!!!!YOU NEED TO REDO LPC!!!!!!!!!!") + + def test_unique_top_methods(): + """ + Test the unique top() methods for TrashBin and WasteChute. + + Well objects should remain the same + """ + ######################## + # unique top() methods # + ######################## + + # ---------------------------- # + # Changed in API version: 2.18 # + # ---------------------------- # + + assert isinstance(trash_bin.top(), protocol_api.TrashBin) + assert isinstance(waste_chute.top(), protocol_api.WasteChute) + assert isinstance(source_reservoir.wells_by_name()["A1"].top(), types.Location) + + ################################################################################################### + ### THE ORDER OF THESE FUNCTION CALLS MATTER. CHANGING THEM WILL CAUSE THE PROTOCOL NOT TO WORK ### + ################################################################################################### + test_pipetting() + test_config.gripper_moves.do_moves(ctx=ctx, labware=dest_pcr_plate, original_labware_location="C2") + test_module_usage() + test_manual_moves() + if test_config.test_set_offset: + test_labware_set_offset() + test_unique_top_methods() + test_labware_waste_chute_disposal_with_gripper() + + ################################################################################################### + ### THE ORDER OF THESE FUNCTION CALLS MATTER. CHANGING THEM WILL CAUSE THE PROTOCOL NOT TO WORK ### + ################################################################################################### + + +# Cannot test in this protocol +# - Waste Chute w/ Lid diff --git a/analyses-snapshot-testing/files/protocols/OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3.py b/analyses-snapshot-testing/files/protocols/OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3.py new file mode 100644 index 00000000000..c571d2a593a --- /dev/null +++ b/analyses-snapshot-testing/files/protocols/OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3.py @@ -0,0 +1,605 @@ +"""Smoke Test v3.0 """ + +# https://opentrons.atlassian.net/projects/RQA?selectedItem=com.atlassian.plugins.atlassian-connect-plugin:com.kanoah.test-manager__main-project-page#!/testCase/QB-T497 + +############# +# CHANGELOG # +############# + +# ---- +# 2.19 +# ---- + +# NOTHING NEW +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# The only change was changing pipette overlap values, which is not anything that can be validated by the smoke test +# Just make sure the protocol runs normally + +# ---- +# 2.18 +# ---- + +# - labware.set_offset +# - Runtime Parameters added +# - TrashContainer.top() and Well.top() now return objects of the same type +# - pipette.drop_tip() if location argument not specified the tips will be dropped at different locations in the bin +# - pipette.drop_tip() if location is specified, the tips will be dropped in the same place every time + +# ---- +# 2.17 +# ---- + +# NOTHING NEW +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# There were no new positive test cases for 2.17 +# The negative test cases are captured in the 2.17 dispense changes protocol + +# ---- +# 2.16 +# ---- + +# - prepare_to_aspirate added +# - fixed_trash property changed +# - instrument_context.trash_container property changed + +# ---- +# 2.15 +# ---- + +# - move_labware added - Manual Deck State Modification +# - ProtocolContext.load_adapter added +# - OFF_DECK location added + +# ---- +# 2.14 +# ---- + +# - ProtocolContext.defined_liquid and Well.load_liquid added +# - load_labware without parameters should still find the labware + +# ---- +# 2.13 +# ---- + +# - Heater-Shaker Module support added + +from opentrons import protocol_api, types + +metadata = { + "protocolName": "🛠️ 2.19 Smoke Test V3 🪄", + "author": "Opentrons Engineering ", + "source": "Software Testing Team", + "description": ("Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ "), +} + +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +############################## +# Runtime Parameters Support # +############################## + +# -------------------------- # +# Added in API version: 2.18 # +# -------------------------- # + + +def add_parameters(parameters: protocol_api.Parameters): + reservoir_choices = [ + {"display_name": "Nest 12 Well 15 mL", "value": "nest_12_reservoir_15ml"}, + {"display_name": "USA Scientific 12 Well 22 mL", "value": "usascientific_12_reservoir_22ml"}, + ] + + well_plate_choices = [ + {"display_name": "Nest 96 Well 100 µL", "value": "nest_96_wellplate_100ul_pcr_full_skirt"}, + {"display_name": "Corning 96 Well 360 µL", "value": "corning_96_wellplate_360ul_flat"}, + {"display_name": "Opentrons Tough 96 Well 200 µL", "value": "opentrons_96_wellplate_200ul_pcr_full_skirt"}, + ] + + parameters.add_str( + variable_name="reservoir_name", + display_name="Reservoir Name", + description="Name of the reservoir", + default="nest_12_reservoir_15ml", + choices=reservoir_choices, + ) + + parameters.add_str( + variable_name="well_plate_name", + display_name="Well Plate Name", + description="Name of the well plate", + default="nest_96_wellplate_100ul_pcr_full_skirt", + choices=well_plate_choices, + ) + + parameters.add_int( + variable_name="delay_time", + display_name="Delay Time", + description="Time to delay in seconds", + default=3, + minimum=1, + maximum=10, + unit="seconds", + ) + + parameters.add_bool( + variable_name="robot_lights", + display_name="Robot Lights", + description="Turn on the robot lights?", + default=True, + ) + + parameters.add_float( + variable_name="heater_shaker_temperature", + display_name="Heater Shaker Temperature", + description="Temperature to set the heater shaker to", + default=38.0, + minimum=37.0, + maximum=100.0, + unit="°C", + ) + + +def run(ctx: protocol_api.ProtocolContext) -> None: + """This method is run by the protocol engine.""" + + ############################## + # Runtime Parameters Support # + ############################## + + # -------------------------- # + # Added in API version: 2.18 # + # -------------------------- # + + RESERVOIR_NAME: str = ctx.params.reservoir_name + WELL_PLATE_NAME: str = ctx.params.well_plate_name + DELAY_TIME: int = ctx.params.delay_time + ROBOT_LIGHTS: bool = ctx.params.robot_lights + HEATER_SHAKER_TEMPERATURE: float = ctx.params.heater_shaker_temperature + + ctx.set_rail_lights(ROBOT_LIGHTS) + ctx.comment(f"Let there be light! {ctx.rail_lights_on} 🌠🌠🌠") + ctx.comment(f"Is the door is closed? {ctx.door_closed} 🚪🚪🚪") + ctx.comment(f"Is this a simulation? {ctx.is_simulating()} 🔮🔮🔮") + ctx.comment(f"Running against API Version: {ctx.api_version}") + + # deck positions + tips_300ul_position = "5" + tips_20ul_position = "4" + dye_source_position = "3" + logo_position = "2" + temperature_position = "9" + custom_lw_position = "6" + hs_position = "1" + + # Thermocycler has a default position that covers Slots 7, 8, 10, and 11. + # This is the only valid location for the Thermocycler on the OT-2 deck. + # This position is a default parameter when declaring the TC so you do not need to specify. + + # 300ul tips + tips_300ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_300ul", + location=tips_300ul_position, + label="300ul tips", + ) + ] + + # 20ul tips + tips_20ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_20ul", + location=tips_20ul_position, + label="20ul tips", + ) + ] + + # pipettes + pipette_left = ctx.load_instrument(instrument_name="p300_multi_gen2", mount="left", tip_racks=tips_300ul) + + pipette_right = ctx.load_instrument(instrument_name="p20_single_gen2", mount="right", tip_racks=tips_20ul) + + ######################### + # Heater-Shaker Support # + ######################### + + # -------------------------- # + # Added in API version: 2.13 # + # -------------------------- # + + hs_module = ctx.load_module("heaterShakerModuleV1", hs_position) + temperature_module = ctx.load_module("temperature module gen2", temperature_position) + thermocycler_module = ctx.load_module("thermocycler module gen2") + + # module labware + temp_adapter = temperature_module.load_adapter("opentrons_96_well_aluminum_block") + temp_plate = temp_adapter.load_labware( + WELL_PLATE_NAME, + label="Temperature-Controlled plate", + ) + hs_plate = hs_module.load_labware(name=WELL_PLATE_NAME, adapter="opentrons_96_pcr_adapter") + tc_plate = thermocycler_module.load_labware(WELL_PLATE_NAME) + + ################################### + # Load Labware with no parameters # + ################################### + + # -------------------------- # + # Fixed in API version: 2.14 # + # -------------------------- # + + custom_labware = ctx.load_labware( + "cpx_4_tuberack_100ul", + custom_lw_position, + label="4 custom tubes", + ) + + # create plates and pattern list + logo_destination_plate = ctx.load_labware( + load_name=WELL_PLATE_NAME, + location=logo_position, + label="logo destination", + ) + + dye_container = ctx.load_labware( + load_name=RESERVOIR_NAME, + location=dye_source_position, + label="dye container", + ) + + dye_source = dye_container.wells_by_name()["A2"] + + # Well Location set-up + dye_destination_wells = [ + logo_destination_plate.wells_by_name()["C7"], + logo_destination_plate.wells_by_name()["D6"], + logo_destination_plate.wells_by_name()["D7"], + logo_destination_plate.wells_by_name()["D8"], + logo_destination_plate.wells_by_name()["E5"], + ] + + ####################################### + # define_liquid & load_liquid Support # + ####################################### + + # -------------------------- # + # Added in API version: 2.14 # + # -------------------------- # + + water = ctx.define_liquid( + name="water", description="H₂O", display_color="#42AB2D" + ) # subscript 2 https://www.compart.com/en/unicode/U+2082 + + acetone = ctx.define_liquid( + name="acetone", description="C₃H₆O", display_color="#38588a" + ) # subscript 3 https://www.compart.com/en/unicode/U+2083 + # subscript 6 https://www.compart.com/en/unicode/U+2086 + + dye_container.wells_by_name()["A1"].load_liquid(liquid=water, volume=4000) + dye_container.wells_by_name()["A2"].load_liquid(liquid=water, volume=2000) + dye_container.wells_by_name()["A5"].load_liquid(liquid=acetone, volume=555.55555) + + # 2 different liquids in the same well + dye_container.wells_by_name()["A8"].load_liquid(liquid=water, volume=900.00) + dye_container.wells_by_name()["A8"].load_liquid(liquid=acetone, volume=1001.11) + + hs_module.close_labware_latch() + + pipette_right.pick_up_tip() + + ################################## + # Manual Deck State Modification # + ################################## + + # -------------------------- # + # Added in API version: 2.15 # + # -------------------------- # + + # Putting steps for this at beginning of protocol so y # >= 2.14 define_liquid and load_liquidou can do the manual stuff + # then walk away to let the rest of the protocol execute + + # The test flow is as follows: + # 1. Remove the existing PCR plate from slot 2 + # 2. Move the reservoir from slot 3 to slot 2 + # 3. Pickup P20 tip, move pipette to reservoir A1 in slot 2 + # 4. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 2 + # 5. Move the reservoir back to slot 3 from slot 2 + # 6. Move pipette to reservoir A1 in slot 3 + # 7. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 3 + # 8. Move custom labware from slot 6 to slot 2 + # 9. Move pipette to well A1 in slot 2 + # 10. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + # 11. Move the custom labware back to slot 6 from slot 2 + # 12. Move pipette to well A1 in slot 6 + # 13. Pause and ask user to validate that the tip is in the middle of well A1 in slot 6 + # 14. Move the offdeck PCR plate back to slot 2 + # 15. Move pipette to well A1 in slot 2 + # 16. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + + # In effect, nothing will actually change to the protocol, + # but we will be able to test that the UI responds appropriately. + + # Note: + # logo_destination_plate is a nest_96_wellplate_100ul_pcr_full_skirt - starting position is slot 2 + # dye_container is aRESERVOIR_NAME- starting position is slot 3 + + # Step 1 + ctx.move_labware( + labware=logo_destination_plate, + new_location=protocol_api.OFF_DECK, + ) + + # Step 2 + ctx.move_labware(labware=dye_container, new_location="2") + + # Step 3 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 4 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 2?") + + # Step 5 + ctx.move_labware(labware=dye_container, new_location="3") + + # Step 6 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 7 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3?") + + # Step 8 + ctx.move_labware(labware=custom_labware, new_location="2") + + # Step 9 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 10 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 2?") + + # Step 11 + ctx.move_labware(labware=custom_labware, new_location="6") + + # Step 12 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 13 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 6?") + + # Step 14 + ctx.move_labware(labware=logo_destination_plate, new_location="2") + + # Step 15 + pipette_right.move_to(location=logo_destination_plate.wells_by_name()["A1"].top()) + + # Step 16 + ctx.pause("Is the pipette tip in the middle of well A1 in slot 2?") + + ####################### + # prepare_to_aspirate # + ####################### + + # -------------------------- # + # Added in API version: 2.16 # + # -------------------------- # + + pipette_right.prepare_to_aspirate() + pipette_right.move_to(dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause( + "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + ) + pipette_right.aspirate(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause("Did the pipette move up out of the well, only once, after aspirating?") + pipette_right.dispense(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + + ######################################### + # protocol_context.fixed_trash property # + ######################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(ctx.fixed_trash) + ctx.pause("Is the pipette over the trash? Pipette will home after this pause.") + ctx.home() + + ############################################### + # instrument_context.trash_container property # + ############################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(pipette_right.trash_container) + ctx.pause("Is the pipette over the trash?") + + # Distribute dye + pipette_right.distribute( + volume=18, + source=dye_source, + dest=dye_destination_wells, + new_tip="never", + ) + pipette_right.drop_tip() + + # transfer + transfer_destinations = [ + logo_destination_plate.wells_by_name()["A11"], + logo_destination_plate.wells_by_name()["B11"], + logo_destination_plate.wells_by_name()["C11"], + ] + pipette_right.pick_up_tip() + pipette_right.transfer( + volume=60, + source=dye_container.wells_by_name()["A2"], + dest=transfer_destinations, + new_tip="never", + touch_tip=True, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + mix_after=(1, 20), + mix_touch_tip=True, + ) + + # consolidate + pipette_right.consolidate( + volume=20, + source=transfer_destinations, + dest=dye_container.wells_by_name()["A5"], + new_tip="never", + touch_tip=False, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + ) + + # well to well + pipette_right.return_tip() + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=5, location=logo_destination_plate.wells_by_name()["A11"]) + pipette_right.air_gap(volume=10) + ctx.delay(seconds=DELAY_TIME) + pipette_right.dispense(volume=5, location=logo_destination_plate.wells_by_name()["H11"]) + + # move to + pipette_right.move_to(logo_destination_plate.wells_by_name()["E12"].top()) + pipette_right.move_to(logo_destination_plate.wells_by_name()["E11"].bottom()) + pipette_right.blow_out() + # touch tip + # pipette ends in the middle of the well as of 6.3.0 in all touch_tip + pipette_right.touch_tip(location=logo_destination_plate.wells_by_name()["H1"]) + ctx.pause("Is the pipette tip in the middle of the well?") + pipette_right.return_tip() + + # Play with the modules + temperature_module.await_temperature(25) + + hs_module.set_and_wait_for_shake_speed(466) + ctx.delay(seconds=DELAY_TIME) + + hs_module.set_and_wait_for_temperature(HEATER_SHAKER_TEMPERATURE) + + thermocycler_module.open_lid() + thermocycler_module.close_lid() + thermocycler_module.set_lid_temperature(38) # 37 is the minimum + thermocycler_module.set_block_temperature(temperature=28, hold_time_seconds=5) + thermocycler_module.deactivate_block() + thermocycler_module.deactivate_lid() + thermocycler_module.open_lid() + + hs_module.deactivate_shaker() + + # dispense to modules + + # to temperature module + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=15, location=dye_source) + pipette_right.dispense(volume=15, location=temp_plate.well(0)) + pipette_right.drop_tip() + + # to heater shaker + pipette_left.pick_up_tip() + pipette_left.aspirate(volume=50, location=dye_source) + pipette_left.dispense(volume=50, location=hs_plate.well(0)) + hs_module.set_and_wait_for_shake_speed(350) + ctx.delay(DELAY_TIME) + hs_module.deactivate_shaker() + + # to custom labware + # This labware does not EXIST!!!! so... + # Use tip rack lid to catch dye on wet run + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=10, location=dye_source, rate=2.0) + pipette_right.dispense(volume=10, location=custom_labware.well(3), rate=1.5) + pipette_right.drop_tip() + + # to thermocycler + pipette_left.aspirate(volume=75, location=dye_source) + pipette_left.dispense(volume=60, location=tc_plate.wells_by_name()["A6"]) + pipette_left.drop_tip() + + ######################## + # unique top() methods # + ######################## + + # ---------------------------- # + # Changed in API version: 2.18 # + # ---------------------------- # + + assert isinstance(ctx.fixed_trash.top(), protocol_api.TrashBin) + assert isinstance(dye_container.wells_by_name()["A1"].top(), types.Location) + + ############################# + # drop_tip location changes # + ############################# + + # ---------------------------- # + # Changed in API version: 2.18 # + # ---------------------------- # + + ctx.pause("Watch the next 5 tips drop in the trash. They should drop in different locations of the trash each time.") + for _ in range(5): + pipette_right.pick_up_tip() + pipette_right.drop_tip() + + ctx.pause("Watch the next 5 tips drop in the trash. They should drop in the same location of the trash each time.") + for _ in range(5): + pipette_right.pick_up_tip() + pipette_right.drop_tip(location=ctx.fixed_trash) + + ###################### + # labware.set_offset # + ###################### + + # -------------------------- # + # Added in API version: 2.18 # + # -------------------------- # + + SET_OFFSET_AMOUNT = 10.0 + + pipette_right.pick_up_tip() + + ctx.move_labware(labware=logo_destination_plate, new_location=protocol_api.OFF_DECK) + pipette_right.move_to(dye_container.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3? It should be at the LPC calibrated height.") + + dye_container.set_offset( + x=0.0, + y=0.0, + z=SET_OFFSET_AMOUNT, + ) + + pipette_right.move_to(dye_container.wells_by_name()["A1"].top()) + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3? It should be 10mm higher than the LPC calibrated height.") + + ctx.move_labware(labware=dye_container, new_location="2") + pipette_right.move_to(dye_container.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 2? It should be at the LPC calibrated height.") + + dye_container.set_offset( + x=0.0, + y=0.0, + z=SET_OFFSET_AMOUNT, + ) + + pipette_right.move_to(dye_container.wells_by_name()["A1"].top()) + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 2? It should be 10mm higher than the LPC calibrated height.") + + ctx.move_labware(labware=dye_container, new_location="3") + pipette_right.move_to(dye_container.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3? It should be 10mm higher than the LPC calibrated height.") + + ctx.move_labware(labware=logo_destination_plate, new_location="2") + pipette_right.move_to(logo_destination_plate.wells_by_name()["A1"].top()) + + ctx.pause("Is the pipette tip in the middle of well A1 in slot 2? It should be at the LPC calibrated height.") + + ctx.pause("!!!!!!!!!!YOU NEED TO REDO LPC!!!!!!!!!!") + + pipette_right.return_tip()