Skip to content

feat(step-generation): py command generation for mix() #17833

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

Merged
merged 8 commits into from
Mar 24, 2025
Merged

Conversation

jerader
Copy link
Collaborator

@jerader jerader commented Mar 20, 2025

closes AUTH-1097

Overview

this PR introduces the py command generation for mix()! It is emitted only if there are acceptable args. Meaning no aspirate delay, no dispense delay.

TODO: figure out the flow rates! user can specify both aspirate and dispense flow rates but mix can only use one. how do we know which one to use?

Test Plan and Hands on Testing

Review the code and smoke test

here is an example i made that passes analysis for the different variations of commands

from contextlib import nullcontext as pd_step
from opentrons import protocol_api, types

metadata = {
    "protocolName": "test mixing",
    "created": "2025-03-20T17:54:17.554Z",
    "lastModified": "2025-03-20T17:55:04.093Z",
    "protocolDesigner": "8.4.3",
    "source": "Protocol Designer",
}

requirements = {
    "robotType": "Flex",
    "apiLevel": "2.23",
}

def run(protocol: protocol_api.ProtocolContext):
    # Load Labware:
    tip_rack_1 = protocol.load_labware(
        "opentrons_flex_96_tiprack_50ul",
        "C2",
        namespace="opentrons",
        version=1,
    )
    reservoir_1 = protocol.load_labware(
        "axygen_1_reservoir_90ml",
        "D1",
        namespace="opentrons",
        version=2,
    )

    # Load Pipettes:
    pipette_left = protocol.load_instrument("flex_1channel_50", "left", tip_racks=[tip_rack_1])

    # Load Trash Bins:
    trash_bin_1 = protocol.load_trash_bin("A3")

    # PROTOCOL STEPS

    # Step 1: NO MIX SINCE DELAY IS SET
    pipette_left.pick_up_tip(location=tip_rack_1)
    pipette_left.configure_for_volume(10)
    pipette_left.aspirate(
        volume=10,
        location=reservoir_1["A1"].bottom(z=1),
        rate=35 / pipette_left.flow_rate.aspirate,
    )
    protocol.delay(seconds=10)
    pipette_left.dispense(
        volume=10,
        location=reservoir_1["A1"].bottom(z=1),
        rate=57 / pipette_left.flow_rate.dispense,
    )
    protocol.delay(seconds=10)
    pipette_left.aspirate(
        volume=10,
        location=reservoir_1["A1"].bottom(z=1),
        rate=35 / pipette_left.flow_rate.aspirate,
    )
    protocol.delay(seconds=10)
    pipette_left.dispense(
        volume=10,
        location=reservoir_1["A1"].bottom(z=1),
        rate=57 / pipette_left.flow_rate.dispense,
    )
    protocol.delay(seconds=10)
    pipette_left.drop_tip()

    # Step 2: MIX SINCE DELAY IS NOT SET
    pipette_left.pick_up_tip(location=tip_rack_1)
    pipette_left.configure_for_volume(10)
    pipette_left.flow_rate.aspirate = 35
    pipette_left.flow_rate.dispense = 57
    pipette_left.mix(
        repetitions=2,
        volume=10,
        location=reservoir_1["A1"].bottom(z=1),
    )
    pipette_left.flow_rate.blow_out = 57
    pipette_left.blow_out(trash_bin_1)
    pipette_left.touch_tip(
        reservoir_1["A1"],
        v_offset=-1,
    )
    pipette_left.drop_tip()

Changelog

  • py command for mix() in the mixUtil
  • add test cases for the different py command variations
  • extend util to include invariant context and plug into all usages of the util

Risk assessment

low, behind a ff

Copy link

codecov bot commented Mar 20, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 62.52%. Comparing base (c7bcbad) to head (0015389).
Report is 27 commits behind head on edge.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             edge   #17833       +/-   ##
===========================================
+ Coverage   23.41%   62.52%   +39.11%     
===========================================
  Files        2891     2921       +30     
  Lines      222561   226685     +4124     
  Branches    19013    19793      +780     
===========================================
+ Hits        52115   141739    +89624     
+ Misses     170435    84761    -85674     
- Partials       11      185      +174     
Flag Coverage Δ
protocol-designer 18.85% <90.80%> (-0.01%) ⬇️
step-generation 4.50% <100.00%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
shared-data/command/types/index.ts 100.00% <100.00%> (ø)
...ration/src/commandCreators/compound/consolidate.ts 99.10% <100.00%> (+3.46%) ⬆️
...eration/src/commandCreators/compound/distribute.ts 97.96% <100.00%> (+3.36%) ⬆️
...tep-generation/src/commandCreators/compound/mix.ts 88.68% <100.00%> (+1.82%) ⬆️
...eneration/src/commandCreators/compound/transfer.ts 98.14% <100.00%> (+1.97%) ⬆️

... and 1737 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

repetitions=2,
volume=5,
location=mockPythonName["A1"].bottom(z=3.2),
rate=(2.1 / mockPythonName.flow_rate.aspirate) + (2.2 / mockPythonName.flow_rate.dispense),
Copy link
Collaborator Author

@jerader jerader Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to figure out how to show both flow rates or if we have to remove one?

Copy link
Collaborator Author

@jerader jerader Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay discussed with jeremy in standup and actually we can just define it above the mix()

like this

mockPythonName.flow_rate.aspirate = 35
mockPythonName.flow_rate.dispense = 57
mockPythonName.mix(
    repetitions=2,
    volume=5,
    location=mockPythonName["C1"].bottom(z=3.2),
)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, that's a very creative approach!

I think it would work. One potential negative is that the pipette.flow_rates are going to jump around a lot during the course of a protocol, when I think the original intent was that they should be used to set a general default and not change so often. But I think we can live with this, since this is the only way to configure a different aspirate and dispense flow rate for the mix().

@jerader jerader marked this pull request as ready for review March 20, 2025 18:26
@jerader jerader requested a review from a team as a code owner March 20, 2025 18:26
@jerader jerader requested a review from ddcc4 March 20, 2025 18:26
mockPythonName.dispense(...)
mockPythonName.flow_rate.aspirate = 3.78
mockPythonName.flow_rate.dispense = 3.78
mockPythonName.mix(...)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, we already had a test for a Mix step. That's nice :)

// If delay is specified, emit individual py commands
// otherwise, emit mix()
const hasUnsupportedMixApiArg =
aspirateDelaySeconds != null || dispenseDelaySeconds != null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a null check? Like, can we still call the PAPI mix() function if aspirateDelaySeconds is set to 0?

Copy link
Collaborator Author

@jerader jerader Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i have not considered someone setting the delay seconds to 0. I think you're right. The seconds is null if the user never touched the field so i guess we need to check for both.

Copy link
Collaborator

@ddcc4 ddcc4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this looks good.

My only outstanding question is whether aspirateDelaySeconds != null || dispenseDelaySeconds != null is the right thing to check for.

@jerader jerader merged commit ff983ee into edge Mar 24, 2025
51 checks passed
@jerader jerader deleted the sg_mix-py branch March 24, 2025 20:43
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.

2 participants