-
Notifications
You must be signed in to change notification settings - Fork 178
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): Support V2 pipette configs in the OT-2 and Protocol Engine #13104
feat(api): Support V2 pipette configs in the OT-2 and Protocol Engine #13104
Conversation
Codecov Report
@@ Coverage Diff @@
## edge #13104 +/- ##
===========================================
- Coverage 72.08% 60.24% -11.84%
===========================================
Files 1530 795 -735
Lines 50741 16173 -34568
Branches 3144 2892 -252
===========================================
- Hits 36576 9744 -26832
+ Misses 13657 5963 -7694
+ Partials 508 466 -42
Flags with carried forward coverage won't be shown. Click here to find out more.
|
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 this is gonna be rough but important to do, but I think there's some things we can do to make it a bit less of a bitter pill to swallow.
Couple inline questions and some larger stuff:
subtle but probably important distinctions
I don't quite get the difference between pipette.active_tip_settings.default_return_height
and
small changes, lots and lots of small consequences
There's a lot of changes that are sometimes almost the only changes in the file that are the consequence of a little type change that we can avoid by changing the new types or changing the upstreams. For instance, if you define opentrons_shared_data.pipette.pipette_definitions.PipetteChannelType
as PipetteChannelType(int, Enum)
then you get int comparisons for free:
Python 3.7.17 (default, Jun 27 2023, 10:52:02)
[Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from enum import Enum
>>> class PipetteChannelType(int, Enum):
... SINGLE_CHANNEL = 1
... EIGHT_CHANNEL = 8
... NINETY_SIX_CHANNEL = 96
...
>>> PipetteChannelType.SINGLE_CHANNEL
<PipetteChannelType.SINGLE_CHANNEL: 1>
>>> PipetteChannelType.SINGLE_CHANNEL ==1
True
>>> PipetteChannelType.SINGLE_CHANNEL == 8
False
>>> PipetteChannelType.EIGHT_CHANNEL == 8
True
>>> PipetteChannelType.EIGHT_CHANNEL == 1
False
Combine that, maybe a custom comparison operator (if it's even needed, may get that for free too) to sort them, and some handling for the PipetteInfo
model in the old calibration stuff to build its channels
element from the enum, and a lot of the changes in robot_server.robot.calibration
fall away.
This seems like not so big a win, but one of the reasons prs like this are tough to deal with is that git(hub) has no way of emphasizing or separating parts of the diff, and so stuff that's complete boilerplate like adding as_int
everywhere is presented on the same level as stuff that's more important. Let's cut down on some of those boilerplate changes.
raise KeyError( | ||
"If you specify attached_instruments, the model " | ||
"should be pipette names or pipette models, but " | ||
f'{passed_ai["model"]} is not' | ||
) | ||
|
||
self._attached_instruments = { | ||
m: _sanitize_attached_instrument(attached_instruments.get(m)) | ||
m: _sanitize_attached_instrument(attached_instruments.get(m)) # type: ignore |
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's this suppressing?
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.
Just moved the #type: ignore from the internal function to have that be slightly better typed. It's suppressing a TypeDict
type.
|
||
|
||
# TODO (lc 07-05-2023) Move this back to the combined Pipette object file. | ||
def piecewise_volume_conversion( |
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 really should not be here. we should avoid touching it if we possibly can and if we can't we should just move it once
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.
okay. I'll leave them both in their respective pipette.py
files.
self._config.supported_tips[list(self._config.supported_tips.keys())[0]], | ||
) | ||
self._fallback_tip_length = self._active_tip_settings.default_tip_length | ||
self._aspirate_flow_rates_lookup = ( |
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'm suspicious of these parentheses I think there might be a syntax problem 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.
I think this is what the formatter did but I'll double check.
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 okay, I just see that a lot when there's a missing comma or somethign so the formatter groups statements wrong
@@ -302,6 +428,10 @@ def working_volume(self) -> float: | |||
def working_volume(self, tip_volume: float) -> None: | |||
"""The working volume is the current tip max volume""" | |||
self._working_volume = min(self.config.max_volume, tip_volume) | |||
self._active_tip_settings = self._config.supported_tips[ | |||
PipetteTipType(int(self._working_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.
do we definitely have configurations for all the pipettes for all the working volumes they use, including the ot2 filter tips?
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 double check
return_tip_scale=config.return_tip_height, | ||
nominal_tip_overlap=config.tip_overlap, | ||
return_tip_scale=tip_configuration.default_return_tip_height, | ||
nominal_tip_overlap=config.tip_overlap_dictionary, |
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.
don't you have to modify get_pipette_static_config
too?
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 because I kept the old names in the pipette dict as keys.
@@ -105,6 +106,69 @@ def load_serial_lookup_table() -> Dict[str, str]: | |||
return _lookup_table | |||
|
|||
|
|||
def load_liquid_model( | |||
max_volume: PipetteModelType, |
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 don't love this match of argname and argtype. The point of this pr is to continue to tease apart the overloaded concepts that were smushed into pipette names and models into structured data; this conflates two of them again. let's call it model
or something and get the max volume from the model def.
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.
Not sure what you're asking for here. Do you just want me to call the max_volume
, model
instead? I guess it ultimately doesn't matter to me. I would also make the change in the load_data
function in this file since it's using the same nomenclature.
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.
Yeah that makes sense to me
return PipetteLiquidPropertiesDefinition.parse_obj(liquid_dict) | ||
|
||
|
||
def _change_to_camel_case(c: str) -> str: |
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 no
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.
😆
|
||
|
||
def _change_to_camel_case(c: str) -> str: | ||
# Tiny helper function to convert to camelCase. |
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 would almost infinitely prefer an explicit map of old to new keys
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.
sigh Okayyyy
@@ -88,20 +93,65 @@ def as_tuple( | |||
return (self.major, self.minor) | |||
|
|||
|
|||
# TODO (lc 12-5-2022) Ideally we can deprecate 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.
honestly i think this sort of class is entirely reasonable
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 suppose it could be, but ideally we move away from this being an external concept entirely. You truly should be able to request a certain pipette size/channels without having to worry about generation of the pipette.
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 suppose it could be, but ideally we move away from this being an external concept entirely. You truly should be able to request a certain pipette size/channels without having to worry about generation of the pipette.
Hmm. Even in a world where protocol authors request pipettes like that, it seems like this concept would still exist in HTTP. Something would still need to tell the app what pipettes the user needs to attach in order to run a protocol, and the app would still need to determine whether the pipettes that are already attached are a match.
Or is your vision that those would get reported as a fine-grained list of models? Like: "You can attach a p1000_single_1_0
, or p1000_single_1_3
, or p1000_single_1_4
, or ..." etc.
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 my ideal world, the app shouldn't have to determine if pipettes match at all or not. The robot server/protocol engine should just say 'hey this doesn't match what's currently on the robot'.
For simulation, we should have something that sends back a list of compatible generations (like we kinda talked about in the partial tip meeting). We haven't -- and probably won't in the future -- cared about the functionality between models.
@@ -555,14 +559,16 @@ async def _do_plunger_home( | |||
await stack.enter_async_context(self._motion_lock) | |||
with self._backend.save_current(): | |||
self._backend.set_active_current( | |||
{checked_axis: instr.config.plunger_current} | |||
{checked_axis: instr.plunger_motor_current.run} |
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.
honestly wondering if, just for the sake of this pr, we mapped at least some of the new values to the old names. most of the time we wouldn't use them, but it would prevent us from needing a million changes that have no semantic meaning but just need different letters in sequence
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 the config itself? Otherwise if we mapped it in the Pipette
class there would still be a million changes. Plus mapping in a Pydantic model will get confusing since it's supposed to server as a schema.
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.
Okay, this structure looks good to me. We obviously need to fix up tests and such, and run some protocols on a machine with this, but in particular we should pay close attention to anything going on with hardware testing - they poke into internals a lot and I'm a bit worried.
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.
Makin' my way downtown. I still have a way left to go, but here are some questions so far, and a couple requests. 🕺
"defaultTipOverlapDictionary": { | ||
"type": "object", | ||
"description": "Map of tip overlap values with defaults", | ||
"required": ["default"], | ||
"$comment": "Other keys in here should be labware URIs", | ||
"properties": { | ||
"default": { "type": "number" } | ||
}, | ||
"additionalProperties": { "type": "number" } | ||
}, |
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.
Hmm, what makes tip overlap different from flow rate, tip length, and tip height? Tip overlap is specified here as a dict that's keyed on labware URIs, but those other things are specified as a dict that's keyed on a "tip type" like "t300."
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 was was previously used. I made it match the V1 schema for sake of not touching a bunch more code. Protocol Engine and other users of this data still pass in LabwareUri to get this data.
The tip length imo should also eventually be removed from the schema all together because it doesn't make sense to be there. It is not a property of a pipette but rather a property of a tip rack.
You can think of the top level t...
keys as being notation for representing all tips under the 300ul volume which may infact have different properties. That is why right now we're keying the aspirate/dispense by labware uri as well. Unfortunately don't have a fully fleshed out future yet for Liquid classes but you can imagine this type of stuff expanding when we get there.
return f"{config_name[0]}" + "".join(s.capitalize() for s in config_name[1::]) | ||
|
||
|
||
def update_pipette_configuration( |
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.
You might just not have gotten to this yet, but this function seems like a high-value target for unit test coverage, since it has a type-unsafe Dict[str, Any]
parameter and it's doing legacy name mapping and 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.
yep, thanks for the reminder though :D
shared-data/python/opentrons_shared_data/pipette/model_constants.py
Outdated
Show resolved
Hide resolved
shared-data/python/opentrons_shared_data/pipette/model_constants.py
Outdated
Show resolved
Hide resolved
shared-data/python/opentrons_shared_data/pipette/pipette_definition.py
Outdated
Show resolved
Hide resolved
|
||
|
||
@dataclass | ||
class PipetteModelVersionType: |
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.
Question:
What does the Type
suffix signify? What kind of Python type isn't a Type
? I've seen this in other places in our codebase, too.
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 instance, I did it to try and distinguish as much as possible from the PipetteModel
/PipetteName
dev_types we already have in our code base.
Normally in the past, it's been used when we have type defines at the top of the file. IMO I think it just sounds nicer. For example in my most recent PR, I had a type called something to the effect of OverrideType
.
@@ -88,20 +93,65 @@ def as_tuple( | |||
return (self.major, self.minor) | |||
|
|||
|
|||
# TODO (lc 12-5-2022) Ideally we can deprecate 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.
I suppose it could be, but ideally we move away from this being an external concept entirely. You truly should be able to request a certain pipette size/channels without having to worry about generation of the pipette.
Hmm. Even in a world where protocol authors request pipettes like that, it seems like this concept would still exist in HTTP. Something would still need to tell the app what pipettes the user needs to attach in order to run a protocol, and the app would still need to determine whether the pipettes that are already attached are a match.
Or is your vision that those would get reported as a fine-grained list of models? Like: "You can attach a p1000_single_1_0
, or p1000_single_1_3
, or p1000_single_1_4
, or ..." etc.
We were in a state where the hardware controller was using the updated schemas but the Protocol Engine was not. This is the first step to porting over all of the systems to use the new schema.
ca3e159
to
2b19e8e
Compare
Successfully smoke tested, for Flex and OT-2, attaching single and multi-channel pipettes, loading pipettes in protocols (with both GEN-3 and user facing names for Flex), analyzing on both app and robot and running protocols on robot. Additionally confirmed that mutable configs were functioning on OT-2. |
Overview
Well..uh I am sorry in advance. This PR touches a lot of code. The most important things to check out are the changes to
pipette_data_provider.py
andot2/pipette.py
. There are few small changes in theshared-data/python
code that could also use some eyes. Luckily some of our older tests caught some mistakes in the pipette configs which was great (and hence why there are a lot of shared-data data changes).I think I'll still need to do one more follow-up PR in Protocol Engine to support dynamically changing fallback tip length during run-time but I decided not to do that for this PR.
To-Do
There is one more failing test related to supporting "old" and "new aspirate functions on super old pipettes. I haven't yet decided how to handle this, but it would not block folks from beginning to test the PR so I decided to just put this up for review.
Test Plan
This PR needs to be thoroughly tested on both an OT-2 and Flex. It touches a lot of core functionality including:
At a minimum we need to test on both an OT2/Flex:
Specific to OT-2
Changelog
Review requests
Please look closely at changes to
ot2/pipette
andpipette_data_provision.py
. Make sure I did not miss any critical components in Protocol Engine for loading pipette configs.Risk assessment
High. This PR affects a critical part of the stack. Please see the test plan above.