|
2 | 2 |
|
3 | 3 | from __future__ import annotations
|
4 | 4 |
|
5 |
| -from typing import Optional, TYPE_CHECKING, cast, Union, List |
| 5 | +from typing import Optional, TYPE_CHECKING, cast, Union, List, Tuple |
6 | 6 | from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface
|
7 | 7 | from opentrons.hardware_control import SyncHardwareAPI
|
8 | 8 | from opentrons.hardware_control.dev_types import PipetteDict
|
9 | 9 | from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version
|
10 | 10 | from opentrons.protocols.api_support.types import APIVersion
|
11 |
| -from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 |
| 11 | +from opentrons.protocols.advanced_control.transfers.common import ( |
| 12 | + TransferTipPolicyV2, |
| 13 | + check_valid_volume_parameters, |
| 14 | + expand_for_volume_constraints, |
| 15 | +) |
12 | 16 | from opentrons.protocol_engine import commands as cmd
|
13 | 17 | from opentrons.protocol_engine import (
|
14 | 18 | DeckPoint,
|
|
38 | 42 | )
|
39 | 43 | from opentrons.protocol_api._nozzle_layout import NozzleLayout
|
40 | 44 | from . import overlap_versions, pipette_movement_conflict
|
| 45 | +from . import transfer_components_executor as tx_comps_executor |
41 | 46 |
|
42 | 47 | from .well import WellCore
|
43 | 48 | from ..instrument import AbstractInstrument
|
|
46 | 51 | if TYPE_CHECKING:
|
47 | 52 | from .protocol import ProtocolCore
|
48 | 53 | from opentrons.protocol_api._liquid import LiquidClass
|
| 54 | + from opentrons.protocol_api._liquid_properties import TransferProperties |
49 | 55 |
|
50 | 56 | _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
|
51 | 57 |
|
@@ -892,16 +898,230 @@ def load_liquid_class(
|
892 | 898 | )
|
893 | 899 | return result.liquidClassId
|
894 | 900 |
|
| 901 | + # TODO: update with getNextTip implementation |
| 902 | + def get_next_tip(self) -> None: |
| 903 | + """Get the next tip to pick up.""" |
| 904 | + |
895 | 905 | def transfer_liquid(
|
896 | 906 | self,
|
897 |
| - liquid_class_id: str, |
| 907 | + liquid_class: LiquidClass, |
898 | 908 | volume: float,
|
899 |
| - source: List[WellCore], |
900 |
| - dest: List[WellCore], |
| 909 | + source: List[Tuple[Location, WellCore]], |
| 910 | + dest: List[Tuple[Location, WellCore]], |
901 | 911 | new_tip: TransferTipPolicyV2,
|
902 |
| - trash_location: Union[WellCore, Location, TrashBin, WasteChute], |
| 912 | + tiprack_uri: str, |
| 913 | + trash_location: Union[Location, TrashBin, WasteChute], |
903 | 914 | ) -> None:
|
904 |
| - """Execute transfer using liquid class properties.""" |
| 915 | + """Execute transfer using liquid class properties. |
| 916 | +
|
| 917 | + Args: |
| 918 | + liquid_class: The liquid class to use for transfer properties. |
| 919 | + volume: Volume to transfer per well. |
| 920 | + source: List of source wells, with each well represented as a tuple of |
| 921 | + types.Location and WellCore. |
| 922 | + types.Location is only necessary for saving the last accessed location. |
| 923 | + dest: List of destination wells, with each well represented as a tuple of |
| 924 | + types.Location and WellCore. |
| 925 | + types.Location is only necessary for saving the last accessed location. |
| 926 | + new_tip: Whether the transfer should use a new tip 'once', 'never', 'always', |
| 927 | + or 'per source'. |
| 928 | + tiprack_uri: The URI of the tiprack that the transfer settings are for. |
| 929 | + tip_drop_location: Location where the tip will be dropped (if appropriate). |
| 930 | + """ |
| 931 | + # This function is WIP |
| 932 | + # TODO: use the ID returned by load_liquid_class in command annotations |
| 933 | + self.load_liquid_class( |
| 934 | + liquid_class=liquid_class, |
| 935 | + pipette_load_name=self.get_pipette_name(), # TODO: update this to use load name instead |
| 936 | + tiprack_uri=tiprack_uri, |
| 937 | + ) |
| 938 | + transfer_props = liquid_class.get_for( |
| 939 | + # update this to fetch load name instead |
| 940 | + pipette=self.get_pipette_name(), |
| 941 | + tiprack=tiprack_uri, |
| 942 | + ) |
| 943 | + aspirate_props = transfer_props.aspirate |
| 944 | + |
| 945 | + check_valid_volume_parameters( |
| 946 | + disposal_volume=0, # No disposal volume for 1-to-1 transfer |
| 947 | + air_gap=aspirate_props.retract.air_gap_by_volume.get_for_volume(volume), |
| 948 | + max_volume=self.get_max_volume(), |
| 949 | + ) |
| 950 | + source_dest_per_volume_step = expand_for_volume_constraints( |
| 951 | + volumes=[volume for _ in range(len(source))], |
| 952 | + targets=zip(source, dest), |
| 953 | + max_volume=self.get_max_volume(), |
| 954 | + ) |
| 955 | + if new_tip == TransferTipPolicyV2.ONCE: |
| 956 | + # TODO: update this once getNextTip is implemented |
| 957 | + self.get_next_tip() |
| 958 | + for step_volume, (src, dest) in source_dest_per_volume_step: # type: ignore[assignment] |
| 959 | + if new_tip == TransferTipPolicyV2.ALWAYS: |
| 960 | + # TODO: update this once getNextTip is implemented |
| 961 | + self.get_next_tip() |
| 962 | + |
| 963 | + # TODO: add aspirate and dispense |
| 964 | + |
| 965 | + if new_tip == TransferTipPolicyV2.ALWAYS: |
| 966 | + if isinstance(trash_location, (TrashBin, WasteChute)): |
| 967 | + self.drop_tip_in_disposal_location( |
| 968 | + disposal_location=trash_location, |
| 969 | + home_after=False, |
| 970 | + alternate_tip_drop=True, |
| 971 | + ) |
| 972 | + elif isinstance(trash_location, Location): |
| 973 | + self.drop_tip( |
| 974 | + location=trash_location, |
| 975 | + well_core=trash_location.labware.as_well()._core, # type: ignore[arg-type] |
| 976 | + home_after=False, |
| 977 | + alternate_drop_location=True, |
| 978 | + ) |
| 979 | + |
| 980 | + def aspirate_liquid_class( |
| 981 | + self, |
| 982 | + volume: float, |
| 983 | + source: Tuple[Location, WellCore], |
| 984 | + transfer_properties: TransferProperties, |
| 985 | + transfer_type: tx_comps_executor.TransferType, |
| 986 | + tip_contents: List[tx_comps_executor.LiquidAndAirGapPair], |
| 987 | + ) -> tx_comps_executor.LiquidAndAirGapPair: |
| 988 | + """Execute aspiration steps. |
| 989 | +
|
| 990 | + 1. Submerge |
| 991 | + 2. Mix |
| 992 | + 3. pre-wet |
| 993 | + 4. Aspirate |
| 994 | + 5. Delay- wait inside the liquid |
| 995 | + 6. Aspirate retract |
| 996 | +
|
| 997 | + Return: The last liquid and air gap pair in tip. |
| 998 | + """ |
| 999 | + aspirate_props = transfer_properties.aspirate |
| 1000 | + source_loc, source_well = source |
| 1001 | + aspirate_point = ( |
| 1002 | + tx_comps_executor.absolute_point_from_position_reference_and_offset( |
| 1003 | + well=source_well, |
| 1004 | + position_reference=aspirate_props.position_reference, |
| 1005 | + offset=aspirate_props.offset, |
| 1006 | + ) |
| 1007 | + ) |
| 1008 | + aspirate_location = Location(aspirate_point, labware=source_loc.labware) |
| 1009 | + if len(tip_contents) > 0: |
| 1010 | + last_liquid_and_airgap_in_tip = tip_contents[-1] |
| 1011 | + else: |
| 1012 | + last_liquid_and_airgap_in_tip = tx_comps_executor.LiquidAndAirGapPair( |
| 1013 | + liquid=0, |
| 1014 | + air_gap=0, |
| 1015 | + ) |
| 1016 | + components_executor = tx_comps_executor.TransferComponentsExecutor( |
| 1017 | + instrument_core=self, |
| 1018 | + transfer_properties=transfer_properties, |
| 1019 | + target_location=aspirate_location, |
| 1020 | + target_well=source_well, |
| 1021 | + transfer_type=transfer_type, |
| 1022 | + tip_state=tx_comps_executor.TipState( |
| 1023 | + last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip |
| 1024 | + ), |
| 1025 | + ) |
| 1026 | + components_executor.submerge(submerge_properties=aspirate_props.submerge) |
| 1027 | + # TODO: when aspirating for consolidation, do not perform mix |
| 1028 | + components_executor.mix( |
| 1029 | + mix_properties=aspirate_props.mix, last_dispense_push_out=False |
| 1030 | + ) |
| 1031 | + # TODO: when aspirating for consolidation, do not preform pre-wet |
| 1032 | + components_executor.pre_wet( |
| 1033 | + volume=volume, |
| 1034 | + ) |
| 1035 | + components_executor.aspirate_and_wait(volume=volume) |
| 1036 | + components_executor.retract_after_aspiration(volume=volume) |
| 1037 | + return components_executor.tip_state.last_liquid_and_air_gap_in_tip |
| 1038 | + |
| 1039 | + def dispense_liquid_class( |
| 1040 | + self, |
| 1041 | + volume: float, |
| 1042 | + dest: Tuple[Location, WellCore], |
| 1043 | + source: Optional[Tuple[Location, WellCore]], |
| 1044 | + transfer_properties: TransferProperties, |
| 1045 | + transfer_type: tx_comps_executor.TransferType, |
| 1046 | + tip_contents: List[tx_comps_executor.LiquidAndAirGapPair], |
| 1047 | + trash_location: Union[Location, TrashBin, WasteChute], |
| 1048 | + ) -> tx_comps_executor.LiquidAndAirGapPair: |
| 1049 | + """Execute single-dispense steps. |
| 1050 | + 1. Move pipette to the ‘submerge’ position with normal speed. |
| 1051 | + - The pipette will move in an arc- move to max z height of labware |
| 1052 | + (if asp & disp are in same labware) |
| 1053 | + or max z height of all labware (if asp & disp are in separate labware) |
| 1054 | + 2. Air gap removal: |
| 1055 | + - If dispense location is above the meniscus, DO NOT remove air gap |
| 1056 | + (it will be dispensed along with rest of the liquid later). |
| 1057 | + All other scenarios, remove the air gap by doing a dispense |
| 1058 | + - Flow rate = min(dispenseFlowRate, (airGapByVolume)/sec) |
| 1059 | + - Use the post-dispense delay |
| 1060 | + 4. Move to the dispense position at the specified ‘submerge’ speed |
| 1061 | + (even if we might not be moving into the liquid) |
| 1062 | + - Do a delay (submerge delay) |
| 1063 | + 6. Dispense: |
| 1064 | + - Dispense at the specified flow rate. |
| 1065 | + - Do a push out as specified ONLY IF there is no mix following the dispense AND the tip is empty. |
| 1066 | + Volume for push out is the volume being dispensed. So if we are dispensing 50uL, use pushOutByVolume[50] as push out volume. |
| 1067 | + 7. Delay |
| 1068 | + 8. Mix using the same flow rate and delays as specified for asp+disp, |
| 1069 | + with the volume and the number of repetitions specified. Use the delays in asp & disp. |
| 1070 | + - If the dispense position is outside the liquid, then raise error if mix is enabled. |
| 1071 | + - If the user wants to perform a mix then they should specify a dispense position that’s inside the liquid OR do mix() on the wells after transfer. |
| 1072 | + - Do push out at the last dispense. |
| 1073 | + 9. Retract |
| 1074 | +
|
| 1075 | + Return: |
| 1076 | + The last liquid and air gap pair in tip. |
| 1077 | + """ |
| 1078 | + dispense_props = transfer_properties.dispense |
| 1079 | + dest_loc, dest_well = dest |
| 1080 | + dispense_point = ( |
| 1081 | + tx_comps_executor.absolute_point_from_position_reference_and_offset( |
| 1082 | + well=dest_well, |
| 1083 | + position_reference=dispense_props.position_reference, |
| 1084 | + offset=dispense_props.offset, |
| 1085 | + ) |
| 1086 | + ) |
| 1087 | + dispense_location = Location(dispense_point, labware=dest_loc.labware) |
| 1088 | + if len(tip_contents) > 0: |
| 1089 | + last_liquid_and_airgap_in_tip = tip_contents[-1] |
| 1090 | + else: |
| 1091 | + last_liquid_and_airgap_in_tip = tx_comps_executor.LiquidAndAirGapPair( |
| 1092 | + liquid=0, |
| 1093 | + air_gap=0, |
| 1094 | + ) |
| 1095 | + components_executor = tx_comps_executor.TransferComponentsExecutor( |
| 1096 | + instrument_core=self, |
| 1097 | + transfer_properties=transfer_properties, |
| 1098 | + target_location=dispense_location, |
| 1099 | + target_well=dest_well, |
| 1100 | + transfer_type=transfer_type, |
| 1101 | + tip_state=tx_comps_executor.TipState( |
| 1102 | + last_liquid_and_air_gap_in_tip=last_liquid_and_airgap_in_tip |
| 1103 | + ), |
| 1104 | + ) |
| 1105 | + components_executor.submerge(submerge_properties=dispense_props.submerge) |
| 1106 | + if dispense_props.mix.enabled: |
| 1107 | + push_out_vol = 0.0 |
| 1108 | + else: |
| 1109 | + # TODO: if distributing, do a push out only at the last dispense |
| 1110 | + push_out_vol = dispense_props.push_out_by_volume.get_for_volume(volume) |
| 1111 | + components_executor.dispense_and_wait( |
| 1112 | + volume=volume, |
| 1113 | + push_out_override=push_out_vol, |
| 1114 | + ) |
| 1115 | + components_executor.mix( |
| 1116 | + mix_properties=dispense_props.mix, |
| 1117 | + last_dispense_push_out=True, |
| 1118 | + ) |
| 1119 | + components_executor.retract_after_dispensing( |
| 1120 | + trash_location=trash_location, |
| 1121 | + source_location=source[0] if source else None, |
| 1122 | + source_well=source[1] if source else None, |
| 1123 | + ) |
| 1124 | + return components_executor.tip_state.last_liquid_and_air_gap_in_tip |
905 | 1125 |
|
906 | 1126 | def retract(self) -> None:
|
907 | 1127 | """Retract this instrument to the top of the gantry."""
|
@@ -994,3 +1214,7 @@ def nozzle_configuration_valid_for_lld(self) -> bool:
|
994 | 1214 | return self._engine_client.state.pipettes.get_nozzle_configuration_supports_lld(
|
995 | 1215 | self.pipette_id
|
996 | 1216 | )
|
| 1217 | + |
| 1218 | + def delay(self, seconds: float) -> None: |
| 1219 | + """Call a protocol delay.""" |
| 1220 | + self._protocol_core.delay(seconds=seconds, msg=None) |
0 commit comments