diff --git a/api/src/opentrons/protocol_api/_liquid.py b/api/src/opentrons/protocol_api/_liquid.py index 75e2c6fb6f2..0cb104099eb 100644 --- a/api/src/opentrons/protocol_api/_liquid.py +++ b/api/src/opentrons/protocol_api/_liquid.py @@ -1,13 +1,19 @@ +from __future__ import annotations + from dataclasses import dataclass -from typing import Optional, Sequence +from typing import Optional, Dict from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, +) + +from ._liquid_properties import ( AspirateProperties, SingleDispenseProperties, MultiDispenseProperties, - ByPipetteSetting, - ByTipTypeSetting, + build_aspirate_properties, + build_single_dispense_properties, + build_multi_dispense_properties, ) @@ -59,16 +65,29 @@ class LiquidClass: _name: str _display_name: str - _by_pipette_setting: Sequence[ByPipetteSetting] + _by_pipette_setting: Dict[str, Dict[str, TransferProperties]] @classmethod def create(cls, liquid_class_definition: LiquidClassSchemaV1) -> "LiquidClass": """Liquid class factory method.""" + by_pipette_settings: Dict[str, Dict[str, TransferProperties]] = {} + for by_pipette in liquid_class_definition.byPipette: + tip_settings: Dict[str, TransferProperties] = {} + for tip_type in by_pipette.byTipType: + tip_settings[tip_type.tiprack] = TransferProperties( + _aspirate=build_aspirate_properties(tip_type.aspirate), + _dispense=build_single_dispense_properties(tip_type.singleDispense), + _multi_dispense=build_multi_dispense_properties( + tip_type.multiDispense + ), + ) + by_pipette_settings[by_pipette.pipetteModel] = tip_settings + return cls( _name=liquid_class_definition.liquidClassName, _display_name=liquid_class_definition.displayName, - _by_pipette_setting=liquid_class_definition.byPipette, + _by_pipette_setting=by_pipette_settings, ) @property @@ -81,26 +100,16 @@ def display_name(self) -> str: def get_for(self, pipette: str, tiprack: str) -> TransferProperties: """Get liquid class transfer properties for the specified pipette and tip.""" - settings_for_pipette: Sequence[ByPipetteSetting] = [ - pip_setting - for pip_setting in self._by_pipette_setting - if pip_setting.pipetteModel == pipette - ] - if len(settings_for_pipette) == 0: + try: + settings_for_pipette = self._by_pipette_setting[pipette] + except KeyError: raise ValueError( f"No properties found for {pipette} in {self._name} liquid class" ) - settings_for_tip: Sequence[ByTipTypeSetting] = [ - tip_setting - for tip_setting in settings_for_pipette[0].byTipType - if tip_setting.tiprack == tiprack - ] - if len(settings_for_tip) == 0: + try: + transfer_properties = settings_for_pipette[tiprack] + except KeyError: raise ValueError( f"No properties found for {tiprack} in {self._name} liquid class" ) - return TransferProperties( - _aspirate=settings_for_tip[0].aspirate, - _dispense=settings_for_tip[0].singleDispense, - _multi_dispense=settings_for_tip[0].multiDispense, - ) + return transfer_properties diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py new file mode 100644 index 00000000000..f0dd0adfe92 --- /dev/null +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -0,0 +1,503 @@ +from dataclasses import dataclass +from typing import Optional, Dict, Sequence + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + AspirateProperties as SharedDataAspirateProperties, + SingleDispenseProperties as SharedDataSingleDispenseProperties, + MultiDispenseProperties as SharedDataMultiDispenseProperties, + DelayProperties as SharedDataDelayProperties, + TouchTipProperties as SharedDataTouchTipProperties, + MixProperties as SharedDataMixProperties, + BlowoutProperties as SharedDataBlowoutProperties, + Submerge as SharedDataSubmerge, + RetractAspirate as SharedDataRetractAspirate, + RetractDispense as SharedDataRetractDispense, + BlowoutLocation, + PositionReference, + Coordinate, +) + +# TODO replace this with a class that can extrapolate given volumes to the correct float, +# also figure out how we want people to be able to set this +LiquidHandlingPropertyByVolume = Dict[str, float] + + +@dataclass +class DelayProperties: + + _enabled: bool + _duration: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + # TODO insert bool validation here + if enable and self._duration is None: + raise ValueError("duration must be set before enabling delay.") + self._enabled = enable + + @property + def duration(self) -> Optional[float]: + return self._duration + + @duration.setter + def duration(self, new_duration: float) -> None: + # TODO insert positive float validation here + self._duration = new_duration + + +@dataclass +class TouchTipProperties: + + _enabled: bool + _z_offset: Optional[float] + _mm_to_edge: Optional[float] + _speed: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + # TODO insert bool validation here + if enable and ( + self._z_offset is None or self._mm_to_edge is None or self._speed is None + ): + raise ValueError( + "z_offset, mm_to_edge and speed must be set before enabling touch tip." + ) + self._enabled = enable + + @property + def z_offset(self) -> Optional[float]: + return self._z_offset + + @z_offset.setter + def z_offset(self, new_offset: float) -> None: + # TODO validation for float + self._z_offset = new_offset + + @property + def mm_to_edge(self) -> Optional[float]: + return self._mm_to_edge + + @mm_to_edge.setter + def mm_to_edge(self, new_mm: float) -> None: + # TODO validation for float + self._z_offset = new_mm + + @property + def speed(self) -> Optional[float]: + return self._speed + + @speed.setter + def speed(self, new_speed: float) -> None: + # TODO insert positive float validation here + self._speed = new_speed + + +@dataclass +class MixProperties: + + _enabled: bool + _repetitions: Optional[int] + _volume: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + # TODO insert bool validation here + if enable and (self._repetitions is None or self._volume is None): + raise ValueError("repetitions and volume must be set before enabling mix.") + self._enabled = enable + + @property + def repetitions(self) -> Optional[int]: + return self._repetitions + + @repetitions.setter + def repetitions(self, new_repetitions: int) -> None: + # TODO validations for positive int + self._repetitions = new_repetitions + + @property + def volume(self) -> Optional[float]: + return self._volume + + @volume.setter + def volume(self, new_volume: float) -> None: + # TODO validations for volume float + self._volume = new_volume + + +@dataclass +class BlowoutProperties: + + _enabled: bool + _location: Optional[BlowoutLocation] + _flow_rate: Optional[float] + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, enable: bool) -> None: + # TODO insert bool validation here + if enable and (self._location is None or self._flow_rate is None): + raise ValueError( + "location and flow_rate must be set before enabling blowout." + ) + self._enabled = enable + + @property + def location(self) -> Optional[BlowoutLocation]: + return self._location + + @location.setter + def location(self, new_location: str) -> None: + # TODO blowout location validation + self._location = BlowoutLocation(new_location) + + @property + def flow_rate(self) -> Optional[float]: + return self._flow_rate + + @flow_rate.setter + def flow_rate(self, new_flow_rate: float) -> None: + # TODO validations for positive float + self._flow_rate = new_flow_rate + + +@dataclass +class SubmergeRetractCommon: + + _position_reference: PositionReference + _offset: Coordinate + _speed: float + _delay: DelayProperties + + @property + def position_reference(self) -> PositionReference: + return self._position_reference + + @position_reference.setter + def position_reference(self, new_position: str) -> None: + # TODO validation for position reference + self._position_reference = PositionReference(new_position) + + @property + def offset(self) -> Coordinate: + return self._offset + + @offset.setter + def offset(self, new_offset: Sequence[float]) -> None: + # TODO validate valid coordinates + self._offset = Coordinate(x=new_offset[0], y=new_offset[1], z=new_offset[2]) + + @property + def speed(self) -> float: + return self._speed + + @speed.setter + def speed(self, new_speed: float) -> None: + # TODO insert positive float validation here + self._speed = new_speed + + @property + def delay(self) -> DelayProperties: + return self._delay + + +@dataclass +class Submerge(SubmergeRetractCommon): + ... + + +@dataclass +class RetractAspirate(SubmergeRetractCommon): + + _air_gap_by_volume: LiquidHandlingPropertyByVolume + _touch_tip: TouchTipProperties + + @property + def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._air_gap_by_volume + + @property + def touch_tip(self) -> TouchTipProperties: + return self._touch_tip + + +@dataclass +class RetractDispense(SubmergeRetractCommon): + + _air_gap_by_volume: LiquidHandlingPropertyByVolume + _touch_tip: TouchTipProperties + _blowout: BlowoutProperties + + @property + def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._air_gap_by_volume + + @property + def touch_tip(self) -> TouchTipProperties: + return self._touch_tip + + @property + def blowout(self) -> BlowoutProperties: + return self._blowout + + +@dataclass +class BaseLiquidHandlingProperties: + + _submerge: Submerge + _position_reference: PositionReference + _offset: Coordinate + _flow_rate_by_volume: LiquidHandlingPropertyByVolume + _delay: DelayProperties + + @property + def submerge(self) -> Submerge: + return self._submerge + + @property + def position_reference(self) -> PositionReference: + return self._position_reference + + @position_reference.setter + def position_reference(self, new_position: str) -> None: + # TODO validation for position reference + self._position_reference = PositionReference(new_position) + + @property + def offset(self) -> Coordinate: + return self._offset + + @offset.setter + def offset(self, new_offset: Sequence[float]) -> None: + # TODO validate valid coordinates + self._offset = Coordinate(x=new_offset[0], y=new_offset[1], z=new_offset[2]) + + @property + def flow_rate_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._flow_rate_by_volume + + @property + def delay(self) -> DelayProperties: + return self._delay + + +@dataclass +class AspirateProperties(BaseLiquidHandlingProperties): + + _retract: RetractAspirate + _pre_wet: bool + _mix: MixProperties + + @property + def pre_wet(self) -> bool: + return self._pre_wet + + @pre_wet.setter + def pre_wet(self, new_setting: bool) -> None: + # TODO boolean validation + self._pre_wet = new_setting + + @property + def retract(self) -> RetractAspirate: + return self._retract + + @property + def mix(self) -> MixProperties: + return self._mix + + +@dataclass +class SingleDispenseProperties(BaseLiquidHandlingProperties): + + _retract: RetractDispense + _push_out_by_volume: LiquidHandlingPropertyByVolume + _mix: MixProperties + + @property + def push_out_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._push_out_by_volume + + @property + def retract(self) -> RetractDispense: + return self._retract + + @property + def mix(self) -> MixProperties: + return self._mix + + +@dataclass +class MultiDispenseProperties(BaseLiquidHandlingProperties): + + _retract: RetractDispense + _conditioning_by_volume: LiquidHandlingPropertyByVolume + _disposal_by_volume: LiquidHandlingPropertyByVolume + + @property + def retract(self) -> RetractDispense: + return self._retract + + @property + def conditioning_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._conditioning_by_volume + + @property + def disposal_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._disposal_by_volume + + +def _build_delay_properties( + delay_properties: SharedDataDelayProperties, +) -> DelayProperties: + if delay_properties.params is not None: + duration = delay_properties.params.duration + else: + duration = None + return DelayProperties(_enabled=delay_properties.enable, _duration=duration) + + +def _build_touch_tip_properties( + touch_tip_properties: SharedDataTouchTipProperties, +) -> TouchTipProperties: + if touch_tip_properties.params is not None: + z_offset = touch_tip_properties.params.zOffset + mm_to_edge = touch_tip_properties.params.mmToEdge + speed = touch_tip_properties.params.speed + else: + z_offset = None + mm_to_edge = None + speed = None + return TouchTipProperties( + _enabled=touch_tip_properties.enable, + _z_offset=z_offset, + _mm_to_edge=mm_to_edge, + _speed=speed, + ) + + +def _build_mix_properties( + mix_properties: SharedDataMixProperties, +) -> MixProperties: + if mix_properties.params is not None: + repetitions = mix_properties.params.repetitions + volume = mix_properties.params.volume + else: + repetitions = None + volume = None + return MixProperties( + _enabled=mix_properties.enable, _repetitions=repetitions, _volume=volume + ) + + +def _build_blowout_properties( + blowout_properties: SharedDataBlowoutProperties, +) -> BlowoutProperties: + if blowout_properties.params is not None: + location = blowout_properties.params.location + flow_rate = blowout_properties.params.flowRate + else: + location = None + flow_rate = None + return BlowoutProperties( + _enabled=blowout_properties.enable, _location=location, _flow_rate=flow_rate + ) + + +def _build_submerge( + submerge_properties: SharedDataSubmerge, +) -> Submerge: + return Submerge( + _position_reference=submerge_properties.positionReference, + _offset=submerge_properties.offset, + _speed=submerge_properties.speed, + _delay=_build_delay_properties(submerge_properties.delay), + ) + + +def _build_retract_aspirate( + retract_aspirate: SharedDataRetractAspirate, +) -> RetractAspirate: + return RetractAspirate( + _position_reference=retract_aspirate.positionReference, + _offset=retract_aspirate.offset, + _speed=retract_aspirate.speed, + _air_gap_by_volume=retract_aspirate.airGapByVolume, + _touch_tip=_build_touch_tip_properties(retract_aspirate.touchTip), + _delay=_build_delay_properties(retract_aspirate.delay), + ) + + +def _build_retract_dispense( + retract_dispense: SharedDataRetractDispense, +) -> RetractDispense: + return RetractDispense( + _position_reference=retract_dispense.positionReference, + _offset=retract_dispense.offset, + _speed=retract_dispense.speed, + _air_gap_by_volume=retract_dispense.airGapByVolume, + _blowout=_build_blowout_properties(retract_dispense.blowout), + _touch_tip=_build_touch_tip_properties(retract_dispense.touchTip), + _delay=_build_delay_properties(retract_dispense.delay), + ) + + +def build_aspirate_properties( + aspirate_properties: SharedDataAspirateProperties, +) -> AspirateProperties: + return AspirateProperties( + _submerge=_build_submerge(aspirate_properties.submerge), + _retract=_build_retract_aspirate(aspirate_properties.retract), + _position_reference=aspirate_properties.positionReference, + _offset=aspirate_properties.offset, + _flow_rate_by_volume=aspirate_properties.flowRateByVolume, + _pre_wet=aspirate_properties.preWet, + _mix=_build_mix_properties(aspirate_properties.mix), + _delay=_build_delay_properties(aspirate_properties.delay), + ) + + +def build_single_dispense_properties( + single_dispense_properties: SharedDataSingleDispenseProperties, +) -> SingleDispenseProperties: + return SingleDispenseProperties( + _submerge=_build_submerge(single_dispense_properties.submerge), + _retract=_build_retract_dispense(single_dispense_properties.retract), + _position_reference=single_dispense_properties.positionReference, + _offset=single_dispense_properties.offset, + _flow_rate_by_volume=single_dispense_properties.flowRateByVolume, + _mix=_build_mix_properties(single_dispense_properties.mix), + _push_out_by_volume=single_dispense_properties.pushOutByVolume, + _delay=_build_delay_properties(single_dispense_properties.delay), + ) + + +def build_multi_dispense_properties( + multi_dispense_properties: Optional[SharedDataMultiDispenseProperties], +) -> Optional[MultiDispenseProperties]: + if multi_dispense_properties is None: + return None + return MultiDispenseProperties( + _submerge=_build_submerge(multi_dispense_properties.submerge), + _retract=_build_retract_dispense(multi_dispense_properties.retract), + _position_reference=multi_dispense_properties.positionReference, + _offset=multi_dispense_properties.offset, + _flow_rate_by_volume=multi_dispense_properties.flowRateByVolume, + _conditioning_by_volume=multi_dispense_properties.conditioningByVolume, + _disposal_by_volume=multi_dispense_properties.disposalByVolume, + _delay=_build_delay_properties(multi_dispense_properties.delay), + ) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 7b549fc035d..9ccaac498f0 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -1763,7 +1763,7 @@ def test_define_liquid_class( ) -> None: """It should create a LiquidClass and cache the definition.""" expected_liquid_class = LiquidClass( - _name="water1", _display_name="water 1", _by_pipette_setting=[] + _name="water1", _display_name="water 1", _by_pipette_setting={} ) decoy.when(liquid_classes.load_definition("water")).then_return( minimal_liquid_class_def1 diff --git a/api/tests/opentrons/protocol_api/test_liquid_class.py b/api/tests/opentrons/protocol_api/test_liquid_class.py index 48f3788f496..be0b432e32f 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class.py @@ -12,7 +12,7 @@ def test_create_liquid_class( ) -> None: """It should create a LiquidClass from provided definition.""" assert LiquidClass.create(minimal_liquid_class_def1) == LiquidClass( - _name="water1", _display_name="water 1", _by_pipette_setting=[] + _name="water1", _display_name="water 1", _by_pipette_setting={} ) @@ -22,7 +22,7 @@ def test_get_for_pipette_and_tip( """It should get the properties for the specified pipette and tip.""" liq_class = LiquidClass.create(minimal_liquid_class_def2) result = liq_class.get_for("p20_single_gen2", "opentrons_96_tiprack_20ul") - assert result.aspirate.flowRateByVolume == {"default": 50, "10": 40, "20": 30} + assert result.aspirate.flow_rate_by_volume == {"default": 50, "10": 40, "20": 30} def test_get_for_raises_for_incorrect_pipette_or_tip( diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py new file mode 100644 index 00000000000..b1699701f3c --- /dev/null +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -0,0 +1,175 @@ +"""Tests for LiquidClass properties and related functions.""" + +from opentrons_shared_data import load_shared_data +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, + Coordinate, +) + +from opentrons.protocol_api._liquid_properties import ( + build_aspirate_properties, + build_single_dispense_properties, + build_multi_dispense_properties, +) + + +def test_build_aspirate_settings() -> None: + """It should convert the shared data aspirate settings to the PAPI type.""" + fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) + aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate + + aspirate_properties = build_aspirate_properties(aspirate_data) + + assert aspirate_properties.submerge.position_reference.value == "liquid-meniscus" + assert aspirate_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) + assert aspirate_properties.submerge.speed == 100 + assert aspirate_properties.submerge.delay.enabled is True + assert aspirate_properties.submerge.delay.duration == 1.5 + + assert aspirate_properties.retract.position_reference.value == "well-top" + assert aspirate_properties.retract.offset == Coordinate(x=0, y=0, z=5) + assert aspirate_properties.retract.speed == 100 + assert aspirate_properties.retract.air_gap_by_volume == { + "default": 2, + "5": 3, + "10": 4, + } + assert aspirate_properties.retract.touch_tip.enabled is True + assert aspirate_properties.retract.touch_tip.z_offset == 2 + assert aspirate_properties.retract.touch_tip.mm_to_edge == 1 + assert aspirate_properties.retract.touch_tip.speed == 50 + assert aspirate_properties.retract.delay.enabled is True + assert aspirate_properties.retract.delay.duration == 1 + + assert aspirate_properties.position_reference.value == "well-bottom" + assert aspirate_properties.offset == Coordinate(x=0, y=0, z=-5) + assert aspirate_properties.flow_rate_by_volume == { + "default": 50, + "10": 40, + "20": 30, + } + assert aspirate_properties.pre_wet is True + assert aspirate_properties.mix.enabled is True + assert aspirate_properties.mix.repetitions == 3 + assert aspirate_properties.mix.volume == 15 + assert aspirate_properties.delay.enabled is True + assert aspirate_properties.delay.duration == 2 + + +def test_build_single_dispense_settings() -> None: + """It should convert the shared data single dispense settings to the PAPI type.""" + fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) + single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense + + single_dispense_properties = build_single_dispense_properties(single_dispense_data) + + assert ( + single_dispense_properties.submerge.position_reference.value + == "liquid-meniscus" + ) + assert single_dispense_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) + assert single_dispense_properties.submerge.speed == 100 + assert single_dispense_properties.submerge.delay.enabled is True + assert single_dispense_properties.submerge.delay.duration == 1.5 + + assert single_dispense_properties.retract.position_reference.value == "well-top" + assert single_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) + assert single_dispense_properties.retract.speed == 100 + assert single_dispense_properties.retract.air_gap_by_volume == { + "default": 2, + "5": 3, + "10": 4, + } + assert single_dispense_properties.retract.touch_tip.enabled is True + assert single_dispense_properties.retract.touch_tip.z_offset == 2 + assert single_dispense_properties.retract.touch_tip.mm_to_edge == 1 + assert single_dispense_properties.retract.touch_tip.speed == 50 + assert single_dispense_properties.retract.blowout.enabled is True + assert single_dispense_properties.retract.blowout.location is not None + assert single_dispense_properties.retract.blowout.location.value == "trash" + assert single_dispense_properties.retract.blowout.flow_rate == 100 + assert single_dispense_properties.retract.delay.enabled is True + assert single_dispense_properties.retract.delay.duration == 1 + + assert single_dispense_properties.position_reference.value == "well-bottom" + assert single_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) + assert single_dispense_properties.flow_rate_by_volume == { + "default": 50, + "10": 40, + "20": 30, + } + assert single_dispense_properties.mix.enabled is True + assert single_dispense_properties.mix.repetitions == 3 + assert single_dispense_properties.mix.volume == 15 + assert single_dispense_properties.push_out_by_volume == { + "default": 5, + "10": 7, + "20": 10, + } + assert single_dispense_properties.delay.enabled is True + assert single_dispense_properties.delay.duration == 2.5 + + +def test_build_multi_dispense_settings() -> None: + """It should convert the shared data multi dispense settings to the PAPI type.""" + fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) + multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense + + assert multi_dispense_data is not None + multi_dispense_properties = build_multi_dispense_properties(multi_dispense_data) + assert multi_dispense_properties is not None + + assert ( + multi_dispense_properties.submerge.position_reference.value == "liquid-meniscus" + ) + assert multi_dispense_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) + assert multi_dispense_properties.submerge.speed == 100 + assert multi_dispense_properties.submerge.delay.enabled is True + assert multi_dispense_properties.submerge.delay.duration == 1.5 + + assert multi_dispense_properties.retract.position_reference.value == "well-top" + assert multi_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) + assert multi_dispense_properties.retract.speed == 100 + assert multi_dispense_properties.retract.air_gap_by_volume == { + "default": 2, + "5": 3, + "10": 4, + } + assert multi_dispense_properties.retract.touch_tip.enabled is True + assert multi_dispense_properties.retract.touch_tip.z_offset == 2 + assert multi_dispense_properties.retract.touch_tip.mm_to_edge == 1 + assert multi_dispense_properties.retract.touch_tip.speed == 50 + assert multi_dispense_properties.retract.blowout.enabled is False + assert multi_dispense_properties.retract.blowout.location is None + assert multi_dispense_properties.retract.blowout.flow_rate is None + assert multi_dispense_properties.retract.delay.enabled is True + assert multi_dispense_properties.retract.delay.duration == 1 + + assert multi_dispense_properties.position_reference.value == "well-bottom" + assert multi_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) + assert multi_dispense_properties.flow_rate_by_volume == { + "default": 50, + "10": 40, + "20": 30, + } + assert multi_dispense_properties.conditioning_by_volume == { + "default": 10, + "5": 5, + } + assert multi_dispense_properties.disposal_by_volume == { + "default": 2, + "5": 3, + } + assert multi_dispense_properties.delay.enabled is True + assert multi_dispense_properties.delay.duration == 1 + + +def test_build_multi_dispense_settings_none( + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should return None if there are no multi dispense properties in the model.""" + transfer_settings = minimal_liquid_class_def2.byPipette[0].byTipType[0] + assert build_multi_dispense_properties(transfer_settings.multiDispense) is None diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 2bedbd5fb6f..2c8e8b158af 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -1227,7 +1227,7 @@ def test_define_liquid_class( ) -> None: """It should create the liquid class definition.""" expected_liquid_class = LiquidClass( - _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting=[] + _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting={} ) decoy.when(mock_core.define_liquid_class("volatile_90")).then_return( expected_liquid_class diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 049edae5c0f..eed90cc2478 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -27,7 +27,7 @@ def test_liquid_class_creation_and_property_fetching( assert ( glycerol_50.get_for( pipette_left.name, tiprack.load_name - ).dispense.flowRateByVolume["default"] + ).dispense.flow_rate_by_volume["default"] == 50 ) assert ( diff --git a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx index 8730c0dc5df..4bbd370c00f 100644 --- a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx +++ b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx @@ -48,7 +48,7 @@ export function ProtocolSectionsContainer(): JSX.Element | null { { stepNumber: INSTRUMENTS_STEP, title: 'instruments_title', - Component: () => , + Component: InstrumentsSection, }, { stepNumber: MODULES_STEP,