diff --git a/Makefile b/Makefile index d3ddd15f359..7a5bfc7a7e9 100755 --- a/Makefile +++ b/Makefile @@ -152,6 +152,10 @@ push: sleep 1 $(MAKE) -C $(UPDATE_SERVER_DIR) push +.PHONY: push-folder +PUSH_HELPER := abr-testing/abr_testing/tools/make_push.py +push-folder: + $(OT_PYTHON) $(PUSH_HELPER) .PHONY: push-ot3 push-ot3: diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py index 75b73b8f16b..46cc409e53d 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -286,6 +286,7 @@ def run( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() @@ -294,7 +295,7 @@ def run( ip_or_all = input("IP Address or ALL: ") calibration_data = [] if ip_or_all.upper() == "ALL": - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) for ip in ip_address_list: saved_file_path, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 6552534c4ae..8f82567a7d1 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -44,6 +44,7 @@ def create_data_dictionary( headers: List[str] = [] headers_lpc: List[str] = [] list_of_heights: List[List[Any]] = [[], [], [], [], [], [], [], []] + hellma_plate_orientation = False # default hellma plate is not rotated. for filename in os.listdir(storage_directory): file_path = os.path.join(storage_directory, filename) if file_path.endswith(".json"): @@ -67,6 +68,10 @@ def create_data_dictionary( if run_id in runs_to_save: print(f"started reading run {run_id}.") robot = file_results.get("robot_name") + parameters = file_results.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") software_version = file_results.get("API_Version", "") left_pipette = file_results.get("left", "") @@ -123,7 +128,7 @@ def create_data_dictionary( file_results, labware_name="opentrons_tough_pcr_auto_sealing_lid" ) plate_reader_dict = read_robot_logs.plate_reader_commands( - file_results, hellma_plate_standards + file_results, hellma_plate_standards, hellma_plate_orientation ) list_of_heights = read_robot_logs.liquid_height_commands( file_results, list_of_heights diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index 964a8a06e18..fe89f9f1543 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -104,10 +104,11 @@ def get_all_run_logs( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) runs_from_storage = read_robot_logs.get_run_ids_from_google_drive(google_drive) for ip in ip_address_list: runs = get_run_ids_from_robot(ip) diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 40712118fe5..7bc83e0a54b 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -250,7 +250,9 @@ def liquid_height_commands( def plate_reader_commands( - file_results: Dict[str, Any], hellma_plate_standards: List[Dict[str, Any]] + file_results: Dict[str, Any], + hellma_plate_standards: List[Dict[str, Any]], + orientation: bool, ) -> Dict[str, object]: """Plate Reader Command Counts.""" commandData = file_results.get("commands", "") @@ -279,38 +281,46 @@ def plate_reader_commands( read = "yes" elif read == "yes" and commandType == "comment": result = command["params"].get("message", "") - formatted_result = result.split("result: ")[1] - result_dict = eval(formatted_result) - result_dict_keys = list(result_dict.keys()) - if len(result_dict_keys) > 1: - read_type = "multi" - else: - read_type = "single" - for wavelength in result_dict_keys: - one_wavelength_dict = result_dict.get(wavelength) - result_ndarray = plate_reader.convert_read_dictionary_to_array( - one_wavelength_dict - ) - for item in hellma_plate_standards: - wavelength_of_interest = item["wavelength"] - if str(wavelength) == str(wavelength_of_interest): - error_cells = plate_reader.check_byonoy_data_accuracy( - result_ndarray, item, False + if "result:" in result: + plate_name = result.split("result:")[0] + formatted_result = result.split("result: ")[1] + print(formatted_result) + result_dict = eval(formatted_result) + result_dict_keys = list(result_dict.keys()) + if len(result_dict_keys) > 1: + read_type = "multi" + else: + read_type = "single" + if "hellma_plate" in plate_name: + for wavelength in result_dict_keys: + one_wavelength_dict = result_dict.get(wavelength) + result_ndarray = plate_reader.convert_read_dictionary_to_array( + one_wavelength_dict ) - if len(error_cells[0]) > 0: - percent = (96 - len(error_cells)) / 96 * 100 - for cell in error_cells: - print( - "FAIL: Cell " + str(cell) + " out of accuracy spec." + for item in hellma_plate_standards: + wavelength_of_interest = item["wavelength"] + if str(wavelength) == str(wavelength_of_interest): + error_cells = plate_reader.check_byonoy_data_accuracy( + result_ndarray, item, orientation ) - else: - percent = 100 - print( - f"PASS: {wavelength_of_interest} meet accuracy specification" - ) - final_result[read_type, wavelength, read_num] = percent - read_num += 1 - read = "no" + if len(error_cells[0]) > 0: + percent = (96 - len(error_cells)) / 96 * 100 + for cell in error_cells: + print( + "FAIL: Cell " + + str(cell) + + " out of accuracy spec." + ) + else: + percent = 100 + print( + f"PASS: {wavelength_of_interest} meet accuracy spec." + ) + final_result[read_type, wavelength, read_num] = percent + read_num += 1 + else: + final_result = result_dict + read = "no" plate_dict = { "Plate Reader # of Reads": read_count, "Plate Reader Avg Read Time (sec)": avg_read_time, diff --git a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py index f55c9ebb51f..76852f70b9c 100644 --- a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py +++ b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py @@ -46,7 +46,7 @@ def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: labware_defs = [] for root, directories, _ in os.walk(root_dir): for directory in directories: - if directory == "active_protocols": + if directory not in exclude: active_dir = os.path.join(root, directory) for file in os.listdir( active_dir @@ -100,7 +100,6 @@ def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: exclude = [ "__init__.py", "helpers.py", - "shared_vars_and_funcs.py", ] print("Simulating Protocols") file_dict, labware_defs = get_files() diff --git a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py index 57695f03557..10c7ea12782 100644 --- a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py +++ b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py @@ -225,7 +225,11 @@ def parse_results_volume( else: print(f"Expected JSON object (dict) but got {type(json_data).__name__}.") commands = {} - + hellma_plate_orientation = False + parameters = json_data.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) start_time = datetime.fromisoformat(commands[0]["createdAt"]) end_time = datetime.fromisoformat(commands[len(commands) - 1]["completedAt"]) header = ["", "Protocol Name", "Date", "Time"] @@ -283,7 +287,7 @@ def parse_results_volume( temp_module_dict = read_robot_logs.temperature_module_commands(json_data) thermo_cycler_dict = read_robot_logs.thermocycler_commands(json_data) plate_reader_dict = read_robot_logs.plate_reader_commands( - json_data, hellma_plate_standards + json_data, hellma_plate_standards, hellma_plate_orientation ) instrument_dict = read_robot_logs.instrument_commands( json_data, labware_name=None @@ -499,12 +503,12 @@ def check_params(protocol_path: str) -> str: def get_extra_files(protocol_file_path: str) -> tuple[str, List[Path]]: """Get supporting files for protocol simulation if needed.""" params = check_params(protocol_file_path) - needs_files = input("Does your protocol utilize custom labware? (y/n): ") + needs_files = input("Does your protocol utilize custom labware? (Y/N): ") labware_files = [] - if needs_files == "y": + if needs_files == "Y": num_labware = input("How many custom labware?: ") for labware_num in range(int(num_labware)): - path = input("Enter custom labware definition: ") + path = input("Enter custom labware definition path: ") labware_files.append(Path(path)) return (params, labware_files) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py b/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py deleted file mode 100644 index 50fb82e94d5..00000000000 --- a/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py +++ /dev/null @@ -1,343 +0,0 @@ -"""FLEX Normalize with Tubes.""" -from opentrons.protocol_api import ProtocolContext, ParameterContext, Well -from abr_testing.protocols import helpers -from typing import List - -metadata = { - "protocolName": "Flex Normalize with Tubes", - "author": "Opentrons ", - "source": "Protocol Library", -} - -requirements = {"robotType": "Flex", "apiLevel": "2.21"} - -# SCRIPT SETTINGS -ABR_TEST = True -if ABR_TEST: - DRYRUN = True # True = skip incubation times, shorten mix, for testing purposes - TIP_TRASH = ( - False # True = Used tips go in Trash, False = Used tips go back into rack - ) -else: - DRYRUN = False # True = skip incubation times, shorten mix, for testing purposes - TIP_TRASH = True - - -def add_parameters(parameters: ParameterContext) -> None: - """Parameters.""" - helpers.create_csv_parameter(parameters) - helpers.create_dot_bottom_parameter(parameters) - helpers.create_two_pipette_mount_parameters(parameters) - - -def run(ctx: ProtocolContext) -> None: - """Protocol.""" - mount_pos_50ul = ctx.params.pipette_mount_1 # type: ignore[attr-defined] - mount_pos_1000ul = ctx.params.pipette_mount_2 # type: ignore[attr-defined] - dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] - parsed_csv = ctx.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] - if DRYRUN: - ctx.comment("THIS IS A DRY RUN") - else: - ctx.comment("THIS IS A REACTION RUN") - - # labware - tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "1") - tiprack_200_1 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "4") - reagent_tube = ctx.load_labware( - "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "5", "Reagent Tube" - ) - sample_plate = ctx.load_labware( - "armadillo_96_wellplate_200ul_pcr_full_skirt", "2", "Sample Plate" - ) - - # reagent - RSB = reagent_tube.wells()[0] - - # pipette - p1000 = ctx.load_instrument( - "flex_1channel_1000", mount_pos_1000ul, tip_racks=[tiprack_200_1] - ) - p50 = ctx.load_instrument( - "flex_1channel_50", mount_pos_50ul, tip_racks=[tiprack_50_1] - ) - - wells_with_liquids: List[Well] = [RSB] - helpers.load_wells_with_water(ctx, wells_with_liquids, [4000.0]) - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_with_liquids) - MaxTubeVol = 200 - RSBVol = 0.0 - - data = parsed_csv - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - - if DilutionVol <= 1: - ctx.comment("Sample " + CurrentWell + ": Conc. Too Low, Will Skip") - elif DilutionVol > MaxTubeVol - InitialVol: - DilutionVol = MaxTubeVol - InitialVol - ctx.comment( - "Sample " - + CurrentWell - + ": Conc. Too High, Will add, " - + str(DilutionVol) - + "ul, Max = " - + str(MaxTubeVol) - + "ul" - ) - RSBVol += MaxTubeVol - InitialVol - else: - if DilutionVol <= 20: - ctx.comment( - "Sample " - + CurrentWell - + ": Using p50, will add " - + str(round(DilutionVol, 1)) - ) - elif DilutionVol > 20: - ctx.comment( - "Sample " - + CurrentWell - + ": Using p1000, will add " - + str(round(DilutionVol, 1)) - ) - RSBVol += DilutionVol - current += 1 - - if RSBVol >= 14000: - ctx.pause("Caution, more than 15ml Required") - else: - ctx.comment("RSB Minimum: " + str(round(RSBVol / 1000, 1) + 1) + "ml") - - PiR2 = 176.71 - InitialRSBVol = RSBVol - RSBHeight = (InitialRSBVol / PiR2) + 17.5 - - ctx.pause("Proceed") - ctx.comment("==============================================") - ctx.comment("Normalizing Samples") - ctx.comment("==============================================") - - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - - ctx.comment("Number " + str(data[current]) + ": Sample " + str(CurrentWell)) - # ctx.comment("Vol Height = "+str(round(RSBHeight,2))) - HeightDrop = DilutionVol / PiR2 - # ctx.comment("Vol Drop = "+str(round(HeightDrop,2))) - - if DilutionVol <= 0: - # If the No Volume - ctx.comment("Conc. Too Low, Skipping") - - elif DilutionVol >= MaxTubeVol - InitialVol: - # If the Required Dilution volume is >= Max Volume - DilutionVol = MaxTubeVol - InitialVol - ctx.comment( - "Conc. Too High, Will add, " - + str(DilutionVol) - + "ul, Max = " - + str(MaxTubeVol) - + "ul" - ) - p1000.pick_up_tip() - p1000.require_liquid_presence(RSB) - p1000.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - # ctx.comment("New Vol Height = "+str(round(RSBHeight,2))) - p1000.dispense(DilutionVol, sample_plate.wells_by_name()[CurrentWell]) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - HighVolMix = 10 - for Mix in range(HighVolMix): - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].center()) - p1000.aspirate(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(0.5) - ) # original = () - p1000.aspirate(100) - p1000.dispense(100) - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].center()) - p1000.dispense(100) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - Mix += 1 - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - ctx.delay(seconds=3) - p1000.blow_out() - p1000.drop_tip() if DRYRUN is False else p1000.return_tip() - - else: - if DilutionVol <= 20: - # If the Required Dilution volume is <= 20ul - ctx.comment("Using p50 to add " + str(round(DilutionVol, 1))) - p50.pick_up_tip() - if round(float(data[current][3]), 1) <= 20: - p50.require_liquid_presence(RSB) - p50.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - else: - p50.require_liquid_presence(RSB) - p50.aspirate(20, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - p50.dispense(DilutionVol, sample_plate.wells_by_name()[CurrentWell]) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - p50.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(z=dot_bottom) - ) # original = () - # Mix volume <=20ul - if DilutionVol + InitialVol <= 20: - p50.mix(10, DilutionVol + InitialVol) - elif DilutionVol + InitialVol > 20: - p50.mix(10, 20) - p50.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - ctx.delay(seconds=3) - p50.blow_out() - p50.drop_tip() if DRYRUN is False else p50.return_tip() - - elif DilutionVol > 20: - # If the required volume is >20 - ctx.comment("Using p1000 to add " + str(round(DilutionVol, 1))) - p1000.pick_up_tip() - p1000.require_liquid_presence(RSB) - p1000.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - if DilutionVol + InitialVol >= 120: - HighVolMix = 10 - for Mix in range(HighVolMix): - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].center() - ) - p1000.aspirate(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom( - z=dot_bottom - ) - ) # original = () - p1000.aspirate(DilutionVol + InitialVol - 100) - p1000.dispense(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].center() - ) - p1000.dispense(DilutionVol + InitialVol - 100) - Mix += 1 - wells_with_liquids.append( - sample_plate.wells_by_name()[CurrentWell] - ) - else: - p1000.dispense( - DilutionVol, sample_plate.wells_by_name()[CurrentWell] - ) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(z=dot_bottom) - ) # original = () - p1000.mix(10, DilutionVol + InitialVol) - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - ctx.delay(seconds=3) - p1000.blow_out() - p1000.drop_tip() if DRYRUN is False else p1000.return_tip() - current += 1 - - ctx.comment("==============================================") - ctx.comment("Results") - ctx.comment("==============================================") - - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - if DilutionVol > MaxTubeVol - InitialVol: - DilutionVol = MaxTubeVol - InitialVol - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - ctx.comment( - "Sample " - + CurrentWell - + ": " - + str(round(FinalVol, 1)) - + " at " - + str(round(FinalConc, 1)) - + "ng/ul" - ) - - current += 1 - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_with_liquids) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py new file mode 100644 index 00000000000..05a6300e053 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py @@ -0,0 +1,124 @@ +"""Tartrazine Protocol.""" +from opentrons.protocol_api import ProtocolContext, ParameterContext, Well +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + AbsorbanceReaderContext, + HeaterShakerContext, +) +from datetime import datetime +from typing import Dict, List +import statistics + +metadata = { + "protocolName": "Tartrazine Protocol", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + + +def run(ctx: ProtocolContext) -> None: + """Protocol.""" + mount_pos_50ul = ctx.params.pipette_mount # type: ignore[attr-defined] + # Plate Reader + plate_reader: AbsorbanceReaderContext = ctx.load_module( + helpers.abs_mod_str, "A3" + ) # type: ignore[assignment] + hs: HeaterShakerContext = ctx.load_module(helpers.hs_str, "A1") # type: ignore[assignment] + hs_adapter = hs.load_adapter("opentrons_96_pcr_adapter") + tube_rack = ctx.load_labware( + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", "C2", "Reagent Tube" + ) + tartrazine_tube = tube_rack["A3"] + + sample_plate_1 = ctx.load_labware( + "nest_96_wellplate_200ul_flat", "D1", "Sample Plate 1" + ) + sample_plate_2 = ctx.load_labware( + "nest_96_wellplate_200ul_flat", "C1", "Sample Plate 2" + ) + sample_plate_3 = ctx.load_labware( + "nest_96_wellplate_200ul_flat", "B1", "Sample Plate 3" + ) + sample_plate_list = [sample_plate_1, sample_plate_2, sample_plate_3] + tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "D3") + tiprack_50_2 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "C3") + tiprack_50_3 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "B3") + tip_racks = [tiprack_50_1, tiprack_50_2, tiprack_50_3] + + # Pipette + p50 = ctx.load_instrument("flex_1channel_50", mount_pos_50ul, tip_racks=tip_racks) + + # Probe wells + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Tartrazine": [{"well": tartrazine_tube, "volume": 45.0}] + } + helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, p50) + + i = 0 + all_percent_error_dict = {} + cv_dict = {} + for sample_plate in sample_plate_list: + deck_locations = ["D1", "C1", "B1"] + for well in sample_plate.wells(): + p50.pick_up_tip() + height = helpers.find_liquid_height(p50, tartrazine_tube) + p50.aspirate(10, tartrazine_tube.bottom(z=height)) + p50.air_gap(5) + p50.dispense(5, well.top()) + p50.dispense(10, well.bottom(z=0.5)) + p50.blow_out() + p50.return_tip() + helpers.move_labware_to_hs(ctx, sample_plate, hs, hs_adapter) + helpers.set_hs_speed(ctx, hs, 1500, 2.0, True) + hs.open_labware_latch() + plate_reader.close_lid() + plate_reader.initialize("single", [450]) + plate_reader.open_lid() + ctx.move_labware(sample_plate, plate_reader, use_gripper=True) + sample_plate_name = "sample plate_" + str(i + 1) + csv_string = sample_plate_name + "_" + str(datetime.now()) + plate_reader.close_lid() + result = plate_reader.read(csv_string) + for wavelength in result: + dict_of_wells = result[wavelength] + readings_and_wells = dict_of_wells.items() + readings = dict_of_wells.values() + avg = statistics.mean(readings) + # Check if every average is within +/- 5% of 2.85 + percent_error_dict = {} + percent_error_sum = 0.0 + for reading in readings_and_wells: + well_name = str(reading[0]) + measurement = reading[1] + percent_error = (measurement - 2.85) / 2.85 * 100 + percent_error_dict[well_name] = percent_error + percent_error_sum += percent_error + avg_percent_error = percent_error_sum / 96.0 + standard_deviation = statistics.stdev(readings) + try: + cv = standard_deviation / avg + except ZeroDivisionError: + cv = 0.0 + cv_percent = cv * 100 + cv_dict[sample_plate_name] = { + "CV": cv_percent, + "Mean": avg, + "SD": standard_deviation, + "Avg Percent Error": avg_percent_error, + } + all_percent_error_dict[sample_plate_name] = percent_error_dict + plate_reader.open_lid() + ctx.move_labware(sample_plate, deck_locations[i], use_gripper=True) + i += 1 + + # Print percent error dictionary + ctx.comment("Percent Error: " + str(all_percent_error_dict)) + # Print cv dictionary + ctx.comment("Plate Reader result: " + str(cv_dict)) diff --git a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py index 2e835ac04dd..4894cae41d4 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py +++ b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py @@ -19,10 +19,7 @@ } -requirements = { - "robotType": "Flex", - "apiLevel": "2.21", -} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} HELLMA_PLATE_SLOT = "D4" PLATE_READER_SLOT = "C3" @@ -58,14 +55,11 @@ def add_parameters(parameters: ParameterContext) -> None: """Add Parameters.""" helpers.create_hs_speed_parameter(parameters) helpers.create_dot_bottom_parameter(parameters) - parameters.add_str( + parameters.add_bool( variable_name="plate_orientation", display_name="Hellma Plate Orientation", - default="0_deg", - choices=[ - {"display_name": "0 degree Rotation", "value": "0_deg"}, - {"display_name": "180 degree Rotation", "value": "180_deg"}, - ], + default=True, + description="If hellma plate is rotated, set to True.", ) @@ -73,6 +67,7 @@ def plate_reader_actions( protocol: ProtocolContext, plate_reader: AbsorbanceReaderContext, hellma_plate: Labware, + hellma_plate_name: str, ) -> None: """Plate reader single and multi wavelength readings.""" wavelengths = [450, 650] @@ -84,7 +79,7 @@ def plate_reader_actions( protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) plate_reader.close_lid() result = plate_reader.read(str(datetime.now())) - msg = f"result: {result}" + msg = f"{hellma_plate_name} result: {result}" protocol.comment(msg=msg) plate_reader.open_lid() protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) @@ -95,7 +90,7 @@ def plate_reader_actions( protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) plate_reader.close_lid() result = plate_reader.read(str(datetime.now())) - msg = f"result: {result}" + msg = f"{hellma_plate_name} result: {result}" protocol.comment(msg=msg) plate_reader.open_lid() protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) @@ -107,6 +102,8 @@ def run(protocol: ProtocolContext) -> None: # LOAD PARAMETERS heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + plate_orientation = protocol.params.plate_orientation # type: ignore[attr-defined] + plate_name_str = "hellma_plate_" + str(plate_orientation) global p200_tips global p50_tips # WASTE BIN @@ -182,7 +179,7 @@ def run(protocol: ProtocolContext) -> None: PPC = reagent_plate.wells_by_name()["A6"] EPM = reagent_plate.wells_by_name()["A7"] # Load Liquids - plate_reader_actions(protocol, plate_reader, hellma_plate) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) # tip and sample tracking if COLUMNS == 1: @@ -948,4 +945,4 @@ def tipcheck() -> None: p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() - plate_reader_actions(protocol, plate_reader, hellma_plate) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) diff --git a/abr-testing/abr_testing/tools/make_push.py b/abr-testing/abr_testing/tools/make_push.py new file mode 100644 index 00000000000..28a69b11103 --- /dev/null +++ b/abr-testing/abr_testing/tools/make_push.py @@ -0,0 +1,95 @@ +"""Push one or more folders to one or more robots.""" +import subprocess +import multiprocessing +import json + +global folders +# Opentrons folders that can be pushed to robot +folders = [ + "abr-testing", + "hardware-testing", + "abr-testing + hardware-testing", + "other", +] + + +def push_subroutine(cmd: str) -> None: + """Pushes specified folder to specified robot.""" + try: + subprocess.run(cmd) + except Exception: + print("failed to push folder") + raise + + +def main(folder_to_push: str, robot_to_push: str) -> int: + """Main process!""" + cmd = "make -C {folder} push-ot3 host={ip}" + robot_ip_path = "" + push_cmd = "" + folder_int = int(folder_to_push) + if folders[folder_int].lower() == "abr-testing + hardware-testing": + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips_dict = robot_json.get("ip_address_list") + robot_ips = list(robot_ips_dict.keys()) + ip_file.close() + else: + robot_ips = [robot_to_push] + for folder_name in folders[:-2]: + # Push abr-testing and hardware-testing folders to all robots + for robot in robot_ips: + print_proc = multiprocessing.Process( + target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) + ) + print_proc.start() + print_proc.join() + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = multiprocessing.Process( + target=push_subroutine, args=(push_cmd,) + ) + process.start() + process.join() + print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) + print_proc.start() + print_proc.join() + else: + + if folder_int == (len(folders) - 1): + folder_name = input("Which folder? ") + else: + folder_name = folders[folder_int] + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips = robot_json.get("ip_address_list") + ip_file.close() + else: + robot_ips = [robot_to_push] + + # Push folder to robots + for robot in robot_ips: + print_proc = multiprocessing.Process( + target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) + ) + print_proc.start() + print_proc.join() + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = multiprocessing.Process(target=push_subroutine, args=(push_cmd,)) + process.start() + process.join() + print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) + print_proc.start() + print_proc.join() + return 0 + + +if __name__ == "__main__": + for i, folder in enumerate(folders): + print(f"{i}) {folder}") + folder_to_push = input("Please Select a Folder to Push: ") + robot_to_push = input("Type in robots ip (type all for all): ") + print(main(folder_to_push, robot_to_push)) diff --git a/analyses-snapshot-testing/Makefile b/analyses-snapshot-testing/Makefile index de5e0381131..6918d17bf3e 100644 --- a/analyses-snapshot-testing/Makefile +++ b/analyses-snapshot-testing/Makefile @@ -3,12 +3,12 @@ CACHEBUST ?= $(shell date +%s) ANALYSIS_REF ?= edge PROTOCOL_NAMES ?= all OVERRIDE_PROTOCOL_NAMES ?= all -OPENTRONS_VERSION ?= edge +LOCAL_IMAGE_TAG ?= local +ANALYZER_IMAGE_NAME ?= opentrons-analysis -export OPENTRONS_VERSION # used for server -export ANALYSIS_REF # used for analysis and snapshot test -export PROTOCOL_NAMES # used for the snapshot test -export OVERRIDE_PROTOCOL_NAMES # used for the snapshot test +export ANALYSIS_REF # tag, branch or commit for the opentrons repository. Used as the image tag for the analyzer image +export PROTOCOL_NAMES # tell the test which protocols to run +export OVERRIDE_PROTOCOL_NAMES # tell the test which override protocols to run ifeq ($(CI), true) PYTHON=python @@ -93,23 +93,47 @@ build-base-image: .PHONY: build-opentrons-analysis build-opentrons-analysis: - @echo "Building docker image for $(ANALYSIS_REF)" - @echo "The image will be named opentrons-analysis:$(ANALYSIS_REF)" - @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" - docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-analysis:$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. + @echo "Building docker image for opentrons repository reference$(ANALYSIS_REF)" + @echo "The image will be named $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF)" + @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. -.PHONY: local-build -local-build: +.PHONY: build-local +build-local: @echo "Building docker image for your local opentrons code" - @echo "The image will be named opentrons-analysis:local" - @echo "For a fresh build, run 'make local-build NO_CACHE=1'" - docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) $(BUILD_FLAGS) -t opentrons-analysis:local -f citools/Dockerfile.local .. || true + @echo "This image will be named $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) -t $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG) -f citools/Dockerfile.local .. @echo "Build complete" +.PHONY: snapshot-test-local +snapshot-test-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test -vv + +.PHONY: snapshot-test-update-local +snapshot-test-update-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-update-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test --snapshot-update + .PHONY: generate-protocols generate-protocols: $(PYTHON) -m pipenv run python -m automation.data.protocol_registry +# Tools for running the robot server in a container + +OPENTRONS_VERSION ?= edge +export OPENTRONS_VERSION # used for the robot server image as the tag, branch or commit for the opentrons repository + .PHONY: build-rs build-rs: @echo "Building docker image for opentrons-robot-server:$(OPENTRONS_VERSION)" diff --git a/analyses-snapshot-testing/README.md b/analyses-snapshot-testing/README.md index 78423b8447f..03ce1d87518 100644 --- a/analyses-snapshot-testing/README.md +++ b/analyses-snapshot-testing/README.md @@ -4,7 +4,7 @@ 1. Follow the instructions in [DEV_SETUP.md](../DEV_SETUP.md) 1. `cd analyses-snapshot-testing` -1. use pyenv to install python 3.12 and set it as the local python version for this directory +1. use pyenv to install python 3.13 and set it as the local python version for this directory 1. `make setup` 1. Have docker installed and ready @@ -72,10 +72,17 @@ cd analyses-snapshot-testing \ > This copies in your local code to the container and runs the analyses battery against it. -1. `make build-base-image` -1. `make build-local` -1. `make local-snapshot-test` +`cd PYENV_ROOT && git pull` - make sure pyenv is up to date so you may install python 3.13.0 +`pyenv install 3.13.0` - install python 3.13.0 +`cd /analyses-snapshot-testing` - navigate to the analyses-snapshot-testing directory +`pyenv local 3.13.0` - set the local python version to 3.13.0 +`make setup` - install the requirements +`make snapshot-test-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created You have the option to specify one or many protocols to run the analyses on. This is also described above [Running the tests against specific protocols](#running-the-tests-against-specific-protocols) -- `make local-snapshot-test PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` +- `make snapshot-test-local PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` + +### Updating the snapshots locally + +- `make snapshot-test-update-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created, updating the snapshots by passing the `--update-snapshots` flag to the test diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json index 3d8b4b072eb..f59c9684e23 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json @@ -16668,6 +16668,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json index ddb334a58e0..05fa920a764 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json @@ -41584,6 +41584,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "Bacterial culture medium (e.g., LB broth)", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json index 38872b09ff8..c709366a42a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json @@ -4919,6 +4919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json index aac975221e8..2da7a9c47bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json @@ -11824,6 +11824,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json index ff626992e43..4f1452dcdfc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json @@ -11452,6 +11452,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json index 8cd99860d7e..39491fae6aa 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json @@ -2917,6 +2917,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json index c62ceb23edd..a561da0a387 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3113,6 +3113,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json index a2aca7e252a..fe3d81be11b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9569,6 +9569,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json index bce38cbe476..f85b03c5703 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json @@ -66156,6 +66156,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json index 2ca289680ef..0f7d7d308b5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 7a7269decb6..d9895fb2c9e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -154,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json index 4891466d0b7..f892fc456ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json @@ -49707,6 +49707,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json index 64072eb8834..a877268d0bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json index dfef8b35364..3a0f63a8f99 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 0096a483ffe..957e685c737 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -17072,6 +17072,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json index e4924262e1a..35dc7ecc804 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9569,6 +9569,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json index 7bff37154bf..db42ce35fdc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json @@ -49392,6 +49392,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json index d0b11f42740..cf56c96470e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json @@ -6263,6 +6263,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a ROW partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json index f7457a3c48d..e4de2f89a14 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json @@ -33697,6 +33697,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json index 6fb9e302070..7ce2978d56a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json @@ -19352,6 +19352,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Coomassie Brilliant Blue G-250 solution ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json index f2c63721b33..d33b6cf51cb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json @@ -7997,6 +7997,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json index c463feb0552..8fece97c06c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json @@ -6203,6 +6203,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "water for ER testing", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json index 0b2e524dee6..c30b18aa93e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4492,6 +4492,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json index 6053323ac4b..3f500210e5a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json @@ -34590,6 +34590,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json index ababd25acfa..059f375baec 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json @@ -9297,6 +9297,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Rami Farawi ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json index 858286887b6..fd7b30ca845 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json @@ -19714,6 +19714,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT-2 protocol with 1ch and 8ch pipette partial/single tip configurations. Mixing tipracks and using separate tipracks. ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json index d810bd75c88..b63443781ac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json @@ -103,6 +103,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index 90bfa119fb7..a126374479b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json index 3af042768f6..f102cab8bc5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json index 843078fa552..484c6600849 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json index dfc888c15b5..3a839b9cdbd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json @@ -23420,6 +23420,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cells in DNA/ RNA Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json index d2955132ff2..72f8481bc29 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json index 2b447932025..4c45089bc7c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json @@ -6064,6 +6064,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index 0aaa562c15c..58b6e3ffb42 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -1284,6 +1284,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json index 952985449d9..dde453f20ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index d28023877a0..8cb5125c17c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -2395,6 +2395,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json index 9cad51f6d80..27656b80cca 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json @@ -1894,6 +1894,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json index c5c5f1a2e67..84bff8651d2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json @@ -49707,6 +49707,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json index 7c04e4274de..63ed50d9c04 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json index 3646ae2d522..4744b1f1992 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json @@ -22844,6 +22844,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json index e608af8c173..353a1b46f45 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json @@ -46193,6 +46193,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Resuspended in PBS", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json index c76b2aca7f9..059e7fc2b84 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json @@ -1258,6 +1258,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json index a107fa87e60..47c65a0dfc5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json index 86d3274f412..3c69dda38e7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Multiple RTP Variables with Same Name" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json index 0de0eff0022..fde783d94b8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json @@ -197,6 +197,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json index 726906c04d4..b8ef1cbc5f2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json index 1dcac6e453a..180178d1d44 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json index d8409d8db46..8623a021746 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json index afa5bb0b4d2..8b06eca9390 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json @@ -295,6 +295,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Golden RTP Examples" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json index 385da3c78a4..b1528f23cbf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json @@ -10386,6 +10386,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Diluent liquid is filled in the reservoir", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json index 5681dc28194..1441d3d1cac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json @@ -16386,6 +16386,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cell Pellet", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json index 1bf35620512..d27c90a866c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json index b2ec113fe4e..7209e028a2b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json index e545da56bd4..e30b5bee0d8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json index 19ac0d4e0f7..3ac36a59ee5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json @@ -6263,6 +6263,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a COLUMN partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json index 8f88134625a..da1993d6e56 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json @@ -94,6 +94,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json index cf3e8bf4aa3..0e079e7daa2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json @@ -3946,6 +3946,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json index 66877246558..eba57a84196 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json @@ -56935,6 +56935,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json index 63567ca7c96..f052823d867 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json @@ -512,6 +512,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Heater-shaker conflict OT-2" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json index cae3345ff13..2b5614762ba 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json index d67ff04865b..fd1c3550795 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json @@ -45393,6 +45393,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json index 80a9f7d117a..0028c36df1b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json @@ -3435,6 +3435,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.3" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json index ad8638a9e6d..86023eb8c12 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json @@ -2849,6 +2849,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json index cbd7839e9ad..b79aec33a1b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json @@ -65064,6 +65064,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "100 mM ABC in MS grade water, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json index 1b70c59e4b6..a79c72e6781 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json @@ -5042,6 +5042,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json index 30ddffb8e03..3a2911f043d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -4920,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json index bc24730fad8..c577f539508 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json @@ -31929,6 +31929,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Dilution Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json index 19cf70d2edb..00efc4b9178 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json index bd4f009a701..ae9e8d99862 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json index df37cc2db4b..48077d59118 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json index 47ce454e920..ac4311409dc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json @@ -75905,6 +75905,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json index 0c559ae74b3..235d5eb9fe3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json index 44584111a12..e0fd663c213 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json index 63aed19f5f3..d4cf07c0f99 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json @@ -2666,6 +2666,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json index 80ce54abbcb..2312c3a011e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json @@ -59919,6 +59919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 9f3a0d8a1fb..ac524674f7e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -14372,6 +14372,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json index 3cc6db1a5cd..7fb0dceab92 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json @@ -49169,6 +49169,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Volume in Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json index 5d219d91f72..7eedccb2cf8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json @@ -5373,6 +5373,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json index 2b9cd2584d3..70bb212b45b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Str RTP with unit" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json index 47511dff64f..ea3c1cc76b0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json @@ -4997,6 +4997,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json index e2fadc01642..cadf197c142 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index 919f1980537..e433acf53ff 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -3766,6 +3766,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json index b8dd13f5f42..c21c19205cf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json @@ -18477,6 +18477,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Equilibration Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json index 1d83bf0706f..933aa66cf7d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json @@ -3727,6 +3727,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json index 349fbd62034..794499f75ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json @@ -52167,6 +52167,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 2c3d142321b..d9f59af3587 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -2468,6 +2468,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json index 8c086d8fdff..405df785df9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json @@ -6474,6 +6474,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json index 85ee931590d..004f5251126 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json @@ -32273,6 +32273,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json index cbad73a3a2d..8a871949e46 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json @@ -2775,6 +2775,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index cbf301b89e7..5538166da59 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json index b0f0b8ac0bd..23fd7f389a0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json @@ -6024,6 +6024,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json index 8d4e3a960dd..aba00388845 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json @@ -1435,6 +1435,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json index 5a508d84d58..7cb88cd0308 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json @@ -153,6 +153,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json index 7808bbc2d03..e2a5dced311 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json index f951219fdff..5bc309d3cac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json @@ -9575,6 +9575,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json index eca34fc28c3..68185db5dbd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json index 965ca7d3ead..7aecea25f6a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json @@ -50265,6 +50265,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Digested Protein samples, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json index 1c3e57b481a..36400ae7de7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json index 42781ff6ea1..fe8184c0608 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json index be8c1a00d13..e623aec42f7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json @@ -22844,6 +22844,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json index 8a0a8a6a2ee..0aacb0b3e73 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json @@ -16593,6 +16593,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "generic", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 321a04e20ac..e6cb5eace9a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -14092,6 +14092,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json index f4e89bf46a3..b0eb2e93f00 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json @@ -9431,6 +9431,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Unsafe protocol ❗❗❗❗❗❗❗❗❗❗❗ will collide with tube.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json index 6afef67d006..86a33113a16 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json @@ -10373,6 +10373,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json index 01ce458ff53..52120bd6dc3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json index 60a0f1c77a3..1d043a44952 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json index 8e14d013357..93eff2447db 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json @@ -46385,6 +46385,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json index b4324589435..ec2e77260d2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json @@ -30083,6 +30083,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "ATL4", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json index ef9acd1b1a3..30f2c70b0ea 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json @@ -15276,6 +15276,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index c8389b97d75..1f453e29cf8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -2438,6 +2438,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json index 3a44acf987c..4af69fce36b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json @@ -9492,6 +9492,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json index d3338855040..ed08b660a33 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json index f86080f047c..a18485392e9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json @@ -6240,6 +6240,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json index a5b5bdb65cc..9f12179d1e2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -3766,6 +3766,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json index 5b0df3b070c..b41b7117e24 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json @@ -3706,6 +3706,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json index 4433e026fd1..6dffb02e16c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json @@ -28815,6 +28815,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json index 7005e6011ab..43f62a32282 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4492,6 +4492,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index b3637624ed4..7956e369c52 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -4920,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json index 6b2391f6118..cf8ec946db5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json index 4bcec7cf7de..db24530e196 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json @@ -41229,6 +41229,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Magnetic Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json index 5460d2d1fd7..851fd7e1fbb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json @@ -125769,6 +125769,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json index 290674f3bd6..8c741ed84ba 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json @@ -3912,6 +3912,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json index 67a07aa1297..8c38d6c5f57 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json index 14cc53aba17..4d2cd13d215 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json @@ -1977,6 +1977,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index d6eb8a28124..c2e5c309dcf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -145,6 +145,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index f59969368ab..7c817b2b869 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 4c6c38162b3..f69446ee9cb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -154,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json index 4665f21b62e..fe3f96abc67 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json index aadd38b4eaa..1a48b84ae0a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json @@ -12315,6 +12315,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json index 27e9d4f2c51..253a2bcff9d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json @@ -10777,6 +10777,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json index 131c7514649..06153dd11b4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json @@ -66156,6 +66156,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json index 5efbff81ebc..cf748ae6fa0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json @@ -7184,6 +7184,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json index 1759b7b244f..ac74dbfc2a4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json @@ -12796,6 +12796,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json index 4ad4434ab42..c9afc886f56 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json @@ -7179,6 +7179,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json index 4e8f71a17c1..b591039cbbb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json @@ -13346,6 +13346,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Index Plate color", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 1149640d8b1..f449eff0d94 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -1314,6 +1314,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json index 6e02fa8a3f3..a4d46be3d94 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json @@ -18032,6 +18032,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json index 9d35aba10fc..2f0c52a853f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json @@ -70949,6 +70949,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json index 52e87c76f46..1b664b4e963 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json @@ -5113,6 +5113,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Test this wet!!!", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json index 6f5f1f09b83..7fd14d2f851 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json @@ -25208,6 +25208,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json index f0d2d744031..7916f424286 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json @@ -40886,6 +40886,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Lysis Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 3056b873a74..718e0a0df13 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -17430,6 +17430,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json index 026977dbcc6..5b6c3c3c690 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json @@ -2836,6 +2836,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json index 99ccd21cc19..170de395195 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json @@ -59919,6 +59919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json index 6b342319f31..1ad848a9ef8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json index 7ea850030fd..87b61a0454d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json @@ -43,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Duplicate choice value" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index 1e9b318abf5..aba7dd56957 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -3591,6 +3591,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json index 65c2da26059..bd408636813 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json @@ -8241,6 +8241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT2 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json index 8b7cf7214ac..2651a003e75 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index b262ea72c0f..c52ed516ba1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -13164,6 +13164,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json index beb0aa09c29..62ea1e316b2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 2288dccf926..9e3cf07280a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -145,6 +145,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json index 0353b26aed1..2eb5308529f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Thermocycler conflict 1" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json index b22e56cb8ed..1bb680c2c4f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json @@ -39234,6 +39234,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json index 6a1c9e67b51..6c6c30ace61 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json @@ -34667,6 +34667,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json index bd05f58334f..0dd0410636f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json @@ -45,6 +45,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json index 44fbc26f5b6..1e4573b1d8e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json @@ -2580,6 +2580,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "AA BB", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json index 013da0c0d7d..f6c1ad84067 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json index 7f0ba6fd654..aca7454ff36 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json index a0e23ed018b..803f4133451 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json index 3ab5889bbf7..ea9fbf3efb7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json index ca6f70d1692..81ebf160345 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json @@ -26843,6 +26843,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json index 368bbe05d9b..709b448717c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json @@ -103,6 +103,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json index 61a7e9595ff..ed5d5a67171 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json @@ -3878,6 +3878,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json index 00f911388c0..d946aae6d9d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json @@ -40778,6 +40778,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json index 4bcefec1199..d1a7a88d075 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json @@ -1448,6 +1448,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json index d1feceae4d0..d454695d871 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json @@ -18009,6 +18009,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json index 4e89581c149..3d7f6d10b51 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json @@ -7101,6 +7101,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json index ab9fd95e4c0..d86eae54045 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json @@ -10615,6 +10615,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json index b12618b009e..e0c21a82e55 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json @@ -29,6 +29,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index f8f121ce092..50ab65351e1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -15386,6 +15386,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json index d452cf7ab52..950c5ee4395 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json @@ -1339,6 +1339,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index 2c598934321..e4fed39c549 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -180,6 +180,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json index 8ca9a88cdbf..920a648041a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json @@ -36949,6 +36949,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Amplified Libraries_1", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json index 04d54b06b4e..7ad30e9d04e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json @@ -1385,6 +1385,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json index 3152a671909..acf7455e286 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json @@ -14192,6 +14192,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json index 09e15f48097..1fdb58d69ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json @@ -3878,6 +3878,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json index 1652972327b..fe2e22fae05 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json index 13f15c638d0..cd25845d931 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3105,6 +3105,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/api/docs/v2/modules/thermocycler.rst b/api/docs/v2/modules/thermocycler.rst index 9322e0a96f0..17d57e84292 100644 --- a/api/docs/v2/modules/thermocycler.rst +++ b/api/docs/v2/modules/thermocycler.rst @@ -15,7 +15,7 @@ The examples in this section will use a Thermocycler Module GEN2 loaded as follo .. code-block:: python tc_mod = protocol.load_module(module_name="thermocyclerModuleV2") - plate = tc_mod.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt") + plate = tc_mod.load_labware(name="opentrons_96_wellplate_200ul_pcr_full_skirt") .. versionadded:: 2.13 @@ -139,6 +139,70 @@ However, this code would generate 60 lines in the protocol's run log, while exec .. versionadded:: 2.0 +Auto-sealing Lids +================= + +Starting in robot software version 8.2.0, you can use the Opentrons Tough PCR Auto-sealing Lid to reduce evaporation on the Thermocycler. The auto-sealing lids are designed for automated use with the Flex Gripper, although you can move them manually if needed. They also work with the Opentrons Flex Deck Riser adapter, which keeps lids away from the unsterilized deck and provides better access for the gripper. + +Use the following API load names for the auto-sealing lid and deck riser: + +.. list-table:: + :header-rows: 1 + + * - Labware + - API load name + * - Opentrons Tough PCR Auto-sealing Lid + - ``opentrons_tough_pcr_auto_sealing_lid`` + * - Opentrons Flex Deck Riser + - ``opentrons_flex_deck_riser`` + +Load the riser directly onto the deck with :py:meth:`.ProtocolContext.load_adapter`. Load the auto-sealing lid onto a compatible location (the deck, the riser, or another lid) with the appropriate ``load_labware()`` method. You can create a stack of up to five auto-sealing lids. If you try to stack more than five lids, the API will raise an error. + +Setting up the riser and preparing a lid to use on the Thermocycler generally consists of the following steps: + + 1. Load the riser on the deck. + 2. Load the lids onto the adapter. + 3. Load or move a PCR plate onto the Thermocycler. + 4. Move a lid onto the PCR plate. + 5. Close the Thermocycler. + +The following code sample shows how to perform these steps, using the riser and three auto-sealing lids. In a full protocol, you would likely have additional steps, such as pipetting to or from the PCR plate. + +.. code-block:: python + + # load riser + riser = protocol.load_adapter( + load_name="opentrons_flex_deck_riser", location="A2" + ) + + # load three lids + lid_1 = riser.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_2 = lid_1.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_3 = lid_2.load_labware("opentrons_tough_pcr_auto_sealing_lid") + + # load plate on Thermocycler + plate = protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", location=tc_mod + ) + + # move lid to PCR plate + protocol.move_labware(labware=lid_3, new_location=plate, use_gripper=True) + + # close Thermocycler + tc_mod.close_lid() + +.. warning:: + When using the auto-sealing lids, `do not` affix a rubber automation seal to the inside of the Thermocycler lid. The Thermocycler will not close properly. + +When you're finished with a lid, use the gripper to dispose of it in either the waste chute or a trash bin:: + + tc_mod.open_lid() + protocol.move_labware(labware=lid_3, new_location=trash, use_gripper=True) + +.. versionadded:: 2.16 + :py:class:`.TrashBin` and :py:class:`.WasteChute` objects can accept lids. + +You can then move the PCR plate off of the Thermocycler. The Flex Gripper can't move a plate that has a lid on top of it. Always move the lid first, then the plate. Changes with the GEN2 Thermocycler Module ========================================= diff --git a/api/docs/v2/parameters/use_case_sample_count.rst b/api/docs/v2/parameters/use_case_sample_count.rst index 15933752592..d7ce6529e48 100644 --- a/api/docs/v2/parameters/use_case_sample_count.rst +++ b/api/docs/v2/parameters/use_case_sample_count.rst @@ -166,7 +166,7 @@ Now we'll bring sample count into consideration as we :ref:`load the liquids ` now only checks on the first aspiration of the :py:meth:`.mix` cycle. + Version 2.20 ------------ diff --git a/api/pytest.ini b/api/pytest.ini index a8e3bbb1933..61288b3f3c1 100644 --- a/api/pytest.ini +++ b/api/pytest.ini @@ -5,3 +5,9 @@ markers = ot3_only: Test only functions using the OT3 hardware addopts = --color=yes --strict-markers asyncio_mode = auto + +# TODO this should be looked into being removed upon updating the Decoy library. The purpose of this warning is to +# catch missing attributes, but it raises for any property referenced in a test which accounts for about ~250 warnings +# which aren't serving any useful purpose and obscure other warnings. +filterwarnings = + ignore::decoy.warnings.MissingSpecAttributeWarning diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 8489da83d68..4c994fcf630 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -53,6 +53,7 @@ LoadedPipette, LoadedModule, Liquid, + LiquidClassRecordWithId, StateSummary, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree @@ -333,6 +334,7 @@ async def _do_analyze( wells=[], hasEverEnteredErrorRecovery=False, files=[], + liquidClasses=[], ), parameters=[], ) @@ -399,6 +401,7 @@ async def _analyze( pipettes=analysis.state_summary.pipettes, modules=analysis.state_summary.modules, liquids=analysis.state_summary.liquids, + liquidClasses=analysis.state_summary.liquidClasses, ) _call_for_output_of_kind( @@ -486,4 +489,5 @@ class AnalyzeResults(BaseModel): pipettes: List[LoadedPipette] modules: List[LoadedModule] liquids: List[Liquid] + liquidClasses: List[LiquidClassRecordWithId] errors: List[ErrorOccurrence] diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index 55565745d3a..53fab18392c 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -75,6 +75,7 @@ DEFAULT_GRIPPER_MOUNT_OFFSET: Final[Offset] = (84.55, -12.75, 93.85) DEFAULT_SAFE_HOME_DISTANCE: Final = 5 DEFAULT_CALIBRATION_AXIS_MAX_SPEED: Final = 30 +DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED: Final = 90 DEFAULT_MAX_SPEEDS: Final[ByGantryLoad[Dict[OT3AxisKind, float]]] = ByGantryLoad( high_throughput={ diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index c5294938fa0..8b81d2c66ef 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -69,6 +69,11 @@ def update_constraints_for_calibration_with_gantry_load( ) -> None: ... + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + ... + def update_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad ) -> None: diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 66ffc1efab1..87f886f1c74 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -50,6 +50,7 @@ get_system_constraints, get_system_constraints_for_calibration, get_system_constraints_for_plunger_acceleration, + get_system_constraints_for_emulsifying_pipette, ) from .tip_presence_manager import TipPresenceManager @@ -393,6 +394,18 @@ def update_constraints_for_calibration_with_gantry_load( f"Set system constraints for calibration: {self._move_manager.get_constraints()}" ) + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + self._move_manager.update_constraints( + get_system_constraints_for_emulsifying_pipette( + self._configuration.motion_settings, gantry_load, mount + ) + ) + log.debug( + f"Set system constraints for emulsifying pipette: {self._move_manager.get_constraints()}" + ) + def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: self._move_manager.update_constraints( get_system_constraints(self._configuration.motion_settings, gantry_load) @@ -1008,6 +1021,7 @@ def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str: lookup_name = { FirmwarePipetteName.p1000_single: "P1KS", FirmwarePipetteName.p1000_multi: "P1KM", + FirmwarePipetteName.p1000_multi_em: "P1KP", FirmwarePipetteName.p50_single: "P50S", FirmwarePipetteName.p50_multi: "P50M", FirmwarePipetteName.p1000_96: "P1KH", diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index e487f963ece..533fffe5642 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -234,6 +234,11 @@ def update_constraints_for_calibration_with_gantry_load( ) -> None: self._sim_gantry_load = gantry_load + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + pass + def update_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad ) -> None: diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index 3662e51f89b..57e74537bfd 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -2,7 +2,10 @@ from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional from typing_extensions import Literal from logging import getLogger -from opentrons.config.defaults_ot3 import DEFAULT_CALIBRATION_AXIS_MAX_SPEED +from opentrons.config.defaults_ot3 import ( + DEFAULT_CALIBRATION_AXIS_MAX_SPEED, + DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED, +) from opentrons.config.types import OT3MotionSettings, OT3CurrentSettings, GantryLoad from opentrons.hardware_control.types import ( Axis, @@ -300,6 +303,31 @@ def get_system_constraints_for_plunger_acceleration( return new_constraints +def get_system_constraints_for_emulsifying_pipette( + config: OT3MotionSettings, + gantry_load: GantryLoad, + mount: OT3Mount, +) -> "SystemConstraints[Axis]": + old_constraints = config.by_gantry_load(gantry_load) + new_constraints = {} + axis_kinds = set([k for _, v in old_constraints.items() for k in v.keys()]) + for axis_kind in axis_kinds: + for axis in Axis.of_kind(axis_kind): + if axis == Axis.of_main_tool_actuator(mount): + _max_speed = float(DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED) + else: + _max_speed = old_constraints["default_max_speed"][axis_kind] + new_constraints[axis] = AxisConstraints.build( + max_acceleration=old_constraints["acceleration"][axis_kind], + max_speed_discont=old_constraints["max_speed_discontinuity"][axis_kind], + max_direction_change_speed_discont=old_constraints[ + "direction_change_speed_discontinuity" + ][axis_kind], + max_speed=_max_speed, + ) + return new_constraints + + def _convert_to_node_id_dict( axis_pos: Coordinates[Axis, CoordinateValue], ) -> NodeIdMotionValues: diff --git a/api/src/opentrons/hardware_control/dev_types.py b/api/src/opentrons/hardware_control/dev_types.py index a6773cb9184..575a5e612d9 100644 --- a/api/src/opentrons/hardware_control/dev_types.py +++ b/api/src/opentrons/hardware_control/dev_types.py @@ -100,6 +100,8 @@ class PipetteDict(InstrumentDict): pipette_bounding_box_offsets: PipetteBoundingBoxOffsetDefinition current_nozzle_map: NozzleMap lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float class PipetteStateDict(TypedDict): diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 7fc15c4c2d3..2d63342cf19 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -28,7 +28,7 @@ CommandPreconditionViolated, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -584,21 +584,9 @@ def get_nominal_tip_overlap_dictionary_by_configuration( # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, action, self._active_tip_settings, self._pipetting_function_version + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 931c99fd4c6..7bd41e02e74 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -260,6 +260,13 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 109747ea1b9..b9355874906 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -27,7 +27,7 @@ InvalidInstrumentData, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -41,6 +41,7 @@ UlPerMmAction, PipetteName, PipetteModel, + Quirks, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -225,6 +226,9 @@ def active_tip_settings(self) -> SupportedTipsDefinition: def push_out_volume(self) -> float: return self._active_tip_settings.default_push_out_volume + def is_high_speed_pipette(self) -> bool: + return Quirks.highSpeed in self._config.quirks + def act_as(self, name: PipetteName) -> None: """Reconfigure to act as ``name``. ``name`` must be either the actual name of the pipette, or a name in its back-compatibility @@ -529,23 +533,13 @@ def tip_presence_responses(self) -> int: # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - elif action == "blowout": - return self._config.shaft_ul_per_mm - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, + action, + self._active_tip_settings, + self._pipetting_function_version, + self._config.shaft_ul_per_mm, + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( @@ -585,6 +579,7 @@ def as_dict(self) -> "Pipette.DictType": "versioned_tip_overlap": self.tip_overlap, "back_compat_names": self._config.pipette_backcompat_names, "supported_tips": self.liquid_class.supported_tips, + "shaft_ul_per_mm": self._config.shaft_ul_per_mm, } ) return self._config_as_dict diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index f64078fcbff..dda5031a8a3 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -282,6 +282,13 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index f00fffc6d9d..de2de9ae9ab 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -634,10 +634,21 @@ async def cache_pipette( self._feature_flags.use_old_aspiration_functions, ) self._pipette_handler.hardware_instruments[mount] = p + if self._pipette_handler.has_pipette(mount): + self._confirm_pipette_motion_constraints(mount) # TODO (lc 12-5-2022) Properly support backwards compatibility # when applicable return skipped + def _confirm_pipette_motion_constraints( + self, + mount: OT3Mount, + ) -> None: + if self._pipette_handler.get_pipette(mount).is_high_speed_pipette(): + self._backend.update_constraints_for_emulsifying_pipette( + mount, self.gantry_load + ) + async def cache_gripper(self, instrument_data: AttachedGripper) -> bool: """Set up gripper based on scanned information.""" grip_cal = load_gripper_calibration_offset(instrument_data.get("id")) @@ -776,6 +787,8 @@ async def _update_position_estimation( """ Function to update motor estimation for a set of axes """ + await self._backend.update_motor_status() + if axes is None: axes = [ax for ax in Axis] diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 2f35bb46764..41a061f5a94 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -30,7 +30,16 @@ ) from .disposal_locations import TrashBin, WasteChute from ._liquid import Liquid, LiquidClass -from ._types import OFF_DECK +from ._types import ( + OFF_DECK, + PLUNGER_BLOWOUT, + PLUNGER_TOP, + PLUNGER_BOTTOM, + PLUNGER_DROPTIP, + ASPIRATE_ACTION, + DISPENSE_ACTION, + BLOWOUT_ACTION, +) from ._nozzle_layout import ( COLUMN, PARTIAL_COLUMN, @@ -69,12 +78,22 @@ "Liquid", "LiquidClass", "Parameters", + # Partial Tip types "COLUMN", "PARTIAL_COLUMN", "SINGLE", "ROW", "ALL", + # Deck location types "OFF_DECK", + # Pipette plunger types + "PLUNGER_BLOWOUT", + "PLUNGER_TOP", + "PLUNGER_BOTTOM", + "PLUNGER_DROPTIP", + "ASPIRATE_ACTION", + "DISPENSE_ACTION", + "BLOWOUT_ACTION", "RuntimeParameterRequiredError", "CSVParameter", # For internal Opentrons use only: diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py index 06a23a29eb8..5aaed51edbe 100644 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from numpy import interp -from typing import Optional, Dict, Sequence, Union, Tuple +from typing import Optional, Dict, Sequence, Tuple from opentrons_shared_data.liquid_classes.liquid_class_definition import ( AspirateProperties as SharedDataAspirateProperties, @@ -23,12 +23,9 @@ class LiquidHandlingPropertyByVolume: - def __init__(self, properties_by_volume: Dict[str, float]) -> None: - self._default = properties_by_volume["default"] + def __init__(self, by_volume_property: Sequence[Tuple[float, float]]) -> None: self._properties_by_volume: Dict[float, float] = { - float(volume): value - for volume, value in properties_by_volume.items() - if volume != "default" + float(volume): value for volume, value in by_volume_property } # Volumes need to be sorted for proper interpolation of non-defined volumes, and the # corresponding values need to be in the same order for them to be interpolated correctly @@ -36,18 +33,17 @@ def __init__(self, properties_by_volume: Dict[str, float]) -> None: self._sorted_values: Tuple[float, ...] = () self._sort_volume_and_values() - @property - def default(self) -> float: - """Get the default value not associated with any volume for this property.""" - return self._default - - def as_dict(self) -> Dict[Union[float, str], float]: + def as_dict(self) -> Dict[float, float]: """Get a dictionary representation of all set volumes and values along with the default.""" - return self._properties_by_volume | {"default": self._default} + return self._properties_by_volume def get_for_volume(self, volume: float) -> float: """Get a value by volume for this property. Volumes not defined will be interpolated between set volumes.""" validated_volume = validation.ensure_positive_float(volume) + if len(self._properties_by_volume) == 0: + raise ValueError( + "No properties found for any volumes. Cannot interpolate for the given volume." + ) try: return self._properties_by_volume[validated_volume] except KeyError: @@ -66,9 +62,9 @@ def delete_for_volume(self, volume: float) -> None: """Remove an existing volume and value from the property.""" try: del self._properties_by_volume[volume] - self._sort_volume_and_values() except KeyError: raise KeyError(f"No value set for volume {volume} uL") + self._sort_volume_and_values() def _sort_volume_and_values(self) -> None: """Sort volume in increasing order along with corresponding values in matching order.""" diff --git a/api/src/opentrons/protocol_api/_types.py b/api/src/opentrons/protocol_api/_types.py index 9890e29c2bc..0e73405b3b7 100644 --- a/api/src/opentrons/protocol_api/_types.py +++ b/api/src/opentrons/protocol_api/_types.py @@ -17,3 +17,27 @@ class OffDeckType(enum.Enum): See :ref:`off-deck-location` for details on using ``OFF_DECK`` with :py:obj:`ProtocolContext.move_labware()`. """ + + +class PlungerPositionTypes(enum.Enum): + PLUNGER_TOP = "top" + PLUNGER_BOTTOM = "bottom" + PLUNGER_BLOWOUT = "blow_out" + PLUNGER_DROPTIP = "drop_tip" + + +PLUNGER_TOP: Final = PlungerPositionTypes.PLUNGER_TOP +PLUNGER_BOTTOM: Final = PlungerPositionTypes.PLUNGER_BOTTOM +PLUNGER_BLOWOUT: Final = PlungerPositionTypes.PLUNGER_BLOWOUT +PLUNGER_DROPTIP: Final = PlungerPositionTypes.PLUNGER_DROPTIP + + +class PipetteActionTypes(enum.Enum): + ASPIRATE_ACTION = "aspirate" + DISPENSE_ACTION = "dispense" + BLOWOUT_ACTION = "blowout" + + +ASPIRATE_ACTION: Final = PipetteActionTypes.ASPIRATE_ACTION +DISPENSE_ACTION: Final = PipetteActionTypes.DISPENSE_ACTION +BLOWOUT_ACTION: Final = PipetteActionTypes.BLOWOUT_ACTION diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index 70c6186a2d7..4d868bd30ac 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -1,5 +1,6 @@ """ProtocolEngine-based Labware core implementations.""" -from typing import List, Optional, cast + +from typing import List, Optional, cast, Dict from opentrons_shared_data.labware.types import ( LabwareParameters as LabwareParametersDict, @@ -22,7 +23,9 @@ from opentrons.types import DeckSlotName, NozzleMapInterface, Point, StagingSlotName +from ..._liquid import Liquid from ..labware import AbstractLabware, LabwareLoadParams + from .well import WellCore @@ -202,3 +205,21 @@ def get_deck_slot(self) -> Optional[DeckSlotName]: LocationIsStagingSlotError, ): return None + + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + self._engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId=self._labware_id, liquidId=liquid._id, volumeByWell=volumes + ) + ) + + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + self._engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId=self._labware_id, + liquidId="EMPTY", + volumeByWell={well: 0.0 for well in wells}, + ) + ) diff --git a/api/src/opentrons/protocol_api/core/engine/robot.py b/api/src/opentrons/protocol_api/core/engine/robot.py index 477f1968c5a..df80917e091 100644 --- a/api/src/opentrons/protocol_api/core/engine/robot.py +++ b/api/src/opentrons/protocol_api/core/engine/robot.py @@ -1,13 +1,16 @@ -from typing import Optional, Dict +from typing import Optional, Dict, Union from opentrons.hardware_control import SyncHardwareAPI from opentrons.types import Mount, MountType, Point, AxisType, AxisMapType +from opentrons_shared_data.pipette import types as pip_types +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_engine.types import DeckPoint, MotorAxis from opentrons.protocol_api.core.robot import AbstractRobot + _AXIS_TYPE_TO_MOTOR_AXIS = { AxisType.X: MotorAxis.X, AxisType.Y: MotorAxis.Y, @@ -39,12 +42,57 @@ def __init__( def _convert_to_engine_mount(self, axis_map: AxisMapType) -> Dict[MotorAxis, float]: return {_AXIS_TYPE_TO_MOTOR_AXIS[ax]: dist for ax, dist in axis_map.items()} - def get_pipette_type_from_engine(self, mount: Mount) -> Optional[str]: + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[pip_types.PipetteNameType]: """Get the pipette attached to the given mount.""" - engine_mount = MountType[mount.name] + if isinstance(mount, Mount): + engine_mount = MountType[mount.name] + else: + if mount.lower() == "right": + engine_mount = MountType.RIGHT + else: + engine_mount = MountType.LEFT maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) return maybe_pipette.pipetteName if maybe_pipette else None + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + return 0.0 + return self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, position_name.value + ) + + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + raise RuntimeError( + f"Cannot load plunger position as no pipette is attached to {mount}" + ) + convert_volume = ( + self._engine_client.state.pipettes.lookup_volume_to_mm_conversion( + maybe_pipette.id, volume, action.value + ) + ) + plunger_bottom = ( + self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, "bottom" + ) + ) + mm = volume / convert_volume + if robot_type == "OT-2 Standard": + position = plunger_bottom + mm + else: + position = plunger_bottom - mm + return round(position, 6) + def move_to(self, mount: Mount, destination: Point, speed: Optional[float]) -> None: engine_mount = MountType[mount.name] engine_destination = DeckPoint( diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index dba1dc6c840..34616d9eb55 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -142,22 +142,6 @@ def load_liquid( ) ) - def load_empty( - self, - ) -> None: - """Inform the system that a well is known to be empty. - - This should be done early in the protocol, at the same time as a load_liquid command might - be used. - """ - self._engine_client.execute_command( - cmd.LoadLiquidParams( - labwareId=self._labware_id, - liquidId="EMPTY", - volumeByWell={self._name: 0.0}, - ) - ) - def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" well_size = self._engine_client.state.labware.get_well_size( diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index 283aa4c4443..8bb5c66eb90 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -1,8 +1,9 @@ """The interface that implements InstrumentContext.""" + from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Generic, List, NamedTuple, Optional, TypeVar +from typing import Any, Generic, List, NamedTuple, Optional, TypeVar, Dict from opentrons_shared_data.labware.types import ( LabwareUri, @@ -11,6 +12,7 @@ ) from opentrons.types import DeckSlotName, Point, NozzleMapInterface +from .._liquid import Liquid from .well import WellCoreType @@ -133,5 +135,13 @@ def get_well_core(self, well_name: str) -> WellCoreType: def get_deck_slot(self) -> Optional[DeckSlotName]: """Get the deck slot the labware or its parent is in, if any.""" + @abstractmethod + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + + @abstractmethod + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + LabwareCoreType = TypeVar("LabwareCoreType", bound=AbstractLabware[Any]) diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py index 1b00dfcfecf..3957edb106c 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Dict from opentrons.calibration_storage import helpers from opentrons.protocols.geometry.labware_geometry import LabwareGeometry @@ -8,6 +8,7 @@ from opentrons_shared_data.labware.types import LabwareParameters, LabwareDefinition +from ..._liquid import Liquid from ..labware import AbstractLabware, LabwareLoadParams from .legacy_well_core import LegacyWellCore from .well_geometry import WellGeometry @@ -220,3 +221,11 @@ def get_deck_slot(self) -> Optional[DeckSlotName]: """Get the deck slot the labware is in, if in a deck slot.""" slot = self._geometry.parent.labware.first_parent() return DeckSlotName.from_primitive(slot) if slot is not None else None + + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + assert False, "load_liquid only supported in API version 2.22 & later" + + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + assert False, "load_empty only supported in API version 2.22 & later" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index e672a6fe839..d0b95ed82ca 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -37,7 +37,6 @@ class LegacyProtocolCore( LegacyInstrumentCore, LegacyLabwareCore, legacy_module_core.LegacyModuleCore, - # None, ] ): def __init__( diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py index 891f0f1b681..a88dd2eee80 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py @@ -114,10 +114,6 @@ def load_liquid( """Load liquid into a well.""" raise APIVersionError(api_element="Loading a liquid") - def load_empty(self) -> None: - """Mark a well as empty.""" - assert False, "load_empty only supported on engine core" - def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" return self._geometry.from_center_cartesian(x, y, z) diff --git a/api/src/opentrons/protocol_api/core/robot.py b/api/src/opentrons/protocol_api/core/robot.py index 7eade528413..95def3e17f3 100644 --- a/api/src/opentrons/protocol_api/core/robot.py +++ b/api/src/opentrons/protocol_api/core/robot.py @@ -1,12 +1,28 @@ from abc import abstractmethod, ABC -from typing import Optional +from typing import Optional, Union from opentrons.types import AxisMapType, Mount, Point +from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons.protocol_api._types import PlungerPositionTypes, PipetteActionTypes class AbstractRobot(ABC): @abstractmethod - def get_pipette_type_from_engine(self, mount: Mount) -> Optional[str]: + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[PipetteNameType]: + ... + + @abstractmethod + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + ... + + @abstractmethod + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: ... @abstractmethod diff --git a/api/src/opentrons/protocol_api/core/well.py b/api/src/opentrons/protocol_api/core/well.py index 24489bb04e7..bd58963a59c 100644 --- a/api/src/opentrons/protocol_api/core/well.py +++ b/api/src/opentrons/protocol_api/core/well.py @@ -79,10 +79,6 @@ def load_liquid( ) -> None: """Load liquid into a well.""" - @abstractmethod - def load_empty(self) -> None: - """Mark a well as containing no liquid.""" - @abstractmethod def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 3aee270e9a7..a8d0a4b5765 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -512,6 +512,8 @@ def mix( ``pipette.mix(1, location=wellplate['A1'])`` is a valid call, but ``pipette.mix(1, wellplate['A1'])`` is not. + .. versionchanged:: 2.21 + Does not repeatedly check for liquid presence. """ _log.debug( "mixing {}uL with {} repetitions in {} at rate={}".format( diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 6161a785829..4ac7713f83a 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -1,4 +1,4 @@ -""" opentrons.protocol_api.labware: classes and functions for labware handling +"""opentrons.protocol_api.labware: classes and functions for labware handling This module provides things like :py:class:`Labware`, and :py:class:`Well` to encapsulate labware instances used in protocols @@ -13,7 +13,18 @@ import logging from itertools import dropwhile -from typing import TYPE_CHECKING, Any, List, Dict, Optional, Union, Tuple, cast +from typing import ( + TYPE_CHECKING, + Any, + List, + Dict, + Optional, + Union, + Tuple, + cast, + Sequence, + Mapping, +) from opentrons_shared_data.labware.types import LabwareDefinition, LabwareParameters @@ -281,19 +292,15 @@ def load_liquid(self, liquid: Liquid, volume: float) -> None: :param Liquid liquid: The liquid to load into the well. :param float volume: The volume of liquid to load, in µL. - .. note:: - In API version 2.22 and later, use :py:meth:`~.Well.load_empty()` to mark a well as empty at the beginning of a protocol, rather than using this method with ``volume=0``. + .. deprecated:: 2.22 + In API version 2.22 and later, use :py:meth:`~Labware.load_liquid`, :py:meth:`~Labware.load_liquid_by_well`, + or :py:meth:`~Labware.load_empty` to load liquid into a well. """ self._core.load_liquid( liquid=liquid, volume=volume, ) - @requires_version(2, 22) - def load_empty(self) -> None: - """Mark a well as empty.""" - self._core.load_empty() - def _from_center_cartesian(self, x: float, y: float, z: float) -> Point: """ Private version of from_center_cartesian. Present only for backward @@ -1113,6 +1120,141 @@ def reset(self) -> None: """ self._core.reset_tips() + @requires_version(2, 22) + def load_liquid( + self, wells: Sequence[Union[str, Well]], volume: float, liquid: Liquid + ) -> None: + """Mark several wells as containing the same amount of liquid. + + This method should be called at the beginning of a protocol, soon after loading the labware and before + liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware + has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or + :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked. + + For example, to load 10µL of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`) + into all the wells of a labware, you could call ``labware.load_liquid(labware.wells(), 10, water)``. + + If you want to load different volumes of liquid into different wells, use :py:meth:`~Labware.load_liquid_by_well`. + + If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`. + + :param wells: The wells to load the liquid into. + :type wells: List of well names or list of Well objects, for instance from :py:meth:`~Labware.wells`. + + :param volume: The volume of liquid to load into each well, in 10µL. + :type volume: float + + :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid` + :type liquid: Liquid + """ + well_names: List[str] = [] + for well in wells: + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in labware {self.name}. The elements of wells should name wells in this labware." + ) + well_names.append(well) + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in labware {self.name}. The elements of wells should be wells of this labware." + ) + well_names.append(well.well_name) + else: + raise TypeError( + f"Unexpected type for element {repr(well)}. The elements of wells should be Well instances or well names." + ) + if not isinstance(volume, (float, int)): + raise TypeError( + f"Unexpected type for volume {repr(volume)}. Volume should be a number in microliters." + ) + self._core.load_liquid({well_name: volume for well_name in well_names}, liquid) + + @requires_version(2, 22) + def load_liquid_by_well( + self, volumes: Mapping[Union[str, Well], float], liquid: Liquid + ) -> None: + """Mark several wells as containing unique volumes of liquid. + + This method should be called at the beginning of a protocol, soon after loading the labware and before + liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware + has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or + :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked. + + For example, to load a decreasing amount of of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`) + into each successive well of a row, you could call + ``labware.load_liquid_by_well({'A1': 1000, 'A2': 950, 'A3': 900, ..., 'A12': 600}, water)`` + + If you want to load the same volume of a liquid into multiple wells, it is often easier to use :py:meth:`~Labware.load_liquid`. + + If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`. + + :param volumes: A dictionary of well names (or :py:class:`Well` objects, for instance from ``labware['A1']``) + :type wells: Dict[Union[str, Well], float] + + :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid` + :type liquid: Liquid + """ + verified_volumes: Dict[str, float] = {} + for well, volume in volumes.items(): + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in {self.name}. The keys of volumes should name wells in this labware" + ) + verified_volumes[well] = volume + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in {self.name}. The keys of volumes should be wells of this labware" + ) + verified_volumes[well.well_name] = volume + else: + raise TypeError( + f"Unexpected type for well name {repr(well)}. The keys of volumes should be Well instances or well names." + ) + if not isinstance(volume, (float, int)): + raise TypeError( + f"Unexpected type for volume {repr(volume)}. The values of volumes should be numbers in microliters." + ) + self._core.load_liquid(verified_volumes, liquid) + + @requires_version(2, 22) + def load_empty(self, wells: Sequence[Union[Well, str]]) -> None: + """Mark several wells as empty. + + This method should be called at the beginning of a protocol, soon after loading the labware and before liquid handling + operations begin. It is a base of information for liquid tracking functionality. If a well in a labware has not been named + in a call to :py:meth:`Labware.load_empty`, :py:meth:`Labware.load_liquid`, or :py:meth:`Labware.load_liquid_by_well`, the + volume it contains is unknown and the well's liquid will not be tracked. + + For instance, to mark all wells in the labware as empty, you can call ``labware.load_empty(labware.wells())``. + + :param wells: The list of wells to mark empty. To mark all wells as empty, pass ``labware.wells()``. You can also specify + wells by their names (for instance, ``labware.load_empty(['A1', 'A2'])``). + :type wells: Union[List[Well], List[str]] + """ + well_names: List[str] = [] + for well in wells: + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in {self.name}. The elements of wells should name wells in this labware." + ) + well_names.append(well) + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in {self.name}. The elements of wells should be wells of this labware." + ) + well_names.append(well.well_name) + else: + raise TypeError( + f"Unexpected type for well name {repr(well)}. The elements of wells should be Well instances or well names." + ) + self._core.load_empty(well_names) + # TODO(mc, 2022-11-09): implementation detail, move to core def split_tipracks(tip_racks: List[Labware]) -> Tuple[Labware, List[Labware]]: diff --git a/api/src/opentrons/protocol_api/robot_context.py b/api/src/opentrons/protocol_api/robot_context.py index 272330e1664..5b0e578f9bb 100644 --- a/api/src/opentrons/protocol_api/robot_context.py +++ b/api/src/opentrons/protocol_api/robot_context.py @@ -19,6 +19,7 @@ from .core.common import ProtocolCore, RobotCore from .module_contexts import ModuleContext from .labware import Labware +from ._types import PipetteActionTypes, PlungerPositionTypes class HardwareManager(NamedTuple): @@ -200,14 +201,43 @@ def axis_coordinates_for( raise TypeError("You must specify a location to move to.") def plunger_coordinates_for_volume( - self, mount: Union[Mount, str], volume: float - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], volume: float, action: PipetteActionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from volume. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + + pipette_position = self._core.get_plunger_position_from_volume( + mount, volume, action, self._protocol_core.robot_type + ) + return {pipette_axis: pipette_position} def plunger_coordinates_for_named_position( - self, mount: Union[Mount, str], position_name: str - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], position_name: PlungerPositionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from position_name. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + pipette_position = self._core.get_plunger_position_from_name( + mount, position_name + ) + return {pipette_axis: pipette_position} def build_axis_map(self, axis_map: StringAxisMap) -> AxisMapType: """Take in a :py:class:`.types.StringAxisMap` and output a :py:class:`.types.AxisMapType`. diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 25599189916..7efaef7199d 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -57,6 +57,8 @@ ModuleModel, ModuleDefinition, Liquid, + LiquidClassRecord, + LiquidClassRecordWithId, AllNozzleLayoutConfiguration, SingleNozzleLayoutConfiguration, RowNozzleLayoutConfiguration, @@ -122,6 +124,8 @@ "ModuleModel", "ModuleDefinition", "Liquid", + "LiquidClassRecord", + "LiquidClassRecordWithId", "AllNozzleLayoutConfiguration", "SingleNozzleLayoutConfiguration", "RowNozzleLayoutConfiguration", diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index 8743fd1383b..1ca848858b6 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -80,6 +80,10 @@ async def execute( # noqa: C901 raise CannotPerformModuleAction( "Cannot perform Read action on Absorbance Reader without calling `.initialize(...)` first." ) + if abs_reader_substate.is_lid_on is False: + raise CannotPerformModuleAction( + "Cannot perform Read action on Absorbance Reader with the lid open. Try calling `.close_lid()` first." + ) # TODO: we need to return a file ID and increase the file count even when a moduel is not attached if ( diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 1dce8e8c2ca..38dbe03c7e0 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -15,6 +15,7 @@ from .movement_common import ( LiquidHandlingWellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) from .command import ( @@ -60,7 +61,7 @@ class AspirateResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[AspirateResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -120,6 +121,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, @@ -127,6 +129,8 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: current_well=current_well, operation_volume=-params.volume, ) + if isinstance(move_result, DefinedErrorData): + return move_result aspirate_result = await aspirate_in_place( pipette_id=pipette_id, @@ -185,7 +189,11 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: ) -class Aspirate(BaseCommand[AspirateParams, AspirateResult, OverpressureError]): +class Aspirate( + BaseCommand[ + AspirateParams, AspirateResult, OverpressureError | StallOrCollisionError + ] +): """Aspirate command model.""" commandType: AspirateCommandType = "aspirate" diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index 2469036186c..b2e8765b4a1 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -11,7 +11,12 @@ FlowRateMixin, blow_out_in_place, ) -from .movement_common import WellLocationMixin, DestinationPositionResult, move_to_well +from .movement_common import ( + WellLocationMixin, + DestinationPositionResult, + move_to_well, + StallOrCollisionError, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -19,7 +24,6 @@ DefinedErrorData, SuccessData, ) -from ..errors.error_occurrence import ErrorOccurrence from ..state.update_types import StateUpdate from opentrons.hardware_control import HardwareControlAPI @@ -48,7 +52,7 @@ class BlowOutResult(DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[BlowOutResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -74,11 +78,14 @@ async def execute(self, params: BlowOutParams) -> _ExecuteReturn: """Move to and blow-out the requested well.""" move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, ) + if isinstance(move_result, DefinedErrorData): + return move_result blow_out_result = await blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate, @@ -112,7 +119,13 @@ async def execute(self, params: BlowOutParams) -> _ExecuteReturn: ) -class BlowOut(BaseCommand[BlowOutParams, BlowOutResult, ErrorOccurrence]): +class BlowOut( + BaseCommand[ + BlowOutParams, + BlowOutResult, + OverpressureError | StallOrCollisionError, + ] +): """Blow-out command model.""" commandType: BlowOutCommandType = "blowout" diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 7db8045f0cb..c33f55e2e01 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -13,6 +13,7 @@ LiquidNotFoundError, TipPhysicallyAttachedError, ) +from .movement_common import StallOrCollisionError from . import absorbance_reader from . import heater_shaker @@ -754,6 +755,7 @@ DefinedErrorData[OverpressureError], DefinedErrorData[LiquidNotFoundError], DefinedErrorData[GripperMovementError], + DefinedErrorData[StallOrCollisionError], ] diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 78b9ebed6b3..18f157934d4 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -19,6 +19,7 @@ from .movement_common import ( LiquidHandlingWellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) from .command import ( @@ -57,7 +58,7 @@ class DispenseResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[DispenseResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -88,11 +89,14 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + if isinstance(move_result, DefinedErrorData): + return move_result dispense_result = await dispense_in_place( pipette_id=params.pipetteId, volume=volume, @@ -159,7 +163,11 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: ) -class Dispense(BaseCommand[DispenseParams, DispenseResult, OverpressureError]): +class Dispense( + BaseCommand[ + DispenseParams, DispenseResult, OverpressureError | StallOrCollisionError + ] +): """Dispense command model.""" commandType: DispenseCommandType = "dispense" diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 52f6c86ffbd..4faee3d5e2f 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -15,7 +15,11 @@ PipetteIdMixin, TipPhysicallyAttachedError, ) -from .movement_common import DestinationPositionResult, move_to_well +from .movement_common import ( + DestinationPositionResult, + move_to_well, + StallOrCollisionError, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -69,7 +73,9 @@ class DropTipResult(DestinationPositionResult): _ExecuteReturn = ( - SuccessData[DropTipResult] | DefinedErrorData[TipPhysicallyAttachedError] + SuccessData[DropTipResult] + | DefinedErrorData[TipPhysicallyAttachedError] + | DefinedErrorData[StallOrCollisionError] ) @@ -117,11 +123,14 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: move_result = await move_to_well( movement=self._movement_handler, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=tip_drop_location, ) + if isinstance(move_result, DefinedErrorData): + return move_result try: await self._tip_handler.drop_tip( @@ -166,7 +175,11 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: ) -class DropTip(BaseCommand[DropTipParams, DropTipResult, TipPhysicallyAttachedError]): +class DropTip( + BaseCommand[ + DropTipParams, DropTipResult, TipPhysicallyAttachedError | StallOrCollisionError + ] +): """Drop tip command model.""" commandType: DropTipCommandType = "dropTip" diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index b6c51613263..b99e6ac11b1 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -27,6 +27,7 @@ from .movement_common import ( WellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) from .command import ( @@ -91,9 +92,11 @@ class TryLiquidProbeResult(DestinationPositionResult): _LiquidProbeExecuteReturn = Union[ SuccessData[LiquidProbeResult], - DefinedErrorData[LiquidNotFoundError], + DefinedErrorData[LiquidNotFoundError] | DefinedErrorData[StallOrCollisionError], ] -_TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult] +_TryLiquidProbeExecuteReturn = ( + SuccessData[TryLiquidProbeResult] | DefinedErrorData[StallOrCollisionError] +) class _ExecuteCommonResult(NamedTuple): @@ -110,8 +113,9 @@ async def _execute_common( state_view: StateView, movement: MovementHandler, pipetting: PipettingHandler, + model_utils: ModelUtils, params: _CommonParams, -) -> _ExecuteCommonResult: +) -> _ExecuteCommonResult | DefinedErrorData[StallOrCollisionError]: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName @@ -145,12 +149,14 @@ async def _execute_common( # liquid_probe process start position move_result = await move_to_well( movement=movement, + model_utils=model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, ) - + if isinstance(move_result, DefinedErrorData): + return move_result try: z_pos = await pipetting.liquid_probe_in_place( pipette_id=pipette_id, @@ -206,9 +212,16 @@ async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: MustHomeError: as an undefined error, if the plunger is not in a valid position. """ - z_pos_or_error, state_update, deck_point = await _execute_common( - self._state_view, self._movement, self._pipetting, params + result = await _execute_common( + state_view=self._state_view, + movement=self._movement, + pipetting=self._pipetting, + model_utils=self._model_utils, + params=params, ) + if isinstance(result, DefinedErrorData): + return result + z_pos_or_error, state_update, deck_point = result if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): state_update.set_liquid_probed( labware_id=params.labwareId, @@ -282,9 +295,16 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: found, `tryLiquidProbe` returns a success result with `z_position=null` instead of a defined error. """ - z_pos_or_error, state_update, deck_point = await _execute_common( - self._state_view, self._movement, self._pipetting, params + result = await _execute_common( + state_view=self._state_view, + movement=self._movement, + pipetting=self._pipetting, + model_utils=self._model_utils, + params=params, ) + if isinstance(result, DefinedErrorData): + return result + z_pos_or_error, state_update, deck_point = result if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): z_pos = None @@ -316,7 +336,11 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: class LiquidProbe( - BaseCommand[LiquidProbeParams, LiquidProbeResult, LiquidNotFoundError] + BaseCommand[ + LiquidProbeParams, + LiquidProbeResult, + LiquidNotFoundError | StallOrCollisionError, + ] ): """The model for a full `liquidProbe` command.""" @@ -328,7 +352,7 @@ class LiquidProbe( class TryLiquidProbe( - BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, ErrorOccurrence] + BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, StallOrCollisionError] ): """The model for a full `tryLiquidProbe` command.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_relative.py b/api/src/opentrons/protocol_engine/commands/move_relative.py index cc2ae7c597a..54c877a3693 100644 --- a/api/src/opentrons/protocol_engine/commands/move_relative.py +++ b/api/src/opentrons/protocol_engine/commands/move_relative.py @@ -5,14 +5,23 @@ from typing_extensions import Literal -from ..state import update_types -from ..types import MovementAxis, DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence -from .movement_common import DestinationPositionResult +from ..types import MovementAxis +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) +from .movement_common import ( + DestinationPositionResult, + move_relative, + StallOrCollisionError, +) if TYPE_CHECKING: from ..execution import MovementHandler + from ..resources.model_utils import ModelUtils MoveRelativeCommandType = Literal["moveRelative"] @@ -39,40 +48,41 @@ class MoveRelativeResult(DestinationPositionResult): class MoveRelativeImplementation( - AbstractCommandImpl[MoveRelativeParams, SuccessData[MoveRelativeResult]] + AbstractCommandImpl[ + MoveRelativeParams, + SuccessData[MoveRelativeResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Move relative command implementation.""" - def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + def __init__( + self, movement: MovementHandler, model_utils: ModelUtils, **kwargs: object + ) -> None: self._movement = movement + self._model_utils = model_utils async def execute( self, params: MoveRelativeParams - ) -> SuccessData[MoveRelativeResult]: + ) -> SuccessData[MoveRelativeResult] | DefinedErrorData[StallOrCollisionError]: """Move (jog) a given pipette a relative distance.""" - state_update = update_types.StateUpdate() - - x, y, z = await self._movement.move_relative( + result = await move_relative( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, axis=params.axis, distance=params.distance, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.pipette_location = update_types.PipetteLocationUpdate( - pipette_id=params.pipetteId, - # TODO(jbl 2023-02-14): Need to investigate whether move relative should clear current location - new_location=update_types.NO_CHANGE, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveRelativeResult(position=deck_point), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveRelativeResult(position=result.public.position), + state_update=result.state_update, + ) class MoveRelative( - BaseCommand[MoveRelativeParams, MoveRelativeResult, ErrorOccurrence] + BaseCommand[MoveRelativeParams, MoveRelativeResult, StallOrCollisionError] ): """Command to move (jog) a given pipette a relative distance.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index f3dd3f57c56..7380a01951a 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -7,8 +7,7 @@ from opentrons_shared_data.pipette.types import PipetteNameType from ..errors import LocationNotAccessibleByPipetteError -from ..state import update_types -from ..types import DeckPoint, AddressableOffsetVector +from ..types import AddressableOffsetVector from ..resources import fixture_validation from .pipetting_common import ( PipetteIdMixin, @@ -16,13 +15,21 @@ from .movement_common import ( MovementMixin, DestinationPositionResult, + move_to_addressable_area, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] @@ -76,25 +83,29 @@ class MoveToAddressableAreaResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToAddressableAreaResult] | DefinedErrorData[StallOrCollisionError] +) + + class MoveToAddressableAreaImplementation( - AbstractCommandImpl[ - MoveToAddressableAreaParams, SuccessData[MoveToAddressableAreaResult] - ] + AbstractCommandImpl[MoveToAddressableAreaParams, _ExecuteReturn] ): """Move to addressable area command implementation.""" def __init__( - self, movement: MovementHandler, state_view: StateView, **kwargs: object + self, + movement: MovementHandler, + state_view: StateView, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._movement = movement self._state_view = state_view + self._model_utils = model_utils - async def execute( - self, params: MoveToAddressableAreaParams - ) -> SuccessData[MoveToAddressableAreaResult]: + async def execute(self, params: MoveToAddressableAreaParams) -> _ExecuteReturn: """Move the requested pipette to the requested addressable area.""" - state_update = update_types.StateUpdate() - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName ) @@ -117,7 +128,9 @@ async def execute( f"Cannot move pipette to staging slot {params.addressableAreaName}" ) - x, y, z = await self._movement.move_to_addressable_area( + result = await move_to_addressable_area( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, addressable_area_name=params.addressableAreaName, offset=params.offset, @@ -127,22 +140,20 @@ async def execute( stay_at_highest_possible_z=params.stayAtHighestPossibleZ, highest_possible_z_extra_offset=extra_z_offset, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_addressable_area_name=params.addressableAreaName, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z)), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToAddressableAreaResult(position=result.public.position), + state_update=result.state_update, + ) class MoveToAddressableArea( BaseCommand[ - MoveToAddressableAreaParams, MoveToAddressableAreaResult, ErrorOccurrence + MoveToAddressableAreaParams, + MoveToAddressableAreaResult, + StallOrCollisionError, ] ): """Move to addressable area command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py index 835209c4386..679e769cc2e 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py @@ -5,8 +5,7 @@ from typing_extensions import Literal from ..errors import LocationNotAccessibleByPipetteError -from ..state import update_types -from ..types import DeckPoint, AddressableOffsetVector +from ..types import AddressableOffsetVector from ..resources import fixture_validation from .pipetting_common import ( PipetteIdMixin, @@ -14,13 +13,21 @@ from .movement_common import ( MovementMixin, DestinationPositionResult, + move_to_addressable_area, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToAddressableAreaForDropTipCommandType = Literal["moveToAddressableAreaForDropTip"] @@ -85,26 +92,32 @@ class MoveToAddressableAreaForDropTipResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToAddressableAreaForDropTipResult] + | DefinedErrorData[StallOrCollisionError] +) + + class MoveToAddressableAreaForDropTipImplementation( - AbstractCommandImpl[ - MoveToAddressableAreaForDropTipParams, - SuccessData[MoveToAddressableAreaForDropTipResult], - ] + AbstractCommandImpl[MoveToAddressableAreaForDropTipParams, _ExecuteReturn] ): """Move to addressable area for drop tip command implementation.""" def __init__( - self, movement: MovementHandler, state_view: StateView, **kwargs: object + self, + movement: MovementHandler, + state_view: StateView, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._movement = movement self._state_view = state_view + self._model_utils = model_utils async def execute( self, params: MoveToAddressableAreaForDropTipParams - ) -> SuccessData[MoveToAddressableAreaForDropTipResult]: + ) -> _ExecuteReturn: """Move the requested pipette to the requested addressable area in preperation of a drop tip.""" - state_update = update_types.StateUpdate() - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName ) @@ -122,7 +135,9 @@ async def execute( else: offset = params.offset - x, y, z = await self._movement.move_to_addressable_area( + result = await move_to_addressable_area( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, addressable_area_name=params.addressableAreaName, offset=offset, @@ -131,26 +146,22 @@ async def execute( speed=params.speed, ignore_tip_configuration=params.ignoreTipConfiguration, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_addressable_area_name=params.addressableAreaName, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToAddressableAreaForDropTipResult( - position=DeckPoint(x=x, y=y, z=z) - ), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToAddressableAreaForDropTipResult( + position=result.public.position, + ), + state_update=result.state_update, + ) class MoveToAddressableAreaForDropTip( BaseCommand[ MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, - ErrorOccurrence, + StallOrCollisionError, ] ): """Move to addressable area for drop tip command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py index 99df6be868b..36b7ff64ed0 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py @@ -6,15 +6,25 @@ from typing_extensions import Literal -from ..state import update_types from ..types import DeckPoint from .pipetting_common import PipetteIdMixin -from .movement_common import MovementMixin, DestinationPositionResult -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .movement_common import ( + MovementMixin, + DestinationPositionResult, + move_to_coordinates, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) if TYPE_CHECKING: from ..execution import MovementHandler + from ..resources.model_utils import ModelUtils MoveToCoordinatesCommandType = Literal["moveToCoordinates"] @@ -35,44 +45,47 @@ class MoveToCoordinatesResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToCoordinatesResult] | DefinedErrorData[StallOrCollisionError] +) + + class MoveToCoordinatesImplementation( - AbstractCommandImpl[MoveToCoordinatesParams, SuccessData[MoveToCoordinatesResult]] + AbstractCommandImpl[MoveToCoordinatesParams, _ExecuteReturn] ): """Move to coordinates command implementation.""" def __init__( self, movement: MovementHandler, + model_utils: ModelUtils, **kwargs: object, ) -> None: self._movement = movement + self._model_utils = model_utils - async def execute( - self, params: MoveToCoordinatesParams - ) -> SuccessData[MoveToCoordinatesResult]: + async def execute(self, params: MoveToCoordinatesParams) -> _ExecuteReturn: """Move the requested pipette to the requested coordinates.""" - state_update = update_types.StateUpdate() - - x, y, z = await self._movement.move_to_coordinates( + result = await move_to_coordinates( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, deck_coordinates=params.coordinates, direct=params.forceDirect, additional_min_travel_z=params.minimumZHeight, speed=params.speed, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.pipette_location = update_types.PipetteLocationUpdate( - pipette_id=params.pipetteId, new_location=None, new_deck_point=deck_point - ) - - return SuccessData( - public=MoveToCoordinatesResult(position=DeckPoint(x=x, y=y, z=z)), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToCoordinatesResult(position=result.public.position), + state_update=result.state_update, + ) class MoveToCoordinates( - BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, ErrorOccurrence] + BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, StallOrCollisionError] ): """Move to well command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 67819cb0d58..6aaf398650f 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -11,15 +11,22 @@ WellLocationMixin, MovementMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) from ..errors import LabwareIsTipRackError if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToWellCommandType = Literal["moveToWell"] @@ -37,17 +44,27 @@ class MoveToWellResult(DestinationPositionResult): class MoveToWellImplementation( - AbstractCommandImpl[MoveToWellParams, SuccessData[MoveToWellResult]] + AbstractCommandImpl[ + MoveToWellParams, + SuccessData[MoveToWellResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Move to well command implementation.""" def __init__( - self, state_view: StateView, movement: MovementHandler, **kwargs: object + self, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement + self._model_utils = model_utils - async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResult]: + async def execute( + self, params: MoveToWellParams + ) -> SuccessData[MoveToWellResult] | DefinedErrorData[StallOrCollisionError]: """Move the requested pipette to the requested well.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -63,6 +80,7 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul ) move_result = await move_to_well( + model_utils=self._model_utils, movement=self._movement, pipette_id=pipette_id, labware_id=labware_id, @@ -72,14 +90,18 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul minimum_z_height=params.minimumZHeight, speed=params.speed, ) - - return SuccessData( - public=MoveToWellResult(position=move_result.public.position), - state_update=move_result.state_update, - ) + if isinstance(move_result, DefinedErrorData): + return move_result + else: + return SuccessData( + public=MoveToWellResult(position=move_result.public.position), + state_update=move_result.state_update, + ) -class MoveToWell(BaseCommand[MoveToWellParams, MoveToWellResult, ErrorOccurrence]): +class MoveToWell( + BaseCommand[MoveToWellParams, MoveToWellResult, StallOrCollisionError] +): """Move to well command model.""" commandType: MoveToWellCommandType = "moveToWell" diff --git a/api/src/opentrons/protocol_engine/commands/movement_common.py b/api/src/opentrons/protocol_engine/commands/movement_common.py index 4d5d38544f6..7917daa8613 100644 --- a/api/src/opentrons/protocol_engine/commands/movement_common.py +++ b/api/src/opentrons/protocol_engine/commands/movement_common.py @@ -2,15 +2,28 @@ from __future__ import annotations -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING, Literal + from pydantic import BaseModel, Field -from ..types import WellLocation, LiquidHandlingWellLocation, DeckPoint, CurrentWell -from ..state.update_types import StateUpdate -from .command import SuccessData + +from opentrons_shared_data.errors import ErrorCodes +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError +from ..errors import ErrorOccurrence +from ..types import ( + WellLocation, + LiquidHandlingWellLocation, + DeckPoint, + CurrentWell, + MovementAxis, + AddressableOffsetVector, +) +from ..state.update_types import StateUpdate, PipetteLocationUpdate +from .command import SuccessData, DefinedErrorData if TYPE_CHECKING: from ..execution.movement import MovementHandler + from ..resources.model_utils import ModelUtils class WellLocationMixin(BaseModel): @@ -79,6 +92,22 @@ class MovementMixin(BaseModel): ) +class StallOrCollisionError(ErrorOccurrence): + """Returned when the machine detects that axis encoders are reading a different position than expected. + + All axes are stopped at the point where the error was encountered. + + The next thing to move the machine must account for the robot not having a valid estimate + of its position. It should be a `home` or `unsafe/updatePositionEstimators`. + """ + + isDefined: bool = True + errorType: Literal["stallOrCollision"] = "stallOrCollision" + + errorCode: str = ErrorCodes.STALL_OR_COLLISION_DETECTED.value.code + detail: str = ErrorCodes.STALL_OR_COLLISION_DETECTED.value.detail + + class DestinationPositionResult(BaseModel): """Mixin for command results that move a pipette.""" @@ -99,11 +128,14 @@ class DestinationPositionResult(BaseModel): ) -MoveToWellOperationReturn = SuccessData[DestinationPositionResult] +MoveToWellOperationReturn = ( + SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError] +) async def move_to_well( movement: MovementHandler, + model_utils: ModelUtils, pipette_id: str, labware_id: str, well_name: str, @@ -115,26 +147,181 @@ async def move_to_well( operation_volume: Optional[float] = None, ) -> MoveToWellOperationReturn: """Execute a move to well microoperation.""" - position = await movement.move_to_well( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - well_location=well_location, - current_well=current_well, - force_direct=force_direct, - minimum_z_height=minimum_z_height, - speed=speed, - operation_volume=operation_volume, - ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - return SuccessData( - public=DestinationPositionResult( - position=deck_point, - ), - state_update=StateUpdate().set_pipette_location( + try: + position = await movement.move_to_well( pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, - ), - ) + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=current_well, + force_direct=force_direct, + minimum_z_height=minimum_z_height, + speed=speed, + operation_volume=operation_volume, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DestinationPositionResult( + position=deck_point, + ), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ), + ) + + +async def move_relative( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + axis: MovementAxis, + distance: float, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move by a fixed displacement from the current position.""" + try: + position = await movement.move_relative(pipette_id, axis, distance) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DestinationPositionResult( + position=deck_point, + ), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, new_deck_point=deck_point + ), + ) + + +async def move_to_addressable_area( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + addressable_area_name: str, + offset: AddressableOffsetVector, + force_direct: bool = False, + minimum_z_height: float | None = None, + speed: float | None = None, + stay_at_highest_possible_z: bool = False, + ignore_tip_configuration: bool | None = True, + highest_possible_z_extra_offset: float | None = None, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move to an addressable area identified by name.""" + try: + x, y, z = await movement.move_to_addressable_area( + pipette_id=pipette_id, + addressable_area_name=addressable_area_name, + offset=offset, + force_direct=force_direct, + minimum_z_height=minimum_z_height, + speed=speed, + stay_at_highest_possible_z=stay_at_highest_possible_z, + ignore_tip_configuration=ignore_tip_configuration, + highest_possible_z_extra_offset=highest_possible_z_extra_offset, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=x, y=y, z=z) + return SuccessData( + public=DestinationPositionResult(position=deck_point), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, + new_addressable_area_name=addressable_area_name, + new_deck_point=deck_point, + ), + ) + + +async def move_to_coordinates( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + deck_coordinates: DeckPoint, + direct: bool, + additional_min_travel_z: float | None, + speed: float | None = None, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move to a set of coordinates.""" + try: + x, y, z = await movement.move_to_coordinates( + pipette_id=pipette_id, + deck_coordinates=deck_coordinates, + direct=direct, + additional_min_travel_z=additional_min_travel_z, + speed=speed, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=x, y=y, z=z) + + return SuccessData( + public=DestinationPositionResult(position=DeckPoint(x=x, y=y, z=z)), + state_update=StateUpdate( + pipette_location=PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=None, + new_deck_point=deck_point, + ) + ), + ) diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 210027d0a45..af8723a5bba 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -14,7 +14,11 @@ from .pipetting_common import ( PipetteIdMixin, ) -from .movement_common import DestinationPositionResult, move_to_well +from .movement_common import ( + DestinationPositionResult, + StallOrCollisionError, + move_to_well, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -88,7 +92,8 @@ class TipPhysicallyMissingError(ErrorOccurrence): _ExecuteReturn = Union[ SuccessData[PickUpTipResult], - DefinedErrorData[TipPhysicallyMissingError], + DefinedErrorData[TipPhysicallyMissingError] + | DefinedErrorData[StallOrCollisionError], ] @@ -121,11 +126,14 @@ async def execute( ) move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + if isinstance(move_result, DefinedErrorData): + return move_result try: tip_geometry = await self._tip_handler.pick_up_tip( @@ -194,7 +202,11 @@ async def execute( class PickUpTip( - BaseCommand[PickUpTipParams, PickUpTipResult, TipPhysicallyMissingError] + BaseCommand[ + PickUpTipParams, + PickUpTipResult, + TipPhysicallyMissingError | StallOrCollisionError, + ] ): """Pick up tip command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py index c7a5f278e52..2d7c507d321 100644 --- a/api/src/opentrons/protocol_engine/commands/touch_tip.py +++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py @@ -9,20 +9,27 @@ from ..errors import TouchTipDisabledError, LabwareIsTipRackError from ..types import DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) from .pipetting_common import ( PipetteIdMixin, ) from .movement_common import ( WellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) if TYPE_CHECKING: from ..execution import MovementHandler, GantryMover from ..state.state import StateView + from ..resources.model_utils import ModelUtils TouchTipCommandType = Literal["touchTip"] @@ -54,7 +61,10 @@ class TouchTipResult(DestinationPositionResult): class TouchTipImplementation( - AbstractCommandImpl[TouchTipParams, SuccessData[TouchTipResult]] + AbstractCommandImpl[ + TouchTipParams, + SuccessData[TouchTipResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Touch tip command implementation.""" @@ -63,13 +73,17 @@ def __init__( state_view: StateView, movement: MovementHandler, gantry_mover: GantryMover, + model_utils: ModelUtils, **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement self._gantry_mover = gantry_mover + self._model_utils = model_utils - async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: + async def execute( + self, params: TouchTipParams + ) -> SuccessData[TouchTipResult] | DefinedErrorData[StallOrCollisionError]: """Touch tip to sides of a well using the requested pipette.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -85,11 +99,14 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: center_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, ) + if isinstance(center_result, DefinedErrorData): + return center_result touch_speed = self._state_view.pipettes.get_movement_speed( pipette_id, params.speed @@ -128,7 +145,7 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: ) -class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, ErrorOccurrence]): +class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, StallOrCollisionError]): """Touch up tip command model.""" commandType: TouchTipCommandType = "touchTip" diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index 7306bc4e4d1..c77a9e1bad2 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -64,6 +64,7 @@ HardwareAxis.Q: MotorAxis.AXIS_96_CHANNEL_CAM, } + # The height of the bottom of the pipette nozzle at home position without any tips. # We rely on this being the same for every OT-3 pipette. # @@ -305,7 +306,6 @@ async def move_mount_to( ) -> Point: """Move the given hardware mount to a waypoint.""" assert len(waypoints) > 0, "Must have at least one waypoint" - log.info(f"Moving mount {mount}") for waypoint in waypoints: log.info(f"The current waypoint moving is {waypoint}") await self._hardware_api.move_to( @@ -340,6 +340,10 @@ async def move_axes( mount, refresh=True ) log.info(f"The current position of the robot is: {current_position}.") + converted_current_position_deck = ( + self._hardware_api.get_deck_from_machine(current_position) + ) + log.info(f"The current position of the robot is: {current_position}.") pos_hw = target_axis_map_from_relative(pos_hw, current_position) log.info( diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index d3998c69bd1..6387bf5dcf1 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -67,6 +67,8 @@ class LoadedStaticPipetteData: back_left_corner_offset: Point front_right_corner_offset: Point pipette_lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float class VirtualPipetteDataProvider: @@ -252,6 +254,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_back_left = config.pipette_bounding_box_offsets.back_left_corner pip_front_right = config.pipette_bounding_box_offsets.front_right_corner + plunger_positions = config.plunger_positions_configurations[liquid_class] return LoadedStaticPipetteData( model=str(pipette_model), display_name=config.display_name, @@ -280,6 +283,13 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_front_right[0], pip_front_right[1], pip_front_right[2] ), pipette_lld_settings=config.lld_settings, + plunger_positions={ + "top": plunger_positions.top, + "bottom": plunger_positions.bottom, + "blow_out": plunger_positions.blow_out, + "drop_tip": plunger_positions.drop_tip, + }, + shaft_ul_per_mm=config.shaft_ul_per_mm, ) def get_virtual_pipette_static_config( @@ -327,6 +337,8 @@ def get_pipette_static_config( front_right_offset[0], front_right_offset[1], front_right_offset[2] ), pipette_lld_settings=pipette_dict["lld_settings"], + plunger_positions=pipette_dict["plunger_positions"], + shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"], ) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index e0f2cef1155..d20b8665318 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -10,11 +10,15 @@ Mapping, Optional, Tuple, + cast, ) from typing_extensions import assert_never from opentrons_shared_data.pipette import pipette_definition +from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm +from opentrons_shared_data.pipette.types import UlPerMmAction + from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control import CriticalPoint @@ -99,6 +103,8 @@ class StaticPipetteConfig: bounding_nozzle_offsets: BoundingNozzlesOffsets default_nozzle_map: NozzleMap # todo(mm, 2024-10-14): unused, remove? lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float @dataclasses.dataclass @@ -288,6 +294,8 @@ def _update_pipette_config(self, state_update: update_types.StateUpdate) -> None ), default_nozzle_map=config.nozzle_map, lld_settings=config.pipette_lld_settings, + plunger_positions=config.plunger_positions, + shaft_ul_per_mm=config.shaft_ul_per_mm, ) self._state.flow_rates_by_id[ state_update.pipette_config.pipette_id @@ -772,3 +780,31 @@ def get_nozzle_configuration_supports_lld(self, pipette_id: str) -> bool: ): return False return True + + def lookup_volume_to_mm_conversion( + self, pipette_id: str, volume: float, action: str + ) -> float: + """Get the volumn to mm conversion for a pipette.""" + try: + lookup_volume = self.get_working_volume(pipette_id) + except errors.TipNotAttachedError: + lookup_volume = self.get_maximum_volume(pipette_id) + + pipette_config = self.get_config(pipette_id) + lookup_table_from_config = pipette_config.tip_configuration_lookup_table + try: + tip_settings = lookup_table_from_config[lookup_volume] + except KeyError: + tip_settings = list(lookup_table_from_config.values())[0] + return calculate_ul_per_mm( + volume, + cast(UlPerMmAction, action), + tip_settings, + shaft_ul_per_mm=pipette_config.shaft_ul_per_mm, + ) + + def lookup_plunger_position_name( + self, pipette_id: str, position_name: str + ) -> float: + """Get the plunger position provided for the given pipette id.""" + return self.get_config(pipette_id).plunger_positions[position_name] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 47b15e4eb3b..58e977cc2f4 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -9,7 +9,7 @@ from opentrons_shared_data.robot.types import RobotDefinition from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy -from opentrons.protocol_engine.types import ModuleOffsetData +from opentrons.protocol_engine.types import LiquidClassRecordWithId, ModuleOffsetData from opentrons.util.change_notifier import ChangeNotifier from ..resources import DeckFixedLabware @@ -156,7 +156,12 @@ def get_summary(self) -> StateSummary: wells=self._wells.get_all(), hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(), files=self._state.files.file_ids, - # TODO(dc): Do we want to just dump all the liquid classes into the summary? + liquidClasses=[ + LiquidClassRecordWithId( + liquidClassId=liquid_class_id, **dict(liquid_class_record) + ) + for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items() + ], ) diff --git a/api/src/opentrons/protocol_engine/state/state_summary.py b/api/src/opentrons/protocol_engine/state/state_summary.py index 7e47ccbbb37..d6b18613071 100644 --- a/api/src/opentrons/protocol_engine/state/state_summary.py +++ b/api/src/opentrons/protocol_engine/state/state_summary.py @@ -11,6 +11,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, WellInfoSummary, ) @@ -32,3 +33,4 @@ class StateSummary(BaseModel): liquids: List[Liquid] = Field(default_factory=list) wells: List[WellInfoSummary] = Field(default_factory=list) files: List[str] = Field(default_factory=list) + liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list) diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py index 2f217120143..567ba39144c 100644 --- a/api/src/opentrons/protocol_engine/state/update_types.py +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -322,6 +322,11 @@ def reduce(cls: typing.Type[Self], *args: Self) -> Self: # These convenience functions let the caller avoid the boilerplate of constructing a # complicated dataclass tree. + @typing.overload + def set_pipette_location( + self: Self, *, pipette_id: str, new_deck_point: DeckPoint + ) -> Self: + """Schedule a pipette's coordinates to be changed while preserving its logical location.""" @typing.overload def set_pipette_location( @@ -362,10 +367,13 @@ def set_pipette_location( # noqa: D102 ), new_deck_point=new_deck_point, ) + elif new_labware_id == NO_CHANGE or new_well_name == NO_CHANGE: + self.pipette_location = PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=NO_CHANGE, + new_deck_point=new_deck_point, + ) else: - # These asserts should always pass because of the overloads. - assert new_labware_id != NO_CHANGE - assert new_well_name != NO_CHANGE self.pipette_location = PipetteLocationUpdate( pipette_id=pipette_id, diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 1a11a99df86..2a0bbf78c28 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -887,6 +887,15 @@ def dict_to_tuple(d: dict[str, Any]) -> tuple[tuple[str, Any], ...]: return hash(dict_to_tuple(self.dict())) +class LiquidClassRecordWithId(LiquidClassRecord, frozen=True): + """A LiquidClassRecord with its ID, for use in summary lists.""" + + liquidClassId: str = Field( + ..., + description="Unique identifier for this liquid class.", + ) + + class SpeedRange(NamedTuple): """Minimum and maximum allowed speeds for a shaking module.""" diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index fa57ce0dcd5..1f73d63c8c6 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -292,6 +292,11 @@ def mount_for_axis(cls, axis: "AxisType") -> Mount: } return map_mount_to_axis[axis] + @classmethod + def plunger_axis_for_mount(cls, mount: Mount) -> "AxisType": + map_plunger_axis_mount = {Mount.LEFT: cls.P_L, Mount.RIGHT: cls.P_R} + return map_plunger_axis_mount[mount] + @classmethod def ot2_axes(cls) -> List["AxisType"]: return [ diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index cf8fdd0e97c..e8ca2b059ff 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -804,10 +804,10 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: namespace="test-fixture-2", byPipette=[ ByPipetteSetting( - pipetteModel="p20_single_gen2", + pipetteModel="flex_1channel_50", byTipType=[ ByTipTypeSetting( - tiprack="opentrons_96_tiprack_20ul", + tiprack="opentrons_flex_96_tiprack_50ul", aspirate=AspirateProperties( submerge=Submerge( positionReference=PositionReference.LIQUID_MENISCUS, @@ -821,13 +821,13 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], preWet=True, mix=MixProperties(enable=False), delay=DelayProperties( @@ -845,16 +845,16 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], blowout=BlowoutProperties(enable=False), touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], mix=MixProperties(enable=False), - pushOutByVolume={"default": 5, "10": 7, "20": 10}, + pushOutByVolume=[(10.0, 7.0), (20.0, 10.0)], delay=DelayProperties(enable=False), ), multiDispense=None, diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py index 2e650a2c246..d7125cfb027 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py @@ -3,7 +3,7 @@ from opentrons_hardware.hardware_control.motion_planning import Move from opentrons.hardware_control.backends import ot3utils from opentrons_hardware.firmware_bindings.constants import NodeId -from opentrons.hardware_control.types import Axis, OT3Mount +from opentrons.hardware_control.types import Axis, OT3Mount, OT3AxisKind from numpy import float64 as f64 from opentrons.config import defaults_ot3, types as conf_types @@ -95,6 +95,22 @@ def test_get_system_contraints_for_plunger() -> None: assert updated_contraints[axis].max_acceleration == set_acceleration +@pytest.mark.parametrize(["mount"], [[OT3Mount.LEFT], [OT3Mount.RIGHT]]) +def test_get_system_constraints_for_emulsifying_pipette(mount: OT3Mount) -> None: + set_max_speed = 90 + config = defaults_ot3.build_with_defaults({}) + pipette_ax = Axis.of_main_tool_actuator(mount) + default_pip_max_speed = config.motion_settings.default_max_speed[ + conf_types.GantryLoad.LOW_THROUGHPUT + ][OT3AxisKind.P] + updated_constraints = ot3utils.get_system_constraints_for_emulsifying_pipette( + config.motion_settings, conf_types.GantryLoad.LOW_THROUGHPUT, mount + ) + other_pipette = list(set(Axis.pipette_axes()) - {pipette_ax})[0] + assert updated_constraints[pipette_ax].max_speed == set_max_speed + assert updated_constraints[other_pipette].max_speed == default_pip_max_speed + + @pytest.mark.parametrize( ["moving", "expected"], [ diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index 847c80d2125..6f4458f87ff 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -1,4 +1,5 @@ """Tests for opentrons.protocol_api.core.engine.LabwareCore.""" + from typing import cast import pytest @@ -25,7 +26,7 @@ LabwareOffsetLocation, LabwareOffsetVector, ) - +from opentrons.protocol_api._liquid import Liquid from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.engine import LabwareCore, WellCore from opentrons.calibration_storage.helpers import uri_from_details @@ -80,7 +81,9 @@ def test_get_load_params(subject: LabwareCore) -> None: version=42, parameters=LabwareDefinitionParameters.construct(loadName="world"), # type: ignore[call-arg] ordering=[], - metadata=LabwareDefinitionMetadata.construct(displayName="what a cool labware"), # type: ignore[call-arg] + metadata=LabwareDefinitionMetadata.construct( + displayName="what a cool labware" + ), # type: ignore[call-arg] ) ], ) @@ -455,3 +458,40 @@ def test_get_deck_slot( ).then_raise(LabwareNotOnDeckError("oh no")) assert subject.get_deck_slot() is None + + +def test_load_liquid( + decoy: Decoy, mock_engine_client: EngineClient, subject: LabwareCore +) -> None: + """It should pass loaded liquids to the engine.""" + mock_liquid = Liquid( + _id="liquid-id", name="water", description=None, display_color=None + ) + subject.load_liquid(volumes={"A1": 20, "B1": 30, "C1": 40}, liquid=mock_liquid) + + decoy.verify( + mock_engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId="cool-labware", + liquidId="liquid-id", + volumeByWell={"A1": 20, "B1": 30, "C1": 40}, + ) + ), + times=1, + ) + + +def test_load_empty( + decoy: Decoy, mock_engine_client: EngineClient, subject: LabwareCore +) -> None: + """It should pass empty liquids to the engine.""" + subject.load_empty(wells=["A1", "B1", "C1"]) + decoy.verify( + mock_engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId="cool-labware", + liquidId="EMPTY", + volumeByWell={"A1": 0.0, "B1": 0.0, "C1": 0.0}, + ) + ) + ) diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1b8445ed7b2..8282f660a44 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1501,6 +1501,7 @@ def test_mix_no_lpd( mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1516,6 +1517,7 @@ def test_mix_no_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1523,19 +1525,63 @@ def test_mix_no_lpd( subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, - ) - decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) + # Slight differences in dispense push-out logic for 2.14 and 2.15 api levels + if subject.api_version < APIVersion(2, 16): + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=10, + ) + else: + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, + ) + decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=0, ) @@ -1551,6 +1597,7 @@ def test_mix_with_lpd( """It should aspirate/dispense to a well several times and do 1 lpd.""" mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1566,6 +1613,7 @@ def test_mix_with_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1577,19 +1625,45 @@ def test_mix_with_lpd( subject.liquid_presence_detection = True subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, ) - decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=1, ) diff --git a/api/tests/opentrons/protocol_api/test_labware.py b/api/tests/opentrons/protocol_api/test_labware.py index 4610145162f..5e49cd29947 100644 --- a/api/tests/opentrons/protocol_api/test_labware.py +++ b/api/tests/opentrons/protocol_api/test_labware.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import inspect from typing import cast @@ -21,6 +22,7 @@ from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.core_map import LoadedCoreMap from opentrons.protocol_api import TemperatureModuleContext +from opentrons.protocol_api._liquid import Liquid from opentrons.types import Point @@ -364,3 +366,318 @@ def test_separate_calibration_raises_on_high_api_version( """It should raise an error, on high API versions.""" with pytest.raises(UnsupportedAPIError): subject.separate_calibration + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load volumes for list of wells.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid(["A1", subject["B1"]], 10, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 10, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid load inputs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(core_2.get_well_core("B1")).then_return(mock_well_core_2) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + with pytest.raises(KeyError): + subject.load_liquid(["A1", "C1"], 10, mock_liquid) + + with pytest.raises(KeyError): + subject.load_liquid([subject["A1"], other_labware["A1"]], 10, mock_liquid) + + with pytest.raises(TypeError): + subject.load_liquid([2], 10, mock_liquid) # type: ignore[list-item] + + with pytest.raises(TypeError): + subject.load_liquid(["A1"], "A1", mock_liquid) # type: ignore[arg-type] + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid(["A1", subject["B1"]], 10, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 10, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_by_well_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load liquids of different volumes in different wells.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + decoy.when(mock_well_core_2.get_display_name()).then_return("well 2") + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid_by_well({"A1": 10, subject["B1"]: 11}, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 11, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_by_well_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid well specs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_well_core_1.get_display_name()).then_return("well 1") + decoy.when(mock_well_core_2.get_display_name()).then_return("well 2") + decoy.when(mock_well_core_1.get_top(z_offset=0.0)).then_return(Point(4, 5, 6)) + decoy.when(mock_well_core_1.get_top(z_offset=0.0)).then_return(Point(7, 8, 9)) + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + decoy.when(mock_well_core_3.get_display_name()).then_return("well 3") + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_3) + decoy.when(mock_well_core_3.get_top(z_offset=0.0)).then_return(Point(1, 2, 3)) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + mock_liquid = decoy.mock(cls=Liquid) + with pytest.raises(KeyError): + subject.load_liquid_by_well({"A1": 10, "C1": 11}, mock_liquid) + + with pytest.raises(KeyError): + subject.load_liquid_by_well( + {subject["A1"]: 10, other_labware["A1"]: 11}, mock_liquid + ) + + with pytest.raises(TypeError): + subject.load_liquid_by_well({2: 10}, mock_liquid) # type: ignore[dict-item] + + with pytest.raises(TypeError): + subject.load_liquid_by_well({"A1": "A3"}, mock_liquid) # type: ignore[dict-item] + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_empty_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load lists of wells as empty.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + subject.load_empty(["A1", subject["B1"]]) + decoy.verify(mock_labware_core.load_empty(["A1", "B1"])) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_empty_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid well specs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(core_2.get_well_core("B1")).then_return(mock_well_core_2) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + with pytest.raises(KeyError): + subject.load_empty(["A1", "C1"]) + + with pytest.raises(KeyError): + subject.load_empty([subject["A1"], other_labware["A1"]]) + + with pytest.raises(TypeError): + subject.load_empty([2]) # type: ignore[list-item] diff --git a/api/tests/opentrons/protocol_api/test_liquid_class.py b/api/tests/opentrons/protocol_api/test_liquid_class.py index 463889b3da6..7118080eda0 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class.py @@ -21,9 +21,8 @@ def test_get_for_pipette_and_tip( ) -> None: """It should get the properties for the specified pipette and tip.""" liq_class = LiquidClass.create(minimal_liquid_class_def2) - result = liq_class.get_for("p20_single_gen2", "opentrons_96_tiprack_20ul") + result = liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") assert result.aspirate.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } @@ -36,7 +35,7 @@ def test_get_for_raises_for_incorrect_pipette_or_tip( liq_class = LiquidClass.create(minimal_liquid_class_def2) with pytest.raises(ValueError): - liq_class.get_for("p20_single_gen2", "no_such_tiprack") + liq_class.get_for("flex_1channel_50", "no_such_tiprack") with pytest.raises(ValueError): - liq_class.get_for("p300_single", "opentrons_96_tiprack_20ul") + liq_class.get_for("no_such_pipette", "opentrons_flex_96_tiprack_50ul") diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py index e1e9b540149..f7033afb5be 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -16,7 +16,7 @@ def test_build_aspirate_settings() -> None: """It should convert the shared data aspirate settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate @@ -32,7 +32,6 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.retract.offset == Coordinate(x=0, y=0, z=5) assert aspirate_properties.retract.speed == 100 assert aspirate_properties.retract.air_gap_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -45,7 +44,7 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.position_reference.value == "well-bottom" assert aspirate_properties.offset == Coordinate(x=0, y=0, z=-5) - assert aspirate_properties.flow_rate_by_volume.as_dict() == {"default": 50.0} + assert aspirate_properties.flow_rate_by_volume.as_dict() == {10: 50.0} assert aspirate_properties.pre_wet is True assert aspirate_properties.mix.enabled is True assert aspirate_properties.mix.repetitions == 3 @@ -56,7 +55,7 @@ def test_build_aspirate_settings() -> None: def test_build_single_dispense_settings() -> None: """It should convert the shared data single dispense settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense @@ -75,7 +74,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) assert single_dispense_properties.retract.speed == 100 assert single_dispense_properties.retract.air_gap_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -93,7 +91,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.position_reference.value == "well-bottom" assert single_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) assert single_dispense_properties.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } @@ -101,7 +98,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.mix.repetitions == 3 assert single_dispense_properties.mix.volume == 15 assert single_dispense_properties.push_out_by_volume.as_dict() == { - "default": 5.0, 10.0: 7.0, 20.0: 10.0, } @@ -111,7 +107,7 @@ def test_build_single_dispense_settings() -> None: def test_build_multi_dispense_settings() -> None: """It should convert the shared data multi dispense settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense @@ -131,7 +127,6 @@ def test_build_multi_dispense_settings() -> None: assert multi_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) assert multi_dispense_properties.retract.speed == 100 assert multi_dispense_properties.retract.air_gap_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -148,16 +143,13 @@ def test_build_multi_dispense_settings() -> None: assert multi_dispense_properties.position_reference.value == "well-bottom" assert multi_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) assert multi_dispense_properties.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } assert multi_dispense_properties.conditioning_by_volume.as_dict() == { - "default": 10.0, 5.0: 5.0, } assert multi_dispense_properties.disposal_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, } assert multi_dispense_properties.delay.enabled is True @@ -174,14 +166,12 @@ def test_build_multi_dispense_settings_none( def test_liquid_handling_property_by_volume() -> None: """It should create a class that can interpolate values and add and delete new points.""" - subject = LiquidHandlingPropertyByVolume({"default": 42, "5": 50, "10.0": 250}) - assert subject.as_dict() == {"default": 42, 5.0: 50, 10.0: 250} - assert subject.default == 42.0 + subject = LiquidHandlingPropertyByVolume([(5.0, 50.0), (10.0, 250.0)]) + assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 subject.set_for_volume(volume=7, value=175.5) assert subject.as_dict() == { - "default": 42, 5.0: 50, 10.0: 250, 7.0: 175.5, @@ -189,7 +179,7 @@ def test_liquid_handling_property_by_volume() -> None: assert subject.get_for_volume(7) == 175.5 subject.delete_for_volume(7) - assert subject.as_dict() == {"default": 42, 5.0: 50, 10.0: 250} + assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 with pytest.raises(KeyError, match="No value set for volume"): diff --git a/api/tests/opentrons/protocol_api/test_robot_context.py b/api/tests/opentrons/protocol_api/test_robot_context.py index c1bdfe48c3f..36b94c52b15 100644 --- a/api/tests/opentrons/protocol_api/test_robot_context.py +++ b/api/tests/opentrons/protocol_api/test_robot_context.py @@ -17,6 +17,9 @@ from opentrons.protocol_api.core.common import ProtocolCore, RobotCore from opentrons.protocol_api import RobotContext, ModuleContext from opentrons.protocol_api.deck import Deck +from opentrons_shared_data.pipette.types import PipetteNameType + +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes @pytest.fixture @@ -58,7 +61,12 @@ def subject( api_version: APIVersion, ) -> RobotContext: """Get a RobotContext test subject with its dependencies mocked out.""" - decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return(None) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.RIGHT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) return RobotContext( core=mock_core, api_version=api_version, protocol_core=mock_protocol ) @@ -176,3 +184,73 @@ def test_get_axes_coordinates_for( """Test `RobotContext.get_axis_coordinates_for`.""" res = subject.axis_coordinates_for(mount, location_to_move) assert res == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "volume", "action", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, 200, PipetteActionTypes.ASPIRATE_ACTION, {AxisType.P_R: 100}), + (Mount.LEFT, 100, PipetteActionTypes.DISPENSE_ACTION, {AxisType.P_L: 100}), + ], +) +def test_plunger_coordinates_for_volume( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + volume: float, + action: PipetteActionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_volume`.""" + decoy.when( + subject._core.get_plunger_position_from_volume( + mount, volume, action, "OT-3 Standard" + ) + ).then_return(100) + + result = subject.plunger_coordinates_for_volume(mount, volume, action) + assert result == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "position_name", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, PlungerPositionTypes.PLUNGER_TOP, {AxisType.P_R: 3}), + ( + Mount.RIGHT, + PlungerPositionTypes.PLUNGER_BOTTOM, + {AxisType.P_R: 3}, + ), + ], +) +def test_plunger_coordinates_for_named_position( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + position_name: PlungerPositionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_named_position`.""" + decoy.when( + subject._core.get_plunger_position_from_name(mount, position_name) + ).then_return(3) + result = subject.plunger_coordinates_for_named_position(mount, position_name) + assert result == expected_axis_map + + +def test_plunger_methods_raise_without_pipette( + mock_core: RobotCore, mock_protocol: ProtocolCore, api_version: APIVersion +) -> None: + """Test that `RobotContext` plunger functions raise without pipette attached.""" + subject = RobotContext( + core=mock_core, api_version=api_version, protocol_core=mock_protocol + ) + with pytest.raises(ValueError): + subject.plunger_coordinates_for_named_position( + Mount.LEFT, PlungerPositionTypes.PLUNGER_TOP + ) + + with pytest.raises(ValueError): + subject.plunger_coordinates_for_volume( + Mount.LEFT, 200, PipetteActionTypes.ASPIRATE_ACTION + ) diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py index b4817567dde..c0ef530289b 100644 --- a/api/tests/opentrons/protocol_api/test_well.py +++ b/api/tests/opentrons/protocol_api/test_well.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import pytest from decoy import Decoy @@ -8,8 +9,6 @@ from opentrons.protocol_api._liquid import Liquid from opentrons.types import Point, Location -from . import versions_at_or_above - @pytest.fixture def mock_well_core(decoy: Decoy) -> WellCore: @@ -142,13 +141,6 @@ def test_load_liquid(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> N ) -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) -def test_load_empty(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: - """It should mark a location as empty.""" - subject.load_empty() - decoy.verify(mock_well_core.load_empty(), times=1) - - def test_diameter(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: """It should get the diameter from the core.""" decoy.when(mock_well_core.diameter).then_return(12.3) diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 97de9fb0c48..20bbd2b646c 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -32,7 +32,7 @@ def test_liquid_class_creation_and_property_fetching( assert ( water.get_for( pipette_load_name, tiprack.load_name - ).dispense.flow_rate_by_volume.default + ).dispense.flow_rate_by_volume.get_for_volume(1) == 50 ) assert ( diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 8c2c2b06439..11078fb43cf 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -2,11 +2,15 @@ from datetime import datetime -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) from decoy import matchers, Decoy import pytest from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point from opentrons.protocol_engine import ( @@ -507,3 +511,63 @@ async def test_aspirate_implementation_meniscus( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + model_utils: ModelUtils, + state_view: StateView, +) -> None: + """It should return an overpressure error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) + + data = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=-50, + ), + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index 86940b15266..c06b62ace97 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -1,9 +1,12 @@ """Test blow-out command.""" from datetime import datetime + from decoy import Decoy, matchers +import pytest from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point from opentrons.protocol_engine import ( @@ -25,8 +28,10 @@ PipettingHandler, ) from opentrons.hardware_control import HardwareControlAPI -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError -import pytest +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) @pytest.fixture @@ -183,3 +188,57 @@ async def test_overpressure_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + pipetting: PipettingHandler, + subject: BlowOutImplementation, + model_utils: ModelUtils, + movement: MovementHandler, +) -> None: + """It should return an overpressure error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "C6" + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + + data = BlowOutParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=location, + flowRate=1.234, + ) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py index d237c9e6090..9be08a0a71b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py @@ -63,6 +63,13 @@ async def test_configure_for_volume_implementation( back_left_corner_offset=Point(10, 20, 30), front_right_corner_offset=Point(40, 50, 60), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 6744add9b4a..e0e18307b69 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -5,7 +5,10 @@ import pytest from decoy import Decoy, matchers -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) from opentrons.protocol_engine import ( LiquidHandlingWellLocation, @@ -26,6 +29,7 @@ ) from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError @pytest.fixture @@ -234,3 +238,60 @@ async def test_overpressure_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: DispenseImplementation, + model_utils: ModelUtils, + state_view: StateView, +) -> None: + """It should return a stall error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + data = DispenseParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 4818912b117..038ea12255b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -5,6 +5,8 @@ import pytest from decoy import Decoy, matchers +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + from opentrons.protocol_engine import ( DropTipWellLocation, DropTipWellOrigin, @@ -21,12 +23,14 @@ from opentrons.protocol_engine.commands.pipetting_common import ( TipPhysicallyAttachedError, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.errors.exceptions import TipAttachedError from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import MovementHandler, TipHandler + from opentrons.types import Point @@ -333,3 +337,71 @@ async def test_tip_attached_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_tip_handler: TipHandler, + mock_model_utils: ModelUtils, +) -> None: + """A DropTip command should have an execution implementation.""" + subject = DropTipImplementation( + state_view=mock_state_view, + movement=mock_movement_handler, + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + ) + + params = DropTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + + decoy.when( + mock_state_view.geometry.get_checked_tip_drop_location( + pipette_id="abc", + labware_id="123", + well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, + ) + ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(mock_model_utils.generate_id()).then_return("error-id") + decoy.when(mock_model_utils.get_timestamp()).then_return( + datetime(year=1, month=2, day=3) + ) + + result = await subject.execute(params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id="error-id", + createdAt=datetime(year=1, month=2, day=3), + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index d13ededae85..ab46c4b03e2 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -14,6 +14,7 @@ ) from opentrons_shared_data.errors.exceptions import ( PipetteLiquidNotFoundError, + StallOrCollisionDetectedError, ) from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError @@ -31,6 +32,7 @@ TryLiquidProbeImplementation, ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.execution import ( @@ -403,3 +405,58 @@ async def test_liquid_probe_location_checking( ).then_return(True) with pytest.raises(MustHomeError): await subject.execute(data) + + +async def test_liquid_probe_stall( + decoy: Decoy, + movement: MovementHandler, + state_view: StateView, + pipetting: PipettingHandler, + subject: EitherImplementation, + params_type: EitherParamsType, + model_utils: ModelUtils, +) -> None: + """It should move to the destination and do a liquid probe there.""" + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + + data = params_type( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=location, + ) + + decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id="abc")).then_return( + 0 + ) + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld("abc") + ).then_return(True) + + decoy.when( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + + error_id = "error-id" + timestamp = datetime(year=2020, month=1, day=2) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(error_id) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index a42bbc4e4d9..570666e9c98 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -69,6 +69,13 @@ async def test_load_pipette_implementation( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) decoy.when( @@ -137,6 +144,13 @@ async def test_load_pipette_implementation_96_channel( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py index 01522e4dc45..1e2d98ebf21 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py @@ -1,25 +1,38 @@ """Test move relative commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers +import pytest + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.types import DeckPoint, MovementAxis from opentrons.protocol_engine.execution import MovementHandler from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.commands.move_relative import ( MoveRelativeParams, MoveRelativeResult, MoveRelativeImplementation, ) +from opentrons.protocol_engine.resources.model_utils import ModelUtils + + +@pytest.fixture +def subject( + movement: MovementHandler, model_utils: ModelUtils +) -> MoveRelativeImplementation: + """Build a MoveRelativeImplementation with injected dependencies.""" + return MoveRelativeImplementation(movement=movement, model_utils=model_utils) async def test_move_relative_implementation( - decoy: Decoy, - movement: MovementHandler, + decoy: Decoy, movement: MovementHandler, subject: MoveRelativeImplementation ) -> None: """A MoveRelative command should have an execution implementation.""" - subject = MoveRelativeImplementation(movement=movement) data = MoveRelativeParams( pipetteId="pipette-id", axis=MovementAxis.X, @@ -46,3 +59,34 @@ async def test_move_relative_implementation( ) ), ) + + +async def test_move_relative_stalls( + decoy: Decoy, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveRelativeImplementation, +) -> None: + """A MoveRelative command should handle stalls.""" + data = MoveRelativeParams(pipetteId="pipette-id", axis=MovementAxis.Y, distance=40) + + decoy.when( + await movement.move_relative( + pipette_id="pipette-id", axis=MovementAxis.Y, distance=40 + ) + ).then_raise(StallOrCollisionDetectedError()) + + timestamp = datetime.now() + test_id = "test-id" + + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(test_id) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 6925fd7cce4..9f1470b95da 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -1,7 +1,10 @@ """Test move to addressable area commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers import pytest +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector, LoadedPipette from opentrons.protocol_engine.execution import MovementHandler @@ -9,12 +12,24 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point, MountType -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_addressable_area import ( MoveToAddressableAreaParams, MoveToAddressableAreaResult, MoveToAddressableAreaImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.resources.model_utils import ModelUtils + + +@pytest.fixture +def subject( + movement: MovementHandler, state_view: StateView, model_utils: ModelUtils +) -> MoveToAddressableAreaImplementation: + """Build an execution implementation with injected dependencies.""" + return MoveToAddressableAreaImplementation( + movement=movement, state_view=state_view, model_utils=model_utils + ) @pytest.mark.parametrize( @@ -39,12 +54,9 @@ async def test_move_to_addressable_area_implementation_non_gen1( state_view: StateView, movement: MovementHandler, pipette_name: PipetteNameType, + subject: MoveToAddressableAreaImplementation, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaParams( pipetteId="abc", addressableAreaName="123", @@ -67,6 +79,7 @@ async def test_move_to_addressable_area_implementation_non_gen1( minimum_z_height=4.56, speed=7.89, stay_at_highest_possible_z=True, + ignore_tip_configuration=True, highest_possible_z_extra_offset=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -102,12 +115,9 @@ async def test_move_to_addressable_area_implementation_with_gen1( state_view: StateView, movement: MovementHandler, pipette_name: PipetteNameType, + subject: MoveToAddressableAreaImplementation, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaParams( pipetteId="abc", addressableAreaName="123", @@ -130,6 +140,7 @@ async def test_move_to_addressable_area_implementation_with_gen1( minimum_z_height=4.56, speed=7.89, stay_at_highest_possible_z=True, + ignore_tip_configuration=True, highest_possible_z_extra_offset=5.0, ) ).then_return(Point(x=9, y=8, z=7)) @@ -146,3 +157,54 @@ async def test_move_to_addressable_area_implementation_with_gen1( ) ), ) + + +async def test_move_to_addressable_area_implementation_handles_stalls( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToAddressableAreaImplementation, +) -> None: + """A MoveToAddressableArea command should handle stalls.""" + data = MoveToAddressableAreaParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + stayAtHighestPossibleZ=True, + ) + test_id = "test-id" + timestamp = datetime.now() + + decoy.when(state_view.pipettes.get("abc")).then_return( + LoadedPipette( + id="abc", pipetteName=PipetteNameType.P1000_SINGLE, mount=MountType.LEFT + ) + ) + decoy.when(model_utils.generate_id()).then_return(test_id) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + stay_at_highest_possible_z=True, + ignore_tip_configuration=True, + highest_possible_z_extra_offset=5.0, + ) + ).then_raise(StallOrCollisionDetectedError()) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py index faca36d8121..019ec6bec3f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -1,5 +1,10 @@ """Test move to addressable area for drop tip commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers +import pytest + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector from opentrons.protocol_engine.execution import MovementHandler @@ -7,24 +12,33 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.resources.model_utils import ModelUtils +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_addressable_area_for_drop_tip import ( MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, MoveToAddressableAreaForDropTipImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError + + +@pytest.fixture +def subject( + state_view: StateView, movement: MovementHandler, model_utils: ModelUtils +) -> MoveToAddressableAreaForDropTipImplementation: + """Get a command implementation with injected dependencies.""" + return MoveToAddressableAreaForDropTipImplementation( + state_view=state_view, movement=movement, model_utils=model_utils + ) async def test_move_to_addressable_area_for_drop_tip_implementation( decoy: Decoy, state_view: StateView, movement: MovementHandler, + subject: MoveToAddressableAreaForDropTipImplementation, ) -> None: """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" - subject = MoveToAddressableAreaForDropTipImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaForDropTipParams( pipetteId="abc", addressableAreaName="123", @@ -50,7 +64,9 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( force_direct=True, minimum_z_height=4.56, speed=7.89, + stay_at_highest_possible_z=False, ignore_tip_configuration=False, + highest_possible_z_extra_offset=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -66,3 +82,56 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( ) ), ) + + +async def test_move_to_addressable_area_for_drop_tip_handles_stalls( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToAddressableAreaForDropTipImplementation, +) -> None: + """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" + data = MoveToAddressableAreaForDropTipParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + alternateDropLocation=True, + ignoreTipConfiguration=False, + ) + + decoy.when( + state_view.geometry.get_next_tip_drop_location_for_addressable_area( + addressable_area_name="123", pipette_id="abc" + ) + ).then_return(AddressableOffsetVector(x=10, y=11, z=12)) + + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=10, y=11, z=12), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + stay_at_highest_possible_z=False, + ignore_tip_configuration=False, + highest_possible_z_extra_offset=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + timestamp = datetime.now() + test_id = "test-id" + decoy.when(model_utils.generate_id()).then_return(test_id) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py index 2e3ada1d3d3..85afb189988 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py @@ -1,14 +1,19 @@ """Test move-to-coordinates commands.""" -from decoy import Decoy +from datetime import datetime + +import pytest +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError -from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.execution import MovementHandler from opentrons.protocol_engine.state import update_types -from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import DeckPoint +from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_coordinates import ( MoveToCoordinatesParams, MoveToCoordinatesResult, @@ -16,26 +21,18 @@ ) +@pytest.fixture +def subject( + movement: MovementHandler, model_utils: ModelUtils +) -> MoveToCoordinatesImplementation: + """Build a command implementation with injected dependencies.""" + return MoveToCoordinatesImplementation(movement=movement, model_utils=model_utils) + + async def test_move_to_coordinates_implementation( - decoy: Decoy, - state_view: StateView, - hardware_api: HardwareControlAPI, - movement: MovementHandler, + decoy: Decoy, movement: MovementHandler, subject: MoveToCoordinatesImplementation ) -> None: - """Test the `moveToCoordinates` implementation. - - It should: - - 1. Query the hardware controller for the given pipette's current position - and how high it can go with its current tip. - 2. Plan the movement, taking the above into account, plus the input parameters. - 3. Iterate through the waypoints of the movement. - """ - subject = MoveToCoordinatesImplementation( - state_view=state_view, - movement=movement, - ) - + """Test the `moveToCoordinates` implementation.""" params = MoveToCoordinatesParams( pipetteId="pipette-id", coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), @@ -66,3 +63,42 @@ async def test_move_to_coordinates_implementation( ) ), ) + + +async def test_move_to_coordinates_stall( + decoy: Decoy, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToCoordinatesImplementation, +) -> None: + """It should handle stall errors.""" + params = MoveToCoordinatesParams( + pipetteId="pipette-id", + coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), + minimumZHeight=1234, + forceDirect=True, + speed=567.8, + ) + + decoy.when( + await movement.move_to_coordinates( + pipette_id="pipette-id", + deck_coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), + direct=True, + additional_min_travel_z=1234, + speed=567.8, + ) + ).then_raise(StallOrCollisionDetectedError()) + test_id = "test-id" + timestamp = datetime.now() + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(test_id) + + result = await subject.execute(params=params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index 61863363656..484cbb23835 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -1,6 +1,11 @@ """Test move to well commands.""" + +from datetime import datetime + import pytest -from decoy import Decoy +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine import ( WellLocation, @@ -12,13 +17,15 @@ from opentrons.protocol_engine.state import update_types from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_well import ( MoveToWellParams, MoveToWellResult, MoveToWellImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.resources.model_utils import ModelUtils @pytest.fixture @@ -27,13 +34,22 @@ def mock_state_view(decoy: Decoy) -> StateView: return decoy.mock(cls=StateView) +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + async def test_move_to_well_implementation( decoy: Decoy, state_view: StateView, movement: MovementHandler, + mock_model_utils: ModelUtils, ) -> None: """A MoveToWell command should have an execution implementation.""" - subject = MoveToWellImplementation(state_view=state_view, movement=movement) + subject = MoveToWellImplementation( + state_view=state_view, movement=movement, model_utils=mock_model_utils + ) data = MoveToWellParams( pipetteId="abc", @@ -77,9 +93,12 @@ async def test_move_to_well_with_tip_rack_and_volume_offset( decoy: Decoy, mock_state_view: StateView, movement: MovementHandler, + mock_model_utils: ModelUtils, ) -> None: """It should disallow movement to a tip rack when volumeOffset is specified.""" - subject = MoveToWellImplementation(state_view=mock_state_view, movement=movement) + subject = MoveToWellImplementation( + state_view=mock_state_view, movement=movement, model_utils=mock_model_utils + ) data = MoveToWellParams( pipetteId="abc", @@ -95,3 +114,52 @@ async def test_move_to_well_with_tip_rack_and_volume_offset( with pytest.raises(errors.LabwareIsTipRackError): await subject.execute(data) + + +async def test_move_to_well_stall_defined_error( + decoy: Decoy, + mock_state_view: StateView, + movement: MovementHandler, + mock_model_utils: ModelUtils, +) -> None: + """It should catch StallOrCollisionError exceptions and make them DefinedErrors.""" + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when( + movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + current_well=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + decoy.when(mock_model_utils.generate_id()).then_return(error_id) + decoy.when(mock_model_utils.get_timestamp()).then_return(error_timestamp) + + subject = MoveToWellImplementation( + state_view=mock_state_view, movement=movement, model_utils=mock_model_utils + ) + + data = MoveToWellParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + ) + + result = await subject.execute(data) + assert isinstance(result, DefinedErrorData) + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 00dad1557d2..07170e08288 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -1,9 +1,13 @@ """Test pick up tip commands.""" + from datetime import datetime from decoy import Decoy, matchers from unittest.mock import sentinel + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + from opentrons.types import MountType, Point from opentrons.protocol_engine import ( @@ -19,6 +23,7 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import TipGeometry +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData from opentrons.protocol_engine.commands.pick_up_tip import ( PickUpTipParams, @@ -199,3 +204,61 @@ async def test_tip_physically_missing_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + tip_handler: TipHandler, + model_utils: ModelUtils, +) -> None: + """It should return a TipPhysicallyMissingError if the HW API indicates that.""" + subject = PickUpTipImplementation( + state_view=state_view, + movement=movement, + tip_handler=tip_handler, + model_utils=model_utils, + ) + + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + error_id = "error-id" + error_created_at = datetime(1234, 5, 6) + + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset()) + ) + ).then_return(WellLocation(offset=WellOffset())) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(offset=WellOffset()), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_created_at) + + result = await subject.execute( + PickUpTipParams(pipetteId=pipette_id, labwareId=labware_id, wellName=well_name) + ) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, createdAt=error_created_at, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py index c0ec729aefe..0d4071efd6c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py @@ -1,10 +1,12 @@ """Test touch tip commands.""" + import pytest from decoy import Decoy from opentrons.hardware_control.types import CriticalPoint from opentrons.motion_planning import Waypoint from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint, errors +from opentrons.protocol_engine.resources import ModelUtils from opentrons.protocol_engine.execution import MovementHandler, GantryMover from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView @@ -24,6 +26,12 @@ def mock_state_view(decoy: Decoy) -> StateView: return decoy.mock(cls=StateView) +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + @pytest.fixture def mock_movement_handler(decoy: Decoy) -> MovementHandler: """Get a mock MovementHandler.""" @@ -41,12 +49,14 @@ def subject( mock_state_view: StateView, mock_movement_handler: MovementHandler, mock_gantry_mover: GantryMover, + mock_model_utils: ModelUtils, ) -> TouchTipImplementation: """Get the test subject.""" return TouchTipImplementation( state_view=mock_state_view, movement=mock_movement_handler, gantry_mover=mock_gantry_mover, + model_utils=mock_model_utils, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py index 53eb1f5a59e..ef6d79629be 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py +++ b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py @@ -23,13 +23,13 @@ async def test_verify_tip_presence_implementation( expectedState=TipPresenceStatus.PRESENT, ) - decoy.when( + result = await subject.execute(data) + + assert result == SuccessData(public=VerifyTipPresenceResult()) + decoy.verify( await tip_handler.verify_tip_presence( pipette_id="pipette-id", expected=TipPresenceStatus.PRESENT, + follow_singular_sensor=None, ) - ).then_return(None) - - result = await subject.execute(data) - - assert result == SuccessData(public=VerifyTipPresenceResult()) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py index 79131994299..da381635ce3 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py @@ -37,11 +37,6 @@ async def test_update_position_estimators_implementation( decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( Axis.Y ) - decoy.when( - await ot3_hardware_api.update_axis_position_estimations( - [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] - ) - ).then_return(None) result = await subject.execute(data) diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index b7a020c2d35..3ee027c24c1 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -154,6 +154,13 @@ def loaded_static_pipette_data( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 086b3ec297b..cbf7fa6174e 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -65,6 +65,13 @@ def test_get_virtual_pipette_static_config( back_left_corner_offset=Point(0, 0, 10.45), front_right_corner_offset=Point(0, 0, 10.45), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -8.5, + "blow_out": -13.0, + "drop_tip": -27.0, + }, + shaft_ul_per_mm=0.785, ) @@ -94,6 +101,13 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 71.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, ) subject_instance.configure_virtual_pipette_for_volume( "my-pipette", 1, result1.model @@ -120,6 +134,13 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 61.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, ) @@ -149,6 +170,13 @@ def test_load_virtual_pipette_by_model_string( back_left_corner_offset=Point(-16.0, 43.15, 35.52), front_right_corner_offset=Point(16.0, -43.15, 35.52), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -14.5, + "blow_out": -19.0, + "drop_tip": -33.4, + }, + shaft_ul_per_mm=9.621, ) @@ -246,6 +274,8 @@ def pipette_dict( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + "plunger_positions": {"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + "shaft_ul_per_mm": 5.0, } @@ -292,6 +322,8 @@ def test_get_pipette_static_config( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + plunger_positions={"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + shaft_ul_per_mm=5.0, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 42ee037c1ce..abfb31f5f2a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -2611,6 +2611,13 @@ def test_get_next_drop_tip_location( back_right_corner=Point(x=40, y=20, z=60), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) ) decoy.when(mock_pipette_view.get_mount("pip-123")).then_return(pipette_mount) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index 31b1a7f3a2c..60c857e4911 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -220,6 +220,13 @@ def test_handles_load_pipette( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) config_update = update_types.PipetteConfigUpdate( pipette_id="pipette-id", @@ -772,6 +779,13 @@ def test_add_pipette_config( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) subject.handle_action( @@ -810,6 +824,13 @@ def test_add_pipette_config( back_right_corner=Point(x=4, y=2, z=3), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) assert subject.state.flow_rates_by_id["pipette-id"].default_aspirate == {"a": 1.0} assert subject.state.flow_rates_by_id["pipette-id"].default_dispense == {"b": 2.0} diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index 64e663a24e5..14c43bf70f6 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -291,6 +291,13 @@ def test_get_pipette_working_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) }, ) @@ -322,6 +329,13 @@ def test_get_pipette_working_volume_raises_if_tip_volume_is_none( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) }, ) @@ -364,6 +378,13 @@ def test_get_pipette_available_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), "pipette-id-none": StaticPipetteConfig( min_volume=1, @@ -380,6 +401,13 @@ def test_get_pipette_available_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), }, ) @@ -492,6 +520,13 @@ def test_get_static_config( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) subject = get_pipette_view( @@ -543,6 +578,13 @@ def test_get_nominal_tip_overlap( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) subject = get_pipette_view(static_config_by_id={"pipette-id": config}) @@ -967,6 +1009,13 @@ def test_get_pipette_bounds_at_location( bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, pipette_bounding_box_offsets=bounding_box_offsets, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) }, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index abb408d7418..8abcc6a24e2 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -119,6 +119,13 @@ def test_get_next_tip_returns_none( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -177,6 +184,13 @@ def test_get_next_tip_returns_first_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -229,6 +243,13 @@ def test_get_next_tip_used_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -314,6 +335,13 @@ def test_get_next_tip_skips_picked_up_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -377,6 +405,13 @@ def test_get_next_tip_with_starting_tip( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -444,6 +479,13 @@ def test_get_next_tip_with_starting_tip_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -514,6 +556,13 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -545,6 +594,13 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -615,6 +671,13 @@ def test_get_next_tip_with_starting_tip_out_of_tips( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -685,6 +748,13 @@ def test_get_next_tip_with_column_and_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -734,6 +804,13 @@ def test_reset_tips( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) @@ -796,6 +873,13 @@ def test_handle_pipette_config_action( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -929,6 +1013,13 @@ def test_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -989,6 +1080,13 @@ def test_next_tip_uses_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -1087,6 +1185,13 @@ def test_next_tip_automatic_tip_tracking_with_partial_configurations( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -1239,6 +1344,13 @@ def test_next_tip_automatic_tip_tracking_tiprack_limits( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index bc581114ab2..d7e4b32e02a 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -997,8 +997,7 @@ async def test_estop_noops_if_invalid( subject.estop() # Should not raise. decoy.verify( - action_dispatcher.dispatch(), # type: ignore - ignore_extra_args=True, + action_dispatcher.dispatch(expected_action), times=0, ) decoy.verify( diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 2f06e27c2c2..15e0192175e 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -448,6 +448,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( await run_func() +@pytest.mark.filterwarnings("ignore::decoy.warnings.RedundantVerifyWarning") @pytest.mark.parametrize( "schema_version, json_protocol", [ diff --git a/components/src/atoms/Checkbox/index.tsx b/components/src/atoms/Checkbox/index.tsx index 8ace61cb0bf..44c2ba8ee04 100644 --- a/components/src/atoms/Checkbox/index.tsx +++ b/components/src/atoms/Checkbox/index.tsx @@ -48,7 +48,7 @@ export function Checkbox(props: CheckboxProps): JSX.Element { align-items: ${ALIGN_CENTER}; flex-direction: ${DIRECTION_ROW}; color: ${isChecked ? COLORS.white : COLORS.black90}; - background-color: ${isChecked ? COLORS.blue50 : COLORS.blue35}; + background-color: ${isChecked ? COLORS.blue50 : COLORS.blue30}; border-radius: ${type === 'round' ? BORDERS.borderRadiusFull : BORDERS.borderRadius8}; @@ -68,6 +68,9 @@ export function Checkbox(props: CheckboxProps): JSX.Element { background-color: ${COLORS.grey35}; color: ${COLORS.grey50}; } + &:hover { + background-color: ${isChecked ? COLORS.blue55 : COLORS.blue35}; + } @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing20}; diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx index 99fde7dd81f..85f76f901b2 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx @@ -16,7 +16,7 @@ export function ListButtonAccordionContainer( const { id, children } = props return ( - + {children} ) diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx index b9e31ad2782..7eb96e0bac4 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx @@ -18,8 +18,8 @@ interface ListItemCustomizeProps { label?: string dropdown?: DropdownMenuProps tag?: TagProps - /** temporary prop for dropdown menu */ - forceDirection?: boolean + /** optional placement of the menu */ + menuPlacement?: 'auto' | 'top' | 'bottom' } export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { @@ -31,7 +31,7 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { linkText, dropdown, tag, - forceDirection = false, + menuPlacement = 'auto', } = props return ( @@ -53,7 +53,7 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { ) : null} {dropdown != null ? ( - + ) : null} {tag != null ? : null} diff --git a/components/src/atoms/MenuList/MenuItem.tsx b/components/src/atoms/MenuList/MenuItem.tsx index cd34c7c7f44..94857fc56dd 100644 --- a/components/src/atoms/MenuList/MenuItem.tsx +++ b/components/src/atoms/MenuList/MenuItem.tsx @@ -16,6 +16,8 @@ export const MenuItem = styled.button` padding: ${SPACING.spacing8} ${SPACING.spacing12} ${SPACING.spacing8} ${SPACING.spacing12}; border: ${props => (props.border != null ? props.border : 'inherit')}; + border-radius: ${props => + props.borderRadius != null ? props.borderRadius : 'inherit'}; &:hover { background-color: ${COLORS.blue10}; diff --git a/components/src/atoms/Tag/index.tsx b/components/src/atoms/Tag/index.tsx index c41025dd25b..74c72da486e 100644 --- a/components/src/atoms/Tag/index.tsx +++ b/components/src/atoms/Tag/index.tsx @@ -1,7 +1,7 @@ import { css } from 'styled-components' import { BORDERS, COLORS } from '../../helix-design-system' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { ALIGN_CENTER, DIRECTION_ROW, FLEX_MAX_CONTENT } from '../../styles' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Icon } from '../../icons' import { LegacyStyledText } from '../StyledText' @@ -19,6 +19,7 @@ export interface TagProps { iconPosition?: 'left' | 'right' /** Tagicon */ iconName?: IconName + shrinkToContent?: boolean } const defaultColors = { @@ -42,11 +43,12 @@ const TAG_PROPS_BY_TYPE: Record< } export function Tag(props: TagProps): JSX.Element { - const { iconName, type, text, iconPosition } = props + const { iconName, type, text, iconPosition, shrinkToContent = false } = props const DEFAULT_CONTAINER_STYLE = css` padding: ${SPACING.spacing2} ${SPACING.spacing8}; border-radius: ${BORDERS.borderRadius4}; + width: ${shrinkToContent ? FLEX_MAX_CONTENT : 'none'}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing12}; diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index 42e8822fc35..da34a8ba710 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -1,27 +1,18 @@ import styled from 'styled-components' import { Flex } from '../../primitives' import { + ALIGN_CENTER, CURSOR_DEFAULT, CURSOR_POINTER, + FLEX_MAX_CONTENT, Icon, - SPACING, - StyledText, JUSTIFY_CENTER, JUSTIFY_START, - ALIGN_CENTER, - FLEX_MAX_CONTENT, + SPACING, + StyledText, } from '../../index' -import { - black90, - blue30, - blue50, - grey30, - grey40, - white, -} from '../../helix-design-system/colors' -import { borderRadius8 } from '../../helix-design-system/borders' +import { BORDERS, COLORS } from '../../helix-design-system' import type { IconName } from '../../index' - interface EmptySelectorButtonProps { onClick: () => void text: string @@ -41,10 +32,9 @@ export function EmptySelectorButton( ` border: none; width: ${FLEX_MAX_CONTENT}; height: ${FLEX_MAX_CONTENT}; - cursor: ${({ disabled }) => (disabled ? CURSOR_DEFAULT : CURSOR_POINTER)}; + cursor: ${CURSOR_POINTER}; + background-color: ${COLORS.blue30}; + border-radius: ${BORDERS.borderRadius8}; + &:focus-visible { - outline: 2px solid ${white}; - box-shadow: 0 0 0 4px ${blue50}; - border-radius: ${borderRadius8}; + outline: 2px solid ${COLORS.white}; + box-shadow: 0 0 0 4px ${COLORS.blue50}; + border-radius: ${BORDERS.borderRadius8}; + } + &:hover { + background-color: ${COLORS.blue35}; + } + &:disabled { + background-color: ${COLORS.grey20}; + cursor: ${CURSOR_DEFAULT}; } ` diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index af24cbf075d..acfba246dc2 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -69,8 +69,8 @@ export interface DropdownMenuProps { onBlur?: React.FocusEventHandler /** optional disabled */ disabled?: boolean - /** force direction for pd after release this will be fixed and remove */ - forceDirection?: boolean + /** optional placement of the menu */ + menuPlacement?: 'auto' | 'top' | 'bottom' } // TODO: (smb: 4/15/22) refactor this to use html select for accessibility @@ -90,7 +90,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { disabled = false, onFocus, onBlur, - forceDirection = false, + menuPlacement = 'auto', } = props const [targetProps, tooltipProps] = useHoverTooltip() const [showDropdownMenu, setShowDropdownMenu] = React.useState(false) @@ -108,7 +108,11 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { }) React.useEffect(() => { - if (forceDirection) return + if (menuPlacement !== 'auto') { + setDropdownPosition(menuPlacement) + return + } + const handlePositionCalculation = (): void => { const dropdownRect = dropDownMenuWrapperRef.current?.getBoundingClientRect() if (dropdownRect != null) { @@ -253,7 +257,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { : TYPOGRAPHY.pRegular}; `} > - + {currentOption.name} @@ -325,11 +332,12 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { ) } -const MENU_TEXT_STYLE = css` +const LINE_CLAMP_TEXT_STYLE = css` display: -webkit-box; -webkit-box-orient: vertical; overflow: ${OVERFLOW_HIDDEN}; text-overflow: ellipsis; word-wrap: break-word; -webkit-line-clamp: 1; + word-break: break-all; ` diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx index de1748601c8..147b8b0eda2 100644 --- a/components/src/organisms/Toolbox/index.tsx +++ b/components/src/organisms/Toolbox/index.tsx @@ -140,7 +140,6 @@ export function Toolbox(props: ToolboxProps): JSX.Element { CSVReport: CSVSection( title=TestSection.CONNECTIVITY.value, lines=test_connectivity.build_csv_lines(), - ) + ), + CSVSection( + title=TestSection.Z_AXIS.value, + lines=test_z_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.L_AXIS.value, + lines=test_l_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.X_AXIS.value, + lines=test_x_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.DOOR_SWITCH.value, + lines=test_door_switch.build_csv_lines(), + ), + CSVSection( + title=TestSection.ESTOP.value, + lines=test_estop.build_csv_lines(), + ), ], ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py index 04d833fa8a5..e219b68dae3 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py @@ -26,6 +26,53 @@ class StackerInfo: sn: str +class StackerAxis(Enum): + """Stacker Axis.""" + + X = "X" + Z = "Z" + L = "L" + + def __str__(self) -> str: + """Name.""" + return self.name + + +class Direction(Enum): + """Direction.""" + + RETRACT = 0 + EXTENT = 1 + + def __str__(self) -> str: + """Convert to tag for clear logging.""" + return "negative" if self == Direction.RETRACT else "positive" + + def opposite(self) -> "Direction": + """Get opposite direction.""" + return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT + + def distance(self, distance: float) -> float: + """Get signed distance, where retract direction is negative.""" + return distance * -1 if self == Direction.RETRACT else distance + + +@dataclass +class MoveParams: + """Move Parameters.""" + + max_speed: float | None = None + acceleration: float | None = None + max_speed_discont: float | None = None + + def __str__(self) -> str: + """Convert to string.""" + v = "V:" + str(self.max_speed) if self.max_speed else "" + a = "A:" + str(self.acceleration) if self.acceleration else "" + d = "D:" + str(self.max_speed_discont) if self.max_speed_discont else "" + return f"{v} {a} {d}".strip() + + class FlexStacker: """FLEX Stacker Driver.""" @@ -87,6 +134,80 @@ def set_serial_number(self, sn: str) -> None: return self._send_and_recv(f"M996 {sn}\n", "M996 OK") + def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M119 .*{axis.name}{direction.name[0]}:(\d) .* OK\n") + res = self._send_and_recv("M119\n", "M119 XE:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for limit switch: {res}" + return bool(int(match.group(1))) + + def get_platform_sensor(self, direction: Direction) -> bool: + """Get platform sensor status. + + :return: True if platform is present, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M121 .*{direction.name[0]}:(\d) .* OK\n") + res = self._send_and_recv("M121\n", "M119 E:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for platform sensor: {res}" + return bool(int(match.group(1))) + + def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M122 (\d) OK\n") + res = self._send_and_recv("M122\n", "M122 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for hopper door switch: {res}" + return bool(int(match.group(1))) + + def get_estop(self) -> bool: + """Get E-Stop status. + + :return: True if E-Stop is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M112 (\d) OK\n") + res = self._send_and_recv("M112\n", "M112 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for E-Stop switch: {res}" + return bool(int(match.group(1))) + + def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> None: + """Move axis.""" + if self._simulating: + return + self._send_and_recv(f"G0 {axis.name}{distance} {params or ''}\n", "G0 OK") + + def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> None: + """Move until limit switch is triggered.""" + if self._simulating: + return + self._send_and_recv( + f"G5 {axis.name}{direction.value} {params or ''}\n", "G0 OK" + ) + def __del__(self) -> None: """Close serial port.""" if not self._simulating: diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py new file mode 100644 index 00000000000..ab104a10d01 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py @@ -0,0 +1,36 @@ +"""Test Door Switch.""" + + +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("close-door", [CSVResult]), + CSVLine("open-door", [CSVResult]), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + ui.print_header("Close Door") + if not driver._simulating: + ui.get_user_ready("Close the hopper door") + closed = driver.get_hopper_door_closed() + report(section, "close-door", [CSVResult.from_bool(closed)]) + + ui.print_header("Open Door") + if not driver._simulating: + ui.get_user_ready("Open the hopper door") + closed = driver.get_hopper_door_closed() + report(section, "open-door", [CSVResult.from_bool(not closed)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py new file mode 100644 index 00000000000..c0ee8b4150b --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py @@ -0,0 +1,91 @@ +"""Test E-Stop.""" + + +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, Direction, StackerAxis + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-estop", [CSVResult]), + CSVLine("x-move-disabled", [CSVResult]), + CSVLine("z-move-disabled", [CSVResult]), + CSVLine("l-move-disabled", [CSVResult]), + CSVLine("untrigger-estop", [CSVResult]), + ] + + +def axis_at_limit(driver: FlexStacker, axis: StackerAxis) -> Direction: + """Check which direction an axis is at the limit switch.""" + if axis is StackerAxis.L: + # L axis only has one limit switch + if driver.get_limit_switch(axis, Direction.RETRACT): + print(axis, "is at ", Direction.RETRACT, "limit switch") + return Direction.RETRACT + else: + for dir in Direction: + if driver.get_limit_switch(axis, dir): + print(axis, "is at ", dir, "limit switch") + return dir + raise RuntimeError(f"{axis} is not at any limit switch") + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not driver._simulating and driver.get_estop(): + raise RuntimeError("E-Stop is either triggered/not attached.") + + x_limit = axis_at_limit(driver, StackerAxis.X) + z_limit = axis_at_limit(driver, StackerAxis.Z) + l_limit = axis_at_limit(driver, StackerAxis.L) + + ui.print_header("Trigger E-Stop") + if not driver._simulating: + ui.get_user_ready("Trigger the E-Stop") + + if not driver.get_estop(): + print("E-Stop is not triggered") + report(section, "trigger-estop", [CSVResult.FAIL]) + return + + report(section, "trigger-estop", [CSVResult.PASS]) + + print("try to move X axis...") + driver.move_in_mm(StackerAxis.X, x_limit.opposite().distance(10)) + print("X should not move") + report( + section, + "x-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.X, x_limit))], + ) + + print("try to move Z axis...") + driver.move_in_mm(StackerAxis.Z, z_limit.opposite().distance(10)) + print("Z should not move") + report( + section, + "z-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.Z, z_limit))], + ) + + print("try to move L axis...") + driver.move_in_mm(StackerAxis.L, l_limit.opposite().distance(10)) + print("L should not move") + report( + section, + "l-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.L, l_limit))], + ) + + if not driver._simulating: + ui.get_user_ready("Untrigger the E-Stop") + report(section, "untrigger-estop", [CSVResult.from_bool(not driver.get_estop())]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py new file mode 100644 index 00000000000..d892bdc1fd7 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py @@ -0,0 +1,70 @@ +"""Test L Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction + + +class LimitSwitchError(Exception): + """Limit Switch Error.""" + + pass + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-latch-switch", [CSVResult]), + CSVLine("release/open-latch", [CSVResult]), + CSVLine("hold/close-latch", [CSVResult]), + ] + + +def get_latch_held_switch(driver: FlexStacker) -> bool: + """Get limit switch.""" + held_switch = driver.get_limit_switch(StackerAxis.L, Direction.RETRACT) + print("(Held Switch triggered) : ", held_switch) + return held_switch + + +def close_latch(driver: FlexStacker) -> None: + """Close latch.""" + driver.move_to_limit_switch(StackerAxis.L, Direction.EXTENT) + + +def open_latch(driver: FlexStacker) -> None: + """Open latch.""" + driver.move_in_mm(StackerAxis.L, -22) + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not get_latch_held_switch(driver): + print("Switch is not triggered, try to trigger it by closing latch...") + close_latch(driver) + if not get_latch_held_switch(driver): + print("!!! Held switch is still not triggered !!!") + report(section, "trigger-latch-switch", [CSVResult.FAIL]) + return + + report(section, "trigger-latch-switch", [CSVResult.PASS]) + + ui.print_header("Latch Release/Open") + open_latch(driver) + success = not get_latch_held_switch(driver) + report(section, "release/open-latch", [CSVResult.from_bool(success)]) + + ui.print_header("Latch Hold/Close") + if not success: + print("Latch must be open to close it") + report(section, "hold/close-latch", [CSVResult.FAIL]) + else: + close_latch(driver) + success = get_latch_held_switch(driver) + report(section, "hold/close-latch", [CSVResult.from_bool(success)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py new file mode 100644 index 00000000000..802c12bcae5 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py @@ -0,0 +1,81 @@ +"""Test X Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + CSVLine( + "platform-sensor-trigger-positive-untrigger-negative", + [bool, bool, CSVResult], + ), + CSVLine( + "platform-sensor-trigger-negative-untrigger-positive", + [bool, bool, CSVResult], + ), + ] + + +def test_platform_sensors_for_direction( + driver: FlexStacker, direction: Direction, report: CSVReport, section: str +) -> None: + """Test platform sensors for a given direction.""" + ui.print_header(f"Platform Sensor - {direction} direction") + sensor_result = driver.get_platform_sensor(direction) + opposite_result = not driver.get_platform_sensor(direction.opposite()) + print(f"{direction} sensor triggered: {sensor_result}") + print(f"{direction.opposite()} sensor untriggered: {opposite_result}") + report( + section, + f"platform-sensor-trigger-{direction}-untrigger-{direction.opposite()}", + [ + sensor_result, + opposite_result, + CSVResult.from_bool(sensor_result and opposite_result), + ], + ) + + +def platform_is_removed(driver: FlexStacker) -> bool: + """Check if the platform is removed from the carrier.""" + plus_side = driver.get_platform_sensor(Direction.EXTENT) + minus_side = driver.get_platform_sensor(Direction.RETRACT) + return not plus_side and not minus_side + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not driver._simulating and not platform_is_removed(driver): + print("FAILURE - Cannot start tests with platform on the carrier") + return + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.EXTENT, report, section + ) + + if not driver._simulating: + ui.get_user_ready("Place the platform on the X carrier") + + test_platform_sensors_for_direction(driver, Direction.EXTENT, report, section) + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.RETRACT, report, section + ) + + test_platform_sensors_for_direction(driver, Direction.RETRACT, report, section) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py new file mode 100644 index 00000000000..58fc733e0dc --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py @@ -0,0 +1,34 @@ +"""Test Z Axis.""" +from typing import List, Union +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.EXTENT, report, section + ) + + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.RETRACT, report, section + ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py new file mode 100644 index 00000000000..2aca90c8886 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py @@ -0,0 +1,38 @@ +"""Utility functions for the Flex Stacker EVT QC module.""" +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction, MoveParams + + +def test_limit_switches_per_direction( + driver: FlexStacker, + axis: StackerAxis, + direction: Direction, + report: CSVReport, + section: str, + speed: float = 50.0, +) -> None: + """Sequence to test the limit switch for one direction.""" + ui.print_header(f"{axis} Limit Switch - {direction} direction") + # first make sure switch is not already triggered by moving in the opposite direction + if driver.get_limit_switch(axis, direction): + print(f"{direction} switch already triggered, moving away...\n") + SAFE_DISTANCE_MM = 10 + driver.move_in_mm(axis, direction.opposite().distance(SAFE_DISTANCE_MM)) + + # move until the limit switch is reached + print(f"moving towards {direction} limit switch...\n") + driver.move_to_limit_switch(axis, direction, MoveParams(max_speed=speed)) + result = driver.get_limit_switch(axis, direction) + opposite_result = not driver.get_limit_switch(axis, direction.opposite()) + print(f"{direction} switch triggered: {result}") + print(f"{direction.opposite()} switch untriggered: {opposite_result}") + report( + section, + f"limit-switch-trigger-{direction}-untrigger-{direction.opposite()}", + [result, opposite_result, CSVResult.from_bool(result and opposite_result)], + ) diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 90637e81540..6be7cc92fab 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -119,6 +119,7 @@ class TestConfig: num_trials: int droplet_wait_seconds: int simulate: bool + skip_all_pressure: bool @dataclass @@ -700,9 +701,12 @@ async def _test_for_leak( accumulate_raw_data_cb ), "pressure fixture requires recording data to disk" await _move_to_fixture(api, mount) - test_passed = await _fixture_check_pressure( - api, mount, test_config, fixture, write_cb, accumulate_raw_data_cb - ) + if not test_config.skip_all_pressure: + test_passed = await _fixture_check_pressure( + api, mount, test_config, fixture, write_cb, accumulate_raw_data_cb + ) + else: + test_passed = True else: await _pick_up_tip_for_tip_volume(api, mount, tip_volume=tip_volume) await _move_to_reservoir_liquid(api, mount) @@ -1129,7 +1133,9 @@ async def _read_pressure(_sensor_id: SensorId) -> float: return all(results) -async def _test_diagnostics(api: OT3API, mount: OT3Mount, write_cb: Callable) -> bool: +async def _test_diagnostics( + api: OT3API, mount: OT3Mount, write_cb: Callable, cfg: TestConfig +) -> bool: # ENVIRONMENT SENSOR environment_pass = await _test_diagnostics_environment(api, mount, write_cb) print(f"environment: {_bool_to_pass_fail(environment_pass)}") @@ -1146,9 +1152,14 @@ async def _test_diagnostics(api: OT3API, mount: OT3Mount, write_cb: Callable) -> print(f"capacitance: {_bool_to_pass_fail(capacitance_pass)}") write_cb(["diagnostics-capacitance", _bool_to_pass_fail(capacitance_pass)]) # PRESSURE - pressure_pass = await _test_diagnostics_pressure(api, mount, write_cb) - print(f"pressure: {_bool_to_pass_fail(pressure_pass)}") + if not cfg.skip_all_pressure: + pressure_pass = await _test_diagnostics_pressure(api, mount, write_cb) + print(f"pressure: {_bool_to_pass_fail(pressure_pass)}") + else: + print("Skipping pressure") + pressure_pass = True write_cb(["diagnostics-pressure", _bool_to_pass_fail(pressure_pass)]) + return environment_pass and pressure_pass and encoder_pass and capacitance_pass @@ -1674,7 +1685,9 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 if not test_config.skip_diagnostics: await api.move_to(mount, hover_over_slot_3) await api.move_rel(mount, Point(z=-20)) - test_passed = await _test_diagnostics(api, mount, csv_cb.write) + test_passed = await _test_diagnostics( + api, mount, csv_cb.write, test_config + ) await api.retract(mount) csv_cb.results("diagnostics", test_passed) if not test_config.skip_plunger: @@ -1806,6 +1819,7 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 arg_parser.add_argument("--skip-plunger", action="store_true") arg_parser.add_argument("--skip-tip-presence", action="store_true") arg_parser.add_argument("--skip-liquid-probe", action="store_true") + arg_parser.add_argument("--skip-all-pressure", action="store_true") arg_parser.add_argument("--fixture-side", choices=["left", "right"], default="left") arg_parser.add_argument("--port", type=str, default="") arg_parser.add_argument("--num-trials", type=int, default=2) @@ -1841,11 +1855,11 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 _cfg = TestConfig( operator_name=operator, skip_liquid=args.skip_liquid, - skip_fixture=args.skip_fixture, + skip_fixture=args.skip_fixture or args.skip_all_pressure, skip_diagnostics=args.skip_diagnostics, skip_plunger=args.skip_plunger, skip_tip_presence=args.skip_tip_presence, - skip_liquid_probe=args.skip_liquid_probe, + skip_liquid_probe=args.skip_liquid_probe or args.skip_all_pressure, fixture_port=args.port, fixture_side=args.fixture_side, fixture_aspirate_sample_count=args.aspirate_sample_count, @@ -1859,6 +1873,7 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 num_trials=args.num_trials, droplet_wait_seconds=args.wait, simulate=args.simulate, + skip_all_pressure=args.skip_all_pressure, ) # NOTE: overwrite default aspirate sample-count from user's input # FIXME: this value is being set in a few places, maybe there's a way to clean this up diff --git a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py index 41c70ed35a2..710d3c17578 100644 --- a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py +++ b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py @@ -2,6 +2,7 @@ import sys import paramiko as pmk import time +import json import multiprocessing from typing import Optional, List, Any @@ -69,11 +70,10 @@ def run(file_name: str) -> List[Any]: robot_ips = [] robot_names = [] with open(file_name) as file: - for line in file.readlines(): - info = line.split(",") - if "Y" in info[2]: - robot_ips.append(info[0]) - robot_names.append(info[1]) + file_dict = json.load(file) + robot_dict = file_dict.get("ip_address_list") + robot_ips = list(robot_dict.keys()) + robot_names = list(robot_dict.values()) print("Executing Script on All Robots:") # Launch the processes for each robot. processes = [] @@ -89,10 +89,8 @@ def run(file_name: str) -> List[Any]: # Wait for all processes to finish. file_name = sys.argv[1] processes = run(file_name) - for process in processes: process.start() time.sleep(20) - for process in processes: process.join() diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index a33af383518..77b1dce5b3e 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -359,6 +359,7 @@ class PipetteName(int, Enum): p1000_96 = 0x04 p50_96 = 0x05 p200_96 = 0x06 + p1000_multi_em = 0x07 unknown = 0xFFFF diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index c4a8fc441d0..c18772fe656 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -27,6 +27,7 @@ NAME_LOOKUP: Dict[str, PipetteName] = { "P1KS": PipetteName.p1000_single, "P1KM": PipetteName.p1000_multi, + "P1KP": PipetteName.p1000_multi_em, "P50S": PipetteName.p50_single, "P50M": PipetteName.p50_multi, "P1KH": PipetteName.p1000_96, diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py b/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py index 857c0d08f92..64ed76a6856 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py @@ -2,7 +2,7 @@ import numpy as np from hypothesis import given, assume, strategies as st from hypothesis.extra import numpy as hynp -from typing import Iterator, List, Tuple +from typing import Iterator, List, Tuple, Dict from opentrons_hardware.hardware_control.motion_planning import move_manager from opentrons_hardware.hardware_control.motion_planning.types import ( @@ -210,3 +210,60 @@ def test_close_move_plan( ) assert converged, f"Failed to converge: {blend_log}" + + +def test_pipette_high_speed_motion() -> None: + """Test that updated motion constraint doesn't get overridden by motion planning.""" + origin: Dict[str, int] = { + "X": 499, + "Y": 499, + "Z": 499, + "A": 499, + "B": 499, + "C": 499, + } + target_list = [] + axis_kinds = ["X", "Y", "Z", "A", "B", "C"] + constraints: SystemConstraints[str] = {} + for axis_kind in axis_kinds: + constraints[axis_kind] = AxisConstraints.build( + max_acceleration=500, + max_speed_discont=500, + max_direction_change_speed_discont=500, + max_speed=500, + ) + origin_mapping: Dict[str, float] = {axis_kind: float(origin[axis_kind])} + target_list.append(MoveTarget.build(origin_mapping, 500)) + + set_axis_kind = "A" + dummy_em_pipette_max_speed = 90.0 + manager = move_manager.MoveManager(constraints=constraints) + + new_axis_constraint = AxisConstraints.build( + max_acceleration=float(constraints[set_axis_kind].max_acceleration), + max_speed_discont=float(constraints[set_axis_kind].max_speed_discont), + max_direction_change_speed_discont=float( + constraints[set_axis_kind].max_direction_change_speed_discont + ), + max_speed=90.0, + ) + new_constraints = {} + + for axis_kind in constraints.keys(): + if axis_kind == set_axis_kind: + new_constraints[axis_kind] = new_axis_constraint + else: + new_constraints[axis_kind] = constraints[axis_kind] + + manager.update_constraints(constraints=new_constraints) + converged, blend_log = manager.plan_motion( + origin=origin, + target_list=target_list, + iteration_limit=20, + ) + for move in blend_log[0]: + unit_vector = move.unit_vector + for block in move.blocks: + top_set_axis_speed = unit_vector[set_axis_kind] * block.final_speed + if top_set_axis_speed != 0: + assert abs(top_set_axis_speed) == dummy_em_pipette_max_speed diff --git a/hardware/tests/opentrons_hardware/instruments/test_serials.py b/hardware/tests/opentrons_hardware/instruments/test_serials.py index 7b398eda286..2820b5ffbe5 100644 --- a/hardware/tests/opentrons_hardware/instruments/test_serials.py +++ b/hardware/tests/opentrons_hardware/instruments/test_serials.py @@ -40,6 +40,12 @@ 1, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", ), + ( + "P1KPV30", + PipetteName.p1000_multi_em, + 30, + b"\x00" * 16, + ), ], ) def test_scan_valid_pipette_serials( diff --git a/opentrons-ai-server/Pipfile b/opentrons-ai-server/Pipfile index 4586798349a..5c7cdd10e16 100644 --- a/opentrons-ai-server/Pipfile +++ b/opentrons-ai-server/Pipfile @@ -20,6 +20,7 @@ asgi-correlation-id = "==4.3.3" gspread = "==6.1.4" google-auth = "==2.36.0" google-auth-oauthlib = "==1.2.1" +anthropic = "*" [dev-packages] docker = "==7.1.0" diff --git a/opentrons-ai-server/Pipfile.lock b/opentrons-ai-server/Pipfile.lock index a4b9ba0dca5..9e3821d1d23 100644 --- a/opentrons-ai-server/Pipfile.lock +++ b/opentrons-ai-server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "56aef120fbddf42f146e054b7d59ee0f59be75aa6e43f332f86b7ba8fa2499e0" + "sha256": "0b186e696fa0bf94be8a3edfa27b4f680b067a108088447add8e3ee9e4cabd91" }, "pipfile-spec": 6, "requires": { @@ -26,85 +26,85 @@ }, "aiohttp": { "hashes": [ - "sha256:024409c1b1d6076d0ed933dcebd7e4fc6f3320a227bfa0c1b6b93a8b5a146f04", - "sha256:04b24497b3baf15035730de5f207ade88a67d4483a5f16ced7ece348933a5b47", - "sha256:08474e71772a516ba2e2167b4707af8361d2c452b3d8a5364c984f4867869499", - "sha256:0e7a0762cc29cd3acd01a4d2b547b3af7956ad230ebb80b529a8e4f3e4740fe8", - "sha256:104deb7873681273c5daa13c41924693df394043a118dae90387d35bc5531788", - "sha256:104ea21994b1403e4c1b398866f1187c1694fa291314ad7216ec1d8ec6b49f38", - "sha256:113bf06b029143e94a47c4f36e11a8b7e396e9d1f1fc8cea58e6b7e370cfed38", - "sha256:12071dd2cc95ba81e0f2737bebcb98b2a8656015e87772e84e8fb9e635b5da6e", - "sha256:170fb2324826bb9f08055a8291f42192ae5ee2f25b2966c8f0f4537c61d73a7b", - "sha256:21b4545e8d96870da9652930c5198366605ff8f982757030e2148cf341e5746b", - "sha256:229ae13959a5f499d90ffbb4b9eac2255d8599315027d6f7c22fa9803a94d5b1", - "sha256:2ec5efbc872b00ddd85e3904059d274f284cff314e13f48776050ca2c58f451d", - "sha256:31b91ff3a1fcb206a1fa76e0de1f08c9ffb1dc0deb7296fa2618adfe380fc676", - "sha256:329f5059e0bf6983dceebac8e6ed20e75eaff6163b3414f4a4cb59e0d7037672", - "sha256:37f8cf3c43f292d9bb3e6760476c2b55b9663a581fad682a586a410c43a7683e", - "sha256:3e1ed8d152cccceffb1ee7a2ac227c16372e453fb11b3aeaa56783049b85d3f6", - "sha256:3ed360d6672a9423aad39902a4e9fe305464d20ed7931dbdba30a4625782d875", - "sha256:40dc9446cff326672fcbf93efdb8ef7e949824de1097624efe4f61ac7f0d2c43", - "sha256:4d218d3eca40196384ad3b481309c56fd60e664128885d1734da0a8aa530d433", - "sha256:4e4e155968040e32c124a89852a1a5426d0e920a35f4331e1b3949037bfe93a3", - "sha256:4f698aa61879df64425191d41213dfd99efdc1627e6398e6d7aa5c312fac9702", - "sha256:508cfcc99534b1282595357592d8367b44392b21f6eb5d4dc021f8d0d809e94d", - "sha256:577c7429f8869fa30186fc2c9eee64d75a30b51b61f26aac9725866ae5985cfd", - "sha256:57e17c6d71f2dc857a8a1d09be1be7802e35d90fb4ba4b06cf1aab6414a57894", - "sha256:5ecc2fb1a0a9d48cf773add34196cddf7e488e48e9596e090849751bf43098f4", - "sha256:600b1d9f86a130131915e2f2127664311b33902c486b21a747d626f5144b4471", - "sha256:62502b8ffee8c6a4b5c6bf99d1de277d42bf51b2fb713975d9b63b560150b7ac", - "sha256:62a2f5268b672087c45b33479ba1bb1d5a48c6d76c133cfce3a4f77410c200d1", - "sha256:6362f50a6f0e5482c4330d2151cb682779230683da0e155c15ec9fc58cb50b6a", - "sha256:6533dd06df3d17d1756829b68b365b1583929b54082db8f65083a4184bf68322", - "sha256:6c5a6958f4366496004cf503d847093d464814543f157ef3b738bbf604232415", - "sha256:72cd984f7f14e8c01b3e38f18f39ea85dba84e52ea05e37116ba5e2a72eef396", - "sha256:76d6ee8bb132f8ee0fcb0e205b4708ddb6fba524eb515ee168113063d825131b", - "sha256:7867d0808614f04e78e0a8d5a2c1f8ac6bc626a0c0e2f62be48be6b749e2f8b2", - "sha256:7d664e5f937c08adb7908ea9f391fbf2928a9b09cb412ac0aba602bde9e499e4", - "sha256:85ae6f182be72c3531915e90625cc65afce4df8a0fc4988bd52d8a5d5faaeb68", - "sha256:89a96a0696dc67d548f69cb518c581a7a33cc1f26ab42229dea1709217c9d926", - "sha256:8b323b5d3aef7dd811424c269322eec58a977c0c8152e650159e47210d900504", - "sha256:8c47a0ba6c2b3d3e5715f8338d657badd21f778c6be16701922c65521c5ecfc9", - "sha256:8fef105113d56e817cb9bcc609667ee461321413a7b972b03f5b4939f40f307c", - "sha256:900ff74d78eb580ae4aa5883242893b123a0c442a46570902500f08d6a7e6696", - "sha256:9095580806d9ed07c0c29b23364a0b1fb78258ef9f4bddf7e55bac0e475d4edf", - "sha256:91d3991fad8b65e5dbc13cd95669ea689fe0a96ff63e4e64ac24ed724e4f8103", - "sha256:9231d610754724273a6ac05a1f177979490bfa6f84d49646df3928af2e88cfd5", - "sha256:97056d3422594e0787733ac4c45bef58722d452f4dc6615fee42f59fe51707dd", - "sha256:a896059b6937d1a22d8ee8377cdcd097bd26cd8c653b8f972051488b9baadee9", - "sha256:aabc4e92cb153636d6be54e84dad1b252ddb9aebe077942b6dcffe5e468d476a", - "sha256:ad14cdc0fba4df31c0f6e06c21928c5b924725cbf60d0ccc5f6e7132636250e9", - "sha256:ae36ae52b0c22fb69fb8b744eff82a20db512a29eafc6e3a4ab43b17215b219d", - "sha256:b3e4fb7f5354d39490d8209aefdf5830b208d01c7293a2164e404312c3d8bc55", - "sha256:b40c304ab01e89ad0aeeecf91bbaa6ae3b00e27b796c9e8d50b71a4a7e885cc8", - "sha256:b7349205bb163318dcc102329d30be59a647a3d24c82c3d91ed35b7e7301ea7e", - "sha256:b8b95a63a8e8b5f0464bd8b1b0d59d2bec98a59b6aacc71e9be23df6989b3dfb", - "sha256:bb2e82e515e268b965424ecabebd91834a41b36260b6ef5db015ee12ddb28ef3", - "sha256:c0315978b2a4569e03fb59100f6a7e7d23f718a4521491f5c13d946d37549f3d", - "sha256:c1828e10c3a49e2b234b87600ecb68a92b8a8dcf8b99bca9447f16c4baaa1630", - "sha256:c1c49bc393d854d4421ebc174a0a41f9261f50d3694d8ca277146cbbcfd24ee7", - "sha256:c415b9601ff50709d6050c8a9281733a9b042b9e589265ac40305b875cf9c463", - "sha256:c54c635d1f52490cde7ef3a423645167a8284e452a35405d5c7dc1242a8e75c9", - "sha256:c5e6a1f8b0268ffa1c84d7c3558724956002ba8361176e76406233e704bbcffb", - "sha256:c98a596ac20e8980cc6f34c0c92a113e98eb08f3997c150064d26d2aeb043e5a", - "sha256:cd0834e4260eab78671b81d34f110fbaac449563e48d419cec0030d9a8e58693", - "sha256:cdad66685fcf2ad14ce522cf849d4a025f4fd206d6cfc3f403d9873e4c243b03", - "sha256:d1ea006426edf7e1299c52a58b0443158012f7a56fed3515164b60bfcb1503a9", - "sha256:d33b4490026968bdc7f0729b9d87a3a6b1e09043557d2fc1c605c6072deb2f11", - "sha256:d5cae4cd271e20b7ab757e966cc919186b9f02535418ab36c471a5377ef4deaa", - "sha256:dd505a1121ad5b666191840b7bd1d8cb917df2647deeca6f3474331b72452362", - "sha256:e1668ef2f3a7ec9881f4b6a917e5f97c87a343fa6b0d5fc826b7b0297ddd0887", - "sha256:e7bcfcede95531589295f56e924702cef7f9685c9e4e5407592e04ded6a65bf3", - "sha256:ebf610c37df4f09c71c9bbf8309b4b459107e6fe889ac0d7e16f6e4ebd975f86", - "sha256:f3bf5c132eb48002bcc3825702d241d35b4e9585009e65e9dcf9c4635d0b7424", - "sha256:f40380c96dd407dfa84eb2d264e68aa47717b53bdbe210a59cc3c35a4635f195", - "sha256:f57a0de48dda792629e7952d34a0c7b81ea336bb9b721391c7c58145b237fe55", - "sha256:f6b925c7775ab857bdc1e52e1f5abcae7d18751c09b751aeb641a5276d9b990e", - "sha256:f8f0d79b923070f25674e4ea8f3d61c9d89d24d9598d50ff32c5b9b23c79a25b", - "sha256:feca9fafa4385aea6759c171cd25ea82f7375312fca04178dae35331be45e538" + "sha256:08ebe7a1d6c1e5ca766d68407280d69658f5f98821c2ba6c41c63cabfed159af", + "sha256:0a90a0dc4b054b5af299a900bf950fe8f9e3e54322bc405005f30aa5cacc5c98", + "sha256:0cba0b8d25aa2d450762f3dd6df85498f5e7c3ad0ddeb516ef2b03510f0eea32", + "sha256:0ebdf5087e2ce903d8220cc45dcece90c2199ae4395fd83ca616fcc81010db2c", + "sha256:10a5f91c319d9d4afba812f72984816b5fcd20742232ff7ecc1610ffbf3fc64d", + "sha256:122768e3ae9ce74f981b46edefea9c6e5a40aea38aba3ac50168e6370459bf20", + "sha256:14eb6c628432720e41b4fab1ada879d56cfe7034159849e083eb536b4c2afa99", + "sha256:177b000efaf8d2f7012c649e8aee5b0bf488677b1162be5e7511aa4f9d567607", + "sha256:1c2496182e577042e0e07a328d91c949da9e77a2047c7291071e734cd7a6e780", + "sha256:1e33a7eddcd07545ccf5c3ab230f60314a17dc33e285475e8405e26e21f02660", + "sha256:2793d3297f3e49015140e6d3ea26142c967e07998e2fb00b6ee8d041138fbc4e", + "sha256:2914061f5ca573f990ec14191e6998752fa8fe50d518e3405410353c3f44aa5d", + "sha256:2adb967454e10e69478ba4a8d8afbba48a7c7a8619216b7c807f8481cc66ddfb", + "sha256:2b02a68b9445c70d7f5c8b578c5f5e5866b1d67ca23eb9e8bc8658ae9e3e2c74", + "sha256:3129151378f858cdc4a0a4df355c9a0d060ab49e2eea7e62e9f085bac100551b", + "sha256:32334f35824811dd20a12cc90825d000e6b50faaeaa71408d42269151a66140d", + "sha256:33af11eca7bb0f5c6ffaf5e7d9d2336c2448f9c6279b93abdd6f3c35f9ee321f", + "sha256:34f37c59b12bc3afc52bab6fcd9cd3be82ff01c4598a84cbea934ccb3a9c54a0", + "sha256:3666c750b73ce463a413692e3a57c60f7089e2d9116a2aa5a0f0eaf2ae325148", + "sha256:374baefcb1b6275f350da605951f5f02487a9bc84a574a7d5b696439fabd49a3", + "sha256:382f853516664d2ebfc75dc01da4a10fdef5edcb335fe7b45cf471ce758ecb18", + "sha256:3b1f4844909321ef2c1cee50ddeccbd6018cd8c8d1ddddda3f553e94a5859497", + "sha256:3f617a48b70f4843d54f52440ea1e58da6bdab07b391a3a6aed8d3b311a4cc04", + "sha256:435f7a08d8aa42371a94e7c141205a9cb092ba551084b5e0c57492e6673601a3", + "sha256:44b69c69c194ffacbc50165911cf023a4b1b06422d1e1199d3aea82eac17004e", + "sha256:486273d3b5af75a80c31c311988931bdd2a4b96a74d5c7f422bad948f99988ef", + "sha256:4a23475d8d5c56e447b7752a1e2ac267c1f723f765e406c81feddcd16cdc97bc", + "sha256:4c979fc92aba66730b66099cd5becb42d869a26c0011119bc1c2478408a8bf7a", + "sha256:4d7fad8c456d180a6d2f44c41cfab4b80e2e81451815825097db48b8293f59d5", + "sha256:50e0aee4adc9abcd2109c618a8d1b2c93b85ac277b24a003ab147d91e068b06d", + "sha256:556564d89e2f4a6e8fe000894c03e4e84cf0b6cfa5674e425db122633ee244d1", + "sha256:5587da333b7d280a312715b843d43e734652aa382cba824a84a67c81f75b338b", + "sha256:57993f406ce3f114b2a6756d7809be3ffd0cc40f33e8f8b9a4aa1b027fd4e3eb", + "sha256:5d6e069b882c1fdcbe5577dc4be372eda705180197140577a4cddb648c29d22e", + "sha256:5d878a0186023ac391861958035174d0486f3259cabf8fd94e591985468da3ea", + "sha256:5d90b5a3b0f32a5fecf5dd83d828713986c019585f5cddf40d288ff77f366615", + "sha256:5e9a766c346b2ed7e88937919d84ed64b4ef489dad1d8939f806ee52901dc142", + "sha256:64e8f5178958a9954043bc8cd10a5ae97352c3f2fc99aa01f2aebb0026010910", + "sha256:66e58a2e8c7609a3545c4b38fb8b01a6b8346c4862e529534f7674c5265a97b8", + "sha256:68d1f46f9387db3785508f5225d3acbc5825ca13d9c29f2b5cce203d5863eb79", + "sha256:6ad9a7d2a3a0f235184426425f80bd3b26c66b24fd5fddecde66be30c01ebe6e", + "sha256:6e8e19a80ba194db5c06915a9df23c0c06e0e9ca9a4db9386a6056cca555a027", + "sha256:73a664478ae1ea011b5a710fb100b115ca8b2146864fa0ce4143ff944df714b8", + "sha256:766d0ebf8703d28f854f945982aa09224d5a27a29594c70d921c43c3930fe7ac", + "sha256:783741f534c14957fbe657d62a34b947ec06db23d45a2fd4a8aeb73d9c84d7e6", + "sha256:79efd1ee3827b2f16797e14b1e45021206c3271249b4d0025014466d416d7413", + "sha256:83a70e22e0f6222effe7f29fdeba6c6023f9595e59a0479edacfbd7de4b77bb7", + "sha256:85de9904bc360fd29a98885d2bfcbd4e02ab33c53353cb70607f2bea2cb92468", + "sha256:8d954ba0eae7f33884d27dc00629ca4389d249eb8d26ca07c30911257cae8c96", + "sha256:9075313f8e41b481e4cb10af405054564b0247dc335db5398ed05f8ec38787e2", + "sha256:97fba98fc5d9ccd3d33909e898d00f2494d6a9eec7cbda3d030632e2c8bb4d00", + "sha256:994cb893936dd2e1803655ae8667a45066bfd53360b148e22b4e3325cc5ea7a3", + "sha256:9aa4e68f1e4f303971ec42976fb170204fb5092de199034b57199a1747e78a2d", + "sha256:9b6d15adc9768ff167614ca853f7eeb6ee5f1d55d5660e3af85ce6744fed2b82", + "sha256:9bbb2dbc2701ab7e9307ca3a8fa4999c5b28246968e0a0202a5afabf48a42e22", + "sha256:9c8d1db4f65bbc9d75b7b271d68fb996f1c8c81a525263862477d93611856c2d", + "sha256:a7b0a1618060e3f5aa73d3526ca2108a16a1b6bf86612cd0bb2ddcbef9879d06", + "sha256:afa55e863224e664a782effa62245df73fdfc55aee539bed6efacf35f6d4e4b7", + "sha256:b339d91ac9060bd6ecdc595a82dc151045e5d74f566e0864ef3f2ba0887fec42", + "sha256:b470de64d17156c37e91effc109d3b032b39867000e2c126732fe01d034441f9", + "sha256:b4ec8afd362356b8798c8caa806e91deb3f0602d8ffae8e91d2d3ced2a90c35e", + "sha256:c28c1677ea33ccb8b14330560094cc44d3ff4fad617a544fd18beb90403fe0f1", + "sha256:c681f34e2814bc6e1eef49752b338061b94a42c92734d0be9513447d3f83718c", + "sha256:cccb2937bece1310c5c0163d0406aba170a2e5fb1f0444d7b0e7fdc9bd6bb713", + "sha256:cdc6f8dce09281ae534eaf08a54f0d38612398375f28dad733a8885f3bf9b978", + "sha256:d23854e5867650d40cba54d49956aad8081452aa80b2cf0d8c310633f4f48510", + "sha256:d2d942421cf3a1d1eceae8fa192f1fbfb74eb9d3e207d35ad2696bd2ce2c987c", + "sha256:d2f991c18132f3e505c108147925372ffe4549173b7c258cf227df1c5977a635", + "sha256:d3a2bcf6c81639a165da93469e1e0aff67c956721f3fa9c0560f07dd1e505116", + "sha256:d84930b4145991214602372edd7305fc76b700220db79ac0dd57d3afd0f0a1ca", + "sha256:de3b4d5fb5d69749104b880a157f38baeea7765c93d9cd3837cedd5b84729e10", + "sha256:e57a10aacedcf24666f4c90d03e599f71d172d1c5e00dcf48205c445806745b0", + "sha256:f1d06c8fd8b453c3e553c956bd3b8395100401060430572174bb7876dd95ad49", + "sha256:f833a80d9de9307d736b6af58c235b17ef7f90ebea7b9c49cd274dec7a66a2f1", + "sha256:fb0544a0e8294a5a5e20d3cacdaaa9a911d7c0a9150f5264aef36e7d8fdfa07e", + "sha256:ff5d22eece44528023254b595c670dfcf9733ac6af74c4b6cb4f6a784dc3870c" ], "markers": "python_version >= '3.9'", - "version": "==3.11.0" + "version": "==3.11.2" }, "aiosignal": { "hashes": [ @@ -122,6 +122,15 @@ "markers": "python_version >= '3.8'", "version": "==0.7.0" }, + "anthropic": { + "hashes": [ + "sha256:94671cc80765f9ce693f76d63a97ee9bef4c2d6063c044e983d21a2e262f63ba", + "sha256:ea17093ae0ce0e1768b0c46501d6086b5bcd74ff39d68cd2d6396374e9de7c09" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.39.0" + }, "anyio": { "hashes": [ "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", @@ -781,11 +790,11 @@ }, "httpcore": { "hashes": [ - "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", - "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" + "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", + "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd" ], "markers": "python_version >= '3.8'", - "version": "==1.0.6" + "version": "==1.0.7" }, "httptools": { "hashes": [ @@ -868,6 +877,85 @@ "markers": "python_version >= '3.7'", "version": "==3.1.4" }, + "jiter": { + "hashes": [ + "sha256:0302f0940b1455b2a7fb0409b8d5b31183db70d2b07fd177906d83bf941385d1", + "sha256:097676a37778ba3c80cb53f34abd6943ceb0848263c21bf423ae98b090f6c6ba", + "sha256:0a7d5e85766eff4c9be481d77e2226b4c259999cb6862ccac5ef6621d3c8dcce", + "sha256:0e2b445e5ee627fb4ee6bbceeb486251e60a0c881a8e12398dfdff47c56f0723", + "sha256:12fd88cfe6067e2199964839c19bd2b422ca3fd792949b8f44bb8a4e7d21946a", + "sha256:191fbaee7cf46a9dd9b817547bf556facde50f83199d07fc48ebeff4082f9df4", + "sha256:1e44fff69c814a2e96a20b4ecee3e2365e9b15cf5fe4e00869d18396daa91dab", + "sha256:1e47a554de88dff701226bb5722b7f1b6bccd0b98f1748459b7e56acac2707a5", + "sha256:25d0e5bf64e368b0aa9e0a559c3ab2f9b67e35fe7269e8a0d81f48bbd10e8963", + "sha256:262e96d06696b673fad6f257e6a0abb6e873dc22818ca0e0600f4a1189eb334f", + "sha256:2b7de0b6f6728b678540c7927587e23f715284596724be203af952418acb8a2d", + "sha256:3298af506d4271257c0a8f48668b0f47048d69351675dd8500f22420d4eec378", + "sha256:3d8bae77c82741032e9d89a4026479061aba6e646de3bf5f2fc1ae2bbd9d06e0", + "sha256:3dc9939e576bbc68c813fc82f6620353ed68c194c7bcf3d58dc822591ec12490", + "sha256:448cf4f74f7363c34cdef26214da527e8eeffd88ba06d0b80b485ad0667baf5d", + "sha256:47ac4c3cf8135c83e64755b7276339b26cd3c7ddadf9e67306ace4832b283edf", + "sha256:4aa919ebfc5f7b027cc368fe3964c0015e1963b92e1db382419dadb098a05192", + "sha256:576eb0f0c6207e9ede2b11ec01d9c2182973986514f9c60bc3b3b5d5798c8f50", + "sha256:5970cf8ec943b51bce7f4b98d2e1ed3ada170c2a789e2db3cb484486591a176a", + "sha256:5ae2d01e82c94491ce4d6f461a837f63b6c4e6dd5bb082553a70c509034ff3d4", + "sha256:5c08adf93e41ce2755970e8aa95262298afe2bf58897fb9653c47cd93c3c6cdc", + "sha256:60b49c245cd90cde4794f5c30f123ee06ccf42fb8730a019a2870cd005653ebd", + "sha256:627164ec01d28af56e1f549da84caf0fe06da3880ebc7b7ee1ca15df106ae172", + "sha256:6592f4067c74176e5f369228fb2995ed01400c9e8e1225fb73417183a5e635f0", + "sha256:65df9dbae6d67e0788a05b4bad5706ad40f6f911e0137eb416b9eead6ba6f044", + "sha256:701d90220d6ecb3125d46853c8ca8a5bc158de8c49af60fd706475a49fee157e", + "sha256:70a497859c4f3f7acd71c8bd89a6f9cf753ebacacf5e3e799138b8e1843084e3", + "sha256:75bf3b7fdc5c0faa6ffffcf8028a1f974d126bac86d96490d1b51b3210aa0f3f", + "sha256:7824c3ecf9ecf3321c37f4e4d4411aad49c666ee5bc2a937071bdd80917e4533", + "sha256:7ba52e6aaed2dc5c81a3d9b5e4ab95b039c4592c66ac973879ba57c3506492bb", + "sha256:7ba9a358d59a0a55cccaa4957e6ae10b1a25ffdabda863c0343c51817610501d", + "sha256:7ded4e4b75b68b843b7cea5cd7c55f738c20e1394c68c2cb10adb655526c5f1b", + "sha256:80dae4f1889b9d09e5f4de6b58c490d9c8ce7730e35e0b8643ab62b1538f095c", + "sha256:81d968dbf3ce0db2e0e4dec6b0a0d5d94f846ee84caf779b07cab49f5325ae43", + "sha256:8a9803396032117b85ec8cbf008a54590644a062fedd0425cbdb95e4b2b60479", + "sha256:8dbbd52c50b605af13dbee1a08373c520e6fcc6b5d32f17738875847fea4e2cd", + "sha256:8f212eeacc7203256f526f550d105d8efa24605828382cd7d296b703181ff11d", + "sha256:935f10b802bc1ce2b2f61843e498c7720aa7f4e4bb7797aa8121eab017293c3d", + "sha256:93c20d2730a84d43f7c0b6fb2579dc54335db742a59cf9776d0b80e99d587382", + "sha256:9463b62bd53c2fb85529c700c6a3beb2ee54fde8bef714b150601616dcb184a6", + "sha256:9cd3cccccabf5064e4bb3099c87bf67db94f805c1e62d1aefd2b7476e90e0ee2", + "sha256:9ecbf4e20ec2c26512736284dc1a3f8ed79b6ca7188e3b99032757ad48db97dc", + "sha256:9f9568cd66dbbdab67ae1b4c99f3f7da1228c5682d65913e3f5f95586b3cb9a9", + "sha256:ad04a23a91f3d10d69d6c87a5f4471b61c2c5cd6e112e85136594a02043f462c", + "sha256:ad36a1155cbd92e7a084a568f7dc6023497df781adf2390c345dd77a120905ca", + "sha256:af29c5c6eb2517e71ffa15c7ae9509fa5e833ec2a99319ac88cc271eca865519", + "sha256:b096ca72dd38ef35675e1d3b01785874315182243ef7aea9752cb62266ad516f", + "sha256:b1a0508fddc70ce00b872e463b387d49308ef02b0787992ca471c8d4ba1c0fa1", + "sha256:bc1b55314ca97dbb6c48d9144323896e9c1a25d41c65bcb9550b3e0c270ca560", + "sha256:be6de02939aac5be97eb437f45cfd279b1dc9de358b13ea6e040e63a3221c40d", + "sha256:c1288bc22b9e36854a0536ba83666c3b1fb066b811019d7b682c9cf0269cdf9f", + "sha256:c244261306f08f8008b3087059601997016549cb8bb23cf4317a4827f07b7d74", + "sha256:c65a3ce72b679958b79d556473f192a4dfc5895e8cc1030c9f4e434690906076", + "sha256:c915e1a1960976ba4dfe06551ea87063b2d5b4d30759012210099e712a414d9f", + "sha256:d9e247079d88c00e75e297e6cb3a18a039ebcd79fefc43be9ba4eb7fb43eb726", + "sha256:da8589f50b728ea4bf22e0632eefa125c8aa9c38ed202a5ee6ca371f05eeb3ff", + "sha256:dacca921efcd21939123c8ea8883a54b9fa7f6545c8019ffcf4f762985b6d0c8", + "sha256:de3674a5fe1f6713a746d25ad9c32cd32fadc824e64b9d6159b3b34fd9134143", + "sha256:df0a1d05081541b45743c965436f8b5a1048d6fd726e4a030113a2699a6046ea", + "sha256:e0c91a0304373fdf97d56f88356a010bba442e6d995eb7773cbe32885b71cdd8", + "sha256:e550e29cdf3577d2c970a18f3959e6b8646fd60ef1b0507e5947dc73703b5627", + "sha256:e80052d3db39f9bb8eb86d207a1be3d9ecee5e05fdec31380817f9609ad38e60", + "sha256:e81ccccd8069110e150613496deafa10da2f6ff322a707cbec2b0d52a87b9671", + "sha256:f0aacaa56360139c53dcf352992b0331f4057a0373bbffd43f64ba0c32d2d155", + "sha256:f114a4df1e40c03c0efbf974b376ed57756a1141eb27d04baee0680c5af3d424", + "sha256:f20de711224f2ca2dbb166a8d512f6ff48c9c38cc06b51f796520eb4722cc2ce", + "sha256:f22cf8f236a645cb6d8ffe2a64edb5d2b66fb148bf7c75eea0cb36d17014a7bc", + "sha256:f281aae41b47e90deb70e7386558e877a8e62e1693e0086f37d015fa1c102289", + "sha256:f3ea649e7751a1a29ea5ecc03c4ada0a833846c59c6da75d747899f9b48b7282", + "sha256:f52ce5799df5b6975439ecb16b1e879d7655e1685b6e3758c9b1b97696313bfb", + "sha256:f7605d24cd6fab156ec89e7924578e21604feee9c4f1e9da34d8b67f63e54892", + "sha256:f84c9996664c460f24213ff1e5881530abd8fafd82058d39af3682d5fd2d6316", + "sha256:f892e547e6e79a1506eb571a676cf2f480a4533675f834e9ae98de84f9b941ac" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.1" + }, "joblib": { "hashes": [ "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", @@ -2752,11 +2840,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:1456af3358be1a0e49dd8428bfb81863406659d9fad871362bf18a098eeac90a", - "sha256:dd83003963ca957a6e4835d192d7f163fb55312ce3d3f798f625ac9438616e4f" + "sha256:0251f63257eb8d5f4b414669e25f98898a2bac58fd6ffa1c9df6cf3dd823abd9", + "sha256:e536390ff6934627af351accd8da10bec6cf75e40add465ab0a9c088d7be765c" ], "markers": "python_version >= '3.8'", - "version": "==1.35.59" + "version": "==1.35.62" }, "certifi": { "hashes": [ diff --git a/opentrons-ai-server/api/domain/anthropic_predict.py b/opentrons-ai-server/api/domain/anthropic_predict.py new file mode 100644 index 00000000000..abd94b631ba --- /dev/null +++ b/opentrons-ai-server/api/domain/anthropic_predict.py @@ -0,0 +1,206 @@ +import uuid +from pathlib import Path +from typing import Any, Dict, List + +import requests +import structlog +from anthropic import Anthropic +from anthropic.types import Message, MessageParam +from ddtrace import tracer + +from api.domain.config_anthropic import DOCUMENTS, PROMPT, SYSTEM_PROMPT +from api.settings import Settings + +settings: Settings = Settings() +logger = structlog.stdlib.get_logger(settings.logger_name) +ROOT_PATH: Path = Path(Path(__file__)).parent.parent.parent + + +class AnthropicPredict: + def __init__(self, settings: Settings) -> None: + self.settings: Settings = settings + self.client: Anthropic = Anthropic(api_key=settings.anthropic_api_key.get_secret_value()) + self.model_name: str = settings.anthropic_model_name + self.system_prompt: str = SYSTEM_PROMPT + self.path_docs: Path = ROOT_PATH / "api" / "storage" / "docs" + self._messages: List[MessageParam] = [ + { + "role": "user", + "content": [ + {"type": "text", "text": DOCUMENTS.format(doc_content=self.get_docs()), "cache_control": {"type": "ephemeral"}} # type: ignore + ], + } + ] + self.tools: List[Dict[str, Any]] = [ + { + "name": "simulate_protocol", + "description": "Simulates the python protocol on user input. Returned value is text indicating if protocol is successful.", + "input_schema": { + "type": "object", + "properties": { + "protocol": {"type": "string", "description": "protocol in python for simulation"}, + }, + "required": ["protocol"], + }, + } + ] + + @tracer.wrap() + def get_docs(self) -> str: + """ + Processes documents from a directory and returns their content wrapped in XML tags. + Each document is wrapped in tags with metadata subtags. + + Returns: + str: XML-formatted string containing all documents and their metadata + """ + logger.info("Getting docs", extra={"path": str(self.path_docs)}) + xml_output = [""] + for file_path in self.path_docs.iterdir(): + try: + content = file_path.read_text(encoding="utf-8") + document_xml = [ + "", + f" {file_path.name}", + " ", + f" {content}", + " ", + "", + ] + xml_output.extend(document_xml) + + except Exception as e: + logger.error("Error procesing file", extra={"file": file_path.name, "error": str(e)}) + continue + + xml_output.append("") + return "\n".join(xml_output) + + @tracer.wrap() + def generate_message(self, max_tokens: int = 4096) -> Message: + + response = self.client.messages.create( + model=self.model_name, + system=self.system_prompt, + max_tokens=max_tokens, + messages=self._messages, + tools=self.tools, # type: ignore + extra_headers={"anthropic-beta": "prompt-caching-2024-07-31"}, + ) + + logger.info( + "Token usage", + extra={ + "input_tokens": response.usage.input_tokens, + "output_tokens": response.usage.output_tokens, + "cache_read": getattr(response.usage, "cache_read_input_tokens", "---"), + "cache_create": getattr(response.usage, "cache_creation_input_tokens", "---"), + }, + ) + return response + + @tracer.wrap() + def predict(self, prompt: str) -> str | None: + try: + self._messages.append({"role": "user", "content": PROMPT.format(USER_PROMPT=prompt)}) + response = self.generate_message() + if response.content[-1].type == "tool_use": + tool_use = response.content[-1] + self._messages.append({"role": "assistant", "content": response.content}) + result = self.handle_tool_use(tool_use.name, tool_use.input) # type: ignore + self._messages.append( + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": result, + } + ], + } + ) + follow_up = self.generate_message() + response_text = follow_up.content[0].text # type: ignore + self._messages.append({"role": "assistant", "content": response_text}) + return response_text + + elif response.content[0].type == "text": + response_text = response.content[0].text + self._messages.append({"role": "assistant", "content": response_text}) + return response_text + + logger.error("Unexpected response type") + return None + except IndexError as e: + logger.error("Invalid response format", extra={"error": str(e)}) + return None + except Exception as e: + logger.error("Error in predict method", extra={"error": str(e)}) + return None + + @tracer.wrap() + def handle_tool_use(self, func_name: str, func_params: Dict[str, Any]) -> str: + if func_name == "simulate_protocol": + results = self.simulate_protocol(**func_params) + return results + + logger.error("Unknown tool", extra={"tool": func_name}) + raise ValueError(f"Unknown tool: {func_name}") + + @tracer.wrap() + def reset(self) -> None: + self._messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": DOCUMENTS.format(doc_content=self.get_docs()), "cache_control": {"type": "ephemeral"}} # type: ignore + ], + } + ] + + @tracer.wrap() + def simulate_protocol(self, protocol: str) -> str: + url = "https://Opentrons-simulator.hf.space/protocol" + protocol_name = str(uuid.uuid4()) + ".py" + data = {"name": protocol_name, "content": protocol} + hf_token: str = settings.huggingface_api_key.get_secret_value() + headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(hf_token)} + response = requests.post(url, json=data, headers=headers) + + if response.status_code != 200: + logger.error("Simulation request failed", extra={"status": response.status_code, "error": response.text}) + return f"Error: {response.text}" + + response_data = response.json() + if "error_message" in response_data: + logger.error("Simulation error", extra={"error": response_data["error_message"]}) + return str(response_data["error_message"]) + elif "protocol_name" in response_data: + return str(response_data["run_status"]) + else: + logger.error("Unexpected response", extra={"response": response_data}) + return "Unexpected response" + + +def main() -> None: + """Intended for testing this class locally.""" + import sys + from pathlib import Path + + # # Add project root to Python path + root_dir = Path(__file__).parent.parent.parent + sys.path.insert(0, str(root_dir)) + + from rich import print + from rich.prompt import Prompt + + settings = Settings() + llm = AnthropicPredict(settings) + prompt = Prompt.ask("Type a prompt to send to the Anthropic API:") + completion = llm.predict(prompt) + print(completion) + + +if __name__ == "__main__": + main() diff --git a/opentrons-ai-server/api/domain/config_anthropic.py b/opentrons-ai-server/api/domain/config_anthropic.py new file mode 100644 index 00000000000..9d511012592 --- /dev/null +++ b/opentrons-ai-server/api/domain/config_anthropic.py @@ -0,0 +1,217 @@ +SYSTEM_PROMPT = """ +You are a friendly and knowledgeable AI assistant specializing in Opentrons protocol development. +You help scientists create and optimize protocols using the Opentrons Python API v2. + +Your key responsibilities: +1. Welcome scientists warmly and understand their protocol needs +2. Generate accurate Python protocols using standard Opentrons labware +3. Provide clear explanations and documentation +4. Flag potential safety or compatibility issues +5. Suggest protocol optimizations when appropriate + +Call protocol simulation tool to validate the code - only when it is called explicitly by the user. +For all other queries, provide direct responses. + +Important guidelines: +- Always verify labware compatibility before generating protocols +- Include appropriate error handling in generated code +- Provide clear setup instructions and prerequisites +- Flag any potential safety concerns +- Format code examples using standard Python conventions + +If you encounter requests outside your knowledge of Opentrons capabilities, +ask for clarification rather than making assumptions. +""" + +DOCUMENTS = """ +{doc_content} +""" + +PROMPT = """ +Here are the inputs you will work with: + + +{USER_PROMPT} + + + +Follow these instructions to handle the user's prompt: + +1. Analyze the user's prompt to determine if it's: + a) A request to generate a protocol + b) A question about the Opentrons Python API v2 + c) A common task (e.g., value changes, OT-2 to Flex conversion, slot correction) + d) An unrelated or unclear request + +2. If the prompt is unrelated or unclear, ask the user for clarification. For example: + I apologize, but your prompt seems unclear. Could you please provide more details? + + +3. If the prompt is a question about the API, answer it using only the information + provided in the section. Provide references and place them under the tag. + Format your response like this: + API answer: + [Your answer here, based solely on the provided API documentation] + + References + [References] + + +4. If the prompt is a request to generate a protocol, follow these steps: + + a) Check if the prompt contains all necessary information: + - Modules + - Adapters + - Labware + - Pipette mounts + - Well allocations, liquids, samples + - Commands (steps) + + b) If any crucial information is missing, ask for clarification: + + To generate an accurate protocol, I need more information about [missing elements]. + Please provide details about: + [List of missing elements] + + + c) If all necessary information is available, generate the protocol using the following structure: + + ```python + from opentrons import protocol_api + + metadata = {{ + 'protocolName': '[Protocol name based on user prompt]', + 'author': 'AI Assistant', + 'description': '[Brief description based on user prompt]' + }} + + requirements = {{ + 'robotType': '[Robot type based on user prompt, OT-2 or Flex, default is OT-2]', + 'apiLevel': '[apiLevel, default is 2.19 ]' + }} + + def run(protocol: protocol_api.ProtocolContext): + # Load modules (if any) + [Module loading code with comments] + + # Load adapters (if any) + [Adapter loading code with comments] + + # Load labware + [Labware loading code with comments] + + # Load pipettes + [Pipette loading code with comments] + + # For Flex protocols using API version 2.16 or later, load trash bin + trash = protocol.load_trash_bin('A3') + + # Protocol steps + [Step-by-step protocol commands with comments] + [Please make sure that the transfer function is used with the new_tip parameter correctly] + ``` + + d) Use the `transfer` function to handle iterations over wells and volumes. Provide lists of source and + destination wells to leverage the function's built-in iteration capabilities. + - The most important thing is to avoid unnecessary loops. Incorrect usages of the loops is as follows: + ```python + for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='always') + ``` + This approach unnecessarily calls the transfer method multiple times and can lead to inefficiencies or errors. + + Correct usage is: + ```python + pipette.transfer(volume, source_wells, destination_wells, new_tip='always') + ``` + + The `transfer` function can handle lists of sources and destinations, automatically pairing them and iterating over them. + Even it can stretch if one of the lists is longer. So no need for explicit loops. + + - Next problem is proper use of `new_tip` parameter. Incorrect usage is using new_tip='once' inside a loop + when intending to reuse the same tip. + ```python + for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='once') + ``` + Correct usage is: + ```python + pipette.transfer(volume, source_wells, destination_wells, new_tip='once') + ``` + + When new_tip='once', the pipette picks up a tip at the beginning of the transfer and uses it throughout. + Using it inside a loop can cause the pipette to attempt to pick up a tip that is already in use, leading to errors. + + + e) In the end, make sure you show generate well-written protocol with proper short but useful comments. + +5. Common model issues to avoid: + - Model outputs `p300_multi` instead of `p300_multi_gen2`. + - Model outputs `thermocyclerModuleV1` instead of `thermocyclerModuleV2`. + - Model outputs `opentrons_flex_96_tiprack_50ul` instead of `opentrons_flex_96_filtertiprack_50ul`. + - Model outputs `opentrons_96_pcr_adapter_nest_wellplate_100ul` instead of + `opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt`. + - Do not forget to define `from opentrons import protocol_api`. + - PCR plate cannot go directly on the Temperature Module. Looking at the documentation and white paper, + you need an appropriate thermal adapter/block between the Temperature Module and the labware. + For PCR plates, you need to: + - First load a PCR thermal block adapter on the module using load_adapter() + - Then load the PCR plate onto the adapter + - If prompt contains CSV file but not provided, then create a CSV data structure as a placeholder. + - ProtocolContext.load_trash_bin method is not available in API version 2.15, must be higher >=2.16. + - If tip rack type is not specified, please use regular tip rack rather than filter tip rack. + - API for `Opentrons 96 PCR Heater-Shaker Adapter with NEST Well Plate 100 ul`is + opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt. + - Include only apiLevel in the requirements dictionary. + - Make sure models does not generate errors such as "Variable 'diluent' is not defined". Define everything then use it. + - If the labware is already with `aluminumblock`, then no need to use `load_adapter`. For example, + `opentrons_96_aluminumblock_nest_wellplate_100ul`, `opentrons_24_aluminumblock_nest_1.5ml_snapcap`: + - Correct + ```python + temp_module = protocol.load_module('temperature module gen2', '4') + dilution_plate = temp_module.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + ``` + + - Incorrect + ```python + temp_module = protocol.load_module('temperature module gen2', 3) + temp_adapter = temp_module.load_adapter('opentrons_96_well_aluminum_block') + dilution_plate = temp_adapter.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + ``` + - when description says explicitly how many rows, you need to use it otherwise you encounter out of tips error: for example, + "For each of the 8 rows in the plate:" + - correct: + ```python + for i in range(8): + row = plate.rows()[i] + ``` + - incorrect: + ```python + for row in plate.rows(): + ``` + - Always check out_of_tips_error_219.md before generating the code + - Use load_trash_bin() for Flex. It is not supported on OT-2. + - By default 'A3' is trash for Flex, it must be defined as: trash = protocol.load_trash_bin('A3'). + - Trying to access .bottom on a list of well locations instead of a single well object. + - Keeping the same tip for all transfers refers `new_tip='once'`, but model outputs `new_tip='always'`. + - If tip racks are not defined, please define them by counting source and destination labware so that outof tips error will be avoided. + - The model generates a protocol that attempted to access non-existent wells (A7-A12) in a 24-well tuberack + which only has positions A1-D6, causing a KeyError when trying to reference well 'A7'. + - Model tries to close thermocycler before opening it. Attempted to access labware inside a closed thermocycler, + the thermocycler must be opened first. + - Required Validation Steps: + - Verify all variables are defined before use + - Confirm tip rack quantity matches transfer count + - Validate all well positions exist in labware + - Check module-labware compatibility + - Verify correct API version for all features used + +6. If slots are not defined, refer to deck_layout.md for proper slot definitions. + Make sure slots are different for different labware. If the source and destination are not defined, + then you define yourself but inform user with your choice, because user may want to change them. + +7. If the request lacks sufficient information to generate a protocol, use casual_examples.md + as a reference to generate a basic protocol. + +Remember to use only the information provided in the . Do not introduce any external information or assumptions. +""" diff --git a/opentrons-ai-server/api/handler/fast.py b/opentrons-ai-server/api/handler/fast.py index 9182f827a9a..b93eb6580ce 100644 --- a/opentrons-ai-server/api/handler/fast.py +++ b/opentrons-ai-server/api/handler/fast.py @@ -1,7 +1,7 @@ import asyncio import os import time -from typing import Annotated, Any, Awaitable, Callable, List, Literal, Union +from typing import Annotated, Any, Awaitable, Callable, List, Literal, Optional, Union import structlog from asgi_correlation_id import CorrelationIdMiddleware @@ -17,6 +17,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from uvicorn.protocols.utils import get_path_with_query_string +from api.domain.anthropic_predict import AnthropicPredict from api.domain.fake_responses import FakeResponse, get_fake_response from api.domain.openai_predict import OpenAIPredict from api.handler.custom_logging import setup_logging @@ -43,7 +44,7 @@ auth: VerifyToken = VerifyToken() openai: OpenAIPredict = OpenAIPredict(settings) google_sheets_client = GoogleSheetsClient(settings) - +claude: AnthropicPredict = AnthropicPredict(settings) # Initialize FastAPI app with metadata app = FastAPI( @@ -179,7 +180,7 @@ async def create_chat_completion( body: ChatRequest, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate a chat completion response using OpenAI. + Generate a chat completion response using LLM. - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. @@ -196,7 +197,12 @@ async def create_chat_completion( fake: FakeResponse = get_fake_response(body.fake_key) return ChatResponse(reply=fake.chat_response.reply, fake=fake.chat_response.fake) return ChatResponse(reply="Default fake response. ", fake=body.fake) - response: Union[str, None] = openai.predict(prompt=body.message, chat_completion_message_params=body.history) + + response: Optional[str] = None + if "openai" in settings.model.lower(): + response = openai.predict(prompt=body.message, chat_completion_message_params=body.history) + else: + response = claude.predict(prompt=body.message) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -221,7 +227,7 @@ async def update_protocol( body: UpdateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate an updated protocol using OpenAI. + Generate an updated protocol using LLM. - **request**: The HTTP request containing the existing protocol and other relevant parameters. - **returns**: A chat response or an error message. @@ -236,7 +242,11 @@ async def update_protocol( if body.fake: return ChatResponse(reply="Fake response", fake=bool(body.fake)) - response: Union[str, None] = openai.predict(prompt=body.prompt, chat_completion_message_params=None) + response: Optional[str] = None + if "openai" in settings.model.lower(): + response = openai.predict(prompt=body.prompt, chat_completion_message_params=None) + else: + response = claude.predict(prompt=body.prompt) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -261,7 +271,7 @@ async def create_protocol( body: CreateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate an updated protocol using OpenAI. + Generate an updated protocol using LLM. - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. @@ -277,7 +287,11 @@ async def create_protocol( if body.fake: return ChatResponse(reply="Fake response", fake=body.fake) - response: Union[str, None] = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) + response: Optional[str] = None + if "openai" in settings.model.lower(): + response = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) + else: + response = claude.predict(prompt=str(body.model_dump())) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) diff --git a/opentrons-ai-server/api/settings.py b/opentrons-ai-server/api/settings.py index 9557b51614b..7d6bcf91459 100644 --- a/opentrons-ai-server/api/settings.py +++ b/opentrons-ai-server/api/settings.py @@ -25,6 +25,8 @@ class Settings(BaseSettings): log_level: str = "info" service_name: str = "local-ai-api" openai_model_name: str = "gpt-4-1106-preview" + anthropic_model_name: str = "claude-3-5-sonnet-20241022" + model: str = "claude" auth0_domain: str = "opentrons-dev.us.auth0.com" auth0_api_audience: str = "sandbox-ai-api" auth0_issuer: str = "https://identity.auth-dev.opentrons.com/" @@ -43,6 +45,7 @@ class Settings(BaseSettings): huggingface_api_key: SecretStr = SecretStr("default_huggingface_api_key") google_credentials_json: SecretStr = SecretStr("default_google_credentials_json") datadog_api_key: SecretStr = SecretStr("default_datadog_api_key") + anthropic_api_key: SecretStr = SecretStr("default_anthropic_api_key") @property def json_logging(self) -> bool: diff --git a/opentrons-ai-server/api/storage/docs/OT2ToFlex.md b/opentrons-ai-server/api/storage/docs/OT2ToFlex.md new file mode 100644 index 00000000000..9263cb453a7 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/OT2ToFlex.md @@ -0,0 +1,209 @@ +# Adapting OT-2 to Flex + +## Metadata and Requirements: + +- **API Level**: Flex requires an `apiLevel` of 2.15 or higher. If the OT-2 protocol specified `apiLevel` in the `metadata` dictionary, move it to the `requirements` dictionary. Ensure it is not specified in both places to avoid errors. + +- **Robot Type**: Specify `"robotType": "Flex"` in the `requirements` dictionary. If `robotType` is omitted, the API assumes the protocol is designed for the OT-2. + +### Example Conversion: + +#### Original OT-2 Code: + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "My Protocol", + "description": "This protocol uses the OT-2", + "apiLevel": "2.19" +} +``` + +#### Updated Flex Code: + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "My Protocol", + "description": "This protocol uses the Flex", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.19" +} +``` + +## Pipettes and Tip Rack Load Names: + +- Flex uses different pipettes and tip racks with unique load names. When converting, load Flex pipettes of the same or larger capacity than the OT-2 pipettes. + +- Using smaller capacity tips than in the OT-2 protocol may require further adjustments to avoid running out of tips, resulting in more steps and longer execution times. + +### Example Conversion: + +#### Original OT-2 Code: + +```python +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + left_pipette = protocol.load_instrument( + "p300_single_gen2", "left", tip_racks=[tips] + ) +``` + +#### Updated Flex Code: + +```python +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "D1") + left_pipette = protocol.load_instrument( + "flex_1channel_1000", "left", tip_racks=[tips] + ) +``` + +## Trash Container: + +- OT-2 protocols have a fixed trash in slot 12. For Flex protocols using API version 2.16 or later, load a trash bin in slot A3 to match the OT-2 fixed trash position: + + ```python + trash = protocol.load_trash_bin("A3") + ``` + + **Note**: Load the trash before any commands that may require discarding tips; otherwise, the robot cannot find it. + +## Deck Slot Labels: + +- Update numeric labels for deck slots (matching OT-2) to coordinate labels (matching Flex). This is optional but recommended for clarity. + +### Deck Slot Correspondence: + +The correspondence between deck labels is based on the relative locations of the slots: + +``` +- 10 to A1 +- 11 to A2 +- Trash to A3 +- 7 to B1 +- 8 to B2 +- 9 to B3 +- 4 to C1 +- 5 to C2 +- 6 to C3 +- 1 to D1 +- 2 to D2 +- 3 to D3 +``` + +**Slots A4, B4, C4, and D4 on Flex have no equivalent on OT-2.** + +## Module Load Names: + +- If your OT-2 protocol uses older generations of the Temperature Module or Thermocycler Module, update the load names you pass to `load_module()` to ones compatible with Flex: + + - Temperature Module: `"temperature module gen2"` + - Thermocycler Module: `"thermocycler module gen2"` or `"thermocyclerModuleV2"` + +- The Heater-Shaker Module (`heaterShakerModuleV1`) is compatible with both Flex and OT-2. + +- **Magnetic Module**: Not compatible with Flex. For protocols that load `magnetic module`, `magdeck`, or `magnetic module gen2`, you need to modify the protocol to use the Magnetic Block and Flex Gripper instead. This requires reworking some protocol steps to achieve similar results. + +### Main Difference: + +- **OT-2**: Pipettes an entire plate's worth of liquid from the Heater-Shaker to the Magnetic Module and then engages the module. + +- **Flex**: The gripper moves the plate to the Magnetic Block in one step, eliminating the need for pipetting between modules. + +### Example Conversion for Magnetic Module: + +#### Original OT-2 Code: + +```python +hs_mod.set_and_wait_for_shake_speed(2000) +protocol.delay(minutes=5) +hs_mod.deactivate_shaker() + +for i in sample_plate.wells(): + # Mix, transfer, and blow-out all samples + pipette.pick_up_tip() + pipette.aspirate(100, hs_plate[i]) + pipette.dispense(100, hs_plate[i]) + pipette.aspirate(100, hs_plate[i]) + pipette.air_gap(10) + pipette.dispense(pipette.current_volume, mag_plate[i]) + pipette.aspirate(50, hs_plate[i]) + pipette.air_gap(10) + pipette.dispense(pipette.current_volume, mag_plate[i]) + pipette.blow_out(mag_plate[i].bottom(0.5)) + pipette.drop_tip() + +mag_mod.engage() +``` + +#### Updated Flex Code: + +```python +hs_mod.set_and_wait_for_shake_speed(2000) +protocol.delay(minutes=5) +hs_mod.deactivate_shaker() + +# Move entire plate using the gripper +hs_mod.open_labware_latch() +protocol.move_labware(sample_plate, mag_block, use_gripper=True) +``` + +## Flex vs. OT-2 Pipettes: + +When converting pipettes, consider the volume ranges: + +### OT-2 Pipettes: + +- **P20 Single-Channel GEN2**: 1–20 µL, `p20_single_gen2` +- **P20 Multi-Channel GEN2**: 1–20 µL, `p20_multi_gen2` +- **P300 Single-Channel GEN2**: 20–300 µL, `p300_single_gen2` +- **P300 Multi-Channel GEN2**: 20–300 µL, `p300_multi_gen2` +- **P1000 Single-Channel GEN2**: 100–1000 µL, `p1000_single_gen2` + +### Flex Pipettes: + +- **Flex 1-Channel Pipette**: 1–50 µL, `flex_1channel_50` +- **Flex 1-Channel Pipette**: 5–1000 µL, `flex_1channel_1000` +- **Flex 8-Channel Pipette**: 1–50 µL, `flex_8channel_50` +- **Flex 8-Channel Pipette**: 5–1000 µL, `flex_8channel_1000` +- **Flex 96-Channel Pipette**: 5–1000 µL, `flex_96channel_1000` + +## Tip Racks: + +### OT-2 Tip Racks: + +- `geb_96_tiprack_1000ul` +- `geb_96_tiprack_10ul` +- `opentrons_96_filtertiprack_1000ul` +- `opentrons_96_filtertiprack_10ul` +- `opentrons_96_filtertiprack_200ul` +- `opentrons_96_filtertiprack_20ul` +- `opentrons_96_tiprack_1000ul` +- `opentrons_96_tiprack_10ul` +- `opentrons_96_tiprack_20ul` +- `opentrons_96_tiprack_300ul` + +### Flex Tip Racks: + +- `opentrons_flex_96_filtertiprack_1000ul` +- `opentrons_flex_96_filtertiprack_200ul` +- `opentrons_flex_96_filtertiprack_50ul` +- `opentrons_flex_96_tiprack_1000ul` +- `opentrons_flex_96_tiprack_200ul` +- `opentrons_flex_96_tiprack_50ul` + +**Note**: When converting, match the pipette and tip rack volumes to ensure the protocol functions correctly. + +## Additional Notes: + +- **Trash Bin**: Remember to load the trash bin before any commands that may require discarding tips. + +- **Deck Slots**: Adjust deck slot labels to match the Flex coordinate system for clarity, although numeric labels are still valid. + +- **Verification**: After adapting the protocol, verify that the new design achieves similar results, especially if significant changes were made (e.g., replacing the Magnetic Module with the Magnetic Block). diff --git a/opentrons-ai-server/api/storage/docs/casual_examples.md b/opentrons-ai-server/api/storage/docs/casual_examples.md new file mode 100644 index 00000000000..4d1e778d5d5 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/casual_examples.md @@ -0,0 +1,979 @@ +The following examples show casual descriptions and their corresponding potential protocols. + +#### Example 1: PCR protocol + + +I want to run a PCR setup protocol with temperature control. I need to prepare 64 samples (that's 8 full columns) using both mastermix and samples. Let's keep the samples cold at 4°C and the mastermix at 10°C using temperature modules. + +Here's what I want to do: + +First, I'll use a multichannel P20 pipette mounted on the left side. I'll have three plates: one for samples (on the cold module), one for mastermix (on the slightly warmer module), and one destination plate where we'll mix everything together. + +The steps should go like this: + +1. Start by setting both temperature modules - 4°C for samples and 10°C for mastermix +2. Take one tip and use it to transfer 7 µL of mastermix to each destination well for all 64 samples +3. For the samples, I want to transfer 5 µL from each well to the corresponding destination well. Use fresh tips for each column, mix 9 times with 12 µL volume, and make sure to blow out into the destination well +4. When we're done, turn off both temperature modules + +Remember to work column by column since we're using a multichannel pipette, and we'll be handling the first 8 columns of each plate. + + + +from opentrons import protocol_api + +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): # Module loading +temp_mod_sample = protocol.load_module('temperature module gen2', 1) +temp_mod_mastermix = protocol.load_module('temperature module gen2', 3) + + # Load thermal adapters + sample_adapter = temp_mod_sample.load_adapter("opentrons_96_well_aluminum_block") + mastermix_adapter = temp_mod_mastermix.load_adapter("opentrons_96_well_aluminum_block") + + # Load labware onto the adapters + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + mastermix_plate = mastermix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 7) + tiprack = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + + # Pipette loading + p20_single = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tiprack]) + + # Well allocation + num_samples = 64 + num_cols = num_samples // 8 + sample_wells = sample_plate.columns()[:num_cols] + mastermix_wells = mastermix_plate.columns()[:num_cols] + dest_wells = dest_plate.columns()[:num_cols] + + # Volume and temperature parameters + sample_temp = 4 # Celsius + mastermix_temp = 10 # Celsius + mastermix_vol = 7 # uL + sample_vol = 5 # uL + mix_cycles = 9 + total_mix_vol = sample_vol + mastermix_vol # 12 uL total + + # Execute commands in order specified in description + # Command 1: Set sample temperature + temp_mod_sample.set_temperature(sample_temp) + + # Command 2: Set mastermix temperature + temp_mod_mastermix.set_temperature(mastermix_temp) + + # Command 3: Transfer mastermix + p20_single.transfer( + mastermix_vol, + mastermix_wells, + dest_wells, + new_tip='once' + ) + + # Command 4: Transfer samples and mix + p20_single.transfer( + sample_vol, + sample_wells, + dest_wells, + new_tip='always', + mix_after=(mix_cycles, total_mix_vol), + blow_out=True, + blowout_location='destination well' + ) + + # Command 5 and 6: Deactivate temperature modules + temp_mod_mastermix.deactivate() + temp_mod_sample.deactivate() + + + +#### Example 2: PCR protocol + + +I want to set up a PCR reaction plate using both single-channel and multi-channel pipettes. Here's what we need to do: + +First, using the single-channel pipette on the right: + +- We'll add 7 microliters of mastermix from tubes in the tube rack to specific wells in our destination plate. The source tubes and destination wells are listed in a CSV file. Let's use a fresh tip for each different mastermix tube we work with. + +Then, using the 8-channel pipette on the left: + +- We're going to transfer 3 microliters of samples in triplicate. Here's how: +- Take samples from column 1 of the source plate and transfer them to: + - Column 1 of the destination plate (change tip) + - Column 2 of the destination plate (change tip) + - Column 3 of the destination plate +- Repeat this same pattern for the remaining columns in the source plate, always making three copies of each column and changing tips between transfers. + + + +from opentrons import protocol_api + +requirements = { +'robotType': 'Flex', +'apiLevel': '2.15' +} + +def run(protocol: protocol_api.ProtocolContext): + + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + NUM_COL = 3 + STRIDE = 3 + + # Load labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D3') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # Transfer in triplicate + col_ctr = 0 + for s in dna_plate.rows()[0][:NUM_COL]: + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+1], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+2], new_tip='never') + multi_pip.drop_tip() + + col_ctr += STRIDE + + + +#### Example 3: Transfer reagent protocol + + +I want to do a series of liquid transfers using two different pipettes. Here's what we need to do: + +First, using the P20 single-channel pipette on the right: + +- Take 15 microliters from the first well of our reservoir and transfer it to every well in both of our destination plates. We can use the same tip for all these transfers. +- Then, transfer 20 microliters from each well of our 384-well source plate to the corresponding wells in our first destination plate (the 384-well plate). We can keep using the same tip for these transfers too. + +Next, using the P300 single-channel pipette on the left: + +- Transfer 100 microliters from each well of our 96-well source plate to the corresponding wells in our second destination plate (the 96-well plate). For this step, we'll need to use a fresh tip for each transfer. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +"protocolName": "Reagent Transfer ", +"author": "OGA", +"description": "Transfer reagents from multile source labware to multiple destination labware", +"apiLevel": "2.16", +} + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("nest_1_reservoir_195ml", location=7) +source_2 = protocol.load_labware("biorad_384_wellplate_50ul", location=8) +source_3 = protocol.load_labware("biorad_96_wellplate_200ul_pcr", location=9) +destination_1 = protocol.load_labware("corning_384_wellplate_112ul_flat", location=1) +destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location=2) + + tiprack300 = protocol.load_labware("opentrons_96_tiprack_300ul", location=10) + tiprack20 = protocol.load_labware("opentrons_96_tiprack_20ul", location=11) + + # pipettes + p300s = protocol.load_instrument("p300_single_gen2", mount="left", tip_racks=[tiprack300]) + p20s = protocol.load_instrument("p20_single_gen2", mount="right", tip_racks=[tiprack20]) + + # volumes setup + transfer_vol_1 = 15 + transfer_vol_2 = 20 + transfer_vol_3 = 100 + + # wells setup + source_wells_1 = source_1.wells_by_name()['A1'] + source_wells_2 = source_2.wells() + source_wells_3 = source_3.wells() + destination_wells_1 = destination_1.wells() + destination_wells_2 = destination_2.wells() + all_destinations = destination_wells_1 + destination_wells_2 + + # commands + p20s.transfer(transfer_vol_1, source_wells_1, all_destinations, new_tip="once") + p20s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") + p300s.transfer(transfer_vol_3, source_wells_3, destination_wells_2, new_tip="always") + + + +#### Example 4: Transfer reagent protocol + + +I want to pool samples from multiple tube racks into a deep well plate. I'll be using a single-channel P300 pipette mounted on the right side. + +Here's what we need to do: + +1. Take 20 µL from each tube in the first tube rack and pool them all into well A1 of the deep well plate. We'll use a fresh tip for each tube. + +2. Then, take 20 µL from each tube in the second tube rack and pool them all into well B1 of the deep well plate. Again, use a fresh tip for each tube. + +3. Next, take 20 µL from each tube in the third tube rack and pool them all into well C1 of the deep well plate. Use a fresh tip for each tube. + +4. Finally, take 20 µL from each tube in the fourth tube rack and pool them all into well D1 of the deep well plate. Use a fresh tip for each tube. + + + +from opentrons import protocol_api + +metadata = { +'protocolName': 'Sample Aliquoting & Plate Prep', +'author': 'ChatGPT', +'apiLevel': '2.16' +} + +def run(protocol: protocol_api.ProtocolContext): + + # Load labware + source_labware1 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 1) + source_labware2 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 2) + source_labware3 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 4) + source_labware4 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 5) + destination_labware = protocol.load_labware('nest_96_wellplate_2ml_deep', 3) + + tiprack1 = protocol.load_labware('opentrons_96_filtertiprack_200ul', 7) + tiprack2 = protocol.load_labware('opentrons_96_filtertiprack_200ul', 8) + + # Load pipette + p300_single = protocol.load_instrument('p300_single_gen2', 'right', tip_racks=[tiprack1, tiprack2]) + + # Transfer samples + p300_single.transfer(20, source_labware1.wells(), destination_labware.wells_by_name()['A1'], new_tip='always') + p300_single.transfer(20, source_labware2.wells(), destination_labware.wells_by_name()['B1'], new_tip='always') + p300_single.transfer(20, source_labware3.wells(), destination_labware.wells_by_name()['C1'], new_tip='always') + p300_single.transfer(20, source_labware4.wells(), destination_labware.wells_by_name()['D1'], new_tip='always') + + + +#### Example 5: Reagent transfer protocol + + +I want to perform a series of liquid transfers using two different single-channel pipettes. Here's what we need to do: + +First, using the 50 µL pipette mounted on the left: + +- Take 15 µL from the reservoir and transfer it to every well in both our 384-well and 96-well destination plates. We can use the same tip for all these transfers. +- Then, transfer 20 µL from each well of our 384-well source plate to the corresponding wells in our 384-well destination plate. We can keep using the same tip for these transfers too. + +Finally, using the 1000 µL pipette mounted on the right: + +- Transfer 100 µL from each well of our 96-well source plate to the corresponding wells in our 96-well destination plate. For this step, we'll need to use a fresh tip for each transfer. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +"protocolName": "Reagent Transfer ", +"author": "Opentrons Generative AI", +"description": "Transfer reagents from multile source labware to multiple destination labware", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("nest_1_reservoir_195ml", location='B1') +source_2 = protocol.load_labware("biorad_384_wellplate_50ul", location='B2') +source_3 = protocol.load_labware("biorad_96_wellplate_200ul_pcr", location='B3') +destination_1 = protocol.load_labware("corning_384_wellplate_112ul_flat", location='D1') +destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D2') + + tip1000 = protocol.load_labware('opentrons_flex_96_filtertiprack_200ul', 'A1') + tip50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p1000s = protocol.load_instrument('flex_1channel_1000','right',tip_racks = [tip1000]) + p50s = protocol.load_instrument('flex_1channel_50','left',tip_racks = [tip50]) + + # load trash bin + trash = protocol.load_trash_bin('A3') + + # volumes setup + transfer_vol_1 = 15 + transfer_vol_2 = 20 + transfer_vol_3 = 100 + + # wells setup + source_wells_1 = source_1.wells_by_name()['A1'] + source_wells_2 = source_2.wells() + source_wells_3 = source_3.wells() + destination_wells_1 = destination_1.wells() + destination_wells_2 = destination_2.wells() + + # commands + p50s.transfer(transfer_vol_1, source_wells_1, destination_wells_1+destination_wells_2, new_tip="once") + p50s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") + p1000s.transfer(transfer_vol_3, source_wells_3, destination_wells_2, new_tip="always") + + + +#### Example 6: Reagent transfer protocol + + +I want to pool samples from two different plates into a reservoir using a single-channel pipette mounted on the left side. Here's what we need to do: + +First, let's pool samples from our first source plate: + +- Take 100 µL from each well in the first plate and transfer it to the first well of our reservoir +- We can use the same tip for all these transfers to save time + +Then, for our second source plate: + +- Again, take 100 µL from each well and add it to the same well in our reservoir where we pooled the first set +- Keep using the same tip for these transfers too + +Remember, we're treating these as two separate steps, but both are basically pooling samples from different source plates into the same destination well. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +'protocolName': 'Reagent Transfer', +'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +# protocol run function + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C1') +source_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C2') +destination_1 = protocol.load_labware("nest_1_reservoir_195ml", location='D1') + + tiprack200 = protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location='B2') + + # pipettes + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[tiprack200]) + + # load trash bin + trash = protocol.load_trash_bin('A3') + + # volume setup + transfer_vol_1 = 100 + transfer_vol_2 = 100 + + # wells setup + source_wells_1 = source_1.wells() + source_wells_2 = source_2.wells() + destination_wells_1 = destination_1.wells_by_name()['A1'] + + # commands + p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + + p1000s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") + + + +#### Example 7: PCR protocol + + +I want to run a PCR protocol using three temperature-controlled modules: a thermocycler and two temperature modules (one for samples and one for mastermix). Here's what we need to do: + +First, let's set up our temperatures: + +- Set the thermocycler block to 22°C and its lid to 95°C +- Warm up the sample temperature module to 37°C +- Cool down the mastermix module to 10°C + +For the liquid handling steps, using our 96-channel pipette: + +1. Transfer 20 µL of mastermix, taking it from 5mm below the liquid surface and dispensing it 2mm from the bottom of the destination wells. We can use the same tip for this. + +2. Next, transfer 20 µL of sample, aspirating from 3mm above the well bottom and dispensing 7mm from the top of the destination wells. Do this at half the normal flow rate. Mix everything well - 5 cycles with the total 40 µL volume. When finished, pull the tips out slowly at 5 mm/s. Use the same tip for this transfer. + +For the PCR cycling: + +1. Move our plate to the thermocycler and close the lid +2. Run these steps: + - One cycle at 74°C for 65 seconds + - 25 cycles of: + - 60°C for 7 seconds + - 84°C for 19 seconds + - 57°C for 44 seconds + - One final cycle at 75°C for 8 minutes + - Hold everything at 4°C + +Finally: + +1. Open the thermocycler lid and move the plate back to its original position +2. We'll pause here - you'll need to seal the plate and put it in the fridge at 4°C +3. Turn off all the temperature modules + + + +from opentrons import protocol_api + +metadata = { +'protocol_name': 'PCR Amplification protocol', +'author': 'Opentrons Generative AI', +'description': 'PCR Amplification protocol with 25 cycles', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +def run(protocol: protocol_api.ProtocolContext): # Sample parameters +sample_volume_ul = 20 +master_mix_volume_ul = 20 +mix_cycles = 5 +total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul +return_slot = 'C3' + + master_mix_temperature_c = 10 + sample_temperature_c = 37 + step1_cycles = 1 + step2_cycles = 25 + step3_cycles = 1 + + # Thermocycler parameters + lid_temperature_c = 95 + initial_block_temperature_c = 22 + final_hold_temperature_c = 4 + + # Modules + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 'D1') + master_mix_temperature_module = protocol.load_module('temperature module gen2', 'D3') + + # Adapters + sample_adapter = sample_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + master_mix_adapter = master_mix_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + + # Labware + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + destination_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'C3') + tips_50ul = [ + protocol.load_labware( + 'opentrons_flex_96_filtertiprack_50ul', + slot, + adapter="opentrons_flex_96_tiprack_adapter" + ) + for slot in ['A2', 'B2', 'C2'] + ] + + # Pipette + pipette_96channel = protocol.load_instrument('flex_96channel_1000', 'left', tip_racks=tips_50ul) + # load trash bin + trash = protocol.load_trash_bin('A3') + + # Well allocation + sample_source_wells = sample_plate['A1'] + destination_wells = destination_plate['A1'] + master_mix_source_well = master_mix_plate['A1'] + + # Set thermocycler block and lid temperature + thermocycler_module.set_block_temperature(initial_block_temperature_c) + thermocycler_module.open_lid() + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Temperature module setup + sample_temperature_module.set_temperature(sample_temperature_c) + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Master mix transfer + pipette_96channel.transfer( + master_mix_volume_ul, + master_mix_source_well.top(-5), + destination_wells.bottom(2), + new_tip='once' + ) + + # Sample transfer + pipette_96channel.pick_up_tip() + pipette_96channel.aspirate(sample_volume_ul, sample_source_wells.bottom(3), rate=0.5) + pipette_96channel.dispense(sample_volume_ul, destination_wells.top(-7), rate=0.5) + pipette_96channel.mix(mix_cycles, total_mix_volume_ul) + pipette_96channel.move_to(destination_wells.top(), speed=5) + pipette_96channel.drop_tip() + + # Moving the plate to the thermocycler + protocol.move_labware(destination_plate, thermocycler_module, use_gripper=True) + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=step1_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=step2_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=step3_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.set_block_temperature(final_hold_temperature_c) + thermocycler_module.open_lid() + + # Moving the plate back to its original location + protocol.move_labware(destination_plate, return_slot, use_gripper=True) + + # Optional: pause for manual intervention + protocol.pause("Pick up the destination plate, seal it, and refrigerate at 4C.") + + # Deactivate temperature modules at the end of the protocol + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + + + +#### Example 8: Serial dilution protocol + + +I want to perform a serial dilution using an 8-channel pipette mounted on the right side. Here's what we need to do: + +First, let's set up our key measurements: + +- We're doing a 1:3 dilution series with 10 dilution steps +- We'll work with a total volume of 150 µL in each well +- This means we'll transfer 50 µL between wells and add 100 µL of diluent +- We'll use a 10 µL air gap for all our transfers + +Here's the step-by-step process: + +1. Start by adding diluent to our plate: + + - Using one tip, transfer 100 µL of diluent (green liquid) from the reservoir to wells A2 through A11 + - Keep using the same tip and remember to use the air gap for each transfer + +2. Now for the serial dilution: + + - Get a fresh tip + - Starting with well A1 (which has our red sample), transfer 50 µL to well A2 + - Mix well - 5 times with 75 µL + - Continue this pattern down the row: + - Transfer 50 µL from A2 to A3, mix + - A3 to A4, mix + - And so on until you reach A11 + - Use the same tip for all these transfers and remember the air gap + +3. Finally, let's add our blank: + - Get a fresh tip + - Transfer 100 µL of diluent to well A12 + - Use the air gap for this transfer too + + + +metadata = { + 'protocolName': 'Customizable Serial Dilution', + 'author': 'Opentrons ', + 'source': 'Protocol Library' +} + +requirements = { +"robotType": "Flex", +"apiLevel": "2.19" +} + +def run(protocol): + + # Constants + DILUTION_FACTOR = 3 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150.0 + AIR_GAP_VOLUME = 10 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Labware setup + trough = protocol.load_labware('nest_12_reservoir_15ml', 'D2') + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D3') + tip_name = "opentrons_flex_96_filtertiprack_1000ul" + tipracks = [ + protocol.load_labware(tip_name, slot) + for slot in ["C1", "D1"] + ] + + # Pipette setup + pipette = protocol.load_instrument('flex_8channel_1000', 'right', tipracks) + + # Waste setup + trash = protocol.load_trash_bin("A3") + + # Reagent setup + diluent = trough.wells()[0] + source = plate.columns()[0] + + # Define and load liquids + diluent_liquid = protocol.define_liquid( + name="Dilutent", + description="Diluent liquid is filled in the reservoir", + display_color="#33FF33" + ) + sample_liquid = protocol.define_liquid( + name="Sample", + description="Non-diluted samples are loaded in the 1st column", + display_color="#FF0000" + ) + + diluent.load_liquid(liquid=diluent_liquid, volume=0.8 * diluent.max_volume) + for well in source: + well.load_liquid(liquid=sample_liquid, volume=TOTAL_MIXING_VOLUME) + + # Set up dilution destinations + dilution_destination_sets = [[row] for row in plate.rows()[0][1:NUM_DILUTIONS+1]] + dilution_source_sets = [[row] for row in plate.rows()[0][:NUM_DILUTIONS]] + blank_set = [plate.rows()[0][NUM_DILUTIONS+1]] + + # 1. Distribute diluent + all_diluent_destinations = [well for wells in dilution_destination_sets for well in wells] + pipette.pick_up_tip() + for dest in all_diluent_destinations: + pipette.transfer( + diluent_volume, + diluent, + dest, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + # 2. Perform serial dilutions + pipette.pick_up_tip() + for source_set, dest_set in zip(dilution_source_sets, dilution_destination_sets): + for s, d in zip(source_set, dest_set): + pipette.transfer( + transfer_volume, + s, + d, + air_gap=AIR_GAP_VOLUME, + mix_after=(5, TOTAL_MIXING_VOLUME/2), + new_tip='never' + ) + pipette.drop_tip() + + # 3. Add blank + pipette.pick_up_tip() + for blank_well in blank_set: + pipette.transfer( + diluent_volume, + diluent, + blank_well, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + + +#### Example 9: Serial dilution + + +I want to perform a serial dilution protocol using a multi-channel P300 pipette mounted on the left side. We'll be working with a temperature-controlled setup and need to achieve a 1.5x dilution factor across 10 wells, with a total mixing volume of 150 µL per well. + +Here's what we need to do: + +First, let's calculate our volumes: + +- Transfer volume will be 150 µL divided by 1.5 +- Diluent volume will be 150 µL minus our transfer volume + +Now for the actual steps: + +1. Let's start by adding diluent to our dilution wells: + + - Take diluent from the first reservoir well and add our calculated diluent volume to wells 2 through 10 in the first row of our temperature-controlled plate + - Use a 10 µL air gap for each transfer + - Use fresh tips for each well + +2. Now for the serial dilution: + + - Starting from well 1, we'll transfer our calculated transfer volume to well 2 + - After each transfer, mix 5 times using (150 µL - 5 µL) + - Keep using a 10 µL air gap + - Use new tips for each transfer + - Continue this pattern, moving from well to well until we reach well 10 + +3. Finally, add a blank to the last well: + - Transfer our calculated diluent volume from the first reservoir well to well 10 + - Use a 10 µL air gap + - Use a fresh tip for this transfer + + + +metadata = { + 'protocolName': 'Serial Dilution for Eskil', + 'author': 'John C. Lynch', + 'source': 'Custom Protocol Request', + 'apiLevel': '2.19' +} + +def run(protocol): + + # Constants + PLATE_TYPE = 'opentrons_96_aluminumblock_nest_wellplate_100ul' + DILUTION_FACTOR = 1.5 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Load temperature module and labware + temp_module = protocol.load_module('temperature module gen2', '4') + reservoir = protocol.load_labware('nest_12_reservoir_15ml', '1') + dilution_plate = temp_module.load_labware(PLATE_TYPE) + + # Load tipracks + tipracks = [ + protocol.load_labware('opentrons_96_tiprack_300ul', slot) + for slot in ['2', '3'] + ] + + # Load pipette + pipette = protocol.load_instrument( + 'p300_multi_gen2', + mount='left', + tip_racks=tipracks + ) + + # 1. Distribute diluent + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][1:NUM_DILUTIONS], + air_gap=10, + new_tip='always' + ) + + # 2. Perform serial dilutions + sources = dilution_plate.rows()[0][:NUM_DILUTIONS-1] + dests = dilution_plate.rows()[0][1:NUM_DILUTIONS] + + pipette.transfer( + transfer_volume, + sources, + dests, + air_gap=10, + mix_after=(5, TOTAL_MIXING_VOLUME-5), + new_tip='always' + ) + + # 3. Add blank + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][-1], + air_gap=10, + new_tip='always' + ) + + + +#### Example 10 + + +I want to perform a serial dilution using a single-channel pipette mounted on the left side. Here's what we need to do: + +First, let's add our diluent: + +- Take 100 µL of diluent from the first well of our reservoir and distribute it to every well in our plate. + +Then, for the serial dilution: + +- For each of the 8 rows in our plate: + 1. Start by transferring 100 µL of our solution from well A2 of the reservoir to the first well of the row + 2. Mix it well - 3 times with 50 µL + 3. Then move along the row from left to right: + - Transfer 100 µL from each well to the next well + - Mix 3 times with 50 µL after each transfer + - Continue this pattern for 11 transfers to complete the row + 4. Repeat this process for all 8 rows + + + +from opentrons import protocol_api + +metadata = { +"protocolName": "Serial Dilution Tutorial – Flex 1-channel", +"description": """serial dilution""", +"author": "New API User" +} + +requirements = { +"robotType": "Flex", +"apiLevel": "2.16" +} + +def run(protocol: protocol_api.ProtocolContext): +tips = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D1") +reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2") +plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "D3") +trash = protocol.load_trash_bin("A3") +left_pipette = protocol.load_instrument("flex_1channel_1000", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) + + + +#### Example 11 + + +Liquid Transfer with Heater-Shaker + +prompt description: + +```text +Hi, please help me write a protocol for Flex. +Pipette: 96 channel +Decklayout: +Slot C1: Nest 195 1 well reservoir +Slot D1: Heater shaker module with corning 96 well flat bottom plate +Slot A2: 200 ul tiprack +Slot B2: 1000 ul tiprack for flex +Slot C2: 50 ul tiprack +Slot D2: PCR plate Steps: + +Open the labware latch and allow the user to load the plate +Close the labware latch +Using 200 ul tip transfer 70 ul liquid from slot c1 to d1. +Using 50 ul tip transfer 10 ul liquid from slot d2 to D1. +shake the plate at 2000 rpm for 1 minute +``` + + + + + +```python +from opentrons import protocol_api + +metadata = {{ + 'protocolName': 'Liquid Transfer with Heater Shaker', + 'author': 'User', + 'description': "Transfer liquids between reservoir, PCR plate, and heater shaker module's plate." +}} +requirements = {{"robotType": "Flex", "apiLevel": "2.19"}} + +def run(protocol: protocol_api.ProtocolContext): + # Load trash before commands + # use a waste chute or a trashbin depending on the setup + trash = protocol.load_trash_bin("A3") + #chute = protocol.load_waste_chute() + + # Modules + heater_shaker_module = protocol.load_module('heaterShakerModuleV1', 'D1') + heater_shaker_plate = heater_shaker_module.load_labware('corning_96_wellplate_360ul_flat') + + # Labware + reservoir = protocol.load_labware('nest_1_reservoir_195ml', 'C1') + pcr_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D2') + tiprack_200ul = protocol.load_labware('opentrons_flex_96_tiprack_200ul', 'A2', adapter = "opentrons_flex_96_tiprack_adapter") + tiprack_1000ul = protocol.load_labware('opentrons_flex_96_tiprack_1000ul', 'B2', adapter = "opentrons_flex_96_tiprack_adapter") + tiprack_50ul = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'C2', adapter = "opentrons_flex_96_tiprack_adapter") + + # Pipettes + pip96 = protocol.load_instrument('flex_96channel_1000', mount='left', tip_racks=[tiprack_200ul, tiprack_50ul]) + + # Steps + # 1. Open the labware latch and allow the user to load the plate + heater_shaker_module.open_labware_latch() + + + protocol.pause("Please put the Corning 96 well plate and press continue") + # 2. Close the labware latch + heater_shaker_module.close_labware_latch() + protocol.comment("Just a message is displayed. This step is") + # 3. Using 200 ul tip transfer 70 ul liquid from slot c1 to d1. + + pip96.transfer(70, reservoir['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 4. Using 50 ul tip transfer 10 ul liquid from slot d2 to D1. + pip96.transfer(10, pcr_plate['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 5. Shake the plate at 2000 rpm for 1 minute + heater_shaker_module.set_and_wait_for_shake_speed(rpm=2000) + protocol.delay(minutes=1) + heater_shaker_module.deactivate_shaker() +``` + + diff --git a/opentrons-ai-server/api/storage/docs/commands-v0.0.1.md b/opentrons-ai-server/api/storage/docs/commands-v0.0.1.md new file mode 100644 index 00000000000..000e874f843 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/commands-v0.0.1.md @@ -0,0 +1,1259 @@ +#### + +Note when working with temperature module +PCR plate does not go directly on the module. We need thermal adapter. +Temperature Module White Paper suggests using the "PCR block" and a water. + +Hence the following pattern: + +```python +temp_module = protocol.load_module('temperature module gen2', 1) +adapter = temp_module.load_adapter("opentrons_96_well_aluminum_block") +plate = adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') +``` + +#### + +A good example of using a `transfer` method: +The following is correct: + +```python +p1000s.transfer(transfer_vol, src, dest_wells, new_tip='always') +``` + +The following is incorrect: + +```python +for src in src_wells: + p1000s.transfer(transfer_vol, src, dest_wells, new_tip='always') +``` + +Note that `transfer` function uses `for` operator implicitly. + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 50 uL from wells A1, A2 in source labware 1 +to B6, B7 in source labware 2. Reuse the same tip for each transfer. + +The following is correct: + +```python +transfer_vol_1 = 50 # setup volume + +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] # source setup wells +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] # destination setup wells + +p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") # transfer function without any loop +``` + +The following is not correct since it calls transfer function twice rather than once: + +```python +p300_single.transfer(50, source_labware_1.wells_by_name()['A1'], source_labware_2.wells_by_name()['B6'], new_tip='once') +p300_single.transfer(50, source_labware_1.wells_by_name()['A2'], source_labware_2.wells_by_name()['B7'], new_tip='never') +``` + +#### + +Use the left-mounted P1000 Single-Channel GEN2 pipette to transfer 200 uL of reagent from wells A7, A6, A5, A2, A3 +of the source labware to the corresponding wells A5, A9, A1, A10, A2 of the destination labware. Use a new tip for each transfer. + +```python +TRANSFER_VOL_1 = 200 +SRC_WELL_1 = [source.wells_by_name()[well] for well in ['A7', 'A6', 'A5', 'A2', 'A3']] +DEST_WELL_1 = [destination.wells_by_name()[well] for well in ['A5', 'A9', 'A1', 'A10', 'A2']] + +# command 1 +p1000s_1.transfer(TRANSFER_VOL_1, SRC_WELL_1, DEST_WELL_1, new_tip="always") +``` + +#### + +Use the right-mounted P1000 Single-Channel GEN2 pipette to transfer 18 uL of liquid from wells A9, A12, A6, A10, A3 +of the source labware to the corresponding wells A7, A11, A6, A3, A9 of the destination labware. Use the same tip for all transfers. + +```python +TRANSFER_VOL_2 = 18 +SRC_WELL_2 = [source.wells_by_name()[well] for well in ['A9', 'A12', 'A6', 'A10', 'A3']] +DEST_WELL_2 = [source.wells_by_name()[well] for well in ['A7', 'A11', 'A6', 'A3', 'A9']] + +# command 2 +p1000s_2.transfer(TRANSFER_VOL_2, SRC_WELL_2, DEST_WELL_2, new_tip="once") +``` + +#### + +Using P300 Single-Channel GEN2 pipette on the left mount, transfer 119 uL of reagent +from first well in source labware to E12, G12, B9, A6, D7 wells in the destination labware. +Use a new tip for each transfer. + +```python +vol = 119 +src_well = source.wells_by_name()['A1'] +dest_wells = [destination.wells_by_name()[well] for well in ['E12', 'G12', 'B9', 'A6', 'D7']] + +# commands +p300s.transfer(vol, src_well, dest_wells, new_tip="always") +``` + +#### + +Using P20 Single Channel, transfer 13ul of reagent from the first tube of the source rack to each well in the destination plate. +Use the same tip for each transfer. + +```python +# parameters +vol = 13 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p20s.transfer(vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using P20 Single Channel GEN2 pipette on right mount, transfer 16 uL from the first well of source labware 1 to each well +in destination labware 1 and destination labware 2. Reuse the same tip + +```python +# volumes setup +transfer_vol_1 = 16 + +# wells setup +source_wells_1 = source_1.wells_by_name()['A1'] +destination_wells_1 = destination_1.wells() +destination_wells_2 = destination_2.wells() +all_destinations = destination_wells_1 + destination_wells_2 + +# commands +p20s.transfer(transfer_vol_1, source_wells_1, all_destinations, new_tip="once") +``` + +#### + +Using P20 Single Channel GEN2 pipette on right mount, transfer 23 uL from each well in source labware 2 to +each well in the destination labware 1. Reuse the same tip. + +```python +# volumes setup +transfer_vol_2 = 23 + +# wells setup +source_wells_2 = source_2.wells() +destination_wells_1 = destination_1.wells() + +# commands +p20s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") +``` + +#### + +Using P20 Multi-Channel GEN2 pipette on the right mount, transfer 5 uL of reagent +from first column in source labware to columns 5, 9, 1, 10, and 2 in the destination labware. +Use the same tip everytime. + +```python +# parameters +vol = 5 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(vol, src_col, dest_cols, new_tip="once") +``` + +#### + +Using P20 Multi-Channel GEN2 pipette on the left mount, transfer 24 uL of reagent +from columns 4, 3, 6, 1, 11 in source labware to columns 5, 9, 1, 10, 2 in the same source labware. +Use a new tip everytime. + +```python +# parameters +vol = 24 +src = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest = [source.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(vol, src, dest, new_tip="always") +``` + +#### + +Using P300 Multi Channel, transfer 55 uL of sample from each column of the source plate +into the corresponding columns of the destination deep well plate. +Change tips for each transfer. + +```python +# parameters +vol = 55 +src_cols = source.columns() +dest_cols = destination.columns() + +# commands +p300m.transfer(vol, src_cols, dest_cols, new_tip='always') +``` + +#### + +Using P300 Single Channel GEN2, transfer 70ul of reagent from the first tube of the source rack to each well in the destination plate. +Keep the same tip for each transfer. + +```python +# parameters +vol = 70 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p300s.transfer(vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using P300 Single Channel GEN2, transfer 75ul of samples from each tube in the source tube rack to each well of the destination plate. +Use a new tip for each transfer. + +```python +# parameters +vol = 75 +src_wells = source.wells() +dest_wells = destination.wells() + +# commands +p300s.transfer(vol, src_wells, dest_wells, new_tip='always') +``` + +#### + +Using P300 Multi-channel pipette on the left mount, transfer 65 uL of reagent from first column in the source labware 1 +to all the columns in destination labware 1. Keep the same set of tips for this entire set of transfers within this step. + +```python +transfer_vol_1 = 65 + +# wells setup +source_wells_1 = source_1.columns_by_name()['1'] +destination_wells_1 = destination_1.columns() + +p300m.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using P1000 Single-Channel GEN2 pipette on left mount, transfer 175.0 uL of reagent +from H10, F12, D7, B1, C8 wells in source labware +to first well in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 175.0 +src_wells = [source.wells_by_name()[well] for well in ['H10', 'F12', 'D7', 'B1', 'C8']] +dest_well = destination.wells_by_name()['A1'] + +# commands +p1000s.transfer(transfer_vol, src_wells, dest_well, new_tip="always") +``` + +#### + +Using P300 Single-channel GEN2 pipette on left mount, transfer 51 uL from wells A1, A2 in source labware 1 +to B6, B7 in source labware 2. Reuse the same tip. + +```python +# volume setup +transfer_vol_1 = 51 + +# well setup +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + +# commands +p300s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using P20 Single-channel GEN2 pipetet on right mount, transfer 14 uL from wells C4, C6 in source labware 2 +to A3, A4 in source labware 1. Reuse the same tip. + +```python +# volume setup +transfer_vol_2 = 14 + +# well setup +source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] +destination_wells_2 = [source_1.wells_by_name()[wells] for wells in ['A3', 'A4']] + +# commands +p20s.transfer(transfer_vol_2, source_wells_2, destination_wells_2, new_tip="once") +``` + +#### + +Using P20 Single-channel GEN2 pipette on right mount, transfer 17 uL from wells B6, B7 in source labware 2 +to A1, B1 in destination labware 1. Use a new tip each time. + +```python +# volume setup +transfer_vol = 17 +# well setup +source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] +destination_wells_1 = [destination_1.wells_by_name()[wells] for wells in ['A1', 'B1']] +# commands +p20s.transfer(transfer_vol, source_wells_2, destination_wells_1, new_tip="always") +``` + +#### + +Using P20 Single-channel GEN2 pipette on right mount, transfer 15 uL from wells C4, C6 in source labware 2 +to A1, B1 in destination labware 2. Use a new tip each time. + +```python +# volume setup +transfer_vol = 15 + +# well setup +source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] +destination_wells_2 = [destination_2.wells_by_name()[wells] for wells in ['A1', 'B1']] + +# commands +p20s.transfer(transfer_vol, source_wells_2, destination_wells_2, new_tip="always") +``` + +#### + +Using the P300 Single-Channel GEN2, pool [transfer_vol]ul from all tubes in source labware into A1 of the destination labware. +Change tips between each tube. + +```python +# well setup +source_wells = source.wells() +destination_wells = [destination.wells_by_name()[wells] for wells in ['A1']] +# Transfer samples +p300_single.transfer(transfer_vol, source_wells, destination_wells, new_tip='always') +``` + +#### + +Using P300 single-channel GEN2 pipette, pool 95 uL of liquid from all the wells in source labware 1 to +the first well in destination labware 1. Use the same tip throughout. + +```python +# volume setup +transfer_vol_1 = 95 +# wells setup +source_wells_1 = source_1.wells() +destination_wells_1 = destination_1.wells_by_name()['A1'] + +# commands +p300s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using the P20 Multi-Channel GEN2 pipette on the right mount, transfer 3 uL of reagent from the first column in the source labware +to columns 5, 9, 1, 10, 2 in the destination labware. Use a new set of tips for each transfer. + +```python +# parameters +transfer_vol = 3 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(transfer_vol, src_col, dest_cols, new_tip='always') +``` + +#### + +Using the P20 Multi-Channel GEN2 pipette on the right mount, transfer 8 uL of reagent from source columns 4, 3, 6, 1, +and 11 to columns 5, 9, 1, 10, and 2 in the destination labware. Use the same set of tips for all transfers. + +```python +# parameters +transfer_vol = 8 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(transfer_vol, src_cols, dest_cols, new_tip="once") +``` + +#### + +Using P300 Multi-Channel GEN2 pipette on the left mount, transfer 38 uL of reagent from 4, 3, 6, 1, 11 +columns in the source labware to 5, 9, 1, 10, 2 columns in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 38 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p300m.transfer(transfer_vol, src_cols, dest_cols, new_tip="always") +``` + +#### + +Using P20 Single GEN2 pipette on the right mount, transfer 10 uL of reagent +from the first well of source labware 2 to all the wells in the destination labware. Reuse the same tip. + +```python +# volumes setup +transfer_vol_1 = 10 +# wells setup +source_wells_2 = source_labware_2.wells_by_name()['A1'] +destination_wells_1 = [dest.wells() for dest in destination_list] # a list of destinations +# commands +p20s.transfer(transfer_vol_1, source_wells_2, destination_wells_1, new_tip="once") +``` + +#### + +Using P300 Single GEN2 on the left mount, perform a well to well transfer of 90 uL from source +labware to the destination labware. Use a new tip each time. + +```python +# volumes setup +transfer_vol +# wells setup +source_wells = [src.wells() for src in source_labware] +destination_wells = [dest.wells() for dest in destination_list] # a list of destinations +# commands +p300s.transfer([transfer_vol], source_wells, destination_wells, new_tip="always") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, +transfer 186.0 uL of reagent from A7, A6, A5, A2, A3 of the source labware to A5, A9, A1, A10, A2 the destination labware. +Use a new tip for all transfers. + +```python +# parameters +TRANSFER_VOL = 186.0 +SRC_WELLS = [source.wells_by_name()[well] for well in ['A7', 'A6', 'A5', 'A2', 'A3']] +DEST_WELLS = [destination.wells_by_name()[well] for well in ['A5', 'A9', 'A1', 'A10', 'A2']] + +# command 1 +p1000s_1.transfer(TRANSFER_VOL, SRC_WELLS, DEST_WELLS, new_tip="always") +``` + +#### + +Use Flex 1-Channel 1000 uL Pipette on right mount, +transfer 10 uL of liquid from A9, A12, A6, A10, A3 of source labware to A7, A11, A6, A3, A9 of the destination labware. +Use the same tip for all transfers. + +```python +# parameters +TRANSFER_VOL = 10 +# well setup +SRC_WELLS = [source.wells_by_name()[well] for well in ['A9', 'A12', 'A6', 'A10', 'A3']] + = [destination.wells_by_name()[well] for well in ['A7', 'A11', 'A6', 'A3', 'A9']] + +# command 1 +[pipette object].transfer(TRANSFER_VOL, SRC_WELLS, DEST_WELLS, new_tip="once") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 127.0 uL of reagent from the first well in source labware +to E12, G12, B9, A6, D7 wells in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 127.0 +src_well = source.wells_by_name()['A1'] +dest_wells = [destination[well] for well in ['E12', 'G12', 'B9', 'A6', 'D7']] + +# commands +[pipette object].transfer(transfer_vol, src_well, dest_wells, new_tip="always") +``` + +#### + +Using Flex 1-Channel 50 uL Pipette, transfer 2ul of reagent from the first tube of the source rack to each well in the destination plate. +Use the same tip for each transfer. + +```python +# parameters +transfer_vol = 2 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p50s.transfer(transfer_vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using the Flex 1-Channel 50 uL Pipette, transfer 25 uL from the first well of source labware 1 to each well +in destination labware 1 and destination labware 2. Use the same tip for each transfer. + +```python +# volumes setup +transfer_vol_1 = 25 + +# wells setup +source_wells_1 = source_1.wells_by_name()['A1'] +destination_wells_1 = destination_1.wells() +destination_wells_2 = destination_2.wells() +all_dest = destination_wells_1+destination_wells_2 + +# commands +p50s.transfer(transfer_vol_1, source_wells_1, all_dest, new_tip="once") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on right mount, transfer 5 uL of reagent from the first column in source labware +to columns 4, 8, 1, 9, and 2 in the destination labware. Use the same tip for all transfers. + +```python +# parameters +transfer_vol = 5 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['4', '8', '1', '9', '2']] + +# commands +p50m.transfer(transfer_vol, src_col, dest_cols, new_tip="once") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on left mount, transfer 24.0 uL of reagent from columns 3, 2, 5, 1, 10 +to columns 4, 8, 1, 9, 2 in the same source labware. Use a new tip for each transfer. + +```python +#parameters +transfer_vol = 24.0 +src_cols = [source.columns_by_name()[idx] for idx in ['3', '2', '5', '1', '10']] +dest_cols = [source.columns_by_name()[idx] for idx in ['4', '8', '1', '9', '2']] + +# commands +p50m.transfer(transfer_vol, src_cols, dest_cols, new_tip="always") +``` + +#### + +Using Flex 8-Channel 1000 uL Pipette , transfer 70ul of sample from each well of the first column of the source plate into the first column of +the destination plate. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 70 +src_col = source.columns_by_name()['1'] +dest_col = destination.columns_by_name()['1'] + +# commands +p1000m.transfer(transfer_vol, src_col, dest_col, new_tip='always') +``` + +#### + +Transfer 80ul of reagent from the first tube of the source rack to each well in the destination plate. +Use the same tip for each transfer. + +```python +# parameters +transfer_vol = 80 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p1000s.transfer(transfer_vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette, aliquot 190 ul of samples from each tube in the source tube rack to +all wells of the destination plate evenly. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 190 +src_wells = source.wells() +dest_wells = destination.wells() + +# commands +p1000s.transfer(transfer_vol, src_wells, dest_wells, new_tip='always') +``` + +#### + +Using Flex 8-Channel 1000 uL Pipette on left mount, transfer 40 uL from the first column in the source labware 1 +to the first column in destination labware 1. Keep the same tip for this entire set of transfers within this step. + +```python +# volumes setup +transfer_vol_1 = 40 +# wells setup +source_wells_1 = source_1.columns_by_name()['1'] +destination_wells_1 = destination_1.columns_by_name()['1'] +p1000m.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 197 uL of reagent +from H10, F12, D7, B1, C8 wells in source labware to the first well in the destination labware. +Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 197 +src_wells = [source.wells_by_name()[well] for well in ['H10', 'F12', 'D7', 'B1', 'C8']] +dest_well = destination.wells_by_name()['A1'] + +# commands +p1000s.transfer(transfer_vol, src_wells, dest_well, new_tip="always") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 52 uL from wells A1, A2 in source labware 1 +to B6, B7 in source labware 2. Reuse the same tip for each transfer. + +```python +# volume setup +transfer_vol_1 = 52 + +# well setup +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + +# commands +p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using Flex 1-Channel 50 uL Pipette on right mount, transfer 20 uL from wells B6, B7 in source labware 2 +to A1, B1 in destination labware 1. Use a new tip for each transfer. + +```python +# volume setup +transfer_vol_3 = 20 + +# well setup +source_wells_3 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] +destination_wells_3 = [destination_1.wells_by_name()[wells] for wells in ['A1', 'B1']] + +# commands +p50s.transfer(transfer_vol_3, source_wells_3, destination_wells_3, new_tip="always") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette , pool 25ul from all tubes in source labware1 into A1 of the destination labware. +Change tips between each tube. + +```python +vol = 25 +source_wells = source_labware1.wells() +dest_well = destination_labware.wells_by_name()['A1'] + +p1000s.transfer(vol, source_wells, dest_well, new_tip='always') +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette, pool 90 uL of liquid from all the wells in source labware 1 to +the first well in destination labware 1. Reuse the same tip. + +```python +# volume setup +transfer_vol_1 = 90 +# wells setup +source_wells_1 = source_1.wells() +destination_wells_1 = destination_1.wells_by_name()['A1'] +# commands +p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on right mount, transfer 7 uL of reagent from the first column in source labware to +4, 8, 1, 9, and 2 columns in the destination labware. Use a new tip for each transfer. + +```python +#parameters +transfer_vol = 7 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['4', '8', '1', '9', '2']] + +# commands +p50m.transfer(transfer_vol, src_col, dest_cols, new_tip="always") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on right mount, transfer 6 uL of reagent from 4, 3, 6, 1, 11 columns in source labware +to 5, 9, 1, 10, 2 columns in the destination labware. Using the same tip for all transfers. + +```python +# parameters +transfer_vol = 6 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p50m.transfer(transfer_vol, src_cols, dest_cols, new_tip="once") +``` + +#### + +Using Flex 8-Channel 1000 uL Pipette on left mount, transfer 78 uL of reagent from 4, 3, 6, 1, 11 columns in the source labware +to 5, 9, 1, 10, 2 columns in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 78 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p1000m.transfer(transfer_vol, src_cols, dest_cols, new_tip="always") +``` + +#### + +Using Flex 1-Channel 50 uL Pipette on right mount, transfer 25 uL of reagent +from the first well of source labware 2 to all wells in destination labware. Reuse the same tip. + +```python +# volumes setup +transfer_vol_1 = 25 + +# wells setup +source_wells_2 = source_labware_2.wells_by_name()['A1'] +destination_wells_1 = [dest.wells() for dest in destination] + +# commands +p50s.transfer(transfer_vol_1, source_wells_2, destination_wells_1, new_tip="once") +``` + +#### + +- when command says 'Use a new tip for each transfer', or something similar, + set the `new_tip` parameter to "always": `new_tip='always'`. +- when command says 'Use the same tip for all transfers.', 'reuse the same tip' or something similar. + set the `new_tip` parameter to "once": `new_tip='once'`. + +#### + +Note that when command says `Use the same tip for all transfers` or similar. +Do not use new_tip='once' inside loop as shown below + +```python +for src, dest in LIST: + p50_multi_right.transfer(transfer_vol, src, dest, new_tip='once') +``` + +Instead, remove `for` and use like so: + +```python +p50_multi_right.transfer(transfer_vol, src, dest, new_tip='once') +``` + +Note that no `for` loop is used. + +#### + +Source labware is ['labware name'], placed on [temperature module] on slot 3 + +```python +# modules +temperature_module = protocol.load_module(['temperature module gen2'], 3) + +# labware +source = temperature_module.load_labware(['labware name']) +``` + +#### + +Thermocycler module GEN 2 is present on slot A1+B1. `A1+B1` referes to 7, please use the slot number 7. + +Correct thermocycler load: + +```python +thermocycler = protocol.load_module('thermocyclerModuleV2') # by default slot number is 7 +``` + +Incorrect thermocycler load: + +```python +thermocycler = protocol.load_module('thermocyclerModuleV2', 'A1+B1') +``` + +#### + +- Sample temperature module GEN 2 is placed on slot D1 +- Opentrons 96 Well Aluminum Block adapter is placed on sample temperature module GEN 2 + +Corresponding protocol + +```python +temp_mod_sample = protocol.load_module('temperature module gen2', 'D1') +temp_sample_adapter = temp_mod_sample.load_adapter('opentrons_96_well_aluminum_block') +``` + +#### + +Open thermocycler lid + +```python +[thermocycler_object].open_lid() +``` + +#### + +Set the thermocycler block temperature to 1 C. + +```python +plate_temperature_c = 1 +[thermocycler_object].set_block_temperature(plate_temperature_c) +``` + +#### + +Set the thermocycler lid temperature to 50 C. + +```python +lid_temperature_c = 50 +[thermocycler_object].set_lid_temperature(lid_temperature_c) +``` + +#### + +Set the sample temperature module to 3 C. + +```python +sample_temperature_c = 3 +[temperature_module].set_temperature(sample_temperature_c) +``` + +#### + +Transfer 17 uL of mastermix from the mastermix source wells to the destination wells. +Use the same pipette tip for all transfers. + +```python +[pippette_object].transfer( + 17, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' +) +``` + +#### + +Transfer 4 uL of the sample from the source to the destination. +Mix the sample and mastermix for a total volume of 15 uL 10 times. +Blow out to 'destination well' after each transfer. Use a new tip for each transfer. + +```python +[pippette_object].transfer( + 4, + [sample_source_wells],` + [sample_destination_wells], + new_tip='always', + mix_after=(10, 15), + blow_out=True, + blowout_location='destination well' +) +``` + +#### + +Close the thermocycler lid. + +```python +[thermocycler_module].close_lid() +``` + +#### + +Execute the thermocycler with the following profile: + +- 75 C for 66 seconds for 1 cycle (repetition). + +```python +[thermocycler_module].execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 66}], + repetitions=1, + block_max_volume=[total_mix_volume_ul] +) +``` + +Note that you must calculate `block_max_volume` based on the whole prompt context. + +#### + +Execute the thermocycler with the following profile: + +- 61C for 8 seconds, 85°C for 20 seconds, 58°C for 45 seconds for 14 cycles. + +```python +[thermocycler_module].execute_profile( + steps=[ + {'temperature': temp, 'hold_time_seconds': duration} + for temp, duration in zip([61, 85, 58], [8, 20, 45]) + ], + repetitions=14, + block_max_volume=[total_mix_volume_ul] +) +``` + +Note that you must calculate `block_max_volume` based on the whole prompt context. + +#### + +Hold the thermocycler block at 4°C. + +```python +hold_temperature_c = 10 +[thermocycler_module].set_block_temperature(hold_temperature_c) +``` + +#### + +Deactivate the mastermix temperature module. + +```python +[master_mix_temperature_module].deactivate() +``` + +#### + +Sample source wells: the first 48 wells column-wise in the sample source plate. +Note that the pipette is a single channel. + +Use `[source_labware].wells()`. For example, + +```python +number_of_samples = 48 +source_wells = sample_plate.wells()[:number_of_samples] +``` + +#### + +Sample source wells: the first 48 wells column-wise in the sample source plate. +Note that the pipette is a multi-channel. + +- Estimate the columns using the number samples + +```python +number_of_samples = 48 +number_of_columns = math.ceil(number_of_samples / 8) +``` + +- Then, use `[source_labware].columns()` method to access the columns. + For example, + +```python +source_wells = sample_plate.columns()[:number_of_columns] +``` + +#### + +When a command says `move destination labware` or something, use `move_labware`. +We need to specify two arguments: + +- labware: The labware object you want to move. +- new_location: The destination where you want to move the labware. + +This can be any empty deck slot or a module that is ready to accept labware. +Example for the slot, + +```python +protocol.move_labware([labware]], ['C4'], use_gripper=True) +``` + +Example for the module, + +```python +protocol.move_labware([labware]], [thermocycler], use_gripper=True) +``` + +#### + +Pause the protocol + +```python +protocol.pause("Pause please") +``` + +#### + +Transfer 21 uL of liquid from 6 mm below the top surface of mastermix well to 3 mm above the bottom of destination well. +Use the same tip for each transfer. + +```python +[pipette_object].transfer(21, mastermix_well.top(-6), dest.bottom(3), new_tip='once') +``` + +#### + +5 mm above the top of the well + +```python +plate['A1'].top(z=5) +``` + +5 mm below the top of the well + +```python +plate['A1'].top(z=-5) +``` + +5 mm above the bottom of the well + +```python +plate['A1'].bottom(z=1) +``` + +5 mm below the bottom of the well + +```python +plate['A1'].bottom(z=-5) +``` + +Transfer 20 uL of liquid from 5 mm below the top surface of the mastermix well to 2 mm above the bottom of the destination well. +Use the same tip for each transfer. + +```python +pipette_96channel.transfer(20, mastermix_source_well.top(-5), destination_wells.bottom(2), new_tip='once') +``` + +#### + +Remove the tip slowly out of the well at 5 mm/s speed + +```python +pipette.move_to([well].top(), speed=5) +``` + +Move to the top of the well at 5 mm/s speed + +```python +pipette.move_to([well].top(), speed=5) +``` + +Move to 2 mm below the top of well A1 + +```python +pipette.move_to(plate['A1'].top(z=-2)) +``` + +Move to 2 mm above the bottom of well A1 + +```python +pipette.move_to(plate['A1'].bottom(z=2)) +``` + +#### + +Transfer 20 ul of liquid from 3 mm above the source well bottom to destination well 7 mm beneath the top surface. Flow rate is at half the default. +Mix the sample and mastermix of 40 ul total volume 5 times. Remove the tip slowly out of the well at 5 mm/s speed. Use the same tip for each transfer. + +```python +pipette_96channel.pick_up_tip() +pipette_96channel.aspirate(20, sample_source_wells.bottom(3), rate=0.5) +pipette_96channel.dispense(20, destination_wells.top(-7), rate=0.5) +pipette_96channel.mix(5, 40) +pipette_96channel.move_to(destination_wells.top(), speed=5) +pipette_96channel.drop_tip() +``` + +#### + +Load three opentrons_flex_96_filtertiprack_50ul tip racks in slots A2, B2, and C2 + +```python +tips_50ul = [ + protocol.load_labware( + 'opentrons_flex_96_filtertiprack_50ul', + slot + ) + for slot in ['A2', 'B2', 'C2'] +] +``` + +or + +```python +tips_50ul_a = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'A2') +tips_50ul_b = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'B2') +tips_50ul_c = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'C2') +tips_50ul = [tips_50ul_a, tips_50ul_b, tips_50ul_c] +``` + +#### + +Move the destination labware to the thermocycler using a gripper. + +```python +protocol.move_labware(destination_plate, thermocycler_module, use_gripper=True) +``` + +#### + +I am going to be running a protocol on my Opentrons Flex. +I have a 96-channel pipette on the system. My destination plates will be +4 'nest_96_wellplate_2ml_deep' plates. My source labware will be +a 'nest_1_reservoir_195ml'. + +```python +pipette_96_channel = protocol.load_instrument( + 'flex_96channel_1000', mount='left' + ) +source_reservoir = protocol.load_labware('nest_1_reservoir_195ml', '1') +destination_plates = [ + protocol.load_labware('nest_96_wellplate_2ml_deep', slot) + for slot in ['2', '3', '4', '5'] +] +``` + +#### Example 5 + +Transfer 25 uL from multiple source wells to a single destination well, use a new tip every time, and touch the tip after dispense. + +```python +pipette.transfer(25, source_wells, dest_well, new_tip='always', touch_tip=True) +``` + +#### + +Transfer 10 uL from source to destination, with an air gap of 5 uL after aspiration. + +```python +pipette.transfer(10, source_well, dest_well, air_gap=5) +``` + +#### + +Transfer 200 uL from source to destination, blowing out in the source well after dispensing. Use the same tip for each transfer. + +```python +pipette.transfer(200, source_well, dest_well, trash=False, blow_out=True, blowout_location='source well') +``` + +#### + +Transfer 12 uL from source to destination, mix the destination well 5 times with 10 uL after dispensing, and do not touch the tip. + +```python +pipette.transfer(12, source_well, dest_well, mix_after=(5, 10)) +``` + +#### + +Transfer 30 uL from one source to multiple destinations, after each aspirate and touch tip after dispensing. + +```python +pipette.transfer(30, source_well, dest_wells, air_gap=10, touch_tip=True) +``` + +#### + +Flex 1-Channel 1000 uL Pipette is mounted on the left side. +mastermix source wells: first N wells column-wise in mastermix plate. +Note that the pipette is a single channel. + +```python +pipette = protocol.load_instrument('flex_1channel_1000', 'left', tip_racks=[tips_1000ul]) +sample_source_wells = sample_plate.wells()[:N] +``` + +#### + +Source Labware: `Opentrons 96 Flat Bottom Heater-Shaker Adapter with NEST 96 Well Plate 200 uL Flat` in slot D1 + +```python +source = protocol.load_labware('opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'D1') +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 150 uL from wells `A1, A2` in source labware 1 +to `B6, B7` in source labware 2. Use the same tip for each transfer. + +First collect all wells for source and destination. + +```python +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] +``` + +Then use a transfer method like so: + +```python +p1000s.transfer(150, source_wells_1, destination_wells_1, new_tip="once") +``` + +Note that we are using a single transfer function for multiple wells. + +The following is totally wrong: + +```python +pipette_1000ul.transfer(50, source_labware_1.wells_by_name()['A1'], source_labware_2.wells_by_name()['B6'], new_tip='once') +pipette_1000ul.transfer(50, source_labware_1.wells_by_name()['A2'], source_labware_2.wells_by_name()['B7'], new_tip='never') +``` + +#### + +Using the multi-channel pipette, transfer 3ul of sample from each column in the source plate to +the destination plate in duplicate. Changing tips between each column. Duplicate means that +aspirate the sample from the sample plate column 1 to the destination plate column 1, change tip, +then aspirate from sample plate column 1 to destination plate column 2. Then, transfer the sample +from the sample plate column 2 to the destination plate column 3, change tip, then transfer +the sample from sample plate column 2 to destination plate column 4. Repeat this pattern for +the remainder of the source columns. + +```python +source_columns = source_plate.columns()[:number_of_columns] +destination_columns = destination_plate.columns()[:number_of_columns * 2] # Twice the number for duplicates + +for col_ctr, s in enumerate(source_columns, start=0): + dest_index = 2 * col_ctr + pipette_multi.transfer(3, s, destination_columns[dest_index], new_tip='always') + pipette_multi.transfer(3, s, destination_columns[dest_index + 1], new_tip='always') +``` + +Note that two transfer methods is used to account for duplication. 'for' loop is used since description +says change tip for each column. + +#### + +Using the multi-channel pipette, transfer 3ul of sample from each column in the source plate to +the destination plate in triplicate. Changing tips between each column. +The triplicate means that for first source columns, +aspirate the sample from the source column 1 to the destination plate column 1, change tip, +then aspirate from source column 1 to destination plate column 2, change tip, +then aspirate from source column 1 to destination plate column 3, change tip. +For second source column, +aspirate the sample from the source column 2 to the destination column 4, change tip, +then aspirate the sample from source column 2 to destination column 5, change tip, +then aspirate the sample from source column 2 to destination column 6, change tip. + +Repeat this pattern for the remainder of the source columns. + +```python +source_columns = source_plate.columns()[:number_of_columns] +destination_columns = destination_plate.columns()[:number_of_columns * 2] # Twice the number for duplicates + +for col_ctr, s in enumerate(source_columns, start=0): + dest_index = 2 * col_ctr + pipette_multi.transfer(3, s, destination_columns[dest_index], new_tip='always') + pipette_multi.transfer(3, s, destination_columns[dest_index + 1], new_tip='always') + pipette_multi.transfer(3, s, destination_columns[dest_index + 2], new_tip='always') +``` + +Note that two transfer methods is used to account for duplication. 'for' loop is used since description +says change tip for each column. diff --git a/opentrons-ai-server/api/storage/docs/deck_layout.md b/opentrons-ai-server/api/storage/docs/deck_layout.md new file mode 100644 index 00000000000..e2532de3712 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/deck_layout.md @@ -0,0 +1,167 @@ +# Deck Layout Rules + +## Overview + +This document collects all of the guidelines around recommended deck slot locations in one place. Previously, this information was scattered in multiple documents, or the logic was built into Opentrons products like Protocol Designer or the OT App. + +## Deck Slot Guidelines - OT-2 + +OT-2 deck slots: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, trash bin. + +### Modules + +- Heater-Shaker + + - Recommended: Slot 1 + - Allowed: Slots 3, 4, 6, 7, 1 + - Not allowed: Slots 2, 5, 8, 9, 11 + +- Magnetic Module + + - Recommended: Slot 1 + - Allowed: Slots 3, 4, 6, 7, 9, 10 + - Not allowed: Slots 2, 5, 8, 11 + +- Magnetic Block: Not compatible with OT-2 + +- Plate Reader Module (Absorbance): Not compatible with OT-2 + +- Temperature Module + + - Recommended: Slot 3 + - Allowed: Slot 1, 4, 6, 7, 9, 10 + - Not allowed: Slots 2, 5, 8, 11 + +- Thermocycler Module + + - Recommended/Allowed: Slots 7, 8, 10, and 11 (All four slots) + - Not allowed: Any other location + - Note: Only one Thermocycler module can be added to the deck. + +- Fixtures - N/A + - The OT-2 waste bin is fixed on the deck map, taking up what would have been Slot 12. + +### Labware + +Note: We should default to placing the shortest labware near the front and left of the OT-2 (Slot 1 then 2 then 3 then 4 then 5, etc.), followed by progressively taller labware towards the back and right. From shortest to tallest the order should be: Well plates, then Reservoirs, then Tube racks, then Tip racks. + +- Well plates + + - Recommended: Slots 1, 2, or 3 + - If needed: Slots 4, 5, or 6 + - Allowed: Any slot + +- Reservoirs + + - Recommended: Slots 4, 5, or 6 + - If available: Slots 1, 2, or 3 + - If needed: Slots 7, 8, or 9 + - Allowed: Any slot + +- Tube racks + + - Recommended: Slots 7, 8, or 9 + - If available: Slots 1, 4, 2, 5, 3, or 6 (Slots on the far left side are preferable to ones in the middle or left since they’re easier to access.) + - Allowed: Any slot + +- Tip racks + - Recommended: Slots 11, 10, 9, 8, 7 (Start towards the back right and move left then to the front) + - If available: Slots 6, 5, 4, 3, 2, 1 + - Allowed: Any slot + +## Deck Slot Guidelines - Flex + +Flex deck layout: D1, D2, D3, C1, C2, C3, B1, B2, B3, A1, A2, A3 (Trash bin) + +### Modules + +- Heater-Shaker + + - Recommended: Slot D1 + - Allowed: Slots A1, B1, C1, D1, A3, B3, C3, or D3 + - Not allowed: Slots A2, B2, C2, or D2 + +- Magnetic Module: Not compatible with Flex + +- Magnetic Block + + - Recommended: Slot D2 + - Allowed: Slots A1, B1, C1, D1, A2, B2, C2, D2, A3, B3, C3, or D3 + - Not allowed: On staging area slots + +- Plate Reader Module (Absorbance) + + - Recommended: D3 + - Allowed: Slots A3, B3, C3, or D + - Not allowed: Slots A1, B1, C1, D1, A2, B2, C2, or D2 + +- Temperature Module + + - Recommended: D1 + - Allowed: A1, B1, C1, D1, A3, B3, C3, or D3 + - Not allowed: A2, B2, C2, or D2 + +- Thermocycler Module + - Recommended/Allowed: A1 + B1 (Both slots) + - Not allowed: Any other location + +### Fixtures + +- Staging area slots + + - Allowed: A3, B3, C3, or D3 + - Not allowed: A1, B1, C1, D1, A2, B2, C2, or D2 + - Notes: When a staging area slot is added, a new deck slot is created in the far right column in slots A4, B4, C4, or D4. The gripper can access these deck slots, but pipetting in column 4 is not possible. + - Because the staging area slots can only be accessed by the gripper, tube racks should not be placed in these locations since the gripper cannot safely move this type of labware. All other labware types are compatible with staging area slots. + - The trash bin cannot occupy the same deck slot as a staging area slot. + +- Trash bin + + - Recommended: A3 + - Allowed: A1, B1, C1, D1, A3, B3, C3, or D3 + - Not allowed: A2, B2, C2, or D2 + - Note: The trash bin cannot occupy the same deck slot as a staging area slot. + +- Waste chute + - Recommended/Allowed: D3 (The waste chute fixture diverts waste to an off-deck receptacle and is designed to be placed exclusively in slot D3). + - Not allowed: Any other location + +### Labware + +Note: We should default to placing the shortest labware near the front and left of the Flex (Slot D1 then D2 then D3 then C1 then C2, etc.), followed by progressively taller labware towards the back and right. From shortest to tallest the order should be: Well plates, then Reservoirs, then Tube racks, then Tip racks. + +- Well plates + + - Recommended: Slots D1, D2, or D3 + - If needed: Slots C1, C2, C3, B1, B2, B3, A1, A2, or A3 + - Allowed: Any slot + +- Reservoirs + + - Recommended: Slots C1, C2, or C3 + - If available: Slots D1, D2, or D3 + - If needed: Slots B1, B2, B3, A1, A2, or A3 + - Allowed: Any slot + +- Tube racks + + - Recommended: Slots B1, B2, B3 + - If available: Slots D1, C1, D2, C2, D3, or C3 (Slots on the far left side are preferable to ones in the middle or left since they’re easier to access.) + - Allowed: Any slot + +- Tip racks + + - Recommended: Slots A3 (if trash bin is not present), A2, A1, B3, B2, B1 (Start towards the back right and move left then to the front) + - If available: Slots C3, C2, C1, D3, D2, or D1 + - Allowed: Any slot + +- Adapters + - Opentrons Flex 96 Tip Rack Adapter + - Recommended: A2, B2, C2, D2 (to avoid modules in columns 1 and 3) + - Allowed: Any slot + +## Reference documents + +1. Confluence: https://opentrons.atlassian.net/wiki/spaces/RPDO/pages/3859939364 +2. Flex product manual (See the section on Deck Fixtures in Chapter 3): https://insights.opentrons.com/hubfs/Products/Flex/Opentrons Flex manual REV2.pdf +3. OT-2 product manual: https://insights.opentrons.com/hubfs/Products/OT-2/OT-2R User Manual.pdf diff --git a/opentrons-ai-server/api/storage/docs/full-examples.md b/opentrons-ai-server/api/storage/docs/full-examples.md new file mode 100644 index 00000000000..959dd4cf4d9 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/full-examples.md @@ -0,0 +1,1263 @@ +## Below seven examples are shown in pairs: a description () and a corresponding protocol (). + +[1] Example + +Application: Reagent Filling - One source to Multiple destinations +Robot: OT-2 +API: 2.15 + +Modules: + +- No modules + +Labware: + +- Source labware: Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt in slot 4 +- Destination Labware: NEST 2 mL 96-Well Deep Well Plate, V Bottom in slot 9 +- Tiprack: Opentrons OT-2 96 Tip Rack 300 uL in slot 1 + +Pipette mount: + +- P300 Multi Channel is mounted on the right + +Commands: + +1. Transfer 50 uL of sample from each column of the source plate into the corresponding columns of the destination deep well plate. + Change tips for each transfer. + + + +# metadata +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'chatGPT', + 'description': 'Transfer reagent', + 'apiLevel': '2.15' +} + +def run(protocol): # labware +tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 1) +source = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 4) +destination = protocol.load_labware('nest_96_wellplate_2ml_deep', 9) + + # pipettes + p300m = protocol.load_instrument('p300_multi_gen2', mount="right", tip_racks=[tiprack]) + + # parameters + TRANSFER_VOL = 50 + SRC_COLS = source.columns() + DEST_COLS = destination.columns() + + # commands + p300m.transfer(TRANSFER_VOL, SRC_COLS, DEST_COLS, new_tip='always') + + + +[2] Example + +Metadata: + +- Application: Reagent transfer +- Robot: OT-2 +- API: 2.15 + +Labware: + +- Source Labware 1: NEST 1 Well Reservoir 195 mL is positioned in slot 7 +- Destination Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot 1 +- Destination Labware 2: Corning 96 Well Plate 360 uL Flat is positioned in slot 2 +- Tiprack 1: Opentrons 96 Tip Rack 300 uL is positioned in slot 10 + +Pipette Mount: + +- Left Mount: P300 Multi-Channel GEN2 + +Commands: + +1. Using P300 Multi-channel pipette on the left mount, transfer 50 uL of reagent from first column in the source labware 1 + to all the columns in destination labware 1. Keep the same set of tips for this entire set of transfers within this step. +2. Using P300 Multi-channel pipette on the left mount, transfer 100 uL from first column in the source labware 1 + to each column in destination labware 2. Keep the same set of tips for this entire set of transfers within this step. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +"protocolName": "Reagent Transfer protocol", +"author": "Opentrons Generative AI", +"description": "Transfer reagents from multile source labware to multiple destination labware", +"apiLevel": "2.15" +} + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("nest_1_reservoir_195ml", location=7) +destination_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location=1) +destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location=2) + + tiprack300 = protocol.load_labware("opentrons_96_tiprack_300ul", location=10) + + # pipettes + p300m = protocol.load_instrument("p300_multi_gen2", mount="left", tip_racks=[tiprack300]) + + # wells setup + source_wells_1 = source_1.columns()[0] + destination_wells_1 = destination_1.columns() + destination_wells_2 = destination_2.columns() + + # volumes setup + transfer_vol_1 = 50 + transfer_vol_2 = 100 + + p300m.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + p300m.transfer(transfer_vol_2, source_wells_1, destination_wells_2, new_tip="once") + + + +[3] Example + + +- Application: Reagent transfer +- Robot: OT-2 +- API: 2.15 + +Labware: + +- Source Labware: Thermo Scientific Nunc 96 Well Plate 2000 uL in slot 7 +- Destination Labware: Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap in slot 3 +- Tiprack: Opentrons 96 Filter Tip Rack 1000 uL in slot 4 + +Pipette mount: + +- P1000 Single-Channel GEN2 is mounted on the left + +Commands: + +1. Using P1000 Single-Channel GEN2 pipette on left mount, transfer 195.0 uL of reagent + from H10, F12, D7, B1, C8 wells in source labware + to first well in the destination labware. Use a new tip for each transfer. + + + +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'chatGPT', + 'description': 'P1000 Single-Channel GEN2 transfer 195.0 ul', + 'apiLevel': '2.15' +} + +def run(protocol): + + # labware + tiprack = protocol.load_labware('opentrons_96_filtertiprack_1000ul', 4) + source = protocol.load_labware('thermoscientificnunc_96_wellplate_2000ul', 7) + destination = protocol.load_labware('opentrons_24_aluminumblock_nest_0.5ml_screwcap', 3) + + # pipettes + p1000s = protocol.load_instrument('p1000_single_gen2', mount="left", tip_racks=[tiprack]) + + # parameters + TRANSFER_VOL = 195.0 + SRC_WELLS = ['H10', 'F12', 'D7', 'B1', 'C8'] + DEST_WELL = destination.wells()[0] + + # commands + for src in SRC_WELLS: + p1000s.transfer(TRANSFER_VOL, source.wells_by_name()[src], DEST_WELL, new_tip="always") + + + +[4] Example + +Metadata and requirements: + +- Application: Reagent transfer +- Robot: Flex +- API: 2.15 + +Labware: + +- Source Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot C1 +- Source Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot C2 +- Destination Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot D1 +- Destination Labware 2: Corning 96 Well Plate 360 uL Flat is positioned in slot D2 +- Tiprack 1: Opentrons Flex 96 Filter Tip Rack 200 uL is positioned in slot B2 +- Tiprack 2: Opentrons Flex 96 Filter Tip Rack 50 uL is positioned in slot A2 + +Pipette Mount: + +- Flex 1-Channel 1000 uL Pipette is mounted on the left side +- Flex 1-Channel 50 uL Pipette is mounted on the right side + +Commands: + +1. Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 50 uL from wells A1, A2 in source labware 1 + to B6, B7 in source labware 2. Reuse the same tip for each transfer. +2. Using Flex 1-Channel 50 uL Pipette on right mount, transfer 15 uL from wells C4, C6 in source labware 2 + to A3, A4 in source labware 1. Reuse the same tip for each transfer. +3. Using Flex 1-Channel 50 uL Pipette on right mount, transfer 10 uL from wells B6, B7 in source labware 2 + to A1, B1 in destination labware 1. Use a new tip each time for each transfer. +4. Using Flex 1-Channel 50 uL Pipette on right mount, transfer 10 uL from wells C4, C6 in source labware 2 + to A1, B1 in destination labware 2. Use a new tip each time for each transfer. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +'protocolName': 'Reagent Transfer', +'author': 'Opentrons Generative AI', +} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +# protocol run function + +def run(protocol: protocol_api.ProtocolContext): + + # labware + source_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C1') + source_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C2') + destination_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D1') + destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D2') + tiprack200 = protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location='B2') + tiprack50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[tiprack200]) + p50s = protocol.load_instrument("flex_1channel_50", mount="right", tip_racks=[tiprack50]) + + # well setup + source_wells_1 = [source_1[wells] for wells in ['A1', 'A2']] + source_wells_2 = [source_2[wells] for wells in ['C4', 'C6']] + source_wells_3 = [source_2[wells] for wells in ['B6', 'B7']] + source_wells_4 = [source_2[wells] for wells in ['C4', 'C6']] + destination_wells_1 = [source_2[wells] for wells in ['B6', 'B7']] + destination_wells_2 = [source_1[wells] for wells in ['A3', 'A4']] + destination_wells_3 = [destination_1[wells] for wells in ['A1', 'B1']] + destination_wells_4 = [destination_2[wells] for wells in ['A1', 'B1']] + + # volume setup + transfer_vol_1 = 50 + transfer_vol_2 = 15 + transfer_vol_3 = 10 + transfer_vol_4 = 10 + + # commands + p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + p50s.transfer(transfer_vol_2, source_wells_2, destination_wells_2, new_tip="once") + p50s.transfer(transfer_vol_3, source_wells_3, destination_wells_3, new_tip="always") + p50s.transfer(transfer_vol_4, source_wells_4, destination_wells_4, new_tip="always") + + + +[5] Example + +Metadata and requirements: + +- Application: Reagent transfer +- Robot: Flex +- API: 2.15 + +Labware: + +- Source Labware: Opentrons 96 Flat Bottom Adapter with NEST 96 Well Plate 200 uL Flat on slot D1 +- Destination Labware: Opentrons 96 Flat Bottom Adapter with NEST 96 Well Plate 200 uL Flat on slot C2 +- Tiprack: Opentrons Flex 96 Filter Tip Rack 50 uL on slot C1 + +Pipette Mount: + +- Flex 8-Channel 50 uL Pipette is mounted on the right side + +Commands: + +1. Using Flex 8-Channel 50 uL Pipette on right mount, transfer 8 uL of reagent from 4, 3, 6, 1, 11 columns in source labware + to 5, 9, 1, 10, 2 columns in the destination labware. Using the same tip for all transfers. + + + +from opentrons import protocol_api + +metadata = { +'protocolName': 'Reagent Transfer', +'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +def run(protocol: protocol_api.ProtocolContext): + + # labware + tiprack = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'C1') + source = protocol.load_labware('opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'D1') + destination = protocol.load_labware('opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'C2') + + # pipettes + p50m = protocol.load_instrument('flex_8channel_50', mount="right", tip_racks=[tiprack]) + + # parameters + transfer_vol = 8 + src_cols = [3, 2, 5, 0, 10] + dest_cols = [4, 8, 0, 9, 1] + + # commands + p50m.pick_up_tip() + for src_col, dest_col in zip(src_cols, dest_cols): + p50m.transfer(transfer_vol, source.columns()[src_col], destination.columns()[dest_col], new_tip="never") + p50m.drop_tip() + + + +[6] Example + +Metadata: + +- Author: Bob +- Protocol Name: PCR + +Requirements: + +- `requirements = {"robotType": "OT-2", "apiLevel": "2.15"}` + +Modules: + +- The thermocycler module is located in slot 7. +- The sample temperature module is positioned in slot 1. +- The mastermix temperature module is positioned in slot 3. + +Labware: + +- The source sample labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed on the temperature module in slot 1. +- The source mastermix labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed on the temperature module in slot 3. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed on the thermocycler module in slot 7. +- A 20 uL filter tip rack is used in slot 4. + +Pipette Mount: + +- A P20 Multi-Channel Gen2 pipette is mounted on the left side. + +Well Allocation: + +- Sample source wells: the first 64 wells column-wise in the sample source plate. +- Mastermix source wells: the first 64 wells column-wise in the mastermix plate. +- Destination wells: the first 64 wells column-wise in the thermocycler. + +Commands: + +1. Set the total number of samples to 64. +2. Open the thermocycler lid. +3. Set the thermocycler block temperature to 6C. +4. Set the thermocycler lid temperature to 55C. +5. Set the sample temperature module to 4C. +6. Set the mastermix temperature module to 10C. +7. Transfer 7 uL of mastermix from the mastermix source wells to the destination wells. Use the same pipette tip for all transfers. +8. Transfer 5 uL of the sample from the source to the destination. Mix the sample and mastermix for a total volume of 12 uL 9 times. + Blow out to `destination well` after each transfer. Use a new tip for each transfer. +9. Close the thermocycler lid. +10. Execute the thermocycler with the following profile: + - 74C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume +11. Execute the thermocycler with the following profile: + - 60C for 7 seconds, 84C for 19 seconds, 57C for 44 seconds for 13 cycles, block max volume is sample and mastermix volume +12. Execute the thermocycler with the following profile: + - 75C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume +13. Hold the thermocycler block at 4C. +14. Open the thermocycler lid. +15. Deactivate the mastermix temperature module. +16. Deactivate the sample temperature module. + + + +import math +from opentrons import protocol_api + +metadata = { +'protocol_name': 'QIAGEN OneStep RT-PCR Kit PCR Amplification', +'author': 'chatGPT', +'description': 'Amplification using QIAGEN OneStep RT-PCR Kit with 13 cycles', +'apiLevel': '2.15' +} + +def run(protocol: protocol_api.ProtocolContext): # Sample preparation parameters +number_of_samples = 64 +sample_volume_ul = 5 # Volume in microliters +master_mix_volume_ul = 7 # Volume in microliters +mixing_cycles = 9 +total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul +master_mix_temperature_c = 10 # Temperature in Celsius +sample_temperature_c = 4 # Temperature in Celsius + + # Thermocycler parameters + lid_temperature_c = 55 # Lid temperature in Celsius + initial_plate_temperature_c = 6 # Initial plate temperature in Celsius + hold_temperature_c = 4 # Hold temperature in Celsius for infinite hold + + # Modules loading + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 1) + master_mix_temperature_module = protocol.load_module('temperature module gen2', 3) + + # Labware loading + tips_20ul = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + pcr_plate = thermocycler_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + sample_plate = sample_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + # Pipette loading + multi_channel_pipette = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tips_20ul]) + + # Well allocation + number_of_columns = math.ceil(number_of_samples / 8) + sample_source_wells = sample_plate.columns()[:number_of_columns] + sample_destination_wells = pcr_plate.columns()[:number_of_columns] + master_mix_source_wells = master_mix_plate.columns()[:number_of_columns] + master_mix_destination_wells = pcr_plate.columns()[:number_of_columns] + + # Command 2: Open lid + thermocycler_module.open_lid() + + # Command 3: Set initial plate temperature + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + + # Command 4: Set lid temperature + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Command 5: Set sample temperature + sample_temperature_module.set_temperature(sample_temperature_c) + + # Command 6: Set master mix temperature + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Command 7: Transfer master mix to destination wells + multi_channel_pipette.transfer( + master_mix_volume_ul, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' + ) + + # Command 8: Transfer samples to destination wells and mix + multi_channel_pipette.transfer( + sample_volume_ul, + sample_source_wells, + sample_destination_wells, + new_tip='always', + mix_after=(mixing_cycles, total_mix_volume_ul), + blow_out=True, + blowout_location='destination well' + ) + + # Command 9: Close lid + thermocycler_module.close_lid() + + # Commands 10-12: PCR cycling + thermocycler_module.execute_profile( + steps=[{'temperature': 74, 'hold_time_seconds': 65}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[ + {'temperature': temp, 'hold_time_seconds': duration} + for temp, duration in zip([60, 84, 57], [7, 19, 44]) + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + # Command 13: Set final hold temperature + thermocycler_module.set_block_temperature(hold_temperature_c) + + # Command 14: Open lid post-PCR + thermocycler_module.open_lid() + + # Commands 15 & 16: Deactivate temperature modules + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + + + +[7] Example + +Metadata: + +- Author: Bob +- ProtocolName: PCR + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +Modules: + +- Thermocycler module GEN 2 is present on slot A1+B1 +- Temperature module GEN 2 is placed on slot D1 +- Mastermix temperature module GEN 2 is placed on slot D3 + +Adapter: + +- Opentrons 96 Well Aluminum Block adapter is placed on the temperature module GEN 2 +- Opentrons 96 Well Aluminum Block adapter is placed on the mastermix temperature module GEN 2 + +Labware: + +- Source labware: `Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt` placed on the temperature module +- Source mastermix labware: `Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt` placed on temperature module +- Destination labware: `Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt`, placed on C3 +- Load three `opentrons_flex_96_filtertiprack_50ul` tip racks on `opentrons_flex_96_tiprack_adapter` adapters in slots A2, B2, and C2 + +Pipette mount: + +- `Flex 96-Channel 1000 uL Pipette` is mounted on the left side + +Well Allocation: + +- source well: A1 well of source labware +- destination well: A1 well of destination labware +- mastermix well: A1 well of mastermix labware + +Commands: + +1. Set the thermocycler block temperature to 22 C. +2. Open the thermocycler lid. +3. Set the thermocycler lid temperature to 95 C. +4. Set the temperature module to 37 C. +5. Set master mix temperature module to 10 C. +6. Transfer 20 uL of liquid from 5 mm below the top surface of mastermix well to 2 mm above the bottom of destination well. Use the same tip for each transfer. +7. Transfer 20 ul of liquid from 3 mm above the source well bottom to destination well 7 mm beneath the top surface. Flow rate is at half the default. + Mix the sample and mastermix of 40 ul total volume 5 times. Remove the tip slowly out of the well at 5 mm/s speed. Use the same tip for each transfer. +8. Move the destination labware to the thermocycler using gripper. +9. Close the thermocycler lid. +10. Execute the thermocycle using the following profile: + +- 74 degree C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume + +11. Execute the thermocycle using the following profile: + +- 60 degree C for 7 seconds, 84 degree C for 19 seconds, 57 degree C for 44 seconds for 25 cycles, block max volume is sample and mastermix volume + +12. Execute the thermocycle using the following profile: + +- 75 degree C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume + +13. Hold thermocycler block at 4 C. +14. Open thermocycler lid. +15. Move the destination labware from thermocycler back to its original slot C3 using gripper. +16. Pause the protocol and tell the user to pick up the destination plate, seal it and refrigerate it at 4 C. +17. Deactivate the temperature modules. +18. Deactivate the mastermix temperature modules. + + + +from opentrons import protocol_api +metadata = { + 'protocol_name': 'PCR Amplification protocol', + 'author': 'Opentrons Generative AI', + 'description': 'PCR Amplification protocol with 25 cycles', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +def run(protocol: protocol_api.ProtocolContext): # Sample parameters +sample_volume_ul = 20 +master_mix_volume_ul = 20 +mix_cycles = 5 +total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul +return_slot = 'C3' + + master_mix_temperature_c = 10 + sample_temperature_c = 37 + step1_cycles = 1 + step2_cycles = 25 + step3_cycles = 1 + + # Thermocycler parameters + lid_temperature_c = 95 + initial_block_temperature_c = 22 + final_hold_temperature_c = 4 + + # Modules + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 'D1') + master_mix_temperature_module = protocol.load_module('temperature module gen2', 'D3') + + # Adapters + sample_adapter = sample_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + master_mix_adapter = master_mix_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + + # Labware + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + destination_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'C3') + tips_50ul = [ + protocol.load_labware( + 'opentrons_flex_96_filtertiprack_50ul', + slot, + adapter="opentrons_flex_96_tiprack_adapter" + ) + for slot in ['A2', 'B2', 'C2'] + ] + + # Pipette + pipette_96channel = protocol.load_instrument('flex_96channel_1000', 'left', tip_racks=tips_50ul) + + # Well allocation + sample_source_wells = sample_plate['A1'] + destination_wells = destination_plate['A1'] + master_mix_source_well = master_mix_plate['A1'] + + # Set thermocycler block and lid temperature + thermocycler_module.set_block_temperature(initial_block_temperature_c) + thermocycler_module.open_lid() + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Temperature module setup + sample_temperature_module.set_temperature(sample_temperature_c) + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Master mix transfer + pipette_96channel.transfer( + master_mix_volume_ul, + master_mix_source_well.top(-5), + destination_wells.bottom(2), + new_tip='once' + ) + + # Sample transfer + pipette_96channel.pick_up_tip() + pipette_96channel.aspirate(sample_volume_ul, sample_source_wells.bottom(3), rate=0.5) + pipette_96channel.dispense(sample_volume_ul, destination_wells.top(-7), rate=0.5) + pipette_96channel.mix(mix_cycles, total_mix_volume_ul) + pipette_96channel.move_to(destination_wells.top(), speed=5) + pipette_96channel.drop_tip() + + # Moving the plate to the thermocycler + protocol.move_labware(destination_plate, thermocycler_module, use_gripper=True) + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=step1_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=step2_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=step3_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.set_block_temperature(final_hold_temperature_c) + thermocycler_module.open_lid() + + # Moving the plate back to its original location + protocol.move_labware(destination_plate, return_slot, use_gripper=True) + + # Optional: pause for manual intervention + protocol.pause("Pick up the destination plate, seal it, and refrigerate at 4C.") + + # Deactivate temperature modules at the end of the protocol + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + + + +## Common rules for transfer + +================= COMMON RULES for TRANSFER ================= + +- when we allocate wells for source and destination, we need to pay attention to pipette type. + For example, see the command below + +``` +Sample source wells: the first 64 well column-wise in the sample source plate. +``` + +- pipette (eg., Flex 8-Channel 1000 uL Pipette), given the number of wells + we need to estimate the columns and use method `labware.columns()` to access the columns. + For example, + +```python +number_of_columns = math.ceil([number_of_samples] / 8) +source_wells = labware.columns()[:number_of_columns] +``` + +- pipette (eg., Flex 1-Channel 1000 uL Pipette), + we use `labware.wells()`. For example, + +```python +source_wells = labware.wells()[:[number_of_samples]] +``` + +- If prompt says row-wise, we need to use `rows()` +- If prompt does not mention column-wise, we use `wells()` since it is default. +- If the number of samples are not specified, then use all wells. + +```python +source_wells = sample_plate.wells() +``` + +- If `blowout_location` location is mentioned explicitly, then incorporate to transfer method. +- Avoid using `for` with transfer + the following is incorrect: + +```python +source_columns = [source_labware.columns_by_name()[str(index)] for index in [3, 2, 5, 1, 10]] +destination_columns = [source_labware.columns_by_name()[str(index)] for index in [4, 8, 1, 9, 2]] + +# Transfer reagents +for src, dest in zip(source_columns, destination_columns): + pipette.transfer(14.0, src, dest, new_tip='always') +``` + +The correct: + +```python +source_columns = [source_labware.columns_by_name()[str(index)] for index in [3, 2, 5, 1, 10]] +destination_columns = [source_labware.columns_by_name()[str(index)] for index in [4, 8, 1, 9, 2]] + +# Transfer reagents +pipette.transfer(14.0, source_columns, destination_columns, new_tip='always') +``` + +## Other Examples with More Explanation + +### First Example + +**Description** + +Write a protocol using the Opentrons Python Protocol API v2 for the OT-2 robot based on the following details: + +**Labware:** + +- **Source Labware:** `Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap` placed in slot 3. +- **Destination Labware:** `Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt` placed in slot 9. +- **Tip Rack:** `Opentrons OT-2 96 Tip Rack 20 µL` placed in slot 2. + +**Pipette Mount:** + +- A **P20 Single Channel** pipette is mounted on the **right** side. + +**Commands:** + +1. Using the P20 Single Channel pipette, **transfer 1 µL** of reagent from the **first tube (A1)** of the source rack to **each well** in the destination plate. +2. **Use the same tip** for each transfer (do not change tips between transfers). + +**Protocol** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'Your Name', + 'description': 'Transfer 1 µL of reagent from source tube A1 to all wells in the destination plate using the same tip.', +} +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + tiprack = protocol.load_labware('opentrons_96_tiprack_20ul', 2) + source_rack = protocol.load_labware('opentrons_24_tuberack_nest_1.5ml_snapcap', 3) + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 9) + + # Load pipette + p20 = protocol.load_instrument('p20_single_gen2', mount='right', tip_racks=[tiprack]) + + # Define source and destination wells + source_well = source_rack.wells_by_name()['A1'] + destination_wells = dest_plate.wells() + + # Perform transfer + p20.transfer( + 1, # Volume in µL + source_well, # Source well + destination_wells, # List of destination wells + new_tip='once' # Use the same tip for all transfers + ) +``` + +**Explanation:** + +- **Labware Setup:** + - **Tip Rack:** Loaded into slot 2 for the P20 pipette to pick up tips. + - **Source Rack:** Contains the reagent in tube A1, placed in slot 3. + - **Destination Plate:** The 96-well plate where the reagent will be dispensed, placed in slot 9. +- **Pipette Initialization:** + - The P20 Single Channel pipette is initialized on the right mount with the specified tip rack. +- **Transfer Details:** + - **Volume:** 1 µL is transferred from the source to each destination well. + - **Source Well:** Tube A1 in the source rack. + - **Destination Wells:** All wells in the 96-well plate. + - **Tip Usage:** `new_tip='once'` ensures the same tip is used throughout the entire transfer process. +- **Method Used:** + - The `transfer` method is used without any explicit loops because it inherently handles the iteration over the list of destination wells. + +--- + +### Second Example + +**Description** + +Using a **Flex 1-Channel 1000 µL Pipette** mounted on the **left** side, transfer **150 µL** from wells **A1** and **A2** in **source labware 1** to wells **B6** and **B7** in **source labware 2**. **Use the same tip** for each transfer. + +**Protocol** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multiple Well Transfer', + 'author': 'Your Name', + 'description': 'Transfer 150 µL from specific source wells to specific destination wells using the same tip.', +} +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + source_labware_1 = protocol.load_labware('source_labware_1_definition', slot=1) + source_labware_2 = protocol.load_labware('source_labware_2_definition', slot=2) + tiprack = protocol.load_labware('opentrons_96_tiprack_1000ul', slot=3) + + # Load pipette + p1000 = protocol.load_instrument('flex_1channel_1000', mount='left', tip_racks=[tiprack]) + + # Define source and destination wells + source_wells = [source_labware_1.wells_by_name()[well] for well in ['A1', 'A2']] + destination_wells = [source_labware_2.wells_by_name()[well] for well in ['B6', 'B7']] + + # Perform transfer + p1000.transfer( + 150, # Volume in µL + source_wells, # List of source wells + destination_wells, # List of destination wells + new_tip='once' # Use the same tip for all transfers + ) +``` + +**Explanation:** + +- **Labware Setup:** + - **Source Labware 1:** Contains the initial samples in wells A1 and A2, loaded into slot 1. + - **Source Labware 2:** Will receive the transferred samples in wells B6 and B7, loaded into slot 2. + - **Tip Rack:** Loaded into slot 3 for the pipette to pick up tips. +- **Pipette Initialization:** + - The Flex 1-Channel 1000 µL pipette is initialized on the left mount with the specified tip rack. +- **Defining Wells:** + - **Source Wells:** A list containing wells A1 and A2 from source labware 1. + - **Destination Wells:** A list containing wells B6 and B7 from source labware 2. +- **Transfer Details:** + - **Volume:** 150 µL is transferred from each source well to the corresponding destination well. + - **Tip Usage:** `new_tip='once'` ensures the same tip is used for all transfers. +- **Method Used:** + - The `transfer` method is used with lists of source and destination wells. This method pairs each source well with its corresponding destination well, eliminating the need for explicit loops. + +**Note:** The use of a single `transfer` function with lists allows for multiple transfers in a streamlined manner. + +--- + +By using the `transfer` method effectively, we can simplify the protocol code and make it more readable. The method automatically handles the pairing and iteration over wells, so explicit loops are unnecessary. Additionally, specifying `new_tip='once'` optimizes the protocol by reducing tip usage when appropriate. + +## Best Practices for Optimizing the transfer Method in Pipetting Automation + +1. **Optimizing `transfer` Usage Without Loops** + + - **Issue**: Using the `transfer` method inside a `for` loop is unnecessary because `transfer` can handle lists implicitly. + - **Solution**: Remove the `for` loop and use the `transfer` method directly with lists for efficient code. + + **Example:** + + - _Inefficient Code (Excerpt-1):_ + + ```python + for source_well, destination_well in zip(source_wells, destination_wells): + pipette.pick_up_tip() + pipette.transfer(TRANSFER_VOL, source_well, destination_well, new_tip='never') + pipette.drop_tip() + ``` + + - _Optimized Code (Excerpt-2):_ + ```python + pipette.transfer(TRANSFER_VOL, source_wells, destination_wells, new_tip='always') + ``` + +2. **Correct Use of `new_tip='once'`** + + - **Note**: When instructed to "Use the same tip for all transfers" or similar, avoid using `new_tip='once'` inside a `for` loop, as this is incorrect. + - **Solution**: Use the `transfer` method without a `for` loop to ensure the same tip is used throughout. + + **Incorrect Usage:** + + ```python + for src, dest in zip(source_columns, destination_columns): + pipette.transfer(transfer_vol, src, dest, new_tip='once') + ``` + + **Correct Usage:** + + ```python + pipette.transfer(transfer_vol, source_columns, destination_columns, new_tip='once') + ``` + +3. **Importing Necessary Libraries** + + - **Reminder**: Always import necessary libraries, such as `math`, when using functions like `ceil` or other mathematical methods. + + ```python + import math + ``` + +4. **Using `columns` Method with Multi-Channel Pipettes** + + - **Guideline**: For multi-channel pipettes (e.g., P20 Multi-Channel Gen2), utilize the `columns` method to access labware columns effectively. + + **Example:** + + ```python + source_columns = source_plate.columns() + destination_columns = destination_plate.columns() + ``` + +--- + +### Another Example + +```python +"from opentrons import protocol_api + +# metadata +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.19"} + + +def run(protocol: protocol_api.ProtocolContext): + # labware + source_labware_1 = [protocol.load_labware("corning_96_wellplate_360ul_flat", location=slot) for slot in ['D1', 'D2', 'D3']] + source_labware_2 = protocol.load_labware("nest_1_reservoir_195ml", location='A1') + destination = [protocol.load_labware("corning_96_wellplate_360ul_flat", location=slot) for slot in ['C1', 'C2', 'C3']] + tiprack200 = [protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location=slot) for slot in ['B1', 'B2', 'B3']] + tiprack50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p50s = protocol.load_instrument("flex_1channel_50", mount="right", tip_racks=[tiprack50]) + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[*tiprack200]) + # load trash bin + trash = protocol.load_trash_bin('A3') + + # volumes setup + transfer_vol_1 = 20 + transfer_vol_2 = 100 + + # wells setup + source_wells_1 = [src.wells() for src in source_labware_1] + source_wells_2 = source_labware_2.wells_by_name()['A1'] + destination_wells_1 = [dest.wells() for dest in destination] + + # commands + p50s.transfer(transfer_vol_1, source_wells_2, destination_wells_1, new_tip="once") + p1000s.transfer(transfer_vol_2, source_wells_1, destination_wells_1, new_tip="always") +``` + +#### another example + +```python +from opentrons import protocol_api + +# metadata +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.19"} + + +def run(protocol: protocol_api.ProtocolContext): + + # labware + source_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C1') + source_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C2') + destination_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D1') + destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D2') + tiprack200 = protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location='B2') + tiprack50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[tiprack200]) + p50s = protocol.load_instrument("flex_1channel_50", mount="right", tip_racks=[tiprack50]) + # load trash bin + trash = protocol.load_trash_bin('A3') + # volume setup + transfer_vol_1 = 50 + transfer_vol_2 = 15 + transfer_vol_3 = 10 + transfer_vol_4 = 10 + + # well setup + source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] + source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] + source_wells_3 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + source_wells_4 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] + destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + destination_wells_2 = [source_1.wells_by_name()[wells] for wells in ['A3', 'A4']] + destination_wells_3 = [destination_1.wells_by_name()[wells] for wells in ['A1', 'B1']] + destination_wells_4 = [destination_2.wells_by_name()[wells] for wells in ['A1', 'B1']] + + # commands + p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + p50s.transfer(transfer_vol_2, source_wells_2, destination_wells_2, new_tip="once") + p50s.transfer(transfer_vol_3, source_wells_3, destination_wells_3, new_tip="always") + p50s.transfer(transfer_vol_4, source_wells_4, destination_wells_4, new_tip="always") +``` + +### pcr example + +```python +import math +from opentrons import protocol_api + +metadata = { + 'protocol_name': 'QIAGEN OneStep RT-PCR Kit PCR Amplification', + 'author': 'chatGPT', + 'description': 'Amplification using QIAGEN OneStep RT-PCR Kit with 13 cycles', + 'apiLevel': '2.16' +} + + +def run(protocol: protocol_api.ProtocolContext): + # Sample preparation parameters + number_of_samples = 64 + sample_volume_ul = 5 # Volume in microliters + master_mix_volume_ul = 7 # Volume in microliters + mixing_cycles = 9 + total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul + master_mix_temperature_c = 10 # Temperature in Celsius + sample_temperature_c = 4 # Temperature in Celsius + + # Thermocycler parameters + lid_temperature_c = 55 # Lid temperature in Celsius + initial_plate_temperature_c = 6 # Initial plate temperature in Celsius + hold_temperature_c = 4 # Hold temperature in Celsius for infinite hold + + # Modules loading + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 1) + master_mix_temperature_module = protocol.load_module('temperature module gen2', 3) + + # Labware loading + tips_20ul = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + pcr_plate = thermocycler_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + sample_plate = sample_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + # Pipette loading + multi_channel_pipette = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tips_20ul]) + + # Well allocation + number_of_columns = math.ceil(number_of_samples / 8) + sample_source_wells = sample_plate.columns()[:number_of_columns] + sample_destination_wells = pcr_plate.columns()[:number_of_columns] + master_mix_source_wells = master_mix_plate.columns()[:number_of_columns] + master_mix_destination_wells = pcr_plate.columns()[:number_of_columns] + + # Command 2: Open lid + thermocycler_module.open_lid() + + # Command 3: Set initial plate temperature + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + + # Command 4: Set lid temperature + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Command 5: Set sample temperature + sample_temperature_module.set_temperature(sample_temperature_c) + + # Command 6: Set master mix temperature + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Command 7: Transfer master mix to destination wells + multi_channel_pipette.transfer( + master_mix_volume_ul, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' + ) + + # Command 8: Transfer samples to destination wells and mix + multi_channel_pipette.transfer( + sample_volume_ul, + sample_source_wells, + sample_destination_wells, + new_tip='always', + mix_after=(mixing_cycles, total_mix_volume_ul), + blow_out=True, + blowout_location='destination well' + ) + + # Command 9: Close lid + thermocycler_module.close_lid() + + # Commands 10-12: PCR cycling + thermocycler_module.execute_profile( + steps=[{'temperature': 74, 'hold_time_seconds': 65}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[ + {'temperature': temp, 'hold_time_seconds': duration} + for temp, duration in zip([60, 84, 57], [7, 19, 44]) + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + # Command 13: Set final hold temperature + thermocycler_module.set_block_temperature(hold_temperature_c) + + # Command 14: Open lid post-PCR + thermocycler_module.open_lid() + + # Commands 15 & 16: Deactivate temperature modules + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + +``` + +### Liquid transfer with Heater Shaker module + + +Write a protocol using the Opentrons Python Protocol API v2.19 for Opentrons Flex robot for the following description: + +Metadata: + +- Author: User +- ProtocolName: Liquid Transfer with Heater Shaker +- Description: Transfer liquids between reservoir, PCR plate, and heater shaker module's plate. + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Labware: + +- Trash: Load a trash bin in slot A3 +- Heater Shaker Module: Load in slot D1 with a Corning 96 Well Plate 360 µL Flat +- Reservoir: NEST 1 Well Reservoir 195 mL in slot C1 +- PCR Plate: NEST 96 Well Plate 200 µL Flat in slot D2 +- Tipracks: + - Opentrons Flex 96 Tiprack 200 µL in slot A2 + - Opentrons Flex 96 Tiprack 1000 µL in slot B2 + - Opentrons Flex 96 Tiprack 50 µL in slot C2 + All tipracks should use the Opentrons Flex 96 Tiprack Adapter + +Pipette: + +- Flex 96-Channel 1000 µL pipette mounted on the left + +Steps: + +1. Open the Heater Shaker Module's labware latch +2. Pause the protocol and prompt the user to load the Corning 96 well plate +3. Close the Heater Shaker Module's labware latch +4. Using the 96-channel pipette with 200 µL tips, transfer 70 µL from the reservoir (A1) to the Heater Shaker plate (A1) +5. Using the 96-channel pipette with 50 µL tips, transfer 10 µL from the PCR plate (A1) to the Heater Shaker plate (A1) +6. Shake the plate on the Heater Shaker Module at 2000 rpm for 1 minute +7. Deactivate the shaker + +Notes: + +- Use new tips for each transfer +- The protocol includes comments and pauses for user interaction + + + + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Liquid Transfer with Heater Shaker', + 'author': 'User', + 'description': 'Transfer liquids between reservoir, PCR plate, and heater shaker module\'s plate.' +} + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.19' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load trash bin + trash = protocol.load_trash_bin('A3') + + # Load modules + heater_shaker = protocol.load_module('heaterShakerModuleV1', 'D1') + + # Load labware + heater_shaker_plate = heater_shaker.load_labware('corning_96_wellplate_360ul_flat') + reservoir = protocol.load_labware('nest_1_reservoir_195ml', 'C1') + pcr_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D2') + + # Load tipracks with adapters + tiprack_200 = protocol.load_labware('opentrons_flex_96_tiprack_200ul', 'A2', adapter='opentrons_flex_96_tiprack_adapter') + tiprack_1000 = protocol.load_labware('opentrons_flex_96_tiprack_1000ul', 'B2', adapter='opentrons_flex_96_tiprack_adapter') + tiprack_50 = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'C2', adapter='opentrons_flex_96_tiprack_adapter') + + # Load pipette + pipette = protocol.load_instrument('flex_96channel_1000', 'left', tip_racks=[tiprack_200, tiprack_1000, tiprack_50]) + + # Protocol steps + # 1. Open the Heater Shaker Module's labware latch + heater_shaker.open_labware_latch() + + # 2. Pause for user to load the plate + protocol.pause('Please load the Corning 96 well plate onto the Heater Shaker Module and resume the protocol.') + + # 3. Close the Heater Shaker Module's labware latch + heater_shaker.close_labware_latch() + + # 4. Transfer 70 µL from reservoir to Heater Shaker plate + pipette.transfer(70, reservoir['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 5. Transfer 10 µL from PCR plate to Heater Shaker plate + pipette.transfer(10, pcr_plate['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 6. Shake the plate + heater_shaker.set_and_wait_for_shake_speed(rpm=2000) + protocol.delay(minutes=1) + + # 7. Deactivate the shaker + heater_shaker.deactivate_shaker() +``` + + diff --git a/opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md b/opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md new file mode 100644 index 00000000000..0617543b916 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md @@ -0,0 +1,382 @@ +# Preventing "Out of Tips" and Index Errors in Opentrons Protocols + +"Out of tips" and index errors are common issues that can halt the execution of protocols on Opentrons robots. These errors occur when the protocol attempts to use more pipette tips than are available or when it tries to access wells beyond the labware's dimensions. Proper planning and understanding of tip consumption and labware indexing are essential to prevent such errors and ensure smooth laboratory operations. + +## Common Scenarios Leading to Errors + +### 1. Single Pipette Exceeds Tip Rack Capacity + +**Scenario:** +A single-channel pipette performs repeated operations using tips from a single tip rack without accounting for tip depletion. + +**Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Single Tip Rack Exhaustion Example', + 'author': 'Opentrons', + 'description': 'A protocol that runs out of tips after exceeding tip rack capacity', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tip_rack]) + + # Perform operations + for _ in range(100): + pipette.pick_up_tip() + pipette.aspirate(100, plate['A1']) + pipette.dispense(100, plate['B1']) + pipette.drop_tip() +``` + +**Issue Explanation:** +The protocol attempts 100 tip pickups using a single tip rack containing only 96 tips. After 96 successful pickups, the pipette runs out of tips, resulting in an error on the 97th attempt. + +--- + +### 2. Multi-Channel Pipette with Insufficient Tip Racks + +**Scenario:** +A multi-channel pipette uses tips from a single tip rack but requires more tips than are available due to the number of channels used per operation. + +**Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Tip Rack Exhaustion Example', + 'author': 'Opentrons', + 'description': 'A protocol where a multi-channel pipette runs out of tips', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack]) + + # Perform operations + for i in range(20): + pipette.pick_up_tip() + pipette.aspirate(100, plate.rows()[0][i]) + pipette.dispense(100, plate.rows()[1][i]) + pipette.drop_tip() +``` + +**Issue Explanation:** +A multi-channel pipette uses 8 tips per pick-up. Over 20 iterations, it requires 160 tips (20 iterations × 8 tips). A single 96-tip rack is exhausted after 12 iterations (96 tips / 8 tips per iteration), causing an error during the 13th iteration. Additionally, attempting to access `plate.rows()[0][i]` where `i` exceeds 11 (the maximum index for 12 columns) results in an index error. + +**Solution:** + +- **Load Additional Tip Racks:** Introduce more tip racks to provide enough tips for all operations. +- **Validate Index Ranges:** Ensure that the loop indices do not exceed the labware dimensions. + +**Corrected Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Tip Rack Exhaustion Example - Solved', + 'author': 'Opentrons', + 'description': 'Multi-channel pipette avoids running out of tips and index errors', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack1 = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + tip_rack2 = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack1, tip_rack2]) + + # Perform operations within available columns range + for i in range(12): # Restrict to 12 columns + pipette.pick_up_tip() + pipette.aspirate(100, plate.columns()[i][0]) + pipette.dispense(100, plate.columns()[i][1]) + pipette.drop_tip() +``` + +--- + +### 3. Nested Loops Causing Excessive Tip Usage + +**Scenario:** +Nested loops in the protocol lead to a higher number of tip pickups than anticipated, exhausting the available tips. + +**Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Nested Loops Tip Exhaustion Example', + 'author': 'Opentrons', + 'description': 'A protocol demonstrating tip exhaustion due to nested loops', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tip_rack]) + + # Perform operations + for row in range(8): + for col in range(12): + for _ in range(2): + pipette.pick_up_tip() + pipette.aspirate(100, plate.rows()[row][col]) + pipette.dispense(100, plate.rows()[row][(col + 1) % 12]) + pipette.drop_tip() +``` + +**Issue Explanation:** +The nested loops result in 192 tip pickups (8 rows × 12 columns × 2 repetitions). With only 96 tips available, the protocol runs out of tips halfway through, causing an error. + +**Solution:** +Introduce additional tip racks to provide enough tips for all operations. + +**Corrected Protocol Example:** + +```python +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack1 = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + tip_rack2 = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + + # Load pipette + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tip_rack1, tip_rack2]) + + # Perform operations (same as above) +``` + +--- + +## Calculating Tip Usage in Many-to-Many Transfers + +In protocols involving many-to-many transfers, it's crucial to calculate the number of tips required accurately to avoid "out of tips" errors. + +### Guidelines for Many-to-Many Transfers + +- **Even Divisibility:** Ensure the number of wells in the larger group (source or destination) is evenly divisible by the number of wells in the smaller group. +- **Stretching the Smaller Group:** Conceptually "stretch" the smaller group of wells to match the length of the larger group. Each well in the smaller group may be used multiple times. +- **Tip Requirement:** The number of tips required is always equal to the number of wells in the larger group. +- **Multi-Channel Pipettes:** For multi-channel pipettes, remember that each operation uses multiple tips (e.g., 8 tips for an 8-channel pipette). If using a 96-channel pipette, each operation consumes 96 tips. + +### Example Calculation + +- **Scenario:** Transfer from 24 source wells to 96 destination wells. +- **Process:** + - The 24 source wells are stretched to match the 96 destination wells. + - Each source well is used multiple times to cover all destination wells. + - **Total Transfers:** 96. + - **Tips Required:** + - **Single-Channel Pipette:** 96 tips (one per transfer). + - **Multi-Channel Pipette (8-channel):** 12 transfers (96 wells / 8 channels), using 8 tips per transfer, totaling 96 tips. + +--- + +## Key Points to Avoid Index Errors + +- **Validate Access Ranges:** Always ensure that your loops and operations do not exceed the dimensions of the labware being used. For example, a 96-well plate has 12 columns and 8 rows; accessing an index beyond these ranges will cause an error. +- **Sufficient Resources:** Make sure the number of loaded tip racks can handle the total number of operations required by the protocol. + +**Example Problem:** + +A multi-channel pipette runs out of tips after 12 operations due to using 8 tips per operation, and the code attempts to access non-existent column indices beyond the 12 columns available in a 96-well plate. + +**Incorrect Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Index Error Example', + 'author': 'Opentrons', + 'description': 'A protocol that causes index errors due to invalid column access', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack]) + + # Perform operations + for i in range(20): # Exceeds available columns + pipette.pick_up_tip() + pipette.aspirate(100, plate.columns()[i][0]) + pipette.dispense(100, plate.columns()[i][1]) + pipette.drop_tip() +``` + +**Solution:** + +- **Restrict Loop Indices:** Adjust the loop to stay within the valid column indices (0 to 11 for a 96-well plate). +- **Load Additional Tip Racks:** Ensure enough tips are available for all operations. + +**Corrected Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Index Error Example - Solved', + 'author': 'Opentrons', + 'description': 'A protocol that avoids index errors by validating column indices', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack1 = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + tip_rack2 = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack1, tip_rack2]) + + # Perform operations within available columns range + for i in range(12): # Valid column indices for a 96-well plate + pipette.pick_up_tip() + pipette.aspirate(100, plate.columns()[i][0]) + pipette.dispense(100, plate.columns()[i][1]) + pipette.drop_tip() +``` + +--- + +## Best Practices to Avoid "Out of Tips" and Index Errors + +### 1. Calculate Required Tips in Advance + +- **Estimate Operations:** Calculate the total number of pipetting actions that require new tips, including loops and many-to-many transfers. +- **Consider Multiple Pipettes:** Calculate tip requirements separately for each pipette, accounting for their specific usage patterns. + +### 2. Load Sufficient Tip Racks + +- **Tip Rack Capacity:** Standard 96-tip racks hold 96 tips. Ensure the total number of tips available meets or exceeds your calculated requirement. +- **Add Buffers:** Include extra tip racks to handle unexpected needs or minor calculation errors. + +### 3. Validate Labware Indexing + +- **Check Labware Dimensions:** Before accessing wells or columns in loops, confirm the dimensions of your labware to avoid index errors. +- **Adjust Loop Ranges:** Ensure that loop indices do not exceed the maximum indices of the labware being used. + +### 4. Associate Tip Racks with Pipettes + +- **Specify Tip Racks:** Explicitly associate each pipette with its corresponding tip racks for efficient tip tracking. +- **Multiple Tip Racks:** Use multiple tip racks for pipettes with high tip consumption. + +### 5. Implement Tip Replenishment Strategies + +- **Dynamic Replenishment:** Use commands like `move_labware()` to swap in fresh tip racks during long protocols. +- **Manual Replenishment:** Plan steps within the protocol to allow for manual replacement of tip racks if automatic replenishment isn't feasible. + +### 6. Optimize Tip Usage + +- **Reuse Tips When Appropriate:** If protocol requirements allow, reuse the same tip for multiple transfers to reduce tip consumption. +- **Minimize Tip Pickups:** Combine transfers when possible to limit the number of tip pickups. + +### 7. Handle Special Cases Carefully + +- **Multi-Channel Pipettes:** Remember that multi-channel pipettes consume multiple tips per pickup. Adjust tip rack quantities accordingly. +- **Nested Loops:** Be cautious with nested loops, as they can exponentially increase tip usage. Validate tip requirements before execution. +- **Many-to-Many Transfers:** Apply the specific calculations for many-to-many transfers to determine accurate tip usage. + +### 8. Implement Error Handling and Testing + +- **Catch Errors Early:** Incorporate checks to detect potential "out of tips" or index errors before they cause runtime issues. +- **Conduct Dry Runs:** Perform simulations or test runs to ensure all logical paths are covered and tip requirements are met. + +--- + +## Example when using serial dilution protocol + +Below protocol produces `OutofTips` error, since it excauts all tips by using `plate.rows()`: +(One needs to be careful) + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'OT-2', + 'apiLevel': '2.16' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 1) + reservoir = protocol.load_labware('nest_12_reservoir_15ml', 2) + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 3) + + # Load pipette + p300 = protocol.load_instrument('p300_single_gen2', 'left', tip_racks=[tiprack]) + + # Distribute diluent + p300.transfer(100, reservoir['A1'], plate.wells()) + + # Perform serial dilution + for row in plate.rows(): + # Transfer and mix solution from reservoir to first well + p300.transfer(100, reservoir['A2'], row[0], mix_after=(3, 50), new_tip='always') + + # Serial dilution within the row + p300.transfer(100, row[:11], row[1:], mix_after=(3, 50), new_tip='always') + +``` + +Correct way is follows: + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'OT-2', + 'apiLevel': '2.16' +} + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", 2) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", 3) + left_pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` diff --git a/opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md b/opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md new file mode 100644 index 00000000000..40b1ce10739 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md @@ -0,0 +1,1065 @@ +# PCR protocol examples + +## 1. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the OT-2 robot according to the following description: + +Requirements: + +- requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +Modules: + +- Temperature module GEN2 for samples in slot 1 +- Temperature module GEN2 for mastermix in slot 3 + +Labware: + +- Sample source: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on sample temperature module +- Mastermix source: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on mastermix temperature module +- Destination: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt in slot 7 +- Tips: Opentrons 96 Filter Tip Rack 20 uL in slot 4 + +Pipette: + +- Left mount: P20 Multi-Channel Gen2 + +Sample Setup: + +- Number of samples: 64 (8 columns) +- Well allocation: First 64 wells (column-wise) in all plates + +Temperature Settings: + +- Sample temperature: 4C +- Mastermix temperature: 10C + +Protocol Steps: + +1. Set temperature modules to specified temperatures +2. Transfer 7 uL mastermix to destination wells (reuse tip) +3. Transfer 5 uL sample to destination wells, mix 9 times with 12 uL total volume + (use new tip for each transfer, blow out to destination well) +4. Deactivate both temperature modules + + + + +```python +from opentrons import protocol_api + +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Module loading + temp_mod_sample = protocol.load_module('temperature module gen2', 1) + temp_mod_mastermix = protocol.load_module('temperature module gen2', 3) + + # Load thermal adapters + sample_adapter = temp_mod_sample.load_adapter("opentrons_96_well_aluminum_block") + mastermix_adapter = temp_mod_mastermix.load_adapter("opentrons_96_well_aluminum_block") + + # Load labware onto the adapters + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + mastermix_plate = mastermix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 7) + tiprack = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + + # Pipette loading + p20_single = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tiprack]) + + # Well allocation + num_samples = 64 + num_cols = num_samples // 8 + sample_wells = sample_plate.columns()[:num_cols] + mastermix_wells = mastermix_plate.columns()[:num_cols] + dest_wells = dest_plate.columns()[:num_cols] + + # Volume and temperature parameters + sample_temp = 4 # Celsius + mastermix_temp = 10 # Celsius + mastermix_vol = 7 # uL + sample_vol = 5 # uL + mix_cycles = 9 + total_mix_vol = sample_vol + mastermix_vol # 12 uL total + + # Execute commands in order specified in description + # Command 1: Set sample temperature + temp_mod_sample.set_temperature(sample_temp) + + # Command 2: Set mastermix temperature + temp_mod_mastermix.set_temperature(mastermix_temp) + + # Command 3: Transfer mastermix + p20_single.transfer( + mastermix_vol, + mastermix_wells, + dest_wells, + new_tip='once' + ) + + # Command 4: Transfer samples and mix + p20_single.transfer( + sample_vol, + sample_wells, + dest_wells, + new_tip='always', + mix_after=(mix_cycles, total_mix_vol), + blow_out=True, + blowout_location='destination well' + ) + + # Command 5 and 6: Deactivate temperature modules + temp_mod_mastermix.deactivate() + temp_mod_sample.deactivate() +``` + + + +## 2. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot according to the following description: + +Requirements: + +- requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +Module: + +- Temperature module GEN2 on slot 1 + +Labware: + +- Sample Source: Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap on temperature module +- Mastermix Source: Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap on slot 3 +- Destination: Opentrons 96 Well Plate 200 uL PCR Full Skirt on slot 7 +- Tips: Opentrons 96 Filter Tip Rack 20 uL on slot 4 + +Pipette: + +- Right mount: P20 Single Channel GEN2 + +Sample Setup: + +- Number of samples: 24 +- Well allocation: First 24 wells (column-wise) in all plates + +Protocol Steps: + +1. Set temperature module to 4°C +2. Transfer 8 uL mastermix to destination wells (reuse same tip) +3. Transfer 7 uL sample to destination wells, mix 4 times with 15 uL total volume + (use new tip for each transfer, blow out to destination well) +4. Deactivate temperature module + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'OT-2', + 'apiLevel': '2.19' +} + +def run(protocol: protocol_api.ProtocolContext): + + # Protocol parameters + num_samples = 24 + sample_vol = 7 # uL + mastermix_vol = 8 # uL + mix_cycles = 4 + total_vol = sample_vol + mastermix_vol # 15 uL total + temp_celsius = 4 + + # Load temperature module + temp_module = protocol.load_module('temperature module gen2', 1) + + # Load labware + source_samples = temp_module.load_labware('opentrons_24_aluminumblock_nest_1.5ml_snapcap') + source_mastermix = protocol.load_labware('opentrons_24_tuberack_nest_1.5ml_snapcap', 3) + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 7) + tiprack = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + + # Load pipette + p20_single = protocol.load_instrument('p20_single_gen2', 'right', tip_racks=[tiprack]) + + # Define wells + sample_wells = source_samples.wells()[:num_samples] + mastermix_wells = source_mastermix.wells()[:num_samples] + dest_wells = dest_plate.wells()[:num_samples] + + # Set temperature + temp_module.set_temperature(temp_celsius) + + # Transfer mastermix + p20_single.transfer( + mastermix_vol, + mastermix_wells, + dest_wells, + new_tip='once' + ) + + # Transfer samples and mix + p20_single.transfer( + sample_vol, + sample_wells, + dest_wells, + mix_after=(mix_cycles, total_vol), + blow_out=True, + blowout_location='destination well', + new_tip='always' + ) + + # Deactivate temperature module + temp_module.deactivate() +``` + + + +## 3. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot according to the following description: + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Temperature Modules: + +- Sample module: Temperature Module GEN2 on slot D1 +- Mastermix module: Temperature Module GEN2 on slot D3 + +Module Adapters: + +- Opentrons 96 Well Aluminum Block on both temperature modules + +Labware: + +1. Source Plates: + - Sample plate: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on sample module + - Mastermix plate: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on mastermix module +2. Destination: + - Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on slot A1 +3. Tips: + - Opentrons Flex 96 Filter Tip Rack 1000 uL on slots C1 and C2 + +Pipettes: + +- Left mount: Flex 8-Channel 1000 uL +- Right mount: Flex 8-Channel 50 uL + +Sample Setup: + +- Total samples: 72 +- Well usage: First 72 wells (column-wise) in all plates + +Protocol Steps: + +1. Temperature Setup: + a. Set sample module to 37°C + b. Set mastermix module to 4°C + +2. Mastermix Transfer: + + - Transfer 15 uL mastermix from source to destination wells + - Use the same tip for all transfers + +3. Sample Transfer: + + - Transfer 10 uL sample from source to destination wells + - Mix 9 times with 25 uL total volume after each transfer + - Use a new tip for each transfer + - Blow out to destination well after each transfer + +4. Module Shutdown: + a. Deactivate mastermix temperature module + b. Deactivate sample temperature module + + + + +```python +from opentrons import protocol_api + +requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Trash + trash = protocol.load_trash_bin("A3") + + # Protocol parameters + num_samples = 72 + mastermix_vol = 15 # uL + sample_vol = 10 # uL + mix_cycles = 9 + total_vol = mastermix_vol + sample_vol + + # Temperature settings + temp_mastermix = 4 # C + temp_sample = 37 # C + + # Load temperature modules + temp_mod_sample = protocol.load_module('temperature module gen2', 'D1') + temp_mod_mastermix = protocol.load_module('temperature module gen2', 'D3') + + # Load module adapters + block_sample = temp_mod_sample.load_adapter('opentrons_96_well_aluminum_block') + block_mastermix = temp_mod_mastermix.load_adapter('opentrons_96_well_aluminum_block') + + # Load labware + plate_sample = block_sample.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + plate_mastermix = block_mastermix.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + plate_dest = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'A1') + + # Load tip racks + tips_1000 = protocol.load_labware('opentrons_flex_96_filtertiprack_1000ul', 'C1') + tips_50 = protocol.load_labware('opentrons_flex_96_filtertiprack_1000ul', 'C2') + + # Load pipettes + p50_multi = protocol.load_instrument('flex_8channel_50', 'right', tip_racks=[tips_50]) + p1000_multi = protocol.load_instrument('flex_8channel_1000', 'left', tip_racks=[tips_1000]) + + # Set up well arrays + source_mastermix = plate_mastermix.columns()[:num_samples//8] + source_sample = plate_sample.columns()[:num_samples//8] + wells_dest = plate_dest.columns()[:num_samples//8] + + # Step 1: Set temperatures + temp_mod_sample.set_temperature(temp_sample) + temp_mod_mastermix.set_temperature(temp_mastermix) + + # Step 2: Transfer mastermix + p50_multi.transfer( + mastermix_vol, + source_mastermix, + wells_dest, + new_tip='once' + ) + + # Step 3: Transfer samples + p1000_multi.transfer( + sample_vol, + source_sample, + wells_dest, + new_tip='always', + mix_after=(mix_cycles, total_vol), + blow_out=True, + blowout_location='destination well' + ) + + # Step 4: Deactivate modules + temp_mod_mastermix.deactivate() + temp_mod_sample.deactivate() +``` + + + +## 4. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.19"}` + +Modules: + +- No modules + +Labware: + +- The source sample labware, a Biorad 96 well plate 200ul full skirt, is placed in slot D1. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot B1. +- A 50 uL filter tip rack is used in slot A1. +- A 50 uL filter tip rack is used in slot A2. + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- The number of sample columns is 3. + +Commands: + +- Using the single-chaneel pipette, for each row in the csv, aspirate 7ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- Using the multi-channel pipette, transfer 5ul of sample from the sample plate to the destination plate, column for column, up to the number of samples specified. Grab new tips for each column. + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.19' +} + +def run(protocol: protocol_api.ProtocolContext): + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + num_col = 3 + + # Load labware + mmx_tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'D1') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'B1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A1') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + + # Load pipettes + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # Trash + trash = protocol.load_trash_bin("A3") + + # 1. Transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, source=mmx_tuberack[source_tube], dest=dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # 2. Transfer sample + for s, d in zip(dna_plate.rows()[0][:num_col], dest_plate.rows()[0][:num_col]): + multi_pip.pick_up_tip() + multi_pip.transfer(5, source=s, dest=d, new_tip='never') + multi_pip.drop_tip() + +``` + + + +## 5. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.19"}` + +Modules: + +- Thermocycler module + +Labware: + +- The source sample labware, an biorad_96_wellplate_200ul_pcr, is placed in slot D1. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an opentrons_96_aluminumblock_nest_wellplate_100ul, is placed in thermocycler. +- A 50uL tip rack for the single channel pipette is in A2 +- A 50uL tip rack for the single channel pipette is in B2 + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- The number of columns is 3. + +Commands: + +- Open the thermocycler lid. +- Set the thermocycler block temperature to 6C. +- Set the thermocycler lid temperature to 55C. +- For each row in the csv, aspirate 7ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- Using the multi-channel pipette, transfer 5ul of sample from the sample plate to the destination plate, column for column, up to the number of samples specified. +- Close the thermocycler lid. +- Execute the thermocycler with the following profile: + - 74C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume +- Execute the thermocycler with the following profile for 13 cycles: + - 60C for 7 seconds, + - 84C for 19 seconds, + - 57C for 44 seconds, + block max volume is sample and mastermix volume. +- Execute the thermocycler with the following profile: + - 75C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume +- Hold the thermocycler block at 4C. +- Open the thermocycler lid. + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.19' +} + + +def run(protocol: protocol_api.ProtocolContext): + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + num_col = 3 + sample_volume_ul = 5 # in microliters + master_mix_volume_ul = 7 # in microliters + total_mix_volume_ul = sample_volume_ul + sample_volume_ul + + # Thermocycler parameters + lid_temperature_c = 55 # in Celsius + initial_plate_temperature_c = 6 # in Celsius + hold_temperature_c = 4 # in Celsius + + # Module + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + + # Labware setup + mmx_tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'D1') + dest_plate = thermocycler_module.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + + # Tip racks + tiprack_for_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_for_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipettes + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_for_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_for_multi]) + + # Trash + trash = protocol.load_trash_bin("A3") + + # Open lid, set temperatures + thermocycler_module.open_lid() + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(master_mix_volume_ul, mmx_tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # Transfer samples + for s, d in zip(dna_plate.rows()[0][:num_col], dest_plate.rows()[0][:num_col]): + multi_pip.pick_up_tip() + multi_pip.transfer(sample_volume_ul, s, d, new_tip='never') + multi_pip.drop_tip() + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + # Thermo set temperature, open lid + thermocycler_module.set_block_temperature(hold_temperature_c) + thermocycler_module.open_lid() + +``` + + + +## 6. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.15"}` + +Modules: + +- The thermocycler module + +Labware: + +- The source sample labware, an biorad_96_wellplate_200ul_pcr, is placed in slot D3. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an opentrons_96_aluminumblock_nest_wellplate_100ul, is placed in thermocycler. +- A 50uL tip rack for the single channel pipette is in A2 +- A 50uL tip rack for the single channel pipette is in B2 + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- The number of sample columns is 3. +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. + The mastermix volume for each destination well is also provided in each row of the csv, and is in the 3rd column. + +Commands: + +- Open the thermocycler lid. +- Set the thermocycler block temperature to 6C. +- Set the thermocycler lid temperature to 55C. +- For each row in the csv, there is the source tube, destination well, and transfer volume. Transfer the volume specified in the csv (3rd column of csv) of mastermix from the source tube in the tube rack (first column of csv) + to the destination well (second column of csv) in the destination plate. Use one tip per mastermix tube. +- Using the multi-channel pipette, aspirate 5ul of sample from the sample plate to the destination plate, column for column, up to the number of columns specified. +- Close the thermocycler lid. +- Execute the thermocycler with the following profile: + - 74C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume +- Execute the thermocycler with the following profile: + - 60C for 7 seconds, + - 84C for 19 seconds, + - 57C for 44 seconds for 13 cycles, + block max volume is sample and mastermix volume +- Execute the thermocycler with the following profile: + - 75C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume +- Hold the thermocycler block at 4C. +- Open the thermocycler lid. + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.15' +} + + +def run(protocol: protocol_api.ProtocolContext): + + csv_samp = """ + Primer Tube,Destination well,Transfer volume + A1,A1,4 + B1,B1,4 + C1,C1,4 + D1,D1,4 + A2,E1,4 + B2,F1,4 + C2,G1,5 + D2,H1,5 + A3,A2,5 + B3,B2,5 + C3,C2,5 + D3,D2,5 + A4,E2,7 + B4,F2,7 + C4,G2,7 + D4,H2,7 + A5,A3,7 + B5,B3,3 + C5,C3,3 + D5,D3,4 + A6,E3,2 + B6,F3,8 + C6,G3,5 + D6,H3,20 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + num_col = 3 + sample_temperature_c = 4 # Temperature in Celsius + sample_volume_ul = 5 # Volume in microliters + total_mix_volume_ul = 10 + + # Thermocycler parameters + lid_temperature_c = 55 # Celsius + initial_plate_temperature_c = 6 # in Celsius + hold_temperature_c = 4 # in Celsius + + # Module + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + + # Labware setup + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'D3') + dest_plate = thermocycler_module.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + + # Tip racks + tiprack_for_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_for_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', "B2") + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_for_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_for_multi]) + + thermocycler_module.open_lid() + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + thermocycler_module.set_lid_temperature(lid_temperature_c) + for source_tube, dest_well, transfer_vol in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(int(transfer_vol), tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + for s, d in zip(dna_plate.rows()[0][:num_col], dest_plate.rows()[0][:num_col]): + multi_pip.pick_up_tip() + multi_pip.transfer(sample_volume_ul, s, d, new_tip='never') + multi_pip.drop_tip() + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.set_block_temperature(hold_temperature_c) + thermocycler_module.open_lid() + +``` + + + +## 7. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.15"}` + +Modules: + +- No modules + +Labware: + +- The source sample labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot D3. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot B1. +- Tiprack 1: opentrons_flex_96_tiprack_50ul is in A2 +- Tiprack 2: opentrons_flex_96_tiprack_50ul is in B2 + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- 3 columns of samples. + +Commands: + +- For each row in the csv, transfer 7ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- For each column in the source plate, we are going to the destination plate in duplicate, changing tips between each column. For example, using the multi-channel pipette, transfer 3ul of sample from the sample plate column 1 to the destination plate plate column 1, change tip, then aspirate from sample plate column 1 to destination plate column 2. Then, transfer 3ul of sample from the sample plate column 2 to the destination plate plate column 3, change tip, then transfer from sample plate column 2 to destination plate column 4. Repeat this pattern for the remainder of the source columns + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.15' +} + +def run(protocol: protocol_api.ProtocolContext): + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to nested list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + NUM_COL = 3 + STRIDE = 2 + + # Load labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D3') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'B1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # Transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # transfer in duplicate + col_ctr = 0 + for s in dna_plate.rows()[0][:NUM_COL]: + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+1], new_tip='never') + multi_pip.drop_tip() + + col_ctr += STRIDE + +``` + + + +## 8. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.15"}` + +Modules: + +- No modules + +Labware: + +- The source sample labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot D3. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot D1. +- Tiprack in A2: opentrons_flex_96_tiprack_50ul +- Tiprack in B2: opentrons_flex_96_tiprack_50ul + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- 3 columns of samples. + +Commands: + +- For each row in the csv, aspirate 7 ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- For each column in the source plate, we are going to the destination plate in triplicate, changing tips between each column. For example, using the multi-channel pipette, + transfer 3 ul of sample from the sample plate column 1 to the destination plate plate column 1, change tip, then aspirate from sample plate column 1 to destination plate column 2, change tip, then aspirate form sample plate column 1 to destination plate column 3. Repeat this pattern for the remainder of the source columns + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.15' +} + + +def run(protocol: protocol_api.ProtocolContext): + + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + NUM_COL = 3 + STRIDE = 3 + + # Load labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D3') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # Transfer in triplicate + col_ctr = 0 + for s in dna_plate.rows()[0][:NUM_COL]: + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+1], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+2], new_tip='never') + multi_pip.drop_tip() + + col_ctr += STRIDE +``` + + diff --git a/opentrons-ai-server/api/storage/docs/serial_dilution_samples.md b/opentrons-ai-server/api/storage/docs/serial_dilution_samples.md new file mode 100644 index 00000000000..ad9a5ee24be --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/serial_dilution_samples.md @@ -0,0 +1,842 @@ +# Serial dilution examples + +## 1. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – Flex 1-channel +- Description: serial dilution + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +Labware: + +- Tiprack: `Opentrons Flex 96 Tip Rack 200 µL` in slot D1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot D2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot D3 +- Trash bin in slot A3 + +Pipette mount: + +- Flex 1-channel 1000 µL pipette is mounted on the left + +Commands: + +1. Use the left-mounted Flex 1-channel 1000 µL pipette to distribute 100 µL of diluent from well A1 of the reservoir to all wells of the plate. +2. For each of the 8 rows in the plate: + a. Transfer 100 µL of solution from well A2 of the reservoir to the first well of the row, mixing 3 times with 50 µL after transfer. + b. Perform a serial dilution by transferring 100 µL from each well to the next well in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + + + + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "Serial Dilution Tutorial – Flex 1-channel", + "description": """serial dilution""", + "author": "New API User" + } + +requirements = { + "robotType": "Flex", + "apiLevel": "2.16" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D1") + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2") + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "D3") + trash = protocol.load_trash_bin("A3") + left_pipette = protocol.load_instrument("flex_1channel_1000", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 2. Serial dilution example + + + +serial&heater-shaker +serial&heater-shaker +100% +10 +A3 + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – Flex 8-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot D1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot D2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot D3 +- Trash bin in slot A3 + +Pipette mount: + +- Flex 8-channel 1000 µL pipette is mounted on the right + +Commands: + +1. Use the right-mounted Flex 8-channel 1000 µL pipette to distribute 100 µL of diluent from well A1 of the reservoir to the first row of the plate. +2. Transfer 100 µL of solution from well A2 of the reservoir to the first column of the first row in the plate, mixing 3 times with 50 µL after transfer. +3. Perform a serial dilution by transferring 100 µL from each column to the next column in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – Flex 8-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot D1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot D2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot D3 +- Trash bin in slot A3 + +Pipette mount: + +- Flex 8-channel 1000 µL pipette is mounted on the right + +Commands: + +1. Use the right-mounted Flex 8-channel 1000 µL pipette to distribute 100 µL of diluent from well A1 of the reservoir to the first row of the plate. +2. Transfer 100 µL of solution from well A2 of the reservoir to the first column of the first row in the plate, mixing 3 times with 50 µL after transfer. +3. Perform a serial dilution by transferring 100 µL from each column to the next column in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + Turn on screen reader support + To enable screen reader support, press ⌘+Option+Z To learn about keyboard shortcuts, press ⌘slash + + + + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "Serial Dilution Tutorial – Flex 8-channel", + "description": """This protocol is the outcome of following the + Python Protocol API Tutorial located at + https://docs.opentrons.com/v2/tutorial.html. It takes a + solution and progressively dilutes it by transferring it + stepwise across a plate.""", + "author": "New API User" + } + +requirements = { + "robotType": "Flex", + "apiLevel": "2.16" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", "D1") + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2") + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "D3") + trash = protocol.load_trash_bin("A3") + left_pipette = protocol.load_instrument("flex_8channel_1000", "right", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.rows()[0]) + + # no loop, 8-channel pipette + + # save the destination row to a variable + row = plate.rows()[0] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 3. Serial dilution example + + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – OT-2 single-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. +- apiLevel: 2.16 + +Requirements: + +- robotType: OT-2 +- apiLevel: 2.16 + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot 1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot 2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot 3 + +Pipette mount: + +- P300 Single-Channel GEN2 pipette is mounted on the left + +Commands: + +1. Use the left-mounted P300 Single-Channel GEN2 pipette to distribute 100 µL of diluent from well A1 of the reservoir to all wells of the plate. +2. For each of the 8 rows in the plate: + a. Transfer 100 µL of solution from well A2 of the reservoir to the first well of the row, mixing 3 times with 50 µL after transfer. + b. Perform a serial dilution by transferring 100 µL from each well to the next well in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + + + + + +```python +from opentrons import protocol_api + +metadata = { + "apiLevel": "2.16", + "protocolName": "Serial Dilution Tutorial – OT-2 single-channel", + "description": """This protocol is the outcome of following the + Python Protocol API Tutorial located at + https://docs.opentrons.com/v2/tutorial.html. It takes a + solution and progressively dilutes it by transferring it + stepwise across a plate.""", + "author": "New API User" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", 2) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", 3) + left_pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 4. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – OT-2 8-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. +- apiLevel: 2.16 + +Requirements: + +- robotType: OT-2 +- apiLevel: 2.16 + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot 1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot 2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot 3 + +Pipette mount: + +- P300 8-Channel GEN2 pipette is mounted on the right + +Commands: + +1. Use the right-mounted P300 8-Channel GEN2 pipette to distribute 100 µL of diluent from well A1 of the reservoir to the first row of the plate. +2. Transfer 100 µL of solution from well A2 of the reservoir to the first column of the plate (row A), mixing 3 times with 50 µL after transfer. +3. Perform a serial dilution by transferring 100 µL from each column to the next column in the row (from left to right), for a total of 11 transfers across the plate. Mix 3 times with 50 µL after each transfer. + + + + +```python +from opentrons import protocol_api + +metadata = { + "apiLevel": "2.16", + "protocolName": "Serial Dilution Tutorial – OT-2 8-channel", + "description": """This protocol is the outcome of following the + Python Protocol API Tutorial located at + https://docs.opentrons.com/v2/tutorial.html. It takes a + solution and progressively dilutes it by transferring it + stepwise across a plate.""", + "author": "New API User" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", 2) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", 3) + left_pipette = protocol.load_instrument("p300_multi_gen2", "right", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.rows()[0]) + + # no loop, 8-channel pipette + + # save the destination row to a variable + row = plate.rows()[0] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 5. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot for the following description: + +Metadata: + +- Author: John C. Lynch +- ProtocolName: Serial Dilution for Eskil +- Description: Execute serial dilution protocol +- Source: Custom Protocol Request +- API Level: 2.19 + +Requirements: + +- requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +Constants: + +- Plate type: Opentrons 96 Aluminum Block NEST Well Plate 100 µL +- Dilution factor: 1.5 +- Number of dilutions: 10 +- Total mixing volume: 150 uL + +Modules: + +- Temperature module loaded in slot 4 + +Labware: + +- NEST 12 Well Reservoir 15 mL in slot 1 +- Dilution plate (Opentrons 96 Aluminum Block NEST Well Plate 100 uL) loaded on temperature module +- Two Opentrons 96 Tip Racks 300 uL in slots 2 and 3 + +Pipette Mount: + +- P300 Multi-Channel GEN2 pipette mounted on the left side + +Calculations: + +- Transfer volume = Total mixing volume / Dilution factor +- Diluent volume = Total mixing volume - Transfer volume + +Commands: + +1. Using the P300 Multi-Channel pipette, transfer diluent from the first well of the reservoir to wells 2-10 of the first row of the dilution plate: + + - Volume: calculated diluent volume + - Use a 10 uL air gap + - Use new tips for each transfer + +2. Perform serial dilutions across the first row of the dilution plate: + + - For each pair of adjacent wells (from well 1 to well 9, transferring to wells 2 to 10): + - Transfer the calculated transfer volume + - Use a 10 uL air gap + - After each transfer, mix 5 times with (Total mixing volume - 5 uL) + - Use new tips for each transfer + +3. Add blank to the last well: + - Transfer calculated diluent volume from the first reservoir well to the last well of the first row in the dilution plate + - Use a 10 uL air gap + - Use a new tip + + + + +```python +metadata = { + 'protocolName': 'Serial Dilution for Eskil', + 'author': 'John C. Lynch', + 'source': 'Custom Protocol Request', + 'apiLevel': '2.19' +} + +def run(protocol): + + # Constants + PLATE_TYPE = 'opentrons_96_aluminumblock_nest_wellplate_100ul' + DILUTION_FACTOR = 1.5 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Load temperature module and labware + temp_module = protocol.load_module('temperature module gen2', '4') + reservoir = protocol.load_labware('nest_12_reservoir_15ml', '1') + dilution_plate = temp_module.load_labware(PLATE_TYPE) + + # Load tipracks + tipracks = [ + protocol.load_labware('opentrons_96_tiprack_300ul', slot) + for slot in ['2', '3'] + ] + + # Load pipette + pipette = protocol.load_instrument( + 'p300_multi_gen2', + mount='left', + tip_racks=tipracks + ) + + # 1. Distribute diluent + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][1:NUM_DILUTIONS], + air_gap=10, + new_tip='always' + ) + + # 2. Perform serial dilutions + sources = dilution_plate.rows()[0][:NUM_DILUTIONS-1] + dests = dilution_plate.rows()[0][1:NUM_DILUTIONS] + + pipette.transfer( + transfer_volume, + sources, + dests, + air_gap=10, + mix_after=(5, TOTAL_MIXING_VOLUME-5), + new_tip='always' + ) + + # 3. Add blank + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][-1], + air_gap=10, + new_tip='always' + ) +``` + + + +## 6. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: Opentrons +- ProtocolName: Customizable Serial Dilution +- Source: Protocol Library + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Inside the run function: + +Constants: + +- Dilution factor: 3 +- Number of dilutions: 10 +- Total mixing volume: 150.0 µL +- Air gap volume: 10 µL + +Calculations: + +- Transfer volume = Total mixing volume / Dilution factor +- Diluent volume = Total mixing volume - Transfer volume + +Labware: + +- NEST 12 Well Reservoir 15 mL in slot D2 +- NEST 96 Well Plate 200 µL Flat in slot D3 +- Two Opentrons Flex 96 Filter Tip Racks 1000 µL in slots C1 and D1 +- Trash bin in slot A3 + +Pipette Mount: + +- Flex 1-channel 1000 µL pipette mounted on the right side + +Liquid Definitions: + +- Diluent liquid: Green color (#33FF33), loaded in reservoir at 80% max volume +- Sample liquid: Red color (#FF0000), loaded in first column of plate at 150 µL per well + +Commands: + +1. Distribute diluent to dilution plate: + + - Pick up one tip + - Transfer calculated diluent volume from reservoir to all wells in columns 2-11 + - Use 10 µL air gap for each transfer + - Reuse the same tip for all transfers + - Drop tip after completion + +2. Perform serial dilutions: + + - Pick up one tip + - For each well in columns 1-10 (source) to columns 2-11 (destination): + - Transfer calculated transfer volume + - Use 10 µL air gap + - After each transfer, mix 5 times with half the total mixing volume + - Reuse the same tip for all transfers + - Drop tip after completion + +3. Add blank to last column: + - Pick up one tip + - Transfer calculated diluent volume from reservoir to all wells in column 12 + - Use 10 µL air gap + - Reuse the same tip for all transfers + - Drop tip after completion + + + + +```python +metadata = { + 'protocolName': 'Customizable Serial Dilution', + 'author': 'Opentrons ', + 'source': 'Protocol Library' +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.19" +} + +def run(protocol): + + # Constants + DILUTION_FACTOR = 3 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150.0 + AIR_GAP_VOLUME = 10 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Labware setup + trough = protocol.load_labware('nest_12_reservoir_15ml', 'D2') + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D3') + tip_name = "opentrons_flex_96_filtertiprack_1000ul" + tipracks = [ + protocol.load_labware(tip_name, slot) + for slot in ["C1", "D1"] + ] + + # Pipette setup + pipette = protocol.load_instrument('flex_1channel_1000', 'right', tipracks) + + # Waste setup + trash = protocol.load_trash_bin("A3") + + # Reagent setup + diluent = trough.wells()[0] + source = plate.columns()[0] + + # Define and load liquids + diluent_liquid = protocol.define_liquid( + name="Dilutent", + description="Diluent liquid is filled in the reservoir", + display_color="#33FF33" + ) + sample_liquid = protocol.define_liquid( + name="Sample", + description="Non-diluted samples are loaded in the 1st column", + display_color="#FF0000" + ) + + diluent.load_liquid(liquid=diluent_liquid, volume=0.8 * diluent.max_volume) + for well in source: + well.load_liquid(liquid=sample_liquid, volume=TOTAL_MIXING_VOLUME) + + # Set up dilution destinations + dilution_destination_sets = plate.columns()[1:NUM_DILUTIONS+1] + dilution_source_sets = plate.columns()[:NUM_DILUTIONS] + blank_set = plate.columns()[NUM_DILUTIONS+1] + + # 1. Distribute diluent + all_diluent_destinations = [well for wells in dilution_destination_sets for well in wells] + pipette.pick_up_tip() + for dest in all_diluent_destinations: + pipette.transfer( + diluent_volume, + diluent, + dest, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + # 2. Perform serial dilutions + pipette.pick_up_tip() + for source_set, dest_set in zip(dilution_source_sets, dilution_destination_sets): + for s, d in zip(source_set, dest_set): + pipette.transfer( + transfer_volume, + s, + d, + air_gap=AIR_GAP_VOLUME, + mix_after=(5, TOTAL_MIXING_VOLUME/2), + new_tip='never' + ) + pipette.drop_tip() + + # 3. Add blank + pipette.pick_up_tip() + for blank_well in blank_set: + pipette.transfer( + diluent_volume, + diluent, + blank_well, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() +``` + + + +## 7. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: Opentrons +- ProtocolName: Customizable Serial Dilution +- Source: Protocol Library + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Inside the run function: + +Constants: + +- Dilution factor: 3 +- Number of dilutions: 10 +- Total mixing volume: 150.0 µL +- Air gap volume: 10 µL + +Calculations: + +- Transfer volume = Total mixing volume / Dilution factor +- Diluent volume = Total mixing volume - Transfer volume + +Labware: + +- NEST 12 Well Reservoir 15 mL in slot D2 +- NEST 96 Well Plate 200 µL Flat in slot D3 +- Two Opentrons Flex 96 Filter Tip Racks 1000 µL in slots C1 and D1 +- Trash bin in slot A3 + +Pipette Mount: + +- Flex 8-channel 1000 µL pipette mounted on the right side + +Liquid Definitions: + +- Diluent liquid: Green color (#33FF33), loaded in reservoir at 80% max volume +- Sample liquid: Red color (#FF0000), loaded in first column of plate at 150 µL per well + +Commands: + +1. Distribute diluent to dilution plate: + + - Pick up one tip with 8-channel pipette + - Transfer calculated diluent volume from reservoir to wells 2-11 in row A + - Use 10 µL air gap for each transfer + - Reuse the same tip for all transfers + - Drop tip after completion + +2. Perform serial dilutions along row A: + + - Pick up one tip with 8-channel pipette + - For each well pair from A1 to A10 (source) to A2 to A11 (destination): + - Transfer calculated transfer volume + - Use 10 µL air gap + - After each transfer, mix 5 times with half the total mixing volume + - Reuse the same tip for all transfers + - Drop tip after completion + +3. Add blank to well A12: + - Pick up one tip with 8-channel pipette + - Transfer calculated diluent volume from reservoir to well A12 + - Use 10 µL air gap + - Reuse the same tip + - Drop tip after completion + + + + +```python +metadata = { + 'protocolName': 'Customizable Serial Dilution', + 'author': 'Opentrons ', + 'source': 'Protocol Library' +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.19" +} + +def run(protocol): + + # Constants + DILUTION_FACTOR = 3 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150.0 + AIR_GAP_VOLUME = 10 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Labware setup + trough = protocol.load_labware('nest_12_reservoir_15ml', 'D2') + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D3') + tip_name = "opentrons_flex_96_filtertiprack_1000ul" + tipracks = [ + protocol.load_labware(tip_name, slot) + for slot in ["C1", "D1"] + ] + + # Pipette setup + pipette = protocol.load_instrument('flex_8channel_1000', 'right', tipracks) + + # Waste setup + trash = protocol.load_trash_bin("A3") + + # Reagent setup + diluent = trough.wells()[0] + source = plate.columns()[0] + + # Define and load liquids + diluent_liquid = protocol.define_liquid( + name="Dilutent", + description="Diluent liquid is filled in the reservoir", + display_color="#33FF33" + ) + sample_liquid = protocol.define_liquid( + name="Sample", + description="Non-diluted samples are loaded in the 1st column", + display_color="#FF0000" + ) + + diluent.load_liquid(liquid=diluent_liquid, volume=0.8 * diluent.max_volume) + for well in source: + well.load_liquid(liquid=sample_liquid, volume=TOTAL_MIXING_VOLUME) + + # Set up dilution destinations + dilution_destination_sets = [[row] for row in plate.rows()[0][1:NUM_DILUTIONS+1]] + dilution_source_sets = [[row] for row in plate.rows()[0][:NUM_DILUTIONS]] + blank_set = [plate.rows()[0][NUM_DILUTIONS+1]] + + # 1. Distribute diluent + all_diluent_destinations = [well for wells in dilution_destination_sets for well in wells] + pipette.pick_up_tip() + for dest in all_diluent_destinations: + pipette.transfer( + diluent_volume, + diluent, + dest, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + # 2. Perform serial dilutions + pipette.pick_up_tip() + for source_set, dest_set in zip(dilution_source_sets, dilution_destination_sets): + for s, d in zip(source_set, dest_set): + pipette.transfer( + transfer_volume, + s, + d, + air_gap=AIR_GAP_VOLUME, + mix_after=(5, TOTAL_MIXING_VOLUME/2), + new_tip='never' + ) + pipette.drop_tip() + + # 3. Add blank + pipette.pick_up_tip() + for blank_well in blank_set: + pipette.transfer( + diluent_volume, + diluent, + blank_well, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() +``` + + diff --git a/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md b/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md new file mode 100644 index 00000000000..f4b54d4308a --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md @@ -0,0 +1,157 @@ +Standard API + +### Approved Pipette Loadnames + +Note that the labware names are hard to differentiate sometimes, +since there are cases that they differ in terms of last digits only. + +#### OT-2 Approved Loadnames + +For OT-2 robots, use the following approved loadnames: + +- p20_single_gen2 +- p300_single_gen2 +- p1000_single_gen2 +- p300_multi_gen2 +- p20_multi_gen2 + +#### Flex Approved Loadnames + +For Flex robots, use these approved loadnames: + +- flex_1channel_50 +- flex_1channel_1000 +- flex_8channel_50 +- flex_8channel_1000 +- flex_96channel_1000 + +### Agilent Labware + +- Agilent 1 Well Reservoir 290 mL: agilent_1_reservoir_290ml + +### Applied Biosystems Labware + +- Applied Biosystems MicroAmp 384 Well Plate 40 uL: appliedbiosystemsmicroamp_384_wellplate_40ul + +### Axygen Labware + +- Axygen 1 Well Reservoir 90 mL: axygen_1_reservoir_90ml + +### Bio-Rad Labware + +- Bio-Rad 384 Well Plate 50 uL: biorad_384_wellplate_50ul +- Bio-Rad 96 Well Plate 200 uL PCR: biorad_96_wellplate_200ul_pcr + +### Corning Labware + +- Corning 12 Well Plate 6.9 mL Flat: corning_12_wellplate_6.9ml_flat +- Corning 24 Well Plate 3.4 mL Flat: corning_24_wellplate_3.4ml_flat +- Corning 384 Well Plate 112 uL Flat: corning_384_wellplate_112ul_flat +- Corning 48 Well Plate 1.6 mL Flat: corning_48_wellplate_1.6ml_flat +- Corning 6 Well Plate 16.8 mL Flat: corning_6_wellplate_16.8ml_flat +- Corning 96 Well Plate 360 uL Flat: corning_96_wellplate_360ul_flat + +### GEB Labware + +- GEB 96 Tip Rack 1000 uL: geb_96_tiprack_1000ul +- GEB 96 Tip Rack 10 uL: geb_96_tiprack_10ul + +### NEST Labware + +- NEST 12 Well Reservoir 15 mL: nest_12_reservoir_15ml +- NEST 1 Well Reservoir 195 mL: nest_1_reservoir_195ml +- NEST 1 Well Reservoir 290 mL: nest_1_reservoir_290ml +- NEST 96 Well Plate 100 uL PCR Full Skirt: nest_96_wellplate_100ul_pcr_full_skirt +- NEST 96 Well Plate 200 uL Flat: nest_96_wellplate_200ul_flat +- NEST 96 Deep Well Plate 2mL: nest_96_wellplate_2ml_deep + +### Opentrons Labware + +- Opentrons 10 Tube Rack with Falcon 4x50 mL, 6x15 mL Conical: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical +- Opentrons 10 Tube Rack with NEST 4x50 mL, 6x15 mL Conical: opentrons_10_tuberack_nest_4x50ml_6x15ml_conical +- Opentrons 15 Tube Rack with Falcon 15 mL Conical: opentrons_15_tuberack_falcon_15ml_conical +- Opentrons 15 Tube Rack with NEST 15 mL Conical: opentrons_15_tuberack_nest_15ml_conical +- Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap: opentrons_24_aluminumblock_generic_2ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap: opentrons_24_aluminumblock_nest_0.5ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 1.5 mL Screwcap: opentrons_24_aluminumblock_nest_1.5ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap: opentrons_24_aluminumblock_nest_1.5ml_snapcap +- Opentrons 24 Well Aluminum Block with NEST 2 mL Screwcap: opentrons_24_aluminumblock_nest_2ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 2 mL Snapcap: opentrons_24_aluminumblock_nest_2ml_snapcap +- Opentrons 24 Tube Rack with Eppendorf 1.5 mL Safe-Lock Snapcap: opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap +- Opentrons 24 Tube Rack with Eppendorf 2 mL Safe-Lock Snapcap: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap +- Opentrons 24 Tube Rack with Generic 2 mL Screwcap: opentrons_24_tuberack_generic_2ml_screwcap +- Opentrons 24 Tube Rack with NEST 0.5 mL Screwcap: opentrons_24_tuberack_nest_0.5ml_screwcap # not opentrons_24_tuberack_nest_0_5ml_screwcap +- Opentrons 24 Tube Rack with NEST 1.5 mL Screwcap: opentrons_24_tuberack_nest_1.5ml_screwcap # not opentrons_24_tuberack_nest_1_5ml_screwcap +- Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap: opentrons_24_tuberack_nest_1.5ml_snapcap # note the use of dot. (`.`); opentrons_24_tuberack_nest_1_5ml_snapcap is incorrect +- Opentrons 24 Tube Rack with NEST 2 mL Screwcap: opentrons_24_tuberack_nest_2ml_screwcap +- Opentrons 24 Tube Rack with NEST 2 mL Snapcap: opentrons_24_tuberack_nest_2ml_snapcap +- Opentrons 6 Tube Rack with Falcon 50 mL Conical: opentrons_6_tuberack_falcon_50ml_conical +- Opentrons 6 Tube Rack with NEST 50 mL Conical: opentrons_6_tuberack_nest_50ml_conical +- Opentrons 96 Well Aluminum Block with Bio-Rad Well Plate 200 uL: opentrons_96_aluminumblock_biorad_wellplate_200ul +- Opentrons 96 Well Aluminum Block with Generic PCR Strip 200 uL: opentrons_96_aluminumblock_generic_pcr_strip_200ul +- Opentrons 96 Well Aluminum Block with NEST Well Plate 100 uL: opentrons_96_aluminumblock_nest_wellplate_100ul +- Opentrons 96 Deep Well Heater-Shaker Adapter: opentrons_96_deep_well_adapter +- Opentrons 96 Deep Well Heater-Shaker Adapter with NEST Deep Well Plate 2 mL: opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep +- Opentrons OT-2 96 Filter Tip Rack 1000 uL: opentrons_96_filtertiprack_1000ul +- Opentrons OT-2 96 Filter Tip Rack 10 uL: opentrons_96_filtertiprack_10ul +- Opentrons OT-2 96 Filter Tip Rack 200 uL: opentrons_96_filtertiprack_200ul +- Opentrons OT-2 96 Filter Tip Rack 20 uL: opentrons_96_filtertiprack_20ul +- Opentrons 96 Flat Bottom Heater-Shaker Adapter: opentrons_96_flat_bottom_adapter +- Opentrons 96 Flat Bottom Heater-Shaker Adapter with NEST 96 Well Plate 200 uL Flat: opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat +- Opentrons 96 PCR Heater-Shaker Adapter: opentrons_96_pcr_adapter +- Opentrons 96 PCR Heater-Shaker Adapter with NEST Well Plate 100 ul: opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt +- Opentrons OT-2 96 Tip Rack 1000 uL: opentrons_96_tiprack_1000ul +- Opentrons OT-2 96 Tip Rack 10 uL: opentrons_96_tiprack_10ul +- Opentrons OT-2 96 Tip Rack 20 uL: opentrons_96_tiprack_20ul +- Opentrons OT-2 96 Tip Rack 300 uL: opentrons_96_tiprack_300ul +- Opentrons 96 Well Aluminum Block: opentrons_96_well_aluminum_block +- Opentrons 96 Well Aluminum Block adapter: opentrons_96_well_aluminum_block +- Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt: opentrons_96_wellplate_200ul_pcr_full_skirt +- Opentrons Aluminum Flat Bottom Plate: opentrons_aluminum_flat_bottom_plate +- Opentrons Flex 96 Filter Tip Rack 1000 uL: opentrons_flex_96_filtertiprack_1000ul # note that 1000ul not 200ul +- Opentrons Flex 96 Filter Tip Rack 200 uL: opentrons_flex_96_filtertiprack_200ul # note that 200ul not 1000ul +- Opentrons Flex 96 Filter Tip Rack 50 uL: opentrons_flex_96_filtertiprack_50ul +- Opentrons Flex 96 Tip Rack 1000 uL: opentrons_flex_96_tiprack_1000ul +- Opentrons Flex 96 Tip Rack 200 uL: opentrons_flex_96_tiprack_200ul +- Opentrons Flex 96 Tip Rack 50 uL: opentrons_flex_96_tiprack_50ul +- Opentrons Flex 96 Tip Rack Adapter: opentrons_flex_96_tiprack_adapter +- Opentrons Universal Flat Heater-Shaker Adapter: opentrons_universal_flat_adapter +- Opentrons Universal Flat Heater-Shaker Adapter with Corning 384 Well Plate 112 ul Flat: opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat + +### Other Labware Brands + +- Thermo Scientific Nunc 96 Well Plate 1300 uL: thermoscientificnunc_96_wellplate_1300ul +- Thermo Scientific Nunc 96 Well Plate 2000 uL: thermoscientificnunc_96_wellplate_2000ul +- USA Scientific 12 Well Reservoir 22 mL: usascientific_12_reservoir_22ml +- USA Scientific 96 Deep Well Plate 2.4 mL: usascientific_96_wellplate_2.4ml_deep + +### Additional Opentrons Tube Racks + +- 4-in-1 Tube Rack Set 15: opentrons_15_tuberack_nest_15ml_conical +- 4-in-1 Tube Rack Set 50: opentrons_6_tuberack_nest_50ml_conical + +### Flex Pipettes + +- Flex 1-Channel 50 uL Pipette (single channel): flex_1channel_50 +- Flex 1-Channel 1000 uL Pipette (single channel): flex_1channel_1000 +- Flex 8-Channel 50 uL Pipette (multi-channel): flex_8channel_50 +- Flex 8-Channel 1000 uL Pipette (multi-channel): flex_8channel_1000 +- Flex 96-Channel 1000 uL Pipette (multi-channel): flex_96channel_1000 + +### Modules + +- temperature module: temperature module gen2 +- thermocycler module: thermocycler module +- thermocycler module gen2: thermocyclerModuleV2 + +### Single channel pipettes: + +- Flex 1-Channel 50 uL Pipette +- Flex 1-Channel 1000 uL Pipette +- flex_1channel_1000 + +### Multi channel pipettes: + +- Flex 8-Channel 50 uL Pipette +- Flex 8-Channel 1000 uL Pipette +- Flex 96-Channel 1000 uL Pipette diff --git a/opentrons-ai-server/api/storage/docs/transfer_function_notes.md b/opentrons-ai-server/api/storage/docs/transfer_function_notes.md new file mode 100644 index 00000000000..dcf4f315545 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/transfer_function_notes.md @@ -0,0 +1,651 @@ + + +**Introduction** + +The `transfer` function in the Opentrons API v2 simplifies liquid handling operations by abstracting the complexities involved in pipetting tasks. It allows users to perform liquid transfers efficiently without the need to write explicit loops for iterating over wells or volumes. This manual provides a comprehensive guide on using the `transfer` function effectively, including best practices, common pitfalls, and practical examples. This updated version incorporates additional examples and clarifications based on real-world protocols, including the use of modules and dynamic data-driven transfers. + +--- + +**Basic Usage of `transfer`** + +The `transfer` function enables the movement of liquids from one location to another with optional parameters to control tip usage, mixing, air gaps, and more. Its basic syntax is: + +```python +pipette.transfer( + volume, # Volume to transfer (single value or list) + source, # Source well(s) + destination, # Destination well(s) + new_tip='always' # Tip usage strategy ('always', 'once', or 'never') + # Additional optional parameters... +) +``` + +- **Volume**: The amount of liquid to transfer, specified in microliters (µL). It can be a single value or a list of volumes. +- **Source**: The starting location(s) of the liquid, specified as a well or a list of wells. +- **Destination**: The target location(s) for the liquid, specified as a well or a list of wells. +- **`new_tip`**: Controls how tips are used during the transfer: + - `'always'`: Change tips between each transfer step. + - `'once'`: Use the same tip for all transfers. + - `'never'`: Do not change tips (use with caution). + +--- + +**Understanding Pipette Types** + +Choosing the correct method for accessing wells or columns depends on the type of pipette used. + +### Single-Channel Pipettes + +Single-channel pipettes interact with individual wells. When using single-channel pipettes, access wells using the `wells()` method. + +**Example:** + +```python +source_wells = source_labware.wells()[:number_of_samples] +``` + +### Multi-Channel Pipettes + +Multi-channel pipettes interact with rows or columns simultaneously. When using multi-channel pipettes, access columns using the `columns()` method. + +**Example:** + +```python +import math + +number_of_samples = 48 +number_of_columns = math.ceil(number_of_samples / 8) +source_columns = source_labware.columns()[:number_of_columns] +``` + +--- + +**Well Selection Methods** + +Accurate well selection is crucial for successful liquid transfers. + +### Accessing Wells + +- **Access all wells**: + + ```python + all_wells = labware.wells() + ``` + +- **Access specific wells by name**: + + ```python + well_a1 = labware.wells_by_name()['A1'] + ``` + +- **Access a list of wells by name**: + + ```python + specific_wells = [labware.wells_by_name()[well] for well in ['A1', 'B2', 'C3']] + ``` + +### Accessing Columns + +- **Access all columns**: + + ```python + all_columns = labware.columns() + ``` + +- **Access specific columns by index (0-based)**: + + ```python + first_three_columns = labware.columns()[:3] + ``` + +- **Access columns by name (1-based)**: + + ```python + column_one = labware.columns_by_name()['1'] + ``` + +- **Access multiple columns by name**: + + ```python + specific_columns = [labware.columns_by_name()[idx] for idx in ['1', '3', '5']] + ``` + +### Accessing Rows + +- **Access all rows**: + + ```python + all_rows = labware.rows() + ``` + +- **Access specific rows by name**: + + ```python + row_a = labware.rows_by_name()['A'] + ``` + +--- + +**Handling the `new_tip` Parameter** + +The `new_tip` parameter controls tip usage during transfers. + +- **`new_tip='always'`**: Use a new tip for each transfer. This is appropriate when avoiding cross-contamination is critical. + +- **`new_tip='once'`**: Use the same tip for all transfers in the `transfer` function call. Use this when transferring from a single source to multiple destinations and cross-contamination is not a concern. + +- **`new_tip='never'`**: Never change tips during the transfer. Use with caution, ensuring that cross-contamination will not occur. + +**Important Note:** Do not use `new_tip='once'` inside a loop; instead, pass lists of wells to the `transfer` function and let it handle the iteration. + +--- + +**Avoiding Unnecessary Loops** + +**Incorrect Usage:** + +```python +for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='always') +``` + +**Issue:** This approach unnecessarily calls the `transfer` method multiple times and can lead to inefficiencies or errors. + +**Correct Usage:** + +```python +pipette.transfer(volume, source_wells, destination_wells, new_tip='always') +``` + +**Explanation:** The `transfer` function can handle lists of sources and destinations, automatically pairing them and iterating over them. + +--- + +**Proper Use of `new_tip`** + +**Incorrect Usage:** + +Using `new_tip='once'` inside a loop when intending to reuse the same tip. + +```python +for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='once') +``` + +**Correct Usage:** + +```python +pipette.transfer(volume, source_wells, destination_wells, new_tip='once') +``` + +**Explanation:** When `new_tip='once'`, the pipette picks up a tip at the beginning of the transfer and uses it throughout. Using it inside a loop can cause the pipette to attempt to pick up a tip that is already in use, leading to errors. + +--- + +**Preventing "Out of Tips" Errors** + +- **Tip Rack Capacity:** Be mindful of the number of tips available in your tip racks. For example, a standard 96-tip rack cannot provide more than 96 tips. + +- **Calculating Tip Usage:** Estimate the number of tips required based on the `new_tip` parameter and the number of transfers. + +- **Loading Additional Tip Racks:** If your protocol requires more tips than are available in a single rack, load additional tip racks. + +**Example:** + +```python +tiprack1 = protocol.load_labware('opentrons_96_tiprack_300ul', 2) +tiprack2 = protocol.load_labware('opentrons_96_tiprack_300ul', 3) +pipette = protocol.load_instrument('p300_single_gen2', 'left', tip_racks=[tiprack1, tiprack2]) +``` + +--- + +**Index Errors** + +- **Labware Dimensions:** Ensure that your loops do not exceed the dimensions of the labware (e.g., a 96-well plate has 12 columns and 8 rows). + +- **Valid Indices:** Adjust loop ranges to stay within valid indices. + +**Incorrect Usage:** + +```python +for i in range(13): # Exceeds available columns (0-11) + pipette.transfer(volume, source_columns[i], dest_columns[i]) +``` + +**Correct Usage:** + +```python +for i in range(12): # Valid column indices for a 96-well plate + pipette.transfer(volume, source_columns[i], dest_columns[i]) +``` + +--- + +**Calculating Tip Usage** + +- **Estimate in Advance:** Before running the protocol, calculate the number of tips required based on the number of transfers and the `new_tip` parameter. + +- **Account for Pipette Type:** Remember that multi-channel pipettes use multiple tips per pick-up (e.g., an 8-channel pipette uses 8 tips per pick-up). + +- **Example Calculation:** + + If you are transferring samples to 96 wells using a single-channel pipette with `new_tip='always'`, you will need 96 tips. If you are using a multi-channel pipette (8-channel) to transfer to 12 columns, you will need 12 tip pickups (12 columns x 8 tips per pickup = 96 tips). + +--- + +**Optimizing Transfers** + +- **Use Lists in `transfer`:** Provide lists of source and destination wells to the `transfer` function to leverage its built-in iteration. + +- **Minimize Tip Usage:** When appropriate, reuse tips by setting `new_tip='once'` to conserve tips and reduce waste. + +- **Avoid Unnecessary Loops:** Let the `transfer` function handle iteration over wells and volumes. + +--- + +**Efficient Labware Access** + +- **Match Pipette Type to Access Method:** Use `wells()` for single-channel pipettes and `columns()` for multi-channel pipettes. + +- **Use Labware Methods Correctly:** Ensure you are accessing wells and columns using the correct methods to prevent errors. + +--- + +**Example 1: Single Source to Multiple Destinations** + +**Task:** Transfer 1 µL of reagent from tube A1 in the source rack to all wells in the destination plate using the same tip. + +**Protocol:** + +```python +def run(protocol): + # Labware + tiprack = protocol.load_labware('opentrons_96_tiprack_20ul', 2) + source_rack = protocol.load_labware('opentrons_24_tuberack_nest_1.5ml_snapcap', 3) + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 9) + + # Pipette + p20 = protocol.load_instrument('p20_single_gen2', mount='right', tip_racks=[tiprack]) + + # Wells + src_well = source_rack.wells_by_name()['A1'] + dest_wells = dest_plate.wells() + + # Transfer + p20.transfer(1, src_well, dest_wells, new_tip='once') +``` + +--- + +**Example 2: Well-to-Well Transfers with Reused Tips** + +**Task:** Transfer 50 µL from wells A1 and A2 in source labware 1 to wells B6 and B7 in source labware 2 using the same tip. + +**Protocol:** + +```python +def run(protocol): + # Labware + source_labware_1 = protocol.load_labware('source_labware_1_definition', 1) + source_labware_2 = protocol.load_labware('source_labware_2_definition', 2) + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 3) + + # Pipette + p300 = protocol.load_instrument('p300_single_gen2', mount='left', tip_racks=[tiprack]) + + # Wells + source_wells = [source_labware_1.wells_by_name()[well] for well in ['A1', 'A2']] + destination_wells = [source_labware_2.wells_by_name()[well] for well in ['B6', 'B7']] + + # Transfer + p300.transfer(50, source_wells, destination_wells, new_tip='once') +``` + +--- + +**Example 3: Column-wise Transfers with Multi-Channel Pipette** + +**Task:** Using a P300 Multi-Channel pipette, transfer 55 µL of sample from each column of the source plate into the corresponding columns of the destination plate, changing tips between each transfer. + +**Protocol:** + +```python +def run(protocol): + # Labware + source_plate = protocol.load_labware('source_plate_definition', 1) + destination_plate = protocol.load_labware('destination_plate_definition', 2) + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 3) + + # Pipette + p300_multi = protocol.load_instrument('p300_multi_gen2', mount='left', tip_racks=[tiprack]) + + # Columns + src_cols = source_plate.columns() + dest_cols = destination_plate.columns() + + # Transfer + p300_multi.transfer(55, src_cols, dest_cols, new_tip='always') +``` + +--- + +**Example 4: Complex Transfers with Different Pipettes** + +**Task:** Transfer 15 µL from wells C4 and C6 in source labware 2 to wells A3 and A4 in source labware 1 using the same tip. + +**Protocol:** + +```python +def run(protocol): + # Labware + source_1 = protocol.load_labware('source_labware_1_definition', 1) + source_2 = protocol.load_labware('source_labware_2_definition', 2) + tiprack = protocol.load_labware('opentrons_96_tiprack_20ul', 3) + + # Pipette + p20 = protocol.load_instrument('p20_single_gen2', mount='right', tip_racks=[tiprack]) + + # Wells + src_wells = [source_2.wells_by_name()[well] for well in ['C4', 'C6']] + dest_wells = [source_1.wells_by_name()[well] for well in ['A3', 'A4']] + + # Transfer + p20.transfer(15, src_wells, dest_wells, new_tip='once') +``` + +--- + +**Example 5: Transfers Involving Modules** + +**Task:** Perform transfers involving thermocycler and temperature modules, handling temperature settings and PCR amplification steps. + +**Protocol:** + +```python +def run(protocol): + import math + + # Sample preparation parameters + number_of_samples = 64 + sample_volume_ul = 5 + master_mix_volume_ul = 7 + mixing_cycles = 9 + total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul + + # Modules + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temp_module = protocol.load_module('temperature module gen2', 1) + master_mix_temp_module = protocol.load_module('temperature module gen2', 3) + + # Labware + tips_20ul = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + pcr_plate = thermocycler_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + sample_plate = sample_temp_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_temp_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + # Pipette + p20_multi = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tips_20ul]) + + # Well allocation + number_of_columns = math.ceil(number_of_samples / 8) + sample_source_wells = sample_plate.columns()[:number_of_columns] + sample_destination_wells = pcr_plate.columns()[:number_of_columns] + master_mix_source_wells = master_mix_plate.columns()[:number_of_columns] + master_mix_destination_wells = pcr_plate.columns()[:number_of_columns] + + # Set temperatures + sample_temp_module.set_temperature(4) + master_mix_temp_module.set_temperature(10) + + # Transfer master mix + p20_multi.transfer( + master_mix_volume_ul, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' + ) + + # Transfer samples and mix + p20_multi.transfer( + sample_volume_ul, + sample_source_wells, + sample_destination_wells, + new_tip='always', + mix_after=(mixing_cycles, total_mix_volume_ul), + blow_out=True, + blowout_location='destination well' + ) + + # PCR cycling steps (simplified for brevity) + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {{'temperature': 74, 'hold_time_seconds': 65}}, + {{'temperature': 60, 'hold_time_seconds': 7}}, + {{'temperature': 84, 'hold_time_seconds': 19}}, + {{'temperature': 57, 'hold_time_seconds': 44}} + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.open_lid() + + # Deactivate modules + master_mix_temp_module.deactivate() + sample_temp_module.deactivate() +``` + +--- + +**Example 6: Dynamic Transfers Using CSV Data** + +**Task:** Perform transfers based on data provided in a CSV file, without using the thermocycler. + +**Protocol:** + +```python +def run(protocol): + # CSV data as a multi-line string + csv_data = ''' + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + ''' + + # Parse CSV data + csv_lines = [line.strip().split(',') for line in csv_data.strip().splitlines() if line.strip()] + headers = csv_lines[0] + data = csv_lines[1:] + + # Labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dest_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'B1') + tiprack_single = [protocol.load_labware('opentrons_96_tiprack_50ul', slot) for slot in ['A1']] + p50 = protocol.load_instrument('p50_single', 'right', tip_racks=tiprack_single) + + # Transfers based on CSV data + for row in data: + source_tube = row[0] + dest_well = row[1] + p50.transfer(7, tuberack.wells_by_name()[source_tube], dest_plate.wells_by_name()[dest_well], new_tip='always') +``` + +--- + +**Additional Examples** + +**Example 7: Transfer with Heater-Shaker Module** + +**Task:** Transfer liquids between a reservoir, a PCR plate, and a heater-shaker module's plate, including shaking the plate. + +**Protocol:** + +```python +def run(protocol): + # Modules + heater_shaker_module = protocol.load_module('heaterShakerModuleV1', 'D1') + heater_shaker_plate = heater_shaker_module.load_labware('corning_96_wellplate_360ul_flat') + + # Labware + reservoir = protocol.load_labware('nest_1_reservoir_195ml', 'C1') + pcr_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D2') + tiprack_200ul = protocol.load_labware('opentrons_96_tiprack_200ul', 'A2') + tiprack_50ul = protocol.load_labware('opentrons_96_tiprack_50ul', 'C2') + + # Pipette + pipette = protocol.load_instrument('p300_multi', mount='left', tip_racks=[tiprack_200ul, tiprack_50ul]) + + # Steps + heater_shaker_module.open_labware_latch() + protocol.pause("Please place the plate on the Heater-Shaker Module.") + heater_shaker_module.close_labware_latch() + + # Transfer 70 µL from reservoir to heater-shaker plate + pipette.transfer(70, reservoir['A1'], heater_shaker_plate['A1'], new_tip='always') + + # Transfer 10 µL from PCR plate to heater-shaker plate + pipette.transfer(10, pcr_plate['A1'], heater_shaker_plate['A1'], new_tip='always') + + # Shake the plate + heater_shaker_module.set_and_wait_for_shake_speed(rpm=2000) + protocol.delay(minutes=1) + heater_shaker_module.deactivate_shaker() +``` + +--- + +**Advanced Usage** + +Advanced features of the `transfer` function include specifying aspiration and dispense locations, mixing, air gaps, blow out, and using modules with `transfer`. + +--- + +**Specifying Aspiration and Dispense Locations** + +You can specify precise locations within wells for aspiration and dispensing. + +**Example:** + +```python +pipette.transfer( + 20, + source_well.bottom(3), # 3 mm above the bottom + destination_well.top(-7), # 7 mm below the top + new_tip='once' +) +``` + +--- + +**Using Mix After/Before** + +Mixing can be performed before or after the transfer. + +**Example:** + +```python +pipette.transfer( + 10, + source_well, + destination_well, + mix_after=(5, 10) # Mix 5 times with a volume of 10 µL after dispensing +) +``` + +--- + +**Handling Air Gaps and Blow Out** + +Air gaps and blow-out can prevent dripping and ensure complete dispensing. + +**Example:** + +```python +pipette.transfer( + 10, + source_well, + destination_well, + air_gap=5, # Add a 5 µL air gap after aspiration + blow_out=True, + blowout_location='destination well' +) +``` + +--- + +**Using Modules with `transfer`** + +The `transfer` function can be used effectively with various modules like the thermocycler, temperature modules, and heater-shaker modules. When using modules: + +- **Set Module Temperatures Before Transfers:** Ensure that temperature modules are set to the desired temperature before performing transfers. + +- **Load Labware on Modules:** Use the module's `load_labware` or `load_adapter` method to place labware on the module. + +**Example:** + +```python +# Load modules +temp_module = protocol.load_module('temperature module gen2', '1') +thermocycler_module = protocol.load_module('thermocyclerModuleV2') + +# Load labware on modules +temp_plate = temp_module.load_labware('opentrons_96_aluminumblock_biorad_wellplate_200ul') +pcr_plate = thermocycler_module.load_labware('nest_96_wellplate_100ul_pcr_full_skirt') + +# Set temperatures +temp_module.set_temperature(4) +thermocycler_module.set_block_temperature(95) +``` + +--- + +**Dynamic Transfers Based on Data** + +For protocols that require dynamic transfers based on external data (e.g., CSV files), you can parse the data and use it to control the `transfer` function. + +- **Parsing CSV Data:** Use Python's built-in functions or the `csv` module to read and parse CSV data. + +- **Using Parsed Data in Transfers:** Use the parsed data to define source wells, destination wells, and volumes. + +**Example:** + +```python +import csv +from io import StringIO + +def run(protocol): + # CSV data as a string + csv_data = ''' + Source Well,Destination Well,Volume + A1,B1,50 + A2,B2,100 + A3,B3,150 + ''' + + # Parse CSV data + reader = csv.DictReader(StringIO(csv_data.strip())) + transfers = list(reader) + + # Labware + source_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', '1') + dest_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', '2') + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tiprack]) + + # Perform transfers based on CSV data + for transfer in transfers: + source_well = source_plate.wells_by_name()[transfer['Source Well']] + dest_well = dest_plate.wells_by_name()[transfer['Destination Well']] + volume = float(transfer['Volume']) + pipette.transfer(volume, source_well, dest_well, new_tip='always') + +``` + + diff --git a/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts b/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts index db903a1f824..5f5c9aa4012 100644 --- a/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts +++ b/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts @@ -150,11 +150,13 @@ describe('reduxActionToAnalyticsEvent', () => { expect(result).toEqual({ name: 'transferStep', properties: { - commandCreatorFnName: 'transfer', // existing fields id: 'stepId', pipette: 'pipetteId', otherField: 123, + aspirateFlowRate: undefined, + dispenseFlowRate: undefined, + aspirateAirGap: undefined, nested: { inner: true }, // de-nested fields __nested__inner: true, diff --git a/protocol-designer/src/analytics/constants.ts b/protocol-designer/src/analytics/constants.ts new file mode 100644 index 00000000000..a9f0d9a5e7c --- /dev/null +++ b/protocol-designer/src/analytics/constants.ts @@ -0,0 +1,7 @@ +export const FORM_WARNINGS_EVENT = 'formWarnings' +export const FORM_ERRORS_EVENT = 'formErrors' +export const OPEN_STEP_DETAILS_EVENT = 'openStepDetails' +export const ONBOARDING_FLOW_DURATION_EVENT = 'onboardingFlowDuration' +export const SELECT_ALL_STEPS_EVENT = 'selectAllSteps' +export const EXIT_BATCH_EDIT_MODE_EVENT = 'exitBatchEditMode' +export const DESELECT_ALL_STEPS_EVENT = 'deselectAllSteps' diff --git a/protocol-designer/src/analytics/middleware.ts b/protocol-designer/src/analytics/middleware.ts index 514f4a59b14..6c798e353ff 100644 --- a/protocol-designer/src/analytics/middleware.ts +++ b/protocol-designer/src/analytics/middleware.ts @@ -1,4 +1,5 @@ import uniq from 'lodash/uniq' +import omit from 'lodash/omit' import { FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, MOVABLE_TRASH_ADDRESSABLE_AREAS, @@ -16,13 +17,23 @@ import { getFileMetadata, getRobotStateTimeline, } from '../file-data/selectors' -import { FIXED_TRASH_ID } from '../constants' +import { + DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + DEFAULT_MM_FROM_BOTTOM_DISPENSE, + FIXED_TRASH_ID, +} from '../constants' import { trackEvent } from './mixpanel' import { getHasOptedIn } from './selectors' import { flattenNestedProperties } from './utils/flattenNestedProperties' import type { Middleware } from 'redux' -import type { NormalizedPipetteById } from '@opentrons/step-generation' +import type { + ConsolidateArgs, + DistributeArgs, + MixArgs, + NormalizedPipetteById, + TransferArgs, +} from '@opentrons/step-generation' import type { AddressableAreaName, LoadLabwareCreateCommand, @@ -39,6 +50,20 @@ import type { SetFeatureFlagAction } from '../feature-flags/actions' import type { CreatePipettesAction } from '../step-forms/actions' import type { AnalyticsEventAction } from './actions' import type { AnalyticsEvent } from './mixpanel' + +const DEFAULT_VALUE = 'default' +const PIPETTING_ARGS_FILTER_LIST = [ + 'touchTipAfterAspirateOffsetMmFromBottom', + 'touchTipAfterDispenseOffsetMmFromBottom', + 'commandCreatorFnName', + 'blowoutFlowRateUlSec', + 'blowoutOffsetFromTopMm', + 'touchTipMmFromBottom', + 'aspirateAirGapVolume', + 'aspirateFlowRateUlSec', + 'dispenseFlowRateUlSec', +] + interface TransformedPipetteInfo { [pipetteId: string]: { name: string @@ -94,43 +119,131 @@ export const reduxActionToAnalyticsEvent = ( ? 'pause' : stepName - if ( - modifiedStepName === 'engageMagnet' || - modifiedStepName === 'disengageMagnet' - ) { - return { - name: `magnetStep`, - properties: { type: modifiedStepName }, + switch (modifiedStepName) { + case 'engageMagnet': + case 'disengageMagnet': { + return { + name: `magnetStep`, + properties: { type: modifiedStepName }, + } + } + case 'setTemperature': + case 'deactivateTemperature': { + return { + name: `temperatureStep`, + properties: { type: modifiedStepName }, + } } - } else if ( - modifiedStepName === 'deactivateTemperature' || - modifiedStepName === 'setTemperature' - ) { - return { - name: `temperatureStep`, - properties: { type: modifiedStepName }, + case 'thermocyclerProfile': + case 'thermocyclerState': { + return { + name: 'thermocyclerStep', + properties: { type: modifiedStepName }, + } } - } else if ( - modifiedStepName === 'thermocyclerProfile' || - modifiedStepName === 'thermocyclerState' - ) { - return { - name: 'thermocyclerStep', - properties: { type: modifiedStepName }, + case 'heaterShaker': { + return { + name: 'heaterShakerStep', + properties: {}, + } } - } else if (modifiedStepName === 'heaterShaker') { - return { - name: 'heaterShakerStep', - properties: {}, + case 'transfer': + case 'consolidate': + case 'distribute': { + const stepArgModified = omit( + stepArgs as TransferArgs | ConsolidateArgs | DistributeArgs, + PIPETTING_ARGS_FILTER_LIST + ) + return { + name: `${modifiedStepName}Step`, + properties: { + ...stepArgModified, + aspirateAirGap: stepArgModified.aspirateAirGapVolume, + aspirateFlowRate: stepArgModified.aspirateFlowRateUlSec, + dispenseFlowRate: stepArgModified.dispenseFlowRateUlSec, + blowoutFlowRate: stepArgModified.blowoutFlowRateUlSec, + aspirateOffsetFromBottomMm: + stepArgModified.aspirateOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_ASPIRATE + ? DEFAULT_VALUE + : stepArgModified.aspirateOffsetFromBottomMm, + dispenseOffsetFromBottomMm: + stepArgModified.dispenseOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_DISPENSE + ? DEFAULT_VALUE + : stepArgModified.dispenseOffsetFromBottomMm, + aspirateXOffset: + stepArgModified.aspirateXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateXOffset, + aspirateYOffset: + stepArgModified.aspirateYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateYOffset, + dispenseXOffset: + stepArgModified.dispenseXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseXOffset, + dispenseYOffset: + stepArgModified.dispenseYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseYOffset, + + ...additionalProperties, + }, + } } - } else { - return { - name: `${modifiedStepName}Step`, - properties: { ...stepArgs, ...additionalProperties }, + case 'mix': { + const stepArgModified = omit( + stepArgs as MixArgs, + PIPETTING_ARGS_FILTER_LIST + ) + return { + name: `mixStep`, + properties: { + ...stepArgModified, + aspirateFlowRate: stepArgModified.aspirateFlowRateUlSec, + dispenseFlowRate: stepArgModified.dispenseFlowRateUlSec, + blowoutFlowRate: stepArgModified.blowoutFlowRateUlSec, + aspirateOffsetFromBottomMm: + stepArgModified.aspirateOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_ASPIRATE + ? DEFAULT_VALUE + : stepArgModified.aspirateOffsetFromBottomMm, + dispenseOffsetFromBottomMm: + stepArgModified.dispenseOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_DISPENSE + ? DEFAULT_VALUE + : stepArgModified.dispenseOffsetFromBottomMm, + aspirateXOffset: + stepArgModified.aspirateXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateXOffset, + aspirateYOffset: + stepArgModified.aspirateYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateYOffset, + dispenseXOffset: + stepArgModified.dispenseXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseXOffset, + dispenseYOffset: + stepArgModified.dispenseYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseYOffset, + ...additionalProperties, + }, + } } + default: + return { + name: `${modifiedStepName}Step`, + properties: { ...stepArgs, ...additionalProperties }, + } } } } + if (action.type === 'COMPUTE_ROBOT_STATE_TIMELINE_SUCCESS') { const robotTimeline = getRobotStateTimeline(state) const { errors, timeline } = robotTimeline diff --git a/protocol-designer/src/assets/images/onboarding_animation_1.webm b/protocol-designer/src/assets/images/onboarding_animation_1.webm new file mode 100644 index 00000000000..6eed789cb61 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_1.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_2.webm b/protocol-designer/src/assets/images/onboarding_animation_2.webm new file mode 100644 index 00000000000..7dbc51c26ad Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_2.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_3.webm b/protocol-designer/src/assets/images/onboarding_animation_3.webm new file mode 100644 index 00000000000..19d29e2b939 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_3.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_4.webm b/protocol-designer/src/assets/images/onboarding_animation_4.webm new file mode 100644 index 00000000000..d59f86faf45 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_4.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_5.webm b/protocol-designer/src/assets/images/onboarding_animation_5.webm new file mode 100644 index 00000000000..4fc1580c4f8 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_5.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_6.webm b/protocol-designer/src/assets/images/onboarding_animation_6.webm new file mode 100644 index 00000000000..ea00e3fcdb1 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_6.webm differ diff --git a/protocol-designer/src/assets/images/placeholder_image_delete.png b/protocol-designer/src/assets/images/placeholder_image_delete.png deleted file mode 100644 index f429a3862dc..00000000000 Binary files a/protocol-designer/src/assets/images/placeholder_image_delete.png and /dev/null differ diff --git a/protocol-designer/src/assets/localization/en/application.json b/protocol-designer/src/assets/localization/en/application.json index b4d1ebf8157..0cbdb9cc6d3 100644 --- a/protocol-designer/src/assets/localization/en/application.json +++ b/protocol-designer/src/assets/localization/en/application.json @@ -34,7 +34,7 @@ "heaterShaker": "heater-shaker", "magnet": "magnet", "mix": "mix", - "moveLabware": "move labware", + "moveLabware": "move", "moveLiquid": "transfer", "pause": "pause", "profile_settings": "profile settings", diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index aeaa00c86c1..f2e132728a5 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -11,7 +11,7 @@ "edit": "Edit", "fixtures_added": "Fixtures added", "fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.", - "incompatible_tip_body": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.", + "incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.", "incompatible_tips": "Incompatible tips", "labware_name": "Labware name", "left_right": "Left + Right", @@ -36,7 +36,7 @@ "staging_area_has_labware": "This staging area slot has labware", "staging_area_will_delete_labware": "The staging area slot that you are about to delete has labware placed on it. If you make these changes to your protocol starting deck, the labware will be deleted as well.", "stagingArea": "Staging area", - "swap_pipettes": "Swap pipettes", + "swap_pipette_mounts": "Swap pipette mounts", "tell_us": "Tell us about your protocol", "trash_required": "A trash entity is required", "trashBin": "Trash Bin", diff --git a/protocol-designer/src/assets/localization/en/protocol_overview.json b/protocol-designer/src/assets/localization/en/protocol_overview.json index b03097576cd..6afdf715b6c 100644 --- a/protocol-designer/src/assets/localization/en/protocol_overview.json +++ b/protocol-designer/src/assets/localization/en/protocol_overview.json @@ -22,9 +22,9 @@ "name": "Name", "no_deck_hardware": "No deck hardware", "no_labware": "No labware", - "no_liquids_defined": "No liquids defined", + "no_liquids_defined": "Edit protocol to define liquids", "no_liquids": "No liquids", - "no_steps": "No steps defined", + "no_steps": "Edit protocol to define steps", "number_of_steps": "Number of steps", "protocol_metadata": "Protocol Metadata", "protocol_steps": "Protocol Steps", diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index a9a45b9d2ed..50c6f7879f8 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -27,7 +27,7 @@ "duplicate": "Duplicate step", "edit_step": "Edit step", "engage_height": "Engage height", - "final_deck_state": "Final deck state", + "ending_deck": "Ending deck", "flow_type_title": "{{type}} flow rate", "from": "from", "heater_shaker": { @@ -69,7 +69,7 @@ "consolidate_disposal": "Consolidatingfrom{{sourceWells}} of {{source}}to{{destination}}", "transfer_disposal": "Transferringfrom{{sourceWells}} of {{source}}to{{destination}}" }, - "multi_dispense_options": "Distribute options", + "multi_dispense_options": "Disposal volume", "multiAspirate": "Consolidate path", "multiDispense": "Distribute path", "new_location": "New location", @@ -104,7 +104,7 @@ "shake": "Shake", "single": "Single path", "speed": "Speed", - "starting_deck_state": "Starting deck state", + "starting_deck": "Starting deck", "step_substeps": "{{stepType}} details", "temperature": "Temperature", "temperature_module": { @@ -120,8 +120,8 @@ "substep_settings": "Set block temperature tofor", "thermocycler_profile": { "end_hold": { - "block": "End at thermocycler block", - "lid_position": "Thermocycler lid" + "block": "End with block at", + "lid_position": "and lid" }, "lid_temperature": "and lid temperature at", "volume": "Run thermocycler profile with" diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index 31be366c9d7..0522cc1291a 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -1,6 +1,9 @@ { + "__end__": "Ending deck", + "__initial_setup__": "Starting deck", + "__presaved_step__": "Unsaved step", "adapter_compatible_lab": "Adapter compatible labware", - "adapter": "Adapter", + "adapter": "Adapters", "add_fixture": "Add a fixture", "add_hardware_labware": "Add hardware/labware", "add_hw_lw": "Add hardware/labware", @@ -9,11 +12,11 @@ "add_module": "Add a module", "add_rest": "Add labware and liquids to complete deck setup", "alter_pause": "You may also need to alter the time you pause while your magnet is engaged.", - "aluminumBlock": "Aluminum block", + "aluminumBlock": "Aluminum blocks", "clear_labware": "Clear labware", "clear_slot": "Clear slot", "clear": "Clear", - "command_click_to_multi_select": "Command + Click for multi-select", + "command_click_to_multi_select": "^/⌘ + click to select multiple", "convert_gen1_to_gen2": "To convert engage heights from GEN1 to GEN2, divide your engage height by 2.", "convert_gen2_to_gen1": "To convert engage heights from GEN2 to GEN1, multiply your engage height by 2.", "custom": "Custom labware definitions", @@ -47,16 +50,16 @@ "protocol_starting_deck": "Protocol starting deck", "read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules", "rename_lab": "Rename labware", - "reservoir": "Reservoir", - "shift_click_to_select_all": "Shift + Click to select all", + "reservoir": "Reservoirs", + "shift_click_to_select_range": "⇧ + click to select range", "starting_deck_state": "Starting deck state", "tc_slots_occupied_flex": "The Thermocycler needs slots A1 and B1. Slot A1 is occupied", "tc_slots_occupied_ot2": "The Thermocycler needs slots 7, 8, 10, and 11. One or more of those slots is occupied", - "tipRack": "Tip rack", + "tipRack": "Tip racks", "trash_required": "A trash bin or waste chute is required", - "tubeRack": "Tube rack", + "tubeRack": "Tube racks", "untitled_protocol": "Untitled protocol", "upload_custom_labware": "Upload custom labware", "we_added_hardware": "We've added your deck hardware!", - "wellPlate": "Well plate" + "wellPlate": "Well plates" } diff --git a/protocol-designer/src/atoms/constants.ts b/protocol-designer/src/atoms/constants.ts index e04701a7639..620a3d10dbc 100644 --- a/protocol-designer/src/atoms/constants.ts +++ b/protocol-designer/src/atoms/constants.ts @@ -1,11 +1,8 @@ -import styled, { css } from 'styled-components' +import { css } from 'styled-components' import { - BORDERS, COLORS, DIRECTION_COLUMN, OVERFLOW_HIDDEN, - SPACING, - TYPOGRAPHY, } from '@opentrons/components' import type { FlattenSimpleInterpolation } from 'styled-components' @@ -35,40 +32,3 @@ export const COLUMN_STYLE = css` min-width: calc((${MIN_OVERVIEW_WIDTH} - ${COLUMN_GRID_GAP}) * 0.5); flex: 1; ` - -export const DescriptionField = styled.textarea` - min-height: 5rem; - width: 100%; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; - border-radius: ${BORDERS.borderRadius4}; - padding: ${SPACING.spacing8}; - font-size: ${TYPOGRAPHY.fontSizeP}; - resize: none; - - &:active:enabled { - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - } - - &:hover { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey60}; - } - - &:focus-visible { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey55}; - outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; - outline-offset: 2px; - } - - &:focus-within { - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - } - - &:disabled { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - } - input[type='number']::-webkit-inner-spin-button, - input[type='number']::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } -` diff --git a/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx b/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx index 2ffeb22bfe0..86dd338c6d0 100644 --- a/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx +++ b/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx @@ -37,7 +37,7 @@ describe('StepCreationButton', () => { const addStep = screen.getByRole('button', { name: '+ Add Step' }) fireEvent.click(addStep) screen.getByText('comment') - screen.getByText('move labware') + screen.getByText('move') screen.getByText('transfer') screen.getByText('mix') screen.getByText('pause') diff --git a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx index 01cb48e5bf7..4fe43e2f016 100644 --- a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx +++ b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx @@ -439,7 +439,7 @@ describe('ConnectedStepItem', () => { vi.mocked(getSelectedStepId).mockReturnValue(moveLabwareStepId) props.stepId = moveLabwareStepId render(props) - screen.getByText('2. move labware') + screen.getByText('2. move') screen.getByText('Manually') screen.getByText('labware') screen.getByText('new location') diff --git a/protocol-designer/src/labware-ingred/actions/thunks.ts b/protocol-designer/src/labware-ingred/actions/thunks.ts index dedfae883d8..38cccb252fb 100644 --- a/protocol-designer/src/labware-ingred/actions/thunks.ts +++ b/protocol-designer/src/labware-ingred/actions/thunks.ts @@ -73,7 +73,6 @@ export const createContainer: ( args.slot || getNextAvailableDeckSlot(initialDeckSetup, robotType, labwareDef) const isTiprack = getIsTiprack(labwareDef) - if (slot) { const id = `${uuid()}:${args.labwareDefURI}` const adapterId = diff --git a/protocol-designer/src/labware-ingred/utils.ts b/protocol-designer/src/labware-ingred/utils.ts index d4c6dc5e1bf..377dff50eb5 100644 --- a/protocol-designer/src/labware-ingred/utils.ts +++ b/protocol-designer/src/labware-ingred/utils.ts @@ -1,5 +1,6 @@ import { FIXED_TRASH_ID, + FLEX_MODULE_ADDRESSABLE_AREAS, getAreSlotsAdjacent, getDeckDefFromRobotType, getIsLabwareAboveHeight, @@ -7,6 +8,7 @@ import { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, MOVABLE_TRASH_ADDRESSABLE_AREAS, OT2_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, WASTE_CHUTE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { COLUMN_4_SLOTS } from '@opentrons/step-generation' @@ -30,6 +32,16 @@ export function getNextAvailableDeckSlot( module => module.type === HEATERSHAKER_MODULE_TYPE )?.slot + const hasTC = Object.values(initialDeckSetup.modules).find( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + let moduleSlots = Object.values(initialDeckSetup.modules) + .filter(module => module.slot) + .map(mod => mod.slot) + if (hasTC) { + moduleSlots = [...moduleSlots, '8', '10', '11'] + } + return deckDef.locations.addressableAreas.find(slot => { const cutoutIds = Object.values(initialDeckSetup.additionalEquipmentOnDeck) .filter(ae => ae.name === 'stagingArea') @@ -47,12 +59,17 @@ export function getNextAvailableDeckSlot( MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slot.id) || WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slot.id) || slot.id === FIXED_TRASH_ID + ) { + isSlotEmpty = false + } else if ( + moduleSlots.includes(slot.id) || + FLEX_MODULE_ADDRESSABLE_AREAS.includes(slot.id) ) { isSlotEmpty = false // return slot as full if slot is adjacent to heater-shaker for ot-2 and taller than 53mm } else if ( heaterShakerSlot != null && - deckDef.robot.model === OT2_ROBOT_TYPE && + robotType === OT2_ROBOT_TYPE && isSlotEmpty && labwareDefinition != null ) { diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index 65c3243590d..b7509372dcd 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -331,6 +331,7 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { liquidColor: selectLiquidColor, }} onClick={field.onChange} + menuPlacement="bottom" /> ) }} diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx index fb1d775b702..35bf74003a8 100644 --- a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx +++ b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx @@ -5,8 +5,10 @@ import { SketchPicker } from 'react-color' import { yupResolver } from '@hookform/resolvers/yup' import * as Yup from 'yup' import { Controller, useForm } from 'react-hook-form' +import styled from 'styled-components' import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data' import { + BORDERS, Btn, COLORS, DIRECTION_COLUMN, @@ -28,7 +30,7 @@ import * as labwareIngredActions from '../../labware-ingred/actions' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { swatchColors } from '../../components/swatchColors' import { HandleEnter } from '../../atoms/HandleEnter' -import { DescriptionField, LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' import type { ColorResult, RGBColor } from 'react-color' import type { ThunkDispatch } from 'redux-thunk' @@ -310,3 +312,13 @@ export function DefineLiquidsModal( ) } + +export const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` diff --git a/protocol-designer/src/organisms/DisabledScreen/index.tsx b/protocol-designer/src/organisms/DisabledScreen/index.tsx index bef9f0cd643..779ba446f40 100644 --- a/protocol-designer/src/organisms/DisabledScreen/index.tsx +++ b/protocol-designer/src/organisms/DisabledScreen/index.tsx @@ -23,6 +23,7 @@ export function DisabledScreen(): JSX.Element { backgroundColor={`${COLORS.black90}${COLORS.opacity40HexCode}`} overflow={OVERFLOW_HIDDEN} noPadding + zIndexOverlay={15} > - {t('swap')} + {t('swap_pipette_mounts')} @@ -565,11 +565,13 @@ export function EditInstrumentsModal( TYPOGRAPHY.textDecorationUnderline } > - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - + + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + {' '} )} @@ -594,4 +596,7 @@ const StyledLabel = styled.label` input[type='file'] { display: none; } + &:hover { + color: ${COLORS.blue50}; + } ` diff --git a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx index 96732965dea..2a1a8cf7e4e 100644 --- a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx +++ b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx @@ -26,7 +26,7 @@ describe('IncompatibleTipsModal', () => { render(props) screen.getByText('Incompatible tips') screen.getByText( - 'Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.' + 'Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.' ) fireEvent.click(screen.getByText('Show more tip types')) expect(vi.mocked(setFeatureFlags)).toHaveBeenCalled() diff --git a/protocol-designer/src/organisms/ProtocolNavBar/__tests__/ProtocolNavBar.test.tsx b/protocol-designer/src/organisms/ProtocolNavBar/__tests__/ProtocolNavBar.test.tsx index 2b000511486..85dcc4c9140 100644 --- a/protocol-designer/src/organisms/ProtocolNavBar/__tests__/ProtocolNavBar.test.tsx +++ b/protocol-designer/src/organisms/ProtocolNavBar/__tests__/ProtocolNavBar.test.tsx @@ -47,7 +47,6 @@ describe('ProtocolNavBar', () => { ] as TabProps[], hasTrashEntity: false, showLiquidOverflowMenu: vi.fn(), - isAddingHardwareOrLabware: false, } vi.mocked(getFileMetadata).mockReturnValue({ protocolName: 'mockProtocolName', @@ -73,14 +72,14 @@ describe('ProtocolNavBar', () => { }) it('should render protocol name and add hardware/labware - protocol name', () => { - props = { ...props, isAddingHardwareOrLabware: true } + props = { ...props, hasZoomInSlot: true } render(props) screen.getByText('mockProtocolName') screen.getByText('Add hardware/labware') }) it('should render protocol name and add hardware/labware - no protocol name', () => { - props = { ...props, isAddingHardwareOrLabware: true } + props = { ...props, hasZoomInSlot: true } vi.mocked(getFileMetadata).mockReturnValue({}) render(props) screen.getByText('Untitled protocol') diff --git a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx index cb47c8a20e6..07196f3d78b 100644 --- a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx +++ b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx @@ -29,19 +29,15 @@ interface ProtocolNavBarProps { tabs?: TabProps[] hasTrashEntity?: boolean showLiquidOverflowMenu?: (liquidOverflowMenu: boolean) => void - isAddingHardwareOrLabware?: boolean liquidPage?: boolean - isOffDeck?: boolean } export function ProtocolNavBar({ hasZoomInSlot, - isAddingHardwareOrLabware = false, tabs = [], hasTrashEntity, showLiquidOverflowMenu, liquidPage = false, - isOffDeck = false, }: ProtocolNavBarProps): JSX.Element { const { t } = useTranslation('starting_deck_state') const metadata = useSelector(getFileMetadata) @@ -49,28 +45,29 @@ export function ProtocolNavBar({ const navigate = useNavigate() const dispatch = useDispatch() + const showProtocolEditButtons = !(hasZoomInSlot || liquidPage) + + let metadataText = t('edit_protocol') + if (liquidPage) { + metadataText = t('add_liquid') + } else if (hasZoomInSlot) { + metadataText = t('add_hardware_labware') + } return ( - - {hasZoomInSlot ? null : } + + {showProtocolEditButtons ? : null} - + {metadata?.protocolName != null && metadata?.protocolName !== '' ? metadata?.protocolName : t('untitled_protocol')} - - {isAddingHardwareOrLabware || isOffDeck - ? t('add_hardware_labware') - : t('edit_protocol')} + + {metadataText} @@ -98,26 +95,30 @@ export function ProtocolNavBar({ ) } -const NavContainer = styled(Flex)` +const NavContainer = styled(Flex)<{ showShadow: boolean }>` + z-index: ${props => (props.showShadow === true ? 11 : 0)}; padding: ${SPACING.spacing12}; width: 100%; justify-content: ${JUSTIFY_SPACE_BETWEEN}; align-items: ${ALIGN_CENTER}; - box-shadow: 0px 1px 3px 0px ${COLORS.black90}${COLORS.opacity20HexCode}; + box-shadow: ${props => + props.showShadow + ? `0px 1px 3px 0px ${COLORS.black90}${COLORS.opacity20HexCode}` + : 'none'}; ` interface MetadataProps extends StyleProps { - isAddingHardwareOrLabware: boolean + showProtocolEditButtons: boolean } const MetadataContainer = styled.div.withConfig({ - shouldForwardProp: p => isntStyleProp(p) && p !== 'isAddingHardwareOrLabware', + shouldForwardProp: p => isntStyleProp(p) && p !== 'showProtocolEditButtons', })` display: flex; flex-direction: ${DIRECTION_COLUMN}; text-align: ${props => - props.isAddingHardwareOrLabware === true - ? TYPOGRAPHY.textAlignLeft - : TYPOGRAPHY.textAlignCenter}; + props.showProtocolEditButtons === true + ? TYPOGRAPHY.textAlignCenter + : TYPOGRAPHY.textAlignLeft}; // For screens between 600px and 767px, set width to 88px @media only screen and (max-width: 767px) { diff --git a/protocol-designer/src/organisms/SelectWellsModal/index.tsx b/protocol-designer/src/organisms/SelectWellsModal/index.tsx index 57ce723f278..7a1946861c2 100644 --- a/protocol-designer/src/organisms/SelectWellsModal/index.tsx +++ b/protocol-designer/src/organisms/SelectWellsModal/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import omit from 'lodash/omit' @@ -65,15 +65,22 @@ export const SelectWellsModal = ( (labwareId != null ? labwareEntities[labwareId]?.def : null) ?? null const pipette = pipetteId != null ? pipetteEntities[pipetteId] : null - const initialSelectedPrimaryWells = Array.isArray(wellFieldData) - ? arrayToWellGroup(wellFieldData as string[]) - : {} + const [selectedPrimaryWells, setSelectedPrimaryWells] = useState( + Array.isArray(wellFieldData) + ? arrayToWellGroup(wellFieldData as string[]) + : {} + ) + useEffect(() => { + if (Array.isArray(wellFieldData)) { + setSelectedPrimaryWells( + wellFieldData.length === 0 + ? {} + : arrayToWellGroup(wellFieldData as string[]) + ) + } + }, [wellFieldData]) - const [ - selectedPrimaryWells, - setSelectedPrimaryWells, - ] = React.useState(initialSelectedPrimaryWells) - const [highlightedWells, setHighlightedWells] = React.useState({}) + const [highlightedWells, setHighlightedWells] = useState({}) if (!isOpen) return null diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index 0ee7205fd97..37a080ac885 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -63,7 +63,11 @@ export const SlotInformation: FC = ({ @@ -76,7 +80,10 @@ export const SlotInformation: FC = ({ {liquids.length > 1 ? ( {stackInformation ?? t('none')} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx index 24e7b67dc85..fcc9956ad6b 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx @@ -1,18 +1,21 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' +import styled from 'styled-components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { + BORDERS, COLORS, DIRECTION_COLUMN, Flex, InputField, SPACING, StyledText, + TYPOGRAPHY, } from '@opentrons/components' -import { DescriptionField } from '../../atoms' import { HandleEnter } from '../../atoms/HandleEnter' import { analyticsEvent } from '../../analytics/actions' +import { ONBOARDING_FLOW_DURATION_EVENT } from '../../analytics/constants' import { WizardBody } from './WizardBody' import type { AnalyticsEvent } from '../../analytics/mixpanel' @@ -33,7 +36,7 @@ export function AddMetadata(props: AddMetadataProps): JSX.Element | null { const handleProceed = (): void => { const duration = new Date().getTime() - analyticsStartTime.getTime() const onboardingDuration: AnalyticsEvent = { - name: 'onboardingFlowDuration', + name: ONBOARDING_FLOW_DURATION_EVENT, properties: { duration: `${duration / 1000} seconds` }, } dispatch(analyticsEvent(onboardingDuration)) @@ -91,3 +94,13 @@ export function AddMetadata(props: AddMetadataProps): JSX.Element | null { ) } + +export const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx new file mode 100644 index 00000000000..6f0a6c6caa0 --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx @@ -0,0 +1,126 @@ +import { css } from 'styled-components' + +import { + ALIGN_CENTER, + COLORS, + DropdownMenu, + Flex, + JUSTIFY_FLEX_END, + JUSTIFY_FLEX_START, + JUSTIFY_SPACE_BETWEEN, + Link, + SPACING, + StyledText, + Tag, + TYPOGRAPHY, + WRAP, +} from '@opentrons/components' + +import { useResponsiveBreakpoints } from '../../resources/useResponsiveBreakpoints' +import type { DropdownMenuProps, TagProps } from '@opentrons/components' + +interface PDListItemCustomizeProps { + header: string + leftHeaderItem?: JSX.Element + onClick?: () => void + linkText?: string + label?: string + dropdown?: DropdownMenuProps + tag?: TagProps +} + +export function PDListItemCustomize({ + header, + leftHeaderItem, + onClick, + linkText, + label, + dropdown, + tag, +}: PDListItemCustomizeProps): JSX.Element { + const responsiveType = useResponsiveBreakpoints() + const isLargeScreen = responsiveType === 'xl' || responsiveType === 'lg' + const flexSize = responsiveType === 'xl' ? '0 0 1.5' : '0 0 1' + + const renderDropdownAndTag = (): JSX.Element => ( + <> + {label != null && ( + + {label} + + )} + {dropdown != null && ( + + + + )} + {tag != null && } + + ) + + const renderLinkButton = (): JSX.Element | null => + onClick != null && linkText != null ? ( + + {linkText} + + ) : null + + return ( + + + {leftHeaderItem != null && {leftHeaderItem}} + {header} + + + + {responsiveType !== 'xs' && ( + + {renderDropdownAndTag()} + + )} + {renderLinkButton()} + + + ) +} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 023b588d7c0..68223dbc183 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -8,7 +8,6 @@ import { EmptySelectorButton, Flex, ListItem, - ListItemCustomize, SPACING, StyledText, TYPOGRAPHY, @@ -22,6 +21,7 @@ import { getNumSlotsAvailable, } from './utils' import { HandleEnter } from '../../atoms/HandleEnter' +import { PDListItemCustomize as ListItemCustomize } from './PDListItemCustomize' import type { DropdownBorder } from '@opentrons/components' import type { AdditionalEquipment, WizardTileProps } from './types' @@ -158,7 +158,6 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { return ( { setValue( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 105fc2ecea5..1b69a3e02db 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -8,7 +8,6 @@ import { EmptySelectorButton, Flex, ListItem, - ListItemCustomize, SPACING, StyledText, TYPOGRAPHY, @@ -36,6 +35,7 @@ import { } from './constants' import { getNumOptions, getNumSlotsAvailable } from './utils' import { HandleEnter } from '../../atoms/HandleEnter' +import { PDListItemCustomize as ListItemCustomize } from '../CreateNewProtocolWizard/PDListItemCustomize' import type { DropdownBorder } from '@opentrons/components' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' @@ -258,7 +258,6 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { return ( - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - + + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + )} @@ -465,7 +468,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { transform="rotate(90deg)" /> - {t('swap_pipettes')} + {t('swap_pipette_mounts')} @@ -557,4 +560,7 @@ const StyledLabel = styled.label` input[type='file'] { display: none; } + &:hover { + color: ${COLORS.blue50}; + } ` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx index b5d69253435..442f6d0e4ed 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx @@ -1,6 +1,6 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import { css } from 'styled-components' +import { useState, useLayoutEffect } from 'react' import { ALIGN_CENTER, ALIGN_END, @@ -18,7 +18,12 @@ import { TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' -import temporaryImg from '../../assets/images/placeholder_image_delete.png' +import one from '../../assets/images/onboarding_animation_1.webm' +import two from '../../assets/images/onboarding_animation_2.webm' +import three from '../../assets/images/onboarding_animation_3.webm' +import four from '../../assets/images/onboarding_animation_4.webm' +import five from '../../assets/images/onboarding_animation_5.webm' +import six from '../../assets/images/onboarding_animation_6.webm' import { BUTTON_LINK_STYLE } from '../../atoms' interface WizardBodyProps { @@ -29,9 +34,18 @@ interface WizardBodyProps { disabled?: boolean goBack?: () => void subHeader?: string - imgSrc?: string tooltipOnDisabled?: string } + +const ONBOARDING_ANIMATIONS: Record = { + 1: one, + 2: two, + 3: three, + 4: four, + 5: five, + 6: six, +} + export function WizardBody(props: WizardBodyProps): JSX.Element { const { stepNumber, @@ -41,13 +55,26 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { subHeader, proceed, disabled = false, - imgSrc, tooltipOnDisabled, } = props const { t } = useTranslation('shared') const [targetProps, tooltipProps] = useHoverTooltip({ placement: 'top', }) + const [asset, setAsset] = useState(null) + const [loaded, setLoaded] = useState(false) + + useLayoutEffect(() => { + const videoAsset = ONBOARDING_ANIMATIONS[stepNumber] + setLoaded(false) + setAsset(videoAsset) + const timeout = setTimeout(() => { + setLoaded(true) + }, 100) + return () => { + clearTimeout(timeout) + } + }, [stepNumber]) return ( ) : null} - + - + css={css` + opacity: ${loaded ? 1 : 0}; + transition: opacity 0.5s ease-in-out; + `} + > + + ) } - -const StyledImg = styled.img` - border-radius: ${BORDERS.borderRadius16}; - max-height: 844px; -` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx index 085e2e76efc..f0493707774 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx @@ -37,6 +37,6 @@ describe('WizardBody', () => { expect(props.proceed).toHaveBeenCalled() fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(props.goBack).toHaveBeenCalled() - screen.getByRole('img', { name: '' }) + screen.getByLabelText('onboarding animation for page 1') }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx index 9669bf8ef14..47b942b10da 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx @@ -200,8 +200,9 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { } const isLabwareOccludedByThermocyclerLid = moduleOnDeck.type === THERMOCYCLER_MODULE_TYPE && - (moduleOnDeck.moduleState as ThermocyclerModuleState).lidOpen === - false + (moduleOnDeck.moduleState as ThermocyclerModuleState).lidOpen !== + true && + tab === 'protocolSteps' const tempInnerProps = getModuleInnerProps(moduleOnDeck.moduleState) const innerProps = @@ -210,9 +211,9 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { ...tempInnerProps, lidMotorState: (tempInnerProps as ThermocyclerVizProps).lidMotorState !== - 'closed' - ? 'open' - : 'closed', + 'open' + ? 'closed' + : 'open', } : tempInnerProps diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 4e2db1df4b8..62a5f92d46e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -52,11 +52,11 @@ import { getDismissedHints } from '../../../tutorial/selectors' import { createContainerAboveModule } from '../../../step-forms/actions/thunks' import { ConfirmDeleteStagingAreaModal } from '../../../organisms' import { BUTTON_LINK_STYLE } from '../../../atoms' -import { FIXTURES, MOAM_MODELS } from './constants' import { getSlotInformation } from '../utils' -import { getModuleModelsBySlot, getDeckErrors } from './utils' -import { MagnetModuleChangeContent } from './MagnetModuleChangeContent' +import { ALL_ORDERED_CATEGORIES, FIXTURES, MOAM_MODELS } from './constants' import { LabwareTools } from './LabwareTools' +import { MagnetModuleChangeContent } from './MagnetModuleChangeContent' +import { getModuleModelsBySlot, getDeckErrors } from './utils' import type { ModuleModel } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../../types' @@ -71,6 +71,8 @@ interface DeckSetupToolsProps { } | null } +export type CategoryExpand = Record + export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const { onCloseClick, setHoveredLabware, onDeckProps } = props const { t, i18n } = useTranslation(['starting_deck_state', 'shared']) @@ -117,6 +119,28 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const [tab, setTab] = useState<'hardware' | 'labware'>( moduleModels?.length === 0 || slot === 'offDeck' ? 'labware' : 'hardware' ) + + const setAllCategories = (state: boolean): Record => + ALL_ORDERED_CATEGORIES.reduce>( + (acc, category) => ({ ...acc, [category]: state }), + {} + ) + const allCategoriesExpanded = setAllCategories(true) + const allCategoriesCollapsed = setAllCategories(false) + const [ + areCategoriesExpanded, + setAreCategoriesExpanded, + ] = useState(allCategoriesCollapsed) + const [searchTerm, setSearchTerm] = useState('') + + useEffect(() => { + if (searchTerm !== '') { + setAreCategoriesExpanded(allCategoriesExpanded) + } else { + setAreCategoriesExpanded(allCategoriesCollapsed) + } + }, [searchTerm]) + const hasMagneticModule = Object.values(deckSetup.modules).some( module => module.type === MAGNETIC_MODULE_TYPE ) @@ -124,6 +148,12 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { Object.values(deckSetup.modules).find(module => module.slot === slot) ?.model === MAGNETIC_MODULE_V1 + const handleCollapseAllCategories = (): void => { + setAreCategoriesExpanded(allCategoriesCollapsed) + } + const handleResetSearchTerm = (): void => { + setSearchTerm('') + } const changeModuleWarning = useBlockingHint({ hintKey: 'change_magnet_module_model', handleCancel: () => { @@ -207,6 +237,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { ) } + const handleResetLabwareTools = (): void => { + handleCollapseAllCategories() + handleResetSearchTerm() + } + const handleClear = (): void => { onDeckProps?.setHoveredModule(null) onDeckProps?.setHoveredFixture(null) @@ -242,7 +277,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } } handleResetToolbox() + handleResetLabwareTools() setSelectedHardware(null) + if (selectedHardware != null) { + setTab('hardware') + } } const handleConfirm = (): void => { // clear entities first before recreating them @@ -548,7 +587,15 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { )} ) : ( - + )} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index dec0d114f83..be4f457429e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import reduce from 'lodash/reduce' import styled from 'styled-components' @@ -49,7 +49,11 @@ import { selectLabware, selectNestedLabware, } from '../../../labware-ingred/actions' -import { ORDERED_CATEGORIES } from './constants' +import { + ALL_ORDERED_CATEGORIES, + CUSTOM_CATEGORY, + ORDERED_CATEGORIES, +} from './constants' import { getLabwareIsRecommended, getLabwareCompatibleWithAdapter, @@ -59,8 +63,8 @@ import type { DeckSlotId, LabwareDefinition2 } from '@opentrons/shared-data' import type { ModuleOnDeck } from '../../../step-forms' import type { ThunkDispatch } from '../../../types' import type { LabwareDefByDefURI } from '../../../labware-defs' +import type { CategoryExpand } from './DeckSetupTools' -const CUSTOM_CATEGORY = 'custom' const STANDARD_X_DIMENSION = 127.75 const STANDARD_Y_DIMENSION = 85.48 const PLATE_READER_LOADNAME = @@ -68,10 +72,28 @@ const PLATE_READER_LOADNAME = interface LabwareToolsProps { slot: DeckSlotId setHoveredLabware: (defUri: string | null) => void + searchTerm: string + setSearchTerm: React.Dispatch> + areCategoriesExpanded: CategoryExpand + setAreCategoriesExpanded: React.Dispatch> + handleReset: () => void +} + +interface LabwareInfo { + uri: string + def: LabwareDefinition2 } export function LabwareTools(props: LabwareToolsProps): JSX.Element { - const { slot, setHoveredLabware } = props + const { + slot, + setHoveredLabware, + searchTerm, + setSearchTerm, + areCategoriesExpanded, + setAreCategoriesExpanded, + handleReset, + } = props const { t } = useTranslation(['starting_deck_state', 'shared']) const robotType = useSelector(getRobotType) const dispatch = useDispatch>() @@ -87,10 +109,6 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { selectedModuleModel, selectedNestedLabwareDefUri, } = zoomedInSlotInfo - const [selectedCategory, setSelectedCategory] = React.useState( - null - ) - const [searchTerm, setSearchTerm] = React.useState('') const searchFilter = (termToCheck: string): boolean => termToCheck.toLowerCase().includes(searchTerm.toLowerCase()) @@ -101,7 +119,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map( moduleId => modulesById[moduleId] ) - const [filterRecommended, setFilterRecommended] = React.useState( + const [filterRecommended, setFilterRecommended] = useState( moduleType != null ) // for OT-2 usage only due to H-S collisions @@ -110,11 +128,11 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { hardwareModule.type === HEATERSHAKER_MODULE_TYPE && getAreSlotsHorizontallyAdjacent(hardwareModule.slot, slot) ) - const [filterHeight, setFilterHeight] = React.useState( + const [filterHeight, setFilterHeight] = useState( robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false ) - const getLabwareCompatible = React.useCallback( + const getLabwareCompatible = useCallback( (def: LabwareDefinition2) => { // assume that custom (non-standard) labware is (potentially) compatible if (moduleType == null || !getLabwareDefIsStandard(def)) { @@ -125,7 +143,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { [moduleType] ) - const getIsLabwareFiltered = React.useCallback( + const getIsLabwareFiltered = useCallback( (labwareDef: LabwareDefinition2) => { const { dimensions, parameters } = labwareDef const { xDimension, yDimension } = dimensions @@ -155,11 +173,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] ) - const customLabwareURIs: string[] = React.useMemo( - () => Object.keys(customLabwareDefs), - [customLabwareDefs] - ) - const labwareByCategory = React.useMemo(() => { + + const labwareByCategory = useMemo(() => { return reduce< LabwareDefByDefURI, { [category: string]: LabwareDefinition2[] } @@ -184,28 +199,51 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { ) }, [permittedTipracks]) - const populatedCategories: { [category: string]: boolean } = React.useMemo( + const filteredLabwareByCategory: Record = useMemo( () => - ORDERED_CATEGORIES.reduce((acc, category) => { + ALL_ORDERED_CATEGORIES.reduce((acc, category) => { + if (category === 'custom') { + return { + ...acc, + [category]: filterRecommended + ? [] + : Object.entries(customLabwareDefs).reduce( + (accInner, [uri, def]) => { + return searchFilter(def.metadata.displayName) + ? [...accInner, { uri, def }] + : accInner + }, + [] + ), + } + } const isDeckLocationCategory = slot === 'offDeck' ? category !== 'adapter' : true - return category in labwareByCategory && - isDeckLocationCategory && - labwareByCategory[category].some(lw => - searchFilter(lw.metadata.displayName) - ) - ? { - ...acc, - [category]: labwareByCategory[category].some( - def => !getIsLabwareFiltered(def) - ), - } - : acc + if (!(category in labwareByCategory) || !isDeckLocationCategory) { + return { ...acc, [category]: [] } + } + return { + ...acc, + [category]: labwareByCategory[category].reduce( + (accInner, def) => { + return searchFilter(def.metadata.displayName) && + !getIsLabwareFiltered(def) + ? [...accInner, { def, uri: getLabwareDefURI(def) }] + : accInner + }, + [] + ), + } }, {}), [labwareByCategory, getIsLabwareFiltered, searchTerm] ) - const handleCategoryClick = (category: string): void => { - setSelectedCategory(selectedCategory === category ? null : category) + + const handleCategoryClick = (category: string, expand?: boolean): void => { + const updatedExpandState = { + ...areCategoriesExpanded, + [category]: expand ?? !areCategoriesExpanded[category], + } + setAreCategoriesExpanded(updatedExpandState) } return ( @@ -223,9 +261,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { size="medium" leftIcon="search" showDeleteIcon - onDelete={() => { - setSearchTerm('') - }} + onDelete={handleReset} /> {moduleType != null || (isNextToHeaterShaker && robotType === OT2_ROBOT_TYPE) ? ( @@ -253,7 +289,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { gridGap={SPACING.spacing4} paddingTop={SPACING.spacing8} > - {customLabwareURIs.length === 0 ? null : ( + {filteredLabwareByCategory[CUSTOM_CATEGORY].length > 0 ? ( - {customLabwareURIs.map((labwareURI, index) => ( - { - setHoveredLabware(null) - }} - setHovered={() => { - setHoveredLabware(labwareURI) - }} - buttonValue={labwareURI} - onChange={e => { - e.stopPropagation() - dispatch(selectLabware({ labwareDefUri: labwareURI })) - }} - isSelected={labwareURI === selectedLabwareDefUri} - /> - ))} + {filteredLabwareByCategory[CUSTOM_CATEGORY].map( + ({ uri }, index) => ( + { + setHoveredLabware(null) + }} + setHovered={() => { + setHoveredLabware(uri) + }} + buttonValue={uri} + onChange={e => { + e.stopPropagation() + dispatch(selectLabware({ labwareDefUri: uri })) + }} + isSelected={uri === selectedLabwareDefUri} + /> + ) + )} - )} + ) : null} {ORDERED_CATEGORIES.map(category => { - const isPopulated = populatedCategories[category] - if (isPopulated) { + if (filteredLabwareByCategory[category].length > 0) { return ( - {labwareByCategory[category]?.map((labwareDef, index) => { - const isFiltered = getIsLabwareFiltered(labwareDef) - const labwareURI = getLabwareDefURI(labwareDef) - const loadName = labwareDef.parameters.loadName - const isMatch = searchFilter( - labwareDef.metadata.displayName - ) - if (!isFiltered && isMatch) { - return ( - + {filteredLabwareByCategory[category]?.map( + ({ def, uri }, index) => { + const loadName = def.parameters.loadName + + return searchFilter(def.metadata.displayName) && + !getIsLabwareFiltered(def) ? ( + { setHoveredLabware(null) }} setHovered={() => { - setHoveredLabware(labwareURI) + setHoveredLabware(uri) }} id={`${index}_${category}_${loadName}`} - buttonText={labwareDef.metadata.displayName} - buttonValue={labwareURI} + buttonText={def.metadata.displayName} + buttonValue={uri} onChange={e => { e.stopPropagation() dispatch( selectLabware({ labwareDefUri: - labwareURI === selectedLabwareDefUri + uri === selectedLabwareDefUri ? null - : labwareURI, + : uri, }) ) // reset the nested labware def uri in case it is not compatible @@ -346,10 +376,10 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { }) ) }} - isSelected={labwareURI === selectedLabwareDefUri} + isSelected={uri === selectedLabwareDefUri} /> - {labwareURI === selectedLabwareDefUri && + {uri === selectedLabwareDefUri && getLabwareCompatibleWithAdapter(loadName) ?.length > 0 && ( {has96Channel && loadName === ADAPTER_96_CHANNEL @@ -440,10 +468,10 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { )} - - ) + + ) : null } - })} + )} @@ -464,8 +492,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { data-testid="customLabwareInput" type="file" onChange={e => { - setSelectedCategory(CUSTOM_CATEGORY) dispatch(createCustomLabwareDef(e)) + handleCategoryClick(CUSTOM_CATEGORY, true) }} /> diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx index 36cd92f4ec6..8cc15363ea6 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx @@ -1,19 +1,17 @@ import { useTranslation } from 'react-i18next' import { useState } from 'react' -import styled from 'styled-components' import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import { BORDERS, COLORS, - CURSOR_AUTO, - CURSOR_POINTER, DIRECTION_COLUMN, + Divider, Flex, + MenuItem, NO_WRAP, POSITION_ABSOLUTE, RobotCoordsForeignDiv, - SPACING, StyledText, useOnClickOutside, } from '@opentrons/components' @@ -68,6 +66,7 @@ interface SlotOverflowMenuProps { setShowMenuList: (value: SetStateAction) => void addEquipment: (slotId: string) => void menuListSlotPosition?: CoordinateTuple + invertY?: true } export function SlotOverflowMenu( props: SlotOverflowMenuProps @@ -77,6 +76,7 @@ export function SlotOverflowMenu( setShowMenuList, addEquipment, menuListSlotPosition, + invertY = false, } = props const { t } = useTranslation('starting_deck_state') const navigate = useNavigate() @@ -115,9 +115,16 @@ export function SlotOverflowMenu( const isLabwareTiprack = labwareOnSlot?.def.parameters.isTiprack ?? false const isLabwareAnAdapter = labwareOnSlot?.def.allowedRoles?.includes('adapter') ?? false + + const isTiprackAdapter = + labwareOnSlot?.def.parameters.quirks?.includes( + 'tiprackAdapterFor96Channel' + ) ?? false + const nestedLabwareOnSlot = Object.values(deckSetupLabware).find( lw => lw.slot === labwareOnSlot?.id ) + const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( ae => ae.location?.split('cutout')[1] === location ) @@ -168,19 +175,20 @@ export function SlotOverflowMenu( nestedLabwareOnSlot == null) || nestedLabwareOnSlot != null - const showEditAndLiquidsBtns = + const canRenameLabwareAndEditLiquids = (labwareOnSlot != null && !isLabwareAnAdapter && !isLabwareTiprack && + !isTiprackAdapter && nestedLabwareOnSlot == null) || - nestedLabwareOnSlot != null + (nestedLabwareOnSlot != null && !isTiprackAdapter) let position = ROBOT_BOTTOM_HALF_SLOTS.includes(location) ? BOTTOM_SLOT_Y_POSITION : TOP_SLOT_Y_POSITION if (showDuplicateBtn && !ROBOT_BOTTOM_HALF_SLOTS.includes(location)) { - position += showEditAndLiquidsBtns + position += canRenameLabwareAndEditLiquids ? TOP_SLOT_Y_POSITION_ALL_BUTTONS : TOP_SLOT_Y_POSITION_2_BUTTONS } @@ -233,7 +241,7 @@ export function SlotOverflowMenu( e.stopPropagation() }} > - { addEquipment(location) setShowMenuList(false) @@ -244,38 +252,37 @@ export function SlotOverflowMenu( ? t(isOffDeckLocation ? 'add_labware' : 'add_hw_lw') : t(isOffDeckLocation ? 'edit_labware' : 'edit_hw_lw')} - - {showEditAndLiquidsBtns ? ( - <> - { - setShowNickNameModal(true) - e.preventDefault() - e.stopPropagation() - }} - > - - {t('rename_lab')} - - - { - if (nestedLabwareOnSlot != null) { - dispatch(openIngredientSelector(nestedLabwareOnSlot.id)) - } else if (labwareOnSlot != null) { - dispatch(openIngredientSelector(labwareOnSlot.id)) - } - navigate('/liquids') - }} - > - - {selectionHasLiquids ? t('edit_liquid') : t('add_liquid')} - - - + + {canRenameLabwareAndEditLiquids ? ( + { + setShowNickNameModal(true) + e.preventDefault() + e.stopPropagation() + }} + > + + {t('rename_lab')} + + ) : null} + { + if (nestedLabwareOnSlot != null) { + dispatch(openIngredientSelector(nestedLabwareOnSlot.id)) + } else if (labwareOnSlot != null) { + dispatch(openIngredientSelector(labwareOnSlot.id)) + } + navigate('/liquids') + }} + disabled={!canRenameLabwareAndEditLiquids} + > + + {selectionHasLiquids ? t('edit_liquid') : t('add_liquid')} + + {showDuplicateBtn ? ( - { if ( labwareOnSlot != null && @@ -292,9 +299,10 @@ export function SlotOverflowMenu( {t('duplicate')} - + ) : null} - + { if (matchingLabware != null) { @@ -310,7 +318,7 @@ export function SlotOverflowMenu( {t(isOffDeckLocation ? 'clear_labware' : 'clear_slot')} - + ) @@ -327,7 +335,7 @@ export function SlotOverflowMenu( innerDivProps={{ style: { position: POSITION_ABSOLUTE, - transform: 'rotate(180deg) scaleX(-1)', + transform: `rotate(180deg) scaleX(-1) ${invertY ? 'scaleY(-1)' : ''}`, }, }} > @@ -337,19 +345,3 @@ export function SlotOverflowMenu( slotOverflowBody ) } - -const MenuButton = styled.button` - background-color: ${COLORS.transparent}; - border-radius: inherit; - cursor: ${CURSOR_POINTER}; - padding: ${SPACING.spacing8} ${SPACING.spacing12}; - border: none; - border-radius: inherit; - &:hover { - background-color: ${COLORS.blue10}; - } - &:disabled { - color: ${COLORS.grey40}; - cursor: ${CURSOR_AUTO}; - } -` diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx index 351cafc6fea..605af62250e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/HoveredItems.test.tsx @@ -11,6 +11,7 @@ import { import { LabwareRender, Module } from '@opentrons/components' import { selectors } from '../../../../labware-ingred/selectors' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' +import { getDesignerTab } from '../../../../file-data/selectors' import { FixtureRender } from '../FixtureRender' import { HoveredItems } from '../HoveredItems' import type * as OpentronsComponents from '@opentrons/components' @@ -18,6 +19,7 @@ import type * as OpentronsComponents from '@opentrons/components' vi.mock('../FixtureRender') vi.mock('../../../../labware-ingred/selectors') vi.mock('../../../../labware-defs/selectors') +vi.mock('../../../../file-data/selectors') vi.mock('@opentrons/components', async importOriginal => { const actual = await importOriginal() return { @@ -54,6 +56,7 @@ describe('HoveredItems', () => { vi.mocked(FixtureRender).mockReturnValue(
mock FixtureRender
) vi.mocked(LabwareRender).mockReturnValue(
mock LabwareRender
) vi.mocked(Module).mockReturnValue(
mock Module
) + vi.mocked(getDesignerTab).mockReturnValue('startingDeck') }) it('renders a hovered fixture', () => { render(props) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx index b4d444a1cf3..479724f3527 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx @@ -20,10 +20,6 @@ import { selectors } from '../../../../labware-ingred/selectors' import { createCustomLabwareDef } from '../../../../labware-defs/actions' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' import { getRobotType } from '../../../../file-data/selectors' -import { - selectLabware, - selectNestedLabware, -} from '../../../../labware-ingred/actions' import { LabwareTools } from '../LabwareTools' import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' @@ -48,6 +44,11 @@ describe('LabwareTools', () => { props = { slot: 'D3', setHoveredLabware: vi.fn(), + searchTerm: '', + setSearchTerm: vi.fn(), + areCategoriesExpanded: {}, + setAreCategoriesExpanded: vi.fn(), + handleReset: vi.fn(), } vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) @@ -80,18 +81,14 @@ describe('LabwareTools', () => { it('renders an empty slot with all the labware options', () => { render(props) screen.getByText('Add labware') - screen.getByText('Tube rack') - screen.getByText('Well plate') - screen.getByText('Reservoir') - screen.getByText('Aluminum block') - screen.getByText('Adapter') + screen.getByText('Tube racks') + screen.getByText('Well plates') + screen.getByText('Reservoirs') + screen.getByText('Aluminum blocks') + screen.getByText('Adapters') // click and expand well plate accordion fireEvent.click(screen.getAllByTestId('ListButton_noActive')[1]) - fireEvent.click( - screen.getByRole('label', { name: 'Corning 384 Well Plate' }) - ) - // set labware - expect(vi.mocked(selectLabware)).toHaveBeenCalled() + expect(props.setAreCategoriesExpanded).toBeCalled() }) it('renders deck slot and selects an adapter and labware', () => { vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ @@ -102,23 +99,10 @@ describe('LabwareTools', () => { selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, }) render(props) - screen.getByText('Adapter') + screen.getByText('Adapters') fireEvent.click(screen.getAllByTestId('ListButton_noActive')[4]) // set adapter - fireEvent.click( - screen.getByRole('label', { - name: 'Fixture Opentrons Universal Flat Heater-Shaker Adapter', - }) - ) - // set labware - screen.getByText('Adapter compatible labware') - screen.getByText('Fixture Corning 96 Well Plate 360 µL Flat') - fireEvent.click( - screen.getByRole('label', { - name: 'Fixture Corning 96 Well Plate 360 µL Flat', - }) - ) - expect(vi.mocked(selectNestedLabware)).toHaveBeenCalled() + expect(props.setAreCategoriesExpanded).toBeCalled() }) it('renders the custom labware flow', () => { diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx index a34f6a794b8..5d4341959e2 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx @@ -13,12 +13,14 @@ import { Module } from '@opentrons/components' import { selectors } from '../../../../labware-ingred/selectors' import { getInitialDeckSetup } from '../../../../step-forms/selectors' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' +import { getDesignerTab } from '../../../../file-data/selectors' import { LabwareOnDeck } from '../../../../components/DeckSetup/LabwareOnDeck' import { FixtureRender } from '../FixtureRender' import { SelectedHoveredItems } from '../SelectedHoveredItems' import type * as OpentronsComponents from '@opentrons/components' import type { LabwareDefinition2 } from '@opentrons/shared-data' +vi.mock('../../../../file-data/selectors') vi.mock('../../../../step-forms/selectors') vi.mock('../FixtureRender') vi.mock('../../../../labware-ingred/selectors') @@ -48,6 +50,7 @@ describe('SelectedHoveredItems', () => { hoveredFixture: null, slotPosition: [0, 0, 0], } + vi.mocked(getDesignerTab).mockReturnValue('startingDeck') vi.mocked(getInitialDeckSetup).mockReturnValue({ modules: {}, additionalEquipmentOnDeck: {}, diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx index 42c9c026d70..2ba0d4df60f 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx @@ -108,7 +108,7 @@ describe('SlotOverflowMenu', () => { expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalled() expect(props.setShowMenuList).toHaveBeenCalled() }) - it('renders 2 buttons when there is nothing on the slot', () => { + it('renders 3 buttons when there is nothing on the slot', () => { props.location = 'A1' render(props) fireEvent.click( @@ -116,7 +116,10 @@ describe('SlotOverflowMenu', () => { ) expect(props.addEquipment).toHaveBeenCalled() expect(props.setShowMenuList).toHaveBeenCalled() - expect(screen.getAllByRole('button')).toHaveLength(2) + expect(screen.getAllByRole('button')).toHaveLength(3) + expect(screen.getByRole('button', { name: 'Add liquid' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'Clear slot' })).toBeDisabled() + screen.getByTestId('divider') }) it('renders Edit liquid button when there is liquid on the labware', () => { vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({ diff --git a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts index 53571367f8b..e1acb64424d 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts @@ -56,6 +56,8 @@ export const ORDERED_CATEGORIES: string[] = [ 'aluminumBlock', 'adapter', ] +export const CUSTOM_CATEGORY = 'custom' +export const ALL_ORDERED_CATEGORIES = [CUSTOM_CATEGORY, ...ORDERED_CATEGORIES] export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { [TEMPERATURE_MODULE_TYPE]: [ diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index 524a19dfc1c..7a1c7c09be3 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -118,18 +118,16 @@ export const getLabwareIsRecommended = ( ): boolean => { // special-casing the thermocycler module V2 recommended labware since the thermocyclerModuleTypes // have different recommended labware - const moduleType = moduleModel != null ? getModuleType(moduleModel) : null - if (moduleModel === THERMOCYCLER_MODULE_V2) { - return ( - def.parameters.loadName === 'opentrons_96_wellplate_200ul_pcr_full_skirt' - ) - } else { - return moduleType != null - ? RECOMMENDED_LABWARE_BY_MODULE[moduleType].includes( - def.parameters.loadName - ) - : false + if (moduleModel == null) { + // permissive early exit if no module passed + return true } + const moduleType = getModuleType(moduleModel) + return moduleModel === THERMOCYCLER_MODULE_V2 + ? def.parameters.loadName === 'opentrons_96_wellplate_200ul_pcr_full_skirt' + : RECOMMENDED_LABWARE_BY_MODULE[moduleType].includes( + def.parameters.loadName + ) } export const getLabwareCompatibleWithAdapter = ( diff --git a/protocol-designer/src/pages/Designer/LabwareLabel.tsx b/protocol-designer/src/pages/Designer/LabwareLabel.tsx index 3c85c292071..440ddd14a47 100644 --- a/protocol-designer/src/pages/Designer/LabwareLabel.tsx +++ b/protocol-designer/src/pages/Designer/LabwareLabel.tsx @@ -1,5 +1,7 @@ import { useRef, useState, useEffect } from 'react' +import { useSelector } from 'react-redux' import { DeckLabelSet } from '@opentrons/components' +import { getDesignerTab } from '../../file-data/selectors' import type { DeckLabelProps } from '@opentrons/components' import type { CoordinateTuple, @@ -22,16 +24,20 @@ export const LabwareLabel = (props: ModuleLabelProps): JSX.Element => { nestedLabwareInfo = [], } = props const labelContainerRef = useRef(null) + const designerTab = useSelector(getDesignerTab) const [labelContainerHeight, setLabelContainerHeight] = useState(0) - const deckLabels = [ - ...nestedLabwareInfo, - { - text: labwareDef.metadata.displayName, - isSelected: isSelected, - isLast: isLast, - }, - ] + const deckLabels = + designerTab === 'startingDeck' + ? [ + ...nestedLabwareInfo, + { + text: labwareDef.metadata.displayName, + isSelected: isSelected, + isLast: isLast, + }, + ] + : [] useEffect(() => { if (labelContainerRef.current) { diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index 36fb6984a66..f631ee3391a 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + ALIGN_CENTER, BORDERS, COLORS, DIRECTION_COLUMN, @@ -26,6 +27,8 @@ import { SlotOverflowMenu } from '../DeckSetup/SlotOverflowMenu' import type { DeckSlotId } from '@opentrons/shared-data' import type { DeckSetupTabType } from '../types' +const OFFDECK_MAP_WIDTH = '41.625rem' + interface OffDeckDetailsProps extends DeckSetupTabType { addLabware: () => void } @@ -43,19 +46,30 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { const allWellContentsForActiveItem = useSelector( wellContentsSelectors.getAllWellContentsForActiveItem ) + const containerWidth = tab === 'startingDeck' ? '100vw' : '75vh' + const paddingLeftWithHover = + hoverSlot == null + ? `calc((${containerWidth} - (${SPACING.spacing24} * 2) - ${OFFDECK_MAP_WIDTH}) / 2)` + : SPACING.spacing24 + const paddingLeft = tab === 'startingDeck' ? paddingLeftWithHover : undefined + const padding = + tab === 'protocolSteps' + ? SPACING.spacing24 + : `${SPACING.spacing24} ${paddingLeft}` + const stepDetailsContainerWidth = `calc(((${containerWidth} - ${OFFDECK_MAP_WIDTH}) / 2) - (${SPACING.spacing24} * 3))` return ( {hoverSlot != null ? ( - + ) : null} { setShowMenuListForId(null) }} + menuListSlotPosition={[0, 0, 0]} + invertY /> ) : null} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx index 3267fd79256..5a89f0c9fe4 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx @@ -100,6 +100,7 @@ export function DisposalField(props: DisposalFieldProps): JSX.Element { pipetteId={pipette} flowRateType="blowout" volume={propsForFields.volume?.value ?? 0} + padding="0" tiprack={propsForFields.tipRack.value} /> 0 ? selectedWells.length.toString() : null + + const [primaryWellCount, setPrimaryWellCount] = useState(calculateWellCount) const pipette = pipetteId != null ? pipetteEntities[pipetteId] : null const nozzleType = getNozzleType(pipette, nozzles) + const previousNozzleType = useRef(nozzleType) + + useEffect(() => { + if (previousNozzleType.current !== nozzleType) { + setPrimaryWellCount(null) + updateValue([]) + previousNozzleType.current = nozzleType + } + }, [nozzleType, updateValue]) + + useEffect(() => { + setPrimaryWellCount(calculateWellCount) + }, [selectedWells]) const getModalKey = (): string => { return `${String(stepId)}${name}${pipetteId || 'noPipette'}${ diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index b930af715cb..2088409227e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import get from 'lodash/get' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -29,6 +29,10 @@ import { getFormLevelErrorsForUnsavedForm, getDynamicFieldFormErrorsForUnsavedForm, } from '../../../../step-forms/selectors' +import { + FORM_ERRORS_EVENT, + FORM_WARNINGS_EVENT, +} from '../../../../analytics/constants' import { CommentTools, HeaterShakerTools, @@ -50,6 +54,7 @@ import { import type { StepFieldName } from '../../../../steplist/fieldLevel' import type { FormData, StepType } from '../../../../form-types' import type { AnalyticsEvent } from '../../../../analytics/mixpanel' +import type { FormWarningType } from '../../../../steplist' import type { FieldPropsByName, FocusHandlers, @@ -154,6 +159,29 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { error => error.tab === 'dispense' && error.page === toolboxStep ) + const visibleFormWarningsTypes = visibleFormWarnings.map( + warning => warning.type + ) + const visibleFormErrorsTypes = visibleFormErrors.map(error => error.title) + + useEffect(() => { + const dispatchAnalyticsEvent = ( + eventName: string, + eventProperties: FormWarningType[] | string[] + ): void => { + if (eventProperties.length > 0) { + const event: AnalyticsEvent = { + name: eventName, + properties: { eventProperties }, + } + dispatch(analyticsEvent(event)) + } + } + + dispatchAnalyticsEvent(FORM_WARNINGS_EVENT, visibleFormWarningsTypes) + dispatchAnalyticsEvent(FORM_ERRORS_EVENT, visibleFormErrorsTypes) + }, [visibleFormWarningsTypes, visibleFormErrorsTypes]) + if (!ToolsComponent) { // early-exit if step form doesn't exist, this is a good check for when new steps // are added @@ -226,8 +254,8 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { setShowFormErrors(false) } else { setShowFormErrors(true) - handleScrollToTop() } + handleScrollToTop() } else { handleSaveClick() } @@ -276,6 +304,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { onClick={() => { setToolboxStep(0) setShowFormErrors(false) + handleScrollToTop() }} > {i18n.format(t('shared:back'), 'capitalize')} @@ -300,7 +329,11 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { } > -
+
{robotType === FLEX_ROBOT_TYPE ? ( <> diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx index 10dd04db7d5..813dc5ba022 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx @@ -186,6 +186,11 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { )} + + - - {enableReturnTip ? ( <> diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx index c3f94123220..b2183d7b678 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx @@ -64,7 +64,8 @@ export function ThermocyclerState(props: ThermocyclerStateProps): JSX.Element { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx index 36c82e86199..09135a62b49 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx @@ -79,7 +79,6 @@ interface StepSummaryProps { export function StepSummary(props: StepSummaryProps): JSX.Element | null { const { currentStep, stepDetails } = props const { t } = useTranslation(['protocol_steps', 'application']) - const labwareNicknamesById = useSelector(getLabwareNicknamesById) const additionalEquipmentEntities = useSelector( getAdditionalEquipmentEntities @@ -91,7 +90,6 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { return null } const { stepType } = currentStep - let stepSummaryContent: JSX.Element | null = null switch (stepType) { case 'mix': @@ -184,16 +182,20 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { ) : ( - - + + + + {stepSummaryContent != null ? ( - {stepSummaryContent} + + {stepSummaryContent} + ) : null} {stepDetails != null && stepDetails !== '' ? ( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx index 6dd9aebf527..afd08e74e21 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx @@ -17,17 +17,16 @@ import type { AdditionalEquipmentName } from '@opentrons/step-generation' import type { StepItemSourceDestRow, SubstepIdentifier, - WellIngredientNames, } from '../../../../steplist' interface MultichannelSubstepProps { trashName: AdditionalEquipmentName | null rowGroup: StepItemSourceDestRow[] - ingredNames: WellIngredientNames stepId: string substepIndex: number selectSubstep: (substepIdentifier: SubstepIdentifier) => void highlighted?: boolean + isSameLabware?: boolean } export function MultichannelSubstep( @@ -38,8 +37,8 @@ export function MultichannelSubstep( stepId, selectSubstep, substepIndex, - ingredNames, trashName, + isSameLabware, } = props const { t } = useTranslation('application') const [collapsed, setCollapsed] = useState(true) @@ -105,11 +104,11 @@ export function MultichannelSubstep( trashName={trashName} key={rowKey} volume={row.volume} - ingredNames={ingredNames} source={row.source} dest={row.dest} stepId={stepId} substepIndex={substepIndex} + isSameLabware={isSameLabware} /> ) })} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx index 7973c5ef376..82da4015e45 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx @@ -4,7 +4,6 @@ import { MultichannelSubstep } from './MultichannelSubstep' import type { SourceDestSubstepItem, SubstepIdentifier, - WellIngredientNames, } from '../../../../steplist' import { useSelector } from 'react-redux' import { @@ -14,13 +13,12 @@ import { interface PipettingSubstepsProps { substeps: SourceDestSubstepItem - ingredNames: WellIngredientNames selectSubstep: (substepIdentifier: SubstepIdentifier) => void hoveredSubstep?: SubstepIdentifier | null } export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { - const { substeps, selectSubstep, hoveredSubstep, ingredNames } = props + const { substeps, selectSubstep, hoveredSubstep } = props const stepId = substeps.parentStepId const formData = useSelector(getSavedStepForms)[stepId] const additionalEquipment = useSelector(getAdditionalEquipment) @@ -30,23 +28,32 @@ export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { ? additionalEquipment[destLocationId]?.name : null + const isSameLabware = formData.aspirate_labware === formData.dispense_labware + const renderSubsteps = substeps.multichannel - ? substeps.multiRows.map((rowGroup, groupKey) => ( - - )) + ? substeps.multiRows.map((rowGroup, groupKey) => { + const filteredRowGroup = rowGroup.filter( + item => item.source !== undefined + ) + if (filteredRowGroup.length === 0) return null + + return ( + + ) + }) : substeps.rows.map((row, substepIndex) => ( )) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx index 4ed55987f08..9cde39888ce 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx @@ -45,8 +45,8 @@ import type { IconName } from '@opentrons/components' import type { StepIdType } from '../../../../form-types' import type { BaseState } from '../../../../types' -const STARTING_DECK_STATE = 'Starting deck state' -const FINAL_DECK_STATE = 'Final deck state' +const STARTING_DECK_STATE = 'Starting deck' +const FINAL_DECK_STATE = 'Ending deck' const PX_HEIGHT_TO_TOP_OF_CONTAINER = 32 export interface StepContainerProps { title: string diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx index 7418746c882..3dd7f529743 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx @@ -17,6 +17,7 @@ import { hoverOnStep, toggleViewSubstep, } from '../../../../ui/steps/actions/actions' +import { OPEN_STEP_DETAILS_EVENT } from '../../../../analytics/constants' import { getBatchEditFormHasUnsavedChanges, getCurrentFormHasUnsavedChanges, @@ -63,7 +64,9 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { const isPipetteStep = savedStepFormData.stepType === 'moveLiquid' || savedStepFormData.stepType === 'mix' - const isThermocyclerProfile = savedStepFormData.stepType === 'thermocycler' + const isThermocyclerProfile = + savedStepFormData.stepType === 'thermocycler' && + savedStepFormData.thermocyclerFormType === 'thermocyclerProfile' const duplicateStep = ( stepId: StepIdType @@ -81,7 +84,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { } const selectViewDetailsEvent: AnalyticsEvent = { - name: 'openStepDetails', + name: OPEN_STEP_DETAILS_EVENT, properties: {}, } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx index a731a975b48..7c62f23140d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx @@ -1,75 +1,52 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import noop from 'lodash/noop' -import { AIR } from '@opentrons/step-generation' import { ALIGN_CENTER, - COLORS, DIRECTION_COLUMN, DeckInfoLabel, Flex, JUSTIFY_SPACE_BETWEEN, - LiquidIcon, ListItem, SPACING, StyledText, Tag, } from '@opentrons/components' -import { selectors } from '../../../../labware-ingred/selectors' -import { - MIXED_WELL_COLOR, - swatchColors, -} from '../../../../components/swatchColors' -import { compactPreIngreds, formatVolume } from './utils' +import { formatVolume } from './utils' import type { AdditionalEquipmentName } from '@opentrons/step-generation' -import type { - SubstepIdentifier, - SubstepWellData, - WellIngredientNames, -} from '../../../../steplist' +import type { SubstepIdentifier, SubstepWellData } from '../../../../steplist' interface SubstepProps { trashName: AdditionalEquipmentName | null - ingredNames: WellIngredientNames stepId: string substepIndex: number volume?: number | string | null source?: SubstepWellData dest?: SubstepWellData selectSubstep?: (substepIdentifier: SubstepIdentifier) => void + isSameLabware?: boolean } function SubstepComponent(props: SubstepProps): JSX.Element { const { volume, - ingredNames, stepId, substepIndex, source, dest, trashName, selectSubstep: propSelectSubstep, + isSameLabware, } = props - const { t } = useTranslation(['application', 'protocol_steps', 'shared']) - const compactedSourcePreIngreds = source - ? compactPreIngreds(source.preIngreds) - : {} + const { i18n, t } = useTranslation([ + 'application', + 'protocol_steps', + 'shared', + ]) const selectSubstep = propSelectSubstep ?? noop - const ingredIds: string[] = Object.keys(compactedSourcePreIngreds) - const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) - const noColor = ingredIds.filter(id => id !== AIR).length === 0 - let color = MIXED_WELL_COLOR - if (ingredIds.length === 1) { - color = - liquidDisplayColors[Number(ingredIds[0])] ?? swatchColors(ingredIds[0]) - } else if (noColor) { - color = COLORS.transparent - } - const volumeTag = ( ) - const isMix = source?.well === dest?.well + const isMix = source?.well === dest?.well && isSameLabware return ( - {ingredIds.length > 0 ? ( - - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {t('protocol_steps:mix')} @@ -121,33 +88,27 @@ function SubstepComponent(props: SubstepProps): JSX.Element { {t('protocol_steps:in')} ) : ( <> - - - {ingredIds.length > 0 ? ( - - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {source != null ? ( + {source != null ? ( + + {t('protocol_steps:aspirated')} @@ -157,53 +118,51 @@ function SubstepComponent(props: SubstepProps): JSX.Element { {t('protocol_steps:from')} - ) : null} - - - - - {ingredIds.length > 0 ? ( - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {dest != null || trashName != null ? ( - - - {t('protocol_steps:dispensed')} - - {volumeTag} - - {t('protocol_steps:into')} - + + + ) : null} + {dest != null ? ( + + + {dest != null || trashName != null ? ( + + + {t('protocol_steps:dispensed')} + + {volumeTag} + + {t('protocol_steps:into')} + - - - ) : null} - - + + + ) : null} + + + ) : null} )} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx index 3a1ddff44f0..e2460741ebf 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx @@ -8,7 +8,6 @@ import { StyledText, Toolbox, } from '@opentrons/components' -import { selectors as labwareIngredSelectors } from '../../../../labware-ingred/selectors' import { getSubsteps } from '../../../../file-data/selectors' import { getHoveredSubstep } from '../../../../ui/steps' import { @@ -40,7 +39,6 @@ export function SubstepsToolbox( const substeps = useSelector(getSubsteps)[stepId] const formData = useSelector(getSavedStepForms)[stepId] const hoveredSubstep = useSelector(getHoveredSubstep) - const ingredNames = useSelector(labwareIngredSelectors.getLiquidNamesById) const highlightSubstep = (payload: SubstepIdentifier): HoverOnSubstepAction => dispatch(hoverOnSubstep(payload)) @@ -85,7 +83,6 @@ export function SubstepsToolbox( ) : ( { > { }} /> - + ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts index c7f6f812dc2..56b78507a12 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts @@ -1,8 +1,6 @@ import round from 'lodash/round' -import omitBy from 'lodash/omitBy' import uniq from 'lodash/uniq' import { UAParser } from 'ua-parser-js' -import type { WellIngredientVolumeData } from '../../../../steplist' import type { StepIdType } from '../../../../form-types' export const capitalizeFirstLetterAfterNumber = (title: string): string => @@ -29,31 +27,6 @@ export const formatPercentage = (part: number, total: number): string => { return `${round((part / total) * 100, PERCENTAGE_DECIMALS_ALLOWED)}%` } -export const compactPreIngreds = ( - preIngreds: WellIngredientVolumeData -): Partial< - | { - [ingredId: string]: - | { - volume: number - } - | undefined - } - | { - [well: string]: - | { - [ingredId: string]: { - volume: number - } - } - | undefined - } -> => { - return omitBy(preIngreds, ingred => { - return typeof ingred?.volume === 'number' && ingred.volume <= 0 - }) -} - export const getMetaSelectedSteps = ( multiSelectItemIds: StepIdType[] | null, stepId: StepIdType, diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx index f68928c3488..dcfb3dfd58b 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -10,6 +10,7 @@ import { import { getSelectedStepId, getSelectedSubstep, + getSelectedTerminalItemId, } from '../../../../ui/steps/selectors' import { getDesignerTab } from '../../../../file-data/selectors' import { getEnableHotKeysDisplay } from '../../../../feature-flags/selectors' @@ -60,6 +61,7 @@ describe('ProtocolSteps', () => { vi.mocked(DeckSetupContainer).mockReturnValue(
mock DeckSetupContainer
) + vi.mocked(getSelectedTerminalItemId).mockReturnValue(null) vi.mocked(OffDeck).mockReturnValue(
mock OffDeck
) vi.mocked(getUnsavedForm).mockReturnValue(null) vi.mocked(getSelectedSubstep).mockReturnValue(null) @@ -95,8 +97,8 @@ describe('ProtocolSteps', () => { it('renders the hot keys display', () => { render() screen.getByText('Double-click to edit') - screen.getByText('Shift + Click to select all') - screen.getByText('Command + Click for multi-select') + screen.getByText('⇧ + click to select range') + screen.getByText('^/⌘ + click to select multiple') }) it('renders the current step name', () => { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index b5e5810c2da..8cb7d8fbfe2 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -3,12 +3,10 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, - Box, COLORS, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, - JUSTIFY_FLEX_END, JUSTIFY_FLEX_START, JUSTIFY_SPACE_BETWEEN, POSITION_FIXED, @@ -27,6 +25,8 @@ import { getSelectedSubstep, getSelectedStepId, getHoveredStepId, + getSelectedTerminalItemId, + getHoveredTerminalItemId, } from '../../../ui/steps/selectors' import { DeckSetupContainer } from '../DeckSetup' import { OffDeck } from '../Offdeck' @@ -42,6 +42,8 @@ const CONTENT_MAX_WIDTH = '46.9375rem' export function ProtocolSteps(): JSX.Element { const { i18n, t } = useTranslation('starting_deck_state') const formData = useSelector(getUnsavedForm) + const selectedTerminalItem = useSelector(getSelectedTerminalItemId) + const hoveredTerminalItem = useSelector(getHoveredTerminalItemId) const isMultiSelectMode = useSelector(getIsMultiSelectMode) const selectedSubstep = useSelector(getSelectedSubstep) const enableHoyKeyDisplay = useSelector(getEnableHotKeysDisplay) @@ -89,16 +91,19 @@ export function ProtocolSteps(): JSX.Element { {tab === 'protocolSteps' ? ( ) : null} - - {currentStep != null ? ( + + {currentStep != null && hoveredTerminalItem == null ? ( {i18n.format(currentStep.stepName, 'capitalize')} ) : null} + {(hoveredTerminalItem != null || selectedTerminalItem != null) && + currentHoveredStepId == null ? ( + + {t(hoveredTerminalItem ?? selectedTerminalItem)} + + ) : null} + ) : null} + {selectedTerminalItem != null && currentHoveredStepId == null ? ( + + ) : null} {enableHoyKeyDisplay ? ( - - - - - - - + + + + + ) : null} {formData == null && selectedSubstep ? ( diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index 3469a843eb4..8993d271420 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -151,14 +151,10 @@ export function Designer(): JSX.Element { ) : null} {tab === 'startingDeck' ? ( diff --git a/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx b/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx index 20f6c8264d3..0f5bacc7eea 100644 --- a/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx @@ -1,6 +1,5 @@ -import { useCallback, useMemo } from 'react' +import { useMemo } from 'react' import { useSelector } from 'react-redux' -import debounce from 'lodash/debounce' import { ALIGN_CENTER, @@ -27,8 +26,6 @@ import type { import type { Dispatch, SetStateAction } from 'react' -const DEBOUNCE_DURATION_MS = 600 - interface SlotHoverProps { hover: string | null setHover: Dispatch> @@ -57,13 +54,6 @@ export function SlotHover(props: SlotHoverProps): JSX.Element | null { deckDef.cutoutFixtures ) ?? 'cutoutD1' - const debouncedSetHover = useCallback( - debounce((slotId: string | null) => { - setHover(slotId) - }, DEBOUNCE_DURATION_MS), - [setHover] - ) - // return null for TC slots if (slotPosition === null || (hasTCOnSlot && tcSlots.includes(slotId))) return null @@ -126,10 +116,10 @@ export function SlotHover(props: SlotHoverProps): JSX.Element | null { opacity: hoverOpacity, flex: '1', onMouseEnter: () => { - debouncedSetHover(slotId) + setHover(slotId) }, onMouseLeave: () => { - debouncedSetHover(null) + setHover(null) }, }} > @@ -152,10 +142,10 @@ export function SlotHover(props: SlotHoverProps): JSX.Element | null { opacity: hoverOpacity, flex: '1', onMouseEnter: () => { - debouncedSetHover(slotId) + setHover(slotId) }, onMouseLeave: () => { - debouncedSetHover(null) + setHover(null) }, }} > diff --git a/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx b/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx index c7b345bf4da..c1903fcc587 100644 --- a/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx @@ -11,6 +11,7 @@ import { Btn, TYPOGRAPHY, ToggleGroup, + Box, } from '@opentrons/components' import { BUTTON_LINK_STYLE } from '../../atoms' @@ -88,11 +89,13 @@ export function StartingDeck({ ) : ( )} - + + + ) diff --git a/protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts b/protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts new file mode 100644 index 00000000000..e9efd9282f6 --- /dev/null +++ b/protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useResponsiveBreakpoints } from '../useResponsiveBreakpoints' + +describe('useResponsiveBreakpoints', () => { + const resizeWindow = (width: number) => { + window.innerWidth = width + window.dispatchEvent(new Event('resize')) + } + + it('should return "xs" when width is less than BREAKPOINT_SM_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(900) + }) + expect(result.current).toBe('xs') + }) + + it('should return "sm" when width is between BREAKPOINT_SM_WIDTH and BREAKPOINT_MD_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1000) + }) + expect(result.current).toBe('sm') + }) + + it('should return "md" when width is between BREAKPOINT_MD_WIDTH and BREAKPOINT_LG_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1120) + }) + expect(result.current).toBe('md') + }) + + it('should return "lg" when width is between BREAKPOINT_LG_WIDTH and BREAKPOINT_XL_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1200) + }) + expect(result.current).toBe('lg') + }) + + it('should return "xl" when width is greater than BREAKPOINT_XL_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1500) + }) + expect(result.current).toBe('xl') + }) +}) diff --git a/protocol-designer/src/resources/useResponsiveBreakpoints.ts b/protocol-designer/src/resources/useResponsiveBreakpoints.ts new file mode 100644 index 00000000000..b815322f983 --- /dev/null +++ b/protocol-designer/src/resources/useResponsiveBreakpoints.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react' + +type ResponsiveType = 'xl' | 'lg' | 'md' | 'sm' | 'xs' + +const BREAKPOINT_XL_WIDTH = 1440 +const BREAKPOINT_LG_WIDTH = 1160 +const BREAKPOINT_MD_WIDTH = 1106 +const BREAKPOINT_SM_WIDTH = 978 + +export const useResponsiveBreakpoints = (): ResponsiveType => { + const [responsiveType, setResponsiveType] = useState('xl') + + useEffect(() => { + const handleResize = (): void => { + const width = window.innerWidth + if (width < BREAKPOINT_SM_WIDTH) { + setResponsiveType('xs') + } else if (width < BREAKPOINT_MD_WIDTH) { + setResponsiveType('sm') + } else if (width < BREAKPOINT_LG_WIDTH) { + setResponsiveType('md') + } else if (width <= BREAKPOINT_XL_WIDTH) { + setResponsiveType('lg') + } else { + setResponsiveType('xl') + } + } + handleResize() + window.addEventListener('resize', handleResize) + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return responsiveType +} diff --git a/protocol-designer/src/steplist/formLevel/createBlankForm.ts b/protocol-designer/src/steplist/formLevel/createBlankForm.ts index c8e22a1fb88..764529dc379 100644 --- a/protocol-designer/src/steplist/formLevel/createBlankForm.ts +++ b/protocol-designer/src/steplist/formLevel/createBlankForm.ts @@ -17,7 +17,7 @@ const getStepType = (stepType: StepType): string => { return 'heater-shaker' } case 'moveLabware': { - return 'move labware' + return 'move' } case 'moveLiquid': { return 'transfer' diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts index de8a28df5c3..ab2a53eba81 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts @@ -164,6 +164,23 @@ const updatePatchOnTiprackChange = ( return patch } +const updatePatchOnNozzleChange = ( + patch: FormPatch, + rawForm: FormData, + pipetteEntities: PipetteEntities +): FormPatch => { + if ( + Object.values(pipetteEntities).find(pip => pip.spec.channels === 96) && + fieldHasChanged(rawForm, patch, 'nozzles') + ) { + return { + ...patch, + ...getDefaultFields('wells'), + } + } + return patch +} + export function dependentFieldsUpdateMix( originalPatch: FormPatch, rawForm: FormData, // raw = NOT hydrated @@ -188,5 +205,7 @@ export function dependentFieldsUpdateMix( ), chainPatch => updatePatchOnPipetteChange(chainPatch, rawForm), chainPatch => updatePatchOnTiprackChange(chainPatch, rawForm), + chainPatch => + updatePatchOnNozzleChange(chainPatch, rawForm, pipetteEntities), ]) } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts index 752a6629386..847d45ed4bc 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts @@ -543,29 +543,30 @@ const updatePatchOnPipetteChannelChange = ( const sourceLabwareId: string = appliedPatch.aspirate_labware as string const destLabwareId: string = appliedPatch.dispense_labware as string const sourceLabware = labwareEntities[sourceLabwareId] - const sourceLabwareDef = sourceLabware.def const destLabware = labwareEntities[destLabwareId] - update = { - aspirate_wells: getAllWellsFromPrimaryWells( - appliedPatch.aspirate_wells as string[], - sourceLabwareDef, - channels as 8 | 96 - ), - dispense_wells: - destLabwareId.includes('trashBin') || - destLabwareId.includes('wasteChute') - ? getDefaultWells({ - labwareId: destLabwareId, - pipetteId, - labwareEntities, - pipetteEntities, - }) - : getAllWellsFromPrimaryWells( - appliedPatch.dispense_wells as string[], - destLabware.def, - channels as 8 | 96 - ), + if (sourceLabwareId != null && destLabwareId != null) { + update = { + aspirate_wells: getAllWellsFromPrimaryWells( + appliedPatch.aspirate_wells as string[], + sourceLabware.def, + channels as 8 | 96 + ), + dispense_wells: + destLabwareId.includes('trashBin') || + destLabwareId.includes('wasteChute') + ? getDefaultWells({ + labwareId: destLabwareId, + pipetteId, + labwareEntities, + pipetteEntities, + }) + : getAllWellsFromPrimaryWells( + appliedPatch.dispense_wells as string[], + destLabware.def, + channels as 8 | 96 + ), + } } } @@ -579,7 +580,9 @@ function updatePatchOnWellRatioChange( const appliedPatch = { ...rawForm, ...patch } const isDisposalLocation = rawForm.dispense_labware?.includes('wasteChute') || - rawForm.dispense_labware?.includes('trashBin') + rawForm.dispense_labware?.includes('trashBin') || + rawForm.dispense_labware?.includes('movableTrash') || + rawForm.dispense_labware?.includes('fixedTrash') const prevWellRatio = getWellRatio( rawForm.aspirate_wells as string[], @@ -669,6 +672,24 @@ export function updatePatchBlowoutFields( return patch } + +const updatePatchOnNozzleChange = ( + patch: FormPatch, + rawForm: FormData, + pipetteEntities: PipetteEntities +): FormPatch => { + if ( + Object.values(pipetteEntities).find(pip => pip.spec.channels === 96) && + fieldHasChanged(rawForm, patch, 'nozzles') + ) { + return { + ...patch, + ...getDefaultFields('aspirate_wells', 'dispense_wells'), + } + } + return patch +} + export function dependentFieldsUpdateMoveLiquid( originalPatch: FormPatch, rawForm: FormData, // raw = NOT hydrated @@ -706,5 +727,7 @@ export function dependentFieldsUpdateMoveLiquid( clampDispenseAirGapVolume(chainPatch, rawForm, pipetteEntities), chainPatch => updatePatchOnTiprackChange(chainPatch, rawForm, pipetteEntities), + chainPatch => + updatePatchOnNozzleChange(chainPatch, rawForm, pipetteEntities), ]) } diff --git a/protocol-designer/src/ui/steps/actions/actions.ts b/protocol-designer/src/ui/steps/actions/actions.ts index da7757c1b21..ab3a0f19263 100644 --- a/protocol-designer/src/ui/steps/actions/actions.ts +++ b/protocol-designer/src/ui/steps/actions/actions.ts @@ -2,6 +2,11 @@ import last from 'lodash/last' import { analyticsEvent } from '../../../analytics/actions' import { PRESAVED_STEP_ID } from '../../../steplist/types' import { selectors as stepFormSelectors } from '../../../step-forms' +import { + DESELECT_ALL_STEPS_EVENT, + EXIT_BATCH_EDIT_MODE_EVENT, + SELECT_ALL_STEPS_EVENT, +} from '../../../analytics/constants' import { getMultiSelectLastSelected } from '../selectors' import { resetScrollElements } from '../utils' import type { Timeline } from '@opentrons/step-generation' @@ -187,7 +192,7 @@ export const selectAllSteps = (): ThunkAction< // dispatch an analytics event to indicate all steps have been selected // because there is no 'SELECT_ALL_STEPS' action that middleware can catch const selectAllStepsEvent: AnalyticsEvent = { - name: 'selectAllSteps', + name: SELECT_ALL_STEPS_EVENT, properties: {}, } dispatch(analyticsEvent(selectAllStepsEvent)) @@ -220,13 +225,13 @@ export const deselectAllSteps = ( // for analytics purposes we want to differentiate between // deselecting all, and using the "exit batch edit mode" button const exitBatchEditModeEvent: AnalyticsEvent = { - name: 'exitBatchEditMode', + name: EXIT_BATCH_EDIT_MODE_EVENT, properties: {}, } dispatch(analyticsEvent(exitBatchEditModeEvent)) } else { const deselectAllStepsEvent: AnalyticsEvent = { - name: 'deselectAllSteps', + name: DESELECT_ALL_STEPS_EVENT, properties: {}, } dispatch(analyticsEvent(deselectAllStepsEvent)) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index 46b2c86bd40..c3346e33351 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -34,6 +34,7 @@ def _build_run( liquids=[], wells=[], files=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) return MaintenanceRun.construct( @@ -50,6 +51,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + liquidClasses=state_summary.liquidClasses, hasEverEnteredErrorRecovery=state_summary.hasEverEnteredErrorRecovery, ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py index e4c5971f5d1..8bde7ea7aff 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py @@ -12,6 +12,7 @@ LabwareOffset, LabwareOffsetCreate, Liquid, + LiquidClassRecordWithId, ) from robot_server.service.json_api import ResourceModel @@ -67,6 +68,10 @@ class MaintenanceRun(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 1e377aec3dd..61a66866bb0 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -19,6 +19,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, ) @@ -185,6 +186,10 @@ class CompletedAnalysis(BaseModel): default_factory=list, description="Liquids used by the protocol", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + default_factory=list, + description="Liquid classes used by the protocol", + ) errors: List[ErrorOccurrence] = Field( ..., description=( diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 71d170c6581..2f46f7857cb 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -19,6 +19,7 @@ LoadedLabware, LoadedModule, Liquid, + LiquidClassRecordWithId, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree @@ -152,6 +153,7 @@ async def update( pipettes: List[LoadedPipette], errors: List[ErrorOccurrence], liquids: List[Liquid], + liquidClasses: List[LiquidClassRecordWithId], ) -> None: """Promote a pending analysis to completed, adding details of its results. @@ -167,6 +169,7 @@ async def update( errors: See `CompletedAnalysis.errors`. Also used to infer whether the completed analysis result is `OK` or `NOT_OK`. liquids: See `CompletedAnalysis.liquids`. + liquidClasses: See `CompletedAnalysis.liquidClasses`. robot_type: See `CompletedAnalysis.robotType`. """ protocol_id = self._pending_store.get_protocol_id(analysis_id=analysis_id) @@ -201,6 +204,7 @@ async def update( pipettes=pipettes, errors=errors, liquids=liquids, + liquidClasses=liquidClasses, ) completed_analysis_resource = CompletedAnalysisResource( id=completed_analysis.id, @@ -241,6 +245,7 @@ async def save_initialization_failed_analysis( pipettes=[], errors=errors, liquids=[], + liquidClasses=[], ) completed_analysis_resource = CompletedAnalysisResource( id=completed_analysis.id, diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index 89387c5cefc..cf1d0687062 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -107,6 +107,7 @@ async def analyze( pipettes=result.state_summary.pipettes, errors=result.state_summary.errors, liquids=result.state_summary.liquids, + liquidClasses=result.state_summary.liquidClasses, ) async def update_to_failed_analysis( @@ -136,6 +137,7 @@ async def update_to_failed_analysis( ) ], liquids=[], + liquidClasses=[], ) def __del__(self) -> None: diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 6825dfb14a3..cbbcd022eb6 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -66,6 +66,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + liquidClasses=state_summary.liquidClasses, outputFileIds=state_summary.files, runTimeParameters=run_time_parameters, ) @@ -80,6 +81,7 @@ def _build_run( pipettes=[], modules=[], liquids=[], + liquidClasses=[], wells=[], files=[], hasEverEnteredErrorRecovery=False, @@ -124,6 +126,7 @@ def _build_run( completedAt=state.completedAt, startedAt=state.startedAt, liquids=state.liquids, + liquidClasses=state.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=state.files, hasEverEnteredErrorRecovery=state.hasEverEnteredErrorRecovery, diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index b18906fb39d..4d5da7560c0 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ LabwareOffset, LabwareOffsetCreate, Liquid, + LiquidClassRecordWithId, CommandNote, ) from opentrons.protocol_engine.types import ( @@ -134,6 +135,10 @@ class Run(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", @@ -215,6 +220,10 @@ class BadRun(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", diff --git a/robot-server/tests/data_files/test_data_files_store.py b/robot-server/tests/data_files/test_data_files_store.py index 581577d0a16..9a9b722e6ec 100644 --- a/robot-server/tests/data_files/test_data_files_store.py +++ b/robot-server/tests/data_files/test_data_files_store.py @@ -99,6 +99,7 @@ def _get_sample_analysis_resource( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index 717280a6703..516221c500c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -590,6 +590,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv6 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index 35801f8719a..022c86da35e 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -623,6 +623,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv8 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index f85e307e961..961a9a26601 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -626,6 +626,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv8 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index fd98c29a2dc..580688e6e65 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -51,6 +51,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] runTimeParameters: [] outputFileIds: [] protocolId: '{protocol_id}' diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 3ab7386ba4f..1caf41fbfd1 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -53,6 +53,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] protocolId: '{protocol_id}' - name: Execute a setup command diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index 2ad0a92eb8c..732726d39e9 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -47,6 +47,7 @@ stages: outputFileIds: [] protocolId: '{protocol_id}' liquids: [] + liquidClasses: [] save: json: original_run_data: data @@ -240,6 +241,7 @@ stages: createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" startedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" liquids: [] + liquidClasses: [] runTimeParameters: [] outputFileIds: [] completedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" diff --git a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml index 14ae502d800..95f5077c30e 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml @@ -95,6 +95,7 @@ stages: labware: [] labwareOffsets: [] liquids: [] + liquidClasses: [] runTimeParameters: [] outputFileIds: [] modules: [] diff --git a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml index 1f44f7101c7..505ff2d8831 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml @@ -118,6 +118,7 @@ stages: name: sample_plates.csv outputFileIds: [] liquids: [] + liquidClasses: [] protocolId: '{protocol_id}' - name: Play the run diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 35fb6da06c1..29a9c81a3b7 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -75,6 +75,7 @@ async def test_create_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -150,6 +151,7 @@ async def test_get_run_data_from_url( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -200,6 +202,7 @@ async def test_get_run() -> None: labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -226,6 +229,7 @@ async def test_get_current_run( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) decoy.when(mock_maintenance_run_data_manager.current_run_id).then_return( diff --git a/robot-server/tests/maintenance_runs/router/test_labware_router.py b/robot-server/tests/maintenance_runs/router/test_labware_router.py index d8a8fdab603..4e5ae1152f2 100644 --- a/robot-server/tests/maintenance_runs/router/test_labware_router.py +++ b/robot-server/tests/maintenance_runs/router/test_labware_router.py @@ -38,6 +38,7 @@ def run() -> MaintenanceRun: modules=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index a4431f7b463..07bc9c2e476 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -69,6 +69,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) @@ -140,6 +141,7 @@ async def test_create( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) @@ -193,6 +195,7 @@ async def test_create_with_options( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) @@ -262,6 +265,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) assert subject.current_run_id == run_id diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 1200f5aff43..d15e9925a18 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -203,6 +203,7 @@ async def test_returned_in_order_added( commands=[], errors=[], liquids=[], + liquidClasses=[], ) subject.add_pending( @@ -266,6 +267,7 @@ async def test_update_adds_details_and_completes_analysis( commands=[], errors=[], liquids=[], + liquidClasses=[], ) result = await subject.get("analysis-id") @@ -283,6 +285,7 @@ async def test_update_adds_details_and_completes_analysis( commands=[], errors=[], liquids=[], + liquidClasses=[], ) assert await subject.get_by_protocol("protocol-id") == [result] assert json.loads(result_as_document) == { @@ -315,6 +318,7 @@ async def test_update_adds_details_and_completes_analysis( "commands": [], "errors": [], "liquids": [], + "liquidClasses": [], "modules": [], } @@ -364,6 +368,7 @@ async def test_update_adds_rtp_values_to_completed_store( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) @@ -384,6 +389,7 @@ async def test_update_adds_rtp_values_to_completed_store( commands=[], errors=[], liquids=[], + liquidClasses=[], ) decoy.verify( await mock_completed_store.make_room_and_add( @@ -487,6 +493,7 @@ async def test_update_infers_status_from_errors( modules=[], pipettes=[], liquids=[], + liquidClasses=[], ) analysis = (await subject.get_by_protocol("protocol-id"))[0] assert isinstance(analysis, CompletedAnalysis) @@ -528,6 +535,7 @@ async def test_save_initialization_failed_analysis( commands=[], errors=[error_occurence], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 42c12565c14..a8112cdda16 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -209,6 +209,7 @@ async def test_get_by_analysis_id_as_document( "errors": [], "labware": [], "liquids": [], + "liquidClasses": [], "modules": [], "pipettes": [], } diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index 5d3d9da8a13..3fab95879fe 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -189,6 +189,7 @@ async def test_analyze( modules=[], labwareOffsets=[], liquids=[], + liquidClasses=[], wells=[], files=[], hasEverEnteredErrorRecovery=False, @@ -211,6 +212,7 @@ async def test_analyze( pipettes=[analysis_pipette], errors=[], liquids=[], + liquidClasses=[], ) ) @@ -294,5 +296,6 @@ async def test_analyze_updates_pending_on_error( pipettes=[], errors=[error_occurrence], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index ca965d471a8..499bf480cf0 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -526,6 +526,7 @@ def get_completed_analysis_resource( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) @@ -566,6 +567,7 @@ async def test_get_referenced_data_files( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) analysis_resource2 = CompletedAnalysisResource( @@ -582,6 +584,7 @@ async def test_get_referenced_data_files( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index 637a2ee082f..0ae2c591ebd 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -1495,6 +1495,7 @@ async def test_get_protocol_analyses( commands=[], errors=[], liquids=[], + liquidClasses=[], ) decoy.when(protocol_store.has("protocol-id")).then_return(True) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 063d1211336..aa27d37e66b 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -139,6 +139,7 @@ async def test_create_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -227,6 +228,7 @@ async def test_create_protocol_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -395,6 +397,7 @@ async def test_get_run_data_from_url( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -443,6 +446,7 @@ async def test_get_run() -> None: labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -490,6 +494,7 @@ async def test_get_runs_not_empty( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -507,6 +512,7 @@ async def test_get_runs_not_empty( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -587,6 +593,7 @@ async def test_update_run_to_not_current( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -623,6 +630,7 @@ async def test_update_current_none_noop( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py index 900eac530f1..1252d983efb 100644 --- a/robot-server/tests/runs/router/test_labware_router.py +++ b/robot-server/tests/runs/router/test_labware_router.py @@ -40,6 +40,7 @@ def run() -> Run: labwareOffsets=[], protocolId=None, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 5e4aed1f3e2..d27e1aebaff 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -105,6 +105,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) @@ -288,6 +289,7 @@ async def test_create( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=[bool_parameter, file_parameter], outputFileIds=engine_state_summary.files, ) @@ -395,6 +397,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -438,6 +441,7 @@ async def test_get_historical_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -482,6 +486,7 @@ async def test_get_historical_run_no_data( pipettes=[], modules=[], liquids=[], + liquidClasses=[], runTimeParameters=run_time_parameters, outputFileIds=[], ) @@ -503,6 +508,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="current-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="current-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) current_run_time_parameters: List[pe_types.RunTimeParameter] = [ @@ -523,6 +529,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="old-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="old-module-id")], # type: ignore[call-arg] liquids=[], + liquidClasses=[], wells=[], ) historical_run_time_parameters: List[pe_types.RunTimeParameter] = [ @@ -584,6 +591,7 @@ async def test_get_all_runs( pipettes=historical_run_data.pipettes, modules=historical_run_data.modules, liquids=historical_run_data.liquids, + liquidClasses=historical_run_data.liquidClasses, runTimeParameters=historical_run_time_parameters, outputFileIds=historical_run_data.files, ), @@ -601,6 +609,7 @@ async def test_get_all_runs( pipettes=current_run_data.pipettes, modules=current_run_data.modules, liquids=current_run_data.liquids, + liquidClasses=current_run_data.liquidClasses, runTimeParameters=current_run_time_parameters, outputFileIds=current_run_data.files, ), @@ -700,6 +709,7 @@ async def test_update_current( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -757,6 +767,7 @@ async def test_update_current_noop( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 60aefa9820d..432e8a08231 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -1940,16 +1940,35 @@ "airGapByVolume": { "title": "Airgapbyvolume", "description": "Settings for air gap keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2073,16 +2092,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2218,16 +2256,35 @@ "airGapByVolume": { "title": "Airgapbyvolume", "description": "Settings for air gap keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2313,16 +2370,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2339,16 +2415,35 @@ "pushOutByVolume": { "title": "Pushoutbyvolume", "description": "Settings for pushout keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2417,16 +2512,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2434,16 +2548,35 @@ "conditioningByVolume": { "title": "Conditioningbyvolume", "description": "Settings for conditioning volume keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2451,16 +2584,35 @@ "disposalByVolume": { "title": "Disposalbyvolume", "description": "Settings for disposal volume keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2690,6 +2842,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_em", "p1000_96", "p200_96" ], diff --git a/shared-data/js/__tests__/liquidClassSchema.test.ts b/shared-data/js/__tests__/liquidClassSchema.test.ts new file mode 100644 index 00000000000..75e477637c9 --- /dev/null +++ b/shared-data/js/__tests__/liquidClassSchema.test.ts @@ -0,0 +1,66 @@ +/** Ensure that the liquid class schema itself functions as intended, + * and that all v1 liquid class fixtures will validate */ +import Ajv from 'ajv' +import path from 'path' +import glob from 'glob' +import { describe, expect, it } from 'vitest' +import liquidClassSchemaV1 from '../../liquid-class/schemas/1.json' + +const fixtureV1Glob = path.join( + __dirname, + '../../liquid-class/fixtures/1/*.json' +) +const defV1Glob = path.join( + __dirname, + '../../liquid-class/definitions/1/*.json' +) + +const ajv = new Ajv({ allErrors: true, jsonPointers: true }) + +const validateSchemaV1 = ajv.compile(liquidClassSchemaV1) + +describe('validate v1 liquid class definitions and fixtures', () => { + const fixtures = glob.sync(fixtureV1Glob) + + fixtures.forEach(fixturePath => { + const fixtureDef = require(fixturePath) + + it('fixture validates against schema', () => { + const valid = validateSchemaV1(fixtureDef) + const validationErrors = validateSchemaV1.errors + + if (validationErrors) { + console.log( + path.parse(fixturePath).base + + ' ' + + JSON.stringify(validationErrors, null, 4) + ) + } + + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) + + const defs = glob.sync(defV1Glob) + + defs.forEach(defPath => { + const liquidClassDef = require(defPath) + + it('liquid class definition validates against v1 schema', () => { + const valid = validateSchemaV1(liquidClassDef) + const validationErrors = validateSchemaV1.errors + + if (validationErrors) { + console.log( + path.parse(defPath).base + + ' ' + + JSON.stringify(validationErrors, null, 4) + ) + } + + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) +}) diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 27104527e4d..608bb982887 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -145,6 +145,7 @@ export const OT3_PIPETTES = [ 'p50_single_flex', 'p50_multi_flex', 'p1000_multi_flex', + 'p1000_multi_em_flex', 'p1000_96', 'p200_96', ] diff --git a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json index b08bb84409f..e86f24c6015 100644 --- a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json +++ b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json @@ -66,6 +66,11 @@ "y": 0, "z": 8.193 }, + "biorad_96_wellplate_200ul_pcr": { + "x": 0, + "y": 0, + "z": 8.08 + }, "opentrons_flex_deck_riser": { "x": 0, "y": 0, diff --git a/shared-data/liquid-class/definitions/1/water.json b/shared-data/liquid-class/definitions/1/water.json index b5fc2f75486..b84e1676d5b 100644 --- a/shared-data/liquid-class/definitions/1/water.json +++ b/shared-data/liquid-class/definitions/1/water.json @@ -33,12 +33,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -60,12 +59,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50, - "1": 35, - "10": 24, - "50": 35 - }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], "preWet": false, "mix": { "enable": false, @@ -105,12 +103,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -135,9 +132,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[1.0, 50.0]], "mix": { "enable": false, "params": { @@ -145,14 +140,13 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 2, - "1": 7, - "4.999": 7, - "5": 2, - "10": 2, - "50": 2 - }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], "delay": { "enable": true, "params": { @@ -184,12 +178,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -214,21 +207,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[50.0, 50.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": true, "params": { @@ -268,12 +257,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -295,12 +283,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50, - "1": 35, - "10": 24, - "50": 35 - }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], "preWet": false, "mix": { "enable": false, @@ -340,12 +327,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -370,9 +356,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[1.0, 50.0]], "mix": { "enable": false, "params": { @@ -380,14 +364,13 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 2, - "1": 7, - "4.999": 7, - "5": 2, - "10": 2, - "50": 2 - }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], "delay": { "enable": true, "params": { @@ -419,12 +402,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -449,21 +431,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[1.0, 50.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": true, "params": { @@ -503,12 +481,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -530,12 +507,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "preWet": false, "mix": { "enable": false, @@ -575,12 +551,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -605,12 +580,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "mix": { "enable": false, "params": { @@ -618,9 +592,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -652,12 +624,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -682,24 +653,21 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -734,12 +702,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -761,9 +728,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -803,12 +768,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -833,9 +797,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -843,9 +805,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -877,12 +837,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -907,21 +866,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -956,12 +911,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -983,9 +937,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1025,12 +977,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1055,9 +1006,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1065,9 +1014,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1099,12 +1046,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1129,21 +1075,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1183,12 +1125,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1210,12 +1151,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "preWet": false, "mix": { "enable": false, @@ -1255,12 +1195,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1285,12 +1224,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "mix": { "enable": false, "params": { @@ -1298,9 +1236,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1332,12 +1268,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1362,24 +1297,21 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1414,12 +1346,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1441,9 +1372,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1483,12 +1412,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -1513,9 +1441,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1523,9 +1449,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -1557,12 +1481,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -1587,21 +1510,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1636,12 +1555,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1663,9 +1581,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1705,12 +1621,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1735,9 +1650,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1745,9 +1658,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1779,12 +1690,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1809,21 +1719,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1863,12 +1769,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1890,9 +1795,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -1932,12 +1835,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1962,9 +1864,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -1972,9 +1872,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -2006,12 +1904,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -2036,21 +1933,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -2085,12 +1978,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -2112,9 +2004,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -2154,12 +2044,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -2184,9 +2073,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -2194,9 +2081,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -2228,12 +2113,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -2258,21 +2142,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -2307,12 +2187,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -2334,9 +2213,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -2376,12 +2253,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -2406,9 +2282,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -2416,9 +2290,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -2450,12 +2322,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -2480,21 +2351,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { diff --git a/shared-data/liquid-class/fixtures/fixture_glycerol50.json b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json similarity index 82% rename from shared-data/liquid-class/fixtures/fixture_glycerol50.json rename to shared-data/liquid-class/fixtures/1/fixture_glycerol50.json index 8befe1d6a5b..20fe7b44a3c 100644 --- a/shared-data/liquid-class/fixtures/fixture_glycerol50.json +++ b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json @@ -33,11 +33,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "touchTip": { "enable": true, "params": { @@ -59,9 +58,7 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[10.0, 50.0]], "preWet": true, "mix": { "enable": true, @@ -101,11 +98,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "blowout": { "enable": true, "params": { @@ -134,11 +130,10 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50, - "10": 40, - "20": 30 - }, + "flowRateByVolume": [ + [10.0, 40.0], + [20.0, 30.0] + ], "mix": { "enable": true, "params": { @@ -146,11 +141,10 @@ "volume": 15 } }, - "pushOutByVolume": { - "default": 5, - "10": 7, - "20": 10 - }, + "pushOutByVolume": [ + [10.0, 7.0], + [20.0, 10.0] + ], "delay": { "enable": true, "params": { @@ -182,11 +176,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "touchTip": { "enable": true, "params": { @@ -211,19 +204,12 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50, - "10": 40, - "20": 30 - }, - "conditioningByVolume": { - "default": 10, - "5": 5 - }, - "disposalByVolume": { - "default": 2, - "5": 3 - }, + "flowRateByVolume": [ + [10.0, 40.0], + [20.0, 30.0] + ], + "conditioningByVolume": [[5.0, 5.0]], + "disposalByVolume": [[5.0, 3.0]], "delay": { "enable": true, "params": { diff --git a/shared-data/liquid-class/schemas/1.json b/shared-data/liquid-class/schemas/1.json index 1a5eb18d51a..f3aa85a6168 100644 --- a/shared-data/liquid-class/schemas/1.json +++ b/shared-data/liquid-class/schemas/1.json @@ -90,59 +90,59 @@ "additionalProperties": false }, "airGapByVolume": { - "type": "object", + "type": "array", "description": "Settings for air gap keyed by target aspiration volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "flowRateByVolume": { - "type": "object", + "type": "array", "description": "Settings for flow rate keyed by target aspiration/dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "pushOutByVolume": { - "type": "object", + "type": "array", "description": "Settings for pushout keyed by target aspiration volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "disposalByVolume": { - "type": "object", - "description": "Settings for disposal volume keyed by target dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "type": "array", + "description": "An array of two tuples containing positive numbers.", + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "conditioningByVolume": { - "type": "object", + "type": "array", "description": "Settings for conditioning volume keyed by target dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "mix": { "type": "object", @@ -409,7 +409,6 @@ "positionReference", "offset", "flowRateByVolume", - "mix", "conditioningByVolume", "disposalByVolume", "delay" diff --git a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json new file mode 100644 index 00000000000..c267504b404 --- /dev/null +++ b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json @@ -0,0 +1,320 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", + "displayName": "FLEX 8-Channel EM 1000 μL", + "model": "p1000", + "displayCategory": "FLEX", + "validNozzleMaps": { + "maps": { + "SingleA1": ["A1"], + "SingleH1": ["H1"], + "H1toG1": ["G1", "H1"], + "H1toF1": ["F1", "G1", "H1"], + "H1toE1": ["E1", "F1", "G1", "H1"], + "H1toD1": ["D1", "E1", "F1", "G1", "H1"], + "H1toC1": ["C1", "D1", "E1", "F1", "G1", "H1"], + "H1toB1": ["B1", "C1", "D1", "E1", "F1", "G1", "H1"], + "Full": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + }, + "pickUpTipConfigurations": { + "pressFit": { + "presses": 1, + "increment": 0.0, + "configurationsByNozzleMap": { + "SingleA1": { + "default": { + "speed": 10.0, + "distance": 11.0, + "current": 0.15, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.26, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.26 + } + } + } + }, + "SingleH1": { + "default": { + "speed": 10.0, + "distance": 11.0, + "current": 0.15, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.2, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.2 + } + } + } + }, + "H1toG1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.2, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.73, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.73 + } + } + } + }, + "H1toF1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.2, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toE1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.35, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toD1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.4, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toC1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.4, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toB1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.5, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.9, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.9 + } + } + } + }, + "Full": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.55, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + } + } + } + } + } + } + }, + "dropTipConfigurations": { + "plungerEject": { + "current": 1.0, + "speed": 10 + } + }, + "plungerMotorConfigurations": { + "idle": 0.3, + "run": 1.0 + }, + "plungerPositionsConfigurations": { + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 91.5 + } + }, + "availableSensors": { + "sensors": ["capacitive", "environment"], + "capacitive": { + "count": 2 + }, + "environment": { + "count": 1 + } + }, + "partialTipConfigurations": { + "partialTipSupported": true, + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] + }, + "backCompatNames": [], + "channels": 8, + "shaftDiameter": 4.5, + "shaftULperMM": 15.904, + "backlashDistance": 0.1, + "quirks": ["highSpeed"], + "plungerHomingConfigurations": { + "current": 1.0, + "speed": 30 + } +} diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json index 5719bb3437c..5f3fa397dc8 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json @@ -624,8 +624,8 @@ "plungerPositionsConfigurations": { "default": { "top": 0.5, - "bottom": 68.5, - "blowout": 73.5, + "bottom": 67.5, + "blowout": 72.5, "drop": 80 } }, @@ -649,7 +649,7 @@ "channels": 96, "shaftDiameter": 2, "shaftULperMM": 3.14159, - "backlashDistance": 3.0, + "backlashDistance": 0.3, "quirks": [], "plungerHomingConfigurations": { "current": 0.8, diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json new file mode 100644 index 00000000000..b92e7415fe3 --- /dev/null +++ b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json @@ -0,0 +1,55 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", + "pathTo3D": "pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf", + "nozzleOffset": [-8.0, -16.0, -259.15], + "pipetteBoundingBoxOffsets": { + "backLeftCorner": [-38.5, 0.0, -259.15], + "frontRightCorner": [11.5, -95.0, -259.15] + }, + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], + "nozzleMap": { + "A1": [-8.0, -16.0, -259.15], + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] + }, + "lldSettings": { + "t50": { + "minHeight": 1.0, + "minVolume": 0 + }, + "t200": { + "minHeight": 1.0, + "minVolume": 0 + }, + "t1000": { + "minHeight": 1.5, + "minVolume": 0 + } + } +} diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json new file mode 100644 index 00000000000..52c7b58171d --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json @@ -0,0 +1,236 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "uiMaxFlowRate": 1431.0, + "defaultAspirateFlowRate": { + "default": 478, + "valuesByApiLevel": { "2.14": 478 } + }, + "defaultDispenseFlowRate": { + "default": 478, + "valuesByApiLevel": { "2.14": 478 } + }, + "defaultBlowOutFlowRate": { + "default": 478, + "valuesByApiLevel": { "2.14": 478 } + }, + "defaultFlowAcceleration": 24000.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.12, -57.973785, 8.495981], + [0.11, 40.31047, -3.298129], + [0.09, 19.330223, -0.990302], + [0.375, 6.200306, 0.19139], + [1.17, 4.795927, 0.718032], + [1.92, 2.746428, 3.115947], + [2.145, 1.592373, 5.331732], + [2.4, 1.336497, 5.880586], + [2.66, 1.043996, 6.582588], + [2.84, 0.280189, 8.614315], + [2.985, -0.698973, 11.395134], + [3.085, -5.627462, 26.106674], + [3.625, 1.899561, 2.885808], + [4.43, 1.977851, 2.602006], + [5.155, 0.596916, 8.71955], + [6.71, 0.366092, 9.909446], + [8.62, 0.233878, 10.796602], + [11.015, 0.158281, 11.448248], + [13.97, 0.101002, 12.079177], + [17.545, 0.047056, 12.832813], + [22.075, 0.043416, 12.896662], + [27.955, 0.049456, 12.763333], + [34.695, 0.00096, 14.119053], + [43.535, 0.018347, 13.515795], + [54.08, 0.001949, 14.229706] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.12, -57.973785, 8.495981], + [0.11, 40.31047, -3.298129], + [0.09, 19.330223, -0.990302], + [0.375, 6.200306, 0.19139], + [1.17, 4.795927, 0.718032], + [1.92, 2.746428, 3.115947], + [2.145, 1.592373, 5.331732], + [2.4, 1.336497, 5.880586], + [2.66, 1.043996, 6.582588], + [2.84, 0.280189, 8.614315], + [2.985, -0.698973, 11.395134], + [3.085, -5.627462, 26.106674], + [3.625, 1.899561, 2.885808], + [4.43, 1.977851, 2.602006], + [5.155, 0.596916, 8.71955], + [6.71, 0.366092, 9.909446], + [8.62, 0.233878, 10.796602], + [11.015, 0.158281, 11.448248], + [13.97, 0.101002, 12.079177], + [17.545, 0.047056, 12.832813], + [22.075, 0.043416, 12.896662], + [27.955, 0.049456, 12.763333], + [34.695, 0.00096, 14.119053], + [43.535, 0.018347, 13.515795], + [54.08, 0.001949, 14.229706] + ] + } + }, + "defaultPushOutVolume": 7 + }, + "t200": { + "uiMaxFlowRate": 1431.0, + "defaultAspirateFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultDispenseFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultBlowOutFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultFlowAcceleration": 24000.0, + "defaultTipLength": 58.35, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.28375, -141.180627, 42.499381], + [0.26125, 27.065799, -5.240543], + [0.715, 4.916546, 0.54595], + [1.685, 3.844391, 1.31254], + [2.6025, 2.148973, 4.169319], + [3.75875, 1.461751, 5.957816], + [4.9975, 0.733738, 8.694235], + [6.41375, 0.377599, 10.474036], + [8.1225, 0.214926, 11.517382], + [10.2425, 0.152451, 12.024835], + [12.80125, 0.081051, 12.75615], + [15.9875, 0.062849, 12.989161], + [19.9625, 0.051585, 13.169235], + [24.83625, 0.030593, 13.588301], + [30.89125, 0.024593, 13.737307], + [38.42625, 0.020128, 13.875257], + [47.71875, 0.014091, 14.107204], + [59.28375, 0.011625, 14.224918], + [73.41375, 0.00635, 14.537608], + [90.84375, 0.004458, 14.676515], + [112.32, 0.003084, 14.801312], + [138.7675, 0.002045, 14.917998], + [171.29875, 0.001319, 15.018758], + [211.27375, 0.000719, 15.121662] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.28375, -141.180627, 42.499381], + [0.26125, 27.065799, -5.240543], + [0.715, 4.916546, 0.54595], + [1.685, 3.844391, 1.31254], + [2.6025, 2.148973, 4.169319], + [3.75875, 1.461751, 5.957816], + [4.9975, 0.733738, 8.694235], + [6.41375, 0.377599, 10.474036], + [8.1225, 0.214926, 11.517382], + [10.2425, 0.152451, 12.024835], + [12.80125, 0.081051, 12.75615], + [15.9875, 0.062849, 12.989161], + [19.9625, 0.051585, 13.169235], + [24.83625, 0.030593, 13.588301], + [30.89125, 0.024593, 13.737307], + [38.42625, 0.020128, 13.875257], + [47.71875, 0.014091, 14.107204], + [59.28375, 0.011625, 14.224918], + [73.41375, 0.00635, 14.537608], + [90.84375, 0.004458, 14.676515], + [112.32, 0.003084, 14.801312], + [138.7675, 0.002045, 14.917998], + [171.29875, 0.001319, 15.018758], + [211.27375, 0.000719, 15.121662] + ] + } + }, + "defaultPushOutVolume": 5 + }, + "t1000": { + "uiMaxFlowRate": 1431.0, + "defaultAspirateFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultDispenseFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultBlowOutFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultFlowAcceleration": 24000.0, + "defaultTipLength": 95.6, + "defaultReturnTipHeight": 0.82, + "aspirate": { + "default": { + "1": [ + [2.1443, 1.9858, 4.2677], + [3.0286, 1.2526, 5.84], + [4.9557, 0.6268, 7.7351], + [9.7943, 0.2745, 9.4811], + [12.1514, 0.1715, 10.4901], + [14.9414, 0.0897, 11.4833], + [51.46, 0.0424, 12.1913], + [92.68, 0.0095, 13.881], + [112.4886, 0.0049, 14.3053], + [243.5986, 0.0028, 14.5507], + [356.5686, 0.0009, 15.0019], + [430.99, 0.0005, 15.1492], + [628.7886, 0.0003, 15.2496], + [1001.15, 0.0001, 15.3472], + [1106.0857, 0.0001, 15.3551] + ] + } + }, + "dispense": { + "default": { + "1": [ + [2.1443, 1.9858, 4.2677], + [3.0286, 1.2526, 5.84], + [4.9557, 0.6268, 7.7351], + [9.7943, 0.2745, 9.4811], + [12.1514, 0.1715, 10.4901], + [14.9414, 0.0897, 11.4833], + [51.46, 0.0424, 12.1913], + [92.68, 0.0095, 13.881], + [112.4886, 0.0049, 14.3053], + [243.5986, 0.0028, 14.5507], + [356.5686, 0.0009, 15.0019], + [430.99, 0.0005, 15.1492], + [628.7886, 0.0003, 15.2496], + [1001.15, 0.0001, 15.3472], + [1106.0857, 0.0001, 15.3551] + ] + } + }, + "defaultPushOutVolume": 20 + } + }, + "maxVolume": 1000, + "minVolume": 5, + "defaultTipracks": [ + "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_filtertiprack_1000ul/1", + "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + ] +} diff --git a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py index 0462ac5c0e4..62add6a32b0 100644 --- a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py +++ b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py @@ -1,7 +1,7 @@ """Python shared data models for liquid class definitions.""" from enum import Enum -from typing import TYPE_CHECKING, Literal, Union, Optional, Dict, Any, Sequence +from typing import TYPE_CHECKING, Literal, Union, Optional, Dict, Any, Sequence, Tuple from pydantic import ( BaseModel, @@ -28,8 +28,8 @@ _NonNegativeNumber = Union[_StrictNonNegativeInt, _StrictNonNegativeFloat] """Non-negative JSON number type, written to preserve lack of decimal point.""" -LiquidHandlingPropertyByVolume = Dict[str, _NonNegativeNumber] -"""Settings for liquid class settings keyed by target aspiration/dispense volume.""" +LiquidHandlingPropertyByVolume = Sequence[Tuple[_NonNegativeNumber, _NonNegativeNumber]] +"""Settings for liquid class settings that are interpolated by volume.""" class PositionReference(Enum): diff --git a/shared-data/python/opentrons_shared_data/pipette/dev_types.py b/shared-data/python/opentrons_shared_data/pipette/dev_types.py index 00676e9be08..8ae367378f2 100644 --- a/shared-data/python/opentrons_shared_data/pipette/dev_types.py +++ b/shared-data/python/opentrons_shared_data/pipette/dev_types.py @@ -31,6 +31,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_em", "p1000_96", "p200_96", ] @@ -57,6 +58,7 @@ class PipetteNameType(str, Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" + P1000_MULTI_EM = "p1000_multi_em" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index f8c361cca0c..40027d54394 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -114,11 +114,13 @@ def load_serial_lookup_table() -> Dict[str, str]: "eight_channel": "M", "single_channel": "S", "ninety_six_channel": "H", + "eight_channel_em": "P", } _channel_model_str = { "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_em": "multi_em", } _model_shorthand = {"p1000": "p1k", "p300": "p3h"} for channel_dir in _dirs_in(config_path): diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py b/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py index 244016d5943..f5113cff9e7 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py @@ -80,7 +80,7 @@ def channels_from_string(channels: str) -> PipetteChannelType: """ if channels == "96": return PipetteChannelType.NINETY_SIX_CHANNEL - elif channels == "multi": + elif "multi" in channels: return PipetteChannelType.EIGHT_CHANNEL elif channels == "single": return PipetteChannelType.SINGLE_CHANNEL @@ -287,8 +287,14 @@ def convert_pipette_model( # We need to figure out how to default the pipette model as well # rather than returning a p1000 if model and not provided_version: - pipette_type, parsed_channels, parsed_version = model.split("_") - channels = channels_from_string(parsed_channels) + # pipette_type, parsed_channels, parsed_version = model.split("_") + exploded = model.split("_") + if len(exploded) == 3: + (pipette_type, parsed_channels, parsed_version) = exploded + channels = channels_from_string(parsed_channels) + else: + pipette_type, parsed_channels, parsed_oem, parsed_version = exploded + channels = channels_from_string(f"{parsed_channels}_{parsed_oem}") version = version_from_string(parsed_version) elif model and provided_version: pipette_type, parsed_channels = model.split("_") diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py index e787ac2a1cf..d72a09e666b 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py @@ -355,6 +355,7 @@ def _update_all_models(configuration_to_update: List[str]) -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_em": "multi_em", } for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/opentrons_shared_data/pipette/types.py b/shared-data/python/opentrons_shared_data/pipette/types.py index 85ad1a81f01..c52e57eb20e 100644 --- a/shared-data/python/opentrons_shared_data/pipette/types.py +++ b/shared-data/python/opentrons_shared_data/pipette/types.py @@ -109,6 +109,7 @@ class Quirks(enum.Enum): dropTipShake = "dropTipShake" doubleDropTip = "doubleDropTip" needsUnstick = "needsUnstick" + highSpeed = "highSpeed" class AvailableUnits(enum.Enum): @@ -216,6 +217,7 @@ def dict_for_encode(self) -> bool: "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_em", "p1000_96", "p200_96", ] @@ -242,6 +244,7 @@ class PipetteNameType(str, enum.Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" + P1000_MULTI_EM = "p1000_multi_em" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py index 3423f0f49e5..774231ac40d 100644 --- a/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py +++ b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py @@ -1,11 +1,46 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional -from opentrons_shared_data.pipette.pipette_definition import PipetteFunctionKeyType +from opentrons_shared_data.pipette.pipette_definition import ( + PipetteFunctionKeyType, + SupportedTipsDefinition, +) +from opentrons_shared_data.pipette.types import UlPerMmAction PIPETTING_FUNCTION_FALLBACK_VERSION: PipetteFunctionKeyType = "1" PIPETTING_FUNCTION_LATEST_VERSION: PipetteFunctionKeyType = "2" +def calculate_ul_per_mm( + ul: float, + action: UlPerMmAction, + active_tip_settings: SupportedTipsDefinition, + requested_pipetting_version: Optional[PipetteFunctionKeyType] = None, + shaft_ul_per_mm: Optional[float] = None, +) -> float: + assumed_requested_pipetting_version = ( + requested_pipetting_version + if requested_pipetting_version + else PIPETTING_FUNCTION_LATEST_VERSION + ) + if action == "aspirate": + fallback = active_tip_settings.aspirate.default[ + PIPETTING_FUNCTION_FALLBACK_VERSION + ] + sequence = active_tip_settings.aspirate.default.get( + assumed_requested_pipetting_version, fallback + ) + elif action == "blowout" and shaft_ul_per_mm: + return shaft_ul_per_mm + else: + fallback = active_tip_settings.dispense.default[ + PIPETTING_FUNCTION_FALLBACK_VERSION + ] + sequence = active_tip_settings.dispense.default.get( + assumed_requested_pipetting_version, fallback + ) + return piecewise_volume_conversion(ul, sequence) + + def piecewise_volume_conversion( ul: float, sequence: List[Tuple[float, float, float]] ) -> float: diff --git a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py index b64f0a0b5c4..aae0c1a4e1b 100644 --- a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py +++ b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py @@ -49,6 +49,7 @@ def get_all_pipette_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/tests/pipette/test_validate_schema.py b/shared-data/python/tests/pipette/test_validate_schema.py index 0b703504957..57f19dfe3ad 100644 --- a/shared-data/python/tests/pipette/test_validate_schema.py +++ b/shared-data/python/tests/pipette/test_validate_schema.py @@ -22,6 +22,7 @@ def iterate_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_em": "multi_em", } defn_root = get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" assert os.listdir(defn_root), "A path is wrong" @@ -63,6 +64,7 @@ def test_pick_up_configs_configuration_by_nozzle_map_keys() -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): @@ -105,6 +107,7 @@ def test_pick_up_configs_configuration_ordered_from_smallest_to_largest() -> Non "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/tsconfig-data.json b/shared-data/tsconfig-data.json index 4b9ff960c84..e79657a21f8 100644 --- a/shared-data/tsconfig-data.json +++ b/shared-data/tsconfig-data.json @@ -12,6 +12,7 @@ "deck/**/*.json", "labware/**/*.json", "liquid/**/*.json", + "liquid-class/**/*.json", "command/**/*.json", "commandAnnotation/**/*.json", "gripper/**/*.json", diff --git a/shared-data/tsconfig.json b/shared-data/tsconfig.json index a50e215ee95..57f8970d0c6 100644 --- a/shared-data/tsconfig.json +++ b/shared-data/tsconfig.json @@ -18,6 +18,7 @@ "command", "errors", "liquid/types", + "liquid-class", "commandAnnotation/types", "vite.config.mts" ]