-
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): add execution of dispense steps for liquid class based transfer #17138
base: AUTH-866-add-transfer-flow-builder
Are you sure you want to change the base?
feat(api): add execution of dispense steps for liquid class based transfer #17138
Conversation
def update( | ||
self, | ||
liquid: Optional[float] = None, | ||
air_gap: Optional[float] = None, | ||
ready_to_aspirate: Optional[bool] = None, | ||
) -> None: | ||
"""Update the tip state contents with given values.""" | ||
if liquid is not None: | ||
self.last_liquid_and_air_gap_in_tip.liquid = liquid | ||
if air_gap is not None: | ||
self.last_liquid_and_air_gap_in_tip.air_gap = air_gap | ||
if ready_to_aspirate is not None: | ||
self.ready_to_aspirate = ready_to_aspirate |
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.
Remove
`TransferComponentsExecutor`s should be ready_to_aspirate == True. | ||
""" | ||
|
||
ready_to_aspirate: bool = 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.
Consider making this an enum that keeps track of where the plunger is
c3cc384
to
9b2de59
Compare
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.
Overall good, just some minor code organization, clarifications and reminders that may be addressed in other PRs in this stack
|
||
ready_to_aspirate: bool = True | ||
# TODO: maybe use the tip contents from engine state instead. | ||
last_liquid_and_air_gap_in_tip: LiquidAndAirGapPair = LiquidAndAirGapPair( |
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.
Reminder this needs to be fixed before the final merge into edge
# 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." |
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.
Are these intended to be internal asserts for our logic, cause if not we should raise a more well formed error (otherwise this is a-okay)
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.
They're internal asserts for internal logic
self.aspirate_and_wait(volume=mix_properties.volume) | ||
# TODO: Update to doing a push out at the end of mix for a post-dispense mix | ||
self.dispense_and_wait(volume=mix_properties.volume, push_out=0) | ||
if n == 1: |
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 still think I'd prefer this to be a straightforward loop to n-1 repetitions, then follow that with the push out dispense_and_wait
, to avoid the if
check and simplify this a little
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.
Adding onto this comment: Can you just shove the logic into the dispense_and_wait()
expression? I.e.,
self.dispense_and_wait(
...,
push_out_override=push_out_vol if last_dispense_push_out is True and n == 1 else 0
)
) | ||
retract_delay = retract_props.delay | ||
if retract_delay.enabled: | ||
assert retract_delay.duration 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.
stray thought, but maybe we can add a @property
to these liquid class properties to encapsulate the enabled is True
and .foo is not None
checks
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.
That's a good thought and might help remove all of these redundant checks
self._instrument.delay(retract_delay.duration) | ||
|
||
blowout_props = retract_props.blowout | ||
if ( |
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 block of logic starting here is a little confusing with how it's set up. I'm not sure if it can be simplified, but if not having some comments and organizing the blocks a little differently might make this easier to follow
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.
True. I'll add comments to explain.
and touch_tip_props.z_offset is not None | ||
and touch_tip_props.mm_to_edge is not None | ||
) | ||
# TODO: update this once touch tip has mmToEdge |
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.
we do have mmToEdge now!
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.
Will address it in a follow-up PR.
@@ -130,6 +136,8 @@ def test_aspirate_and_wait( | |||
transfer_properties=sample_transfer_props, | |||
target_location=Location(Point(1, 2, 3), labware=None), | |||
target_well=source_well, | |||
tip_state=TipState(), |
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.
based on our conversation last week and the fix for the mutable object in TipState
, a reminder that some of these tests might be able to be cleaned up
# TODO: when aspirating for consolidation, do not perform mix | ||
components_executer.mix(mix_properties=aspirate_props.mix) | ||
components_executor.mix( |
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.
Wait, when aspirating for liquid classes, you ALWAYS mix? Like, the user doesn't have an option to not mix?
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.
No, you don't always mix. The TransferComponentsExecutor.mix()
checks whether mix is enabled or not and then performs the mix accordingly.
components_executer.retract_after_aspiration(volume=volume) | ||
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 |
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.
To make sure I understand: You pass in a last_liquid_and_airgap_in_tip
to the constructor when you make the components_executor
. Then after you call submerge()
, mix()
, aspirate_and_wait()
, etc., the components_executor
will have a different last_liquid_and_air_gap_in_tip that you're reading back out 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
) | ||
dispense_location = Location(dispense_point, labware=dest_loc.labware) | ||
if len(tip_contents) > 0: | ||
last_liquid_and_airgap_in_tip = tip_contents[-1] |
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 think I'm still a little confused about the division of labor between the TransferComponentsExecutor
and the functions that use it. Like, the reason you have to keep passing last_liquid_and_airgap_in_tip
around is because you create and destroy the TransferComponentsExecutor
for each of the substeps?
|
||
@dataclass | ||
class LiquidAndAirGapPair: | ||
"""Pairing of a liquid and air gap in a tip, in that order.""" |
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.
What order?? :)
Can you spell out whether the air is above or below the liquid physically?
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.
Air is below the liquid. Will update the comment
volume=air_gap_volume, | ||
) | ||
if self._transfer_type == TransferType.ONE_TO_ONE: | ||
self._remove_air_gap(location=submerge_start_location) |
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 wasn't sure about this from reading your comment above: When you're puffing out the air here, is the tip inside the liquid or not?
(But also it might be nice to mention this _remove_air_gap()
substep in the comment above.)
rate=1, | ||
flow_rate=flow_rate, | ||
in_place=True, | ||
is_meniscus=None, | ||
push_out=0, | ||
) | ||
self._tip_state.remove_air_gap(last_air_gap) |
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, do me a favor: It's getting a bit hard to keep track of which of the similarly-named functions do something physical vs. merely altering variables. Could you rename the functions in TipState
to make them more distinguishable, and make it obvious that they're not doing anything physical? E.g., maybe TransferComponentsExecutor._remove_air_gap()
vs TipState.delete_air_gap()
.
self.aspirate_and_wait(volume=mix_properties.volume) | ||
# TODO: Update to doing a push out at the end of mix for a post-dispense mix | ||
self.dispense_and_wait(volume=mix_properties.volume, push_out=0) | ||
if n == 1: |
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.
Adding onto this comment: Can you just shove the logic into the dispense_and_wait()
expression? I.e.,
self.dispense_and_wait(
...,
push_out_override=push_out_vol if last_dispense_push_out is True and n == 1 else 0
)
- If dispense location is above the meniscus, DO NOT remove air gap | ||
(it will be dispensed along with rest of the liquid later). | ||
All other scenarios, remove the air gap by doing a dispense | ||
- Flow rate = min(dispenseFlowRate, (airGapByVolume)/sec) |
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 don't understand this expression. What is sec
here?
- Flow rate = min(dispenseFlowRate, (airGapByVolume)/sec) | ||
- Use the post-dispense delay | ||
4. Move to the dispense position at the specified ‘submerge’ speed | ||
(even if we might not be moving into the liquid) |
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 you explain how this differs from Step 1 where you move to the submerge
position?
7. Delay | ||
8. Mix using the same flow rate and delays as specified for asp+disp, | ||
with the volume and the number of repetitions specified. Use the delays in asp & disp. | ||
- If the dispense position is outside the liquid, then raise error if mix is enabled. |
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.
Heh, I think I'll need you to explain how all these positions relate to each other.
But (1) Where you do enforce raising an error if the dispense position is outside the liquid?
(2) I'm curious how you handle a situation like this:
Before dispense:
| |
| v | tip dispense position
| |
|~~~~~| liquid level
| |
+-----+
After dispense:
|~~~~~| liquid level
| v | tip is now below liquid
| |
| |
| |
+-----+
Would mix be allowed in this case?
Addresses AUTH-866
Overview
Part 2 of the three-part series of implementing transfer function.
This PR adds
InstrumentCore.dispense_liquid_class()
which utilizes theTransferComponentsExecutor
to execute the dispense steps in specific order.dispense_liquid_class()
will then be utilized by theInstrumentCore.transfer_liquid()
method to perform dispense during each transfer step. This method can also be accessed in the protocol by using private API accessors for testing purposes.Changes to
TransferComponentsExecutor
:retract_after_dispensing()
to execute post-dispense retraction stepsTest Plan and Hands on Testing
Review requests
TransferComponentsExecutor
. Those will be addressed in the final PR. But let me know if there are any critical changes that should be made here.Risk assessment
Low. Makes no changes to the existing code.