From 8ae24990ae524f8eebd4e9fc3eeb64eb9133dc5c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 23 Apr 2025 13:59:25 -0400 Subject: [PATCH 1/7] chore: make lpc analytics kind have a nicer name (#18158) none of the other ones are prefixed like this --- app/src/redux/analytics/constants.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/redux/analytics/constants.ts b/app/src/redux/analytics/constants.ts index 4d986162b31f..945d37c4681f 100644 --- a/app/src/redux/analytics/constants.ts +++ b/app/src/redux/analytics/constants.ts @@ -120,8 +120,7 @@ export const ANALYTICS_LANGUAGE_UPDATED_DESKTOP_APP_SETTINGS: 'languageUpdatedDe * LPC Analytics */ -export const ANALYTICS_LPC_ANALYSIS_KIND: 'analytics:lpcAnalysisKind' = - 'analytics:lpcAnalysisKind' +export const ANALYTICS_LPC_ANALYSIS_KIND: 'lpcAnalysisKind' = 'lpcAnalysisKind' /** * Module Actions Analytics From 7465bcf83c63284ef7e056b29cd73b53aa952981 Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:59:16 -0400 Subject: [PATCH 2/7] refactor(api): make estimating liquid height after pipetting account for nozzles/well (#18167) --- .../protocol_api/core/engine/well.py | 13 +++++- .../core/legacy/legacy_well_core.py | 3 +- api/src/opentrons/protocol_api/core/well.py | 3 +- api/src/opentrons/protocol_api/labware.py | 4 +- .../protocol_engine/execution/pipetting.py | 2 + .../protocol_engine/state/geometry.py | 29 +++++++++++++- .../core/engine/test_well_core.py | 24 +++++++++-- .../execution/test_pipetting_handler.py | 5 ++- .../state/test_geometry_view.py | 40 +++++++++++++++++-- 9 files changed, 110 insertions(+), 13 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index e18bdb8ecf6a..964e1f7576c1 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -3,7 +3,7 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN -from opentrons.types import Point +from opentrons.types import Point, Mount, MountType from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset from opentrons.protocol_engine import commands as cmd @@ -13,6 +13,7 @@ SimulatedProbeResult, LiquidTrackingType, ) +from opentrons.protocol_engine.errors import PipetteNotAttachedError from . import point_calculations from . import stringify @@ -181,15 +182,25 @@ def from_center_cartesian(self, x: float, y: float, z: float) -> Point: def estimate_liquid_height_after_pipetting( self, + mount: Mount | str, operation_volume: float, ) -> LiquidTrackingType: """Return an estimate of liquid height after pipetting without raising an error.""" labware_id = self.labware_id well_name = self._name + if isinstance(mount, Mount): + mount_type = MountType.from_hw_mount(mount) + else: + mount_type = MountType(mount) + pipette_from_mount = self._engine_client.state.pipettes.get_by_mount(mount_type) + if pipette_from_mount is None: + raise PipetteNotAttachedError(f"No pipette present on mount {mount}") + pipette_id = pipette_from_mount.id starting_liquid_height = self.current_liquid_height() projected_final_height = self._engine_client.state.geometry.get_well_height_after_liquid_handling_no_error( labware_id=labware_id, well_name=well_name, + pipette_id=pipette_id, initial_height=starting_liquid_height, volume=operation_volume, ) diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py index e0a12039c518..671409c9587a 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py @@ -5,7 +5,7 @@ from opentrons.protocols.api_support.util import APIVersionError -from opentrons.types import Point +from opentrons.types import Point, Mount from opentrons.protocol_engine.types.liquid_level_detection import ( SimulatedProbeResult, @@ -129,6 +129,7 @@ def from_center_cartesian(self, x: float, y: float, z: float) -> Point: def estimate_liquid_height_after_pipetting( self, + mount: Mount | str, operation_volume: float, ) -> LiquidTrackingType: """Estimate what the liquid height will be after pipetting, without raising an error.""" diff --git a/api/src/opentrons/protocol_api/core/well.py b/api/src/opentrons/protocol_api/core/well.py index d02cea5cb24b..02fe1f47f424 100644 --- a/api/src/opentrons/protocol_api/core/well.py +++ b/api/src/opentrons/protocol_api/core/well.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from typing import TypeVar, Optional, Union -from opentrons.types import Point +from opentrons.types import Point, Mount from opentrons.protocol_engine.types import LiquidTrackingType from .._liquid import Liquid @@ -91,6 +91,7 @@ def from_center_cartesian(self, x: float, y: float, z: float) -> Point: @abstractmethod def estimate_liquid_height_after_pipetting( self, + mount: Mount | str, operation_volume: float, ) -> LiquidTrackingType: """Estimate what the liquid height will be after pipetting, without raising an error.""" diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 51f1301bf7b3..6580c004fa33 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -39,6 +39,7 @@ Point, NozzleMapInterface, MeniscusTrackingTarget, + Mount, ) from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( @@ -348,6 +349,7 @@ def current_liquid_volume(self) -> LiquidTrackingType: @requires_version(2, 21) def estimate_liquid_height_after_pipetting( self, + mount: Mount | str, operation_volume: float, ) -> LiquidTrackingType: """Check the height of the liquid within a well. @@ -360,7 +362,7 @@ def estimate_liquid_height_after_pipetting( """ projected_final_height = self._core.estimate_liquid_height_after_pipetting( - operation_volume=operation_volume, + operation_volume=operation_volume, mount=mount ) return projected_final_height diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index b503f4ae69f4..1d21542411dc 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -199,6 +199,7 @@ async def aspirate_while_tracking( labware_id=labware_id, well_name=well_name, operation_volume=volume * -1, + pipette_id=pipette_id, ) if isinstance(aspirate_z_distance, SimulatedProbeResult): raise InvalidLiquidHeightFound( @@ -234,6 +235,7 @@ async def dispense_while_tracking( labware_id=labware_id, well_name=well_name, operation_volume=volume, + pipette_id=pipette_id, ) if isinstance(dispense_z_distance, SimulatedProbeResult): raise InvalidLiquidHeightFound( diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 4ba4c35a32f2..84f66844c318 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -520,6 +520,7 @@ def get_well_position( well_location=well_location, well_depth=well_depth, operation_volume=operation_volume, + pipette_id=pipette_id, ) if not isinstance(offset_adjustment, SimulatedProbeResult): offset = offset.model_copy(update={"z": offset.z + offset_adjustment}) @@ -1844,6 +1845,7 @@ def get_liquid_handling_z_change( self, labware_id: str, well_name: str, + pipette_id: str, operation_volume: float, ) -> float: """Get the change in height from a liquid handling operation.""" @@ -1853,6 +1855,7 @@ def get_liquid_handling_z_change( final_height = self.get_well_height_after_liquid_handling( labware_id=labware_id, well_name=well_name, + pipette_id=pipette_id, initial_height=initial_handling_height, volume=operation_volume, ) @@ -1873,6 +1876,7 @@ def get_well_offset_adjustment( well_name: str, well_location: WellLocationType, well_depth: float, + pipette_id: Optional[str] = None, operation_volume: Optional[float] = None, ) -> LiquidTrackingType: """Return a z-axis distance that accounts for well handling height and operation volume. @@ -1906,9 +1910,14 @@ def get_well_offset_adjustment( volume = well_location.volumeOffset if volume: + if pipette_id is None: + raise ValueError( + "cannot get liquid handling offset without pipette id." + ) liquid_height_after = self.get_well_height_after_liquid_handling( labware_id=labware_id, well_name=well_name, + pipette_id=pipette_id, initial_height=initial_handling_height, volume=volume, ) @@ -2030,6 +2039,7 @@ def get_well_height_after_liquid_handling( self, labware_id: str, well_name: str, + pipette_id: str, initial_height: LiquidTrackingType, volume: float, ) -> LiquidTrackingType: @@ -2044,7 +2054,14 @@ def get_well_height_after_liquid_handling( initial_volume = find_volume_at_well_height( target_height=initial_height, well_geometry=well_geometry ) - final_volume = initial_volume + volume + final_volume = initial_volume + ( + volume + * self.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ) return find_height_at_well_volume( target_volume=final_volume, well_geometry=well_geometry ) @@ -2058,6 +2075,7 @@ def get_well_height_after_liquid_handling_no_error( self, labware_id: str, well_name: str, + pipette_id: str, initial_height: LiquidTrackingType, volume: float, ) -> LiquidTrackingType: @@ -2073,7 +2091,14 @@ def get_well_height_after_liquid_handling_no_error( initial_volume = find_volume_at_well_height( target_height=initial_height, well_geometry=well_geometry ) - final_volume = initial_volume + volume + final_volume = initial_volume + ( + volume + * self.get_nozzles_per_well( + labware_id=labware_id, + target_well_name=well_name, + pipette_id=pipette_id, + ) + ) well_volume = find_height_at_well_volume( target_volume=final_volume, well_geometry=well_geometry, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py index a45ba03aac22..cad3a1fff8c9 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py @@ -10,9 +10,15 @@ RectangularWellDefinition2, CircularWellDefinition2, ) +from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.protocol_api import MAX_SUPPORTED_VERSION -from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset +from opentrons.protocol_engine import ( + WellLocation, + WellOrigin, + WellOffset, + LoadedPipette, +) from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_engine.errors.exceptions import ( @@ -21,7 +27,7 @@ ) from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import UnsupportedAPIError -from opentrons.types import Point +from opentrons.types import Point, Mount, MountType from opentrons_shared_data.labware.labware_definition import ( InnerWellGeometry, ConicalFrustum, @@ -312,11 +318,13 @@ def test_current_liquid_volume( @pytest.mark.parametrize("operation_volume", [0.0, 100, -100, 2, -4, 5]) +@pytest.mark.parametrize("mount", [Mount.LEFT, "left"]) def test_estimate_liquid_height_after_pipetting( decoy: Decoy, subject: WellCore, mock_engine_client: EngineClient, operation_volume: float, + mount: Mount | str, ) -> None: """Make sure estimate_liquid_height_after_pipetting returns the correct value and does not raise an error.""" fake_well_geometry = InnerWellGeometry( @@ -355,14 +363,24 @@ def test_estimate_liquid_height_after_pipetting( mock_engine_client.state.geometry.get_well_height_after_liquid_handling_no_error( labware_id="labware-id", well_name="well-name", + pipette_id="pipette-id", initial_height=initial_liquid_height, volume=operation_volume, ) ).then_return(fake_final_height) + decoy.when( + mock_engine_client.state.pipettes.get_by_mount(MountType.LEFT) + ).then_return( + LoadedPipette( + id="pipette-id", + pipetteName=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + ) # make sure that no error was raised final_height = subject.estimate_liquid_height_after_pipetting( - operation_volume=operation_volume, + operation_volume=operation_volume, mount=mount ) assert final_height == fake_final_height diff --git a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py index e94dc074d5d7..8fd017ec194a 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py @@ -332,7 +332,10 @@ async def test_hw_aspirate_while_tracking( decoy.when( mock_state_view.geometry.get_liquid_handling_z_change( - labware_id="labware-id", well_name="A1", operation_volume=-25.0 + labware_id="labware-id", + well_name="A1", + pipette_id="pipette_id", + operation_volume=-25.0, ) ).then_return(4.544) diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index c29cb066e563..4e301b3a007a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1773,6 +1773,10 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( slot_pos = Point(4, 5, 6) well_def = well_plate_def.wells["B2"] + pip_type = PipetteNameType.P300_SINGLE + decoy.when(mock_pipette_view.get_nozzle_configuration("pipette-id")).then_return( + get_default_nozzle_map(pip_type) + ) decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) decoy.when(mock_labware_view.get_definition("labware-id")).then_return( well_plate_def @@ -1843,7 +1847,10 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) slot_pos = Point(4, 5, 6) well_def = well_plate_def.wells["B2"] - + pip_type = PipetteNameType.P300_SINGLE + decoy.when(mock_pipette_view.get_nozzle_configuration("pipette-id")).then_return( + get_default_nozzle_map(pip_type) + ) decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) decoy.when(mock_labware_view.get_definition("labware-id")).then_return( well_plate_def @@ -1941,6 +1948,10 @@ def test_get_well_position_raises_validation_error( decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( _TEST_INNER_WELL_GEOMETRY ) + pip_type = PipetteNameType.P300_SINGLE + decoy.when(mock_pipette_view.get_nozzle_configuration("pipette-id")).then_return( + get_default_nozzle_map(pip_type) + ) decoy.when( mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") ).then_return(0.5) @@ -4184,17 +4195,27 @@ def test_virtual_get_well_height_after_liquid_handling_no_error( decoy: Decoy, subject: GeometryView, mock_labware_view: LabwareView, + mock_pipette_view: PipetteView, + well_plate_def: LabwareDefinition, initial_liquid_height: LiquidTrackingType, ) -> None: """Make sure SimulatedLiquidProbe doesn't change geometry behavior.""" + pip_type = PipetteNameType.P300_SINGLE + decoy.when(mock_pipette_view.get_nozzle_configuration("pipette-id")).then_return( + get_default_nozzle_map(pip_type) + ) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( _TEST_INNER_WELL_GEOMETRY ) operation_volume = 1000.0 - result_estimate = subject.get_well_height_after_liquid_handling_no_error( labware_id="labware-id", well_name="B2", + pipette_id="pipette-id", initial_height=initial_liquid_height, volume=operation_volume, ) @@ -4228,13 +4249,23 @@ def test_virtual_find_height_and_volume( def test_get_liquid_handling_z_change( decoy: Decoy, subject: GeometryView, + well_plate_def: LabwareDefinition, mock_labware_view: LabwareView, + mock_pipette_view: PipetteView, mock_well_view: WellView, ) -> None: """Test for get_liquid_handling_z_change math.""" + pip_type = PipetteNameType.P300_SINGLE + decoy.when(mock_pipette_view.get_nozzle_configuration("pipette-id")).then_return( + get_default_nozzle_map(pip_type) + ) + decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return( RectangularWellDefinition3.model_construct(totalLiquidVolume=1100000) # type: ignore[call-arg] ) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return( _TEST_INNER_WELL_GEOMETRY ) @@ -4251,7 +4282,10 @@ def test_get_liquid_handling_z_change( ) # make sure that liquid handling z change math stays the same change = subject.get_liquid_handling_z_change( - labware_id="labware-id", well_name="A1", operation_volume=199.0 + labware_id="labware-id", + well_name="A1", + pipette_id="pipette-id", + operation_volume=199.0, ) expected_change = 3.2968 assert isclose(change, expected_change) From 6694bb13d2c648f71a873043fb5ddd4c88419f93 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Fri, 25 Apr 2025 09:46:38 -0400 Subject: [PATCH 3/7] fix(api): make measure liquid height recoverable. (#18173) # Overview This turns the previous measure liquid height implementation that didn't allow for recovery into a recoverable one. This is done by just doing the "require_liquid_presnece" call and then using the new well state endpoint to return the height instead of getting it directly from the engine. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment Since this is just using the same calls as the other api endpoints there is no additional testing to be done, and since it would just fail protocols before and now it is perhaps recoverable then this can only be a good thing. --- api/src/opentrons/protocol_api/instrument_context.py | 3 ++- .../opentrons/protocol_api/test_instrument_context.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 1d2da5739c96..e9b6c776ab96 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2698,7 +2698,8 @@ def measure_liquid_height(self, well: labware.Well) -> LiquidTrackingType: """ self._raise_if_pressure_not_supported_by_pipette() loc = well.top() - return self._core.liquid_probe_without_recovery(well._core, loc) + self._core.liquid_probe_with_recovery(well._core, loc) + return well.current_liquid_height() def _raise_if_configuration_not_supported_by_pipette( self, style: NozzleLayout diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 6b8fc4648b1f..8bdeb9adf98a 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1493,8 +1493,15 @@ def test_measure_liquid_height( original_error=lnfe, message=f"{lnfe.errorType}: {lnfe.detail}", ) + decoy.when(mock_well.current_liquid_height()).then_return(123) decoy.when( - mock_instrument_core.liquid_probe_without_recovery( + mock_instrument_core.liquid_probe_with_recovery( + mock_well._core, mock_well.top() + ) + ) + assert subject.measure_liquid_height(mock_well) == 123 + decoy.when( + mock_instrument_core.liquid_probe_with_recovery( mock_well._core, mock_well.top() ) ).then_raise(errorToRaise) From 1f15b1c3a060510656e794511b768e3f71c85fec Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 25 Apr 2025 11:36:24 -0400 Subject: [PATCH 4/7] fix(api): fix SimulatedProbeResult ser/deser (#18177) We weren't deserializing the simulation standin properly, which broke runlog downloading. Now we are. Also, we shouldn't be dumping it to the runlog anymore. That was happening because of https://github.com/pydantic/pydantic/issues/6830 which causes mis-serialization of fields typed as `Model | NativeType`, where `NativeType` is some scalar python type (i.e. float, int) and `Model` is some pydantic model. With the model first, serializing the container will always result in the serialization of the default construction of the model, even if the attribute actually contained the native type. Flipping the order makes it work correctly, for some reason. Luckily this is consistent enough to be testable. Closes RQA-4147 Closes EXEC-1429 Closes EXEC-1358 --- .../protocol_engine/types/__init__.py | 2 + .../types/liquid_level_detection.py | 34 +++++------ .../opentrons/protocol_engine/test_types.py | 58 ++++++++++++++++++- 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/api/src/opentrons/protocol_engine/types/__init__.py b/api/src/opentrons/protocol_engine/types/__init__.py index 435d23dcca61..9d22d8be216b 100644 --- a/api/src/opentrons/protocol_engine/types/__init__.py +++ b/api/src/opentrons/protocol_engine/types/__init__.py @@ -135,6 +135,7 @@ WellInfoSummary, WellLiquidInfo, LiquidTrackingType, + SimulatedProbeResult, ) from .liquid_handling import FlowRates from .labware_movement import LabwareMovementStrategy, LabwareMovementOffsetData @@ -280,6 +281,7 @@ "WellInfoSummary", "WellLiquidInfo", "LiquidTrackingType", + "SimulatedProbeResult", # Liquid handling "FlowRates", # Labware movement diff --git a/api/src/opentrons/protocol_engine/types/liquid_level_detection.py b/api/src/opentrons/protocol_engine/types/liquid_level_detection.py index 235add1caf16..4d6bd82329e4 100644 --- a/api/src/opentrons/protocol_engine/types/liquid_level_detection.py +++ b/api/src/opentrons/protocol_engine/types/liquid_level_detection.py @@ -1,9 +1,10 @@ """Protocol Engine types to do with liquid level detection.""" + from __future__ import annotations from dataclasses import dataclass from datetime import datetime -from typing import Optional, List -from pydantic import BaseModel, model_serializer, field_validator +from typing import Optional, List, Any +from pydantic import BaseModel, model_serializer, model_validator class SimulatedProbeResult(BaseModel): @@ -17,6 +18,14 @@ def serialize_model(self) -> str: """Serialize instances of this class as a string.""" return "SimulatedProbeResult" + @model_validator(mode="before") + @classmethod + def validate_model(cls, data: object) -> Any: + """Handle deserializing from a simulated probe result.""" + if isinstance(data, str) and data == "SimulatedProbeResult": + return {} + return data + def __add__( self, other: float | SimulatedProbeResult ) -> float | SimulatedProbeResult: @@ -75,7 +84,9 @@ def simulate_probed_aspirate_dispense(self, volume: float) -> None: self.operations_after_probe.append(volume) -LiquidTrackingType = SimulatedProbeResult | float +# Work around https://github.com/pydantic/pydantic/issues/6830 - do not change the order of +# this union +LiquidTrackingType = float | SimulatedProbeResult class LoadedVolumeInfo(BaseModel): @@ -104,23 +115,6 @@ class ProbedVolumeInfo(BaseModel): class WellInfoSummary(BaseModel): """Payload for a well's liquid info in StateSummary.""" - # TODO(cm): 3/21/25: refactor SimulatedLiquidProbe in a way that - # doesn't require models like this one that are just using it to - # need a custom validator - @field_validator("probed_height", "probed_volume", mode="before") - @classmethod - def validate_simulated_probe_result( - cls, input_val: object - ) -> LiquidTrackingType | None: - """Return the appropriate input to WellInfoSummary from json data.""" - if input_val is None: - return None - if isinstance(input_val, LiquidTrackingType): - return input_val - if isinstance(input_val, str) and input_val == "SimulatedProbeResult": - return SimulatedProbeResult() - raise ValueError(f"Invalid input value {input_val} to WellInfoSummary") - labware_id: str well_name: str loaded_volume: Optional[float] = None diff --git a/api/tests/opentrons/protocol_engine/test_types.py b/api/tests/opentrons/protocol_engine/test_types.py index d48c67ee61ed..fb9a0978bb0e 100644 --- a/api/tests/opentrons/protocol_engine/test_types.py +++ b/api/tests/opentrons/protocol_engine/test_types.py @@ -1,8 +1,14 @@ """Test protocol engine types.""" + import pytest -from pydantic import ValidationError +from pydantic import ValidationError, BaseModel -from opentrons.protocol_engine.types import HexColor +from opentrons.protocol_engine.types import ( + HexColor, + SimulatedProbeResult, + LiquidTrackingType, + WellInfoSummary, +) @pytest.mark.parametrize("hex_color", ["#F00", "#FFCC00CC", "#FC0C", "#98e2d1"]) @@ -20,3 +26,51 @@ def test_handles_invalid_hex(invalid_hex_color: str) -> None: HexColor(invalid_hex_color) with pytest.raises(ValidationError): HexColor.model_validate_json(f'"{invalid_hex_color}"') + + +class _TestModel(BaseModel): + """Test model for deserializing SimulatedProbeResults.""" + + value: LiquidTrackingType + + +def test_roundtrips_simulated_liquid_probe() -> None: + """Should be able to roundtrip our simulated results.""" + base = _TestModel(value=SimulatedProbeResult()) + serialized = base.model_dump_json() + deserialized = _TestModel.model_validate_json(serialized) + assert isinstance(deserialized.value, SimulatedProbeResult) + + +def test_roundtrips_nonsimulated_liquid_probe() -> None: + """Should be able to roundtrip our simulated results.""" + base = _TestModel(value=10.0) + serialized = base.model_dump_json() + deserialized = _TestModel.model_validate_json(serialized) + assert deserialized.value == 10.0 + + +def test_fails_deser_wrong_string() -> None: + """Should fail to deserialize the wrong string.""" + with pytest.raises(ValidationError): + _TestModel.model_validate_json('{"value": "not the right string"}') + + +@pytest.mark.parametrize("height", [None, 10.0, SimulatedProbeResult()]) +def test_roundtrips_well_info_summary(height: LiquidTrackingType | None) -> None: + """It should round trip a WellInfoSummary.""" + inp = WellInfoSummary( + labware_id="hi", + well_name="lo", + loaded_volume=None, + probed_height=height, + probed_volume=height, + ) + outp = WellInfoSummary.model_validate_json(inp.model_dump_json()) + if isinstance(height, SimulatedProbeResult): + assert outp.labware_id == inp.labware_id + assert outp.well_name == inp.well_name + assert isinstance(outp.probed_height, SimulatedProbeResult) + assert isinstance(outp.probed_volume, SimulatedProbeResult) + else: + assert outp == inp From 3ae4afac698710272a4e2e6d7f6d33db36645740 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Fri, 25 Apr 2025 14:14:41 -0400 Subject: [PATCH 5/7] chore(localization): sync to Locize for 8.4.0 iteration two (#18154) --- app/src/assets/localization/zh/anonymous.json | 6 +- .../localization/zh/device_settings.json | 2 + .../localization/zh/error_recovery.json | 18 +++-- .../zh/labware_position_check.json | 81 +++++++++++++++++++ .../localization/zh/protocol_setup.json | 5 +- 5 files changed, 101 insertions(+), 11 deletions(-) diff --git a/app/src/assets/localization/zh/anonymous.json b/app/src/assets/localization/zh/anonymous.json index 0981184ba676..6d555af80d04 100644 --- a/app/src/assets/localization/zh/anonymous.json +++ b/app/src/assets/localization/zh/anonymous.json @@ -40,7 +40,7 @@ "module_error_contact_support": "尝试关闭模块电源,然后再打开。如果报错仍然存在,请与支持人员联系。", "network_setup_menu_description": "您将使用此连接来运行软件更新,并将协议加载到您的工作站上。", "new_robot_instructions": "设置新工作站时,请遵循触摸屏上的指示。有关更多信息,请参阅您的工作站快速入门指南。", - "oem_mode_description": "启用OEM模式,以从Flex触摸屏中移除Opentrons的相关信息。", + "oem_mode_description": "启用 OEM 模式以从 Flex 触摸屏中删除所有 Opentrons 实例。", "opentrons_app_successfully_updated": "应用程序已成功更新。", "opentrons_app_update": "应用程序更新", "opentrons_app_update_available": "应用程序可更新", @@ -65,9 +65,9 @@ "share_app_analytics": "共享应用程序分析数据", "share_app_analytics_description": "通过自动发送匿名诊断和使用数据来帮助改进此产品。", "share_display_usage_description": "关于工作站触摸屏的交互数据。", - "share_logs_with_opentrons": "共享工作站日志", + "share_logs_with_opentrons": "分享机器人日志", "share_logs_with_opentrons_description": "通过自动发送匿名的工作站日志来帮助改进此产品。这些日志用于解决工作站问题和发现错误趋势。", - "show_labware_offset_snippets_description": "仅适用于需要在应用程序之外应用耗材校准数据的用户。启用后,在设置协议过程中可访问Jupyter Notebook和SSH的代码片段。", + "show_labware_offset_snippets_description": "仅适用于需要在应用程序外部应用实验室器具偏移数据的用户。启用后,在配置所有必需的偏移量后,可在协议设置期间使用 Jupyter Notebook 和 SSH 的代码片段。", "something_seems_wrong": "您的移液器可能有问题。退出设置并联系支持人员以获取帮助。", "storage_limit_reached_description": "您的工作站已达到可存储的快速移液数量上限。在创建新的快速移液之前,您必须删除一个现有的快速移液。", "system_language_preferences_update_description": "您系统的语言最近已更新。您想使用更新后的语言作为应用的默认语言吗?", diff --git a/app/src/assets/localization/zh/device_settings.json b/app/src/assets/localization/zh/device_settings.json index 591c9cfcb7ad..cf4b25b02303 100644 --- a/app/src/assets/localization/zh/device_settings.json +++ b/app/src/assets/localization/zh/device_settings.json @@ -50,6 +50,7 @@ "clear_option_deck_calibration": "清除甲板校准", "clear_option_gripper_calibration": "清除转板抓手校准", "clear_option_gripper_offset_calibrations": "清除转板抓手校准", + "clear_option_labware_offsets": "清除耗材校准数据", "clear_option_module_calibration": "清除模块校准", "clear_option_pipette_calibrations": "清除移液器校准", "clear_option_pipette_offset_calibrations": "清除移液器偏移校准", @@ -231,6 +232,7 @@ "privacy": "隐私", "problem_during_update": "此次更新耗时比平常要长。", "proceed_without_updating": "跳过更新以继续", + "protocol_run_data": "协议运行数据", "recalibrate_deck": "重新校准甲板", "recalibrate_gripper": "重新校准转板抓手", "recalibrate_module": "重新校准模块", diff --git a/app/src/assets/localization/zh/error_recovery.json b/app/src/assets/localization/zh/error_recovery.json index a740eac779dd..ca9d718c2379 100644 --- a/app/src/assets/localization/zh/error_recovery.json +++ b/app/src/assets/localization/zh/error_recovery.json @@ -21,23 +21,24 @@ "continue_to_drop_tip": "继续丢弃吸头", "do_you_need_to_blowout": "首先,请问需要排出枪头内的液体吗?", "door_open_robot_home": "在手动移动实验用品前,设备需要安全归位。", + "droplets_or_liquid_cause_failure": "吸头内的液滴或液体可能会导致液位检测失败", "ensure_lw_is_accurately_placed": "确保实验耗材已准确放置在甲板槽中,防止进一步出现错误", "error": "错误", "error_details": "错误详情", "error_on_robot": "{{robot}}上的错误", "failed_dispense_step_not_completed": "中断运行的最后一步液体排出失败,恢复程序将不会继续运行这一步骤,请手动完成这一步的移液操作。运行将继续从下一步开始。继续之前,请关闭工作站门。", "failed_step": "失败步骤", - "first_is_gripper_holding_labware": "首先,抓扳手是否夹着实验耗材?", + "first_is_gripper_holding_labware": "首先,转板抓手是否夹着实验耗材?", "go_back": "返回", - "gripper_error": "抓扳手错误", - "gripper_errors_occur_when": "当抓扳手停滞或与甲板上另一物体碰撞时,会发生抓扳手错误,这通常是由于实验耗材放置不当或实验耗材偏移不准确所致", - "gripper_releasing_labware": "抓扳手正在释放实验耗材", - "gripper_will_release_in_s": "抓扳手将在{{seconds}}秒后释放实验耗材", + "gripper_error": "转板抓手错误", + "gripper_errors_occur_when": "当转板抓手停滞或与甲板上另一物体碰撞时,会发生转板抓手错误,这通常是由于实验耗材放置不当或实验耗材偏移不准确所致", + "gripper_releasing_labware": "转板抓手正在释放实验耗材", + "gripper_will_release_in_s": "转板抓手将在{{seconds}}秒后释放实验耗材", "home_and_retry": "归位并重试该步骤", "home_gantry": "归位", "home_now": "现在归位", "homing_pipette_dangerous": "如果移液器中有液体,将{{mount}}移液器归位可能会损坏它。您必须在使用移液器之前取下所有吸头。", - "if_issue_persists_gripper_error": "如果问题持续存在,请取消运行并重新运行抓扳手校准", + "if_issue_persists_gripper_error": "如果问题持续存在,请取消运行并重新运行转板抓手校准", "if_issue_persists_overpressure": "如果问题持续存在,请取消运行并对协议进行必要的更改", "if_issue_persists_tip_not_detected": "如果问题持续存在,请取消运行并启动实验耗材位置检查", "if_tips_are_attached": "如果吸头还在移液器上,您可以在运行终止前选择吹出已吸取的液体并丢弃吸头。", @@ -49,7 +50,8 @@ "labware_released_from_current_height": "将从当前高度释放实验耗材", "launch_recovery_mode": "启动恢复模式", "manually_fill_liquid_in_well": "手动填充孔位{{well}}中的液体", - "manually_fill_well_and_skip": "手动填充孔位并跳到下一步", + "manually_fill_well_and_retry_new_tips": "手动填充好并用新吸头重试", + "manually_fill_well_and_retry_same_tips": "手动填充并用相同的提示重试", "manually_move_lw_and_skip": "手动移动实验耗材并跳至下一步", "manually_move_lw_on_deck": "手动移动实验耗材", "manually_replace_lw_and_retry": "手动更换实验耗材并重试此步骤", @@ -109,6 +111,7 @@ "stand_back_resuming": "请远离,正在恢复当前步骤", "stand_back_retrying": "请远离,正在重试失败步骤", "stand_back_skipping_to_next_step": "请远离,正在跳到下一步骤", + "static_meniscus_less_accurate": "如果使用静态弯月面移液,跳过液体存在检测时液体跟踪可能不太准确。", "take_any_necessary_precautions": "在接住实验耗材之前,请采取必要的预防措施。确认后,夹爪将开始倒计时再释放。", "take_necessary_actions": "首先,采取任何必要的行动,让工作站重新尝试失败的步骤。然后,在继续之前关闭工作站门。", "take_necessary_actions_failed_pickup": "首先,采取任何必要的行动,让工作站重新尝试移液器拾取。然后,在继续之前关闭工作站门。", @@ -119,6 +122,7 @@ "tip_drop_failed": "丢弃吸头失败", "tip_not_detected": "未检测到吸头", "tip_presence_errors_are_caused": "吸头识别错误通常是由实验器皿放置不当或器皿偏移不准确引起的。", + "use_dry_unused_tips": "使用干燥、未使用过的吸头以获得最佳效果", "view_error_details": "查看错误详情", "view_recovery_options": "查看恢复选项", "you_can_still_drop_tips": "在继续选择吸头之前,您仍然可以丢弃移液器上现存的吸头。" diff --git a/app/src/assets/localization/zh/labware_position_check.json b/app/src/assets/localization/zh/labware_position_check.json index 3c66cff922dc..3ecbec490626 100644 --- a/app/src/assets/localization/zh/labware_position_check.json +++ b/app/src/assets/localization/zh/labware_position_check.json @@ -2,41 +2,83 @@ "adapter_in_mod_in_slot": "{{slot}}中{{module}}上的{{adapter}}", "adapter_in_slot": "{{slot}}上的{{adapter}}", "adapter_in_tc": "{{module}}上的{{adapter}}", + "add": "添加", + "add_a_default_offset": "添加耗材默认校准数据,并自动应用于甲板上所有位置", + "add_default_labware_offset": "添加耗材默认校准数据", + "adjust": "调整", + "adjust_applied_location_offset": "调整已应用的校准数据", + "adjust_default_labware_offset": "调整默认耗材校准数据", + "align_to_top_of_labware": "与耗材顶部对齐", "all_modules_and_labware_from_protocol": "{{protocol_name}}协议中使用的所有模块与耗材 ", + "applied_location_offset_adjusted": "已调整位置校准数据", + "applied_location_offsets": "已应用的位置校准数据", "applied_offset_data": "已应用的耗材校准数据", "apply_offset_data": "应用耗材校准数据", "apply_offsets": "应用校准数据", "attach_probe": "安装校准探头", "backmost": "最靠后的", "calibration_probe": "校准探头", + "calibration_probe_not_detected": "未检测到校准探头", + "cancel": "取消", + "changing_default_not_update_hardcoded": "更改默认校准数据不会自动更新代码内的校准数据", "check_item_in_location": "检查{{location}}上的{{item}}", "check_labware_in_slot_title": "检查板位{{slot}}中的耗材{{labware_display_name}}", "check_remaining_labware_with_primary_pipette_section": "使用{{primary_mount}}移液器和吸头检查剩余耗材", "check_tip_location": "A1的吸头尖端", "check_well_location": "耗材上的A1孔", + "clear_deck_all_lw_all_modules_from": "清除所有甲板位上的耗材和 {{slot}}上的所有模块", + "clear_deck_all_lw_leave_modules": "轻触所有甲板位上的耗材,保留模块不动", "cli_ssh": "命令行界面 (SSH)", "close_and_apply_offset_data": "关闭并保存耗材校准数据", + "complete": "完成", + "confirm": "确认", "confirm_detached": "确认移除", + "confirm_go_back_without_saving": "请问您确定要不保存并返回耗材列表吗?", "confirm_pick_up_tip_modal_title": "移液器是否成功拾取了吸头?", "confirm_pick_up_tip_modal_try_again_text": "否,重试", + "confirm_placement": "确认放置", "confirm_position_and_move": "确认位置,移动到板位{{next_slot}}", "confirm_position_and_pick_up_tip": "确认位置,取吸头", "confirm_position_and_return_tip": "确认位置,将吸头返回至板位{{next_slot}}并复位", + "confirm_removal": "确认移除", + "continue": "继续", + "default": "默认", + "default_labware_offset": "默认耗材校准数据", + "default_location_offset_added": "添加了默认耗材校准数据", + "default_location_offset_adjusted": "已调整默认耗材校准数据", + "default_offset_description": "除非您调整校准数据,否则放置的耗材都将使用默认数据。", "detach_probe": "移除校准探头", "ensure_nozzle_position_desktop": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请使用下方的控制按键或键盘微调移液器直至完全对齐。", "ensure_nozzle_position_odd": "请检查并确认{{tip_type}}位于{{item_location}}正上方并水平对齐。如果位置不正确,请点击移动移液器,然后微调移液器直至完全对齐。", + "ensure_probe_attached": "继续操作之前请确保其已正确连接。", + "ensure_probe_position_desktop": "确保校准探头居中并与 {{item_location}}齐平。否则,请使用下面的操作台或键盘来移动移液器,直到其正确对齐。", + "ensure_probe_position_odd": "确保校准探头居中并与 {{item_location}}齐平。否则,请点击 移动移液器 然后调整移液器与其正确对齐。", + "ensure_tip_rack_accurately_placed": "确保吸头盒按照上述步骤准确放置在甲板位中,防止损坏耗材。", + "exit": "退出", "exit_screen_confirm_exit": "不保存耗材校准数据并退出", "exit_screen_go_back": "返回耗材位置校准", "exit_screen_subtitle": "如果您现在退出,所有耗材校准数据都将不保留,且无法恢复。", "exit_screen_title": "确定要在完成耗材位置校准前退出?", + "failed_to_save_final_position": "保存最终位置失败", "get_labware_offset_data": "获取耗材校准数据", + "go_back": "返回", + "go_back_confirmation": "您确定要不保存并返回耗材列表吗?", + "hardcoded": "代码", + "hardcoded_offsets_changed_in_python": "必须在 Python 协议中更改代码耗材校准数据", + "install_probe_1ch": "将校准探针从存放位置取出,确保套环已完全解锁。将探针牢牢地安装到移液器喷嘴上,直至完全卡住。旋转套环,锁定探针。轻触探头,确认已完全固定。探头应该绑定牢固。", + "install_probe_8ch": "将校准探头从存放位置取出,确保套环完全解锁。将探头安装到 最后面 移液器喷嘴处,往上尽可能地压紧。旋转套环,锁定探头。轻触探头,确认已完全固定。探头应该绑定牢固。", + "install_probe_96ch": "将校准探头从存放位置取出,确保套环完全解锁。将探头安装到 A1(左后角) 移液器喷嘴处,往上尽可能地压紧。旋转套环,锁定探头。轻触探头,确认已完全固定。探头应该绑定牢固。", "jog_controls_adjustment": "需要进行调整吗?", + "jog_too_far": "移动过远?", "jupyter_notebook": "Jupyter Notebook", "labware": "耗材", + "labware_default_offset_added": "添加了默认校准数据{{labware}} ", + "labware_default_offset_updated": "{{labware}} 默认校准数据已更新", "labware_display_location_text": "甲板板位{{slot}}", "labware_offset": "耗材校准数据", "labware_offset_data": "耗材校准数据", "labware_offsets_deleted_warning": "一旦开始耗材位置校准,之前创建的耗材校准数据将会丢失。", + "labware_offsets_saved": "{{labware}} 校准数据已保存", "labware_offsets_summary_labware": "耗材", "labware_offsets_summary_location": "位置", "labware_offsets_summary_offset": "耗材校准数据", @@ -59,25 +101,49 @@ "labware_step_detail_modal_nozzle_or_tip_image_3_text": "如遇觉得难以判断,请在吸嘴与吸头之间放入一张常规纸张。当这张纸能刚好卡在两者之间时,可确认高度位置。", "labware_step_detail_tiprack": "移液器吸嘴应居中于{{tiprack_name}}的A1位置上方,并且与吸头顶部水平对齐。", "labware_step_detail_tiprack_plural": "移液器吸嘴应位于{{tiprack_name}}第一列正上方并居中对齐,并且与吸头顶部水平对齐。", + "labware_type": "耗材类型", "learn_more": "了解更多", + "legacy_calibration_probe": "校准探头", + "legacy_clear_all_slots": "清除所有甲板位上的耗材,保留模块在原位", + "legacy_clear_all_slots_odd": "清除所有甲板位上的耗材", + "legacy_install_probe": "将校准探头从存放位置取出,确保套环已完全解锁。将探头安装到 {{location}} 移液器喷嘴处,往上尽可能地压紧。旋转套环,锁定探头。轻触探头,确认已完全固定。", + "legacy_labware_offset_data": "耗材校准数据", + "legacy_labware_position_check_description": "耗材校准是一种引导式工作流程,用于校准甲板位上的耗材,以提高协议运行的精确度。耗材校准首先校准吸头架位置,然后校准协议中使用的所有其他耗材。", + "legacy_no_offset_data": "无可用校准数据", "location": "位置", + "location_header": "位置", + "lpc_complete": "耗材校准完成", "lpc_complete_summary_screen_heading": "耗材位置校准完成", + "manual": "手动地", + "modify_hardcoded_offsets_in_protocol": "代码校准数据必须要在您的Python协议文件中进行修改", "module_display_location_text": "{{moduleName}}位于甲板板位{{slot}}", "module_in_slot": "{{module}}位于板位{{slot}}", + "move_gantry_to_front": "将龙门架移至前端", "move_pipette": "移动移液器", "move_to_a1_position": "将移液器移到A1位置并对齐", "moving_to_slot_title": "正在移动到板位{{slot}}", + "need_help": "请问需要帮助吗?", "new_labware_offset_data": "新的耗材校准数据", + "next_place_a_full_tip_rack_in_location": "接下来,放置 完整的 {{tip_rack}}{{location}}", + "next_place_labware_in_location": "接下来,放置 {{labware}}{{location}}", "ninety_six_probe_location": "A1(左上角)", "no_labware_offsets": "无耗材校准数据", "no_offset_data": "没有可用的校准数据", "no_offset_data_available": "没有可用的耗材校准数据", "no_offset_data_on_robot": "这轮运行中此工作站没有可用的耗材校准数据。", + "not_applicable": "N/A", + "num_missing_offsets": "{{num}} 缺失校准数据", + "num_offsets": "{{num}} 校准数据", + "offset_values": "X {{x}},Y {{y}},Z {{z}}", "offsets": "校准数据", + "offsets_already_applied": "耗材校准数据已应用", + "one_missing_offset": "缺少 1 个校准数据", + "one_offset": "1 个校准数据", "pick_up_tip_from_rack_in_location": "从位于{{location}}的吸头盒上拾取吸头", "picking_up_tip_title": "在板位{{slot}}拾取吸头", "pipette_nozzle": "离您最远的移液器吸嘴", "place_a_full_tip_rack_in_location": "将装满吸头的{{tip_rack}}放入{{location}}", + "place_a_full_tip_rack_in_location_96ch_default": "放置完整的 {{tip_rack}}{{location}} ,不要放置吸头架适配器", "place_labware_in_adapter_in_location": "在{{location}}先放置{{adapter}},再放置{{labware}}", "place_labware_in_location": "将{{labware}}放入{{location}}", "place_modules": "在甲板上放置模块", @@ -88,6 +154,8 @@ "remove_calibration_probe": "移除校准探头", "remove_probe": "将校准探头解锁,将其从吸嘴上拆下,并放回存储位置。", "remove_probe_before_exit": "退出前请移除校准探头", + "remove_probe_before_exiting_error": "退出前,请先移除校准探头。然后,重新启动耗材校准,然后继续。", + "reset_to_default": "恢复默认设置", "return_tip_rack_to_location": "将吸头盒放回{{location}}", "return_tip_section": "放回吸头", "returning_tip_title": "正在板位{{slot}}放回吸头", @@ -96,17 +164,30 @@ "robot_has_offsets_from_previous_runs": "此工作站具有本协议中所用耗材的校准数据。如果您应用了这些校准数据,仍可通过耗材位置校准程序进行调整。", "robot_in_motion": "工作站正在运行,请远离。", "run": "运行", + "save": "保存", "secondary_pipette_tipracks_section": "使用{{secondary_mount}}移液器检查吸头盒", "see_how_offsets_work": "了解耗材校准的工作原理", + "select_labware_to_view_data": "选择一个耗材来查看其存储的校准数据。", + "skip": "跳过", "slot": "板位{{slotName}}", + "slot_applied_location_offset_updated": "{{slot}} 的位置数据已更新", + "slot_in_module_applied_location_offset_updated": "在{{slot}} 上的 {{module}} 位置校准数据已更新", "slot_location": "板位位置", "slot_name": "板位{{slotName}}", + "something_went_wrong": "出了点问题", + "specific_slots_can_be_adjusted": "可以根据需要调整特定的甲板位置", + "start_over": "重新开始", "start_position_check": "开始耗材位置校准程序,移至板位{{initial_labware_slot}}", + "store_probe": "退出之前,请解锁校准探头,将其从移液器下方取出,然后将其放回存放位置。", "stored_offset_data": "应用已存储的耗材校准数据?", "stored_offsets_for_this_protocol": "适用于本协议的已存储耗材校准数据", "table_view": "表格视图", "tip_rack": "吸头盒", + "total_offsets": "全部校准数据", + "try_again": "再次尝试", + "unsaved_changes_will_be_lost": "未保存的更改将会丢失", "view_current_offsets": "查看当前校准数据", "view_data": "查看数据", + "view_labware_list": "查看耗材列表", "what_is_labware_offset_data": "什么是耗材校准数据?" } diff --git a/app/src/assets/localization/zh/protocol_setup.json b/app/src/assets/localization/zh/protocol_setup.json index bd936d8540a1..47383f748bf6 100644 --- a/app/src/assets/localization/zh/protocol_setup.json +++ b/app/src/assets/localization/zh/protocol_setup.json @@ -82,7 +82,6 @@ "deck_conflict_info": "通过移除位置 {{cutout}} 中的 {{currentFixture}} 来更新甲板配置。从甲板配置中移除对应装置或更新协议。", "deck_conflict_info_thermocycler": "通过移除位置 A1 和 B1 中的固定装置来更新甲板配置。从甲板配置中移除对应装置或更新协议。", "deck_hardware": "甲板硬件", - "deck_hardware_ready": "甲板硬件准备", "deck_map": "甲板布局图", "default_values": "默认值", "download_files": "下载文件", @@ -112,6 +111,7 @@ "instrument_calibrations_missing": "缺少{{count}}个校准", "instrument_calibrations_missing_plural": "缺少{{count}}个校准", "instruments": "硬件", + "instruments_attached": "已附仪器", "instruments_connected": "已连接{{count}}个硬件", "instruments_connected_plural": "已连接{{count}}个硬件", "jupyter_notebook": "Jupyter Notebook", @@ -183,8 +183,10 @@ "module_setup_step_title": "甲板硬件", "module_slot_location": "{{slotName}}号板位,{{moduleName}}", "modules": "模块", + "modules_and_fixtures_ready": "模块和固定装置已准备就绪", "modules_connected": "连接了{{count}}个模块", "modules_connected_plural": "连接了{{count}}个模块", + "modules_ready": "模块已准备就绪", "modules_setup_step_title": "模块设置", "mount": "{{mount}}安装支架", "mount_title": "{{mount}}安装支架:", @@ -240,6 +242,7 @@ "on_adapter": "在{{adapterName}}上", "on_adapter_in_mod": "在{{moduleName}}中的{{adapterName}}上", "on_deck": "在甲板上", + "one_missing_offset": "缺少 1 个校准数据", "opening": "打开中...", "parameters": "参数", "pipette_mismatch": "移液器型号不匹配。", From 6d39eec78e15762f3b8b5f7cc12384cb908fbc96 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 25 Apr 2025 16:29:13 -0400 Subject: [PATCH 6/7] fix(shared-data): remove extra flex (#18186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2025-04-25 at 4 05 35 PM Closes RQA-4148 --- .../definitions/2/ev_resin_tips_flex_96_tiprack_adapter/1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-data/labware/definitions/2/ev_resin_tips_flex_96_tiprack_adapter/1.json b/shared-data/labware/definitions/2/ev_resin_tips_flex_96_tiprack_adapter/1.json index 25d2e12b366e..edbb53416762 100644 --- a/shared-data/labware/definitions/2/ev_resin_tips_flex_96_tiprack_adapter/1.json +++ b/shared-data/labware/definitions/2/ev_resin_tips_flex_96_tiprack_adapter/1.json @@ -8,7 +8,7 @@ ] }, "metadata": { - "displayName": "Opentrons Flex EV Resin Tips Tall Adapter for Third-party Evotips in Flex 96 Tip Rack Adapter", + "displayName": "Opentrons Flex EV Resin Tips Tall Adapter for Third-party Evotips in 96 Tip Rack Adapter", "displayCategory": "adapter", "displayVolumeUnits": "µL", "tags": [] From 7b5c092267c5d73a66cd944cfb80134936c27d94 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Fri, 25 Apr 2025 16:47:04 -0400 Subject: [PATCH 7/7] fix(api): correct the behavior of blowout when using a .meniscus target (#18185) # Overview Due to the way that the movement logic of blow out is handled, it expects an absolute point. So when a location is a meniscus relative type, then it miscalculates the "delta" of the point. The delta it computes is -1 * the absolute point of the well.top. To correct this if the location passed into blowout is a meniscus relative location, we take the offset from that and apply it to the current well height instead. this gives us the correct absolute point. The other option would have been to hook up all the layers to handle LiquidHandlingWellLocation types in addition to WellLocation but that has its own headaches since its not actually liquid handling, and there is no operational volume. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment Using blowout with a meniscus relative location without a liquid-probe or load liquid will now fail analysis. there is no interaction with a "minimum liquid height" so there is no worries there. even with a liquid height of 0 --- .../protocol_api/instrument_context.py | 15 ++++- .../protocol_api/test_instrument_context.py | 55 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index e9b6c776ab96..4975c848b9b8 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -606,7 +606,20 @@ def blow_out( "Blow_out being performed on a tiprack. " "Please re-check your code" ) - move_to_location = target.location or target.well.top() + if target.location: + # because the lower levels of blowout don't handle LiquidHandlingWellLocation and + # there is no "operation_volume" for blowout we need to convert the relative location + # given with a .meniscus to an absolute point. To maintain the meniscus behavior + # we can just add the offset to the current liquid height. + if target.location.meniscus_tracking: + move_to_location = target.well.bottom( + target.well.current_liquid_height() # type: ignore [arg-type] + + target.location.point.z + ) + else: + move_to_location = target.location + else: + move_to_location = target.well.top() well = target.well elif isinstance(target, validation.PointTarget): move_to_location = target.location diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 8bdeb9adf98a..9ac1aae69fd6 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -59,7 +59,13 @@ from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute -from opentrons.types import Location, Mount, Point, NozzleMapInterface +from opentrons.types import ( + Location, + Mount, + Point, + NozzleMapInterface, + MeniscusTrackingTarget, +) from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps from opentrons_shared_data.errors.exceptions import ( @@ -553,6 +559,53 @@ def test_blow_out_to_well_location( ) +def test_blow_out_to_well_meniscus_location( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should blow out to a well location.""" + liquid_height = 10.0 + well_bottom = Point(2, 2, 2) + relative_height = 3 + mock_well = decoy.mock(cls=Well) + input_location_absolute = Location( + point=well_bottom + Point(0, 0, liquid_height) + Point(0, 0, relative_height), + labware=mock_well, + ) + decoy.when(mock_well.current_liquid_height()).then_return(liquid_height) + decoy.when(mock_well.bottom(liquid_height + relative_height)).then_return( + Location( + point=well_bottom + Point(0, 0, liquid_height + relative_height), + labware=mock_well, + ) + ) + + input_location = Location( + point=Point(0, 0, relative_height), + labware=mock_well, + _meniscus_tracking=MeniscusTrackingTarget.END, + ) + last_location = Location(point=Point(9, 9, 9), labware=None) + decoy.when(mock_instrument_core.get_mount()).then_return(Mount.RIGHT) + + decoy.when(mock_protocol_core.get_last_location(Mount.RIGHT)).then_return( + last_location + ) + decoy.when( + mock_validation.validate_location( + location=input_location, last_location=last_location + ) + ).then_return(WellTarget(well=mock_well, location=input_location, in_place=False)) + + subject.blow_out(location=input_location) + + mock_instrument_core.blow_out( + location=input_location_absolute, well_core=mock_well._core, in_place=False + ) + + def test_blow_out_to_location( decoy: Decoy, mock_instrument_core: InstrumentCore,