-
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
chore: Separate lt and ht pickup and dropoff #13962
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## edge #13962 +/- ##
==========================================
+ Coverage 70.66% 70.68% +0.02%
==========================================
Files 2505 1627 -878
Lines 70819 54225 -16594
Branches 8743 3849 -4894
==========================================
- Hits 50043 38331 -11712
+ Misses 18640 15232 -3408
+ Partials 2136 662 -1474
Flags with carried forward coverage won't be shown. Click here to find out more.
|
febb4ab
to
1a525f3
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.
Code in general looks good, just had a comment about some updated field descriptions.
I think also we'll want to do some extra testing on mutable configurations since there are a lot of changes there.
presses: int = Field( | ||
default=0.0, description="The number of tries required to force pick up a 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.
Can we keep the previous descriptions for these fields? They make more sense imo.
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 not what that is, though. You're going incrementally deeper every time, it's not just a retry
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.
Added suggestions below.
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 FANTASTIC! THANK YOU!!
What does cam stand for?
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.
Changes make sense to me. Just a couple of questions.
Sounds like this will need thorough testing of all pipettes? (even OT2) Let me know if you or @caila-marashaj need any help with that. The 96 channel partial tip protocols we've tested so far are here
"48": 0.75, | ||
"96": 1.5 | ||
} | ||
"availableConfigurations": [1, 8, 12, 16, 24, 48, 96] |
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.
Interesting, why are the other versions limited to [1, 8, 12, 96] configurations while this one has all available? Is there something in the old versions that limits their use of other partial configurations?
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.
Human error 😆
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.
@Laura-Danielle Human error like "3.5 should also only have [1, 8, 12, 96] available" or human error like "all versions should have 24 and 48"?
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.
Human error as in I forgot about 16, 24, 48 as tip options. To be fair though, we don't really use this config as much as I thought we would.
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 add them
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 not request changes as to ensure I don't block this PR from being merged. However, I do have a few comments that I think we should address before merging.
if len(current_cols) == len(physical_cols) // 2: | ||
return NozzleConfigurationType.QUADRANT | ||
|
||
return NozzleConfigurationType.SUBRECT |
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 we just remove QUADRANT
in favor of SUBRECT
since a quadrant is a a subrectangle anyways? I can't see a need to distinguish between the two.
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 allow actual quadrants but not arbitrary subrects though, right? I feel like it's worth being able to distinguish for that reason
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 I've been referring to quadrant as representing arbitrary sub-rectangles. We're never actually verifying (until here I guess) that it is truly a quadrant, but I don't think we really need to do that. I'm currently working on boundary box checks again -- finally -- and I can't see any need to distinguish between the two.
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!
@@ -359,6 +359,16 @@ class Config: | |||
} | |||
|
|||
|
|||
class PipetteRowDefinition(BaseModel): |
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 we just call this PipetteRowColumnDefinition
or something to that effect and get rid of the separate definitions for the two? They have the exact same fields.
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 have the same fields but those fields do not have the same types in the json, and the only reason they have the same type here is because our version of pydantic isn't capable of doing fine enough divisions. I actually wrote a whole validator for this before realizing I did it based on future pydantic docs that we can't use, and as soon as we update I'll be adding them again. Specifically, the row names and column names I'd like to be separate and verified independently.
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 mean they have the same basic type with some pattern validation in JSON. If you want to split them off here already, then I'd add the validation checks in these pydantic models.
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 do not have the same pattern validation. The column names look for /[0-9]+/
and the row names look for /[A-Z]+/
. I'll add the validation
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 we should bias to the side of keeping more complex constraints in the jsonschema, though. It's just so much better at it.
difference = self.map_store[self.back_left] - self.map_store[self.front_right] | ||
y_rows_length = int(difference[1] // INTERNOZZLE_SPACING) | ||
return map_store_list[starting_idx + y_rows_length] | ||
front_left = next(iter(self.columns.values()))[-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.
This technically is only the 'front left' of the 8 channel. It would give an incorrect value if the 96 channel was in any configuration that did not include the leftmost nozzle. Should maybe just determine this in the build
function and save.
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.
So actually, and I think that maybe I should clarify this more, in an individual NozzleMap
instance the map_store
, rows
, and columns
actually only refer to the tips that are valid in the map. The first element in columns will always be the left-most column of this specific partial tip configuration, therefore the last element of that first column will always be the frontest, leftest channel. This is verified in the test_*_pipette_map_entries
tests in test_nozzle_manager
.
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 the frontest/leftest channel of that configuration, not of the whole pipette -- whereas previously it would only ever refer to the front left-most? (H1) of the pipette.
I can't think of an exact use-case where it could be a problem to change that behavior, but something to look out for/be aware of in case there is weird behavior somewhere. I would also change the comment inside the property to be a little more explicit about what's happening.
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.
Sure. I think the idea is that if you want the front-left most of the pipette you would get it from the physical nozzlemap instance that the manager keeps around.
wrapping=[PythonException(e)], | ||
) from e | ||
|
||
def _rows_in_map() -> Iterator[Tuple[str, List[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.
imo I think this would be cleaner if we just took the first/last index from front/back row/column and then used list splicing.
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.
less lines of code too :D
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.
Yes, but that would require (if I'm understanding right) parsing the row and column from the name of the well, which is something I tried to avoid as much as I could in favor of keeping the well names opaque atomic artifacts, which is an approach that feels more robust. For instance, if we wanted to, there would be nothing preventing us from naming a tip location just 1
or for that matter steve
in this implementation - they're fully arbitrary.
The iteration process also keeps a robustness element of handling arbitrarily and possibly irregular pipettes - because we're iterating linearly, we don't actually care how many things we emit, and that's another thing that isn't really necessary but that I like a lot 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.
No you shouldn't need to.
You could just return the back_left_row/back_left_column as a tuple of (name, contents) rather than just name. Then you could do:
list(ordered_dict.items()).index((name, contents))
As long as that matches the internals then you will get the correct index. I think this should also take care of irregular pipette patterns, unless I don't understand what types of irregularities you have in mind.
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 ok I get it. Yeah I can do that. It won't support irregular format pipettes, like if rows had different number of columns, but we won't have those anyway probably.
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.
@Laura-Danielle how does 39fe151 look
shared-data/python/opentrons_shared_data/pipette/pipette_definition.py
Outdated
Show resolved
Hide resolved
shared-data/python/opentrons_shared_data/pipette/pipette_definition.py
Outdated
Show resolved
Hide resolved
shared-data/python/opentrons_shared_data/pipette/pipette_definition.py
Outdated
Show resolved
Hide resolved
shared-data/python/opentrons_shared_data/pipette/pipette_definition.py
Outdated
Show resolved
Hide resolved
presses: int = Field( | ||
default=0.0, description="The number of tries required to force pick up a 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.
Added suggestions below.
"distance": 13.0, | ||
"currentByTipCount": { | ||
"1": 0.2, | ||
"2": 0.14, |
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.
Is this one meant to be lower than everything else
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, these are sort of known-to-be-messed-up first pass numbers
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.
nice, lgtm!
These configs had a couple things to change: - pickup and drop actually represent configuration for two different actions: the press fit / plunger eject sequence that the low throughput pipettes do, and the cam action sequence that the high throughput pipettes do when handling a full rack. information was actually overloaded and lost: there was only room for one speed for both separate actions. By separating the configs for each action, we get that information back - fold in the per tip current information into the pickup config so it looks a bit more natural and also can be distributed between the action kinds
Now that we have separate configuration elements for picking up tips with press fit vs cam action and dropping them with plunger eject and cam action, we need to use them in a way that is nicely integrated with the nozzle configuration stuff. That means that the plan methods should check the current nozzle configuration and use the appropriate config and behavior.
When we're calibrating an OT-2 multi channel, we do it with just one tip. This means we have to drop the current to have the tip attached with the appropriate amount of force. We now have a bunch of other code focused on doing exactly that tied up with the partial tip pickup mechanics, so use it instead.
These are good for 1, 8, and 96 but probably not anything else.
We had implemented partial tip configurations by doing math on the offsets of all the nozzles. This works, but has two downsides: 1. It's sort of hard to read unless you have a good mental model of the physical layouts 2. It relies on doing exact numerical comparisons In general, I think it is never a good idea to try and go _from_ numbers _to_ symbolic knowledge. It's too vulnerable to small perturbations, and it's too annoying to read. Instead, let's encode the structure of the pipette nozzles in the pipette geometry defs by writing down the rows and columns of the pipette layout. Then, we can do our partial configuration logic based on the rows and columns of the pipette. We now do not hit a bug when we request a partial configuration that doesn't include A1; and we now do not rely on array stride math; and we now technically support arbitrary axis-aligned rectangles (at least on this layer - definitely verboten above).
…ition.py Co-authored-by: Laura Cox <[email protected]>
…ition.py Co-authored-by: Laura Cox <[email protected]>
…ition.py Co-authored-by: Laura Cox <[email protected]>
…ition.py Co-authored-by: Laura Cox <[email protected]>
ee0636d
to
a0293f0
Compare
This PR looks gigantic but most of it is little fiddly fixups to pipette config files that are schema checked and don't change data.
Our 96 channel pipettes pick up full racks of tips using a cam grabber, but they're also capable of picking up less tips than that through the normal press-fit routine that the low throughput pipettes use. Those routines need different configuration values that do not overlap, and we previously had them overlapping.
This commit
Review requests
Testing