-
Notifications
You must be signed in to change notification settings - Fork 179
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
feat(api): implement the liquid transfer function #17179
Conversation
pip_api_name | ||
for pip_api_name, pip_name in PIPETTE_API_NAMES_MAP.items() | ||
if pip_name == pipette.pipetteName | ||
][0] |
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.
Small nitpick: This can be more efficiently implemented as:
load_name = next(
(pip_api_name
for pip_api_name, pip_name in PIPETTE_API_NAMES_MAP.items()
if pip_name == pipette.pipetteName),
None
)
assert load_name
return load_name
@@ -883,7 +904,7 @@ def load_liquid_class( | |||
|
|||
liquid_class_record = LiquidClassRecord( | |||
liquidClassName=liquid_class.name, | |||
pipetteModel=self.get_model(), # TODO: verify this is the correct 'model' to use | |||
pipetteModel=pipette_load_name, # TODO: verify this is the correct 'model' to use |
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.
Does comment still apply?
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.
Nope. I'll remove it.
pipette_load_name=self.get_pipette_name(), # TODO: update this to use load name instead | ||
tiprack_uri=tiprack_uri, | ||
pipette_load_name=self.get_pipette_load_name(), | ||
tiprack_uri=tiprack_uri_for_transfer_props, | ||
) | ||
transfer_props = liquid_class.get_for( |
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.
self.load_liquid_class()
above also calls liquid_class.get_for()
, right?
Can you refactor self.load_liquid_class()
to just take in the transfer_props
directly as an argument?
@@ -1034,7 +1153,9 @@ def aspirate_liquid_class( | |||
) | |||
components_executor.aspirate_and_wait(volume=volume) | |||
components_executor.retract_after_aspiration(volume=volume) | |||
return components_executor.tip_state.last_liquid_and_air_gap_in_tip | |||
last_contents = components_executor.tip_state.last_liquid_and_air_gap_in_tip | |||
tip_contents[-1] = last_contents |
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.
Hm, I'm a little confused what this is trying to do: it sometimes modifies the list that's passed in, and it sometimes modifies a list that was created local to this function?
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.
Talked about it in person. The list that's passed in is not re-used by the caller function so it doesn't matter if the list gets modified here or not, since the list in the caller function gets replaced by ones returned by the aspirate_liquid_class
and dispense_liquid_class
. But I can see how it can cause confusion so I'll update this to use a copy of the list for any modifications.
mock_instrument_core.air_gap_in_place( | ||
volume=air_gap_volume, flow_rate=air_gap_volume | ||
), | ||
mock_instrument_core.delay(0.2), | ||
) | ||
if add_final_air_gap is True | ||
else None, | ||
mock_instrument_core.delay(0.2) if add_final_air_gap is True else None, |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
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.
It all looks good to me, and tested this on the robot so we're ready to hand this off for hardware testing
] | ||
for step_volume, src_dest_combo in source_dest_per_volume_step: | ||
step_source, step_destination = src_dest_combo | ||
is_last_step = len([source_dest_per_volume_step]) > 0 |
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 does this work? Why does len([source_dest_per_volume_step]) > 0
mean it's the last step? And wouldn't the length always be > 0?
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.
Oh shoot! I meant to fix this! Thanks for catching it!
# Neither aspirate nor a dispense process should be adding liquid | ||
# when there is an air gap present. | ||
assert ( | ||
self.last_liquid_and_air_gap_in_tip.air_gap == 0 | ||
), "Air gap present in the tip." | ||
self.last_liquid_and_air_gap_in_tip.liquid += volume | ||
|
||
def remove_liquid(self, volume: float) -> None: | ||
def delete_liquid(self, volume: float) -> None: | ||
# Neither aspirate nor a dispense process should be removing liquid | ||
# when there is an air gap present. | ||
assert ( | ||
self.last_liquid_and_air_gap_in_tip.air_gap == 0 | ||
), "Air gap present in the tip." | ||
self.last_liquid_and_air_gap_in_tip.liquid -= volume |
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.
Is it allowed to make last_liquid_and_air_gap_in_tip.liquid
go below zero here?
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.
Nope. I can add an assertion there to make sure we never remove more liquid than is present
# Mapping of public Python Protocol API pipette load names | ||
# to names used by the internal Opentrons system | ||
# TODO (spp, 2025-01-02): make the API load name a part of the pipette's definition | ||
PIPETTE_API_NAMES_MAP = { |
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.
Is this same dict in api/src/opentrons/protocol_api/validation.py
as well? (I also needed it for Python generation from PD -- see here.)
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.
Yep. I've moved it to shared data for precisely this reason that PD will need to use it too
source_dest_per_volume_step | ||
) | ||
except StopIteration: | ||
is_last_step = 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.
Is source_dest_per_volume_step
an iterator or a list?
Can you turn it into a list?
If so, could you just enumerate
over the list and check if you're at the index of the last item?
Like,
for idx, (next_step_volume, next_src_dest_combo) in enumerate(source_dest_per_volume_step):
is_last_step = (idx == len(source_dest_per_volume_step) - 1)
do stuff ..
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.
It's an iterator.
Yes I can turn it into a list but that defeats the purpose of having expand_for_volume_constraints
as a generator function that uses memory more efficiently. The iterator can have quite a number of elements on some occasions. So unless there's a compelling reason to not do it the way I have over here, I think we should avoid converting it to list.
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.
Ah I see. In that case, I wouldn't do this inline in the code here -- it's too much logic to pack in here. Instead, I would define a helper function that implements another generator to wrap expand_for_volume_constraints()
that includes whether we're at the last step.
But you also control expand_for_volume_constraints()
, don't you? Could you just make it include is_last_step
in the values that it yields?
bdfe17b
to
f1c2919
Compare
…17244) - Functions should not modify any argument that's passed in (unless that's the purpose of the function). - Objects should not modify any arguments given to their constructor (unless the object is supposed to take ownership of the argument).
…tep if not using new tip
Argument unpacking to make `decoy.verify()` work with optional calls.
7287185
to
3a1b24b
Compare
Closes AUTH-866
Overview
Final PR of the 3-PR series.
Implements
InstrumentCore.transfer_liquid()
.The
transfer_liquid()
method does the following:aspirate_liquid_class()
anddispense_liquid_class()
for every source-> destination transfer.This PR also updates the tiprack 'names' to URIs in the liquid class definition in shared data for 'water' only. We will have a separate PR for updating rest of the definitions.
Test Plan and Hands on Testing
transfer_liquid()
execution succeeds for various supported configurationsAdditionally, we will be adding analysis snapshot tests with protocols that use various configurations for transferring with liquid classes.
Review requests
transfer_liquid()
turned out to be not easily unit-testable, mainly owing to the location of implementation (insideInstrumentCore
). I plan on moving this function out of the InstrumentCore so that it can be unit-tested without the constraints of rest of the Instrument core. But this still serves as a robust implementation for doing end-to-end, integration testing so that's what I am focusing on in this PR.Risk assessment
Low. Doesn't change existing API