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

feat(api): add execution of dispense steps for liquid class based transfer #17138

Open
wants to merge 4 commits into
base: AUTH-866-add-transfer-flow-builder
Choose a base branch
from

Conversation

sanni-t
Copy link
Member

@sanni-t sanni-t commented Dec 18, 2024

Addresses AUTH-866

Overview

Part 2 of the three-part series of implementing transfer function.

This PR adds InstrumentCore.dispense_liquid_class() which utilizes the TransferComponentsExecutor to execute the dispense steps in specific order. dispense_liquid_class() will then be utilized by the InstrumentCore.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:

  1. adds retract_after_dispensing() to execute post-dispense retraction steps
  2. adds a tip state tracker to the class in order to keep track of whether the pipette is 'ready to aspirate' and also track the liquid and air gap in the tip that either the single aspirate or dispense steps will be handling.

Test Plan and Hands on Testing

  • Integrated and on-robot testing will have to wait until this implementation is wired up to the public API
  • Added unit tests for all new functions and classes

Review requests

  • Some suggestions from the aspirate PR will still hold to this PR- eg, naming of TransferComponentsExecutor. Those will be addressed in the final PR. But let me know if there are any critical changes that should be made here.
  • Usual code review stuff

Risk assessment

Low. Makes no changes to the existing code.

Comment on lines 87 to 99
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
Copy link
Member Author

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
Copy link
Member Author

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

@sanni-t sanni-t changed the base branch from AUTH-866-add-transfer-flow-builder to edge December 19, 2024 18:44
@sanni-t sanni-t force-pushed the AUTH-866-add-transfer-flow-builder-2 branch from c3cc384 to 9b2de59 Compare December 20, 2024 06:21
@sanni-t sanni-t changed the base branch from edge to AUTH-866-add-transfer-flow-builder December 20, 2024 06:21
@sanni-t sanni-t marked this pull request as ready for review January 2, 2025 15:53
@sanni-t sanni-t requested a review from a team as a code owner January 2, 2025 15:53
@sanni-t sanni-t requested review from jbleon95 and ddcc4 January 2, 2025 15:53
Copy link
Contributor

@jbleon95 jbleon95 left a 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(
Copy link
Contributor

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."
Copy link
Contributor

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)

Copy link
Member Author

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:
Copy link
Contributor

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

Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Member Author

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 (
Copy link
Contributor

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

Copy link
Member Author

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
Copy link
Contributor

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!

Copy link
Member Author

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(),
Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Contributor

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?

Copy link
Member Author

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]
Copy link
Contributor

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."""
Copy link
Contributor

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?

Copy link
Member Author

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)
Copy link
Contributor

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)
Copy link
Contributor

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:
Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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.
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants