Skip to content

Commit

Permalink
Merge branch 'chore_release-7.1.0' into app_single-slot-conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
brenthagen committed Dec 11, 2023
2 parents bc22694 + f7caea6 commit 0622175
Show file tree
Hide file tree
Showing 38 changed files with 505 additions and 277 deletions.
40 changes: 26 additions & 14 deletions api/src/opentrons/hardware_control/backends/ot3controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,13 +735,19 @@ async def home_tip_motors(
if not self._feature_flags.stall_detection_enabled
else False,
)
positions = await runner.run(can_messenger=self._messenger)
if NodeId.pipette_left in positions:
self._gear_motor_position = {
NodeId.pipette_left: positions[NodeId.pipette_left].motor_position
}
else:
log.debug("no position returned from NodeId.pipette_left")
try:
positions = await runner.run(can_messenger=self._messenger)
if NodeId.pipette_left in positions:
self._gear_motor_position = {
NodeId.pipette_left: positions[NodeId.pipette_left].motor_position
}
else:
log.debug("no position returned from NodeId.pipette_left")
self._gear_motor_position = {}
except Exception as e:
log.error("Clearing tip motor position due to failed movement")
self._gear_motor_position = {}
raise e

async def tip_action(
self,
Expand All @@ -755,13 +761,19 @@ async def tip_action(
if not self._feature_flags.stall_detection_enabled
else False,
)
positions = await runner.run(can_messenger=self._messenger)
if NodeId.pipette_left in positions:
self._gear_motor_position = {
NodeId.pipette_left: positions[NodeId.pipette_left].motor_position
}
else:
log.debug("no position returned from NodeId.pipette_left")
try:
positions = await runner.run(can_messenger=self._messenger)
if NodeId.pipette_left in positions:
self._gear_motor_position = {
NodeId.pipette_left: positions[NodeId.pipette_left].motor_position
}
else:
log.debug("no position returned from NodeId.pipette_left")
self._gear_motor_position = {}
except Exception as e:
log.error("Clearing tip motor position due to failed movement")
self._gear_motor_position = {}
raise e

@requires_update
@requires_estop
Expand Down
36 changes: 23 additions & 13 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,10 +905,11 @@ async def home_gear_motors(self) -> None:
GantryLoad.HIGH_THROUGHPUT
][OT3AxisKind.Q]

max_distance = self._backend.axis_bounds[Axis.Q][1]
# if position is not known, move toward limit switch at a constant velocity
if not any(self._backend.gear_motor_position):
if len(self._backend.gear_motor_position.keys()) == 0:
await self._backend.home_tip_motors(
distance=self._backend.axis_bounds[Axis.Q][1],
distance=max_distance,
velocity=homing_velocity,
)
return
Expand All @@ -917,7 +918,13 @@ async def home_gear_motors(self) -> None:
Axis.P_L
]

if current_pos_float > self._config.safe_home_distance:
# We filter out a distance more than `max_distance` because, if the tip motor was stopped during
# a slow-home motion, the position may be stuck at an enormous large value.
if (
current_pos_float > self._config.safe_home_distance
and current_pos_float < max_distance
):

fast_home_moves = self._build_moves(
{Axis.Q: current_pos_float}, {Axis.Q: self._config.safe_home_distance}
)
Expand All @@ -931,7 +938,9 @@ async def home_gear_motors(self) -> None:

# move until the limit switch is triggered, with no acceleration
await self._backend.home_tip_motors(
distance=(current_pos_float + self._config.safe_home_distance),
distance=min(
current_pos_float + self._config.safe_home_distance, max_distance
),
velocity=homing_velocity,
)

