-
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): add new InstrumentContext.transfer_liquid() method #16819
base: edge
Are you sure you want to change the base?
Conversation
…_liquid in all cores
…ers in cores, add tests
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## edge #16819 +/- ##
==========================================
+ Coverage 73.90% 79.17% +5.26%
==========================================
Files 43 120 +77
Lines 3231 4514 +1283
==========================================
+ Hits 2388 3574 +1186
- Misses 843 940 +97
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.
Some minor requests but otherwise looks good for the initial transfer_liquid code
@@ -37,6 +41,10 @@ def as_dict(self) -> Dict[float, float]: | |||
"""Get a dictionary representation of all set volumes and values along with the default.""" | |||
return self._properties_by_volume | |||
|
|||
def as_list_of_tuples(self) -> List[Tuple[float, float]]: | |||
"""Get as list of tuples.""" | |||
return [(k, v) for k, v in self._properties_by_volume.items()] |
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 could be simplified to list(self._properties_by_volume.items())
@@ -89,6 +89,12 @@ def execute_command_without_recovery( | |||
) -> commands.TryLiquidProbeResult: | |||
pass | |||
|
|||
@overload | |||
def execute_command_without_recovery( |
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 meant to be temporary until further transfer builder work? If so could there be a TODO 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.
No, it's not temporary. This just defining the overload signature that uses LoadLiquidClassParams
and returns LoadLiquidClassResult
. The actual execution is passed on to an existing function in this file.
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.
The function right below actually
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.
Hey, I got partway through this PR, and I'll continue looking at this later.
But this is my first time looking into a lot of these files, and I need to familiarize myself with them and the conventions we use.
@@ -37,6 +41,10 @@ def as_dict(self) -> Dict[float, float]: | |||
"""Get a dictionary representation of all set volumes and values along with the default.""" | |||
return self._properties_by_volume | |||
|
|||
def as_list_of_tuples(self) -> List[Tuple[float, float]]: | |||
"""Get as list of tuples.""" | |||
return [(k, v) for k, v in self._properties_by_volume.items()] |
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 list(self._properties_by_volume.items())
will do what you want?
@@ -101,6 +109,14 @@ def duration(self, new_duration: float) -> None: | |||
validated_duration = validation.ensure_positive_float(new_duration) | |||
self._duration = validated_duration | |||
|
|||
def as_schema_v1_model(self) -> SharedDataDelayProperties: |
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 on the versioning: Do you expect the v1
to stick around forever? Like, if we later create a v2 of the schema, are you going to have an as_schema_v2_model()
function too? And how would that even work -- like, wouldn't you need
def as_schema_v1_model(self) -> SharedDataDelayPropertiesV1 ...
def as_schema_v2_model(self) -> SharedDataDelayPropertiesV2 ...
What I'm getting at is: if this code is NOT expected to handle v2, v3, etc., the same way, then having v1
in all the function names feels like it's just adding clutter that won't really help make the code extensible for future versions. But is that just a convention we follow?
liquid_class: LiquidClass, | ||
pipette_load_name: str, | ||
tiprack_uri: 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.
Hey, could you explain what Instrument
is supposed to represent conceptually? Like, I think it makes sense that an instrument can aspirate
or transferLiquid
, but is loading liquid classes also something that belongs to the instrument?
|
||
liquid_class_record = LiquidClassRecord( | ||
liquidClassName=liquid_class.name, | ||
pipetteModel=self.get_model(), # TODO: verify this is the correct 'model' to use |
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.
The implementation just treats pipetteModel
as an opaque string, so it can be whatever you want :)
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.
Ya, but these need to be consistent. We have two different ways to refer to pipettes- the API load name and the name used by rest of the system. The definition contains properties keyed by API load name and so when looking up values from the definition we have to use the API load name. But most of the get_name
or get_model
functions use the other name.
@@ -309,6 +310,32 @@ def configure_nozzle_layout( | |||
""" | |||
... | |||
|
|||
@abstractmethod | |||
def load_liquid_class( |
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.
Hm, and what's the difference between protocol_api/core/engine/instrument.py
and protocol_api/core/instrument.py
?
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.
protocol_api/core/instrument.py
has the outside-facing interfaces that the public context refers to. So just a bunch of abstractmethos declarations. The actual implementation of these methods is carried out by the three 'cores'- engine core/ legacy core/ legacy simulator core. The public context gets the relevant 'core' to use during initialization. All our new features go on the engine core.
liquid_class_id: str, | ||
volume: float, | ||
source: List[WellCore], | ||
dest: List[WellCore], |
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.
Did you say yesterday that source
and dest
are expected to be exactly the same length?
If so, maybe it's less error-prone if we made the user give us a single list of (source, dest)
well pairs, rather than 2 lists?
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 will just be more work for protocol authors to create a paired list. Most of the time users use things like source=labware1.columns()[0]
and dest=labware2.columns()[0]
.
source | ||
) | ||
flat_dest_list = validation.ensure_valid_flat_wells_list_for_transfer_v2(dest) | ||
for well in flat_sources_list + flat_dest_list: |
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 there any restriction on how wacky the user can get with sources_list
and dest_list
? Like:
- Can the same well appear multiple times in either list?
- Can the same well appear in both source and dest?
- Can you transfer from a well to the same well? (
source=[A1, A2], dest=[A1, A2]
)
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 to all above. And they will be valid transfers as far as the API is concerned. Whether it makes sense scientifically, we can't predict.
flat_sources_list = validation.ensure_valid_flat_wells_list_for_transfer_v2( | ||
source | ||
) | ||
flat_dest_list = validation.ensure_valid_flat_wells_list_for_transfer_v2(dest) |
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.
Small nitpick: source
and dest
should both either be singular or plural, for symmetry.
Closes AUTH-843
Overview
Adds
InstrumentContext.transfer_liquid()
method that does the following-transfer_liquid()
This PR does not cover engine core's transfer method execution.
Test Plan and Hands on Testing
Since this is mostly adding the scaffolding to implement the actual execution of transfer method, this PR is not testable on the robot yet. Unit tests are crucial at this stage and have been added for all changes.
Changelog
InstrumentContext.transfer_liquid()
InstrumentCore.load_liquid_class()
and a placeholderInstrumentCore.transfer_liquid()
protocol_api.validation
that get used inInstrumentContext.transfer_liquid()
as_schema_v1_model()
to the liquid properties in order to fetch the pydantic model representation of the liquid class's properties. This is used byload_liquid_class()
in order to create aliquidClassRecord
Review requests
Do the validations make sense? Am I missing anything?
Does the
transfer_liquid()
interface look good?Risk assessment
No risk so far since this is a code-only change.