-
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): fix InstrumentContext.name for Flex and update LiquidClass.get_for() #17289
Conversation
Don't we already consistently call it |
"""Get liquid class transfer properties for the specified pipette and tip.""" | ||
from . import InstrumentContext, Labware |
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 can't we just import this at the top of the file (and get rid of the if TYPE_CHECKING
bit)?
You are clearly using the symbols InstrumentContext
and Labware
in your implementation (versus only importing it for the typechecker), so why not treat it as a normal import?
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 to this, I can see limited use of getting liquid class properties without having to provide the names, but I'd almost rather that be it's own function which would allow us to remove this internal import. That or we could always flip around the isinstance
to check for str instead of InstrumentContext
or Labware
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 can't we just import this at the top of the file (and get rid of the if TYPE_CHECKING bit)?
Because it results in a circular import for InstrumentContext
. InstrumentContext
already imports from _liquid
.
I can see limited use of getting liquid class properties without having to provide the names, but I'd almost rather that be it's own function
Not sure I understand. Can you elaborate?
That or we could always flip around the isinstance to check for str instead of InstrumentContext or Labware
If we want to verify that the args are not of some third, unsupported type then we will have to check if isinstance
of InstrumentContext
and Labware
. Sure we can maybe check that if it's not a string and if there's an error doing pipette.load_name
or tiprack.uri
, the args are wrong. But it's not as foolproof as doing isinstance
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.
TY! Looks good except for a possible GEN1/GEN2 cross-compatibility thing.
@@ -64,18 +67,42 @@ def name(self) -> str: | |||
def display_name(self) -> str: | |||
return self._display_name | |||
|
|||
def get_for(self, pipette: str, tiprack: str) -> TransferProperties: | |||
def get_for( | |||
self, pipette: Union[str, InstrumentContext], tiprack: Union[str, Labware] |
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.
Nit: "tip rack" is two words, so can we name the argument tip_rack
instead of tiprack
?
def get_load_name(self) -> str: | ||
"""Get the pipette's requested API load name. | ||
|
||
For OT-2 pipettes, this is the same as pipette name. | ||
""" | ||
return self.get_hardware_state()["name"] | ||
|
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.
For OT-2 pipettes, this is the same as pipette name.
Not always, I don't think. What about GEN1/GEN2 cross-compatibility? If a protocol requests p300_single
but gets a p300_single_gen2
, I'd expect load_name
to return p300_single
. I think this get_hardware_state()
-based implementation will return p300_single_gen2
.
I don't have a robot in front of me to test this, so feel free to dismiss this change request if I'm wrong about this.
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 is what I was missing! The description of InstrumentCore.get_pipette_name()
(which is what InstrumentContext.name
returns) says
Will match the load name of the actually loaded pipette, which may differ from the requested load name.
Now I know what it means.
I actually want to return the name of the loaded pipette p300_single_gen2
and not the requested pipette p300_single
.
I feel like fixing InstrumentContext.name
might be the more appropriate thing here. We can gate the change behind API version. Otherwise InstrumentContext.name
would just be something that returns the same thing as InstrumentContext.load_name
for OT-2 pipettes and something unusable for the API users when using a Flex.
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.
👍 Makes sense.
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, now I'm confused, but I wasn't following this too closely.
When public users use liquid classes, they'll call get_for(pipette_name, tiprack_name)
, right?
In this example here, is the pipette_name going to be p300_single
or p300_single_gen2
?
(If your answer is p300_single_gen2
, won't that be confusing to the user?)
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.
In this example here, is the pipette_name going to be p300_single or p300_single_gen2?
p300_single_gen2
(If your answer is p300_single_gen2, won't that be confusing to the user?)
It won't be confusing. The pipette name method will return p300_single_gen2
because that's the pipette that's actually attached to the robot and being used. All properties of that instrument context's instance (eg. default flow rates, plunger speeds, model, firmware version, etc) are properties of the attached pipette, and not the requested pipette (if it is different). So following those lines, it would be expected to get the actual name of the pipette that is attached, rather than the one requested.
There might be a reason to return the requested pipette name, but that's not the concern here and if we decide to add that, it should be called something different like requested_pipette_name
.
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 meant confusing as in: As far as the user knows, they requested p300_single
, so they would expect to continue refer to the pipette by that name in all the code they write. Or are you saying that the user is responsible for knowing that p300_single
gets mapped to p300_single_gen2
?
This will come up when the user tries to write their own liquid classes, right? Like if the user has to write:
my_pipette = protocol.load_instrument("p300_single")
my_liquid_class = {
"???": {
"blah_tiprack_300ul": {
"aspirate": {..},
what should the user put for ???
, and how will they know that's what they need to put there?
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.
Heads up for the sake of this discussion that the term "load name" is a little ambiguous.
One axis is: The opentrons.protocol_api
-level strings exposed in this table, versus the internal names defined by opentrons.protocol_engine
.
And another axis is: The pipette that the protocol requested, versus the pipette that the system provided.
"Load name" can be about either or both of those axes, depending on how you define it.
(So in this PR, @sanni-t is correcting the first axis—the public names from the table versus the internal names. In #17289 (comment), @ddcc4 is bringing up a point about the second axis—requested vs. provided.)
if self._protocol_core.api_version < _FLEX_PIPETTE_NAMES_FIXED_IN: | ||
return ( | ||
pipette.pipetteName.value | ||
if isinstance(pipette.pipetteName, PipetteNameType) | ||
else pipette.pipetteName | ||
) |
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 worry a bit about having an apiLevel
-based conditional like this in the "deeper" opentrons.protocol_api.core
layer when it could otherwise be in the opentrons.protocol_api
layer. I don't think we want our internal code to have to cope with a vague concept, "the pipette name," whose precise definition changes depending on the apiLevel
. I think InstrumentCore
could instead expose more precise concepts like "the load name specified in the ProtocolContext.load_instrument()
call," etc. Then when we write our internal code, we explicitly choose the one that's appropriate. And the user-facing opentrons.protocol_api
layer may choose between them in an apiLevel
-dependent way.
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 sounds good too.
def get_load_name(self) -> str: | ||
"""Get the pipette's requested API load name. | ||
|
||
For OT-2 pipettes, this is the same as pipette name. | ||
""" | ||
return self.get_hardware_state()["name"] | ||
|
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.
Heads up for the sake of this discussion that the term "load name" is a little ambiguous.
One axis is: The opentrons.protocol_api
-level strings exposed in this table, versus the internal names defined by opentrons.protocol_engine
.
And another axis is: The pipette that the protocol requested, versus the pipette that the system provided.
"Load name" can be about either or both of those axes, depending on how you define it.
(So in this PR, @sanni-t is correcting the first axis—the public names from the table versus the internal names. In #17289 (comment), @ddcc4 is bringing up a point about the second axis—requested vs. provided.)
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.
Looks good to me once tests and lint pass. I think the failure is just because of unrelated breakage in edge
, so maybe just make sure everything is passing locally.
@ddcc4's comment is a good point but I think the question of requested vs. provided is outside the intended scope of this PR, right? And my comment isn't a blocker.
Right. I talked to Sanniti about this yesterday. I'm OK with the implementation here for now. (The original goal was that a protocol written for |
Co-authored-by: Max Marrone <[email protected]>
Co-authored-by: Max Marrone <[email protected]>
Closes AUTH-1295, AUTH-1299
Overview
InstrumentContext.load_name
to fetch the python API load name of a pipetteLiquidClass.get_for()
to accept the a loaded instrument object and loaded tiprack object forpipette
andtiprack
args respectively.get_for()
fromtiprack
totip_rack
to be consistent with API naming conventions.Test Plan and Hands on Testing
Added unit and integration tests
Review requests
LiquidClass.get_for()
method will still accept the pipette name and tiprack URI as arguments. This is to allow fetching liquid class properties without needing to load pipettes and tipracks. Let me know if this is not a good ideaInstrumentContext.load_name
? I want to avoid any confusion with the existingname
property and this new propertyRisk assessment
None