Expand Down Expand Up @@ -2001,15 +2010,16 @@ async def get_tip_presence_status(
Check tip presence status. If a high throughput pipette is present,
move the tip motors down before checking the sensor status.
"""
real_mount = OT3Mount.from_mount(mount)
async with contextlib.AsyncExitStack() as stack:
if (
real_mount == OT3Mount.LEFT
and self._gantry_load == GantryLoad.HIGH_THROUGHPUT
):
await stack.enter_async_context(self._high_throughput_check_tip())
result = await self._backend.get_tip_status(real_mount)
return result
async with self._motion_lock:
real_mount = OT3Mount.from_mount(mount)
async with contextlib.AsyncExitStack() as stack:
if (
real_mount == OT3Mount.LEFT
and self._gantry_load == GantryLoad.HIGH_THROUGHPUT
):
await stack.enter_async_context(self._high_throughput_check_tip())
result = await self._backend.get_tip_status(real_mount)
return result

async def verify_tip_presence(
self, mount: Union[top_types.Mount, OT3Mount], expected: TipStateType
Expand Down
3 changes: 2 additions & 1 deletion api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,14 @@ def _load_fixed_trash(self) -> None:
)

def append_disposal_location(
self, disposal_location: Union[TrashBin, WasteChute]
self, disposal_location: Union[Labware, TrashBin, WasteChute]
) -> None:
"""Append a disposal location object to the core"""
self._disposal_locations.append(disposal_location)

def get_disposal_locations(self) -> List[Union[Labware, TrashBin, WasteChute]]:
"""Get disposal locations."""

return self._disposal_locations

def get_max_speeds(self) -> AxisMaxSpeeds:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __init__(
self._loaded_modules: Set["AbstractModule"] = set()
self._module_cores: List[legacy_module_core.LegacyModuleCore] = []
self._labware_cores: List[LegacyLabwareCore] = [self.fixed_trash]
self._disposal_locations: List[Union[Labware, TrashBin, WasteChute]] = []

@property
def api_version(self) -> APIVersion:
Expand Down Expand Up @@ -133,11 +134,13 @@ def is_simulating(self) -> bool:
return self._sync_hardware.is_simulator # type: ignore[no-any-return]

def append_disposal_location(
self, disposal_location: Union[TrashBin, WasteChute]
self, disposal_location: Union[Labware, TrashBin, WasteChute]
) -> None:
raise APIVersionError(
"Disposal locations are not supported in this API Version."
)
if isinstance(disposal_location, (TrashBin, WasteChute)):
raise APIVersionError(
"Trash Bin and Waste Chute Disposal locations are not supported in this API Version."
)
self._disposal_locations.append(disposal_location)

def add_labware_definition(
self,
Expand Down Expand Up @@ -383,10 +386,7 @@ def get_loaded_instruments(

def get_disposal_locations(self) -> List[Union[Labware, TrashBin, WasteChute]]:
"""Get valid disposal locations."""
trash = self._deck_layout["12"]
if isinstance(trash, Labware):
return [trash]
raise APIVersionError("No dynamically loadable disposal locations.")
return self._disposal_locations

def pause(self, msg: Optional[str]) -> None:
"""Pause the protocol."""
Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_api/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def add_labware_definition(

@abstractmethod
def append_disposal_location(
self, disposal_location: Union[TrashBin, WasteChute]
self, disposal_location: Union[Labware, TrashBin, WasteChute]
) -> None:
"""Append a disposal location object to the core"""
...
Expand Down
33 changes: 24 additions & 9 deletions api/src/opentrons/protocol_api/instrument_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1636,7 +1636,11 @@ def channels(self) -> int:
@property # type: ignore
@requires_version(2, 16)
def active_channels(self) -> int:
"""The number of channels configured for active use using configure_nozzle_layout()."""
"""The number of channels the pipette will use to pick up tips.
By default, all channels on the pipette. Use :py:meth:`.configure_nozzle_layout`
to set the pipette to use fewer channels.
"""
return self._core.get_active_channels()

@property # type: ignore
Expand Down Expand Up @@ -1798,24 +1802,35 @@ def configure_nozzle_layout(
.. note::
When picking up fewer than 96 tips at once, the tip rack *must not* be
placed in a tip rack adapter in the deck. If you try to perform partial tip
pickup on a tip rack that is in an adapter, the API will raise an error.
placed in a tip rack adapter in the deck. If you try to pick up fewer than 96
tips from a tip rack that is in an adapter, the API will raise an error.
:param style: The shape of the nozzle layout.
- ``COLUMN`` sets the pipette to use 8 nozzles, aligned from front to back
with respect to the deck. This corresponds to a column of wells on labware.
- ``ALL`` resets the pipette to use all of its nozzles. Calling ``configure_nozzle_layout`` with no arguments also resets the pipette.
- ``ALL`` resets the pipette to use all of its nozzles. Calling
``configure_nozzle_layout`` with no arguments also resets the pipette.
:type style: ``NozzleLayout`` or ``None``
:param start: The nozzle at the back left of the layout, which the robot uses
to determine how it will move to different locations on the deck. The string
should be of the same format used when identifying wells by name. Use
``"A1"`` to have the pipette use its leftmost nozzles to pick up tips
*left-to-right* from a tip rack. Use ``"A12"`` to have the pipette use its
rightmost nozzles to pick up tips *right-to-left* from a tip rack.
should be of the same format used when identifying wells by name.
Required unless setting ``style=ALL``.
.. note::
When using the ``COLUMN`` layout, the only fully supported value is
``start="A12"``. You can use ``start="A1"``, but this will disable tip
tracking and you will have to specify the ``location`` every time you
call :py:meth:`.pick_up_tip`, such that the pipette picks up columns of
tips *from right to left* on the tip rack.
:type start: str or ``None``
:param tip_racks: List of tip racks to use during this configuration.
:param tip_racks: Behaves the same as setting the ``tip_racks`` parameter of
:py:meth:`.load_instrument`. If not specified, the new configuration resets
:py:obj:`.InstrumentContext.tip_racks` and you must specify the location
every time you call :py:meth:`~.InstrumentContext.pick_up_tip`.
:type tip_racks: List[:py:class:`.Labware`]
"""
# TODO: add the following back into the docstring when QUADRANT is supported
#
Expand Down
48 changes: 43 additions & 5 deletions api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
from opentrons.commands import protocol_commands as cmds, types as cmd_types
from opentrons.commands.publisher import CommandPublisher, publish
from opentrons.protocols.api_support import instrument as instrument_support
from opentrons.protocols.api_support.deck_type import NoTrashDefinedError
from opentrons.protocols.api_support.deck_type import (
NoTrashDefinedError,
should_load_fixed_trash_for_python_protocol,
)
from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocols.api_support.util import (
AxisMaxSpeeds,
Expand Down Expand Up @@ -138,7 +141,26 @@ def __init__(
mount: None for mount in Mount
}
self._bundled_data: Dict[str, bytes] = bundled_data or {}

# With the addition of Moveable Trashes and Waste Chute support, it is not necessary
# to ensure that the list of "disposal locations", essentially the list of trashes,
# is initialized correctly on protocols utilizing former API versions prior to 2.16
# and also to ensure that any protocols after 2.16 intialize a Fixed Trash for OT-2
# protocols so that no load trash bin behavior is required within the protocol itself.
# Protocols prior to 2.16 expect the Fixed Trash to exist as a Labware object, while
# protocols after 2.16 expect trash to exist as either a TrashBin or WasteChute object.

self._load_fixed_trash()
if should_load_fixed_trash_for_python_protocol(self._api_version):
self._core.append_disposal_location(self.fixed_trash)
elif (
self._api_version >= APIVersion(2, 16)
and self._core.robot_type == "OT-2 Standard"
):
_fixed_trash_trashbin = TrashBin(
location=DeckSlotName.FIXED_TRASH, addressable_area_name="fixedTrash"
)
self._core.append_disposal_location(_fixed_trash_trashbin)

self._commands: List[str] = []
self._unsubscribe_commands: Optional[Callable[[], None]] = None
Expand Down Expand Up @@ -861,10 +883,10 @@ def load_instrument(
log=logger,
)

trash: Optional[Labware]
trash: Optional[Union[Labware, TrashBin]]
try:
trash = self.fixed_trash
except NoTrashDefinedError:
except (NoTrashDefinedError, APIVersionError):
trash = None

instrument = InstrumentContext(
Expand Down Expand Up @@ -1024,17 +1046,33 @@ def deck(self) -> Deck:

@property # type: ignore
@requires_version(2, 0)
def fixed_trash(self) -> Labware:
def fixed_trash(self) -> Union[Labware, TrashBin]:
"""The trash fixed to slot 12 of the robot deck.
It has one well and should be accessed like labware in your protocol.
In API Versions prior to 2.16 it has one well and should be accessed like labware in your protocol.
e.g. ``protocol.fixed_trash['A1']``
In API Version 2.16 and above it returns a Trash fixture for OT-2 Protocols.
"""
if self._api_version >= APIVersion(2, 16):
if self._core.robot_type == "OT-3 Standard":
raise APIVersionError(
"Fixed Trash is not supported on Flex protocols in API Version 2.16 and above."
)
disposal_locations = self._core.get_disposal_locations()
if len(disposal_locations) == 0:
raise NoTrashDefinedError(
"No trash container has been defined in this protocol."
)
if isinstance(disposal_locations[0], TrashBin):
return disposal_locations[0]

fixed_trash = self._core_map.get(self._core.fixed_trash)
if fixed_trash is None:
raise NoTrashDefinedError(
"No trash container has been defined in this protocol."
)

return fixed_trash

def _load_fixed_trash(self) -> None:
Expand Down
10 changes: 9 additions & 1 deletion api/src/opentrons/protocol_engine/commands/load_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,18 @@ async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult:
)

if isinstance(params.location, AddressableAreaLocation):
area_name = params.location.addressableAreaName
if not fixture_validation.is_deck_slot(params.location.addressableAreaName):
raise LabwareIsNotAllowedInLocationError(
f"Cannot load {params.loadName} onto addressable area {params.location.addressableAreaName}"
f"Cannot load {params.loadName} onto addressable area {area_name}"
)
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
area_name
)
elif isinstance(params.location, DeckSlotLocation):
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.location.slotName.id
)

