Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(hardware_control): Fixing liquid probe bugs found in end to end testing #15619

Merged
merged 22 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions api/lldfunctionexamples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from opentrons import protocol_api, types
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved
import dataclasses
import typing


TIPRACK_NAME = "opentrons_flex_96_tiprack_1000ul"
PLATE_NAME = "nest_96_wellplate_200ul_flat"
RESERVOIR_NAME = "nest_12_reservoir_15ml"
PIPETTE_SINGLE_CHANNEL_NAME = "flex_1channel_1000"
TRASH_NAME = "opentrons_1_trash_1100ml_fixed"

metadata = {
"protocolName": "Liquid presence detection enable/disable"
}
requirements = {
"robotType": "Flex",
"apiLevel": "2.20"
}

def run(ctx: protocol_api.ProtocolContext):
tiprack = (ctx.load_labware(TIPRACK_NAME, 'D1')).wells_by_name()
reservoir = (ctx.load_labware(RESERVOIR_NAME, 'C2')).wells_by_name()
plate = (ctx.load_labware(PLATE_NAME, 'D2')).wells_by_name()
trash_bin = ctx.load_trash_bin('B3')
pipette = ctx.load_instrument(PIPETTE_SINGLE_CHANNEL_NAME, mount="left", liquid_presence_detection=False)
assert pipette.liquid_detection == False


water = ctx.define_liquid(name="water", description="Normal water", display_color="#42AB2D")
reservoir["A1"].load_liquid(liquid=water, volume=10000)
reservoir["A2"].load_liquid(liquid=water, volume=10000)
reservoir["A3"].load_liquid(liquid=water, volume=10000)


# ####### FIRST CYCLE, SHOULD NOT DO LLD #######
# pipette.pick_up_tip(tiprack["A1"])

# # Liquid presence detection does not take place in the following aspirate command
# # because the pipette was loaded in with liquid_presence_detection = False
# pipette.aspirate(5, reservoir["A1"])
# pipette.dispense(5, plate["A1"])

# #Make sure to get a new tip each time before attempting liquid presence detection
# pipette.drop_tip(trash_bin)





# ####### SECOND CYCLE, SHOULD NOT DO LLD #######
# pipette.pick_up_tip(tiprack["A2"])

# print(pipette.liquid_detection) #should be: False
# pipette.liquid_detection = True
# print(pipette.liquid_detection) #should be: True
# pipette.aspirate(5, reservoir["A2"])
# pipette.dispense(5, plate["A1"])
# pipette.drop_tip(trash_bin)




####### THIRD CYCLE, SHOULD DO LLD #######
pipette.pick_up_tip(tiprack["A3"])

print(pipette.liquid_detection) #should be: False
pipette.require_liquid_presence(plate["A4"]) #should run without error
pipette.drop_tip(trash_bin)




# ####### FOURTH CYCLE, SHOULD TRY TO DO LLD #######
# pipette.pick_up_tip(tiprack["A4"])

# pipette.require_liquid_presence(plate["A2"]) #should error because no liquid in there but provide chance for recovery

# pipette.drop_tip(trash_bin)



# ####### FIFTH CYCLE, SHOULD DO LLD #######
# pipette.pick_up_tip(tiprack["A5"])

# assert pipette.detect_liquid_presence(plate["A1"]) == True

# pipette.drop_tip(trash_bin)
# pipette.pick_up_tip(tiprack["A6"])

# #assert pipette.detect_liquid_presence(plate["A2"]) == False

# pipette.drop_tip(trash_bin)

6 changes: 3 additions & 3 deletions api/src/opentrons/config/defaults_ot3.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
DEFAULT_MODULE_OFFSET = [0.0, 0.0, 0.0]

DEFAULT_LIQUID_PROBE_SETTINGS: Final[LiquidProbeSettings] = LiquidProbeSettings(
mount_speed=10,
plunger_speed=5,
mount_speed=5,
plunger_speed=20,
plunger_impulse_time=0.2,
sensor_threshold_pascals=15,
output_option=OutputOptions.sync_buffer_to_csv,
Expand Down Expand Up @@ -328,7 +328,7 @@ def _build_default_liquid_probe(
or output_option is OutputOptions.stream_to_csv
):
data_files = _build_log_files_with_default(
from_conf.get("data_files", {}), default.data_files
from_conf.get("data_files", None), default.data_files
)
return LiquidProbeSettings(
mount_speed=from_conf.get("mount_speed", default.mount_speed),
Expand Down
21 changes: 10 additions & 11 deletions api/src/opentrons/hardware_control/backends/ot3controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@
PipetteLiquidNotFoundError,
CommunicationError,
PythonException,
UnsupportedHardwareCommand,
)

from .subsystem_manager import SubsystemManager
Expand Down Expand Up @@ -1363,16 +1362,16 @@ async def liquid_probe(
probe: InstrumentProbeType = InstrumentProbeType.PRIMARY,
force_both_sensors: bool = False,
) -> float:
if output_option == OutputOptions.sync_buffer_to_csv:
if (
self._subsystem_manager.device_info[
SubSystem.of_mount(mount)
].revision.tertiary
!= "1"
):
raise UnsupportedHardwareCommand(
"Liquid Probe not supported on this pipette firmware"
)
# if output_option == OutputOptions.sync_buffer_to_csv:
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved
# if (
# self._subsystem_manager.device_info[
# SubSystem.of_mount(mount)
# ].revision.tertiary
# != "1"
# ):
# raise UnsupportedHardwareCommand(
# "Liquid Probe not supported on this pipette firmware"
# )

head_node = axis_to_node(Axis.by_mount(mount))
tool = sensor_node_for_pipette(OT3Mount(mount.value))
Expand Down
11 changes: 7 additions & 4 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2636,10 +2636,13 @@ async def liquid_probe(
pos = await self.gantry_position(checked_mount, refresh=True)
while (probe_start_pos.z - pos.z) < max_z_dist:
# safe distance so we don't accidentally aspirate liquid if we're already close to liquid
safe_plunger_pos = pos._replace(z=(pos.z + 2))
safe_plunger_pos = top_types.Point(pos.x, pos.y, pos.z + 2)
# overlap amount we want to use between passes
pass_start_pos = pos._replace(z=(pos.z + 0.5))

pass_start_pos = top_types.Point(pos.x, pos.y, pos.z + 0.5)
max_z_time = (
max_z_dist - (probe_start_pos.z - safe_plunger_pos.z)
) / probe_settings.mount_speed
pass_travel = min(max_z_time * probe_settings.plunger_speed, p_travel)
# Prep the plunger
await self.move_to(checked_mount, safe_plunger_pos)
if probe_settings.aspirate_while_sensing:
Expand All @@ -2655,7 +2658,7 @@ async def liquid_probe(
checked_mount,
probe_settings,
probe if probe else InstrumentProbeType.PRIMARY,
p_travel,
pass_travel,
)
# if we made it here without an error we found the liquid
error = None
Expand Down
20 changes: 20 additions & 0 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,16 @@ def liquid_probe_with_recovery(self, well_core: WellCore) -> None:
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)
)

