-
Notifications
You must be signed in to change notification settings - Fork 177
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
refactor(engine): dodge thermocycler when moving pipettes #8931
Conversation
Codecov Report
@@ Coverage Diff @@
## edge #8931 +/- ##
==========================================
+ Coverage 74.79% 74.81% +0.02%
==========================================
Files 1860 1860
Lines 49294 49355 +61
Branches 4852 4852
==========================================
+ Hits 36869 36926 +57
- Misses 11581 11585 +4
Partials 844 844
Flags with carried forward coverage won't be shown. Click here to find out more.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Organizational comments below
@@ -216,3 +232,34 @@ def get_tip_drop_location( | |||
z=well_location.offset.z - tip_z_offset, | |||
) | |||
) | |||
|
|||
def should_dodge_thermocycler( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this make more sense in the MotionView
class? All other logic in this class is related to static geometry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is put in the MotionView class then the test for get_movement_waypoints
becomes much more complicated, and would require stubbing out an internal function, which we are trying to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I think I meant ModuleView
. If the signature changes to
def should_dodge_thermocycler(
self,
start: DeckSlotName,
end: DeckSlotName,
) -> bool:
...then ModuleView
has everything it needs, and MotionView
can call it by calling self._geometry.get_ancestor_slot_name
Another idea that does not involve moving this function would be to have it return the a list of extra_waypoints
directly (or an empty list), simplifying the logic in MotionView
if self._modules.get_all() and ModuleModel.THERMOCYCLER_MODULE_V1 in [ | ||
mod.model for mod in self._modules.get_all() | ||
]: | ||
transit = ( | ||
get_slot_name(from_labware).value, | ||
get_slot_name(to_labware).value, | ||
) | ||
if transit in BAD_PAIRS: | ||
return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could all be moved to a selector in ModulesView
, and it might be easier to test/reason with if you do. Something along the lines of:
def should_dodge_thermocycler(self, start: DeckSlotName, end: DeckSlotName) -> bool:
has_tc = any(
m.model == ModuleModel.THERMOCYCLER_MODULE_V1
for m in self._state.modules_by_id.values()
)
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic requires access to both Modules and Labware. GeometryView
already has both so it seems appropriate to keep it here. Even though should_dodge_thermocycler
primarily depends on the presence of a module, it is a concern of how non-module labware are placed on deck so GeometryView
does feel like the appropriate place, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code LGTM if this works on a robot.
slot_def = standard_deck_def["locations"]["orderedSlots"][1] | ||
slot_pos = slot_def["position"] | ||
expected_center = Point( | ||
x=slot_pos[0] + slot_def["boundingBox"]["xDimension"] / 2, | ||
y=slot_pos[1] + slot_def["boundingBox"]["yDimension"] / 2, | ||
z=slot_pos[2] + slot_def["boundingBox"]["zDimension"] / 2, | ||
) | ||
result = subject.get_slot_center_position(DeckSlotName.SLOT_2) | ||
assert result == expected_center |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is mirroring the production code in how it calculates the center point. Should we hard-code absolute deck coordinates in this test, instead?
@@ -216,3 +232,34 @@ def get_tip_drop_location( | |||
z=well_location.offset.z - tip_z_offset, | |||
) | |||
) | |||
|
|||
def should_dodge_thermocycler( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I think I meant ModuleView
. If the signature changes to
def should_dodge_thermocycler(
self,
start: DeckSlotName,
end: DeckSlotName,
) -> bool:
...then ModuleView
has everything it needs, and MotionView
can call it by calling self._geometry.get_ancestor_slot_name
Another idea that does not involve moving this function would be to have it return the a list of extra_waypoints
directly (or an empty list), simplifying the logic in MotionView
if self._modules.get_all() and ModuleModel.THERMOCYCLER_MODULE_V1 in [ | ||
mod.model for mod in self._modules.get_all() | ||
]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the multiple calls to get_all
? Could we create ModuleView.has_thermocycler
or something instead? This suggestion is moot if should_dodge_thermocycler
moves to ModuleView
@@ -188,6 +188,8 @@ class WaypointSpec: | |||
labware_z: Optional[float] = None | |||
all_labware_z: Optional[float] = None | |||
max_travel_z: float = 50 | |||
should_dodge_thermocycler: bool = False | |||
extra_waypoints: Sequence[Tuple[float, float]] = () | |||
|
|||
|
|||
# TODO(mc, 2021-01-14): these tests probably need to be rethought; this fixture |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for leaving these tests in such an awful state...
@@ -104,6 +105,16 @@ def get_movement_waypoints( | |||
else: | |||
move_type = MoveType.GENERAL_ARC | |||
min_travel_z = self._geometry.get_all_labware_highest_z() | |||
if location is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we somehow encode this branch logic into should_dodge_thermocycler
? Perhaps by making the starting slot/labware argument optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would that relate to the bad pairs then? nvm, I misunderstood. Sure, that won't be too bad
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to leave this as is. If location is None
then location.labware_id
doesn't exist. So we have to check it here anyway.
Code looks pretty good! Will approve once tested on a real robot (lmk if that's already happened) |
Yep, tested on a robot and verified that pipette moves through slot 5 when thermocycler is loaded while moves directly when there's no thermocycler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🌡️
Co-authored-by: Max Marrone <max@opentrons.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is already merged but just chiming in to say I like where this ended up 👍
Addresses #8911
Overview
Makes the pipettes move around the thermocycler instead of over it/ crashing into it. The logic is copied from the legacy movement planning code as is. It adds an intermediate waypoint over slot 5 when performing a move that would otherwise have to go over the slots containing a thermocycler.
Changelog
motion.py
: pipettes will move through slot 5 to dodge.should_dodge_thermocycler
function togeometry.py
ModuleView
Review requests
current_well
is not known?BAD_PAIRS
, once with thermocycler loaded and once without and make sure that the movement is routed over slot 5 when thermocycler is present.Risk assessment
Medium. Should only affect limited set of movements when thermocycler is being used.