-
Notifications
You must be signed in to change notification settings - Fork 186
fix(api): optimize liquid class transfers by only changing volume mode when necessary #20009
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
Changes from all commits
b1313f2
15b637e
28d223e
cd7314c
7dd081a
19da9f8
abd426a
08c3aea
2b64c81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2153,7 +2153,12 @@ def aspirate_liquid_class( | |
| air_gap=0, | ||
| ) | ||
| ) | ||
| if volume_for_pipette_mode_configuration is not None: | ||
| if volume_for_pipette_mode_configuration is not None and ( | ||
| self._protocol_core.api_version < APIVersion(2, 28) | ||
| or self._engine_client.state.pipettes.get_will_volume_mode_change( | ||
| self._pipette_id, volume_for_pipette_mode_configuration | ||
| ) | ||
| ): | ||
| prep_location = Location( | ||
| point=source_well.get_top(LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP.z), | ||
| labware=source_loc.labware, | ||
|
|
@@ -2171,10 +2176,14 @@ def aspirate_liquid_class( | |
| location=prep_location, | ||
| ) | ||
| last_liquid_and_airgap_in_tip.air_gap = 0 | ||
| # TODO: do volume configuration + prepare for aspirate only if the mode needs to be changed | ||
| self.configure_for_volume(volume_for_pipette_mode_configuration) | ||
| self.prepare_to_aspirate() | ||
|
|
||
| elif not self._engine_client.state.pipettes.get_ready_to_aspirate( | ||
| self._pipette_id | ||
| ): | ||
| self.prepare_to_aspirate() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, still trying to wrap my head around what this is doing. BEFORE, Can that (volume_for_pipette_mode_configuration=None and not being ready to aspirate here) ever happen?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right in that this could cause a difference in behavior in existing protocols and I can make sure this doesn't trigger in those instances. The fact that we were always unconditionally going down this
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, my question was more: was there ever a case when
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so, because the only time this would be
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, thanks! |
||
|
|
||
| aspirate_point = ( | ||
| tx_comps_executor.absolute_point_from_position_reference_and_offset( | ||
| well=source_well, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,7 @@ class LoadedStaticPipetteData: | |
| shaft_ul_per_mm: float | ||
| available_sensors: pipette_definition.AvailableSensorDefinition | ||
| volume_mode: pip_types.LiquidClasses # pip_types Liquid Classes refers to volume modes | ||
| available_volume_modes_min_vol: Dict[pip_types.LiquidClasses, float] # Ditto | ||
|
|
||
|
|
||
| class VirtualPipetteDataProvider: | ||
|
|
@@ -300,6 +301,10 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 | |
| available_sensors=config.available_sensors | ||
| or pipette_definition.AvailableSensorDefinition(sensors=[]), | ||
| volume_mode=liquid_class, | ||
| available_volume_modes_min_vol={ | ||
| volume_mode: props.min_volume | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey, is Do you mean the minimum supported volume in this particular mode? If it's the minimum volume for the pipette in general, what's the point of introducing your
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is in practice the minimum supported volume of each mode, and we determine if we switch to low volume mode if both we have a low volume mode AND we are providing a volume below the default minimum.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the min volume should reference the min volume in each of default or lowVolumeDefault
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ryanthecoder or @jbleon95: For the I.e., for a low-volume-capable pipette, we would have something like:
?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, a |
||
| for volume_mode, props in config.liquid_properties.items() | ||
| }, | ||
| ) | ||
|
|
||
| def get_virtual_pipette_static_config( | ||
|
|
@@ -356,6 +361,10 @@ def get_pipette_static_config( | |
| shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"], | ||
| available_sensors=available_sensors, | ||
| volume_mode=pipette_dict["volume_mode"], | ||
| available_volume_modes_min_vol={ | ||
| volume_mode: props.min_volume | ||
| for volume_mode, props in pipette_dict["available_volume_modes"].items() | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,6 +111,7 @@ class StaticPipetteConfig: | |
| shaft_ul_per_mm: float | ||
| available_sensors: pipette_definition.AvailableSensorDefinition | ||
| volume_mode: VolumeModes | ||
| available_volume_modes_min_vol: Dict[VolumeModes, float] | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
|
|
@@ -318,6 +319,7 @@ def _update_pipette_config(self, state_update: update_types.StateUpdate) -> None | |
| shaft_ul_per_mm=config.shaft_ul_per_mm, | ||
| available_sensors=config.available_sensors, | ||
| volume_mode=config.volume_mode, | ||
| available_volume_modes_min_vol=config.available_volume_modes_min_vol, | ||
| ) | ||
| self._state.flow_rates_by_id[ | ||
| state_update.pipette_config.pipette_id | ||
|
|
@@ -876,6 +878,30 @@ def get_is_low_volume_mode(self, pipette_id: str) -> bool: | |
| """Determine if the pipette is currently in low volume mode.""" | ||
| return self.get_config(pipette_id).volume_mode == VolumeModes.lowVolumeDefault | ||
|
|
||
| def get_volume_mode_from_volume( | ||
| self, pipette_id: str, volume: float | ||
| ) -> VolumeModes: | ||
| """Get the volume mode for the given pipette and volume quantity.""" | ||
| available_volume_modes_min_vol = self.get_config( | ||
| pipette_id | ||
| ).available_volume_modes_min_vol | ||
| has_low_volume_mode = ( | ||
| VolumeModes.lowVolumeDefault in available_volume_modes_min_vol | ||
| ) | ||
|
|
||
| if not has_low_volume_mode: | ||
| return VolumeModes.default | ||
| if volume >= available_volume_modes_min_vol[VolumeModes.default]: | ||
| return VolumeModes.default | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, this PR seems to do a lot of plumbing (adding I thought you were going to use If you just need the "high-volume mode transition point," would it be easier to just add that one number to the PipetteDict?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I originally was passing the whole dictionary to the engine before I realized it added a lot of cruft to unit tests and decided it was easier to just pass what we need. I can go back and refactor PipetteDict to be more straightforward if we can't think of another reason to keep the full object for any future work |
||
| return VolumeModes.lowVolumeDefault | ||
|
|
||
| def get_will_volume_mode_change(self, pipette_id: str, volume: float) -> bool: | ||
| """Determine if the pipette will change volume mode based on current volume mode and new volume.""" | ||
| return ( | ||
| self.get_volume_mode_from_volume(pipette_id, volume) | ||
| != self.get_config(pipette_id).volume_mode | ||
| ) | ||
|
|
||
| def lookup_volume_to_mm_conversion( | ||
| self, pipette_id: str, volume: float, action: str | ||
| ) -> float: | ||
|
|
||
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, my philosophical objection to adding more queries to the engine is that it makes life harder for future-us when we want to be able to say what steps a transfer will do without actually performing the transfer.
Does @ncdiehl still want PD step generation to match Python command-for-command? This would make that a bit harder since PD doesn't have an engine to query.
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 think this necessarily precludes us from doing that in the future, because honestly all we need to know is the pipette's initial state, since we'll prepare to aspirate within the transfer component executor after this. But perhaps I should clean this up a bit to make that more clear or make it more functional. But I think on its own this is a correct code, especially if an overpressure error happens during the course of a protocol which we can't predict.
As for PD, I don't think we have the equivalent to this, but this is checking a variable that is updated in response to
pick_up_tip, any form ofblow_out, a fulldispenseand that I imagine we could track