loc = Location(well_core.get_top(10), "Well")
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved

self.move_to(
location=loc,
well_core=well_core,
force_direct=False,
minimum_z_height=None,
speed=None,
)
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved

self._engine_client.execute_command(
cmd.LiquidProbeParams(
labwareId=labware_id,
Expand All @@ -867,6 +877,16 @@ def liquid_probe_without_recovery(self, well_core: WellCore) -> float:
origin=WellOrigin.TOP, offset=WellOffset(x=0, y=0, z=0)
)

loc = Location(well_core.get_top(10), "Well")
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved

self.move_to(
location=loc,
well_core=well_core,
force_direct=False,
minimum_z_height=None,
speed=None,
)

result = self._engine_client.execute_command_without_recovery(
cmd.LiquidProbeParams(
labwareId=labware_id,
Expand Down
9 changes: 0 additions & 9 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
CommandParameterLimitViolated,
UnexpectedTipRemovalError,
)
from opentrons.protocol_engine.errors.exceptions import WellDoesNotExistError
from opentrons.legacy_broker import LegacyBroker
from opentrons.hardware_control.dev_types import PipetteDict
from opentrons import types
Expand Down Expand Up @@ -2055,8 +2054,6 @@ def detect_liquid_presence(self, well: labware.Well) -> bool:

:returns: A boolean.
"""
if not isinstance(well, labware.Well):
raise WellDoesNotExistError("You must provide a valid well to check.")
try:
self._core.liquid_probe_without_recovery(well._core)
except ProtocolCommandFailedError as e:
Expand All @@ -2072,9 +2069,6 @@ def require_liquid_presence(self, well: labware.Well) -> None:

:returns: None.
"""
if not isinstance(well, labware.Well):
raise WellDoesNotExistError("You must provide a valid well to check.")

self._core.liquid_probe_with_recovery(well._core)

@requires_version(2, 20)
Expand All @@ -2087,8 +2081,5 @@ def measure_liquid_height(self, well: labware.Well) -> float:

This is intended for Opentrons internal use only and is not a guaranteed API.
"""
if not isinstance(well, labware.Well):
raise WellDoesNotExistError("You must provide a valid well to check.")

height = self._core.liquid_probe_without_recovery(well._core)
return height
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn:
Return the z-position of the found liquid.

Raises:
TipNotAttachedError: if there is not tip attached to the pipette
TipNotAttachedError: if there is no tip attached to the pipette
MustHomeError: if the plunger is not in a valid position
TipNotEmptyError: if the tip starts with liquid in it(AKA it's wet and unusable for LLD)
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved
LiquidNotFoundError: if liquid is not found during the probe process.
"""
pipette_id = params.pipetteId
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/protocol_engine/execution/pipetting.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def __init__(self, state_view: StateView, hardware_api: HardwareControlAPI) -> N

def get_is_empty(self, pipette_id: str) -> bool:
"""Get whether a pipette has a working volume equal to 0."""
return self._state_view.pipettes.get_working_volume(pipette_id) == 0
return self._state_view.pipettes.get_aspirated_volume(pipette_id) == 0
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved

def get_is_ready_to_aspirate(self, pipette_id: str) -> bool:
"""Get whether a pipette is ready to aspirate."""
Expand Down Expand Up @@ -235,7 +235,7 @@ def __init__(

def get_is_empty(self, pipette_id: str) -> bool:
"""Get whether a pipette has a working volume equal to 0."""
return self._state_view.pipettes.get_working_volume(pipette_id) == 0
return self._state_view.pipettes.get_aspirated_volume(pipette_id) == 0
aaron-kulkarni marked this conversation as resolved.
Show resolved Hide resolved

def get_is_ready_to_aspirate(self, pipette_id: str) -> bool:
"""Get whether a pipette is ready to aspirate."""
Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_engine/state/pipettes.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ def get_current_tip_lld_settings(self, pipette_id: str) -> float:
if attached_tip is None or attached_tip.volume is None:
return 0
lld_settings = self.get_pipette_lld_settings(pipette_id)
tipVolume = str(attached_tip.volume)
tipVolume = "t" + str(int(attached_tip.volume))
if (
lld_settings is None
or lld_settings[tipVolume] is None
Expand Down
Loading