verified_location = self._state_view.geometry.ensure_location_not_occupied(
params.location
Expand Down
4 changes: 4 additions & 0 deletions api/src/opentrons/protocol_engine/commands/load_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ def __init__(

async def execute(self, params: LoadModuleParams) -> LoadModuleResult:
"""Check that the requested module is attached and assign its identifier."""
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.location.slotName.id
)

verified_location = self._state_view.geometry.ensure_location_not_occupied(
params.location
)
Expand Down
9 changes: 9 additions & 0 deletions api/src/opentrons/protocol_engine/commands/move_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from opentrons.types import Point
from ..types import (
LabwareLocation,
DeckSlotLocation,
OnLabwareLocation,
AddressableAreaLocation,
LabwareMovementStrategy,
Expand Down Expand Up @@ -115,6 +116,10 @@ async def execute( # noqa: C901
raise LabwareMovementNotAllowedError(
f"Cannot move {current_labware.loadName} to addressable area {area_name}"
)
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
area_name
)

if fixture_validation.is_gripper_waste_chute(area_name):
# When dropping off labware in the waste chute, some bigger pieces
# of labware (namely tipracks) can get stuck between a gripper
Expand All @@ -129,6 +134,10 @@ async def execute( # noqa: C901
y=0,
z=0,
)
elif isinstance(params.newLocation, DeckSlotLocation):
self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.newLocation.slotName.id
)

available_new_location = self._state_view.geometry.ensure_location_not_occupied(
location=params.newLocation
Expand Down
Loading

0 comments on commit 0622175

Please sign in to comment.