From d77329a95970580341c4a5a3e8f089bbaf040110 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:53:42 -0500 Subject: [PATCH 01/52] fix(protocol-designer): fix layout and menu items of SlotOverflowMenu (#16840) This PR fixes the layout and content of SlotOverflowMenu component. It aligns text to the left of the menu button. Also, it always renders 'Add liquid' option, but disables if no labware is present in the slot. Last, it adds a Divider above 'Clear slot' option. Closes RQA-3335 --- components/src/atoms/MenuList/MenuItem.tsx | 2 + .../Designer/DeckSetup/SlotOverflowMenu.tsx | 94 ++++++++----------- .../__tests__/SlotOverflowMenu.test.tsx | 7 +- 3 files changed, 45 insertions(+), 58 deletions(-) diff --git a/components/src/atoms/MenuList/MenuItem.tsx b/components/src/atoms/MenuList/MenuItem.tsx index cd34c7c7f44..94857fc56dd 100644 --- a/components/src/atoms/MenuList/MenuItem.tsx +++ b/components/src/atoms/MenuList/MenuItem.tsx @@ -16,6 +16,8 @@ export const MenuItem = styled.button` padding: ${SPACING.spacing8} ${SPACING.spacing12} ${SPACING.spacing8} ${SPACING.spacing12}; border: ${props => (props.border != null ? props.border : 'inherit')}; + border-radius: ${props => + props.borderRadius != null ? props.borderRadius : 'inherit'}; &:hover { background-color: ${COLORS.blue10}; diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx index 36cd92f4ec6..b45f314f689 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx @@ -1,19 +1,17 @@ import { useTranslation } from 'react-i18next' import { useState } from 'react' -import styled from 'styled-components' import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import { BORDERS, COLORS, - CURSOR_AUTO, - CURSOR_POINTER, DIRECTION_COLUMN, + Divider, Flex, + MenuItem, NO_WRAP, POSITION_ABSOLUTE, RobotCoordsForeignDiv, - SPACING, StyledText, useOnClickOutside, } from '@opentrons/components' @@ -168,7 +166,7 @@ export function SlotOverflowMenu( nestedLabwareOnSlot == null) || nestedLabwareOnSlot != null - const showEditAndLiquidsBtns = + const canRenameLabwareAndEditLiquids = (labwareOnSlot != null && !isLabwareAnAdapter && !isLabwareTiprack && @@ -180,7 +178,7 @@ export function SlotOverflowMenu( : TOP_SLOT_Y_POSITION if (showDuplicateBtn && !ROBOT_BOTTOM_HALF_SLOTS.includes(location)) { - position += showEditAndLiquidsBtns + position += canRenameLabwareAndEditLiquids ? TOP_SLOT_Y_POSITION_ALL_BUTTONS : TOP_SLOT_Y_POSITION_2_BUTTONS } @@ -233,7 +231,7 @@ export function SlotOverflowMenu( e.stopPropagation() }} > - { addEquipment(location) setShowMenuList(false) @@ -244,38 +242,37 @@ export function SlotOverflowMenu( ? t(isOffDeckLocation ? 'add_labware' : 'add_hw_lw') : t(isOffDeckLocation ? 'edit_labware' : 'edit_hw_lw')} - - {showEditAndLiquidsBtns ? ( - <> - { - setShowNickNameModal(true) - e.preventDefault() - e.stopPropagation() - }} - > - - {t('rename_lab')} - - - { - if (nestedLabwareOnSlot != null) { - dispatch(openIngredientSelector(nestedLabwareOnSlot.id)) - } else if (labwareOnSlot != null) { - dispatch(openIngredientSelector(labwareOnSlot.id)) - } - navigate('/liquids') - }} - > - - {selectionHasLiquids ? t('edit_liquid') : t('add_liquid')} - - - + + {canRenameLabwareAndEditLiquids ? ( + { + setShowNickNameModal(true) + e.preventDefault() + e.stopPropagation() + }} + > + + {t('rename_lab')} + + ) : null} + { + if (nestedLabwareOnSlot != null) { + dispatch(openIngredientSelector(nestedLabwareOnSlot.id)) + } else if (labwareOnSlot != null) { + dispatch(openIngredientSelector(labwareOnSlot.id)) + } + navigate('/liquids') + }} + disabled={!canRenameLabwareAndEditLiquids} + > + + {selectionHasLiquids ? t('edit_liquid') : t('add_liquid')} + + {showDuplicateBtn ? ( - { if ( labwareOnSlot != null && @@ -292,9 +289,10 @@ export function SlotOverflowMenu( {t('duplicate')} - + ) : null} - + { if (matchingLabware != null) { @@ -310,7 +308,7 @@ export function SlotOverflowMenu( {t(isOffDeckLocation ? 'clear_labware' : 'clear_slot')} - + ) @@ -337,19 +335,3 @@ export function SlotOverflowMenu( slotOverflowBody ) } - -const MenuButton = styled.button` - background-color: ${COLORS.transparent}; - border-radius: inherit; - cursor: ${CURSOR_POINTER}; - padding: ${SPACING.spacing8} ${SPACING.spacing12}; - border: none; - border-radius: inherit; - &:hover { - background-color: ${COLORS.blue10}; - } - &:disabled { - color: ${COLORS.grey40}; - cursor: ${CURSOR_AUTO}; - } -` diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx index 42c9c026d70..2ba0d4df60f 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx @@ -108,7 +108,7 @@ describe('SlotOverflowMenu', () => { expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalled() expect(props.setShowMenuList).toHaveBeenCalled() }) - it('renders 2 buttons when there is nothing on the slot', () => { + it('renders 3 buttons when there is nothing on the slot', () => { props.location = 'A1' render(props) fireEvent.click( @@ -116,7 +116,10 @@ describe('SlotOverflowMenu', () => { ) expect(props.addEquipment).toHaveBeenCalled() expect(props.setShowMenuList).toHaveBeenCalled() - expect(screen.getAllByRole('button')).toHaveLength(2) + expect(screen.getAllByRole('button')).toHaveLength(3) + expect(screen.getByRole('button', { name: 'Add liquid' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'Clear slot' })).toBeDisabled() + screen.getByTestId('divider') }) it('renders Edit liquid button when there is liquid on the labware', () => { vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({ From 860e0daa94d43f77d9839c9c441806f0bd08dbaf Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:11:24 -0500 Subject: [PATCH 02/52] fix(protocol-designer): fix disposal volume settings (#16877) In transfer toolbox multi-dispense advanced settings, fix copy and padding for disposal volume. --- .../src/assets/localization/en/protocol_steps.json | 2 +- .../ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index a9a45b9d2ed..6105b29b24d 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -69,7 +69,7 @@ "consolidate_disposal": "Consolidatingfrom{{sourceWells}} of {{source}}to{{destination}}", "transfer_disposal": "Transferringfrom{{sourceWells}} of {{source}}to{{destination}}" }, - "multi_dispense_options": "Distribute options", + "multi_dispense_options": "Disposal volume", "multiAspirate": "Consolidate path", "multiDispense": "Distribute path", "new_location": "New location", diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx index 3267fd79256..5a89f0c9fe4 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx @@ -100,6 +100,7 @@ export function DisposalField(props: DisposalFieldProps): JSX.Element { pipetteId={pipette} flowRateType="blowout" volume={propsForFields.volume?.value ?? 0} + padding="0" tiprack={propsForFields.tipRack.value} /> Date: Mon, 18 Nov 2024 17:11:48 -0500 Subject: [PATCH 03/52] fix(protocol-designer): update logic for ProtocolNavBar dropshadow (#16871) We only want to show the ProtocolNavBar dropshadow if we are zoomed in on a slot, to add liquid, hardware, or labware. This PR also refactors the logic used for text copy and alignment in the NavBar, consolidating `hasZoomInSlot` and `isAddingHardwareOrLabware` props. Closes RQA-3340 --- .../src/organisms/DisabledScreen/index.tsx | 1 + .../__tests__/ProtocolNavBar.test.tsx | 5 +- .../src/organisms/ProtocolNavBar/index.tsx | 47 ++++++++++--------- .../src/pages/Designer/index.tsx | 6 +-- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/protocol-designer/src/organisms/DisabledScreen/index.tsx b/protocol-designer/src/organisms/DisabledScreen/index.tsx index bef9f0cd643..779ba446f40 100644 --- a/protocol-designer/src/organisms/DisabledScreen/index.tsx +++ b/protocol-designer/src/organisms/DisabledScreen/index.tsx @@ -23,6 +23,7 @@ export function DisabledScreen(): JSX.Element { backgroundColor={`${COLORS.black90}${COLORS.opacity40HexCode}`} overflow={OVERFLOW_HIDDEN} noPadding + zIndexOverlay={15} > { ] as TabProps[], hasTrashEntity: false, showLiquidOverflowMenu: vi.fn(), - isAddingHardwareOrLabware: false, } vi.mocked(getFileMetadata).mockReturnValue({ protocolName: 'mockProtocolName', @@ -73,14 +72,14 @@ describe('ProtocolNavBar', () => { }) it('should render protocol name and add hardware/labware - protocol name', () => { - props = { ...props, isAddingHardwareOrLabware: true } + props = { ...props, hasZoomInSlot: true } render(props) screen.getByText('mockProtocolName') screen.getByText('Add hardware/labware') }) it('should render protocol name and add hardware/labware - no protocol name', () => { - props = { ...props, isAddingHardwareOrLabware: true } + props = { ...props, hasZoomInSlot: true } vi.mocked(getFileMetadata).mockReturnValue({}) render(props) screen.getByText('Untitled protocol') diff --git a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx index cb47c8a20e6..83ad6eadc5c 100644 --- a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx +++ b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx @@ -29,19 +29,15 @@ interface ProtocolNavBarProps { tabs?: TabProps[] hasTrashEntity?: boolean showLiquidOverflowMenu?: (liquidOverflowMenu: boolean) => void - isAddingHardwareOrLabware?: boolean liquidPage?: boolean - isOffDeck?: boolean } export function ProtocolNavBar({ hasZoomInSlot, - isAddingHardwareOrLabware = false, tabs = [], hasTrashEntity, showLiquidOverflowMenu, liquidPage = false, - isOffDeck = false, }: ProtocolNavBarProps): JSX.Element { const { t } = useTranslation('starting_deck_state') const metadata = useSelector(getFileMetadata) @@ -49,28 +45,29 @@ export function ProtocolNavBar({ const navigate = useNavigate() const dispatch = useDispatch() + const showProtocolEditButtons = !(hasZoomInSlot || liquidPage) + + let metadataText = t('edit_protocol') + if (liquidPage) { + metadataText = t('add_liquid') + } else if (hasZoomInSlot) { + metadataText = t('add_hardware_labware') + } return ( - - {hasZoomInSlot ? null : } + + {showProtocolEditButtons ? : null} - + {metadata?.protocolName != null && metadata?.protocolName !== '' ? metadata?.protocolName : t('untitled_protocol')} - - {isAddingHardwareOrLabware || isOffDeck - ? t('add_hardware_labware') - : t('edit_protocol')} + + {metadataText} @@ -98,26 +95,30 @@ export function ProtocolNavBar({ ) } -const NavContainer = styled(Flex)` +const NavContainer = styled(Flex)<{ showShadow: boolean }>` + z-index: 11; padding: ${SPACING.spacing12}; width: 100%; justify-content: ${JUSTIFY_SPACE_BETWEEN}; align-items: ${ALIGN_CENTER}; - box-shadow: 0px 1px 3px 0px ${COLORS.black90}${COLORS.opacity20HexCode}; + box-shadow: ${props => + props.showShadow + ? `0px 1px 3px 0px ${COLORS.black90}${COLORS.opacity20HexCode}` + : 'none'}; ` interface MetadataProps extends StyleProps { - isAddingHardwareOrLabware: boolean + showProtocolEditButtons: boolean } const MetadataContainer = styled.div.withConfig({ - shouldForwardProp: p => isntStyleProp(p) && p !== 'isAddingHardwareOrLabware', + shouldForwardProp: p => isntStyleProp(p) && p !== 'showProtocolEditButtons', })` display: flex; flex-direction: ${DIRECTION_COLUMN}; text-align: ${props => - props.isAddingHardwareOrLabware === true - ? TYPOGRAPHY.textAlignLeft - : TYPOGRAPHY.textAlignCenter}; + props.showProtocolEditButtons === true + ? TYPOGRAPHY.textAlignCenter + : TYPOGRAPHY.textAlignLeft}; // For screens between 600px and 767px, set width to 88px @media only screen and (max-width: 767px) { diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index 3469a843eb4..8993d271420 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -151,14 +151,10 @@ export function Designer(): JSX.Element { ) : null} {tab === 'startingDeck' ? ( From 0814d91df42ab0877028e5dfc1227bb27133a799 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:44:07 -0500 Subject: [PATCH 04/52] fix(protocol-designer): default unknown TC lid state to closed (#16838) Latest designs specify that the thermocycler lid position should be default to closed at the Protocol Steps tab (all upstream steps of the first thermocycler step). [reference](https://github.com/Opentrons/opentrons/pull/16826) --- .../src/pages/Designer/DeckSetup/DeckSetupDetails.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx index 9669bf8ef14..f8935e848d5 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx @@ -200,8 +200,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { } const isLabwareOccludedByThermocyclerLid = moduleOnDeck.type === THERMOCYCLER_MODULE_TYPE && - (moduleOnDeck.moduleState as ThermocyclerModuleState).lidOpen === - false + (moduleOnDeck.moduleState as ThermocyclerModuleState).lidOpen !== true const tempInnerProps = getModuleInnerProps(moduleOnDeck.moduleState) const innerProps = @@ -210,9 +209,9 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { ...tempInnerProps, lidMotorState: (tempInnerProps as ThermocyclerVizProps).lidMotorState !== - 'closed' - ? 'open' - : 'closed', + 'open' + ? 'closed' + : 'open', } : tempInnerProps From 05eeed733cecbda5471d12033b3501eaf1b72a34 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 19 Nov 2024 10:15:16 -0500 Subject: [PATCH 05/52] feat(api): express stalls in a recoverable way (#16861) Commands that use `move_to_well`, `move_to_coordinates`, `move_to_addressable_area`, and `move_relative` now will return stalls as DefinedErrors, which means they can be hooked into error recovery en masse. This just leaves move labware. Closes EXEC-831 ## Reviews - Feel like it paid off? ## Testing - Do some stalls and make sure they end up with defined errors ## Further work and questions - I think we should probably handle errors during `aspirate`'s automatic prepare for aspirate at the beginning and probably the same with drop tip but we didn't handle it at all so far so I don't know --------- Co-authored-by: Max Marrone --- .../protocol_engine/commands/aspirate.py | 12 +- .../protocol_engine/commands/blow_out.py | 21 +- .../commands/command_unions.py | 2 + .../protocol_engine/commands/dispense.py | 12 +- .../protocol_engine/commands/drop_tip.py | 13 +- .../protocol_engine/commands/liquid_probe.py | 44 +++- .../protocol_engine/commands/move_relative.py | 58 +++-- .../commands/move_to_addressable_area.py | 63 +++-- .../move_to_addressable_area_for_drop_tip.py | 65 +++-- .../commands/move_to_coordinates.py | 55 ++-- .../protocol_engine/commands/move_to_well.py | 44 +++- .../commands/movement_common.py | 241 ++++++++++++++++-- .../protocol_engine/commands/pick_up_tip.py | 18 +- .../protocol_engine/commands/touch_tip.py | 27 +- .../protocol_engine/state/update_types.py | 14 +- .../protocol_engine/commands/test_aspirate.py | 66 ++++- .../protocol_engine/commands/test_blow_out.py | 63 ++++- .../protocol_engine/commands/test_dispense.py | 63 ++++- .../protocol_engine/commands/test_drop_tip.py | 72 ++++++ .../commands/test_liquid_probe.py | 57 +++++ .../commands/test_move_relative.py | 54 +++- .../commands/test_move_to_addressable_area.py | 82 +++++- ...t_move_to_addressable_area_for_drop_tip.py | 81 +++++- .../commands/test_move_to_coordinates.py | 80 ++++-- .../commands/test_move_to_well.py | 76 +++++- .../commands/test_pick_up_tip.py | 63 +++++ .../commands/test_touch_tip.py | 10 + 27 files changed, 1238 insertions(+), 218 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 1dce8e8c2ca..38dbe03c7e0 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -15,6 +15,7 @@ from .movement_common import ( LiquidHandlingWellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) from .command import ( @@ -60,7 +61,7 @@ class AspirateResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[AspirateResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -120,6 +121,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, @@ -127,6 +129,8 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: current_well=current_well, operation_volume=-params.volume, ) + if isinstance(move_result, DefinedErrorData): + return move_result aspirate_result = await aspirate_in_place( pipette_id=pipette_id, @@ -185,7 +189,11 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: ) -class Aspirate(BaseCommand[AspirateParams, AspirateResult, OverpressureError]): +class Aspirate( + BaseCommand[ + AspirateParams, AspirateResult, OverpressureError | StallOrCollisionError + ] +): """Aspirate command model.""" commandType: AspirateCommandType = "aspirate" diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index 2469036186c..b2e8765b4a1 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -11,7 +11,12 @@ FlowRateMixin, blow_out_in_place, ) -from .movement_common import WellLocationMixin, DestinationPositionResult, move_to_well +from .movement_common import ( + WellLocationMixin, + DestinationPositionResult, + move_to_well, + StallOrCollisionError, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -19,7 +24,6 @@ DefinedErrorData, SuccessData, ) -from ..errors.error_occurrence import ErrorOccurrence from ..state.update_types import StateUpdate from opentrons.hardware_control import HardwareControlAPI @@ -48,7 +52,7 @@ class BlowOutResult(DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[BlowOutResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -74,11 +78,14 @@ async def execute(self, params: BlowOutParams) -> _ExecuteReturn: """Move to and blow-out the requested well.""" move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, well_location=params.wellLocation, ) + if isinstance(move_result, DefinedErrorData): + return move_result blow_out_result = await blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate, @@ -112,7 +119,13 @@ async def execute(self, params: BlowOutParams) -> _ExecuteReturn: ) -class BlowOut(BaseCommand[BlowOutParams, BlowOutResult, ErrorOccurrence]): +class BlowOut( + BaseCommand[ + BlowOutParams, + BlowOutResult, + OverpressureError | StallOrCollisionError, + ] +): """Blow-out command model.""" commandType: BlowOutCommandType = "blowout" diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 7db8045f0cb..c33f55e2e01 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -13,6 +13,7 @@ LiquidNotFoundError, TipPhysicallyAttachedError, ) +from .movement_common import StallOrCollisionError from . import absorbance_reader from . import heater_shaker @@ -754,6 +755,7 @@ DefinedErrorData[OverpressureError], DefinedErrorData[LiquidNotFoundError], DefinedErrorData[GripperMovementError], + DefinedErrorData[StallOrCollisionError], ] diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 78b9ebed6b3..18f157934d4 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -19,6 +19,7 @@ from .movement_common import ( LiquidHandlingWellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) from .command import ( @@ -57,7 +58,7 @@ class DispenseResult(BaseLiquidHandlingResult, DestinationPositionResult): _ExecuteReturn = Union[ SuccessData[DispenseResult], - DefinedErrorData[OverpressureError], + DefinedErrorData[OverpressureError] | DefinedErrorData[StallOrCollisionError], ] @@ -88,11 +89,14 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + if isinstance(move_result, DefinedErrorData): + return move_result dispense_result = await dispense_in_place( pipette_id=params.pipetteId, volume=volume, @@ -159,7 +163,11 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: ) -class Dispense(BaseCommand[DispenseParams, DispenseResult, OverpressureError]): +class Dispense( + BaseCommand[ + DispenseParams, DispenseResult, OverpressureError | StallOrCollisionError + ] +): """Dispense command model.""" commandType: DispenseCommandType = "dispense" diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 6b8b5132504..18c90360c42 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -15,7 +15,11 @@ PipetteIdMixin, TipPhysicallyAttachedError, ) -from .movement_common import DestinationPositionResult, move_to_well +from .movement_common import ( + DestinationPositionResult, + move_to_well, + StallOrCollisionError, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -69,7 +73,9 @@ class DropTipResult(DestinationPositionResult): _ExecuteReturn = ( - SuccessData[DropTipResult] | DefinedErrorData[TipPhysicallyAttachedError] + SuccessData[DropTipResult] + | DefinedErrorData[TipPhysicallyAttachedError] + | DefinedErrorData[StallOrCollisionError] ) @@ -117,11 +123,14 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: move_result = await move_to_well( movement=self._movement_handler, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=tip_drop_location, ) + if isinstance(move_result, DefinedErrorData): + return move_result try: await self._tip_handler.drop_tip( diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index b6c51613263..b99e6ac11b1 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -27,6 +27,7 @@ from .movement_common import ( WellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) from .command import ( @@ -91,9 +92,11 @@ class TryLiquidProbeResult(DestinationPositionResult): _LiquidProbeExecuteReturn = Union[ SuccessData[LiquidProbeResult], - DefinedErrorData[LiquidNotFoundError], + DefinedErrorData[LiquidNotFoundError] | DefinedErrorData[StallOrCollisionError], ] -_TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult] +_TryLiquidProbeExecuteReturn = ( + SuccessData[TryLiquidProbeResult] | DefinedErrorData[StallOrCollisionError] +) class _ExecuteCommonResult(NamedTuple): @@ -110,8 +113,9 @@ async def _execute_common( state_view: StateView, movement: MovementHandler, pipetting: PipettingHandler, + model_utils: ModelUtils, params: _CommonParams, -) -> _ExecuteCommonResult: +) -> _ExecuteCommonResult | DefinedErrorData[StallOrCollisionError]: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName @@ -145,12 +149,14 @@ async def _execute_common( # liquid_probe process start position move_result = await move_to_well( movement=movement, + model_utils=model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, ) - + if isinstance(move_result, DefinedErrorData): + return move_result try: z_pos = await pipetting.liquid_probe_in_place( pipette_id=pipette_id, @@ -206,9 +212,16 @@ async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn: MustHomeError: as an undefined error, if the plunger is not in a valid position. """ - z_pos_or_error, state_update, deck_point = await _execute_common( - self._state_view, self._movement, self._pipetting, params + result = await _execute_common( + state_view=self._state_view, + movement=self._movement, + pipetting=self._pipetting, + model_utils=self._model_utils, + params=params, ) + if isinstance(result, DefinedErrorData): + return result + z_pos_or_error, state_update, deck_point = result if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): state_update.set_liquid_probed( labware_id=params.labwareId, @@ -282,9 +295,16 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: found, `tryLiquidProbe` returns a success result with `z_position=null` instead of a defined error. """ - z_pos_or_error, state_update, deck_point = await _execute_common( - self._state_view, self._movement, self._pipetting, params + result = await _execute_common( + state_view=self._state_view, + movement=self._movement, + pipetting=self._pipetting, + model_utils=self._model_utils, + params=params, ) + if isinstance(result, DefinedErrorData): + return result + z_pos_or_error, state_update, deck_point = result if isinstance(z_pos_or_error, PipetteLiquidNotFoundError): z_pos = None @@ -316,7 +336,11 @@ async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn: class LiquidProbe( - BaseCommand[LiquidProbeParams, LiquidProbeResult, LiquidNotFoundError] + BaseCommand[ + LiquidProbeParams, + LiquidProbeResult, + LiquidNotFoundError | StallOrCollisionError, + ] ): """The model for a full `liquidProbe` command.""" @@ -328,7 +352,7 @@ class LiquidProbe( class TryLiquidProbe( - BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, ErrorOccurrence] + BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, StallOrCollisionError] ): """The model for a full `tryLiquidProbe` command.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_relative.py b/api/src/opentrons/protocol_engine/commands/move_relative.py index cc2ae7c597a..54c877a3693 100644 --- a/api/src/opentrons/protocol_engine/commands/move_relative.py +++ b/api/src/opentrons/protocol_engine/commands/move_relative.py @@ -5,14 +5,23 @@ from typing_extensions import Literal -from ..state import update_types -from ..types import MovementAxis, DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence -from .movement_common import DestinationPositionResult +from ..types import MovementAxis +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) +from .movement_common import ( + DestinationPositionResult, + move_relative, + StallOrCollisionError, +) if TYPE_CHECKING: from ..execution import MovementHandler + from ..resources.model_utils import ModelUtils MoveRelativeCommandType = Literal["moveRelative"] @@ -39,40 +48,41 @@ class MoveRelativeResult(DestinationPositionResult): class MoveRelativeImplementation( - AbstractCommandImpl[MoveRelativeParams, SuccessData[MoveRelativeResult]] + AbstractCommandImpl[ + MoveRelativeParams, + SuccessData[MoveRelativeResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Move relative command implementation.""" - def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + def __init__( + self, movement: MovementHandler, model_utils: ModelUtils, **kwargs: object + ) -> None: self._movement = movement + self._model_utils = model_utils async def execute( self, params: MoveRelativeParams - ) -> SuccessData[MoveRelativeResult]: + ) -> SuccessData[MoveRelativeResult] | DefinedErrorData[StallOrCollisionError]: """Move (jog) a given pipette a relative distance.""" - state_update = update_types.StateUpdate() - - x, y, z = await self._movement.move_relative( + result = await move_relative( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, axis=params.axis, distance=params.distance, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.pipette_location = update_types.PipetteLocationUpdate( - pipette_id=params.pipetteId, - # TODO(jbl 2023-02-14): Need to investigate whether move relative should clear current location - new_location=update_types.NO_CHANGE, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveRelativeResult(position=deck_point), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveRelativeResult(position=result.public.position), + state_update=result.state_update, + ) class MoveRelative( - BaseCommand[MoveRelativeParams, MoveRelativeResult, ErrorOccurrence] + BaseCommand[MoveRelativeParams, MoveRelativeResult, StallOrCollisionError] ): """Command to move (jog) a given pipette a relative distance.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py index f3dd3f57c56..7380a01951a 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -7,8 +7,7 @@ from opentrons_shared_data.pipette.types import PipetteNameType from ..errors import LocationNotAccessibleByPipetteError -from ..state import update_types -from ..types import DeckPoint, AddressableOffsetVector +from ..types import AddressableOffsetVector from ..resources import fixture_validation from .pipetting_common import ( PipetteIdMixin, @@ -16,13 +15,21 @@ from .movement_common import ( MovementMixin, DestinationPositionResult, + move_to_addressable_area, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] @@ -76,25 +83,29 @@ class MoveToAddressableAreaResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToAddressableAreaResult] | DefinedErrorData[StallOrCollisionError] +) + + class MoveToAddressableAreaImplementation( - AbstractCommandImpl[ - MoveToAddressableAreaParams, SuccessData[MoveToAddressableAreaResult] - ] + AbstractCommandImpl[MoveToAddressableAreaParams, _ExecuteReturn] ): """Move to addressable area command implementation.""" def __init__( - self, movement: MovementHandler, state_view: StateView, **kwargs: object + self, + movement: MovementHandler, + state_view: StateView, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._movement = movement self._state_view = state_view + self._model_utils = model_utils - async def execute( - self, params: MoveToAddressableAreaParams - ) -> SuccessData[MoveToAddressableAreaResult]: + async def execute(self, params: MoveToAddressableAreaParams) -> _ExecuteReturn: """Move the requested pipette to the requested addressable area.""" - state_update = update_types.StateUpdate() - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName ) @@ -117,7 +128,9 @@ async def execute( f"Cannot move pipette to staging slot {params.addressableAreaName}" ) - x, y, z = await self._movement.move_to_addressable_area( + result = await move_to_addressable_area( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, addressable_area_name=params.addressableAreaName, offset=params.offset, @@ -127,22 +140,20 @@ async def execute( stay_at_highest_possible_z=params.stayAtHighestPossibleZ, highest_possible_z_extra_offset=extra_z_offset, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_addressable_area_name=params.addressableAreaName, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z)), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToAddressableAreaResult(position=result.public.position), + state_update=result.state_update, + ) class MoveToAddressableArea( BaseCommand[ - MoveToAddressableAreaParams, MoveToAddressableAreaResult, ErrorOccurrence + MoveToAddressableAreaParams, + MoveToAddressableAreaResult, + StallOrCollisionError, ] ): """Move to addressable area command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py index 835209c4386..679e769cc2e 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py @@ -5,8 +5,7 @@ from typing_extensions import Literal from ..errors import LocationNotAccessibleByPipetteError -from ..state import update_types -from ..types import DeckPoint, AddressableOffsetVector +from ..types import AddressableOffsetVector from ..resources import fixture_validation from .pipetting_common import ( PipetteIdMixin, @@ -14,13 +13,21 @@ from .movement_common import ( MovementMixin, DestinationPositionResult, + move_to_addressable_area, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToAddressableAreaForDropTipCommandType = Literal["moveToAddressableAreaForDropTip"] @@ -85,26 +92,32 @@ class MoveToAddressableAreaForDropTipResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToAddressableAreaForDropTipResult] + | DefinedErrorData[StallOrCollisionError] +) + + class MoveToAddressableAreaForDropTipImplementation( - AbstractCommandImpl[ - MoveToAddressableAreaForDropTipParams, - SuccessData[MoveToAddressableAreaForDropTipResult], - ] + AbstractCommandImpl[MoveToAddressableAreaForDropTipParams, _ExecuteReturn] ): """Move to addressable area for drop tip command implementation.""" def __init__( - self, movement: MovementHandler, state_view: StateView, **kwargs: object + self, + movement: MovementHandler, + state_view: StateView, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._movement = movement self._state_view = state_view + self._model_utils = model_utils async def execute( self, params: MoveToAddressableAreaForDropTipParams - ) -> SuccessData[MoveToAddressableAreaForDropTipResult]: + ) -> _ExecuteReturn: """Move the requested pipette to the requested addressable area in preperation of a drop tip.""" - state_update = update_types.StateUpdate() - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.addressableAreaName ) @@ -122,7 +135,9 @@ async def execute( else: offset = params.offset - x, y, z = await self._movement.move_to_addressable_area( + result = await move_to_addressable_area( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, addressable_area_name=params.addressableAreaName, offset=offset, @@ -131,26 +146,22 @@ async def execute( speed=params.speed, ignore_tip_configuration=params.ignoreTipConfiguration, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_addressable_area_name=params.addressableAreaName, - new_deck_point=deck_point, - ) - - return SuccessData( - public=MoveToAddressableAreaForDropTipResult( - position=DeckPoint(x=x, y=y, z=z) - ), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToAddressableAreaForDropTipResult( + position=result.public.position, + ), + state_update=result.state_update, + ) class MoveToAddressableAreaForDropTip( BaseCommand[ MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, - ErrorOccurrence, + StallOrCollisionError, ] ): """Move to addressable area for drop tip command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py index 99df6be868b..36b7ff64ed0 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py @@ -6,15 +6,25 @@ from typing_extensions import Literal -from ..state import update_types from ..types import DeckPoint from .pipetting_common import PipetteIdMixin -from .movement_common import MovementMixin, DestinationPositionResult -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .movement_common import ( + MovementMixin, + DestinationPositionResult, + move_to_coordinates, + StallOrCollisionError, +) +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) if TYPE_CHECKING: from ..execution import MovementHandler + from ..resources.model_utils import ModelUtils MoveToCoordinatesCommandType = Literal["moveToCoordinates"] @@ -35,44 +45,47 @@ class MoveToCoordinatesResult(DestinationPositionResult): pass +_ExecuteReturn = ( + SuccessData[MoveToCoordinatesResult] | DefinedErrorData[StallOrCollisionError] +) + + class MoveToCoordinatesImplementation( - AbstractCommandImpl[MoveToCoordinatesParams, SuccessData[MoveToCoordinatesResult]] + AbstractCommandImpl[MoveToCoordinatesParams, _ExecuteReturn] ): """Move to coordinates command implementation.""" def __init__( self, movement: MovementHandler, + model_utils: ModelUtils, **kwargs: object, ) -> None: self._movement = movement + self._model_utils = model_utils - async def execute( - self, params: MoveToCoordinatesParams - ) -> SuccessData[MoveToCoordinatesResult]: + async def execute(self, params: MoveToCoordinatesParams) -> _ExecuteReturn: """Move the requested pipette to the requested coordinates.""" - state_update = update_types.StateUpdate() - - x, y, z = await self._movement.move_to_coordinates( + result = await move_to_coordinates( + movement=self._movement, + model_utils=self._model_utils, pipette_id=params.pipetteId, deck_coordinates=params.coordinates, direct=params.forceDirect, additional_min_travel_z=params.minimumZHeight, speed=params.speed, ) - deck_point = DeckPoint.construct(x=x, y=y, z=z) - state_update.pipette_location = update_types.PipetteLocationUpdate( - pipette_id=params.pipetteId, new_location=None, new_deck_point=deck_point - ) - - return SuccessData( - public=MoveToCoordinatesResult(position=DeckPoint(x=x, y=y, z=z)), - state_update=state_update, - ) + if isinstance(result, DefinedErrorData): + return result + else: + return SuccessData( + public=MoveToCoordinatesResult(position=result.public.position), + state_update=result.state_update, + ) class MoveToCoordinates( - BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, ErrorOccurrence] + BaseCommand[MoveToCoordinatesParams, MoveToCoordinatesResult, StallOrCollisionError] ): """Move to well command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 67819cb0d58..6aaf398650f 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -11,15 +11,22 @@ WellLocationMixin, MovementMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) from ..errors import LabwareIsTipRackError if TYPE_CHECKING: from ..execution import MovementHandler from ..state.state import StateView + from ..resources.model_utils import ModelUtils MoveToWellCommandType = Literal["moveToWell"] @@ -37,17 +44,27 @@ class MoveToWellResult(DestinationPositionResult): class MoveToWellImplementation( - AbstractCommandImpl[MoveToWellParams, SuccessData[MoveToWellResult]] + AbstractCommandImpl[ + MoveToWellParams, + SuccessData[MoveToWellResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Move to well command implementation.""" def __init__( - self, state_view: StateView, movement: MovementHandler, **kwargs: object + self, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement + self._model_utils = model_utils - async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResult]: + async def execute( + self, params: MoveToWellParams + ) -> SuccessData[MoveToWellResult] | DefinedErrorData[StallOrCollisionError]: """Move the requested pipette to the requested well.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -63,6 +80,7 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul ) move_result = await move_to_well( + model_utils=self._model_utils, movement=self._movement, pipette_id=pipette_id, labware_id=labware_id, @@ -72,14 +90,18 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul minimum_z_height=params.minimumZHeight, speed=params.speed, ) - - return SuccessData( - public=MoveToWellResult(position=move_result.public.position), - state_update=move_result.state_update, - ) + if isinstance(move_result, DefinedErrorData): + return move_result + else: + return SuccessData( + public=MoveToWellResult(position=move_result.public.position), + state_update=move_result.state_update, + ) -class MoveToWell(BaseCommand[MoveToWellParams, MoveToWellResult, ErrorOccurrence]): +class MoveToWell( + BaseCommand[MoveToWellParams, MoveToWellResult, StallOrCollisionError] +): """Move to well command model.""" commandType: MoveToWellCommandType = "moveToWell" diff --git a/api/src/opentrons/protocol_engine/commands/movement_common.py b/api/src/opentrons/protocol_engine/commands/movement_common.py index 4d5d38544f6..7917daa8613 100644 --- a/api/src/opentrons/protocol_engine/commands/movement_common.py +++ b/api/src/opentrons/protocol_engine/commands/movement_common.py @@ -2,15 +2,28 @@ from __future__ import annotations -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING, Literal + from pydantic import BaseModel, Field -from ..types import WellLocation, LiquidHandlingWellLocation, DeckPoint, CurrentWell -from ..state.update_types import StateUpdate -from .command import SuccessData + +from opentrons_shared_data.errors import ErrorCodes +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError +from ..errors import ErrorOccurrence +from ..types import ( + WellLocation, + LiquidHandlingWellLocation, + DeckPoint, + CurrentWell, + MovementAxis, + AddressableOffsetVector, +) +from ..state.update_types import StateUpdate, PipetteLocationUpdate +from .command import SuccessData, DefinedErrorData if TYPE_CHECKING: from ..execution.movement import MovementHandler + from ..resources.model_utils import ModelUtils class WellLocationMixin(BaseModel): @@ -79,6 +92,22 @@ class MovementMixin(BaseModel): ) +class StallOrCollisionError(ErrorOccurrence): + """Returned when the machine detects that axis encoders are reading a different position than expected. + + All axes are stopped at the point where the error was encountered. + + The next thing to move the machine must account for the robot not having a valid estimate + of its position. It should be a `home` or `unsafe/updatePositionEstimators`. + """ + + isDefined: bool = True + errorType: Literal["stallOrCollision"] = "stallOrCollision" + + errorCode: str = ErrorCodes.STALL_OR_COLLISION_DETECTED.value.code + detail: str = ErrorCodes.STALL_OR_COLLISION_DETECTED.value.detail + + class DestinationPositionResult(BaseModel): """Mixin for command results that move a pipette.""" @@ -99,11 +128,14 @@ class DestinationPositionResult(BaseModel): ) -MoveToWellOperationReturn = SuccessData[DestinationPositionResult] +MoveToWellOperationReturn = ( + SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError] +) async def move_to_well( movement: MovementHandler, + model_utils: ModelUtils, pipette_id: str, labware_id: str, well_name: str, @@ -115,26 +147,181 @@ async def move_to_well( operation_volume: Optional[float] = None, ) -> MoveToWellOperationReturn: """Execute a move to well microoperation.""" - position = await movement.move_to_well( - pipette_id=pipette_id, - labware_id=labware_id, - well_name=well_name, - well_location=well_location, - current_well=current_well, - force_direct=force_direct, - minimum_z_height=minimum_z_height, - speed=speed, - operation_volume=operation_volume, - ) - deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) - return SuccessData( - public=DestinationPositionResult( - position=deck_point, - ), - state_update=StateUpdate().set_pipette_location( + try: + position = await movement.move_to_well( pipette_id=pipette_id, - new_labware_id=labware_id, - new_well_name=well_name, - new_deck_point=deck_point, - ), - ) + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=current_well, + force_direct=force_direct, + minimum_z_height=minimum_z_height, + speed=speed, + operation_volume=operation_volume, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DestinationPositionResult( + position=deck_point, + ), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, + new_deck_point=deck_point, + ), + ) + + +async def move_relative( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + axis: MovementAxis, + distance: float, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move by a fixed displacement from the current position.""" + try: + position = await movement.move_relative(pipette_id, axis, distance) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) + return SuccessData( + public=DestinationPositionResult( + position=deck_point, + ), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, new_deck_point=deck_point + ), + ) + + +async def move_to_addressable_area( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + addressable_area_name: str, + offset: AddressableOffsetVector, + force_direct: bool = False, + minimum_z_height: float | None = None, + speed: float | None = None, + stay_at_highest_possible_z: bool = False, + ignore_tip_configuration: bool | None = True, + highest_possible_z_extra_offset: float | None = None, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move to an addressable area identified by name.""" + try: + x, y, z = await movement.move_to_addressable_area( + pipette_id=pipette_id, + addressable_area_name=addressable_area_name, + offset=offset, + force_direct=force_direct, + minimum_z_height=minimum_z_height, + speed=speed, + stay_at_highest_possible_z=stay_at_highest_possible_z, + ignore_tip_configuration=ignore_tip_configuration, + highest_possible_z_extra_offset=highest_possible_z_extra_offset, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=x, y=y, z=z) + return SuccessData( + public=DestinationPositionResult(position=deck_point), + state_update=StateUpdate().set_pipette_location( + pipette_id=pipette_id, + new_addressable_area_name=addressable_area_name, + new_deck_point=deck_point, + ), + ) + + +async def move_to_coordinates( + movement: MovementHandler, + model_utils: ModelUtils, + pipette_id: str, + deck_coordinates: DeckPoint, + direct: bool, + additional_min_travel_z: float | None, + speed: float | None = None, +) -> SuccessData[DestinationPositionResult] | DefinedErrorData[StallOrCollisionError]: + """Move to a set of coordinates.""" + try: + x, y, z = await movement.move_to_coordinates( + pipette_id=pipette_id, + deck_coordinates=deck_coordinates, + direct=direct, + additional_min_travel_z=additional_min_travel_z, + speed=speed, + ) + except StallOrCollisionDetectedError as e: + return DefinedErrorData( + public=StallOrCollisionError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + ), + state_update=StateUpdate().clear_all_pipette_locations(), + ) + else: + deck_point = DeckPoint.construct(x=x, y=y, z=z) + + return SuccessData( + public=DestinationPositionResult(position=DeckPoint(x=x, y=y, z=z)), + state_update=StateUpdate( + pipette_location=PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=None, + new_deck_point=deck_point, + ) + ), + ) diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 928a4a9ced8..101d9f2e02c 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -14,7 +14,11 @@ from .pipetting_common import ( PipetteIdMixin, ) -from .movement_common import DestinationPositionResult, move_to_well +from .movement_common import ( + DestinationPositionResult, + StallOrCollisionError, + move_to_well, +) from .command import ( AbstractCommandImpl, BaseCommand, @@ -88,7 +92,8 @@ class TipPhysicallyMissingError(ErrorOccurrence): _ExecuteReturn = Union[ SuccessData[PickUpTipResult], - DefinedErrorData[TipPhysicallyMissingError], + DefinedErrorData[TipPhysicallyMissingError] + | DefinedErrorData[StallOrCollisionError], ] @@ -121,11 +126,14 @@ async def execute( ) move_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + if isinstance(move_result, DefinedErrorData): + return move_result try: tip_geometry = await self._tip_handler.pick_up_tip( @@ -194,7 +202,11 @@ async def execute( class PickUpTip( - BaseCommand[PickUpTipParams, PickUpTipResult, TipPhysicallyMissingError] + BaseCommand[ + PickUpTipParams, + PickUpTipResult, + TipPhysicallyMissingError | StallOrCollisionError, + ] ): """Pick up tip command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py index c7a5f278e52..2d7c507d321 100644 --- a/api/src/opentrons/protocol_engine/commands/touch_tip.py +++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py @@ -9,20 +9,27 @@ from ..errors import TouchTipDisabledError, LabwareIsTipRackError from ..types import DeckPoint -from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -from ..errors.error_occurrence import ErrorOccurrence +from .command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, + DefinedErrorData, +) from .pipetting_common import ( PipetteIdMixin, ) from .movement_common import ( WellLocationMixin, DestinationPositionResult, + StallOrCollisionError, move_to_well, ) if TYPE_CHECKING: from ..execution import MovementHandler, GantryMover from ..state.state import StateView + from ..resources.model_utils import ModelUtils TouchTipCommandType = Literal["touchTip"] @@ -54,7 +61,10 @@ class TouchTipResult(DestinationPositionResult): class TouchTipImplementation( - AbstractCommandImpl[TouchTipParams, SuccessData[TouchTipResult]] + AbstractCommandImpl[ + TouchTipParams, + SuccessData[TouchTipResult] | DefinedErrorData[StallOrCollisionError], + ] ): """Touch tip command implementation.""" @@ -63,13 +73,17 @@ def __init__( state_view: StateView, movement: MovementHandler, gantry_mover: GantryMover, + model_utils: ModelUtils, **kwargs: object, ) -> None: self._state_view = state_view self._movement = movement self._gantry_mover = gantry_mover + self._model_utils = model_utils - async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: + async def execute( + self, params: TouchTipParams + ) -> SuccessData[TouchTipResult] | DefinedErrorData[StallOrCollisionError]: """Touch tip to sides of a well using the requested pipette.""" pipette_id = params.pipetteId labware_id = params.labwareId @@ -85,11 +99,14 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: center_result = await move_to_well( movement=self._movement, + model_utils=self._model_utils, pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, well_location=params.wellLocation, ) + if isinstance(center_result, DefinedErrorData): + return center_result touch_speed = self._state_view.pipettes.get_movement_speed( pipette_id, params.speed @@ -128,7 +145,7 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]: ) -class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, ErrorOccurrence]): +class TouchTip(BaseCommand[TouchTipParams, TouchTipResult, StallOrCollisionError]): """Touch up tip command model.""" commandType: TouchTipCommandType = "touchTip" diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py index 2f217120143..567ba39144c 100644 --- a/api/src/opentrons/protocol_engine/state/update_types.py +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -322,6 +322,11 @@ def reduce(cls: typing.Type[Self], *args: Self) -> Self: # These convenience functions let the caller avoid the boilerplate of constructing a # complicated dataclass tree. + @typing.overload + def set_pipette_location( + self: Self, *, pipette_id: str, new_deck_point: DeckPoint + ) -> Self: + """Schedule a pipette's coordinates to be changed while preserving its logical location.""" @typing.overload def set_pipette_location( @@ -362,10 +367,13 @@ def set_pipette_location( # noqa: D102 ), new_deck_point=new_deck_point, ) + elif new_labware_id == NO_CHANGE or new_well_name == NO_CHANGE: + self.pipette_location = PipetteLocationUpdate( + pipette_id=pipette_id, + new_location=NO_CHANGE, + new_deck_point=new_deck_point, + ) else: - # These asserts should always pass because of the overloads. - assert new_labware_id != NO_CHANGE - assert new_well_name != NO_CHANGE self.pipette_location = PipetteLocationUpdate( pipette_id=pipette_id, diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 8c2c2b06439..11078fb43cf 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -2,11 +2,15 @@ from datetime import datetime -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) from decoy import matchers, Decoy import pytest from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point from opentrons.protocol_engine import ( @@ -507,3 +511,63 @@ async def test_aspirate_implementation_meniscus( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + model_utils: ModelUtils, + state_view: StateView, +) -> None: + """It should return an overpressure error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( + True + ) + + data = AspirateParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=-50, + ), + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index 86940b15266..c06b62ace97 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -1,9 +1,12 @@ """Test blow-out command.""" from datetime import datetime + from decoy import Decoy, matchers +import pytest from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point from opentrons.protocol_engine import ( @@ -25,8 +28,10 @@ PipettingHandler, ) from opentrons.hardware_control import HardwareControlAPI -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError -import pytest +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) @pytest.fixture @@ -183,3 +188,57 @@ async def test_overpressure_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + pipetting: PipettingHandler, + subject: BlowOutImplementation, + model_utils: ModelUtils, + movement: MovementHandler, +) -> None: + """It should return an overpressure error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "C6" + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + + data = BlowOutParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=location, + flowRate=1.234, + ) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 6744add9b4a..e0e18307b69 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -5,7 +5,10 @@ import pytest from decoy import Decoy, matchers -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from opentrons_shared_data.errors.exceptions import ( + PipetteOverpressureError, + StallOrCollisionDetectedError, +) from opentrons.protocol_engine import ( LiquidHandlingWellLocation, @@ -26,6 +29,7 @@ ) from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.commands.pipetting_common import OverpressureError +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError @pytest.fixture @@ -234,3 +238,60 @@ async def test_overpressure_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: DispenseImplementation, + model_utils: ModelUtils, + state_view: StateView, +) -> None: + """It should return a stall error if the hardware API indicates that.""" + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) + + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + + data = DispenseParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, + createdAt=error_timestamp, + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 126d55defef..b11887a8824 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -5,6 +5,8 @@ import pytest from decoy import Decoy, matchers +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + from opentrons.protocol_engine import ( DropTipWellLocation, DropTipWellOrigin, @@ -21,12 +23,14 @@ from opentrons.protocol_engine.commands.pipetting_common import ( TipPhysicallyAttachedError, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.errors.exceptions import TipAttachedError from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.execution import MovementHandler, TipHandler + from opentrons.types import Point @@ -332,3 +336,71 @@ async def test_tip_attached_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + mock_state_view: StateView, + mock_movement_handler: MovementHandler, + mock_tip_handler: TipHandler, + mock_model_utils: ModelUtils, +) -> None: + """A DropTip command should have an execution implementation.""" + subject = DropTipImplementation( + state_view=mock_state_view, + movement=mock_movement_handler, + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + ) + + params = DropTipParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + ) + + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + + decoy.when( + mock_state_view.geometry.get_checked_tip_drop_location( + pipette_id="abc", + labware_id="123", + well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, + ) + ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) + + decoy.when( + await mock_movement_handler.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(mock_model_utils.generate_id()).then_return("error-id") + decoy.when(mock_model_utils.get_timestamp()).then_return( + datetime(year=1, month=2, day=3) + ) + + result = await subject.execute(params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id="error-id", + createdAt=datetime(year=1, month=2, day=3), + wrappedErrors=[matchers.Anything()], + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index d13ededae85..ab46c4b03e2 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -14,6 +14,7 @@ ) from opentrons_shared_data.errors.exceptions import ( PipetteLiquidNotFoundError, + StallOrCollisionDetectedError, ) from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError @@ -31,6 +32,7 @@ TryLiquidProbeImplementation, ) from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.execution import ( @@ -403,3 +405,58 @@ async def test_liquid_probe_location_checking( ).then_return(True) with pytest.raises(MustHomeError): await subject.execute(data) + + +async def test_liquid_probe_stall( + decoy: Decoy, + movement: MovementHandler, + state_view: StateView, + pipetting: PipettingHandler, + subject: EitherImplementation, + params_type: EitherParamsType, + model_utils: ModelUtils, +) -> None: + """It should move to the destination and do a liquid probe there.""" + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + + data = params_type( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=location, + ) + + decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id="abc")).then_return( + 0 + ) + decoy.when( + state_view.pipettes.get_nozzle_configuration_supports_lld("abc") + ).then_return(True) + + decoy.when( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=location, + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ), + ).then_raise(StallOrCollisionDetectedError()) + + error_id = "error-id" + timestamp = datetime(year=2020, month=1, day=2) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(error_id) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py index 01522e4dc45..1e2d98ebf21 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_relative.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_relative.py @@ -1,25 +1,38 @@ """Test move relative commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers +import pytest + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.types import DeckPoint, MovementAxis from opentrons.protocol_engine.execution import MovementHandler from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.commands.move_relative import ( MoveRelativeParams, MoveRelativeResult, MoveRelativeImplementation, ) +from opentrons.protocol_engine.resources.model_utils import ModelUtils + + +@pytest.fixture +def subject( + movement: MovementHandler, model_utils: ModelUtils +) -> MoveRelativeImplementation: + """Build a MoveRelativeImplementation with injected dependencies.""" + return MoveRelativeImplementation(movement=movement, model_utils=model_utils) async def test_move_relative_implementation( - decoy: Decoy, - movement: MovementHandler, + decoy: Decoy, movement: MovementHandler, subject: MoveRelativeImplementation ) -> None: """A MoveRelative command should have an execution implementation.""" - subject = MoveRelativeImplementation(movement=movement) data = MoveRelativeParams( pipetteId="pipette-id", axis=MovementAxis.X, @@ -46,3 +59,34 @@ async def test_move_relative_implementation( ) ), ) + + +async def test_move_relative_stalls( + decoy: Decoy, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveRelativeImplementation, +) -> None: + """A MoveRelative command should handle stalls.""" + data = MoveRelativeParams(pipetteId="pipette-id", axis=MovementAxis.Y, distance=40) + + decoy.when( + await movement.move_relative( + pipette_id="pipette-id", axis=MovementAxis.Y, distance=40 + ) + ).then_raise(StallOrCollisionDetectedError()) + + timestamp = datetime.now() + test_id = "test-id" + + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(test_id) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py index 6925fd7cce4..9f1470b95da 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area.py @@ -1,7 +1,10 @@ """Test move to addressable area commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers import pytest +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector, LoadedPipette from opentrons.protocol_engine.execution import MovementHandler @@ -9,12 +12,24 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point, MountType -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_addressable_area import ( MoveToAddressableAreaParams, MoveToAddressableAreaResult, MoveToAddressableAreaImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.resources.model_utils import ModelUtils + + +@pytest.fixture +def subject( + movement: MovementHandler, state_view: StateView, model_utils: ModelUtils +) -> MoveToAddressableAreaImplementation: + """Build an execution implementation with injected dependencies.""" + return MoveToAddressableAreaImplementation( + movement=movement, state_view=state_view, model_utils=model_utils + ) @pytest.mark.parametrize( @@ -39,12 +54,9 @@ async def test_move_to_addressable_area_implementation_non_gen1( state_view: StateView, movement: MovementHandler, pipette_name: PipetteNameType, + subject: MoveToAddressableAreaImplementation, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaParams( pipetteId="abc", addressableAreaName="123", @@ -67,6 +79,7 @@ async def test_move_to_addressable_area_implementation_non_gen1( minimum_z_height=4.56, speed=7.89, stay_at_highest_possible_z=True, + ignore_tip_configuration=True, highest_possible_z_extra_offset=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -102,12 +115,9 @@ async def test_move_to_addressable_area_implementation_with_gen1( state_view: StateView, movement: MovementHandler, pipette_name: PipetteNameType, + subject: MoveToAddressableAreaImplementation, ) -> None: """A MoveToAddressableArea command should have an execution implementation.""" - subject = MoveToAddressableAreaImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaParams( pipetteId="abc", addressableAreaName="123", @@ -130,6 +140,7 @@ async def test_move_to_addressable_area_implementation_with_gen1( minimum_z_height=4.56, speed=7.89, stay_at_highest_possible_z=True, + ignore_tip_configuration=True, highest_possible_z_extra_offset=5.0, ) ).then_return(Point(x=9, y=8, z=7)) @@ -146,3 +157,54 @@ async def test_move_to_addressable_area_implementation_with_gen1( ) ), ) + + +async def test_move_to_addressable_area_implementation_handles_stalls( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToAddressableAreaImplementation, +) -> None: + """A MoveToAddressableArea command should handle stalls.""" + data = MoveToAddressableAreaParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + stayAtHighestPossibleZ=True, + ) + test_id = "test-id" + timestamp = datetime.now() + + decoy.when(state_view.pipettes.get("abc")).then_return( + LoadedPipette( + id="abc", pipetteName=PipetteNameType.P1000_SINGLE, mount=MountType.LEFT + ) + ) + decoy.when(model_utils.generate_id()).then_return(test_id) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + stay_at_highest_possible_z=True, + ignore_tip_configuration=True, + highest_possible_z_extra_offset=5.0, + ) + ).then_raise(StallOrCollisionDetectedError()) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py index faca36d8121..019ec6bec3f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_addressable_area_for_drop_tip.py @@ -1,5 +1,10 @@ """Test move to addressable area for drop tip commands.""" -from decoy import Decoy +from datetime import datetime + +from decoy import Decoy, matchers +import pytest + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine import DeckPoint, AddressableOffsetVector from opentrons.protocol_engine.execution import MovementHandler @@ -7,24 +12,33 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.resources.model_utils import ModelUtils +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_addressable_area_for_drop_tip import ( MoveToAddressableAreaForDropTipParams, MoveToAddressableAreaForDropTipResult, MoveToAddressableAreaForDropTipImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError + + +@pytest.fixture +def subject( + state_view: StateView, movement: MovementHandler, model_utils: ModelUtils +) -> MoveToAddressableAreaForDropTipImplementation: + """Get a command implementation with injected dependencies.""" + return MoveToAddressableAreaForDropTipImplementation( + state_view=state_view, movement=movement, model_utils=model_utils + ) async def test_move_to_addressable_area_for_drop_tip_implementation( decoy: Decoy, state_view: StateView, movement: MovementHandler, + subject: MoveToAddressableAreaForDropTipImplementation, ) -> None: """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" - subject = MoveToAddressableAreaForDropTipImplementation( - movement=movement, state_view=state_view - ) - data = MoveToAddressableAreaForDropTipParams( pipetteId="abc", addressableAreaName="123", @@ -50,7 +64,9 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( force_direct=True, minimum_z_height=4.56, speed=7.89, + stay_at_highest_possible_z=False, ignore_tip_configuration=False, + highest_possible_z_extra_offset=None, ) ).then_return(Point(x=9, y=8, z=7)) @@ -66,3 +82,56 @@ async def test_move_to_addressable_area_for_drop_tip_implementation( ) ), ) + + +async def test_move_to_addressable_area_for_drop_tip_handles_stalls( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToAddressableAreaForDropTipImplementation, +) -> None: + """A MoveToAddressableAreaForDropTip command should have an execution implementation.""" + data = MoveToAddressableAreaForDropTipParams( + pipetteId="abc", + addressableAreaName="123", + offset=AddressableOffsetVector(x=1, y=2, z=3), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + alternateDropLocation=True, + ignoreTipConfiguration=False, + ) + + decoy.when( + state_view.geometry.get_next_tip_drop_location_for_addressable_area( + addressable_area_name="123", pipette_id="abc" + ) + ).then_return(AddressableOffsetVector(x=10, y=11, z=12)) + + decoy.when( + await movement.move_to_addressable_area( + pipette_id="abc", + addressable_area_name="123", + offset=AddressableOffsetVector(x=10, y=11, z=12), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + stay_at_highest_possible_z=False, + ignore_tip_configuration=False, + highest_possible_z_extra_offset=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + timestamp = datetime.now() + test_id = "test-id" + decoy.when(model_utils.generate_id()).then_return(test_id) + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + + result = await subject.execute(data) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py index 2e3ada1d3d3..85afb189988 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_coordinates.py @@ -1,14 +1,19 @@ """Test move-to-coordinates commands.""" -from decoy import Decoy +from datetime import datetime + +import pytest +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError -from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.execution import MovementHandler from opentrons.protocol_engine.state import update_types -from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import DeckPoint +from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_coordinates import ( MoveToCoordinatesParams, MoveToCoordinatesResult, @@ -16,26 +21,18 @@ ) +@pytest.fixture +def subject( + movement: MovementHandler, model_utils: ModelUtils +) -> MoveToCoordinatesImplementation: + """Build a command implementation with injected dependencies.""" + return MoveToCoordinatesImplementation(movement=movement, model_utils=model_utils) + + async def test_move_to_coordinates_implementation( - decoy: Decoy, - state_view: StateView, - hardware_api: HardwareControlAPI, - movement: MovementHandler, + decoy: Decoy, movement: MovementHandler, subject: MoveToCoordinatesImplementation ) -> None: - """Test the `moveToCoordinates` implementation. - - It should: - - 1. Query the hardware controller for the given pipette's current position - and how high it can go with its current tip. - 2. Plan the movement, taking the above into account, plus the input parameters. - 3. Iterate through the waypoints of the movement. - """ - subject = MoveToCoordinatesImplementation( - state_view=state_view, - movement=movement, - ) - + """Test the `moveToCoordinates` implementation.""" params = MoveToCoordinatesParams( pipetteId="pipette-id", coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), @@ -66,3 +63,42 @@ async def test_move_to_coordinates_implementation( ) ), ) + + +async def test_move_to_coordinates_stall( + decoy: Decoy, + movement: MovementHandler, + model_utils: ModelUtils, + subject: MoveToCoordinatesImplementation, +) -> None: + """It should handle stall errors.""" + params = MoveToCoordinatesParams( + pipetteId="pipette-id", + coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), + minimumZHeight=1234, + forceDirect=True, + speed=567.8, + ) + + decoy.when( + await movement.move_to_coordinates( + pipette_id="pipette-id", + deck_coordinates=DeckPoint(x=1.11, y=2.22, z=3.33), + direct=True, + additional_min_travel_z=1234, + speed=567.8, + ) + ).then_raise(StallOrCollisionDetectedError()) + test_id = "test-id" + timestamp = datetime.now() + decoy.when(model_utils.get_timestamp()).then_return(timestamp) + decoy.when(model_utils.generate_id()).then_return(test_id) + + result = await subject.execute(params=params) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=test_id, createdAt=timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index 61863363656..484cbb23835 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -1,6 +1,11 @@ """Test move to well commands.""" + +from datetime import datetime + import pytest -from decoy import Decoy +from decoy import Decoy, matchers + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError from opentrons.protocol_engine import ( WellLocation, @@ -12,13 +17,15 @@ from opentrons.protocol_engine.state import update_types from opentrons.types import Point -from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData from opentrons.protocol_engine.commands.move_to_well import ( MoveToWellParams, MoveToWellResult, MoveToWellImplementation, ) +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.resources.model_utils import ModelUtils @pytest.fixture @@ -27,13 +34,22 @@ def mock_state_view(decoy: Decoy) -> StateView: return decoy.mock(cls=StateView) +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + async def test_move_to_well_implementation( decoy: Decoy, state_view: StateView, movement: MovementHandler, + mock_model_utils: ModelUtils, ) -> None: """A MoveToWell command should have an execution implementation.""" - subject = MoveToWellImplementation(state_view=state_view, movement=movement) + subject = MoveToWellImplementation( + state_view=state_view, movement=movement, model_utils=mock_model_utils + ) data = MoveToWellParams( pipetteId="abc", @@ -77,9 +93,12 @@ async def test_move_to_well_with_tip_rack_and_volume_offset( decoy: Decoy, mock_state_view: StateView, movement: MovementHandler, + mock_model_utils: ModelUtils, ) -> None: """It should disallow movement to a tip rack when volumeOffset is specified.""" - subject = MoveToWellImplementation(state_view=mock_state_view, movement=movement) + subject = MoveToWellImplementation( + state_view=mock_state_view, movement=movement, model_utils=mock_model_utils + ) data = MoveToWellParams( pipetteId="abc", @@ -95,3 +114,52 @@ async def test_move_to_well_with_tip_rack_and_volume_offset( with pytest.raises(errors.LabwareIsTipRackError): await subject.execute(data) + + +async def test_move_to_well_stall_defined_error( + decoy: Decoy, + mock_state_view: StateView, + movement: MovementHandler, + mock_model_utils: ModelUtils, +) -> None: + """It should catch StallOrCollisionError exceptions and make them DefinedErrors.""" + error_id = "error-id" + error_timestamp = datetime(year=2020, month=1, day=2) + decoy.when( + movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + force_direct=True, + minimum_z_height=4.56, + speed=7.89, + current_well=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + decoy.when(mock_model_utils.generate_id()).then_return(error_id) + decoy.when(mock_model_utils.get_timestamp()).then_return(error_timestamp) + + subject = MoveToWellImplementation( + state_view=mock_state_view, movement=movement, model_utils=mock_model_utils + ) + + data = MoveToWellParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + ) + + result = await subject.execute(data) + assert isinstance(result, DefinedErrorData) + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, createdAt=error_timestamp, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate(pipette_location=update_types.CLEAR), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 00dad1557d2..07170e08288 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -1,9 +1,13 @@ """Test pick up tip commands.""" + from datetime import datetime from decoy import Decoy, matchers from unittest.mock import sentinel + +from opentrons_shared_data.errors.exceptions import StallOrCollisionDetectedError + from opentrons.types import MountType, Point from opentrons.protocol_engine import ( @@ -19,6 +23,7 @@ from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.types import TipGeometry +from opentrons.protocol_engine.commands.movement_common import StallOrCollisionError from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData from opentrons.protocol_engine.commands.pick_up_tip import ( PickUpTipParams, @@ -199,3 +204,61 @@ async def test_tip_physically_missing_error( ), ), ) + + +async def test_stall_error( + decoy: Decoy, + state_view: StateView, + movement: MovementHandler, + tip_handler: TipHandler, + model_utils: ModelUtils, +) -> None: + """It should return a TipPhysicallyMissingError if the HW API indicates that.""" + subject = PickUpTipImplementation( + state_view=state_view, + movement=movement, + tip_handler=tip_handler, + model_utils=model_utils, + ) + + pipette_id = "pipette-id" + labware_id = "labware-id" + well_name = "well-name" + error_id = "error-id" + error_created_at = datetime(1234, 5, 6) + + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset()) + ) + ).then_return(WellLocation(offset=WellOffset())) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id", + labware_id="labware-id", + well_name="well-name", + well_location=WellLocation(offset=WellOffset()), + current_well=None, + force_direct=False, + minimum_z_height=None, + speed=None, + operation_volume=None, + ) + ).then_raise(StallOrCollisionDetectedError()) + + decoy.when(model_utils.generate_id()).then_return(error_id) + decoy.when(model_utils.get_timestamp()).then_return(error_created_at) + + result = await subject.execute( + PickUpTipParams(pipetteId=pipette_id, labwareId=labware_id, wellName=well_name) + ) + + assert result == DefinedErrorData( + public=StallOrCollisionError.construct( + id=error_id, createdAt=error_created_at, wrappedErrors=[matchers.Anything()] + ), + state_update=update_types.StateUpdate( + pipette_location=update_types.CLEAR, + ), + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py index c0ec729aefe..0d4071efd6c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py @@ -1,10 +1,12 @@ """Test touch tip commands.""" + import pytest from decoy import Decoy from opentrons.hardware_control.types import CriticalPoint from opentrons.motion_planning import Waypoint from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint, errors +from opentrons.protocol_engine.resources import ModelUtils from opentrons.protocol_engine.execution import MovementHandler, GantryMover from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView @@ -24,6 +26,12 @@ def mock_state_view(decoy: Decoy) -> StateView: return decoy.mock(cls=StateView) +@pytest.fixture +def mock_model_utils(decoy: Decoy) -> ModelUtils: + """Get a mock ModelUtils.""" + return decoy.mock(cls=ModelUtils) + + @pytest.fixture def mock_movement_handler(decoy: Decoy) -> MovementHandler: """Get a mock MovementHandler.""" @@ -41,12 +49,14 @@ def subject( mock_state_view: StateView, mock_movement_handler: MovementHandler, mock_gantry_mover: GantryMover, + mock_model_utils: ModelUtils, ) -> TouchTipImplementation: """Get the test subject.""" return TouchTipImplementation( state_view=mock_state_view, movement=mock_movement_handler, gantry_mover=mock_gantry_mover, + model_utils=mock_model_utils, ) From 8ed0043868e21ef7d2e60ca560d634f9e985e340 Mon Sep 17 00:00:00 2001 From: syao1226 <146495172+syao1226@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:17:47 -0500 Subject: [PATCH 06/52] fix(protocol-designer): fix move step form title (#16883) fix RQA-3406 # Overview changing "move labware" to "move" in step form and addStepOverflow menu. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --------- Co-authored-by: shiyaochen --- protocol-designer/src/assets/localization/en/application.json | 2 +- .../src/components/__tests__/StepCreationButton.test.tsx | 2 +- .../src/containers/__tests__/ConnectedStepItem.test.tsx | 2 +- protocol-designer/src/steplist/formLevel/createBlankForm.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/application.json b/protocol-designer/src/assets/localization/en/application.json index b4d1ebf8157..0cbdb9cc6d3 100644 --- a/protocol-designer/src/assets/localization/en/application.json +++ b/protocol-designer/src/assets/localization/en/application.json @@ -34,7 +34,7 @@ "heaterShaker": "heater-shaker", "magnet": "magnet", "mix": "mix", - "moveLabware": "move labware", + "moveLabware": "move", "moveLiquid": "transfer", "pause": "pause", "profile_settings": "profile settings", diff --git a/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx b/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx index 2ffeb22bfe0..86dd338c6d0 100644 --- a/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx +++ b/protocol-designer/src/components/__tests__/StepCreationButton.test.tsx @@ -37,7 +37,7 @@ describe('StepCreationButton', () => { const addStep = screen.getByRole('button', { name: '+ Add Step' }) fireEvent.click(addStep) screen.getByText('comment') - screen.getByText('move labware') + screen.getByText('move') screen.getByText('transfer') screen.getByText('mix') screen.getByText('pause') diff --git a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx index 01cb48e5bf7..4fe43e2f016 100644 --- a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx +++ b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx @@ -439,7 +439,7 @@ describe('ConnectedStepItem', () => { vi.mocked(getSelectedStepId).mockReturnValue(moveLabwareStepId) props.stepId = moveLabwareStepId render(props) - screen.getByText('2. move labware') + screen.getByText('2. move') screen.getByText('Manually') screen.getByText('labware') screen.getByText('new location') diff --git a/protocol-designer/src/steplist/formLevel/createBlankForm.ts b/protocol-designer/src/steplist/formLevel/createBlankForm.ts index c8e22a1fb88..764529dc379 100644 --- a/protocol-designer/src/steplist/formLevel/createBlankForm.ts +++ b/protocol-designer/src/steplist/formLevel/createBlankForm.ts @@ -17,7 +17,7 @@ const getStepType = (stepType: StepType): string => { return 'heater-shaker' } case 'moveLabware': { - return 'move labware' + return 'move' } case 'moveLiquid': { return 'transfer' From 9cbc2cbbf79571faa159a993de2dde03981475a3 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 19 Nov 2024 11:38:21 -0500 Subject: [PATCH 07/52] fix(protocol-designer,components): fix dropdown menu expands direction issue (#16841) * fix(protocol-designer,components): fix dropdown menu expands direction issue --- .../ListItemChildren/ListItemCustomize.tsx | 8 ++++---- .../src/molecules/DropdownMenu/index.tsx | 20 +++++++++++++------ .../AssignLiquidsModal/LiquidToolbox.tsx | 1 + .../SelectFixtures.tsx | 2 +- .../CreateNewProtocolWizard/SelectModules.tsx | 2 +- .../StepForm/StepFormToolbox.tsx | 6 +++++- .../StepTools/MoveLabwareTools/index.tsx | 1 + .../ThermocyclerTools/ThermocyclerState.tsx | 3 ++- 8 files changed, 29 insertions(+), 14 deletions(-) diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx index b9e31ad2782..7eb96e0bac4 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx @@ -18,8 +18,8 @@ interface ListItemCustomizeProps { label?: string dropdown?: DropdownMenuProps tag?: TagProps - /** temporary prop for dropdown menu */ - forceDirection?: boolean + /** optional placement of the menu */ + menuPlacement?: 'auto' | 'top' | 'bottom' } export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { @@ -31,7 +31,7 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { linkText, dropdown, tag, - forceDirection = false, + menuPlacement = 'auto', } = props return ( @@ -53,7 +53,7 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { ) : null} {dropdown != null ? ( - + ) : null} {tag != null ? : null} diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index af24cbf075d..acfba246dc2 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -69,8 +69,8 @@ export interface DropdownMenuProps { onBlur?: React.FocusEventHandler /** optional disabled */ disabled?: boolean - /** force direction for pd after release this will be fixed and remove */ - forceDirection?: boolean + /** optional placement of the menu */ + menuPlacement?: 'auto' | 'top' | 'bottom' } // TODO: (smb: 4/15/22) refactor this to use html select for accessibility @@ -90,7 +90,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { disabled = false, onFocus, onBlur, - forceDirection = false, + menuPlacement = 'auto', } = props const [targetProps, tooltipProps] = useHoverTooltip() const [showDropdownMenu, setShowDropdownMenu] = React.useState(false) @@ -108,7 +108,11 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { }) React.useEffect(() => { - if (forceDirection) return + if (menuPlacement !== 'auto') { + setDropdownPosition(menuPlacement) + return + } + const handlePositionCalculation = (): void => { const dropdownRect = dropDownMenuWrapperRef.current?.getBoundingClientRect() if (dropdownRect != null) { @@ -253,7 +257,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { : TYPOGRAPHY.pRegular}; `} > - + {currentOption.name} @@ -325,11 +332,12 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { ) } -const MENU_TEXT_STYLE = css` +const LINE_CLAMP_TEXT_STYLE = css` display: -webkit-box; -webkit-box-orient: vertical; overflow: ${OVERFLOW_HIDDEN}; text-overflow: ellipsis; word-wrap: break-word; -webkit-line-clamp: 1; + word-break: break-all; ` diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index 65c3243590d..b7509372dcd 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -331,6 +331,7 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { liquidColor: selectLiquidColor, }} onClick={field.onChange} + menuPlacement="bottom" /> ) }} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 023b588d7c0..54a0c6b499c 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -158,7 +158,7 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { return ( { setValue( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 105fc2ecea5..6533e440804 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -258,7 +258,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { return ( } > -
+
{robotType === FLEX_ROBOT_TYPE ? ( <> diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx index c3f94123220..b2183d7b678 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx @@ -64,7 +64,8 @@ export function ThermocyclerState(props: ThermocyclerStateProps): JSX.Element { From 44a80fbc36336ac1c0f59798cbadb531db0b2915 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:00:25 -0500 Subject: [PATCH 08/52] feat(protocol-designer): mixpanel events pt2 redesign (#16758) closes AUTH-1017 --- .../reduxActionToAnalyticsEvent.test.ts | 4 +- protocol-designer/src/analytics/constants.ts | 7 + protocol-designer/src/analytics/middleware.ts | 175 ++++++++++++++---- .../CreateNewProtocolWizard/AddMetadata.tsx | 3 +- .../StepForm/StepFormToolbox.tsx | 30 ++- .../Timeline/StepOverflowMenu.tsx | 3 +- .../src/ui/steps/actions/actions.ts | 11 +- 7 files changed, 195 insertions(+), 38 deletions(-) create mode 100644 protocol-designer/src/analytics/constants.ts diff --git a/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts b/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts index db903a1f824..5f5c9aa4012 100644 --- a/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts +++ b/protocol-designer/src/analytics/__tests__/reduxActionToAnalyticsEvent.test.ts @@ -150,11 +150,13 @@ describe('reduxActionToAnalyticsEvent', () => { expect(result).toEqual({ name: 'transferStep', properties: { - commandCreatorFnName: 'transfer', // existing fields id: 'stepId', pipette: 'pipetteId', otherField: 123, + aspirateFlowRate: undefined, + dispenseFlowRate: undefined, + aspirateAirGap: undefined, nested: { inner: true }, // de-nested fields __nested__inner: true, diff --git a/protocol-designer/src/analytics/constants.ts b/protocol-designer/src/analytics/constants.ts new file mode 100644 index 00000000000..a9f0d9a5e7c --- /dev/null +++ b/protocol-designer/src/analytics/constants.ts @@ -0,0 +1,7 @@ +export const FORM_WARNINGS_EVENT = 'formWarnings' +export const FORM_ERRORS_EVENT = 'formErrors' +export const OPEN_STEP_DETAILS_EVENT = 'openStepDetails' +export const ONBOARDING_FLOW_DURATION_EVENT = 'onboardingFlowDuration' +export const SELECT_ALL_STEPS_EVENT = 'selectAllSteps' +export const EXIT_BATCH_EDIT_MODE_EVENT = 'exitBatchEditMode' +export const DESELECT_ALL_STEPS_EVENT = 'deselectAllSteps' diff --git a/protocol-designer/src/analytics/middleware.ts b/protocol-designer/src/analytics/middleware.ts index 514f4a59b14..6c798e353ff 100644 --- a/protocol-designer/src/analytics/middleware.ts +++ b/protocol-designer/src/analytics/middleware.ts @@ -1,4 +1,5 @@ import uniq from 'lodash/uniq' +import omit from 'lodash/omit' import { FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, MOVABLE_TRASH_ADDRESSABLE_AREAS, @@ -16,13 +17,23 @@ import { getFileMetadata, getRobotStateTimeline, } from '../file-data/selectors' -import { FIXED_TRASH_ID } from '../constants' +import { + DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + DEFAULT_MM_FROM_BOTTOM_DISPENSE, + FIXED_TRASH_ID, +} from '../constants' import { trackEvent } from './mixpanel' import { getHasOptedIn } from './selectors' import { flattenNestedProperties } from './utils/flattenNestedProperties' import type { Middleware } from 'redux' -import type { NormalizedPipetteById } from '@opentrons/step-generation' +import type { + ConsolidateArgs, + DistributeArgs, + MixArgs, + NormalizedPipetteById, + TransferArgs, +} from '@opentrons/step-generation' import type { AddressableAreaName, LoadLabwareCreateCommand, @@ -39,6 +50,20 @@ import type { SetFeatureFlagAction } from '../feature-flags/actions' import type { CreatePipettesAction } from '../step-forms/actions' import type { AnalyticsEventAction } from './actions' import type { AnalyticsEvent } from './mixpanel' + +const DEFAULT_VALUE = 'default' +const PIPETTING_ARGS_FILTER_LIST = [ + 'touchTipAfterAspirateOffsetMmFromBottom', + 'touchTipAfterDispenseOffsetMmFromBottom', + 'commandCreatorFnName', + 'blowoutFlowRateUlSec', + 'blowoutOffsetFromTopMm', + 'touchTipMmFromBottom', + 'aspirateAirGapVolume', + 'aspirateFlowRateUlSec', + 'dispenseFlowRateUlSec', +] + interface TransformedPipetteInfo { [pipetteId: string]: { name: string @@ -94,43 +119,131 @@ export const reduxActionToAnalyticsEvent = ( ? 'pause' : stepName - if ( - modifiedStepName === 'engageMagnet' || - modifiedStepName === 'disengageMagnet' - ) { - return { - name: `magnetStep`, - properties: { type: modifiedStepName }, + switch (modifiedStepName) { + case 'engageMagnet': + case 'disengageMagnet': { + return { + name: `magnetStep`, + properties: { type: modifiedStepName }, + } + } + case 'setTemperature': + case 'deactivateTemperature': { + return { + name: `temperatureStep`, + properties: { type: modifiedStepName }, + } } - } else if ( - modifiedStepName === 'deactivateTemperature' || - modifiedStepName === 'setTemperature' - ) { - return { - name: `temperatureStep`, - properties: { type: modifiedStepName }, + case 'thermocyclerProfile': + case 'thermocyclerState': { + return { + name: 'thermocyclerStep', + properties: { type: modifiedStepName }, + } } - } else if ( - modifiedStepName === 'thermocyclerProfile' || - modifiedStepName === 'thermocyclerState' - ) { - return { - name: 'thermocyclerStep', - properties: { type: modifiedStepName }, + case 'heaterShaker': { + return { + name: 'heaterShakerStep', + properties: {}, + } } - } else if (modifiedStepName === 'heaterShaker') { - return { - name: 'heaterShakerStep', - properties: {}, + case 'transfer': + case 'consolidate': + case 'distribute': { + const stepArgModified = omit( + stepArgs as TransferArgs | ConsolidateArgs | DistributeArgs, + PIPETTING_ARGS_FILTER_LIST + ) + return { + name: `${modifiedStepName}Step`, + properties: { + ...stepArgModified, + aspirateAirGap: stepArgModified.aspirateAirGapVolume, + aspirateFlowRate: stepArgModified.aspirateFlowRateUlSec, + dispenseFlowRate: stepArgModified.dispenseFlowRateUlSec, + blowoutFlowRate: stepArgModified.blowoutFlowRateUlSec, + aspirateOffsetFromBottomMm: + stepArgModified.aspirateOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_ASPIRATE + ? DEFAULT_VALUE + : stepArgModified.aspirateOffsetFromBottomMm, + dispenseOffsetFromBottomMm: + stepArgModified.dispenseOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_DISPENSE + ? DEFAULT_VALUE + : stepArgModified.dispenseOffsetFromBottomMm, + aspirateXOffset: + stepArgModified.aspirateXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateXOffset, + aspirateYOffset: + stepArgModified.aspirateYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateYOffset, + dispenseXOffset: + stepArgModified.dispenseXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseXOffset, + dispenseYOffset: + stepArgModified.dispenseYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseYOffset, + + ...additionalProperties, + }, + } } - } else { - return { - name: `${modifiedStepName}Step`, - properties: { ...stepArgs, ...additionalProperties }, + case 'mix': { + const stepArgModified = omit( + stepArgs as MixArgs, + PIPETTING_ARGS_FILTER_LIST + ) + return { + name: `mixStep`, + properties: { + ...stepArgModified, + aspirateFlowRate: stepArgModified.aspirateFlowRateUlSec, + dispenseFlowRate: stepArgModified.dispenseFlowRateUlSec, + blowoutFlowRate: stepArgModified.blowoutFlowRateUlSec, + aspirateOffsetFromBottomMm: + stepArgModified.aspirateOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_ASPIRATE + ? DEFAULT_VALUE + : stepArgModified.aspirateOffsetFromBottomMm, + dispenseOffsetFromBottomMm: + stepArgModified.dispenseOffsetFromBottomMm === + DEFAULT_MM_FROM_BOTTOM_DISPENSE + ? DEFAULT_VALUE + : stepArgModified.dispenseOffsetFromBottomMm, + aspirateXOffset: + stepArgModified.aspirateXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateXOffset, + aspirateYOffset: + stepArgModified.aspirateYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.aspirateYOffset, + dispenseXOffset: + stepArgModified.dispenseXOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseXOffset, + dispenseYOffset: + stepArgModified.dispenseYOffset === 0 + ? DEFAULT_VALUE + : stepArgModified.dispenseYOffset, + ...additionalProperties, + }, + } } + default: + return { + name: `${modifiedStepName}Step`, + properties: { ...stepArgs, ...additionalProperties }, + } } } } + if (action.type === 'COMPUTE_ROBOT_STATE_TIMELINE_SUCCESS') { const robotTimeline = getRobotStateTimeline(state) const { errors, timeline } = robotTimeline diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx index 24e7b67dc85..afb8e83e9c7 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx @@ -13,6 +13,7 @@ import { import { DescriptionField } from '../../atoms' import { HandleEnter } from '../../atoms/HandleEnter' import { analyticsEvent } from '../../analytics/actions' +import { ONBOARDING_FLOW_DURATION_EVENT } from '../../analytics/constants' import { WizardBody } from './WizardBody' import type { AnalyticsEvent } from '../../analytics/mixpanel' @@ -33,7 +34,7 @@ export function AddMetadata(props: AddMetadataProps): JSX.Element | null { const handleProceed = (): void => { const duration = new Date().getTime() - analyticsStartTime.getTime() const onboardingDuration: AnalyticsEvent = { - name: 'onboardingFlowDuration', + name: ONBOARDING_FLOW_DURATION_EVENT, properties: { duration: `${duration / 1000} seconds` }, } dispatch(analyticsEvent(onboardingDuration)) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index 5c123fa39a2..5b58ac60c5a 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import get from 'lodash/get' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -29,6 +29,10 @@ import { getFormLevelErrorsForUnsavedForm, getDynamicFieldFormErrorsForUnsavedForm, } from '../../../../step-forms/selectors' +import { + FORM_ERRORS_EVENT, + FORM_WARNINGS_EVENT, +} from '../../../../analytics/constants' import { CommentTools, HeaterShakerTools, @@ -50,6 +54,7 @@ import { import type { StepFieldName } from '../../../../steplist/fieldLevel' import type { FormData, StepType } from '../../../../form-types' import type { AnalyticsEvent } from '../../../../analytics/mixpanel' +import type { FormWarningType } from '../../../../steplist' import type { FieldPropsByName, FocusHandlers, @@ -154,6 +159,29 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { error => error.tab === 'dispense' && error.page === toolboxStep ) + const visibleFormWarningsTypes = visibleFormWarnings.map( + warning => warning.type + ) + const visibleFormErrorsTypes = visibleFormErrors.map(error => error.title) + + useEffect(() => { + const dispatchAnalyticsEvent = ( + eventName: string, + eventProperties: FormWarningType[] | string[] + ): void => { + if (eventProperties.length > 0) { + const event: AnalyticsEvent = { + name: eventName, + properties: { eventProperties }, + } + dispatch(analyticsEvent(event)) + } + } + + dispatchAnalyticsEvent(FORM_WARNINGS_EVENT, visibleFormWarningsTypes) + dispatchAnalyticsEvent(FORM_ERRORS_EVENT, visibleFormErrorsTypes) + }, [visibleFormWarningsTypes, visibleFormErrorsTypes]) + if (!ToolsComponent) { // early-exit if step form doesn't exist, this is a good check for when new steps // are added diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx index 7418746c882..5bb125b3269 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx @@ -17,6 +17,7 @@ import { hoverOnStep, toggleViewSubstep, } from '../../../../ui/steps/actions/actions' +import { OPEN_STEP_DETAILS_EVENT } from '../../../../analytics/constants' import { getBatchEditFormHasUnsavedChanges, getCurrentFormHasUnsavedChanges, @@ -81,7 +82,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { } const selectViewDetailsEvent: AnalyticsEvent = { - name: 'openStepDetails', + name: OPEN_STEP_DETAILS_EVENT, properties: {}, } diff --git a/protocol-designer/src/ui/steps/actions/actions.ts b/protocol-designer/src/ui/steps/actions/actions.ts index da7757c1b21..ab3a0f19263 100644 --- a/protocol-designer/src/ui/steps/actions/actions.ts +++ b/protocol-designer/src/ui/steps/actions/actions.ts @@ -2,6 +2,11 @@ import last from 'lodash/last' import { analyticsEvent } from '../../../analytics/actions' import { PRESAVED_STEP_ID } from '../../../steplist/types' import { selectors as stepFormSelectors } from '../../../step-forms' +import { + DESELECT_ALL_STEPS_EVENT, + EXIT_BATCH_EDIT_MODE_EVENT, + SELECT_ALL_STEPS_EVENT, +} from '../../../analytics/constants' import { getMultiSelectLastSelected } from '../selectors' import { resetScrollElements } from '../utils' import type { Timeline } from '@opentrons/step-generation' @@ -187,7 +192,7 @@ export const selectAllSteps = (): ThunkAction< // dispatch an analytics event to indicate all steps have been selected // because there is no 'SELECT_ALL_STEPS' action that middleware can catch const selectAllStepsEvent: AnalyticsEvent = { - name: 'selectAllSteps', + name: SELECT_ALL_STEPS_EVENT, properties: {}, } dispatch(analyticsEvent(selectAllStepsEvent)) @@ -220,13 +225,13 @@ export const deselectAllSteps = ( // for analytics purposes we want to differentiate between // deselecting all, and using the "exit batch edit mode" button const exitBatchEditModeEvent: AnalyticsEvent = { - name: 'exitBatchEditMode', + name: EXIT_BATCH_EDIT_MODE_EVENT, properties: {}, } dispatch(analyticsEvent(exitBatchEditModeEvent)) } else { const deselectAllStepsEvent: AnalyticsEvent = { - name: 'deselectAllSteps', + name: DESELECT_ALL_STEPS_EVENT, properties: {}, } dispatch(analyticsEvent(deselectAllStepsEvent)) From 1c4385c3846dbe72175e7c611fff71ac956b60e2 Mon Sep 17 00:00:00 2001 From: syao1226 <146495172+syao1226@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:15:22 -0500 Subject: [PATCH 09/52] =?UTF-8?q?fix(protocol-designer):=20fix=20well=20se?= =?UTF-8?q?lection=20and=20uncessary=20mixes=20detail=E2=80=A6=20(#16850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …s with partial-tip 96 channel fix RQA-3564, RQA-3619, RQA-3620 # Overview In this PR, I’ve fixed the unnecessary mixes in the transfer details when choosing nozzles as a column, cleared out the selected wells when switching nozzles, and ensured the liquid is correctly shown as mixed when aspirating and dispensing into the same well of the same labware. ## Test Plan and Hands on Testing ## Changelog - Used `useEffect()` to reset `primaryWellCount` and `selectedWells` when `nozzleType` changes - Added optional `isSameLabware` prop to check whether its source and dest labware are the same - Added a filter to exclude substep details with undefined source wells (unselected wells when using column nozzles) ## Review requests ## Risk assessment --------- Co-authored-by: shiyaochen Co-authored-by: shiyaochen --- .../src/organisms/SelectWellsModal/index.tsx | 25 +++--- .../PipetteFields/WellSelectionField.tsx | 19 ++++- .../Timeline/MultichannelSubstep.tsx | 3 + .../Timeline/PipettingSubsteps.tsx | 43 ++++++---- .../ProtocolSteps/Timeline/Substep.tsx | 78 ++++++++++--------- .../dependentFieldsUpdateMix.ts | 19 +++++ .../dependentFieldsUpdateMoveLiquid.ts | 20 +++++ 7 files changed, 144 insertions(+), 63 deletions(-) diff --git a/protocol-designer/src/organisms/SelectWellsModal/index.tsx b/protocol-designer/src/organisms/SelectWellsModal/index.tsx index 57ce723f278..7a1946861c2 100644 --- a/protocol-designer/src/organisms/SelectWellsModal/index.tsx +++ b/protocol-designer/src/organisms/SelectWellsModal/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import omit from 'lodash/omit' @@ -65,15 +65,22 @@ export const SelectWellsModal = ( (labwareId != null ? labwareEntities[labwareId]?.def : null) ?? null const pipette = pipetteId != null ? pipetteEntities[pipetteId] : null - const initialSelectedPrimaryWells = Array.isArray(wellFieldData) - ? arrayToWellGroup(wellFieldData as string[]) - : {} + const [selectedPrimaryWells, setSelectedPrimaryWells] = useState( + Array.isArray(wellFieldData) + ? arrayToWellGroup(wellFieldData as string[]) + : {} + ) + useEffect(() => { + if (Array.isArray(wellFieldData)) { + setSelectedPrimaryWells( + wellFieldData.length === 0 + ? {} + : arrayToWellGroup(wellFieldData as string[]) + ) + } + }, [wellFieldData]) - const [ - selectedPrimaryWells, - setSelectedPrimaryWells, - ] = React.useState(initialSelectedPrimaryWells) - const [highlightedWells, setHighlightedWells] = React.useState({}) + const [highlightedWells, setHighlightedWells] = useState({}) if (!isOpen) return null diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx index 25f65cfa320..b1e332b662c 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -58,12 +59,28 @@ export const WellSelectionField = ( const stepId = useSelector(getSelectedStepId) const pipetteEntities = useSelector(stepFormSelectors.getPipetteEntities) const wellSelectionLabwareKey = useSelector(getWellSelectionLabwareKey) - const primaryWellCount = + + const calculateWellCount = Array.isArray(selectedWells) && selectedWells.length > 0 ? selectedWells.length.toString() : null + + const [primaryWellCount, setPrimaryWellCount] = useState(calculateWellCount) const pipette = pipetteId != null ? pipetteEntities[pipetteId] : null const nozzleType = getNozzleType(pipette, nozzles) + const previousNozzleType = useRef(nozzleType) + + useEffect(() => { + if (previousNozzleType.current !== nozzleType) { + setPrimaryWellCount(null) + updateValue([]) + previousNozzleType.current = nozzleType + } + }, [nozzleType, updateValue]) + + useEffect(() => { + setPrimaryWellCount(calculateWellCount) + }, [selectedWells]) const getModalKey = (): string => { return `${String(stepId)}${name}${pipetteId || 'noPipette'}${ diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx index 6dd9aebf527..920ee3c0938 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx @@ -28,6 +28,7 @@ interface MultichannelSubstepProps { substepIndex: number selectSubstep: (substepIdentifier: SubstepIdentifier) => void highlighted?: boolean + isSameLabware?: boolean } export function MultichannelSubstep( @@ -40,6 +41,7 @@ export function MultichannelSubstep( substepIndex, ingredNames, trashName, + isSameLabware, } = props const { t } = useTranslation('application') const [collapsed, setCollapsed] = useState(true) @@ -110,6 +112,7 @@ export function MultichannelSubstep( dest={row.dest} stepId={stepId} substepIndex={substepIndex} + isSameLabware={isSameLabware} /> ) })} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx index 7973c5ef376..5c2be0bc9fa 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx @@ -30,23 +30,33 @@ export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { ? additionalEquipment[destLocationId]?.name : null + const isSameLabware = formData.aspirate_labware === formData.dispense_labware + const renderSubsteps = substeps.multichannel - ? substeps.multiRows.map((rowGroup, groupKey) => ( - - )) + ? substeps.multiRows.map((rowGroup, groupKey) => { + const filteredRowGroup = rowGroup.filter( + item => item.source !== undefined + ) + if (filteredRowGroup.length === 0) return null + + return ( + + ) + }) : substeps.rows.map((row, substepIndex) => ( )) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx index a731a975b48..a34dc799337 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx @@ -39,6 +39,7 @@ interface SubstepProps { source?: SubstepWellData dest?: SubstepWellData selectSubstep?: (substepIdentifier: SubstepIdentifier) => void + isSameLabware?: boolean } function SubstepComponent(props: SubstepProps): JSX.Element { @@ -51,6 +52,7 @@ function SubstepComponent(props: SubstepProps): JSX.Element { dest, trashName, selectSubstep: propSelectSubstep, + isSameLabware, } = props const { t } = useTranslation(['application', 'protocol_steps', 'shared']) const compactedSourcePreIngreds = source @@ -77,7 +79,7 @@ function SubstepComponent(props: SubstepProps): JSX.Element { /> ) - const isMix = source?.well === dest?.well + const isMix = source?.well === dest?.well && isSameLabware return ( - - {ingredIds.length > 0 ? ( - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {dest != null || trashName != null ? ( - - - {t('protocol_steps:dispensed')} - - {volumeTag} - - {t('protocol_steps:into')} - + {dest !== undefined ? ( + + {ingredIds.length > 0 ? ( + + + + {ingredIds.map(groupId => ingredNames[groupId]).join(',')} + + + ) : null} + {dest != null || trashName != null ? ( + + + {t('protocol_steps:dispensed')} + + {volumeTag} + + {t('protocol_steps:into')} + - - - ) : null} - + + + ) : null} + + ) : null} )} diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts index de8a28df5c3..ab2a53eba81 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts @@ -164,6 +164,23 @@ const updatePatchOnTiprackChange = ( return patch } +const updatePatchOnNozzleChange = ( + patch: FormPatch, + rawForm: FormData, + pipetteEntities: PipetteEntities +): FormPatch => { + if ( + Object.values(pipetteEntities).find(pip => pip.spec.channels === 96) && + fieldHasChanged(rawForm, patch, 'nozzles') + ) { + return { + ...patch, + ...getDefaultFields('wells'), + } + } + return patch +} + export function dependentFieldsUpdateMix( originalPatch: FormPatch, rawForm: FormData, // raw = NOT hydrated @@ -188,5 +205,7 @@ export function dependentFieldsUpdateMix( ), chainPatch => updatePatchOnPipetteChange(chainPatch, rawForm), chainPatch => updatePatchOnTiprackChange(chainPatch, rawForm), + chainPatch => + updatePatchOnNozzleChange(chainPatch, rawForm, pipetteEntities), ]) } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts index 752a6629386..c473af451ea 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts @@ -669,6 +669,24 @@ export function updatePatchBlowoutFields( return patch } + +const updatePatchOnNozzleChange = ( + patch: FormPatch, + rawForm: FormData, + pipetteEntities: PipetteEntities +): FormPatch => { + if ( + Object.values(pipetteEntities).find(pip => pip.spec.channels === 96) && + fieldHasChanged(rawForm, patch, 'nozzles') + ) { + return { + ...patch, + ...getDefaultFields('aspirate_wells', 'dispense_wells'), + } + } + return patch +} + export function dependentFieldsUpdateMoveLiquid( originalPatch: FormPatch, rawForm: FormData, // raw = NOT hydrated @@ -706,5 +724,7 @@ export function dependentFieldsUpdateMoveLiquid( clampDispenseAirGapVolume(chainPatch, rawForm, pipetteEntities), chainPatch => updatePatchOnTiprackChange(chainPatch, rawForm, pipetteEntities), + chainPatch => + updatePatchOnNozzleChange(chainPatch, rawForm, pipetteEntities), ]) } From b7a554033c678a93a463e1e691d1ae4a5c49d16e Mon Sep 17 00:00:00 2001 From: Elyor Kodirov Date: Wed, 20 Nov 2024 00:19:41 +0500 Subject: [PATCH 10/52] feat(opentrons-ai-server): anthropic integration (#16881) # Overview closes AUTH-1050 This PR introduces Anthropic model, named `claude-3.5-sonnet` in addition to OpenAI. ## Test Plan and Hands on Testing Please interact with UI ## Changelog - integrated anthropic model and set it as default ## Review requests **Review scripts:** - `opentrons-ai-server/api/domain/anthropic_predict.py` - `opentrons-ai-server/api/domain/config_anthropic.py` - `opentrons-ai-server/api/handler/fast.py` **UI** - Please interact with UI and create protocols. - not required to check `storage/docs` unless if you have time ## Risk assessment Low --- opentrons-ai-server/Pipfile | 1 + opentrons-ai-server/Pipfile.lock | 256 ++-- .../api/domain/anthropic_predict.py | 206 +++ .../api/domain/config_anthropic.py | 217 +++ opentrons-ai-server/api/handler/fast.py | 30 +- opentrons-ai-server/api/settings.py | 3 + .../api/storage/docs/OT2ToFlex.md | 209 +++ .../api/storage/docs/casual_examples.md | 979 +++++++++++++ .../api/storage/docs/commands-v0.0.1.md | 1259 ++++++++++++++++ .../api/storage/docs/deck_layout.md | 167 +++ .../api/storage/docs/full-examples.md | 1263 +++++++++++++++++ .../api/storage/docs/out_of_tips_error_219.md | 382 +++++ .../storage/docs/pcr_protocols_with_csv.md | 1065 ++++++++++++++ .../storage/docs/serial_dilution_samples.md | 842 +++++++++++ .../api/storage/docs/standard-api-v0.0.1.md | 157 ++ .../storage/docs/transfer_function_notes.md | 651 +++++++++ 16 files changed, 7595 insertions(+), 92 deletions(-) create mode 100644 opentrons-ai-server/api/domain/anthropic_predict.py create mode 100644 opentrons-ai-server/api/domain/config_anthropic.py create mode 100644 opentrons-ai-server/api/storage/docs/OT2ToFlex.md create mode 100644 opentrons-ai-server/api/storage/docs/casual_examples.md create mode 100644 opentrons-ai-server/api/storage/docs/commands-v0.0.1.md create mode 100644 opentrons-ai-server/api/storage/docs/deck_layout.md create mode 100644 opentrons-ai-server/api/storage/docs/full-examples.md create mode 100644 opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md create mode 100644 opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md create mode 100644 opentrons-ai-server/api/storage/docs/serial_dilution_samples.md create mode 100644 opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md create mode 100644 opentrons-ai-server/api/storage/docs/transfer_function_notes.md diff --git a/opentrons-ai-server/Pipfile b/opentrons-ai-server/Pipfile index 4586798349a..5c7cdd10e16 100644 --- a/opentrons-ai-server/Pipfile +++ b/opentrons-ai-server/Pipfile @@ -20,6 +20,7 @@ asgi-correlation-id = "==4.3.3" gspread = "==6.1.4" google-auth = "==2.36.0" google-auth-oauthlib = "==1.2.1" +anthropic = "*" [dev-packages] docker = "==7.1.0" diff --git a/opentrons-ai-server/Pipfile.lock b/opentrons-ai-server/Pipfile.lock index a4b9ba0dca5..9e3821d1d23 100644 --- a/opentrons-ai-server/Pipfile.lock +++ b/opentrons-ai-server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "56aef120fbddf42f146e054b7d59ee0f59be75aa6e43f332f86b7ba8fa2499e0" + "sha256": "0b186e696fa0bf94be8a3edfa27b4f680b067a108088447add8e3ee9e4cabd91" }, "pipfile-spec": 6, "requires": { @@ -26,85 +26,85 @@ }, "aiohttp": { "hashes": [ - "sha256:024409c1b1d6076d0ed933dcebd7e4fc6f3320a227bfa0c1b6b93a8b5a146f04", - "sha256:04b24497b3baf15035730de5f207ade88a67d4483a5f16ced7ece348933a5b47", - "sha256:08474e71772a516ba2e2167b4707af8361d2c452b3d8a5364c984f4867869499", - "sha256:0e7a0762cc29cd3acd01a4d2b547b3af7956ad230ebb80b529a8e4f3e4740fe8", - "sha256:104deb7873681273c5daa13c41924693df394043a118dae90387d35bc5531788", - "sha256:104ea21994b1403e4c1b398866f1187c1694fa291314ad7216ec1d8ec6b49f38", - "sha256:113bf06b029143e94a47c4f36e11a8b7e396e9d1f1fc8cea58e6b7e370cfed38", - "sha256:12071dd2cc95ba81e0f2737bebcb98b2a8656015e87772e84e8fb9e635b5da6e", - "sha256:170fb2324826bb9f08055a8291f42192ae5ee2f25b2966c8f0f4537c61d73a7b", - "sha256:21b4545e8d96870da9652930c5198366605ff8f982757030e2148cf341e5746b", - "sha256:229ae13959a5f499d90ffbb4b9eac2255d8599315027d6f7c22fa9803a94d5b1", - "sha256:2ec5efbc872b00ddd85e3904059d274f284cff314e13f48776050ca2c58f451d", - "sha256:31b91ff3a1fcb206a1fa76e0de1f08c9ffb1dc0deb7296fa2618adfe380fc676", - "sha256:329f5059e0bf6983dceebac8e6ed20e75eaff6163b3414f4a4cb59e0d7037672", - "sha256:37f8cf3c43f292d9bb3e6760476c2b55b9663a581fad682a586a410c43a7683e", - "sha256:3e1ed8d152cccceffb1ee7a2ac227c16372e453fb11b3aeaa56783049b85d3f6", - "sha256:3ed360d6672a9423aad39902a4e9fe305464d20ed7931dbdba30a4625782d875", - "sha256:40dc9446cff326672fcbf93efdb8ef7e949824de1097624efe4f61ac7f0d2c43", - "sha256:4d218d3eca40196384ad3b481309c56fd60e664128885d1734da0a8aa530d433", - "sha256:4e4e155968040e32c124a89852a1a5426d0e920a35f4331e1b3949037bfe93a3", - "sha256:4f698aa61879df64425191d41213dfd99efdc1627e6398e6d7aa5c312fac9702", - "sha256:508cfcc99534b1282595357592d8367b44392b21f6eb5d4dc021f8d0d809e94d", - "sha256:577c7429f8869fa30186fc2c9eee64d75a30b51b61f26aac9725866ae5985cfd", - "sha256:57e17c6d71f2dc857a8a1d09be1be7802e35d90fb4ba4b06cf1aab6414a57894", - "sha256:5ecc2fb1a0a9d48cf773add34196cddf7e488e48e9596e090849751bf43098f4", - "sha256:600b1d9f86a130131915e2f2127664311b33902c486b21a747d626f5144b4471", - "sha256:62502b8ffee8c6a4b5c6bf99d1de277d42bf51b2fb713975d9b63b560150b7ac", - "sha256:62a2f5268b672087c45b33479ba1bb1d5a48c6d76c133cfce3a4f77410c200d1", - "sha256:6362f50a6f0e5482c4330d2151cb682779230683da0e155c15ec9fc58cb50b6a", - "sha256:6533dd06df3d17d1756829b68b365b1583929b54082db8f65083a4184bf68322", - "sha256:6c5a6958f4366496004cf503d847093d464814543f157ef3b738bbf604232415", - "sha256:72cd984f7f14e8c01b3e38f18f39ea85dba84e52ea05e37116ba5e2a72eef396", - "sha256:76d6ee8bb132f8ee0fcb0e205b4708ddb6fba524eb515ee168113063d825131b", - "sha256:7867d0808614f04e78e0a8d5a2c1f8ac6bc626a0c0e2f62be48be6b749e2f8b2", - "sha256:7d664e5f937c08adb7908ea9f391fbf2928a9b09cb412ac0aba602bde9e499e4", - "sha256:85ae6f182be72c3531915e90625cc65afce4df8a0fc4988bd52d8a5d5faaeb68", - "sha256:89a96a0696dc67d548f69cb518c581a7a33cc1f26ab42229dea1709217c9d926", - "sha256:8b323b5d3aef7dd811424c269322eec58a977c0c8152e650159e47210d900504", - "sha256:8c47a0ba6c2b3d3e5715f8338d657badd21f778c6be16701922c65521c5ecfc9", - "sha256:8fef105113d56e817cb9bcc609667ee461321413a7b972b03f5b4939f40f307c", - "sha256:900ff74d78eb580ae4aa5883242893b123a0c442a46570902500f08d6a7e6696", - "sha256:9095580806d9ed07c0c29b23364a0b1fb78258ef9f4bddf7e55bac0e475d4edf", - "sha256:91d3991fad8b65e5dbc13cd95669ea689fe0a96ff63e4e64ac24ed724e4f8103", - "sha256:9231d610754724273a6ac05a1f177979490bfa6f84d49646df3928af2e88cfd5", - "sha256:97056d3422594e0787733ac4c45bef58722d452f4dc6615fee42f59fe51707dd", - "sha256:a896059b6937d1a22d8ee8377cdcd097bd26cd8c653b8f972051488b9baadee9", - "sha256:aabc4e92cb153636d6be54e84dad1b252ddb9aebe077942b6dcffe5e468d476a", - "sha256:ad14cdc0fba4df31c0f6e06c21928c5b924725cbf60d0ccc5f6e7132636250e9", - "sha256:ae36ae52b0c22fb69fb8b744eff82a20db512a29eafc6e3a4ab43b17215b219d", - "sha256:b3e4fb7f5354d39490d8209aefdf5830b208d01c7293a2164e404312c3d8bc55", - "sha256:b40c304ab01e89ad0aeeecf91bbaa6ae3b00e27b796c9e8d50b71a4a7e885cc8", - "sha256:b7349205bb163318dcc102329d30be59a647a3d24c82c3d91ed35b7e7301ea7e", - "sha256:b8b95a63a8e8b5f0464bd8b1b0d59d2bec98a59b6aacc71e9be23df6989b3dfb", - "sha256:bb2e82e515e268b965424ecabebd91834a41b36260b6ef5db015ee12ddb28ef3", - "sha256:c0315978b2a4569e03fb59100f6a7e7d23f718a4521491f5c13d946d37549f3d", - "sha256:c1828e10c3a49e2b234b87600ecb68a92b8a8dcf8b99bca9447f16c4baaa1630", - "sha256:c1c49bc393d854d4421ebc174a0a41f9261f50d3694d8ca277146cbbcfd24ee7", - "sha256:c415b9601ff50709d6050c8a9281733a9b042b9e589265ac40305b875cf9c463", - "sha256:c54c635d1f52490cde7ef3a423645167a8284e452a35405d5c7dc1242a8e75c9", - "sha256:c5e6a1f8b0268ffa1c84d7c3558724956002ba8361176e76406233e704bbcffb", - "sha256:c98a596ac20e8980cc6f34c0c92a113e98eb08f3997c150064d26d2aeb043e5a", - "sha256:cd0834e4260eab78671b81d34f110fbaac449563e48d419cec0030d9a8e58693", - "sha256:cdad66685fcf2ad14ce522cf849d4a025f4fd206d6cfc3f403d9873e4c243b03", - "sha256:d1ea006426edf7e1299c52a58b0443158012f7a56fed3515164b60bfcb1503a9", - "sha256:d33b4490026968bdc7f0729b9d87a3a6b1e09043557d2fc1c605c6072deb2f11", - "sha256:d5cae4cd271e20b7ab757e966cc919186b9f02535418ab36c471a5377ef4deaa", - "sha256:dd505a1121ad5b666191840b7bd1d8cb917df2647deeca6f3474331b72452362", - "sha256:e1668ef2f3a7ec9881f4b6a917e5f97c87a343fa6b0d5fc826b7b0297ddd0887", - "sha256:e7bcfcede95531589295f56e924702cef7f9685c9e4e5407592e04ded6a65bf3", - "sha256:ebf610c37df4f09c71c9bbf8309b4b459107e6fe889ac0d7e16f6e4ebd975f86", - "sha256:f3bf5c132eb48002bcc3825702d241d35b4e9585009e65e9dcf9c4635d0b7424", - "sha256:f40380c96dd407dfa84eb2d264e68aa47717b53bdbe210a59cc3c35a4635f195", - "sha256:f57a0de48dda792629e7952d34a0c7b81ea336bb9b721391c7c58145b237fe55", - "sha256:f6b925c7775ab857bdc1e52e1f5abcae7d18751c09b751aeb641a5276d9b990e", - "sha256:f8f0d79b923070f25674e4ea8f3d61c9d89d24d9598d50ff32c5b9b23c79a25b", - "sha256:feca9fafa4385aea6759c171cd25ea82f7375312fca04178dae35331be45e538" + "sha256:08ebe7a1d6c1e5ca766d68407280d69658f5f98821c2ba6c41c63cabfed159af", + "sha256:0a90a0dc4b054b5af299a900bf950fe8f9e3e54322bc405005f30aa5cacc5c98", + "sha256:0cba0b8d25aa2d450762f3dd6df85498f5e7c3ad0ddeb516ef2b03510f0eea32", + "sha256:0ebdf5087e2ce903d8220cc45dcece90c2199ae4395fd83ca616fcc81010db2c", + "sha256:10a5f91c319d9d4afba812f72984816b5fcd20742232ff7ecc1610ffbf3fc64d", + "sha256:122768e3ae9ce74f981b46edefea9c6e5a40aea38aba3ac50168e6370459bf20", + "sha256:14eb6c628432720e41b4fab1ada879d56cfe7034159849e083eb536b4c2afa99", + "sha256:177b000efaf8d2f7012c649e8aee5b0bf488677b1162be5e7511aa4f9d567607", + "sha256:1c2496182e577042e0e07a328d91c949da9e77a2047c7291071e734cd7a6e780", + "sha256:1e33a7eddcd07545ccf5c3ab230f60314a17dc33e285475e8405e26e21f02660", + "sha256:2793d3297f3e49015140e6d3ea26142c967e07998e2fb00b6ee8d041138fbc4e", + "sha256:2914061f5ca573f990ec14191e6998752fa8fe50d518e3405410353c3f44aa5d", + "sha256:2adb967454e10e69478ba4a8d8afbba48a7c7a8619216b7c807f8481cc66ddfb", + "sha256:2b02a68b9445c70d7f5c8b578c5f5e5866b1d67ca23eb9e8bc8658ae9e3e2c74", + "sha256:3129151378f858cdc4a0a4df355c9a0d060ab49e2eea7e62e9f085bac100551b", + "sha256:32334f35824811dd20a12cc90825d000e6b50faaeaa71408d42269151a66140d", + "sha256:33af11eca7bb0f5c6ffaf5e7d9d2336c2448f9c6279b93abdd6f3c35f9ee321f", + "sha256:34f37c59b12bc3afc52bab6fcd9cd3be82ff01c4598a84cbea934ccb3a9c54a0", + "sha256:3666c750b73ce463a413692e3a57c60f7089e2d9116a2aa5a0f0eaf2ae325148", + "sha256:374baefcb1b6275f350da605951f5f02487a9bc84a574a7d5b696439fabd49a3", + "sha256:382f853516664d2ebfc75dc01da4a10fdef5edcb335fe7b45cf471ce758ecb18", + "sha256:3b1f4844909321ef2c1cee50ddeccbd6018cd8c8d1ddddda3f553e94a5859497", + "sha256:3f617a48b70f4843d54f52440ea1e58da6bdab07b391a3a6aed8d3b311a4cc04", + "sha256:435f7a08d8aa42371a94e7c141205a9cb092ba551084b5e0c57492e6673601a3", + "sha256:44b69c69c194ffacbc50165911cf023a4b1b06422d1e1199d3aea82eac17004e", + "sha256:486273d3b5af75a80c31c311988931bdd2a4b96a74d5c7f422bad948f99988ef", + "sha256:4a23475d8d5c56e447b7752a1e2ac267c1f723f765e406c81feddcd16cdc97bc", + "sha256:4c979fc92aba66730b66099cd5becb42d869a26c0011119bc1c2478408a8bf7a", + "sha256:4d7fad8c456d180a6d2f44c41cfab4b80e2e81451815825097db48b8293f59d5", + "sha256:50e0aee4adc9abcd2109c618a8d1b2c93b85ac277b24a003ab147d91e068b06d", + "sha256:556564d89e2f4a6e8fe000894c03e4e84cf0b6cfa5674e425db122633ee244d1", + "sha256:5587da333b7d280a312715b843d43e734652aa382cba824a84a67c81f75b338b", + "sha256:57993f406ce3f114b2a6756d7809be3ffd0cc40f33e8f8b9a4aa1b027fd4e3eb", + "sha256:5d6e069b882c1fdcbe5577dc4be372eda705180197140577a4cddb648c29d22e", + "sha256:5d878a0186023ac391861958035174d0486f3259cabf8fd94e591985468da3ea", + "sha256:5d90b5a3b0f32a5fecf5dd83d828713986c019585f5cddf40d288ff77f366615", + "sha256:5e9a766c346b2ed7e88937919d84ed64b4ef489dad1d8939f806ee52901dc142", + "sha256:64e8f5178958a9954043bc8cd10a5ae97352c3f2fc99aa01f2aebb0026010910", + "sha256:66e58a2e8c7609a3545c4b38fb8b01a6b8346c4862e529534f7674c5265a97b8", + "sha256:68d1f46f9387db3785508f5225d3acbc5825ca13d9c29f2b5cce203d5863eb79", + "sha256:6ad9a7d2a3a0f235184426425f80bd3b26c66b24fd5fddecde66be30c01ebe6e", + "sha256:6e8e19a80ba194db5c06915a9df23c0c06e0e9ca9a4db9386a6056cca555a027", + "sha256:73a664478ae1ea011b5a710fb100b115ca8b2146864fa0ce4143ff944df714b8", + "sha256:766d0ebf8703d28f854f945982aa09224d5a27a29594c70d921c43c3930fe7ac", + "sha256:783741f534c14957fbe657d62a34b947ec06db23d45a2fd4a8aeb73d9c84d7e6", + "sha256:79efd1ee3827b2f16797e14b1e45021206c3271249b4d0025014466d416d7413", + "sha256:83a70e22e0f6222effe7f29fdeba6c6023f9595e59a0479edacfbd7de4b77bb7", + "sha256:85de9904bc360fd29a98885d2bfcbd4e02ab33c53353cb70607f2bea2cb92468", + "sha256:8d954ba0eae7f33884d27dc00629ca4389d249eb8d26ca07c30911257cae8c96", + "sha256:9075313f8e41b481e4cb10af405054564b0247dc335db5398ed05f8ec38787e2", + "sha256:97fba98fc5d9ccd3d33909e898d00f2494d6a9eec7cbda3d030632e2c8bb4d00", + "sha256:994cb893936dd2e1803655ae8667a45066bfd53360b148e22b4e3325cc5ea7a3", + "sha256:9aa4e68f1e4f303971ec42976fb170204fb5092de199034b57199a1747e78a2d", + "sha256:9b6d15adc9768ff167614ca853f7eeb6ee5f1d55d5660e3af85ce6744fed2b82", + "sha256:9bbb2dbc2701ab7e9307ca3a8fa4999c5b28246968e0a0202a5afabf48a42e22", + "sha256:9c8d1db4f65bbc9d75b7b271d68fb996f1c8c81a525263862477d93611856c2d", + "sha256:a7b0a1618060e3f5aa73d3526ca2108a16a1b6bf86612cd0bb2ddcbef9879d06", + "sha256:afa55e863224e664a782effa62245df73fdfc55aee539bed6efacf35f6d4e4b7", + "sha256:b339d91ac9060bd6ecdc595a82dc151045e5d74f566e0864ef3f2ba0887fec42", + "sha256:b470de64d17156c37e91effc109d3b032b39867000e2c126732fe01d034441f9", + "sha256:b4ec8afd362356b8798c8caa806e91deb3f0602d8ffae8e91d2d3ced2a90c35e", + "sha256:c28c1677ea33ccb8b14330560094cc44d3ff4fad617a544fd18beb90403fe0f1", + "sha256:c681f34e2814bc6e1eef49752b338061b94a42c92734d0be9513447d3f83718c", + "sha256:cccb2937bece1310c5c0163d0406aba170a2e5fb1f0444d7b0e7fdc9bd6bb713", + "sha256:cdc6f8dce09281ae534eaf08a54f0d38612398375f28dad733a8885f3bf9b978", + "sha256:d23854e5867650d40cba54d49956aad8081452aa80b2cf0d8c310633f4f48510", + "sha256:d2d942421cf3a1d1eceae8fa192f1fbfb74eb9d3e207d35ad2696bd2ce2c987c", + "sha256:d2f991c18132f3e505c108147925372ffe4549173b7c258cf227df1c5977a635", + "sha256:d3a2bcf6c81639a165da93469e1e0aff67c956721f3fa9c0560f07dd1e505116", + "sha256:d84930b4145991214602372edd7305fc76b700220db79ac0dd57d3afd0f0a1ca", + "sha256:de3b4d5fb5d69749104b880a157f38baeea7765c93d9cd3837cedd5b84729e10", + "sha256:e57a10aacedcf24666f4c90d03e599f71d172d1c5e00dcf48205c445806745b0", + "sha256:f1d06c8fd8b453c3e553c956bd3b8395100401060430572174bb7876dd95ad49", + "sha256:f833a80d9de9307d736b6af58c235b17ef7f90ebea7b9c49cd274dec7a66a2f1", + "sha256:fb0544a0e8294a5a5e20d3cacdaaa9a911d7c0a9150f5264aef36e7d8fdfa07e", + "sha256:ff5d22eece44528023254b595c670dfcf9733ac6af74c4b6cb4f6a784dc3870c" ], "markers": "python_version >= '3.9'", - "version": "==3.11.0" + "version": "==3.11.2" }, "aiosignal": { "hashes": [ @@ -122,6 +122,15 @@ "markers": "python_version >= '3.8'", "version": "==0.7.0" }, + "anthropic": { + "hashes": [ + "sha256:94671cc80765f9ce693f76d63a97ee9bef4c2d6063c044e983d21a2e262f63ba", + "sha256:ea17093ae0ce0e1768b0c46501d6086b5bcd74ff39d68cd2d6396374e9de7c09" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.39.0" + }, "anyio": { "hashes": [ "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", @@ -781,11 +790,11 @@ }, "httpcore": { "hashes": [ - "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", - "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" + "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", + "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd" ], "markers": "python_version >= '3.8'", - "version": "==1.0.6" + "version": "==1.0.7" }, "httptools": { "hashes": [ @@ -868,6 +877,85 @@ "markers": "python_version >= '3.7'", "version": "==3.1.4" }, + "jiter": { + "hashes": [ + "sha256:0302f0940b1455b2a7fb0409b8d5b31183db70d2b07fd177906d83bf941385d1", + "sha256:097676a37778ba3c80cb53f34abd6943ceb0848263c21bf423ae98b090f6c6ba", + "sha256:0a7d5e85766eff4c9be481d77e2226b4c259999cb6862ccac5ef6621d3c8dcce", + "sha256:0e2b445e5ee627fb4ee6bbceeb486251e60a0c881a8e12398dfdff47c56f0723", + "sha256:12fd88cfe6067e2199964839c19bd2b422ca3fd792949b8f44bb8a4e7d21946a", + "sha256:191fbaee7cf46a9dd9b817547bf556facde50f83199d07fc48ebeff4082f9df4", + "sha256:1e44fff69c814a2e96a20b4ecee3e2365e9b15cf5fe4e00869d18396daa91dab", + "sha256:1e47a554de88dff701226bb5722b7f1b6bccd0b98f1748459b7e56acac2707a5", + "sha256:25d0e5bf64e368b0aa9e0a559c3ab2f9b67e35fe7269e8a0d81f48bbd10e8963", + "sha256:262e96d06696b673fad6f257e6a0abb6e873dc22818ca0e0600f4a1189eb334f", + "sha256:2b7de0b6f6728b678540c7927587e23f715284596724be203af952418acb8a2d", + "sha256:3298af506d4271257c0a8f48668b0f47048d69351675dd8500f22420d4eec378", + "sha256:3d8bae77c82741032e9d89a4026479061aba6e646de3bf5f2fc1ae2bbd9d06e0", + "sha256:3dc9939e576bbc68c813fc82f6620353ed68c194c7bcf3d58dc822591ec12490", + "sha256:448cf4f74f7363c34cdef26214da527e8eeffd88ba06d0b80b485ad0667baf5d", + "sha256:47ac4c3cf8135c83e64755b7276339b26cd3c7ddadf9e67306ace4832b283edf", + "sha256:4aa919ebfc5f7b027cc368fe3964c0015e1963b92e1db382419dadb098a05192", + "sha256:576eb0f0c6207e9ede2b11ec01d9c2182973986514f9c60bc3b3b5d5798c8f50", + "sha256:5970cf8ec943b51bce7f4b98d2e1ed3ada170c2a789e2db3cb484486591a176a", + "sha256:5ae2d01e82c94491ce4d6f461a837f63b6c4e6dd5bb082553a70c509034ff3d4", + "sha256:5c08adf93e41ce2755970e8aa95262298afe2bf58897fb9653c47cd93c3c6cdc", + "sha256:60b49c245cd90cde4794f5c30f123ee06ccf42fb8730a019a2870cd005653ebd", + "sha256:627164ec01d28af56e1f549da84caf0fe06da3880ebc7b7ee1ca15df106ae172", + "sha256:6592f4067c74176e5f369228fb2995ed01400c9e8e1225fb73417183a5e635f0", + "sha256:65df9dbae6d67e0788a05b4bad5706ad40f6f911e0137eb416b9eead6ba6f044", + "sha256:701d90220d6ecb3125d46853c8ca8a5bc158de8c49af60fd706475a49fee157e", + "sha256:70a497859c4f3f7acd71c8bd89a6f9cf753ebacacf5e3e799138b8e1843084e3", + "sha256:75bf3b7fdc5c0faa6ffffcf8028a1f974d126bac86d96490d1b51b3210aa0f3f", + "sha256:7824c3ecf9ecf3321c37f4e4d4411aad49c666ee5bc2a937071bdd80917e4533", + "sha256:7ba52e6aaed2dc5c81a3d9b5e4ab95b039c4592c66ac973879ba57c3506492bb", + "sha256:7ba9a358d59a0a55cccaa4957e6ae10b1a25ffdabda863c0343c51817610501d", + "sha256:7ded4e4b75b68b843b7cea5cd7c55f738c20e1394c68c2cb10adb655526c5f1b", + "sha256:80dae4f1889b9d09e5f4de6b58c490d9c8ce7730e35e0b8643ab62b1538f095c", + "sha256:81d968dbf3ce0db2e0e4dec6b0a0d5d94f846ee84caf779b07cab49f5325ae43", + "sha256:8a9803396032117b85ec8cbf008a54590644a062fedd0425cbdb95e4b2b60479", + "sha256:8dbbd52c50b605af13dbee1a08373c520e6fcc6b5d32f17738875847fea4e2cd", + "sha256:8f212eeacc7203256f526f550d105d8efa24605828382cd7d296b703181ff11d", + "sha256:935f10b802bc1ce2b2f61843e498c7720aa7f4e4bb7797aa8121eab017293c3d", + "sha256:93c20d2730a84d43f7c0b6fb2579dc54335db742a59cf9776d0b80e99d587382", + "sha256:9463b62bd53c2fb85529c700c6a3beb2ee54fde8bef714b150601616dcb184a6", + "sha256:9cd3cccccabf5064e4bb3099c87bf67db94f805c1e62d1aefd2b7476e90e0ee2", + "sha256:9ecbf4e20ec2c26512736284dc1a3f8ed79b6ca7188e3b99032757ad48db97dc", + "sha256:9f9568cd66dbbdab67ae1b4c99f3f7da1228c5682d65913e3f5f95586b3cb9a9", + "sha256:ad04a23a91f3d10d69d6c87a5f4471b61c2c5cd6e112e85136594a02043f462c", + "sha256:ad36a1155cbd92e7a084a568f7dc6023497df781adf2390c345dd77a120905ca", + "sha256:af29c5c6eb2517e71ffa15c7ae9509fa5e833ec2a99319ac88cc271eca865519", + "sha256:b096ca72dd38ef35675e1d3b01785874315182243ef7aea9752cb62266ad516f", + "sha256:b1a0508fddc70ce00b872e463b387d49308ef02b0787992ca471c8d4ba1c0fa1", + "sha256:bc1b55314ca97dbb6c48d9144323896e9c1a25d41c65bcb9550b3e0c270ca560", + "sha256:be6de02939aac5be97eb437f45cfd279b1dc9de358b13ea6e040e63a3221c40d", + "sha256:c1288bc22b9e36854a0536ba83666c3b1fb066b811019d7b682c9cf0269cdf9f", + "sha256:c244261306f08f8008b3087059601997016549cb8bb23cf4317a4827f07b7d74", + "sha256:c65a3ce72b679958b79d556473f192a4dfc5895e8cc1030c9f4e434690906076", + "sha256:c915e1a1960976ba4dfe06551ea87063b2d5b4d30759012210099e712a414d9f", + "sha256:d9e247079d88c00e75e297e6cb3a18a039ebcd79fefc43be9ba4eb7fb43eb726", + "sha256:da8589f50b728ea4bf22e0632eefa125c8aa9c38ed202a5ee6ca371f05eeb3ff", + "sha256:dacca921efcd21939123c8ea8883a54b9fa7f6545c8019ffcf4f762985b6d0c8", + "sha256:de3674a5fe1f6713a746d25ad9c32cd32fadc824e64b9d6159b3b34fd9134143", + "sha256:df0a1d05081541b45743c965436f8b5a1048d6fd726e4a030113a2699a6046ea", + "sha256:e0c91a0304373fdf97d56f88356a010bba442e6d995eb7773cbe32885b71cdd8", + "sha256:e550e29cdf3577d2c970a18f3959e6b8646fd60ef1b0507e5947dc73703b5627", + "sha256:e80052d3db39f9bb8eb86d207a1be3d9ecee5e05fdec31380817f9609ad38e60", + "sha256:e81ccccd8069110e150613496deafa10da2f6ff322a707cbec2b0d52a87b9671", + "sha256:f0aacaa56360139c53dcf352992b0331f4057a0373bbffd43f64ba0c32d2d155", + "sha256:f114a4df1e40c03c0efbf974b376ed57756a1141eb27d04baee0680c5af3d424", + "sha256:f20de711224f2ca2dbb166a8d512f6ff48c9c38cc06b51f796520eb4722cc2ce", + "sha256:f22cf8f236a645cb6d8ffe2a64edb5d2b66fb148bf7c75eea0cb36d17014a7bc", + "sha256:f281aae41b47e90deb70e7386558e877a8e62e1693e0086f37d015fa1c102289", + "sha256:f3ea649e7751a1a29ea5ecc03c4ada0a833846c59c6da75d747899f9b48b7282", + "sha256:f52ce5799df5b6975439ecb16b1e879d7655e1685b6e3758c9b1b97696313bfb", + "sha256:f7605d24cd6fab156ec89e7924578e21604feee9c4f1e9da34d8b67f63e54892", + "sha256:f84c9996664c460f24213ff1e5881530abd8fafd82058d39af3682d5fd2d6316", + "sha256:f892e547e6e79a1506eb571a676cf2f480a4533675f834e9ae98de84f9b941ac" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.1" + }, "joblib": { "hashes": [ "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", @@ -2752,11 +2840,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:1456af3358be1a0e49dd8428bfb81863406659d9fad871362bf18a098eeac90a", - "sha256:dd83003963ca957a6e4835d192d7f163fb55312ce3d3f798f625ac9438616e4f" + "sha256:0251f63257eb8d5f4b414669e25f98898a2bac58fd6ffa1c9df6cf3dd823abd9", + "sha256:e536390ff6934627af351accd8da10bec6cf75e40add465ab0a9c088d7be765c" ], "markers": "python_version >= '3.8'", - "version": "==1.35.59" + "version": "==1.35.62" }, "certifi": { "hashes": [ diff --git a/opentrons-ai-server/api/domain/anthropic_predict.py b/opentrons-ai-server/api/domain/anthropic_predict.py new file mode 100644 index 00000000000..abd94b631ba --- /dev/null +++ b/opentrons-ai-server/api/domain/anthropic_predict.py @@ -0,0 +1,206 @@ +import uuid +from pathlib import Path +from typing import Any, Dict, List + +import requests +import structlog +from anthropic import Anthropic +from anthropic.types import Message, MessageParam +from ddtrace import tracer + +from api.domain.config_anthropic import DOCUMENTS, PROMPT, SYSTEM_PROMPT +from api.settings import Settings + +settings: Settings = Settings() +logger = structlog.stdlib.get_logger(settings.logger_name) +ROOT_PATH: Path = Path(Path(__file__)).parent.parent.parent + + +class AnthropicPredict: + def __init__(self, settings: Settings) -> None: + self.settings: Settings = settings + self.client: Anthropic = Anthropic(api_key=settings.anthropic_api_key.get_secret_value()) + self.model_name: str = settings.anthropic_model_name + self.system_prompt: str = SYSTEM_PROMPT + self.path_docs: Path = ROOT_PATH / "api" / "storage" / "docs" + self._messages: List[MessageParam] = [ + { + "role": "user", + "content": [ + {"type": "text", "text": DOCUMENTS.format(doc_content=self.get_docs()), "cache_control": {"type": "ephemeral"}} # type: ignore + ], + } + ] + self.tools: List[Dict[str, Any]] = [ + { + "name": "simulate_protocol", + "description": "Simulates the python protocol on user input. Returned value is text indicating if protocol is successful.", + "input_schema": { + "type": "object", + "properties": { + "protocol": {"type": "string", "description": "protocol in python for simulation"}, + }, + "required": ["protocol"], + }, + } + ] + + @tracer.wrap() + def get_docs(self) -> str: + """ + Processes documents from a directory and returns their content wrapped in XML tags. + Each document is wrapped in tags with metadata subtags. + + Returns: + str: XML-formatted string containing all documents and their metadata + """ + logger.info("Getting docs", extra={"path": str(self.path_docs)}) + xml_output = [""] + for file_path in self.path_docs.iterdir(): + try: + content = file_path.read_text(encoding="utf-8") + document_xml = [ + "", + f" {file_path.name}", + " ", + f" {content}", + " ", + "", + ] + xml_output.extend(document_xml) + + except Exception as e: + logger.error("Error procesing file", extra={"file": file_path.name, "error": str(e)}) + continue + + xml_output.append("") + return "\n".join(xml_output) + + @tracer.wrap() + def generate_message(self, max_tokens: int = 4096) -> Message: + + response = self.client.messages.create( + model=self.model_name, + system=self.system_prompt, + max_tokens=max_tokens, + messages=self._messages, + tools=self.tools, # type: ignore + extra_headers={"anthropic-beta": "prompt-caching-2024-07-31"}, + ) + + logger.info( + "Token usage", + extra={ + "input_tokens": response.usage.input_tokens, + "output_tokens": response.usage.output_tokens, + "cache_read": getattr(response.usage, "cache_read_input_tokens", "---"), + "cache_create": getattr(response.usage, "cache_creation_input_tokens", "---"), + }, + ) + return response + + @tracer.wrap() + def predict(self, prompt: str) -> str | None: + try: + self._messages.append({"role": "user", "content": PROMPT.format(USER_PROMPT=prompt)}) + response = self.generate_message() + if response.content[-1].type == "tool_use": + tool_use = response.content[-1] + self._messages.append({"role": "assistant", "content": response.content}) + result = self.handle_tool_use(tool_use.name, tool_use.input) # type: ignore + self._messages.append( + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": result, + } + ], + } + ) + follow_up = self.generate_message() + response_text = follow_up.content[0].text # type: ignore + self._messages.append({"role": "assistant", "content": response_text}) + return response_text + + elif response.content[0].type == "text": + response_text = response.content[0].text + self._messages.append({"role": "assistant", "content": response_text}) + return response_text + + logger.error("Unexpected response type") + return None + except IndexError as e: + logger.error("Invalid response format", extra={"error": str(e)}) + return None + except Exception as e: + logger.error("Error in predict method", extra={"error": str(e)}) + return None + + @tracer.wrap() + def handle_tool_use(self, func_name: str, func_params: Dict[str, Any]) -> str: + if func_name == "simulate_protocol": + results = self.simulate_protocol(**func_params) + return results + + logger.error("Unknown tool", extra={"tool": func_name}) + raise ValueError(f"Unknown tool: {func_name}") + + @tracer.wrap() + def reset(self) -> None: + self._messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": DOCUMENTS.format(doc_content=self.get_docs()), "cache_control": {"type": "ephemeral"}} # type: ignore + ], + } + ] + + @tracer.wrap() + def simulate_protocol(self, protocol: str) -> str: + url = "https://Opentrons-simulator.hf.space/protocol" + protocol_name = str(uuid.uuid4()) + ".py" + data = {"name": protocol_name, "content": protocol} + hf_token: str = settings.huggingface_api_key.get_secret_value() + headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(hf_token)} + response = requests.post(url, json=data, headers=headers) + + if response.status_code != 200: + logger.error("Simulation request failed", extra={"status": response.status_code, "error": response.text}) + return f"Error: {response.text}" + + response_data = response.json() + if "error_message" in response_data: + logger.error("Simulation error", extra={"error": response_data["error_message"]}) + return str(response_data["error_message"]) + elif "protocol_name" in response_data: + return str(response_data["run_status"]) + else: + logger.error("Unexpected response", extra={"response": response_data}) + return "Unexpected response" + + +def main() -> None: + """Intended for testing this class locally.""" + import sys + from pathlib import Path + + # # Add project root to Python path + root_dir = Path(__file__).parent.parent.parent + sys.path.insert(0, str(root_dir)) + + from rich import print + from rich.prompt import Prompt + + settings = Settings() + llm = AnthropicPredict(settings) + prompt = Prompt.ask("Type a prompt to send to the Anthropic API:") + completion = llm.predict(prompt) + print(completion) + + +if __name__ == "__main__": + main() diff --git a/opentrons-ai-server/api/domain/config_anthropic.py b/opentrons-ai-server/api/domain/config_anthropic.py new file mode 100644 index 00000000000..9d511012592 --- /dev/null +++ b/opentrons-ai-server/api/domain/config_anthropic.py @@ -0,0 +1,217 @@ +SYSTEM_PROMPT = """ +You are a friendly and knowledgeable AI assistant specializing in Opentrons protocol development. +You help scientists create and optimize protocols using the Opentrons Python API v2. + +Your key responsibilities: +1. Welcome scientists warmly and understand their protocol needs +2. Generate accurate Python protocols using standard Opentrons labware +3. Provide clear explanations and documentation +4. Flag potential safety or compatibility issues +5. Suggest protocol optimizations when appropriate + +Call protocol simulation tool to validate the code - only when it is called explicitly by the user. +For all other queries, provide direct responses. + +Important guidelines: +- Always verify labware compatibility before generating protocols +- Include appropriate error handling in generated code +- Provide clear setup instructions and prerequisites +- Flag any potential safety concerns +- Format code examples using standard Python conventions + +If you encounter requests outside your knowledge of Opentrons capabilities, +ask for clarification rather than making assumptions. +""" + +DOCUMENTS = """ +{doc_content} +""" + +PROMPT = """ +Here are the inputs you will work with: + + +{USER_PROMPT} + + + +Follow these instructions to handle the user's prompt: + +1. Analyze the user's prompt to determine if it's: + a) A request to generate a protocol + b) A question about the Opentrons Python API v2 + c) A common task (e.g., value changes, OT-2 to Flex conversion, slot correction) + d) An unrelated or unclear request + +2. If the prompt is unrelated or unclear, ask the user for clarification. For example: + I apologize, but your prompt seems unclear. Could you please provide more details? + + +3. If the prompt is a question about the API, answer it using only the information + provided in the section. Provide references and place them under the tag. + Format your response like this: + API answer: + [Your answer here, based solely on the provided API documentation] + + References + [References] + + +4. If the prompt is a request to generate a protocol, follow these steps: + + a) Check if the prompt contains all necessary information: + - Modules + - Adapters + - Labware + - Pipette mounts + - Well allocations, liquids, samples + - Commands (steps) + + b) If any crucial information is missing, ask for clarification: + + To generate an accurate protocol, I need more information about [missing elements]. + Please provide details about: + [List of missing elements] + + + c) If all necessary information is available, generate the protocol using the following structure: + + ```python + from opentrons import protocol_api + + metadata = {{ + 'protocolName': '[Protocol name based on user prompt]', + 'author': 'AI Assistant', + 'description': '[Brief description based on user prompt]' + }} + + requirements = {{ + 'robotType': '[Robot type based on user prompt, OT-2 or Flex, default is OT-2]', + 'apiLevel': '[apiLevel, default is 2.19 ]' + }} + + def run(protocol: protocol_api.ProtocolContext): + # Load modules (if any) + [Module loading code with comments] + + # Load adapters (if any) + [Adapter loading code with comments] + + # Load labware + [Labware loading code with comments] + + # Load pipettes + [Pipette loading code with comments] + + # For Flex protocols using API version 2.16 or later, load trash bin + trash = protocol.load_trash_bin('A3') + + # Protocol steps + [Step-by-step protocol commands with comments] + [Please make sure that the transfer function is used with the new_tip parameter correctly] + ``` + + d) Use the `transfer` function to handle iterations over wells and volumes. Provide lists of source and + destination wells to leverage the function's built-in iteration capabilities. + - The most important thing is to avoid unnecessary loops. Incorrect usages of the loops is as follows: + ```python + for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='always') + ``` + This approach unnecessarily calls the transfer method multiple times and can lead to inefficiencies or errors. + + Correct usage is: + ```python + pipette.transfer(volume, source_wells, destination_wells, new_tip='always') + ``` + + The `transfer` function can handle lists of sources and destinations, automatically pairing them and iterating over them. + Even it can stretch if one of the lists is longer. So no need for explicit loops. + + - Next problem is proper use of `new_tip` parameter. Incorrect usage is using new_tip='once' inside a loop + when intending to reuse the same tip. + ```python + for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='once') + ``` + Correct usage is: + ```python + pipette.transfer(volume, source_wells, destination_wells, new_tip='once') + ``` + + When new_tip='once', the pipette picks up a tip at the beginning of the transfer and uses it throughout. + Using it inside a loop can cause the pipette to attempt to pick up a tip that is already in use, leading to errors. + + + e) In the end, make sure you show generate well-written protocol with proper short but useful comments. + +5. Common model issues to avoid: + - Model outputs `p300_multi` instead of `p300_multi_gen2`. + - Model outputs `thermocyclerModuleV1` instead of `thermocyclerModuleV2`. + - Model outputs `opentrons_flex_96_tiprack_50ul` instead of `opentrons_flex_96_filtertiprack_50ul`. + - Model outputs `opentrons_96_pcr_adapter_nest_wellplate_100ul` instead of + `opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt`. + - Do not forget to define `from opentrons import protocol_api`. + - PCR plate cannot go directly on the Temperature Module. Looking at the documentation and white paper, + you need an appropriate thermal adapter/block between the Temperature Module and the labware. + For PCR plates, you need to: + - First load a PCR thermal block adapter on the module using load_adapter() + - Then load the PCR plate onto the adapter + - If prompt contains CSV file but not provided, then create a CSV data structure as a placeholder. + - ProtocolContext.load_trash_bin method is not available in API version 2.15, must be higher >=2.16. + - If tip rack type is not specified, please use regular tip rack rather than filter tip rack. + - API for `Opentrons 96 PCR Heater-Shaker Adapter with NEST Well Plate 100 ul`is + opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt. + - Include only apiLevel in the requirements dictionary. + - Make sure models does not generate errors such as "Variable 'diluent' is not defined". Define everything then use it. + - If the labware is already with `aluminumblock`, then no need to use `load_adapter`. For example, + `opentrons_96_aluminumblock_nest_wellplate_100ul`, `opentrons_24_aluminumblock_nest_1.5ml_snapcap`: + - Correct + ```python + temp_module = protocol.load_module('temperature module gen2', '4') + dilution_plate = temp_module.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + ``` + + - Incorrect + ```python + temp_module = protocol.load_module('temperature module gen2', 3) + temp_adapter = temp_module.load_adapter('opentrons_96_well_aluminum_block') + dilution_plate = temp_adapter.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + ``` + - when description says explicitly how many rows, you need to use it otherwise you encounter out of tips error: for example, + "For each of the 8 rows in the plate:" + - correct: + ```python + for i in range(8): + row = plate.rows()[i] + ``` + - incorrect: + ```python + for row in plate.rows(): + ``` + - Always check out_of_tips_error_219.md before generating the code + - Use load_trash_bin() for Flex. It is not supported on OT-2. + - By default 'A3' is trash for Flex, it must be defined as: trash = protocol.load_trash_bin('A3'). + - Trying to access .bottom on a list of well locations instead of a single well object. + - Keeping the same tip for all transfers refers `new_tip='once'`, but model outputs `new_tip='always'`. + - If tip racks are not defined, please define them by counting source and destination labware so that outof tips error will be avoided. + - The model generates a protocol that attempted to access non-existent wells (A7-A12) in a 24-well tuberack + which only has positions A1-D6, causing a KeyError when trying to reference well 'A7'. + - Model tries to close thermocycler before opening it. Attempted to access labware inside a closed thermocycler, + the thermocycler must be opened first. + - Required Validation Steps: + - Verify all variables are defined before use + - Confirm tip rack quantity matches transfer count + - Validate all well positions exist in labware + - Check module-labware compatibility + - Verify correct API version for all features used + +6. If slots are not defined, refer to deck_layout.md for proper slot definitions. + Make sure slots are different for different labware. If the source and destination are not defined, + then you define yourself but inform user with your choice, because user may want to change them. + +7. If the request lacks sufficient information to generate a protocol, use casual_examples.md + as a reference to generate a basic protocol. + +Remember to use only the information provided in the . Do not introduce any external information or assumptions. +""" diff --git a/opentrons-ai-server/api/handler/fast.py b/opentrons-ai-server/api/handler/fast.py index 9182f827a9a..b93eb6580ce 100644 --- a/opentrons-ai-server/api/handler/fast.py +++ b/opentrons-ai-server/api/handler/fast.py @@ -1,7 +1,7 @@ import asyncio import os import time -from typing import Annotated, Any, Awaitable, Callable, List, Literal, Union +from typing import Annotated, Any, Awaitable, Callable, List, Literal, Optional, Union import structlog from asgi_correlation_id import CorrelationIdMiddleware @@ -17,6 +17,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from uvicorn.protocols.utils import get_path_with_query_string +from api.domain.anthropic_predict import AnthropicPredict from api.domain.fake_responses import FakeResponse, get_fake_response from api.domain.openai_predict import OpenAIPredict from api.handler.custom_logging import setup_logging @@ -43,7 +44,7 @@ auth: VerifyToken = VerifyToken() openai: OpenAIPredict = OpenAIPredict(settings) google_sheets_client = GoogleSheetsClient(settings) - +claude: AnthropicPredict = AnthropicPredict(settings) # Initialize FastAPI app with metadata app = FastAPI( @@ -179,7 +180,7 @@ async def create_chat_completion( body: ChatRequest, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate a chat completion response using OpenAI. + Generate a chat completion response using LLM. - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. @@ -196,7 +197,12 @@ async def create_chat_completion( fake: FakeResponse = get_fake_response(body.fake_key) return ChatResponse(reply=fake.chat_response.reply, fake=fake.chat_response.fake) return ChatResponse(reply="Default fake response. ", fake=body.fake) - response: Union[str, None] = openai.predict(prompt=body.message, chat_completion_message_params=body.history) + + response: Optional[str] = None + if "openai" in settings.model.lower(): + response = openai.predict(prompt=body.message, chat_completion_message_params=body.history) + else: + response = claude.predict(prompt=body.message) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -221,7 +227,7 @@ async def update_protocol( body: UpdateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate an updated protocol using OpenAI. + Generate an updated protocol using LLM. - **request**: The HTTP request containing the existing protocol and other relevant parameters. - **returns**: A chat response or an error message. @@ -236,7 +242,11 @@ async def update_protocol( if body.fake: return ChatResponse(reply="Fake response", fake=bool(body.fake)) - response: Union[str, None] = openai.predict(prompt=body.prompt, chat_completion_message_params=None) + response: Optional[str] = None + if "openai" in settings.model.lower(): + response = openai.predict(prompt=body.prompt, chat_completion_message_params=None) + else: + response = claude.predict(prompt=body.prompt) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -261,7 +271,7 @@ async def create_protocol( body: CreateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate an updated protocol using OpenAI. + Generate an updated protocol using LLM. - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. @@ -277,7 +287,11 @@ async def create_protocol( if body.fake: return ChatResponse(reply="Fake response", fake=body.fake) - response: Union[str, None] = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) + response: Optional[str] = None + if "openai" in settings.model.lower(): + response = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) + else: + response = claude.predict(prompt=str(body.model_dump())) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) diff --git a/opentrons-ai-server/api/settings.py b/opentrons-ai-server/api/settings.py index 9557b51614b..7d6bcf91459 100644 --- a/opentrons-ai-server/api/settings.py +++ b/opentrons-ai-server/api/settings.py @@ -25,6 +25,8 @@ class Settings(BaseSettings): log_level: str = "info" service_name: str = "local-ai-api" openai_model_name: str = "gpt-4-1106-preview" + anthropic_model_name: str = "claude-3-5-sonnet-20241022" + model: str = "claude" auth0_domain: str = "opentrons-dev.us.auth0.com" auth0_api_audience: str = "sandbox-ai-api" auth0_issuer: str = "https://identity.auth-dev.opentrons.com/" @@ -43,6 +45,7 @@ class Settings(BaseSettings): huggingface_api_key: SecretStr = SecretStr("default_huggingface_api_key") google_credentials_json: SecretStr = SecretStr("default_google_credentials_json") datadog_api_key: SecretStr = SecretStr("default_datadog_api_key") + anthropic_api_key: SecretStr = SecretStr("default_anthropic_api_key") @property def json_logging(self) -> bool: diff --git a/opentrons-ai-server/api/storage/docs/OT2ToFlex.md b/opentrons-ai-server/api/storage/docs/OT2ToFlex.md new file mode 100644 index 00000000000..9263cb453a7 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/OT2ToFlex.md @@ -0,0 +1,209 @@ +# Adapting OT-2 to Flex + +## Metadata and Requirements: + +- **API Level**: Flex requires an `apiLevel` of 2.15 or higher. If the OT-2 protocol specified `apiLevel` in the `metadata` dictionary, move it to the `requirements` dictionary. Ensure it is not specified in both places to avoid errors. + +- **Robot Type**: Specify `"robotType": "Flex"` in the `requirements` dictionary. If `robotType` is omitted, the API assumes the protocol is designed for the OT-2. + +### Example Conversion: + +#### Original OT-2 Code: + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "My Protocol", + "description": "This protocol uses the OT-2", + "apiLevel": "2.19" +} +``` + +#### Updated Flex Code: + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "My Protocol", + "description": "This protocol uses the Flex", +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.19" +} +``` + +## Pipettes and Tip Rack Load Names: + +- Flex uses different pipettes and tip racks with unique load names. When converting, load Flex pipettes of the same or larger capacity than the OT-2 pipettes. + +- Using smaller capacity tips than in the OT-2 protocol may require further adjustments to avoid running out of tips, resulting in more steps and longer execution times. + +### Example Conversion: + +#### Original OT-2 Code: + +```python +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + left_pipette = protocol.load_instrument( + "p300_single_gen2", "left", tip_racks=[tips] + ) +``` + +#### Updated Flex Code: + +```python +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_flex_96_tiprack_1000ul", "D1") + left_pipette = protocol.load_instrument( + "flex_1channel_1000", "left", tip_racks=[tips] + ) +``` + +## Trash Container: + +- OT-2 protocols have a fixed trash in slot 12. For Flex protocols using API version 2.16 or later, load a trash bin in slot A3 to match the OT-2 fixed trash position: + + ```python + trash = protocol.load_trash_bin("A3") + ``` + + **Note**: Load the trash before any commands that may require discarding tips; otherwise, the robot cannot find it. + +## Deck Slot Labels: + +- Update numeric labels for deck slots (matching OT-2) to coordinate labels (matching Flex). This is optional but recommended for clarity. + +### Deck Slot Correspondence: + +The correspondence between deck labels is based on the relative locations of the slots: + +``` +- 10 to A1 +- 11 to A2 +- Trash to A3 +- 7 to B1 +- 8 to B2 +- 9 to B3 +- 4 to C1 +- 5 to C2 +- 6 to C3 +- 1 to D1 +- 2 to D2 +- 3 to D3 +``` + +**Slots A4, B4, C4, and D4 on Flex have no equivalent on OT-2.** + +## Module Load Names: + +- If your OT-2 protocol uses older generations of the Temperature Module or Thermocycler Module, update the load names you pass to `load_module()` to ones compatible with Flex: + + - Temperature Module: `"temperature module gen2"` + - Thermocycler Module: `"thermocycler module gen2"` or `"thermocyclerModuleV2"` + +- The Heater-Shaker Module (`heaterShakerModuleV1`) is compatible with both Flex and OT-2. + +- **Magnetic Module**: Not compatible with Flex. For protocols that load `magnetic module`, `magdeck`, or `magnetic module gen2`, you need to modify the protocol to use the Magnetic Block and Flex Gripper instead. This requires reworking some protocol steps to achieve similar results. + +### Main Difference: + +- **OT-2**: Pipettes an entire plate's worth of liquid from the Heater-Shaker to the Magnetic Module and then engages the module. + +- **Flex**: The gripper moves the plate to the Magnetic Block in one step, eliminating the need for pipetting between modules. + +### Example Conversion for Magnetic Module: + +#### Original OT-2 Code: + +```python +hs_mod.set_and_wait_for_shake_speed(2000) +protocol.delay(minutes=5) +hs_mod.deactivate_shaker() + +for i in sample_plate.wells(): + # Mix, transfer, and blow-out all samples + pipette.pick_up_tip() + pipette.aspirate(100, hs_plate[i]) + pipette.dispense(100, hs_plate[i]) + pipette.aspirate(100, hs_plate[i]) + pipette.air_gap(10) + pipette.dispense(pipette.current_volume, mag_plate[i]) + pipette.aspirate(50, hs_plate[i]) + pipette.air_gap(10) + pipette.dispense(pipette.current_volume, mag_plate[i]) + pipette.blow_out(mag_plate[i].bottom(0.5)) + pipette.drop_tip() + +mag_mod.engage() +``` + +#### Updated Flex Code: + +```python +hs_mod.set_and_wait_for_shake_speed(2000) +protocol.delay(minutes=5) +hs_mod.deactivate_shaker() + +# Move entire plate using the gripper +hs_mod.open_labware_latch() +protocol.move_labware(sample_plate, mag_block, use_gripper=True) +``` + +## Flex vs. OT-2 Pipettes: + +When converting pipettes, consider the volume ranges: + +### OT-2 Pipettes: + +- **P20 Single-Channel GEN2**: 1–20 µL, `p20_single_gen2` +- **P20 Multi-Channel GEN2**: 1–20 µL, `p20_multi_gen2` +- **P300 Single-Channel GEN2**: 20–300 µL, `p300_single_gen2` +- **P300 Multi-Channel GEN2**: 20–300 µL, `p300_multi_gen2` +- **P1000 Single-Channel GEN2**: 100–1000 µL, `p1000_single_gen2` + +### Flex Pipettes: + +- **Flex 1-Channel Pipette**: 1–50 µL, `flex_1channel_50` +- **Flex 1-Channel Pipette**: 5–1000 µL, `flex_1channel_1000` +- **Flex 8-Channel Pipette**: 1–50 µL, `flex_8channel_50` +- **Flex 8-Channel Pipette**: 5–1000 µL, `flex_8channel_1000` +- **Flex 96-Channel Pipette**: 5–1000 µL, `flex_96channel_1000` + +## Tip Racks: + +### OT-2 Tip Racks: + +- `geb_96_tiprack_1000ul` +- `geb_96_tiprack_10ul` +- `opentrons_96_filtertiprack_1000ul` +- `opentrons_96_filtertiprack_10ul` +- `opentrons_96_filtertiprack_200ul` +- `opentrons_96_filtertiprack_20ul` +- `opentrons_96_tiprack_1000ul` +- `opentrons_96_tiprack_10ul` +- `opentrons_96_tiprack_20ul` +- `opentrons_96_tiprack_300ul` + +### Flex Tip Racks: + +- `opentrons_flex_96_filtertiprack_1000ul` +- `opentrons_flex_96_filtertiprack_200ul` +- `opentrons_flex_96_filtertiprack_50ul` +- `opentrons_flex_96_tiprack_1000ul` +- `opentrons_flex_96_tiprack_200ul` +- `opentrons_flex_96_tiprack_50ul` + +**Note**: When converting, match the pipette and tip rack volumes to ensure the protocol functions correctly. + +## Additional Notes: + +- **Trash Bin**: Remember to load the trash bin before any commands that may require discarding tips. + +- **Deck Slots**: Adjust deck slot labels to match the Flex coordinate system for clarity, although numeric labels are still valid. + +- **Verification**: After adapting the protocol, verify that the new design achieves similar results, especially if significant changes were made (e.g., replacing the Magnetic Module with the Magnetic Block). diff --git a/opentrons-ai-server/api/storage/docs/casual_examples.md b/opentrons-ai-server/api/storage/docs/casual_examples.md new file mode 100644 index 00000000000..4d1e778d5d5 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/casual_examples.md @@ -0,0 +1,979 @@ +The following examples show casual descriptions and their corresponding potential protocols. + +#### Example 1: PCR protocol + + +I want to run a PCR setup protocol with temperature control. I need to prepare 64 samples (that's 8 full columns) using both mastermix and samples. Let's keep the samples cold at 4°C and the mastermix at 10°C using temperature modules. + +Here's what I want to do: + +First, I'll use a multichannel P20 pipette mounted on the left side. I'll have three plates: one for samples (on the cold module), one for mastermix (on the slightly warmer module), and one destination plate where we'll mix everything together. + +The steps should go like this: + +1. Start by setting both temperature modules - 4°C for samples and 10°C for mastermix +2. Take one tip and use it to transfer 7 µL of mastermix to each destination well for all 64 samples +3. For the samples, I want to transfer 5 µL from each well to the corresponding destination well. Use fresh tips for each column, mix 9 times with 12 µL volume, and make sure to blow out into the destination well +4. When we're done, turn off both temperature modules + +Remember to work column by column since we're using a multichannel pipette, and we'll be handling the first 8 columns of each plate. + + + +from opentrons import protocol_api + +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): # Module loading +temp_mod_sample = protocol.load_module('temperature module gen2', 1) +temp_mod_mastermix = protocol.load_module('temperature module gen2', 3) + + # Load thermal adapters + sample_adapter = temp_mod_sample.load_adapter("opentrons_96_well_aluminum_block") + mastermix_adapter = temp_mod_mastermix.load_adapter("opentrons_96_well_aluminum_block") + + # Load labware onto the adapters + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + mastermix_plate = mastermix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 7) + tiprack = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + + # Pipette loading + p20_single = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tiprack]) + + # Well allocation + num_samples = 64 + num_cols = num_samples // 8 + sample_wells = sample_plate.columns()[:num_cols] + mastermix_wells = mastermix_plate.columns()[:num_cols] + dest_wells = dest_plate.columns()[:num_cols] + + # Volume and temperature parameters + sample_temp = 4 # Celsius + mastermix_temp = 10 # Celsius + mastermix_vol = 7 # uL + sample_vol = 5 # uL + mix_cycles = 9 + total_mix_vol = sample_vol + mastermix_vol # 12 uL total + + # Execute commands in order specified in description + # Command 1: Set sample temperature + temp_mod_sample.set_temperature(sample_temp) + + # Command 2: Set mastermix temperature + temp_mod_mastermix.set_temperature(mastermix_temp) + + # Command 3: Transfer mastermix + p20_single.transfer( + mastermix_vol, + mastermix_wells, + dest_wells, + new_tip='once' + ) + + # Command 4: Transfer samples and mix + p20_single.transfer( + sample_vol, + sample_wells, + dest_wells, + new_tip='always', + mix_after=(mix_cycles, total_mix_vol), + blow_out=True, + blowout_location='destination well' + ) + + # Command 5 and 6: Deactivate temperature modules + temp_mod_mastermix.deactivate() + temp_mod_sample.deactivate() + + + +#### Example 2: PCR protocol + + +I want to set up a PCR reaction plate using both single-channel and multi-channel pipettes. Here's what we need to do: + +First, using the single-channel pipette on the right: + +- We'll add 7 microliters of mastermix from tubes in the tube rack to specific wells in our destination plate. The source tubes and destination wells are listed in a CSV file. Let's use a fresh tip for each different mastermix tube we work with. + +Then, using the 8-channel pipette on the left: + +- We're going to transfer 3 microliters of samples in triplicate. Here's how: +- Take samples from column 1 of the source plate and transfer them to: + - Column 1 of the destination plate (change tip) + - Column 2 of the destination plate (change tip) + - Column 3 of the destination plate +- Repeat this same pattern for the remaining columns in the source plate, always making three copies of each column and changing tips between transfers. + + + +from opentrons import protocol_api + +requirements = { +'robotType': 'Flex', +'apiLevel': '2.15' +} + +def run(protocol: protocol_api.ProtocolContext): + + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + NUM_COL = 3 + STRIDE = 3 + + # Load labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D3') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # Transfer in triplicate + col_ctr = 0 + for s in dna_plate.rows()[0][:NUM_COL]: + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+1], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+2], new_tip='never') + multi_pip.drop_tip() + + col_ctr += STRIDE + + + +#### Example 3: Transfer reagent protocol + + +I want to do a series of liquid transfers using two different pipettes. Here's what we need to do: + +First, using the P20 single-channel pipette on the right: + +- Take 15 microliters from the first well of our reservoir and transfer it to every well in both of our destination plates. We can use the same tip for all these transfers. +- Then, transfer 20 microliters from each well of our 384-well source plate to the corresponding wells in our first destination plate (the 384-well plate). We can keep using the same tip for these transfers too. + +Next, using the P300 single-channel pipette on the left: + +- Transfer 100 microliters from each well of our 96-well source plate to the corresponding wells in our second destination plate (the 96-well plate). For this step, we'll need to use a fresh tip for each transfer. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +"protocolName": "Reagent Transfer ", +"author": "OGA", +"description": "Transfer reagents from multile source labware to multiple destination labware", +"apiLevel": "2.16", +} + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("nest_1_reservoir_195ml", location=7) +source_2 = protocol.load_labware("biorad_384_wellplate_50ul", location=8) +source_3 = protocol.load_labware("biorad_96_wellplate_200ul_pcr", location=9) +destination_1 = protocol.load_labware("corning_384_wellplate_112ul_flat", location=1) +destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location=2) + + tiprack300 = protocol.load_labware("opentrons_96_tiprack_300ul", location=10) + tiprack20 = protocol.load_labware("opentrons_96_tiprack_20ul", location=11) + + # pipettes + p300s = protocol.load_instrument("p300_single_gen2", mount="left", tip_racks=[tiprack300]) + p20s = protocol.load_instrument("p20_single_gen2", mount="right", tip_racks=[tiprack20]) + + # volumes setup + transfer_vol_1 = 15 + transfer_vol_2 = 20 + transfer_vol_3 = 100 + + # wells setup + source_wells_1 = source_1.wells_by_name()['A1'] + source_wells_2 = source_2.wells() + source_wells_3 = source_3.wells() + destination_wells_1 = destination_1.wells() + destination_wells_2 = destination_2.wells() + all_destinations = destination_wells_1 + destination_wells_2 + + # commands + p20s.transfer(transfer_vol_1, source_wells_1, all_destinations, new_tip="once") + p20s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") + p300s.transfer(transfer_vol_3, source_wells_3, destination_wells_2, new_tip="always") + + + +#### Example 4: Transfer reagent protocol + + +I want to pool samples from multiple tube racks into a deep well plate. I'll be using a single-channel P300 pipette mounted on the right side. + +Here's what we need to do: + +1. Take 20 µL from each tube in the first tube rack and pool them all into well A1 of the deep well plate. We'll use a fresh tip for each tube. + +2. Then, take 20 µL from each tube in the second tube rack and pool them all into well B1 of the deep well plate. Again, use a fresh tip for each tube. + +3. Next, take 20 µL from each tube in the third tube rack and pool them all into well C1 of the deep well plate. Use a fresh tip for each tube. + +4. Finally, take 20 µL from each tube in the fourth tube rack and pool them all into well D1 of the deep well plate. Use a fresh tip for each tube. + + + +from opentrons import protocol_api + +metadata = { +'protocolName': 'Sample Aliquoting & Plate Prep', +'author': 'ChatGPT', +'apiLevel': '2.16' +} + +def run(protocol: protocol_api.ProtocolContext): + + # Load labware + source_labware1 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 1) + source_labware2 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 2) + source_labware3 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 4) + source_labware4 = protocol.load_labware('opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap', 5) + destination_labware = protocol.load_labware('nest_96_wellplate_2ml_deep', 3) + + tiprack1 = protocol.load_labware('opentrons_96_filtertiprack_200ul', 7) + tiprack2 = protocol.load_labware('opentrons_96_filtertiprack_200ul', 8) + + # Load pipette + p300_single = protocol.load_instrument('p300_single_gen2', 'right', tip_racks=[tiprack1, tiprack2]) + + # Transfer samples + p300_single.transfer(20, source_labware1.wells(), destination_labware.wells_by_name()['A1'], new_tip='always') + p300_single.transfer(20, source_labware2.wells(), destination_labware.wells_by_name()['B1'], new_tip='always') + p300_single.transfer(20, source_labware3.wells(), destination_labware.wells_by_name()['C1'], new_tip='always') + p300_single.transfer(20, source_labware4.wells(), destination_labware.wells_by_name()['D1'], new_tip='always') + + + +#### Example 5: Reagent transfer protocol + + +I want to perform a series of liquid transfers using two different single-channel pipettes. Here's what we need to do: + +First, using the 50 µL pipette mounted on the left: + +- Take 15 µL from the reservoir and transfer it to every well in both our 384-well and 96-well destination plates. We can use the same tip for all these transfers. +- Then, transfer 20 µL from each well of our 384-well source plate to the corresponding wells in our 384-well destination plate. We can keep using the same tip for these transfers too. + +Finally, using the 1000 µL pipette mounted on the right: + +- Transfer 100 µL from each well of our 96-well source plate to the corresponding wells in our 96-well destination plate. For this step, we'll need to use a fresh tip for each transfer. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +"protocolName": "Reagent Transfer ", +"author": "Opentrons Generative AI", +"description": "Transfer reagents from multile source labware to multiple destination labware", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("nest_1_reservoir_195ml", location='B1') +source_2 = protocol.load_labware("biorad_384_wellplate_50ul", location='B2') +source_3 = protocol.load_labware("biorad_96_wellplate_200ul_pcr", location='B3') +destination_1 = protocol.load_labware("corning_384_wellplate_112ul_flat", location='D1') +destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D2') + + tip1000 = protocol.load_labware('opentrons_flex_96_filtertiprack_200ul', 'A1') + tip50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p1000s = protocol.load_instrument('flex_1channel_1000','right',tip_racks = [tip1000]) + p50s = protocol.load_instrument('flex_1channel_50','left',tip_racks = [tip50]) + + # load trash bin + trash = protocol.load_trash_bin('A3') + + # volumes setup + transfer_vol_1 = 15 + transfer_vol_2 = 20 + transfer_vol_3 = 100 + + # wells setup + source_wells_1 = source_1.wells_by_name()['A1'] + source_wells_2 = source_2.wells() + source_wells_3 = source_3.wells() + destination_wells_1 = destination_1.wells() + destination_wells_2 = destination_2.wells() + + # commands + p50s.transfer(transfer_vol_1, source_wells_1, destination_wells_1+destination_wells_2, new_tip="once") + p50s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") + p1000s.transfer(transfer_vol_3, source_wells_3, destination_wells_2, new_tip="always") + + + +#### Example 6: Reagent transfer protocol + + +I want to pool samples from two different plates into a reservoir using a single-channel pipette mounted on the left side. Here's what we need to do: + +First, let's pool samples from our first source plate: + +- Take 100 µL from each well in the first plate and transfer it to the first well of our reservoir +- We can use the same tip for all these transfers to save time + +Then, for our second source plate: + +- Again, take 100 µL from each well and add it to the same well in our reservoir where we pooled the first set +- Keep using the same tip for these transfers too + +Remember, we're treating these as two separate steps, but both are basically pooling samples from different source plates into the same destination well. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +'protocolName': 'Reagent Transfer', +'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +# protocol run function + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C1') +source_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C2') +destination_1 = protocol.load_labware("nest_1_reservoir_195ml", location='D1') + + tiprack200 = protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location='B2') + + # pipettes + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[tiprack200]) + + # load trash bin + trash = protocol.load_trash_bin('A3') + + # volume setup + transfer_vol_1 = 100 + transfer_vol_2 = 100 + + # wells setup + source_wells_1 = source_1.wells() + source_wells_2 = source_2.wells() + destination_wells_1 = destination_1.wells_by_name()['A1'] + + # commands + p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + + p1000s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") + + + +#### Example 7: PCR protocol + + +I want to run a PCR protocol using three temperature-controlled modules: a thermocycler and two temperature modules (one for samples and one for mastermix). Here's what we need to do: + +First, let's set up our temperatures: + +- Set the thermocycler block to 22°C and its lid to 95°C +- Warm up the sample temperature module to 37°C +- Cool down the mastermix module to 10°C + +For the liquid handling steps, using our 96-channel pipette: + +1. Transfer 20 µL of mastermix, taking it from 5mm below the liquid surface and dispensing it 2mm from the bottom of the destination wells. We can use the same tip for this. + +2. Next, transfer 20 µL of sample, aspirating from 3mm above the well bottom and dispensing 7mm from the top of the destination wells. Do this at half the normal flow rate. Mix everything well - 5 cycles with the total 40 µL volume. When finished, pull the tips out slowly at 5 mm/s. Use the same tip for this transfer. + +For the PCR cycling: + +1. Move our plate to the thermocycler and close the lid +2. Run these steps: + - One cycle at 74°C for 65 seconds + - 25 cycles of: + - 60°C for 7 seconds + - 84°C for 19 seconds + - 57°C for 44 seconds + - One final cycle at 75°C for 8 minutes + - Hold everything at 4°C + +Finally: + +1. Open the thermocycler lid and move the plate back to its original position +2. We'll pause here - you'll need to seal the plate and put it in the fridge at 4°C +3. Turn off all the temperature modules + + + +from opentrons import protocol_api + +metadata = { +'protocol_name': 'PCR Amplification protocol', +'author': 'Opentrons Generative AI', +'description': 'PCR Amplification protocol with 25 cycles', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +def run(protocol: protocol_api.ProtocolContext): # Sample parameters +sample_volume_ul = 20 +master_mix_volume_ul = 20 +mix_cycles = 5 +total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul +return_slot = 'C3' + + master_mix_temperature_c = 10 + sample_temperature_c = 37 + step1_cycles = 1 + step2_cycles = 25 + step3_cycles = 1 + + # Thermocycler parameters + lid_temperature_c = 95 + initial_block_temperature_c = 22 + final_hold_temperature_c = 4 + + # Modules + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 'D1') + master_mix_temperature_module = protocol.load_module('temperature module gen2', 'D3') + + # Adapters + sample_adapter = sample_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + master_mix_adapter = master_mix_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + + # Labware + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + destination_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'C3') + tips_50ul = [ + protocol.load_labware( + 'opentrons_flex_96_filtertiprack_50ul', + slot, + adapter="opentrons_flex_96_tiprack_adapter" + ) + for slot in ['A2', 'B2', 'C2'] + ] + + # Pipette + pipette_96channel = protocol.load_instrument('flex_96channel_1000', 'left', tip_racks=tips_50ul) + # load trash bin + trash = protocol.load_trash_bin('A3') + + # Well allocation + sample_source_wells = sample_plate['A1'] + destination_wells = destination_plate['A1'] + master_mix_source_well = master_mix_plate['A1'] + + # Set thermocycler block and lid temperature + thermocycler_module.set_block_temperature(initial_block_temperature_c) + thermocycler_module.open_lid() + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Temperature module setup + sample_temperature_module.set_temperature(sample_temperature_c) + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Master mix transfer + pipette_96channel.transfer( + master_mix_volume_ul, + master_mix_source_well.top(-5), + destination_wells.bottom(2), + new_tip='once' + ) + + # Sample transfer + pipette_96channel.pick_up_tip() + pipette_96channel.aspirate(sample_volume_ul, sample_source_wells.bottom(3), rate=0.5) + pipette_96channel.dispense(sample_volume_ul, destination_wells.top(-7), rate=0.5) + pipette_96channel.mix(mix_cycles, total_mix_volume_ul) + pipette_96channel.move_to(destination_wells.top(), speed=5) + pipette_96channel.drop_tip() + + # Moving the plate to the thermocycler + protocol.move_labware(destination_plate, thermocycler_module, use_gripper=True) + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=step1_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=step2_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=step3_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.set_block_temperature(final_hold_temperature_c) + thermocycler_module.open_lid() + + # Moving the plate back to its original location + protocol.move_labware(destination_plate, return_slot, use_gripper=True) + + # Optional: pause for manual intervention + protocol.pause("Pick up the destination plate, seal it, and refrigerate at 4C.") + + # Deactivate temperature modules at the end of the protocol + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + + + +#### Example 8: Serial dilution protocol + + +I want to perform a serial dilution using an 8-channel pipette mounted on the right side. Here's what we need to do: + +First, let's set up our key measurements: + +- We're doing a 1:3 dilution series with 10 dilution steps +- We'll work with a total volume of 150 µL in each well +- This means we'll transfer 50 µL between wells and add 100 µL of diluent +- We'll use a 10 µL air gap for all our transfers + +Here's the step-by-step process: + +1. Start by adding diluent to our plate: + + - Using one tip, transfer 100 µL of diluent (green liquid) from the reservoir to wells A2 through A11 + - Keep using the same tip and remember to use the air gap for each transfer + +2. Now for the serial dilution: + + - Get a fresh tip + - Starting with well A1 (which has our red sample), transfer 50 µL to well A2 + - Mix well - 5 times with 75 µL + - Continue this pattern down the row: + - Transfer 50 µL from A2 to A3, mix + - A3 to A4, mix + - And so on until you reach A11 + - Use the same tip for all these transfers and remember the air gap + +3. Finally, let's add our blank: + - Get a fresh tip + - Transfer 100 µL of diluent to well A12 + - Use the air gap for this transfer too + + + +metadata = { + 'protocolName': 'Customizable Serial Dilution', + 'author': 'Opentrons ', + 'source': 'Protocol Library' +} + +requirements = { +"robotType": "Flex", +"apiLevel": "2.19" +} + +def run(protocol): + + # Constants + DILUTION_FACTOR = 3 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150.0 + AIR_GAP_VOLUME = 10 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Labware setup + trough = protocol.load_labware('nest_12_reservoir_15ml', 'D2') + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D3') + tip_name = "opentrons_flex_96_filtertiprack_1000ul" + tipracks = [ + protocol.load_labware(tip_name, slot) + for slot in ["C1", "D1"] + ] + + # Pipette setup + pipette = protocol.load_instrument('flex_8channel_1000', 'right', tipracks) + + # Waste setup + trash = protocol.load_trash_bin("A3") + + # Reagent setup + diluent = trough.wells()[0] + source = plate.columns()[0] + + # Define and load liquids + diluent_liquid = protocol.define_liquid( + name="Dilutent", + description="Diluent liquid is filled in the reservoir", + display_color="#33FF33" + ) + sample_liquid = protocol.define_liquid( + name="Sample", + description="Non-diluted samples are loaded in the 1st column", + display_color="#FF0000" + ) + + diluent.load_liquid(liquid=diluent_liquid, volume=0.8 * diluent.max_volume) + for well in source: + well.load_liquid(liquid=sample_liquid, volume=TOTAL_MIXING_VOLUME) + + # Set up dilution destinations + dilution_destination_sets = [[row] for row in plate.rows()[0][1:NUM_DILUTIONS+1]] + dilution_source_sets = [[row] for row in plate.rows()[0][:NUM_DILUTIONS]] + blank_set = [plate.rows()[0][NUM_DILUTIONS+1]] + + # 1. Distribute diluent + all_diluent_destinations = [well for wells in dilution_destination_sets for well in wells] + pipette.pick_up_tip() + for dest in all_diluent_destinations: + pipette.transfer( + diluent_volume, + diluent, + dest, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + # 2. Perform serial dilutions + pipette.pick_up_tip() + for source_set, dest_set in zip(dilution_source_sets, dilution_destination_sets): + for s, d in zip(source_set, dest_set): + pipette.transfer( + transfer_volume, + s, + d, + air_gap=AIR_GAP_VOLUME, + mix_after=(5, TOTAL_MIXING_VOLUME/2), + new_tip='never' + ) + pipette.drop_tip() + + # 3. Add blank + pipette.pick_up_tip() + for blank_well in blank_set: + pipette.transfer( + diluent_volume, + diluent, + blank_well, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + + +#### Example 9: Serial dilution + + +I want to perform a serial dilution protocol using a multi-channel P300 pipette mounted on the left side. We'll be working with a temperature-controlled setup and need to achieve a 1.5x dilution factor across 10 wells, with a total mixing volume of 150 µL per well. + +Here's what we need to do: + +First, let's calculate our volumes: + +- Transfer volume will be 150 µL divided by 1.5 +- Diluent volume will be 150 µL minus our transfer volume + +Now for the actual steps: + +1. Let's start by adding diluent to our dilution wells: + + - Take diluent from the first reservoir well and add our calculated diluent volume to wells 2 through 10 in the first row of our temperature-controlled plate + - Use a 10 µL air gap for each transfer + - Use fresh tips for each well + +2. Now for the serial dilution: + + - Starting from well 1, we'll transfer our calculated transfer volume to well 2 + - After each transfer, mix 5 times using (150 µL - 5 µL) + - Keep using a 10 µL air gap + - Use new tips for each transfer + - Continue this pattern, moving from well to well until we reach well 10 + +3. Finally, add a blank to the last well: + - Transfer our calculated diluent volume from the first reservoir well to well 10 + - Use a 10 µL air gap + - Use a fresh tip for this transfer + + + +metadata = { + 'protocolName': 'Serial Dilution for Eskil', + 'author': 'John C. Lynch', + 'source': 'Custom Protocol Request', + 'apiLevel': '2.19' +} + +def run(protocol): + + # Constants + PLATE_TYPE = 'opentrons_96_aluminumblock_nest_wellplate_100ul' + DILUTION_FACTOR = 1.5 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Load temperature module and labware + temp_module = protocol.load_module('temperature module gen2', '4') + reservoir = protocol.load_labware('nest_12_reservoir_15ml', '1') + dilution_plate = temp_module.load_labware(PLATE_TYPE) + + # Load tipracks + tipracks = [ + protocol.load_labware('opentrons_96_tiprack_300ul', slot) + for slot in ['2', '3'] + ] + + # Load pipette + pipette = protocol.load_instrument( + 'p300_multi_gen2', + mount='left', + tip_racks=tipracks + ) + + # 1. Distribute diluent + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][1:NUM_DILUTIONS], + air_gap=10, + new_tip='always' + ) + + # 2. Perform serial dilutions + sources = dilution_plate.rows()[0][:NUM_DILUTIONS-1] + dests = dilution_plate.rows()[0][1:NUM_DILUTIONS] + + pipette.transfer( + transfer_volume, + sources, + dests, + air_gap=10, + mix_after=(5, TOTAL_MIXING_VOLUME-5), + new_tip='always' + ) + + # 3. Add blank + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][-1], + air_gap=10, + new_tip='always' + ) + + + +#### Example 10 + + +I want to perform a serial dilution using a single-channel pipette mounted on the left side. Here's what we need to do: + +First, let's add our diluent: + +- Take 100 µL of diluent from the first well of our reservoir and distribute it to every well in our plate. + +Then, for the serial dilution: + +- For each of the 8 rows in our plate: + 1. Start by transferring 100 µL of our solution from well A2 of the reservoir to the first well of the row + 2. Mix it well - 3 times with 50 µL + 3. Then move along the row from left to right: + - Transfer 100 µL from each well to the next well + - Mix 3 times with 50 µL after each transfer + - Continue this pattern for 11 transfers to complete the row + 4. Repeat this process for all 8 rows + + + +from opentrons import protocol_api + +metadata = { +"protocolName": "Serial Dilution Tutorial – Flex 1-channel", +"description": """serial dilution""", +"author": "New API User" +} + +requirements = { +"robotType": "Flex", +"apiLevel": "2.16" +} + +def run(protocol: protocol_api.ProtocolContext): +tips = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D1") +reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2") +plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "D3") +trash = protocol.load_trash_bin("A3") +left_pipette = protocol.load_instrument("flex_1channel_1000", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) + + + +#### Example 11 + + +Liquid Transfer with Heater-Shaker + +prompt description: + +```text +Hi, please help me write a protocol for Flex. +Pipette: 96 channel +Decklayout: +Slot C1: Nest 195 1 well reservoir +Slot D1: Heater shaker module with corning 96 well flat bottom plate +Slot A2: 200 ul tiprack +Slot B2: 1000 ul tiprack for flex +Slot C2: 50 ul tiprack +Slot D2: PCR plate Steps: + +Open the labware latch and allow the user to load the plate +Close the labware latch +Using 200 ul tip transfer 70 ul liquid from slot c1 to d1. +Using 50 ul tip transfer 10 ul liquid from slot d2 to D1. +shake the plate at 2000 rpm for 1 minute +``` + + + + + +```python +from opentrons import protocol_api + +metadata = {{ + 'protocolName': 'Liquid Transfer with Heater Shaker', + 'author': 'User', + 'description': "Transfer liquids between reservoir, PCR plate, and heater shaker module's plate." +}} +requirements = {{"robotType": "Flex", "apiLevel": "2.19"}} + +def run(protocol: protocol_api.ProtocolContext): + # Load trash before commands + # use a waste chute or a trashbin depending on the setup + trash = protocol.load_trash_bin("A3") + #chute = protocol.load_waste_chute() + + # Modules + heater_shaker_module = protocol.load_module('heaterShakerModuleV1', 'D1') + heater_shaker_plate = heater_shaker_module.load_labware('corning_96_wellplate_360ul_flat') + + # Labware + reservoir = protocol.load_labware('nest_1_reservoir_195ml', 'C1') + pcr_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D2') + tiprack_200ul = protocol.load_labware('opentrons_flex_96_tiprack_200ul', 'A2', adapter = "opentrons_flex_96_tiprack_adapter") + tiprack_1000ul = protocol.load_labware('opentrons_flex_96_tiprack_1000ul', 'B2', adapter = "opentrons_flex_96_tiprack_adapter") + tiprack_50ul = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'C2', adapter = "opentrons_flex_96_tiprack_adapter") + + # Pipettes + pip96 = protocol.load_instrument('flex_96channel_1000', mount='left', tip_racks=[tiprack_200ul, tiprack_50ul]) + + # Steps + # 1. Open the labware latch and allow the user to load the plate + heater_shaker_module.open_labware_latch() + + + protocol.pause("Please put the Corning 96 well plate and press continue") + # 2. Close the labware latch + heater_shaker_module.close_labware_latch() + protocol.comment("Just a message is displayed. This step is") + # 3. Using 200 ul tip transfer 70 ul liquid from slot c1 to d1. + + pip96.transfer(70, reservoir['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 4. Using 50 ul tip transfer 10 ul liquid from slot d2 to D1. + pip96.transfer(10, pcr_plate['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 5. Shake the plate at 2000 rpm for 1 minute + heater_shaker_module.set_and_wait_for_shake_speed(rpm=2000) + protocol.delay(minutes=1) + heater_shaker_module.deactivate_shaker() +``` + + diff --git a/opentrons-ai-server/api/storage/docs/commands-v0.0.1.md b/opentrons-ai-server/api/storage/docs/commands-v0.0.1.md new file mode 100644 index 00000000000..000e874f843 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/commands-v0.0.1.md @@ -0,0 +1,1259 @@ +#### + +Note when working with temperature module +PCR plate does not go directly on the module. We need thermal adapter. +Temperature Module White Paper suggests using the "PCR block" and a water. + +Hence the following pattern: + +```python +temp_module = protocol.load_module('temperature module gen2', 1) +adapter = temp_module.load_adapter("opentrons_96_well_aluminum_block") +plate = adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') +``` + +#### + +A good example of using a `transfer` method: +The following is correct: + +```python +p1000s.transfer(transfer_vol, src, dest_wells, new_tip='always') +``` + +The following is incorrect: + +```python +for src in src_wells: + p1000s.transfer(transfer_vol, src, dest_wells, new_tip='always') +``` + +Note that `transfer` function uses `for` operator implicitly. + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 50 uL from wells A1, A2 in source labware 1 +to B6, B7 in source labware 2. Reuse the same tip for each transfer. + +The following is correct: + +```python +transfer_vol_1 = 50 # setup volume + +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] # source setup wells +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] # destination setup wells + +p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") # transfer function without any loop +``` + +The following is not correct since it calls transfer function twice rather than once: + +```python +p300_single.transfer(50, source_labware_1.wells_by_name()['A1'], source_labware_2.wells_by_name()['B6'], new_tip='once') +p300_single.transfer(50, source_labware_1.wells_by_name()['A2'], source_labware_2.wells_by_name()['B7'], new_tip='never') +``` + +#### + +Use the left-mounted P1000 Single-Channel GEN2 pipette to transfer 200 uL of reagent from wells A7, A6, A5, A2, A3 +of the source labware to the corresponding wells A5, A9, A1, A10, A2 of the destination labware. Use a new tip for each transfer. + +```python +TRANSFER_VOL_1 = 200 +SRC_WELL_1 = [source.wells_by_name()[well] for well in ['A7', 'A6', 'A5', 'A2', 'A3']] +DEST_WELL_1 = [destination.wells_by_name()[well] for well in ['A5', 'A9', 'A1', 'A10', 'A2']] + +# command 1 +p1000s_1.transfer(TRANSFER_VOL_1, SRC_WELL_1, DEST_WELL_1, new_tip="always") +``` + +#### + +Use the right-mounted P1000 Single-Channel GEN2 pipette to transfer 18 uL of liquid from wells A9, A12, A6, A10, A3 +of the source labware to the corresponding wells A7, A11, A6, A3, A9 of the destination labware. Use the same tip for all transfers. + +```python +TRANSFER_VOL_2 = 18 +SRC_WELL_2 = [source.wells_by_name()[well] for well in ['A9', 'A12', 'A6', 'A10', 'A3']] +DEST_WELL_2 = [source.wells_by_name()[well] for well in ['A7', 'A11', 'A6', 'A3', 'A9']] + +# command 2 +p1000s_2.transfer(TRANSFER_VOL_2, SRC_WELL_2, DEST_WELL_2, new_tip="once") +``` + +#### + +Using P300 Single-Channel GEN2 pipette on the left mount, transfer 119 uL of reagent +from first well in source labware to E12, G12, B9, A6, D7 wells in the destination labware. +Use a new tip for each transfer. + +```python +vol = 119 +src_well = source.wells_by_name()['A1'] +dest_wells = [destination.wells_by_name()[well] for well in ['E12', 'G12', 'B9', 'A6', 'D7']] + +# commands +p300s.transfer(vol, src_well, dest_wells, new_tip="always") +``` + +#### + +Using P20 Single Channel, transfer 13ul of reagent from the first tube of the source rack to each well in the destination plate. +Use the same tip for each transfer. + +```python +# parameters +vol = 13 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p20s.transfer(vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using P20 Single Channel GEN2 pipette on right mount, transfer 16 uL from the first well of source labware 1 to each well +in destination labware 1 and destination labware 2. Reuse the same tip + +```python +# volumes setup +transfer_vol_1 = 16 + +# wells setup +source_wells_1 = source_1.wells_by_name()['A1'] +destination_wells_1 = destination_1.wells() +destination_wells_2 = destination_2.wells() +all_destinations = destination_wells_1 + destination_wells_2 + +# commands +p20s.transfer(transfer_vol_1, source_wells_1, all_destinations, new_tip="once") +``` + +#### + +Using P20 Single Channel GEN2 pipette on right mount, transfer 23 uL from each well in source labware 2 to +each well in the destination labware 1. Reuse the same tip. + +```python +# volumes setup +transfer_vol_2 = 23 + +# wells setup +source_wells_2 = source_2.wells() +destination_wells_1 = destination_1.wells() + +# commands +p20s.transfer(transfer_vol_2, source_wells_2, destination_wells_1, new_tip="once") +``` + +#### + +Using P20 Multi-Channel GEN2 pipette on the right mount, transfer 5 uL of reagent +from first column in source labware to columns 5, 9, 1, 10, and 2 in the destination labware. +Use the same tip everytime. + +```python +# parameters +vol = 5 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(vol, src_col, dest_cols, new_tip="once") +``` + +#### + +Using P20 Multi-Channel GEN2 pipette on the left mount, transfer 24 uL of reagent +from columns 4, 3, 6, 1, 11 in source labware to columns 5, 9, 1, 10, 2 in the same source labware. +Use a new tip everytime. + +```python +# parameters +vol = 24 +src = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest = [source.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(vol, src, dest, new_tip="always") +``` + +#### + +Using P300 Multi Channel, transfer 55 uL of sample from each column of the source plate +into the corresponding columns of the destination deep well plate. +Change tips for each transfer. + +```python +# parameters +vol = 55 +src_cols = source.columns() +dest_cols = destination.columns() + +# commands +p300m.transfer(vol, src_cols, dest_cols, new_tip='always') +``` + +#### + +Using P300 Single Channel GEN2, transfer 70ul of reagent from the first tube of the source rack to each well in the destination plate. +Keep the same tip for each transfer. + +```python +# parameters +vol = 70 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p300s.transfer(vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using P300 Single Channel GEN2, transfer 75ul of samples from each tube in the source tube rack to each well of the destination plate. +Use a new tip for each transfer. + +```python +# parameters +vol = 75 +src_wells = source.wells() +dest_wells = destination.wells() + +# commands +p300s.transfer(vol, src_wells, dest_wells, new_tip='always') +``` + +#### + +Using P300 Multi-channel pipette on the left mount, transfer 65 uL of reagent from first column in the source labware 1 +to all the columns in destination labware 1. Keep the same set of tips for this entire set of transfers within this step. + +```python +transfer_vol_1 = 65 + +# wells setup +source_wells_1 = source_1.columns_by_name()['1'] +destination_wells_1 = destination_1.columns() + +p300m.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using P1000 Single-Channel GEN2 pipette on left mount, transfer 175.0 uL of reagent +from H10, F12, D7, B1, C8 wells in source labware +to first well in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 175.0 +src_wells = [source.wells_by_name()[well] for well in ['H10', 'F12', 'D7', 'B1', 'C8']] +dest_well = destination.wells_by_name()['A1'] + +# commands +p1000s.transfer(transfer_vol, src_wells, dest_well, new_tip="always") +``` + +#### + +Using P300 Single-channel GEN2 pipette on left mount, transfer 51 uL from wells A1, A2 in source labware 1 +to B6, B7 in source labware 2. Reuse the same tip. + +```python +# volume setup +transfer_vol_1 = 51 + +# well setup +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + +# commands +p300s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using P20 Single-channel GEN2 pipetet on right mount, transfer 14 uL from wells C4, C6 in source labware 2 +to A3, A4 in source labware 1. Reuse the same tip. + +```python +# volume setup +transfer_vol_2 = 14 + +# well setup +source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] +destination_wells_2 = [source_1.wells_by_name()[wells] for wells in ['A3', 'A4']] + +# commands +p20s.transfer(transfer_vol_2, source_wells_2, destination_wells_2, new_tip="once") +``` + +#### + +Using P20 Single-channel GEN2 pipette on right mount, transfer 17 uL from wells B6, B7 in source labware 2 +to A1, B1 in destination labware 1. Use a new tip each time. + +```python +# volume setup +transfer_vol = 17 +# well setup +source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] +destination_wells_1 = [destination_1.wells_by_name()[wells] for wells in ['A1', 'B1']] +# commands +p20s.transfer(transfer_vol, source_wells_2, destination_wells_1, new_tip="always") +``` + +#### + +Using P20 Single-channel GEN2 pipette on right mount, transfer 15 uL from wells C4, C6 in source labware 2 +to A1, B1 in destination labware 2. Use a new tip each time. + +```python +# volume setup +transfer_vol = 15 + +# well setup +source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] +destination_wells_2 = [destination_2.wells_by_name()[wells] for wells in ['A1', 'B1']] + +# commands +p20s.transfer(transfer_vol, source_wells_2, destination_wells_2, new_tip="always") +``` + +#### + +Using the P300 Single-Channel GEN2, pool [transfer_vol]ul from all tubes in source labware into A1 of the destination labware. +Change tips between each tube. + +```python +# well setup +source_wells = source.wells() +destination_wells = [destination.wells_by_name()[wells] for wells in ['A1']] +# Transfer samples +p300_single.transfer(transfer_vol, source_wells, destination_wells, new_tip='always') +``` + +#### + +Using P300 single-channel GEN2 pipette, pool 95 uL of liquid from all the wells in source labware 1 to +the first well in destination labware 1. Use the same tip throughout. + +```python +# volume setup +transfer_vol_1 = 95 +# wells setup +source_wells_1 = source_1.wells() +destination_wells_1 = destination_1.wells_by_name()['A1'] + +# commands +p300s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using the P20 Multi-Channel GEN2 pipette on the right mount, transfer 3 uL of reagent from the first column in the source labware +to columns 5, 9, 1, 10, 2 in the destination labware. Use a new set of tips for each transfer. + +```python +# parameters +transfer_vol = 3 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(transfer_vol, src_col, dest_cols, new_tip='always') +``` + +#### + +Using the P20 Multi-Channel GEN2 pipette on the right mount, transfer 8 uL of reagent from source columns 4, 3, 6, 1, +and 11 to columns 5, 9, 1, 10, and 2 in the destination labware. Use the same set of tips for all transfers. + +```python +# parameters +transfer_vol = 8 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p20m.transfer(transfer_vol, src_cols, dest_cols, new_tip="once") +``` + +#### + +Using P300 Multi-Channel GEN2 pipette on the left mount, transfer 38 uL of reagent from 4, 3, 6, 1, 11 +columns in the source labware to 5, 9, 1, 10, 2 columns in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 38 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p300m.transfer(transfer_vol, src_cols, dest_cols, new_tip="always") +``` + +#### + +Using P20 Single GEN2 pipette on the right mount, transfer 10 uL of reagent +from the first well of source labware 2 to all the wells in the destination labware. Reuse the same tip. + +```python +# volumes setup +transfer_vol_1 = 10 +# wells setup +source_wells_2 = source_labware_2.wells_by_name()['A1'] +destination_wells_1 = [dest.wells() for dest in destination_list] # a list of destinations +# commands +p20s.transfer(transfer_vol_1, source_wells_2, destination_wells_1, new_tip="once") +``` + +#### + +Using P300 Single GEN2 on the left mount, perform a well to well transfer of 90 uL from source +labware to the destination labware. Use a new tip each time. + +```python +# volumes setup +transfer_vol +# wells setup +source_wells = [src.wells() for src in source_labware] +destination_wells = [dest.wells() for dest in destination_list] # a list of destinations +# commands +p300s.transfer([transfer_vol], source_wells, destination_wells, new_tip="always") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, +transfer 186.0 uL of reagent from A7, A6, A5, A2, A3 of the source labware to A5, A9, A1, A10, A2 the destination labware. +Use a new tip for all transfers. + +```python +# parameters +TRANSFER_VOL = 186.0 +SRC_WELLS = [source.wells_by_name()[well] for well in ['A7', 'A6', 'A5', 'A2', 'A3']] +DEST_WELLS = [destination.wells_by_name()[well] for well in ['A5', 'A9', 'A1', 'A10', 'A2']] + +# command 1 +p1000s_1.transfer(TRANSFER_VOL, SRC_WELLS, DEST_WELLS, new_tip="always") +``` + +#### + +Use Flex 1-Channel 1000 uL Pipette on right mount, +transfer 10 uL of liquid from A9, A12, A6, A10, A3 of source labware to A7, A11, A6, A3, A9 of the destination labware. +Use the same tip for all transfers. + +```python +# parameters +TRANSFER_VOL = 10 +# well setup +SRC_WELLS = [source.wells_by_name()[well] for well in ['A9', 'A12', 'A6', 'A10', 'A3']] + = [destination.wells_by_name()[well] for well in ['A7', 'A11', 'A6', 'A3', 'A9']] + +# command 1 +[pipette object].transfer(TRANSFER_VOL, SRC_WELLS, DEST_WELLS, new_tip="once") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 127.0 uL of reagent from the first well in source labware +to E12, G12, B9, A6, D7 wells in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 127.0 +src_well = source.wells_by_name()['A1'] +dest_wells = [destination[well] for well in ['E12', 'G12', 'B9', 'A6', 'D7']] + +# commands +[pipette object].transfer(transfer_vol, src_well, dest_wells, new_tip="always") +``` + +#### + +Using Flex 1-Channel 50 uL Pipette, transfer 2ul of reagent from the first tube of the source rack to each well in the destination plate. +Use the same tip for each transfer. + +```python +# parameters +transfer_vol = 2 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p50s.transfer(transfer_vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using the Flex 1-Channel 50 uL Pipette, transfer 25 uL from the first well of source labware 1 to each well +in destination labware 1 and destination labware 2. Use the same tip for each transfer. + +```python +# volumes setup +transfer_vol_1 = 25 + +# wells setup +source_wells_1 = source_1.wells_by_name()['A1'] +destination_wells_1 = destination_1.wells() +destination_wells_2 = destination_2.wells() +all_dest = destination_wells_1+destination_wells_2 + +# commands +p50s.transfer(transfer_vol_1, source_wells_1, all_dest, new_tip="once") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on right mount, transfer 5 uL of reagent from the first column in source labware +to columns 4, 8, 1, 9, and 2 in the destination labware. Use the same tip for all transfers. + +```python +# parameters +transfer_vol = 5 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['4', '8', '1', '9', '2']] + +# commands +p50m.transfer(transfer_vol, src_col, dest_cols, new_tip="once") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on left mount, transfer 24.0 uL of reagent from columns 3, 2, 5, 1, 10 +to columns 4, 8, 1, 9, 2 in the same source labware. Use a new tip for each transfer. + +```python +#parameters +transfer_vol = 24.0 +src_cols = [source.columns_by_name()[idx] for idx in ['3', '2', '5', '1', '10']] +dest_cols = [source.columns_by_name()[idx] for idx in ['4', '8', '1', '9', '2']] + +# commands +p50m.transfer(transfer_vol, src_cols, dest_cols, new_tip="always") +``` + +#### + +Using Flex 8-Channel 1000 uL Pipette , transfer 70ul of sample from each well of the first column of the source plate into the first column of +the destination plate. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 70 +src_col = source.columns_by_name()['1'] +dest_col = destination.columns_by_name()['1'] + +# commands +p1000m.transfer(transfer_vol, src_col, dest_col, new_tip='always') +``` + +#### + +Transfer 80ul of reagent from the first tube of the source rack to each well in the destination plate. +Use the same tip for each transfer. + +```python +# parameters +transfer_vol = 80 +src_well = source.wells_by_name()['A1'] +dest_wells = destination.wells() + +# commands +p1000s.transfer(transfer_vol, src_well, dest_wells, new_tip='once') +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette, aliquot 190 ul of samples from each tube in the source tube rack to +all wells of the destination plate evenly. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 190 +src_wells = source.wells() +dest_wells = destination.wells() + +# commands +p1000s.transfer(transfer_vol, src_wells, dest_wells, new_tip='always') +``` + +#### + +Using Flex 8-Channel 1000 uL Pipette on left mount, transfer 40 uL from the first column in the source labware 1 +to the first column in destination labware 1. Keep the same tip for this entire set of transfers within this step. + +```python +# volumes setup +transfer_vol_1 = 40 +# wells setup +source_wells_1 = source_1.columns_by_name()['1'] +destination_wells_1 = destination_1.columns_by_name()['1'] +p1000m.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 197 uL of reagent +from H10, F12, D7, B1, C8 wells in source labware to the first well in the destination labware. +Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 197 +src_wells = [source.wells_by_name()[well] for well in ['H10', 'F12', 'D7', 'B1', 'C8']] +dest_well = destination.wells_by_name()['A1'] + +# commands +p1000s.transfer(transfer_vol, src_wells, dest_well, new_tip="always") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 52 uL from wells A1, A2 in source labware 1 +to B6, B7 in source labware 2. Reuse the same tip for each transfer. + +```python +# volume setup +transfer_vol_1 = 52 + +# well setup +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + +# commands +p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using Flex 1-Channel 50 uL Pipette on right mount, transfer 20 uL from wells B6, B7 in source labware 2 +to A1, B1 in destination labware 1. Use a new tip for each transfer. + +```python +# volume setup +transfer_vol_3 = 20 + +# well setup +source_wells_3 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] +destination_wells_3 = [destination_1.wells_by_name()[wells] for wells in ['A1', 'B1']] + +# commands +p50s.transfer(transfer_vol_3, source_wells_3, destination_wells_3, new_tip="always") +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette , pool 25ul from all tubes in source labware1 into A1 of the destination labware. +Change tips between each tube. + +```python +vol = 25 +source_wells = source_labware1.wells() +dest_well = destination_labware.wells_by_name()['A1'] + +p1000s.transfer(vol, source_wells, dest_well, new_tip='always') +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette, pool 90 uL of liquid from all the wells in source labware 1 to +the first well in destination labware 1. Reuse the same tip. + +```python +# volume setup +transfer_vol_1 = 90 +# wells setup +source_wells_1 = source_1.wells() +destination_wells_1 = destination_1.wells_by_name()['A1'] +# commands +p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on right mount, transfer 7 uL of reagent from the first column in source labware to +4, 8, 1, 9, and 2 columns in the destination labware. Use a new tip for each transfer. + +```python +#parameters +transfer_vol = 7 +src_col = source.columns_by_name()['1'] +dest_cols = [destination.columns_by_name()[idx] for idx in ['4', '8', '1', '9', '2']] + +# commands +p50m.transfer(transfer_vol, src_col, dest_cols, new_tip="always") +``` + +#### + +Using Flex 8-Channel 50 uL Pipette on right mount, transfer 6 uL of reagent from 4, 3, 6, 1, 11 columns in source labware +to 5, 9, 1, 10, 2 columns in the destination labware. Using the same tip for all transfers. + +```python +# parameters +transfer_vol = 6 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p50m.transfer(transfer_vol, src_cols, dest_cols, new_tip="once") +``` + +#### + +Using Flex 8-Channel 1000 uL Pipette on left mount, transfer 78 uL of reagent from 4, 3, 6, 1, 11 columns in the source labware +to 5, 9, 1, 10, 2 columns in the destination labware. Use a new tip for each transfer. + +```python +# parameters +transfer_vol = 78 +src_cols = [source.columns_by_name()[idx] for idx in ['4', '3', '6', '1', '11']] +dest_cols = [destination.columns_by_name()[idx] for idx in ['5', '9', '1', '10', '2']] + +# commands +p1000m.transfer(transfer_vol, src_cols, dest_cols, new_tip="always") +``` + +#### + +Using Flex 1-Channel 50 uL Pipette on right mount, transfer 25 uL of reagent +from the first well of source labware 2 to all wells in destination labware. Reuse the same tip. + +```python +# volumes setup +transfer_vol_1 = 25 + +# wells setup +source_wells_2 = source_labware_2.wells_by_name()['A1'] +destination_wells_1 = [dest.wells() for dest in destination] + +# commands +p50s.transfer(transfer_vol_1, source_wells_2, destination_wells_1, new_tip="once") +``` + +#### + +- when command says 'Use a new tip for each transfer', or something similar, + set the `new_tip` parameter to "always": `new_tip='always'`. +- when command says 'Use the same tip for all transfers.', 'reuse the same tip' or something similar. + set the `new_tip` parameter to "once": `new_tip='once'`. + +#### + +Note that when command says `Use the same tip for all transfers` or similar. +Do not use new_tip='once' inside loop as shown below + +```python +for src, dest in LIST: + p50_multi_right.transfer(transfer_vol, src, dest, new_tip='once') +``` + +Instead, remove `for` and use like so: + +```python +p50_multi_right.transfer(transfer_vol, src, dest, new_tip='once') +``` + +Note that no `for` loop is used. + +#### + +Source labware is ['labware name'], placed on [temperature module] on slot 3 + +```python +# modules +temperature_module = protocol.load_module(['temperature module gen2'], 3) + +# labware +source = temperature_module.load_labware(['labware name']) +``` + +#### + +Thermocycler module GEN 2 is present on slot A1+B1. `A1+B1` referes to 7, please use the slot number 7. + +Correct thermocycler load: + +```python +thermocycler = protocol.load_module('thermocyclerModuleV2') # by default slot number is 7 +``` + +Incorrect thermocycler load: + +```python +thermocycler = protocol.load_module('thermocyclerModuleV2', 'A1+B1') +``` + +#### + +- Sample temperature module GEN 2 is placed on slot D1 +- Opentrons 96 Well Aluminum Block adapter is placed on sample temperature module GEN 2 + +Corresponding protocol + +```python +temp_mod_sample = protocol.load_module('temperature module gen2', 'D1') +temp_sample_adapter = temp_mod_sample.load_adapter('opentrons_96_well_aluminum_block') +``` + +#### + +Open thermocycler lid + +```python +[thermocycler_object].open_lid() +``` + +#### + +Set the thermocycler block temperature to 1 C. + +```python +plate_temperature_c = 1 +[thermocycler_object].set_block_temperature(plate_temperature_c) +``` + +#### + +Set the thermocycler lid temperature to 50 C. + +```python +lid_temperature_c = 50 +[thermocycler_object].set_lid_temperature(lid_temperature_c) +``` + +#### + +Set the sample temperature module to 3 C. + +```python +sample_temperature_c = 3 +[temperature_module].set_temperature(sample_temperature_c) +``` + +#### + +Transfer 17 uL of mastermix from the mastermix source wells to the destination wells. +Use the same pipette tip for all transfers. + +```python +[pippette_object].transfer( + 17, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' +) +``` + +#### + +Transfer 4 uL of the sample from the source to the destination. +Mix the sample and mastermix for a total volume of 15 uL 10 times. +Blow out to 'destination well' after each transfer. Use a new tip for each transfer. + +```python +[pippette_object].transfer( + 4, + [sample_source_wells],` + [sample_destination_wells], + new_tip='always', + mix_after=(10, 15), + blow_out=True, + blowout_location='destination well' +) +``` + +#### + +Close the thermocycler lid. + +```python +[thermocycler_module].close_lid() +``` + +#### + +Execute the thermocycler with the following profile: + +- 75 C for 66 seconds for 1 cycle (repetition). + +```python +[thermocycler_module].execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 66}], + repetitions=1, + block_max_volume=[total_mix_volume_ul] +) +``` + +Note that you must calculate `block_max_volume` based on the whole prompt context. + +#### + +Execute the thermocycler with the following profile: + +- 61C for 8 seconds, 85°C for 20 seconds, 58°C for 45 seconds for 14 cycles. + +```python +[thermocycler_module].execute_profile( + steps=[ + {'temperature': temp, 'hold_time_seconds': duration} + for temp, duration in zip([61, 85, 58], [8, 20, 45]) + ], + repetitions=14, + block_max_volume=[total_mix_volume_ul] +) +``` + +Note that you must calculate `block_max_volume` based on the whole prompt context. + +#### + +Hold the thermocycler block at 4°C. + +```python +hold_temperature_c = 10 +[thermocycler_module].set_block_temperature(hold_temperature_c) +``` + +#### + +Deactivate the mastermix temperature module. + +```python +[master_mix_temperature_module].deactivate() +``` + +#### + +Sample source wells: the first 48 wells column-wise in the sample source plate. +Note that the pipette is a single channel. + +Use `[source_labware].wells()`. For example, + +```python +number_of_samples = 48 +source_wells = sample_plate.wells()[:number_of_samples] +``` + +#### + +Sample source wells: the first 48 wells column-wise in the sample source plate. +Note that the pipette is a multi-channel. + +- Estimate the columns using the number samples + +```python +number_of_samples = 48 +number_of_columns = math.ceil(number_of_samples / 8) +``` + +- Then, use `[source_labware].columns()` method to access the columns. + For example, + +```python +source_wells = sample_plate.columns()[:number_of_columns] +``` + +#### + +When a command says `move destination labware` or something, use `move_labware`. +We need to specify two arguments: + +- labware: The labware object you want to move. +- new_location: The destination where you want to move the labware. + +This can be any empty deck slot or a module that is ready to accept labware. +Example for the slot, + +```python +protocol.move_labware([labware]], ['C4'], use_gripper=True) +``` + +Example for the module, + +```python +protocol.move_labware([labware]], [thermocycler], use_gripper=True) +``` + +#### + +Pause the protocol + +```python +protocol.pause("Pause please") +``` + +#### + +Transfer 21 uL of liquid from 6 mm below the top surface of mastermix well to 3 mm above the bottom of destination well. +Use the same tip for each transfer. + +```python +[pipette_object].transfer(21, mastermix_well.top(-6), dest.bottom(3), new_tip='once') +``` + +#### + +5 mm above the top of the well + +```python +plate['A1'].top(z=5) +``` + +5 mm below the top of the well + +```python +plate['A1'].top(z=-5) +``` + +5 mm above the bottom of the well + +```python +plate['A1'].bottom(z=1) +``` + +5 mm below the bottom of the well + +```python +plate['A1'].bottom(z=-5) +``` + +Transfer 20 uL of liquid from 5 mm below the top surface of the mastermix well to 2 mm above the bottom of the destination well. +Use the same tip for each transfer. + +```python +pipette_96channel.transfer(20, mastermix_source_well.top(-5), destination_wells.bottom(2), new_tip='once') +``` + +#### + +Remove the tip slowly out of the well at 5 mm/s speed + +```python +pipette.move_to([well].top(), speed=5) +``` + +Move to the top of the well at 5 mm/s speed + +```python +pipette.move_to([well].top(), speed=5) +``` + +Move to 2 mm below the top of well A1 + +```python +pipette.move_to(plate['A1'].top(z=-2)) +``` + +Move to 2 mm above the bottom of well A1 + +```python +pipette.move_to(plate['A1'].bottom(z=2)) +``` + +#### + +Transfer 20 ul of liquid from 3 mm above the source well bottom to destination well 7 mm beneath the top surface. Flow rate is at half the default. +Mix the sample and mastermix of 40 ul total volume 5 times. Remove the tip slowly out of the well at 5 mm/s speed. Use the same tip for each transfer. + +```python +pipette_96channel.pick_up_tip() +pipette_96channel.aspirate(20, sample_source_wells.bottom(3), rate=0.5) +pipette_96channel.dispense(20, destination_wells.top(-7), rate=0.5) +pipette_96channel.mix(5, 40) +pipette_96channel.move_to(destination_wells.top(), speed=5) +pipette_96channel.drop_tip() +``` + +#### + +Load three opentrons_flex_96_filtertiprack_50ul tip racks in slots A2, B2, and C2 + +```python +tips_50ul = [ + protocol.load_labware( + 'opentrons_flex_96_filtertiprack_50ul', + slot + ) + for slot in ['A2', 'B2', 'C2'] +] +``` + +or + +```python +tips_50ul_a = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'A2') +tips_50ul_b = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'B2') +tips_50ul_c = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'C2') +tips_50ul = [tips_50ul_a, tips_50ul_b, tips_50ul_c] +``` + +#### + +Move the destination labware to the thermocycler using a gripper. + +```python +protocol.move_labware(destination_plate, thermocycler_module, use_gripper=True) +``` + +#### + +I am going to be running a protocol on my Opentrons Flex. +I have a 96-channel pipette on the system. My destination plates will be +4 'nest_96_wellplate_2ml_deep' plates. My source labware will be +a 'nest_1_reservoir_195ml'. + +```python +pipette_96_channel = protocol.load_instrument( + 'flex_96channel_1000', mount='left' + ) +source_reservoir = protocol.load_labware('nest_1_reservoir_195ml', '1') +destination_plates = [ + protocol.load_labware('nest_96_wellplate_2ml_deep', slot) + for slot in ['2', '3', '4', '5'] +] +``` + +#### Example 5 + +Transfer 25 uL from multiple source wells to a single destination well, use a new tip every time, and touch the tip after dispense. + +```python +pipette.transfer(25, source_wells, dest_well, new_tip='always', touch_tip=True) +``` + +#### + +Transfer 10 uL from source to destination, with an air gap of 5 uL after aspiration. + +```python +pipette.transfer(10, source_well, dest_well, air_gap=5) +``` + +#### + +Transfer 200 uL from source to destination, blowing out in the source well after dispensing. Use the same tip for each transfer. + +```python +pipette.transfer(200, source_well, dest_well, trash=False, blow_out=True, blowout_location='source well') +``` + +#### + +Transfer 12 uL from source to destination, mix the destination well 5 times with 10 uL after dispensing, and do not touch the tip. + +```python +pipette.transfer(12, source_well, dest_well, mix_after=(5, 10)) +``` + +#### + +Transfer 30 uL from one source to multiple destinations, after each aspirate and touch tip after dispensing. + +```python +pipette.transfer(30, source_well, dest_wells, air_gap=10, touch_tip=True) +``` + +#### + +Flex 1-Channel 1000 uL Pipette is mounted on the left side. +mastermix source wells: first N wells column-wise in mastermix plate. +Note that the pipette is a single channel. + +```python +pipette = protocol.load_instrument('flex_1channel_1000', 'left', tip_racks=[tips_1000ul]) +sample_source_wells = sample_plate.wells()[:N] +``` + +#### + +Source Labware: `Opentrons 96 Flat Bottom Heater-Shaker Adapter with NEST 96 Well Plate 200 uL Flat` in slot D1 + +```python +source = protocol.load_labware('opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'D1') +``` + +#### + +Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 150 uL from wells `A1, A2` in source labware 1 +to `B6, B7` in source labware 2. Use the same tip for each transfer. + +First collect all wells for source and destination. + +```python +source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] +destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] +``` + +Then use a transfer method like so: + +```python +p1000s.transfer(150, source_wells_1, destination_wells_1, new_tip="once") +``` + +Note that we are using a single transfer function for multiple wells. + +The following is totally wrong: + +```python +pipette_1000ul.transfer(50, source_labware_1.wells_by_name()['A1'], source_labware_2.wells_by_name()['B6'], new_tip='once') +pipette_1000ul.transfer(50, source_labware_1.wells_by_name()['A2'], source_labware_2.wells_by_name()['B7'], new_tip='never') +``` + +#### + +Using the multi-channel pipette, transfer 3ul of sample from each column in the source plate to +the destination plate in duplicate. Changing tips between each column. Duplicate means that +aspirate the sample from the sample plate column 1 to the destination plate column 1, change tip, +then aspirate from sample plate column 1 to destination plate column 2. Then, transfer the sample +from the sample plate column 2 to the destination plate column 3, change tip, then transfer +the sample from sample plate column 2 to destination plate column 4. Repeat this pattern for +the remainder of the source columns. + +```python +source_columns = source_plate.columns()[:number_of_columns] +destination_columns = destination_plate.columns()[:number_of_columns * 2] # Twice the number for duplicates + +for col_ctr, s in enumerate(source_columns, start=0): + dest_index = 2 * col_ctr + pipette_multi.transfer(3, s, destination_columns[dest_index], new_tip='always') + pipette_multi.transfer(3, s, destination_columns[dest_index + 1], new_tip='always') +``` + +Note that two transfer methods is used to account for duplication. 'for' loop is used since description +says change tip for each column. + +#### + +Using the multi-channel pipette, transfer 3ul of sample from each column in the source plate to +the destination plate in triplicate. Changing tips between each column. +The triplicate means that for first source columns, +aspirate the sample from the source column 1 to the destination plate column 1, change tip, +then aspirate from source column 1 to destination plate column 2, change tip, +then aspirate from source column 1 to destination plate column 3, change tip. +For second source column, +aspirate the sample from the source column 2 to the destination column 4, change tip, +then aspirate the sample from source column 2 to destination column 5, change tip, +then aspirate the sample from source column 2 to destination column 6, change tip. + +Repeat this pattern for the remainder of the source columns. + +```python +source_columns = source_plate.columns()[:number_of_columns] +destination_columns = destination_plate.columns()[:number_of_columns * 2] # Twice the number for duplicates + +for col_ctr, s in enumerate(source_columns, start=0): + dest_index = 2 * col_ctr + pipette_multi.transfer(3, s, destination_columns[dest_index], new_tip='always') + pipette_multi.transfer(3, s, destination_columns[dest_index + 1], new_tip='always') + pipette_multi.transfer(3, s, destination_columns[dest_index + 2], new_tip='always') +``` + +Note that two transfer methods is used to account for duplication. 'for' loop is used since description +says change tip for each column. diff --git a/opentrons-ai-server/api/storage/docs/deck_layout.md b/opentrons-ai-server/api/storage/docs/deck_layout.md new file mode 100644 index 00000000000..e2532de3712 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/deck_layout.md @@ -0,0 +1,167 @@ +# Deck Layout Rules + +## Overview + +This document collects all of the guidelines around recommended deck slot locations in one place. Previously, this information was scattered in multiple documents, or the logic was built into Opentrons products like Protocol Designer or the OT App. + +## Deck Slot Guidelines - OT-2 + +OT-2 deck slots: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, trash bin. + +### Modules + +- Heater-Shaker + + - Recommended: Slot 1 + - Allowed: Slots 3, 4, 6, 7, 1 + - Not allowed: Slots 2, 5, 8, 9, 11 + +- Magnetic Module + + - Recommended: Slot 1 + - Allowed: Slots 3, 4, 6, 7, 9, 10 + - Not allowed: Slots 2, 5, 8, 11 + +- Magnetic Block: Not compatible with OT-2 + +- Plate Reader Module (Absorbance): Not compatible with OT-2 + +- Temperature Module + + - Recommended: Slot 3 + - Allowed: Slot 1, 4, 6, 7, 9, 10 + - Not allowed: Slots 2, 5, 8, 11 + +- Thermocycler Module + + - Recommended/Allowed: Slots 7, 8, 10, and 11 (All four slots) + - Not allowed: Any other location + - Note: Only one Thermocycler module can be added to the deck. + +- Fixtures - N/A + - The OT-2 waste bin is fixed on the deck map, taking up what would have been Slot 12. + +### Labware + +Note: We should default to placing the shortest labware near the front and left of the OT-2 (Slot 1 then 2 then 3 then 4 then 5, etc.), followed by progressively taller labware towards the back and right. From shortest to tallest the order should be: Well plates, then Reservoirs, then Tube racks, then Tip racks. + +- Well plates + + - Recommended: Slots 1, 2, or 3 + - If needed: Slots 4, 5, or 6 + - Allowed: Any slot + +- Reservoirs + + - Recommended: Slots 4, 5, or 6 + - If available: Slots 1, 2, or 3 + - If needed: Slots 7, 8, or 9 + - Allowed: Any slot + +- Tube racks + + - Recommended: Slots 7, 8, or 9 + - If available: Slots 1, 4, 2, 5, 3, or 6 (Slots on the far left side are preferable to ones in the middle or left since they’re easier to access.) + - Allowed: Any slot + +- Tip racks + - Recommended: Slots 11, 10, 9, 8, 7 (Start towards the back right and move left then to the front) + - If available: Slots 6, 5, 4, 3, 2, 1 + - Allowed: Any slot + +## Deck Slot Guidelines - Flex + +Flex deck layout: D1, D2, D3, C1, C2, C3, B1, B2, B3, A1, A2, A3 (Trash bin) + +### Modules + +- Heater-Shaker + + - Recommended: Slot D1 + - Allowed: Slots A1, B1, C1, D1, A3, B3, C3, or D3 + - Not allowed: Slots A2, B2, C2, or D2 + +- Magnetic Module: Not compatible with Flex + +- Magnetic Block + + - Recommended: Slot D2 + - Allowed: Slots A1, B1, C1, D1, A2, B2, C2, D2, A3, B3, C3, or D3 + - Not allowed: On staging area slots + +- Plate Reader Module (Absorbance) + + - Recommended: D3 + - Allowed: Slots A3, B3, C3, or D + - Not allowed: Slots A1, B1, C1, D1, A2, B2, C2, or D2 + +- Temperature Module + + - Recommended: D1 + - Allowed: A1, B1, C1, D1, A3, B3, C3, or D3 + - Not allowed: A2, B2, C2, or D2 + +- Thermocycler Module + - Recommended/Allowed: A1 + B1 (Both slots) + - Not allowed: Any other location + +### Fixtures + +- Staging area slots + + - Allowed: A3, B3, C3, or D3 + - Not allowed: A1, B1, C1, D1, A2, B2, C2, or D2 + - Notes: When a staging area slot is added, a new deck slot is created in the far right column in slots A4, B4, C4, or D4. The gripper can access these deck slots, but pipetting in column 4 is not possible. + - Because the staging area slots can only be accessed by the gripper, tube racks should not be placed in these locations since the gripper cannot safely move this type of labware. All other labware types are compatible with staging area slots. + - The trash bin cannot occupy the same deck slot as a staging area slot. + +- Trash bin + + - Recommended: A3 + - Allowed: A1, B1, C1, D1, A3, B3, C3, or D3 + - Not allowed: A2, B2, C2, or D2 + - Note: The trash bin cannot occupy the same deck slot as a staging area slot. + +- Waste chute + - Recommended/Allowed: D3 (The waste chute fixture diverts waste to an off-deck receptacle and is designed to be placed exclusively in slot D3). + - Not allowed: Any other location + +### Labware + +Note: We should default to placing the shortest labware near the front and left of the Flex (Slot D1 then D2 then D3 then C1 then C2, etc.), followed by progressively taller labware towards the back and right. From shortest to tallest the order should be: Well plates, then Reservoirs, then Tube racks, then Tip racks. + +- Well plates + + - Recommended: Slots D1, D2, or D3 + - If needed: Slots C1, C2, C3, B1, B2, B3, A1, A2, or A3 + - Allowed: Any slot + +- Reservoirs + + - Recommended: Slots C1, C2, or C3 + - If available: Slots D1, D2, or D3 + - If needed: Slots B1, B2, B3, A1, A2, or A3 + - Allowed: Any slot + +- Tube racks + + - Recommended: Slots B1, B2, B3 + - If available: Slots D1, C1, D2, C2, D3, or C3 (Slots on the far left side are preferable to ones in the middle or left since they’re easier to access.) + - Allowed: Any slot + +- Tip racks + + - Recommended: Slots A3 (if trash bin is not present), A2, A1, B3, B2, B1 (Start towards the back right and move left then to the front) + - If available: Slots C3, C2, C1, D3, D2, or D1 + - Allowed: Any slot + +- Adapters + - Opentrons Flex 96 Tip Rack Adapter + - Recommended: A2, B2, C2, D2 (to avoid modules in columns 1 and 3) + - Allowed: Any slot + +## Reference documents + +1. Confluence: https://opentrons.atlassian.net/wiki/spaces/RPDO/pages/3859939364 +2. Flex product manual (See the section on Deck Fixtures in Chapter 3): https://insights.opentrons.com/hubfs/Products/Flex/Opentrons Flex manual REV2.pdf +3. OT-2 product manual: https://insights.opentrons.com/hubfs/Products/OT-2/OT-2R User Manual.pdf diff --git a/opentrons-ai-server/api/storage/docs/full-examples.md b/opentrons-ai-server/api/storage/docs/full-examples.md new file mode 100644 index 00000000000..959dd4cf4d9 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/full-examples.md @@ -0,0 +1,1263 @@ +## Below seven examples are shown in pairs: a description () and a corresponding protocol (). + +[1] Example + +Application: Reagent Filling - One source to Multiple destinations +Robot: OT-2 +API: 2.15 + +Modules: + +- No modules + +Labware: + +- Source labware: Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt in slot 4 +- Destination Labware: NEST 2 mL 96-Well Deep Well Plate, V Bottom in slot 9 +- Tiprack: Opentrons OT-2 96 Tip Rack 300 uL in slot 1 + +Pipette mount: + +- P300 Multi Channel is mounted on the right + +Commands: + +1. Transfer 50 uL of sample from each column of the source plate into the corresponding columns of the destination deep well plate. + Change tips for each transfer. + + + +# metadata +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'chatGPT', + 'description': 'Transfer reagent', + 'apiLevel': '2.15' +} + +def run(protocol): # labware +tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 1) +source = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 4) +destination = protocol.load_labware('nest_96_wellplate_2ml_deep', 9) + + # pipettes + p300m = protocol.load_instrument('p300_multi_gen2', mount="right", tip_racks=[tiprack]) + + # parameters + TRANSFER_VOL = 50 + SRC_COLS = source.columns() + DEST_COLS = destination.columns() + + # commands + p300m.transfer(TRANSFER_VOL, SRC_COLS, DEST_COLS, new_tip='always') + + + +[2] Example + +Metadata: + +- Application: Reagent transfer +- Robot: OT-2 +- API: 2.15 + +Labware: + +- Source Labware 1: NEST 1 Well Reservoir 195 mL is positioned in slot 7 +- Destination Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot 1 +- Destination Labware 2: Corning 96 Well Plate 360 uL Flat is positioned in slot 2 +- Tiprack 1: Opentrons 96 Tip Rack 300 uL is positioned in slot 10 + +Pipette Mount: + +- Left Mount: P300 Multi-Channel GEN2 + +Commands: + +1. Using P300 Multi-channel pipette on the left mount, transfer 50 uL of reagent from first column in the source labware 1 + to all the columns in destination labware 1. Keep the same set of tips for this entire set of transfers within this step. +2. Using P300 Multi-channel pipette on the left mount, transfer 100 uL from first column in the source labware 1 + to each column in destination labware 2. Keep the same set of tips for this entire set of transfers within this step. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +"protocolName": "Reagent Transfer protocol", +"author": "Opentrons Generative AI", +"description": "Transfer reagents from multile source labware to multiple destination labware", +"apiLevel": "2.15" +} + +def run(protocol: protocol_api.ProtocolContext): # labware +source_1 = protocol.load_labware("nest_1_reservoir_195ml", location=7) +destination_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location=1) +destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location=2) + + tiprack300 = protocol.load_labware("opentrons_96_tiprack_300ul", location=10) + + # pipettes + p300m = protocol.load_instrument("p300_multi_gen2", mount="left", tip_racks=[tiprack300]) + + # wells setup + source_wells_1 = source_1.columns()[0] + destination_wells_1 = destination_1.columns() + destination_wells_2 = destination_2.columns() + + # volumes setup + transfer_vol_1 = 50 + transfer_vol_2 = 100 + + p300m.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + p300m.transfer(transfer_vol_2, source_wells_1, destination_wells_2, new_tip="once") + + + +[3] Example + + +- Application: Reagent transfer +- Robot: OT-2 +- API: 2.15 + +Labware: + +- Source Labware: Thermo Scientific Nunc 96 Well Plate 2000 uL in slot 7 +- Destination Labware: Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap in slot 3 +- Tiprack: Opentrons 96 Filter Tip Rack 1000 uL in slot 4 + +Pipette mount: + +- P1000 Single-Channel GEN2 is mounted on the left + +Commands: + +1. Using P1000 Single-Channel GEN2 pipette on left mount, transfer 195.0 uL of reagent + from H10, F12, D7, B1, C8 wells in source labware + to first well in the destination labware. Use a new tip for each transfer. + + + +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'chatGPT', + 'description': 'P1000 Single-Channel GEN2 transfer 195.0 ul', + 'apiLevel': '2.15' +} + +def run(protocol): + + # labware + tiprack = protocol.load_labware('opentrons_96_filtertiprack_1000ul', 4) + source = protocol.load_labware('thermoscientificnunc_96_wellplate_2000ul', 7) + destination = protocol.load_labware('opentrons_24_aluminumblock_nest_0.5ml_screwcap', 3) + + # pipettes + p1000s = protocol.load_instrument('p1000_single_gen2', mount="left", tip_racks=[tiprack]) + + # parameters + TRANSFER_VOL = 195.0 + SRC_WELLS = ['H10', 'F12', 'D7', 'B1', 'C8'] + DEST_WELL = destination.wells()[0] + + # commands + for src in SRC_WELLS: + p1000s.transfer(TRANSFER_VOL, source.wells_by_name()[src], DEST_WELL, new_tip="always") + + + +[4] Example + +Metadata and requirements: + +- Application: Reagent transfer +- Robot: Flex +- API: 2.15 + +Labware: + +- Source Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot C1 +- Source Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot C2 +- Destination Labware 1: Corning 96 Well Plate 360 uL Flat is positioned in slot D1 +- Destination Labware 2: Corning 96 Well Plate 360 uL Flat is positioned in slot D2 +- Tiprack 1: Opentrons Flex 96 Filter Tip Rack 200 uL is positioned in slot B2 +- Tiprack 2: Opentrons Flex 96 Filter Tip Rack 50 uL is positioned in slot A2 + +Pipette Mount: + +- Flex 1-Channel 1000 uL Pipette is mounted on the left side +- Flex 1-Channel 50 uL Pipette is mounted on the right side + +Commands: + +1. Using Flex 1-Channel 1000 uL Pipette on left mount, transfer 50 uL from wells A1, A2 in source labware 1 + to B6, B7 in source labware 2. Reuse the same tip for each transfer. +2. Using Flex 1-Channel 50 uL Pipette on right mount, transfer 15 uL from wells C4, C6 in source labware 2 + to A3, A4 in source labware 1. Reuse the same tip for each transfer. +3. Using Flex 1-Channel 50 uL Pipette on right mount, transfer 10 uL from wells B6, B7 in source labware 2 + to A1, B1 in destination labware 1. Use a new tip each time for each transfer. +4. Using Flex 1-Channel 50 uL Pipette on right mount, transfer 10 uL from wells C4, C6 in source labware 2 + to A1, B1 in destination labware 2. Use a new tip each time for each transfer. + + + +from opentrons import protocol_api + +# metadata + +metadata = { +'protocolName': 'Reagent Transfer', +'author': 'Opentrons Generative AI', +} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +# protocol run function + +def run(protocol: protocol_api.ProtocolContext): + + # labware + source_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C1') + source_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C2') + destination_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D1') + destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D2') + tiprack200 = protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location='B2') + tiprack50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[tiprack200]) + p50s = protocol.load_instrument("flex_1channel_50", mount="right", tip_racks=[tiprack50]) + + # well setup + source_wells_1 = [source_1[wells] for wells in ['A1', 'A2']] + source_wells_2 = [source_2[wells] for wells in ['C4', 'C6']] + source_wells_3 = [source_2[wells] for wells in ['B6', 'B7']] + source_wells_4 = [source_2[wells] for wells in ['C4', 'C6']] + destination_wells_1 = [source_2[wells] for wells in ['B6', 'B7']] + destination_wells_2 = [source_1[wells] for wells in ['A3', 'A4']] + destination_wells_3 = [destination_1[wells] for wells in ['A1', 'B1']] + destination_wells_4 = [destination_2[wells] for wells in ['A1', 'B1']] + + # volume setup + transfer_vol_1 = 50 + transfer_vol_2 = 15 + transfer_vol_3 = 10 + transfer_vol_4 = 10 + + # commands + p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + p50s.transfer(transfer_vol_2, source_wells_2, destination_wells_2, new_tip="once") + p50s.transfer(transfer_vol_3, source_wells_3, destination_wells_3, new_tip="always") + p50s.transfer(transfer_vol_4, source_wells_4, destination_wells_4, new_tip="always") + + + +[5] Example + +Metadata and requirements: + +- Application: Reagent transfer +- Robot: Flex +- API: 2.15 + +Labware: + +- Source Labware: Opentrons 96 Flat Bottom Adapter with NEST 96 Well Plate 200 uL Flat on slot D1 +- Destination Labware: Opentrons 96 Flat Bottom Adapter with NEST 96 Well Plate 200 uL Flat on slot C2 +- Tiprack: Opentrons Flex 96 Filter Tip Rack 50 uL on slot C1 + +Pipette Mount: + +- Flex 8-Channel 50 uL Pipette is mounted on the right side + +Commands: + +1. Using Flex 8-Channel 50 uL Pipette on right mount, transfer 8 uL of reagent from 4, 3, 6, 1, 11 columns in source labware + to 5, 9, 1, 10, 2 columns in the destination labware. Using the same tip for all transfers. + + + +from opentrons import protocol_api + +metadata = { +'protocolName': 'Reagent Transfer', +'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +def run(protocol: protocol_api.ProtocolContext): + + # labware + tiprack = protocol.load_labware('opentrons_flex_96_filtertiprack_50ul', 'C1') + source = protocol.load_labware('opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'D1') + destination = protocol.load_labware('opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', 'C2') + + # pipettes + p50m = protocol.load_instrument('flex_8channel_50', mount="right", tip_racks=[tiprack]) + + # parameters + transfer_vol = 8 + src_cols = [3, 2, 5, 0, 10] + dest_cols = [4, 8, 0, 9, 1] + + # commands + p50m.pick_up_tip() + for src_col, dest_col in zip(src_cols, dest_cols): + p50m.transfer(transfer_vol, source.columns()[src_col], destination.columns()[dest_col], new_tip="never") + p50m.drop_tip() + + + +[6] Example + +Metadata: + +- Author: Bob +- Protocol Name: PCR + +Requirements: + +- `requirements = {"robotType": "OT-2", "apiLevel": "2.15"}` + +Modules: + +- The thermocycler module is located in slot 7. +- The sample temperature module is positioned in slot 1. +- The mastermix temperature module is positioned in slot 3. + +Labware: + +- The source sample labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed on the temperature module in slot 1. +- The source mastermix labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed on the temperature module in slot 3. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed on the thermocycler module in slot 7. +- A 20 uL filter tip rack is used in slot 4. + +Pipette Mount: + +- A P20 Multi-Channel Gen2 pipette is mounted on the left side. + +Well Allocation: + +- Sample source wells: the first 64 wells column-wise in the sample source plate. +- Mastermix source wells: the first 64 wells column-wise in the mastermix plate. +- Destination wells: the first 64 wells column-wise in the thermocycler. + +Commands: + +1. Set the total number of samples to 64. +2. Open the thermocycler lid. +3. Set the thermocycler block temperature to 6C. +4. Set the thermocycler lid temperature to 55C. +5. Set the sample temperature module to 4C. +6. Set the mastermix temperature module to 10C. +7. Transfer 7 uL of mastermix from the mastermix source wells to the destination wells. Use the same pipette tip for all transfers. +8. Transfer 5 uL of the sample from the source to the destination. Mix the sample and mastermix for a total volume of 12 uL 9 times. + Blow out to `destination well` after each transfer. Use a new tip for each transfer. +9. Close the thermocycler lid. +10. Execute the thermocycler with the following profile: + - 74C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume +11. Execute the thermocycler with the following profile: + - 60C for 7 seconds, 84C for 19 seconds, 57C for 44 seconds for 13 cycles, block max volume is sample and mastermix volume +12. Execute the thermocycler with the following profile: + - 75C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume +13. Hold the thermocycler block at 4C. +14. Open the thermocycler lid. +15. Deactivate the mastermix temperature module. +16. Deactivate the sample temperature module. + + + +import math +from opentrons import protocol_api + +metadata = { +'protocol_name': 'QIAGEN OneStep RT-PCR Kit PCR Amplification', +'author': 'chatGPT', +'description': 'Amplification using QIAGEN OneStep RT-PCR Kit with 13 cycles', +'apiLevel': '2.15' +} + +def run(protocol: protocol_api.ProtocolContext): # Sample preparation parameters +number_of_samples = 64 +sample_volume_ul = 5 # Volume in microliters +master_mix_volume_ul = 7 # Volume in microliters +mixing_cycles = 9 +total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul +master_mix_temperature_c = 10 # Temperature in Celsius +sample_temperature_c = 4 # Temperature in Celsius + + # Thermocycler parameters + lid_temperature_c = 55 # Lid temperature in Celsius + initial_plate_temperature_c = 6 # Initial plate temperature in Celsius + hold_temperature_c = 4 # Hold temperature in Celsius for infinite hold + + # Modules loading + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 1) + master_mix_temperature_module = protocol.load_module('temperature module gen2', 3) + + # Labware loading + tips_20ul = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + pcr_plate = thermocycler_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + sample_plate = sample_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + # Pipette loading + multi_channel_pipette = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tips_20ul]) + + # Well allocation + number_of_columns = math.ceil(number_of_samples / 8) + sample_source_wells = sample_plate.columns()[:number_of_columns] + sample_destination_wells = pcr_plate.columns()[:number_of_columns] + master_mix_source_wells = master_mix_plate.columns()[:number_of_columns] + master_mix_destination_wells = pcr_plate.columns()[:number_of_columns] + + # Command 2: Open lid + thermocycler_module.open_lid() + + # Command 3: Set initial plate temperature + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + + # Command 4: Set lid temperature + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Command 5: Set sample temperature + sample_temperature_module.set_temperature(sample_temperature_c) + + # Command 6: Set master mix temperature + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Command 7: Transfer master mix to destination wells + multi_channel_pipette.transfer( + master_mix_volume_ul, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' + ) + + # Command 8: Transfer samples to destination wells and mix + multi_channel_pipette.transfer( + sample_volume_ul, + sample_source_wells, + sample_destination_wells, + new_tip='always', + mix_after=(mixing_cycles, total_mix_volume_ul), + blow_out=True, + blowout_location='destination well' + ) + + # Command 9: Close lid + thermocycler_module.close_lid() + + # Commands 10-12: PCR cycling + thermocycler_module.execute_profile( + steps=[{'temperature': 74, 'hold_time_seconds': 65}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[ + {'temperature': temp, 'hold_time_seconds': duration} + for temp, duration in zip([60, 84, 57], [7, 19, 44]) + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + # Command 13: Set final hold temperature + thermocycler_module.set_block_temperature(hold_temperature_c) + + # Command 14: Open lid post-PCR + thermocycler_module.open_lid() + + # Commands 15 & 16: Deactivate temperature modules + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + + + +[7] Example + +Metadata: + +- Author: Bob +- ProtocolName: PCR + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +Modules: + +- Thermocycler module GEN 2 is present on slot A1+B1 +- Temperature module GEN 2 is placed on slot D1 +- Mastermix temperature module GEN 2 is placed on slot D3 + +Adapter: + +- Opentrons 96 Well Aluminum Block adapter is placed on the temperature module GEN 2 +- Opentrons 96 Well Aluminum Block adapter is placed on the mastermix temperature module GEN 2 + +Labware: + +- Source labware: `Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt` placed on the temperature module +- Source mastermix labware: `Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt` placed on temperature module +- Destination labware: `Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt`, placed on C3 +- Load three `opentrons_flex_96_filtertiprack_50ul` tip racks on `opentrons_flex_96_tiprack_adapter` adapters in slots A2, B2, and C2 + +Pipette mount: + +- `Flex 96-Channel 1000 uL Pipette` is mounted on the left side + +Well Allocation: + +- source well: A1 well of source labware +- destination well: A1 well of destination labware +- mastermix well: A1 well of mastermix labware + +Commands: + +1. Set the thermocycler block temperature to 22 C. +2. Open the thermocycler lid. +3. Set the thermocycler lid temperature to 95 C. +4. Set the temperature module to 37 C. +5. Set master mix temperature module to 10 C. +6. Transfer 20 uL of liquid from 5 mm below the top surface of mastermix well to 2 mm above the bottom of destination well. Use the same tip for each transfer. +7. Transfer 20 ul of liquid from 3 mm above the source well bottom to destination well 7 mm beneath the top surface. Flow rate is at half the default. + Mix the sample and mastermix of 40 ul total volume 5 times. Remove the tip slowly out of the well at 5 mm/s speed. Use the same tip for each transfer. +8. Move the destination labware to the thermocycler using gripper. +9. Close the thermocycler lid. +10. Execute the thermocycle using the following profile: + +- 74 degree C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume + +11. Execute the thermocycle using the following profile: + +- 60 degree C for 7 seconds, 84 degree C for 19 seconds, 57 degree C for 44 seconds for 25 cycles, block max volume is sample and mastermix volume + +12. Execute the thermocycle using the following profile: + +- 75 degree C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume + +13. Hold thermocycler block at 4 C. +14. Open thermocycler lid. +15. Move the destination labware from thermocycler back to its original slot C3 using gripper. +16. Pause the protocol and tell the user to pick up the destination plate, seal it and refrigerate it at 4 C. +17. Deactivate the temperature modules. +18. Deactivate the mastermix temperature modules. + + + +from opentrons import protocol_api +metadata = { + 'protocol_name': 'PCR Amplification protocol', + 'author': 'Opentrons Generative AI', + 'description': 'PCR Amplification protocol with 25 cycles', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +def run(protocol: protocol_api.ProtocolContext): # Sample parameters +sample_volume_ul = 20 +master_mix_volume_ul = 20 +mix_cycles = 5 +total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul +return_slot = 'C3' + + master_mix_temperature_c = 10 + sample_temperature_c = 37 + step1_cycles = 1 + step2_cycles = 25 + step3_cycles = 1 + + # Thermocycler parameters + lid_temperature_c = 95 + initial_block_temperature_c = 22 + final_hold_temperature_c = 4 + + # Modules + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 'D1') + master_mix_temperature_module = protocol.load_module('temperature module gen2', 'D3') + + # Adapters + sample_adapter = sample_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + master_mix_adapter = master_mix_temperature_module.load_adapter('opentrons_96_well_aluminum_block') + + # Labware + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + destination_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'C3') + tips_50ul = [ + protocol.load_labware( + 'opentrons_flex_96_filtertiprack_50ul', + slot, + adapter="opentrons_flex_96_tiprack_adapter" + ) + for slot in ['A2', 'B2', 'C2'] + ] + + # Pipette + pipette_96channel = protocol.load_instrument('flex_96channel_1000', 'left', tip_racks=tips_50ul) + + # Well allocation + sample_source_wells = sample_plate['A1'] + destination_wells = destination_plate['A1'] + master_mix_source_well = master_mix_plate['A1'] + + # Set thermocycler block and lid temperature + thermocycler_module.set_block_temperature(initial_block_temperature_c) + thermocycler_module.open_lid() + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Temperature module setup + sample_temperature_module.set_temperature(sample_temperature_c) + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Master mix transfer + pipette_96channel.transfer( + master_mix_volume_ul, + master_mix_source_well.top(-5), + destination_wells.bottom(2), + new_tip='once' + ) + + # Sample transfer + pipette_96channel.pick_up_tip() + pipette_96channel.aspirate(sample_volume_ul, sample_source_wells.bottom(3), rate=0.5) + pipette_96channel.dispense(sample_volume_ul, destination_wells.top(-7), rate=0.5) + pipette_96channel.mix(mix_cycles, total_mix_volume_ul) + pipette_96channel.move_to(destination_wells.top(), speed=5) + pipette_96channel.drop_tip() + + # Moving the plate to the thermocycler + protocol.move_labware(destination_plate, thermocycler_module, use_gripper=True) + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=step1_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=step2_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=step3_cycles, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.set_block_temperature(final_hold_temperature_c) + thermocycler_module.open_lid() + + # Moving the plate back to its original location + protocol.move_labware(destination_plate, return_slot, use_gripper=True) + + # Optional: pause for manual intervention + protocol.pause("Pick up the destination plate, seal it, and refrigerate at 4C.") + + # Deactivate temperature modules at the end of the protocol + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + + + +## Common rules for transfer + +================= COMMON RULES for TRANSFER ================= + +- when we allocate wells for source and destination, we need to pay attention to pipette type. + For example, see the command below + +``` +Sample source wells: the first 64 well column-wise in the sample source plate. +``` + +- pipette (eg., Flex 8-Channel 1000 uL Pipette), given the number of wells + we need to estimate the columns and use method `labware.columns()` to access the columns. + For example, + +```python +number_of_columns = math.ceil([number_of_samples] / 8) +source_wells = labware.columns()[:number_of_columns] +``` + +- pipette (eg., Flex 1-Channel 1000 uL Pipette), + we use `labware.wells()`. For example, + +```python +source_wells = labware.wells()[:[number_of_samples]] +``` + +- If prompt says row-wise, we need to use `rows()` +- If prompt does not mention column-wise, we use `wells()` since it is default. +- If the number of samples are not specified, then use all wells. + +```python +source_wells = sample_plate.wells() +``` + +- If `blowout_location` location is mentioned explicitly, then incorporate to transfer method. +- Avoid using `for` with transfer + the following is incorrect: + +```python +source_columns = [source_labware.columns_by_name()[str(index)] for index in [3, 2, 5, 1, 10]] +destination_columns = [source_labware.columns_by_name()[str(index)] for index in [4, 8, 1, 9, 2]] + +# Transfer reagents +for src, dest in zip(source_columns, destination_columns): + pipette.transfer(14.0, src, dest, new_tip='always') +``` + +The correct: + +```python +source_columns = [source_labware.columns_by_name()[str(index)] for index in [3, 2, 5, 1, 10]] +destination_columns = [source_labware.columns_by_name()[str(index)] for index in [4, 8, 1, 9, 2]] + +# Transfer reagents +pipette.transfer(14.0, source_columns, destination_columns, new_tip='always') +``` + +## Other Examples with More Explanation + +### First Example + +**Description** + +Write a protocol using the Opentrons Python Protocol API v2 for the OT-2 robot based on the following details: + +**Labware:** + +- **Source Labware:** `Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap` placed in slot 3. +- **Destination Labware:** `Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt` placed in slot 9. +- **Tip Rack:** `Opentrons OT-2 96 Tip Rack 20 µL` placed in slot 2. + +**Pipette Mount:** + +- A **P20 Single Channel** pipette is mounted on the **right** side. + +**Commands:** + +1. Using the P20 Single Channel pipette, **transfer 1 µL** of reagent from the **first tube (A1)** of the source rack to **each well** in the destination plate. +2. **Use the same tip** for each transfer (do not change tips between transfers). + +**Protocol** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'Your Name', + 'description': 'Transfer 1 µL of reagent from source tube A1 to all wells in the destination plate using the same tip.', +} +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + tiprack = protocol.load_labware('opentrons_96_tiprack_20ul', 2) + source_rack = protocol.load_labware('opentrons_24_tuberack_nest_1.5ml_snapcap', 3) + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 9) + + # Load pipette + p20 = protocol.load_instrument('p20_single_gen2', mount='right', tip_racks=[tiprack]) + + # Define source and destination wells + source_well = source_rack.wells_by_name()['A1'] + destination_wells = dest_plate.wells() + + # Perform transfer + p20.transfer( + 1, # Volume in µL + source_well, # Source well + destination_wells, # List of destination wells + new_tip='once' # Use the same tip for all transfers + ) +``` + +**Explanation:** + +- **Labware Setup:** + - **Tip Rack:** Loaded into slot 2 for the P20 pipette to pick up tips. + - **Source Rack:** Contains the reagent in tube A1, placed in slot 3. + - **Destination Plate:** The 96-well plate where the reagent will be dispensed, placed in slot 9. +- **Pipette Initialization:** + - The P20 Single Channel pipette is initialized on the right mount with the specified tip rack. +- **Transfer Details:** + - **Volume:** 1 µL is transferred from the source to each destination well. + - **Source Well:** Tube A1 in the source rack. + - **Destination Wells:** All wells in the 96-well plate. + - **Tip Usage:** `new_tip='once'` ensures the same tip is used throughout the entire transfer process. +- **Method Used:** + - The `transfer` method is used without any explicit loops because it inherently handles the iteration over the list of destination wells. + +--- + +### Second Example + +**Description** + +Using a **Flex 1-Channel 1000 µL Pipette** mounted on the **left** side, transfer **150 µL** from wells **A1** and **A2** in **source labware 1** to wells **B6** and **B7** in **source labware 2**. **Use the same tip** for each transfer. + +**Protocol** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multiple Well Transfer', + 'author': 'Your Name', + 'description': 'Transfer 150 µL from specific source wells to specific destination wells using the same tip.', +} +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + source_labware_1 = protocol.load_labware('source_labware_1_definition', slot=1) + source_labware_2 = protocol.load_labware('source_labware_2_definition', slot=2) + tiprack = protocol.load_labware('opentrons_96_tiprack_1000ul', slot=3) + + # Load pipette + p1000 = protocol.load_instrument('flex_1channel_1000', mount='left', tip_racks=[tiprack]) + + # Define source and destination wells + source_wells = [source_labware_1.wells_by_name()[well] for well in ['A1', 'A2']] + destination_wells = [source_labware_2.wells_by_name()[well] for well in ['B6', 'B7']] + + # Perform transfer + p1000.transfer( + 150, # Volume in µL + source_wells, # List of source wells + destination_wells, # List of destination wells + new_tip='once' # Use the same tip for all transfers + ) +``` + +**Explanation:** + +- **Labware Setup:** + - **Source Labware 1:** Contains the initial samples in wells A1 and A2, loaded into slot 1. + - **Source Labware 2:** Will receive the transferred samples in wells B6 and B7, loaded into slot 2. + - **Tip Rack:** Loaded into slot 3 for the pipette to pick up tips. +- **Pipette Initialization:** + - The Flex 1-Channel 1000 µL pipette is initialized on the left mount with the specified tip rack. +- **Defining Wells:** + - **Source Wells:** A list containing wells A1 and A2 from source labware 1. + - **Destination Wells:** A list containing wells B6 and B7 from source labware 2. +- **Transfer Details:** + - **Volume:** 150 µL is transferred from each source well to the corresponding destination well. + - **Tip Usage:** `new_tip='once'` ensures the same tip is used for all transfers. +- **Method Used:** + - The `transfer` method is used with lists of source and destination wells. This method pairs each source well with its corresponding destination well, eliminating the need for explicit loops. + +**Note:** The use of a single `transfer` function with lists allows for multiple transfers in a streamlined manner. + +--- + +By using the `transfer` method effectively, we can simplify the protocol code and make it more readable. The method automatically handles the pairing and iteration over wells, so explicit loops are unnecessary. Additionally, specifying `new_tip='once'` optimizes the protocol by reducing tip usage when appropriate. + +## Best Practices for Optimizing the transfer Method in Pipetting Automation + +1. **Optimizing `transfer` Usage Without Loops** + + - **Issue**: Using the `transfer` method inside a `for` loop is unnecessary because `transfer` can handle lists implicitly. + - **Solution**: Remove the `for` loop and use the `transfer` method directly with lists for efficient code. + + **Example:** + + - _Inefficient Code (Excerpt-1):_ + + ```python + for source_well, destination_well in zip(source_wells, destination_wells): + pipette.pick_up_tip() + pipette.transfer(TRANSFER_VOL, source_well, destination_well, new_tip='never') + pipette.drop_tip() + ``` + + - _Optimized Code (Excerpt-2):_ + ```python + pipette.transfer(TRANSFER_VOL, source_wells, destination_wells, new_tip='always') + ``` + +2. **Correct Use of `new_tip='once'`** + + - **Note**: When instructed to "Use the same tip for all transfers" or similar, avoid using `new_tip='once'` inside a `for` loop, as this is incorrect. + - **Solution**: Use the `transfer` method without a `for` loop to ensure the same tip is used throughout. + + **Incorrect Usage:** + + ```python + for src, dest in zip(source_columns, destination_columns): + pipette.transfer(transfer_vol, src, dest, new_tip='once') + ``` + + **Correct Usage:** + + ```python + pipette.transfer(transfer_vol, source_columns, destination_columns, new_tip='once') + ``` + +3. **Importing Necessary Libraries** + + - **Reminder**: Always import necessary libraries, such as `math`, when using functions like `ceil` or other mathematical methods. + + ```python + import math + ``` + +4. **Using `columns` Method with Multi-Channel Pipettes** + + - **Guideline**: For multi-channel pipettes (e.g., P20 Multi-Channel Gen2), utilize the `columns` method to access labware columns effectively. + + **Example:** + + ```python + source_columns = source_plate.columns() + destination_columns = destination_plate.columns() + ``` + +--- + +### Another Example + +```python +"from opentrons import protocol_api + +# metadata +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.19"} + + +def run(protocol: protocol_api.ProtocolContext): + # labware + source_labware_1 = [protocol.load_labware("corning_96_wellplate_360ul_flat", location=slot) for slot in ['D1', 'D2', 'D3']] + source_labware_2 = protocol.load_labware("nest_1_reservoir_195ml", location='A1') + destination = [protocol.load_labware("corning_96_wellplate_360ul_flat", location=slot) for slot in ['C1', 'C2', 'C3']] + tiprack200 = [protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location=slot) for slot in ['B1', 'B2', 'B3']] + tiprack50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p50s = protocol.load_instrument("flex_1channel_50", mount="right", tip_racks=[tiprack50]) + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[*tiprack200]) + # load trash bin + trash = protocol.load_trash_bin('A3') + + # volumes setup + transfer_vol_1 = 20 + transfer_vol_2 = 100 + + # wells setup + source_wells_1 = [src.wells() for src in source_labware_1] + source_wells_2 = source_labware_2.wells_by_name()['A1'] + destination_wells_1 = [dest.wells() for dest in destination] + + # commands + p50s.transfer(transfer_vol_1, source_wells_2, destination_wells_1, new_tip="once") + p1000s.transfer(transfer_vol_2, source_wells_1, destination_wells_1, new_tip="always") +``` + +#### another example + +```python +from opentrons import protocol_api + +# metadata +metadata = { + 'protocolName': 'Reagent Transfer', + 'author': 'Opentrons Generative AI', +} + +requirements = {"robotType": "Flex", "apiLevel": "2.19"} + + +def run(protocol: protocol_api.ProtocolContext): + + # labware + source_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C1') + source_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='C2') + destination_1 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D1') + destination_2 = protocol.load_labware("corning_96_wellplate_360ul_flat", location='D2') + tiprack200 = protocol.load_labware("opentrons_flex_96_filtertiprack_200ul", location='B2') + tiprack50 = protocol.load_labware("opentrons_flex_96_filtertiprack_50ul", location='A2') + + # pipettes + p1000s = protocol.load_instrument("flex_1channel_1000", mount="left", tip_racks=[tiprack200]) + p50s = protocol.load_instrument("flex_1channel_50", mount="right", tip_racks=[tiprack50]) + # load trash bin + trash = protocol.load_trash_bin('A3') + # volume setup + transfer_vol_1 = 50 + transfer_vol_2 = 15 + transfer_vol_3 = 10 + transfer_vol_4 = 10 + + # well setup + source_wells_1 = [source_1.wells_by_name()[wells] for wells in ['A1', 'A2']] + source_wells_2 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] + source_wells_3 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + source_wells_4 = [source_2.wells_by_name()[wells] for wells in ['C4', 'C6']] + destination_wells_1 = [source_2.wells_by_name()[wells] for wells in ['B6', 'B7']] + destination_wells_2 = [source_1.wells_by_name()[wells] for wells in ['A3', 'A4']] + destination_wells_3 = [destination_1.wells_by_name()[wells] for wells in ['A1', 'B1']] + destination_wells_4 = [destination_2.wells_by_name()[wells] for wells in ['A1', 'B1']] + + # commands + p1000s.transfer(transfer_vol_1, source_wells_1, destination_wells_1, new_tip="once") + p50s.transfer(transfer_vol_2, source_wells_2, destination_wells_2, new_tip="once") + p50s.transfer(transfer_vol_3, source_wells_3, destination_wells_3, new_tip="always") + p50s.transfer(transfer_vol_4, source_wells_4, destination_wells_4, new_tip="always") +``` + +### pcr example + +```python +import math +from opentrons import protocol_api + +metadata = { + 'protocol_name': 'QIAGEN OneStep RT-PCR Kit PCR Amplification', + 'author': 'chatGPT', + 'description': 'Amplification using QIAGEN OneStep RT-PCR Kit with 13 cycles', + 'apiLevel': '2.16' +} + + +def run(protocol: protocol_api.ProtocolContext): + # Sample preparation parameters + number_of_samples = 64 + sample_volume_ul = 5 # Volume in microliters + master_mix_volume_ul = 7 # Volume in microliters + mixing_cycles = 9 + total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul + master_mix_temperature_c = 10 # Temperature in Celsius + sample_temperature_c = 4 # Temperature in Celsius + + # Thermocycler parameters + lid_temperature_c = 55 # Lid temperature in Celsius + initial_plate_temperature_c = 6 # Initial plate temperature in Celsius + hold_temperature_c = 4 # Hold temperature in Celsius for infinite hold + + # Modules loading + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temperature_module = protocol.load_module('temperature module gen2', 1) + master_mix_temperature_module = protocol.load_module('temperature module gen2', 3) + + # Labware loading + tips_20ul = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + pcr_plate = thermocycler_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + sample_plate = sample_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_temperature_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + # Pipette loading + multi_channel_pipette = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tips_20ul]) + + # Well allocation + number_of_columns = math.ceil(number_of_samples / 8) + sample_source_wells = sample_plate.columns()[:number_of_columns] + sample_destination_wells = pcr_plate.columns()[:number_of_columns] + master_mix_source_wells = master_mix_plate.columns()[:number_of_columns] + master_mix_destination_wells = pcr_plate.columns()[:number_of_columns] + + # Command 2: Open lid + thermocycler_module.open_lid() + + # Command 3: Set initial plate temperature + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + + # Command 4: Set lid temperature + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Command 5: Set sample temperature + sample_temperature_module.set_temperature(sample_temperature_c) + + # Command 6: Set master mix temperature + master_mix_temperature_module.set_temperature(master_mix_temperature_c) + + # Command 7: Transfer master mix to destination wells + multi_channel_pipette.transfer( + master_mix_volume_ul, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' + ) + + # Command 8: Transfer samples to destination wells and mix + multi_channel_pipette.transfer( + sample_volume_ul, + sample_source_wells, + sample_destination_wells, + new_tip='always', + mix_after=(mixing_cycles, total_mix_volume_ul), + blow_out=True, + blowout_location='destination well' + ) + + # Command 9: Close lid + thermocycler_module.close_lid() + + # Commands 10-12: PCR cycling + thermocycler_module.execute_profile( + steps=[{'temperature': 74, 'hold_time_seconds': 65}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[ + {'temperature': temp, 'hold_time_seconds': duration} + for temp, duration in zip([60, 84, 57], [7, 19, 44]) + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + # Command 13: Set final hold temperature + thermocycler_module.set_block_temperature(hold_temperature_c) + + # Command 14: Open lid post-PCR + thermocycler_module.open_lid() + + # Commands 15 & 16: Deactivate temperature modules + master_mix_temperature_module.deactivate() + sample_temperature_module.deactivate() + +``` + +### Liquid transfer with Heater Shaker module + + +Write a protocol using the Opentrons Python Protocol API v2.19 for Opentrons Flex robot for the following description: + +Metadata: + +- Author: User +- ProtocolName: Liquid Transfer with Heater Shaker +- Description: Transfer liquids between reservoir, PCR plate, and heater shaker module's plate. + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Labware: + +- Trash: Load a trash bin in slot A3 +- Heater Shaker Module: Load in slot D1 with a Corning 96 Well Plate 360 µL Flat +- Reservoir: NEST 1 Well Reservoir 195 mL in slot C1 +- PCR Plate: NEST 96 Well Plate 200 µL Flat in slot D2 +- Tipracks: + - Opentrons Flex 96 Tiprack 200 µL in slot A2 + - Opentrons Flex 96 Tiprack 1000 µL in slot B2 + - Opentrons Flex 96 Tiprack 50 µL in slot C2 + All tipracks should use the Opentrons Flex 96 Tiprack Adapter + +Pipette: + +- Flex 96-Channel 1000 µL pipette mounted on the left + +Steps: + +1. Open the Heater Shaker Module's labware latch +2. Pause the protocol and prompt the user to load the Corning 96 well plate +3. Close the Heater Shaker Module's labware latch +4. Using the 96-channel pipette with 200 µL tips, transfer 70 µL from the reservoir (A1) to the Heater Shaker plate (A1) +5. Using the 96-channel pipette with 50 µL tips, transfer 10 µL from the PCR plate (A1) to the Heater Shaker plate (A1) +6. Shake the plate on the Heater Shaker Module at 2000 rpm for 1 minute +7. Deactivate the shaker + +Notes: + +- Use new tips for each transfer +- The protocol includes comments and pauses for user interaction + + + + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Liquid Transfer with Heater Shaker', + 'author': 'User', + 'description': 'Transfer liquids between reservoir, PCR plate, and heater shaker module\'s plate.' +} + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.19' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load trash bin + trash = protocol.load_trash_bin('A3') + + # Load modules + heater_shaker = protocol.load_module('heaterShakerModuleV1', 'D1') + + # Load labware + heater_shaker_plate = heater_shaker.load_labware('corning_96_wellplate_360ul_flat') + reservoir = protocol.load_labware('nest_1_reservoir_195ml', 'C1') + pcr_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D2') + + # Load tipracks with adapters + tiprack_200 = protocol.load_labware('opentrons_flex_96_tiprack_200ul', 'A2', adapter='opentrons_flex_96_tiprack_adapter') + tiprack_1000 = protocol.load_labware('opentrons_flex_96_tiprack_1000ul', 'B2', adapter='opentrons_flex_96_tiprack_adapter') + tiprack_50 = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'C2', adapter='opentrons_flex_96_tiprack_adapter') + + # Load pipette + pipette = protocol.load_instrument('flex_96channel_1000', 'left', tip_racks=[tiprack_200, tiprack_1000, tiprack_50]) + + # Protocol steps + # 1. Open the Heater Shaker Module's labware latch + heater_shaker.open_labware_latch() + + # 2. Pause for user to load the plate + protocol.pause('Please load the Corning 96 well plate onto the Heater Shaker Module and resume the protocol.') + + # 3. Close the Heater Shaker Module's labware latch + heater_shaker.close_labware_latch() + + # 4. Transfer 70 µL from reservoir to Heater Shaker plate + pipette.transfer(70, reservoir['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 5. Transfer 10 µL from PCR plate to Heater Shaker plate + pipette.transfer(10, pcr_plate['A1'], heater_shaker_plate['A1'], new_tip='always') + + # 6. Shake the plate + heater_shaker.set_and_wait_for_shake_speed(rpm=2000) + protocol.delay(minutes=1) + + # 7. Deactivate the shaker + heater_shaker.deactivate_shaker() +``` + + diff --git a/opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md b/opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md new file mode 100644 index 00000000000..0617543b916 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/out_of_tips_error_219.md @@ -0,0 +1,382 @@ +# Preventing "Out of Tips" and Index Errors in Opentrons Protocols + +"Out of tips" and index errors are common issues that can halt the execution of protocols on Opentrons robots. These errors occur when the protocol attempts to use more pipette tips than are available or when it tries to access wells beyond the labware's dimensions. Proper planning and understanding of tip consumption and labware indexing are essential to prevent such errors and ensure smooth laboratory operations. + +## Common Scenarios Leading to Errors + +### 1. Single Pipette Exceeds Tip Rack Capacity + +**Scenario:** +A single-channel pipette performs repeated operations using tips from a single tip rack without accounting for tip depletion. + +**Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Single Tip Rack Exhaustion Example', + 'author': 'Opentrons', + 'description': 'A protocol that runs out of tips after exceeding tip rack capacity', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tip_rack]) + + # Perform operations + for _ in range(100): + pipette.pick_up_tip() + pipette.aspirate(100, plate['A1']) + pipette.dispense(100, plate['B1']) + pipette.drop_tip() +``` + +**Issue Explanation:** +The protocol attempts 100 tip pickups using a single tip rack containing only 96 tips. After 96 successful pickups, the pipette runs out of tips, resulting in an error on the 97th attempt. + +--- + +### 2. Multi-Channel Pipette with Insufficient Tip Racks + +**Scenario:** +A multi-channel pipette uses tips from a single tip rack but requires more tips than are available due to the number of channels used per operation. + +**Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Tip Rack Exhaustion Example', + 'author': 'Opentrons', + 'description': 'A protocol where a multi-channel pipette runs out of tips', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack]) + + # Perform operations + for i in range(20): + pipette.pick_up_tip() + pipette.aspirate(100, plate.rows()[0][i]) + pipette.dispense(100, plate.rows()[1][i]) + pipette.drop_tip() +``` + +**Issue Explanation:** +A multi-channel pipette uses 8 tips per pick-up. Over 20 iterations, it requires 160 tips (20 iterations × 8 tips). A single 96-tip rack is exhausted after 12 iterations (96 tips / 8 tips per iteration), causing an error during the 13th iteration. Additionally, attempting to access `plate.rows()[0][i]` where `i` exceeds 11 (the maximum index for 12 columns) results in an index error. + +**Solution:** + +- **Load Additional Tip Racks:** Introduce more tip racks to provide enough tips for all operations. +- **Validate Index Ranges:** Ensure that the loop indices do not exceed the labware dimensions. + +**Corrected Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Tip Rack Exhaustion Example - Solved', + 'author': 'Opentrons', + 'description': 'Multi-channel pipette avoids running out of tips and index errors', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack1 = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + tip_rack2 = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack1, tip_rack2]) + + # Perform operations within available columns range + for i in range(12): # Restrict to 12 columns + pipette.pick_up_tip() + pipette.aspirate(100, plate.columns()[i][0]) + pipette.dispense(100, plate.columns()[i][1]) + pipette.drop_tip() +``` + +--- + +### 3. Nested Loops Causing Excessive Tip Usage + +**Scenario:** +Nested loops in the protocol lead to a higher number of tip pickups than anticipated, exhausting the available tips. + +**Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Nested Loops Tip Exhaustion Example', + 'author': 'Opentrons', + 'description': 'A protocol demonstrating tip exhaustion due to nested loops', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tip_rack]) + + # Perform operations + for row in range(8): + for col in range(12): + for _ in range(2): + pipette.pick_up_tip() + pipette.aspirate(100, plate.rows()[row][col]) + pipette.dispense(100, plate.rows()[row][(col + 1) % 12]) + pipette.drop_tip() +``` + +**Issue Explanation:** +The nested loops result in 192 tip pickups (8 rows × 12 columns × 2 repetitions). With only 96 tips available, the protocol runs out of tips halfway through, causing an error. + +**Solution:** +Introduce additional tip racks to provide enough tips for all operations. + +**Corrected Protocol Example:** + +```python +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack1 = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + tip_rack2 = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + + # Load pipette + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tip_rack1, tip_rack2]) + + # Perform operations (same as above) +``` + +--- + +## Calculating Tip Usage in Many-to-Many Transfers + +In protocols involving many-to-many transfers, it's crucial to calculate the number of tips required accurately to avoid "out of tips" errors. + +### Guidelines for Many-to-Many Transfers + +- **Even Divisibility:** Ensure the number of wells in the larger group (source or destination) is evenly divisible by the number of wells in the smaller group. +- **Stretching the Smaller Group:** Conceptually "stretch" the smaller group of wells to match the length of the larger group. Each well in the smaller group may be used multiple times. +- **Tip Requirement:** The number of tips required is always equal to the number of wells in the larger group. +- **Multi-Channel Pipettes:** For multi-channel pipettes, remember that each operation uses multiple tips (e.g., 8 tips for an 8-channel pipette). If using a 96-channel pipette, each operation consumes 96 tips. + +### Example Calculation + +- **Scenario:** Transfer from 24 source wells to 96 destination wells. +- **Process:** + - The 24 source wells are stretched to match the 96 destination wells. + - Each source well is used multiple times to cover all destination wells. + - **Total Transfers:** 96. + - **Tips Required:** + - **Single-Channel Pipette:** 96 tips (one per transfer). + - **Multi-Channel Pipette (8-channel):** 12 transfers (96 wells / 8 channels), using 8 tips per transfer, totaling 96 tips. + +--- + +## Key Points to Avoid Index Errors + +- **Validate Access Ranges:** Always ensure that your loops and operations do not exceed the dimensions of the labware being used. For example, a 96-well plate has 12 columns and 8 rows; accessing an index beyond these ranges will cause an error. +- **Sufficient Resources:** Make sure the number of loaded tip racks can handle the total number of operations required by the protocol. + +**Example Problem:** + +A multi-channel pipette runs out of tips after 12 operations due to using 8 tips per operation, and the code attempts to access non-existent column indices beyond the 12 columns available in a 96-well plate. + +**Incorrect Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Index Error Example', + 'author': 'Opentrons', + 'description': 'A protocol that causes index errors due to invalid column access', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack]) + + # Perform operations + for i in range(20): # Exceeds available columns + pipette.pick_up_tip() + pipette.aspirate(100, plate.columns()[i][0]) + pipette.dispense(100, plate.columns()[i][1]) + pipette.drop_tip() +``` + +**Solution:** + +- **Restrict Loop Indices:** Adjust the loop to stay within the valid column indices (0 to 11 for a 96-well plate). +- **Load Additional Tip Racks:** Ensure enough tips are available for all operations. + +**Corrected Protocol Example:** + +```python +from opentrons import protocol_api + +metadata = { + 'protocolName': 'Multi-Channel Index Error Example - Solved', + 'author': 'Opentrons', + 'description': 'A protocol that avoids index errors by validating column indices', + 'apiLevel': '2.13' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + plate = protocol.load_labware('corning_96_wellplate_360ul_flat', '1') + tip_rack1 = protocol.load_labware('opentrons_96_tiprack_300ul', '2') + tip_rack2 = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + + # Load pipette + pipette = protocol.load_instrument('p300_multi', 'right', tip_racks=[tip_rack1, tip_rack2]) + + # Perform operations within available columns range + for i in range(12): # Valid column indices for a 96-well plate + pipette.pick_up_tip() + pipette.aspirate(100, plate.columns()[i][0]) + pipette.dispense(100, plate.columns()[i][1]) + pipette.drop_tip() +``` + +--- + +## Best Practices to Avoid "Out of Tips" and Index Errors + +### 1. Calculate Required Tips in Advance + +- **Estimate Operations:** Calculate the total number of pipetting actions that require new tips, including loops and many-to-many transfers. +- **Consider Multiple Pipettes:** Calculate tip requirements separately for each pipette, accounting for their specific usage patterns. + +### 2. Load Sufficient Tip Racks + +- **Tip Rack Capacity:** Standard 96-tip racks hold 96 tips. Ensure the total number of tips available meets or exceeds your calculated requirement. +- **Add Buffers:** Include extra tip racks to handle unexpected needs or minor calculation errors. + +### 3. Validate Labware Indexing + +- **Check Labware Dimensions:** Before accessing wells or columns in loops, confirm the dimensions of your labware to avoid index errors. +- **Adjust Loop Ranges:** Ensure that loop indices do not exceed the maximum indices of the labware being used. + +### 4. Associate Tip Racks with Pipettes + +- **Specify Tip Racks:** Explicitly associate each pipette with its corresponding tip racks for efficient tip tracking. +- **Multiple Tip Racks:** Use multiple tip racks for pipettes with high tip consumption. + +### 5. Implement Tip Replenishment Strategies + +- **Dynamic Replenishment:** Use commands like `move_labware()` to swap in fresh tip racks during long protocols. +- **Manual Replenishment:** Plan steps within the protocol to allow for manual replacement of tip racks if automatic replenishment isn't feasible. + +### 6. Optimize Tip Usage + +- **Reuse Tips When Appropriate:** If protocol requirements allow, reuse the same tip for multiple transfers to reduce tip consumption. +- **Minimize Tip Pickups:** Combine transfers when possible to limit the number of tip pickups. + +### 7. Handle Special Cases Carefully + +- **Multi-Channel Pipettes:** Remember that multi-channel pipettes consume multiple tips per pickup. Adjust tip rack quantities accordingly. +- **Nested Loops:** Be cautious with nested loops, as they can exponentially increase tip usage. Validate tip requirements before execution. +- **Many-to-Many Transfers:** Apply the specific calculations for many-to-many transfers to determine accurate tip usage. + +### 8. Implement Error Handling and Testing + +- **Catch Errors Early:** Incorporate checks to detect potential "out of tips" or index errors before they cause runtime issues. +- **Conduct Dry Runs:** Perform simulations or test runs to ensure all logical paths are covered and tip requirements are met. + +--- + +## Example when using serial dilution protocol + +Below protocol produces `OutofTips` error, since it excauts all tips by using `plate.rows()`: +(One needs to be careful) + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'OT-2', + 'apiLevel': '2.16' +} + +def run(protocol: protocol_api.ProtocolContext): + # Load labware + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 1) + reservoir = protocol.load_labware('nest_12_reservoir_15ml', 2) + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 3) + + # Load pipette + p300 = protocol.load_instrument('p300_single_gen2', 'left', tip_racks=[tiprack]) + + # Distribute diluent + p300.transfer(100, reservoir['A1'], plate.wells()) + + # Perform serial dilution + for row in plate.rows(): + # Transfer and mix solution from reservoir to first well + p300.transfer(100, reservoir['A2'], row[0], mix_after=(3, 50), new_tip='always') + + # Serial dilution within the row + p300.transfer(100, row[:11], row[1:], mix_after=(3, 50), new_tip='always') + +``` + +Correct way is follows: + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'OT-2', + 'apiLevel': '2.16' +} + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", 2) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", 3) + left_pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` diff --git a/opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md b/opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md new file mode 100644 index 00000000000..40b1ce10739 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/pcr_protocols_with_csv.md @@ -0,0 +1,1065 @@ +# PCR protocol examples + +## 1. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the OT-2 robot according to the following description: + +Requirements: + +- requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +Modules: + +- Temperature module GEN2 for samples in slot 1 +- Temperature module GEN2 for mastermix in slot 3 + +Labware: + +- Sample source: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on sample temperature module +- Mastermix source: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on mastermix temperature module +- Destination: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt in slot 7 +- Tips: Opentrons 96 Filter Tip Rack 20 uL in slot 4 + +Pipette: + +- Left mount: P20 Multi-Channel Gen2 + +Sample Setup: + +- Number of samples: 64 (8 columns) +- Well allocation: First 64 wells (column-wise) in all plates + +Temperature Settings: + +- Sample temperature: 4C +- Mastermix temperature: 10C + +Protocol Steps: + +1. Set temperature modules to specified temperatures +2. Transfer 7 uL mastermix to destination wells (reuse tip) +3. Transfer 5 uL sample to destination wells, mix 9 times with 12 uL total volume + (use new tip for each transfer, blow out to destination well) +4. Deactivate both temperature modules + + + + +```python +from opentrons import protocol_api + +requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Module loading + temp_mod_sample = protocol.load_module('temperature module gen2', 1) + temp_mod_mastermix = protocol.load_module('temperature module gen2', 3) + + # Load thermal adapters + sample_adapter = temp_mod_sample.load_adapter("opentrons_96_well_aluminum_block") + mastermix_adapter = temp_mod_mastermix.load_adapter("opentrons_96_well_aluminum_block") + + # Load labware onto the adapters + sample_plate = sample_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + mastermix_plate = mastermix_adapter.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 7) + tiprack = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + + # Pipette loading + p20_single = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tiprack]) + + # Well allocation + num_samples = 64 + num_cols = num_samples // 8 + sample_wells = sample_plate.columns()[:num_cols] + mastermix_wells = mastermix_plate.columns()[:num_cols] + dest_wells = dest_plate.columns()[:num_cols] + + # Volume and temperature parameters + sample_temp = 4 # Celsius + mastermix_temp = 10 # Celsius + mastermix_vol = 7 # uL + sample_vol = 5 # uL + mix_cycles = 9 + total_mix_vol = sample_vol + mastermix_vol # 12 uL total + + # Execute commands in order specified in description + # Command 1: Set sample temperature + temp_mod_sample.set_temperature(sample_temp) + + # Command 2: Set mastermix temperature + temp_mod_mastermix.set_temperature(mastermix_temp) + + # Command 3: Transfer mastermix + p20_single.transfer( + mastermix_vol, + mastermix_wells, + dest_wells, + new_tip='once' + ) + + # Command 4: Transfer samples and mix + p20_single.transfer( + sample_vol, + sample_wells, + dest_wells, + new_tip='always', + mix_after=(mix_cycles, total_mix_vol), + blow_out=True, + blowout_location='destination well' + ) + + # Command 5 and 6: Deactivate temperature modules + temp_mod_mastermix.deactivate() + temp_mod_sample.deactivate() +``` + + + +## 2. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot according to the following description: + +Requirements: + +- requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +Module: + +- Temperature module GEN2 on slot 1 + +Labware: + +- Sample Source: Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap on temperature module +- Mastermix Source: Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap on slot 3 +- Destination: Opentrons 96 Well Plate 200 uL PCR Full Skirt on slot 7 +- Tips: Opentrons 96 Filter Tip Rack 20 uL on slot 4 + +Pipette: + +- Right mount: P20 Single Channel GEN2 + +Sample Setup: + +- Number of samples: 24 +- Well allocation: First 24 wells (column-wise) in all plates + +Protocol Steps: + +1. Set temperature module to 4°C +2. Transfer 8 uL mastermix to destination wells (reuse same tip) +3. Transfer 7 uL sample to destination wells, mix 4 times with 15 uL total volume + (use new tip for each transfer, blow out to destination well) +4. Deactivate temperature module + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'OT-2', + 'apiLevel': '2.19' +} + +def run(protocol: protocol_api.ProtocolContext): + + # Protocol parameters + num_samples = 24 + sample_vol = 7 # uL + mastermix_vol = 8 # uL + mix_cycles = 4 + total_vol = sample_vol + mastermix_vol # 15 uL total + temp_celsius = 4 + + # Load temperature module + temp_module = protocol.load_module('temperature module gen2', 1) + + # Load labware + source_samples = temp_module.load_labware('opentrons_24_aluminumblock_nest_1.5ml_snapcap') + source_mastermix = protocol.load_labware('opentrons_24_tuberack_nest_1.5ml_snapcap', 3) + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 7) + tiprack = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + + # Load pipette + p20_single = protocol.load_instrument('p20_single_gen2', 'right', tip_racks=[tiprack]) + + # Define wells + sample_wells = source_samples.wells()[:num_samples] + mastermix_wells = source_mastermix.wells()[:num_samples] + dest_wells = dest_plate.wells()[:num_samples] + + # Set temperature + temp_module.set_temperature(temp_celsius) + + # Transfer mastermix + p20_single.transfer( + mastermix_vol, + mastermix_wells, + dest_wells, + new_tip='once' + ) + + # Transfer samples and mix + p20_single.transfer( + sample_vol, + sample_wells, + dest_wells, + mix_after=(mix_cycles, total_vol), + blow_out=True, + blowout_location='destination well', + new_tip='always' + ) + + # Deactivate temperature module + temp_module.deactivate() +``` + + + +## 3. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot according to the following description: + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Temperature Modules: + +- Sample module: Temperature Module GEN2 on slot D1 +- Mastermix module: Temperature Module GEN2 on slot D3 + +Module Adapters: + +- Opentrons 96 Well Aluminum Block on both temperature modules + +Labware: + +1. Source Plates: + - Sample plate: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on sample module + - Mastermix plate: Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on mastermix module +2. Destination: + - Opentrons 96 Tough Well Plate 200 uL PCR Full Skirt on slot A1 +3. Tips: + - Opentrons Flex 96 Filter Tip Rack 1000 uL on slots C1 and C2 + +Pipettes: + +- Left mount: Flex 8-Channel 1000 uL +- Right mount: Flex 8-Channel 50 uL + +Sample Setup: + +- Total samples: 72 +- Well usage: First 72 wells (column-wise) in all plates + +Protocol Steps: + +1. Temperature Setup: + a. Set sample module to 37°C + b. Set mastermix module to 4°C + +2. Mastermix Transfer: + + - Transfer 15 uL mastermix from source to destination wells + - Use the same tip for all transfers + +3. Sample Transfer: + + - Transfer 10 uL sample from source to destination wells + - Mix 9 times with 25 uL total volume after each transfer + - Use a new tip for each transfer + - Blow out to destination well after each transfer + +4. Module Shutdown: + a. Deactivate mastermix temperature module + b. Deactivate sample temperature module + + + + +```python +from opentrons import protocol_api + +requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +def run(protocol: protocol_api.ProtocolContext): + # Trash + trash = protocol.load_trash_bin("A3") + + # Protocol parameters + num_samples = 72 + mastermix_vol = 15 # uL + sample_vol = 10 # uL + mix_cycles = 9 + total_vol = mastermix_vol + sample_vol + + # Temperature settings + temp_mastermix = 4 # C + temp_sample = 37 # C + + # Load temperature modules + temp_mod_sample = protocol.load_module('temperature module gen2', 'D1') + temp_mod_mastermix = protocol.load_module('temperature module gen2', 'D3') + + # Load module adapters + block_sample = temp_mod_sample.load_adapter('opentrons_96_well_aluminum_block') + block_mastermix = temp_mod_mastermix.load_adapter('opentrons_96_well_aluminum_block') + + # Load labware + plate_sample = block_sample.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + plate_mastermix = block_mastermix.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + plate_dest = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'A1') + + # Load tip racks + tips_1000 = protocol.load_labware('opentrons_flex_96_filtertiprack_1000ul', 'C1') + tips_50 = protocol.load_labware('opentrons_flex_96_filtertiprack_1000ul', 'C2') + + # Load pipettes + p50_multi = protocol.load_instrument('flex_8channel_50', 'right', tip_racks=[tips_50]) + p1000_multi = protocol.load_instrument('flex_8channel_1000', 'left', tip_racks=[tips_1000]) + + # Set up well arrays + source_mastermix = plate_mastermix.columns()[:num_samples//8] + source_sample = plate_sample.columns()[:num_samples//8] + wells_dest = plate_dest.columns()[:num_samples//8] + + # Step 1: Set temperatures + temp_mod_sample.set_temperature(temp_sample) + temp_mod_mastermix.set_temperature(temp_mastermix) + + # Step 2: Transfer mastermix + p50_multi.transfer( + mastermix_vol, + source_mastermix, + wells_dest, + new_tip='once' + ) + + # Step 3: Transfer samples + p1000_multi.transfer( + sample_vol, + source_sample, + wells_dest, + new_tip='always', + mix_after=(mix_cycles, total_vol), + blow_out=True, + blowout_location='destination well' + ) + + # Step 4: Deactivate modules + temp_mod_mastermix.deactivate() + temp_mod_sample.deactivate() +``` + + + +## 4. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.19"}` + +Modules: + +- No modules + +Labware: + +- The source sample labware, a Biorad 96 well plate 200ul full skirt, is placed in slot D1. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot B1. +- A 50 uL filter tip rack is used in slot A1. +- A 50 uL filter tip rack is used in slot A2. + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- The number of sample columns is 3. + +Commands: + +- Using the single-chaneel pipette, for each row in the csv, aspirate 7ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- Using the multi-channel pipette, transfer 5ul of sample from the sample plate to the destination plate, column for column, up to the number of samples specified. Grab new tips for each column. + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.19' +} + +def run(protocol: protocol_api.ProtocolContext): + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + num_col = 3 + + # Load labware + mmx_tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'D1') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'B1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A1') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + + # Load pipettes + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # Trash + trash = protocol.load_trash_bin("A3") + + # 1. Transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, source=mmx_tuberack[source_tube], dest=dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # 2. Transfer sample + for s, d in zip(dna_plate.rows()[0][:num_col], dest_plate.rows()[0][:num_col]): + multi_pip.pick_up_tip() + multi_pip.transfer(5, source=s, dest=d, new_tip='never') + multi_pip.drop_tip() + +``` + + + +## 5. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.19"}` + +Modules: + +- Thermocycler module + +Labware: + +- The source sample labware, an biorad_96_wellplate_200ul_pcr, is placed in slot D1. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an opentrons_96_aluminumblock_nest_wellplate_100ul, is placed in thermocycler. +- A 50uL tip rack for the single channel pipette is in A2 +- A 50uL tip rack for the single channel pipette is in B2 + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- The number of columns is 3. + +Commands: + +- Open the thermocycler lid. +- Set the thermocycler block temperature to 6C. +- Set the thermocycler lid temperature to 55C. +- For each row in the csv, aspirate 7ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- Using the multi-channel pipette, transfer 5ul of sample from the sample plate to the destination plate, column for column, up to the number of samples specified. +- Close the thermocycler lid. +- Execute the thermocycler with the following profile: + - 74C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume +- Execute the thermocycler with the following profile for 13 cycles: + - 60C for 7 seconds, + - 84C for 19 seconds, + - 57C for 44 seconds, + block max volume is sample and mastermix volume. +- Execute the thermocycler with the following profile: + - 75C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume +- Hold the thermocycler block at 4C. +- Open the thermocycler lid. + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.19' +} + + +def run(protocol: protocol_api.ProtocolContext): + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + num_col = 3 + sample_volume_ul = 5 # in microliters + master_mix_volume_ul = 7 # in microliters + total_mix_volume_ul = sample_volume_ul + sample_volume_ul + + # Thermocycler parameters + lid_temperature_c = 55 # in Celsius + initial_plate_temperature_c = 6 # in Celsius + hold_temperature_c = 4 # in Celsius + + # Module + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + + # Labware setup + mmx_tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'D1') + dest_plate = thermocycler_module.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + + # Tip racks + tiprack_for_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_for_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipettes + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_for_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_for_multi]) + + # Trash + trash = protocol.load_trash_bin("A3") + + # Open lid, set temperatures + thermocycler_module.open_lid() + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + thermocycler_module.set_lid_temperature(lid_temperature_c) + + # Transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(master_mix_volume_ul, mmx_tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # Transfer samples + for s, d in zip(dna_plate.rows()[0][:num_col], dest_plate.rows()[0][:num_col]): + multi_pip.pick_up_tip() + multi_pip.transfer(sample_volume_ul, s, d, new_tip='never') + multi_pip.drop_tip() + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + # Thermo set temperature, open lid + thermocycler_module.set_block_temperature(hold_temperature_c) + thermocycler_module.open_lid() + +``` + + + +## 6. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.15"}` + +Modules: + +- The thermocycler module + +Labware: + +- The source sample labware, an biorad_96_wellplate_200ul_pcr, is placed in slot D3. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an opentrons_96_aluminumblock_nest_wellplate_100ul, is placed in thermocycler. +- A 50uL tip rack for the single channel pipette is in A2 +- A 50uL tip rack for the single channel pipette is in B2 + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- The number of sample columns is 3. +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. + The mastermix volume for each destination well is also provided in each row of the csv, and is in the 3rd column. + +Commands: + +- Open the thermocycler lid. +- Set the thermocycler block temperature to 6C. +- Set the thermocycler lid temperature to 55C. +- For each row in the csv, there is the source tube, destination well, and transfer volume. Transfer the volume specified in the csv (3rd column of csv) of mastermix from the source tube in the tube rack (first column of csv) + to the destination well (second column of csv) in the destination plate. Use one tip per mastermix tube. +- Using the multi-channel pipette, aspirate 5ul of sample from the sample plate to the destination plate, column for column, up to the number of columns specified. +- Close the thermocycler lid. +- Execute the thermocycler with the following profile: + - 74C for 65 seconds for 1 cycle, block max volume is sample and mastermix volume +- Execute the thermocycler with the following profile: + - 60C for 7 seconds, + - 84C for 19 seconds, + - 57C for 44 seconds for 13 cycles, + block max volume is sample and mastermix volume +- Execute the thermocycler with the following profile: + - 75C for 480 seconds for 1 cycle, block max volume is sample and mastermix volume +- Hold the thermocycler block at 4C. +- Open the thermocycler lid. + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.15' +} + + +def run(protocol: protocol_api.ProtocolContext): + + csv_samp = """ + Primer Tube,Destination well,Transfer volume + A1,A1,4 + B1,B1,4 + C1,C1,4 + D1,D1,4 + A2,E1,4 + B2,F1,4 + C2,G1,5 + D2,H1,5 + A3,A2,5 + B3,B2,5 + C3,C2,5 + D3,D2,5 + A4,E2,7 + B4,F2,7 + C4,G2,7 + D4,H2,7 + A5,A3,7 + B5,B3,3 + C5,C3,3 + D5,D3,4 + A6,E3,2 + B6,F3,8 + C6,G3,5 + D6,H3,20 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + num_col = 3 + sample_temperature_c = 4 # Temperature in Celsius + sample_volume_ul = 5 # Volume in microliters + total_mix_volume_ul = 10 + + # Thermocycler parameters + lid_temperature_c = 55 # Celsius + initial_plate_temperature_c = 6 # in Celsius + hold_temperature_c = 4 # in Celsius + + # Module + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + + # Labware setup + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'D3') + dest_plate = thermocycler_module.load_labware('opentrons_96_aluminumblock_nest_wellplate_100ul') + + # Tip racks + tiprack_for_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_for_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', "B2") + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_for_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_for_multi]) + + thermocycler_module.open_lid() + thermocycler_module.set_block_temperature(initial_plate_temperature_c) + thermocycler_module.set_lid_temperature(lid_temperature_c) + for source_tube, dest_well, transfer_vol in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(int(transfer_vol), tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + for s, d in zip(dna_plate.rows()[0][:num_col], dest_plate.rows()[0][:num_col]): + multi_pip.pick_up_tip() + multi_pip.transfer(sample_volume_ul, s, d, new_tip='never') + multi_pip.drop_tip() + + # PCR cycling + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {'temperature': 74, 'hold_time_seconds': 65} + ], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[ + {'temperature': 60, 'hold_time_seconds': 7}, + {'temperature': 84, 'hold_time_seconds': 19}, + {'temperature': 57, 'hold_time_seconds': 44} + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.execute_profile( + steps=[{'temperature': 75, 'hold_time_seconds': 480}], + repetitions=1, + block_max_volume=total_mix_volume_ul + ) + + thermocycler_module.set_block_temperature(hold_temperature_c) + thermocycler_module.open_lid() + +``` + + + +## 7. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.15"}` + +Modules: + +- No modules + +Labware: + +- The source sample labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot D3. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot B1. +- Tiprack 1: opentrons_flex_96_tiprack_50ul is in A2 +- Tiprack 2: opentrons_flex_96_tiprack_50ul is in B2 + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- 3 columns of samples. + +Commands: + +- For each row in the csv, transfer 7ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- For each column in the source plate, we are going to the destination plate in duplicate, changing tips between each column. For example, using the multi-channel pipette, transfer 3ul of sample from the sample plate column 1 to the destination plate plate column 1, change tip, then aspirate from sample plate column 1 to destination plate column 2. Then, transfer 3ul of sample from the sample plate column 2 to the destination plate plate column 3, change tip, then transfer from sample plate column 2 to destination plate column 4. Repeat this pattern for the remainder of the source columns + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.15' +} + +def run(protocol: protocol_api.ProtocolContext): + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to nested list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + NUM_COL = 3 + STRIDE = 2 + + # Load labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D3') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'B1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # Transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # transfer in duplicate + col_ctr = 0 + for s in dna_plate.rows()[0][:NUM_COL]: + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+1], new_tip='never') + multi_pip.drop_tip() + + col_ctr += STRIDE + +``` + + + +## 8. PCR protocol + + +Write a protocol using the Opentrons Python Protocol API v2 for the Flex robot according to the following description: + +Requirements: + +- `requirements = {"robotType": "Flex", "apiLevel": "2.15"}` + +Modules: + +- No modules + +Labware: + +- The source sample labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot D3. +- The source mastermix labware, an opentrons 24 tuberack nest 2ml snap cap, is placed on slot C1. +- The destination labware, an Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt, is placed in slot D1. +- Tiprack in A2: opentrons_flex_96_tiprack_50ul +- Tiprack in B2: opentrons_flex_96_tiprack_50ul + +Pipette Mount: + +- Flex 1-Channel 50 uL Pipette is mounted on the right side +- Flex 8-Channel 50 uL Pipette is mounted on the left side + +Well Allocation: + +- For mastermix, the csv provided has source tube in the first column, and destination well in the second column. +- 3 columns of samples. + +Commands: + +- For each row in the csv, aspirate 7 ul of mastermix from the source tube in the tube rack (left column of csv) to the destination well (right column of csv) in the destination plate. Use one tip per mastermix tube. +- For each column in the source plate, we are going to the destination plate in triplicate, changing tips between each column. For example, using the multi-channel pipette, + transfer 3 ul of sample from the sample plate column 1 to the destination plate plate column 1, change tip, then aspirate from sample plate column 1 to destination plate column 2, change tip, then aspirate form sample plate column 1 to destination plate column 3. Repeat this pattern for the remainder of the source columns + + + + + +```python +from opentrons import protocol_api + +requirements = { + 'robotType': 'Flex', + 'apiLevel': '2.15' +} + + +def run(protocol: protocol_api.ProtocolContext): + + csv_samp = """ + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + A3,A2 + B3,B2 + C3,C2 + D3,D2 + A4,E2 + B4,F2 + C4,G2 + D4,H2 + A5,A3 + B5,B3 + C5,C3 + D5,D3 + A6,E3 + B6,F3 + C6,G3 + D6,H3 + """ + # Convert to list + csv_lines = [[val.strip() for val in line.split(',')] + for line in csv_samp.splitlines() + if line.split(',')[0].strip()][1:] + + NUM_COL = 3 + STRIDE = 3 + + # Load labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dna_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D3') + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 'D1') + + tiprack_single = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'A2') + tiprack_multi = protocol.load_labware('opentrons_flex_96_tiprack_50ul', 'B2') + + # Load pipette + single_pip = protocol.load_instrument("flex_1channel_50", 'right', tip_racks=[tiprack_single]) + multi_pip = protocol.load_instrument("flex_8channel_50", 'left', tip_racks=[tiprack_multi]) + + # transfer mastermix + for source_tube, dest_well in csv_lines: + single_pip.pick_up_tip() + single_pip.transfer(7, tuberack[source_tube], dest_plate[dest_well], new_tip='never') + single_pip.drop_tip() + + # Transfer in triplicate + col_ctr = 0 + for s in dna_plate.rows()[0][:NUM_COL]: + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+1], new_tip='never') + multi_pip.drop_tip() + + multi_pip.pick_up_tip() + multi_pip.transfer(3, s, dest_plate.rows()[0][col_ctr+2], new_tip='never') + multi_pip.drop_tip() + + col_ctr += STRIDE +``` + + diff --git a/opentrons-ai-server/api/storage/docs/serial_dilution_samples.md b/opentrons-ai-server/api/storage/docs/serial_dilution_samples.md new file mode 100644 index 00000000000..ad9a5ee24be --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/serial_dilution_samples.md @@ -0,0 +1,842 @@ +# Serial dilution examples + +## 1. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – Flex 1-channel +- Description: serial dilution + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +Labware: + +- Tiprack: `Opentrons Flex 96 Tip Rack 200 µL` in slot D1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot D2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot D3 +- Trash bin in slot A3 + +Pipette mount: + +- Flex 1-channel 1000 µL pipette is mounted on the left + +Commands: + +1. Use the left-mounted Flex 1-channel 1000 µL pipette to distribute 100 µL of diluent from well A1 of the reservoir to all wells of the plate. +2. For each of the 8 rows in the plate: + a. Transfer 100 µL of solution from well A2 of the reservoir to the first well of the row, mixing 3 times with 50 µL after transfer. + b. Perform a serial dilution by transferring 100 µL from each well to the next well in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + + + + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "Serial Dilution Tutorial – Flex 1-channel", + "description": """serial dilution""", + "author": "New API User" + } + +requirements = { + "robotType": "Flex", + "apiLevel": "2.16" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_flex_96_tiprack_200ul", "D1") + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2") + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "D3") + trash = protocol.load_trash_bin("A3") + left_pipette = protocol.load_instrument("flex_1channel_1000", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 2. Serial dilution example + + + +serial&heater-shaker +serial&heater-shaker +100% +10 +A3 + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – Flex 8-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot D1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot D2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot D3 +- Trash bin in slot A3 + +Pipette mount: + +- Flex 8-channel 1000 µL pipette is mounted on the right + +Commands: + +1. Use the right-mounted Flex 8-channel 1000 µL pipette to distribute 100 µL of diluent from well A1 of the reservoir to the first row of the plate. +2. Transfer 100 µL of solution from well A2 of the reservoir to the first column of the first row in the plate, mixing 3 times with 50 µL after transfer. +3. Perform a serial dilution by transferring 100 µL from each column to the next column in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – Flex 8-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.16"} + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot D1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot D2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot D3 +- Trash bin in slot A3 + +Pipette mount: + +- Flex 8-channel 1000 µL pipette is mounted on the right + +Commands: + +1. Use the right-mounted Flex 8-channel 1000 µL pipette to distribute 100 µL of diluent from well A1 of the reservoir to the first row of the plate. +2. Transfer 100 µL of solution from well A2 of the reservoir to the first column of the first row in the plate, mixing 3 times with 50 µL after transfer. +3. Perform a serial dilution by transferring 100 µL from each column to the next column in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + Turn on screen reader support + To enable screen reader support, press ⌘+Option+Z To learn about keyboard shortcuts, press ⌘slash + + + + +```python +from opentrons import protocol_api + +metadata = { + "protocolName": "Serial Dilution Tutorial – Flex 8-channel", + "description": """This protocol is the outcome of following the + Python Protocol API Tutorial located at + https://docs.opentrons.com/v2/tutorial.html. It takes a + solution and progressively dilutes it by transferring it + stepwise across a plate.""", + "author": "New API User" + } + +requirements = { + "robotType": "Flex", + "apiLevel": "2.16" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", "D1") + reservoir = protocol.load_labware("nest_12_reservoir_15ml", "D2") + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "D3") + trash = protocol.load_trash_bin("A3") + left_pipette = protocol.load_instrument("flex_8channel_1000", "right", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.rows()[0]) + + # no loop, 8-channel pipette + + # save the destination row to a variable + row = plate.rows()[0] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 3. Serial dilution example + + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – OT-2 single-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. +- apiLevel: 2.16 + +Requirements: + +- robotType: OT-2 +- apiLevel: 2.16 + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot 1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot 2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot 3 + +Pipette mount: + +- P300 Single-Channel GEN2 pipette is mounted on the left + +Commands: + +1. Use the left-mounted P300 Single-Channel GEN2 pipette to distribute 100 µL of diluent from well A1 of the reservoir to all wells of the plate. +2. For each of the 8 rows in the plate: + a. Transfer 100 µL of solution from well A2 of the reservoir to the first well of the row, mixing 3 times with 50 µL after transfer. + b. Perform a serial dilution by transferring 100 µL from each well to the next well in the row (from left to right), for a total of 11 transfers. Mix 3 times with 50 µL after each transfer. + + + + + +```python +from opentrons import protocol_api + +metadata = { + "apiLevel": "2.16", + "protocolName": "Serial Dilution Tutorial – OT-2 single-channel", + "description": """This protocol is the outcome of following the + Python Protocol API Tutorial located at + https://docs.opentrons.com/v2/tutorial.html. It takes a + solution and progressively dilutes it by transferring it + stepwise across a plate.""", + "author": "New API User" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", 2) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", 3) + left_pipette = protocol.load_instrument("p300_single_gen2", "left", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.wells()) + + # loop through each row + for i in range(8): + + # save the destination row to a variable + row = plate.rows()[i] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 4. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot for the following description: + +Metadata: + +- Author: New API User +- ProtocolName: Serial Dilution Tutorial – OT-2 8-channel +- Description: This protocol is the outcome of following the Python Protocol API Tutorial located at https://docs.opentrons.com/v2/tutorial.html. It takes a solution and progressively dilutes it by transferring it stepwise across a plate. +- apiLevel: 2.16 + +Requirements: + +- robotType: OT-2 +- apiLevel: 2.16 + +Labware: + +- Tiprack: `Opentrons 96 Tip Rack 300 µL` in slot 1 +- Reservoir: `NEST 12 Well Reservoir 15 mL` in slot 2 +- Plate: `NEST 96 Well Plate 200 µL Flat` in slot 3 + +Pipette mount: + +- P300 8-Channel GEN2 pipette is mounted on the right + +Commands: + +1. Use the right-mounted P300 8-Channel GEN2 pipette to distribute 100 µL of diluent from well A1 of the reservoir to the first row of the plate. +2. Transfer 100 µL of solution from well A2 of the reservoir to the first column of the plate (row A), mixing 3 times with 50 µL after transfer. +3. Perform a serial dilution by transferring 100 µL from each column to the next column in the row (from left to right), for a total of 11 transfers across the plate. Mix 3 times with 50 µL after each transfer. + + + + +```python +from opentrons import protocol_api + +metadata = { + "apiLevel": "2.16", + "protocolName": "Serial Dilution Tutorial – OT-2 8-channel", + "description": """This protocol is the outcome of following the + Python Protocol API Tutorial located at + https://docs.opentrons.com/v2/tutorial.html. It takes a + solution and progressively dilutes it by transferring it + stepwise across a plate.""", + "author": "New API User" + } + +def run(protocol: protocol_api.ProtocolContext): + tips = protocol.load_labware("opentrons_96_tiprack_300ul", 1) + reservoir = protocol.load_labware("nest_12_reservoir_15ml", 2) + plate = protocol.load_labware("nest_96_wellplate_200ul_flat", 3) + left_pipette = protocol.load_instrument("p300_multi_gen2", "right", tip_racks=[tips]) + + # distribute diluent + left_pipette.transfer(100, reservoir["A1"], plate.rows()[0]) + + # no loop, 8-channel pipette + + # save the destination row to a variable + row = plate.rows()[0] + + # transfer solution to first well in column + left_pipette.transfer(100, reservoir["A2"], row[0], mix_after=(3, 50)) + + # dilute the sample down the row + left_pipette.transfer(100, row[:11], row[1:], mix_after=(3, 50)) +``` + + + +## 5. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for OT-2 robot for the following description: + +Metadata: + +- Author: John C. Lynch +- ProtocolName: Serial Dilution for Eskil +- Description: Execute serial dilution protocol +- Source: Custom Protocol Request +- API Level: 2.19 + +Requirements: + +- requirements = {"robotType": "OT-2", "apiLevel": "2.19"} + +Constants: + +- Plate type: Opentrons 96 Aluminum Block NEST Well Plate 100 µL +- Dilution factor: 1.5 +- Number of dilutions: 10 +- Total mixing volume: 150 uL + +Modules: + +- Temperature module loaded in slot 4 + +Labware: + +- NEST 12 Well Reservoir 15 mL in slot 1 +- Dilution plate (Opentrons 96 Aluminum Block NEST Well Plate 100 uL) loaded on temperature module +- Two Opentrons 96 Tip Racks 300 uL in slots 2 and 3 + +Pipette Mount: + +- P300 Multi-Channel GEN2 pipette mounted on the left side + +Calculations: + +- Transfer volume = Total mixing volume / Dilution factor +- Diluent volume = Total mixing volume - Transfer volume + +Commands: + +1. Using the P300 Multi-Channel pipette, transfer diluent from the first well of the reservoir to wells 2-10 of the first row of the dilution plate: + + - Volume: calculated diluent volume + - Use a 10 uL air gap + - Use new tips for each transfer + +2. Perform serial dilutions across the first row of the dilution plate: + + - For each pair of adjacent wells (from well 1 to well 9, transferring to wells 2 to 10): + - Transfer the calculated transfer volume + - Use a 10 uL air gap + - After each transfer, mix 5 times with (Total mixing volume - 5 uL) + - Use new tips for each transfer + +3. Add blank to the last well: + - Transfer calculated diluent volume from the first reservoir well to the last well of the first row in the dilution plate + - Use a 10 uL air gap + - Use a new tip + + + + +```python +metadata = { + 'protocolName': 'Serial Dilution for Eskil', + 'author': 'John C. Lynch', + 'source': 'Custom Protocol Request', + 'apiLevel': '2.19' +} + +def run(protocol): + + # Constants + PLATE_TYPE = 'opentrons_96_aluminumblock_nest_wellplate_100ul' + DILUTION_FACTOR = 1.5 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Load temperature module and labware + temp_module = protocol.load_module('temperature module gen2', '4') + reservoir = protocol.load_labware('nest_12_reservoir_15ml', '1') + dilution_plate = temp_module.load_labware(PLATE_TYPE) + + # Load tipracks + tipracks = [ + protocol.load_labware('opentrons_96_tiprack_300ul', slot) + for slot in ['2', '3'] + ] + + # Load pipette + pipette = protocol.load_instrument( + 'p300_multi_gen2', + mount='left', + tip_racks=tipracks + ) + + # 1. Distribute diluent + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][1:NUM_DILUTIONS], + air_gap=10, + new_tip='always' + ) + + # 2. Perform serial dilutions + sources = dilution_plate.rows()[0][:NUM_DILUTIONS-1] + dests = dilution_plate.rows()[0][1:NUM_DILUTIONS] + + pipette.transfer( + transfer_volume, + sources, + dests, + air_gap=10, + mix_after=(5, TOTAL_MIXING_VOLUME-5), + new_tip='always' + ) + + # 3. Add blank + pipette.transfer( + diluent_volume, + reservoir.wells()[0], + dilution_plate.rows()[0][-1], + air_gap=10, + new_tip='always' + ) +``` + + + +## 6. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: Opentrons +- ProtocolName: Customizable Serial Dilution +- Source: Protocol Library + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Inside the run function: + +Constants: + +- Dilution factor: 3 +- Number of dilutions: 10 +- Total mixing volume: 150.0 µL +- Air gap volume: 10 µL + +Calculations: + +- Transfer volume = Total mixing volume / Dilution factor +- Diluent volume = Total mixing volume - Transfer volume + +Labware: + +- NEST 12 Well Reservoir 15 mL in slot D2 +- NEST 96 Well Plate 200 µL Flat in slot D3 +- Two Opentrons Flex 96 Filter Tip Racks 1000 µL in slots C1 and D1 +- Trash bin in slot A3 + +Pipette Mount: + +- Flex 1-channel 1000 µL pipette mounted on the right side + +Liquid Definitions: + +- Diluent liquid: Green color (#33FF33), loaded in reservoir at 80% max volume +- Sample liquid: Red color (#FF0000), loaded in first column of plate at 150 µL per well + +Commands: + +1. Distribute diluent to dilution plate: + + - Pick up one tip + - Transfer calculated diluent volume from reservoir to all wells in columns 2-11 + - Use 10 µL air gap for each transfer + - Reuse the same tip for all transfers + - Drop tip after completion + +2. Perform serial dilutions: + + - Pick up one tip + - For each well in columns 1-10 (source) to columns 2-11 (destination): + - Transfer calculated transfer volume + - Use 10 µL air gap + - After each transfer, mix 5 times with half the total mixing volume + - Reuse the same tip for all transfers + - Drop tip after completion + +3. Add blank to last column: + - Pick up one tip + - Transfer calculated diluent volume from reservoir to all wells in column 12 + - Use 10 µL air gap + - Reuse the same tip for all transfers + - Drop tip after completion + + + + +```python +metadata = { + 'protocolName': 'Customizable Serial Dilution', + 'author': 'Opentrons ', + 'source': 'Protocol Library' +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.19" +} + +def run(protocol): + + # Constants + DILUTION_FACTOR = 3 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150.0 + AIR_GAP_VOLUME = 10 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Labware setup + trough = protocol.load_labware('nest_12_reservoir_15ml', 'D2') + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D3') + tip_name = "opentrons_flex_96_filtertiprack_1000ul" + tipracks = [ + protocol.load_labware(tip_name, slot) + for slot in ["C1", "D1"] + ] + + # Pipette setup + pipette = protocol.load_instrument('flex_1channel_1000', 'right', tipracks) + + # Waste setup + trash = protocol.load_trash_bin("A3") + + # Reagent setup + diluent = trough.wells()[0] + source = plate.columns()[0] + + # Define and load liquids + diluent_liquid = protocol.define_liquid( + name="Dilutent", + description="Diluent liquid is filled in the reservoir", + display_color="#33FF33" + ) + sample_liquid = protocol.define_liquid( + name="Sample", + description="Non-diluted samples are loaded in the 1st column", + display_color="#FF0000" + ) + + diluent.load_liquid(liquid=diluent_liquid, volume=0.8 * diluent.max_volume) + for well in source: + well.load_liquid(liquid=sample_liquid, volume=TOTAL_MIXING_VOLUME) + + # Set up dilution destinations + dilution_destination_sets = plate.columns()[1:NUM_DILUTIONS+1] + dilution_source_sets = plate.columns()[:NUM_DILUTIONS] + blank_set = plate.columns()[NUM_DILUTIONS+1] + + # 1. Distribute diluent + all_diluent_destinations = [well for wells in dilution_destination_sets for well in wells] + pipette.pick_up_tip() + for dest in all_diluent_destinations: + pipette.transfer( + diluent_volume, + diluent, + dest, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + # 2. Perform serial dilutions + pipette.pick_up_tip() + for source_set, dest_set in zip(dilution_source_sets, dilution_destination_sets): + for s, d in zip(source_set, dest_set): + pipette.transfer( + transfer_volume, + s, + d, + air_gap=AIR_GAP_VOLUME, + mix_after=(5, TOTAL_MIXING_VOLUME/2), + new_tip='never' + ) + pipette.drop_tip() + + # 3. Add blank + pipette.pick_up_tip() + for blank_well in blank_set: + pipette.transfer( + diluent_volume, + diluent, + blank_well, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() +``` + + + +## 7. Serial dilution example + + +Write a protocol using the Opentrons Python Protocol API v2 for Flex robot for the following description: + +Metadata: + +- Author: Opentrons +- ProtocolName: Customizable Serial Dilution +- Source: Protocol Library + +Requirements: + +- requirements = {"robotType": "Flex", "apiLevel": "2.19"} + +Inside the run function: + +Constants: + +- Dilution factor: 3 +- Number of dilutions: 10 +- Total mixing volume: 150.0 µL +- Air gap volume: 10 µL + +Calculations: + +- Transfer volume = Total mixing volume / Dilution factor +- Diluent volume = Total mixing volume - Transfer volume + +Labware: + +- NEST 12 Well Reservoir 15 mL in slot D2 +- NEST 96 Well Plate 200 µL Flat in slot D3 +- Two Opentrons Flex 96 Filter Tip Racks 1000 µL in slots C1 and D1 +- Trash bin in slot A3 + +Pipette Mount: + +- Flex 8-channel 1000 µL pipette mounted on the right side + +Liquid Definitions: + +- Diluent liquid: Green color (#33FF33), loaded in reservoir at 80% max volume +- Sample liquid: Red color (#FF0000), loaded in first column of plate at 150 µL per well + +Commands: + +1. Distribute diluent to dilution plate: + + - Pick up one tip with 8-channel pipette + - Transfer calculated diluent volume from reservoir to wells 2-11 in row A + - Use 10 µL air gap for each transfer + - Reuse the same tip for all transfers + - Drop tip after completion + +2. Perform serial dilutions along row A: + + - Pick up one tip with 8-channel pipette + - For each well pair from A1 to A10 (source) to A2 to A11 (destination): + - Transfer calculated transfer volume + - Use 10 µL air gap + - After each transfer, mix 5 times with half the total mixing volume + - Reuse the same tip for all transfers + - Drop tip after completion + +3. Add blank to well A12: + - Pick up one tip with 8-channel pipette + - Transfer calculated diluent volume from reservoir to well A12 + - Use 10 µL air gap + - Reuse the same tip + - Drop tip after completion + + + + +```python +metadata = { + 'protocolName': 'Customizable Serial Dilution', + 'author': 'Opentrons ', + 'source': 'Protocol Library' +} + +requirements = { + "robotType": "Flex", + "apiLevel": "2.19" +} + +def run(protocol): + + # Constants + DILUTION_FACTOR = 3 + NUM_DILUTIONS = 10 + TOTAL_MIXING_VOLUME = 150.0 + AIR_GAP_VOLUME = 10 + + # Calculated volumes + transfer_volume = TOTAL_MIXING_VOLUME / DILUTION_FACTOR + diluent_volume = TOTAL_MIXING_VOLUME - transfer_volume + + # Labware setup + trough = protocol.load_labware('nest_12_reservoir_15ml', 'D2') + plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D3') + tip_name = "opentrons_flex_96_filtertiprack_1000ul" + tipracks = [ + protocol.load_labware(tip_name, slot) + for slot in ["C1", "D1"] + ] + + # Pipette setup + pipette = protocol.load_instrument('flex_8channel_1000', 'right', tipracks) + + # Waste setup + trash = protocol.load_trash_bin("A3") + + # Reagent setup + diluent = trough.wells()[0] + source = plate.columns()[0] + + # Define and load liquids + diluent_liquid = protocol.define_liquid( + name="Dilutent", + description="Diluent liquid is filled in the reservoir", + display_color="#33FF33" + ) + sample_liquid = protocol.define_liquid( + name="Sample", + description="Non-diluted samples are loaded in the 1st column", + display_color="#FF0000" + ) + + diluent.load_liquid(liquid=diluent_liquid, volume=0.8 * diluent.max_volume) + for well in source: + well.load_liquid(liquid=sample_liquid, volume=TOTAL_MIXING_VOLUME) + + # Set up dilution destinations + dilution_destination_sets = [[row] for row in plate.rows()[0][1:NUM_DILUTIONS+1]] + dilution_source_sets = [[row] for row in plate.rows()[0][:NUM_DILUTIONS]] + blank_set = [plate.rows()[0][NUM_DILUTIONS+1]] + + # 1. Distribute diluent + all_diluent_destinations = [well for wells in dilution_destination_sets for well in wells] + pipette.pick_up_tip() + for dest in all_diluent_destinations: + pipette.transfer( + diluent_volume, + diluent, + dest, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() + + # 2. Perform serial dilutions + pipette.pick_up_tip() + for source_set, dest_set in zip(dilution_source_sets, dilution_destination_sets): + for s, d in zip(source_set, dest_set): + pipette.transfer( + transfer_volume, + s, + d, + air_gap=AIR_GAP_VOLUME, + mix_after=(5, TOTAL_MIXING_VOLUME/2), + new_tip='never' + ) + pipette.drop_tip() + + # 3. Add blank + pipette.pick_up_tip() + for blank_well in blank_set: + pipette.transfer( + diluent_volume, + diluent, + blank_well, + air_gap=AIR_GAP_VOLUME, + new_tip='never' + ) + pipette.drop_tip() +``` + + diff --git a/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md b/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md new file mode 100644 index 00000000000..f4b54d4308a --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md @@ -0,0 +1,157 @@ +Standard API + +### Approved Pipette Loadnames + +Note that the labware names are hard to differentiate sometimes, +since there are cases that they differ in terms of last digits only. + +#### OT-2 Approved Loadnames + +For OT-2 robots, use the following approved loadnames: + +- p20_single_gen2 +- p300_single_gen2 +- p1000_single_gen2 +- p300_multi_gen2 +- p20_multi_gen2 + +#### Flex Approved Loadnames + +For Flex robots, use these approved loadnames: + +- flex_1channel_50 +- flex_1channel_1000 +- flex_8channel_50 +- flex_8channel_1000 +- flex_96channel_1000 + +### Agilent Labware + +- Agilent 1 Well Reservoir 290 mL: agilent_1_reservoir_290ml + +### Applied Biosystems Labware + +- Applied Biosystems MicroAmp 384 Well Plate 40 uL: appliedbiosystemsmicroamp_384_wellplate_40ul + +### Axygen Labware + +- Axygen 1 Well Reservoir 90 mL: axygen_1_reservoir_90ml + +### Bio-Rad Labware + +- Bio-Rad 384 Well Plate 50 uL: biorad_384_wellplate_50ul +- Bio-Rad 96 Well Plate 200 uL PCR: biorad_96_wellplate_200ul_pcr + +### Corning Labware + +- Corning 12 Well Plate 6.9 mL Flat: corning_12_wellplate_6.9ml_flat +- Corning 24 Well Plate 3.4 mL Flat: corning_24_wellplate_3.4ml_flat +- Corning 384 Well Plate 112 uL Flat: corning_384_wellplate_112ul_flat +- Corning 48 Well Plate 1.6 mL Flat: corning_48_wellplate_1.6ml_flat +- Corning 6 Well Plate 16.8 mL Flat: corning_6_wellplate_16.8ml_flat +- Corning 96 Well Plate 360 uL Flat: corning_96_wellplate_360ul_flat + +### GEB Labware + +- GEB 96 Tip Rack 1000 uL: geb_96_tiprack_1000ul +- GEB 96 Tip Rack 10 uL: geb_96_tiprack_10ul + +### NEST Labware + +- NEST 12 Well Reservoir 15 mL: nest_12_reservoir_15ml +- NEST 1 Well Reservoir 195 mL: nest_1_reservoir_195ml +- NEST 1 Well Reservoir 290 mL: nest_1_reservoir_290ml +- NEST 96 Well Plate 100 uL PCR Full Skirt: nest_96_wellplate_100ul_pcr_full_skirt +- NEST 96 Well Plate 200 uL Flat: nest_96_wellplate_200ul_flat +- NEST 96 Deep Well Plate 2mL: nest_96_wellplate_2ml_deep + +### Opentrons Labware + +- Opentrons 10 Tube Rack with Falcon 4x50 mL, 6x15 mL Conical: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical +- Opentrons 10 Tube Rack with NEST 4x50 mL, 6x15 mL Conical: opentrons_10_tuberack_nest_4x50ml_6x15ml_conical +- Opentrons 15 Tube Rack with Falcon 15 mL Conical: opentrons_15_tuberack_falcon_15ml_conical +- Opentrons 15 Tube Rack with NEST 15 mL Conical: opentrons_15_tuberack_nest_15ml_conical +- Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap: opentrons_24_aluminumblock_generic_2ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap: opentrons_24_aluminumblock_nest_0.5ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 1.5 mL Screwcap: opentrons_24_aluminumblock_nest_1.5ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap: opentrons_24_aluminumblock_nest_1.5ml_snapcap +- Opentrons 24 Well Aluminum Block with NEST 2 mL Screwcap: opentrons_24_aluminumblock_nest_2ml_screwcap +- Opentrons 24 Well Aluminum Block with NEST 2 mL Snapcap: opentrons_24_aluminumblock_nest_2ml_snapcap +- Opentrons 24 Tube Rack with Eppendorf 1.5 mL Safe-Lock Snapcap: opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap +- Opentrons 24 Tube Rack with Eppendorf 2 mL Safe-Lock Snapcap: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap +- Opentrons 24 Tube Rack with Generic 2 mL Screwcap: opentrons_24_tuberack_generic_2ml_screwcap +- Opentrons 24 Tube Rack with NEST 0.5 mL Screwcap: opentrons_24_tuberack_nest_0.5ml_screwcap # not opentrons_24_tuberack_nest_0_5ml_screwcap +- Opentrons 24 Tube Rack with NEST 1.5 mL Screwcap: opentrons_24_tuberack_nest_1.5ml_screwcap # not opentrons_24_tuberack_nest_1_5ml_screwcap +- Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap: opentrons_24_tuberack_nest_1.5ml_snapcap # note the use of dot. (`.`); opentrons_24_tuberack_nest_1_5ml_snapcap is incorrect +- Opentrons 24 Tube Rack with NEST 2 mL Screwcap: opentrons_24_tuberack_nest_2ml_screwcap +- Opentrons 24 Tube Rack with NEST 2 mL Snapcap: opentrons_24_tuberack_nest_2ml_snapcap +- Opentrons 6 Tube Rack with Falcon 50 mL Conical: opentrons_6_tuberack_falcon_50ml_conical +- Opentrons 6 Tube Rack with NEST 50 mL Conical: opentrons_6_tuberack_nest_50ml_conical +- Opentrons 96 Well Aluminum Block with Bio-Rad Well Plate 200 uL: opentrons_96_aluminumblock_biorad_wellplate_200ul +- Opentrons 96 Well Aluminum Block with Generic PCR Strip 200 uL: opentrons_96_aluminumblock_generic_pcr_strip_200ul +- Opentrons 96 Well Aluminum Block with NEST Well Plate 100 uL: opentrons_96_aluminumblock_nest_wellplate_100ul +- Opentrons 96 Deep Well Heater-Shaker Adapter: opentrons_96_deep_well_adapter +- Opentrons 96 Deep Well Heater-Shaker Adapter with NEST Deep Well Plate 2 mL: opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep +- Opentrons OT-2 96 Filter Tip Rack 1000 uL: opentrons_96_filtertiprack_1000ul +- Opentrons OT-2 96 Filter Tip Rack 10 uL: opentrons_96_filtertiprack_10ul +- Opentrons OT-2 96 Filter Tip Rack 200 uL: opentrons_96_filtertiprack_200ul +- Opentrons OT-2 96 Filter Tip Rack 20 uL: opentrons_96_filtertiprack_20ul +- Opentrons 96 Flat Bottom Heater-Shaker Adapter: opentrons_96_flat_bottom_adapter +- Opentrons 96 Flat Bottom Heater-Shaker Adapter with NEST 96 Well Plate 200 uL Flat: opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat +- Opentrons 96 PCR Heater-Shaker Adapter: opentrons_96_pcr_adapter +- Opentrons 96 PCR Heater-Shaker Adapter with NEST Well Plate 100 ul: opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt +- Opentrons OT-2 96 Tip Rack 1000 uL: opentrons_96_tiprack_1000ul +- Opentrons OT-2 96 Tip Rack 10 uL: opentrons_96_tiprack_10ul +- Opentrons OT-2 96 Tip Rack 20 uL: opentrons_96_tiprack_20ul +- Opentrons OT-2 96 Tip Rack 300 uL: opentrons_96_tiprack_300ul +- Opentrons 96 Well Aluminum Block: opentrons_96_well_aluminum_block +- Opentrons 96 Well Aluminum Block adapter: opentrons_96_well_aluminum_block +- Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt: opentrons_96_wellplate_200ul_pcr_full_skirt +- Opentrons Aluminum Flat Bottom Plate: opentrons_aluminum_flat_bottom_plate +- Opentrons Flex 96 Filter Tip Rack 1000 uL: opentrons_flex_96_filtertiprack_1000ul # note that 1000ul not 200ul +- Opentrons Flex 96 Filter Tip Rack 200 uL: opentrons_flex_96_filtertiprack_200ul # note that 200ul not 1000ul +- Opentrons Flex 96 Filter Tip Rack 50 uL: opentrons_flex_96_filtertiprack_50ul +- Opentrons Flex 96 Tip Rack 1000 uL: opentrons_flex_96_tiprack_1000ul +- Opentrons Flex 96 Tip Rack 200 uL: opentrons_flex_96_tiprack_200ul +- Opentrons Flex 96 Tip Rack 50 uL: opentrons_flex_96_tiprack_50ul +- Opentrons Flex 96 Tip Rack Adapter: opentrons_flex_96_tiprack_adapter +- Opentrons Universal Flat Heater-Shaker Adapter: opentrons_universal_flat_adapter +- Opentrons Universal Flat Heater-Shaker Adapter with Corning 384 Well Plate 112 ul Flat: opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat + +### Other Labware Brands + +- Thermo Scientific Nunc 96 Well Plate 1300 uL: thermoscientificnunc_96_wellplate_1300ul +- Thermo Scientific Nunc 96 Well Plate 2000 uL: thermoscientificnunc_96_wellplate_2000ul +- USA Scientific 12 Well Reservoir 22 mL: usascientific_12_reservoir_22ml +- USA Scientific 96 Deep Well Plate 2.4 mL: usascientific_96_wellplate_2.4ml_deep + +### Additional Opentrons Tube Racks + +- 4-in-1 Tube Rack Set 15: opentrons_15_tuberack_nest_15ml_conical +- 4-in-1 Tube Rack Set 50: opentrons_6_tuberack_nest_50ml_conical + +### Flex Pipettes + +- Flex 1-Channel 50 uL Pipette (single channel): flex_1channel_50 +- Flex 1-Channel 1000 uL Pipette (single channel): flex_1channel_1000 +- Flex 8-Channel 50 uL Pipette (multi-channel): flex_8channel_50 +- Flex 8-Channel 1000 uL Pipette (multi-channel): flex_8channel_1000 +- Flex 96-Channel 1000 uL Pipette (multi-channel): flex_96channel_1000 + +### Modules + +- temperature module: temperature module gen2 +- thermocycler module: thermocycler module +- thermocycler module gen2: thermocyclerModuleV2 + +### Single channel pipettes: + +- Flex 1-Channel 50 uL Pipette +- Flex 1-Channel 1000 uL Pipette +- flex_1channel_1000 + +### Multi channel pipettes: + +- Flex 8-Channel 50 uL Pipette +- Flex 8-Channel 1000 uL Pipette +- Flex 96-Channel 1000 uL Pipette diff --git a/opentrons-ai-server/api/storage/docs/transfer_function_notes.md b/opentrons-ai-server/api/storage/docs/transfer_function_notes.md new file mode 100644 index 00000000000..dcf4f315545 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/transfer_function_notes.md @@ -0,0 +1,651 @@ + + +**Introduction** + +The `transfer` function in the Opentrons API v2 simplifies liquid handling operations by abstracting the complexities involved in pipetting tasks. It allows users to perform liquid transfers efficiently without the need to write explicit loops for iterating over wells or volumes. This manual provides a comprehensive guide on using the `transfer` function effectively, including best practices, common pitfalls, and practical examples. This updated version incorporates additional examples and clarifications based on real-world protocols, including the use of modules and dynamic data-driven transfers. + +--- + +**Basic Usage of `transfer`** + +The `transfer` function enables the movement of liquids from one location to another with optional parameters to control tip usage, mixing, air gaps, and more. Its basic syntax is: + +```python +pipette.transfer( + volume, # Volume to transfer (single value or list) + source, # Source well(s) + destination, # Destination well(s) + new_tip='always' # Tip usage strategy ('always', 'once', or 'never') + # Additional optional parameters... +) +``` + +- **Volume**: The amount of liquid to transfer, specified in microliters (µL). It can be a single value or a list of volumes. +- **Source**: The starting location(s) of the liquid, specified as a well or a list of wells. +- **Destination**: The target location(s) for the liquid, specified as a well or a list of wells. +- **`new_tip`**: Controls how tips are used during the transfer: + - `'always'`: Change tips between each transfer step. + - `'once'`: Use the same tip for all transfers. + - `'never'`: Do not change tips (use with caution). + +--- + +**Understanding Pipette Types** + +Choosing the correct method for accessing wells or columns depends on the type of pipette used. + +### Single-Channel Pipettes + +Single-channel pipettes interact with individual wells. When using single-channel pipettes, access wells using the `wells()` method. + +**Example:** + +```python +source_wells = source_labware.wells()[:number_of_samples] +``` + +### Multi-Channel Pipettes + +Multi-channel pipettes interact with rows or columns simultaneously. When using multi-channel pipettes, access columns using the `columns()` method. + +**Example:** + +```python +import math + +number_of_samples = 48 +number_of_columns = math.ceil(number_of_samples / 8) +source_columns = source_labware.columns()[:number_of_columns] +``` + +--- + +**Well Selection Methods** + +Accurate well selection is crucial for successful liquid transfers. + +### Accessing Wells + +- **Access all wells**: + + ```python + all_wells = labware.wells() + ``` + +- **Access specific wells by name**: + + ```python + well_a1 = labware.wells_by_name()['A1'] + ``` + +- **Access a list of wells by name**: + + ```python + specific_wells = [labware.wells_by_name()[well] for well in ['A1', 'B2', 'C3']] + ``` + +### Accessing Columns + +- **Access all columns**: + + ```python + all_columns = labware.columns() + ``` + +- **Access specific columns by index (0-based)**: + + ```python + first_three_columns = labware.columns()[:3] + ``` + +- **Access columns by name (1-based)**: + + ```python + column_one = labware.columns_by_name()['1'] + ``` + +- **Access multiple columns by name**: + + ```python + specific_columns = [labware.columns_by_name()[idx] for idx in ['1', '3', '5']] + ``` + +### Accessing Rows + +- **Access all rows**: + + ```python + all_rows = labware.rows() + ``` + +- **Access specific rows by name**: + + ```python + row_a = labware.rows_by_name()['A'] + ``` + +--- + +**Handling the `new_tip` Parameter** + +The `new_tip` parameter controls tip usage during transfers. + +- **`new_tip='always'`**: Use a new tip for each transfer. This is appropriate when avoiding cross-contamination is critical. + +- **`new_tip='once'`**: Use the same tip for all transfers in the `transfer` function call. Use this when transferring from a single source to multiple destinations and cross-contamination is not a concern. + +- **`new_tip='never'`**: Never change tips during the transfer. Use with caution, ensuring that cross-contamination will not occur. + +**Important Note:** Do not use `new_tip='once'` inside a loop; instead, pass lists of wells to the `transfer` function and let it handle the iteration. + +--- + +**Avoiding Unnecessary Loops** + +**Incorrect Usage:** + +```python +for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='always') +``` + +**Issue:** This approach unnecessarily calls the `transfer` method multiple times and can lead to inefficiencies or errors. + +**Correct Usage:** + +```python +pipette.transfer(volume, source_wells, destination_wells, new_tip='always') +``` + +**Explanation:** The `transfer` function can handle lists of sources and destinations, automatically pairing them and iterating over them. + +--- + +**Proper Use of `new_tip`** + +**Incorrect Usage:** + +Using `new_tip='once'` inside a loop when intending to reuse the same tip. + +```python +for src, dest in zip(source_wells, destination_wells): + pipette.transfer(volume, src, dest, new_tip='once') +``` + +**Correct Usage:** + +```python +pipette.transfer(volume, source_wells, destination_wells, new_tip='once') +``` + +**Explanation:** When `new_tip='once'`, the pipette picks up a tip at the beginning of the transfer and uses it throughout. Using it inside a loop can cause the pipette to attempt to pick up a tip that is already in use, leading to errors. + +--- + +**Preventing "Out of Tips" Errors** + +- **Tip Rack Capacity:** Be mindful of the number of tips available in your tip racks. For example, a standard 96-tip rack cannot provide more than 96 tips. + +- **Calculating Tip Usage:** Estimate the number of tips required based on the `new_tip` parameter and the number of transfers. + +- **Loading Additional Tip Racks:** If your protocol requires more tips than are available in a single rack, load additional tip racks. + +**Example:** + +```python +tiprack1 = protocol.load_labware('opentrons_96_tiprack_300ul', 2) +tiprack2 = protocol.load_labware('opentrons_96_tiprack_300ul', 3) +pipette = protocol.load_instrument('p300_single_gen2', 'left', tip_racks=[tiprack1, tiprack2]) +``` + +--- + +**Index Errors** + +- **Labware Dimensions:** Ensure that your loops do not exceed the dimensions of the labware (e.g., a 96-well plate has 12 columns and 8 rows). + +- **Valid Indices:** Adjust loop ranges to stay within valid indices. + +**Incorrect Usage:** + +```python +for i in range(13): # Exceeds available columns (0-11) + pipette.transfer(volume, source_columns[i], dest_columns[i]) +``` + +**Correct Usage:** + +```python +for i in range(12): # Valid column indices for a 96-well plate + pipette.transfer(volume, source_columns[i], dest_columns[i]) +``` + +--- + +**Calculating Tip Usage** + +- **Estimate in Advance:** Before running the protocol, calculate the number of tips required based on the number of transfers and the `new_tip` parameter. + +- **Account for Pipette Type:** Remember that multi-channel pipettes use multiple tips per pick-up (e.g., an 8-channel pipette uses 8 tips per pick-up). + +- **Example Calculation:** + + If you are transferring samples to 96 wells using a single-channel pipette with `new_tip='always'`, you will need 96 tips. If you are using a multi-channel pipette (8-channel) to transfer to 12 columns, you will need 12 tip pickups (12 columns x 8 tips per pickup = 96 tips). + +--- + +**Optimizing Transfers** + +- **Use Lists in `transfer`:** Provide lists of source and destination wells to the `transfer` function to leverage its built-in iteration. + +- **Minimize Tip Usage:** When appropriate, reuse tips by setting `new_tip='once'` to conserve tips and reduce waste. + +- **Avoid Unnecessary Loops:** Let the `transfer` function handle iteration over wells and volumes. + +--- + +**Efficient Labware Access** + +- **Match Pipette Type to Access Method:** Use `wells()` for single-channel pipettes and `columns()` for multi-channel pipettes. + +- **Use Labware Methods Correctly:** Ensure you are accessing wells and columns using the correct methods to prevent errors. + +--- + +**Example 1: Single Source to Multiple Destinations** + +**Task:** Transfer 1 µL of reagent from tube A1 in the source rack to all wells in the destination plate using the same tip. + +**Protocol:** + +```python +def run(protocol): + # Labware + tiprack = protocol.load_labware('opentrons_96_tiprack_20ul', 2) + source_rack = protocol.load_labware('opentrons_24_tuberack_nest_1.5ml_snapcap', 3) + dest_plate = protocol.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt', 9) + + # Pipette + p20 = protocol.load_instrument('p20_single_gen2', mount='right', tip_racks=[tiprack]) + + # Wells + src_well = source_rack.wells_by_name()['A1'] + dest_wells = dest_plate.wells() + + # Transfer + p20.transfer(1, src_well, dest_wells, new_tip='once') +``` + +--- + +**Example 2: Well-to-Well Transfers with Reused Tips** + +**Task:** Transfer 50 µL from wells A1 and A2 in source labware 1 to wells B6 and B7 in source labware 2 using the same tip. + +**Protocol:** + +```python +def run(protocol): + # Labware + source_labware_1 = protocol.load_labware('source_labware_1_definition', 1) + source_labware_2 = protocol.load_labware('source_labware_2_definition', 2) + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 3) + + # Pipette + p300 = protocol.load_instrument('p300_single_gen2', mount='left', tip_racks=[tiprack]) + + # Wells + source_wells = [source_labware_1.wells_by_name()[well] for well in ['A1', 'A2']] + destination_wells = [source_labware_2.wells_by_name()[well] for well in ['B6', 'B7']] + + # Transfer + p300.transfer(50, source_wells, destination_wells, new_tip='once') +``` + +--- + +**Example 3: Column-wise Transfers with Multi-Channel Pipette** + +**Task:** Using a P300 Multi-Channel pipette, transfer 55 µL of sample from each column of the source plate into the corresponding columns of the destination plate, changing tips between each transfer. + +**Protocol:** + +```python +def run(protocol): + # Labware + source_plate = protocol.load_labware('source_plate_definition', 1) + destination_plate = protocol.load_labware('destination_plate_definition', 2) + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', 3) + + # Pipette + p300_multi = protocol.load_instrument('p300_multi_gen2', mount='left', tip_racks=[tiprack]) + + # Columns + src_cols = source_plate.columns() + dest_cols = destination_plate.columns() + + # Transfer + p300_multi.transfer(55, src_cols, dest_cols, new_tip='always') +``` + +--- + +**Example 4: Complex Transfers with Different Pipettes** + +**Task:** Transfer 15 µL from wells C4 and C6 in source labware 2 to wells A3 and A4 in source labware 1 using the same tip. + +**Protocol:** + +```python +def run(protocol): + # Labware + source_1 = protocol.load_labware('source_labware_1_definition', 1) + source_2 = protocol.load_labware('source_labware_2_definition', 2) + tiprack = protocol.load_labware('opentrons_96_tiprack_20ul', 3) + + # Pipette + p20 = protocol.load_instrument('p20_single_gen2', mount='right', tip_racks=[tiprack]) + + # Wells + src_wells = [source_2.wells_by_name()[well] for well in ['C4', 'C6']] + dest_wells = [source_1.wells_by_name()[well] for well in ['A3', 'A4']] + + # Transfer + p20.transfer(15, src_wells, dest_wells, new_tip='once') +``` + +--- + +**Example 5: Transfers Involving Modules** + +**Task:** Perform transfers involving thermocycler and temperature modules, handling temperature settings and PCR amplification steps. + +**Protocol:** + +```python +def run(protocol): + import math + + # Sample preparation parameters + number_of_samples = 64 + sample_volume_ul = 5 + master_mix_volume_ul = 7 + mixing_cycles = 9 + total_mix_volume_ul = sample_volume_ul + master_mix_volume_ul + + # Modules + thermocycler_module = protocol.load_module('thermocyclerModuleV2') + sample_temp_module = protocol.load_module('temperature module gen2', 1) + master_mix_temp_module = protocol.load_module('temperature module gen2', 3) + + # Labware + tips_20ul = protocol.load_labware('opentrons_96_filtertiprack_20ul', 4) + pcr_plate = thermocycler_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + sample_plate = sample_temp_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + master_mix_plate = master_mix_temp_module.load_labware('opentrons_96_wellplate_200ul_pcr_full_skirt') + + # Pipette + p20_multi = protocol.load_instrument('p20_multi_gen2', 'left', tip_racks=[tips_20ul]) + + # Well allocation + number_of_columns = math.ceil(number_of_samples / 8) + sample_source_wells = sample_plate.columns()[:number_of_columns] + sample_destination_wells = pcr_plate.columns()[:number_of_columns] + master_mix_source_wells = master_mix_plate.columns()[:number_of_columns] + master_mix_destination_wells = pcr_plate.columns()[:number_of_columns] + + # Set temperatures + sample_temp_module.set_temperature(4) + master_mix_temp_module.set_temperature(10) + + # Transfer master mix + p20_multi.transfer( + master_mix_volume_ul, + master_mix_source_wells, + master_mix_destination_wells, + new_tip='once' + ) + + # Transfer samples and mix + p20_multi.transfer( + sample_volume_ul, + sample_source_wells, + sample_destination_wells, + new_tip='always', + mix_after=(mixing_cycles, total_mix_volume_ul), + blow_out=True, + blowout_location='destination well' + ) + + # PCR cycling steps (simplified for brevity) + thermocycler_module.close_lid() + thermocycler_module.execute_profile( + steps=[ + {{'temperature': 74, 'hold_time_seconds': 65}}, + {{'temperature': 60, 'hold_time_seconds': 7}}, + {{'temperature': 84, 'hold_time_seconds': 19}}, + {{'temperature': 57, 'hold_time_seconds': 44}} + ], + repetitions=13, + block_max_volume=total_mix_volume_ul + ) + thermocycler_module.open_lid() + + # Deactivate modules + master_mix_temp_module.deactivate() + sample_temp_module.deactivate() +``` + +--- + +**Example 6: Dynamic Transfers Using CSV Data** + +**Task:** Perform transfers based on data provided in a CSV file, without using the thermocycler. + +**Protocol:** + +```python +def run(protocol): + # CSV data as a multi-line string + csv_data = ''' + Primer Tube,Destination well + A1,A1 + B1,B1 + C1,C1 + D1,D1 + A2,E1 + B2,F1 + C2,G1 + D2,H1 + ''' + + # Parse CSV data + csv_lines = [line.strip().split(',') for line in csv_data.strip().splitlines() if line.strip()] + headers = csv_lines[0] + data = csv_lines[1:] + + # Labware + tuberack = protocol.load_labware('opentrons_24_tuberack_nest_2ml_snapcap', 'C1') + dest_plate = protocol.load_labware('biorad_96_wellplate_200ul_pcr', 'B1') + tiprack_single = [protocol.load_labware('opentrons_96_tiprack_50ul', slot) for slot in ['A1']] + p50 = protocol.load_instrument('p50_single', 'right', tip_racks=tiprack_single) + + # Transfers based on CSV data + for row in data: + source_tube = row[0] + dest_well = row[1] + p50.transfer(7, tuberack.wells_by_name()[source_tube], dest_plate.wells_by_name()[dest_well], new_tip='always') +``` + +--- + +**Additional Examples** + +**Example 7: Transfer with Heater-Shaker Module** + +**Task:** Transfer liquids between a reservoir, a PCR plate, and a heater-shaker module's plate, including shaking the plate. + +**Protocol:** + +```python +def run(protocol): + # Modules + heater_shaker_module = protocol.load_module('heaterShakerModuleV1', 'D1') + heater_shaker_plate = heater_shaker_module.load_labware('corning_96_wellplate_360ul_flat') + + # Labware + reservoir = protocol.load_labware('nest_1_reservoir_195ml', 'C1') + pcr_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', 'D2') + tiprack_200ul = protocol.load_labware('opentrons_96_tiprack_200ul', 'A2') + tiprack_50ul = protocol.load_labware('opentrons_96_tiprack_50ul', 'C2') + + # Pipette + pipette = protocol.load_instrument('p300_multi', mount='left', tip_racks=[tiprack_200ul, tiprack_50ul]) + + # Steps + heater_shaker_module.open_labware_latch() + protocol.pause("Please place the plate on the Heater-Shaker Module.") + heater_shaker_module.close_labware_latch() + + # Transfer 70 µL from reservoir to heater-shaker plate + pipette.transfer(70, reservoir['A1'], heater_shaker_plate['A1'], new_tip='always') + + # Transfer 10 µL from PCR plate to heater-shaker plate + pipette.transfer(10, pcr_plate['A1'], heater_shaker_plate['A1'], new_tip='always') + + # Shake the plate + heater_shaker_module.set_and_wait_for_shake_speed(rpm=2000) + protocol.delay(minutes=1) + heater_shaker_module.deactivate_shaker() +``` + +--- + +**Advanced Usage** + +Advanced features of the `transfer` function include specifying aspiration and dispense locations, mixing, air gaps, blow out, and using modules with `transfer`. + +--- + +**Specifying Aspiration and Dispense Locations** + +You can specify precise locations within wells for aspiration and dispensing. + +**Example:** + +```python +pipette.transfer( + 20, + source_well.bottom(3), # 3 mm above the bottom + destination_well.top(-7), # 7 mm below the top + new_tip='once' +) +``` + +--- + +**Using Mix After/Before** + +Mixing can be performed before or after the transfer. + +**Example:** + +```python +pipette.transfer( + 10, + source_well, + destination_well, + mix_after=(5, 10) # Mix 5 times with a volume of 10 µL after dispensing +) +``` + +--- + +**Handling Air Gaps and Blow Out** + +Air gaps and blow-out can prevent dripping and ensure complete dispensing. + +**Example:** + +```python +pipette.transfer( + 10, + source_well, + destination_well, + air_gap=5, # Add a 5 µL air gap after aspiration + blow_out=True, + blowout_location='destination well' +) +``` + +--- + +**Using Modules with `transfer`** + +The `transfer` function can be used effectively with various modules like the thermocycler, temperature modules, and heater-shaker modules. When using modules: + +- **Set Module Temperatures Before Transfers:** Ensure that temperature modules are set to the desired temperature before performing transfers. + +- **Load Labware on Modules:** Use the module's `load_labware` or `load_adapter` method to place labware on the module. + +**Example:** + +```python +# Load modules +temp_module = protocol.load_module('temperature module gen2', '1') +thermocycler_module = protocol.load_module('thermocyclerModuleV2') + +# Load labware on modules +temp_plate = temp_module.load_labware('opentrons_96_aluminumblock_biorad_wellplate_200ul') +pcr_plate = thermocycler_module.load_labware('nest_96_wellplate_100ul_pcr_full_skirt') + +# Set temperatures +temp_module.set_temperature(4) +thermocycler_module.set_block_temperature(95) +``` + +--- + +**Dynamic Transfers Based on Data** + +For protocols that require dynamic transfers based on external data (e.g., CSV files), you can parse the data and use it to control the `transfer` function. + +- **Parsing CSV Data:** Use Python's built-in functions or the `csv` module to read and parse CSV data. + +- **Using Parsed Data in Transfers:** Use the parsed data to define source wells, destination wells, and volumes. + +**Example:** + +```python +import csv +from io import StringIO + +def run(protocol): + # CSV data as a string + csv_data = ''' + Source Well,Destination Well,Volume + A1,B1,50 + A2,B2,100 + A3,B3,150 + ''' + + # Parse CSV data + reader = csv.DictReader(StringIO(csv_data.strip())) + transfers = list(reader) + + # Labware + source_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', '1') + dest_plate = protocol.load_labware('nest_96_wellplate_200ul_flat', '2') + tiprack = protocol.load_labware('opentrons_96_tiprack_300ul', '3') + pipette = protocol.load_instrument('p300_single', 'left', tip_racks=[tiprack]) + + # Perform transfers based on CSV data + for transfer in transfers: + source_well = source_plate.wells_by_name()[transfer['Source Well']] + dest_well = dest_plate.wells_by_name()[transfer['Destination Well']] + volume = float(transfer['Volume']) + pipette.transfer(volume, source_well, dest_well, new_tip='always') + +``` + + From 54950a312cea62802d93edea665b42ae3b9c67b9 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 19 Nov 2024 16:01:26 -0500 Subject: [PATCH 11/52] (protocol-designer) fix liquid long name display issue in slot detail (#16891) * (protocol-designer) fix liquid long name display issue in slot detail --- protocol-designer/src/organisms/SlotInformation/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index 0ee7205fd97..6a818717751 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -161,6 +161,7 @@ function StackInfo({ title, stackInformation }: StackInfoProps): JSX.Element { ? TYPOGRAPHY.textAlignLeft : TYPOGRAPHY.textAlignRight } + css={LINE_CLAMP_TEXT_STYLE(3)} > {stackInformation ?? t('none')} From 485880ab4c0bc93726598561b468640b6ab974f4 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:06:26 -0500 Subject: [PATCH 12/52] fix(protocol-designer): add white space below deck and remove module tag (#16856) closes RQA-3604 RQA-3603 --- .../src/organisms/SlotInformation/index.tsx | 10 +++++++-- .../DeckSetup/__tests__/HoveredItems.test.tsx | 3 +++ .../__tests__/SelectedHoveredItems.test.tsx | 3 +++ .../src/pages/Designer/LabwareLabel.tsx | 22 ++++++++++++------- .../src/pages/ProtocolOverview/SlotHover.tsx | 20 +++++------------ .../pages/ProtocolOverview/StartingDeck.tsx | 13 ++++++----- 6 files changed, 41 insertions(+), 30 deletions(-) diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index 6a818717751..7c1a1841a22 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -76,7 +76,10 @@ export const SlotInformation: FC = ({ {liquids.length > 1 ? ( { const actual = await importOriginal() return { @@ -54,6 +56,7 @@ describe('HoveredItems', () => { vi.mocked(FixtureRender).mockReturnValue(
mock FixtureRender
) vi.mocked(LabwareRender).mockReturnValue(
mock LabwareRender
) vi.mocked(Module).mockReturnValue(
mock Module
) + vi.mocked(getDesignerTab).mockReturnValue('startingDeck') }) it('renders a hovered fixture', () => { render(props) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx index a34f6a794b8..5d4341959e2 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx @@ -13,12 +13,14 @@ import { Module } from '@opentrons/components' import { selectors } from '../../../../labware-ingred/selectors' import { getInitialDeckSetup } from '../../../../step-forms/selectors' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' +import { getDesignerTab } from '../../../../file-data/selectors' import { LabwareOnDeck } from '../../../../components/DeckSetup/LabwareOnDeck' import { FixtureRender } from '../FixtureRender' import { SelectedHoveredItems } from '../SelectedHoveredItems' import type * as OpentronsComponents from '@opentrons/components' import type { LabwareDefinition2 } from '@opentrons/shared-data' +vi.mock('../../../../file-data/selectors') vi.mock('../../../../step-forms/selectors') vi.mock('../FixtureRender') vi.mock('../../../../labware-ingred/selectors') @@ -48,6 +50,7 @@ describe('SelectedHoveredItems', () => { hoveredFixture: null, slotPosition: [0, 0, 0], } + vi.mocked(getDesignerTab).mockReturnValue('startingDeck') vi.mocked(getInitialDeckSetup).mockReturnValue({ modules: {}, additionalEquipmentOnDeck: {}, diff --git a/protocol-designer/src/pages/Designer/LabwareLabel.tsx b/protocol-designer/src/pages/Designer/LabwareLabel.tsx index 3c85c292071..440ddd14a47 100644 --- a/protocol-designer/src/pages/Designer/LabwareLabel.tsx +++ b/protocol-designer/src/pages/Designer/LabwareLabel.tsx @@ -1,5 +1,7 @@ import { useRef, useState, useEffect } from 'react' +import { useSelector } from 'react-redux' import { DeckLabelSet } from '@opentrons/components' +import { getDesignerTab } from '../../file-data/selectors' import type { DeckLabelProps } from '@opentrons/components' import type { CoordinateTuple, @@ -22,16 +24,20 @@ export const LabwareLabel = (props: ModuleLabelProps): JSX.Element => { nestedLabwareInfo = [], } = props const labelContainerRef = useRef(null) + const designerTab = useSelector(getDesignerTab) const [labelContainerHeight, setLabelContainerHeight] = useState(0) - const deckLabels = [ - ...nestedLabwareInfo, - { - text: labwareDef.metadata.displayName, - isSelected: isSelected, - isLast: isLast, - }, - ] + const deckLabels = + designerTab === 'startingDeck' + ? [ + ...nestedLabwareInfo, + { + text: labwareDef.metadata.displayName, + isSelected: isSelected, + isLast: isLast, + }, + ] + : [] useEffect(() => { if (labelContainerRef.current) { diff --git a/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx b/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx index 20f6c8264d3..0f5bacc7eea 100644 --- a/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/SlotHover.tsx @@ -1,6 +1,5 @@ -import { useCallback, useMemo } from 'react' +import { useMemo } from 'react' import { useSelector } from 'react-redux' -import debounce from 'lodash/debounce' import { ALIGN_CENTER, @@ -27,8 +26,6 @@ import type { import type { Dispatch, SetStateAction } from 'react' -const DEBOUNCE_DURATION_MS = 600 - interface SlotHoverProps { hover: string | null setHover: Dispatch> @@ -57,13 +54,6 @@ export function SlotHover(props: SlotHoverProps): JSX.Element | null { deckDef.cutoutFixtures ) ?? 'cutoutD1' - const debouncedSetHover = useCallback( - debounce((slotId: string | null) => { - setHover(slotId) - }, DEBOUNCE_DURATION_MS), - [setHover] - ) - // return null for TC slots if (slotPosition === null || (hasTCOnSlot && tcSlots.includes(slotId))) return null @@ -126,10 +116,10 @@ export function SlotHover(props: SlotHoverProps): JSX.Element | null { opacity: hoverOpacity, flex: '1', onMouseEnter: () => { - debouncedSetHover(slotId) + setHover(slotId) }, onMouseLeave: () => { - debouncedSetHover(null) + setHover(null) }, }} > @@ -152,10 +142,10 @@ export function SlotHover(props: SlotHoverProps): JSX.Element | null { opacity: hoverOpacity, flex: '1', onMouseEnter: () => { - debouncedSetHover(slotId) + setHover(slotId) }, onMouseLeave: () => { - debouncedSetHover(null) + setHover(null) }, }} > diff --git a/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx b/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx index c7b345bf4da..c1903fcc587 100644 --- a/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx @@ -11,6 +11,7 @@ import { Btn, TYPOGRAPHY, ToggleGroup, + Box, } from '@opentrons/components' import { BUTTON_LINK_STYLE } from '../../atoms' @@ -88,11 +89,13 @@ export function StartingDeck({ ) : ( )} - + + +
) From 805e86df295a42bcd57b5ba9e7b825b360849073 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 19 Nov 2024 16:14:45 -0500 Subject: [PATCH 13/52] fix(protocol-designer): support responsiveness more (#16754) * fix(protocol-designer): support responsiveness more --- .../PDListItemCustomize.tsx | 128 ++++++++++++++++++ .../SelectFixtures.tsx | 2 +- .../CreateNewProtocolWizard/SelectModules.tsx | 2 +- .../CreateNewProtocolWizard/WizardBody.tsx | 16 ++- .../useResponsiveBreakpoints.test.ts | 50 +++++++ .../src/resources/useResponsiveBreakpoints.ts | 36 +++++ 6 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx create mode 100644 protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts create mode 100644 protocol-designer/src/resources/useResponsiveBreakpoints.ts diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx new file mode 100644 index 00000000000..107449fcbaa --- /dev/null +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx @@ -0,0 +1,128 @@ +import { css } from 'styled-components' + +import { + ALIGN_CENTER, + COLORS, + DropdownMenu, + Flex, + JUSTIFY_FLEX_END, + JUSTIFY_FLEX_START, + JUSTIFY_SPACE_BETWEEN, + Link, + SPACING, + StyledText, + Tag, + TYPOGRAPHY, + WRAP, +} from '@opentrons/components' + +import { useResponsiveBreakpoints } from '../../resources/useResponsiveBreakpoints' +import type { DropdownMenuProps, TagProps } from '@opentrons/components' + +interface PDListItemCustomizeProps { + header: string + leftHeaderItem?: JSX.Element + onClick?: () => void + linkText?: string + label?: string + dropdown?: DropdownMenuProps + tag?: TagProps + forceDirection?: boolean +} + +export function PDListItemCustomize({ + header, + leftHeaderItem, + onClick, + linkText, + label, + dropdown, + tag, + forceDirection = false, +}: PDListItemCustomizeProps): JSX.Element { + const responsiveType = useResponsiveBreakpoints() + const isLargeScreen = responsiveType === 'xl' || responsiveType === 'lg' + const flexSize = responsiveType === 'xl' ? '0 0 1.5' : '0 0 1' + + const renderDropdownAndTag = (): JSX.Element => ( + <> + {label != null && ( + + {label} + + )} + {dropdown != null && ( + + + + )} + {tag != null && } + + ) + + const renderLinkButton = (): JSX.Element | null => + onClick != null && linkText != null ? ( + + {linkText} + + ) : null + + return ( + + + {leftHeaderItem != null && {leftHeaderItem}} + {header} + + + + {responsiveType !== 'xs' && ( + + {renderDropdownAndTag()} + + )} + {renderLinkButton()} + + + ) +} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 54a0c6b499c..b5b86a6fc22 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -8,7 +8,6 @@ import { EmptySelectorButton, Flex, ListItem, - ListItemCustomize, SPACING, StyledText, TYPOGRAPHY, @@ -22,6 +21,7 @@ import { getNumSlotsAvailable, } from './utils' import { HandleEnter } from '../../atoms/HandleEnter' +import { PDListItemCustomize as ListItemCustomize } from './PDListItemCustomize' import type { DropdownBorder } from '@opentrons/components' import type { AdditionalEquipment, WizardTileProps } from './types' diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 6533e440804..0fdf6511d4b 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -8,7 +8,6 @@ import { EmptySelectorButton, Flex, ListItem, - ListItemCustomize, SPACING, StyledText, TYPOGRAPHY, @@ -36,6 +35,7 @@ import { } from './constants' import { getNumOptions, getNumSlotsAvailable } from './utils' import { HandleEnter } from '../../atoms/HandleEnter' +import { PDListItemCustomize as ListItemCustomize } from '../CreateNewProtocolWizard/PDListItemCustomize' import type { DropdownBorder } from '@opentrons/components' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx index b5d69253435..bfac8f51a19 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx @@ -104,7 +104,7 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { ) : null} - + - + + + ) } diff --git a/protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts b/protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts new file mode 100644 index 00000000000..e9efd9282f6 --- /dev/null +++ b/protocol-designer/src/resources/__tests__/useResponsiveBreakpoints.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { useResponsiveBreakpoints } from '../useResponsiveBreakpoints' + +describe('useResponsiveBreakpoints', () => { + const resizeWindow = (width: number) => { + window.innerWidth = width + window.dispatchEvent(new Event('resize')) + } + + it('should return "xs" when width is less than BREAKPOINT_SM_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(900) + }) + expect(result.current).toBe('xs') + }) + + it('should return "sm" when width is between BREAKPOINT_SM_WIDTH and BREAKPOINT_MD_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1000) + }) + expect(result.current).toBe('sm') + }) + + it('should return "md" when width is between BREAKPOINT_MD_WIDTH and BREAKPOINT_LG_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1120) + }) + expect(result.current).toBe('md') + }) + + it('should return "lg" when width is between BREAKPOINT_LG_WIDTH and BREAKPOINT_XL_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1200) + }) + expect(result.current).toBe('lg') + }) + + it('should return "xl" when width is greater than BREAKPOINT_XL_WIDTH', () => { + const { result } = renderHook(() => useResponsiveBreakpoints()) + act(() => { + resizeWindow(1500) + }) + expect(result.current).toBe('xl') + }) +}) diff --git a/protocol-designer/src/resources/useResponsiveBreakpoints.ts b/protocol-designer/src/resources/useResponsiveBreakpoints.ts new file mode 100644 index 00000000000..b815322f983 --- /dev/null +++ b/protocol-designer/src/resources/useResponsiveBreakpoints.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react' + +type ResponsiveType = 'xl' | 'lg' | 'md' | 'sm' | 'xs' + +const BREAKPOINT_XL_WIDTH = 1440 +const BREAKPOINT_LG_WIDTH = 1160 +const BREAKPOINT_MD_WIDTH = 1106 +const BREAKPOINT_SM_WIDTH = 978 + +export const useResponsiveBreakpoints = (): ResponsiveType => { + const [responsiveType, setResponsiveType] = useState('xl') + + useEffect(() => { + const handleResize = (): void => { + const width = window.innerWidth + if (width < BREAKPOINT_SM_WIDTH) { + setResponsiveType('xs') + } else if (width < BREAKPOINT_MD_WIDTH) { + setResponsiveType('sm') + } else if (width < BREAKPOINT_LG_WIDTH) { + setResponsiveType('md') + } else if (width <= BREAKPOINT_XL_WIDTH) { + setResponsiveType('lg') + } else { + setResponsiveType('xl') + } + } + handleResize() + window.addEventListener('resize', handleResize) + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + return responsiveType +} From 4db61dfa4b708dc7069a49bd3a25fbb13a877746 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 19 Nov 2024 16:41:37 -0500 Subject: [PATCH 14/52] feat(api): add labware-scope liquid loading api (#16745) We had the ability to load liquids on individual Wells, but this creates an engine command per well - which can be very onerous for large labware - and requires loops and such to load liquids. The common case for loading liquids is that everything is the same, and the second common case is that you care about a labware as a whole at a time (e.g. because you're reading from a platemap). We can support these uses better by having Labware.load_liquid (for many wells with the same amount of liquid), Labware.load_liquid_by_well (more complex, for more complex use cases) and Labware.load_empty, all taking arguments of the type that you can derive from other PAPI methods or domain-specific reasoning. Also, remove well.load_empty because it wasn't shipped yet and is now duplicative. Closes EXEC-825 --- .../v2/parameters/use_case_sample_count.rst | 2 +- .../protocol_api/core/engine/labware.py | 23 +- .../protocol_api/core/engine/well.py | 16 - .../opentrons/protocol_api/core/labware.py | 12 +- .../core/legacy/legacy_labware_core.py | 11 +- .../core/legacy/legacy_well_core.py | 4 - api/src/opentrons/protocol_api/core/well.py | 4 - api/src/opentrons/protocol_api/labware.py | 160 ++++++++- .../core/engine/test_labware_core.py | 44 ++- .../opentrons/protocol_api/test_labware.py | 317 ++++++++++++++++++ api/tests/opentrons/protocol_api/test_well.py | 10 +- 11 files changed, 555 insertions(+), 48 deletions(-) diff --git a/api/docs/v2/parameters/use_case_sample_count.rst b/api/docs/v2/parameters/use_case_sample_count.rst index 15933752592..d7ce6529e48 100644 --- a/api/docs/v2/parameters/use_case_sample_count.rst +++ b/api/docs/v2/parameters/use_case_sample_count.rst @@ -166,7 +166,7 @@ Now we'll bring sample count into consideration as we :ref:`load the liquids Optional[DeckSlotName]: LocationIsStagingSlotError, ): return None + + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + self._engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId=self._labware_id, liquidId=liquid._id, volumeByWell=volumes + ) + ) + + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + self._engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId=self._labware_id, + liquidId="EMPTY", + volumeByWell={well: 0.0 for well in wells}, + ) + ) diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index dba1dc6c840..34616d9eb55 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -142,22 +142,6 @@ def load_liquid( ) ) - def load_empty( - self, - ) -> None: - """Inform the system that a well is known to be empty. - - This should be done early in the protocol, at the same time as a load_liquid command might - be used. - """ - self._engine_client.execute_command( - cmd.LoadLiquidParams( - labwareId=self._labware_id, - liquidId="EMPTY", - volumeByWell={self._name: 0.0}, - ) - ) - def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" well_size = self._engine_client.state.labware.get_well_size( diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index 283aa4c4443..8bb5c66eb90 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -1,8 +1,9 @@ """The interface that implements InstrumentContext.""" + from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Generic, List, NamedTuple, Optional, TypeVar +from typing import Any, Generic, List, NamedTuple, Optional, TypeVar, Dict from opentrons_shared_data.labware.types import ( LabwareUri, @@ -11,6 +12,7 @@ ) from opentrons.types import DeckSlotName, Point, NozzleMapInterface +from .._liquid import Liquid from .well import WellCoreType @@ -133,5 +135,13 @@ def get_well_core(self, well_name: str) -> WellCoreType: def get_deck_slot(self) -> Optional[DeckSlotName]: """Get the deck slot the labware or its parent is in, if any.""" + @abstractmethod + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + + @abstractmethod + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + LabwareCoreType = TypeVar("LabwareCoreType", bound=AbstractLabware[Any]) diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py index 1b00dfcfecf..3957edb106c 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Dict from opentrons.calibration_storage import helpers from opentrons.protocols.geometry.labware_geometry import LabwareGeometry @@ -8,6 +8,7 @@ from opentrons_shared_data.labware.types import LabwareParameters, LabwareDefinition +from ..._liquid import Liquid from ..labware import AbstractLabware, LabwareLoadParams from .legacy_well_core import LegacyWellCore from .well_geometry import WellGeometry @@ -220,3 +221,11 @@ def get_deck_slot(self) -> Optional[DeckSlotName]: """Get the deck slot the labware is in, if in a deck slot.""" slot = self._geometry.parent.labware.first_parent() return DeckSlotName.from_primitive(slot) if slot is not None else None + + def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None: + """Load liquid into wells of the labware.""" + assert False, "load_liquid only supported in API version 2.22 & later" + + def load_empty(self, wells: List[str]) -> None: + """Mark wells of the labware as empty.""" + assert False, "load_empty only supported in API version 2.22 & later" 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 891f0f1b681..a88dd2eee80 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 @@ -114,10 +114,6 @@ def load_liquid( """Load liquid into a well.""" raise APIVersionError(api_element="Loading a liquid") - def load_empty(self) -> None: - """Mark a well as empty.""" - assert False, "load_empty only supported on engine core" - def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" return self._geometry.from_center_cartesian(x, y, z) diff --git a/api/src/opentrons/protocol_api/core/well.py b/api/src/opentrons/protocol_api/core/well.py index 24489bb04e7..bd58963a59c 100644 --- a/api/src/opentrons/protocol_api/core/well.py +++ b/api/src/opentrons/protocol_api/core/well.py @@ -79,10 +79,6 @@ def load_liquid( ) -> None: """Load liquid into a well.""" - @abstractmethod - def load_empty(self) -> None: - """Mark a well as containing no liquid.""" - @abstractmethod def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 6161a785829..4ac7713f83a 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -1,4 +1,4 @@ -""" opentrons.protocol_api.labware: classes and functions for labware handling +"""opentrons.protocol_api.labware: classes and functions for labware handling This module provides things like :py:class:`Labware`, and :py:class:`Well` to encapsulate labware instances used in protocols @@ -13,7 +13,18 @@ import logging from itertools import dropwhile -from typing import TYPE_CHECKING, Any, List, Dict, Optional, Union, Tuple, cast +from typing import ( + TYPE_CHECKING, + Any, + List, + Dict, + Optional, + Union, + Tuple, + cast, + Sequence, + Mapping, +) from opentrons_shared_data.labware.types import LabwareDefinition, LabwareParameters @@ -281,19 +292,15 @@ def load_liquid(self, liquid: Liquid, volume: float) -> None: :param Liquid liquid: The liquid to load into the well. :param float volume: The volume of liquid to load, in µL. - .. note:: - In API version 2.22 and later, use :py:meth:`~.Well.load_empty()` to mark a well as empty at the beginning of a protocol, rather than using this method with ``volume=0``. + .. deprecated:: 2.22 + In API version 2.22 and later, use :py:meth:`~Labware.load_liquid`, :py:meth:`~Labware.load_liquid_by_well`, + or :py:meth:`~Labware.load_empty` to load liquid into a well. """ self._core.load_liquid( liquid=liquid, volume=volume, ) - @requires_version(2, 22) - def load_empty(self) -> None: - """Mark a well as empty.""" - self._core.load_empty() - def _from_center_cartesian(self, x: float, y: float, z: float) -> Point: """ Private version of from_center_cartesian. Present only for backward @@ -1113,6 +1120,141 @@ def reset(self) -> None: """ self._core.reset_tips() + @requires_version(2, 22) + def load_liquid( + self, wells: Sequence[Union[str, Well]], volume: float, liquid: Liquid + ) -> None: + """Mark several wells as containing the same amount of liquid. + + This method should be called at the beginning of a protocol, soon after loading the labware and before + liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware + has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or + :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked. + + For example, to load 10µL of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`) + into all the wells of a labware, you could call ``labware.load_liquid(labware.wells(), 10, water)``. + + If you want to load different volumes of liquid into different wells, use :py:meth:`~Labware.load_liquid_by_well`. + + If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`. + + :param wells: The wells to load the liquid into. + :type wells: List of well names or list of Well objects, for instance from :py:meth:`~Labware.wells`. + + :param volume: The volume of liquid to load into each well, in 10µL. + :type volume: float + + :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid` + :type liquid: Liquid + """ + well_names: List[str] = [] + for well in wells: + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in labware {self.name}. The elements of wells should name wells in this labware." + ) + well_names.append(well) + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in labware {self.name}. The elements of wells should be wells of this labware." + ) + well_names.append(well.well_name) + else: + raise TypeError( + f"Unexpected type for element {repr(well)}. The elements of wells should be Well instances or well names." + ) + if not isinstance(volume, (float, int)): + raise TypeError( + f"Unexpected type for volume {repr(volume)}. Volume should be a number in microliters." + ) + self._core.load_liquid({well_name: volume for well_name in well_names}, liquid) + + @requires_version(2, 22) + def load_liquid_by_well( + self, volumes: Mapping[Union[str, Well], float], liquid: Liquid + ) -> None: + """Mark several wells as containing unique volumes of liquid. + + This method should be called at the beginning of a protocol, soon after loading the labware and before + liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware + has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or + :py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked. + + For example, to load a decreasing amount of of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`) + into each successive well of a row, you could call + ``labware.load_liquid_by_well({'A1': 1000, 'A2': 950, 'A3': 900, ..., 'A12': 600}, water)`` + + If you want to load the same volume of a liquid into multiple wells, it is often easier to use :py:meth:`~Labware.load_liquid`. + + If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`. + + :param volumes: A dictionary of well names (or :py:class:`Well` objects, for instance from ``labware['A1']``) + :type wells: Dict[Union[str, Well], float] + + :param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid` + :type liquid: Liquid + """ + verified_volumes: Dict[str, float] = {} + for well, volume in volumes.items(): + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in {self.name}. The keys of volumes should name wells in this labware" + ) + verified_volumes[well] = volume + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in {self.name}. The keys of volumes should be wells of this labware" + ) + verified_volumes[well.well_name] = volume + else: + raise TypeError( + f"Unexpected type for well name {repr(well)}. The keys of volumes should be Well instances or well names." + ) + if not isinstance(volume, (float, int)): + raise TypeError( + f"Unexpected type for volume {repr(volume)}. The values of volumes should be numbers in microliters." + ) + self._core.load_liquid(verified_volumes, liquid) + + @requires_version(2, 22) + def load_empty(self, wells: Sequence[Union[Well, str]]) -> None: + """Mark several wells as empty. + + This method should be called at the beginning of a protocol, soon after loading the labware and before liquid handling + operations begin. It is a base of information for liquid tracking functionality. If a well in a labware has not been named + in a call to :py:meth:`Labware.load_empty`, :py:meth:`Labware.load_liquid`, or :py:meth:`Labware.load_liquid_by_well`, the + volume it contains is unknown and the well's liquid will not be tracked. + + For instance, to mark all wells in the labware as empty, you can call ``labware.load_empty(labware.wells())``. + + :param wells: The list of wells to mark empty. To mark all wells as empty, pass ``labware.wells()``. You can also specify + wells by their names (for instance, ``labware.load_empty(['A1', 'A2'])``). + :type wells: Union[List[Well], List[str]] + """ + well_names: List[str] = [] + for well in wells: + if isinstance(well, str): + if well not in self.wells_by_name(): + raise KeyError( + f"{well} is not a well in {self.name}. The elements of wells should name wells in this labware." + ) + well_names.append(well) + elif isinstance(well, Well): + if well.parent is not self: + raise KeyError( + f"{well.well_name} is not a well in {self.name}. The elements of wells should be wells of this labware." + ) + well_names.append(well.well_name) + else: + raise TypeError( + f"Unexpected type for well name {repr(well)}. The elements of wells should be Well instances or well names." + ) + self._core.load_empty(well_names) + # TODO(mc, 2022-11-09): implementation detail, move to core def split_tipracks(tip_racks: List[Labware]) -> Tuple[Labware, List[Labware]]: diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index 847c80d2125..6f4458f87ff 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -1,4 +1,5 @@ """Tests for opentrons.protocol_api.core.engine.LabwareCore.""" + from typing import cast import pytest @@ -25,7 +26,7 @@ LabwareOffsetLocation, LabwareOffsetVector, ) - +from opentrons.protocol_api._liquid import Liquid from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.engine import LabwareCore, WellCore from opentrons.calibration_storage.helpers import uri_from_details @@ -80,7 +81,9 @@ def test_get_load_params(subject: LabwareCore) -> None: version=42, parameters=LabwareDefinitionParameters.construct(loadName="world"), # type: ignore[call-arg] ordering=[], - metadata=LabwareDefinitionMetadata.construct(displayName="what a cool labware"), # type: ignore[call-arg] + metadata=LabwareDefinitionMetadata.construct( + displayName="what a cool labware" + ), # type: ignore[call-arg] ) ], ) @@ -455,3 +458,40 @@ def test_get_deck_slot( ).then_raise(LabwareNotOnDeckError("oh no")) assert subject.get_deck_slot() is None + + +def test_load_liquid( + decoy: Decoy, mock_engine_client: EngineClient, subject: LabwareCore +) -> None: + """It should pass loaded liquids to the engine.""" + mock_liquid = Liquid( + _id="liquid-id", name="water", description=None, display_color=None + ) + subject.load_liquid(volumes={"A1": 20, "B1": 30, "C1": 40}, liquid=mock_liquid) + + decoy.verify( + mock_engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId="cool-labware", + liquidId="liquid-id", + volumeByWell={"A1": 20, "B1": 30, "C1": 40}, + ) + ), + times=1, + ) + + +def test_load_empty( + decoy: Decoy, mock_engine_client: EngineClient, subject: LabwareCore +) -> None: + """It should pass empty liquids to the engine.""" + subject.load_empty(wells=["A1", "B1", "C1"]) + decoy.verify( + mock_engine_client.execute_command( + cmd.LoadLiquidParams( + labwareId="cool-labware", + liquidId="EMPTY", + volumeByWell={"A1": 0.0, "B1": 0.0, "C1": 0.0}, + ) + ) + ) diff --git a/api/tests/opentrons/protocol_api/test_labware.py b/api/tests/opentrons/protocol_api/test_labware.py index 4610145162f..5e49cd29947 100644 --- a/api/tests/opentrons/protocol_api/test_labware.py +++ b/api/tests/opentrons/protocol_api/test_labware.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import inspect from typing import cast @@ -21,6 +22,7 @@ from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.core_map import LoadedCoreMap from opentrons.protocol_api import TemperatureModuleContext +from opentrons.protocol_api._liquid import Liquid from opentrons.types import Point @@ -364,3 +366,318 @@ def test_separate_calibration_raises_on_high_api_version( """It should raise an error, on high API versions.""" with pytest.raises(UnsupportedAPIError): subject.separate_calibration + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load volumes for list of wells.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid(["A1", subject["B1"]], 10, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 10, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid load inputs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(core_2.get_well_core("B1")).then_return(mock_well_core_2) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + with pytest.raises(KeyError): + subject.load_liquid(["A1", "C1"], 10, mock_liquid) + + with pytest.raises(KeyError): + subject.load_liquid([subject["A1"], other_labware["A1"]], 10, mock_liquid) + + with pytest.raises(TypeError): + subject.load_liquid([2], 10, mock_liquid) # type: ignore[list-item] + + with pytest.raises(TypeError): + subject.load_liquid(["A1"], "A1", mock_liquid) # type: ignore[arg-type] + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid(["A1", subject["B1"]], 10, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 10, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_by_well_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load liquids of different volumes in different wells.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + decoy.when(mock_well_core_2.get_display_name()).then_return("well 2") + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + mock_liquid = decoy.mock(cls=Liquid) + + subject.load_liquid_by_well({"A1": 10, subject["B1"]: 11}, mock_liquid) + decoy.verify( + mock_labware_core.load_liquid( + { + "A1": 10, + "B1": 11, + }, + mock_liquid, + ) + ) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_liquid_by_well_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid well specs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_well_core_1.get_display_name()).then_return("well 1") + decoy.when(mock_well_core_2.get_display_name()).then_return("well 2") + decoy.when(mock_well_core_1.get_top(z_offset=0.0)).then_return(Point(4, 5, 6)) + decoy.when(mock_well_core_1.get_top(z_offset=0.0)).then_return(Point(7, 8, 9)) + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + decoy.when(mock_well_core_3.get_display_name()).then_return("well 3") + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_3) + decoy.when(mock_well_core_3.get_top(z_offset=0.0)).then_return(Point(1, 2, 3)) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + mock_liquid = decoy.mock(cls=Liquid) + with pytest.raises(KeyError): + subject.load_liquid_by_well({"A1": 10, "C1": 11}, mock_liquid) + + with pytest.raises(KeyError): + subject.load_liquid_by_well( + {subject["A1"]: 10, other_labware["A1"]: 11}, mock_liquid + ) + + with pytest.raises(TypeError): + subject.load_liquid_by_well({2: 10}, mock_liquid) # type: ignore[dict-item] + + with pytest.raises(TypeError): + subject.load_liquid_by_well({"A1": "A3"}, mock_liquid) # type: ignore[dict-item] + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_empty_handles_valid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should load lists of wells as empty.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + subject.load_empty(["A1", subject["B1"]]) + decoy.verify(mock_labware_core.load_empty(["A1", "B1"])) + + +@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) +def test_load_empty_rejects_invalid_inputs( + decoy: Decoy, + mock_labware_core: LabwareCore, + api_version: APIVersion, + mock_protocol_core: ProtocolCore, + mock_map_core: LoadedCoreMap, +) -> None: + """It should require valid well specs.""" + mock_well_core_1 = decoy.mock(cls=WellCore) + mock_well_core_2 = decoy.mock(cls=WellCore) + + grid = well_grid.WellGrid( + columns_by_name={"1": ["A1", "B1"]}, + rows_by_name={"A": ["A1"], "B": ["B1"]}, + ) + decoy.when(mock_well_core_1.get_name()).then_return("A1") + decoy.when(mock_well_core_2.get_name()).then_return("B1") + decoy.when(mock_labware_core.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(mock_labware_core.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(mock_labware_core.get_well_core("B1")).then_return(mock_well_core_2) + decoy.when(well_grid.create([["A1", "B1"]])).then_return(grid) + subject = Labware( + core=mock_labware_core, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + + core_2 = decoy.mock(cls=LabwareCore) + mock_well_core_3 = decoy.mock(cls=WellCore) + grid_2 = well_grid.WellGrid( + columns_by_name={"1": ["A1"]}, rows_by_name={"A": ["A1"]} + ) + decoy.when(mock_well_core_3.get_name()).then_return("A1") + decoy.when(core_2.get_well_columns()).then_return([["A1", "B1"]]) + decoy.when(core_2.get_well_core("A1")).then_return(mock_well_core_1) + decoy.when(core_2.get_well_core("B1")).then_return(mock_well_core_2) + + decoy.when(well_grid.create([["A1"]])).then_return(grid_2) + other_labware = Labware( + core=core_2, + api_version=api_version, + protocol_core=mock_protocol_core, + core_map=mock_map_core, + ) + with pytest.raises(KeyError): + subject.load_empty(["A1", "C1"]) + + with pytest.raises(KeyError): + subject.load_empty([subject["A1"], other_labware["A1"]]) + + with pytest.raises(TypeError): + subject.load_empty([2]) # type: ignore[list-item] diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py index b4817567dde..c0ef530289b 100644 --- a/api/tests/opentrons/protocol_api/test_well.py +++ b/api/tests/opentrons/protocol_api/test_well.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" + import pytest from decoy import Decoy @@ -8,8 +9,6 @@ from opentrons.protocol_api._liquid import Liquid from opentrons.types import Point, Location -from . import versions_at_or_above - @pytest.fixture def mock_well_core(decoy: Decoy) -> WellCore: @@ -142,13 +141,6 @@ def test_load_liquid(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> N ) -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) -def test_load_empty(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: - """It should mark a location as empty.""" - subject.load_empty() - decoy.verify(mock_well_core.load_empty(), times=1) - - def test_diameter(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: """It should get the diameter from the core.""" decoy.when(mock_well_core.diameter).then_return(12.3) From 7568194e50d0bca3cbcffad0b4b5dc2d67eae864 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:43:02 -0500 Subject: [PATCH 15/52] fix(protocol-designer): update swap pipette and empty state copy (#16892) closes RQA-3625 --- .../src/assets/localization/en/create_new_protocol.json | 2 +- .../src/assets/localization/en/protocol_overview.json | 4 ++-- .../src/organisms/EditInstrumentsModal/index.tsx | 2 +- .../src/pages/CreateNewProtocolWizard/SelectPipettes.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index aeaa00c86c1..269c252dd39 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -36,7 +36,7 @@ "staging_area_has_labware": "This staging area slot has labware", "staging_area_will_delete_labware": "The staging area slot that you are about to delete has labware placed on it. If you make these changes to your protocol starting deck, the labware will be deleted as well.", "stagingArea": "Staging area", - "swap_pipettes": "Swap pipettes", + "swap_pipette_mounts": "Swap pipette mounts", "tell_us": "Tell us about your protocol", "trash_required": "A trash entity is required", "trashBin": "Trash Bin", diff --git a/protocol-designer/src/assets/localization/en/protocol_overview.json b/protocol-designer/src/assets/localization/en/protocol_overview.json index b03097576cd..6afdf715b6c 100644 --- a/protocol-designer/src/assets/localization/en/protocol_overview.json +++ b/protocol-designer/src/assets/localization/en/protocol_overview.json @@ -22,9 +22,9 @@ "name": "Name", "no_deck_hardware": "No deck hardware", "no_labware": "No labware", - "no_liquids_defined": "No liquids defined", + "no_liquids_defined": "Edit protocol to define liquids", "no_liquids": "No liquids", - "no_steps": "No steps defined", + "no_steps": "Edit protocol to define steps", "number_of_steps": "Number of steps", "protocol_metadata": "Protocol Metadata", "protocol_steps": "Protocol Steps", diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx index b933baf5217..c986c1ce9ba 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -252,7 +252,7 @@ export function EditInstrumentsModal( transform="rotate(90deg)" /> - {t('swap')} + {t('swap_pipette_mounts')}
diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index fc811b2665a..3f48e08e6bd 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -465,7 +465,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { transform="rotate(90deg)" /> - {t('swap_pipettes')} + {t('swap_pipette_mounts')} From 29d7e87bf4f3bf5b92eb14febadcbe64a9261c0a Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Tue, 19 Nov 2024 16:44:40 -0500 Subject: [PATCH 16/52] fix(shared-data): extents update from hardware (#16895) # Overview Hardware got back to me with some updated numbers. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../definitions/2/general/ninety_six_channel/p200/3_0.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json index 5719bb3437c..5f3fa397dc8 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json @@ -624,8 +624,8 @@ "plungerPositionsConfigurations": { "default": { "top": 0.5, - "bottom": 68.5, - "blowout": 73.5, + "bottom": 67.5, + "blowout": 72.5, "drop": 80 } }, @@ -649,7 +649,7 @@ "channels": 96, "shaftDiameter": 2, "shaftULperMM": 3.14159, - "backlashDistance": 3.0, + "backlashDistance": 0.3, "quirks": [], "plungerHomingConfigurations": { "current": 0.8, From db61a6f142a9d93d93476ad98c26fa19f04a025d Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:37:48 -0500 Subject: [PATCH 17/52] fix(protocol-designer): render labware on Thermocycler in deck setup (#16900) --- .../src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx | 4 +--- .../src/pages/CreateNewProtocolWizard/SelectFixtures.tsx | 1 - .../src/pages/CreateNewProtocolWizard/SelectModules.tsx | 1 - .../src/pages/Designer/DeckSetup/DeckSetupDetails.tsx | 4 +++- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx index 107449fcbaa..6f0a6c6caa0 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/PDListItemCustomize.tsx @@ -27,7 +27,6 @@ interface PDListItemCustomizeProps { label?: string dropdown?: DropdownMenuProps tag?: TagProps - forceDirection?: boolean } export function PDListItemCustomize({ @@ -38,7 +37,6 @@ export function PDListItemCustomize({ label, dropdown, tag, - forceDirection = false, }: PDListItemCustomizeProps): JSX.Element { const responsiveType = useResponsiveBreakpoints() const isLargeScreen = responsiveType === 'xl' || responsiveType === 'lg' @@ -53,7 +51,7 @@ export function PDListItemCustomize({ )} {dropdown != null && ( - + )} {tag != null && } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index b5b86a6fc22..68223dbc183 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -158,7 +158,6 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { return ( { setValue( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 0fdf6511d4b..1b69a3e02db 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -258,7 +258,6 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { return ( Date: Wed, 20 Nov 2024 09:46:19 -0500 Subject: [PATCH 18/52] fix(shared-data): RABR-669 add biorad pcr plate compatibility with tough_pcr_lid (#16849) # Overview Adds compatibility between `biorad_96_wellplate_200ul_pcr` and `opentrons_tough_pcr_auto_sealing_lid` with stacking z offset ## Test Plan and Hands on Testing - Tested on thermocycler in ABR ## Changelog - added `biorad_96_wellplate_200ul_pcr` stacking offset to shared data file ## Review requests ## Risk assessment --- .../2/opentrons_tough_pcr_auto_sealing_lid/1.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json index f872649a027..9ae49fd8a5e 100644 --- a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json +++ b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json @@ -66,6 +66,11 @@ "y": 0, "z": 8.193 }, + "biorad_96_wellplate_200ul_pcr": { + "x": 0, + "y": 0, + "z": 8.08 + }, "opentrons_flex_deck_riser": { "x": 0, "y": 0, From 71b21ae6702ac2f81931f9b38aafcaa476c4c669 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:11:21 -0500 Subject: [PATCH 19/52] fix(protocol-designer): fix labware tools filtering and expand/collapse behavior (#16889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR addresses several functional issues with our LabwareTools component, produced when adding labware to the starting deck state. Here, I add filtering to populated expanded categories, allow independent expand/collapse toggling for multiple categories simultaneously, and auto expand/collapse all categories based on the state of the current search term. NOTE: This component is pretty bloated as is, and I think we can address condensing filtering logic and improving performance in a followup. Also, I notice a ton of rerenders after mounting— I will address in a followup. Closes RQA-3590 --- .../ListButtonAccordionContainer.tsx | 2 +- .../localization/en/starting_deck_state.json | 12 +- .../Designer/DeckSetup/DeckSetupTools.tsx | 55 ++++- .../pages/Designer/DeckSetup/LabwareTools.tsx | 206 ++++++++++-------- .../DeckSetup/__tests__/LabwareTools.test.tsx | 42 ++-- .../src/pages/Designer/DeckSetup/constants.ts | 2 + .../src/pages/Designer/DeckSetup/utils.ts | 20 +- 7 files changed, 199 insertions(+), 140 deletions(-) diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx index 99fde7dd81f..85f76f901b2 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx @@ -16,7 +16,7 @@ export function ListButtonAccordionContainer( const { id, children } = props return ( - + {children} ) diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index 31be366c9d7..fcf88c2866e 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -1,6 +1,6 @@ { "adapter_compatible_lab": "Adapter compatible labware", - "adapter": "Adapter", + "adapter": "Adapters", "add_fixture": "Add a fixture", "add_hardware_labware": "Add hardware/labware", "add_hw_lw": "Add hardware/labware", @@ -9,7 +9,7 @@ "add_module": "Add a module", "add_rest": "Add labware and liquids to complete deck setup", "alter_pause": "You may also need to alter the time you pause while your magnet is engaged.", - "aluminumBlock": "Aluminum block", + "aluminumBlock": "Aluminum blocks", "clear_labware": "Clear labware", "clear_slot": "Clear slot", "clear": "Clear", @@ -47,16 +47,16 @@ "protocol_starting_deck": "Protocol starting deck", "read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules", "rename_lab": "Rename labware", - "reservoir": "Reservoir", + "reservoir": "Reservoirs", "shift_click_to_select_all": "Shift + Click to select all", "starting_deck_state": "Starting deck state", "tc_slots_occupied_flex": "The Thermocycler needs slots A1 and B1. Slot A1 is occupied", "tc_slots_occupied_ot2": "The Thermocycler needs slots 7, 8, 10, and 11. One or more of those slots is occupied", - "tipRack": "Tip rack", + "tipRack": "Tip racks", "trash_required": "A trash bin or waste chute is required", - "tubeRack": "Tube rack", + "tubeRack": "Tube racks", "untitled_protocol": "Untitled protocol", "upload_custom_labware": "Upload custom labware", "we_added_hardware": "We've added your deck hardware!", - "wellPlate": "Well plate" + "wellPlate": "Well plates" } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 4e2db1df4b8..62a5f92d46e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -52,11 +52,11 @@ import { getDismissedHints } from '../../../tutorial/selectors' import { createContainerAboveModule } from '../../../step-forms/actions/thunks' import { ConfirmDeleteStagingAreaModal } from '../../../organisms' import { BUTTON_LINK_STYLE } from '../../../atoms' -import { FIXTURES, MOAM_MODELS } from './constants' import { getSlotInformation } from '../utils' -import { getModuleModelsBySlot, getDeckErrors } from './utils' -import { MagnetModuleChangeContent } from './MagnetModuleChangeContent' +import { ALL_ORDERED_CATEGORIES, FIXTURES, MOAM_MODELS } from './constants' import { LabwareTools } from './LabwareTools' +import { MagnetModuleChangeContent } from './MagnetModuleChangeContent' +import { getModuleModelsBySlot, getDeckErrors } from './utils' import type { ModuleModel } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../../types' @@ -71,6 +71,8 @@ interface DeckSetupToolsProps { } | null } +export type CategoryExpand = Record + export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const { onCloseClick, setHoveredLabware, onDeckProps } = props const { t, i18n } = useTranslation(['starting_deck_state', 'shared']) @@ -117,6 +119,28 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const [tab, setTab] = useState<'hardware' | 'labware'>( moduleModels?.length === 0 || slot === 'offDeck' ? 'labware' : 'hardware' ) + + const setAllCategories = (state: boolean): Record => + ALL_ORDERED_CATEGORIES.reduce>( + (acc, category) => ({ ...acc, [category]: state }), + {} + ) + const allCategoriesExpanded = setAllCategories(true) + const allCategoriesCollapsed = setAllCategories(false) + const [ + areCategoriesExpanded, + setAreCategoriesExpanded, + ] = useState(allCategoriesCollapsed) + const [searchTerm, setSearchTerm] = useState('') + + useEffect(() => { + if (searchTerm !== '') { + setAreCategoriesExpanded(allCategoriesExpanded) + } else { + setAreCategoriesExpanded(allCategoriesCollapsed) + } + }, [searchTerm]) + const hasMagneticModule = Object.values(deckSetup.modules).some( module => module.type === MAGNETIC_MODULE_TYPE ) @@ -124,6 +148,12 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { Object.values(deckSetup.modules).find(module => module.slot === slot) ?.model === MAGNETIC_MODULE_V1 + const handleCollapseAllCategories = (): void => { + setAreCategoriesExpanded(allCategoriesCollapsed) + } + const handleResetSearchTerm = (): void => { + setSearchTerm('') + } const changeModuleWarning = useBlockingHint({ hintKey: 'change_magnet_module_model', handleCancel: () => { @@ -207,6 +237,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { ) } + const handleResetLabwareTools = (): void => { + handleCollapseAllCategories() + handleResetSearchTerm() + } + const handleClear = (): void => { onDeckProps?.setHoveredModule(null) onDeckProps?.setHoveredFixture(null) @@ -242,7 +277,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } } handleResetToolbox() + handleResetLabwareTools() setSelectedHardware(null) + if (selectedHardware != null) { + setTab('hardware') + } } const handleConfirm = (): void => { // clear entities first before recreating them @@ -548,7 +587,15 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { )} ) : ( - + )} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index dec0d114f83..be4f457429e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import reduce from 'lodash/reduce' import styled from 'styled-components' @@ -49,7 +49,11 @@ import { selectLabware, selectNestedLabware, } from '../../../labware-ingred/actions' -import { ORDERED_CATEGORIES } from './constants' +import { + ALL_ORDERED_CATEGORIES, + CUSTOM_CATEGORY, + ORDERED_CATEGORIES, +} from './constants' import { getLabwareIsRecommended, getLabwareCompatibleWithAdapter, @@ -59,8 +63,8 @@ import type { DeckSlotId, LabwareDefinition2 } from '@opentrons/shared-data' import type { ModuleOnDeck } from '../../../step-forms' import type { ThunkDispatch } from '../../../types' import type { LabwareDefByDefURI } from '../../../labware-defs' +import type { CategoryExpand } from './DeckSetupTools' -const CUSTOM_CATEGORY = 'custom' const STANDARD_X_DIMENSION = 127.75 const STANDARD_Y_DIMENSION = 85.48 const PLATE_READER_LOADNAME = @@ -68,10 +72,28 @@ const PLATE_READER_LOADNAME = interface LabwareToolsProps { slot: DeckSlotId setHoveredLabware: (defUri: string | null) => void + searchTerm: string + setSearchTerm: React.Dispatch> + areCategoriesExpanded: CategoryExpand + setAreCategoriesExpanded: React.Dispatch> + handleReset: () => void +} + +interface LabwareInfo { + uri: string + def: LabwareDefinition2 } export function LabwareTools(props: LabwareToolsProps): JSX.Element { - const { slot, setHoveredLabware } = props + const { + slot, + setHoveredLabware, + searchTerm, + setSearchTerm, + areCategoriesExpanded, + setAreCategoriesExpanded, + handleReset, + } = props const { t } = useTranslation(['starting_deck_state', 'shared']) const robotType = useSelector(getRobotType) const dispatch = useDispatch>() @@ -87,10 +109,6 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { selectedModuleModel, selectedNestedLabwareDefUri, } = zoomedInSlotInfo - const [selectedCategory, setSelectedCategory] = React.useState( - null - ) - const [searchTerm, setSearchTerm] = React.useState('') const searchFilter = (termToCheck: string): boolean => termToCheck.toLowerCase().includes(searchTerm.toLowerCase()) @@ -101,7 +119,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map( moduleId => modulesById[moduleId] ) - const [filterRecommended, setFilterRecommended] = React.useState( + const [filterRecommended, setFilterRecommended] = useState( moduleType != null ) // for OT-2 usage only due to H-S collisions @@ -110,11 +128,11 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { hardwareModule.type === HEATERSHAKER_MODULE_TYPE && getAreSlotsHorizontallyAdjacent(hardwareModule.slot, slot) ) - const [filterHeight, setFilterHeight] = React.useState( + const [filterHeight, setFilterHeight] = useState( robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false ) - const getLabwareCompatible = React.useCallback( + const getLabwareCompatible = useCallback( (def: LabwareDefinition2) => { // assume that custom (non-standard) labware is (potentially) compatible if (moduleType == null || !getLabwareDefIsStandard(def)) { @@ -125,7 +143,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { [moduleType] ) - const getIsLabwareFiltered = React.useCallback( + const getIsLabwareFiltered = useCallback( (labwareDef: LabwareDefinition2) => { const { dimensions, parameters } = labwareDef const { xDimension, yDimension } = dimensions @@ -155,11 +173,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] ) - const customLabwareURIs: string[] = React.useMemo( - () => Object.keys(customLabwareDefs), - [customLabwareDefs] - ) - const labwareByCategory = React.useMemo(() => { + + const labwareByCategory = useMemo(() => { return reduce< LabwareDefByDefURI, { [category: string]: LabwareDefinition2[] } @@ -184,28 +199,51 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { ) }, [permittedTipracks]) - const populatedCategories: { [category: string]: boolean } = React.useMemo( + const filteredLabwareByCategory: Record = useMemo( () => - ORDERED_CATEGORIES.reduce((acc, category) => { + ALL_ORDERED_CATEGORIES.reduce((acc, category) => { + if (category === 'custom') { + return { + ...acc, + [category]: filterRecommended + ? [] + : Object.entries(customLabwareDefs).reduce( + (accInner, [uri, def]) => { + return searchFilter(def.metadata.displayName) + ? [...accInner, { uri, def }] + : accInner + }, + [] + ), + } + } const isDeckLocationCategory = slot === 'offDeck' ? category !== 'adapter' : true - return category in labwareByCategory && - isDeckLocationCategory && - labwareByCategory[category].some(lw => - searchFilter(lw.metadata.displayName) - ) - ? { - ...acc, - [category]: labwareByCategory[category].some( - def => !getIsLabwareFiltered(def) - ), - } - : acc + if (!(category in labwareByCategory) || !isDeckLocationCategory) { + return { ...acc, [category]: [] } + } + return { + ...acc, + [category]: labwareByCategory[category].reduce( + (accInner, def) => { + return searchFilter(def.metadata.displayName) && + !getIsLabwareFiltered(def) + ? [...accInner, { def, uri: getLabwareDefURI(def) }] + : accInner + }, + [] + ), + } }, {}), [labwareByCategory, getIsLabwareFiltered, searchTerm] ) - const handleCategoryClick = (category: string): void => { - setSelectedCategory(selectedCategory === category ? null : category) + + const handleCategoryClick = (category: string, expand?: boolean): void => { + const updatedExpandState = { + ...areCategoriesExpanded, + [category]: expand ?? !areCategoriesExpanded[category], + } + setAreCategoriesExpanded(updatedExpandState) } return ( @@ -223,9 +261,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { size="medium" leftIcon="search" showDeleteIcon - onDelete={() => { - setSearchTerm('') - }} + onDelete={handleReset} /> {moduleType != null || (isNextToHeaterShaker && robotType === OT2_ROBOT_TYPE) ? ( @@ -253,7 +289,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { gridGap={SPACING.spacing4} paddingTop={SPACING.spacing8} > - {customLabwareURIs.length === 0 ? null : ( + {filteredLabwareByCategory[CUSTOM_CATEGORY].length > 0 ? ( - {customLabwareURIs.map((labwareURI, index) => ( - { - setHoveredLabware(null) - }} - setHovered={() => { - setHoveredLabware(labwareURI) - }} - buttonValue={labwareURI} - onChange={e => { - e.stopPropagation() - dispatch(selectLabware({ labwareDefUri: labwareURI })) - }} - isSelected={labwareURI === selectedLabwareDefUri} - /> - ))} + {filteredLabwareByCategory[CUSTOM_CATEGORY].map( + ({ uri }, index) => ( + { + setHoveredLabware(null) + }} + setHovered={() => { + setHoveredLabware(uri) + }} + buttonValue={uri} + onChange={e => { + e.stopPropagation() + dispatch(selectLabware({ labwareDefUri: uri })) + }} + isSelected={uri === selectedLabwareDefUri} + /> + ) + )} - )} + ) : null} {ORDERED_CATEGORIES.map(category => { - const isPopulated = populatedCategories[category] - if (isPopulated) { + if (filteredLabwareByCategory[category].length > 0) { return ( - {labwareByCategory[category]?.map((labwareDef, index) => { - const isFiltered = getIsLabwareFiltered(labwareDef) - const labwareURI = getLabwareDefURI(labwareDef) - const loadName = labwareDef.parameters.loadName - const isMatch = searchFilter( - labwareDef.metadata.displayName - ) - if (!isFiltered && isMatch) { - return ( - + {filteredLabwareByCategory[category]?.map( + ({ def, uri }, index) => { + const loadName = def.parameters.loadName + + return searchFilter(def.metadata.displayName) && + !getIsLabwareFiltered(def) ? ( + { setHoveredLabware(null) }} setHovered={() => { - setHoveredLabware(labwareURI) + setHoveredLabware(uri) }} id={`${index}_${category}_${loadName}`} - buttonText={labwareDef.metadata.displayName} - buttonValue={labwareURI} + buttonText={def.metadata.displayName} + buttonValue={uri} onChange={e => { e.stopPropagation() dispatch( selectLabware({ labwareDefUri: - labwareURI === selectedLabwareDefUri + uri === selectedLabwareDefUri ? null - : labwareURI, + : uri, }) ) // reset the nested labware def uri in case it is not compatible @@ -346,10 +376,10 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { }) ) }} - isSelected={labwareURI === selectedLabwareDefUri} + isSelected={uri === selectedLabwareDefUri} /> - {labwareURI === selectedLabwareDefUri && + {uri === selectedLabwareDefUri && getLabwareCompatibleWithAdapter(loadName) ?.length > 0 && ( {has96Channel && loadName === ADAPTER_96_CHANNEL @@ -440,10 +468,10 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { )} - - ) + + ) : null } - })} + )} @@ -464,8 +492,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { data-testid="customLabwareInput" type="file" onChange={e => { - setSelectedCategory(CUSTOM_CATEGORY) dispatch(createCustomLabwareDef(e)) + handleCategoryClick(CUSTOM_CATEGORY, true) }} /> diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx index b4d444a1cf3..479724f3527 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx @@ -20,10 +20,6 @@ import { selectors } from '../../../../labware-ingred/selectors' import { createCustomLabwareDef } from '../../../../labware-defs/actions' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' import { getRobotType } from '../../../../file-data/selectors' -import { - selectLabware, - selectNestedLabware, -} from '../../../../labware-ingred/actions' import { LabwareTools } from '../LabwareTools' import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' @@ -48,6 +44,11 @@ describe('LabwareTools', () => { props = { slot: 'D3', setHoveredLabware: vi.fn(), + searchTerm: '', + setSearchTerm: vi.fn(), + areCategoriesExpanded: {}, + setAreCategoriesExpanded: vi.fn(), + handleReset: vi.fn(), } vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) @@ -80,18 +81,14 @@ describe('LabwareTools', () => { it('renders an empty slot with all the labware options', () => { render(props) screen.getByText('Add labware') - screen.getByText('Tube rack') - screen.getByText('Well plate') - screen.getByText('Reservoir') - screen.getByText('Aluminum block') - screen.getByText('Adapter') + screen.getByText('Tube racks') + screen.getByText('Well plates') + screen.getByText('Reservoirs') + screen.getByText('Aluminum blocks') + screen.getByText('Adapters') // click and expand well plate accordion fireEvent.click(screen.getAllByTestId('ListButton_noActive')[1]) - fireEvent.click( - screen.getByRole('label', { name: 'Corning 384 Well Plate' }) - ) - // set labware - expect(vi.mocked(selectLabware)).toHaveBeenCalled() + expect(props.setAreCategoriesExpanded).toBeCalled() }) it('renders deck slot and selects an adapter and labware', () => { vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ @@ -102,23 +99,10 @@ describe('LabwareTools', () => { selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, }) render(props) - screen.getByText('Adapter') + screen.getByText('Adapters') fireEvent.click(screen.getAllByTestId('ListButton_noActive')[4]) // set adapter - fireEvent.click( - screen.getByRole('label', { - name: 'Fixture Opentrons Universal Flat Heater-Shaker Adapter', - }) - ) - // set labware - screen.getByText('Adapter compatible labware') - screen.getByText('Fixture Corning 96 Well Plate 360 µL Flat') - fireEvent.click( - screen.getByRole('label', { - name: 'Fixture Corning 96 Well Plate 360 µL Flat', - }) - ) - expect(vi.mocked(selectNestedLabware)).toHaveBeenCalled() + expect(props.setAreCategoriesExpanded).toBeCalled() }) it('renders the custom labware flow', () => { diff --git a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts index 53571367f8b..e1acb64424d 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts @@ -56,6 +56,8 @@ export const ORDERED_CATEGORIES: string[] = [ 'aluminumBlock', 'adapter', ] +export const CUSTOM_CATEGORY = 'custom' +export const ALL_ORDERED_CATEGORIES = [CUSTOM_CATEGORY, ...ORDERED_CATEGORIES] export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { [TEMPERATURE_MODULE_TYPE]: [ diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index 524a19dfc1c..7a1c7c09be3 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -118,18 +118,16 @@ export const getLabwareIsRecommended = ( ): boolean => { // special-casing the thermocycler module V2 recommended labware since the thermocyclerModuleTypes // have different recommended labware - const moduleType = moduleModel != null ? getModuleType(moduleModel) : null - if (moduleModel === THERMOCYCLER_MODULE_V2) { - return ( - def.parameters.loadName === 'opentrons_96_wellplate_200ul_pcr_full_skirt' - ) - } else { - return moduleType != null - ? RECOMMENDED_LABWARE_BY_MODULE[moduleType].includes( - def.parameters.loadName - ) - : false + if (moduleModel == null) { + // permissive early exit if no module passed + return true } + const moduleType = getModuleType(moduleModel) + return moduleModel === THERMOCYCLER_MODULE_V2 + ? def.parameters.loadName === 'opentrons_96_wellplate_200ul_pcr_full_skirt' + : RECOMMENDED_LABWARE_BY_MODULE[moduleType].includes( + def.parameters.loadName + ) } export const getLabwareCompatibleWithAdapter = ( From 54a40176935133efdf94119153d46659563e8671 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 20 Nov 2024 11:01:34 -0500 Subject: [PATCH 20/52] feat(shared-data): add peak shared data definition (#16876) # Overview This new pipette type features a different threaded rod drive nut and some other mechanical changes that increase the maximum speed that the plunger can move so that this pipette can be used in emulsification applications. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- shared-data/command/schemas/11.json | 1 + .../eight_channel_emulsify/p1000/3_0.json | 320 ++++++++++++++++++ .../eight_channel_emulsify/p1000/3_0.json | 55 +++ .../p1000/placeholder.gltf | 0 .../p1000/default/3_0.json | 236 +++++++++++++ .../pipette/load_data.py | 2 + .../pipette/pipette_load_name_conversions.py | 12 +- .../scripts/update_configuration_files.py | 1 + .../opentrons_shared_data/pipette/types.py | 2 + .../pipette/test_max_flow_rates_per_volume.py | 1 + .../tests/pipette/test_validate_schema.py | 3 + 11 files changed, 630 insertions(+), 3 deletions(-) create mode 100644 shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json create mode 100644 shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json create mode 100644 shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf create mode 100644 shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 27de3c75b54..37e59f9ef54 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -2690,6 +2690,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_emulsify", "p1000_96", "p200_96" ], diff --git a/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json new file mode 100644 index 00000000000..0d68704a00a --- /dev/null +++ b/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json @@ -0,0 +1,320 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", + "displayName": "FLEX 8-Channel Emulsifying 1000 μL", + "model": "p1000", + "displayCategory": "FLEX", + "validNozzleMaps": { + "maps": { + "SingleA1": ["A1"], + "SingleH1": ["H1"], + "H1toG1": ["G1", "H1"], + "H1toF1": ["F1", "G1", "H1"], + "H1toE1": ["E1", "F1", "G1", "H1"], + "H1toD1": ["D1", "E1", "F1", "G1", "H1"], + "H1toC1": ["C1", "D1", "E1", "F1", "G1", "H1"], + "H1toB1": ["B1", "C1", "D1", "E1", "F1", "G1", "H1"], + "Full": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + }, + "pickUpTipConfigurations": { + "pressFit": { + "presses": 1, + "increment": 0.0, + "configurationsByNozzleMap": { + "SingleA1": { + "default": { + "speed": 10.0, + "distance": 11.0, + "current": 0.15, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 10.08, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.26, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.08, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.26 + } + } + } + }, + "SingleH1": { + "default": { + "speed": 10.0, + "distance": 11.0, + "current": 0.15, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.52, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.2, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.71, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.2 + } + } + } + }, + "H1toG1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.2, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.24, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.73, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.52, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.24, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.73 + } + } + } + }, + "H1toF1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.2, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toE1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.35, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toD1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.4, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toC1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.4, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.2, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.8, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.3, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.2, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.8 + } + } + } + }, + "H1toB1": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.5, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 9.13, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.9, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.23, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.13, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.9 + } + } + } + }, + "Full": { + "default": { + "speed": 10.0, + "distance": 13.0, + "current": 0.55, + "tipOverlaps": { + "v0": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.17, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 10.1, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 10.05, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 10.17 + }, + "v1": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.42, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.27, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.42, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.27, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + }, + "v3": { + "default": 9.28, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 9.67, + "opentrons/opentrons_flex_96_filtertiprack_200ul/1": 9.37, + "opentrons/opentrons_flex_96_filtertiprack_50ul/1": 9.28, + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1": 9.67 + } + } + } + } + } + } + }, + "dropTipConfigurations": { + "plungerEject": { + "current": 1.0, + "speed": 10 + } + }, + "plungerMotorConfigurations": { + "idle": 0.3, + "run": 1.0 + }, + "plungerPositionsConfigurations": { + "default": { + "top": 0.0, + "bottom": 71.5, + "blowout": 76.5, + "drop": 91.5 + } + }, + "availableSensors": { + "sensors": ["capacitive", "environment"], + "capacitive": { + "count": 2 + }, + "environment": { + "count": 1 + } + }, + "partialTipConfigurations": { + "partialTipSupported": true, + "availableConfigurations": [1, 2, 3, 4, 5, 6, 7, 8] + }, + "backCompatNames": [], + "channels": 8, + "shaftDiameter": 4.5, + "shaftULperMM": 15.904, + "backlashDistance": 0.1, + "quirks": [], + "plungerHomingConfigurations": { + "current": 1.0, + "speed": 30 + } +} diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json new file mode 100644 index 00000000000..d464cd5b9fe --- /dev/null +++ b/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json @@ -0,0 +1,55 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", + "pathTo3D": "pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf", + "nozzleOffset": [-8.0, -16.0, -259.15], + "pipetteBoundingBoxOffsets": { + "backLeftCorner": [-38.5, 0.0, -259.15], + "frontRightCorner": [11.5, -95.0, -259.15] + }, + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], + "nozzleMap": { + "A1": [-8.0, -16.0, -259.15], + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] + }, + "lldSettings": { + "t50": { + "minHeight": 1.0, + "minVolume": 0 + }, + "t200": { + "minHeight": 1.0, + "minVolume": 0 + }, + "t1000": { + "minHeight": 1.5, + "minVolume": 0 + } + } +} diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf b/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json new file mode 100644 index 00000000000..95292a3f98b --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json @@ -0,0 +1,236 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "uiMaxFlowRate": 802.9, + "defaultAspirateFlowRate": { + "default": 478, + "valuesByApiLevel": { "2.14": 478 } + }, + "defaultDispenseFlowRate": { + "default": 478, + "valuesByApiLevel": { "2.14": 478 } + }, + "defaultBlowOutFlowRate": { + "default": 478, + "valuesByApiLevel": { "2.14": 478 } + }, + "defaultFlowAcceleration": 24000.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.12, -57.973785, 8.495981], + [0.11, 40.31047, -3.298129], + [0.09, 19.330223, -0.990302], + [0.375, 6.200306, 0.19139], + [1.17, 4.795927, 0.718032], + [1.92, 2.746428, 3.115947], + [2.145, 1.592373, 5.331732], + [2.4, 1.336497, 5.880586], + [2.66, 1.043996, 6.582588], + [2.84, 0.280189, 8.614315], + [2.985, -0.698973, 11.395134], + [3.085, -5.627462, 26.106674], + [3.625, 1.899561, 2.885808], + [4.43, 1.977851, 2.602006], + [5.155, 0.596916, 8.71955], + [6.71, 0.366092, 9.909446], + [8.62, 0.233878, 10.796602], + [11.015, 0.158281, 11.448248], + [13.97, 0.101002, 12.079177], + [17.545, 0.047056, 12.832813], + [22.075, 0.043416, 12.896662], + [27.955, 0.049456, 12.763333], + [34.695, 0.00096, 14.119053], + [43.535, 0.018347, 13.515795], + [54.08, 0.001949, 14.229706] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.12, -57.973785, 8.495981], + [0.11, 40.31047, -3.298129], + [0.09, 19.330223, -0.990302], + [0.375, 6.200306, 0.19139], + [1.17, 4.795927, 0.718032], + [1.92, 2.746428, 3.115947], + [2.145, 1.592373, 5.331732], + [2.4, 1.336497, 5.880586], + [2.66, 1.043996, 6.582588], + [2.84, 0.280189, 8.614315], + [2.985, -0.698973, 11.395134], + [3.085, -5.627462, 26.106674], + [3.625, 1.899561, 2.885808], + [4.43, 1.977851, 2.602006], + [5.155, 0.596916, 8.71955], + [6.71, 0.366092, 9.909446], + [8.62, 0.233878, 10.796602], + [11.015, 0.158281, 11.448248], + [13.97, 0.101002, 12.079177], + [17.545, 0.047056, 12.832813], + [22.075, 0.043416, 12.896662], + [27.955, 0.049456, 12.763333], + [34.695, 0.00096, 14.119053], + [43.535, 0.018347, 13.515795], + [54.08, 0.001949, 14.229706] + ] + } + }, + "defaultPushOutVolume": 7 + }, + "t200": { + "uiMaxFlowRate": 847.9, + "defaultAspirateFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultDispenseFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultBlowOutFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultFlowAcceleration": 24000.0, + "defaultTipLength": 58.35, + "defaultReturnTipHeight": 0.71, + "aspirate": { + "default": { + "1": [ + [0.28375, -141.180627, 42.499381], + [0.26125, 27.065799, -5.240543], + [0.715, 4.916546, 0.54595], + [1.685, 3.844391, 1.31254], + [2.6025, 2.148973, 4.169319], + [3.75875, 1.461751, 5.957816], + [4.9975, 0.733738, 8.694235], + [6.41375, 0.377599, 10.474036], + [8.1225, 0.214926, 11.517382], + [10.2425, 0.152451, 12.024835], + [12.80125, 0.081051, 12.75615], + [15.9875, 0.062849, 12.989161], + [19.9625, 0.051585, 13.169235], + [24.83625, 0.030593, 13.588301], + [30.89125, 0.024593, 13.737307], + [38.42625, 0.020128, 13.875257], + [47.71875, 0.014091, 14.107204], + [59.28375, 0.011625, 14.224918], + [73.41375, 0.00635, 14.537608], + [90.84375, 0.004458, 14.676515], + [112.32, 0.003084, 14.801312], + [138.7675, 0.002045, 14.917998], + [171.29875, 0.001319, 15.018758], + [211.27375, 0.000719, 15.121662] + ] + } + }, + "dispense": { + "default": { + "1": [ + [0.28375, -141.180627, 42.499381], + [0.26125, 27.065799, -5.240543], + [0.715, 4.916546, 0.54595], + [1.685, 3.844391, 1.31254], + [2.6025, 2.148973, 4.169319], + [3.75875, 1.461751, 5.957816], + [4.9975, 0.733738, 8.694235], + [6.41375, 0.377599, 10.474036], + [8.1225, 0.214926, 11.517382], + [10.2425, 0.152451, 12.024835], + [12.80125, 0.081051, 12.75615], + [15.9875, 0.062849, 12.989161], + [19.9625, 0.051585, 13.169235], + [24.83625, 0.030593, 13.588301], + [30.89125, 0.024593, 13.737307], + [38.42625, 0.020128, 13.875257], + [47.71875, 0.014091, 14.107204], + [59.28375, 0.011625, 14.224918], + [73.41375, 0.00635, 14.537608], + [90.84375, 0.004458, 14.676515], + [112.32, 0.003084, 14.801312], + [138.7675, 0.002045, 14.917998], + [171.29875, 0.001319, 15.018758], + [211.27375, 0.000719, 15.121662] + ] + } + }, + "defaultPushOutVolume": 5 + }, + "t1000": { + "uiMaxFlowRate": 744.6, + "defaultAspirateFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultDispenseFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultBlowOutFlowRate": { + "default": 716, + "valuesByApiLevel": { "2.14": 716 } + }, + "defaultFlowAcceleration": 24000.0, + "defaultTipLength": 95.6, + "defaultReturnTipHeight": 0.82, + "aspirate": { + "default": { + "1": [ + [2.1443, 1.9858, 4.2677], + [3.0286, 1.2526, 5.84], + [4.9557, 0.6268, 7.7351], + [9.7943, 0.2745, 9.4811], + [12.1514, 0.1715, 10.4901], + [14.9414, 0.0897, 11.4833], + [51.46, 0.0424, 12.1913], + [92.68, 0.0095, 13.881], + [112.4886, 0.0049, 14.3053], + [243.5986, 0.0028, 14.5507], + [356.5686, 0.0009, 15.0019], + [430.99, 0.0005, 15.1492], + [628.7886, 0.0003, 15.2496], + [1001.15, 0.0001, 15.3472], + [1106.0857, 0.0001, 15.3551] + ] + } + }, + "dispense": { + "default": { + "1": [ + [2.1443, 1.9858, 4.2677], + [3.0286, 1.2526, 5.84], + [4.9557, 0.6268, 7.7351], + [9.7943, 0.2745, 9.4811], + [12.1514, 0.1715, 10.4901], + [14.9414, 0.0897, 11.4833], + [51.46, 0.0424, 12.1913], + [92.68, 0.0095, 13.881], + [112.4886, 0.0049, 14.3053], + [243.5986, 0.0028, 14.5507], + [356.5686, 0.0009, 15.0019], + [430.99, 0.0005, 15.1492], + [628.7886, 0.0003, 15.2496], + [1001.15, 0.0001, 15.3472], + [1106.0857, 0.0001, 15.3551] + ] + } + }, + "defaultPushOutVolume": 20 + } + }, + "maxVolume": 1000, + "minVolume": 5, + "defaultTipracks": [ + "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "opentrons/opentrons_flex_96_tiprack_200ul/1", + "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + ] +} diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index f8c361cca0c..fb121725c37 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -114,11 +114,13 @@ def load_serial_lookup_table() -> Dict[str, str]: "eight_channel": "M", "single_channel": "S", "ninety_six_channel": "H", + "eight_channel_emulsify": "P", } _channel_model_str = { "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_emulsify": "multi_emulsify", } _model_shorthand = {"p1000": "p1k", "p300": "p3h"} for channel_dir in _dirs_in(config_path): diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py b/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py index 244016d5943..f5113cff9e7 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_load_name_conversions.py @@ -80,7 +80,7 @@ def channels_from_string(channels: str) -> PipetteChannelType: """ if channels == "96": return PipetteChannelType.NINETY_SIX_CHANNEL - elif channels == "multi": + elif "multi" in channels: return PipetteChannelType.EIGHT_CHANNEL elif channels == "single": return PipetteChannelType.SINGLE_CHANNEL @@ -287,8 +287,14 @@ def convert_pipette_model( # We need to figure out how to default the pipette model as well # rather than returning a p1000 if model and not provided_version: - pipette_type, parsed_channels, parsed_version = model.split("_") - channels = channels_from_string(parsed_channels) + # pipette_type, parsed_channels, parsed_version = model.split("_") + exploded = model.split("_") + if len(exploded) == 3: + (pipette_type, parsed_channels, parsed_version) = exploded + channels = channels_from_string(parsed_channels) + else: + pipette_type, parsed_channels, parsed_oem, parsed_version = exploded + channels = channels_from_string(f"{parsed_channels}_{parsed_oem}") version = version_from_string(parsed_version) elif model and provided_version: pipette_type, parsed_channels = model.split("_") diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py index e787ac2a1cf..c1e03d5ab9d 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py @@ -355,6 +355,7 @@ def _update_all_models(configuration_to_update: List[str]) -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_emulsify": "multi_emulsify", } for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/opentrons_shared_data/pipette/types.py b/shared-data/python/opentrons_shared_data/pipette/types.py index 85ad1a81f01..33164904d97 100644 --- a/shared-data/python/opentrons_shared_data/pipette/types.py +++ b/shared-data/python/opentrons_shared_data/pipette/types.py @@ -216,6 +216,7 @@ def dict_for_encode(self) -> bool: "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_emulsify", "p1000_96", "p200_96", ] @@ -242,6 +243,7 @@ class PipetteNameType(str, enum.Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" + P1000_MULTI_EMULSIFY = "p1000_multi_emulsify" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py index b64f0a0b5c4..c5e9cc49604 100644 --- a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py +++ b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py @@ -49,6 +49,7 @@ def get_all_pipette_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_emulsify": "multi_emulsify", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/tests/pipette/test_validate_schema.py b/shared-data/python/tests/pipette/test_validate_schema.py index 0b703504957..5d3080dbd7a 100644 --- a/shared-data/python/tests/pipette/test_validate_schema.py +++ b/shared-data/python/tests/pipette/test_validate_schema.py @@ -22,6 +22,7 @@ def iterate_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_emulsify": "multi_emulsify", } defn_root = get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" assert os.listdir(defn_root), "A path is wrong" @@ -63,6 +64,7 @@ def test_pick_up_configs_configuration_by_nozzle_map_keys() -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_emulsify": "multi_emulsify", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): @@ -105,6 +107,7 @@ def test_pick_up_configs_configuration_ordered_from_smallest_to_largest() -> Non "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", + "eight_channel_emulsify": "multi_emulsify", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): From 24dc790d6485cbf799abaacaa6ad96a709e0fa26 Mon Sep 17 00:00:00 2001 From: syao1226 <146495172+syao1226@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:35:39 -0500 Subject: [PATCH 21/52] fix(protocol-designer): switching pipettes when source and/or dest labware fields are unselected (#16894) fix RQA-3622 # Overview updating the `updatePatchOnPipetteChannelChange` in `dependentFieldsUpdateMoveLiquid` to allow switching from a multi-channel to a single channel pipette when source and destination labwares field are empty. ## Test Plan and Hands on Testing - add a transfer step with no labware on deck - switch pipettes correctly responds when labware fields are empty. ## Changelog ## Review requests ## Risk assessment --------- Co-authored-by: shiyaochen --- .../dependentFieldsUpdateMoveLiquid.ts | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts index c473af451ea..d7a35e4ec59 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts @@ -543,29 +543,30 @@ const updatePatchOnPipetteChannelChange = ( const sourceLabwareId: string = appliedPatch.aspirate_labware as string const destLabwareId: string = appliedPatch.dispense_labware as string const sourceLabware = labwareEntities[sourceLabwareId] - const sourceLabwareDef = sourceLabware.def const destLabware = labwareEntities[destLabwareId] - update = { - aspirate_wells: getAllWellsFromPrimaryWells( - appliedPatch.aspirate_wells as string[], - sourceLabwareDef, - channels as 8 | 96 - ), - dispense_wells: - destLabwareId.includes('trashBin') || - destLabwareId.includes('wasteChute') - ? getDefaultWells({ - labwareId: destLabwareId, - pipetteId, - labwareEntities, - pipetteEntities, - }) - : getAllWellsFromPrimaryWells( - appliedPatch.dispense_wells as string[], - destLabware.def, - channels as 8 | 96 - ), + if (sourceLabwareId != null && destLabwareId != null) { + update = { + aspirate_wells: getAllWellsFromPrimaryWells( + appliedPatch.aspirate_wells as string[], + sourceLabware.def, + channels as 8 | 96 + ), + dispense_wells: + destLabwareId.includes('trashBin') || + destLabwareId.includes('wasteChute') + ? getDefaultWells({ + labwareId: destLabwareId, + pipetteId, + labwareEntities, + pipetteEntities, + }) + : getAllWellsFromPrimaryWells( + appliedPatch.dispense_wells as string[], + destLabware.def, + channels as 8 | 96 + ), + } } } From b365e2742f2522191d7b48b820d501e9891e7478 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:53:53 -0500 Subject: [PATCH 22/52] fix(protocol-designer): add scroll to top functionality between parts (#16906) Scroll to top when continuing on a multi-part step form Closes RQA-3555 --- .../pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index 5b58ac60c5a..2088409227e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -254,8 +254,8 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { setShowFormErrors(false) } else { setShowFormErrors(true) - handleScrollToTop() } + handleScrollToTop() } else { handleSaveClick() } @@ -304,6 +304,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { onClick={() => { setToolboxStep(0) setShowFormErrors(false) + handleScrollToTop() }} > {i18n.format(t('shared:back'), 'capitalize')} From db8b1e5835abbaf97b41ba90404471fce34a2148 Mon Sep 17 00:00:00 2001 From: Laura Cox <31892318+Laura-Danielle@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:10:43 +0200 Subject: [PATCH 23/52] feat(api): RobotContext: Add pipette helper functions to convert volume and position type (#16682) --- .../opentrons/hardware_control/dev_types.py | 2 + .../instruments/ot2/pipette.py | 20 +--- .../instruments/ot2/pipette_handler.py | 7 ++ .../instruments/ot3/pipette.py | 27 ++--- .../instruments/ot3/pipette_handler.py | 7 ++ api/src/opentrons/protocol_api/__init__.py | 21 +++- api/src/opentrons/protocol_api/_types.py | 24 ++++ .../protocol_api/core/engine/robot.py | 54 ++++++++- .../core/legacy/legacy_protocol_core.py | 1 - api/src/opentrons/protocol_api/core/robot.py | 20 +++- .../opentrons/protocol_api/robot_context.py | 42 ++++++- .../protocol_engine/execution/gantry_mover.py | 6 +- .../resources/pipette_data_provider.py | 12 ++ .../protocol_engine/state/pipettes.py | 36 ++++++ api/src/opentrons/types.py | 5 + .../protocol_api/test_robot_context.py | 80 ++++++++++++- .../commands/test_configure_for_volume.py | 7 ++ .../commands/test_load_pipette.py | 14 +++ .../execution/test_equipment_handler.py | 7 ++ .../resources/test_pipette_data_provider.py | 32 +++++ .../state/test_geometry_view.py | 7 ++ .../state/test_pipette_store.py | 21 ++++ .../state/test_pipette_view.py | 49 ++++++++ .../protocol_engine/state/test_tip_state.py | 112 ++++++++++++++++++ .../pipette/ul_per_mm.py | 39 +++++- 25 files changed, 601 insertions(+), 51 deletions(-) diff --git a/api/src/opentrons/hardware_control/dev_types.py b/api/src/opentrons/hardware_control/dev_types.py index a6773cb9184..575a5e612d9 100644 --- a/api/src/opentrons/hardware_control/dev_types.py +++ b/api/src/opentrons/hardware_control/dev_types.py @@ -100,6 +100,8 @@ class PipetteDict(InstrumentDict): pipette_bounding_box_offsets: PipetteBoundingBoxOffsetDefinition current_nozzle_map: NozzleMap lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float class PipetteStateDict(TypedDict): diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 7fc15c4c2d3..2d63342cf19 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -28,7 +28,7 @@ CommandPreconditionViolated, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -584,21 +584,9 @@ def get_nominal_tip_overlap_dictionary_by_configuration( # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, action, self._active_tip_settings, self._pipetting_function_version + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 931c99fd4c6..7bd41e02e74 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -260,6 +260,13 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 109747ea1b9..5a4d9261bfd 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -27,7 +27,7 @@ InvalidInstrumentData, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -529,23 +529,13 @@ def tip_presence_responses(self) -> int: # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - elif action == "blowout": - return self._config.shaft_ul_per_mm - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, + action, + self._active_tip_settings, + self._pipetting_function_version, + self._config.shaft_ul_per_mm, + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( @@ -585,6 +575,7 @@ def as_dict(self) -> "Pipette.DictType": "versioned_tip_overlap": self.tip_overlap, "back_compat_names": self._config.pipette_backcompat_names, "supported_tips": self.liquid_class.supported_tips, + "shaft_ul_per_mm": self._config.shaft_ul_per_mm, } ) return self._config_as_dict diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index f64078fcbff..dda5031a8a3 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -282,6 +282,13 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 2f35bb46764..41a061f5a94 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -30,7 +30,16 @@ ) from .disposal_locations import TrashBin, WasteChute from ._liquid import Liquid, LiquidClass -from ._types import OFF_DECK +from ._types import ( + OFF_DECK, + PLUNGER_BLOWOUT, + PLUNGER_TOP, + PLUNGER_BOTTOM, + PLUNGER_DROPTIP, + ASPIRATE_ACTION, + DISPENSE_ACTION, + BLOWOUT_ACTION, +) from ._nozzle_layout import ( COLUMN, PARTIAL_COLUMN, @@ -69,12 +78,22 @@ "Liquid", "LiquidClass", "Parameters", + # Partial Tip types "COLUMN", "PARTIAL_COLUMN", "SINGLE", "ROW", "ALL", + # Deck location types "OFF_DECK", + # Pipette plunger types + "PLUNGER_BLOWOUT", + "PLUNGER_TOP", + "PLUNGER_BOTTOM", + "PLUNGER_DROPTIP", + "ASPIRATE_ACTION", + "DISPENSE_ACTION", + "BLOWOUT_ACTION", "RuntimeParameterRequiredError", "CSVParameter", # For internal Opentrons use only: diff --git a/api/src/opentrons/protocol_api/_types.py b/api/src/opentrons/protocol_api/_types.py index 9890e29c2bc..0e73405b3b7 100644 --- a/api/src/opentrons/protocol_api/_types.py +++ b/api/src/opentrons/protocol_api/_types.py @@ -17,3 +17,27 @@ class OffDeckType(enum.Enum): See :ref:`off-deck-location` for details on using ``OFF_DECK`` with :py:obj:`ProtocolContext.move_labware()`. """ + + +class PlungerPositionTypes(enum.Enum): + PLUNGER_TOP = "top" + PLUNGER_BOTTOM = "bottom" + PLUNGER_BLOWOUT = "blow_out" + PLUNGER_DROPTIP = "drop_tip" + + +PLUNGER_TOP: Final = PlungerPositionTypes.PLUNGER_TOP +PLUNGER_BOTTOM: Final = PlungerPositionTypes.PLUNGER_BOTTOM +PLUNGER_BLOWOUT: Final = PlungerPositionTypes.PLUNGER_BLOWOUT +PLUNGER_DROPTIP: Final = PlungerPositionTypes.PLUNGER_DROPTIP + + +class PipetteActionTypes(enum.Enum): + ASPIRATE_ACTION = "aspirate" + DISPENSE_ACTION = "dispense" + BLOWOUT_ACTION = "blowout" + + +ASPIRATE_ACTION: Final = PipetteActionTypes.ASPIRATE_ACTION +DISPENSE_ACTION: Final = PipetteActionTypes.DISPENSE_ACTION +BLOWOUT_ACTION: Final = PipetteActionTypes.BLOWOUT_ACTION diff --git a/api/src/opentrons/protocol_api/core/engine/robot.py b/api/src/opentrons/protocol_api/core/engine/robot.py index 477f1968c5a..df80917e091 100644 --- a/api/src/opentrons/protocol_api/core/engine/robot.py +++ b/api/src/opentrons/protocol_api/core/engine/robot.py @@ -1,13 +1,16 @@ -from typing import Optional, Dict +from typing import Optional, Dict, Union from opentrons.hardware_control import SyncHardwareAPI from opentrons.types import Mount, MountType, Point, AxisType, AxisMapType +from opentrons_shared_data.pipette import types as pip_types +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_engine.types import DeckPoint, MotorAxis from opentrons.protocol_api.core.robot import AbstractRobot + _AXIS_TYPE_TO_MOTOR_AXIS = { AxisType.X: MotorAxis.X, AxisType.Y: MotorAxis.Y, @@ -39,12 +42,57 @@ def __init__( def _convert_to_engine_mount(self, axis_map: AxisMapType) -> Dict[MotorAxis, float]: return {_AXIS_TYPE_TO_MOTOR_AXIS[ax]: dist for ax, dist in axis_map.items()} - def get_pipette_type_from_engine(self, mount: Mount) -> Optional[str]: + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[pip_types.PipetteNameType]: """Get the pipette attached to the given mount.""" - engine_mount = MountType[mount.name] + if isinstance(mount, Mount): + engine_mount = MountType[mount.name] + else: + if mount.lower() == "right": + engine_mount = MountType.RIGHT + else: + engine_mount = MountType.LEFT maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) return maybe_pipette.pipetteName if maybe_pipette else None + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + return 0.0 + return self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, position_name.value + ) + + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + raise RuntimeError( + f"Cannot load plunger position as no pipette is attached to {mount}" + ) + convert_volume = ( + self._engine_client.state.pipettes.lookup_volume_to_mm_conversion( + maybe_pipette.id, volume, action.value + ) + ) + plunger_bottom = ( + self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, "bottom" + ) + ) + mm = volume / convert_volume + if robot_type == "OT-2 Standard": + position = plunger_bottom + mm + else: + position = plunger_bottom - mm + return round(position, 6) + def move_to(self, mount: Mount, destination: Point, speed: Optional[float]) -> None: engine_mount = MountType[mount.name] engine_destination = DeckPoint( diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index e672a6fe839..d0b95ed82ca 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -37,7 +37,6 @@ class LegacyProtocolCore( LegacyInstrumentCore, LegacyLabwareCore, legacy_module_core.LegacyModuleCore, - # None, ] ): def __init__( diff --git a/api/src/opentrons/protocol_api/core/robot.py b/api/src/opentrons/protocol_api/core/robot.py index 7eade528413..95def3e17f3 100644 --- a/api/src/opentrons/protocol_api/core/robot.py +++ b/api/src/opentrons/protocol_api/core/robot.py @@ -1,12 +1,28 @@ from abc import abstractmethod, ABC -from typing import Optional +from typing import Optional, Union from opentrons.types import AxisMapType, Mount, Point +from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons.protocol_api._types import PlungerPositionTypes, PipetteActionTypes class AbstractRobot(ABC): @abstractmethod - def get_pipette_type_from_engine(self, mount: Mount) -> Optional[str]: + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[PipetteNameType]: + ... + + @abstractmethod + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + ... + + @abstractmethod + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: ... @abstractmethod diff --git a/api/src/opentrons/protocol_api/robot_context.py b/api/src/opentrons/protocol_api/robot_context.py index 272330e1664..5b0e578f9bb 100644 --- a/api/src/opentrons/protocol_api/robot_context.py +++ b/api/src/opentrons/protocol_api/robot_context.py @@ -19,6 +19,7 @@ from .core.common import ProtocolCore, RobotCore from .module_contexts import ModuleContext from .labware import Labware +from ._types import PipetteActionTypes, PlungerPositionTypes class HardwareManager(NamedTuple): @@ -200,14 +201,43 @@ def axis_coordinates_for( raise TypeError("You must specify a location to move to.") def plunger_coordinates_for_volume( - self, mount: Union[Mount, str], volume: float - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], volume: float, action: PipetteActionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from volume. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + + pipette_position = self._core.get_plunger_position_from_volume( + mount, volume, action, self._protocol_core.robot_type + ) + return {pipette_axis: pipette_position} def plunger_coordinates_for_named_position( - self, mount: Union[Mount, str], position_name: str - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], position_name: PlungerPositionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from position_name. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + pipette_position = self._core.get_plunger_position_from_name( + mount, position_name + ) + return {pipette_axis: pipette_position} def build_axis_map(self, axis_map: StringAxisMap) -> AxisMapType: """Take in a :py:class:`.types.StringAxisMap` and output a :py:class:`.types.AxisMapType`. diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index 7306bc4e4d1..c77a9e1bad2 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -64,6 +64,7 @@ HardwareAxis.Q: MotorAxis.AXIS_96_CHANNEL_CAM, } + # The height of the bottom of the pipette nozzle at home position without any tips. # We rely on this being the same for every OT-3 pipette. # @@ -305,7 +306,6 @@ async def move_mount_to( ) -> Point: """Move the given hardware mount to a waypoint.""" assert len(waypoints) > 0, "Must have at least one waypoint" - log.info(f"Moving mount {mount}") for waypoint in waypoints: log.info(f"The current waypoint moving is {waypoint}") await self._hardware_api.move_to( @@ -340,6 +340,10 @@ async def move_axes( mount, refresh=True ) log.info(f"The current position of the robot is: {current_position}.") + converted_current_position_deck = ( + self._hardware_api.get_deck_from_machine(current_position) + ) + log.info(f"The current position of the robot is: {current_position}.") pos_hw = target_axis_map_from_relative(pos_hw, current_position) log.info( diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index d3998c69bd1..6387bf5dcf1 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -67,6 +67,8 @@ class LoadedStaticPipetteData: back_left_corner_offset: Point front_right_corner_offset: Point pipette_lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float class VirtualPipetteDataProvider: @@ -252,6 +254,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_back_left = config.pipette_bounding_box_offsets.back_left_corner pip_front_right = config.pipette_bounding_box_offsets.front_right_corner + plunger_positions = config.plunger_positions_configurations[liquid_class] return LoadedStaticPipetteData( model=str(pipette_model), display_name=config.display_name, @@ -280,6 +283,13 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_front_right[0], pip_front_right[1], pip_front_right[2] ), pipette_lld_settings=config.lld_settings, + plunger_positions={ + "top": plunger_positions.top, + "bottom": plunger_positions.bottom, + "blow_out": plunger_positions.blow_out, + "drop_tip": plunger_positions.drop_tip, + }, + shaft_ul_per_mm=config.shaft_ul_per_mm, ) def get_virtual_pipette_static_config( @@ -327,6 +337,8 @@ def get_pipette_static_config( front_right_offset[0], front_right_offset[1], front_right_offset[2] ), pipette_lld_settings=pipette_dict["lld_settings"], + plunger_positions=pipette_dict["plunger_positions"], + shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"], ) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index e0f2cef1155..d20b8665318 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -10,11 +10,15 @@ Mapping, Optional, Tuple, + cast, ) from typing_extensions import assert_never from opentrons_shared_data.pipette import pipette_definition +from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm +from opentrons_shared_data.pipette.types import UlPerMmAction + from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control import CriticalPoint @@ -99,6 +103,8 @@ class StaticPipetteConfig: bounding_nozzle_offsets: BoundingNozzlesOffsets default_nozzle_map: NozzleMap # todo(mm, 2024-10-14): unused, remove? lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float @dataclasses.dataclass @@ -288,6 +294,8 @@ def _update_pipette_config(self, state_update: update_types.StateUpdate) -> None ), default_nozzle_map=config.nozzle_map, lld_settings=config.pipette_lld_settings, + plunger_positions=config.plunger_positions, + shaft_ul_per_mm=config.shaft_ul_per_mm, ) self._state.flow_rates_by_id[ state_update.pipette_config.pipette_id @@ -772,3 +780,31 @@ def get_nozzle_configuration_supports_lld(self, pipette_id: str) -> bool: ): return False return True + + def lookup_volume_to_mm_conversion( + self, pipette_id: str, volume: float, action: str + ) -> float: + """Get the volumn to mm conversion for a pipette.""" + try: + lookup_volume = self.get_working_volume(pipette_id) + except errors.TipNotAttachedError: + lookup_volume = self.get_maximum_volume(pipette_id) + + pipette_config = self.get_config(pipette_id) + lookup_table_from_config = pipette_config.tip_configuration_lookup_table + try: + tip_settings = lookup_table_from_config[lookup_volume] + except KeyError: + tip_settings = list(lookup_table_from_config.values())[0] + return calculate_ul_per_mm( + volume, + cast(UlPerMmAction, action), + tip_settings, + shaft_ul_per_mm=pipette_config.shaft_ul_per_mm, + ) + + def lookup_plunger_position_name( + self, pipette_id: str, position_name: str + ) -> float: + """Get the plunger position provided for the given pipette id.""" + return self.get_config(pipette_id).plunger_positions[position_name] diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index fa57ce0dcd5..1f73d63c8c6 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -292,6 +292,11 @@ def mount_for_axis(cls, axis: "AxisType") -> Mount: } return map_mount_to_axis[axis] + @classmethod + def plunger_axis_for_mount(cls, mount: Mount) -> "AxisType": + map_plunger_axis_mount = {Mount.LEFT: cls.P_L, Mount.RIGHT: cls.P_R} + return map_plunger_axis_mount[mount] + @classmethod def ot2_axes(cls) -> List["AxisType"]: return [ diff --git a/api/tests/opentrons/protocol_api/test_robot_context.py b/api/tests/opentrons/protocol_api/test_robot_context.py index c1bdfe48c3f..36b94c52b15 100644 --- a/api/tests/opentrons/protocol_api/test_robot_context.py +++ b/api/tests/opentrons/protocol_api/test_robot_context.py @@ -17,6 +17,9 @@ from opentrons.protocol_api.core.common import ProtocolCore, RobotCore from opentrons.protocol_api import RobotContext, ModuleContext from opentrons.protocol_api.deck import Deck +from opentrons_shared_data.pipette.types import PipetteNameType + +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes @pytest.fixture @@ -58,7 +61,12 @@ def subject( api_version: APIVersion, ) -> RobotContext: """Get a RobotContext test subject with its dependencies mocked out.""" - decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return(None) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.RIGHT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) return RobotContext( core=mock_core, api_version=api_version, protocol_core=mock_protocol ) @@ -176,3 +184,73 @@ def test_get_axes_coordinates_for( """Test `RobotContext.get_axis_coordinates_for`.""" res = subject.axis_coordinates_for(mount, location_to_move) assert res == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "volume", "action", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, 200, PipetteActionTypes.ASPIRATE_ACTION, {AxisType.P_R: 100}), + (Mount.LEFT, 100, PipetteActionTypes.DISPENSE_ACTION, {AxisType.P_L: 100}), + ], +) +def test_plunger_coordinates_for_volume( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + volume: float, + action: PipetteActionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_volume`.""" + decoy.when( + subject._core.get_plunger_position_from_volume( + mount, volume, action, "OT-3 Standard" + ) + ).then_return(100) + + result = subject.plunger_coordinates_for_volume(mount, volume, action) + assert result == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "position_name", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, PlungerPositionTypes.PLUNGER_TOP, {AxisType.P_R: 3}), + ( + Mount.RIGHT, + PlungerPositionTypes.PLUNGER_BOTTOM, + {AxisType.P_R: 3}, + ), + ], +) +def test_plunger_coordinates_for_named_position( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + position_name: PlungerPositionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_named_position`.""" + decoy.when( + subject._core.get_plunger_position_from_name(mount, position_name) + ).then_return(3) + result = subject.plunger_coordinates_for_named_position(mount, position_name) + assert result == expected_axis_map + + +def test_plunger_methods_raise_without_pipette( + mock_core: RobotCore, mock_protocol: ProtocolCore, api_version: APIVersion +) -> None: + """Test that `RobotContext` plunger functions raise without pipette attached.""" + subject = RobotContext( + core=mock_core, api_version=api_version, protocol_core=mock_protocol + ) + with pytest.raises(ValueError): + subject.plunger_coordinates_for_named_position( + Mount.LEFT, PlungerPositionTypes.PLUNGER_TOP + ) + + with pytest.raises(ValueError): + subject.plunger_coordinates_for_volume( + Mount.LEFT, 200, PipetteActionTypes.ASPIRATE_ACTION + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py index d237c9e6090..9be08a0a71b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py @@ -63,6 +63,13 @@ async def test_configure_for_volume_implementation( back_left_corner_offset=Point(10, 20, 30), front_right_corner_offset=Point(40, 50, 60), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index a42bbc4e4d9..570666e9c98 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -69,6 +69,13 @@ async def test_load_pipette_implementation( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) decoy.when( @@ -137,6 +144,13 @@ async def test_load_pipette_implementation_96_channel( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index b7a020c2d35..3ee027c24c1 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -154,6 +154,13 @@ def loaded_static_pipette_data( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 086b3ec297b..cbf7fa6174e 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -65,6 +65,13 @@ def test_get_virtual_pipette_static_config( back_left_corner_offset=Point(0, 0, 10.45), front_right_corner_offset=Point(0, 0, 10.45), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -8.5, + "blow_out": -13.0, + "drop_tip": -27.0, + }, + shaft_ul_per_mm=0.785, ) @@ -94,6 +101,13 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 71.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, ) subject_instance.configure_virtual_pipette_for_volume( "my-pipette", 1, result1.model @@ -120,6 +134,13 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 61.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, ) @@ -149,6 +170,13 @@ def test_load_virtual_pipette_by_model_string( back_left_corner_offset=Point(-16.0, 43.15, 35.52), front_right_corner_offset=Point(16.0, -43.15, 35.52), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -14.5, + "blow_out": -19.0, + "drop_tip": -33.4, + }, + shaft_ul_per_mm=9.621, ) @@ -246,6 +274,8 @@ def pipette_dict( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + "plunger_positions": {"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + "shaft_ul_per_mm": 5.0, } @@ -292,6 +322,8 @@ def test_get_pipette_static_config( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + plunger_positions={"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + shaft_ul_per_mm=5.0, ) 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 42ee037c1ce..abfb31f5f2a 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -2611,6 +2611,13 @@ def test_get_next_drop_tip_location( back_right_corner=Point(x=40, y=20, z=60), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) ) decoy.when(mock_pipette_view.get_mount("pip-123")).then_return(pipette_mount) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index 31b1a7f3a2c..60c857e4911 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -220,6 +220,13 @@ def test_handles_load_pipette( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) config_update = update_types.PipetteConfigUpdate( pipette_id="pipette-id", @@ -772,6 +779,13 @@ def test_add_pipette_config( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) subject.handle_action( @@ -810,6 +824,13 @@ def test_add_pipette_config( back_right_corner=Point(x=4, y=2, z=3), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) assert subject.state.flow_rates_by_id["pipette-id"].default_aspirate == {"a": 1.0} assert subject.state.flow_rates_by_id["pipette-id"].default_dispense == {"b": 2.0} diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index 64e663a24e5..14c43bf70f6 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -291,6 +291,13 @@ def test_get_pipette_working_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) }, ) @@ -322,6 +329,13 @@ def test_get_pipette_working_volume_raises_if_tip_volume_is_none( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) }, ) @@ -364,6 +378,13 @@ def test_get_pipette_available_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), "pipette-id-none": StaticPipetteConfig( min_volume=1, @@ -380,6 +401,13 @@ def test_get_pipette_available_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), }, ) @@ -492,6 +520,13 @@ def test_get_static_config( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) subject = get_pipette_view( @@ -543,6 +578,13 @@ def test_get_nominal_tip_overlap( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) subject = get_pipette_view(static_config_by_id={"pipette-id": config}) @@ -967,6 +1009,13 @@ def test_get_pipette_bounds_at_location( bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, pipette_bounding_box_offsets=bounding_box_offsets, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ) }, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index abb408d7418..8abcc6a24e2 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -119,6 +119,13 @@ def test_get_next_tip_returns_none( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -177,6 +184,13 @@ def test_get_next_tip_returns_first_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -229,6 +243,13 @@ def test_get_next_tip_used_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -314,6 +335,13 @@ def test_get_next_tip_skips_picked_up_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -377,6 +405,13 @@ def test_get_next_tip_with_starting_tip( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -444,6 +479,13 @@ def test_get_next_tip_with_starting_tip_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -514,6 +556,13 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -545,6 +594,13 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -615,6 +671,13 @@ def test_get_next_tip_with_starting_tip_out_of_tips( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -685,6 +748,13 @@ def test_get_next_tip_with_column_and_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -734,6 +804,13 @@ def test_reset_tips( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) @@ -796,6 +873,13 @@ def test_handle_pipette_config_action( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -929,6 +1013,13 @@ def test_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -989,6 +1080,13 @@ def test_next_tip_uses_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -1087,6 +1185,13 @@ def test_next_tip_automatic_tip_tracking_with_partial_configurations( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( @@ -1239,6 +1344,13 @@ def test_next_tip_automatic_tip_tracking_tiprack_limits( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, ), ) subject.handle_action( diff --git a/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py index 3423f0f49e5..774231ac40d 100644 --- a/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py +++ b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py @@ -1,11 +1,46 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional -from opentrons_shared_data.pipette.pipette_definition import PipetteFunctionKeyType +from opentrons_shared_data.pipette.pipette_definition import ( + PipetteFunctionKeyType, + SupportedTipsDefinition, +) +from opentrons_shared_data.pipette.types import UlPerMmAction PIPETTING_FUNCTION_FALLBACK_VERSION: PipetteFunctionKeyType = "1" PIPETTING_FUNCTION_LATEST_VERSION: PipetteFunctionKeyType = "2" +def calculate_ul_per_mm( + ul: float, + action: UlPerMmAction, + active_tip_settings: SupportedTipsDefinition, + requested_pipetting_version: Optional[PipetteFunctionKeyType] = None, + shaft_ul_per_mm: Optional[float] = None, +) -> float: + assumed_requested_pipetting_version = ( + requested_pipetting_version + if requested_pipetting_version + else PIPETTING_FUNCTION_LATEST_VERSION + ) + if action == "aspirate": + fallback = active_tip_settings.aspirate.default[ + PIPETTING_FUNCTION_FALLBACK_VERSION + ] + sequence = active_tip_settings.aspirate.default.get( + assumed_requested_pipetting_version, fallback + ) + elif action == "blowout" and shaft_ul_per_mm: + return shaft_ul_per_mm + else: + fallback = active_tip_settings.dispense.default[ + PIPETTING_FUNCTION_FALLBACK_VERSION + ] + sequence = active_tip_settings.dispense.default.get( + assumed_requested_pipetting_version, fallback + ) + return piecewise_volume_conversion(ul, sequence) + + def piecewise_volume_conversion( ul: float, sequence: List[Tuple[float, float, float]] ) -> float: From df0f18cdffd5e3463918a9b3919f37bfda82ee44 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 20 Nov 2024 12:22:51 -0500 Subject: [PATCH 24/52] feat(hardware): add SN support for the P1KP pipette (#16907) # Overview This adds support on the python side for the new pipette so we can provision and load them ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../opentrons/hardware_control/backends/ot3controller.py | 1 + hardware/opentrons_hardware/firmware_bindings/constants.py | 1 + hardware/opentrons_hardware/instruments/pipettes/serials.py | 1 + .../tests/opentrons_hardware/instruments/test_serials.py | 6 ++++++ .../python/opentrons_shared_data/pipette/dev_types.py | 2 ++ 5 files changed, 11 insertions(+) diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 66ffc1efab1..627d6f5c424 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1008,6 +1008,7 @@ def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str: lookup_name = { FirmwarePipetteName.p1000_single: "P1KS", FirmwarePipetteName.p1000_multi: "P1KM", + FirmwarePipetteName.p1000_multi_emulsify: "P1KP", FirmwarePipetteName.p50_single: "P50S", FirmwarePipetteName.p50_multi: "P50M", FirmwarePipetteName.p1000_96: "P1KH", diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index d9dc98def39..435e13ab2a1 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -359,6 +359,7 @@ class PipetteName(int, Enum): p1000_96 = 0x04 p50_96 = 0x05 p200_96 = 0x06 + p1000_multi_emulsify = 0x07 unknown = 0xFFFF diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index c4a8fc441d0..a29366649cf 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -27,6 +27,7 @@ NAME_LOOKUP: Dict[str, PipetteName] = { "P1KS": PipetteName.p1000_single, "P1KM": PipetteName.p1000_multi, + "P1KP": PipetteName.p1000_multi_emulsify, "P50S": PipetteName.p50_single, "P50M": PipetteName.p50_multi, "P1KH": PipetteName.p1000_96, diff --git a/hardware/tests/opentrons_hardware/instruments/test_serials.py b/hardware/tests/opentrons_hardware/instruments/test_serials.py index 7b398eda286..4784ad9a08c 100644 --- a/hardware/tests/opentrons_hardware/instruments/test_serials.py +++ b/hardware/tests/opentrons_hardware/instruments/test_serials.py @@ -40,6 +40,12 @@ 1, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", ), + ( + "P1KPV30", + PipetteName.p1000_multi_emulsify, + 30, + b"\x00"*16, + ), ], ) def test_scan_valid_pipette_serials( diff --git a/shared-data/python/opentrons_shared_data/pipette/dev_types.py b/shared-data/python/opentrons_shared_data/pipette/dev_types.py index 00676e9be08..0b5b5672ca4 100644 --- a/shared-data/python/opentrons_shared_data/pipette/dev_types.py +++ b/shared-data/python/opentrons_shared_data/pipette/dev_types.py @@ -31,6 +31,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_emulsify", "p1000_96", "p200_96", ] @@ -57,6 +58,7 @@ class PipetteNameType(str, Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" + P1000_MULTI_EMULSIFY = "p1000_multi_emulsify" P1000_96 = "p1000_96" P200_96 = "p200_96" From 383936698d9b90dab0b218cba72a04436e1939e0 Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Wed, 20 Nov 2024 13:36:20 -0500 Subject: [PATCH 25/52] chore(shared-data): rename emulsify to em (#16911) # Overview In order to simplify and make future planning for how to display this OEM rename emulsify to EM everywhere for the new 8 channel ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../opentrons/hardware_control/backends/ot3controller.py | 2 +- hardware/opentrons_hardware/firmware_bindings/constants.py | 2 +- hardware/opentrons_hardware/instruments/pipettes/serials.py | 2 +- .../tests/opentrons_hardware/instruments/test_serials.py | 4 ++-- shared-data/command/schemas/11.json | 2 +- shared-data/js/constants.ts | 1 + .../p1000/3_0.json | 2 +- .../p1000/3_0.json | 2 +- .../p1000/placeholder.gltf | 0 .../p1000/default/3_0.json | 0 .../python/opentrons_shared_data/pipette/dev_types.py | 4 ++-- .../python/opentrons_shared_data/pipette/load_data.py | 4 ++-- .../pipette/scripts/update_configuration_files.py | 2 +- shared-data/python/opentrons_shared_data/pipette/types.py | 4 ++-- .../python/tests/pipette/test_max_flow_rates_per_volume.py | 2 +- shared-data/python/tests/pipette/test_validate_schema.py | 6 +++--- 16 files changed, 20 insertions(+), 19 deletions(-) rename shared-data/pipette/definitions/2/general/{eight_channel_emulsify => eight_channel_em}/p1000/3_0.json (99%) rename shared-data/pipette/definitions/2/geometry/{eight_channel_emulsify => eight_channel_em}/p1000/3_0.json (93%) rename shared-data/pipette/definitions/2/geometry/{eight_channel_emulsify => eight_channel_em}/p1000/placeholder.gltf (100%) rename shared-data/pipette/definitions/2/liquid/{eight_channel_emulsify => eight_channel_em}/p1000/default/3_0.json (100%) diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 627d6f5c424..1251fcc4adb 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1008,7 +1008,7 @@ def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str: lookup_name = { FirmwarePipetteName.p1000_single: "P1KS", FirmwarePipetteName.p1000_multi: "P1KM", - FirmwarePipetteName.p1000_multi_emulsify: "P1KP", + FirmwarePipetteName.p1000_multi_em: "P1KP", FirmwarePipetteName.p50_single: "P50S", FirmwarePipetteName.p50_multi: "P50M", FirmwarePipetteName.p1000_96: "P1KH", diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index 435e13ab2a1..ecdc8ae8c64 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -359,7 +359,7 @@ class PipetteName(int, Enum): p1000_96 = 0x04 p50_96 = 0x05 p200_96 = 0x06 - p1000_multi_emulsify = 0x07 + p1000_multi_em = 0x07 unknown = 0xFFFF diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index a29366649cf..c18772fe656 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -27,7 +27,7 @@ NAME_LOOKUP: Dict[str, PipetteName] = { "P1KS": PipetteName.p1000_single, "P1KM": PipetteName.p1000_multi, - "P1KP": PipetteName.p1000_multi_emulsify, + "P1KP": PipetteName.p1000_multi_em, "P50S": PipetteName.p50_single, "P50M": PipetteName.p50_multi, "P1KH": PipetteName.p1000_96, diff --git a/hardware/tests/opentrons_hardware/instruments/test_serials.py b/hardware/tests/opentrons_hardware/instruments/test_serials.py index 4784ad9a08c..2820b5ffbe5 100644 --- a/hardware/tests/opentrons_hardware/instruments/test_serials.py +++ b/hardware/tests/opentrons_hardware/instruments/test_serials.py @@ -42,9 +42,9 @@ ), ( "P1KPV30", - PipetteName.p1000_multi_emulsify, + PipetteName.p1000_multi_em, 30, - b"\x00"*16, + b"\x00" * 16, ), ], ) diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 37e59f9ef54..cc2202f850d 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -2690,7 +2690,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", - "p1000_multi_emulsify", + "p1000_multi_em", "p1000_96", "p200_96" ], diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 8772a5ab3b9..888d9f0c2f7 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -145,6 +145,7 @@ export const OT3_PIPETTES = [ 'p50_single_flex', 'p50_multi_flex', 'p1000_multi_flex', + 'p1000_multi_em_flex', 'p1000_96', 'p200_96', ] diff --git a/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json similarity index 99% rename from shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json rename to shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json index 0d68704a00a..c49ae20d87a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "FLEX 8-Channel Emulsifying 1000 μL", + "displayName": "FLEX 8-Channel EM 1000 μL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json similarity index 93% rename from shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json rename to shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json index d464cd5b9fe..b92e7415fe3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", - "pathTo3D": "pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf", + "pathTo3D": "pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], "pipetteBoundingBoxOffsets": { "backLeftCorner": [-38.5, 0.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf similarity index 100% rename from shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf rename to shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json similarity index 100% rename from shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json diff --git a/shared-data/python/opentrons_shared_data/pipette/dev_types.py b/shared-data/python/opentrons_shared_data/pipette/dev_types.py index 0b5b5672ca4..8ae367378f2 100644 --- a/shared-data/python/opentrons_shared_data/pipette/dev_types.py +++ b/shared-data/python/opentrons_shared_data/pipette/dev_types.py @@ -31,7 +31,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", - "p1000_multi_emulsify", + "p1000_multi_em", "p1000_96", "p200_96", ] @@ -58,7 +58,7 @@ class PipetteNameType(str, Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" - P1000_MULTI_EMULSIFY = "p1000_multi_emulsify" + P1000_MULTI_EM = "p1000_multi_em" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index fb121725c37..40027d54394 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -114,13 +114,13 @@ def load_serial_lookup_table() -> Dict[str, str]: "eight_channel": "M", "single_channel": "S", "ninety_six_channel": "H", - "eight_channel_emulsify": "P", + "eight_channel_em": "P", } _channel_model_str = { "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } _model_shorthand = {"p1000": "p1k", "p300": "p3h"} for channel_dir in _dirs_in(config_path): diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py index c1e03d5ab9d..d72a09e666b 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py @@ -355,7 +355,7 @@ def _update_all_models(configuration_to_update: List[str]) -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/opentrons_shared_data/pipette/types.py b/shared-data/python/opentrons_shared_data/pipette/types.py index 33164904d97..d5315ec12d5 100644 --- a/shared-data/python/opentrons_shared_data/pipette/types.py +++ b/shared-data/python/opentrons_shared_data/pipette/types.py @@ -216,7 +216,7 @@ def dict_for_encode(self) -> bool: "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", - "p1000_multi_emulsify", + "p1000_multi_em", "p1000_96", "p200_96", ] @@ -243,7 +243,7 @@ class PipetteNameType(str, enum.Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" - P1000_MULTI_EMULSIFY = "p1000_multi_emulsify" + P1000_MULTI_EM = "p1000_multi_em" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py index c5e9cc49604..aae0c1a4e1b 100644 --- a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py +++ b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py @@ -49,7 +49,7 @@ def get_all_pipette_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/tests/pipette/test_validate_schema.py b/shared-data/python/tests/pipette/test_validate_schema.py index 5d3080dbd7a..57f19dfe3ad 100644 --- a/shared-data/python/tests/pipette/test_validate_schema.py +++ b/shared-data/python/tests/pipette/test_validate_schema.py @@ -22,7 +22,7 @@ def iterate_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } defn_root = get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" assert os.listdir(defn_root), "A path is wrong" @@ -64,7 +64,7 @@ def test_pick_up_configs_configuration_by_nozzle_map_keys() -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): @@ -107,7 +107,7 @@ def test_pick_up_configs_configuration_ordered_from_smallest_to_largest() -> Non "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): From 2db28056546ced70756553b259ba932edd7f3d02 Mon Sep 17 00:00:00 2001 From: syao1226 <146495172+syao1226@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:12:44 -0500 Subject: [PATCH 26/52] fix(protocol-designer): fix slotOverflowMenu for tipracks on adapters and update path selection for trash bin (#16908) fix RQA-3607 # Overview In this PR, I updated the `SlotOverflowMenu` to disallow adding liquid and renaming labware for tipracks on adapters. Additionally, I updated `updatePatchOnWellRatioChange` to check if the `dispense_labware` includes 'movableTrash' or 'fixedTrash'. This fixes the issue where the consolidate path could not be selected when dispensing liquid into the trash bin. ## Test Plan and Hands on Testing ## Changelog - Added const `isTiprackAdapter` in `SlotOverflowMenu` to check if the slot is a tiprack adapter. ## Review requests ## Risk assessment --------- Co-authored-by: shiyaochen --- .../src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx | 10 +++++++++- .../dependentFieldsUpdateMoveLiquid.ts | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx index b45f314f689..c949fc3ab90 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx @@ -113,9 +113,16 @@ export function SlotOverflowMenu( const isLabwareTiprack = labwareOnSlot?.def.parameters.isTiprack ?? false const isLabwareAnAdapter = labwareOnSlot?.def.allowedRoles?.includes('adapter') ?? false + + const isTiprackAdapter = + labwareOnSlot?.def.parameters.quirks?.includes( + 'tiprackAdapterFor96Channel' + ) ?? false + const nestedLabwareOnSlot = Object.values(deckSetupLabware).find( lw => lw.slot === labwareOnSlot?.id ) + const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( ae => ae.location?.split('cutout')[1] === location ) @@ -170,8 +177,9 @@ export function SlotOverflowMenu( (labwareOnSlot != null && !isLabwareAnAdapter && !isLabwareTiprack && + !isTiprackAdapter && nestedLabwareOnSlot == null) || - nestedLabwareOnSlot != null + (nestedLabwareOnSlot != null && !isTiprackAdapter) let position = ROBOT_BOTTOM_HALF_SLOTS.includes(location) ? BOTTOM_SLOT_Y_POSITION diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts index d7a35e4ec59..847d45ed4bc 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts @@ -580,7 +580,9 @@ function updatePatchOnWellRatioChange( const appliedPatch = { ...rawForm, ...patch } const isDisposalLocation = rawForm.dispense_labware?.includes('wasteChute') || - rawForm.dispense_labware?.includes('trashBin') + rawForm.dispense_labware?.includes('trashBin') || + rawForm.dispense_labware?.includes('movableTrash') || + rawForm.dispense_labware?.includes('fixedTrash') const prevWellRatio = getWellRatio( rawForm.aspirate_wells as string[], From 4e1aecf84bdf5ad3005ddf05c25016742be544bf Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:26:04 -0500 Subject: [PATCH 27/52] fix(protocol-designer): move volume field above paths (#16905) closes RQA-3639 --- .../StepForm/StepTools/MoveLiquidTools/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx index 10dd04db7d5..813dc5ba022 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx @@ -186,6 +186,11 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { )} + + - - {enableReturnTip ? ( <> From d430428be79b9b6be4890e74cf818c7410f4f8a9 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:26:55 -0500 Subject: [PATCH 28/52] fix(protocol-designer): fix OffDeck starting protocol deck component (#16912) Fixes positioning and hover behavior for OffDeck component, overflow menu positioning, and SlotInformation component when hovering an offdeck labware. Closes RQA-3592 --- .../src/organisms/SlotInformation/index.tsx | 6 +++- .../Designer/DeckSetup/SlotOverflowMenu.tsx | 4 ++- .../pages/Designer/Offdeck/OffDeckDetails.tsx | 33 ++++++++++++------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index 7c1a1841a22..37a080ac885 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -63,7 +63,11 @@ export const SlotInformation: FC = ({ diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx index c949fc3ab90..8cc15363ea6 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx @@ -66,6 +66,7 @@ interface SlotOverflowMenuProps { setShowMenuList: (value: SetStateAction) => void addEquipment: (slotId: string) => void menuListSlotPosition?: CoordinateTuple + invertY?: true } export function SlotOverflowMenu( props: SlotOverflowMenuProps @@ -75,6 +76,7 @@ export function SlotOverflowMenu( setShowMenuList, addEquipment, menuListSlotPosition, + invertY = false, } = props const { t } = useTranslation('starting_deck_state') const navigate = useNavigate() @@ -333,7 +335,7 @@ export function SlotOverflowMenu( innerDivProps={{ style: { position: POSITION_ABSOLUTE, - transform: 'rotate(180deg) scaleX(-1)', + transform: `rotate(180deg) scaleX(-1) ${invertY ? 'scaleY(-1)' : ''}`, }, }} > diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index 36fb6984a66..f631ee3391a 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + ALIGN_CENTER, BORDERS, COLORS, DIRECTION_COLUMN, @@ -26,6 +27,8 @@ import { SlotOverflowMenu } from '../DeckSetup/SlotOverflowMenu' import type { DeckSlotId } from '@opentrons/shared-data' import type { DeckSetupTabType } from '../types' +const OFFDECK_MAP_WIDTH = '41.625rem' + interface OffDeckDetailsProps extends DeckSetupTabType { addLabware: () => void } @@ -43,19 +46,30 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { const allWellContentsForActiveItem = useSelector( wellContentsSelectors.getAllWellContentsForActiveItem ) + const containerWidth = tab === 'startingDeck' ? '100vw' : '75vh' + const paddingLeftWithHover = + hoverSlot == null + ? `calc((${containerWidth} - (${SPACING.spacing24} * 2) - ${OFFDECK_MAP_WIDTH}) / 2)` + : SPACING.spacing24 + const paddingLeft = tab === 'startingDeck' ? paddingLeftWithHover : undefined + const padding = + tab === 'protocolSteps' + ? SPACING.spacing24 + : `${SPACING.spacing24} ${paddingLeft}` + const stepDetailsContainerWidth = `calc(((${containerWidth} - ${OFFDECK_MAP_WIDTH}) / 2) - (${SPACING.spacing24} * 3))` return ( {hoverSlot != null ? ( - + ) : null} { setShowMenuListForId(null) }} + menuListSlotPosition={[0, 0, 0]} + invertY /> ) : null} From ac051f72d547e9d62b40698654e6da06aab2b7be Mon Sep 17 00:00:00 2001 From: koji Date: Wed, 20 Nov 2024 16:29:03 -0500 Subject: [PATCH 29/52] fix(protocol-designer): fix navbar z-index issue (#16910) * fix(protocol-designer): fix navbar z-index issue --- components/src/organisms/Toolbox/index.tsx | 1 - protocol-designer/src/organisms/ProtocolNavBar/index.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx index de1748601c8..147b8b0eda2 100644 --- a/components/src/organisms/Toolbox/index.tsx +++ b/components/src/organisms/Toolbox/index.tsx @@ -140,7 +140,6 @@ export function Toolbox(props: ToolboxProps): JSX.Element { ` - z-index: 11; + z-index: ${props => (props.showShadow === true ? 11 : 0)}; padding: ${SPACING.spacing12}; width: 100%; justify-content: ${JUSTIFY_SPACE_BETWEEN}; From 0dacfb327a529ce1a7a320a860b8baf66668f36a Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Wed, 20 Nov 2024 16:50:50 -0500 Subject: [PATCH 30/52] fix(api): update motor position before homing (#16887) --- api/src/opentrons/hardware_control/ot3api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index bd828cd525f..491b6168e58 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -776,7 +776,7 @@ async def _update_position_estimation( """ Function to update motor estimation for a set of axes """ - + await self._backend.update_motor_status() if axes: checked_axes = [ax for ax in axes if ax in Axis] else: From a34f2be23e394d23e40c62799573f71aae851614 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:17:02 -0500 Subject: [PATCH 31/52] feat(hardware-testing): flex stacker diagnostic script for axes (#16898) --- .../modules/flex_stacker_evt_qc/config.py | 32 +++++- .../modules/flex_stacker_evt_qc/driver.py | 107 ++++++++++++++++++ .../flex_stacker_evt_qc/test_l_axis.py | 70 ++++++++++++ .../flex_stacker_evt_qc/test_x_axis.py | 81 +++++++++++++ .../flex_stacker_evt_qc/test_z_axis.py | 34 ++++++ .../modules/flex_stacker_evt_qc/utils.py | 38 +++++++ 6 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py index e8bc37da959..a8fc32ca142 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py @@ -7,6 +7,9 @@ from . import ( test_connectivity, + test_z_axis, + test_x_axis, + test_l_axis, ) @@ -14,6 +17,9 @@ class TestSection(enum.Enum): """Test Section.""" CONNECTIVITY = "CONNECTIVITY" + Z_AXIS = "Z_AXIS" + L_AXIS = "L_AXIS" + X_AXIS = "X_AXIS" @dataclass @@ -29,6 +35,18 @@ class TestConfig: TestSection.CONNECTIVITY, test_connectivity.run, ), + ( + TestSection.Z_AXIS, + test_z_axis.run, + ), + ( + TestSection.L_AXIS, + test_l_axis.run, + ), + ( + TestSection.X_AXIS, + test_x_axis.run, + ), ] @@ -40,6 +58,18 @@ def build_report(test_name: str) -> CSVReport: CSVSection( title=TestSection.CONNECTIVITY.value, lines=test_connectivity.build_csv_lines(), - ) + ), + CSVSection( + title=TestSection.Z_AXIS.value, + lines=test_z_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.L_AXIS.value, + lines=test_l_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.X_AXIS.value, + lines=test_x_axis.build_csv_lines(), + ), ], ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py index 04d833fa8a5..3005405e61b 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py @@ -26,6 +26,53 @@ class StackerInfo: sn: str +class StackerAxis(Enum): + """Stacker Axis.""" + + X = "X" + Z = "Z" + L = "L" + + def __str__(self) -> str: + """Name.""" + return self.name + + +class Direction(Enum): + """Direction.""" + + RETRACT = 0 + EXTENT = 1 + + def __str__(self) -> str: + """Convert to tag for clear logging.""" + return "negative" if self == Direction.RETRACT else "positive" + + def opposite(self) -> "Direction": + """Get opposite direction.""" + return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT + + def distance(self, distance: float) -> float: + """Get signed distance, where retract direction is negative.""" + return distance * -1 if self == Direction.RETRACT else distance + + +@dataclass +class MoveParams: + """Move Parameters.""" + + max_speed: float | None = None + acceleration: float | None = None + max_speed_discont: float | None = None + + def __str__(self) -> str: + """Convert to string.""" + v = "V:" + str(self.max_speed) if self.max_speed else "" + a = "A:" + str(self.acceleration) if self.acceleration else "" + d = "D:" + str(self.max_speed_discont) if self.max_speed_discont else "" + return f"{v} {a} {d}".strip() + + class FlexStacker: """FLEX Stacker Driver.""" @@ -87,6 +134,66 @@ def set_serial_number(self, sn: str) -> None: return self._send_and_recv(f"M996 {sn}\n", "M996 OK") + def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M119 .*{axis.name}{direction.name[0]}:(\d) .* OK\n") + res = self._send_and_recv("M119\n", "M119 XE:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for limit switch: {res}" + return bool(int(match.group(1))) + + def get_platform_sensor(self, direction: Direction) -> bool: + """Get platform sensor status. + + :return: True if platform is present, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M121 .*{direction.name[0]}:(\d) .* OK\n") + res = self._send_and_recv("M121\n", "M119 E:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for platform sensor: {res}" + return bool(int(match.group(1))) + + def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M122 (\d) OK\n") + res = self._send_and_recv("M122\n", "M122 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for hopper door switch: {res}" + return bool(int(match.group(1))) + + def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> None: + """Move axis.""" + if self._simulating: + return + self._send_and_recv(f"G0 {axis.name}{distance} {params or ''}\n", "G0 OK") + + def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> None: + """Move until limit switch is triggered.""" + if self._simulating: + return + self._send_and_recv( + f"G5 {axis.name}{direction.value} {params or ''}\n", "G0 OK" + ) + def __del__(self) -> None: """Close serial port.""" if not self._simulating: diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py new file mode 100644 index 00000000000..d892bdc1fd7 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py @@ -0,0 +1,70 @@ +"""Test L Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction + + +class LimitSwitchError(Exception): + """Limit Switch Error.""" + + pass + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-latch-switch", [CSVResult]), + CSVLine("release/open-latch", [CSVResult]), + CSVLine("hold/close-latch", [CSVResult]), + ] + + +def get_latch_held_switch(driver: FlexStacker) -> bool: + """Get limit switch.""" + held_switch = driver.get_limit_switch(StackerAxis.L, Direction.RETRACT) + print("(Held Switch triggered) : ", held_switch) + return held_switch + + +def close_latch(driver: FlexStacker) -> None: + """Close latch.""" + driver.move_to_limit_switch(StackerAxis.L, Direction.EXTENT) + + +def open_latch(driver: FlexStacker) -> None: + """Open latch.""" + driver.move_in_mm(StackerAxis.L, -22) + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not get_latch_held_switch(driver): + print("Switch is not triggered, try to trigger it by closing latch...") + close_latch(driver) + if not get_latch_held_switch(driver): + print("!!! Held switch is still not triggered !!!") + report(section, "trigger-latch-switch", [CSVResult.FAIL]) + return + + report(section, "trigger-latch-switch", [CSVResult.PASS]) + + ui.print_header("Latch Release/Open") + open_latch(driver) + success = not get_latch_held_switch(driver) + report(section, "release/open-latch", [CSVResult.from_bool(success)]) + + ui.print_header("Latch Hold/Close") + if not success: + print("Latch must be open to close it") + report(section, "hold/close-latch", [CSVResult.FAIL]) + else: + close_latch(driver) + success = get_latch_held_switch(driver) + report(section, "hold/close-latch", [CSVResult.from_bool(success)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py new file mode 100644 index 00000000000..802c12bcae5 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py @@ -0,0 +1,81 @@ +"""Test X Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + CSVLine( + "platform-sensor-trigger-positive-untrigger-negative", + [bool, bool, CSVResult], + ), + CSVLine( + "platform-sensor-trigger-negative-untrigger-positive", + [bool, bool, CSVResult], + ), + ] + + +def test_platform_sensors_for_direction( + driver: FlexStacker, direction: Direction, report: CSVReport, section: str +) -> None: + """Test platform sensors for a given direction.""" + ui.print_header(f"Platform Sensor - {direction} direction") + sensor_result = driver.get_platform_sensor(direction) + opposite_result = not driver.get_platform_sensor(direction.opposite()) + print(f"{direction} sensor triggered: {sensor_result}") + print(f"{direction.opposite()} sensor untriggered: {opposite_result}") + report( + section, + f"platform-sensor-trigger-{direction}-untrigger-{direction.opposite()}", + [ + sensor_result, + opposite_result, + CSVResult.from_bool(sensor_result and opposite_result), + ], + ) + + +def platform_is_removed(driver: FlexStacker) -> bool: + """Check if the platform is removed from the carrier.""" + plus_side = driver.get_platform_sensor(Direction.EXTENT) + minus_side = driver.get_platform_sensor(Direction.RETRACT) + return not plus_side and not minus_side + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not driver._simulating and not platform_is_removed(driver): + print("FAILURE - Cannot start tests with platform on the carrier") + return + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.EXTENT, report, section + ) + + if not driver._simulating: + ui.get_user_ready("Place the platform on the X carrier") + + test_platform_sensors_for_direction(driver, Direction.EXTENT, report, section) + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.RETRACT, report, section + ) + + test_platform_sensors_for_direction(driver, Direction.RETRACT, report, section) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py new file mode 100644 index 00000000000..58fc733e0dc --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py @@ -0,0 +1,34 @@ +"""Test Z Axis.""" +from typing import List, Union +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.EXTENT, report, section + ) + + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.RETRACT, report, section + ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py new file mode 100644 index 00000000000..2aca90c8886 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py @@ -0,0 +1,38 @@ +"""Utility functions for the Flex Stacker EVT QC module.""" +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction, MoveParams + + +def test_limit_switches_per_direction( + driver: FlexStacker, + axis: StackerAxis, + direction: Direction, + report: CSVReport, + section: str, + speed: float = 50.0, +) -> None: + """Sequence to test the limit switch for one direction.""" + ui.print_header(f"{axis} Limit Switch - {direction} direction") + # first make sure switch is not already triggered by moving in the opposite direction + if driver.get_limit_switch(axis, direction): + print(f"{direction} switch already triggered, moving away...\n") + SAFE_DISTANCE_MM = 10 + driver.move_in_mm(axis, direction.opposite().distance(SAFE_DISTANCE_MM)) + + # move until the limit switch is reached + print(f"moving towards {direction} limit switch...\n") + driver.move_to_limit_switch(axis, direction, MoveParams(max_speed=speed)) + result = driver.get_limit_switch(axis, direction) + opposite_result = not driver.get_limit_switch(axis, direction.opposite()) + print(f"{direction} switch triggered: {result}") + print(f"{direction.opposite()} switch untriggered: {opposite_result}") + report( + section, + f"limit-switch-trigger-{direction}-untrigger-{direction.opposite()}", + [result, opposite_result, CSVResult.from_bool(result and opposite_result)], + ) From b6b4526a70bcdaf34660c1f02e39235bbfb8d2aa Mon Sep 17 00:00:00 2001 From: emilyburghardt Date: Thu, 21 Nov 2024 08:41:00 -0700 Subject: [PATCH 32/52] docs(api): updated versioning page and API reference for mix (#16833) # Overview Updating versioning page and API reference for changes to mix behavior ## Test Plan and Hands on Testing http://sandbox.docs.opentrons.com/docs-lld-mix/v2/ ## Changelog 4 lines in versioning.rst to provide description of changes to mix behavior in version 2.21 2 lines in instrument_context.py to provide description of changes to mix behavior in version 2.21 ## Review requests no special requests ## Risk assessment low --- api/docs/v2/versioning.rst | 4 ++++ api/src/opentrons/protocol_api/instrument_context.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/api/docs/v2/versioning.rst b/api/docs/v2/versioning.rst index 72430f9104f..b808893dd53 100644 --- a/api/docs/v2/versioning.rst +++ b/api/docs/v2/versioning.rst @@ -134,6 +134,10 @@ This table lists the correspondence between Protocol API versions and robot soft Changes in API Versions ======================= +Version 2.21 +------------ +- :ref:`Liquid presence detection ` now only checks on the first aspiration of the :py:meth:`.mix` cycle. + Version 2.20 ------------ diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 3aee270e9a7..a8d0a4b5765 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -512,6 +512,8 @@ def mix( ``pipette.mix(1, location=wellplate['A1'])`` is a valid call, but ``pipette.mix(1, wellplate['A1'])`` is not. + .. versionchanged:: 2.21 + Does not repeatedly check for liquid presence. """ _log.debug( "mixing {}uL with {} repetitions in {} at rate={}".format( From 84a57ce957b72026bd6d24ec06af109f93470b53 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Thu, 21 Nov 2024 10:30:33 -0600 Subject: [PATCH 33/52] test(analyses-snapshots): update analyses snapshot local instructions (#16918) --- analyses-snapshot-testing/Makefile | 52 +++++++++++++++++++++-------- analyses-snapshot-testing/README.md | 17 +++++++--- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/analyses-snapshot-testing/Makefile b/analyses-snapshot-testing/Makefile index de5e0381131..6918d17bf3e 100644 --- a/analyses-snapshot-testing/Makefile +++ b/analyses-snapshot-testing/Makefile @@ -3,12 +3,12 @@ CACHEBUST ?= $(shell date +%s) ANALYSIS_REF ?= edge PROTOCOL_NAMES ?= all OVERRIDE_PROTOCOL_NAMES ?= all -OPENTRONS_VERSION ?= edge +LOCAL_IMAGE_TAG ?= local +ANALYZER_IMAGE_NAME ?= opentrons-analysis -export OPENTRONS_VERSION # used for server -export ANALYSIS_REF # used for analysis and snapshot test -export PROTOCOL_NAMES # used for the snapshot test -export OVERRIDE_PROTOCOL_NAMES # used for the snapshot test +export ANALYSIS_REF # tag, branch or commit for the opentrons repository. Used as the image tag for the analyzer image +export PROTOCOL_NAMES # tell the test which protocols to run +export OVERRIDE_PROTOCOL_NAMES # tell the test which override protocols to run ifeq ($(CI), true) PYTHON=python @@ -93,23 +93,47 @@ build-base-image: .PHONY: build-opentrons-analysis build-opentrons-analysis: - @echo "Building docker image for $(ANALYSIS_REF)" - @echo "The image will be named opentrons-analysis:$(ANALYSIS_REF)" - @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" - docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-analysis:$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. + @echo "Building docker image for opentrons repository reference$(ANALYSIS_REF)" + @echo "The image will be named $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF)" + @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. -.PHONY: local-build -local-build: +.PHONY: build-local +build-local: @echo "Building docker image for your local opentrons code" - @echo "The image will be named opentrons-analysis:local" - @echo "For a fresh build, run 'make local-build NO_CACHE=1'" - docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) $(BUILD_FLAGS) -t opentrons-analysis:local -f citools/Dockerfile.local .. || true + @echo "This image will be named $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) -t $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG) -f citools/Dockerfile.local .. @echo "Build complete" +.PHONY: snapshot-test-local +snapshot-test-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test -vv + +.PHONY: snapshot-test-update-local +snapshot-test-update-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-update-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test --snapshot-update + .PHONY: generate-protocols generate-protocols: $(PYTHON) -m pipenv run python -m automation.data.protocol_registry +# Tools for running the robot server in a container + +OPENTRONS_VERSION ?= edge +export OPENTRONS_VERSION # used for the robot server image as the tag, branch or commit for the opentrons repository + .PHONY: build-rs build-rs: @echo "Building docker image for opentrons-robot-server:$(OPENTRONS_VERSION)" diff --git a/analyses-snapshot-testing/README.md b/analyses-snapshot-testing/README.md index 78423b8447f..03ce1d87518 100644 --- a/analyses-snapshot-testing/README.md +++ b/analyses-snapshot-testing/README.md @@ -4,7 +4,7 @@ 1. Follow the instructions in [DEV_SETUP.md](../DEV_SETUP.md) 1. `cd analyses-snapshot-testing` -1. use pyenv to install python 3.12 and set it as the local python version for this directory +1. use pyenv to install python 3.13 and set it as the local python version for this directory 1. `make setup` 1. Have docker installed and ready @@ -72,10 +72,17 @@ cd analyses-snapshot-testing \ > This copies in your local code to the container and runs the analyses battery against it. -1. `make build-base-image` -1. `make build-local` -1. `make local-snapshot-test` +`cd PYENV_ROOT && git pull` - make sure pyenv is up to date so you may install python 3.13.0 +`pyenv install 3.13.0` - install python 3.13.0 +`cd /analyses-snapshot-testing` - navigate to the analyses-snapshot-testing directory +`pyenv local 3.13.0` - set the local python version to 3.13.0 +`make setup` - install the requirements +`make snapshot-test-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created You have the option to specify one or many protocols to run the analyses on. This is also described above [Running the tests against specific protocols](#running-the-tests-against-specific-protocols) -- `make local-snapshot-test PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` +- `make snapshot-test-local PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` + +### Updating the snapshots locally + +- `make snapshot-test-update-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created, updating the snapshots by passing the `--update-snapshots` flag to the test From ead4491c7d8f4bcb528cfdca4552b52ad2fa6f68 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:20:16 -0500 Subject: [PATCH 34/52] feat(hardware-testing): stacker estop and door switch qc scripts (#16924) --- .../modules/flex_stacker_evt_qc/config.py | 20 ++++ .../modules/flex_stacker_evt_qc/driver.py | 14 +++ .../flex_stacker_evt_qc/test_door_switch.py | 36 ++++++++ .../modules/flex_stacker_evt_qc/test_estop.py | 91 +++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py index a8fc32ca142..5b94e014f0f 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py @@ -10,6 +10,8 @@ test_z_axis, test_x_axis, test_l_axis, + test_door_switch, + test_estop, ) @@ -20,6 +22,8 @@ class TestSection(enum.Enum): Z_AXIS = "Z_AXIS" L_AXIS = "L_AXIS" X_AXIS = "X_AXIS" + DOOR_SWITCH = "DOOR_SWITCH" + ESTOP = "ESTOP" @dataclass @@ -47,6 +51,14 @@ class TestConfig: TestSection.X_AXIS, test_x_axis.run, ), + ( + TestSection.DOOR_SWITCH, + test_door_switch.run, + ), + ( + TestSection.ESTOP, + test_estop.run, + ), ] @@ -71,5 +83,13 @@ def build_report(test_name: str) -> CSVReport: title=TestSection.X_AXIS.value, lines=test_x_axis.build_csv_lines(), ), + CSVSection( + title=TestSection.DOOR_SWITCH.value, + lines=test_door_switch.build_csv_lines(), + ), + CSVSection( + title=TestSection.ESTOP.value, + lines=test_estop.build_csv_lines(), + ), ], ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py index 3005405e61b..e219b68dae3 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py @@ -176,6 +176,20 @@ def get_hopper_door_closed(self) -> bool: assert match, f"Incorrect Response for hopper door switch: {res}" return bool(int(match.group(1))) + def get_estop(self) -> bool: + """Get E-Stop status. + + :return: True if E-Stop is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M112 (\d) OK\n") + res = self._send_and_recv("M112\n", "M112 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for E-Stop switch: {res}" + return bool(int(match.group(1))) + def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None ) -> None: diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py new file mode 100644 index 00000000000..ab104a10d01 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py @@ -0,0 +1,36 @@ +"""Test Door Switch.""" + + +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("close-door", [CSVResult]), + CSVLine("open-door", [CSVResult]), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + ui.print_header("Close Door") + if not driver._simulating: + ui.get_user_ready("Close the hopper door") + closed = driver.get_hopper_door_closed() + report(section, "close-door", [CSVResult.from_bool(closed)]) + + ui.print_header("Open Door") + if not driver._simulating: + ui.get_user_ready("Open the hopper door") + closed = driver.get_hopper_door_closed() + report(section, "open-door", [CSVResult.from_bool(not closed)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py new file mode 100644 index 00000000000..c0ee8b4150b --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py @@ -0,0 +1,91 @@ +"""Test E-Stop.""" + + +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, Direction, StackerAxis + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-estop", [CSVResult]), + CSVLine("x-move-disabled", [CSVResult]), + CSVLine("z-move-disabled", [CSVResult]), + CSVLine("l-move-disabled", [CSVResult]), + CSVLine("untrigger-estop", [CSVResult]), + ] + + +def axis_at_limit(driver: FlexStacker, axis: StackerAxis) -> Direction: + """Check which direction an axis is at the limit switch.""" + if axis is StackerAxis.L: + # L axis only has one limit switch + if driver.get_limit_switch(axis, Direction.RETRACT): + print(axis, "is at ", Direction.RETRACT, "limit switch") + return Direction.RETRACT + else: + for dir in Direction: + if driver.get_limit_switch(axis, dir): + print(axis, "is at ", dir, "limit switch") + return dir + raise RuntimeError(f"{axis} is not at any limit switch") + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not driver._simulating and driver.get_estop(): + raise RuntimeError("E-Stop is either triggered/not attached.") + + x_limit = axis_at_limit(driver, StackerAxis.X) + z_limit = axis_at_limit(driver, StackerAxis.Z) + l_limit = axis_at_limit(driver, StackerAxis.L) + + ui.print_header("Trigger E-Stop") + if not driver._simulating: + ui.get_user_ready("Trigger the E-Stop") + + if not driver.get_estop(): + print("E-Stop is not triggered") + report(section, "trigger-estop", [CSVResult.FAIL]) + return + + report(section, "trigger-estop", [CSVResult.PASS]) + + print("try to move X axis...") + driver.move_in_mm(StackerAxis.X, x_limit.opposite().distance(10)) + print("X should not move") + report( + section, + "x-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.X, x_limit))], + ) + + print("try to move Z axis...") + driver.move_in_mm(StackerAxis.Z, z_limit.opposite().distance(10)) + print("Z should not move") + report( + section, + "z-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.Z, z_limit))], + ) + + print("try to move L axis...") + driver.move_in_mm(StackerAxis.L, l_limit.opposite().distance(10)) + print("L should not move") + report( + section, + "l-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.L, l_limit))], + ) + + if not driver._simulating: + ui.get_user_ready("Untrigger the E-Stop") + report(section, "untrigger-estop", [CSVResult.from_bool(not driver.get_estop())]) From da74897b48452a237387040b8d43ab1b8ab80224 Mon Sep 17 00:00:00 2001 From: Anthony Ngumah <68346382+AnthonyNASC20@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:43:42 -0500 Subject: [PATCH 35/52] fix(abr-testing): unify all script compatibility with same ip file (#16922) # Overview Ensures data collection scripts can all use the same IPs.json file when connecting to robots --------- Co-authored-by: rclarke0 --- .../data_collection/abr_calibration_logs.py | 3 ++- .../abr_testing/data_collection/get_run_logs.py | 3 ++- .../hardware_testing/scripts/ABRAsairScript.py | 12 +++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py index 75b73b8f16b..46cc409e53d 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -286,6 +286,7 @@ def run( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() @@ -294,7 +295,7 @@ def run( ip_or_all = input("IP Address or ALL: ") calibration_data = [] if ip_or_all.upper() == "ALL": - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) for ip in ip_address_list: saved_file_path, calibration = read_robot_logs.get_calibration_offsets( ip, storage_directory diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index 964a8a06e18..fe89f9f1543 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -104,10 +104,11 @@ def get_all_run_logs( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) runs_from_storage = read_robot_logs.get_run_ids_from_google_drive(google_drive) for ip in ip_address_list: runs = get_run_ids_from_robot(ip) diff --git a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py index 41c70ed35a2..710d3c17578 100644 --- a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py +++ b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py @@ -2,6 +2,7 @@ import sys import paramiko as pmk import time +import json import multiprocessing from typing import Optional, List, Any @@ -69,11 +70,10 @@ def run(file_name: str) -> List[Any]: robot_ips = [] robot_names = [] with open(file_name) as file: - for line in file.readlines(): - info = line.split(",") - if "Y" in info[2]: - robot_ips.append(info[0]) - robot_names.append(info[1]) + file_dict = json.load(file) + robot_dict = file_dict.get("ip_address_list") + robot_ips = list(robot_dict.keys()) + robot_names = list(robot_dict.values()) print("Executing Script on All Robots:") # Launch the processes for each robot. processes = [] @@ -89,10 +89,8 @@ def run(file_name: str) -> List[Any]: # Wait for all processes to finish. file_name = sys.argv[1] processes = run(file_name) - for process in processes: process.start() time.sleep(20) - for process in processes: process.join() From 87c2ffd32fd81308beb51b793de903bfe8b3eb86 Mon Sep 17 00:00:00 2001 From: Anthony Ngumah <68346382+AnthonyNASC20@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:24:33 -0500 Subject: [PATCH 36/52] feat(abr-testing,opentrons): make file command to push desired folders to list of robot ips (#16909) # Overview Adds the ability to push one or more folders to one or more roots at once ## Test Plan and Hands on Testing Manually tested functionality of command ## Changelog Added push-folder definition to opentrons makefile --------- Co-authored-by: rclarke0 Co-authored-by: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> --- Makefile | 4 + abr-testing/abr_testing/tools/make_push.py | 95 ++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 abr-testing/abr_testing/tools/make_push.py diff --git a/Makefile b/Makefile index d3ddd15f359..7a5bfc7a7e9 100755 --- a/Makefile +++ b/Makefile @@ -152,6 +152,10 @@ push: sleep 1 $(MAKE) -C $(UPDATE_SERVER_DIR) push +.PHONY: push-folder +PUSH_HELPER := abr-testing/abr_testing/tools/make_push.py +push-folder: + $(OT_PYTHON) $(PUSH_HELPER) .PHONY: push-ot3 push-ot3: diff --git a/abr-testing/abr_testing/tools/make_push.py b/abr-testing/abr_testing/tools/make_push.py new file mode 100644 index 00000000000..28a69b11103 --- /dev/null +++ b/abr-testing/abr_testing/tools/make_push.py @@ -0,0 +1,95 @@ +"""Push one or more folders to one or more robots.""" +import subprocess +import multiprocessing +import json + +global folders +# Opentrons folders that can be pushed to robot +folders = [ + "abr-testing", + "hardware-testing", + "abr-testing + hardware-testing", + "other", +] + + +def push_subroutine(cmd: str) -> None: + """Pushes specified folder to specified robot.""" + try: + subprocess.run(cmd) + except Exception: + print("failed to push folder") + raise + + +def main(folder_to_push: str, robot_to_push: str) -> int: + """Main process!""" + cmd = "make -C {folder} push-ot3 host={ip}" + robot_ip_path = "" + push_cmd = "" + folder_int = int(folder_to_push) + if folders[folder_int].lower() == "abr-testing + hardware-testing": + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips_dict = robot_json.get("ip_address_list") + robot_ips = list(robot_ips_dict.keys()) + ip_file.close() + else: + robot_ips = [robot_to_push] + for folder_name in folders[:-2]: + # Push abr-testing and hardware-testing folders to all robots + for robot in robot_ips: + print_proc = multiprocessing.Process( + target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) + ) + print_proc.start() + print_proc.join() + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = multiprocessing.Process( + target=push_subroutine, args=(push_cmd,) + ) + process.start() + process.join() + print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) + print_proc.start() + print_proc.join() + else: + + if folder_int == (len(folders) - 1): + folder_name = input("Which folder? ") + else: + folder_name = folders[folder_int] + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips = robot_json.get("ip_address_list") + ip_file.close() + else: + robot_ips = [robot_to_push] + + # Push folder to robots + for robot in robot_ips: + print_proc = multiprocessing.Process( + target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) + ) + print_proc.start() + print_proc.join() + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = multiprocessing.Process(target=push_subroutine, args=(push_cmd,)) + process.start() + process.join() + print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) + print_proc.start() + print_proc.join() + return 0 + + +if __name__ == "__main__": + for i, folder in enumerate(folders): + print(f"{i}) {folder}") + folder_to_push = input("Please Select a Folder to Push: ") + robot_to_push = input("Type in robots ip (type all for all): ") + print(main(folder_to_push, robot_to_push)) From f37ef0ab64fed89484c19320c34b1b6ef2639fa3 Mon Sep 17 00:00:00 2001 From: Anthony Ngumah <68346382+AnthonyNASC20@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:40:13 -0500 Subject: [PATCH 37/52] fix(abr-testing): Update simulate (#16929) # Overview Fixes the issue of make-simulate not simulating liquid setups in protocols folder --------- Co-authored-by: rclarke0 --- abr-testing/abr_testing/protocol_simulation/abr_sim_check.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py index f55c9ebb51f..76852f70b9c 100644 --- a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py +++ b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py @@ -46,7 +46,7 @@ def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: labware_defs = [] for root, directories, _ in os.walk(root_dir): for directory in directories: - if directory == "active_protocols": + if directory not in exclude: active_dir = os.path.join(root, directory) for file in os.listdir( active_dir @@ -100,7 +100,6 @@ def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: exclude = [ "__init__.py", "helpers.py", - "shared_vars_and_funcs.py", ] print("Simulating Protocols") file_dict, labware_defs = get_files() From fe29ce4cae59b1bf301e77be13946a6429ecf68b Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Thu, 21 Nov 2024 13:49:26 -0500 Subject: [PATCH 38/52] feat(shared-data, api): remove 'default' from all '..ByVolume' properties (#16878) Modifies the schema from ticket AUTH-832, removes `default` key and refactors `byVolume` property schema --------- Co-authored-by: jbleon95 --- .../protocol_api/_liquid_properties.py | 24 +- api/tests/opentrons/conftest.py | 14 +- .../protocol_api/test_liquid_class.py | 7 +- .../test_liquid_class_properties.py | 24 +- .../test_liquid_classes.py | 2 +- shared-data/command/schemas/11.json | 264 ++++-- .../js/__tests__/liquidClassSchema.test.ts | 66 ++ .../liquid-class/definitions/1/water.json | 859 ++++++++---------- .../fixtures/{ => 1}/fixture_glycerol50.json | 68 +- shared-data/liquid-class/schemas/1.json | 73 +- .../liquid_classes/liquid_class_definition.py | 6 +- shared-data/tsconfig-data.json | 1 + shared-data/tsconfig.json | 1 + 13 files changed, 733 insertions(+), 676 deletions(-) create mode 100644 shared-data/js/__tests__/liquidClassSchema.test.ts rename shared-data/liquid-class/fixtures/{ => 1}/fixture_glycerol50.json (82%) diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py index 06a23a29eb8..5aaed51edbe 100644 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from numpy import interp -from typing import Optional, Dict, Sequence, Union, Tuple +from typing import Optional, Dict, Sequence, Tuple from opentrons_shared_data.liquid_classes.liquid_class_definition import ( AspirateProperties as SharedDataAspirateProperties, @@ -23,12 +23,9 @@ class LiquidHandlingPropertyByVolume: - def __init__(self, properties_by_volume: Dict[str, float]) -> None: - self._default = properties_by_volume["default"] + def __init__(self, by_volume_property: Sequence[Tuple[float, float]]) -> None: self._properties_by_volume: Dict[float, float] = { - float(volume): value - for volume, value in properties_by_volume.items() - if volume != "default" + float(volume): value for volume, value in by_volume_property } # Volumes need to be sorted for proper interpolation of non-defined volumes, and the # corresponding values need to be in the same order for them to be interpolated correctly @@ -36,18 +33,17 @@ def __init__(self, properties_by_volume: Dict[str, float]) -> None: self._sorted_values: Tuple[float, ...] = () self._sort_volume_and_values() - @property - def default(self) -> float: - """Get the default value not associated with any volume for this property.""" - return self._default - - def as_dict(self) -> Dict[Union[float, str], float]: + 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 | {"default": self._default} + return self._properties_by_volume def get_for_volume(self, volume: float) -> float: """Get a value by volume for this property. Volumes not defined will be interpolated between set volumes.""" validated_volume = validation.ensure_positive_float(volume) + if len(self._properties_by_volume) == 0: + raise ValueError( + "No properties found for any volumes. Cannot interpolate for the given volume." + ) try: return self._properties_by_volume[validated_volume] except KeyError: @@ -66,9 +62,9 @@ def delete_for_volume(self, volume: float) -> None: """Remove an existing volume and value from the property.""" try: del self._properties_by_volume[volume] - self._sort_volume_and_values() except KeyError: raise KeyError(f"No value set for volume {volume} uL") + self._sort_volume_and_values() def _sort_volume_and_values(self) -> None: """Sort volume in increasing order along with corresponding values in matching order.""" diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index cf8fdd0e97c..e8ca2b059ff 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -804,10 +804,10 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: namespace="test-fixture-2", byPipette=[ ByPipetteSetting( - pipetteModel="p20_single_gen2", + pipetteModel="flex_1channel_50", byTipType=[ ByTipTypeSetting( - tiprack="opentrons_96_tiprack_20ul", + tiprack="opentrons_flex_96_tiprack_50ul", aspirate=AspirateProperties( submerge=Submerge( positionReference=PositionReference.LIQUID_MENISCUS, @@ -821,13 +821,13 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], preWet=True, mix=MixProperties(enable=False), delay=DelayProperties( @@ -845,16 +845,16 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], blowout=BlowoutProperties(enable=False), touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], mix=MixProperties(enable=False), - pushOutByVolume={"default": 5, "10": 7, "20": 10}, + pushOutByVolume=[(10.0, 7.0), (20.0, 10.0)], delay=DelayProperties(enable=False), ), multiDispense=None, diff --git a/api/tests/opentrons/protocol_api/test_liquid_class.py b/api/tests/opentrons/protocol_api/test_liquid_class.py index 463889b3da6..7118080eda0 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class.py @@ -21,9 +21,8 @@ def test_get_for_pipette_and_tip( ) -> None: """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") + result = liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") assert result.aspirate.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } @@ -36,7 +35,7 @@ def test_get_for_raises_for_incorrect_pipette_or_tip( liq_class = LiquidClass.create(minimal_liquid_class_def2) with pytest.raises(ValueError): - liq_class.get_for("p20_single_gen2", "no_such_tiprack") + liq_class.get_for("flex_1channel_50", "no_such_tiprack") with pytest.raises(ValueError): - liq_class.get_for("p300_single", "opentrons_96_tiprack_20ul") + liq_class.get_for("no_such_pipette", "opentrons_flex_96_tiprack_50ul") diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py index e1e9b540149..f7033afb5be 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -16,7 +16,7 @@ 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") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate @@ -32,7 +32,6 @@ def test_build_aspirate_settings() -> None: 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.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -45,7 +44,7 @@ def test_build_aspirate_settings() -> None: 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.as_dict() == {"default": 50.0} + assert aspirate_properties.flow_rate_by_volume.as_dict() == {10: 50.0} assert aspirate_properties.pre_wet is True assert aspirate_properties.mix.enabled is True assert aspirate_properties.mix.repetitions == 3 @@ -56,7 +55,7 @@ def test_build_aspirate_settings() -> None: 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") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense @@ -75,7 +74,6 @@ def test_build_single_dispense_settings() -> None: 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.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -93,7 +91,6 @@ def test_build_single_dispense_settings() -> None: 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.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } @@ -101,7 +98,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.mix.repetitions == 3 assert single_dispense_properties.mix.volume == 15 assert single_dispense_properties.push_out_by_volume.as_dict() == { - "default": 5.0, 10.0: 7.0, 20.0: 10.0, } @@ -111,7 +107,7 @@ def test_build_single_dispense_settings() -> None: 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") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense @@ -131,7 +127,6 @@ def test_build_multi_dispense_settings() -> None: 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.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -148,16 +143,13 @@ def test_build_multi_dispense_settings() -> None: 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.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } assert multi_dispense_properties.conditioning_by_volume.as_dict() == { - "default": 10.0, 5.0: 5.0, } assert multi_dispense_properties.disposal_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, } assert multi_dispense_properties.delay.enabled is True @@ -174,14 +166,12 @@ def test_build_multi_dispense_settings_none( def test_liquid_handling_property_by_volume() -> None: """It should create a class that can interpolate values and add and delete new points.""" - subject = LiquidHandlingPropertyByVolume({"default": 42, "5": 50, "10.0": 250}) - assert subject.as_dict() == {"default": 42, 5.0: 50, 10.0: 250} - assert subject.default == 42.0 + subject = LiquidHandlingPropertyByVolume([(5.0, 50.0), (10.0, 250.0)]) + assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 subject.set_for_volume(volume=7, value=175.5) assert subject.as_dict() == { - "default": 42, 5.0: 50, 10.0: 250, 7.0: 175.5, @@ -189,7 +179,7 @@ def test_liquid_handling_property_by_volume() -> None: assert subject.get_for_volume(7) == 175.5 subject.delete_for_volume(7) - assert subject.as_dict() == {"default": 42, 5.0: 50, 10.0: 250} + assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 with pytest.raises(KeyError, match="No value set for volume"): 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 97de9fb0c48..20bbd2b646c 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -32,7 +32,7 @@ def test_liquid_class_creation_and_property_fetching( assert ( water.get_for( pipette_load_name, tiprack.load_name - ).dispense.flow_rate_by_volume.default + ).dispense.flow_rate_by_volume.get_for_volume(1) == 50 ) assert ( diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index cc2202f850d..1dbe7a1bca4 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -1940,16 +1940,35 @@ "airGapByVolume": { "title": "Airgapbyvolume", "description": "Settings for air gap keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2073,16 +2092,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2218,16 +2256,35 @@ "airGapByVolume": { "title": "Airgapbyvolume", "description": "Settings for air gap keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2313,16 +2370,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2339,16 +2415,35 @@ "pushOutByVolume": { "title": "Pushoutbyvolume", "description": "Settings for pushout keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2417,16 +2512,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2434,16 +2548,35 @@ "conditioningByVolume": { "title": "Conditioningbyvolume", "description": "Settings for conditioning volume keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2451,16 +2584,35 @@ "disposalByVolume": { "title": "Disposalbyvolume", "description": "Settings for disposal volume keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } diff --git a/shared-data/js/__tests__/liquidClassSchema.test.ts b/shared-data/js/__tests__/liquidClassSchema.test.ts new file mode 100644 index 00000000000..75e477637c9 --- /dev/null +++ b/shared-data/js/__tests__/liquidClassSchema.test.ts @@ -0,0 +1,66 @@ +/** Ensure that the liquid class schema itself functions as intended, + * and that all v1 liquid class fixtures will validate */ +import Ajv from 'ajv' +import path from 'path' +import glob from 'glob' +import { describe, expect, it } from 'vitest' +import liquidClassSchemaV1 from '../../liquid-class/schemas/1.json' + +const fixtureV1Glob = path.join( + __dirname, + '../../liquid-class/fixtures/1/*.json' +) +const defV1Glob = path.join( + __dirname, + '../../liquid-class/definitions/1/*.json' +) + +const ajv = new Ajv({ allErrors: true, jsonPointers: true }) + +const validateSchemaV1 = ajv.compile(liquidClassSchemaV1) + +describe('validate v1 liquid class definitions and fixtures', () => { + const fixtures = glob.sync(fixtureV1Glob) + + fixtures.forEach(fixturePath => { + const fixtureDef = require(fixturePath) + + it('fixture validates against schema', () => { + const valid = validateSchemaV1(fixtureDef) + const validationErrors = validateSchemaV1.errors + + if (validationErrors) { + console.log( + path.parse(fixturePath).base + + ' ' + + JSON.stringify(validationErrors, null, 4) + ) + } + + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) + + const defs = glob.sync(defV1Glob) + + defs.forEach(defPath => { + const liquidClassDef = require(defPath) + + it('liquid class definition validates against v1 schema', () => { + const valid = validateSchemaV1(liquidClassDef) + const validationErrors = validateSchemaV1.errors + + if (validationErrors) { + console.log( + path.parse(defPath).base + + ' ' + + JSON.stringify(validationErrors, null, 4) + ) + } + + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) +}) diff --git a/shared-data/liquid-class/definitions/1/water.json b/shared-data/liquid-class/definitions/1/water.json index b5fc2f75486..b84e1676d5b 100644 --- a/shared-data/liquid-class/definitions/1/water.json +++ b/shared-data/liquid-class/definitions/1/water.json @@ -33,12 +33,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -60,12 +59,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50, - "1": 35, - "10": 24, - "50": 35 - }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], "preWet": false, "mix": { "enable": false, @@ -105,12 +103,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -135,9 +132,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[1.0, 50.0]], "mix": { "enable": false, "params": { @@ -145,14 +140,13 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 2, - "1": 7, - "4.999": 7, - "5": 2, - "10": 2, - "50": 2 - }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], "delay": { "enable": true, "params": { @@ -184,12 +178,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -214,21 +207,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[50.0, 50.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": true, "params": { @@ -268,12 +257,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -295,12 +283,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50, - "1": 35, - "10": 24, - "50": 35 - }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], "preWet": false, "mix": { "enable": false, @@ -340,12 +327,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -370,9 +356,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[1.0, 50.0]], "mix": { "enable": false, "params": { @@ -380,14 +364,13 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 2, - "1": 7, - "4.999": 7, - "5": 2, - "10": 2, - "50": 2 - }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], "delay": { "enable": true, "params": { @@ -419,12 +402,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -449,21 +431,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[1.0, 50.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": true, "params": { @@ -503,12 +481,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -530,12 +507,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "preWet": false, "mix": { "enable": false, @@ -575,12 +551,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -605,12 +580,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "mix": { "enable": false, "params": { @@ -618,9 +592,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -652,12 +624,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -682,24 +653,21 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -734,12 +702,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -761,9 +728,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -803,12 +768,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -833,9 +797,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -843,9 +805,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -877,12 +837,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -907,21 +866,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -956,12 +911,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -983,9 +937,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1025,12 +977,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1055,9 +1006,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1065,9 +1014,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1099,12 +1046,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1129,21 +1075,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1183,12 +1125,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1210,12 +1151,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "preWet": false, "mix": { "enable": false, @@ -1255,12 +1195,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1285,12 +1224,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "mix": { "enable": false, "params": { @@ -1298,9 +1236,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1332,12 +1268,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1362,24 +1297,21 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1414,12 +1346,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1441,9 +1372,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1483,12 +1412,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -1513,9 +1441,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1523,9 +1449,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -1557,12 +1481,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -1587,21 +1510,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1636,12 +1555,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1663,9 +1581,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1705,12 +1621,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1735,9 +1650,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1745,9 +1658,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1779,12 +1690,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1809,21 +1719,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1863,12 +1769,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1890,9 +1795,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -1932,12 +1835,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1962,9 +1864,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -1972,9 +1872,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -2006,12 +1904,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -2036,21 +1933,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -2085,12 +1978,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -2112,9 +2004,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -2154,12 +2044,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -2184,9 +2073,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -2194,9 +2081,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -2228,12 +2113,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -2258,21 +2142,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -2307,12 +2187,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -2334,9 +2213,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -2376,12 +2253,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -2406,9 +2282,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -2416,9 +2290,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -2450,12 +2322,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -2480,21 +2351,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { diff --git a/shared-data/liquid-class/fixtures/fixture_glycerol50.json b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json similarity index 82% rename from shared-data/liquid-class/fixtures/fixture_glycerol50.json rename to shared-data/liquid-class/fixtures/1/fixture_glycerol50.json index 8befe1d6a5b..20fe7b44a3c 100644 --- a/shared-data/liquid-class/fixtures/fixture_glycerol50.json +++ b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json @@ -33,11 +33,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "touchTip": { "enable": true, "params": { @@ -59,9 +58,7 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[10.0, 50.0]], "preWet": true, "mix": { "enable": true, @@ -101,11 +98,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "blowout": { "enable": true, "params": { @@ -134,11 +130,10 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50, - "10": 40, - "20": 30 - }, + "flowRateByVolume": [ + [10.0, 40.0], + [20.0, 30.0] + ], "mix": { "enable": true, "params": { @@ -146,11 +141,10 @@ "volume": 15 } }, - "pushOutByVolume": { - "default": 5, - "10": 7, - "20": 10 - }, + "pushOutByVolume": [ + [10.0, 7.0], + [20.0, 10.0] + ], "delay": { "enable": true, "params": { @@ -182,11 +176,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "touchTip": { "enable": true, "params": { @@ -211,19 +204,12 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50, - "10": 40, - "20": 30 - }, - "conditioningByVolume": { - "default": 10, - "5": 5 - }, - "disposalByVolume": { - "default": 2, - "5": 3 - }, + "flowRateByVolume": [ + [10.0, 40.0], + [20.0, 30.0] + ], + "conditioningByVolume": [[5.0, 5.0]], + "disposalByVolume": [[5.0, 3.0]], "delay": { "enable": true, "params": { diff --git a/shared-data/liquid-class/schemas/1.json b/shared-data/liquid-class/schemas/1.json index 1a5eb18d51a..f3aa85a6168 100644 --- a/shared-data/liquid-class/schemas/1.json +++ b/shared-data/liquid-class/schemas/1.json @@ -90,59 +90,59 @@ "additionalProperties": false }, "airGapByVolume": { - "type": "object", + "type": "array", "description": "Settings for air gap keyed by target aspiration volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "flowRateByVolume": { - "type": "object", + "type": "array", "description": "Settings for flow rate keyed by target aspiration/dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "pushOutByVolume": { - "type": "object", + "type": "array", "description": "Settings for pushout keyed by target aspiration volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "disposalByVolume": { - "type": "object", - "description": "Settings for disposal volume keyed by target dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "type": "array", + "description": "An array of two tuples containing positive numbers.", + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "conditioningByVolume": { - "type": "object", + "type": "array", "description": "Settings for conditioning volume keyed by target dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "mix": { "type": "object", @@ -409,7 +409,6 @@ "positionReference", "offset", "flowRateByVolume", - "mix", "conditioningByVolume", "disposalByVolume", "delay" diff --git a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py index 0462ac5c0e4..62add6a32b0 100644 --- a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py +++ b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py @@ -1,7 +1,7 @@ """Python shared data models for liquid class definitions.""" from enum import Enum -from typing import TYPE_CHECKING, Literal, Union, Optional, Dict, Any, Sequence +from typing import TYPE_CHECKING, Literal, Union, Optional, Dict, Any, Sequence, Tuple from pydantic import ( BaseModel, @@ -28,8 +28,8 @@ _NonNegativeNumber = Union[_StrictNonNegativeInt, _StrictNonNegativeFloat] """Non-negative JSON number type, written to preserve lack of decimal point.""" -LiquidHandlingPropertyByVolume = Dict[str, _NonNegativeNumber] -"""Settings for liquid class settings keyed by target aspiration/dispense volume.""" +LiquidHandlingPropertyByVolume = Sequence[Tuple[_NonNegativeNumber, _NonNegativeNumber]] +"""Settings for liquid class settings that are interpolated by volume.""" class PositionReference(Enum): diff --git a/shared-data/tsconfig-data.json b/shared-data/tsconfig-data.json index 4b9ff960c84..e79657a21f8 100644 --- a/shared-data/tsconfig-data.json +++ b/shared-data/tsconfig-data.json @@ -12,6 +12,7 @@ "deck/**/*.json", "labware/**/*.json", "liquid/**/*.json", + "liquid-class/**/*.json", "command/**/*.json", "commandAnnotation/**/*.json", "gripper/**/*.json", diff --git a/shared-data/tsconfig.json b/shared-data/tsconfig.json index a50e215ee95..57f8970d0c6 100644 --- a/shared-data/tsconfig.json +++ b/shared-data/tsconfig.json @@ -18,6 +18,7 @@ "command", "errors", "liquid/types", + "liquid-class", "commandAnnotation/types", "vite.config.mts" ] From ecd916ba89f78f7abdf11ac82d375d212e535399 Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Thu, 21 Nov 2024 13:54:21 -0500 Subject: [PATCH 39/52] docs(api): Opentrons Tough PCR Auto-sealing Lids in Python API (#16925) # Overview Documents how to use the Opentrons Tough PCR Auto-sealing Lids and Opentrons Flex Deck Riser in a Python protocol. ## Test Plan and Hands on Testing - [Sandbox](http://sandbox.docs.opentrons.com/docs-tc-lids/v2/modules/thermocycler.html#auto-sealing-lids) - all new code samples pass simulation ## Changelog - New section on auto-sealing lids - Change load statement at top of article to use Opentrons PCR plate instead of NEST ## Review requests - Completeness, clarity, etc. - Double-check code - There is some deliberate vagueness around lid loading that is meant to future-proof against labware schema and API changes ## Risk assessment none, docs. --- api/docs/v2/modules/thermocycler.rst | 66 +++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/api/docs/v2/modules/thermocycler.rst b/api/docs/v2/modules/thermocycler.rst index 9322e0a96f0..17d57e84292 100644 --- a/api/docs/v2/modules/thermocycler.rst +++ b/api/docs/v2/modules/thermocycler.rst @@ -15,7 +15,7 @@ The examples in this section will use a Thermocycler Module GEN2 loaded as follo .. code-block:: python tc_mod = protocol.load_module(module_name="thermocyclerModuleV2") - plate = tc_mod.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt") + plate = tc_mod.load_labware(name="opentrons_96_wellplate_200ul_pcr_full_skirt") .. versionadded:: 2.13 @@ -139,6 +139,70 @@ However, this code would generate 60 lines in the protocol's run log, while exec .. versionadded:: 2.0 +Auto-sealing Lids +================= + +Starting in robot software version 8.2.0, you can use the Opentrons Tough PCR Auto-sealing Lid to reduce evaporation on the Thermocycler. The auto-sealing lids are designed for automated use with the Flex Gripper, although you can move them manually if needed. They also work with the Opentrons Flex Deck Riser adapter, which keeps lids away from the unsterilized deck and provides better access for the gripper. + +Use the following API load names for the auto-sealing lid and deck riser: + +.. list-table:: + :header-rows: 1 + + * - Labware + - API load name + * - Opentrons Tough PCR Auto-sealing Lid + - ``opentrons_tough_pcr_auto_sealing_lid`` + * - Opentrons Flex Deck Riser + - ``opentrons_flex_deck_riser`` + +Load the riser directly onto the deck with :py:meth:`.ProtocolContext.load_adapter`. Load the auto-sealing lid onto a compatible location (the deck, the riser, or another lid) with the appropriate ``load_labware()`` method. You can create a stack of up to five auto-sealing lids. If you try to stack more than five lids, the API will raise an error. + +Setting up the riser and preparing a lid to use on the Thermocycler generally consists of the following steps: + + 1. Load the riser on the deck. + 2. Load the lids onto the adapter. + 3. Load or move a PCR plate onto the Thermocycler. + 4. Move a lid onto the PCR plate. + 5. Close the Thermocycler. + +The following code sample shows how to perform these steps, using the riser and three auto-sealing lids. In a full protocol, you would likely have additional steps, such as pipetting to or from the PCR plate. + +.. code-block:: python + + # load riser + riser = protocol.load_adapter( + load_name="opentrons_flex_deck_riser", location="A2" + ) + + # load three lids + lid_1 = riser.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_2 = lid_1.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_3 = lid_2.load_labware("opentrons_tough_pcr_auto_sealing_lid") + + # load plate on Thermocycler + plate = protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", location=tc_mod + ) + + # move lid to PCR plate + protocol.move_labware(labware=lid_3, new_location=plate, use_gripper=True) + + # close Thermocycler + tc_mod.close_lid() + +.. warning:: + When using the auto-sealing lids, `do not` affix a rubber automation seal to the inside of the Thermocycler lid. The Thermocycler will not close properly. + +When you're finished with a lid, use the gripper to dispose of it in either the waste chute or a trash bin:: + + tc_mod.open_lid() + protocol.move_labware(labware=lid_3, new_location=trash, use_gripper=True) + +.. versionadded:: 2.16 + :py:class:`.TrashBin` and :py:class:`.WasteChute` objects can accept lids. + +You can then move the PCR plate off of the Thermocycler. The Flex Gripper can't move a plate that has a lid on top of it. Always move the lid first, then the plate. Changes with the GEN2 Thermocycler Module ========================================= From a910cf0a7d160685d590c0047bc56a936f1ef2c6 Mon Sep 17 00:00:00 2001 From: David Chau <46395074+ddcc4@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:38:51 -0500 Subject: [PATCH 40/52] feat(api): add liquid classes to the StateSummary. (#16899) # Overview AUTH-851 Export the liquid classes we loaded into the Protocol Engine to the `StateSummary`, which will let clients see what liquid classes we loaded. The liquid classes are stored internally as a map of `{liquid_class_id -> LiquidClassRecords}`, but following the convention for the other fields in the `StateSummary`, we want to export the liquid classes as a list, so this PR defines a new `LiquidClassRecordWithId` class for the summary. The fields in the `StateSummary` in turn are propagated to the CLI `AnalyzeResults`, and are mirrored to the robot-server `CompletedAnalysis`, `Run`, and `MaintenanceRun` models. So every call-site that uses those classes had to be updated, as well as every test that checks those classes, as well as 200 snapshot tests -- which was kind of painful. ## Test Plan and Hands on Testing I'm relying on the CI tests to make sure I found all the call-sites that are affected. (We don't yet have any protocols that load liquid classes, but when we do, we can probably add integration tests to show that the liquid classes end up in the summaries.) ## Review requests I recommend collapsing the `analyses-snapshot-testing/` when looking at this PR in Github. There are so many snapshot changes that Github sometimes errors out when trying to render the diff. The primary files with code changes are: - `api/src/opentrons/protocol_engine/types.py` - `api/src/opentrons/protocol_engine/state/state.py` - `api/src/opentrons/protocol_engine/state/state_summary.py` - `api/src/opentrons/cli/analyze.py` - `robot-server/robot_server/runs/run_models.py` - `robot-server/robot_server/protocols/analysis_models.py` - `robot-server/robot_server/maintenance_runs/maintenance_run_models.py` The other files are pretty mechanical changes. ## Risk assessment Low risk, should affect dev only. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: y3rsh <502770+y3rsh@users.noreply.github.com> --- ...2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json | 1 + ...74c503f][pl_BacteriaInoculation_Flex_6plates].json | 1 + ...011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json | 1 + ...255c3f3b][pl_Flex_Protein_Digestion_Protocol].json | 1 + ...RIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json | 1 + ..._v2_16_P300M_P20S_aspirateDispenseMix0Volume].json | 1 + ...S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json | 1 + ...[OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...pshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json | 1 + ...Overrides_TooTallLabware_Override_south_west].json | 1 + ...NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json | 1 + ...9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json | 1 + ...BadTypesInRTP_Override_wrong_type_in_maximum].json | 1 + ..._Overrides_TooTallLabware_Override_north_row].json | 1 + ...[OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...[OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json | 1 + ...9b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json | 1 + ...pshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json | 1 + ...napshot[11020a4e17][pl_Bradford_proteinassay].json | 1 + ...2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json | 1 + ...hot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json | 1 + ..._v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json | 1 + ...hot[15a60fccf4][pl_microBioID_beads_touchtip].json | 1 + ...apshot[194e3c49bb][pl_Normalization_with_PCR].json | 1 + ..._None_Overrides_TooTallLabware_Override_west].json | 1 + ...00_96_HS_GRIP_TC_TM_GripperCollisionWithTips].json | 1 + ...ne_None_HS_HeaterShakerConflictWithTrashBin2].json | 1 + ...8a126][pl_Dynabeads_IP_Flex_96well_RIT_final].json | 1 + ...1c19a2055c][OT2_S_v2_4_P300M_None_MM_TM_Zymo].json | 1 + ...t[1d37cbc080][pl_MagMax_RNA_Cells_Flex_multi].json | 1 + ...BadTypesInRTP_Override_wrong_type_in_minimum].json | 1 + ...[238912ff51][Flex_S_v2_18_KAPA_Library_Quant].json | 1 + ...3107][pl_Omega_HDQ_DNA_Cells_Flex_96_channel].json | 1 + ...7a7][OT2_X_v2_18_None_None_NoRTPdisplay_name].json | 1 + ...ysis_snapshot[250e9496be][pl_BCApeptideassay].json | 1 + ...rrides_TooTallLabware_Override_mix_collision].json | 1 + ...pshot[277961bc58][pl_NiNTA_Flex_96well_final].json | 1 + ...8fdeebdd9][pl_Omega_HDQ_DNA_Cells_Flex_multi].json | 1 + ...5df52d][pl_Omega_HDQ_DNA_Bacteria_Flex_multi].json | 1 + ...re_Override_distribute_destination_collision].json | 1 + ...llLabware_Override_transfer_source_collision].json | 1 + ...cb5b23][Flex_S_v2_18_P1000_96_TipTrackingBug].json | 1 + ...napshot[2bc6830494][pl_langone_ribo_pt1_ramp].json | 1 + ...verrides_TooTallLabware_Override_east_column].json | 1 + ...P_HS_MB_TC_TM_TriggerPrepareForMountMovement].json | 1 + ...e8][Flex_S_v2_20_96_None_SINGLE_4Corners50ul].json | 1 + ...ex_S_v2_20_96_None_SINGLE_HappyPathNorthSide].json | 1 + ...2_P300S_None_MM1_MM2_EngageMagHeightFromBase].json | 1 + ...134ee09][pl_Takara_InFusionSnapAssembly_Flex].json | 1 + ...ex_X_v2_18_NO_PIPETTES_DescriptionTooLongRTP].json | 1 + ...des_TooTallLabware_Override_bottom_left_edge].json | 1 + ...des_TooTallLabware_Override_bottom_left_edge].json | 1 + ...fRangeRTP_Override_default_less_than_minimum].json | 1 + ..._Overrides_TooTallLabware_Override_south_row].json | 1 + ...a039d726f][pl_Dynabeads_IP_Flex_96well_final].json | 1 + ...[3cec61dfd9][Flex_S_v2_19_KAPA_Library_Quant].json | 1 + ...Flex_S_v2_18_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json | 1 + ...sis_snapshot[4148613317][Flex_S_v2_19_ligseq].json | 1 + ...lysis_snapshot[42beea80be][pl_96_ch_demo_rtp].json | 1 + ...389e3ab18][OT2_X_v6_P20S_None_SimpleTransfer].json | 1 + ...20422][OT2_S_v2_18_NO_PIPETTES_GoldenRTP_OT2].json | 1 + ...][pl_sample_dilution_with_96_channel_pipette].json | 1 + ...[OT2_S_v2_16_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...S_v2_17_P300M_P20S_HS_TC_TM_dispense_changes].json | 1 + ...8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json | 1 + ..._NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json | 1 + ...Configs_Override_eight_partial_column_no_end].json | 1 + ...pshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json | 1 + ...esInRTP_Override_wrong_type_in_variable_name].json | 1 + ...d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json | 1 + ...ngeRTP_Override_default_greater_than_maximum].json | 1 + ...][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json | 1 + ...gs_Override_eight_partial_column_bottom_left].json | 1 + ...rrides_TooTallLabware_Override_c3_right_edge].json | 1 + ...Configs_Override_eight_partial_column_no_end].json | 1 + ...S_None_verifyNoFloatingPointErrorInPipetting].json | 1 + ...5b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json | 1 + ..._v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json | 1 + ...sis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json | 1 + ...6][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json | 1 + ...2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json | 1 + ...Configs_Override_ninety_six_partial_column_2].json | 1 + ...2_X_v2_18_None_None_duplicateRTPVariableName].json | 1 + ...v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json | 1 + ...ice_Override_int_default_no_matching_choices].json | 1 + ...Configs_Override_ninety_six_partial_column_1].json | 1 + ...v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json | 1 + ...619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json | 1 + ...[pl_Flex_customizable_serial_dilution_upload].json | 1 + ...[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json | 1 + ...Overrides_TooTallLabware_Override_south_east].json | 1 + ..._None_Overrides_TooTallLabware_Override_east].json | 1 + ...es_BadTypesInRTP_Override_wrong_type_in_unit].json | 1 + ...6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json | 1 + ...PETTES_Python310SyntaxRobotAnalysisOnlyError].json | 1 + ...e_Override_consolidate_destination_collision].json | 1 + ...5_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json | 1 + ...ne_None_HS_HeaterShakerConflictWithTrashBin1].json | 1 + ...ice_Override_str_default_no_matching_choices].json | 1 + ...50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json | 1 + ...297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json | 1 + ...00M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json | 1 + ...873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json | 1 + ...Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json | 1 + ...des_InvalidConfigs_Override_return_tip_error].json | 1 + ...shot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json | 1 + ...pshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json | 1 + ...P_Override_wrong_type_in_choice_display_name].json | 1 + ...verrides_TooTallLabware_Override_west_column].json | 1 + ...4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json | 1 + ...e_Override_float_default_no_matching_choices].json | 1 + ...b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json | 1 + ...8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json | 1 + ...hot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json | 1 + ...[OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...bca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json | 1 + ...de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json | 1 + ...5db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json | 1 + ...6_TC_PartialTipPickupThermocyclerLidConflict].json | 1 + ...None_Overrides_TooTallLabware_Override_south].json | 1 + ...validConfigs_Override_drop_tip_with_location].json | 1 + ...35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json | 1 + ...x_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json | 1 + ...shot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json | 1 + ...s_Override_eight_partial_column_bottom_right].json | 1 + ...8][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json | 1 + ..._GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json | 1 + ..._X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json | 1 + ...NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json | 1 + ...apshot[9bcb0a3f13][pl_normalization_with_csv].json | 1 + ..._v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json | 1 + ..._v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json | 1 + ...ypesInRTP_Override_wrong_type_in_description].json | 1 + ...6_GRIP_DeckConfiguration1NoModulesNoFixtures].json | 1 + ...Overrides_TooTallLabware_Override_north_west].json | 1 + ...d2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json | 1 + ...dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json | 1 + ...pshot[a437534569][Flex_S_v2_19_kapahyperplus].json | 1 + ...8c103][pl_Nanopore_Genomic_Ligation_v5_Final].json | 1 + ...apshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json | 1 + ...[OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...ex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json | 1 + ...napshot[a8a5ad823d][pl_cherrypicking_flex_v3].json | 1 + ...pshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json | 1 + ...x_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json | 1 + ...analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json | 1 + ...sis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json | 1 + ..._P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json | 1 + ...gs_Override_eight_partial_column_bottom_left].json | 1 + ...cedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json | 1 + ...4067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json | 1 + ...00_96_TC_pipetteCollisionWithThermocyclerLid].json | 1 + ...des_InvalidConfigs_Override_return_tip_error].json | 1 + ...6_P1000_96_TC_PartialTipPickupTryToReturnTip].json | 1 + ...shot[b48407ff98][pl_cherrypicking_csv_airgap].json | 1 + ..._v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json | 1 + ...validConfigs_Override_drop_tip_with_location].json | 1 + ...pesInRTP_Override_wrong_type_in_choice_value].json | 1 + ...d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json | 1 + ...S_v2_15_P1000S_None_SimpleNormalizeLongRight].json | 1 + ...ware_Override_transfer_destination_collision].json | 1 + ...None_Overrides_TooTallLabware_Override_north].json | 1 + ...4d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json | 1 + ...NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json | 1 + ...NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json | 1 + ...NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json | 1 + ...es_TooTallLabware_Override_bottom_right_edge].json | 1 + ...16_P1000_96_GRIP_DeckConfiguration1NoModules].json | 1 + ..._S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json | 1 + ...pshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json | 1 + ...d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json | 1 + ...OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json | 1 + ...c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json | 1 + ...snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json | 1 + ...s_Override_eight_partial_column_bottom_right].json | 1 + ...38bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json | 1 + ...sis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json | 1 + ...is_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json | 1 + ...QuickZymoMagbeadRNAExtractionCellsOrBacteria].json | 1 + ...7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json | 1 + ...[OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json | 1 + ...t[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json | 1 + ...hot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json | 1 + ...sis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json | 1 + ...][OT2_S_v2_18_None_None_duplicateChoiceValue].json | 1 + ...x_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json | 1 + ...9799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json | 1 + ...a326082e1][pl_Hyperplus_tiptracking_V4_final].json | 1 + ...Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json | 1 + ...Configs_Override_ninety_six_partial_column_3].json | 1 + ...NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json | 1 + ..._PIPETTES_TC_TrashBinAndThermocyclerConflict].json | 1 + ...lumina_Stranded_total_RNA_Ribo_Zero_protocol].json | 1 + ...M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json | 1 + ...3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json | 1 + ...a6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json | 1 + ...BadTypesInRTP_Override_wrong_type_in_default].json | 1 + ...b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json | 1 + ...napshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json | 1 + ...e_Overrides_TooTallLabware_Override_top_edge].json | 1 + ...51df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json | 1 + ...39][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json | 1 + ...abware_Override_consolidate_source_collision].json | 1 + ...P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json | 1 + ...d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json | 1 + ...0_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json | 1 + ...S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json | 1 + ...45e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json | 1 + ...T2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json | 1 + ...Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json | 1 + ...2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json | 1 + ...NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json | 1 + ...hot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json | 1 + ..._TC_pipetteCollisionWithThermocyclerLidClips].json | 1 + ...P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json | 1 + ...Labware_Override_distribute_source_collision].json | 1 + ...pesInRTP_Override_wrong_type_in_display_name].json | 1 + ...S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json | 1 + api/src/opentrons/cli/analyze.py | 4 ++++ api/src/opentrons/protocol_engine/__init__.py | 4 ++++ api/src/opentrons/protocol_engine/state/state.py | 9 +++++++-- .../opentrons/protocol_engine/state/state_summary.py | 2 ++ api/src/opentrons/protocol_engine/types.py | 9 +++++++++ .../maintenance_runs/maintenance_run_data_manager.py | 2 ++ .../maintenance_runs/maintenance_run_models.py | 5 +++++ .../robot_server/protocols/analysis_models.py | 5 +++++ robot-server/robot_server/protocols/analysis_store.py | 5 +++++ .../robot_server/protocols/protocol_analyzer.py | 2 ++ robot-server/robot_server/runs/run_data_manager.py | 3 +++ robot-server/robot_server/runs/run_models.py | 9 +++++++++ .../tests/data_files/test_data_files_store.py | 1 + .../protocols/test_v6_json_upload.tavern.yaml | 1 + .../protocols/test_v8_json_upload_flex.tavern.yaml | 1 + .../protocols/test_v8_json_upload_ot2.tavern.yaml | 1 + .../runs/test_json_v6_protocol_run.tavern.yaml | 1 + .../runs/test_json_v7_protocol_run.tavern.yaml | 1 + .../http_api/runs/test_protocol_run.tavern.yaml | 2 ++ .../test_run_queued_protocol_commands.tavern.yaml | 1 + .../test_run_with_run_time_parameters.tavern.yaml | 1 + .../tests/maintenance_runs/router/test_base_router.py | 4 ++++ .../maintenance_runs/router/test_labware_router.py | 1 + .../tests/maintenance_runs/test_run_data_manager.py | 4 ++++ robot-server/tests/protocols/test_analysis_store.py | 8 ++++++++ .../tests/protocols/test_completed_analysis_store.py | 1 + .../tests/protocols/test_protocol_analyzer.py | 3 +++ robot-server/tests/protocols/test_protocol_store.py | 3 +++ robot-server/tests/protocols/test_protocols_router.py | 1 + robot-server/tests/runs/router/test_base_router.py | 8 ++++++++ robot-server/tests/runs/router/test_labware_router.py | 1 + robot-server/tests/runs/test_run_data_manager.py | 11 +++++++++++ 251 files changed, 331 insertions(+), 2 deletions(-) diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json index 3d8b4b072eb..f59c9684e23 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json @@ -16668,6 +16668,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json index ddb334a58e0..05fa920a764 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json @@ -41584,6 +41584,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "Bacterial culture medium (e.g., LB broth)", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json index 38872b09ff8..c709366a42a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json @@ -4919,6 +4919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json index aac975221e8..2da7a9c47bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json @@ -11824,6 +11824,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json index ff626992e43..4f1452dcdfc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json @@ -11452,6 +11452,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json index 8cd99860d7e..39491fae6aa 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json @@ -2917,6 +2917,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json index c62ceb23edd..a561da0a387 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3113,6 +3113,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json index a2aca7e252a..fe3d81be11b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9569,6 +9569,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json index bce38cbe476..f85b03c5703 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json @@ -66156,6 +66156,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json index 2ca289680ef..0f7d7d308b5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 7a7269decb6..d9895fb2c9e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -154,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json index 4891466d0b7..f892fc456ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json @@ -49707,6 +49707,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json index 64072eb8834..a877268d0bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json index dfef8b35364..3a0f63a8f99 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 0096a483ffe..957e685c737 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -17072,6 +17072,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json index e4924262e1a..35dc7ecc804 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9569,6 +9569,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json index 7bff37154bf..db42ce35fdc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json @@ -49392,6 +49392,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json index d0b11f42740..cf56c96470e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json @@ -6263,6 +6263,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a ROW partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json index f7457a3c48d..e4de2f89a14 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json @@ -33697,6 +33697,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json index 6fb9e302070..7ce2978d56a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json @@ -19352,6 +19352,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Coomassie Brilliant Blue G-250 solution ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json index f2c63721b33..d33b6cf51cb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json @@ -7997,6 +7997,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json index c463feb0552..8fece97c06c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json @@ -6203,6 +6203,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "water for ER testing", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json index 0b2e524dee6..c30b18aa93e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4492,6 +4492,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json index 6053323ac4b..3f500210e5a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json @@ -34590,6 +34590,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json index ababd25acfa..059f375baec 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json @@ -9297,6 +9297,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Rami Farawi ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json index 858286887b6..fd7b30ca845 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json @@ -19714,6 +19714,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT-2 protocol with 1ch and 8ch pipette partial/single tip configurations. Mixing tipracks and using separate tipracks. ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json index d810bd75c88..b63443781ac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json @@ -103,6 +103,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index 90bfa119fb7..a126374479b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json index 3af042768f6..f102cab8bc5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json index 843078fa552..484c6600849 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json index dfc888c15b5..3a839b9cdbd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json @@ -23420,6 +23420,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cells in DNA/ RNA Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json index d2955132ff2..72f8481bc29 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json index 2b447932025..4c45089bc7c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json @@ -6064,6 +6064,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index 0aaa562c15c..58b6e3ffb42 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -1284,6 +1284,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json index 952985449d9..dde453f20ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index d28023877a0..8cb5125c17c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -2395,6 +2395,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json index 9cad51f6d80..27656b80cca 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json @@ -1894,6 +1894,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json index c5c5f1a2e67..84bff8651d2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json @@ -49707,6 +49707,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json index 7c04e4274de..63ed50d9c04 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json index 3646ae2d522..4744b1f1992 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json @@ -22844,6 +22844,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json index e608af8c173..353a1b46f45 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json @@ -46193,6 +46193,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Resuspended in PBS", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json index c76b2aca7f9..059e7fc2b84 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json @@ -1258,6 +1258,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json index a107fa87e60..47c65a0dfc5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json index 86d3274f412..3c69dda38e7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Multiple RTP Variables with Same Name" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json index 0de0eff0022..fde783d94b8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json @@ -197,6 +197,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json index 726906c04d4..b8ef1cbc5f2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json index 1dcac6e453a..180178d1d44 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json index d8409d8db46..8623a021746 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json index afa5bb0b4d2..8b06eca9390 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json @@ -295,6 +295,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Golden RTP Examples" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json index 385da3c78a4..b1528f23cbf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json @@ -10386,6 +10386,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Diluent liquid is filled in the reservoir", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json index 5681dc28194..1441d3d1cac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json @@ -16386,6 +16386,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cell Pellet", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json index 1bf35620512..d27c90a866c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json index b2ec113fe4e..7209e028a2b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json index e545da56bd4..e30b5bee0d8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json index 19ac0d4e0f7..3ac36a59ee5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json @@ -6263,6 +6263,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a COLUMN partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json index 8f88134625a..da1993d6e56 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json @@ -94,6 +94,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json index cf3e8bf4aa3..0e079e7daa2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json @@ -3946,6 +3946,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json index 66877246558..eba57a84196 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json @@ -56935,6 +56935,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json index 63567ca7c96..f052823d867 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json @@ -512,6 +512,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Heater-shaker conflict OT-2" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json index cae3345ff13..2b5614762ba 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json index d67ff04865b..fd1c3550795 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json @@ -45393,6 +45393,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json index 80a9f7d117a..0028c36df1b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json @@ -3435,6 +3435,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.3" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json index ad8638a9e6d..86023eb8c12 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json @@ -2849,6 +2849,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json index cbd7839e9ad..b79aec33a1b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json @@ -65064,6 +65064,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "100 mM ABC in MS grade water, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json index 1b70c59e4b6..a79c72e6781 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json @@ -5042,6 +5042,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json index 30ddffb8e03..3a2911f043d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -4920,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json index bc24730fad8..c577f539508 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json @@ -31929,6 +31929,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Dilution Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json index 19cf70d2edb..00efc4b9178 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json index bd4f009a701..ae9e8d99862 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json index df37cc2db4b..48077d59118 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json index 47ce454e920..ac4311409dc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json @@ -75905,6 +75905,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json index 0c559ae74b3..235d5eb9fe3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json index 44584111a12..e0fd663c213 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json index 63aed19f5f3..d4cf07c0f99 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json @@ -2666,6 +2666,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json index 80ce54abbcb..2312c3a011e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json @@ -59919,6 +59919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 9f3a0d8a1fb..ac524674f7e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -14372,6 +14372,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json index 3cc6db1a5cd..7fb0dceab92 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json @@ -49169,6 +49169,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Volume in Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json index 5d219d91f72..7eedccb2cf8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json @@ -5373,6 +5373,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json index 2b9cd2584d3..70bb212b45b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Str RTP with unit" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json index 47511dff64f..ea3c1cc76b0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json @@ -4997,6 +4997,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json index e2fadc01642..cadf197c142 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index 919f1980537..e433acf53ff 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -3766,6 +3766,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json index b8dd13f5f42..c21c19205cf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json @@ -18477,6 +18477,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Equilibration Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json index 1d83bf0706f..933aa66cf7d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json @@ -3727,6 +3727,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json index 349fbd62034..794499f75ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json @@ -52167,6 +52167,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 2c3d142321b..d9f59af3587 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -2468,6 +2468,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json index 8c086d8fdff..405df785df9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json @@ -6474,6 +6474,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json index 85ee931590d..004f5251126 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json @@ -32273,6 +32273,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json index cbad73a3a2d..8a871949e46 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json @@ -2775,6 +2775,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index cbf301b89e7..5538166da59 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json index b0f0b8ac0bd..23fd7f389a0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json @@ -6024,6 +6024,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json index 8d4e3a960dd..aba00388845 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json @@ -1435,6 +1435,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json index 5a508d84d58..7cb88cd0308 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json @@ -153,6 +153,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json index 7808bbc2d03..e2a5dced311 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json index f951219fdff..5bc309d3cac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json @@ -9575,6 +9575,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json index eca34fc28c3..68185db5dbd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json index 965ca7d3ead..7aecea25f6a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json @@ -50265,6 +50265,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Digested Protein samples, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json index 1c3e57b481a..36400ae7de7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json index 42781ff6ea1..fe8184c0608 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json index be8c1a00d13..e623aec42f7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json @@ -22844,6 +22844,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json index 8a0a8a6a2ee..0aacb0b3e73 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json @@ -16593,6 +16593,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "generic", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 321a04e20ac..e6cb5eace9a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -14092,6 +14092,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json index f4e89bf46a3..b0eb2e93f00 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json @@ -9431,6 +9431,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Unsafe protocol ❗❗❗❗❗❗❗❗❗❗❗ will collide with tube.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json index 6afef67d006..86a33113a16 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json @@ -10373,6 +10373,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json index 01ce458ff53..52120bd6dc3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json index 60a0f1c77a3..1d043a44952 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json index 8e14d013357..93eff2447db 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json @@ -46385,6 +46385,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json index b4324589435..ec2e77260d2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json @@ -30083,6 +30083,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "ATL4", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json index ef9acd1b1a3..30f2c70b0ea 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json @@ -15276,6 +15276,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index c8389b97d75..1f453e29cf8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -2438,6 +2438,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json index 3a44acf987c..4af69fce36b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json @@ -9492,6 +9492,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json index d3338855040..ed08b660a33 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json index f86080f047c..a18485392e9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json @@ -6240,6 +6240,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json index a5b5bdb65cc..9f12179d1e2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -3766,6 +3766,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json index 5b0df3b070c..b41b7117e24 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json @@ -3706,6 +3706,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json index 4433e026fd1..6dffb02e16c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json @@ -28815,6 +28815,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json index 7005e6011ab..43f62a32282 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4492,6 +4492,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index b3637624ed4..7956e369c52 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -4920,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json index 6b2391f6118..cf8ec946db5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json index 4bcec7cf7de..db24530e196 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json @@ -41229,6 +41229,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Magnetic Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json index 5460d2d1fd7..851fd7e1fbb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json @@ -125769,6 +125769,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json index 290674f3bd6..8c741ed84ba 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json @@ -3912,6 +3912,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json index 67a07aa1297..8c38d6c5f57 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json index 14cc53aba17..4d2cd13d215 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json @@ -1977,6 +1977,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index d6eb8a28124..c2e5c309dcf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -145,6 +145,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index f59969368ab..7c817b2b869 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 4c6c38162b3..f69446ee9cb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -154,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json index 4665f21b62e..fe3f96abc67 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json index aadd38b4eaa..1a48b84ae0a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json @@ -12315,6 +12315,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json index 27e9d4f2c51..253a2bcff9d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json @@ -10777,6 +10777,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json index 131c7514649..06153dd11b4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json @@ -66156,6 +66156,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json index 5efbff81ebc..cf748ae6fa0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json @@ -7184,6 +7184,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json index 1759b7b244f..ac74dbfc2a4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json @@ -12796,6 +12796,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json index 4ad4434ab42..c9afc886f56 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json @@ -7179,6 +7179,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json index 4e8f71a17c1..b591039cbbb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json @@ -13346,6 +13346,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Index Plate color", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 1149640d8b1..f449eff0d94 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -1314,6 +1314,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json index 6e02fa8a3f3..a4d46be3d94 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json @@ -18032,6 +18032,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json index 9d35aba10fc..2f0c52a853f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json @@ -70949,6 +70949,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json index 52e87c76f46..1b664b4e963 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json @@ -5113,6 +5113,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Test this wet!!!", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json index 6f5f1f09b83..7fd14d2f851 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json @@ -25208,6 +25208,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json index f0d2d744031..7916f424286 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json @@ -40886,6 +40886,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Lysis Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 3056b873a74..718e0a0df13 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -17430,6 +17430,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json index 026977dbcc6..5b6c3c3c690 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json @@ -2836,6 +2836,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json index 99ccd21cc19..170de395195 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json @@ -59919,6 +59919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json index 6b342319f31..1ad848a9ef8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json index 7ea850030fd..87b61a0454d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json @@ -43,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Duplicate choice value" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index 1e9b318abf5..aba7dd56957 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -3591,6 +3591,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json index 65c2da26059..bd408636813 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json @@ -8241,6 +8241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT2 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json index 8b7cf7214ac..2651a003e75 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index b262ea72c0f..c52ed516ba1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -13164,6 +13164,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json index beb0aa09c29..62ea1e316b2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 2288dccf926..9e3cf07280a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -145,6 +145,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json index 0353b26aed1..2eb5308529f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Thermocycler conflict 1" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json index b22e56cb8ed..1bb680c2c4f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json @@ -39234,6 +39234,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json index 6a1c9e67b51..6c6c30ace61 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json @@ -34667,6 +34667,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json index bd05f58334f..0dd0410636f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json @@ -45,6 +45,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json index 44fbc26f5b6..1e4573b1d8e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json @@ -2580,6 +2580,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "AA BB", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json index 013da0c0d7d..f6c1ad84067 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json index 7f0ba6fd654..aca7454ff36 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json index a0e23ed018b..803f4133451 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json index 3ab5889bbf7..ea9fbf3efb7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json index ca6f70d1692..81ebf160345 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json @@ -26843,6 +26843,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json index 368bbe05d9b..709b448717c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json @@ -103,6 +103,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json index 61a7e9595ff..ed5d5a67171 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json @@ -3878,6 +3878,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json index 00f911388c0..d946aae6d9d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json @@ -40778,6 +40778,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json index 4bcefec1199..d1a7a88d075 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json @@ -1448,6 +1448,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json index d1feceae4d0..d454695d871 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json @@ -18009,6 +18009,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json index 4e89581c149..3d7f6d10b51 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json @@ -7101,6 +7101,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json index ab9fd95e4c0..d86eae54045 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json @@ -10615,6 +10615,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json index b12618b009e..e0c21a82e55 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json @@ -29,6 +29,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index f8f121ce092..50ab65351e1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -15386,6 +15386,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json index d452cf7ab52..950c5ee4395 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json @@ -1339,6 +1339,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index 2c598934321..e4fed39c549 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -180,6 +180,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json index 8ca9a88cdbf..920a648041a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json @@ -36949,6 +36949,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Amplified Libraries_1", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json index 04d54b06b4e..7ad30e9d04e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json @@ -1385,6 +1385,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json index 3152a671909..acf7455e286 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json @@ -14192,6 +14192,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json index 09e15f48097..1fdb58d69ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json @@ -3878,6 +3878,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json index 1652972327b..fe2e22fae05 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json index 13f15c638d0..cd25845d931 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3105,6 +3105,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 8489da83d68..4c994fcf630 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -53,6 +53,7 @@ LoadedPipette, LoadedModule, Liquid, + LiquidClassRecordWithId, StateSummary, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree @@ -333,6 +334,7 @@ async def _do_analyze( wells=[], hasEverEnteredErrorRecovery=False, files=[], + liquidClasses=[], ), parameters=[], ) @@ -399,6 +401,7 @@ async def _analyze( pipettes=analysis.state_summary.pipettes, modules=analysis.state_summary.modules, liquids=analysis.state_summary.liquids, + liquidClasses=analysis.state_summary.liquidClasses, ) _call_for_output_of_kind( @@ -486,4 +489,5 @@ class AnalyzeResults(BaseModel): pipettes: List[LoadedPipette] modules: List[LoadedModule] liquids: List[Liquid] + liquidClasses: List[LiquidClassRecordWithId] errors: List[ErrorOccurrence] diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 25599189916..7efaef7199d 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -57,6 +57,8 @@ ModuleModel, ModuleDefinition, Liquid, + LiquidClassRecord, + LiquidClassRecordWithId, AllNozzleLayoutConfiguration, SingleNozzleLayoutConfiguration, RowNozzleLayoutConfiguration, @@ -122,6 +124,8 @@ "ModuleModel", "ModuleDefinition", "Liquid", + "LiquidClassRecord", + "LiquidClassRecordWithId", "AllNozzleLayoutConfiguration", "SingleNozzleLayoutConfiguration", "RowNozzleLayoutConfiguration", diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 47b15e4eb3b..58e977cc2f4 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -9,7 +9,7 @@ from opentrons_shared_data.robot.types import RobotDefinition from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy -from opentrons.protocol_engine.types import ModuleOffsetData +from opentrons.protocol_engine.types import LiquidClassRecordWithId, ModuleOffsetData from opentrons.util.change_notifier import ChangeNotifier from ..resources import DeckFixedLabware @@ -156,7 +156,12 @@ def get_summary(self) -> StateSummary: wells=self._wells.get_all(), hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(), files=self._state.files.file_ids, - # TODO(dc): Do we want to just dump all the liquid classes into the summary? + liquidClasses=[ + LiquidClassRecordWithId( + liquidClassId=liquid_class_id, **dict(liquid_class_record) + ) + for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items() + ], ) diff --git a/api/src/opentrons/protocol_engine/state/state_summary.py b/api/src/opentrons/protocol_engine/state/state_summary.py index 7e47ccbbb37..d6b18613071 100644 --- a/api/src/opentrons/protocol_engine/state/state_summary.py +++ b/api/src/opentrons/protocol_engine/state/state_summary.py @@ -11,6 +11,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, WellInfoSummary, ) @@ -32,3 +33,4 @@ class StateSummary(BaseModel): liquids: List[Liquid] = Field(default_factory=list) wells: List[WellInfoSummary] = Field(default_factory=list) files: List[str] = Field(default_factory=list) + liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 1a11a99df86..2a0bbf78c28 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -887,6 +887,15 @@ def dict_to_tuple(d: dict[str, Any]) -> tuple[tuple[str, Any], ...]: return hash(dict_to_tuple(self.dict())) +class LiquidClassRecordWithId(LiquidClassRecord, frozen=True): + """A LiquidClassRecord with its ID, for use in summary lists.""" + + liquidClassId: str = Field( + ..., + description="Unique identifier for this liquid class.", + ) + + class SpeedRange(NamedTuple): """Minimum and maximum allowed speeds for a shaking module.""" diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index 46b2c86bd40..c3346e33351 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -34,6 +34,7 @@ def _build_run( liquids=[], wells=[], files=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) return MaintenanceRun.construct( @@ -50,6 +51,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + liquidClasses=state_summary.liquidClasses, hasEverEnteredErrorRecovery=state_summary.hasEverEnteredErrorRecovery, ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py index e4c5971f5d1..8bde7ea7aff 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py @@ -12,6 +12,7 @@ LabwareOffset, LabwareOffsetCreate, Liquid, + LiquidClassRecordWithId, ) from robot_server.service.json_api import ResourceModel @@ -67,6 +68,10 @@ class MaintenanceRun(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 1e377aec3dd..61a66866bb0 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -19,6 +19,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, ) @@ -185,6 +186,10 @@ class CompletedAnalysis(BaseModel): default_factory=list, description="Liquids used by the protocol", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + default_factory=list, + description="Liquid classes used by the protocol", + ) errors: List[ErrorOccurrence] = Field( ..., description=( diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 71d170c6581..2f46f7857cb 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -19,6 +19,7 @@ LoadedLabware, LoadedModule, Liquid, + LiquidClassRecordWithId, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree @@ -152,6 +153,7 @@ async def update( pipettes: List[LoadedPipette], errors: List[ErrorOccurrence], liquids: List[Liquid], + liquidClasses: List[LiquidClassRecordWithId], ) -> None: """Promote a pending analysis to completed, adding details of its results. @@ -167,6 +169,7 @@ async def update( errors: See `CompletedAnalysis.errors`. Also used to infer whether the completed analysis result is `OK` or `NOT_OK`. liquids: See `CompletedAnalysis.liquids`. + liquidClasses: See `CompletedAnalysis.liquidClasses`. robot_type: See `CompletedAnalysis.robotType`. """ protocol_id = self._pending_store.get_protocol_id(analysis_id=analysis_id) @@ -201,6 +204,7 @@ async def update( pipettes=pipettes, errors=errors, liquids=liquids, + liquidClasses=liquidClasses, ) completed_analysis_resource = CompletedAnalysisResource( id=completed_analysis.id, @@ -241,6 +245,7 @@ async def save_initialization_failed_analysis( pipettes=[], errors=errors, liquids=[], + liquidClasses=[], ) completed_analysis_resource = CompletedAnalysisResource( id=completed_analysis.id, diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index 89387c5cefc..cf1d0687062 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -107,6 +107,7 @@ async def analyze( pipettes=result.state_summary.pipettes, errors=result.state_summary.errors, liquids=result.state_summary.liquids, + liquidClasses=result.state_summary.liquidClasses, ) async def update_to_failed_analysis( @@ -136,6 +137,7 @@ async def update_to_failed_analysis( ) ], liquids=[], + liquidClasses=[], ) def __del__(self) -> None: diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 47fe28232d1..3724e96b486 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -66,6 +66,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + liquidClasses=state_summary.liquidClasses, outputFileIds=state_summary.files, runTimeParameters=run_time_parameters, ) @@ -80,6 +81,7 @@ def _build_run( pipettes=[], modules=[], liquids=[], + liquidClasses=[], wells=[], files=[], hasEverEnteredErrorRecovery=False, @@ -124,6 +126,7 @@ def _build_run( completedAt=state.completedAt, startedAt=state.startedAt, liquids=state.liquids, + liquidClasses=state.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=state.files, hasEverEnteredErrorRecovery=state.hasEverEnteredErrorRecovery, diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 2ed77b0d0bc..337366e1478 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ LabwareOffset, LabwareOffsetCreate, Liquid, + LiquidClassRecordWithId, CommandNote, ) from opentrons.protocol_engine.types import ( @@ -134,6 +135,10 @@ class Run(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", @@ -215,6 +220,10 @@ class BadRun(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", diff --git a/robot-server/tests/data_files/test_data_files_store.py b/robot-server/tests/data_files/test_data_files_store.py index 581577d0a16..9a9b722e6ec 100644 --- a/robot-server/tests/data_files/test_data_files_store.py +++ b/robot-server/tests/data_files/test_data_files_store.py @@ -99,6 +99,7 @@ def _get_sample_analysis_resource( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index 717280a6703..516221c500c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -590,6 +590,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv6 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index 35801f8719a..022c86da35e 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -623,6 +623,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv8 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index f85e307e961..961a9a26601 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -626,6 +626,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv8 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index fd98c29a2dc..580688e6e65 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -51,6 +51,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] runTimeParameters: [] outputFileIds: [] protocolId: '{protocol_id}' diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 3ab7386ba4f..1caf41fbfd1 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -53,6 +53,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] protocolId: '{protocol_id}' - name: Execute a setup command diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index 2ad0a92eb8c..732726d39e9 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -47,6 +47,7 @@ stages: outputFileIds: [] protocolId: '{protocol_id}' liquids: [] + liquidClasses: [] save: json: original_run_data: data @@ -240,6 +241,7 @@ stages: createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" startedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" liquids: [] + liquidClasses: [] runTimeParameters: [] outputFileIds: [] completedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" diff --git a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml index 14ae502d800..95f5077c30e 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml @@ -95,6 +95,7 @@ stages: labware: [] labwareOffsets: [] liquids: [] + liquidClasses: [] runTimeParameters: [] outputFileIds: [] modules: [] diff --git a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml index 1f44f7101c7..505ff2d8831 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml @@ -118,6 +118,7 @@ stages: name: sample_plates.csv outputFileIds: [] liquids: [] + liquidClasses: [] protocolId: '{protocol_id}' - name: Play the run diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 35fb6da06c1..29a9c81a3b7 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -75,6 +75,7 @@ async def test_create_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -150,6 +151,7 @@ async def test_get_run_data_from_url( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -200,6 +202,7 @@ async def test_get_run() -> None: labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -226,6 +229,7 @@ async def test_get_current_run( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) decoy.when(mock_maintenance_run_data_manager.current_run_id).then_return( diff --git a/robot-server/tests/maintenance_runs/router/test_labware_router.py b/robot-server/tests/maintenance_runs/router/test_labware_router.py index d8a8fdab603..4e5ae1152f2 100644 --- a/robot-server/tests/maintenance_runs/router/test_labware_router.py +++ b/robot-server/tests/maintenance_runs/router/test_labware_router.py @@ -38,6 +38,7 @@ def run() -> MaintenanceRun: modules=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index a4431f7b463..07bc9c2e476 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -69,6 +69,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) @@ -140,6 +141,7 @@ async def test_create( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) @@ -193,6 +195,7 @@ async def test_create_with_options( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) @@ -262,6 +265,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) assert subject.current_run_id == run_id diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 1200f5aff43..d15e9925a18 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -203,6 +203,7 @@ async def test_returned_in_order_added( commands=[], errors=[], liquids=[], + liquidClasses=[], ) subject.add_pending( @@ -266,6 +267,7 @@ async def test_update_adds_details_and_completes_analysis( commands=[], errors=[], liquids=[], + liquidClasses=[], ) result = await subject.get("analysis-id") @@ -283,6 +285,7 @@ async def test_update_adds_details_and_completes_analysis( commands=[], errors=[], liquids=[], + liquidClasses=[], ) assert await subject.get_by_protocol("protocol-id") == [result] assert json.loads(result_as_document) == { @@ -315,6 +318,7 @@ async def test_update_adds_details_and_completes_analysis( "commands": [], "errors": [], "liquids": [], + "liquidClasses": [], "modules": [], } @@ -364,6 +368,7 @@ async def test_update_adds_rtp_values_to_completed_store( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) @@ -384,6 +389,7 @@ async def test_update_adds_rtp_values_to_completed_store( commands=[], errors=[], liquids=[], + liquidClasses=[], ) decoy.verify( await mock_completed_store.make_room_and_add( @@ -487,6 +493,7 @@ async def test_update_infers_status_from_errors( modules=[], pipettes=[], liquids=[], + liquidClasses=[], ) analysis = (await subject.get_by_protocol("protocol-id"))[0] assert isinstance(analysis, CompletedAnalysis) @@ -528,6 +535,7 @@ async def test_save_initialization_failed_analysis( commands=[], errors=[error_occurence], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 42c12565c14..a8112cdda16 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -209,6 +209,7 @@ async def test_get_by_analysis_id_as_document( "errors": [], "labware": [], "liquids": [], + "liquidClasses": [], "modules": [], "pipettes": [], } diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index 5d3d9da8a13..3fab95879fe 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -189,6 +189,7 @@ async def test_analyze( modules=[], labwareOffsets=[], liquids=[], + liquidClasses=[], wells=[], files=[], hasEverEnteredErrorRecovery=False, @@ -211,6 +212,7 @@ async def test_analyze( pipettes=[analysis_pipette], errors=[], liquids=[], + liquidClasses=[], ) ) @@ -294,5 +296,6 @@ async def test_analyze_updates_pending_on_error( pipettes=[], errors=[error_occurrence], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index ca965d471a8..499bf480cf0 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -526,6 +526,7 @@ def get_completed_analysis_resource( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) @@ -566,6 +567,7 @@ async def test_get_referenced_data_files( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) analysis_resource2 = CompletedAnalysisResource( @@ -582,6 +584,7 @@ async def test_get_referenced_data_files( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index 637a2ee082f..0ae2c591ebd 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -1495,6 +1495,7 @@ async def test_get_protocol_analyses( commands=[], errors=[], liquids=[], + liquidClasses=[], ) decoy.when(protocol_store.has("protocol-id")).then_return(True) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index e43027b3bf1..fab2c0af888 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -157,6 +157,7 @@ async def test_create_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -245,6 +246,7 @@ async def test_create_protocol_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -413,6 +415,7 @@ async def test_get_run_data_from_url( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -461,6 +464,7 @@ async def test_get_run() -> None: labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -508,6 +512,7 @@ async def test_get_runs_not_empty( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -525,6 +530,7 @@ async def test_get_runs_not_empty( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -605,6 +611,7 @@ async def test_update_run_to_not_current( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -641,6 +648,7 @@ async def test_update_current_none_noop( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py index 900eac530f1..1252d983efb 100644 --- a/robot-server/tests/runs/router/test_labware_router.py +++ b/robot-server/tests/runs/router/test_labware_router.py @@ -40,6 +40,7 @@ def run() -> Run: labwareOffsets=[], protocolId=None, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 5e4aed1f3e2..d27e1aebaff 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -105,6 +105,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) @@ -288,6 +289,7 @@ async def test_create( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=[bool_parameter, file_parameter], outputFileIds=engine_state_summary.files, ) @@ -395,6 +397,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -438,6 +441,7 @@ async def test_get_historical_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -482,6 +486,7 @@ async def test_get_historical_run_no_data( pipettes=[], modules=[], liquids=[], + liquidClasses=[], runTimeParameters=run_time_parameters, outputFileIds=[], ) @@ -503,6 +508,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="current-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="current-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) current_run_time_parameters: List[pe_types.RunTimeParameter] = [ @@ -523,6 +529,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="old-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="old-module-id")], # type: ignore[call-arg] liquids=[], + liquidClasses=[], wells=[], ) historical_run_time_parameters: List[pe_types.RunTimeParameter] = [ @@ -584,6 +591,7 @@ async def test_get_all_runs( pipettes=historical_run_data.pipettes, modules=historical_run_data.modules, liquids=historical_run_data.liquids, + liquidClasses=historical_run_data.liquidClasses, runTimeParameters=historical_run_time_parameters, outputFileIds=historical_run_data.files, ), @@ -601,6 +609,7 @@ async def test_get_all_runs( pipettes=current_run_data.pipettes, modules=current_run_data.modules, liquids=current_run_data.liquids, + liquidClasses=current_run_data.liquidClasses, runTimeParameters=current_run_time_parameters, outputFileIds=current_run_data.files, ), @@ -700,6 +709,7 @@ async def test_update_current( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -757,6 +767,7 @@ async def test_update_current_noop( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) From 54dda4098bf1edadb0d75b9920afa9305f1f525c Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:34:45 -0500 Subject: [PATCH 41/52] fix(components): fix EmptySelectorButton style (#16932) Add hover state and fix border radius for EmptySelectorButton component Closes RQA-3659 --- .../src/atoms/buttons/EmptySelectorButton.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index 42e8822fc35..da34a8ba710 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -1,27 +1,18 @@ import styled from 'styled-components' import { Flex } from '../../primitives' import { + ALIGN_CENTER, CURSOR_DEFAULT, CURSOR_POINTER, + FLEX_MAX_CONTENT, Icon, - SPACING, - StyledText, JUSTIFY_CENTER, JUSTIFY_START, - ALIGN_CENTER, - FLEX_MAX_CONTENT, + SPACING, + StyledText, } from '../../index' -import { - black90, - blue30, - blue50, - grey30, - grey40, - white, -} from '../../helix-design-system/colors' -import { borderRadius8 } from '../../helix-design-system/borders' +import { BORDERS, COLORS } from '../../helix-design-system' import type { IconName } from '../../index' - interface EmptySelectorButtonProps { onClick: () => void text: string @@ -41,10 +32,9 @@ export function EmptySelectorButton( ` border: none; width: ${FLEX_MAX_CONTENT}; height: ${FLEX_MAX_CONTENT}; - cursor: ${({ disabled }) => (disabled ? CURSOR_DEFAULT : CURSOR_POINTER)}; + cursor: ${CURSOR_POINTER}; + background-color: ${COLORS.blue30}; + border-radius: ${BORDERS.borderRadius8}; + &:focus-visible { - outline: 2px solid ${white}; - box-shadow: 0 0 0 4px ${blue50}; - border-radius: ${borderRadius8}; + outline: 2px solid ${COLORS.white}; + box-shadow: 0 0 0 4px ${COLORS.blue50}; + border-radius: ${BORDERS.borderRadius8}; + } + &:hover { + background-color: ${COLORS.blue35}; + } + &:disabled { + background-color: ${COLORS.grey20}; + cursor: ${CURSOR_DEFAULT}; } ` From 7411eaf27eb34acd3670d4aa1dd15388fca2217e Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:35:01 -0500 Subject: [PATCH 42/52] fix(components, protocol-designer): hug hotkey shortcut tag width (#16936) Add optional shrinkToContent to Tag component Closes [RQA-3667](https://opentrons.atlassian.net/browse/RQA-3667) --- components/src/atoms/Tag/index.tsx | 6 ++-- .../localization/en/starting_deck_state.json | 4 +-- .../__tests__/ProtocolSteps.test.tsx | 4 +-- .../pages/Designer/ProtocolSteps/index.tsx | 31 ++++++++++++++----- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/components/src/atoms/Tag/index.tsx b/components/src/atoms/Tag/index.tsx index c41025dd25b..74c72da486e 100644 --- a/components/src/atoms/Tag/index.tsx +++ b/components/src/atoms/Tag/index.tsx @@ -1,7 +1,7 @@ import { css } from 'styled-components' import { BORDERS, COLORS } from '../../helix-design-system' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { ALIGN_CENTER, DIRECTION_ROW, FLEX_MAX_CONTENT } from '../../styles' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Icon } from '../../icons' import { LegacyStyledText } from '../StyledText' @@ -19,6 +19,7 @@ export interface TagProps { iconPosition?: 'left' | 'right' /** Tagicon */ iconName?: IconName + shrinkToContent?: boolean } const defaultColors = { @@ -42,11 +43,12 @@ const TAG_PROPS_BY_TYPE: Record< } export function Tag(props: TagProps): JSX.Element { - const { iconName, type, text, iconPosition } = props + const { iconName, type, text, iconPosition, shrinkToContent = false } = props const DEFAULT_CONTAINER_STYLE = css` padding: ${SPACING.spacing2} ${SPACING.spacing8}; border-radius: ${BORDERS.borderRadius4}; + width: ${shrinkToContent ? FLEX_MAX_CONTENT : 'none'}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing12}; diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index fcf88c2866e..080aa23199d 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -13,7 +13,7 @@ "clear_labware": "Clear labware", "clear_slot": "Clear slot", "clear": "Clear", - "command_click_to_multi_select": "Command + Click for multi-select", + "command_click_to_multi_select": "^/⌘ + click to select multiple", "convert_gen1_to_gen2": "To convert engage heights from GEN1 to GEN2, divide your engage height by 2.", "convert_gen2_to_gen1": "To convert engage heights from GEN2 to GEN1, multiply your engage height by 2.", "custom": "Custom labware definitions", @@ -48,7 +48,7 @@ "read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules", "rename_lab": "Rename labware", "reservoir": "Reservoirs", - "shift_click_to_select_all": "Shift + Click to select all", + "shift_click_to_select_range": "⇧ + click to select range", "starting_deck_state": "Starting deck state", "tc_slots_occupied_flex": "The Thermocycler needs slots A1 and B1. Slot A1 is occupied", "tc_slots_occupied_ot2": "The Thermocycler needs slots 7, 8, 10, and 11. One or more of those slots is occupied", diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx index f68928c3488..2ce91e03263 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -95,8 +95,8 @@ describe('ProtocolSteps', () => { it('renders the hot keys display', () => { render() screen.getByText('Double-click to edit') - screen.getByText('Shift + Click to select all') - screen.getByText('Command + Click for multi-select') + screen.getByText('⇧ + click to select range') + screen.getByText('^/⌘ + click to select multiple') }) it('renders the current step name', () => { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index b5e5810c2da..687553f3dc6 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, - Box, COLORS, DIRECTION_COLUMN, Flex, @@ -126,13 +125,29 @@ export function ProtocolSteps(): JSX.Element { {enableHoyKeyDisplay ? ( - - - - - - - + + + + + ) : null} {formData == null && selectedSubstep ? ( From baa6ec61bfe8ffaf01d8e55562a9709e4072318f Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 21 Nov 2024 15:44:50 -0500 Subject: [PATCH 43/52] fix(protocol-designer): fix Incorrect copy in IncompatibleTipsModal (#16939) * fix(protocol-designer): fix Incorrect copy in IncompatibleTipsModal --- .../src/assets/localization/en/create_new_protocol.json | 2 +- .../__tests__/IncompatibleTipsModal.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index 269c252dd39..f2e132728a5 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -11,7 +11,7 @@ "edit": "Edit", "fixtures_added": "Fixtures added", "fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.", - "incompatible_tip_body": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.", + "incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.", "incompatible_tips": "Incompatible tips", "labware_name": "Labware name", "left_right": "Left + Right", diff --git a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx index 96732965dea..2a1a8cf7e4e 100644 --- a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx +++ b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx @@ -26,7 +26,7 @@ describe('IncompatibleTipsModal', () => { render(props) screen.getByText('Incompatible tips') screen.getByText( - 'Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.' + 'Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.' ) fireEvent.click(screen.getByText('Show more tip types')) expect(vi.mocked(setFeatureFlags)).toHaveBeenCalled() From 060bde15a24c4f805c41ae28d14f0454e8c2b262 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 21 Nov 2024 15:48:59 -0500 Subject: [PATCH 44/52] fix(protocol-designer): switch back textarea from component to styled tag (#16938) * fix(protocol-designer): switch back textarea from component to styled tag --- protocol-designer/src/atoms/constants.ts | 42 +------------------ .../organisms/DefineLiquidsModal/index.tsx | 14 ++++++- .../CreateNewProtocolWizard/AddMetadata.tsx | 14 ++++++- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/protocol-designer/src/atoms/constants.ts b/protocol-designer/src/atoms/constants.ts index e04701a7639..620a3d10dbc 100644 --- a/protocol-designer/src/atoms/constants.ts +++ b/protocol-designer/src/atoms/constants.ts @@ -1,11 +1,8 @@ -import styled, { css } from 'styled-components' +import { css } from 'styled-components' import { - BORDERS, COLORS, DIRECTION_COLUMN, OVERFLOW_HIDDEN, - SPACING, - TYPOGRAPHY, } from '@opentrons/components' import type { FlattenSimpleInterpolation } from 'styled-components' @@ -35,40 +32,3 @@ export const COLUMN_STYLE = css` min-width: calc((${MIN_OVERVIEW_WIDTH} - ${COLUMN_GRID_GAP}) * 0.5); flex: 1; ` - -export const DescriptionField = styled.textarea` - min-height: 5rem; - width: 100%; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; - border-radius: ${BORDERS.borderRadius4}; - padding: ${SPACING.spacing8}; - font-size: ${TYPOGRAPHY.fontSizeP}; - resize: none; - - &:active:enabled { - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - } - - &:hover { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey60}; - } - - &:focus-visible { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey55}; - outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; - outline-offset: 2px; - } - - &:focus-within { - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - } - - &:disabled { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - } - input[type='number']::-webkit-inner-spin-button, - input[type='number']::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } -` diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx index fb1d775b702..35bf74003a8 100644 --- a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx +++ b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx @@ -5,8 +5,10 @@ import { SketchPicker } from 'react-color' import { yupResolver } from '@hookform/resolvers/yup' import * as Yup from 'yup' import { Controller, useForm } from 'react-hook-form' +import styled from 'styled-components' import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data' import { + BORDERS, Btn, COLORS, DIRECTION_COLUMN, @@ -28,7 +30,7 @@ import * as labwareIngredActions from '../../labware-ingred/actions' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { swatchColors } from '../../components/swatchColors' import { HandleEnter } from '../../atoms/HandleEnter' -import { DescriptionField, LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' import type { ColorResult, RGBColor } from 'react-color' import type { ThunkDispatch } from 'redux-thunk' @@ -310,3 +312,13 @@ export function DefineLiquidsModal( ) } + +export const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx index afb8e83e9c7..fcc9956ad6b 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx @@ -1,16 +1,18 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' +import styled from 'styled-components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { + BORDERS, COLORS, DIRECTION_COLUMN, Flex, InputField, SPACING, StyledText, + TYPOGRAPHY, } from '@opentrons/components' -import { DescriptionField } from '../../atoms' import { HandleEnter } from '../../atoms/HandleEnter' import { analyticsEvent } from '../../analytics/actions' import { ONBOARDING_FLOW_DURATION_EVENT } from '../../analytics/constants' @@ -92,3 +94,13 @@ export function AddMetadata(props: AddMetadataProps): JSX.Element | null { ) } + +export const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` From 7d50f2dbccfe07a3e446eb6d32acbde86e4178b8 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:07:40 -0500 Subject: [PATCH 45/52] =?UTF-8?q?fix(protocol-designer):=20filter=20out=20?= =?UTF-8?q?module=20addressable=20area=20names=20from=E2=80=A6=20(#16931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … avail slot closes RQA-3662 --- .../src/labware-ingred/actions/thunks.ts | 1 - protocol-designer/src/labware-ingred/utils.ts | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/labware-ingred/actions/thunks.ts b/protocol-designer/src/labware-ingred/actions/thunks.ts index dedfae883d8..38cccb252fb 100644 --- a/protocol-designer/src/labware-ingred/actions/thunks.ts +++ b/protocol-designer/src/labware-ingred/actions/thunks.ts @@ -73,7 +73,6 @@ export const createContainer: ( args.slot || getNextAvailableDeckSlot(initialDeckSetup, robotType, labwareDef) const isTiprack = getIsTiprack(labwareDef) - if (slot) { const id = `${uuid()}:${args.labwareDefURI}` const adapterId = diff --git a/protocol-designer/src/labware-ingred/utils.ts b/protocol-designer/src/labware-ingred/utils.ts index d4c6dc5e1bf..377dff50eb5 100644 --- a/protocol-designer/src/labware-ingred/utils.ts +++ b/protocol-designer/src/labware-ingred/utils.ts @@ -1,5 +1,6 @@ import { FIXED_TRASH_ID, + FLEX_MODULE_ADDRESSABLE_AREAS, getAreSlotsAdjacent, getDeckDefFromRobotType, getIsLabwareAboveHeight, @@ -7,6 +8,7 @@ import { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, MOVABLE_TRASH_ADDRESSABLE_AREAS, OT2_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, WASTE_CHUTE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { COLUMN_4_SLOTS } from '@opentrons/step-generation' @@ -30,6 +32,16 @@ export function getNextAvailableDeckSlot( module => module.type === HEATERSHAKER_MODULE_TYPE )?.slot + const hasTC = Object.values(initialDeckSetup.modules).find( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + let moduleSlots = Object.values(initialDeckSetup.modules) + .filter(module => module.slot) + .map(mod => mod.slot) + if (hasTC) { + moduleSlots = [...moduleSlots, '8', '10', '11'] + } + return deckDef.locations.addressableAreas.find(slot => { const cutoutIds = Object.values(initialDeckSetup.additionalEquipmentOnDeck) .filter(ae => ae.name === 'stagingArea') @@ -47,12 +59,17 @@ export function getNextAvailableDeckSlot( MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slot.id) || WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slot.id) || slot.id === FIXED_TRASH_ID + ) { + isSlotEmpty = false + } else if ( + moduleSlots.includes(slot.id) || + FLEX_MODULE_ADDRESSABLE_AREAS.includes(slot.id) ) { isSlotEmpty = false // return slot as full if slot is adjacent to heater-shaker for ot-2 and taller than 53mm } else if ( heaterShakerSlot != null && - deckDef.robot.model === OT2_ROBOT_TYPE && + robotType === OT2_ROBOT_TYPE && isSlotEmpty && labwareDefinition != null ) { From cb182a0da3a356ce91cbb64bfcffc83709202b09 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:45:04 -0500 Subject: [PATCH 46/52] fix(components, protocol-designer): add hover state for Checkbox component and StyledLabel (#16942) Fix enabled and hover state background color for both checked and unchecked states of Checkbox component, and add hover state for StyledLabel one-off in SelectPipettes component. Closes RQA-3655 --- components/src/atoms/Checkbox/index.tsx | 5 ++++- .../organisms/EditInstrumentsModal/index.tsx | 15 ++++++++----- .../SelectPipettes.tsx | 22 ++++++++++++------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/components/src/atoms/Checkbox/index.tsx b/components/src/atoms/Checkbox/index.tsx index 8ace61cb0bf..44c2ba8ee04 100644 --- a/components/src/atoms/Checkbox/index.tsx +++ b/components/src/atoms/Checkbox/index.tsx @@ -48,7 +48,7 @@ export function Checkbox(props: CheckboxProps): JSX.Element { align-items: ${ALIGN_CENTER}; flex-direction: ${DIRECTION_ROW}; color: ${isChecked ? COLORS.white : COLORS.black90}; - background-color: ${isChecked ? COLORS.blue50 : COLORS.blue35}; + background-color: ${isChecked ? COLORS.blue50 : COLORS.blue30}; border-radius: ${type === 'round' ? BORDERS.borderRadiusFull : BORDERS.borderRadius8}; @@ -68,6 +68,9 @@ export function Checkbox(props: CheckboxProps): JSX.Element { background-color: ${COLORS.grey35}; color: ${COLORS.grey50}; } + &:hover { + background-color: ${isChecked ? COLORS.blue55 : COLORS.blue35}; + } @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing20}; diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx index c986c1ce9ba..eefcad8f4bd 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -565,11 +565,13 @@ export function EditInstrumentsModal( TYPOGRAPHY.textDecorationUnderline } > - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - + + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + {' '} )} @@ -594,4 +596,7 @@ const StyledLabel = styled.label` input[type='file'] { display: none; } + &:hover { + color: ${COLORS.blue50}; + } ` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 3f48e08e6bd..b3c4e691746 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -14,6 +14,7 @@ import { Box, Btn, Checkbox, + COLORS, CURSOR_POINTER, DIRECTION_COLUMN, DIRECTION_ROW, @@ -399,14 +400,16 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { TYPOGRAPHY.textDecorationUnderline } > - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - + + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + )} @@ -557,4 +560,7 @@ const StyledLabel = styled.label` input[type='file'] { display: none; } + &:hover { + color: ${COLORS.blue50}; + } ` From 94bbb99370bb2629bd2647ff06978f49ee96b55a Mon Sep 17 00:00:00 2001 From: Ryan Howard Date: Thu, 21 Nov 2024 17:03:40 -0500 Subject: [PATCH 47/52] enable the pipette script to skip all pressure based things (#16934) # Overview Add an option for skipping pressure, we won't actually need this apparently since they changed their mind about skipping the step of drilling out the back vent of the diaphragm but if they change that in the future we can use this. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../pipette_assembly_qc_ot3/__main__.py | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 90637e81540..6be7cc92fab 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -119,6 +119,7 @@ class TestConfig: num_trials: int droplet_wait_seconds: int simulate: bool + skip_all_pressure: bool @dataclass @@ -700,9 +701,12 @@ async def _test_for_leak( accumulate_raw_data_cb ), "pressure fixture requires recording data to disk" await _move_to_fixture(api, mount) - test_passed = await _fixture_check_pressure( - api, mount, test_config, fixture, write_cb, accumulate_raw_data_cb - ) + if not test_config.skip_all_pressure: + test_passed = await _fixture_check_pressure( + api, mount, test_config, fixture, write_cb, accumulate_raw_data_cb + ) + else: + test_passed = True else: await _pick_up_tip_for_tip_volume(api, mount, tip_volume=tip_volume) await _move_to_reservoir_liquid(api, mount) @@ -1129,7 +1133,9 @@ async def _read_pressure(_sensor_id: SensorId) -> float: return all(results) -async def _test_diagnostics(api: OT3API, mount: OT3Mount, write_cb: Callable) -> bool: +async def _test_diagnostics( + api: OT3API, mount: OT3Mount, write_cb: Callable, cfg: TestConfig +) -> bool: # ENVIRONMENT SENSOR environment_pass = await _test_diagnostics_environment(api, mount, write_cb) print(f"environment: {_bool_to_pass_fail(environment_pass)}") @@ -1146,9 +1152,14 @@ async def _test_diagnostics(api: OT3API, mount: OT3Mount, write_cb: Callable) -> print(f"capacitance: {_bool_to_pass_fail(capacitance_pass)}") write_cb(["diagnostics-capacitance", _bool_to_pass_fail(capacitance_pass)]) # PRESSURE - pressure_pass = await _test_diagnostics_pressure(api, mount, write_cb) - print(f"pressure: {_bool_to_pass_fail(pressure_pass)}") + if not cfg.skip_all_pressure: + pressure_pass = await _test_diagnostics_pressure(api, mount, write_cb) + print(f"pressure: {_bool_to_pass_fail(pressure_pass)}") + else: + print("Skipping pressure") + pressure_pass = True write_cb(["diagnostics-pressure", _bool_to_pass_fail(pressure_pass)]) + return environment_pass and pressure_pass and encoder_pass and capacitance_pass @@ -1674,7 +1685,9 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 if not test_config.skip_diagnostics: await api.move_to(mount, hover_over_slot_3) await api.move_rel(mount, Point(z=-20)) - test_passed = await _test_diagnostics(api, mount, csv_cb.write) + test_passed = await _test_diagnostics( + api, mount, csv_cb.write, test_config + ) await api.retract(mount) csv_cb.results("diagnostics", test_passed) if not test_config.skip_plunger: @@ -1806,6 +1819,7 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 arg_parser.add_argument("--skip-plunger", action="store_true") arg_parser.add_argument("--skip-tip-presence", action="store_true") arg_parser.add_argument("--skip-liquid-probe", action="store_true") + arg_parser.add_argument("--skip-all-pressure", action="store_true") arg_parser.add_argument("--fixture-side", choices=["left", "right"], default="left") arg_parser.add_argument("--port", type=str, default="") arg_parser.add_argument("--num-trials", type=int, default=2) @@ -1841,11 +1855,11 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 _cfg = TestConfig( operator_name=operator, skip_liquid=args.skip_liquid, - skip_fixture=args.skip_fixture, + skip_fixture=args.skip_fixture or args.skip_all_pressure, skip_diagnostics=args.skip_diagnostics, skip_plunger=args.skip_plunger, skip_tip_presence=args.skip_tip_presence, - skip_liquid_probe=args.skip_liquid_probe, + skip_liquid_probe=args.skip_liquid_probe or args.skip_all_pressure, fixture_port=args.port, fixture_side=args.fixture_side, fixture_aspirate_sample_count=args.aspirate_sample_count, @@ -1859,6 +1873,7 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 num_trials=args.num_trials, droplet_wait_seconds=args.wait, simulate=args.simulate, + skip_all_pressure=args.skip_all_pressure, ) # NOTE: overwrite default aspirate sample-count from user's input # FIXME: this value is being set in a few places, maybe there's a way to clean this up From a166bc3e363aef8d5091b10699f521e5f51d708e Mon Sep 17 00:00:00 2001 From: Caila Marashaj <98041399+caila-marashaj@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:05:28 -0500 Subject: [PATCH 48/52] feat(api): add new speed for emulsifying pipette plunger (#16917) --- api/src/opentrons/config/defaults_ot3.py | 1 + .../backends/flex_protocol.py | 5 ++ .../backends/ot3controller.py | 13 ++++ .../hardware_control/backends/ot3simulator.py | 5 ++ .../hardware_control/backends/ot3utils.py | 30 +++++++++- .../instruments/ot3/pipette.py | 4 ++ api/src/opentrons/hardware_control/ot3api.py | 11 ++++ .../backends/test_ot3_utils.py | 18 +++++- .../hardware_control/test_motion_plan.py | 59 ++++++++++++++++++- .../2/general/eight_channel_em/p1000/3_0.json | 2 +- .../eight_channel_em/p1000/default/3_0.json | 6 +- .../opentrons_shared_data/pipette/types.py | 1 + 12 files changed, 148 insertions(+), 7 deletions(-) diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index 55565745d3a..53fab18392c 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -75,6 +75,7 @@ DEFAULT_GRIPPER_MOUNT_OFFSET: Final[Offset] = (84.55, -12.75, 93.85) DEFAULT_SAFE_HOME_DISTANCE: Final = 5 DEFAULT_CALIBRATION_AXIS_MAX_SPEED: Final = 30 +DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED: Final = 90 DEFAULT_MAX_SPEEDS: Final[ByGantryLoad[Dict[OT3AxisKind, float]]] = ByGantryLoad( high_throughput={ diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index c5294938fa0..8b81d2c66ef 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -69,6 +69,11 @@ def update_constraints_for_calibration_with_gantry_load( ) -> None: ... + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + ... + def update_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad ) -> None: diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 1251fcc4adb..87f886f1c74 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -50,6 +50,7 @@ get_system_constraints, get_system_constraints_for_calibration, get_system_constraints_for_plunger_acceleration, + get_system_constraints_for_emulsifying_pipette, ) from .tip_presence_manager import TipPresenceManager @@ -393,6 +394,18 @@ def update_constraints_for_calibration_with_gantry_load( f"Set system constraints for calibration: {self._move_manager.get_constraints()}" ) + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + self._move_manager.update_constraints( + get_system_constraints_for_emulsifying_pipette( + self._configuration.motion_settings, gantry_load, mount + ) + ) + log.debug( + f"Set system constraints for emulsifying pipette: {self._move_manager.get_constraints()}" + ) + def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: self._move_manager.update_constraints( get_system_constraints(self._configuration.motion_settings, gantry_load) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index e487f963ece..533fffe5642 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -234,6 +234,11 @@ def update_constraints_for_calibration_with_gantry_load( ) -> None: self._sim_gantry_load = gantry_load + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + pass + def update_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad ) -> None: diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index e3952cbd907..3fe6bcdd520 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -2,7 +2,10 @@ from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional from typing_extensions import Literal from logging import getLogger -from opentrons.config.defaults_ot3 import DEFAULT_CALIBRATION_AXIS_MAX_SPEED +from opentrons.config.defaults_ot3 import ( + DEFAULT_CALIBRATION_AXIS_MAX_SPEED, + DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED, +) from opentrons.config.types import OT3MotionSettings, OT3CurrentSettings, GantryLoad from opentrons.hardware_control.types import ( Axis, @@ -300,6 +303,31 @@ def get_system_constraints_for_plunger_acceleration( return new_constraints +def get_system_constraints_for_emulsifying_pipette( + config: OT3MotionSettings, + gantry_load: GantryLoad, + mount: OT3Mount, +) -> "SystemConstraints[Axis]": + old_constraints = config.by_gantry_load(gantry_load) + new_constraints = {} + axis_kinds = set([k for _, v in old_constraints.items() for k in v.keys()]) + for axis_kind in axis_kinds: + for axis in Axis.of_kind(axis_kind): + if axis == Axis.of_main_tool_actuator(mount): + _max_speed = float(DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED) + else: + _max_speed = old_constraints["default_max_speed"][axis_kind] + new_constraints[axis] = AxisConstraints.build( + max_acceleration=old_constraints["acceleration"][axis_kind], + max_speed_discont=old_constraints["max_speed_discontinuity"][axis_kind], + max_direction_change_speed_discont=old_constraints[ + "direction_change_speed_discontinuity" + ][axis_kind], + max_speed=_max_speed, + ) + return new_constraints + + def _convert_to_node_id_dict( axis_pos: Coordinates[Axis, CoordinateValue], ) -> NodeIdMotionValues: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 5a4d9261bfd..b9355874906 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -41,6 +41,7 @@ UlPerMmAction, PipetteName, PipetteModel, + Quirks, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -225,6 +226,9 @@ def active_tip_settings(self) -> SupportedTipsDefinition: def push_out_volume(self) -> float: return self._active_tip_settings.default_push_out_volume + def is_high_speed_pipette(self) -> bool: + return Quirks.highSpeed in self._config.quirks + def act_as(self, name: PipetteName) -> None: """Reconfigure to act as ``name``. ``name`` must be either the actual name of the pipette, or a name in its back-compatibility diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 491b6168e58..9de9f2f5448 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -634,10 +634,21 @@ async def cache_pipette( self._feature_flags.use_old_aspiration_functions, ) self._pipette_handler.hardware_instruments[mount] = p + if self._pipette_handler.has_pipette(mount): + self._confirm_pipette_motion_constraints(mount) # TODO (lc 12-5-2022) Properly support backwards compatibility # when applicable return skipped + def _confirm_pipette_motion_constraints( + self, + mount: OT3Mount, + ) -> None: + if self._pipette_handler.get_pipette(mount).is_high_speed_pipette(): + self._backend.update_constraints_for_emulsifying_pipette( + mount, self.gantry_load + ) + async def cache_gripper(self, instrument_data: AttachedGripper) -> bool: """Set up gripper based on scanned information.""" grip_cal = load_gripper_calibration_offset(instrument_data.get("id")) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py index 2e650a2c246..d7125cfb027 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py @@ -3,7 +3,7 @@ from opentrons_hardware.hardware_control.motion_planning import Move from opentrons.hardware_control.backends import ot3utils from opentrons_hardware.firmware_bindings.constants import NodeId -from opentrons.hardware_control.types import Axis, OT3Mount +from opentrons.hardware_control.types import Axis, OT3Mount, OT3AxisKind from numpy import float64 as f64 from opentrons.config import defaults_ot3, types as conf_types @@ -95,6 +95,22 @@ def test_get_system_contraints_for_plunger() -> None: assert updated_contraints[axis].max_acceleration == set_acceleration +@pytest.mark.parametrize(["mount"], [[OT3Mount.LEFT], [OT3Mount.RIGHT]]) +def test_get_system_constraints_for_emulsifying_pipette(mount: OT3Mount) -> None: + set_max_speed = 90 + config = defaults_ot3.build_with_defaults({}) + pipette_ax = Axis.of_main_tool_actuator(mount) + default_pip_max_speed = config.motion_settings.default_max_speed[ + conf_types.GantryLoad.LOW_THROUGHPUT + ][OT3AxisKind.P] + updated_constraints = ot3utils.get_system_constraints_for_emulsifying_pipette( + config.motion_settings, conf_types.GantryLoad.LOW_THROUGHPUT, mount + ) + other_pipette = list(set(Axis.pipette_axes()) - {pipette_ax})[0] + assert updated_constraints[pipette_ax].max_speed == set_max_speed + assert updated_constraints[other_pipette].max_speed == default_pip_max_speed + + @pytest.mark.parametrize( ["moving", "expected"], [ diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py b/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py index 857c0d08f92..64ed76a6856 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py @@ -2,7 +2,7 @@ import numpy as np from hypothesis import given, assume, strategies as st from hypothesis.extra import numpy as hynp -from typing import Iterator, List, Tuple +from typing import Iterator, List, Tuple, Dict from opentrons_hardware.hardware_control.motion_planning import move_manager from opentrons_hardware.hardware_control.motion_planning.types import ( @@ -210,3 +210,60 @@ def test_close_move_plan( ) assert converged, f"Failed to converge: {blend_log}" + + +def test_pipette_high_speed_motion() -> None: + """Test that updated motion constraint doesn't get overridden by motion planning.""" + origin: Dict[str, int] = { + "X": 499, + "Y": 499, + "Z": 499, + "A": 499, + "B": 499, + "C": 499, + } + target_list = [] + axis_kinds = ["X", "Y", "Z", "A", "B", "C"] + constraints: SystemConstraints[str] = {} + for axis_kind in axis_kinds: + constraints[axis_kind] = AxisConstraints.build( + max_acceleration=500, + max_speed_discont=500, + max_direction_change_speed_discont=500, + max_speed=500, + ) + origin_mapping: Dict[str, float] = {axis_kind: float(origin[axis_kind])} + target_list.append(MoveTarget.build(origin_mapping, 500)) + + set_axis_kind = "A" + dummy_em_pipette_max_speed = 90.0 + manager = move_manager.MoveManager(constraints=constraints) + + new_axis_constraint = AxisConstraints.build( + max_acceleration=float(constraints[set_axis_kind].max_acceleration), + max_speed_discont=float(constraints[set_axis_kind].max_speed_discont), + max_direction_change_speed_discont=float( + constraints[set_axis_kind].max_direction_change_speed_discont + ), + max_speed=90.0, + ) + new_constraints = {} + + for axis_kind in constraints.keys(): + if axis_kind == set_axis_kind: + new_constraints[axis_kind] = new_axis_constraint + else: + new_constraints[axis_kind] = constraints[axis_kind] + + manager.update_constraints(constraints=new_constraints) + converged, blend_log = manager.plan_motion( + origin=origin, + target_list=target_list, + iteration_limit=20, + ) + for move in blend_log[0]: + unit_vector = move.unit_vector + for block in move.blocks: + top_set_axis_speed = unit_vector[set_axis_kind] * block.final_speed + if top_set_axis_speed != 0: + assert abs(top_set_axis_speed) == dummy_em_pipette_max_speed diff --git a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json index c49ae20d87a..c267504b404 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json @@ -312,7 +312,7 @@ "shaftDiameter": 4.5, "shaftULperMM": 15.904, "backlashDistance": 0.1, - "quirks": [], + "quirks": ["highSpeed"], "plungerHomingConfigurations": { "current": 1.0, "speed": 30 diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json index 95292a3f98b..52c7b58171d 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json @@ -2,7 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { - "uiMaxFlowRate": 802.9, + "uiMaxFlowRate": 1431.0, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -83,7 +83,7 @@ "defaultPushOutVolume": 7 }, "t200": { - "uiMaxFlowRate": 847.9, + "uiMaxFlowRate": 1431.0, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -162,7 +162,7 @@ "defaultPushOutVolume": 5 }, "t1000": { - "uiMaxFlowRate": 744.6, + "uiMaxFlowRate": 1431.0, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/python/opentrons_shared_data/pipette/types.py b/shared-data/python/opentrons_shared_data/pipette/types.py index d5315ec12d5..c52e57eb20e 100644 --- a/shared-data/python/opentrons_shared_data/pipette/types.py +++ b/shared-data/python/opentrons_shared_data/pipette/types.py @@ -109,6 +109,7 @@ class Quirks(enum.Enum): dropTipShake = "dropTipShake" doubleDropTip = "doubleDropTip" needsUnstick = "needsUnstick" + highSpeed = "highSpeed" class AvailableUnits(enum.Enum): From 49454f084d0679a78a971ae380ac20066428d256 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:57:28 -0500 Subject: [PATCH 49/52] feat(abr-testing): add plate reader + tartrazine protocol (#16920) # Overview Adds protocol that uses tartrazine and takes plate reader measurements ## Test Plan and Hands on Testing - passed simulate test and runs on robot ## Changelog - removed OT3 ABR Normalize with Tubes protocol - added plate reader + tartrazine protocol ## Review requests ## Risk assessment --------- Co-authored-by: CaseyBatten --- .../data_collection/abr_google_drive.py | 7 +- .../data_collection/read_robot_logs.py | 72 ++-- .../protocol_simulation/simulation_metrics.py | 14 +- .../3_OT3 ABR Normalize with Tubes.py | 343 ------------------ .../active_protocols/3_Tartrazine Protocol.py | 124 +++++++ .../8_Illumina and Plate Reader.py | 25 +- .../commands/absorbance_reader/read.py | 4 + 7 files changed, 195 insertions(+), 394 deletions(-) delete mode 100644 abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py create mode 100644 abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index 6552534c4ae..8f82567a7d1 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -44,6 +44,7 @@ def create_data_dictionary( headers: List[str] = [] headers_lpc: List[str] = [] list_of_heights: List[List[Any]] = [[], [], [], [], [], [], [], []] + hellma_plate_orientation = False # default hellma plate is not rotated. for filename in os.listdir(storage_directory): file_path = os.path.join(storage_directory, filename) if file_path.endswith(".json"): @@ -67,6 +68,10 @@ def create_data_dictionary( if run_id in runs_to_save: print(f"started reading run {run_id}.") robot = file_results.get("robot_name") + parameters = file_results.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") software_version = file_results.get("API_Version", "") left_pipette = file_results.get("left", "") @@ -123,7 +128,7 @@ def create_data_dictionary( file_results, labware_name="opentrons_tough_pcr_auto_sealing_lid" ) plate_reader_dict = read_robot_logs.plate_reader_commands( - file_results, hellma_plate_standards + file_results, hellma_plate_standards, hellma_plate_orientation ) list_of_heights = read_robot_logs.liquid_height_commands( file_results, list_of_heights diff --git a/abr-testing/abr_testing/data_collection/read_robot_logs.py b/abr-testing/abr_testing/data_collection/read_robot_logs.py index 40712118fe5..7bc83e0a54b 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -250,7 +250,9 @@ def liquid_height_commands( def plate_reader_commands( - file_results: Dict[str, Any], hellma_plate_standards: List[Dict[str, Any]] + file_results: Dict[str, Any], + hellma_plate_standards: List[Dict[str, Any]], + orientation: bool, ) -> Dict[str, object]: """Plate Reader Command Counts.""" commandData = file_results.get("commands", "") @@ -279,38 +281,46 @@ def plate_reader_commands( read = "yes" elif read == "yes" and commandType == "comment": result = command["params"].get("message", "") - formatted_result = result.split("result: ")[1] - result_dict = eval(formatted_result) - result_dict_keys = list(result_dict.keys()) - if len(result_dict_keys) > 1: - read_type = "multi" - else: - read_type = "single" - for wavelength in result_dict_keys: - one_wavelength_dict = result_dict.get(wavelength) - result_ndarray = plate_reader.convert_read_dictionary_to_array( - one_wavelength_dict - ) - for item in hellma_plate_standards: - wavelength_of_interest = item["wavelength"] - if str(wavelength) == str(wavelength_of_interest): - error_cells = plate_reader.check_byonoy_data_accuracy( - result_ndarray, item, False + if "result:" in result: + plate_name = result.split("result:")[0] + formatted_result = result.split("result: ")[1] + print(formatted_result) + result_dict = eval(formatted_result) + result_dict_keys = list(result_dict.keys()) + if len(result_dict_keys) > 1: + read_type = "multi" + else: + read_type = "single" + if "hellma_plate" in plate_name: + for wavelength in result_dict_keys: + one_wavelength_dict = result_dict.get(wavelength) + result_ndarray = plate_reader.convert_read_dictionary_to_array( + one_wavelength_dict ) - if len(error_cells[0]) > 0: - percent = (96 - len(error_cells)) / 96 * 100 - for cell in error_cells: - print( - "FAIL: Cell " + str(cell) + " out of accuracy spec." + for item in hellma_plate_standards: + wavelength_of_interest = item["wavelength"] + if str(wavelength) == str(wavelength_of_interest): + error_cells = plate_reader.check_byonoy_data_accuracy( + result_ndarray, item, orientation ) - else: - percent = 100 - print( - f"PASS: {wavelength_of_interest} meet accuracy specification" - ) - final_result[read_type, wavelength, read_num] = percent - read_num += 1 - read = "no" + if len(error_cells[0]) > 0: + percent = (96 - len(error_cells)) / 96 * 100 + for cell in error_cells: + print( + "FAIL: Cell " + + str(cell) + + " out of accuracy spec." + ) + else: + percent = 100 + print( + f"PASS: {wavelength_of_interest} meet accuracy spec." + ) + final_result[read_type, wavelength, read_num] = percent + read_num += 1 + else: + final_result = result_dict + read = "no" plate_dict = { "Plate Reader # of Reads": read_count, "Plate Reader Avg Read Time (sec)": avg_read_time, diff --git a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py index 57695f03557..10c7ea12782 100644 --- a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py +++ b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py @@ -225,7 +225,11 @@ def parse_results_volume( else: print(f"Expected JSON object (dict) but got {type(json_data).__name__}.") commands = {} - + hellma_plate_orientation = False + parameters = json_data.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) start_time = datetime.fromisoformat(commands[0]["createdAt"]) end_time = datetime.fromisoformat(commands[len(commands) - 1]["completedAt"]) header = ["", "Protocol Name", "Date", "Time"] @@ -283,7 +287,7 @@ def parse_results_volume( temp_module_dict = read_robot_logs.temperature_module_commands(json_data) thermo_cycler_dict = read_robot_logs.thermocycler_commands(json_data) plate_reader_dict = read_robot_logs.plate_reader_commands( - json_data, hellma_plate_standards + json_data, hellma_plate_standards, hellma_plate_orientation ) instrument_dict = read_robot_logs.instrument_commands( json_data, labware_name=None @@ -499,12 +503,12 @@ def check_params(protocol_path: str) -> str: def get_extra_files(protocol_file_path: str) -> tuple[str, List[Path]]: """Get supporting files for protocol simulation if needed.""" params = check_params(protocol_file_path) - needs_files = input("Does your protocol utilize custom labware? (y/n): ") + needs_files = input("Does your protocol utilize custom labware? (Y/N): ") labware_files = [] - if needs_files == "y": + if needs_files == "Y": num_labware = input("How many custom labware?: ") for labware_num in range(int(num_labware)): - path = input("Enter custom labware definition: ") + path = input("Enter custom labware definition path: ") labware_files.append(Path(path)) return (params, labware_files) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py b/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py deleted file mode 100644 index 50fb82e94d5..00000000000 --- a/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py +++ /dev/null @@ -1,343 +0,0 @@ -"""FLEX Normalize with Tubes.""" -from opentrons.protocol_api import ProtocolContext, ParameterContext, Well -from abr_testing.protocols import helpers -from typing import List - -metadata = { - "protocolName": "Flex Normalize with Tubes", - "author": "Opentrons ", - "source": "Protocol Library", -} - -requirements = {"robotType": "Flex", "apiLevel": "2.21"} - -# SCRIPT SETTINGS -ABR_TEST = True -if ABR_TEST: - DRYRUN = True # True = skip incubation times, shorten mix, for testing purposes - TIP_TRASH = ( - False # True = Used tips go in Trash, False = Used tips go back into rack - ) -else: - DRYRUN = False # True = skip incubation times, shorten mix, for testing purposes - TIP_TRASH = True - - -def add_parameters(parameters: ParameterContext) -> None: - """Parameters.""" - helpers.create_csv_parameter(parameters) - helpers.create_dot_bottom_parameter(parameters) - helpers.create_two_pipette_mount_parameters(parameters) - - -def run(ctx: ProtocolContext) -> None: - """Protocol.""" - mount_pos_50ul = ctx.params.pipette_mount_1 # type: ignore[attr-defined] - mount_pos_1000ul = ctx.params.pipette_mount_2 # type: ignore[attr-defined] - dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] - parsed_csv = ctx.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] - if DRYRUN: - ctx.comment("THIS IS A DRY RUN") - else: - ctx.comment("THIS IS A REACTION RUN") - - # labware - tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "1") - tiprack_200_1 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "4") - reagent_tube = ctx.load_labware( - "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "5", "Reagent Tube" - ) - sample_plate = ctx.load_labware( - "armadillo_96_wellplate_200ul_pcr_full_skirt", "2", "Sample Plate" - ) - - # reagent - RSB = reagent_tube.wells()[0] - - # pipette - p1000 = ctx.load_instrument( - "flex_1channel_1000", mount_pos_1000ul, tip_racks=[tiprack_200_1] - ) - p50 = ctx.load_instrument( - "flex_1channel_50", mount_pos_50ul, tip_racks=[tiprack_50_1] - ) - - wells_with_liquids: List[Well] = [RSB] - helpers.load_wells_with_water(ctx, wells_with_liquids, [4000.0]) - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_with_liquids) - MaxTubeVol = 200 - RSBVol = 0.0 - - data = parsed_csv - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - - if DilutionVol <= 1: - ctx.comment("Sample " + CurrentWell + ": Conc. Too Low, Will Skip") - elif DilutionVol > MaxTubeVol - InitialVol: - DilutionVol = MaxTubeVol - InitialVol - ctx.comment( - "Sample " - + CurrentWell - + ": Conc. Too High, Will add, " - + str(DilutionVol) - + "ul, Max = " - + str(MaxTubeVol) - + "ul" - ) - RSBVol += MaxTubeVol - InitialVol - else: - if DilutionVol <= 20: - ctx.comment( - "Sample " - + CurrentWell - + ": Using p50, will add " - + str(round(DilutionVol, 1)) - ) - elif DilutionVol > 20: - ctx.comment( - "Sample " - + CurrentWell - + ": Using p1000, will add " - + str(round(DilutionVol, 1)) - ) - RSBVol += DilutionVol - current += 1 - - if RSBVol >= 14000: - ctx.pause("Caution, more than 15ml Required") - else: - ctx.comment("RSB Minimum: " + str(round(RSBVol / 1000, 1) + 1) + "ml") - - PiR2 = 176.71 - InitialRSBVol = RSBVol - RSBHeight = (InitialRSBVol / PiR2) + 17.5 - - ctx.pause("Proceed") - ctx.comment("==============================================") - ctx.comment("Normalizing Samples") - ctx.comment("==============================================") - - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - - ctx.comment("Number " + str(data[current]) + ": Sample " + str(CurrentWell)) - # ctx.comment("Vol Height = "+str(round(RSBHeight,2))) - HeightDrop = DilutionVol / PiR2 - # ctx.comment("Vol Drop = "+str(round(HeightDrop,2))) - - if DilutionVol <= 0: - # If the No Volume - ctx.comment("Conc. Too Low, Skipping") - - elif DilutionVol >= MaxTubeVol - InitialVol: - # If the Required Dilution volume is >= Max Volume - DilutionVol = MaxTubeVol - InitialVol - ctx.comment( - "Conc. Too High, Will add, " - + str(DilutionVol) - + "ul, Max = " - + str(MaxTubeVol) - + "ul" - ) - p1000.pick_up_tip() - p1000.require_liquid_presence(RSB) - p1000.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - # ctx.comment("New Vol Height = "+str(round(RSBHeight,2))) - p1000.dispense(DilutionVol, sample_plate.wells_by_name()[CurrentWell]) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - HighVolMix = 10 - for Mix in range(HighVolMix): - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].center()) - p1000.aspirate(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(0.5) - ) # original = () - p1000.aspirate(100) - p1000.dispense(100) - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].center()) - p1000.dispense(100) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - Mix += 1 - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - ctx.delay(seconds=3) - p1000.blow_out() - p1000.drop_tip() if DRYRUN is False else p1000.return_tip() - - else: - if DilutionVol <= 20: - # If the Required Dilution volume is <= 20ul - ctx.comment("Using p50 to add " + str(round(DilutionVol, 1))) - p50.pick_up_tip() - if round(float(data[current][3]), 1) <= 20: - p50.require_liquid_presence(RSB) - p50.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - else: - p50.require_liquid_presence(RSB) - p50.aspirate(20, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - p50.dispense(DilutionVol, sample_plate.wells_by_name()[CurrentWell]) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - p50.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(z=dot_bottom) - ) # original = () - # Mix volume <=20ul - if DilutionVol + InitialVol <= 20: - p50.mix(10, DilutionVol + InitialVol) - elif DilutionVol + InitialVol > 20: - p50.mix(10, 20) - p50.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - ctx.delay(seconds=3) - p50.blow_out() - p50.drop_tip() if DRYRUN is False else p50.return_tip() - - elif DilutionVol > 20: - # If the required volume is >20 - ctx.comment("Using p1000 to add " + str(round(DilutionVol, 1))) - p1000.pick_up_tip() - p1000.require_liquid_presence(RSB) - p1000.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - if DilutionVol + InitialVol >= 120: - HighVolMix = 10 - for Mix in range(HighVolMix): - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].center() - ) - p1000.aspirate(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom( - z=dot_bottom - ) - ) # original = () - p1000.aspirate(DilutionVol + InitialVol - 100) - p1000.dispense(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].center() - ) - p1000.dispense(DilutionVol + InitialVol - 100) - Mix += 1 - wells_with_liquids.append( - sample_plate.wells_by_name()[CurrentWell] - ) - else: - p1000.dispense( - DilutionVol, sample_plate.wells_by_name()[CurrentWell] - ) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(z=dot_bottom) - ) # original = () - p1000.mix(10, DilutionVol + InitialVol) - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - ctx.delay(seconds=3) - p1000.blow_out() - p1000.drop_tip() if DRYRUN is False else p1000.return_tip() - current += 1 - - ctx.comment("==============================================") - ctx.comment("Results") - ctx.comment("==============================================") - - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - if DilutionVol > MaxTubeVol - InitialVol: - DilutionVol = MaxTubeVol - InitialVol - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - ctx.comment( - "Sample " - + CurrentWell - + ": " - + str(round(FinalVol, 1)) - + " at " - + str(round(FinalConc, 1)) - + "ng/ul" - ) - - current += 1 - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_with_liquids) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py new file mode 100644 index 00000000000..05a6300e053 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py @@ -0,0 +1,124 @@ +"""Tartrazine Protocol.""" +from opentrons.protocol_api import ProtocolContext, ParameterContext, Well +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + AbsorbanceReaderContext, + HeaterShakerContext, +) +from datetime import datetime +from typing import Dict, List +import statistics + +metadata = { + "protocolName": "Tartrazine Protocol", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + + +def run(ctx: ProtocolContext) -> None: + """Protocol.""" + mount_pos_50ul = ctx.params.pipette_mount # type: ignore[attr-defined] + # Plate Reader + plate_reader: AbsorbanceReaderContext = ctx.load_module( + helpers.abs_mod_str, "A3" + ) # type: ignore[assignment] + hs: HeaterShakerContext = ctx.load_module(helpers.hs_str, "A1") # type: ignore[assignment] + hs_adapter = hs.load_adapter("opentrons_96_pcr_adapter") + tube_rack = ctx.load_labware( + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", "C2", "Reagent Tube" + ) + tartrazine_tube = tube_rack["A3"] + + sample_plate_1 = ctx.load_labware( + "nest_96_wellplate_200ul_flat", "D1", "Sample Plate 1" + ) + sample_plate_2 = ctx.load_labware( + "nest_96_wellplate_200ul_flat", "C1", "Sample Plate 2" + ) + sample_plate_3 = ctx.load_labware( + "nest_96_wellplate_200ul_flat", "B1", "Sample Plate 3" + ) + sample_plate_list = [sample_plate_1, sample_plate_2, sample_plate_3] + tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "D3") + tiprack_50_2 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "C3") + tiprack_50_3 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "B3") + tip_racks = [tiprack_50_1, tiprack_50_2, tiprack_50_3] + + # Pipette + p50 = ctx.load_instrument("flex_1channel_50", mount_pos_50ul, tip_racks=tip_racks) + + # Probe wells + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Tartrazine": [{"well": tartrazine_tube, "volume": 45.0}] + } + helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, p50) + + i = 0 + all_percent_error_dict = {} + cv_dict = {} + for sample_plate in sample_plate_list: + deck_locations = ["D1", "C1", "B1"] + for well in sample_plate.wells(): + p50.pick_up_tip() + height = helpers.find_liquid_height(p50, tartrazine_tube) + p50.aspirate(10, tartrazine_tube.bottom(z=height)) + p50.air_gap(5) + p50.dispense(5, well.top()) + p50.dispense(10, well.bottom(z=0.5)) + p50.blow_out() + p50.return_tip() + helpers.move_labware_to_hs(ctx, sample_plate, hs, hs_adapter) + helpers.set_hs_speed(ctx, hs, 1500, 2.0, True) + hs.open_labware_latch() + plate_reader.close_lid() + plate_reader.initialize("single", [450]) + plate_reader.open_lid() + ctx.move_labware(sample_plate, plate_reader, use_gripper=True) + sample_plate_name = "sample plate_" + str(i + 1) + csv_string = sample_plate_name + "_" + str(datetime.now()) + plate_reader.close_lid() + result = plate_reader.read(csv_string) + for wavelength in result: + dict_of_wells = result[wavelength] + readings_and_wells = dict_of_wells.items() + readings = dict_of_wells.values() + avg = statistics.mean(readings) + # Check if every average is within +/- 5% of 2.85 + percent_error_dict = {} + percent_error_sum = 0.0 + for reading in readings_and_wells: + well_name = str(reading[0]) + measurement = reading[1] + percent_error = (measurement - 2.85) / 2.85 * 100 + percent_error_dict[well_name] = percent_error + percent_error_sum += percent_error + avg_percent_error = percent_error_sum / 96.0 + standard_deviation = statistics.stdev(readings) + try: + cv = standard_deviation / avg + except ZeroDivisionError: + cv = 0.0 + cv_percent = cv * 100 + cv_dict[sample_plate_name] = { + "CV": cv_percent, + "Mean": avg, + "SD": standard_deviation, + "Avg Percent Error": avg_percent_error, + } + all_percent_error_dict[sample_plate_name] = percent_error_dict + plate_reader.open_lid() + ctx.move_labware(sample_plate, deck_locations[i], use_gripper=True) + i += 1 + + # Print percent error dictionary + ctx.comment("Percent Error: " + str(all_percent_error_dict)) + # Print cv dictionary + ctx.comment("Plate Reader result: " + str(cv_dict)) diff --git a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py index 2e835ac04dd..4894cae41d4 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py +++ b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py @@ -19,10 +19,7 @@ } -requirements = { - "robotType": "Flex", - "apiLevel": "2.21", -} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} HELLMA_PLATE_SLOT = "D4" PLATE_READER_SLOT = "C3" @@ -58,14 +55,11 @@ def add_parameters(parameters: ParameterContext) -> None: """Add Parameters.""" helpers.create_hs_speed_parameter(parameters) helpers.create_dot_bottom_parameter(parameters) - parameters.add_str( + parameters.add_bool( variable_name="plate_orientation", display_name="Hellma Plate Orientation", - default="0_deg", - choices=[ - {"display_name": "0 degree Rotation", "value": "0_deg"}, - {"display_name": "180 degree Rotation", "value": "180_deg"}, - ], + default=True, + description="If hellma plate is rotated, set to True.", ) @@ -73,6 +67,7 @@ def plate_reader_actions( protocol: ProtocolContext, plate_reader: AbsorbanceReaderContext, hellma_plate: Labware, + hellma_plate_name: str, ) -> None: """Plate reader single and multi wavelength readings.""" wavelengths = [450, 650] @@ -84,7 +79,7 @@ def plate_reader_actions( protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) plate_reader.close_lid() result = plate_reader.read(str(datetime.now())) - msg = f"result: {result}" + msg = f"{hellma_plate_name} result: {result}" protocol.comment(msg=msg) plate_reader.open_lid() protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) @@ -95,7 +90,7 @@ def plate_reader_actions( protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) plate_reader.close_lid() result = plate_reader.read(str(datetime.now())) - msg = f"result: {result}" + msg = f"{hellma_plate_name} result: {result}" protocol.comment(msg=msg) plate_reader.open_lid() protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) @@ -107,6 +102,8 @@ def run(protocol: ProtocolContext) -> None: # LOAD PARAMETERS heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + plate_orientation = protocol.params.plate_orientation # type: ignore[attr-defined] + plate_name_str = "hellma_plate_" + str(plate_orientation) global p200_tips global p50_tips # WASTE BIN @@ -182,7 +179,7 @@ def run(protocol: ProtocolContext) -> None: PPC = reagent_plate.wells_by_name()["A6"] EPM = reagent_plate.wells_by_name()["A7"] # Load Liquids - plate_reader_actions(protocol, plate_reader, hellma_plate) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) # tip and sample tracking if COLUMNS == 1: @@ -948,4 +945,4 @@ def tipcheck() -> None: p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() - plate_reader_actions(protocol, plate_reader, hellma_plate) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index 8743fd1383b..1ca848858b6 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -80,6 +80,10 @@ async def execute( # noqa: C901 raise CannotPerformModuleAction( "Cannot perform Read action on Absorbance Reader without calling `.initialize(...)` first." ) + if abs_reader_substate.is_lid_on is False: + raise CannotPerformModuleAction( + "Cannot perform Read action on Absorbance Reader with the lid open. Try calling `.close_lid()` first." + ) # TODO: we need to return a file ID and increase the file count even when a moduel is not attached if ( From 7af746b800c3b7de888aaa6d03c190701c551ffe Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:13:21 -0500 Subject: [PATCH 50/52] =?UTF-8?q?fix(protocol-designer):=20remove=20flicke?= =?UTF-8?q?ring=20in=20timeline=20and=20fix=20distrib=E2=80=A6=20(#16901)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ute details closes RQA-3630 RQA-3624 --- .../localization/en/protocol_steps.json | 8 +- .../localization/en/starting_deck_state.json | 3 + .../Designer/ProtocolSteps/StepSummary.tsx | 31 +++-- .../Timeline/MultichannelSubstep.tsx | 4 - .../Timeline/PipettingSubsteps.tsx | 6 +- .../ProtocolSteps/Timeline/StepContainer.tsx | 4 +- .../Timeline/StepOverflowMenu.tsx | 4 +- .../ProtocolSteps/Timeline/Substep.tsx | 123 ++++++------------ .../Timeline/SubstepsToolbox.tsx | 3 - .../Timeline/TimelineToolbox.tsx | 7 +- .../Designer/ProtocolSteps/Timeline/utils.ts | 27 ---- .../__tests__/ProtocolSteps.test.tsx | 2 + .../pages/Designer/ProtocolSteps/index.tsx | 23 +++- 13 files changed, 89 insertions(+), 156 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 6105b29b24d..50c6f7879f8 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -27,7 +27,7 @@ "duplicate": "Duplicate step", "edit_step": "Edit step", "engage_height": "Engage height", - "final_deck_state": "Final deck state", + "ending_deck": "Ending deck", "flow_type_title": "{{type}} flow rate", "from": "from", "heater_shaker": { @@ -104,7 +104,7 @@ "shake": "Shake", "single": "Single path", "speed": "Speed", - "starting_deck_state": "Starting deck state", + "starting_deck": "Starting deck", "step_substeps": "{{stepType}} details", "temperature": "Temperature", "temperature_module": { @@ -120,8 +120,8 @@ "substep_settings": "Set block temperature tofor", "thermocycler_profile": { "end_hold": { - "block": "End at thermocycler block", - "lid_position": "Thermocycler lid" + "block": "End with block at", + "lid_position": "and lid" }, "lid_temperature": "and lid temperature at", "volume": "Run thermocycler profile with" diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index 080aa23199d..0522cc1291a 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -1,4 +1,7 @@ { + "__end__": "Ending deck", + "__initial_setup__": "Starting deck", + "__presaved_step__": "Unsaved step", "adapter_compatible_lab": "Adapter compatible labware", "adapter": "Adapters", "add_fixture": "Add a fixture", diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx index 36c82e86199..09135a62b49 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx @@ -79,7 +79,6 @@ interface StepSummaryProps { export function StepSummary(props: StepSummaryProps): JSX.Element | null { const { currentStep, stepDetails } = props const { t } = useTranslation(['protocol_steps', 'application']) - const labwareNicknamesById = useSelector(getLabwareNicknamesById) const additionalEquipmentEntities = useSelector( getAdditionalEquipmentEntities @@ -91,7 +90,6 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { return null } const { stepType } = currentStep - let stepSummaryContent: JSX.Element | null = null switch (stepType) { case 'mix': @@ -184,16 +182,20 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { ) : ( - - + + + + {stepSummaryContent != null ? ( - {stepSummaryContent} + + {stepSummaryContent} + ) : null} {stepDetails != null && stepDetails !== '' ? ( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx index 920ee3c0938..afd08e74e21 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/MultichannelSubstep.tsx @@ -17,13 +17,11 @@ import type { AdditionalEquipmentName } from '@opentrons/step-generation' import type { StepItemSourceDestRow, SubstepIdentifier, - WellIngredientNames, } from '../../../../steplist' interface MultichannelSubstepProps { trashName: AdditionalEquipmentName | null rowGroup: StepItemSourceDestRow[] - ingredNames: WellIngredientNames stepId: string substepIndex: number selectSubstep: (substepIdentifier: SubstepIdentifier) => void @@ -39,7 +37,6 @@ export function MultichannelSubstep( stepId, selectSubstep, substepIndex, - ingredNames, trashName, isSameLabware, } = props @@ -107,7 +104,6 @@ export function MultichannelSubstep( trashName={trashName} key={rowKey} volume={row.volume} - ingredNames={ingredNames} source={row.source} dest={row.dest} stepId={stepId} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx index 5c2be0bc9fa..82da4015e45 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx @@ -4,7 +4,6 @@ import { MultichannelSubstep } from './MultichannelSubstep' import type { SourceDestSubstepItem, SubstepIdentifier, - WellIngredientNames, } from '../../../../steplist' import { useSelector } from 'react-redux' import { @@ -14,13 +13,12 @@ import { interface PipettingSubstepsProps { substeps: SourceDestSubstepItem - ingredNames: WellIngredientNames selectSubstep: (substepIdentifier: SubstepIdentifier) => void hoveredSubstep?: SubstepIdentifier | null } export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { - const { substeps, selectSubstep, hoveredSubstep, ingredNames } = props + const { substeps, selectSubstep, hoveredSubstep } = props const stepId = substeps.parentStepId const formData = useSelector(getSavedStepForms)[stepId] const additionalEquipment = useSelector(getAdditionalEquipment) @@ -52,7 +50,6 @@ export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { stepId={substeps.parentStepId} substepIndex={groupKey} selectSubstep={selectSubstep} - ingredNames={ingredNames} isSameLabware={isSameLabware} /> ) @@ -64,7 +61,6 @@ export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { selectSubstep={selectSubstep} stepId={substeps.parentStepId} substepIndex={substepIndex} - ingredNames={ingredNames} volume={row.volume} source={row.source} dest={row.dest} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx index 4ed55987f08..9cde39888ce 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx @@ -45,8 +45,8 @@ import type { IconName } from '@opentrons/components' import type { StepIdType } from '../../../../form-types' import type { BaseState } from '../../../../types' -const STARTING_DECK_STATE = 'Starting deck state' -const FINAL_DECK_STATE = 'Final deck state' +const STARTING_DECK_STATE = 'Starting deck' +const FINAL_DECK_STATE = 'Ending deck' const PX_HEIGHT_TO_TOP_OF_CONTAINER = 32 export interface StepContainerProps { title: string diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx index 5bb125b3269..3dd7f529743 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx @@ -64,7 +64,9 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { const isPipetteStep = savedStepFormData.stepType === 'moveLiquid' || savedStepFormData.stepType === 'mix' - const isThermocyclerProfile = savedStepFormData.stepType === 'thermocycler' + const isThermocyclerProfile = + savedStepFormData.stepType === 'thermocycler' && + savedStepFormData.thermocyclerFormType === 'thermocyclerProfile' const duplicateStep = ( stepId: StepIdType diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx index a34dc799337..7c62f23140d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx @@ -1,38 +1,24 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import noop from 'lodash/noop' -import { AIR } from '@opentrons/step-generation' import { ALIGN_CENTER, - COLORS, DIRECTION_COLUMN, DeckInfoLabel, Flex, JUSTIFY_SPACE_BETWEEN, - LiquidIcon, ListItem, SPACING, StyledText, Tag, } from '@opentrons/components' -import { selectors } from '../../../../labware-ingred/selectors' -import { - MIXED_WELL_COLOR, - swatchColors, -} from '../../../../components/swatchColors' -import { compactPreIngreds, formatVolume } from './utils' +import { formatVolume } from './utils' import type { AdditionalEquipmentName } from '@opentrons/step-generation' -import type { - SubstepIdentifier, - SubstepWellData, - WellIngredientNames, -} from '../../../../steplist' +import type { SubstepIdentifier, SubstepWellData } from '../../../../steplist' interface SubstepProps { trashName: AdditionalEquipmentName | null - ingredNames: WellIngredientNames stepId: string substepIndex: number volume?: number | string | null @@ -45,7 +31,6 @@ interface SubstepProps { function SubstepComponent(props: SubstepProps): JSX.Element { const { volume, - ingredNames, stepId, substepIndex, source, @@ -54,24 +39,14 @@ function SubstepComponent(props: SubstepProps): JSX.Element { selectSubstep: propSelectSubstep, isSameLabware, } = props - const { t } = useTranslation(['application', 'protocol_steps', 'shared']) - const compactedSourcePreIngreds = source - ? compactPreIngreds(source.preIngreds) - : {} + const { i18n, t } = useTranslation([ + 'application', + 'protocol_steps', + 'shared', + ]) const selectSubstep = propSelectSubstep ?? noop - const ingredIds: string[] = Object.keys(compactedSourcePreIngreds) - const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) - const noColor = ingredIds.filter(id => id !== AIR).length === 0 - let color = MIXED_WELL_COLOR - if (ingredIds.length === 1) { - color = - liquidDisplayColors[Number(ingredIds[0])] ?? swatchColors(ingredIds[0]) - } else if (noColor) { - color = COLORS.transparent - } - const volumeTag = ( - {ingredIds.length > 0 ? ( - - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {t('protocol_steps:mix')} @@ -123,33 +88,27 @@ function SubstepComponent(props: SubstepProps): JSX.Element { {t('protocol_steps:in')} ) : ( <> - - - {ingredIds.length > 0 ? ( - - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {source != null ? ( + {source != null ? ( + + {t('protocol_steps:aspirated')} @@ -159,16 +118,19 @@ function SubstepComponent(props: SubstepProps): JSX.Element { {t('protocol_steps:from')} - ) : null} - - - - {dest !== undefined ? ( + + + ) : null} + {dest != null ? ( + - {ingredIds.length > 0 ? ( - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} {dest != null || trashName != null ? ( @@ -195,19 +149,20 @@ function SubstepComponent(props: SubstepProps): JSX.Element { ) : null} - ) : null} - + + ) : null} )} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx index 3a1ddff44f0..e2460741ebf 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx @@ -8,7 +8,6 @@ import { StyledText, Toolbox, } from '@opentrons/components' -import { selectors as labwareIngredSelectors } from '../../../../labware-ingred/selectors' import { getSubsteps } from '../../../../file-data/selectors' import { getHoveredSubstep } from '../../../../ui/steps' import { @@ -40,7 +39,6 @@ export function SubstepsToolbox( const substeps = useSelector(getSubsteps)[stepId] const formData = useSelector(getSavedStepForms)[stepId] const hoveredSubstep = useSelector(getHoveredSubstep) - const ingredNames = useSelector(labwareIngredSelectors.getLiquidNamesById) const highlightSubstep = (payload: SubstepIdentifier): HoverOnSubstepAction => dispatch(hoverOnSubstep(payload)) @@ -85,7 +83,6 @@ export function SubstepsToolbox( ) : ( { > { }} /> - + ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts index c7f6f812dc2..56b78507a12 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts @@ -1,8 +1,6 @@ import round from 'lodash/round' -import omitBy from 'lodash/omitBy' import uniq from 'lodash/uniq' import { UAParser } from 'ua-parser-js' -import type { WellIngredientVolumeData } from '../../../../steplist' import type { StepIdType } from '../../../../form-types' export const capitalizeFirstLetterAfterNumber = (title: string): string => @@ -29,31 +27,6 @@ export const formatPercentage = (part: number, total: number): string => { return `${round((part / total) * 100, PERCENTAGE_DECIMALS_ALLOWED)}%` } -export const compactPreIngreds = ( - preIngreds: WellIngredientVolumeData -): Partial< - | { - [ingredId: string]: - | { - volume: number - } - | undefined - } - | { - [well: string]: - | { - [ingredId: string]: { - volume: number - } - } - | undefined - } -> => { - return omitBy(preIngreds, ingred => { - return typeof ingred?.volume === 'number' && ingred.volume <= 0 - }) -} - export const getMetaSelectedSteps = ( multiSelectItemIds: StepIdType[] | null, stepId: StepIdType, diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx index 2ce91e03263..dcfb3dfd58b 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -10,6 +10,7 @@ import { import { getSelectedStepId, getSelectedSubstep, + getSelectedTerminalItemId, } from '../../../../ui/steps/selectors' import { getDesignerTab } from '../../../../file-data/selectors' import { getEnableHotKeysDisplay } from '../../../../feature-flags/selectors' @@ -60,6 +61,7 @@ describe('ProtocolSteps', () => { vi.mocked(DeckSetupContainer).mockReturnValue(
mock DeckSetupContainer
) + vi.mocked(getSelectedTerminalItemId).mockReturnValue(null) vi.mocked(OffDeck).mockReturnValue(
mock OffDeck
) vi.mocked(getUnsavedForm).mockReturnValue(null) vi.mocked(getSelectedSubstep).mockReturnValue(null) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index 687553f3dc6..8cb7d8fbfe2 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -7,7 +7,6 @@ import { DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, - JUSTIFY_FLEX_END, JUSTIFY_FLEX_START, JUSTIFY_SPACE_BETWEEN, POSITION_FIXED, @@ -26,6 +25,8 @@ import { getSelectedSubstep, getSelectedStepId, getHoveredStepId, + getSelectedTerminalItemId, + getHoveredTerminalItemId, } from '../../../ui/steps/selectors' import { DeckSetupContainer } from '../DeckSetup' import { OffDeck } from '../Offdeck' @@ -41,6 +42,8 @@ const CONTENT_MAX_WIDTH = '46.9375rem' export function ProtocolSteps(): JSX.Element { const { i18n, t } = useTranslation('starting_deck_state') const formData = useSelector(getUnsavedForm) + const selectedTerminalItem = useSelector(getSelectedTerminalItemId) + const hoveredTerminalItem = useSelector(getHoveredTerminalItemId) const isMultiSelectMode = useSelector(getIsMultiSelectMode) const selectedSubstep = useSelector(getSelectedSubstep) const enableHoyKeyDisplay = useSelector(getEnableHotKeysDisplay) @@ -88,16 +91,19 @@ export function ProtocolSteps(): JSX.Element { {tab === 'protocolSteps' ? ( ) : null} - - {currentStep != null ? ( + + {currentStep != null && hoveredTerminalItem == null ? ( {i18n.format(currentStep.stepName, 'capitalize')} ) : null} + {(hoveredTerminalItem != null || selectedTerminalItem != null) && + currentHoveredStepId == null ? ( + + {t(hoveredTerminalItem ?? selectedTerminalItem)} + + ) : null} + ) : null} + {selectedTerminalItem != null && currentHoveredStepId == null ? ( + + ) : null} {enableHoyKeyDisplay ? ( From fa28e4799e5a87a875f4f1e863fa3ac5d960cc52 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:02:21 -0500 Subject: [PATCH 51/52] feat(protocol-designer): onboarding animations (#16927) closes AUTH-974 --- .../assets/images/onboarding_animation_1.webm | Bin 0 -> 17563 bytes .../assets/images/onboarding_animation_2.webm | Bin 0 -> 25474 bytes .../assets/images/onboarding_animation_3.webm | Bin 0 -> 58220 bytes .../assets/images/onboarding_animation_4.webm | Bin 0 -> 91617 bytes .../assets/images/onboarding_animation_5.webm | Bin 0 -> 94602 bytes .../assets/images/onboarding_animation_6.webm | Bin 0 -> 67989 bytes .../images/placeholder_image_delete.png | Bin 199922 -> 0 bytes .../CreateNewProtocolWizard/WizardBody.tsx | 71 +++++++++++++----- .../__tests__/WizardBody.test.tsx | 2 +- 9 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 protocol-designer/src/assets/images/onboarding_animation_1.webm create mode 100644 protocol-designer/src/assets/images/onboarding_animation_2.webm create mode 100644 protocol-designer/src/assets/images/onboarding_animation_3.webm create mode 100644 protocol-designer/src/assets/images/onboarding_animation_4.webm create mode 100644 protocol-designer/src/assets/images/onboarding_animation_5.webm create mode 100644 protocol-designer/src/assets/images/onboarding_animation_6.webm delete mode 100644 protocol-designer/src/assets/images/placeholder_image_delete.png diff --git a/protocol-designer/src/assets/images/onboarding_animation_1.webm b/protocol-designer/src/assets/images/onboarding_animation_1.webm new file mode 100644 index 0000000000000000000000000000000000000000..6eed789cb611aa6bb76320c69bff6507a80dfa35 GIT binary patch literal 17563 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 literal 0 HcmV?d00001 diff --git a/protocol-designer/src/assets/images/onboarding_animation_2.webm b/protocol-designer/src/assets/images/onboarding_animation_2.webm new file mode 100644 index 0000000000000000000000000000000000000000..7dbc51c26ad68669acfa391b98f85a540a14d2cb GIT binary patch literal 25474 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�qrk24{Zyl0OUr^z|$R}$BAblTB6!3 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 CONO5S literal 0 HcmV?d00001 diff --git a/protocol-designer/src/assets/images/onboarding_animation_3.webm b/protocol-designer/src/assets/images/onboarding_animation_3.webm new file mode 100644 index 0000000000000000000000000000000000000000..19d29e2b9396e5f2375a08f2aba90abf5876dd6a GIT binary patch literal 58220 zcmcG#bC4%NyCwW<+nly-+qP}nwrx(^wr#tor)^H#_RfpDd++zf#{1W1R8_=Nb?W4K zPF7@PR90e%?iA$-g#`i$J^$VB0zrlD10jWiLflP_Y=y%AhM|GLn92d>K!AUg10E#V zESFB%u4v_|5H!gOGv(^Qf`3KmDz(;sqgC0in7`MMRff!!tAl`8{}tjWcm2H>bU=3T zFCKvAKk5Eo9R8uIk!qzO7(qxtHXv5U(9MjEfsT=lj*)>uUGRU#B9RdS)k^>393lS%ZY?1Gca)dV`HXcV`63aX93^~x4IJSDI5m^ zihNfC0f2xY$zUM>!8}VVfP(}FQwkAe)qm!dFZ_@Ds`sOQ{59?u`b+1|OE1FyGjW3t z{m35Q)JGn~M=abmb)+A{KHl8n7k_dCKY8eZ_kn@Hp?-4M^=N+{GR_osfY{R|4}TaJ zaK8yZeRKyDCI624Ie+}&H^Sr>-P${!A0PRH&(2z|`{8%&>rTG^O{T%$hQWKb$ou|= zP6!ETKz;Ded3!$tMhmj1{1LxoyE;L8$)&6VR`iWY-v!&tq5?b{tV=ymz0RV`^;tYz z_npm!_|ilT+wGE(i=<3AKKVhKw^^Hp7y%2<_ckdf9>jzs{|1h|4aX9S3~_2ptlo-D z`vegU!{c*GFCidoB0DIvt?Bd2kyY0Gq9ed*GbUaxIK0~v^M>x;d;P|BJ^8MQXna;Eh5%c!FMJO4r#a-zn&|Q>)%?#xm*@H4DJa@YXZ;%4v54D58`V%28 z%2#n#0YHztThLZQ&gR>=>ouBnnErtmz8&%l%A<`cLB#=Dh^y{Y)T$UCZW{}KXTm}A z;utJ&Q>zN5F1U32H8{Z-`}^y+l`D-foB!90T`;}3Ttp4M@Ir7<||3tIng;ll;+voO%c#NK4}r-*};!Ms31 zlJvTpIe~Bql7S>9 zWlz?=AOMNn72@Q1patN`3`?5tvFWgrhCV-2|Nh1;2W~-EZCLhq-2A5?02RvA$wPy< z&jWaB2pu^2Flf;{ERf$RsHJj7D$dZ?BW5q2YR;F}c5QdByt_GeV+s#?w&mM&G3|c4 z*z+))i8qL4RGweyP?ZuV-NQt!EdzXibgmasDn#OiS1^S~l!oxa&P@`runv{3;ZSrm zsG{0h=X*at=%&NqbT*Ken5v zDf9WsI~Ruo>`U*50edX9$ns6b1R4&GhS6tDu)8vGG5mDvBT9)G7#I%UQxgv{xtB7T zmq}|syvaAzO`Mrej?LNO?M*Yup>ls;)tmF?*H^0-Y}WejYE~ zw_$x|Wa60V<<0%x@p|%0FPRm<(R}6xG&Fy5eODk=zU)~nr4ZCa5D$`E6;d4DOfw_G zs#JHi;nNA)>B^T;8P2R^{h_1m=Mk& z>XCr&W}b&?JTiHw{Vfax4A|<*Jq0R=mNrY4|8n)WQ5+dp>P+^)3^D-zEg@|n9DS-( zQ*`FnIm1E4a3_B3TB*5F+`#_o^a-|5dn)|-xYC)3%#gwJLA{(K*|0G1!YHdIF z?Pf~-(cyS&cHGb2u>hg40J?>^r?NH`T3E&DV~euSRuI52=OqlOV?KimN1^~&6 zLL&!LKYYQE!9VbGQt5V*CF^fYpWBYxu@37Ep-5zvDCB4;??z8%2bXuc;OQ<8;*C&1 zp?}@gz2S34Ha74NdXY;2Zo3)l}^q8V-q9r9yC=pBT5B+eNP z$biwV2DU93F^~z9igefp)=~UTUUeG4c&yJJ34V*nE1zwjOg1kGG$z_61D|K<8S zBZ=Vl30>V~7Gc2hZ1X$yl>C)Q!~q}8n&BkK1ER)lDNfZs?-@Sy(j_+9!tbiIv(M5K4>&|2fJItdk}t$@9~jtvW8-4 z4eOpY+FtO{yCcv0H88=3U-Aur9jv}bo@LLYxIBZ_^iV)JQ0tW~lwU_R9vR+x#=VHF z9SRsztDBu!mP0RU+VeR~66#ekLGyi5{TGiyWPiiJ3I4|XOSu~J-E_r4PobId)%eGs zk-pT|`zUA*Lv~QpR(;$sU2g&s?6*+#T-n{!Cj^z#$6AbxsT+SlTAXT{1Dul_Y?4mh zm`$XzQY>EXFfl^mYu!iR*orbi(kyn_vS%!EU%kOgthD@#G9|{Yh*5tm@}i6S}9nWXJaW zJO_4H@qt%YNj}XF)}LQ}zZqr2vU~Wmu}x{W`uPUuyk};D_njf|Cd(CD+;X zz%({UA%9ixGiY$+y5745H>20#C`a6lZ9v~};#f-mZWmHwZfA<2fhUA~@5&8_MF8i( z;Ta3up=2C6$4m2R_6HIwh7^wETuVoVZ4kh5XPhMm_5GEtm`g8BwI^p|g%7ZvefK0=jqj5B(y)62l*Q@W)MH_j zb0!5`_SPoq^X_lKaSx1U7s(Jg;ZH*p2%tydiD*l;{Cr2{I(uOvn5)$*tTBQTw=`R| zTuau_nQ;lFytFa13O=UQBW380U0fST+Xj#tY7M8 z{g8oVuNP!!{R#OBxYYb5GYBax;ELqys-*q)-?dvc62w84y`?AFq;~PJ$!1#lO#ZYr z!SC;isuPmZAl?FP-A^JYddtUfbh|2B4Fhw2{?tPF8+lim&~}2|KXI5~LSkOmW2VJ<<10Ul`ieG-`Kb>M5#sAI-9`92>jjb%Av>j}Y!$7+X zG?UZ1V$d(luzY)K(tdeyW2Lwi6kGwNtmI{7G}Aj3f}t!}^x{ zDep6-qLxq>)OC9&m#x7FP49YaGdf(_y_O%;Z77H2;WM58NL+^lf38=O%_LH%^Ikb` zETW^**0XRV7#e#Qv5X;mMR7J5>alA<$8CH`6-fFfmWcz)I z#^R`Ch4S5=y{g%LK+`So4wuN#Cy+VjPgI}*my)EuRgwIPMiET{Iv72`vPY+E>eqz( z(P9|4SjJv>p9c!1cbtgV*|)@w74 z@Vb@W@>!^U0XiFf0hpDWh@E(Z(N@=H)A03++egFUd({9T%L@dj^Mc&>LS)|RmKgeS zXF`$G_W_$}?YW74pa*OM3fTAbAJrsK3IclZ6sDzIDLk>m>@}u_YOp3rT2f~7d(7l! zS_RbKZ_+^R(5MF0qOT{3(p>^ik!4|Z+p0I+B>LS;P*Y^>#|h<0ch?4(n;>$D#pvP% zuaYOvXOZAln?kJ+Lcp;(*qHh!8GIuSG}J%^&-t1YfvIfaOxWR%drf>CYD=oT5RoV3 zKxLiWSh=9{5}oWzBrEM;T2UJ0d&aUpLm!%89WO!_{aP%r{^_!uK{&Xn&AoJyRC)qn zp=xQGZ)q#I4CgjVt{$(8FG6{%$Xd&htfT5TJ=i}$x9TfLQ9PbMo~-l@UPD~(ObcYfI|8WT+)TjC=bwpZPk`i37>4|}fJ zLO}pKH*YQedEK$b{siI-ViocIw-0ALodotY&ga9+r}Oy(1+yI_n8QlGAL(#=)Z05OfNSs3yPf2HU5e zJo$diQrMpVYAk1x{D<1zbXmgwph6}kZu5T4#4e8CX=vsu@~U^RFy<-%cPfD@E_XT0 zAuZYLG}yA!cR(IKJ8C(c{m*M*x|y@l*%QQNK?c9Cv#m56U?)(hnVfq9*h@-7)J+yj&Go!s)hJ?C0Dh63p?N%15DpN^g8i60fM=kyHXzUZ ztDqp=dd5Pb%3H82wE6a6(PMQxNa519gt%K;C_{Ekw%(bV-I;Y4D;{YcQE(u|?-!S3 zkI<~A`KzL!EFv}di|gz!0_M@)&`!VQG;9vMz1rf%*BMRGg>z*sWb@-pByz0HWt<$T ze$FH?7d0~V57PG1?mD!tj9YmC%?>%=ac=znDC`&N6cg5(jFrU?sqjE)aZoYbU^W7y zZIe=H%Ccl9h_Oiry~Rj9(cWsOm`n&u^a8Fkf^GT`q@VldvslzS>S!F;J@#&(< z=?q{4gqN86(PtCp6@!o@Ww{e5z z!Nf{mgtOB&dXN8z)4387$hM1qny_^ifZj(jOGR-3h)W3%NaL^Sq}koM_%>^8C% z!;=~QNcv<;xgQu&y!f?&U$R5h-&QG!;3j7Mx-`)}slyX#@n`Q*+Bh8O^&Upm;zIK_ zr2UXW`0a6HOdOAD$J&+mEM{gvdkjFPtFWB;g4ZDSL9w%5HAj%T5+K^>#nl3dXxJbH z|MI+$FVo)E5N1lj=DVr;n@)jTNTs2xz^HO*9c8EjUtaIzRuFPu4Fpy#hYnS*b__H6 zxB?_^@*3)r>fTJEzic zVD1Q8n$U=w9pVN)bbB?oL~P00;kqq&aH!PlQ9%|s`&T+qSi?Eui5?pviCjppV0@@E zXRk;LF6u)zw$XC| zP(*{VxzZY_AEl}M2q^6Cn$SS)dc>27Ha+d*g5`c;K-X(}M`a(&89Dum%eXU593Q@M z4%5VKftwOJ?LQ%wH<~3UA5OLdKTdy>A$eJi=n;hCH0S#~>8)rt|kjy;b&d&KAooaAvH2JvUL{k2clamuf zcbOiGQJ6KwVEzSXb`jDKA_Cwh;Hc>oy!$3keNlv6y4?*$vBi&ZK&RAH`ugoL4OEEC ze?ywoc0OnP@qOJ^)U5MX6OKlPlTRf%Nl5RZ@Cr0&tg39S2T=~;l>s~2QYmu3a@9e5 zrwH#Hw_tC8(&v1E#z|gLwTjYU*SuRmV{*jnm{l!A!I&!$R?DpHqIUqnk6}VTExKM= zuT5`f^rm+t7me*rQQ`ZGLN}*>=GD{?X-E9Le?C~-1UInmv&@Ag>BzzC!SLd9oObkh zS$+$z4RY(%Y5BAV9fy|w1@3z=`7|h;YYE-J+ENF_+E;xe4N1LBPY4J4Il~wz!8Z+wQbslNVYNbqRg_#kz*Fx*!Nx@001Y4<2z^3754h4kLd_~{6`k%r0MUa z)?>YM3SiZCKo-&WMDhL2ORO6+75H-Xv-gj_e2FZwiuMYg7mKEHG~bZyOuhnvFDP|N z1XWZfY$aDO2EsipxI4dUaQq&W0N{7%)TqWp!mrPnp#k1~_S04(%$Z)&rR-2hlpOwZ z6lz&4gFNLQse^+EqRkF5NvQ*z;_N}tN9k&;ibhT9Y{T+?-}2@@d_B>+71RlG?)qA+ zaA21#e0k_@Au^tZHCdz@X}y@b!4af@J;;yBBbN|ZoQopW>vZ#H#6R-&YYid++Z)b4AF(@fe>llB^F)lVsvO1W@ zdz*F^&IYznt*O6TG6^#mH_G0m*#`0_yF$zykttwrtWrN$xdz%lM#=)neL|OcObn&1 z>3)Ng=unB=Zfoj)CH2@}mP*`WJp!klt3`U?5p$NK4KIQWo(HsT3Au8P>)C}Skl7?0 zRNd)$Nq83K4fI!^}CpgW9`d+LHtlh`>=p4QXq1{=TXEf!-E+ z1-~L_VODQpQj8-P!(V?<;S3v0?+DSBtMEhmsdy^|M;e=~Yu_r$#E4Buw4#Pu( z{%FW4_XCLI;xY!(^Up)>*U1O(UbzxVHfhZb8eAm;$0;*tA+yy3uA#^BVH-yThQg)c z8U|~SxnbB=Dra?u^t0eF8GF2v?+? zfl87Qb3pTt)*mo|NFISBDE>xqUrR(cD<#sWro68Vh#dnAT@R9bl@;eK@5FnT>qK>^ zB?8$Ah&BN~aF533!u6@k?OUosKy8EC4mD5ZF-Q~}exDP0fQC&wx%L2bzz9$-G zdN-1#SZs`yTO~&x9~^mC%v#Fvh5P@aPX?ZWf7N#3W~jZ+BSWM-<|;uDx=%J`JxCh$ zw1mL*b2SOjS13%hJPBI`x9Fm!yahG^aydPZNsk>BGX(IH552M z8YSkQ&xL>?)(giF9n$kO)K;2Lsrl7%DKN;C#Z8PpR756DnCrSq#st)6dbUM1HOELm zGw!s81PSjjRVk5RV|*SiMiHdVhN4{lj&IhX-1FI0cM;d8W@2q&8JGx29(OjQtD^tB z_UEc$*5odWyTFdrbD<{LPi4J#*;q!x%6jD%UFIk!F=50{6%W}jwugtK!TKcWJ%>z~ zxV$9mDalqB#qKu{`Sj>bgU3TChC8{yo=@gulcI7&O@JL+T>i7KHQt2~yA|Fc8G3R!4 zi0w>@^8MqG#|r}}6?PVe?fkp3tm4?a?K3a2mJTcfn8Sy1}`rJLiL<#03Foz_0 zV)#F-4MgLRT@bKZBpT#7_vvqHz>BSCdxI$8N6JUsPV-N9**vZ?oO*hb3T{}ds;iue zhb86R`{I5E(x5js55|2nFy2Bh=^vh^RcSlBo$A^=g}dUtM$n1m$MUT4Tl~=@RG-$P zDASnBKbk$0X-oQ(K=ziurUJ@?xr%LzlZ-L%}r z;xVC&5!8a6n(%FXn3X*Nr6s%Y^C56!3I<<3^GX8vRylhycLJhFY<2YyOaS_fz%~@0 zxUN>N)D$aMNh4>lLph4VgBCFVaQSmz)9Rf_e!pzYSgIF_`doF_xj>qVbTK#Tre?oZ~bqO*KWlNf$ z3_8ozJYFHb(@7Hi-K_G&X^jHw6xg=+_#<2sy4&(cKA`p31tzaUVTXgdjjj2f$;oizO z5U7wZ*5U*?H(i{KMTwN`*=VR~lp|iWrq7#az!r#RUK=moedpW@)9y zY)6do>yO9Kin!vB2xMFjO-=4#8vmkY2#Drb&~;s<{7y1l{3Iy*uuIO zXf{T!qIVETynlW7V2?80;C1vs;6~%elK8U%Q>r&H_6z4ezR0K-0$)?EOKM~)Rfp{1 zmu+gPe>Em2Ft1s_AeO_PLxtpP%J?Q9VU$JO;gC+$pP%Pglot+S0h`TZ1vF0KWG`^g zIW7SYy8s}fG#_7x+#48ao}gaBKPw4>=F{^|TK!-W_+5h%FJE~2-aFqq-D(P$x;$SQ z4@h}${)3BqYFe7|u7Paw^D^}DLPFfBbESZugH(;T7c=PPeT@DV+g@+_kP*2nG!2QqnfK;}y`lQ4f zUJRAGno6suuHXNX8%K#>7a80n5;2&K=k8(15pSoP@zMxc`H4{XlHcnp>e)^phfH|x zgO+-<;Y}J|G`2DvzG~{_dE(!7;l}DL%5M=ArhRwGRJb7tQj?i zM8p*Orv(&~rUv5jgjDZ5?qxG6)2DyDm>|>#wjRG$8(U88%O}JBq~KV zb(oA*^pKaZB`#~i!8}b9`;_Uxc#I5S;;gEfb88IPyD_7!N+ta=M?6Nw}{h&%QHj!m=&h zx1$Ml5Ai|BdnZsKwBboC4%76Wv>Wsnxg~5&rujR&)%fxT{7VO^?3u*u*D0Y!h5!VU z8D~2+blx>blBk|ln?51^@SmSJtTgW3l#p7O9syiEqRjea$jl%SqMrhN3#&&yQ0@!y z?(uXw&qfW4jxduQ`A#k69BTplnY zv5rG$u61oCd}kEEm$G_Ll$9S=ubLonsR-<4n1Fw5#0_IMj6gw)!x;LfCa@)lpVQ64 zDCaDOYqDS6KfK?oUzg%N(~nKbPYH7*W@mzgLuPu7b_82uaj?vIcjjs&pOtrO-|*(dQJAWEcVx zo*MKXQH3LV-aiLrh9E#HS3_kXTAzxbK!(s=i)r#YN|Q-IEbGhkcZtbMGvO>Ng?Fj$ z?q8DOe+6$LCzms$GAfa=iD&t=yW%p>icDZCKqm9Kq!e+cJ{9}(*NmZR(M%}BimOK5 zpjhF3oc2dSc+>FE!Pa&qY$H7Yz+6ciij}a30W3U)L@FggsX9kW5d)+QSW|eQi#cc8Xp)`Zz`%?c)nga*Kf{M`R+v%McU7#quAA&BS2y-{w1dCTVtx{^?~ss8BOep zU0WPE9=J`&u2Ou6O60M!(M$A@0SI8&*d9YV#ZQ(wrc?dpQz}WfWUY^O#7+?+-?O4b z*02U(5-m3iHWco93x`QdTIy`|1@#Kii*FM13HB?V-L`m)KfkDkDf7(d3nVW`dpWM> z4r9mRFZ(ImOfbyx^DSi1R7$IzbXn3UxRN>{XpqzRP*7yk9|rd&$?5flDyej%<74=j zjlZBK@{<4|Kx4ax)t=b+VmT#VOv9rVaOma2x(=#sPbAIeA$>h z8_^pp{X@SY`Wz-4p+(zOfxh;{h2*_k8Me zex_6p@|?PT1IZ5PTA#1f<|fn}m1d^a4j_y=#Xon9(w0b@ma`%L7bMZ7fI^o^b}R~mw<;ff+~z1`*dG2G-_=tTyF`W zpziTW>!rjI;pHSzhVTAhMOs5wB(}b@PFxPe3Ym2DN5q#RsqbeY3rY~r_3e3U7>I*J z`TmkNPfF~?-K3T1ngL_FcKdL5Al#H73Ji%1TMu=w3Gj!JE(~f`v>J%RFyg}>4*PQd zP@L@#?4+f->OEk*B(T0q0?`{0g)&_q70Yc|6XBjaN?R|Cbg*-c&kz`6A(7K;ao*F_ z;+ys8^X=A5dRR=T0DKGY&yNbO*ena_MeiMEk>E*&T3EFNdG9_U+UGl@rOBHXf&f!# zgU4nvr93-Xky~>~jgfTFP$qLp7>KxEWn;~X7s=b{O8^~<=W<+r8UOiW+C-lsGlK2r z;*n?L@`=~1i7X9GIQ2Z#l&PQwWqc;`O5@x{PRTDW%?ZmXqLX?CsqZhcxyz>SmaeITPN1HRI!pZu23>JK zJL$^K-)O)vl@?FV=~~#wdmyE}eHgZSTcz6Q@=VX0%e^ciH$9yGNu9L3Yq zu_S|>UO_DcU^st7G}4?Xw0IeR62e)s>I9nENVR$~c?&PrM8S$H{LT?cX)8$FI2@Vr z2+2X-kef?P;~Rw;-glwj;PhPdo2Iq3OOZ0rdkxFO#*vjSZt{)wwfz8+!|oFAUN8?6 zw*o&pMBQHQ8s$7NI%=(EQCG4cPne5Ui%H=5X==}1-js0%tI)~IBsOGk zXkxTE(ZLg1Y+THyQX?%A>ptCr2}^u)@Cf0v{SnbqX+Gr%uSI&FRSf~AV^?mcF#*$K zqT;Gz6!cyF@#xf^dKa0JJxUFPkh{m-*2CPu$w9kmT9v#`N5d5QoN9)*M`h93Swszk zY()29t2hBRF(B2QczM!tCFz6oKqjA8AC*wM0=VTlDQX?rcX`*t;fZnuHq|H%`XE{7 z0xoB*Pm5G0KX%uMviCu(CTDvlH-5e5P_@(uEjOdK0UEBf>C8yYhXD*QI$&B^D z?=t~*r8SXaRA&-|&8>cbX=}WeQC_kc;G9j1;6yOIR+kQ4B>8lmmEZm{5Y3h5momEd z6$VT*YmVUmh*G*ItX{>Bqr-M?F?tBb<=NG^mxT@m9vD1=z+zt@e5^~(eB(nro6CRO z^WAr|9jdexdygLtk+!yQAb(I@8>i(k8hJ*y;hTST^CnhehAq{t!kduk(`S(xVOUzx zSml~9b1vx*OjAhZ#dDw%B0(mYFsh;;V^OAs0#n*bX2=I*&%Qf!3C7MTamY@2$n8)m zb^E7xt;lL_QPBWqxra9?sbN_lhX)9<%qOM5-d*HQ1|VOutQ3TB5e; z-&bZFq_`)AuO^}5q#{ygnF|lb-NcSK89$+*Rmrc8*qiY6;t#1^=q?_XUAlA-E<+e` z#WrnIX3qi*RSwR93!?COU(JYx?U9GpRICk&~vlSwLCJ`##70K@9!i=ZJk`7J{%=O@_+Ce@9 zwcgOPcMwAcBXXpa_r!w>b3=Os-3TzLTs*C!of|ZTjujP^cVf0#MQtaCovI6C0jnWr zxJZlNw!yGZJ`)g&s-MFjXpAXMAQKc2Bod<)6CpZByEf-eEN4iiXc{9zGQWE559$qa ziR-whkLMhThenq!_;=EVN@e*QNLgGzpIB=)13!e0GTes6!;i*st`u3Gx8i8N8d-KY zQ1^Tu`cG)J+DH*2BWEQnlt*F{aM3zVYDS2!Vyt2)dXw%$AXHtW#!_GPHM;%)^}bHC z)(w5%D3tTS*Ied}EEz>pk-taVs%`f^3??^X(C7Uq^D?u+ThSdS#C)Q;E zQLKGM2bxs%5Bu&LMo}SdzFMm_*xjuo0BmD)l@J9;To3G1!jOM>J4ir9YMiKl81vJA z|8dCpLxkX%Ot@e4!7y&U3T4P5X~4)?8qrk~?#s$70Gs{&Q$yV*nf0e(kkx!Nw#md- zJ1Zxn31Ex9AY1aaoyJ!KkuUjbUt8UYIL(?-Q+2WV1_1CE3YP?JcMQ z8X5HCmT3|*U3hf0J{zv*{&6i{DFDJ~^0`gC-^-O(hLfl4Gx&^kKuEeNkuM@gpqA@Q zgeiDAB%cY0V@1MI-rb8z>%UQ^Iaw;9rq{8z+rK^2(>athurH6Jk;xXlF*OcL=h)~p z&nm61I8(9T-xO@<&H8iCwa){=oZaw_!MSJ~fKvT6eo;4RY8R)+`Act9Y3zyY#PL34 z^tK3~4Sl5YIE}$G7puGt6l&G9sYGWYw&buH1xXhhl{n2Gr{b@4vkuoSa zpiKOocAW3E1cV`hOIf|AM9yi}IZyM#E+@KEoo`RJSI8b@?)3`7%ERYhG2x_lmXiZZ zM^>MMY@e1LPhi;RKfy_13Z^B+i?TeSw@XQPhc4drn@f974r#i&nIDg4yz7z>hu;?e z8@TMv?+H`ddl@!UpH^tZd!9Fi-qYw7Xv#ChpG?r8qp=pW<1_AKlXT-6KjDcefS#9{ zZ`>dPzwKBZZppPvsz*Dt3lk0axOn<)4PhM}lBa^NGwki6a(pV~#45l6H0oL}=SEi} z8x%Y26`QbJApihCEPhQ+a)w{JXbn1|$f z9JICrS=aqhtJ_mu8$X?~U;(HtzAJx)Pgln7 z4(}fxEc>GL41;A~Z)sCq*!m*HZ^vM|w96PMy8@vYn)mH)X#^0goN~6mP%*}j6Nz&k zkWx4tcInl1c)8BpW$^o1tiaXGBZ6;p)FeJ!Mc0hN8iXZvOjw|LBC@U-?SxXB)Za@nJpQ7XaIVyE z(Jn-`gr_V$MLp{qqNTg=fnJ<8h4`7?)~K^t8qxeS&1mSi4jH1OFBjeAmpnp-%%5?1x10DBb>^n*t5|?aKg99*k#uSs^g8OHRIsdJM z*P&c1`^T$aY8Qx`pk>w^$k%*}LFv|+Te+2tv0Gtu9Cc_J+k-GxAO>Kw(^ov4a|LG-b~QPJ63AVkyud zrS(13NMjsNW^inDaWHyVU;rDPl4&t}NAblA*l~A;7w(rGmKXD@m3XRtvju!aUqN_g zI-Z9NJw<_>k~<_ForVF`!GA}D@Gm!aN|Yh|;n*PI?B=sZqh^Xqh-3r->u)A=&l%{i z^1{~^Fx1rMMgB`_A6r=Ak4U4+$@(j&$CIOl#+bF30&;`zC6gUI8my+{% zU5<`L$Mg+nfG9kdZgCwx?#2xu$gB05Z%uz}O0Bh9JS*s+moe70(!MW-+#+rj!Mu?W zj_XgiW=qII-%sr}W>~|uS2JgHJdM9;%_OkuJn**Fc4Ghr znDd;)&wyO%oX}{ZT4+8Xa_ktERmzOrP30wrdyqI= zYqwXLwpqht)Kc8SN3a|@Hm|5RR76QZ2IWyz3S4Z zC%zu65L?Y^$1j8tGfs3vpDvyg&di1F{qTE*(WOu=Y0D%;WMR$UN@2gyhH?(i?Hant z#hG7VNI28%Rbuzb{f++*`X!>bnx;~yF=@>urMh|KWs(xWvGVY=?a*V=i-tNktmHYz z{#yEKFr1(HKtaWCE%|LSSaag%>D=3JCh4_v|4u-Fl9a`L^Q*y-BvXl}QKykjhV`wA zU)Sm}!q&6UjRqz|KjucG`Piu7Y+&Ty8PX$cnd)nd2#+Us=d%c2X{Y2-u<|Xjg(dqC z1C7clsCQ*12&KBX+7jUDnzLzKMd_Ih1Ye+=4(y#EliMopQ-}P)t8VZRa&>6rtM!^& zVP9REX0f?957E%8c=$V`2sXp_cB*^|c~;FomNS$-EdG0=NPg#=$u=AK$%!6TTpEKz z5_dIX?K3^rgkK6O5Td;uZGc98pJ!(?ztY=Wu^ZFYx+gl!KglfAG114%3#TBP?j98r z60OZUWXd-o7aXlnVgkN@-ft~SG-jw$T;cg-!nMzggOA#Nyd3y7!AcPou7LwtHG84YyZs< zW1HyWhlG%0wvpx5av_lmap?bNdTrjx(m?e3B0hwS@&N*?5t|o=(6Pu#dlyZ>T z61#?M-0zb6l^>Oi|8=;Y?$pV{mNp&NXnWu&kV!7bKU<$)XYB;{Xgc!y5Mr{AHJXT{ z-W#pN`5c&5^|84Nik8!nS&zp1&CY-ePthyAYA7FJ-xMdHqRjG*0(ebt);*;iHopEI! zz?Jfm*8-6OZyXCpJHN|^mo`zI`F(jRbuE=GN(w%AHCpBhcs#_Y9rP=SaUP_c8+%_$ zkfyvvL{%jn`z)0FlT$XhoigH^h}Ws~2l3R#pX$AHLVhz~(ESTxSaR4@Dgx5ufx&5? zQfe%I+8RO@f==*P@OVvnWdei9$r~HypJT9sE?d(g{|^t6;>XqCKIL`u`iH!uJLK}4 zchoqIn%Dd*EDgS#fayb1e36pv&6ObS*Rc{;7rmAruX#Ww{O%qRsSM$+{vQX&aOE#A z?4oRK$LR|Denf(q{b?W?$mZk=6qtDkWL-$48q?dwRD;f|RX^jyKDJ5iw#MNIQo1No zjNO0Nu&1tGefQCU_o+K!Dkwu3v5*(9k4?U!?Au7|#pPBFTb|Mc`IzIYAiZt}rP^MF zR?5tVt?WhcDP>si!%clqsu?1HXYszZ)(JHNfW@~%MkeU>me_uPHSI=Kx0mo!=+l-k zt%H|)@ZyWyu@(kv8uIDp1}3WYUertH4=CiAwZKa9gyu4&0i|<0CEA*{K>bP{es2lN zh>Tc8($-yorN8Y;guu>#l-PwUkhTTWM_LXXb?waw@bMF$ z+Pe!VMbKQHvYgEnHE!6hu0$mj^S!9;_mM^eo3Q27^{>i;KL#!}k~Z{OqZz8$rqf=5 z9XN`odKP-4p**cZAa{0#mMA`_N$ zB7_p$&#L~eCg6QobTgqgjx5>X2$u#Qi7d2{Y$)xbM&6@W8J0hOqfGc1BztwD`tkU4 z#1s#I1O{H75B027W*%ree+f4gPP8-Vkqwk|cOC$qTOWWjXO(;_ycy#FnU7{|-sglB z)NsKJ@Cnx4dn#n7DqojyLj;0ox1JPua$9M!UNZo~>;FD)1wcKlT9)%oo3O>JApXf} zaK-;-KXs0c$&x)34wOS&hNIt~6rR@*pwGaTDm}fo|R; z(Cn7%*+sbdW`6r=7gMy}HF=BY#c$oCd8{KExoPDI9Z}gRzS;Ywz^hsa!d)9O$Q_!N z6+;oIu*ksL>+`EWoH;Qxd#`GuC?~%jB8OM&EwfH^YqYHv86%81led)Pf|mH}X1RPI zR?FTsYFz?~Tj{LY!M7;T4gp=K$8@yK-?r^Xbq=dzmU;9uyAe>^p@G*gf@~ zCVSU-Tj^DG0oH;@jn&A--u!x;hqvl>SVf=*0Q%125dxy#n+`*BQrm;8b{-af6h~!V zyns`q00D(uFi2VefIJMVav)r!@ynsoo8<$L9|sU%2we>}v9tb^`1^x>GDAIDi9*Q| zaFC_Uw*0+FuU1nf)|s({Ymx?V!N*hsaDdCBl_p$u*&-tq3oP_X*nCwU5h6q)09up) zl7BteTRj(SG=aPs0DyhhlHU@*7xe>x|Z}|Nn_LG z?aYC_XQ7eBvpe(X!xeEw26@qEn?ZQC|>Y)w3|ZDS_3ZBLwutv7RD&vAU;AK1Ol>Q$?%tGa9R zw?qdr#5t)a2UCAkZa*$KCPee@82<-YumA{#_5WYP@qfP%W}?2S5BQ#C&U*i#ks{>! z>H>_U>s57iR2ZJ zMaC8dw>LZ#s|3TN#ZSEd11XFJ1QYrK*^?CWj}BiCD>uMk-V(l$g8Vg6-|61Q8!Foz z9!%s<1h@T^x|tV|IcVxJH!=A0t;1=tP%k$C6R0#2s28Lq%hK3eZ;owl7^dRb_;T)n z5>6TV{4`73Xi6y>?ReIUbz!|IzA{nU`O6I^hq-6zo1}lndMu0qfgQx0g}jM?UzguQ;q|K5{uIYv1)*3DP-jcL`1=#ol%)=(3SK1kfSK*D9Hr@<7`3L zN+c9%vKwXGDV_@HdiYiBeRhj=u)P6?D(P$^0%k&zECdOcE@?nrDLCwoTO!BTa2$E& z?drfEkwP|jdhD69X!chOMK@h!wZ)*1b`6iv@QJ;T`uh&EyWIJLoeP06CG{F?Nm{Dp zO6Lv16{yrbmWN7bh%6Qa7sN6uq#*E4)~3j&r!(bO6>tEVz7Bi2we z9OccqG6Sj4h!B*DXTIZ_3IxLnFy>qg6w06E7CTb|CPzugVKl|)@E`hWGDsVgV0sLN zW=&{4JKN1=S;%6&sZ?>(-W)KBYw1r$FZjECo@T1-c^5muExGHk)!x%>Ya^FRCkam?H@Iu#es9DK*@Xx2Cs=*i6kahnrM>692s2tiXU zqDqzlMi?!wXC!c83Q(WL`SCPhWu40$m6ZTCh;^RU9%4c0lHY}@hkBebr!16ml{ki= zEcD<)r**lQ&Z!Gg!bYLNmxJkJV^-QV*B_v~CLstpkTcu`RuvJ~@)%c8t>>h_XLW-buV$Ra!SJhAx zEl1Fcn&583S@P$xj3qo34P7|-i|OUven<$c2vb=!3md)qXX5<4uH6q1S|W0Hxe5(7 zh~jl^Gx!EfWW9ovpRc`WR8jk@Bg`yMhONJFYl-ULu<^bfz(3Oc{k#4+o1wYKq(=GE zQLyS|jM#&>@hSO6elPO2hmgT&XvP*_nkJTJL2FFzl9e=jn!<3+tLzkE_j$D1eJT(- z(4V_w1)w_%`4~X2H=c`4?dr^-64}(iN!zxpXLnQO%4O{f&9Iiw6*(ec(NVTRA?;=K zXTbruN|yS&O+4Bb2gWeb@8xf5Uc~Qk$6IQH2#P#G78g|(Cc?iPIo7^M@k*!spT-70 zc$=zRZ1JWheVYU$p+h%ogrV{~fn_c;yt#$9M1Q5-2OQLgo;N;uz&bZ91^)6vqtQK} z&km+42|<5t4LuO-*Z;QW%196#0Qo=h1_2mvWyBy_2aDmb82E)(C!WZOzgCXPQU(~R#ukHSJ>`h{pQ4iIR)bd5dOoW zg)C&GWr2UA%-56JceVO`T88ZAF+|t{oUovCQKV*~HBy#Az6bQTi;afv)nbEEVGSY^ zA90?r!=9%R@oW2~vQeB?8pGd0m!_JV1f~_nXIZLwU8S8V1Es!o1rE zyPV=RR*QSw6ZOkKdP6JOV#+j4ja3Ei!PWqh^JF$6a6#CNe*f(a!RiS7)-`eO9b}(VC|(K# zJO0mLLX;r@BKI(j`|ks@mAv4QP;)M_U#y0hHbES7lce#s>_n1&#h^Zod1Uqcy>bTn zZgdG)3aTQkK5X+^uuI8PsR3;YSPyJaLY% zWRc*Vmg^g^6J6^z&*DYcbq(mIM>9g(bd}r~GpE$nG&;ArEhYLLMM?2i_}emLW0Eip zIN;ENzXKW#;kuIW{dVoJHyp0x5q44v(=H;@n9aJT2b&?uj$XX4jW{Tp2*-2q+9(S3 zS0$cNPK=`ZJ4ix=i2R#j1`>&2L1m_-z(DMcAhF==tboF|`d3ziK4TCO5t^BnuJg~1 zn?gI2tz8h}wiPzvGF&Z;-9M(!rsaUfTECo0Tr1vyvR>_K=++E5$1K@91}a!VpvU^~ zl=&16YOEf4P-<=@rL##{Ti#Egyiscxv{nK@HfHK6K_*ro73B~BFhwIVNRDvO6U^C9 zWsBFX9`vK4AfluY-`?^mGH4yq%SP0q7fByF*5R)2BlR?~Lx2xnAvUBWHLCBqAbO>6 z?f(evX~&qaek5z;g>p|k-sCRoWb7ZT;ajFocq7(u#2x3`snK!&ep0<}?Lu91 z6Zx>klt529it`7#1NZL{oADQpcEqj((YBcr0vWd1k7<;SnvEo}=)q@!Q3H5eda{z61 znU>V>$CObX;ixmXx%gh+u|)2`j$)$o?+g88H4$?Y0k(3Q&)=06l|2VN7jcro0Z-UD z;Z^9fX(2Xl&XOzF-RF)Z!<@KllHzoKBz5g&Hap+D3;X-{$5s41^0^f?@Rs+Fv zpsu61P6_a-d6;ey4lM0xSQK9}%}mm?yO=W|-{3A7G-S(dp`4xFhlS3M_1A3yZvKNw zN;26w5u|>yw*^GVVp$Xu2Tbne?_>T9->eptU){YFR5k$z?)Oen=JApTnEtvtn7!1v zuX^QFk66VAadG^L{y{6Wy>z@+Qk+t2=d~8sA~*#t5y1KzY-n<0SlRQ*y^cH$Wl?XA z?!C{D!?m*{pY03~4Bup4T*2VO&=WwmYM|F$g^kgmFz)B>0@pol%c|+`6y)G`Ch9CL z7<-Vi*dwb?qih0k*28K7GmDNTYjX%X$)ESP@;yXv)& z_h%o#?Qip0w4ZjY{cJrgY`t>{5m_4p$5B>04(+PwUIVji|EU?=)qFm_d%DkBZJkXp zeU=ovP?J72cTslubbP9&Lq83=wqS2MjSihAG?^+nQuzb4MhixQk~2i<9pl4^_rY4~W$j#cJerhnKTmeb`camalizV}l*9JiHqN z&j}XD5xMJC`u_J@>q9W^NJ(39AdsLkM7+RLTdtluiPuaqJe#HhPq4*<3K51x&gcUj z!Z!NUc47x_S)68JMp1}NR?mq*btv<5tl|e*GXGqXN~}XD4eO=KUqnDJ7c3pWYAcoD zD7D>Rdt1PM9(~R7RZKnsWctyxsK@n&Cw?FO)j%9InHe(V?JwX98YKR@JP}hjVm1ab zZKIU=N@@0@A%Ve+#`iFUG#ala3}QFGvl}kQCU5-*b`L1bA^>n!(R#CXZ#CTUBM|l0 z-sub{?q-hA0jHp5Y@O#{zxgQq5tU+DWy?`vL`=tTcmpd0CQIiQ*2&}7;2!Oge62qu z``p;GtGC3_S1=g$H)|c2K>H9M;CtceeE4|6Ug>Sa+0zt_3had@SSs+07t{Y!AHMxg zPNk5-2~MD%kndY>k~C&(rGA=#rDGJZ>tK@ICN==TqvyX;D~i$t2>8i3peNTCn;?FA zR;AV`KZ2EDOFKeNhg<%IN^7|X2OocWCx_6}@5E2Z9O}_$H>F+ndSg#>zi`%mZ;ha! zpjLmEQU)N0y1=G5xlNO@)yPN!nS3YXu?<6H8nsok7Xu50A%Nhh|3xaNuh0(bh^)}_ zFX_rRKJqNw=c7+r*dws~Bq-m^`{b@)8#|;GH^U)y=4<>rbg&xgCOiVB_5;s;UVxmv zZ9DP1nvuj);xF(%$kOUrPz=G^yhPQYm4gGJ4K=}aY*2(v_MyFYlM{BSaXBaf!=H$X zpWvB&GSHFpicyDaUVk~Bsg6`YO9h0^71Spn<-h%i0s;hf+Di(o*9eJ8ttx)Fd66ik z8amz4+2o|UAqE?*Xs_Iigr>$$NG3iNVSJmOk*0I6X7N z9+V)^igN=z+Gq)kO%{EQSh<%kFmA65i*`{<-zs8CR`W@b0tIc;kS(YLII53H`I@93 z*o(O?8-JDLO(p<8?&`?}fE0EP2Ny18VSf2n#x1lz$3o z%_3i=63W8aR}?lBFcZ*RUW)uIbq1}6zh{iGbq)*srIPqTHx$!qllG$kgwqcDuc49% z4z+0ECqiWOq%~HTi${SvnKMtb2{DgEwAb%`Tj+n@?>GOh#TBodB*Z*O>Oftcf+kF%II`{z5L-?zmt*39Yp?d9q7tHMWYiF2&)yn| zm1~4i-3f1^VOFmy8Z>`1SAut9qC2YI6a=gwJFE2OZU3QP!w(UG?QGUPdTo_KEx#!_^*W7U zG@*k0@z7!E7uU~DT9F#0Ne7!x{C$iZu@+m*wHy+ex8yvHD<{Q`!VH9tU4*@bV}_XL=fzipw~8h5FG22sI#c zbt-tcV?lMcr1>!^%3s%CSUlS=KNK8;8>r8yp;U;4(Du6MWNkgj)XXER@;=bxhp8nb z2LlWDh@!r)Kn3ekzA4X@1dmd&j+;^c)+dE#NXc$rn*-3CnyZ#)XvFd-fy8LCJ%pL% zi8xtx(Jc|+9Xp~dn6Dh3?-q+RRtpZ^S{M16IfeCyQMr-AVQ$JGc;#l;>RSqm_vyu* zdglbUT!A?OY)Mn5);C*SfjS8YgQ`!7f^8izC|A|ev_^F`K}TuDd3N=E}$?wEXdML}`ywsH`V z9cj7AU2FlUKcwbB{izYJK-K4rl%m3`qcRK|?>$=piyViBs1J;KwbWHdN@i|+w4YeqLh+t`nltkD&MdL%wLm`*Ms(En;4Rev! zo#$E~Naq{}1C0a9&JH9~2c0HTXbC`eya%-6K=8dUIA29W z@UNmFG9X|z!}LNv7#bRcTBX8=>>ayqV*(1X)?zR(pmXFE>kBCunyIL!$cBKKD!m}K z)iBHtY@dtw**@lI#>PtlpmXq@1uqG>M&lN|8EQm7Iz#T>QGcsSBMR0?iBJOEHCh4x;Z!X`T>> zJdh~r5-f8%gm?sgZPIsb$&P3J`cSQ_&Hg)|~=+ z-j+)^-_R-*7*YZ;OJ}41X!|S;vY=pYgugpuwS)#9n@eP8-#pJC99)QYFBGf>tgH1S zYjk1dsycS?$sRI)BP_~c-#KwUXR4y;isCHz?HVqi^42O@%CE`f)1@L)KjDtEsqv@t zYdRv+%(-|u=D=wtbkf#k;t2}*vJ>*ON1tYL`zsG6anehX9tubg1KGq=M@S?oD8k3a zNSkookbJW&1JCN8-xv6p1%+)_)SSLOGM~jo`h>4O>cTwEzg}QFH{iJ|KTz$NG9k4^!9mu$FbB>W#XG8*ayvs+ zRb)ZR{j~01jlb(L0aBYez+LPG*GWlIdm9Ny$q-dUMLqdECSm|ZoFuK|6=rSsU~NU-!A#-u`aC+qu|L>bNLseq2^_Vjo?NMXK-(S5T`pN817 zdYqb|R^-FsJ~vL@NoL07`0OxGXXU^PusDdASU1#PkGR793 zj;f?Ky?lx;vl`c9gA8%TWo68FY@Ry$=SdPp`|$dV#42x+dtGc##R*oREY#12)+*eL zQjb3Me)<}k$YEQG@jUJT5~I|+PXOHg%#Z3@r39FWHvP5sBkxp`_$16{h}rg&ZZJ^P z(tckNUFzKJo;5MzaW@m(U+GlO^+@@ZF<_l3f1jhw=6+eK3u$f za6KGdlx)|P5?Q_YSkMgtc%2S-YUbB|D}lq{5b-=T9UXS|6)_`9iSX@!e=_yQtiy)B zL&d@2xv!@p!17H2oZk~X3KOWm5~SfYdzJ5d|HqeTT&|a@4UDe}s>QH#(*{m%+;f9E z@>AjXZ)ci1CvAI~j%nZ)k&8u)Xm zPb|lISLk#Z3lsU;`}!n=gD8(r@x{a}aC{#=JvNnPiTS$rPLK$V3W4}PB$!dZj&do@ z5XGN4midu;1+Adh4ZaNwh2`pn=)Q;Kohxf z-w^gx)|2iYo@Ygos1K6Vnzb|aV_@$|& zdd=I%&XkH#yL(#gx4u@ZcD3*vkh}U+KE(NxPkR=3z=1`l6FDfVbEfcUS#%<`< ziGrMp66wpLNAli|xI#*I9sByI9a_UO*q~lcJ6z1!@pt-lqTEINg?fV6!=NunL-YQ* zReto0+uc1i3+TN96ATOnCnOoB2k&UmfAIhbQu105Uh5xL-h*Pv!_^&nzD1fA{=O1~XOv(Jge!T^nG=ZW@4?$|9SULT~!`P|6 z%p7w^6idr&yFrV}4gq#;=yrT;#IN?*hycSL&bnbe%@p9%%|3k0=<+-9Jjzi!D=8x!+$G)eB~5~E}ftH|8RdH|5~Mkg7M1O zLz)+`!iU?>FUahJfAl+qk*V^%~=NH7`FA@%iCwtPtn5u)=vh6y?kVFGYSB zQ4=}k<(;Mz;?BZnK&_~ehw^(KpaeO_nwB7k;V;5xhN};t2RB}w0#OR#HCnlI#^zMs}uR+hqyLT}!Oteg7g|Fbi7*r8VwZwr1>dW-}bsup8ESx0~rmn_*@{8A;H7)S6 z`8+NAM%`^j;L24Z7yv~~9u?(>G-})9=rc54yph~E>-iPZrJLl{vlA~gK!;Kb^iA}Q&AsA_) zH+#@0dOjzJQSI=-Hj1h#5m5GjAuIfp?-e(JjOrz}F*t`dR zj5}2y(#8^I2e1$L3Wm5qNXY+O>|f6d)$tw5We3OipJxFKzg%nZ4y$2(z{gcilZbbX zsdNK}7{i4hZ~J(u+BBg8%r$7f%Tw{l+z_hcm7;P=-~j`u{!tC(Y4w3ul~ljtkR$H^ zew{0OdbTxJl@8qLQ_HC>A+EoNR@wpV+&w3Jsg3Z!)D&o|R(R)ZZ(l1cEl{6G39S*k zjiiVQ)0ir`$Ts4-cR#ifr*?pEeGP_{V=}Uo92{8OqWLAID92~l>%vXu&0(?t$9LP6kseT3Lz)P>7q$tW$0zw9C zOz+-VKJ?A&&#CvI#L40bO8`Kh!w1vdJ6JlxjFN*BB|c@_v)?DWq*3uTjKW}|$L?Nt zp{sSY<(5XDPK~%Mh|~~01XDgb4<;~~r^dcS(omwGBb*&{9|8nDAYNDK-+isrk*>xhs*OO%HXNJUCywzGWIlem z&icj5GefY9PH}$6X1SIy(#Edrsz*H@-zUkAX66ZAV^AIrwx)9YXHr{NjX9KI;@;Dp z?*!>kuba~ljkK21Jo8i3j)7lUG}VI~^I)T{2FUHEA0v6ccz%0Y)vSsNQM}OJ`9TmSCH>LtXJDoN8z%W^k~cq%G7h6_h0RAUu(U_K%<|hpPh|RlnettP7ai zp`kjU?spB7ziAoE-zk(#Co)1bgLCISrHX>dpu}_&=?(eR7^Qv=QoBMw{`R z@zH}T;SKJhvLW!6n0H^#h^G;pJKXzAtHd&E1(X5+>AD3-58YO?g-PrbtDPTbSB#WM zkukn_R7{y!3r{egPZE9)JT#_FkYgu%Hdah(4H~r{C-tae#x-gwv~+Jju;SO-DyRxX z+(IPJ3|?9p2%;EQi62K~MRwmKg;L8#=2nM#1Ujx1ey<)9Pp(`rG6Q*0^r0{k&Zb4^ z+$cYHIg4qaYVv*eG@%@$&OlC%a?0B{qfg0pYJRiJ3?|uLh~f^~N$2!e+*F%5hat(4 zlI7|O{k#ZL;DowYezMI+rvYhlT;!q<>YV@+*OlrrkjzA*h-LP~=VlG=6YAa)R?nB~ z0;_1mYB1;WQ>tItld48913$fDjzd{M58T?V<6m3`{UbX(R-6&FA73{Z%ra1$6@}oo zkme8+d0D1XG%Uh+j`Ax<(f(EviIcE_dQ&0!G>Q^HPA(#xNTldYI;f)f307>n$JJfs zv*sAW;2e5RN*ETSSNkI!gw$s_E?InYmT&_pxv&`*v3v+_Tc@A@FZ(ZX(!wNhM*cP} z2CY6(5!8!J+LMla*lcU|Df>4gA20kRV+XS=!z-$U-%KLn%jN0ag6R?ESa9*jIu^^_ zd!e*Y(JBV!P3OnyjQr@g(uLSbScx&od=dyok!q?w#{R-xFiqqd+*01pTrcP;8MWx~GRwceu{tE37fB%emZQvjkh3 z1%EUXjlys5>9(n>9`CX&wny~kVpL{e;Y^Gc5$4izXdsU3nje6LE;~KIG0hEebw0Pa zu0E~JfJRymQ*e` z_s}XL??ghn1~B&6Fm=bsBT>3XhQKqWb_E0<79`mHOk8liENsZ_dfqDw7yk)##))^i z?ia)Wjdm@}R+2gHPF+xO)K}!2;$c{&-r=M<ZIhn2NW-ch7sQ0yf-Iqh^Dlqhy1MXBD1Z!0iyA)a+NzdbxkF&M>sX3gp3o7CimE zAOo8}oO8JWr=q8XLjnA(Rvgz&vukFULvNVNp5`7Bx<z!Urw9|Cjru@^5IS*Xum|CV%=2Jz^5xeVa0Wu2p>&k z5E&9n`m88Ly9W;H1)YC>z#)Nkj1K7HyKRv6jMk$W^`?C&v((fXwF#QVo%-Zvf-al6 zReha0{ruG%uX=6iPTvNzkxwIou6m5xd~IVn4o?OGl0}^` zBUu@=Tgf$}Wg1bLrWjT-4N2P9pY4~aC#H%Pt{m69kiBemgl;5il9MYhZ}xO|^-sdZ@O ze#e5w4GXvZs*SjMdAOnwE4Aq35byjzh!KBIA50--3XPm#ooIvyS~3X~Ksh=d9IpJh0pvJiJ7l7UQBmSv!u(I|6jwP~^gw zqP4`q&darT2WvwR@fpS5*!Ue2vhFX=QCNQ?XHJP#f;CilV*V;4#X2A~69?F4^{UEv z_CvDeU^^cY;6Z&$LGGvK9P!OPW-LvxfC-L+e%4uH-AW&qSNJZ0^KsvNPYU*2U z3Ap08_h^#2#t%;%7DCe+Z*`2%f#?DqY;Q^$FV`CsWX}xvVpK?(pf=x2F&`vuM{SrA z8-)u#R*8m1e3=jpVO3 zAIk&k!2yLDs6fczFJ1e(hK>AHLT3#On0|UxaPv19?zI`#<5)_2lxl|ymepJEa~`?h zlvty^RGNEto;u|pUZz9SJWG~G=TPG_xX2L(+i-#Pw;g@@+Tn_;I(n=72iyn5c34;dJ(%`%@@Eq8}>H-qy4zeXI>#;)2NUlmN-t*ff#?im8(o^c?NQ0Z+HBRfdu2pVsz|8!|(KF@-A-lsJFK5)qIEq2 z6&^Z1x!mZ7b`|c|e8m)G2Lxw#SjAyFPBr4d8GRw75&I9?Du)4Exr=RRHF#z!sUtSE zZ3|Jn1l{4>^zrXph_b49hmrci~!*TXzjANLo-gMkttmm8(&4!t4V%0@Es+ zq?_eEys;;otOehioZa_-U)jCJerr|$4)NI*N!A^B)M;VuGWk2T;Z*zX3XxT=Li5!k zQdX6Hy#u|-r&u@ZM%|ZUn8}Te9aN^j1L;G%kN*K?!@g=rJ9MJV@QjPW_KQd+TA*C)E?>JSJtSU}lgxt- zh?k(8>nfGGM{Nl^3&xcIey&|tWlyCU+){*Hu708yLAf!Vor69~b^2aaMt5xb+_3?{ zOCb^%0&lpi{6)%NPPHO@Q*tl3UmN3&?nOQo?0q-1GaUuF8|(G_>9) z3%7SEHs4a0rn(;oYw}885mQ)${b}YN4!qB2CPhIrW*1bS%ypDOyi2*S!0k~vo~CB= zPl;y&R|sT=izHn)h$5e=a8Q(SP@{<1`W8c^7O2FrZkwD0_}6ZVs&FCsdlh~*C?N5@AfCC?9J zn**6cV^N~!1}t*>_~Ml55O8g+9~6HVJu)WZqHr@GSl{+};>tP^Rm(sFplooCvdQHf z1-d*=fYPDM3Ow2^opN0Bzd>ne7)X-S+O|p-<4U}IukZ1=ewDDvQ$Xg1;W5~8-<86Z z?V|5|D(^gSn}I}is+HEkO6}vLp-zgF<=Rdl*%v3bj0NSV*x^ynF>6nLV?O3+rWE3* zcEb7TbNT{?BN`Ox(RP+Gzmy1ZSP6fqjTnbX;UlJWd2N z5~*bCXzJNxoAZ|uBYkO{p2zm6ZI&7Lt8`HU_tRP4ygCLxu#p98(+A?@sGCK!aLRJI z#6+x@8`N_qF!Mtom>FIGneSKAY&!sdW7Psjva*H?fiI!QUC*?!v7ok27gfVx*0{FW zRHuM`SXf9QaEvPa@KE4Tp7)YXIQRQ122N4TDl#XmkQq?2)LHC|zdjBvWeX-_9rJ28 z%kleH_aE&~@_YyrO^ur-fZCN0k9gGq@r=;*S<@5okaO%8 zHM?O6!-^O7i3^FEH9+=SVI7RvP4B%2vBM``i{Bn3F?+a;kDb5cYyKFc4tDo_QPm16k+*KA23jbELODfRuv@$_6V4os)q^24d;OxfolisypB4~uz}Z4F zPaTS%n;iY_#Ol{gyO8jyo}dgc0kh=-A+|AhIC>;^d?M^&UtNuDgueI&2WMLJQ?f|;U zP{71`J3G6!QHA_IotVkAx1ks$SQ^hv{OCgE?k_y?M=gix{birZ`}Hq*P}U?Za$0BQ z=)tOE(}!`nKFi)nyq!y3Uc+egjgEJsl(1Fxj4BG-H5gH{wxjP-CtOpk{$zHyBY09S zW*!T@wh$iq;f(IN^Z9!MOx#ILe{4h<#Dc?qOO8S1;i=>e`J9#Gy)P)3YP3;~^BDv> z-lG3)hJ`Ds`<#t)cwOe-*lp3vXdN#|ZIbkiBCV1$tm@++R$Xv*5U3UVQ$e|(#Zo?~ zA$A{l8u~M0gkidSoNROcd7EA6Hizk7rc%rZGf-ko2H$Uaa2kMi=+6z-2D*uMW6u{dGOq9-j? zR0#<|?NVy*KBQR0%U;~$=7MW%eE99f=$(Zf5;lB|I}?yQ+y;~zQW%u4g@tHYsW+(? zR|>D=_y;heP?eK4QkXIcr}?MPj|iTcHPq5Q|M(T}X~V{DEarZ<2G-( zJi9E-GmIYKCV{r}lf8`OAausm-i)9hFbU@?@0=giT|%wSc$4;einb0QR|_6=DV+5H zWhr-42X-P2Lz=%3oo?)er($XFeFl@S)OS0tu{0JHArmwGW|mkLY^-j83`br)|Ld9| z5@jAdY|~O6uyxlLCnLUpW}F7akGtrmbP6ni(?5l>nTHu8yj+JP#F$R_9+4Zo^m5(Mnrh^CP`M zn~^@hYTkY*ux(pU(oPk?5wD+UYh+2h=-@IyJw)&M;A1I=hcZ%*>g|Z4>A+q9FoAB2!+)G1DeHr5@uxFK70}~Xx>hJzPzh%6QN;hRk^4Mc47m$Tl8uaWx^$tUg>2*v4?C{ z9h}lOzrm`>jo4qV?5%Fz8E|+UlvdMA+}3{vMYeJ#IBck^F6nQZR|g zO_GiK0~d>NB{!~xU8G6|fF+_C-O=C3=#(tXNUlE`#qN1s^CJkO(L|3e$YxS8YS_b$ z5)QXqf07+nPE<(ID_@_hldM`uxM&(AFyc^_WWaWu0)<=F3{B{k1CKuP?<``kP^6Op zq4KOIC5~lgFQS2nP`Ikg6V>g6f=49)7oB{3ANCM_+pg7M@vpuauzt*x#Eol}fqVOpid?FhyfnYPfikiB+O14TA_MQ{aG!TcTRsI9P8cZFrSeL?{Wpn3o zGe8r-5#zAesdxv86=70GX{t~`s~LT5g+2y~E>xRmekEHh?q^8y-kk6T?al=&pZPjE z?j%HK2O7sQdN49L!$B%p>_xWw z{p?uJPpx^-Av`{VGdrE z((xr^%NS_-S~kU_fzVXVR0r%JlM9QrJ8T3w-h^{;jv%SdWMo}hUzt{(orW=xvbk8? z?SiBeV*O=q5rFnv#X=)vv}-=fU~L?5`u%Vbt{CUiPY{R)v|39vf93fIgh6|KttLKo zISl0fV3`4qjUgQn=y&chIic?rUM+_}^GzIrJqH14pImHRP8wgb)CD3CkSS0HKux@J zuyMw3cB%LXuRGs+)~(!-BhFna!0UvV@^1F9Q)pz%YLN}O+-ggkg+U?8Ch?$(2WWPw zy6Uu6Gh{tQQb=xMDl=Z;1d&W}^HhpxD|ZOyyPD==_0`$7NpUpvs}SVgzL!c{JJ}3KL8gb%0iv&~ zT^F_jNxl7O|45rp*lD7Z3D8zlu~-!M)1iC4`(hV^^H?Aqrt|?TP-tcpVg#&2Dc$-A z@%KEFO4N@ANjuu(hF2TC%YLMLywuVL4*cAJszx3RNFh7y!>Y#?{T5qnC;NQbFlKJe zxWwokqCH|erH0{$`@^i#*iO^BhQyKO2TRbli@Q6mJ#}RXI9CwwL)J*v)i)gIPk1BA z{^KpXB)j{8F0~9|iE(g623``M-z|nNhr_G(iNZ}vFXJlMr|u!*8bfLpG~daw?2D|Y zirw6@*v0lzE%J9mn9btLA8Di0Js<@Ir->&OiE4!PA`*E~@`+&3TUx;{6!_}SbWL?Y z^UU=IwZRK}(m&SNP*UbE8lAq*!gJxprIa|k4i*i${uprw(cfQ{1OY=#Am2Sv)-d)^ z(MPA`XpxW55=7t?3WReYS{|88A{Vvob`Kc+6Jsf>4ThNBvlhF)uI<#9c`<;zeKCxH9VWyGkq-DJ)dA zaeaYbxMvkN`{?o1C#f03Vi?m4z_jE-;JlcV<;&enmHC_pK{7^K(polKOpR(u2!zj#7v zhxFv>Mgh7}InxbEr$GFp!Oz0^ z(KK^^ZD5T_cM1no_rCQXUBs%FXR986(|%@0_mAC~6&#-zd;IK4}kZvq6Lt=-75nlBqVT>JS{ zC5fhUh@ipj}Wf4Y?gb_LiNimI&em$aKh3FJmQ>x;Za3W~+Cf==%gW z8rhqAy|HsTSD#tyAEerr*4wUR+H9?u-d`z)IrFa|niK{fuwv{Cbt0vqUq&Bz$0J7;`p?eq8d@mL&aHWJJ_g8}Zw{ z8Lvz-^}+z*pfyny?%G5i+EJC}S4APFHi`JL>drg7;gt=%o+DrKUq3b~bC%Gzci74d ztqtlwW#cJion3{nEpMmss^d`G^v2CH4HhXV_8-Z;v$$zBk1Cx{JlK5T6yo=4($=6> zk&}6~I$Z)_ArzM?@A`g5T(YWnjnv;w`XoZDU`~ugNW(cf=7%HTbVkN#%IqY-!wO_u zc(D1=am-`54GVrkWI2=xdKPb<3Z~C4qtB{MXpi~$4Q`#vN10DgELvePrirxd=+0nD zbZ=AIFW6WQIdhXNi;}E0h?gFgRA?*_uAjty*Z%E_Ecsp` z#J`@ClyfLiD7%~lQ8V*7a<=I{ViSVrpOP(#CnAQI9I)i&O!2+AFP1ed!1bPlmRUa3 zv{P-|HVQG85^Hw{A^9yn7aDBJ630A3yEOu;vQ(@6Zf)d%V*cwD z{G&-@JTp`Yi=9_F^vF3kSW=n`c)(vxvK09}ZJh|k1p6*|x|-VvPOVl$F01G#Pshir zma);#$Rp9v?dt=j;~fIlM2Ss)-sofq4l6>qQ%KyOfdn;MxlfYY`oDzxL!}^!az51; zq26v)WJjHz(?>3k9(_VWRR$v>)K81=>&WX<;EkWLBE}N!aL8NZNwnIWmoKSmYKbp) z>SlOJ9>Bd-co4-H6{U}2%DJm^b>uFC@OHYK?_9-UXs+`jo_%mZ!eaz4JEQV*7Iym5 z*?U+WQXXs^NfJuOnmg?>6w9yHJq7Dk?I}#o=Dz8~x8t8(Dibw}7LMoSx7;$hI+gTrmAnF5qtxsmFJ~9VQC}qzxV0wKSDb&x-Jc`jnEO6N^_Ue z_U-l{I1qSugheW7!j)^ed1C;h!s|sgtY5!GiOK?N3+gt%0c7!`w?^qA_RqWd2dLqG zhV~-SZk_Tfd1G#{F43Y3Ag-8h{sou*ixgaf!bY{Z$`^y?{q?tV#x4X+Aq~bCiv9e_$-M1K2ajJ4qilk0`X@gEsa~L@*|W z-SfqJ;s;t5xG3NgVaCf%9#v2^4ODas5mb!7YeW5}AWjLU0$=*`_|cB7R3ldsbBj8j z>F*7W`$XzfHCY9r%9Wp3Ywr&&`v0gp2kyY4ZA-_tZQHhO+qP|+72CFL+fFLBok}{l zZ;$a_pWkrS+DmhO>nSCDcTsCMlwzeO-_R6z4C=QrP~?&r-mfs_{X^}aS%Y&-`KUXJ z>@iId@4fB1k2RsyQZ5xC7D7Mo2~9`dr-^_x zL89(nGtn^r)^1S{)Wrq!|9bLXgW`*#T%22CsO-w)7k<(DLV+LoV)~VKA>pqmSsufO zWdT>k|7*jOOv&j%w=8j_|C+fMVkP|L*9})dB)?Tl>cX9cr9=F@D2aF{b?2+ylj4i= z9vgt77l)tE%iUinS(men6)96XR>1tj1W-m#QjE(D+|Q?&+-7`}_gtE4g-7Wm$A(26 zM-1@u)1Ye;+!sW`#pAt?} ztuD&k@FEW)x3gkLRkpjguz$}oKJQw=KC_Ud?8Z<2Zpk<*s%tlq>~5^IKk`q{Pb3%A zXo|1a-6NsTA1}B@y7dT|>CeOgn~#RQqgK{Or%!Uc%n!}M;@HjO?3}a<7D{FV-ZU;? zTb}$=y0c&qv&N-LYBibAz!$a1fM5k)P}?V;{VFnFUkTBPuLU}1)ZH8obLtV}pf-L_ z%MJj58VZ1;bR9q#O*r~GZJzOh^0cHTiC@{16&p_JLS8xG8KweIB+?iNR{h^)-+zQz za1PE_x8?@_+3pdV%#lZ0H)FCPrOsfl&fNczJ|?mMg&Ifx`#fF)9a|0yFv%15cSa+C zdHHhWs0wIGo#pSD4$Bvxs_{!2M-P~%nq!n3S2a(Ra)|$XQf>3ezyiu{PtULCtbEp* z+bDRCoMFXGj!qkah%JQ5A=Clk3lUh4EdwyxyOJht-2&|VH0Po0M5{o1Km*YN>EInZ z(~zEt7bIkJU%3b9)_}&ab;nL6`4i z;i=XsP$Q#&`dwK+*(tWFGZX$VAB)CdA4`U8j6o;3^qOXzY~lPznWBTNopK zP;op9O)I7PW!k&?bA3DcN;}&>Oa;TK!vaEbxLQ*k#qPU)7j!Z{xQDMBt&r^k{?n{) zK!9jkO#DdjyN@b2V>Q_un3TjgcvDSDWdGPwI~v~p?va9WzE~Xzc~K`4{h^-c94~PU z5l?JR)zkY9F^ARpthjWKxb>HKtvrllTU7X18 z`;rM)-SKGchfR{gw{o|G*mKr4W!~?Ir=fDov0xh;DJYS2cAc$nU`1traFKFq(O77!$+hq)uE4i82#!*a@}C^rb)b>6)I`RM@6y`OvMPLPoH-@bZJbF zucITB4|$l-IJ=xHo^KPKPw7&{mUjVCpb6=;8-9NZ_1C3|U@-Fz^s8&iuz0;U<_yAP zkT_*P$)u~xV%Y`&PQ+tE;vHiwCDCNC0(k?ZR{QYr%C{%KrZH(zErj} zmfbVzO+6Z+KI;Ag8r&D|l43+HiqIY1zB{`Y=G+wezqCQjoaxnk-%?tL6rJ((TA`NTdh zWjyX+MEu^3*A_vKE4G~nW_Yo+@7{unUH{TfAx@&$se z{x@j_{$A=LIp(WRTtS-uCu;$4nsf?A|Fx{v8{cjokvX*10Pq(WtKJ9H=aRidkH#nW zs$u)$0WtD0fhbDcxir#P?>1O9BG%ve5HN{%V>%oos>AeX8Ub(s&m=2IsQPN$rzheg z!fcLbJq3vW+ro^yN@C{@I*6e6On$u4!9ZCfX1=ZyVXsAGaNQIc=F*;sp&ZBjF|S1Ur0L2aCZbUT+_tBH{FE%3A-;XQx%iUZ7f$(GPJ za``XnYJK-x1UI*xh0gw`H^f|Tv+o&)^?4sL@a~RZ_Yi2CVaETlYS}5z2Qq$j!WN4X zcT&5b&6E@bE& zsGH>t%C9LY^OI#xY{Zc9{rF6Bb(IF?;Y`k=pY>iv>!48KhsDSTf1b4^;E3a?Fnxml;q}NV7qk2lE0F=@x1B4pkEZt|7T3 zWoR&rsd=~VJ;Se*mAo_`RL1-%tPf1?TM>h^R7AW|z*&`S#ILtGH* zRI;cygR=1PddesneW};_n~)5xLM}laX4H+dwa=g zXxN#V&YOEx14+Vix7Eg3c$fpRRCWH%?d@w8H6$=N*@10PTd~tmuvkzaQoHLh)Z>lv zVf#MGQpXhd(fB;MvVd}@NH`qc1#ZPkWP=H~HRAg40nznl-FqTgTp<~h?#bMVB@sd# z(95z-T)h25y2+tl__mJ^3p?&vh>|_yMG+Xxf>WjT z69}`Seg~(ii9AF@xZa2Z`!q=ex^Ek#SH!9mB6gxKF4W)lZUPxEWIJdC%P~@8ih)Y3 z6#LL6ikXK%kHDNu*;~Oy8OH>ap4;**%^DeU1iji`^FTPSZdR z`csylJ#;BhbGXGZcZ6kvg(o;uo;EA-zF!PGPnGUX!AFj~iUR-GR`dgVD4K}D?A1+Q2-@Wa6bYjTf&=_d)d%G;-v57- zC%p-zLi52yWcBMqH#ubuX;aw>#zIQ~IWnK$HsQ*MS3NU^L})pYu6`&xgkOFDf^m<| z&Yj@y6*jKWa2ye7YH;v;GyIxCQ5{gNUy5+;$}7rjcn4@5>NicmvvQcmr>2UPw=Jdg{#dzaY!Hsh z%u-@DUSGq@=fGI_%2Dt*WgR)%pr6sXo<9`e9gy_2FK5T0%R^%_dF1opXxhoc{7Y~FdW@28MT%qcNN9S^$F zoC$3n-EXUcOdszaRsU?V4IC-k^pN& z=u!JNz|_^;e77-yc+n^cwexCVf&*7fso*E-=jT*KO_cc*q7S?+xAyNwogEj=vE*&<$oi? z@K^Ez<5+X_e995T5x*+0#hYQscI~eGZ&6b@UYv;ESj?*o>OHzM_E_Sr`Ods z6Z>w-g~`(mBUs=j2ph8J-iQpqkpLL?7>1@Anrr_FCudwxu4h%2rN)G}Uh4;d7(iIJ z^2`Rxe9~ic0aRdp$ofog6<=vJW0Fo50EENTvxkj)V*F=Mg{qH9GA&m4dk!5cmI}*Z zu(#o4pu%Dz9u0&wkz&yYKA)HOMLj8(!>|wtgmnX&6>7nE&g*}@7pfN`QA?hcXxJWo zW2{%8LaWpYclLL0Gd}0KL4Gld*>@p-^3+iN8~4GdWOcUFAhsNcZ0L$)uge8%>7mE2 zsS^A8oh#@*1_3SJJ`qDzWpu#jYPVs+E9Mx(N2FunpQ6IxU8ZQ`#ze|mTY|lCGe5+9 z7&U#;-Y^(~G`4X221|W&9XwTN%ONzVe5BdP|O%SqW) zMeNywzRzE-VRcoW5}87ZcW9=)ygs{>9UVaIfUd9p8@Tf)h@fgg8e=bb`5?Yce0%uG zCbdC3CJ4;~d9rl3DCds`hSipYZc?Pz$i!FW@jmo?s4u~w1?t_fnV&$WE}`17Ts_=v zV1h0tpAitm16fEn1|Az*>aL^-^BsbdV{ z$BW61ii-fAIp>eM3W;Q-sq7xAc%Jq3#&_I0bys6Dq%i<=rQt}4rW#Tn>5HAFMeMZ+ zNzC{gu?Qpt#6_SsMWZGj20yE(?|b*SV|VKMoy%cnXDMX%s6GS?9T-XD+ssFxetu_hSE z-x8Gycj-uT5%b1knFkrCqMV~`U_G+4Fb+zSL{xQ5A!>xu^>H zMn+*Y?U5lY=XeB=0w+0nf++H}y`b4KxyRJ1pN9oZAV*&FT99^02sI4Z#^W)e{YzVV zPSM9YiSHk7W7UX8TYuyJvf`h8pkv#N_=2TWLW~lUL+rgjH4)L7}|Ig-cHTy|c5S(T9-W@Q^W)dStDOnDUMs1y$ehL0 zAx=!BeXU@lcB+P@JBSurCyyqrcX5ZW%>`H0BQ;pK1x z;cm5prXyKIlME?(?(Z67uO{|jJ|g!mRpkEw34!)8W%9hYQ=v___!BYcbY!MFxz(}m ziT(Ut;El`~#S>Phf&sym`!!vn24S9On8LBfa0%2aIv?;13`xS?Xs~X zWu@r^;B!BrT9z)$g;mhY1XPiU*=bQ(SJK-V*{Ewh#I|iuW4d-(6(_v@U7Pz#J0N}@ zY)Yug%Znqh^@AAZim*^Ytoe(YwMlDG}TP$bk72>xFisZ=Ll159*mPWc^sNSeKYt^lUH*x8*+2ZB;%&(rg=lZAAx?6v{l3dPf2VFF}^0 zb|YRJE&lyw5k&dkO{{_-+Cg!Gpb;!JTcFahvuPXww4D+#MD|GgN}VP?T{(yXM4rR3 zO}YYbwfX^h4>Mlido-sP!s*-C?S-tX!}m!6Wk)b@Z*}K^PWPzflJ1neVB9A-l=tkT+LLT(F^C!zj} z*rvqktIa#@Je|mnzP$pO)!y_9(!6BQ*h&9P=ZB6a|Fpz<9*3rhncKSleFF^ErV6f{ zmNiKePavBjP-uh;gXh(_Zx@!x^TpUEBoqhW;lBs~5oPwvkIkhL&^Z9+<;*`YWkMin zyhL!T^mmdI@5rpMYyg#WKlPE>C*e-oa(~VksBKSxrT=uW4=0$BcXc!GYaJMSwFkqt zP`Dc5JfB61GOOOXV<4GK!}%x=eIzzvNq?3f;)dRPlS)Pv4)G+`H8IKCPYG!ki?S{ObV;Ny))wSK<^IT9?20BHhL zOMkIJOriC`S>+VC!W8tgWQxmKlCkFylJb9Y@d7LZEt8EghSnx^11(;gBn$c$rOQ)A z>Z#?CX4A^A30lMX__+G0px%;dHbWjqE&lkqKA1Q6rcjZHy>Ty?bYyO`#%p5|pR#Ui z@zcjS@lCcl(KxhnEP{P%9z!X>e<1=SJVwHIK<0N*PR^6?b6~$&d2q#FG+qjdk+=ja zGH?G1a@7OD|7)&&=lDMoWtRW^xzc*fw6(U&dYs8GWk*eSD7YEHPM*D>R*~S%+U<9h zjqvo9S50FGv`VoG(5V3c0mePLy%@}wpJmSJ)sSDWrTL-(e+#~_f*EeBF zNh@p8``P*txP^bRfue&tP6bT)rArQJ3n~_Nr?G^xctV`FxnY!#Qht{L=Yr~*s#uE% zQ)va7`}v4`1FpEDn3jk;6VgnXU8Zi}LOOmmZ35wdv`W~l^6ix{?)$IJR3%$#?c2YK+sPv`iWmXxjacPn;2#1576aPBMGn6;T6sr&P6AY zE}vaHi}Y%i6lT*&BAIOdIspvjj)^2akqOMSlWXe~9$1pbqg}zTI6;dRu-{UF(ml2N z_$hWBBgq{C>yHIyf1!x^G*;C;|WfRmHoY4K({uC`9jmNs12C2}X91_`%w(W`nu zHFrmMW;E+LFKrH|venA?9X#7cG3c9x)f%gByf20#p6%ST%cZ})HN(8IYvJOqd~ikp zV0_ibS+K%KhL(qbf}FxJzu9p<-U>E0H^>dZmXtGEQ8&5E)BTzxwOpuzfPW2+Hfc(< zGJjIas^TpbZwx*cHgu+AS-XA(vN(tdq8pZ|GcnA5n?$|uLX<@c1TLv{}BUv%X<7xT2o^_eE2NCr(d z?lJ@%Qe`X6rIKvfCLTeoR?xT_t!h(UFD9ApkpT&iMECrrZ(k#jtF(}+EdtGD)NwSm z(95;@JwkHxkK-^yQqmn}D^gp{s^93+&^ zD9~8 zf!Z7DRY^|V>+&DL0bBpX*?160OK6U>FX%|k|0`Fsy^JaX66i#*FRUmh4FU?KX01lu z2hU3g&Ioyo=KRM7h*Jd+=h~WUGqA}SLpTICrxBk^TIBh?=9#v=U+C9Bbf}j`%Thl zGeMH!BHy#ccJ4_!fU(#Y;B_(qdx6b5VxJ*DclN`Hn7Cvs`p>?@r3qLVKT78*zUraL zcYT?9`pTS$pc@iIpBmtd>2cKx^fEk0cbmG&e-1;X$Ssf50}{+AX%J*^}LYnWUK{L=M2`^${Ozv9Qfo3HG^8uc)MGoFAi5jJZyUSr2efAhrKS@oEOM~bi)lBH) z8f|u1=UFbP`jUFLBP=wcvpardVsYJEN^GS3UEuFPB}~(57OzDyUKgybYyPjT!Ys5Jh2Tb6}J8Zn4mi59@;1uPN$#$4!K9|lMwd==ixwk z0H+JKf*A~GRwizBRg_yxtrJbfz40>8D8PXtNb?Ex2nI~#{l>|#z8(9Y@Eak7r1xY- zS@?AaDXM>Ex4l!|&nSDr`1V@}xW$2X&rZE{f#p^nu_xzpu|{O0oB)p&0e?VoG41%M zR!9s_7{v;EjjW&V=g#gJuX=N_8NTj@qJD9e-4n592g@k)R$`jx=XYQ|<9*w_G>VmW zvR;8^=}L=n1KwIqB9_tfo&8z%nqfk-=wNChW|=yaA1Xx{s^I8%qH)sc+kE+E<&Lzu zehE-ags3)(yK!~I_?%Q-gPpGjBy@0_0bBnZXC%jEP z4FP#ZV*;3em5zo79j+LV!?NGio|)C)M=Xa}2)Rc&*Ypi34f=a0wSnBv;e3LI@U+wC z#fjSl$EH@{#Cl706^mV|e%5+=vck=yn()37T_5+h_UIyaP=V4B4}9WSLBl*#0Sjb% zf#HVoOsIWqQV#Klsf~+|-1?sJQ>j@|)Jps^SEiQR>f9^FsjoAe!V6AA0TL+RHrElt zG@Uj%mWsyQhFi-P0TYH1?oeIP&ZmkI;bq%XjRv6)I+|=8%u+)Ry$=EE%USU0Vi(~> zL6VJ@!|*{Tglj`HbmM8cfdt#~Hp}Yk02m2$SP363AT?n$5l@YqbEl)V_LPcjq~+Jj z-6j=mH63+H6ywF3usvlJ4AJGo;I1YJ?1hW#mgJ@zm1u>ljZPNie!?V7->N&gu@w-) z;J4W8BCa~HzpZ#csFhV_3C|~F+1>c030w_~KjafGjNK0@eHGraUB&j#AGhh%s^6le z9el?XJ08lh?US307>77nn}!}xY1jI*9+OU2;Dc}E!RsVZY7N9yk7Q?}RFGqKR$Mek zO8%}sl0(3nBO&$UC`DgjfBe;h=oQ6)E5{=m!c1E5lM^)^@t~115f~z?p~0-*ln4U+ zxcK-!h2n%rUzR}B4EYfDe#^OMd0f;ND9*GK9LM6ZLi|8&%#a9t;+>YoH{odrvk=25 z+0v*U5swD7cGn9kp_`t@bFky zcx(j6a)dmcNvDjXpa8y0y}ZQtZk0N^{fQ1b*1*DH(L`%iY%>0JIy(XV67e};M>UQc zr(j1|`*38oKsIwK-LDP1?^WphAPK(eObt%wtkHZZ=qgY%rL z&jWYMWL7^MO7sKXtYG2;$zL;NEny)RBpE0v9C!MW=S+Sr3U=%IW=|=dCq2Y8E{Op! zl5HUTQ%&I;8`r;DQz)!G)IM{C>SZbY#gYc;-gV)JydTzq&yC;1e*E)jrwX7FN3G&y zqZlP=$~l5B<+$lJJw|1a@4pVmJuaK8x3esl+>R?#LRR!>iCM&p)g);E9JUoshZyh? z3V+3%Jn{hQxPKR9hqTq}91hkf#-K2K)icjnVsh!eZZ~ICB|$wN*TSBK0(hj3r-g%B zAnhacp4>pwR6|$4{#$c#BCKX8iPoRC$0gh-sFhDb(K1_*|ET#6GUV(Sw))Cy*0_J% zp5#hFKg8wj;<3S~^P)(n{oE`Wt?;`q*k81`opB_S7NloZHoLd*jD>je3&|~tFA+)CbVuYdN+eNL zO1u()I%3ajMSb5b%UEm0+7+u?A>6NJYFoj()2V-I9ZWy`6K$(!q^+K|k4{d}&Ow)I z?Szxar<-yy3bbB?Ax4FAlR&fm&m*CoeUZ_T%A%AhSf`n7mZ;gXB)fZTw(+V8#s{hO zD$oH>-503=Y?CvWq2D9LuwrSF)tmY8E#FjMj4qf951`c#CpS0*%Y^PVK+IAs(-8migm8ohV(e0w69D%v3I*8ZRQ?5cCCl8l&HkUPQgEV% zw!@K5Fin$ZKzcALB8YJN92R}sQv$l$J~U4q7T0k^+BD$+V6KWxP4TG>F0t#$RhaE; zsci4=V~yH|ecg0%Nn{IF$fuC=OBy{NPFmpt85Q#E<@UVsT@RLOT9&D}anz_XFDKzL z*z&P^uqR?+fHhaScbAzyd%Jc5q!BL*fh~XEDmDsg)0&1=6A?BvQlx@b*S- zy-Ma6rREC7kU{1z_G-OGN2gMTjCj3>IIcT$rceVYRE%8x4Z8uk3bU^%c+$hAF;pB7 zU=h;-K@<5SqvV-x(s^kJL=m>~(^Rs|NjqRda2j_ibwsC;+!fL~7daMFEd**@K2Ok; z@8%RXB-B_m1iutn(>v*o&-{p@F+7x;KDoXGOcU$vQ%WkH!FTc+YHOgo_KJZp`||W= zoaf6efFrWG9kUZi^}ryJcdIXY8Q&ueeE0%xTVX(NKcrZCKBMQyw_R`gNv$ z(4971RNV`vVP7~DS|x`REU}b{SAh+Uu)a^IoSujtFa??hrLcT z`hB#lD&J@o5g7adon?od)@G26hJ1!&iKD-0*ZCp=9j(7iV!Y?@>)R*IZ8DwT;U$OJ z7fUeL(K3d6dDd$HMR`9;S^9+4Afsn7m7=^y&QG`xR-7Y4C5jt-FMR-I@sd=a_ZaKEUrd4hr&b1RBSec<<6Xa&gb(NbWrreoo5gs_ydb;w>qpKJ2>xGe*q!44 zw0orgY4^~ivHZbk9{KeN7}fi(xmx+2MiORQg0Hcok(|xD*sKYRdkn^<@(VcHPFeM6 z#yQ3*G{BQh@*>{|F7yi3;rTTArDT@0QmH<1hYHfCYCNeLRkIkomy8t?LJ=d2tqu2w zJ`Y{?l9ih|!)LUM(gHj}ETD*$3*4e=$9zsU=ezrjdZ*T@>>_PmrOwk1G!^YQF$^?S zrMD`>z-1wedd@koDyzE0K`3UAs1uU-C$yuNN-~YSaTnLlt>jO3bfsLSSYhy3iZ-#2U|70G2?{I{0=r|3C_}@iP&~sv zl>-$6veTxacYHJSpN3Nk5(EcM7*2e-Hh-E7Vr}Y3xJYHAY73V`{y7!O63xjzG)C@w zBsflxf@h(pVVpb3Fszn7MEZABLkNuubdSVkt&Ft4eo%qeL*$sIFUTqogVmgKt6X|| zl0W-yr1n=p7(%)@i@Jw-Kc$W+k&dJ)auJaX;C!&qtyZfC zgfi}(a-%&reUNS<_rR2Sg3N1EfPV5PX(=h-*>PRym9lQEtFNN8E4I!;G;Ib*mtXbf z;lH&!a+PN?E;5Y2SGAxe!@T0qv0NaZfCehpz63>LC4msczmtOgq&vgk4(|U=3MhVm zomM9`z16B@ByRdidsQ{rsrrd>2bQ!gyA#Q7a_O&JH9M|0d<*6lcoAbuZuBw&!4fnZWLx@ zteJo(3~d|t1OkwIqt1SaKs|4>9J;FQH|kV#AHT;jVhDNO=kzhZ&J!uabx?z*Nl&pL z3XM*Shlifoyaq=f%~cfbL@??q$VtnVpEGJ=kIZccE0de#u7`mSl&U%V5dD)lc0qjt zwN2{Jw+O@(0G&6E`N$&D-1`MA*kZK9jLn$t&E4L4P>%r&DR@TRD+@rxk>k5@+ytIf zD6&i5f!J!toZ#dWD_S8q?2pjhX9Gw3Z8-+y;?=x%AVIC5h}%67)6N>f`*TXu5BY2I z@L*DLb@y9VydB#6)F(?b?QbLT%au&+Ffz5$@>`Xg$D?iY&Y?1`7_J&M!l>C0$l z>9`LCf511DGgn=ybJfOFA}!i`Jgt#OBwIPUE&ha2b2@T`V2{HuCL97+RnVt-<{ehd zU&ncU7SJe`mY;SLrR0=4bPZ1nfAjEKOippu3$id5VWif9!LfxqYI*gEuFnwW)*j1T ze7d{sPZ?eYlCLWzJ2towZ!z=XX{Ik9f2Nf?N$4hNG zHv#PWkr)>MJhKlg{{lUwDBcCNeLvqZ5&GbK>MHB`=yIkQxA$<7434+8#Vl4yq)Lo9 zZ@1yT(>3uYA!e3vQ@N_*0yF}#jiF?lhfs{C-&u;J5Q7>nS#PZ}y2|Z9QyMf@Co@Z` zsM+;==gE2+UwW|<^SBu{>B$8FDmPJglBb-`pkYxOtXoaLA<&khi77k&Ik|3u@&O?P__FT?ooWRYpCps7TOOpLWF5*_8!^>QOxU&n6qlAkr6`gdN= z7zGXN!J*$>NlAx9#M<`h@-5bZLgDC9^xfm%G5O#TlZgi;EYtIqB08SpR9@y=aBs#00xdC}O9I7%wiwmUuer4>lz6M97mVLn=D&*e%H=HH< zsXTyzxa>%;0T@XBOZ0~282MJyfUKblx?Mw1@Mfq0?tm8;Wzsk(Y_kjWNT7#^c$ zfIok<;9I&=?(~Byhre^q%7C~4=Fvtu2pS3`Vu7IyN!zCZFN1`<&ye=sQZk^SLQwze zdj2v>66^=J+FKRvWDoV&2z{%noNS(O(A9^_uZafTemcnVbvQ)GN) zldanJp&v1uZbMifXu{PEJk|;%Uwv=%el9Q-I+?+Knwm_|k=^%tKSYL-)>m5vB&XjS zTCsL)fvw+qJ}oJ_Q1U2m=G_;DtI!FuA9qEbh~K0F_;6}~Cj@$_JY%AU7t=Iosh@dC zrRzzTsKrEv;kHzq@p7|lwjJ{hN?A7KT(i?9n*rgJ!ock2b#vZ4#iH#9ro<${kZx!j zZUqvTkZrS!KN2K-k!Ahuj%#hl(>RJSokw;TB_Db^q_>`fg1D<%2HYVEYrhTB)B1;; z@^Z$t1@M)$D&5vue;j2w>HOy(*_u{M{R0Zc z5_a=(~6DC;WmB)g+dJW%J9p%+UX zeeqW&V}K8pSnoAR-G0aWb)j(a=mOG^bOF6n>n5Yk7LVQ%e*s1>6F}rJYJN_ax+abs zRGZQuZ7PkD=`*B3kq}rQ#DAHl|GUJ2_;iG=cvdMCOzD5-{9i z=tv)ZBmFfI#O=M>wyoLilVp10aef-JH}N%ElpC56$J@0cQJ+?16;dmrb;&ue@6NsX zS7_;-Npc+tokU@5PMMoe)%x95Jsn20B&X1lvLbGQ%jJ99HU)X6Yy_y*6zPmWAQ|Cb zWEcd5&gSa=SJZ(O1W7XcG`U=|)X0pCi=Nt=+h8Bk#XfWmAmPX#a{_BFYx9hM{GvG$)BFZ zD1O`hFP$2~(Yt%dK=wA|&$f7OWc}~gWmydG& zzPWB~QK>k%iu@%nIqrUt`gsqe%mbB+xxJ^emReP_+pLsO4Wz4qZkn?0K#YsgDctnZ zw&n#Hp~t@H!>5f!u1g$9$ug;ItU_p?ry(yC!+>0PJV2XjN{9;Q)jOmfA8BAO>2dTc z;NPWTT_D7N3AK0sIcb7&fR^q5oh$?V?_Yuc6baBR>kf4VH898ca6enO{rz0Gr!mu( z$T z2k+lM3R3P4%z9Rg_H$rJ_r$~^r)0Otz;uy|%$$e~CFT}2;BbVmlinyME7_XY2r$lg z?2auK`(jJ%MQ0HrMX+C7SBF(QzQc}-awcV(BraKtfh-It z_uVrFe5Zo|@CLRh&%q(2B=%S~V!Nc$<(MlaZgD&-mBSr^{H@s9?97;3qYKm~;$tt+ zM2CZciNZ<@L}pTl2&FD*Bn^+No;XgjUqS@4RhH7NOW-JY{5<+;q5SBJ6LbA0Xa!qM zBO>MiHmfE<`>Ha4rzM+UJk5(sZO!~V1|xhe5ufsD+XIVcukh;;N*}_pR8i`-R?Gb*-%!;gsWJuN9px^m=S-dCGp)fmRhlF)*S?07_x5(C5tC#trJK;~=y zxzUS#hylQgz7l?3Y!N8sVyDCK!sOu;N9-S^x&pCy$3FVN{Qn?>q#9V$PvHTvwLz8Z z`eD~csjMOL=hw`h^JrdReJm?&JQi86W6G@aT4urUp%P6zE$QY=_ZOd5a_l z)by5WLtJg)oVgmp9niYjGD~l0q&MsvRS7xcgK^&f?%qg@BZ+c2#rEaZsmSeFRgot} z>tQ<1+9=1G#*3ez;PAN7%Z}yKm}k(b&Vs-c@F9efq)bP$M;T8j)6tluhaI(IztVL& zHu8`OG^mNKMHRCuQ801QiM7;7Jxp{Y>j2~X46I z$e9olFiLhgCvG*VLBzGJxwZuXl;^$L7&b;r68zywt8z20viE?4XW+VDxzr0UoZpul zfM#OKvB5FQcR#)XYF%~!CIHwaMBS(thE;P10r$u@w`%Zpu?$Sbs6%E4S@GL16ov&9 z+W7qjNiT2wM&Uc;Kbz;!->xlgO}(4)ZFo=hl{UQXmU!v#wWuzz{_3!)M4~t2e~~=T zpEfegpori52}23cy$+$q-K^qU?V?O7;|{2utmviRi`#H>qddjf*hAL{ z$3v%<4$~2pV(^dd-UOMZNxQRTO-a$1HC_1KT`de=*DM(~AVf#gs?KjZ31Vl>Z};V= zFQV&Z>1zj$6prcgnD*?KumcME!nVSY{Co*(kZL>`DIhHf@XK<4x!Evk<4-*r5n!Wrhh3Cdc10=9!SpH285hrwc~;O?g?(B z8jZxTBlKbx-GT7C7atoXjzDakIUMsMfR?fCB5zstj6T>CVGicw`MHPFY(daOBii#u zRD$`#Rn^(anap)XoVaHNmNM=m6^*FyVY+1kZKNlPDCTuq6R}P8S*arIlVyxk&;%ol zDqQaDS%Z>=gHW>nY{kV*!kyMcMS+c$kdaP|ybI6sVuaz2lp&{HzH~lM%?4vX7w{nUrNugE;L@=dBd^1E1hQp-S^jvdL)a5hi1I&SPLB*;Mr{wB7LAN-Pf%kIw&) z(;9&rUUB#nB1Et{*iKHvq|nBp#Dr!op~%I`-8xPgfXwT1EO+eB==hhJ9Zq-%$&EcE z(e+CmLZq>x9$SYBWaZ+Yu7qq{M#B}~ zj6B*-+&Ni;$=^R;Fxn*|9a-J&-jpC&VT~KfbrzrHi8qzkiHmPv0h+; z^}}4fn|WWlKE{mJ!~7Wv&XPPBKWhv(A9LczI9)jJ2KMF-GERr+2Gf%DMf@I6B={T% z@n2H?{{s5{e|5)i9@8l&L(!XF2wt>~op|cN2P)HfN36y-Iok5Gw8r)OzWawMMq2DQ znu`qjhfy~1vO`w4ez{T>3^7;M4E#$tm9B?L75L85A$cR(ke1DL96}~MG1=BA%Hx2k zfztuP(>LyrbP>HP|EQ#=6)emQ+O~H*!3c2%-e!Ft4vUaKGwlgN+o(gRi(~aI;)D~~ zb`L$#J-MAh6(3L$fg{EQ9Y{NVf@M;j*fB4JcXQGGbv>;c}Y=eet7YhIw4&?{0<0d{p&o1*hH$t;nm|pWQFx@?_II- zDu^dFmr5@SmM0ghl3X0XZBq%r0YiB7wbwtNausad55Nf1_gGvG2f?D^7H3a}b_|_+ z#CjqH5>WbVH}Z9};SKs`UMw-x4DD>EIL3?}@zc;&50a${T~}p3gOX-+6L5s-(8C9E z8in&-mAI<{&LvSc-vfsO(cL)e7SGpBz3ZyngNl!W3U>B_A*n5BG)P14)@H88EXO3H zHxf)@CipQs;{zX1vuT&UB&>j17z^STbEL5-1_24&raDHsq2U+&BD!E?CXO17MQce9 z5hca3bQK#xzYYdaBvu>6bEA|4^#>k z`iLraZA`}3qxmO{4@(-Zu52H99Wc$3Oib8-q7M2>{&jHPK;3(pyb0B`0K+@Jnm;eS zN13*fA1=g2AS_&UWE3_#J^@`8_b1VMsHErG6urY=*kS^ii-wTyBUrvsi6krpoEjPsk*KqPaWXFy z0yr-Hdi7?ukHgNV6zOs>xh@+MickD7+1DRy9U^obXk{MlC{Enm<56i{`*f3 zlh5m03>m;+9v6U1O)#G4H)W6O2lCMg5MRYUlWC9g$0y0oKdL$s`MD;!UVw*n#kNHN zPB^h;hyesbHK{VJTiUL-=)&e|&hyRoRI;4x_9xJ8GF`G$P@;cnUC%qlmy1QMz91UxOMO%AT7;czI%0=s5Sn$o&A6AgG*13}g|4(CI9TjJ? z^gV;SOMu`OAP|CvAi+Jj1b24}E`tYm4>q{FL-62(y9U=lAjlwjXE%G#-uvzS&bj~B zQ>UJ;>XPp2`n3t~_XFym3~tEU5{ckJHg5io1`;zv(l~Z6h_-& zJ_yy=0bb6Eb0^{%HJ^a<Aof!O@zfTRb_Ef~v#3Jq=Z1Hy=H=-^ zkkpYy)-I#gIF;<79A%60RccX`EO^)@85G2oQB<4%gx3H3`kAVmya6JZ{Tb^UFjl#E z-YK%59q|)-l_M)zgEdV67yjFJu{)0X*kOTbL;6>}t_M#=GZ%RcL5 z&&rftmunNZ&ucFtq0cjh#2JlBtyVxqGvpOk**>}yn3lt4Pu zI-h3_uA0^!Rk?cpe4m3ca0N@4tKZdI>5!oNsGdUS#*>Vo9~`7#`;f9&UL3Q<&y-_V zP&;5yEEZVOKkbJQ*GR#|dVTi{!Vv|@QC}Lkg4P=ojC3*j#1Mr$|JZXc-zGg*hZr0m zlaUZ!F%E6gYefbLTs%6*;mp@|`E=!}q%|T+lseN;zPDWUCmB#EaQ*B@iPFbrHiRSo zm}dNHV9k!N_coYSRdcnnBMz?vagqY5MoRxuSv_pvkP2L~UGa*8CDe((uvCizvD%s* zj+)VOAnTR)^H+iIiMFCrm+4^KZgDQ*XUVD7!tYgI?ahvM#gjeLloILYx6IBo(T=fLp zisIG2#rA^d;+cTu;kY1FslUP$tjvPt;TpeV)PGi5kY{-KVZ8&`1#xt3mxI-ei>XVZ z56hmNcEy<-2Yt=;JpxART17j@>YgG$Z}VF7lZezX>Xxp)+%fp5(gECop&`S*V;)G$ zhP#*CIQL%Ur5(Ct5F^(~A@nU-Q~Tc0XOn5WYy7o4}mNqo?emzVoy2t`tI-5rqN zZz@q*f5}o6VlHInGkj!0Ps4FXLVP=bi17}a`-|ib5N@LdxA0s1Xh*{$TZ=B$;Zm(UDFnT)~LEJp?`{XhS(ay%>z7bCfs? zVfs1W=Jn-^o3%{2$eE1~oX>efb#3W9-jvJ~p{*LVu-=s!cE^SBNBr*^)KvBDW>T4? zI8bqA)dKMIF;oO+M|&G`4(h1^rZ%g*`qk}LEnLI(GJRE{Us<`0xHJO?$jK=ToLQ-N z*sqKjCSgE(UqGl%f7t}AHbZk9{%sP-#+mJQ2BW#g%&r6ykBOawxY_fG=fE>$P}>@- zRBZOcl-+WNb)1{SxdlHt@4k!M#X;J@ZVq?WjL}9tvW+J(#!t)w;;a^7hckVfAtF*F zQg$Kp(CmhWL(t+w=pFD&^mqU;wS<&l*K4p6@%|ozMYk;0THc?}gl)%)7@8{b*|l{O*v!EmI5(qxJ-Zn(?<)|8+n4@A3vT8LGo6t)sJ&95^*2^9kef*kyu%WR^AmR=C2(i>e-os=D0G%evG_2{^_xu`=44j#1uLggx5gA|pH|$iz&W z0$nQ!S29>DxF2QEtZg-98`QM9d9`Gq(!47X48)z!crBBke^Daua++lz@GLyC!y#n& zbsk(R_Ct~7-Ao;Ep97e2d?X-|z?LV-i#Is7?WJ#Jm{A8B;#VZxmA}Yo)2BFW4b5gN`H&}Sr=Ap?yRyobvp^#iT zs2;4m!1@gncb4`A&`(O#Jh$xrM;OAb=bP1H0w-rOw`F;rq#zeQ4KmltS<|Jb^)#lD zj1r(i2#{teYN3k*%@-W8MJ@Tby~u;Q`(E-;4yWK_7SSypfiUSlKi}EjlNMZaGHg7B zlU`4@U`C#y?^u6lTcQ{W2i5NAZeX0U@F+YxlE7D-Kn+|91;aG6E;K3!i3rol#6q$? zdDPCN%HaIS)N*Un^)*}K^!m_8Vd}{YMO8ua(J^fX`xv{STL+oFNFqxQ-%*^SQT%ZV zu6qSrln$U+1fA-l`{(=KHRGsYmT@LtPP6&C`ddfe*)`$lajb?S_rCit`162dcQb}pR`_! zK30H+K+@?_n9S7JY5!7t`EDjt`GK4~%0VJb5Bt7Jm@U#2=;#qWjz~#7zlTU?(n&g= zX&;=b1lJDQ0H?dC@_tI?0oc~AKrDj4LPaU@mtK+kSOqft08*9+xkL=NZhxoB=-2zG zy-lkMu!Q`2c>)PzOZjxfe$rn1?one&hu$+k6uGU-_ zK)JxVbi|XV8>eYBy-7VVX9pdMYG?Yn-L};b7MMtpWnzPd`{_D6px+-duJHQx_?PGF z{MgbIDyOXK+H&v9F39`*p{4gTFG|FPf>1k+UF4qbT3e>$J{vr}@Gi`7#x`pVv=puU z!aE*Mo8>Qx0Bf0k8DKDnfUNg}T2Q37)3HiRu}_=1HE(;odz%C8xiQFHU!4qW1_OSTqllMY>*iBw3@?0DE!ASY&OOo&v(oU380!0mEI zG#SEE}Dmx zKdx(YRW}%BFi_ugeE5mVman6BIg#GpeU*S*ycPk#%NT50E^OnTdjuw*=hF2nn80O7 zk%a+GJ8aBFdo6_%sg0rK_INCutBvI-k+=>{!74{z2WvCwSI+Is%uECGg?fG8kOJ3W z16HwuKabH5xDDY=+y#5N7Pf1X4+W zRqgsa-bFT4-B>@(PAgO=phvl8%?<=o?dF#65DFN}ccxoUePiEp5OV$m@Ll>wK97>M z?^!(6r~k86SX5Um=|MUn>8A8Va`Ygmr~&IS_<=cn7PDmSWx7`=QV9dm8~U>yIlP`I zh$0Rtf^WF4Oy8ReF#Ph~{Sgf#sccndUm zv%_zoL7WE=cs(vd-wq!>f`;br86jsE`m*n4nfDGCexB|^u(usTl;1&>? z$luls2VltmZO-P-GrX`Q*vx)$H9zB9;fL?>w3st^n1k8=2o;8eEm?+OF%A%#&0i)B z%Qny)%l|-{huvKKY19{Aa7_3~$@pl;;Ljc-SvuTtCHyz|}cp+TJu- z?VCL9<3S-T&C%~`)4<*Hs~^)du7Lv>kwo9>!uf4akmSNJ3R8CI8=_k-&QaR$Qf#B{ zl2ceNLOI2gRVO<9XnoRSQvtL2pKFjlKT~-ODMlAm30cTS3Ulvr8vSNd`(f0Skjw*T z&yktl|M;ULZ*$!!d`OQo~X@wic2_o&ak zN^!ryQfYp=+j%kd^10p=A6{Mz&gZSRlE)K@K>Srh5djj_q z(dBHE-b>z)cWei<(ST!sYWWJjX0H z5!WB-5jo%B*dKeH@2Xywp3)eV}rCnpg2D~SYizz`B zqjmo9!G=-#*CWNjjedfIsCyr=M<1;k$!8%UZR^6O2cuJl-eFOXg1sz2o!|kI=L-V3 zR=4Z`CL7#=bG4akOht#OfJe?~>=W)XL;Wp*jP}>&r98vEsi_NZ6ly8CY}d3Ps~Rol zn&G5jgXEv-aw(D3DeXcCKXCWwfcOi@xJG{4j0e(WnUq&x*b$^%qW_VgGJ_eNW@?XAK9`z`nHF)z zD2Vx>krS~1=Yk<6R8zjVqMZ6cP_O`X?CxnIjbKxyMW2Q$b%gvouA8L~b}~+3yf8bz zsa+LhU?l$h2fFvCdkvHx8n2pKQnNJLA1eKN+7d&2}sr`YI*vAAt)5SBau|K)fTxL5? z4}Mx9K1tdKlGtHZzf!JOmsUoc2>^VsHp=afhQ9SL0qfLI)7x%`-90ERw^~1ATHPDE zkMsnX8kx=KRC2!(uX*V-t8BPcALiF>)VTHYr7Z{UAhTTfcWZrB5&YoF8CSVbVW=Uv z?t25!rji_9YR4c%QSMh0hS};#)q76zQ($|?3mlu{l_=vaXsf(qRkaS*f;jKkz5q+Dx5&_Osp3bG|px6*i1eJTvTSPY(oxNel%DCu9NMi~N^g__#`3)>FoJP8-=zQyejf zevmDei~M3PYV2jd&!^fz-qk7hAo`xM>JsB^TkNpo@cXx{TrMQ%t zUKJIu%G_7y7Td!)^c#W1;^cQ~3NSWEmQd=COvI?L9*tPWpmgyXS|Q_zGF+!auvU!c56*NU^JQYc?(5m zT5`SO?IRcUYBc9fk5^4fHe0=(GWJazct@HEl2|)vosF?qZ3PT^y%dDb|F=H= zKOtIFImJf_ELsafH-pjsbpr6O10Pe=T-wuby0&upZZ=bpoD6QrT%AvVvW$GYNNfvI;F-3>mOGOslccdYr>dTioXil!WaBm#7xdF{oH+!ljm6uquKnYs~zXKv|f- zCOx0S#({1aH7#OeN-9E83lBl{vk1;4r+yFw4`P00k{=J?;H$QbUtQ3W)@3wr^7@D2ap_*Q{1>i#J#hS~XO@wvwm zcg(g7)&DE;>i@>M4upvaqlCd|0$^JBSExWWC&x3EJ1z_bSiR5qVNn$M4X3B7No;#V z|5xrG>w)s M;6)%1at?z3KZ%biF8}}l literal 0 HcmV?d00001 diff --git a/protocol-designer/src/assets/images/onboarding_animation_4.webm b/protocol-designer/src/assets/images/onboarding_animation_4.webm new file mode 100644 index 0000000000000000000000000000000000000000..d59f86faf450dd0f39c6e81ed881ff40a54b36a0 GIT binary patch literal 91617 zcmcG#Q5=liQNWUgEt1WYa475g8=QSSQB84PTief__I zfPggqC*J=b_CKr|sn!~T5rhO}17c+i-OSh+=os1P7#SGU1^-Vh5*Z;-^ZdUo{)hf- z?f*kgbS)6rc_g9{%fzKuk6j=q5QwNHC)m}_Bq9(fEL2TjO@T8w5SaKVI0VFd+-g6q zJ_z`_GYAwdD-cAvKM+{8K18)65cq%AKp@50xxlzTzK;NBGo z1QZY?87u^ZSI?*h#zBIEA%zH@;6L-x8+_`%?)|hEe{286KJxnW`it5GpsYT`XycV_RtvH#LQnvL}7H!)Lr z{p%JksZJ~)EeCo#Ei0$)f78_*^$k9ID_%UYBUK+%L8wgvVKp=Uej zL|h^62AV{kGI{=i*Ic_L7k8a0O8#m^)%aN;vT5G`Es{EQ`1bW=7>kwGUeXoX$OQL0 ztOo;(luw*NJ%A@%TA=>h5vkss$z|Q^`4sd?o{a!0mm{^oU7JTz%V|)EB$@_`A@ze= zHP@omJpFPU(FI!8gJS_*b~23j954MlyX6pWY5!>iTldbIt#jIwR?8kEaumg3Yk^r;5x$9H zCANB_1xi0Mji}1<(6P(_9}7NJecXO(ypzJQ(=!no4?O)ID_(O8zq|RO8gadN`haF` zD^^Gu96zA?Yd`QB`*rY8X9(2}(4erueFv=%I23DN5K9?8=+<{3o}6`w047m{y{PhI zmqh-fpJM?GKcZ;%q#+@H5hl{xGl`jgqL|A6W1#tT9bIiqus0yeO(4|QjHAOR=dQau zBdB8hGCCp~BS1?MAyONC_w)M48UVoE?Zhv2_tW0`%DNKF1V%QiVmhFkYP;}uB`noh z^_4UpHt#HdCX;F=KMS@(h^pG7{wa?TB>EMkWmZ(kI7hbj3sW~dsdjgEinX3{)K|S0 zu*4QAu%%QV2N))Fx%h*0mjPHvDwWDPzHBPk(6&f~*@-*-y<^ZSJ>tC3MR`yz5AQDB zX9l?F83ETwA0^e zwdEs{z5pA-2?C7qD>Mc1;`;AoxC43KL_8d=kinH+%_fO#4SOge`&_;Cr;v{g7~eo3 zc%~&hZVk8qR)mNqKo9T_+w3zcm$2R(O74|A;FqOqfjN|gD+R%p+cdvRT~qiMJYwbu z@jk(CR^GRe&oW&n`sj3RflkbzO=Wn^TJvE==f@hv@Ke8U#575T5s64^@M1mWqQpB!m{R2d>)Vd2NkiTNtcDHWpo^n@)+1HZGcb zCIAjTN$KUIJZn86Wur<+oqWMU+idirl83I%Z-CF^<7i_%53e)1@OF6COS0e0gtX5_ zi;A#cY8YP{rkkqdMozq9>(V-8Y|Rg$H=-ZKDC>g~No(!|OxENCr8Lw}sU z{riV<^h5JO()b>R@#jrTS_)Oxie@QER%V8$EAq|`F873sh~e8uSV19&Hx#; zu2{d@U~bC-spUA3 z$r)8yiEXkTsR3x2teGdhm*8)9>A0NMNv3VefTD3vez^ciDTcpa(xhbDQ6+ie(@UFu z81@Bn+jZ!+^i&E!dlygUyG%!lS@^x2`PyX=X@LIrG&mO(YOb!|=QEl)KIG2ytCT4A ziWO52W}vEJpw`QNom1QJpP-)2YLf4%3aC)LYV*5$LPSc+a;p)>sHHRV9$2N% zluZ@qjj5%4+<*rHLHjfO^;H+FxRGXidzn{Lg==p|)y#LRk&>_mUo^pU)4VC*rw-Xr zW7FKHv{(%?~fY&a!IM++@v=8yM{vZ}7 z)4ly2bx&xw@zMuj4piRNSoPR>hG5$X)5(QozSc6FOeys{<%h}D7(31AU}mC zgUl)N{bpM&zysgmHvPfal`ldskP%OJn$4+BJkqdsyDlJ#@vLa{A#^t^9qvZy zDN!lLwb*S24-!(iWM0Rw`^JTzZ?t%Z zi(Di$(q+v{rXmBqQ4A~QJtLc!)FRbx>h28gdEQKPS#F#x~Qx(}oUWv!LUFMMWCcV_Sl5&it z4QDY0e;EUTgGCXs8V1-?azIF&C@7-Yd+IJoQ2dh& z%!c*#BGu7{{yU!%RPIQ$VU^<9G3d0TV5((YfXtE;e&ywEh!Tzw_Cmuf0hOCkSU^e- ziu=u&P!(-L)k;TOM3Bb~HttfwD=B|pZ#&h*%-hvuTeAw}grHt6`y-813Qz0ni=rhQl)|Y#oV}BiTIfl)tYI5UfShDWF|(T zeZrRoit!ieL>aI+QB<-^kz}&|7q!(Cb=MFz%(%Nu6z-rq#ofI)(H;&C?br2c)*15J z$!(Wz;sh-&lqN6n6#XiZHnKw@ip@))5FGK#+Hfpw3!>H7s?Ti`5~vgtGc%N(m=kws z*thG&jsz7G_v?WGOand5z8#NyQdC#L+{g^0ZFNZX>ELrt#Jgs?LVQ9)r%%J4?RkhR zl?MnX^=?M;~-y{enQ(%0+JV7MzjrwH(4jGKvzirOxrrcxH zYg`SuDy!9QEf0rH%c@VCUB)X--Ja*4U-v)pWg=)%;QR8lge$bzsRi!>dNfH_X;*{J zyX_T6e>K9aS$w&MNyj-;cp7O#mTBO~cdTo<950eOQrMFIK;*(yPc}swQgbAnLEQ1+ zBZ-Caqu3H>6!W31gH8ajDJPwXM23q=`iuFfeA;v4lhYA^ln;W}e={;Kom|cn!B=!( zg$ml=dnFAKJ{?Z6&bknyDHWB=nZK2EL`vAa7()Wut_Wj z9ccO8(0Spl7EloJHawe9U+q|wv)zF~Ghpg4^a<25Lo?+np8pYu5rF*aC-grZKR_XC zs$5hO1TTQhA|8s?Vx&h!g&A38MDm6wr^vrs}8z5 z-|kq}7$L?}#Zu(1Ro&}l`>x+^4t0)LS}U4?+-lHe)2Z`nL9quG^u)LJ4!>7{?S9u4 z5m|n~BVM;77c0gS<`W)b;QP-GKP>;f@EdwFv;H&ThNo(S`Din3Y5#T!-T~#FR@CpNJ=O^<;Tn<~@lQwH$IBg$T^k12 zFtNiNnr1*FIFQkZTM*M;r5ns*j9p*{t3bAdaxn1F>ePw0XXyQo%69hc(FLG zU8R<9;QFo7jo?$!u_(%4sPYWXIp5eHb;;+;Nqe=-R&C0lW8C@laQsxOhIc=fzO`Ny zXv~uHGO7aB$6I|M#5N^;YJDnY2W5TCf53T?rn{)oo`&oNwefi|4WPP^@&b5@@e z6O!lam6G}P;E8mCVV0TXwqsV0VG$=%!*hr zPH&ljN`2`vDasyDC`mZWrtYW=&l4CxLm+1j(mO)$c8CeN?#<{zJo zOFi!mF$8c@r1u7mbjWm4pE@s}E!LM1c;G9ADEMoRZw&hY0uk;lH$3@x}Y&S;7HIL^}5_&%(@qxlyK|r%lP$R z!;IvBG*~mYPya~l+kBuuu{$^lCWAK3 zA(HKkUKe3N^E^dt??VU|lX$VE+FCH%g7zT8^ftiQN1sc`PbYy___Az|%jMKUeahvf4P}Es)nS5c` z+duu8l8-Aj;C;Ix>X{0FcUK5{O8BeCV-xPOGz`uzG_vH39ncG`Pi=mNi~Fg@JBkp} z6F5!)1A07qDO5G%YMAt z&t?2@(33T2h~~;M_ud4LtO}FOcjvp*I||g_BaPWPcnxrdfjfSEH5Dcx{*1o|SZ5`E zyIgNe>UM|P;fnEagr|ah%U|fy11RlBCGwJ%FK5_^ULjkfPeL>(M5#S#z}zp);#bYr z>kM$^`4%6BbYpapK)FsQVqzmC&R?mS=k>I_s-%)w$DB)I3!i_^bH3Cn6tpN7^gOpD zEG6IU1Cqer7wL<|*e+cRHX0o^$*NRUgs)Nr7-^IK(9r+w054*?0U9oK^B!n;D6uow zdIt>>(~pL8mDx=*A9=YldI9-JOA~K)a$gf1f`iaQO6xXQ6eGnF-#x>?57mOLKpHG?R{#Fwo8Si z|58F^VN~4FgyqRv;1i)?MZAok#==3V_&o%3G=FW1X`!UVC{okItF)26$GneRXdHI5 zeaeXU(ytGmSIcZI#N{V3#ww!YcD=|JUrF0J2Ordmc2MPFt9R}l6G(Ouil>vVUu77P zzqMKWa9MAGiU*Ez^B4_o=S+QkDh@9)d^&}8-isH^c9W~c zjRl0_Dc(oANQ2{$^)ndP`|hW&{hcC9ZO@YWgWePKi{M>}Ed8Pv3`6ERN+6_)E(Rkv zuPmY$K~|JAGJb@Zhi^pk^tS)-x94i>52oZ3UngShl(4H+H_h#_)}YG+0&o77N`{;g zn)C+7Df0mTA0%5<;;|Enj9F(d_IW2vF)JjyJhpFP@aaD42EeRcnY})x=e&WL@rEE~ z?x9CVFn7f?0t+@rjsHWF1C#%x=kzRs;{N2`-Y?FjIt-yaJ#U`sgi?0p2!Zkbho!S7 z!srv5p6X1CC{tLnCR4fD17iV~cay7RNkV2dNauCVfz#HmIJ5Yt3^jE}J`;}zRh`0K zXD+{zgSdI0H7u#|S~(Aex&NGIyo#&tH>Shi2lAtC3-n=Y?SX+O-iY+166fks98gm0 z{-OgaR)Glp7){vWfv72;+6f1wjW{;o9cN@a!u(j;=dP*}X~0R4y6J)v-Rt5lyhP8rl#SiPf#y03d{#_jJNbcN0_ zvh2Mu_?uywiCn@ZYPoN(kr(P#@$B2edB~HvSRQU`8DD@E`DmBPO;a92U#UCq_Ddpx zCRaVEIu`D|(P|3R-qYvrtHBeMimau2%ejDrMKP!x?&9*sgWm?DxUegm&U$MGQ34En z;XOpp@4p$!q+PZ&nNNW3>O{DZ`u!iW+?ujx66l_$li~!@I!r_0~9nQrSr({>Yn$P(QR`q4! z;ie|foAE!`i(Ob$R^K(6H&rKo>cfV1eyvRG-RzJLYaKmp=`$bD(L9ux4N`>A7Nrw^ ze8cg<99UOH#l$5yt?apaD3fA|y7j6UV ze%V0jX2wGA8d{llJ3HN4?qW8%12PrM2cZ>3_U1 z&o2bg`K!MnDuB4{MZGQ5YWJqCbB(?LM)!W1Gny;6H#Af?ZT=lCW|qhtz(x06sx6zi zq?#MuR)b5fuXPwH$&S*p2H3D)8Ixc-o-+i(5rQE>WT6ga*4k~*y%6~ThwI_W(L(Qs zFd`qvc{ zxJ2~jX^s}|MMW5z?nhHjW+qaMRcvizjIj4!732J)EhI~PoNENC!HtpxQ5VR!N-B9R zx2}vRPax2y9Cz`DIJh4KXDT=@-Yuu1%OMLNZpd3F*D*`P#Azy~`03CktW!)3f}`j; z_-S`xgl+57|3tx4j86+uNMm+htTB!x=<|&e#9(WIQ71SqfmGzA;`YuJQV}{BAXDNY zx={cdn;9JF<~?W8FFO_*KGzzbs{1D}8XM=+c44&~#<5K2#w-tt#M1-l5}E+5C!sZ2 zCAe@4C9mZZpKFfH)|0HmH?-pY9dXR9wt_@DDFh~NLRD~ide4vN=U+sH#Y!-6*AGR% zW`5TViPvq2lzc@1kJ4V}nmM@A;^xwc9evPnKk_R}4lj5nVZ!x69GVfrI`)2i1?C&! zxX1jc&b+)6W)(9iAO43lmb2m{17~=W_Gn>C!m(>^D$)jdNSOcEEvP+u6p(0UH4zVd zMGPFx*@e^E1n!h;L+!Ten8fvhAU}`aP9iD$w3orj%TMj4ABG@B?RC%cmI1h{%Gt|e z-brYno%``8ERm*aUtW#SHY>6}!9%L9+2*uD_=we5Se7!`N&woRwmC zsY*+~2c0&!FQ~L@Dosk(yAzr?8JbR!cA&w4y7mO&JgJN%9=C4AofN|-Q*nTCUd0be zL6q=oZAPe6`R%0cvXzfR4y#dgeGsDh?L6wXPla58s7gJgC10|gZCw1f4h&x)S`Wmw zz*R8&waX!#&aztZ5w*8IDigs#a5wfJ#=n`xP-M`9zG~Q)IVyL)hErjuh2fIC@-cMU z8^(bLFdF;NRjj&*RPBSk>W!QG?6@!EUN_Dk8)PCSN%ayJaL*8EqUz_;m0sqUP~o*G zzQ&5vW^4qGq$G)Jr>mH{Qt&QEnMzYB9? zua(n{=i*qe(kS|GK6k}vZv*TX8j-T8#y=phjStz!N?8VoY(QAK5=*_WM501z?fR4i z4^@YJB7-m4|0cq^v7j8rlJCU=d@0{|Hh#fNfla4`_uXk2Z9)$4+GZlE=HHw5t#A{C z^ykh;^*o)ykx~OYuVZ^SdYvY)-r8W^?gM-G?k$u3&giKeP2poG4eDfYwU&b_VH zFy(^S4AxDOCUuxl)tBUh(P_Td)qcyfaCG(Md+(EtgW3+D7A_5h6GLh57H?!wP$f(|nyrbTNmgLhzhni4PmzDQ3q@ zzkyRPMsssRr;Ez-Y%BHf7VYTnDQMC!2bM{&M)y~%K8T;qIQKDhGznFb}Gh`k|Hq}KL^EjJuN6=f)h=H zpg;-8Es9Ll0o_qNcb^fFAh4KxX^XG~l;Ze=v>_E***wK{HN2Cm745YZ$M~p!ayfS+ z7z$F?EC(!Ymdxn|yI2J^=KI_IQ)Z^6Os?A$FZIbh8@SxO zwqL!sOcdf;NnWSI9n`v)I}f}eJ*!QH276$~PCyVA=(;xBJjh{JxwwPEdS=iO&DN_J z>O}Znx6J($)JXZZk31on40lb0j9iM5AO(rHNhseT#hT-^pgP(l zy-Qo0b8IUdXKG}#nsu^=59o+ba?Sb+gwT$qGx!N_RUYST)RyAMwEt)zpKGQEKbV^s zss0F;Y;)MNPh5%Z@%EjCm19bYUg80gZ{3YIJ_7ChEni+OSwG^8M+nlg@!lOniHnk~ zH2%b0Pz=_lU){Db4)kRh?!w*}EPZBCvdE&s416o|z)5cw3>{-F>d%f^QY<(fN^l#-VlcCVIp^L) zVa+Jd8;+d={lWCH?Qb6RG>lq!p6r5-O#*x%V48}B0MVDD3zO1qNsXuyQb1*m%1)VF zvgSAXb4Kh*VI7FK>c0Z4ztB)Wlx-knGpm&sgp^HQ+Wpycju`bb zNn+Q3)~n4ljIL2ywgzEc+qtYjrpVA3XsS1l79aUH(_(l^i`v7s-Mwtr!YAG9O z1o(3%*9q4tDq8=M`KCIMJL%sfiA2mZfun8;JytZ#NVsQfN3f`<8pPtM^UZ`Lnl@*qf;&{QRLxn0$1^d>N~7>SbD)hygG4bfxa~tDhPP;oSSIYAU~R4hbo87j|=S zHQ=sx8JFp@x{{Azh_9e%Q7A0IK+%QWDD&A(Oiwbm7^|Ub>E=NCudXdAt<)d>kRkzi z?H+YE!lRL_5Atb&Ba}nt_%JLEtv|m~yrpAzaSwaX(FqZJ7`07jzvR2b4Geb`F8sG- zS9C>s!68Dw5rgZ`%@i3DNgLW_dGbvPhOL3mwYiWMBN+-;QVp}`&bBn+;tB>9{kgN) zMN$F(-K)~GZ}M13tsbK*_isO5?#bXOWbBOZFw)h9&~vbLpQYKWL9i4{KH8kuJ?;#) zcFDj5+>ael3EdQLBg^vHBQ|lE=@ng0Oo)dM^|AEb1t3LMH|6^HWRfqPir{StgNxz) zfWY|$6Gm`q;a?cF&}&!gxHIeQ6Q0uiyR-v#MU25Vm*Z}5|#Ea{bWxMpaf@J*(! zKDMMppFLWy5RC|a7XpDZgSr1?<+QuqyfWrG*#_0Z@6-WytHCkhu6?C}oWkJ#_rIXn zws+mviM-p1kJSZh`r){bvzQ2~ z4YG=Y+LU&o8-KYTM_Yx(Bm(GUONKrN+gWwqu_wh@d*|R)ef3CxwRG&d`H5Kcm^viX zXVt_&3*&oM37Dwxn=9JAalE0MM)&1_ggrY ze5VCBpf*&FgZc^NM>PY?)}oW5xT}aziNwu^N?c`3p~~30vJ>48;~2U^_UFw{wDpy> zjuFFpt%7FqZ?X<*X0}z222{qw_f=O1?B*x#^*~8fW76?NHM1Ph{aoK5vJ7iIV!l}d z$B)QFb^$8h9!H`AUK=n9=TG2sulUHoc-!=Au4EJp2(vJCtupB9xSd#Md{IR^)Z=ov zNgOGNSW_B)OvZGJ7LB8rCP1()CsmCFmKU%~7x+N{COs?n9@)xsB5}=#bli*Q4^md_ zgpOC*oF<{29n6S)4I4Fe5(n%CTw$lC5mzc24xpTr zzqB^#sV3hzb>rBRmk;7;QtGlWxVqmU2E-&!N@Z?fWihm%paUiZP??C!8!>xw%Z_b^ zWahwst_rYvVp|{B=&o4El(|qWoLZY@Q{_ zuVd2X05@YHR>NyLAAkq)`1-re$OV=($(qF2?R;~2praHKxA|y3Rp|hUK!UPG4vQ?t zwGQ97mkD55qs+`KJ(g9HqRg5z3E1zvSPb=9t!mu3-ml>9E{cxIVizZ(*~Oei)p7|iI<*{uYg%uXAoCmcTzT8})Nf!L08scm_Tj6xj-dU%)5$RwWC-L1Hl}HaQ!sxjyG9*YiuraoOqc|axj;VDo_y!FY%qXvMg`%-{M21w# z6=ULP$pQwM4fUOj5yNGsIGwVIPs5Oo;COz{w6F7%-jlfI?D0Gi$1sk6j8bEn{wZk2 zT^gm0XqrkwErUK)>g339lwaRw68vA}zAP zq)D_HwiO@GNTO zs|hEzT$T=k*_g^D$_S~j$0(UcpK1+m2eoWdx1N{%r`a2ZIDRaplp4m1-zhiVNz^po zxkt5wp|$!n;A-!i(jA?38Ryr<#fIs0;KNY}fLNo&giZ<;c!tWQGBA7tW!Z*v;MW9s zOO8gl`0H34D+Q9ZGw5M-pKs1}1eqwS^>KKxA-e9tw$;wK9Cp?YFnFwK9uL<&BoW#k z@qRP09lr6d88M&Co#AT1;FJkvbKbLjrMj>zAj+3-`+ZypcN7-KTyQ_1*Z*70=LAU| z_Rmi>+4$cRKbG9Aov%Zm5H21saTw?j1tu_bHSi@Ybeww)zkAC4C1#sdkc7%7T!s#5 zNLD8_Sj+fQQyjFtlHC@E5T4ZI1z3;0xl3;+LLw!yuMzp+?P>%^#QG%f0J#YZKr(l_ zr<~_1Ciab%>#W=Y_t_X9a&RF7ap(Yw85&{#EQ+J2$d%2Z$t_Dqaaw5|9Htom$%H`Y zfEukO+jpWU&e(m(7^9g5oF)t1{^NNkt6(VUK3)of4BXdp?pl5})DdEaWFeKlk_#)q zd`bZ)!sCPU2~R63mH;~&}}l^ zCaEdEcCc^N1_*y5#`;Lvpo`ERi1-bq9+N+qiF#g<(7wpoO<9gSzk+O z`gf;>JpXA4H}(thjE`{6AD~Dgcir;`TvF&E%2wxIl2Us32rT#Ek#7(Z7wEDuR>SZ` z^f&Dzy3uG$bbVDhEq`CshR$Uqccyy3)bD&`LGb>SR*4WOt!9 ztm$*WB`|esg{%jMbm#xhsDcheN!%348+Lqz`(>8oe4pw_ojc>YdW&a``#=Y%EYdFX#sV9%KcFLo)gK)03_5f4(mtRznTp6R7g)QP#kn4ma zj(YH$K51%3QuLZC9(m2F(6dA$ve=hcp}T)ym&(NX*@!TrRq*$a z>u@norK)Rrjzjv29YkuCuo8iyu1Xxe)tG-lq!MYiSN52L6PL z44Q(hU2rW89wSv%Cf2kLn&JeV$(+m*@uySx9(TDm&5UFf#_WwfY`RF$#P$1D&by!6 z17C1^k?>N50&7$46eYf!#p#e-D5n^-x~A9e9YtsF#A|yXmnJ@bCWnc6#=HgK_mScl zjz;3i)1o@?%VG?|#S8drXqWJQ$mZO9nu5)-PAbB0CG{ad!THscd>6t?c?Lyzk8$vZVDsG*#6|C zPbdb6xpFlwDhMD*;BYyb7 z92#qOEoV$^^N?vPQpL<%g^C)}M5H)oRz3eCUfo9QAR7fzNNWNE708A2$)B=n%4j zt4q=Sw0EPt?OZxAAg(pvREZ0#6@1h804fw!F2(SqQLKKcPc@1V&1r}ew<8(10*<&!1+{|)5UXWq`TGGL7 zd}j#MMo>#&+k=BO0^!5%G#{pC-pJTi7@JbhIoQ5^bOHBSV|}#E*c00U`muwpy)z{oKt(i4VF9^PuznHL4F=`c0dH2Dro5HcrmJO#>~)da$dsH*ddtcwS9WI{ zer_hJ%DwQ?hv$m=u&U2hChtBE;9J9MCz) zY)60-=5*5ZcYaU zQp~-*Os+PldBb__3b~Ne#X^R6RV>ZtDBFAcszjuRRrFGHJurX_Zo^Y8Id`Put3u?~ z@M@KO+`gvEySo$v%!hP2FonH8NE_JiIrQtY%>JT-hIW!SjMmF5Ae*lJ@>0aB0kvIi zeL-%$qcxoEEZ-s-q91O61RYii(u_>UvzVWR8~}V0tiRg zu6N%Im*{p9n<@3R#Ny~cGjXeuTCJa!?g2cO4|iJ(d*5jTYe0`O-krdc-7+WHoV_B) z9D2q-g`EM{h}^CANB{2P8P+2DM2{T)VaN@#tVv&qF4FhBG%#|FMbiKQ8D`)W0O9z* zpg#`$1{xYvjPvw#7le%ym5a@O+qrRtByGN`@3qQsePaIN#OIAO5v5@DYhn#wl@%7m zq73&6otf!HADc_Inez6_1cd%4$CR+(&|Y&|>DR)N_}4l+>BIvpV3$?%pM zqd(%W#&RW$>}7yZQ<}vl?n7*xaSmMe<#`losr@Q@MZu{lL2SI^!y#UxcSX6Y&Yx=%%!!GM_%E+iq^v-cUE)Gj}wP=8``d%@Eh&-iSr zYcnX2Rl2Onw9s{~Q%D;M)jb=~y5si8@F#P)NTug4v5KH&(gHH0?$td7+MirOrm!Ny z13jST+3te^p<%Qk; zGgs+6HIb~ftx!;zJNu z&@9Kqaec%g=XK5Ze}XfTW?-%>1{`ZK9SaN#U^|$p#~Nu;4npK@xY(T`HNu{c&xjt3 zxrs(EgcO}du$Juq9Rm6@N_{y~WGaMrVU$a2Apa)~L)fGV0S98XzG~thg8wwXtl@PL zyd}05D2i3^q7I)(ZtLztgSg!4y#i?FdBmkaH zg4SrhEgx$NkMlK+NMKEQ}0^|PpDi9`1kn!q`JQmdQt(M1q>DCaK@WvFC$JcPn{A6fd@1QR;sc zvx)T4?o7?YGyT63G8p?|o+-h-c`{6VVHg%2jlhlu!KygMu}s;mq%SxQ3+)kbGyH8Q zH_gf&%e^wkgSS7UCEI9i3Y6rOO`9!L?cIu8_0Qz{F<*-pbzr5Y>h}fkk$JSSCWXso z--u$IS_SgTzHfd9P)B~bn9`J`;T}QsM76R{ z{yaeptRRl-TR7~J+&1`1Ikb))a@vWUZ)MexdrfGwkX|tsU_7`@s*CW%0PR<9#FEVq z>h*DkbJz6W(ddkH`O$He-tthTR!!Un(+H(ul{R2-ss$e@ zmV-OgQxau*U{{xdWKX`~Y@ccTlo-2DSqFfPb8_VF6Ioi5;)VKR6Etlj@?AfZ)0|sB zY2XRuHRM6^(g6pan@_dDB?=*b2Sy;~HEML3??ji~2Lvg_YdT*r8y-je_MA(ED3P8S z-FbaXQ1Y%?zAw-5i^`*r*JL8j8-D@6NayG+c$<3fjfJL0H5IES8=l*6EJy{*$+5y_ z3rkD`yNh4au8}@DBhARQQz|=?7t-~K`^p^Me0)u07JQicU|?y%c2Ttwq3t&VyVTU+ zfobGB46Mg{ahy(Du0g@TVONgw)#QECDqh4b$#PPw+Z-pWL zQ4vKxV$LIxD5dtV8Ww|6J7HGzyHQfnRx|5xPNv?R*JhwM!%2enzwcKYoZQU{F#=U) zdbp{~7;a8ZXRmrqjslm6RYT}bDo^@2GG(leU)WqJ;xUF$d;vQ-*4{l^F%vkt0S4_2q%61)CltftAQ18-=mIcE6C8DQgnZ<&C8Ji zAULtW?PWy1&lq`eZ9l3#E2x=VqWiBhKxR7c2!ESb6?-0ec@yNGs$B+ds9Cpc)(jK1 zPyFc#{3yIyFf&1)PGW0{M$1?BM?(7CtVCl9OF=dC_SXQ40kWrVCM++5W9aqhDLxDg zD3YwtvBKbNu`z-vpJuOMs9SZgc=m6VHsRCwKN>Fe zm(vX)2heY`;znGRN~+N7V#XvyW88<8{$zA26A#uywJ~O22VCA>kMjKyQ5bPf33jf& za}RykagPiv_~^Nkm#(JW8}yn1H8nWevv~%>;_UFSMJ^1$%^O{qyZngn#h149j%QE0 zu&if>W`QeBYGfNoo`RsO8|P*#q<<%Z>6G!4G%8@|Kklu~VhLv4 z+)N{sd5=> z>G34LhQj^gIt-v?qbM}%-vL8H$(@z?zX*HBAWeg2Tl8()wr$(fwr$(C?P=S#&1rW} z+qP}q`S#xD#<_7%#Cd+NXI0k9jI4-^s>}}<@9_Y5P6g7!aJ7{bjdLu|Kjqd}Bs>m2 zVnRu$y2IzzsEmK`12l`d1Ny@@`Wz>JHRuX&E7E@Z|6bZnZ2mKHotcXv;xwB67F9r8 zzXe083w@k(E0_3rywk86pY$1yd4$DV%75=J}&E;CX|R* zN+R`g5+H5@f1&TgSOp1AQwzmv47-oxow{2me?k`5lnr=qO+D&8=*m~tyHYzlt03w> zNW1E4rb=vW6nscW^^grrr>yqEmY_i@4Ay{KDBeso=1sD+L-EBC8Y?KzrNZVb+;%p> zaX+CJx{KuT5_w45VDmm+`Cq2Lukr&FV*^7BGDC#cP(p)(sTZkdPj9uf3~n4W5Nut} zNXv3Gva5KFrxwT|e$$`Iq||Vj&8SSumWpQjzrdzJh-~K6IJ2!pZ$2(4;s?qQM_4K+ zFA#ocmc_Dm$eB6P9(|}R5`xg)Mc2Zi0oO-STNkrR)}AdGO6I_Us9l|>@EUF7U-4BkEkb~0iJ)*qz$2~JIseM)QI3!?v1alBWj6R znsua|l1Jl&a!lJ+=ttR>8oYNLGjZ9}Koi1p%hSDLvriHk)d5wd8Lz-K?@E*0tt#_FP}6< zUkho=77NNqFLTU})yIr#5v+p%%q|u$QtwkozxiNFs#$rloOqmO9VMbW$?|gmbp;~T zPESHCMGHTvOTm7i3M*ZsD1p@f3&k%(lEY72Q-V;5Qd|l~(^F1wN;oZxe3w|TfkO5x zfry(t%5U)wPsi^`uinCl?>yd5ZTIP)Z+9>rQY9WG0uXnD{)hEW(;1|~D&v@&) z*2wHFxhlyXoM>1r#VA@=TCXPP;QN3Z{3S>18~eHw405dP*i0&_0q zu?g2Un~Iyu#~~cJo~>k!GcZ)?oq@AeXoYh{G8M{)B`VFMJl?!RCeMkgIfERiUh-nf^u0S`~B6+su2 zl`YFzG5Am9Ze_5SP`MdED@kpHF?CHCn_ya(DZqjx%D#MUNFhd4l1aW9%3%5*F&i$G$5rhP_t8@&f z^a!w>f0>c(V1mS=(c!f0)EXjY%~JWB8g zcU^BZc4RA@VSrHJMx0Jn2J+UBXA*Rh@8#(f=73$iL~z&O3*d>|yv`Ir}4H z4|#(cao!U4XVG*sjsi5$G8im_fzmtaji{m;mjH<-1qFv7bTRIj%KJ{!-%(f^HNwET zY}>54u|h_&%b$#6g@~(a4{{x-vZeMqcSvNMTAx>6G#Ud)r_O|)&k#@Un8Yb=QzQWT znQ%tNHxMcl? zXMF8H7Ghoqjb9~gT+7109Q1o$Tq@+P(!;S#s3K$D{%Mg(jB8%w?pV43q2P2`G;-*kR8+pVc z&7T_try=n!VoJrP6jLDY`o?qil?2v|qrbw95r_utdpKt#qTwRN_rY4KZH5RY7YWD4 z)Q0Rqbh`4U89R3^s0HV(k;P&}oPrFZXCywNX~_JLz$kX*Mg1!*^1r~>-ORs&!e(9{ z;c1n_v>i$8tN%Q5>uJ^mZI1Ss{+_f@lb(~J;XI;)&4X}wWD={&;oW?KD2>k;tZL(^ zmO@U~6!042k9_H}OR!|`ej2;K##}9Gkda9msMf@LGSo)Ta-bt2q5B4zNH~%y39t)6x>>*6ZcvK!2wID>dSM6g_W`r4&{EdpQ(_@>d>o6p5!)@deV;CGJf(w43^V zcgk(BvZs?I^Y)JB4!W=5yPXUwPg;*_xzA6Di*hUhiQ5hdn49rWg(V zhowqShmFU+^V&+ky6YD&zueKoG=~bb4>5xgAQk2kB=iw**Uxvxcsk7l{TYYi)vP%7 zhZFNv1*$t9M4e!(P#%~P8Vl`hTj1)*6VL6+fgl@9N) z?jm#8GmiuR0NaR&`_?R-9ACRaZy`0JW(H+{kqAG1Ye8HD)q3SYwpjWaADu18=Jnc~ z66=~j)_j9!m-QpR^|*}<;Q>+}oayj>i4fO@Lk`+L;9yGCwO!QFyFkz# zREH+8g7}hX1k;vvVc2N2m(`))y-}skK`F_NERrQ{0+oFv0_XUBV(K93HdPReXWemC zW?fQU-t*b_<#UEB{0$!&W|qy$t{wMQBGBST@F!gI6)^E#OnFamHr3TFc6Cro(d|h% zIfM|t@V>lqvc#(qvckiBAcP&E6697{R|D4gD({w_5;r6P{OF6S**~v#c0=!kj2-^r zY!3MPPLUJSS*ihY4Hae%IdCG;lm|lxq^!`InUggdX0bsFfBEFEc+g!AZjZLat&cW1 z&!ri9URI{dF3~kQFXCUsit-sygR3;)$8+UJ2D|Pst}>IUA5+=!c>V-p-YC2>kgXCU zv8LxB-vz19w6PC)WeikXRSYi<-x7Sj##~le0YC?>BHq(2-;Kp=w+!BRhS2dgRF+(-FHJ1*IZ4?fCB*yx)+$R+w}WEoX-f2+3DB~-3?^^FggkCF1LzxA$!#rOl1~o zfE268?yrkB+SYBev*VPfR2*bEIxF?k#q%F0i24(9tp`{Oi^rKaF%QoES@vK|wei_# z00G0iIF-qwGVc@7{e8v?5#Q|#B5zjppsdzr6u|bL$MDuWAOUUvG!+D+<^8lQQzY6G z`?;b5!KM6IRB8X`&kJ3K#1Xu8Yt&KAjgggPoTqYwF?&%e6Ni6uo4s|-?T=@bWE})M zQ;Hj1{4LKZP*~x|sZ<%Sxr1;`yrH4L1~zaPMUxVxgA)GHrkn{{h-%SmYIv~De7}NX zkNX+iA{zQ&K#GSRgUC3h7{M_`91R-&X%Oc3IYT2@Actk~00sf1)|_I*EXhX)5~T0K z<3MWY2lm3=sPREYl>R$F075owv#zmM(-;`q^{+fo=yV+=@C}fO$<%RME_mzifE6LC zLB|N2$e5Ue_jWWqI$iJx?_IA#kY_Z1L=vfS^A%sjR0;VF*dwa0+pItKT*4>UC54-) zDW-Au`gV0A5KTeMYxla>H7>-ndcIR23RYY~qg)FY8hlV8G>**wiS z(C;0`w|xR4QExfxkRaT2ue+d=2YKH+3}A%Njo`x(CFQMKLwvt*%M%n@9s9Y8vP##2 zGEWnH3t52*#e8&KbI$yG(Mm_6m?jv|a1hYV_3BR8#@J4%X!jA0$zVJ<3~cz;Tkf)F zs913XhJ>i^k8f}kNZ^P>)!cK)F$E#0bipfn@W_xL%qjeYsuYZZF@4U1wK7#FLEw zykAZgGHm-6`)N`Gn{%k_jaO9U{bb3A;+7P>wE>`CZNGLq^2;ev1FMuvfi19j7WrE~ zHt#i_z&L*!KUqysdev-($XA`{K-HBziNoYj>;JGMS9bSxo*2!{C%tiyxSr zoeIjjj2WQCuyCLK1b|8KNS5g=s?EL!4vCws?Gl9sISP$Vg2_5PN^(i0C>cBk(WG_~ zeMm5^_l~EY<74b51V+pph>Ek=pr>q%H9p4lKGj889n@lz07FPq3LE|& z6XSb@TsI1DZ(S)$bmS#fW!lP^RFEkV!N!FS@{;a-f;4((h~ji4dAV%3Q7bEnB_bC0 z<1W6B%>uHdi-zuJtICAs)`$%H%V$oJBo(O-rIv@BGey{yI%Y2oE6Sp{8+El>PVbb- zx3w`JqpuPl5nfh}%p@8xdrPur_v|&K=iYLD@+tb)pv&Fz>P|#}~E4n48JV z2{@8eT6qW@n#Y`IWx_i0{D4kiXd*xZiNHL%e8vpbK*%QkGhPdUHc3W$dm%gux3rWc z>J|@0L&lzBDBxNuvL6JHkJ%s0v_=K~hh+UaM-h_%{XF2>UHt@c765&huFh+juBSp6 zOv{tB^o92aSM*=CZhLk+xLBUbeZeW*CtI~cYFt&)a@dcC5!0a~aD%|y{lYFanx?_; zm7N-hi@qu=&r<|9nvv}UQUjzH3h8xHr zh%Dni*FSnw4>DEsp&r#*5p=#TQNIl1E>~$Fd7E@MM`aCP5yEiPa;1s2_bwjQM8*8i zOZ#By&)dps@?;p~3nvhT#6rp!__9^(K;GY83u%1If?CYf)5K?^b&J@_5@ItHNL4Zo zVLtF^dwzL9QbyWMzwWMCt(-jOeV51mZCqZlI7Eec?q>N|Inh^=4{FTT^t|52AY{1A z!q(eo{xR8I*JB>_)|SPcx!O2B82%7J?dbcd>uj2#YwF?ih?O2VSOCp02dYZ-4@bb2 z{?ex;MnV)G7AK*my*}&cxb>0M9+h0jWGifuYZ5jRy6r)z3En|k(}Hl_B3{D4brITH z@1q5R+9t5y!ZYhiu87QPLQslPlR7l4N**sgnY)bd_}=usO!p_`1lm9r-qHoC^Ec9nn?aIUgTX0v~ zLw1Ftd4^{+vxXpNE2Ec1JuPE9X3pHa@`Z@*D=Z~x_LaC(Q8~!ojQ~e(w!>UtQ2Zm% z-pqOh`5odNs_XMJq_w1bjkHfJBl-LCQB-^75Oc(I8NVlLaYGc=tDnau{d!B?>+vFb9k^NnVWrn>EITR=fpnLt#I3FfJg#jBtu1R3K($oSUx|fdy<5f>!T? znv(;nMDU83segO_<_1Z2XLF%0Ehogr5PPvM77^Z(X{!4T<{pwIZFEg+O{$+7FTuo1 zVyE(#602$BLG%~--dvJP$~RbrnGE~7kKpRYhBNnK%c=DEx_CiX!p~6a3bg|m)Ex#_ z>wU1aq_rxtH{;zr6xdbz!s7T^aGrba&6$(@0ejafF^y4=(1lKe`wq{%E#UyisdY?a zmoha%snr0ei8}pLkaWdMl=EW$`@jlbGAMb460L{hXR5G-gJ@ahLiR(f_%ol<&+@yc z*V&rP`~^-hw&JcB3HCfGQp`6x5hr@%B8+_Q(N^20*o%Nf3?);b7B1|RUw-`WySaAz zWC5!4`X!bQVG`02``~1fo@wP;wO|4w7BfnpY!t$~^fnXOV?t%vzk>-Xd#)O<03JT1 zULWX~r;RKEj$taY9DCEv`Ses~{R2AUHI4ew zV3!|{K3aFXl7TVEbh(w)}NTpXCV3;j^C5#Qc<|%+NGQ4-<9P3DP+Z&nQDxWO2-M>o3JJsPD9?6+k21%UntddF(`Wudg9>AR}`%)l%ofbk)C`O;T56z z#KE40`wAkpmfOF4Seosv$<{fk0_wT@(?pd|e^mvBb8<6hCihrO3>iZDTe>i;lnSnPCDc|~yewOU+Ib8ZS<-j&>J`pcr><=TNTVz$u$UR% zSd>#dS27hOMCA%Dw@bSlz6fRUKFO$t;&b2k5~jhcAggg3#B6=IDy`OS!hJKmaapkp zFNVqBI;sjLiWIJXLM6*rlbzbV5N?2!Qe!q4-HJy~i$=j{t!xM_BYZaf>{@-#-tqhI zK=QU@fO)BK9ze~w>1gHA_yme~o0&p0hW%~dfeH(m1I^dGvd30`Tj=() z^IdU)>Yfyaxi)0~+4XZaz+!eV+D~C7PF_A91qM6+xj8IK*y!!eQW`m^BLdRL@O!Jd z4CZdBeD-UBs#&yd9B*VD3u*LqD0eRaNNXC4xKUq|!^z3T-X>{CC)63|$^B;ebU)X+ zBzuT}`64WNgJ?rMAif|LkjNzf@8TH>TT*>>GeO5Nnjss7Nx5tt+-M)fyFM^iRo}a% z*%hbY#(~%2e8c7jme5C$bnQw`HjjBv(uw5$^doQ_@I_0v7sF1Jwg+9XH@;Tl@r+&} z5bI8=rEW!t!XRPw5o=QXh%^U8e)$iIZ_gCg8XQ{kh78jdD^Sfhmt+~SY6!uoj zcq9j;i!ipa(`E8JlvuUPGmP!1{V|jEh$@ge2sO7z8ZvJE!o{Z+d z0Qs9lgG2~X~z(kl>E|wVGU4|qCGS&6eSJ#L+D+gi4dwn{2PU~MxQH) z+W{FPdj$iyX$>^|vC9s1Kg3a{1U;9QJ282giO{1nubGa`JNK{%nYzp1d9kvFq@12| z!<9&jxvBGjS4n~qaGppwMBa#hrXq9J9{rF3b#Xtf-}!e~UBX^fI%Uf0p87_+5w2)C z#|0`t5b>?2HaG>CQ;g-)>g8eVzCyX*S&YSnFhiDkkvDAj^UbPIm|gI`_4s~9P~x5Z z)d?E+`scXL%dJqH_zoZqgvM$Wv=Mm)|Zh zZ`j3OJJm>I*!e*YVV5IgZbV0M8qVB7ftOoqDxGlf+MpLa0S3?yE%M3jpXLSzs%e&? zw;siHwb_@!!Na}mjA8aZH${|e`X)@^=%%@Q#_J)E)&l#zzPsbRjCY#|Qn6yuD>D-~ zy8|VZn>PgP_*TO~gxUx1;$#8f=n|$o%sLjY5&9J37WOZmjrS189lm)FAS+SCG`R~>QPxS-_^)2W+z&iIU+d`Adtv#O3m!gQUa-UY&6| z#9g2f2h813M5zuvgJS_vkknm6wjP}4{RP;~kZ?{K7t|^Ugcp~=AnaYdgZVQeoN#Dk z9g%zaZuShsl%DMgUU27f2^x^W`-O?!NV#g44A{}-IoU^i3)*gymthgp!Cjhz-Mvzp zYeu9{cc@SVanvW-QOlJlHvdsW4)Z9|n}(c&@ilBb!+0y%q+dS3A}Y72-YAbb+C(>; zeXq>ncX&BR`r(sbL-1kXQeoWm`QEVzh`|O@u<+_+48SJ_wI>9D85nVS5RfI}XN z{Vt7Q`$VM9VH5ipmhDWCnYASr=A~V1B26zjUH^d_v#Qj?3&YCogDR<=A7j!ds+H?o z*qOyHi;~pO;n^}f;cIzFHs(7Xmxie48-%Ds;IW`A1-T&UJhYI5z+;1w-{rs_XNuD@ zk>x~y3)w3gXLDS=6hILELtvW6;Y9Ak&kCxGQLBf+c7}=6KUvMcW8foBr7{6t;&7HT zR-YQhCB`XY?#;DxKot1*8B0dT!XnUDv@?wHQXH$+WXv-5juL6fET%?x0G9sPy(vrN zGUFBdvE{K(9Oi>lQ8J_ngZGIG8N7OFc9*&H;)|ox1*_|lVM4s6>okI;A!K48{|7T_ z(B?(r^b=SOBF?wtyevQg0bAGB;q0K!5>{Fz8CUPS;A#uvbMDmTz*gzF3hGVV*f_ zV+*#9Cb+`M{?;f88B+C&4vcgZ>@^JaR9NQy%N19h`MrQbhdw-@gA{4Bq!$Uc z)QI~OLCLnwM6TqR&Qt3cmY$O0;zsevWPcN`95Z@-K1=Uv!& zYwOP1mk(Z?X~frh4Lp4DGWU*{$vgnY-b*IGg)pl0ir$XC%8;BYme14+YwhmF;wOd! zw=>1QD`@_M`I@7#q7--ufV;~s(^AY<25cEKi3=e6Wh)IrK1EXPbr$t7CE7vGJA?eP z!wbat%Ru+MAu|zQK%w=ov)ZzyeP#dHkGtzS z%E&DXhwLY&wR`Y;J%k*=&Z!c+E{S3}JH9I;oY{7HF5?ISb6H_}kx4h~)JX}vS!yZ5 zfq;&N5%U*KwS#;t;xYZT2w`yOZ4-$#JD(d6&wr5D|E9^o_61nz)3cCv7=g^HgXCabkZNd{YVRb(ZBrBoK7E6{CKtMOi(yo59b%nMRMh@OftZE{tyW6 zTr1$p*$Or{Z|e{C?Klv(^79i17kSrWE6<*6&yMHtRR1`(D`Itha{B1{UCWclI>UCh zxw{ySuj6J#GD_EG(p9A2shwd4lju0-3QS^a{EEQ>RvwN+<(s`G+yCtgHruE;vSa7_ z1d-UtPf$mtQGnUj%8chKdNQ#q#=FX6VkrkLo+ogeSalYRJ4p95h{1KD7E5pB_U|66 znoJ|C_iF5OKDSoH_Dhy33dbH;&;#SfhGNJ#r(&LvpweQ4sRJeD-FzTS=8m2&)7xBG zBD|c5sMaO95g(397k;oH_OR?IH6Cc_P=QNVAS^GvF5JHZUESh>2@l2t@gpp&J4|+D zc5ZGL+=busbzopg)O&h91%m%)!5^~y=fU?3>&I9g5kdBIQ}N1Lc5;AJ=S;?hW|c#O z#Q~%(rBY+6NXe2#hi>qhC;%$#-55{#-l*pwH7`?zSyR`;=2cjL-qT204bOo8m2iBO zmLQns5HY0-Lepjvbo=v2ZUM7k_W>U*k&ojLa>%#YjqfNW;TO=#yW4$NvH`iFOLfrp zBgwO~(66OZ71nTmzY&#(2I!a8DBoY$M;mA?n(9e5JRO13^OqUmOpf5 zl0)YjLc1$XAoCH=o#p|e8a-BP`rX@QIg!Y1>%D3x@@vTMIq4$zk`Xj@Ld1xV{dj$| zx#u;VB#&BkhH=oR`gpe%WBr~)6FFD{v&%p-D9cyIa^7-Kxy9mxDM#yX^D^PYzf7nP z#)Xm*rpXXX2wuje1cMLieh^lWgT)Pk&eS%=kU=Nmj$&RHoOfn+ARKj-ckxfJ`B05G z(P0-_BW-$PYQ<~G=2yVWyB~fpd^&<|D%sd9(5>=`Ni`KKTCWHL6_eZ!8E=*`;Pl0XKWc-ZCKC5 zSOmnO7c7I+vD@z7bLOdUaP`1iW&-Y}9;PlRcCd=%Qz);KiZWe0le$Od>{X-jmI-o| ztE%_`p<(CQ1CBkPq!O_azlhp|wV2@8#&6n?!JQRFBj_G!R@Q~|Xi(Wn<1e^7yi-c< zv~&;#6QPK5Y|~;N`x1U=8R+p`r143wE|M1Ci80Z_&tGbDD**P?93z!Q6!^QBDDu~m zyRdRper8rOp`2}?eYOD4--2$l_rwdNlo`W#S!i%hNZ%LN>&Ph`4KT7zk}sN*=oRu5J81?N`3bOQ#o2@2+=3lCz)k zw3Rbr^d#1&NL|_Ozx2cu%Uit44wO(=%ax8FQq+m#IUv<~N3ysv87FL~X@1%y?;ZUY z4&U-`BK$ZkGz)e1EA9<(>EdBu5?U_2;%E5p2C`K!)>OOED;_iu|)(6OL^`KZJ zAFjXJKI)U?-EDt~U_=tp~#)UmI>Ox#-_b0l;6pdpbanZg{XfI6{67rdWx%5_J z20vuv7C!eLWwr}t|K=&TSw+0|_7i-R@AKIX5JSQdtL!5RytoTt+jpVIz#9EQ1nJB^ z`X1R!K%bAmCm3tBZ{^k^>A=^&jGSRxZSS|$1D8mfXN6vhwHfcGw4ro>>yu}IUmM|0 z=uQy|EJcRiF%xr=-x)e$)Dq4E9?)FuxwNR=Al)5COj5RVjWslACfbU{>&38X%$5<` z46Fp*kE7jdI|>NbQf6m`MRkiL;0(}b!VLBHN=;#3p4PvEdRrX4w<)5mNnE z0H;CRY?3>#p6G=tCaPDB#<%y`jjAVW{!}jXxD&rxpM%Wg(xZ|4h@jmsE~SuTNqO>n z6toAi20l2pBazvjl8FFNPxaZawXX?*Wr$Sqg|&E!9P7SL^l_Enomakw2o2=k8$qCr z9PXjw)L}WHdynH(SpqRpT!tu)KGstR+<+u>Vijnx_mdXw8=S6)4Ns{DQ>N;!1y|}j z`-ClK20t4D`7P0`)xL_B`;{)*I`!+1Z`@c&m;8Lm@TeUH_3*fs(SJnJjl+OMY z1QfNA0hHS!)awCF*)fDouMeWmo#M7`|2R4DAu3<5p`_n5cC$fc#+Jr*k-%4yS0WFy zu++KGP3QHls0W4&ad6j++VC$w7~mK$I!)f#S{OL8Lppc35Xcy(aR!-x%gPW>ccgJ_ zH7n@_+H9eXg|y?WKE#^J_1ogW)cEx>G!x9Gac;Y}_;3OVhs~ZTy3D}Kt<6$#I)PT1 zn9AyV=W^iJ`jg`qNN@MGrZOAE0O=Jh$A0jN}TdbIxlz zBhHuJjNvY)23Nnk!9V%*cl1b?Cmv%4J z)@rZ$_USCn>--{UZt?Ic8Q2BX5>HYaHX5@;{y@FEKaW}J>%`3lhieFW%pd&nb?-2c zn^a`qmfbq3)amy)kZ~67BSJx@s^hQl6J!*c&vVK^jYc9L?0L&42?8K>sHzoCaww#M zp3^C5*BrSh*YLyHuj@ef3$9r{po57v+Mqp*Mx5=3N|Y}vi416_uVP!F21aAF)y?@q z70($^|8V{Gdwc8|YJurFAzhv2pi?lWLaryLf$V=B7m*><7-xCI3CZa>1w1h!OLlq9 z5VEa&lW>$ttnSKdHD^P&Q3DI6Q`=FP*bi5|t>FD7x?nrNHx~_CLCy)r?6G{=7?uY! zp>-Y>wY#^DMTbf&QMmEAM-IpO6!0dszm|J}km>q_VbDtGf;Wc(&}X8qqjS@EzNEYD zL1-eNg2&4a{Z$20>dap_GCR%Ht0aZFV!42W;7|E_)>GzR$;}6njPth3%uD!1+WzWj zsU4W(w>EL%?{uu8BW*_+`o1o1d8v*L`yElPXH-16j2VVklJtq@x2+FfjeoB-N?Ze<^9Q||%4ru2l8#!sm%~J6%d&f_ zk*mOhnWvE|zl@<=(<#~6$*k7;($+j6z}s&Kd`{^HW40at#n5e9nmE0rT3P#3<^BMCxikCia-ol=aujXyBC#XJoA)OL<+6w>c?p| zk5yYc$*70340;H|NM9ZLQg%Q~t8Fs_;E@kk>BB+_rOE_b?gk}`*{0C^i5p(`gk5y` zF6Cv1%=;5K4r3b-;o~L~f3njs=QWIo5j1-z`XL~3u&w)-AS&u4oT(dY{QNu~z5kq0 zU;C*Kz33?&R%hj>k_zfs>l3hAJH``TU+1ol4h@brNrv~6G^b15Q->BnQI^AjA-gyQ zHdaw;Bea>tZuqp|{=3LR`haj5SJE0+mj(Q4uTst5)LJmW;w}xP&9-m&u9v;r^$3iN z4|(y%cr+{O&baM+EfQT_6Ltb;|A%OL{{wD9WrIpHGoFrn9TCcHp|UzDGs zTOon(u1`B2NVV|65KTwqWj&)ss~;U}p}|W8y6=%V5AAF!hD{oh&C8+-qdq(E_d=E5 zb)NsQF>8OCe0(!TW$7OkWSfPw-wSos%Dwrv)SFGWhQg_h7wzygy5eexzpcYXkJ^=P z6qwYJ>TdJKun`fb(Tl1mr~tXUY{h5M?zDd5rB&0RU0VSgQpy{1aCMSv_yZnZy%o_eNo&R zdek^6c<>O^o1e)kCL@rLZ+Z)LZ}%y~_rQY7C`pP^qq861NLPp_yXlil3$7o(H_U_H zrwn3TbSqTJg;tIcrvJM1_t|;@BbA!IA<$r=9i_gh|5Te?_*P#k(Sa{-`^SEJ=U<&z z&i@nPr%uwy4@BgyCj2-eLiE+%A}6(oN+WiS zh`BuDBgA0zCSgv4;>t^w(S_36Sp>(faA!IYH&C2qWP+BNL0;2plVUmu7ZuOkAN*kh zRs1sfftempFDYd7Mf3tv76!?%7 zG^6gRzL!V)nZN1(&JM?K=cSjD9pdvoZVgINP5_rbCCo6l6PhWUL9UAb3inel;}(a; zoV33kKG8@OzOJ@B2E*FpCTth0$({2Sy*{klG@m?F0?}ECu&YzlLqIs z+7pw4x?5{*6-0&h@xLy&nb|Wt`I#khWl-sI#1AvJC#35BqL%D{D7}_`d)_7dYViJR zJSEP~6?-T-G&%8-Uy;!r8T`gl!n(xVDFk14*o|#3*h3q-O z4owq3-3NIJR-64&SIF0Zr^+cr}EP+zD{%qmY6J^UzgX^|wUOeADpFEcs97@{$< z2L9_9{+WbeS6fUsz_qm;$c=`({VluSJ}FrOp}Q`$X#7Wqp?HWL0`niPOWxFs4~ezgq@=vB$>xIe_l@-T=>6@hz)-~{BR&;mV*0eo6I=h+bi47* ztylM1UM>X!`x-gLpi*B#x=NG+vCLv^-z|GRdQL!VS+PX|jaV{abWtz3ViW$6HgWPK zieJ`W$sIJ+hL$u0h085K5+qfjyu|dvU#F>NsU_8uBF^2-xTw*0#rc_SARmEy>nZVG zgvO2H1QX_@zO!aKuERxtpxW}GB@;ECu5d-slD3M`A-nEF<_=%(qT&NW_r zG3ipHm~?$Bl%*)K*F`a6i3k;$^l~~RYc?v4YAgBq=D&LNw&SkY4F+#Za@iwJN`EQk zsd+t+WgD#*&IG|TkU7o!d9HXmQ>~*B7sp?Pei&!%?gjT#0liOfB^?g8`L#_KwknxQ zD8^6J?i#0eMR4?1XrNSc(+&<&7>R7V&MtxyxH?B;(5QUYGNR)DkX6@TTG_etlilwMJo1*lW?OTSD13#EAT zPmDpwlCBZ7CjV<}v+(Y()y=A~_S!O6EQ4=_%)}WydNJHRqRTkH;%07@CNN=|MVn09 z>uCzh4qeWH$? z>ji7cW1<2#tpPNyr(iBn+hX%mHgWAoh~P0X>90t-j4(qaQ8l#NrFW*P*>}=@dcluS zA_8BxjHkCTWjqd@enIM%FKlyS6KNaxl&1YXXEX$(2P)-6xj^3-)SY<(68ouNB}Zsy z_=sl>^jUQUnu*+%Pb&Jd5bVr4eO`_J4ja4S^nTI_4ze``Y&4|^7BB!()Ki_j!IOa#W^XZ56H2}?n-2EbPYfi>+u?qUH%XVj6f)FWII&#g}~Ap|m$LIpyceimiK zaKv`kBjUX1B?ob@^HqT972)E^wCmCNT(qT#3ad*yY%65l43ya!R+>@6 zis%e%U815fb@lQNfytc|hz!m&EuU@l?tv$l08)OoY z7!|uvhYi6IDqq%#VgfI!v}Ev=c&!PHpl0P|>Y~qL*XgH<3m$g6;4@6^q=ir(TWT!P zHSW6E9u3$K_%tJL*2Pg=*xh!(e~SZ`c?=9hG$DX)xIXCo1gd)+Ea{wHsIiK{ zo?`Z-@JvY=IVgJ>aUS3l4`DINyKWc2Htk2oXL2X^-OK+?xCQk76}^M&RDG`^_}2jB z-v@SUR)7ivgfM4%S8HLrY<|~r`tv<^ORYwJBd&foyUL5o)%vyiEPA;9tbMwSk1ZC3 zd7?`hHQG9z44)hpYae7|0=G+&&9f8(4J)@{ayW^l^f+r8A$g^(n^*|7Q}X;iBX+dc zTJ&L=Ij2a7!7r_)-bWkeh|3v!wE>6OCnxk*<1d*jMM5VcAK%yk&>Gzn&&_nGCNErU_&`8UPg41Oli5VF1CT zY)Azl07wAEoL?N~f9((4TAB?Qf|BHdLpzl_ro-6(e zutcI0$~R)DK2-WQZ8qM}0q`Hz+5a1BSs>v3|72kJJ6d+CGYi1HZxILQh`0P784&6J zjjTQpNcMj)QbYh?04Y?!fA$M)zFEQ9nGg%EkZ)1CIsX@A%M=e-Qi34vwRpB@}f_yzBN55+;d;3Rv8eDmUTfjQCf*TtU*3oe;^9B?NCI$jK z{3db8V)p`kLg>#T09Q0E7zr-7*Z};G z%C6=%#oz!S@bz@6Pm$b(zl?I@y~+(j1(8u+9v!^>vejXCKp&v~digPm$}Y2?y5St5 zIzczG(J(ur_6~D!HM;SmO1tu8iAp9wQpG(CDg8%VVZuP*tpBzN_MdCP0gjLF;N~l? zb^om&z}NBy6o9U8^R}*7Dl!qSm3<|$>U$#y)Z7jcWAC#Spi<@H9@S4&8K;mP=E2%C zXKQ2aZnY}dAgXmL*~wH_{d4Q`$w2)cN9PftHIsf+yeZL{e3JuR2IKGv5ev|$g1Nr6 ziS>EPgg`-T3HfR;?*vKNq`3i(72#JI_)X~to*v+DpE?Pkgw9Lp58>}oAJr3vD$Y%S zC66XEqVK@a$yK?Qy7W9*pe7C<=Bd^SYr7CX9%dEKZvM>oZ%pGHI+c%IGaFVN52L!A zfMHr@Dc8!4MYg=-m`zDr(hCBSKD(U>K;c(< zAaz-T_tO~kI$WBWIk(?Q1~;I7&eneCBQkif&p zC*qFjwgV64lPlTaapd%GyIycKUq}^8Ufmts#E0#rN?;$GMM;+Hp$nucX>5{%@}9&j zFBki8K8&2yX<`Cn!DhisW)Jdj;J`Izh%Di)pL*RV>w3`RkfQ>R(*Y zZ7h)k$e2nL*A9J;1k*{j8LDuH&>C3+=`Wpx3iOjx2S<|Mai zBpHIw#8aoDw?zUFcW^iFNxUY2A|a$e;OqZ?CgHbyfC&;QEMZBwfAGCaD-}37ef;nx z-K0@><^}*8c$mpVuc7Fa-ea&w>Sa%Uq77oX<$M5Rz3{5Al3t?$yq&Wg-8BWP_CYX# z?3d+JjRu^5{URX(m4)mxi@hfct*Kk;oF2 zeJ6c4BsBIrH(u;2W-xiOs|AoFO^SgkO*_}pMXUIGvV})= zHr{^k&F<~>id9*wUiSfbVi^s=02B#!2ZCV!mnPMS|1uYFfW!2E`5!00HlNCF;6V_(viRGcq_=A)@zR;vLG>84jKUfv+X)^jUWEChke}xNe~7w zVZi_}DcTtc>ThWMU=AfgKZ1y#_c@KiDPhaG49rK=9naqWKOWT1iB`!47ba6n5GITO+qc_0fLXH2)sbM4 zNP8fN&_DD4onQY+a*zQI%lDwgrnKWIdKWwW%!L0kFaS^~jzC5+@U}^#3m)4P)Z^L^ z)FjbU5zDe>RO!^{v)R%2r&OfRF3q^m&E?Va4gkx!3tw?~I$oX!N_s{l9n&N;3ci3Q zkKVqTbQvS5sY{+IDIw|zz4ZS8b3ly07)GV4Do(qDWycvo2W10F{_6XRriFiV8*e*+ z2@@H!0s-e|@D3G^uy3s^C#jX8lLZte5%58KI?$Ppk177%fSdACeAT^QL7H3w5gkqr&;0PgBnme*@s?xupUQ#F?X+xr&}I@#=9NOd z(EnIga8P^@XxNeD8EiGVMBF)kz7XYcEm6{N$1}#+3R2C7*q7oU>bL0~U^3dIwlqf0 z|M9}?(q;l#kA!E=Dy_?Lnc1s4HU|wGZv-9DNL*uHQ$mxqnyp~?00BrWA4zuQ1r6me ztBPc;_NOaOR7xCo`bIsSB5qN4SxcrIF=dr&Wq^AZ7#-yBPePlBi!pr{HZ5Xu1r!0o zHb6)^IA^?_x@^Me;tm)fjO)ZE58i0KPbFknOcfZ`)^?M`D7ba)YgbnWJeQ{#3%y}y zo}VF23bdo+?5jZ3;ux|`se%L!LW6`S+uf1mK}j3D*^M9EXRz-~u7FyaCHsVY<_c9? zLh;mD$LH8JcBFs5YM~ihvQr>iF=*f^unkrUWWSHv0B*=0kj4p+>TZr**;AP|GF@Yktdu#wG>ZHnF z5`0s8y)j{AZoKYmZFL@NAY!8c>Lh{GV(%heE#=@GJ*hbDkV>X}oCjXO$=G_$bAU*7 zn|cf)B-Z|tYhn&87?Kw3;q6(}v$?h}ggg}_`ty1z{6Y}ktEpwW+Zb=>`QM?fh)&-7 z-p|^pCK(5ZWIJqp|Gs@MV${Qng8-vNw}As|0EPfSlAIh+0RR_(f8F}^pTd4m%VQnU zab?%8>Juwm9mhaB;mUnZK zc>A+mY*QCC#vzTiH^bs~nr@8Eg^m{4f#kQr!?%GeXg-_yOn!>^U3rOrvCr0mW#S%+ z6r|M_X`aQzQMNxrX%o{)byPh9dc^{HCCoVrRIY*Pnhq5tWHgT0u8EnA=kqmJofDuF zoTqCpZn{)rF33Y*nr3shRQG%J+l{GXK)Lr{bm6q-q+3uQJ2~IuGJV-)C9q<$jJ9EpgkiS-i+Sd{c)QprJAqp#kvM41r&sz&J!7}cri}U0DjK7j8FrvH2lUB&nH{)qOBtqO$#_hc2;uW zc}l8^6dlG}*yqr0v-3V^42yYxDCN}MO75hixI)gd-AJUmMu*Y`KLu;yFqk+Gci&4j ziY9f)S8m-6y_aTZAz`58AFC}ly@0At^66N`4?J=E>J@>qT)S<;@DO}`Q&n11Q83nsNbp98)*zQRerv zS)>d1gsA!hH|c?m)WcSK){z7*8|GAz3<`C=9_naItqm^)$G4!fNELa%zW6dF@s_pc z&EpOuv=qD>yOS@K#J8M;Y4lPi$$u@k6@H~FXrN>(;qpHoq#k(sMAE`3ueB}bi43_^ z=_;&`Hq$`1V?9-*daLqwSy%C_pPF;meIunc%KTY4EjJ*ILKUSETm@>)rk!ySV(1p<3=x{8P2@yws4 z+jWE4ci!+#72~pY;m__%b~p>e-`HDI8GH zSK`#)C0)0|co#5Qnsw->@@Cyk%;w_d=SqZy4xv(YZ>5UA(qiGg9P5G9y*my_)EpH` z5*_+RyRV?G5|dIhaL!D)o#5PhrFl8}37qQdlD6YSD-lW)8cDw6t5Hz=lQOe9&QgDk z9`Kzc>s2$%78#VyxZmp3jwvg*Fu71sGU~oB%OeT5``(X1Z%m}(@wyGd$wgt*S~--0 zpP|S(53uEs?3N!(%F>*}U+&WW?wcbVi}N2|tGIIN!fm6|KyP&ESib1?e40uDS8*oA zG4;nxqz)#@X446Q?D1vjR0N};TExYL$mtJIFTyz7Br)~PH*umcYA{P)B-Pq#V6AFk zS$Oa7^^!ml%MOR{6J`BhxvQO~GPJwLR z%qEtp0)S0|zQ98Tc?A%J{q zy^OT#JKk4&KxobW2Tu0rJF$=7Z}Ohtukup82h~)|!DW9>`DhP?@V`Ij@joNi^1KV; zJ&*W4ckn=hyg&5d00003$!4t-N{bIcQA*epG99e(RK~IQQL9Xj|8k)zt-@K#`hUIL;+jZ?iWJsn-#?QO&V?JViviXA}|2)mwAg5g0T=&R_Os`r&aV0b?; zm1R<@;1JN=mS@+cUa|RCmK&14D4@TxzQzSZbmrHxMGdBdbWk|!=e(x zR1C~K%9-a~W6Skz<(pc6r?6-`^dZ#%1Mp5)8UxxAv-5oILxwZxY#vaybNbJ;!;@Nb z_4)`yIHDAn-FJ;%H-w`JHj(l50A zQ0T$~n5bytJA3(us~BUEuADQUkOgF-3v7s(Dn73cXHdMdRgHYo{2{xezZ~QNervt# z$#o3O`bT!d=`?kPaPI1vv_+cS%W!-+<0jxX;cH@0HP!5;b18~;PdbVtMNW4+h*51Z z{N6Pz(4NYS=Ipb*uTruNbjC1JXi`YzU%qz3g5JNJ(r3CBU;CMri>~gN?fi>(kY}PF zv=NNQ5j^YtMR`+zrNP=pD&fDeaT(krOT(!VoAtapkn1|Ym%cdtw`uodfu5IJL<_CD z!dCwhVn8Z=fP+~979DBLOR# zWW$HKGPLG_!K9)NeCi3!j1wcFSsH6o1QeRmw99bQJIJB~rseq6v(#EzsIvBwDRKAp zArqTbI?%YN4|>7@ArM#kj)Z5d5cLC>ByTidXB368S^xXVuL%FRb`|Mn-TX<4C<@PK zlX3-i)EDPF+B4HDZgpD{P+Zq0-&0F!%&{!;s%Pk5F|1fYcDz}mlFBu_dc(KN@Wi3U zG@O6S>CTrN(@xi4c{Tp_9t9+E_EidC9h>QPrQ%W<_51=20Kuy#tJr=Z~ZJpK)9?dNNcu^S|$Oyl#+U603*&x6SoA=x6`sZi=z16JL)i^6omi2 z&aOB1%01ZA#6%X$9E}Yc9rPJ|BQ_YIzYo@+A`-u$c@-ll`o{Q5EY10f$$ilRQy9-RFxj`djtw8!!$k%wCA? zWY)G#CZ2CSOnGVwT(L!;SS8QT-(*?%98j;9wJBPo@CE=-$3ESb6DeI7ZI(~_56yf) zsUv0ge}5Qc!o?~CT?ZLo@X;10`ID*4X?Y<+?#+1iKFn zFPxI#h4)#nyOP>M0QQ*iWN;n0#XRplKbXbVICIc#ilz%f1C(Jc=xd{=pjlPfzr0S5 zz4%3CEy;%!HoU@ohonD9GnckIIm;-N70Z=F4!S-JLli_0+Fw{ncGIl*Lo~IaW(_Ss zSwwS%rn|mxz=YS1dNa7@wV3IEYVzr&rxO!UccR*#9fS~#!TE_ahO${=QgQ**EoPw3 z0j!mVr^DY|oICw@i^i~oP%C6xT|8POj1661XIBauJ0eI7rNvNlTS?E2k}K;((KaLZ z^-UtRDQUm2EwjerC~dBg7!(n6Xx&SroBClLO%qvs@iPSGomRQC2bgQK@6{*A|L~zC z4B5P^Rfi7&NGO6|wXeQ^0usrL{yvvPK*>YY)H20OB-42qqn$KHn*VNkd00o`9GE$k zTv#e}Vp!+YtDWlO$Zd&N4!3jAs3Yt}wD|NUoXF`D6fX%YM;#qci>FSL&M-;ZyJqzj zXmfAJ^FV7lX=1O~+eGzUj=lW)B>$*mRmC6C_ zMq23+II3oncWXM4+ytY=B6Xc0h&S088o%y(x|8 z?lG+#M@YlsUwfxZr=!3l=|OaB(*OVf00S9d;-V}gq&%PBxS$?eV^FUUn9G`YgYVIi zbL*LI*QlifnkC6pj~U$Qdq6jLs2rdOJojcUP5hfM(lt+G!+0PSTokSvg_E?*>85tV zJi+`3TON=jNa^h&$=~trO}vu4j0F(^bo6442384hCfy&(3Q5XiSB1T^y<-;*=(NnG z!R0Z+eeqlp%5yc-Ul`Cd!}}xl%CVN2AkGfOLSz~Np0BAw8ahwWlCa97XMdvtPL|AM zEPbZ-y1Q6!MC!2o5c64r#bb&SNWs5p%Pw{+F$!{`WL2<Fn$sJsx4_6=e7jlC!bFHyugr*VP^Vs!$aR36Ze6wik0C+_1CsH^u50^ zQd!@Zm9u7d6RX`kA0CHCLnsqgISoAm{!*ncT&Cp~HII5`JYf9VHbw#6(s?PesI#tZ zN;)ZaH=pvuTov6}e<(p0E~e5rVK5m+sDqy0Mg)+53)uQ-jqpCi3l1mKbSUc9p1ww| zfMQy@ukkA2dh>ic4SCM9O`AppEq3+;9YgUpp6@vFN`x3{uR4^9;*~MF74pCJ38-xl zqdjH+?sp@{-~g5Cn2xFd9F+ivX4g35#;rdcFo&}GG@OG75kL)qe9`7^dqD1nLX0J< zlsV%Vh|*uxU(GbUY5XyoBQLDLlob%XyqnM!B3iutm&OT)6d0gBKlw>S%W43t{~Brn zW31A3M8JkeblT0AQCs1jVzmXTX8RBdrYaQvr3YuDrj%yDQuu1kvvcrL7%ah|CW<78 z_iN#V5$ikR)Pa0=A_7~3ZXyj8Xbm`CETR-x=!)JH(qp&E-30K!JH0METaS%3sN_IJ z7Qz-t`qPAUTL~qp>%3{YP*7%w&MpEk--wsUb}V`%#@WerAiZgp2~cCPmWj8d2uvdP zjcM(^xjR9{iA%bgnk_sYlTg+Y{e!IATgjP}Ry5DT{ZVj)JbiRe#+m;qmHlvqQ>?JA zoouOs)zyU9K2#r*kH`kew&Pf!`%K~!7VEk9c}j@ooOpWsp{6jL?uJuwQL+f3FRp|u(!SdbG`<^19 zevsC=)G2B5xu?ZC;* z+OIzs9~#+3Orz|@Q7+M5cFJP{69o4_xDl_d0~5gatd&l5Y%xn~I9G_h%C;&Mj|sFM zlf80S=SEdQzvN{L%@j}X>Uz0;fL_uK3OXl@wWMS0^60XLw7M-Mo#EMEtE&xScntl! zy=qi;+?zf?xlmCDvt~?{SqzpVZ#(7`!s)cS3A2mY&7;4Iwp~Ke3~buQnS(;J?vu60 zbo-YZ0Y?~E-*x#6*3xMaU8YTMBD?1kK(vX}DEGzE;sB5`MllfY*(;X^V9Uo*T23ug z*e386Wuu?>i`_+3n1tOEwxwl{lQggNwmTzL-3t@$e}{uC zhpgoqi1_*_${nWg)OlE`Ms*Zg7AHj(SXd&$7>zSgM|2LKclPX(XGcnZoCe|$!Ft&E z%A9>M$xF8OfkZRW^J7!C{wsX2pfazbeGY``{)D)#e-WfAy zArbxpmg;W|z-T`yxd@XXbQl_X^CBpgTkbVd`10#;jR`qMITtBN4`ItI{#E6&&^Lyb zEW@N3#@IrW>&d#5==bw`jcwB9+#CWo1=pml7?l)H06HT0yKXt=L~ z!%6KUP|_{~=xm_%;ILHL!3asTxiD>XzR{IIIQ(*g*2f-v)4KDo`;~W#1DMhkM_wxW zcf1+boRq8hR(kvJ)akx;M&sVm;sa+2pdln)#DcQqrx9g1W7r&{cjTz6SJHJGAU#ibcQL#RYKs2HbbAT+puCI ze%svRlmFF3;vt8ZPCYDCu9U1w{kJlb8;c>&;`h12Sj*wl)F=fWo|UO(g6zD{)&IC9 zh(EzTusFU2!by{Ahl?E=3VK9pB2W=%Q0f3C=a@4*_8(4Lx;I&YU}4dQT2}9iq59;3 zt?+&5gGRITYt!3Lyw)AFfVP$?9N`-&Uwv%!&I?n}IATPiCo0KY?-#Q*zG5-~uvWd7 z+KM`o1=vnzJts4{nid!u+gi{XHdypJiq~+m$~6X`d8hF@Y0hK&BlbZ{ojUYuU>d!k z)knu-;3rJarE1MFs=!!Co)+fil?0;>f^Zg}CB9@Cc{VO5y8mm?FXgpmCuh`W=EsNq zIYlB}l<>|{Q=Lkb63UiO?PzXT*O@H!PVJL(EbbO}W7gR(YN8NP` zU-EpPiF>WlrT34ma;Gy$TO;(LP=gf|+_wEGxz0Ftb zuKOsIJK_zg2$&*5pPnOe1!nd~ywof(OfvtSB!%#NMyI78R3^7Fi>$MT(MGe%rR`(N zzQ&_1lt`~PVnof$6{9IUzSFojnqTe2_2%5?3&k!nQ3VV6YPwODhSctwrJ~IT@?BT7 z=Vp25uoEgT{Kz7E>N8S&Om>x1bCZd*J)|P+gnCfCa4Kw-w0nE;CJbSpI{S0_xjfV5 zSZzMy#V=P*f9p5gE*`rL1l}qkJc~~MA zpo#d=(2gK()bNtIQhoOOxW6zhVB87YQd!l&L_GPY6X? zHFsH9kLHh$nEPYM;qVD0MKx`{#7r}&6wIbpUY?oL(rxb{3e#Vg<)y+`Q6l;6x6hB( z!-LyHs%(=21EBQ25dBS!5=nuL}_j)7ABqtUs+)gM0tK z!Ef395~?7-%1~~xXVWmP70H1V-guz#fpx^7={Q>mRT-u~?%K=6o~@bz%{1V?9@c5T zQLAv_&<%}gHDJLP=r^E?SJ7lRPXgcx`Ci>nT@3!wyIlu0{> zDtsXs30dpTvPEHEQkrr9w%jl(XZH1bk^eZ?;k3}o__!x=(zKx6d%*tsRZ!j_+R1q{ z*}i@RK;oBD^^UWC$g{fa`5%`Qw!?-YDkO8J-sy424YZjJ0W%i=Ze&E@9(W>AR z^pQ;QAsP{7qKP4LO;Ld*2#*ggO@=75VW&)@$T?xeMmVrQfH_ zwR(6)I+ZhJeIC&-ZvCY^J@Wkw4Rp&nIZE_)_uKcT%LaS72{iDUpc-d}WZW~Wl8a(n z?wSKPo0xwJ&eX*1P27pfn1C&2NJ;#w^}~Uk0?@oEm(&S4oI2}@ns~Es7q+awg$N+@4mz?{!fR(B{bfZ}qg zYJ?%l6if;0Ape!r1!U*q+k3gwnzJJw3L72V73mz-VQUvA$7jO20%P_wf_)oJ6$|WZ zf?>*0z(VcgV>K_ zG1n&VJC4vChslM%cE>qBtI=Lfyry0iouuynf#VoO&|U8fuM-JfD^@1-0!1L{bs~AY zpv{>$HM8gAAM8H6 zV4xm4Ne&Q@4Ue|$Ezy)?o=aOFd63!a${e5N@#g+{;xol>@O?Nf+ALEku6SQOK;R)V zgA8Y;&1gu=KE1ZDF!|8mPyp^Re=uEIKBBM9GZk`l0+$GIp)X7!f%}J5P6F8QgqB3I zh`qgC^^f%>uc^c!^ux50#*2ilO#?vY$P z9KKCyd2S2Wm>>BICI@zxl80Mofqb^CK zWTcPb0r!HDCd|J!SQB%W+jOE{yi6D)M^0y8S%3HN>^Y+UUmQ(1mWt!X`%0 zC?VrE=!1nguoaZ>tbN2}^2~R+0)FkB&%|#W>Lu!N`yJv1m`XcPj=9N*%pE&UZ8zbr zRwH6N^OK@*zv-{g%0EOEG>a9Q(?54+);`X;uXxGrPI)qtxAT$m6Xy{val%NEV52<| z{VP9cj77i0fL|z3+CBd7-4>%?I&?N-At-os)()*p{^$B)Ea1UsW7o@3C_y|UN6rzsH!faEc}99kpDLd0;a`K& z>wnQ}-l}qT#2?pHGO5?T)Cg_E_;j>=CP!x1+>O|Rvep4&?UT*N*G|cE%}A<|A2+*S zAz_~<^2umC1A7(o#Gux`?0ut+=X5IU@hIy5T07^fI~JyCLT%i2Yr8DF*A96dA!N7b zNm=kMV(5mPaT*}ubBFBjb@U|0=)Bjfprpps~`E$iNyNv(J< z5*t>n$xAd~HJ&}pDv$(IG7$ zt_At^$*-CDI-w%?e((dLgTh`U;@|49ogACSQ3hvcJ5@EYZih5?q!7OPwPomRZv9u8 zdq2f?Sx=}r(6D^jlozy0y7BngUBSG)ZknXs{6hN zrNtX~8BuI@=TntkC1nkd)3ch+Q^(Ig+AM;87EYT?Z1N=IQef2ps(t30S?djO+IK!F4j0EPfSlJIdr1OQHXO22oteoqaMe;pOX{%d|h?7 zu9w&WUkmfpUlOL5%2!18Z~y=R0@_^`48)#J!H%gopKGdkTxNT@lUF~WX#G#Z$s&?8 zfMu}QFFtbnO9A8PTE}r&d<9SKNO||;|K_t}#<-N3V40~2!5dlmXrg%1ATdHHd&U^C zGH6PAq|QKIgzLpl@gZIm_V|!&^ypK%9bgutvT;ULsO;}>st3|h`W@&m5GO71q!3c$ z(bwm78o^g#_@&OdptK#7P8=E=J&xr!08HqQ3vnqE%%jUavxEN`cn(h9k#f|Fs=dcL zI-vaO2iYV2ZKQ6;#Znq;pO^A2W^8t!8Y7{1wB)^j zQ+^majxC>+MYzEDta~WsK_icLq)g8Ap zu|CflPb53?JXb?IfW3XXv*+dLq2(keX5}_V+<~hc zBtCmx{FDr@BWS6A!LbtCn~^6nk=#6XNBXuS13u7+HTIZT z)5(Ij?h5H-UPwfauiTixdEk%aQP7!Ufk&4<(1NFmC&0;SPmZw0ZeEn)c5%i|6%0Q2 z%i;Qdc1pY~780^L*%dqxEsg$< z1+Kj@tyqaE7t*FLrMNw0D8DtSOqr5x-y)tEwGgNE&CG&nt_@G#^_#Fl&nDX%N5*soixvf9nt((Zd0 zDl@vHL-uLe{%W))$o-c3!Eyy~Mi$ZRyFqxu$Q;!p1Rk~Ien|AO@MA7hnbpZa#h@=a z5e)V_b{$zMPMFOQy}{)T1<$K~+pf*?w0HOqA5HUVp5q~YztQm7j2eYZ5jUb-(D`uO z5%Nma9&(xie;gkVb7>c$(yg5nTE3P~7wel3J;3D|jx4sX)l5YDZ=z~nDyd2XX1%h) ztQ*DXi4QC(3k4(r=VShCzyCc_#Rgzb7v}po6dbqT^}<{{BBZ zG*A%guuSqlvWQvx-vyopX&QUS0&NtPc~Iy$N20$h7Mz`Ol{E=1qQPfbCn%GJM}4D> z+m(LP>aXCf?MqhxX6f09BLSudu#sIhl@K0rBqDG58*kb?K6`-7Fx~J-eI`s{`FI-3 z7&uyIwOdwo{*oE2i7#?`8HU{Q!0*@7mp;-;Ko}*+p4OoIJn8Wjake+iz$8+=Ty1P> z)mr0yd;w4TR2|c5Ox`UXT&AiczdaWVe0M7qGCLbZeXoIMR(SRR2!#GTV^d8jgQ+%& zZGiiIRxeue;`$crU{id?jq7=@CdHZt$How3$-m@#6kKzs`!Cc*Wboe$8J)&2Kqa4_7Im~AhxY~ zOM3t+_~gOj(sPWH*14xjOB2^Njj_zL!}@*L8@iyclLKB4JVY7T88uos z224&!2*F$2hZYso0rHfo5qHhCgJ2L=V0(U&%9u-ABJl^3w@u$R@nFr(1|_P>!sCb+ z;5zBk*p|Q|jEq0wj@W|vft4RT=O##dbR>Co7qPN7htHcIZSPco2($4Ykk7Hak@#a< z3AiJ~@3jQg_O5gBgjSKm`p;0PM-o4A38<AM1fG7j3F(7DmIyI|O$u z*3j2_=!D=X8R+qA3nC=4ryQg}I$}xP8+oXO4M!P&I1KC2^=ggW=PE$*nEqbiTs-M*AZoi&`w&6P`ijux)G3tAhP}U7W<5)=j~g<(TS676}my*OY+n zcV}xsB|MohT!yxCM7C6lk-HQ^inwxk_p->Q$|0eonxu}bBoI#9t9ozRG!h6#ox6$m zsz@P-{OVlGC~lOf^KRu0iTx!ZSN&^l*KagERQ$IIph&U=D}33m9NOjK&bWu0I4ah{2pZ2o}PbH zXGFv2Yzc(AZJKc+JVF|v+u&?wE_xEjVg~!@Mo&cN7>*0Fl(fKB=hw&p!;J47kvVI` z$c)x%obb~tzuHP8(H;NNO_7${2#GI*rW?)t&v(?OPO>iKp31Bnq!L!uAEJ}F$Vl7I z?`)zUsw#Ek=V{(^Ij(DMonR>gfgEP6v{CXTsg^E&{xM(0aVf(JI2+_2#CUCX<)ZUS z#llDh{r;IM%bG5vPR+aay)EYnY#apje2hg*#m0HX?+TNo&%ketRS@GOYW_wkGqHo4 zjpv2c;0p9MZ0EsvGsDzi%cW(TE6T^74HJkvF#+~0KSq?yOZGhE{IGtGnv)PREEoST z{?1N*(elaQAMPa?_KbxAp7E+QG{{Auys?XmAk*di`wbXvp$_pVV(xlNt{j(6 zJs*)^yZ(Bs4`r00jw@V!XN>3AC?;Xtn6q}Tliw`tS8;*15S8O324?7JjyCyfoy5Zx%09`6EwP+o>=&%6~LCuH3f4qiw!^ERRyjlYN zV}l^eodymM^50~hS(!x5MI+@n*=qRpd z3d)@1QDGT7v~ae*f@jb!C_{9i0+}Vz(`!P0B|5@^PjU&{X?UR7-D_ilk`$qTGwf9Bn-@pCH8Rv%e-5sI!Eut4Rsdf|)4TE1s4ZhP#LmQ<;_fA0)$cPWAAh8X_K^d!@(R&ohdLKgyrnDKQ)cV(u@oC>$(SC6jAnsqHNAKs#j=T^|)cc*!B(4P?y{7)=1o_GHUan@#1{s zvvc}4_69^7@G_3$JOF6sPsLUwgJF=+^=f_I;Tj?!QgS|`p+^KP@ws5e2P0HRkv%R6 zJ@pA+iWH9=fHE}%Q2J0KfIr>qW_6mqetvx?VXq?8aX&9WR=J95iK30EH#W1fi<0~& zq(cO~uo0+xRSfxXI__Zt*t0#<%OZL%^MIku0aSq0$LS)Tk2xZRWbW`PXDcp)`0Yyl z2Sdl(`M4U?YW$scW=(sMn_|cyv%A?0CG;QwpVd2u1Zi{7|52M@Y`ghzNMSkacser) zRfBYyxyGO{BOW0n$(Pv~99T*jRTABit&>*pzY)Q2mB^3wJ8DZg?^=O*J(F2*eLzCE zQ|O~;{(L!YdhL)AmkrkxAol`Pj$x_WS)WM>_<8-Iy?DKCtImU@!BYU>ngllpa{nhS zlUOv&04!w+Jy*}CblmcFrc(bfXm;t4RBS?X7wEJvU;Gqj9jszmX#YJ<5O`M+8yl;( z?|LB{he6`vwDw|aw!s-@@=J_EE;e&^gw|zfVHSf zYG;-$`L6{r-U*f36N?wR(&EyGF&Ok}g#|s8FUV9T{nc;~FzI-c5>0wmS`z!SYSpcF^~Hw&M#r!DF3P}OjMml=0MlrBz$oeXZrF0piTyeV3#C=j*ou7DqYUPtHc!Rl_+K zTXB$)`J8fY#*Am!=v-PfU8y#6;Fy|r0tDJq=XIk@0-_%!VF)?-Lv(AUN2ZjE#18}m zrz-=p$)mOWIz6=9k=#DSywJ@2tV@@&)UWo~SO$=->Zj-CZ^r`56_*Z~iu&BlZ})5( z719W#07HIjm0yy0(#y%lSkOJ%@Y*sfxjO6~FiriZz#}o!dDq=ymbzdfo56T*~1jdMTs@{yUlyM$S zEl+g-n;!5zJqMTI6oxGdM-vss#q1 zZ)E|#V>zf^{eGkrc09Q~PCq+~bYRbRF4FJ<3;O&EscxYgYe07FrmU;uwS>_;&!H*c>Ek3oqxUkuIWZ$=dC8;$)Sk z+$J|VfLo7U0+b}CeS*X3up2rwwc35i0}tTW&QRZZjMPNG>NeFt$tKfBR*d9cU!#W_ zOv=$nEwASSEkI%TiOPJ*UDC|_r`>s%+;QCSzW2mDe2xs%i(swXrGxr#V==@66C${X zD^J+g=v4|GOUxjuRBa)fYz=@DMp-__^=pUEhpalXeD+2*!**N7X0NHUeo`*s|3s+1 zL2gV)0(Y5Nv)+r3h{8^czb)r&zt?r*I;DLvW>!}%mk=kDVMig&w~9*u$a0kVPoj-! zp)%TRPa!m*E16^fhH@PaX8&lA)8yAl=KUso@gG29Rya!Fd&jq<$Y3f{K{E44KomP& zDHtde@qbf9F{8g0a`DqeJ9Qcdp1g1$zDH4X;j=t1M&vHr3dIjP3 zLtR3pfqZN%>^yi#idFoW;AbVi13ygWVrmO5N@k;Tu%z7@UTs;j@v5ne6?{G)E7R>O zG^0;Q*4_rve%TUwr^(CX0{M4+x^GDUL4a%}X@}4S!J^6__PBio+}HBBV%5ACmOd5p zdzV9aZ=#=$hFfxLwt45$09@h*WvAE7IwP8D3!^mak3W1p8V4RoV^)3GX^eTRM(e}`m$J$&{IFa5rOKh&hsMr zW`m$rj6BNcJ5%yqNRV@#Nr$@fU)n-ae=#bR3GBGU@M#s8HSsVlk6Dd4&a|YOG!SZF zS{s$E;XmR!KYg0$8pbCjq4=q)Vy%8cd*V7}qPu+v_qbgoCZe}XYP_Z5J7ywP1rU_6 zV(_u3PO9D!i}CBy>g*eB^kO1VNISR>7LF0zt=`n1Jn~&_5G(uDnZOVzqGOE_LN)}C zo%V#LWKJIk*F_u6I0c!3U!7#@Z7qazjTDyf`~{9D`@+o>A`w-z^OuH$ZXDk*F*47# z`-;%gxGi;b2cly^R_hF9vXYT(R~QOMh4EjGvwV{u<_quIoi4){sJ;bOOhh<6T#IC7 zm)Be{-Ch*FGr(Tm@R#l&SHp_hgo4fFfbqTjOf5VZ-Q*M*nqYaRo4d^^F#cZ=-VMKI z9;AL+>uQ~7@s>w0wwt@uL`hk$BPj4lv`kwYEYP-t%9r?qHi@{J@k~8(7mi48%?}3Z zi6e5Ede2sG>jk~}X_m#M**j#f_J<4U(@y{UZE{}Sh757~uCG9Yc6Xrey^0YY1o~y) zpwD!(d;VcQy|DR|%r`R|wpCWPjBZ+OooKn-iGEtoUot_#ap}&@!WK8;1ak+x>|p=j z=GmF~@0=cfMm_!u?DNr$)u(Ic`0U2gY5oZMsaS5X=L?g&&t?_aBAEk#eXanB-7vlj zN!^0%xZtf2mc?>CKDASTHdqvIcJ%dr+A^SHvVl928Hkbv>lvW8THj|T|8Mr|X=U0F zD>`tsSSJqazQ04bu1(TUOk+LJi~eZnplkE&Hlt2^i9bVNN6_>Mr+*-}dQM267=XNy z|M;gSsB|T&aBrEG))szO^BglI0gTxa6;wP=JpkDf@WOrg&D3}C&1^yW@w~x#b`Jt4 zp|fk7rrB_wHO|Uh^1I$=x6t=&q@!xt5Z69VNc9&3CgFuY7RErG91|V z&+occrqE!mB>KLgy~^z);2wWT@;5GUQO&al$7+5>&<|Yvrj16@O<)v3 zp-td$;sI)WBL5ExwV=<4YlUpl(}a}o(iJFR@MPbPIAkgC2}PyychCj$uIx{*qa}n9 z#;!jMon_5ZCkno5$Bp8Kt7xcW)VoEUTnq@-d9!k|hvV0iiNF=3Okv7BA6w(=C7q16 zL`1|{@B|=_XSWFq4BLV01;5gs0F?b=6mgxAY)G5Su~S*fZiZzRT=MsoBh8jFnT+6$ zd4ps^XQ-@bJhn_z20vHi-~)O1p3EVq4m%4gJ|MQU2GLx!N66+E%s<7s28Kb8y$F)bwXO}3jHawKg89bWI~p1 zBgbXJ^Y~xjq!chWRC&psLVPhT->`ue~^#vgwTFl_}nv;cyiuCfwOmo z;Oib@MHnDVDYC8@p;`I!79nO5jsvRa0z*ZhCZQqKQ z0d&XV9$>tF-3pWa)h;L;>Z9TrDxj`DB0qb`&$u)mQu*Ca4&p!wn0N#q7^fdb{}JNO z!1}?mP6ye>Hj9(A`kGj3PH3a5UJXQbV8sO@7%)})q4z07YQ6|knu9cyj5X?elqt9K z<;z}&OH1*{lKk@+V&_+PI=J(1CJ}<^kff-?*DeyfS+EFZ_#nR8kd5F+AR=5Q7e`=D zKkwv66nE+tty=ppmSc-Zni3VTOBAu?zk}Y3?#^hH7JO+;UwQeR0jC8I)d6Sd2*|P9 zQmLM2je)90y`X$+{6(ob(&fyHwoJ@Q_UHKDK@7isnbLV`uNNZ!>k^~bEOvv~P`Vpj zZv#H`H(0%I-JC#>Q(Fn|O8ky(0%IEY&%=?){y;nCBNSVLd@#%-2(h3PrYvf{t{$4l zd8z$XYE!DC-LT=4ushldd_@}?TS6o}3Xas}BR(~25Fqp6YR;|5|E4sh%cU&bx6w+7 z0C$ht;n7sOb%jr=lSxi+k(mqrUdgX~VKjp{>Tk{%VhxG%wN)^!t$$@ZjUR{9xTPhA z^@s1f&O9+Cr)>2&I5*X;{%npKbP~DoYjHe*2V}Nng|xp8w_+nRz*Tw!qpyinL-B5N zkk|$jLz7gS4n(l%;h)q&1$*Ihqxy`!q!H=e{aqV7j^^BB zEy~0H{b;#!o~78v3(zMil7`EemZn`d?zkrXMxgPLuoF%-c9U;NVT1IhLA28St5Ub-=KtDo z)A`URko5R(;pmVo-U_acN1&?tl=*xAL<3xv-uy}s47oofh5iRrGP!zfp+L8F<*xTY zI@f8NyLW*qR_7-bI7(A2hzXQ!%)w#`$8Ci!njEDWOraHl@%=_e}#?ANKW8 zoxY0!%A;TXaEItK_wd`n1UrP{d)Buf=uOb;HbU%i7Dkv@cq3%ZiU`UvD1fzyJUM0%=cKn|3aDH=GrBT;X=kXqE#3D2ZF5oX2U3 zn0a+l^pk>MzxF>Z4*?7*ibtwqLs<;>jxGsGR5Mul>l;oxbA~6!d{^JMisikGO|H0h zAKvcP9R|dY?;PE98poj)*a6rceh1Du>~RTwjWYad7?|o+A$X|B?!Y_SsLU0%r`8k_ z$S$Y~Tth!~%Hsj4LdPZkn$mgLIQFIdruwC~6CaG2?;FFu`yC z6^8y1jL1iFe|2qa(6x{%$2+B>W|Ti0iAN^Qu4V&W^+zBm0nBcJP5{1};5UWlxy3F~ z5V}D+w=aER#znfFmA$@Y>mpcy25m&*H1C+0gs@LIA90?44Amei(l6fcOurIEz;p3J zCA98b^vIDAQOxF!HqG2>r17Ph2ni`)Re{x&!A_;ysN}4lXpM&c@Vn2V;OcFrdlcIu zI7E{4OX%1rZO5!<^1#I4f#1#pPHw8yb}6LxiiQ>gn9GPx0-d#@aPcp4PTdM&{|@tZ zA+A-_Wde-rmliHZ+_-(oI~&B9bt;ev4G71YQvtC8(Qv@csnz5(_|7Tx*ht#?3vh-A zLtxpl#|F?y$-?vEnR?YXuV69;L6~{ku6wfPhp-u#&BX2k$`pMH|`o zpPWJBkEH=f%MJc3`i(JcNv``=Bbx#UCbUp>DjkeNNAoc-{s?N@znpUU6Gqf_g@RLn zp|YwzWR<%`p#+4uO}J9YPQUp3>vFec%e&|`xCI#DmB ziB(FkKj;OE6@)>DI0%%`tpcN6=PLtQ3w1@utjdC6q7fZK5+IPP+g?8NG{PAf8p>T5 z0II@Yl-P2@$nhI#rYK5)w|lj4xcp4Ix8F%a(Xc)ls!~8-t+H5n3ZV5QA!n$E8k=@*N_lG^!oFeRyR(B zhU-K@3Ckq(9rpK4R|t#p1X$oo58p@0SQ8>lRons&)5TF$(+v&YV;88bk zD(nIPAC!uT_AxWFl6MDseDa*p5%wmGSU(!Itkr+rekAN(HpwgFH&t-;^t9C!WHSmc z!Je_pZA;xM4Q3?{GpxZapg1b%8AY!~3A139`ss=S(e;q}Np9^3jfpsVueJPVYF;Sy zuk8}I3FCp&Lx(C0TXO(jzX6KzP5BS$Iy;k!k!41-Fh0(10yHxUDQ*htB3W-akdCe5 z|1Q4wF4tyBu=b`^y6a&YO{z=$HVF{v%TPqXnBzYWB^zR=6>Gw2rbEmaXL40>sC(ts z!sPuEUF0J^j!v*j3J?v7mc+0Qwh_ba6vTY;njB?iy|dL~Az+^vYP^9TwdM~VM44*A zanqA+Oc6|vQ9GI8(O?DT*!igRRK>tDuDw)O|@4>U+aQK}G-0jD#ElbxhDq z4a(v=TRhR@tiQn{@X(<&O7~tRp;E&7vzw5CDT*z?G}T3~z!};~EnXJXxFds&*lyXOG5L71q)KQZAu(6Kxpbpo9{ z*oP{M_SMxe7A?O{KgM7O6jPfPq**3k0HaJ*fdt|Jh5*2loESRpo!ODm7vVUfq& zt$6B_0R}^%zOn$lsp{bRLE}Zu=u^v+7IoN;>JT3Kw#H^(bhW8_iHeY#^Yp6ussMBb zf8kp~s9qW-M^BDMko-FfZia#=F(^abCxGo-w;G85!9|tv9a{=-mWc;E6n+9$cv*o{ z{o^A3hcub}4nnUa#Dc#hAWu(0w;Ns#W{x4HIJ57wLJyr(#G#-My|WngKK2|A_Pe@f zc97KG`5fSEiMHUm*rb>zePy*BZFiGq`3QI(cokiBDlr|`Tlk*-rZf=XI`za1l72t) zPC#tz7!gE}CwmtU)ACLq*6Fo-ZI9%hsGZ;Dd*$K6K(Q>(o!;y@T<*0S28-ep0kMo% zb7(u|v;*NTubtke0&GVDDDNEg?zjSl+Fta901^u)JY$#~DTZ--9-4Itkzd!zEpx;q z)R#C&)K8@X0|dFFYJRMNEnWqUjSQsCQ~$>crJ747k7wzHX{Ia&UJH>T(X!OwRIFBbK@cW4CGpYR0UrDI3lRuY(EKIsle1$`AQE(y^*3 z@x4Wlxb}stZYUWY>t{!kA}NT_xW#&M+cL~ojPe{#RL(+|YgL}6?0)jfrK~E#)d!D& z^jI*XjLQSG8c=*mejBP%-9*r9n|TQZ!YF(`P8lij%n!PQ{{Bn?LM3+;HPA#NCy%0) zan33;6e2s&MO`#BkPgd#kj?01X0gnm-0#dwCp~fwkdU&1?V}Z#&m5Rs)j8 z+h)+^y-fQ-cfS2#IIg+`eU^|4{;1ShYv?JO>&D@~hu9*b0UuuHuYY(GbvWYnQyb1; z-GIPRH2lCsDsPU@Vpzc%LQvc8uCvb6kg*Ti3S&saF_`FNS$gulh9BlDmk)OrxXe9+ zDxdaW*D?H%fDGE6>uk2t?p_wmDu&k-WZkQdrnyA$=s~P<$+apF2rIf!T5D;4-R`3{KxGTjEK zs9aANOlUy}q?^rrhsy`^bTbeZ#ANwAI?I3%L$kw|oF=AhuSf_Z8_w`eCY`Nli?Y(3 zpY5}>SOg&;rw4Rpp8#r`jJBUU)i;Y_@j4p$p{;pZ)6rNA2wic8&&c^({x~Mgj4iwa zxGYT-x-T7S1*9l4)j5hKmyrcPCmz`OqU&tbOv>EZRg9ennd8vq&c#~Q6(9$;GX$)$ zP2j#OIT*8`Dsjsmu4F+KJxAEcNA>uS2~NS7zKK){nUymRYW3#X{DW8RrX@ymAzO#zzcqVln(ww9=d6}xv08m z--aXoDV=v|W0HIXYbn-?<=4~46-YyK^u;d@GKL&3B|rZFKYekQ8a!AGF$$$91U4iRQU99 z=um4-itMKy-}vx#xpm?O1*ebwPXk6)yw0b}PpvVu2uzSEsq2ypf~<=*fL}DLHhQw4olWrGUcd6UrD%Y&#{0 zf$Rd=83PY3Z*Q1AYwHMk*Jjm;QDQu_R(&394bplM{JKk+m1nn6uOAZoT+YfqyzL69 z)7!9)ivXGUC8m~GtM+xg>kzVlm=(1Neh!?>A8%5p)#IaL71hHdlCF}S5+?Fyc|x4V`S_%}`MUWsX#%Yty5D_=(?W2~ zjvT*9R9%;M6;tC2hHxm4uh~nK-6)yLP1JSp3`i^F@fl$SCAeN|<|#;rZ}MHPBFwdy zyPGN7^$+Hgc^;t+QgvOUKT8gjnpK%!IR`FHRxLYKgfjzH^*+MVXub-BOM;hi1f)-* z$7o-Q`{=nhH5(VPCIcpm$0mBK_J#DEKsY?=Kf_!Gw*a6_tqfB2$%yNHo|^iM)Vjp< zW|}oJ#~P^}u|3Ms3GWmx=mg1X?yJ_M;5APLoF)WqnBfx=JQfvv7m^ymSvz%QW(NU& zOxtZEai>UgiA&x;Y~3i4}2%r)u8d__b_<%+$+rZ$T!rd}fn$?vZrF`EBeu`6TF zh%D0^*sRcD$dpeC#b_E4Q+Y%Y2g2%4S1mT@k{Nf|cfdWqb1jFM`8F(Hx|t#G5D0*? zhXzXOZOe8+pqnEw|9x{`Gh=n@9VMWW0!TTDMx~o`3*7SYBpA2pOhc~xP=06k|0N(h z-ATHcb!i5eFY&B1mxcet>*9I_zfJEwoXpuK)UYunuy;VjxuTuzR>IPi0Cr(yAWG@! z|0Wp5z{$`jRag}vzn*6J@tBCbDH+;xy9p(w$8w3pviZqp*zB8PP`MkL@Z8)Lzl1oD zMNDMbQRj<|_KYMFA;%8#*$HO)51Q*Bd5W&UcMg^%5|9X8v16R&2hdar=jT&R>EiUQ z#}5N~a&}^14$YU7kd9WKn|=WAYtgp{6m2q-fs4l3E>?3ba7Z3E{^NM=1>?aK(uIW# z8oKt9zJ?&|#p*&w&0KU5(4B3pOe`P6mLCtI=nUf>)Zo34Q?=&e+NZN3 zz~u`>|8$QVbfi}I1}ihMykTm{9g$c&__Xg4%q>e7r`}oMGU<^~u9Q_b$c(`N=uemZ zhLQE0ibG z00002O-OaqqvON6L@^AHoaERCAK{fHcl0e9s{n40#o#)(Rt!EGy#^GhgZozj!!7@1 z5f{!RnqRAYncK-iR?jCVi}+0oj<8^Cn-NCU)YKeZsiY9vBb|gx&R_c}zs{luM@*^- zBBLh0)N@RrXmt*SGneJ)GjCbZ`O^IV6?4;y@qVIX)2cqxbxaS&D!a-+hq z{LD!@XV9)~?Vxcqnby1NX1C)7{fP&X>)pDOtJ%L)gVq{i;s^Awl&k3@)Y4t)QiM^I zMID^O^t6d1tR z$*4e^4#t#HG_>BQ=>Jk0Th8^n2;6cZ?w5#>t9TnWg+HVW%0va>`c)nS4K}C=SB$I> zfVlLM<-Il^Em`KBz533RuWFcIH+_^_PRIDaFe{j<4V=NftU!RffSb5z{uYo=S^K+uj~LF-;z?(zyL?SMRw{lvBuhtF+n{~!nV$If~Q?)m6aRcW~?jfC8lcm;X}zdx(oaqs)d@1)qCcrIoYkxk1a}Wo)rEI@ zNHm3^@80W!x}L3xmBh#I@Nf$Up$*l+C2`EG+)lagovM^r5C&MPoR-g34OH=#b{Hlk zNF70C5&@MfY6Plj1ri@gjIQW02vkBWN+7%z2tV5c*WXaKH(kT}>X^DFT?GH}UDYJ4SxBxs<`e@F}>fwObeS&p~>ZT`GXEukyUNCr6HF8c0R=%nI zeVAIMkj542=MCyh$7h60s+{jIZ~n6Ks}z40G8!=josZ_g%@62jYdY@F|9lgHaR;Id zg=UHhhSV877Gn5wg=TGoHwe&FznyJT+URDOD-e8`0^88RG-YltT)*lPtr;t}aWQx<))id5r_ zdTvM#QK7_MPxcVZwx!cp8zP2Wd2S)=~h+Qq>#BhOt3xWqjdlzWT^F2PIhid6E!itjV56nAv{P$O=YWYNi`fs1?(T<=} zF4wn7{jIqjawx-s1%E4LU*l~|It=@ryX?aKK^GiuMd=ui?maHZLdLmxC7$U-C)NV- zxUupIw7EEPhKYNfQQ0?$@DBxK_rB7Hny*OI)k9;x5K_wKqI8Xk~HkMAz z$RK49!mZQz;6$&e0P2Zx#3U2RpgT&r8Nx;^9qwZzMKZ!Zqjp7T3j*Vbx#l)0^?IZRHBXXdm@97 zf1Yfi?bs&r?ISYW9KpXO9+gqjiXj>krU9vGljlIAOY4CJH2{VHK$7rbD1-n$cXGdv zknl|hlD#!uB6jjzGr+_!w~pY<0qw7f{$v0E00d-PX?=tqoaFP4y#_1v-s?({_LMV3 z>n3+r8LhR-SR0|v^#okgQC|r}xUT&60SfvWT>xPZEkji9Wota-tE4f4K;NvHE(@)i zD#dnvFvnhtSKByOB_>iHuiWTsGM(JT0Fx`3FV!b9P*=SRsR;Q-Wf&oyb?&=7d0QC5 z{;p2m;Aq0_7KS9Nc_g-k3hh;4;Ir~|ho{dlM1m*8a@#^nyGI?2dlOvXD+g*JC}f}0 zlu}+#KlBJjy`DX zLMu~tA(VeAp=L`pdVlmDL{9^;V1Ps-##CLOu}HRR6+{?Xe9mXAd#Ew_v6X}HNLOgp zl2&_P0d~HS)9$aDOWnknXi*e(G@;dtXgpUc+X&6wHUSr)%YzAyVwXD#YX!PE=9km(s@hw!Bl zmr{By;j8Hjvu@E`{R%2Z)%vB)#LWlby#}I)noV0JhM)b1I>@%UIvl;L14B9DL(nhd zQa%5ULEvEIB*f0RR3ee}nl)6)T5-lre-8Z<BJ&P{>m4t(4xMWmZwnb za6Y6rK(z?t_e7TE9W|A6(w)39n2FQsy5OAE`a4E2f#?sq{o%W%C1du;)YzBtZVw2mVROb@ z6=r2NZjN0I5E!3bNm!w)Q$kTtvcJ4#TmY{|ga?M&hwB&00wSLCj3?56>)u_q7KshN zKo~bfJO(46)+2x9%HaSC?nPwEpYE#xSTcr5SWZ7&b?d@z3hXba-bValL~@Dq5kFOQ zreDv<7}ESa0slwik4C5`;d(`@!x&HoJL*q%P+R#=N-~_{fK;Rg-APNXjA{HPo=`al z>E}gCgZU{ukR*m0?q|b@u&xHu=u!9}*46o`mBC@@Y{yY$kOMoqtzQU`15X8_YvhoW zsuw?8ciQ~JO*z;UzZ^3c2y!oOlgv?q2^H(!uTa5I<(NF(Px_O7bn{FD3H0=Vzt_=k>sH7<_A9n8NUH z4m2)ejg_TN<|dq54e`xQB)E-9vi&S)AV9xp)#b<@UcI47;)$f<#!!kD%8Ol^*E$ zL9&w0JxfEF*3hBc8BbRi90r*Xo^s1Cu(E}0+--@0^gb#TNm9Va! zDG?vU`R^Po*@5l)1E=GFvAq}Id-Bt-D;B{j7j`y(q1Cg{L0kW_9VE^zglIO8m2Y-_ zy}uF@et9_f>b*WUXxa3c3f#n>-Ujq30S-JHutgpU>juwy_>Ys^Q7k1KZG1Q8MSiZ4z(K;n}0>D#-7%w zaE@U(f0Lxm44FQ0e^8BAJ}yrikXh&+{_G%Z(a7A&EFfStxlLFhiRBIL}8p-ElS zIY;(Z3yytF#?YB@nU;<+->JpRmaO0!Q+w14peTJ*ng{;?+>XDHjX+8`H8VNWnb~U` zP=a4A2Asu_5bo0Z=1>_6I?fYkh9t{U1&?15PnSw{c=kIJv3KpPvva2%d4hpFB4Wl8 zzNU0~E+EWLqBITPbA(Bb^g%yqni^2NAN4AqJP5S>IKivJg4qHE-88n=S;hgJhwP-3 z5gy!kwYULfB$to9TN69q(7?(4i(@Rtf3ARhQ1l;#4r!tX7d45DdCH>ijCzam^oa;T zhu_7O1%dZCZhrA6thrue7Hahka6ghsl!+aYlu>vzeTP3evVf_om}e5E!cmird1e4s zCiM3+|Bq_VT&t?)mC1BU?c^>jj+e{VK=IO;*vC*o*Ge_3LTO`mN0PT@WV$fo66VEr z=4?WDe0lnE#6PRPaR~R>19=8zPeB}ECvNS*L>yj5)SC1a(=ZdqMsFg(PH0^WA7&ie zL#yBfqUn1STOZ`N9O5L-MX%o>E*u>FOF+aF+5EgWl%$x}x(+<|RO1(GfA!SRVqd`U z7G0Qz4qS$tObJL}@+H>OGE#mb8^u-~p`dyX%#0hxZ2v3N9DV@a1IBPswMyo!YReJz ze(;qhNIy-!AF&#{jeS)?wY4i&qC0j9)NR3(Vo+Yi!(G-&kI_QBE_ zTYTt7+Cy%14137Y)=rM_xN=xXf%snZYF&i<9fEjdvoIwwRRfqi=5X&T(e#I67Z$f^ zpcc$x_TWlzLuuba54!S`geqs-ow|hO-_;MVFg%zv3 zIEO`Ag?5VRhj>h}2V!$m*{9ufoJm4i+Kr9CL^Q(K_?gy&=G}-Hq~_2zX2_E$ZFYX( z0%xp-{e)wcmGYMaAAOV8Ch})#Z|Yb3QV%2x8@m#MJFLV5+@9|Rz9Jw#GGgY+hH$^ah&C}CEluKS85wzd?-;R&8bboIGieV1?X*-e- zcU;_Ph4LBCgJiZBb%a{BXL`tCG6JkR^%%7b1d9^^Ik?DfT_ygVe`tbfu;@G9Wqt3H zVF95~_JNIC3seYw21A!nwwtK4$PyP_SHYa#&qf&ZE4{)(opOJ`Y0RL9N$yxl$0CPt zQ=v>Aa6lF7=kYfQjxE`1*sbpp5UNXh>P-M2-Du-_}q{fVgs2R!6jQa zw11gXiYGh$MXY1h6?zXQU^z@g5mzchU- z$v$zHsJ*%DLI`3XexHs(h>YIW`z3%+G)I`HFb9mFDS9s}G#UhV^ zl|9E5IE$77VqSvR(*jd?prb_&fdyXxh5$g4@Nqx{03df1a(65zwgp0ck|I{NzyJUM z0pveT5A?7a*=cnwX(}YCQM4*ISSuRbSUtsw_NH)hbM6l9RL6;)xx{?-x>fRha1r#| zPdO=4zUL$ZnS?-WTqPR~75lObn9!EoSBKG`39!hfn#*;JbK;Cvv);MEEt9u$Cp%!i zSm`07gD~-J@0j?@q%8oimA<>zMtzomCuIjR9F3Tspk6HCGTkGQWQd%bdS3qSk0MB! zmkZVB)C^ks6qoC}#W}g`7g}C;dj?_08Rru#e$t}T@{W9cORSjG2d(bs*w*b?$?ZZP zxTeCaNlHqlU)#^v3ZD5^rktL24uCd`UPGw;zqC zZbsDuohKH1h15%xXnEfupJ3!_G{W@vE&33T%7K%5r14Wu#j%p`j-LJW5+;Aoc*pUW zc>I#E$~18Brw%)gtJmFwF_QBH-52aM>(|R7BugR__G$FYuHfQe_nbBUl~p zj(w6A--&s8uX#X6xQ);n!9YEhNkQet!<3(W%Wh#c$_9J_t=3=yUq?_#0;@$K$kl?@ z9!oFowgQM5ZYGJC#T>+WGpkeMoF467H^}O;rEU3#gM&-jBIn4il-dK+VQ~r`A-V=_ zRXa*<@KXBxnkLBd3P0g!wSH2<@T;eSL6Di4Ne)#2d-(7?Y~t;2=;398jc8K6k>qXm;H28h#VWTg|$>y#Dz8&0L(4d4d{w8&qOt_Ns% zx>IkVYVQ#~srzXb*m`%SsD)Kk9g@-yE5t~NWt196cBMoSW=4w91y@QL!8hKdX=0~2 zOR3yss*MBQ4BT**N+H;;G-|i;XL)I7cA$-d)QFyd(*fZ`rCjtYTOEgfNJ9H%`ln1K z5{!Gp`EfS%zJR|PP}kB94cCFWw`jPuN@t-NNOju2 zeZn1N9ykRP;ZZvfnHV*9utCY1$B8Quc0EPfSlJH?Wga9IN$zShGGi^4SoGc7z2-~m#0002l zJlRY>!qo2hp1V1NM$5;X{yLRS7}Qr_eEdS%fIl%gvOb2aZ zL!pyk*}6vgLptlAs^4EZ66nW4l1kN$r^)kjwxlAjrD^0gDw=im{FEgyS z3|ayxMa;zislRr68t*rSPWTUjc2#O&$?-D0Y}x3AztO}p<)?_itVNiZWyD7b{vCItx{7_k9BdbrC! zc$0n`%*R~Q-9DIC(2L{{<7>Mv48$2aafFu*!tM@bvkl@}TEx}q){J~7WUSbDw%A(2 z)^vRfr5v0~J1nGLGc|KI6l{{$hTBCp>jANpZ?P!o{4GDMHZ8zG*{UQL4*rF2wC6$F zKmK0@CMQ6l*<=PZT69gMUS<750H6bu3=0ssuO-y_%WIC6%jedz873D1W9BJ~i)k8i zWN}Ri0T6bJ^O(l!NB&d?Pn$kCq4%k;D#PA-ABl>aJ3$LiO%y`|QdlU!eY|Hn|%&GK-ylb;(obv01y4HqkC5Hd>%86S|^}~-yG%lgZvEn&{U|3xc4kVOW zNo@>)TjTnQ!PVL$``7I0yA7j3Gud@BF|0003jHT(FW0UAs# zD@8_5HcgN5h4NMOhnR&JKR^^LyB#fO%B#=>fGuTSNPlW<$l<9$YI*sdN5=oXWGT)h zqY?Xb0;5-no4RElujCZ~EQ8-uaI-BbVFLVsPZT@-O-&e~S14v~pR$f-KS{HcAf}jd zn`|e(0enQbiMKLAbQ*fTAJ-6N1f zuULo5C*Fy%JaHyPY3dbvCp9r64zmI7M6J-bf24RL{*9XOwX*lsU=3*Da6F9E;q`JG zX8tSFzpXGFE|TCC9?Lim_wS6(Fa^3{kni*+M?B|kHaD!P&LBzk2g28rMnc5OHRLoB ziFEjqla_+>*<)XaEus4+Q0uuc$jd{>r=avzdYPrvFeA4j;=zw9j;-W|!OYq1lfCC7 z4zvKx;5`Xcr`Jp6ckvDe4;_TKsoaT_FK@v_3{gP4l(4vl6^jtJl(-hGPL9jjv19KhZKk94kn-# z^dUe(q;@dY*Y%iVUS0EsSOcN1m4wtsV6x)KA())2`G?-iq#h&ywEh8NinqL(McQ^p0YRGS_hP^ zO}ap%M(lwF+yI6EK$7rrH3R@4cS^Uc0IBb^{#@M7mL;oz0007@OF@-k(=0QTD`r%8 zJ0uYt7-YFFh|o?AgR|RF>7OH28W^JN8=yR><5lzS+;PB@MH`8b!(uZS%!lK}{H=-O zwnbZoaje04tA)mWFUzQB-7usuhrr7uMeh!&dn88S$Ug(j;ph71HFWSgkA>!Pp?N8$ z)p5$BF6tZ%47kPcyBzHbaJE2p6&Qs6AdS*=r&A-!WC8_Xtz?&ZQ=ivcODXS99e)V2 zj@pLItD_=Om@7rMracIO^23|NA%%8{MN6-q_@fzc*0e8BK}A2VVG44~FOxt=hSuQ0 z0#|E!A*(lNjjIRXt~E5i(LK3P?83v9jroUZq^c{e(WJb#zG-VwvMHi{|EM|;GA`b} zC7w<<*1jmB=JXvSQEzmAVxsvk7dm82b`XS|<)qd@idWLtH?$`&F8bWzM$ ziwD3Q6*3uUI&^@v!&@uAUv8YsXO1L2f9+YjbMq8z=n@ek#1>cYgHRz$Q;m{inUV<0 z0DA|$VLo@m|4Ai?0j@kpP~^3d5>>6TCA!7RhoBS7Q*W2+S-rdTH(*jPCq&9YZeJ4= z3%`Kh4~p+ZY~A1iMQM}M)oJR&YU=|?lT&^6h4O@Aofl(}&*hZ*s`(R{ewmw-f4g+% zaJPWrT_Z!u3nIEX<8H&pkSl^+KN})aA{EzgH>cu=O_i?-it$*p`m0~&L_NE2Qv$2{ zj%Aw)_lr~=!Wj_+036Fvp+wO(_&c*+%#^w+bQku6CoJ3=isp+5em zSYzARGEIr8%6YL;P+1gFLVT7 zLT^Hi4BXJau{yF}VgUl5D z@^AUsMp~Cd=yyX7%tG+1=RXdB7s-Zo= zvT2EGvuu68nHeMwGmj4qBpbHZrZ9`6eVribNQyxxSYNUkwje;0oPJO0xcI7IEymZm zuvzN;c?^MGoQiL&#LUX3VYjm?W=|ctSb*Ra{hX#z^AjU9n$0?w=(G_kU{_F}y1_*o z6q_cnoE->iCblZ+mryKp5vdXJ z=U`JU{{q~Uubq_?y!()th1b5BR}|6UoY_%Hb)z!%$i-j%ns@pxZfQN>* zP$Y)C8LK+ZOE@;R(8R2z7$d))O5Qql)R-F__PY|)4@K${2oKOh=%!u_BH&aRA3ZE1 zW4j${D7*>zEtu!_05UOQ(d^2h^tk zEQofC$r@JV;^OwVS}x$6H}tr)TVUJ84bN{A|Mc_icE+BU(|{G7ki9qIiLt5m9T$;# z6RoToxBAc$Z%_$N~#%<2ASz`*StbDMhA6N0U#!J zsh?(x#I&;pe=neIStRvpEQHZ}b$EeLn!&>~$jE;d>f=Wcd#IN11Yf?ba3HH~w%)V( zaDF|VP8l@J`C9VI{SF|vHni$f)MR)JI6P+dop#CR!r9gc$a_ghXfamX%>o&SR{(_~ zn*Ly5|Bo!8?8bg4+%Kk#mvU@r8^EarzqwAkJd_|Yf9Qx=E))4@8)WtO5)v;x{zQC0 zkrVNf;#@wNf;4UT(UJyhD?znww8SHZ~QA2K4_@1obeyA zjR@ddSvQV^V-71Kj1Ss#3P4@RYW#D_?{s7uPn!uYyq50AIV6*9fx`G2 zNfa$Cx%_a4uCC6a`4SLt{^B%2c=XP4=0WV15+uXdIq9xYKi5CKY{yPC3mDELqv(WE z9{{np@|bqoZ@kUbWhk=?kK9N)a}bP2!4d5qAK%Z;9|t0+ZOi!}GI6xK1N{71*c0wL z3u_@-gSp0crZ*zSe4H-)9D*$3KJtB-y+6y3P65+34H{ybYMn+H2Gt+D;coj+J;~hY zXY)glN*q~ocyr(WBo#GSB~~YuND*Z?$YaGoq#1A@D>UMRVxk>GuT?^4&58nUg8bl% z%>k2f8UUYmC@C?Qf8RBac#7kX*so4&p&Zz$4{-ag;QTLubr-oLo*H=O@ihgg5ulkI z^PCI2O|4%Nlo+Bq@TgGxejqX0icOog)jOyYfsM~NPTFy0@Ww|6#ADDdE3Q9N zb^jRu1WW7XYh%+|uzB+Y4q)N{bxJopX{RBjUp-jhtI-bVTAtx^&WudGE<82mmTvQ1 z;(r*Dc4Rm7A}y#BTvKH(As;kn>Gx*LQxk z%J=T{b_O0M*jUwhqn@EljCiykvfEsGL8AVVQnRXE1Pe;mD&Kf!j7vJpH`u=}n~-5d zMo|i9QI9LFn-WVafpz-i%-$v%J~2=j8<;HWY3Y3wMUg@|K&_ck(f%o)E zEMve)9+K-P%EWMVcvt(owu)-`gVFyNOaGE2P(C~V=`8?2U{ckjLkNgNE#V(WvpmBB zPeDS-yJ>6CQI`SQ!|c!vdu%B7W9ZGpJf!r37HizWVto|q<$IRjc2q7 zy(hVPg4#l)^eLIOej3ASY7L*rGX@l@>F|1>#x-{F28{@-e}j>4#ucu{h{Q?h%oIYT=MVsihTtFia$g7vqt~m`&x6feJSLtNNU=biU{ed)e=eCrw7VQ zoI8rBDl8i8`gIS`Gxs84{-xi$*1zj89(G937mGrfwpXT$&Ep`PQ-wAkf{^@3lN;2e zi5!}6a7nyKtE#}>cCeaiN%>A&X&rewO1TtKCAS+sT_~nKpVS!7wFmI`%!}rhh^GP9 z95x*sil}Hx1nLwA@hYj8OT>0zbr$P&FscnfC~XXSYh0Fh2%atJN0Qu z0mn z*4=1gkq*gP7t!4i+j}RR>E0qZ;M)_~yfwFKDM=7s6SW3oM(AC_{9wJ+uwVCT^o)W%8Oi}xaX2_aY%zD;&;e%9S=(*oxLv*f~p$LL=C9c$@_zgQDFuh=cz& zubLNLAP2{7j~SsckM6B3NrUKwcFIl~(Y&^9P}mJBLUbSYG|qmq%(8$vt37`bvgns- zL>H}|%dvqEw)U9mpqxt1imDi^BFYgTA=EWKhv1F;FW(FI_mta@8X3k%9^tAqVi~}a zq~0Y21}dB&^y2EmPFPi`JHc8bPZg5WX&DPzDI{4Lf;QcB|H1Yjc#ZeQhu9fhP-h&L z&V!^u%S)cJ{mVw9@?G6P4(Y>m1&v4)1$Hw>}Wa6?aVOC+rH~eo;BhVqSA>b!b z^)Ip*rT<0ZU~ty=oz+$){63Susplg9-H|tmKQuH9=gKd10B|A_mzXn<4xeS^O@`Umwm1*-ot0XzX?GI!GLLlmR|qzSY)0;&}ZUY9;tS?fZ9 zrk7^!o@~DiRJg>R5ZC7uDqBa#EnGii-6=r5u4{)4**9b0^_#;rUwiv#dV6p;pBSHo z#+b}8<%P1{ftEq=Ss578>*G5|jZ&uCa0aq1WlUxlhay0l$uML?h(B5k%x4xIe1Wzw z1{cG`BiWvpmi>ZpY3K+VdpvhlDx0}&`tLl?TuKzah{h!=eK4s@iawfn*l$ zPR<)nXnd5P8DxiTZfhoR&`YyTr*Ez>K>xpI{1S5-AGoH41_znaU+7lFK>Ut%s%5%) zpXDsNg)yw5tl!4IY$bPlyexd+8^*?nFo2(Q`jRl1E8fP5*tG6p9;xyoY4U0!a%H@PhoLzeBg3&6`}z6$#F-5@lp}M4$L(Toa$@ zeJ^59EoUroUjVQkqze90(GCOnqIDz$l>{o=%DIF}>(A4uD3!8ZqTJ2K%x~3r)!$@! zDYkYZKykN3zZ{7T1p&85GV{S1B#n1aG1?6x!p;LFbhIQ+tFRA{O54KWbdo9&p{G1< z%X*(_Kdy2f_25rm<+#$Uxq6!;p}|=$7p?7oagB!6i1WDx6ndR`@(Otz!U3F4{=_n-96F6mc! zL>#b8qJ9DW?5^I5YYPFgfPyIC`25lq4fTwjGb`HOI~4EXMyXQ9P!V4qRkBHf>1J5l z#;n-eN1SStknS{dPq4FPKb+n1N}5ysLNOSBu)yE$(UGvUzPU~n zI9>wE9sc*AOP|c&V$;KF(oUc;60@=Lh8Z6Oq<^puHyN&T@ovRWLJ|cN>Y68RFNYnV zMNwY)IrCD^m2a8^;ST!{59XABMpoc7nDkda#-(O3c>;jzo^qGGyCoCF4uoucehXD&B-+7X^Fekdh}0Eq_SHg7$)7P~$Y z*=-hOK$MY~Uzy7;_@EKk`eih&a(p$_0LA)MTL!NJlJd@Eo$@2~if>Lhby6AH<&#G! z66YgCOeYc$48D2<(*EFry?(_p(_>N9XaRUs2-y}S$i&!9O(FYop7}%ee0Ltm4LT*1 z*2Gl-;fbSs;PWWRx|3f*ewG2Zs*Bn!E+LvnMCWK*1b0x}aHOw%37kdGCK)nRI?d_i z^-g!Z3gF?rMjc{fP>Imo3ABNj z)e6mva^1`XMJ4#kbCRUgW9XVF#Q{gsv{gZHLkq7pz**@!rgNs#Mypa~91O3GzNO9SYYI)==-LH_fA*G(7409dIdn!8d?N#pN}<0Zl^D&nDLrWSmW3yu>j`bo#B; z1wfH^5Evt*^be4}?ZHsMaFgHbb66AkK{8cEdSx2zVuYz4iCRt$u9x1h&vsW^GPtfNm`99 z)^aAi5B=nqklv7OA7N8e$15+vCqQT?4Ln_8a8==P#pH4G$y`wX?sD2FWMg0U_Kv6F zw+qbl%96KcVq5jCQ5dT^Mob-G$Qmsn!fbgbrcE`mg75Rf^M$|lSv5b#)r8j=X8xL9 zi&gX7k6nU;aL_JR8Eg`{Btb4be+N3J!#%1?8{Assym9N3fh5Escv{vcQY#@Nad2yh!Sk4 zB60%}o;<`#znS3>G?GbLk*?5C7a?To;5(5k5pp-_2C{b(*}*v-rmjGkTFx#9y72AZ z#0^AG9T4gJBDf&46gavVhWn;E<18CPnLL_NG``H6vM7YqZYf`t(7aHa@tLkBhZFtHojXjfPU)(Gc&J6F{^QpR{Y2UaH#;566Dly>pPZbTPPZf_sh z%UBb(CIwZUAszYPGOqFnz?Qu{1$rY?6+_+dxn^O)8!9{tH8bkKG=sc6%H@8lEA}IC zatUn@d}%3C`gZl37XKtN(3mEO(aRxz@s;r(J{!kpVo%n=PCm!t*p%3{_SnB3y!hd$y6kV=@Xp!IvQ$A#Rt!sB&&N$-n`wK0 z=|0>%0pJlC1JM%P6;@!jB}Lk^1`Z_54UnVXC~0>O(|)+X;&g;2-tRui77*D)<8KO) zdT$(YL!45A=rZq?b*8G1P5oV+6j;E{sM-XH#|H=ZzM2EV5t!>{#V%6gWGIgn1-aDr z(x&eg#o8^wkT8yw*DfQXU@_|>Z}N!-QDTi~CS$6WvRws2@-clv7|`F4Nxa;unk!%P z`MX(ZyH68HFR5$BYl=>*6wN09ucoAr-^Z{*-WhiF(-eXy*=t9g=IS0X0x_$vaY5Xx zAf+F|2}nCOyu)%D)EKnt4(-?rkud0o3ptN%=tr`5ZAhc)6G+zp#3g1^^#~;wQh$eu zx&V;*XZu+SjeumOu86%r=M7|Rt38^IgXrn%R!ciNwPe@Q zhH3&M9}nQ}{bf$}kQvl9w)}HXbq&C|<8Uxtv41-!+HmrcME$c_l0$8+->6;+B8=HI z?+-^=PAZu*Adu}aqgRQIxQyd79i;u=B!RwiXblK)mp2?4Ys7Zm!uwv{E4kT_=& zMIhhUF%_pXwxI>lv)kixKI8{*TpL+Y6-@J}+hyjfx)7AJPvU^#pWEgC&>rWV*{saq z3$(q=(yD~j(wh0Qk(Ap&CHCs~Oit#p(Q1$V&;moO#fQd~Gt89*?ht~RmP)2ak}9=M ztg3GAMAKiCag2Q8S;3pYCyU%e4Zm{W4zh?)1`I$|NJODT6w}L6bf2sOKEA@YxuZW+ zW3@a)R(M9edOc5 z|2z?IG2aO@oX)IywkK=`uC&w7B2rpVw#=93o023Q3Fw-36d{+XE{KEUj=W(mY}n+$ zYjX6Kxz5kf?NfgAPJ#0>$EB^UNl-+|Z-7zpLbor8r&38m*R)t@r%zwROdvR@cfC{-fadGp02u}nb} z6w)I!dZD>xdswTRR(Mm9wKxg@U6e1uT5HJUdB)qnc2U9UH+Qh**qTgc#>dhyT&Ye?fRqHmvD@oo&XadMkS;Q~u#PYlS} z1!r|O0~b;(?*(P8ypVW7WXx72GA2w#oX7R`WNOdqPIcV5&CQ5x$uu4g3(?gIYH|?c zKa0)VN-#?^pUVg|wO6boi!6=d7q4H=xLprh)we26u!8Mc{PIQFc@mBtrlg!wNFA43 zM4ckcLL4|?SEn(q%T}sTd!N)ZeOw$>nV&v5_HB>k2rR=H6|xcU;b8L&Lf358LbKN5x;A6MVB_WYyWX zK~h%j_sG#twO{6(=NJ3O%v8&4ra(eq_X%6tanHDlo113!&p*R#kY+0AL8Q%5fD>w- z-~rO%`8t%b_%|)6u>@XkT1HVT_!`*Ov}ua;t@y>f-Qq!DXEo9AgycR6Ooq0h7Dr^y zR4u?`VmXJu-x0mAgU}g6#~O+3iM~*ogt)q^7SN?uXjk}3XZl0*D+*Uech4VmHRjYx z519Ln(O1O1IJlx@B=Ycu##;qzjv;%BZ(Et2x1E%rEMSe9n27})j5&s;|A;eFa?zNx zU2}mN^e8RXA7+D0+3%fg!2Vnqr1WUAU>=gJKe`h$MhrC=-^;EaS$y%Bcj(Z z3jEI)6gscJNES~uix=cVexS~C=6mOG&8B;qta(T+F8^z#;BesT%FnGc5Lw{HG6g*7 zm!XX<2FA(Ok(xTAX1F3BErzpl%`v?)1a$ZuT@g_J5;5x;SwLQBF2?p~*3A~J19LG6 zE6|f*2m^`80j&V~gG+$z>#}SA{vp9!x{8H&BH^b3PJ(ONUbRMS`FKNRaF+0q@=hEI zJvfQ%??0qKp|F5IIKcmNIxu$l%Zvj5=`i?W9yg=+uS#GbMX~otyzKsgui|2N>)i@L zoyV+9weWkoRK)YlLD6R$*U+{0U6?UD1^8k#HPAE)BcvP8=s@;SQ)X-Un;S2=Mur_+ zh5EK;qrP|2Fc}~cNtWC1ZRj|n)ny?#cP>mUQo3ia!t3CJ3Gi5kC*{;r^!(me?=@7w zo-kk7SBZebwpV#1_E!cyU(9-kA`1E`*t!v${Hnk|!{`Aq5B3F|f9l=f37JmV4#zWHpULv1KP03$?)?K)lt$$%gNI%UfC|LW%KWOm}D06SxXf9Q2lyFsuIuM;{k z88O7AMgcwdT0Q_e-U!5DC8^`HqW1M7_h_t95Gy;<78U1!;Z7`VHdw#0)Nay}?Q{icGP2L0fVaLaEo#az8_VjImNp8@lD55Grtv&09C?2}QhrMQPU@=Aq+83- z(4uAxG`g~wQd$d#cJ9DP1;)hKA=*gpw%REzE`+F3({Q8=s?jNV71FJvVQS~+GA{F+JahG3+rbK=`V8|F0raL@g`UW- zOG}+-TA|JSOSfeT@$veNjwM1ywr8V2t7eP2pW<5FH*vJitE(DsTe8eey_p{GcH ze{CFVqw*T^zRf{8Y9utBzigd><6_vV;llmbw_~IEYKV7_$-mD)fUMbNTsB=%*W8af z5zPxOOUtX`!;d-0vsD^UIBT&qoYw5B7}I)H1zX`6ClVTKs9LWUqc*-niDFxRFAf+A&9nV;>U-Clb=hY(c0Lywx83l|5@^ ze|}^RyB~<|lF>>ib5|IB4wc=JvH5bMh2@K0GqlKF&eu<#SKHeN0gf5h+D`yqbGGmR%MD_Buuc zchetcjw6{_P^G*1s*I#caI24oXC?{{hYV!r!>f};lEm8q&QKveqoMcHK=49{ORWY- zIa%tY&-=Ui)^EyvkYDBuwHc%)_j74xE)Q;nC^DL)J1RzDxYh#WKe59Z@g(oOZ@9+B zxnh0*L5M%3*CKW~{jrTp?kncCDsu!fKHTiJ4@-FM#k=T}Oj&O;&ScBb8224@T03T& zWVks^kvWR(6Nw3s3b^#ACci-yqeRqGeB;W%BIZ9{4g}Z*37B^^&~rlU6grC~NMV@WyzQkuK%#)C}GVuzzLiv^z$ z4|W1T3^LL3z3aU$mmewVD`MCD;ZnROI>G2!5c!ps^ZIhKw4gle~#hB|}SsS{CLfXmR>>LKI(yqfD(ZHcPlF!YHh+;jMT{81d}SK{AExl}616V!2NGVA z?q>#ErfEo$tA(nw;TebkxDLrr4LTloGQ}*mv14kLkU|+=SX#>ljJ2_op9>d>iJTPy z8g)=Lg=oc<{R0P-U=r#^X9%&R6~|HG4e>0zg(ZRnSvP?na|y%}CH><_mg;Pc++tPv z$(UR~fmLG_L5N7|(c`a!v1lmv53NWQLy`w4Js=le-<>*2BD(}4L*u*hON)zW*R=h4 zZ3;U}hNA2)hHBvASX^x8nd$7Wxt(smm33oivT?yP5U3q|pe`CEZ;f%cbx_*=p0S)t z=qBvY$VsotoEN`JGed?VhMYscaV2R8pp}W}x(tplDJ^4wQ)M@o7zs!0;ISUFp`!M3 zS@nBRobKN#2_YUQ(7(;eJFcBQ7YH6Y-*y9g5Ua8wYjQ*(NDUR?GUBj&(smSIg}x7} zyo?McdK;B;v|xu}LcrMCS{+STXh*lle3efqoA9wUAIwp{khDMJzt^aQO8vo={vS+$ z{fE#60N4|g;T2WP{7&h?jWxG5#7Q*4h*xHQu&6ia+~;?Q11XFbc8S`$2X9RnJ@*p@ zLtVWx!h`GKh%`bN9>+M|*xZ2tega^LVl_hb2#dS{pZSg{-nNy$x(7O2gGhOM-t`DY zJppeaX~%!WukiMw&5xS1;Z2I%zsfLK1Z(OrnT?E5&Ie+(7bhbuu)bx1Gwx-ZW~-g+ntw+y4hiZ#WWc&PHhhCPkr6A~C6&;l9KNxOEqWd^Kyi4ChAQ!nx|7}E zL(zmxBEyMoY&$I*94Z5%b`4OUrT`F^8X0q;-2^EnU>yKD1oZ)-m^xYASeE|z^}}=m z+z2)AxMsqf!EifPqK+Tl**vM>i z15}AxOns`^3mn6qFhX*7)y@y*V4_^dHPeIai=(@r6+rd2mq_b^_V6Khx{XNtXW@vx zkA1}>!cwP%d?$@!*SNrPKddRP4(B$eBfDi&GS4FjAb_s))y3m%w zX?ha^rfKN%StMG%NpfA)=wwnJush0$=&)HSIGCt=N*W<54f4xcr8M#mFby(hRBHX_ z%#?WorNGr!0Zb0Hvs$`f_9I_EYL>8IWBLdgl>m0-hC}drLtZntT&64qq5&rQSn}72 z2nL0S`YySYC@RAFV&jFly_M1E+Q9IDrj(+Z_hs>-WDq28TF;T|mwl4+2lxK}4g3Ep z6%axU zYz}j^^ecf0DhNHe=zJyr&p>QojFR{a3lXNGr>AVHxW98w1RdU=H2b9&$@_e*)!PYG zKKDKjfodmxC#oL*s(xwL?TdlY$C9-v=moX+qy_BaX$)ZScs7RQx97g1x&V2q@I`20 zUgK_x;d;#7ZUmD1Wy}mheD3T!ey|9lh(bo`Yn;Ml1OHxqf0{pmsVV_2_Nii41~C(w zs5A0J16A9T;*ge5m4*TxutSx-5Q%+j0&t89$|-#b+2t)vJM0+A38x7J9a0<7{=$vU zPp;JUiHLsxnSn@rbsrmKV&1nvcalUYqk~=iC>__4TRzGdk`GQ-7&cGjB*ARTQ$Nfxray7g&9bY*u~y+`&g3-iclPn7GfA1@@Z zYw3riFq(?XGtMp(0yM^_>g|V1`Xb*rJJVBBRMjWlD0|%zw`{c#lWRQQEY+AxeM-I5 z#5*-S&VS2TN5%ZfY$SZ+SFV9Ck56f(=GaUhDJGI+JI z?PqizEvDn`-!Jm8V{3I|-O_CTsP3WEwO?r{3H<)6P{g@HgElWu$}okHKb`i0F_xid`|qga?{vCoXxE;tQEQ`g!`0DSb(9D$PCRXJ z{b?I@O%fdWRhxiMOJgJIp`(sXk7F|@?7{#PD{|dU0jze3;}r_fAt~nD?iUP5p-_-N zc**~_eki+K{r_$LGh#H?@&zvMSM{O^xM+Htcm`Qt;Rh;L`NU~~KMp*u~+o0AV+udjSmfoJOo;pRp zUS^8H9l6mEJ_FvSug>*s_e!nLBV>KS;P(rtn;3B7`2Gl3PK=1A65Sr6nRm%u@!dJC(h(5==e;&*sZsj|ykBD9KY<%OWkrj|HID=juZfhU6s04{CF%PY`ApyD z;Ot$lC?_wTrb?0<^QJN`Xt5nyo=IZ zLE0bNc#Etu0q(&SKuKfQmfjSAwo&8FcnxY*7g`1Qawca|bK745Q=#mc0PY|3F!s)v zX>TbST&fUT*{j_#a|txbGh5p2oBR{X;iGg6VFP|!G!GbsPqPfMa)!|nB(5C+Z4wVj zc+-&eW?J5a2P5QJQsH#jD^8^Zb&Bbllg4kli`ZY%#y!?DUWbW2l>tMhi+kz*N`lDL z4DsbHbMz(Qt?U0<1lr}v%YjLo5JumXBR!XSdEF&lwVLwr3f=xhP@ZknIT^mO7E^Tb z`ldBpB^g!fWC-z_8~|a=fc5lMq*s>W1#}dVuu)~!(2fp5>EPaNRIxPC;qM1s+ZR~H zM~duGI-#<$eCKUrW+g#>S_EWu*b7%+9s&CI{S7}{Uv$kZ@p(Xa!~>+Gt&XIp36LSn zqtARa>MU=kVsgguG==1f) zC=6P);)H4g#>nB)X~>8u90sTfOZ=H|%E;zRa@3NO@*B`$o1L%ean!G7rlC%}4M6mb zn6mRomlHf!GHzu9Z8W(^jXA>!>rkZtwgyXSUVMX-%uUH?W&z8DB{BVweoIH8WiOCN z@P{PY20YK#myk&XRqV*ePoMUNk_pBeQ4jT<&c%j9F1R^l(vgw`Jd#Ff`mXp3_{PVa zh99=-EY1zL@L4Rl%6!h5uZ?nl&poZi#jM#rE;|(w73Peim*>Fo;Q8ZHIx6`qip_TS zcA0Jooph^6*M*W%bH3B~x4JLNDI>;A0_?BgO<@1$ErKz{tM#TdA+yZRag!$=L-weAG(o^cu|DGGq>-TlQ zb~8@+Olt%za>`gQ===i^3prJM6fC%HIdF}6;Xe!#1bb*m1C#%@xEt0?HNGY*q!Q7uWy2d0-t>{lfTw}nU?}3Q~_U8FEPeks@XGqs6CMXF6~>kZu|#(ZQq>&)w|1!< zpO76$Oo$0ZKv$EHCfYU39lnk;nwfdQjC;r<{;u}C3K<+tGLQW|bfiYxw^wWY-Yq}( z3{Ykv`6QqJac2Lul_;L|Gp7$6puNtQin~PV*vXLVB#gmr@kNPHh zr51T8lU3IIq=N{0jA03+yHZ*kV z7L9NoW_f}^IVX=A{>4B825hpwspGy>*T%A|nVj+@2ura3_@GBQ`QH3Tsa$JefN}Kc z@&`}o%+IJ48*;bRj4QYD>S=Hg7B>?KZ|DygaE?!RDzJtc(!w}pEln?dN6Uh%$AcA;U5Yqs8iX&N@U>)p8MNVk%cVaR79tOu0Nf2NdY$WDp7vHpJ)nN``%4XEz zqGNt=tfYb4w^`_*S&p{%dFoh_#hz*rwXW!2O$8~m>MR+ia`(BS;=L0<{`FHd zuk_$5R+zC3{u|d=d*qfp1St-lx_$M-jyy{%0-44tt_729k*?rxS|q%m^BhzSUZ*V& zXsJ10E^-UcBWV!jnV!b?5*%BtDZ3$~dNY&6@ZgNocZF*d+$f{MLI^w3%)&{J_3;6t zv4M!&;IFcQUXV*AoRuq;9;6i=h+cXNn$5)gcrd&V4<*iuy$F5f>8!ahe zfNJQ97diKJDnod;E@xA)wdowIP7=f0qLMSY795m9!R;sXDb!!75v-zm)PA&jwi*GX zL*Lw)7y(8&q)9c-4NDAfQ=R^SR%B#JS| z=eM%qNxRsqNt#*3EwY#5_u&^nFj%-T&J`uB=;&6hY>}Ija=J7^Pb6h?q!*o1`&4jB z7}f2@Hq93DbQuW6Y;M2uV+=}Vf?k>Ruv$Be1x*XwqH;wY#$*7mz zNLvxFja5Hw{x3yAj4pXIH}}n+jv@7X z2X!1Nq{idlJ(W3l880ppu5Q$LLdM?Aj*gkarmlzc;ow@+H-#p036;0;eLdwUdR&0| ze&m=$i-cPrVG3&pm5^K;5x%L%7**O3I%BToDldwyQ;VgX1!(IVjp1dXt;vu@&jZsDyvY>9*uvQ&wYqQ(9 znjzWG)|nlEbUImi=>`bOo6~h68${qLU*YrLY0-$6bSMq>ofJWj_^|ka0q4U|4!tP8 zJolPx0rI6a4pERc7F_6lp>Sg92kNE40nR55N1KpTIA{@V>B@sV+g2zh>JLHuTP**T zEE2iP=79)zi$b?+B);AJgGTtR8o)sjuv+Y%VcwT{@<8AXi(c%tRm3o>Q(4GwLcJas z^#e{O#+N=VEllH$%~v{A5nau|2l6mr2h7G%@0rvp&6 z_<&2o{gH|NBi=RWCNDcWlbSve^PTS%Nu(`xZgcj36m0^@Dp}LV!>}IZ<^jmD5zt z+$i?m4l%KSvG)&`QwS7?Ozds23z|M3{Gp>L7qV1LOY(wbt*$7@Z%ik@%l5(iyyL+RFJVobh@0jY1A<({^nDkgF-S?s?ZQ`HFD0blm~}0 z$9|k>F4+iaVzP0IJ(I}q7Fdfvw5%xL=^u7*+X3itj-w*lTayAtuIR2aH@gQ(suA-% zjKCTSaz}sEE4C=wx8gpL1Wwc~8iji>%L>|mdEj7Fy*$j>TipozxyoQK+(KCdW7Oh-yQ!~N_Lacy={i=G4a3Adnck3v}&qW<|g|k=& zA1oIrqbeyzLlL&(vbayR1a}}kuK+8bYd%_y(xc^fs1n(q?3v~8F7I*k4=r6?Y&3W* zU9Xq&kf-sCyt!bXtX5#GxPu$Z*2XMzw9&j1ob0ixH>`H%0&^7jNvz(yW`)8{qsset zB6**~-B!mplt=0BZeRwdGJ|>Gyh~5rxVZsx3;o~Bx5;{+VAov7@flVx{f`_qV^kj%C^Y$cU}u@SKuX z<(xARh|vs7Cr#pm1z-q#Z+JnWTQ&nt8s~PjE|c4uLT^jQr|q~kL|Mh|Agpk>IX=`3 ztRh4McCvQD6tbZY%BErfrn^GlIs$xvK0&h{1g!%Tsx3Y(btY7`Ck&tlkp~woTE3vr zf8%_a0qa+I;#QHeAezQ7W6=U`Wp$k#x?zY(`}(#3Hv;S{R!xb-`#3Yn6a&=SNuUPb zJN4XouOLmY?G`vJN%xznH-E-Yi@WVOMlMSri}+378K<*S0_*ZP_SzGfvgVo@8;b9; zIzg|LP_tYP)IlTm2p`HO^o(kJZD5XM6h|(C02)G&#%+}W`b7|D-bT>O_oR)Mrjq*= z>k_h(bcAzs+1$2}ht+2ln5Rtq2(|CjBL_@H6?K*B%pqvvQ)qKD^o9^V<83Q&M}E#o zofXr!+cP^({9g52ZsM0mRYNKlt=7JjiGJj>yRst|L6|8N!ZNKG9_7QBIi4?nSr|Y@ z5dGDeqz_4M91Lxd`5;8Z(ra1{&#}9wjGA}l=m!Ue)u_L_OMrkfIfcgCY1WQS+F?z= z_qKZS2$}Kc&4tPSIUO*y^q)$UKC!6riYYXuG-I_QPM<2MskJgI;DW60URA6?Olxh- zZANFGY~iZ;hiHthT)AZknfb`5l!yI;IJq3oO<?=4l`(C*pa$~^(F=li5(Y0t^AR>vH<^0v9|!m;NFs} zLK*Wz9J8;Jh+cTngR+AEAyyq!j{H{hHjJIEDwPFaOGpSD4>Gu6#@sZ32{-T8&g@BB zH8X2tgtmYT1LIJJ?Kj*p`AV-@N#ymKvl?I!AsJ@Q;INU&QaK)@YZ1$Qq)*+}^7V@= zmG-i~g)MyMNO$-m;#%eXx3D9mn1(_+d`3*V?RdU-!Y}B^^@ouCFDvm2I`CY5_gM+9 zJ=%d)-aDNB4|)inDebRwBMdw+Te_=Il0HBC5YZe?Q^lNGI;#nScqhUL5@;zWhn8zc zS0reVQRv9)1i+y<(~8R*v%;LA7F7xGPtC8~;BPAnb7}xQqH=`ap4B-moPv~YvCk7o z^|YnkZcwt^^$0m~k&IF6n`MPk?OJNs^sE|EmO(UX=eg)r z#)*W2V+D!1uFeWxk4Y9Gq3JcWfBh)cjUgylv0Zr`&Z&iV@*V~BwT9=}VmQyt=G?i} zaC91{mV7$DBBWed1M7}7()exdW6BamV*!imEc@J?h5uC1jZgs$K z_7C=D2(_%}@qwsyOfTqDdi?9~y9+-YfO7z5TIDcc*8th;z|nh&0(2#MP-3{NibnF+ zIB~QfZFu{5LPNwyAhSkP`gvy+R-Xxm2-f%_`*b2e-g1^()6{8{!{`FQntGUnQTz!+ zc9E+xVlF)Vi@ScaZ6?Hb@ddn>Cmn_s=`nM$=BmKOt}ETcN8s$NC7|^f( z_i(y8>3<6Q%7Cc1?%kndXbI_V5TrpuVCY5(0Ridm8gfQbx|NQhOH#VKk?s~5QURsm zqMmc#bMJl6|9+WIYp=ETv!7k_TYD{2OliuKZhgU3c6y&`U#ZNr5>wN|2r{O1$#cTuUQVCOemXzmH$kpRKuqOtsmW zBy20pj5z$IC6WBgZ=^9_7EVhhEkmf(E`)W)SK7W~qL9DZcSvQA1&Fm{;QLg%h#KQ~ zrYfI{-@X^OP$B3hiIlvud>G^yj^L>}!qh9>bparbcZHiq<-XzZ%xf9{IQ z90hUqk)y94Z(U>}HLk$1Sg$7GNElx%6^jqOf%Rin($}PE;erHf$N^}zOml{#S1~Ls zT1zHTM=C;_R3Q3Hx!_cbZd#}prZ!eRlQjX{a#!Rn03^^>tnG5{9BWEF@`?Qz4BVU2 zFz9=Y3bGzM^g0b%+Xx_pht{LTK^myL{6D7vhU0 zzI9qTEg*7DMSYutfpPMp{eD+k+nk64AW^3y>ewLFtNsL$5JU!rn4jWGc`pv@)n?g3)(`3$-MtOWEFQ(_ND+U=8w| zy}o;_qg-*sUbw^hA${*lJ54ZntYfUR2`7p_N7;hhU&Ay&0ae7h|MiDN)w)@9UhHq` znu1l#GP@1fmJUQ9$Cw!_1G$$eqC0l3mbY>mBg0O|&rpTnYO3g=PjcNp@v-XC(qLt6 zVaOwxDMM})vGtOw3}9k*LfTgvblpngF?D^hQxjun^HhQ15^B4ui@g^&7Al!Z8(=*u zat>)tJ~~s*B&q9lw?@YUNpC`sQq7N(!+IkLnx8XMZL+Rb zQd`D7biNYYCb(&0Ld$dL?m!Jr^&sbO`{JWkl*>!Eh?TVuN_$<9DPz!)FP|N7cE(+zjL$@f2D@y{pUr`5Gu@gnW??XQ0;kyYKGg9tBpInh22xVbrD|cYS_$OP95vE#${~KbU%lgfQxEH!x3X_yraGd)lg!lQ#q2fr@5acTws5(I^%j(Grnum6U$`uv)J=t6}((#Zca8KC^K_ z?8!r%rLd|Ih**-HpI>vMU@g5>^{lz3C7@@uoClW*Clu&Z8xa}&wEU#1&TZ&(hn1mU%~BOl-kdk8_^Sbu?!cJW3|Aoo5})SXc_F@Va$trqu0y6)+T8b6zrCi~6$W>{ut3^6qQ5hs zjEISg^ODfiYNBMGvi~IceN^TUx8}T}Ajy7H#mZnqs&&Ca>^KY8$2W_m8W}YiozZxl zm~ucCadY;t@y#}mu<`_P?Rs&Eg<~t!`eu0UU;6YWnRV|-;$yJ| zbaKxm1heL-;g3dxKd41zUwKET2gj(V?nO>LSp$SLHYx%B*5yaSDi6GcOSN2ECvq}{=5)B(uk zN~J}Sq@(AD#Q4{@7x+4yM_H=$8E+m(JucDy?5^_DtHWQbs43?xOi4DbKzh49%TC3> zx(*ll{6;M^lO6T@>Vh|B)WN9U^u8;-rvDR@CQx4%wFyqS2upa^VDJGzw5Bg=$tgO< zz0>}!JczweFTxiQ|73VodJGX76Ot?^KhMp>!m))9zwHo6^M0sU-<`? zZa1J(_jQ?l5m*@L;>340d%|6j*_W5h; zWRjETUzXrEh9O_O#dG!7f`6*A4JlC-xiOZ8_Fe^IjZxoDgVKM!(TfQrrVoJlvy*f{$z}86Gn>Xvjka2ft*~)=L?X#WF0&VKnQ6&Vy%4Xe z#D3u|8k8N@z<4}`x-$dEJ@@s4yMC_P;bZ@zxJgY`qHvi~B zE`<304c_-wNizbMUiu*9pSm@I%aB^j6z^W(u~wojkn*E?wM!Axqk0L=8murWqEAW5 z-+rJOzS&*@fIF+l2b=J5o*Z)k@hs;pUNR~4)Kx6;gS8C51SqVkEK%MWUsi96222Sk zem&)Cb2}3c5Y(p8u3L$HUXfo3bDgc>@$1x>={Fzr0u@$dW)pIpO}cTP!*V2T`q6K(zHQopVns$fXSypF)07y;luZRS>LLg+#&Hk z-DAwn{U%P4vPebu)1gx&o7wnaCc*Zy3c_C;MYxqK$ipr2>-b#o8TL-1oj<^Z`q^Ph z&UW!)B!fBbCLQ-POJ3@|;IURq%EriHvD+tp9rYfjBqsnmP-kzmSN z9XsX^=KXL_PCTo_MOf1cJ41RkTbebl*pA5rAGAHSeOGLp0RJw)ko3B8p(qDPtA`_PYJnUQ#w6VwG%EXkFX&m+u> zKm+HY`xIn@M2~JF7&yianXYw5MXBUY2B&DzED5k3tS%*A^UKp_EZR{SlSB6F!I;G@k}aWO$<{UNH!d?xRViB9UyTqi&WZ zWF18Dm-}Es&8J>$fe}NxM+pVQAPRT^tT?BG7_;-EsxSGHpkw@(2LeG zNr5+@mhF?nx{;`R@{$SD<~yyj;0Im}wxO-zZlgrqQj1KqVDd>nnL#Rf6HCH9)(Qt5 zh0!&t?GljUyP0&n#?j@s3X6c_H_baaT(i+A$fTWZ1|*-1J@Ars-C4yC4Zgk#vre-% zf{FRR-=`{RxjP0IL=KJ$8zy~}t}~IO#(X}jl@+Y}7L!0v9lRV7JPo*0vhrig zZ3^m_lfBN4>|~w+U%g(haFf}wnr%TQW9;6o9ieJFjzk8JS{MRXf_t46jZx^c-ff$L zC!*z3&XWBf3Z&!kZ_N1!&+dwp;d__f++Cx^2-D15Kbujm^kE<_S@?{%Rm4jA`g=8F z7w$gATq;UQN%%;X{@SVNnh>+|)0{=aQvL9WkLr>uF#IC##P)0q*78djaW;;M(!zX^C4z z_qsv!(U}CB4He*os>H@QC>}oq6DrPB@8BZve6wdE&!H|_L{A6j5s+E8zdunsch{eR zh39!B3IlX-XHCv7wr352v~`MZRlRd0r;xp24*?w`rMg(YH~xq{=>@!f-S&D(j7AI@ zZ-<6ra9fXKm6G9rnhoDnu<(X*{I=8d#P5gxF3kn;CIgM`RV|o{T*xwByvCh+ zOP-%EnknY(yj7#+cno~NxeP92+vwvl{f+iGnSVQu)}i~U!Dxfz?US!2&>-T`>RW-+ z`d*~T)!D9zM{IO*ww95!oE?N04I}9?;aG8LN<>RcUEkT3(yWV%|}KVU_7oiWmgFk%)Oy_Pxd& z!=%dD@<66i>1pNH%p+0K{U8P#49z^_xh=ROr-aKq;E{!vBdzCYXhwiO>T`v=I5@Ev zMg3Ehg!d`zGJ+ZmH7>&W%inaE$jBWx0t$)X#umqqsXSH*>Ht_2#A`eRHw}<0e_s4W=;52AtFB zwuBLWd9SIyM}@l;pSSKFz7>elYuNujZEL@HO=6NV^*Hy4hm-TA`O~c)vOZHaAhoT3 zvZ6$wcNK{zKPzJIzF9M&GD~Y}D@}H$k@{oe+I5OA8^PwSHjdNO-FrGzA+M`@Ik|3w zpKZ$*A=XPcjsc&emH;Fg`Rzk8vFVUBNMif4u3(aSPz>jPT(6xgNFY)izhD7q$WZtd zHNDgM*!A;RZK6WkV|S*RjLu z9;wsHN32CoQt8x!QSoVeb~vQlDOKdO*>Grl&u5T;QiX7|WyfGikp$9L*~+P$O4+T+ zvb0=u0N8T~6eIONx7e^Q)PJ5-3G}hG(tV^12*M*hTyJGXu`CQC4w|qP5H+P{Z%>>(6Cy1 zq+&31ZcMfF{l5)Zm^Q_#@C0@@FJu>J`IaTJ^6d7=>}KKPUVGwFBTb1#U_aN20jL3N zeAV%DnKu)d##&WrEHcu1s-KbjP_C#npzBBygGLJlEH9+B^M5G|;ju;H=0+3XpAh!wH?yZAsZp2W? zp&Jd9c z-wm~u2bHGIzYhF-sHuB&BsZ{iB_0ys8UVw~x3H_-zh$cGoO)ge>0Ih8JSa*^!Z{x5 z&XKXpeJq=~OH)LXAnBTD4Dfb68=(D|?7nFwf%=VRysm~V-hOX)J)Eom~Th&|UY+a;utrkPPuOu_EHjhMgnaiZ^7 zeB9y)FKPzP2hl5uQ=k-R9Gh}c!aV!j<>>7lKli#~rMAEy9Klij&aQtaL&L&DlVymJ-`Jq6< zXJ+1K%&QbZF5X%AQa>1_smKMs3QOn0od}t=N78EMMAAN;Nf@aSh8=9qlNzP zyydx+TJd$QQYK%fTXAoO&$mz3#6(Kn^Ct8QSB0xIuo@^P$!}F<=&BnJs+fQ83IG(j z^ij4-x%AINa&%{d2A^;C)iXs~j8MKnx)h(oN}!mMe;WO=j{x~?{XcC6zoJF;j&lwx z&5S=R0|`(}+rKO*(gF}6PCd_m%i}4_yspjAd-P=#o%5O~CI_G}8_wIPe;Ot7f}3@Y zUF<+N%pxw*V=G6fW-!|)-RVNP@@*2ZUc}D^#B<()yS6?WM4BNuNbAMKd!&QdX$LJf z@<>+_sMFkMB&7UMEKy6C&Pf%qPx-3YfE6fgeMcu87Af97>h|L&b}Xz8ikbLddLYY~ zat@GX?3h%31FL{yw*A%Y0WAaw&F`L|{M~HDS@v9^ zK#RJX$qS9+Rt^9wg<|ghmj69a|Bw7~?F9%E;l(v*v}pnWtA_&d{}NV_{NKtkWM_)J z;|1F%AP;`6xb1tYuCln%J&n7F*Dqwx=|(Q&ffYc3&;H#?CtfzbQDI~UGHeS9G=GqL zSPf|({;NR!<2)&d*4fm@`%;a7{Coa%Az`yNrAR9S0E0n+(f^Y(RuSLiVa-rr z!yh562TuHr3$8qI7yLa>asa*tg-CQDL^dF8Gp%rLmM!Ph{XMbl{nLebzOh(X4ivcg zyK&hc_WhrOkh1vzWeyYz_?O6k8K=KR{#5V&cP;r)tUo3H$^icy5P4lUPe-lD6+BtS zc2bw3U&g}zy~lqW`%}Fr9q>Q+^QY>{f2GQAEtODglm|l{@#*OQKn+WF7L==u0t&?7 zeFabubs*%4-EyaFB^S#7H5k}`(oaYJ6Dl-$yir@yCK_4hxgUzV5B_8S$)fr{IfwoB z|Jz0EKk2j5{|VOLQ{yk7u>WKc`u8qA_;UZ3p??Po=TCNnf7en5#rgB>Hy!XBDC6JL z6jgSnP-x|wY(OgdH&aoe6nRoL`U09+3Z~5D`GDvi?iu zFO9-H09mNei`*3mEE|gR=dI8Gttfr^VNpqEgHXI^wvB6!seuCO!_T1z-`^tieuE`vfNC$Dlq^5LKrGF*8ibZ+0N+yVyG&E=E_w;plq_8G5DAR(wP``-?cus|mBvEu(XL)w2S8vmCf z3)6ooG85WZ8o4=m(78EqnUCd*7KV$cDhaAc%FBs_B{4E`|8KC-voZeP20|d*;z~Gy zd=dy!NEqf32m%riBpECOg2za|48ci?izS5&Vc|dZ!}jp4ZuWB8i?o&d#%Frr-}+8) z&yV!{lm1=${LL_Y+O_qAdHYlQZ2$6Ac-w~A*|U@P4Ri1BA^*nEg(SS&z4e)!`Z+W8 z=C^&Sk2Yr}@7#kyHjJO;)_Y|icKb~(6c82b-Wq#z=B;isxz^gCOSoG>_QO?tmxn@g zZ;noH@8ZB`fYPFV_rIs&0JS9e`To|?CCF3piG_|eoC_R)uiDR0#7P3)28>*OnZQNZ zHy8@ain23b7u9pW<)F$Q(x8bpR2=%oL0|mUwKdX@T(u0!S}M=5gmU#Vq(Kch#LARr zagQf+qN7{M$5Y>L^DYT9yK*OQ?Y?J`y#oDZTxqwNKlEt>i7%uz^0X6vT;a8sABWz! z=uir1m-=3--RF!no^oVVogC)dN#o~@r+QW`cI3N)I5#XP+sedz*z?rC^Z$?`7KE1# zkab>ZcIws<)PoLz9`-+?N7sxRWxjw{Yl~EX8_fS8h3Cvotro`tZbCdwF{ADoq|STy zivFe=i$`hOw-D8II3${I*g3Vd2jWiEA}a?2NBbNb;wv)g9hsUs^v%&dEV2n|PIb}X zKA=T=p}%f(b|GObEPc{02Ipnh;gzM*yLrSL3@#WQT8VJVA>Orv1s3_wCA);BB?lDG zZc%Z3&}n{sCkgYmHw67}R=aXZ)OW5G9$87h&hV){}3 z=|&?$>XLKjbqIu?$oO$r_E4-El}6UqH>S1^B3TjhmWXfDJBJRUD`R~$yAk3jxDjIB z*sSVbQ8Os=g$KhfWIEVL;diT$Y-e~tWoYj`mjs~0I_((WC6%e?ihj#EA1WG#;Sy|f zg@w@L6E4x^6E;3(-S3}L_X@t5%VatDMQ_`VS5=(&WkGM%r?)^QRcw7A1QG@SvM|b~noa-@VI%^4!PaT=ysD%XIx%uL--oRviiPqY*)(YDm z%l7-QSMMkjgfp(viO%N@2;8}hANa&g&z9_sMM~O=?NpwLCE?xvawpy%kjj@O1lK}ocJ|!ete+ztio0zB&yy6v+a9(; zW*ay986x8XE&eB9^G{Z}WuHmWZ%L5V^*MMJVV*@@kvB!b#AB7M7Q;=$Bq?Qv-+em| zk6~_GA@psH$d1@#^8*T&FZ}0s`d!zQW&A&~8S5I-B2|D2DS|pP4xhtpZ5ua%m-Zf6 zy`&ONquG`nlCs-&4AhY(A}cCCmccfdP!@IUkAG{aGnEcuU&tIj34SvqRQY+MW1?={ zvBt4vOLw249x>_$lOWEe$8RUS!Lu3}87*4Cn56!3$GHg&e*Z#L8|HXqCMhp5FD8tI z{EB)}Z&Cr|DM@^Ybyl7DZ8+(seDQ0@B>!F0>Cwrj`;*XYU3~KV zoxb@E__6!4L2`7LX#oNCz{dYamjL~4)Au3Sz@ZZV#Wtr4_4SL!0v0HXzUbe#1-i2$ zTNT$>m*m6BwPxkwpm#=-4mZ)LPw5?yl%`$#2_D6V0GfyF<5*DfO##$3dhWZjD?57KnxITRnf8A8S1O?|!ovhi`uxX*+&mXf zV_77kL4~;tO)(~(IQ=zM=pRlWS5B3^;+=CxFtF^|LdFsYIARD2;)GO4xUA^f5o zE$vs(x0JL9MqJPNDZF?0?gO59Yztj8Sudn@)Wgy+imS;Tj#%?s4GUiKZ@`!=mG%Xn z7!uxU_crB5k06IArY*p-q`Q4#^AbM_giN#M!dNC{YG`)E@9T9C$k0i3zDAvBM_={Q zrVyH!?9*=>jsOtvtjc$pNQ!dx!B%`=EU{K*OREgQ%x}yM0gP!Tixl~84$_Gq2TrAe zE;&h_(^cOx5|`GrIGmHkW|fE+4tF@5S@lOJix!}VIC}0Hn}eKlEUh#{F~(yg+%VN2 z>tJbUIfWiEJ=Ou_w`t*WJm^TUH)!KJpL4oA$|EMtoGn zS;b5yxwC38>nFWMxtLGd-NhfZwBi|uYk6Z9_!x1lEc>_<^S#0$gKb6&0>55{+2U+; zL4n6)`ffOVF%(O0EWzP$+SE0dp3Ls>kN6B;scfyu2S8%JT1fPu!`Isx>TmXeU<-#p zt3~rptG5ta{&fogU`0eL#4RC6UNPvs>tS7^u26MGZ(~K;HA(lay|5*dHI*hyso?Z5aoO2pd1oB5%Jk1M?q5jvbTIDQ&{OQ zQH@z5qs+sIK}9kyhS9e@jXK7~$;@~nyU>zISbxPge z#_h#x-#nl6{^J8O5IKn5mv>#*R^raYZ`#adnzH3?=Y>!<^)d<9%i>37Binz~V57{T za?aLFpudpqep07A8`97vtw+dIFVO$+li7Yt%H$tkkY8lejcqFBlF-J=cq2?&mU_;gCwFo`jTTfd@*8zD4bYxiRP=gALw~Hm zo^*24eCDcU{@IK1O9q3wgYmM5^%(MX;QHHiaP|(0EI$;iIoNgK{>Q zF8-i#G-vC{r)BwG?7u^Qj!^0=!4Se>o~m=^y;cWhLAdjVpyk!&;bmMzj%-cELI1q{ z^(y5(cipj)RQnKd7P|$4gg-$BZuu`Bb76#E2Y(v>)@$OAtpI@?b`^0W<~&fK)UKZ; zj0sEIV|;DM>|&Z)TjKNUTjE*UuYm}Z(E7B~?$iS_{s8{O5ts;lI2{ErJ?Y-5P6i+o zgL4uO|CZlf`3{!{Psw9t(xreGyd`4_!165^z>oiP^(nqru&?G>S8LWsOZtghD@>V) zJy8eOdzjZ@*Iq!lElAN`Alw&d?FSqJcmyv`S}B&TB=#lRo3eA(Z$TOmK5Wg@Ab-pa zvS`pUpTD_9@0~7l(Oeo7eDSUJTCZd_1YXHbA_3Z11?>r*wTMk&XEvc++?)b?Iy)Iz z1nZ{TOVdc@`6R86#=nLZ&mX{OrC3q%6DnNcCMZYbG}nPKE`@BkT%4z8+%}$%U~~V5 zuj6Q)HyU4FM8J-`=HNw6e>OJEfOgEj#$?6}J+9T9oIl5k=P8%3A=$z7Nv9L{xc#v$ zved2P6a|*RpQ(3})N}+lgvpk*il%NxHIYtY1_~@tBk>R;1lJJMqlz#MUENd=((4Ks z;mxp2@7xLF-N&i^X@LnXNrNYb%Z5M=h#JcDQnNH}S+t7RVAH>P{Lo~Gq}t&vH2%!XDb62d9Vee{kTow%SjieCm4^}9f9(UQ+c%z5u!aN(VffBiHrjVfv^um zQG9?!rId3i+VUMI!bR5A{Kabainew@#3j6SVgs>W#a97P!M(0`z08x07Q5Lq0P->0 z0HUY!w?ER70h$(gn@?EB{Fjgcx*4=m@GpO7EqogS?MK5}<=O)-seW0wi&$nCAbRi^ zVZFhE*dJ|lcAPvdjGJ<;!aDaaB>uvN;Cc1i&_nioXju6 zizZHCxh3In`oL{F$!J@;0|fotLTk`$Vf!b=#`DAxX%XaM?C0}s8?fS_j&QejH@@3h za|K-qJ$(OF8XWxB?1hpq8BG0W2sq#8b+8kgMBr)rb=(bHN;Dq^j&_yaIpj?fD@gq9rZzq9&*2my4~eF3Hj-%;v6GR^Te<#fshEfZE_01AnIKY|bc zq3;WpCRP7!78M#85k9!V6|II8y}>5BJbC*&W8FF`RVap5As9oHN8Nh$L&Jb_<5~sA=bf#drLc|j$c@JR(QWez~P5& z{L^fQHT!ik<-Evv(Q48LOQ~6{0)ZuVU|>BNT*AcVqsrl!T^o|HU7l!n`2O4V|5bv1 zq6Vv-=7pOq&gp6LA&Q@4LEfIA=P8YiVWaVajvWZy#=Y$_tsyxJE}s90G$yEdoLS4~ z(N2atjaw{YRPM=fNM!dszO%snG-SwiAhD{H)LnaTJ_y0W!gnZtDa}nU3w7yEwMAq; z?$Mr!RDO8V{oXQBgFG0SdojDssGnR>oIZSh&VyorfaNaleI(xPB5zK$N?slQ)_hT-74 zQn5ulHFPjTFy&Dxz(szdPh#j~ZCII_eXgYIfUjwQIz zYHZ&5pyL7u)s_Ovj7oOJT|ynD;jc*;%lh^%(Dmtt&j;%oc|6_Kz<&yIS(d@3oYM+M zMC|Vb zd8g0(>!J?DU&qIc%2-XmI%mwn3oqL}hmWFOw4V*maUK1*uDLy*xr7^m%&Wtw;on4y zeCyyUeVx;-O#&UJanKpn`VQdE6~~FHN>6Q8n|Zi0rBvv!=7n$HFYB74Y7Rb@;sWxr zWfxBq9v|pm4jqQoR%#1+f!k+{D;A${{5DQvEA{i-;$rAYPXtf(`cP$0&5#bg502?6 z$`q8d6Qs#`IpVYg`m7^ZSTOknjVvElii3Wo1aOC0 zzG|#!2fSPN>=z6k5$)+ZPIqumxGfLwNQE1jJ}bBNV70`}J~}m>;p_WLVt+wA@7H1; zPObU2mZ9@)FK5otC<*0J>O-}2#w#Pf@R;@#0v6Yl5`o&)m| zni&17%l8A8(6ics@PpzpV}#_K%l5&TGHv9H{Mj=rp`Q<$F^b?r1=1c3^U7TxgU$dD z{V&;f^Df_-&6Dyh)d+9KMvLV5EM!uPQ2&952>0pGjUs{h8N!;N~zP{K}w#K!`}i6a!yn5)L3gU*x`=B4g}3kpTodyMuxZMA%|3i9P+OgUIvjq-r`_j}kucZ0<- zJ1uoNCx2VDu2Pa=y1}AkGjxVgx5ALhT0H+Jws!|ToX`j>Tm*b@`sYz*fxkCB zzd!{JKx!g?Ds!SUALB7&8r%XRo@*)nLo0%xIYfpB!U4$X zOs$UFaZ$phdzA4sJafj(v>R(qO*C*7(+RluTAy;7Q-U(9LBzM=m43Z5g7|khuCazR zMG{nVYl!rVj$Eu0-(WO7x&qdPaVF}GE{A_v3gbP&eTb_7B$(x=zQdR~dURXqDjyGI z(*y({nUMEY7yIWB?Qba(Ey1-u+&Hv=M!P4~kaJPXD{ znJ|xZ3!i&viZG1QoB9aoQYE}Lnnko;2Y)L*yM)ff*{~XWK~{$@r?KLjq-|F`cBkN> z_8MIEfM<~gL1r9I&HBHvb%C;05`J0!rU*P-DrtCjAfx`x#u7Sm-nogh@S3mh#lrH& za>&g9iSu$|-$8Cp5~~#bZhomTL@&F%q7AD$2uhu^As0kWb_gW$`ko3&9*7M^ck7X* zjsZB9wDUhWb0X}cAuHN=*guzMqJ%T(>rodN=>D#zzntsmg{X4HR6$b&*xm~CZacp7 z@0NV*x8U6mGgP<5;A3F83vvw0lScOeQ(PL~j7=M;-I^&r{-0!az#>tAwO+XtWzY1u z%3asl*-S#=+ezF>7^P|rN2;#btX^-)~3oR!6{gfGwr>wle@`TMPLB?D~-6(Cv>L&kuul~Z|Tvdr=lSDUK` zE|b>GZ3m~m+U1P_x!YH;@s$d*f*6)Djvj!HP!Q8#L7H*hxRgKJ8-cn`Dpi?sXE+b6x6L31!|>gMi8 zBmhPl6HQEJvg^|_qpMp2tr`=KTkS5&1u3}pV3~X&t)}O6-&2cip@3qA-8r)NkRPe- zApD05!m(_lh8L9kYK%i9Jwk;Vgo;*h3csg0f57`e!lH+iKFO8nsJGBg6K#xVM1%%&{~{hwk|^zE|i{OV5(wk5Yr!l^pAST zV%nd-!iYL_JmO!p7{~U94vBM>o-2u!K0LkHO3Oy#Oz4AFfOPppX_0gg(jm_4RZy~*myqlyFU0o!FSN7f*E%W1L>pvT3 z#R+^mke~+;p*fWm6j>Z7px8AeJ@9NM2R1QenNvwnVf6FK5D0F*l|PHSlBlqB99->< zp5|>=a7*ZKtpR@#RzkFmduZ!UZqKZ7)V3SEEXqzd?fdadPN3!MY?O!DR0XD48HrTG zR?c*KwjaUc7DWc3RduZ&5OR9IGhIXspAu>{F z=C$%|sPI`V(k*;Bjk1QLO38N8;hQy2D%POA5a8p$6GSQ$eTdxL$Dup3ZtE_3gfSNU z5A)~m>g&OP*Wr1Qm8A~jTC2JZ>{9&-hDsTtta)bN`Ywp*>LZ6GPmhc!P*SOzC{F&9sQXKvk#aEXBRm^oIZsoz+n^wUEv^~hYk zc)r%q$OeSw{#W!(w{rKKkB`}DQ8|1wZTa^UGZ3G%=~6dO`nfj>of#A-4J;*Q2n7St z_9u+Vx^~>{d^*oaP6_Ho{ryVPWW=M5>|zqJ1q*h0fwdu@6iP;D-5m5j#%h8N3Xbw0 zQ5rU-PMcD7GTG_Q!?rT$_S)5Wy(ddHdq)X7($dT?-*r5@1bfy?oZwnxcgxvZHEB=J z;GKA$`1KJRAotWRz-sa>C54RcUy05Y@d+r?2^uFO7Xu-6?_@Hf7L<0Ic)#4x5T?n5 zLuNC_iI#-z5epj0tqTXH`h_hQ%?!hQdo4;2|d0wghKPFVgXmR zKjTx+IY%tru^dk;jsI5rpI#*5>u4xgg>c((F^(4&x8DL1m{ab52W1B5dxJ$d?`Grt zzD&)3#@S{?Ka76aBe1G3E06e)zxR*PHdtq8fV7-{FX=cCYce>>PUTN;k0^!(;Oyba zA$|;DRqyVQ=Esa81qSHs>PF)yS})o9Ci{fNC7<-ZVAh1G6>xQghTK^Ab1+kgmUdZq zQdfH1L!LNm=Yz&Bs)liKg9<_m_V4=A8lB>2cHBCpgy}V@pKAHOpUE$?eeG+GLOShRyk*xsE_gB@x{}=C_L{5&AXZ zh$M#qXPN{_u&i$}=-5=z8RwoKxVAgz2iK`;_)=2u4=vVicD7z@bsK&Wt{0IJWa~0= zA{!)M^ZDMu%;tb}8~DE2!D$N(A}n8r(vdLfFd_5F<|)y^Nkjlb7TkkuUE-51R@iu2 zRfPqnCkB&qG^%g9CxzCMgNgmGOvmb0w2LIlVA_dC79PtYr)8#mf|m>Rb? zldhbcj-V*Nb&yo#5(fX#R80?4f53*mZ_5E5#u4e!5xFv)iSmL`+q~2cEXr*n^vyGV zQ5J+`4y|SeTXjPsiT@N>VodaAc1`*b=NQ*y-H(n90=iEv8WvSzfgna~VD+_P3vUuc z6l8&c<|e<82_pTGD$ecTZyW{ak*H8EA#Xb*LJTnuDwO_dKbG+3#SAw(CyvcIQwA4U zS0Rj@O-=F&h)$6>+@@wdoO6JbKb!2IPNmW*H@swDRz$4sA7*PO~Q;9B*^0Gg%Jk8;*@qsJ%WHFMcgiGO1tJb zn*60doMX<6ouV|^S^VAJoqW61Os>>5^G|6mnO2kYgu4s56Gp3a2`PONgzQF0aahCn z^RicztfEt~u5oyk)FcN06#TsZXGoQEDemBOA?gHTPnB?eY3^Gz>|${&Fk@_1NC zUb-CAiIxke1KIAcbzwC*7ItPuZBz_!`*Yypty8W6WWB_mgy}ORBJT3}!u@4*bN+fv zMR-O3A7m-;#Us~he8A)yX~5uqD%scFi7Bs9vZ3xnM&H-<+TqrPb-{Wk5ZMiWr$6!F z_(#s@cxr)~q_b#4tBzb5+9A>97__27WjefVc%VdPPgDUf==F6gl2BJm zI22@<%s(lK(IF|a+Tw;!;EjRzB`8qwQd6fy1rPZcw%=rQq2K5WUit(26GjIcbd|iYYjNY-%&Mr=dpO4&e z0_N}@#pK@kjmrEBrTqfQ;PD%qjQVIrOrDm@mr*V77(n2fq<~Az1$OHf@%y&ZFqq7_ zvu28Lsvqx*5q9O9n#|Z_e~1Cw(vH)SCDML*wEtkIs%=> zEo6|F=ro$xhV0=GTXg_1jU4vtj^KwyK}TMNjxBj0S0hGs7X*ty2n-LiPz#%?K9?KN zX~A~-1`UN}-~QFlhTVXXt#GO*bYbN3RXWW0+52bYV+Sv?7|p=Vqf}HuWl5XKMLR}i zc=LTfFxcemR&iRAlg|<&K0B;mZqdg4)-_l;GT?kG&!_boAtxZ$j<0BVAXj!7ct%}R z+Fy0dhW(1{w7^f&!{$Qntfdi&O(M?07GCIwD6>e|YBf7w7J^BlU1E^BW}o+u$6#9Q z^_K8;|AbfAjKo@UzMl?{cBfG_Hh%Zje#r@`PJ@FqfCG+?*~{lzH~#z1E!;QtRK*RJ zjL#O_N%lt-H5Q`+*2J+jn}F)BLQjgvL_SW&u@;H3RFrEi(7u6^$J0VX=Kqv0UnYtx*WuEm@1Tn(X z^>j|S`N9gBPd{~kOBa4DNxxamDthkOh758$(rAQgTWY%5#EDsE;ylXGuFi<&ju!@n z_%s_UR3~d7;ummqE!1@wXF5o`4@8S=XVyUiYhzjnl4-_$)<4EZzoN@7}3AQ^qPvH zat={n!*nCUKOBAYaZp;hVbttJ~#?@*$yXACDqruhymHeE!w}eE?&hG-t2s z74m7zESZy);G_{>zXrC}q>`$0a;#zp)TN*`+GlrV99AdL?Ewedj|R z7VG6%9nH-y)&qF?Zy?mgQyBl8e^$#;N( zss)b)YRFOXnIHQ;7t1LYZfz~1*cAw@!BoT3W*igK?SsO!Ai$eo{WL37omfr=%%+zH zaZKEa1;}D}L8Gy%7;flNPfhd;bBG(2zEl_N3UBs=LDN+M z1bZklR71ejM$AXSiK6ZtfeGX@&zj}*~Su!#rtYM#_>#h7Ffy7y>9?{Osq zWz~JI$)S|^SAVrbw`10IK~qpcRX>y%YZl~&2od0XlQ{HaYUeJ!6t2#JC|Kzx2Nv!_ z!Rpdn5=O(?TSaRI_{mEG4DJFw$BNma6rrQ-QCwUZ;!nb}X~TRmRCk$!ePU3^ z9Z-%cw+yC2_Ci%jN_TZd(z~q8tRtrjt^7g8VA=m#cysGVZNYXXc7D-!aM8Z%$_D!m z>WW71hkt2barB5%xn`6TG!%X8!`^1*rV08W=i!wtY;}bE~Bt03AlN`X}|^ zXUU2!(K-hMO~u^Ln!3w}tyqT#3`yhG0b10ahNs@v)3H2FXm5~cFg*g_XEJxo?<^7h zPD()S=-!-r;w_?cg6DezkE~j3#JYcO;yoUY`a50TMyYf!_A#h6Hm0-~~^sDzqvkuT$JG>mq{gBtZRL7YX)3%Rc;%!zYzp^=WOXe}qP`OgDTdcO?B3x7 zBm1jOz=>0Dx>TonAFPh#A$EQ}NhjB8at>;; zW&_49dYA5FN?`>-G6YZCjRu$4&IZQs>tX(Ze?ai6+6$@GV01k*4S3J%C+S^2Vx zH}7VFA)<6qFz$vlYO^y-t=V91FOS}VFEMC=5@EdTafZU8TXmg8-^jvGXj&jlv3ERD z6&&1#)~#Q^L&emV(NUp{_$^UX_m#pC$kXla<`BWWP6Sh1iG=3MSS zF~*MWu4Q%Jq~VT#apU*7<|a!#f;2|Ooc*UGW!ZLsi(Jh;!@h=uBs7`+{f zTMosGBQpYYWr{0yOok*PwL&{F(Ee-qK(R^gz%@h0Y_L>i^x9cr478nNdDA21bPHSp zwPRq%YgbC!mt>X1l6uw;lbRyP=4MxKAM)R}@p65=ndIgs%bkinr&zvrJZS&+p?Cok zop{PbTq;Rg6rRDg9%=Z&dd^oF8@dnSRs4h6k)#KFYPb`Ni-~bIwp_bqk5g&o+D={G zX5*si6_cUDpP>@SCja|x^^0a4G$}&z`7v~M($e;+acO-pX0FJeV+Z4E6v0V=n;A> ze}suuVQugW?)2xmncpMHU|hujc!RAJajta&4;(mfyCf;oDKgBKgqAMXk9#`!aKGM1 z`js{pv+jLN!k-aJueBXKoz*xEvG&BEArZWd=oREiETxdA){--t7jm;}Pf51lp-tSv~}-Sx&{$l%PvExW<@UQ3dY|{mbix9{#0qMIGSCJJ$WRW_F34 z2&RZS6tYO?>R8MzMXl?N&pmvx5<{w~wl*rom)VWOv-n($LGj^V`&oa{pvs1nds7k;a z*@$gO4NG4#2rl0>>4wvTLrful(RUX+gG@v%F|dhUu#8hp8`-&4RSJV2b`+hWoCKw{ zSZ;(l)_(r!$4b~>`fLU4eK4Ra9dLBZ-byHPE9I%={w}Q zHLLAPwY#*-iCSe?Z(xsnUcV(ALxbg@iTOhCO@Mt##*tI2Pg*bvCfE%=y}p7a%>>HB zBzLiF5K*D&3(?HN*z}y4lGTYz?N!zn%kaqMBi!CraHKxi`{^TUp2)RIaZEVOrXGN?9))26EF3qiC!Z_kL86!gI?SCq`~19 zIzwR%FtLbD$(26SFAXb@RO4e80%@k4TtH@KMMXlCTdV5g{6tq664su%Pby80{>6G6 zD!&*jpk|2~BseYYa!eGbDpP1#-=pyrTdUIF< z;FK4~ULt*6&#BL<@E8dYSeWr%m?c?1hWXbGu#VtNgpYGwY^r_Yfx=$H3YwwtXBrO> zbB8QZq4|X<`=~~0eLO+!3NX-GRN?h>xB2IzNK!D?Zy-3hH}VC5#!EDFV?2OV{0y4(hjCf6#CYQjEk@1j7x%q5q2v({&HL}Y5`^2{Q*l{TO8#@dbU)u4x5 zlU>rnQ3K~zatsrXFAiJ6j1j9hru2-Kz6tcgv%weSq>3BT&swsS$Nx7KBx)jV3-E~? zOih~1y)v~!_Q;&h?A(ED&;EK@CDBw`yqy%&*)GDSa6im?NR&I1r$6D=Xi_`;Ugz!g-O4w;XXTPD8muGoSAR0t|8tkd>nFIaX zkKaD!8?Ex|IFB`xAEE~YAd@)2g*n`QblH;+tnU|Y>>y14by*I+Y<1j|hIrZAgcH_- z+;|8Pi(^mF=jpkZ*<-bSr1FKJEkHVY5^{i`46gegV6%bHvhetri~fl?xufhQ;7uyy zY@dj0sP{&;-Sn85yrKN19qVg+r~->fbq*Hk?As0WVXIOZgKd@l{sE!g|Hsl>N}l1M z!o^jw?Xgt0f^Gvo?3yjjZA=IL7ZTXAS0RU&h!h#o_Y8_GBy;mmEpuz$*BJ&nF4xPn z&=;pJf#cL_ceeB*b1M_0=%luRjEC!f?r=u^M7>4+a}?fyA7Ia+(Q7%Q}o#$fcvu< zdIV2-*@zQ=FWNGln)kWVxz4)ph9KRlVG#DN^#HlmkvzM@Nz)hbdexi0MzWuGyLS|@ zvcYEnZSQ+s|D7P6Lt{Q1{;kC?=ZkMc)a*!(h%`Ka)Vljo{@d_W2>Q_~*DI|oOqG=t zdM|FDux!)(@mGojh0r+r^N_i2TTkj_lTzR2bPghPncU$UJDu2KpC%>d z>Ug|B|Lc42!Tc|gh7x;vP`zI2Bw*RNg?j=(S54sKU%lQTODq>^#2N|!;#|-3jNL{Y zLF4YtA@CdHIY5r5sJBGV#XdsGktqdaU!(s08yuqNcnE>fz2V1Hv)2%JdlphfMz zsu|k;+su0mCYKQtC{F<|;YPgz?;P)y0j%a`x{O!dP^FeDv|!a%#)5wSKW>jweXBt)8{TPT%jA5KY!yUlN*|!%T)Q zBNE+#K|#(Ez|H&hN3))WXAeak?TLfz*#{P!<6H%z($Iq@-4g%b&EIapm&$*U;23J1?rhW7XL(bF36SmLoXrTj+!D=z(+9skt;&pE>&%SXM z;PF1Xd8s_vFHxJx&hGb}TM2DoCth)E0Mv%BJJk0yCttBjd|0dgJu*Gn7^KZWs+~O5 z<%dU>-@1Ah75g#7v12iM`852~@{NnQ*ZNeGS-D`R<$dq-5G%7;)(4y@@W+2fd+W@xw0Od1gYT*p)95W(RY_ zH5`jxX`u~D2*j&3cSe?~BHARNP_7*mWim;Aymt{HdwPovRVr;vSe=0^4$R62fb&YfFQYJ`yFZ|M*gIs)TV zFF$g~o2o7F))z~otKGh2{^XV(KyhkfPLe&pEyeBfaSp`d^>b)eFbiVXA4JM=Ur&s< zS0TsaD%}|a=hf9RORP0$|63bsGr6VVV@s=f=k}HPeohAUD8&jn&4CQVyo>Wo#`#ip z-*@LEsAe%LfX84!Qn8?!&PQGXa`^(0$dm#%?E>R@yV2$QEt)c^sat}iGqgBkQ?rj+ z!Rv$Y{q|mpeGo30)sy>t4m*Xl1+Y^2Wd%=HL_)N%Xp0ieP)JIzQ!sH3*EypX?sKGI zhkY3&T}%?FlRw*P&G0ZdLxhtiQ-OmLMHR*}T}r;sc->=)ue1K4v+j4q15M~ycoO+9 zo2P-#)vI}F94qRC`H9nI>a|r6^BT74xzejxTe5<1@=v)dLyGHdY?%wk;oGGwNf}Ek z{U{#)PzDf={z=l1^sG`PQv6;W#aGC}0*l~RwseOUj0HTIdStS@e*g$7-Pl*@b_pau zARt#isV*Hvv^UKeTjTSbi+vK430F(ku|*gJs;$ay1*iVwS+7;Q&&$*nrkxk<46YLP z+9t5ecWBB1+SDT;0wdYwxKNhyQ(KW$WJNh3#r}$NFE);a-x6*GDDP4^e=@zC;$e-B z-8BBfu7)03M2yYxWfTMG_$)yZhV5LHoUi_2BF?tbQDUQ5~f$<#!gFV z8_I7rp?Sjj!6A^O$m_sK^JTBa>g>ZJbJoO4jtgmvr1SwkZ86*ePU_T2J_u4a1O7^P z`~_>lOe3)g8CEPNJ7bP#G6!#(`lAT(2Wj}6>t6n@O2|6Bo75)e&O$ zPb?HQ;}7MMZlrXG%q^SlCB;ebQs!pXsor1NECy7GPu0~-RMC7eIYdK}LmHK=b00;+ zr=%r(8OvsjXAuaF#*aM`5N+%3wY)r*$n})ots$mx;N@pSd+{Hl^tyw-|N2)tm=+!- zlO$V@Hkr|+m)!kDNSS^CXa?fY9z0IRtz0-4Wkxl$oXfGwBI~^GVmPK20H<=6PX7R4 zj`dhNVsTEn9bMA ziYgMW>mgISn-0PQhvA&UNs&()tq_7K+D{zgMofNC7T5T&mhRQTlhLm}NqQ4RaK|tM z-VLvcGNv~V$L^5U54qTO^zljWzrG%!j_^jXQf3{{b67t;Gb~%(i)^19K}r8XynJke zwfB*fDo)=wrIpQ4Q)2D0*Z%sMtqORHAgln)mYn*us6AjQeTUwXO28MY3|aXh+wuqy z4i`D7>bn6pSWn~RSLSN;QT0nXdh8k0vF#vOq9_NvEJ)_^di6?)EJ=mAz^CN#pD8{K zjp7|d!E+hR&uoD=*SP--pZpU1IXLhM#_)if`S7t6@1Ek3{eS21B(k^BXJiZ5IhbXv5^Pz1r2QDU1+w=qL~)lvVH2Sg^KFmPyYO zx6J{@HxJ`ZutnqFo5q*jVc3^t&1Cs#W#xNCCcYqWlGTRii+CH6-sn?%vhuS9fc%#9 z#E-H=Vi|{Zs-I_AG((hz2gB$U+%V`#>pc!T@Q|NXOww$atlk~9NFK@C5x=u=M5x0@ zU%ENA0jcQO&=lBrax~ONQu^$a4-8XD_ywcH=9#)Lq})oxs6lZX?|dKr(s3Fw>M7U& zuv+J8z^X#g>cPU!A107{XAs@d%z!G|8$2ZAOdF9N%2o7SeMPTlJ#-~q$w8SpkK2TU z{0ghq2Tg#czUnHLf`taj*=r$ZS zo%YicNvc~dGNw0^xw8@R&sU(mo$^2G3A^gVBac^a&M7QB`2RTrKXrfBIXJ<3#B?<1 z-hT^r@!VBp)FOuV&8Yq#03|@$zd6{IF%k_|epsr&1H^UtuXnr;AGnSqUOYCBOrYso zxlmYd_JuPL3hoem7YwUXT{Y=rkroZVa>t>oTlZa#(Ph__Y|8OprRo3w9jrKkO|u8m z&kJVP-HZojVV9ssZ+{i1^MG0H{(Wl06Vhjq8_G^rbO3Njw(mLaW3~>kw-K3Ma^Y4@ zMyHruTXxm}1}$;eB2N6NhvTWQ56THKaeF5rkDt9E`Tl}=zfAwV4hA#%IX1S2i&#iRbm^f%NhIYU$@Av!rEgAAfdp5kd zVQng1;?{J+@W@ed1(XuAMfD>83BPX~a8wM9Eyx8B#M1062ANO0620ZZ8Tj`~y56}{ z)ji}H#9ViTU_W*K2zqvo`hb~j+q_lr!(fPM4YQO^0CW*nBjo028d%ES3FG*kjpid z8JIM>XOa#OF-lH^LaU8MJd!=C^Ac+gpuPB~=HTeqc62}LHCQyMfFFolH>)D0^ztk3 z8Th5)=*hZl#Awt2I(7o<^mSK~&-SvFdGmzBBXF9Pid%K(uWL0-%0lcjqM|3kR1eCg z;;Ti%lruPthq;x1sqCudr;|0Eh;UWbS(8@{x(I_-8 z$BYYayfJsenoCE<=D6{2Q^FzlvOGj{H3kiiQ9c6WO)PNxQg^dpU^VE2O4!Dn8b3ro`i8wy|Gp75}&v%s!j< z#(FAZhb0A8IbCiB(9v8X5wawxPDNl?3qzZ5Uj{WKW=169T<{(KrZ=WFAfU#PK{u1a z)Yif3s`1_I!=A)xAl%GXRwTLyL1|#Y5d`lsJKLyJZoJ|c zMj*W`diq1tRm+sHPXE9%n56BOW*4b8-XZN{e0RhgZ~8kwXI9+S_1{VH$B?luEn^cy zOlVKm+|JvUpgu~JtC08-kCI65RT0l&+_jUR;K7PXMexLdlXx`dEWJn#_dO56-@_(}5=_3T92RDB>OXLrj6Na0Y8? z9enuXR+mQU6_!@m?9soy-KRZ}?rV#^$ezx|nQ-QE+BJtO1eY}-@5Gynynr|CxG0Mu z1?Fw^JTQS{Q2Q-{zt;t}q}nvk5k%ywl3oj$zUR|`uAcY^ieSnaJ?^TOtA%Mwh0YFM z7<+8V+bK!f0IPQ;L{-kSY_A>n;afQpH4El%AJFScD?NY&R(90f`=SNw1TbBdChI8+ z;pwJ-Iau~XGH4J?bK4`Ty=8yqfmY?h7@(iFiJMyjc-y!*(guo~X#UIE0XV_p?W8Fk zcZ4*w)4B$i0DCky2$H5jGiRthF3=OMndx~0&}N-JV$+yor&b~DFpZ5^+G1%`^0r^6$P06#_&?)p7(ov=$ijgYL7 zyulDy{;J0waXq-Cna@G)za3|=51N(9*eM>RJ6|sek75c{035E%pd?ecNeKZFZ1DDn zMMp*61xiav4+HaQB+Vbw$79I{OWcVs6WHtb(UeNn9rzHHK(oO;Uvj=EZ?Rma9D5wV z%J4-Uo$U268{!Udd8a(`5xo;j^OwGDSa?6OXV@>NCOniHOcL$SB@sHwCEXUPmuyC! zyi@b6!>Z-W<-n5kC!lSD?=m6x>W~_6EOGYZ7M%R z1|_qVpSPb@AE#8p6|xZAkO05|RUQ<|fR?4=%}8DJzz10jT1;r?`kt&Ct3gMOiaNc` zfiMUxDw667T+2n&umA!GvYy=bH*QP_=#~wRU4xnp>iYN&$wl@t_#5VD*TXVo5A{hL zs8g&T<178qL!EM?pHsbta*OmIq^&*7I!PCb+Sr$gNZYBIuntgMpy|-;NNuMt2xf~X zExP^BqI5drtZ&*v8GQkxsNuY5PCeZfJge4U$JpU!sgJPmUar%dcYJCre~Xw6;MnAM z6f?Qr!Y6AktznY>Obc#%!agy27b|6yT{dg=kwI;ul*hk3 zXYOx}@Ys+d0a}(_L6H=_{ixM#8_m6d%AQK9tL32>c#b;{{SH`4n@zF*b-?A5^5CeL zIW;C4QuJG7L=%Lx3oq2y6_hQTo=(r;ts<4Y7E()ww zE9)a?+Ge^C1G{WoXy!) zF}*KM6NJ=aw;V3Pkq=2TIVX1HNB6q};`I@?5ipEq2Pcnxk;nSWxrbC+I)e#k>bTV1 zC-D~;146XTKj5XGMGu;r85Y*47@#Is(BIi|22kdHs*@~k!8q-DiHb{~M&Z|KKJi9J z+$F>8j_R*mF4sdSR95xG^=qDjP+!gi4KmfY&`!!KUR+D_EV*Xk$7aMS6go|K!C4M| zB%;`uN+?C^{+Z^A!z<{Q=&;ShfI*Y6&^_5iX!D@pyQ*EEFO{HX_=zUB*4UiuMwOe| zU)63jy;&d|U#f1N(1?^487j~YKi#i=VnTR*C(vlFZ9*v%h*Y0*+s=gXl6GTLu4JT@ z^E`|e-)IIB`#bZ72Sd__PtOsJla4p9uwC`(=G&nEcssP1ctkIhB(^!;x7H(qzuIkZu*t*i<80H_>Buk_1`g{6fnb=k%AoDDt(FE1oG@=thXc1)Og z4j9Q_aL{t=_k9)r`M;Rc2nt~QIlyGnsx#4vAfJ6b^D0H>>PGi{(+btde9HamN}5Zgs83OQY{08UmwT_vjtFFgQSidKCq5kay%fHiH1ljDb?BV=Vfs29rF5D8kF z1yqIcr;LsGdWP9m85tUs>ou?ZC_z^}?G@Ki zNi@X#xsTE3k0qOVTAnyHldzjDMC;#|^LvVmYo(j1` znqZ57i}9{ziVq^Isj;^^a?X^wbiOU>vfA>A=c=YNYJ1= zvBYNTKl+WOPNw~^%6jbpBE3~W#p~?8l$-<5-(PB4(NNV@r9YY z^1`oqWI5pU>9@eChaIZEGK4#M1^cS|$7ko{Y1pCb{_Xaio5eeUQDv3WIVk~qX5do! z2i`eJ5h=wgJ1c^`^fY$OZPZ3Htn@Z?2uSz`fqB#twZ#i-7AJrD<6E9KvCZaBzm{Cr zWl+%(-6G~kp{>_c6GQH9wJgJ1v{#{^Fp1<-^J;x7lLJ*qPl!si?t7w6}NcVwnc$K5-B+hRw$&S*FvQ;#ecc~7u9VHwJqs-T$*!LITti8OCiF@~{hvdw)~qke0M7b9SF* z4|m+zul+-J3xb)`g7c*2H@8-9K_1~mSiWwzw8|g86cgOdIX>nKthFykZw?hod{K)T z`DG)FBsX30G~M~~oqiw*u9v>zK`nJtyI{(*tn$}R`2WQOLaLe+Yw4}S@i^`MWDLS{ zCCK88EIFi|ab^25>5SdywWb>MKozj)Ga|HRm-a4R!Q-IyQBMU;yxN8U$JMQcBM-v> zw=TaxFlzYHoQ`g=AEDGgQzut+3333>@wlrM;kPdaVgZjD_#U~p&qB^Df+DBHK4ks} zAlwSjs@9eBn21IMwli#ZZ5VPW2bsrRiWr1hwIv7;!VZ=b^0FLopAug8OkF|8yywU* z`foDkvW?$;;zL9Q8YO{s9Sorq2exoK_Ab=Rh!`wa;UCAVBf{ZH23M;2O=QR0^p}l= z`t)6Z)`!8D*-Af#rnH7|Stv?gLx7Nbas~u81=fc%NyD8N+}4BP0TG>YyNK6*7HnXs zY#+hSNZTLX1-yZY4LW!=Y=mphVQk+gUjz2Sh14BAhBIwH02mFro$_y0|KsF?C!S8o ze(U8vdGWNl^|)zo3=M4>9l&mXE^`DZI2vz(<9@KKDDCzq`4%^oJ3RyLn~so{pkF~Y zCOMR9l8-H`xcHLUx(c|hD56$;dnCoquKFsoQ5SqJNXYcPMJ=RowyzBxeAF9Sf^D!c zjVYq<=xs&B%IO9Rn^Llc{afaB>Ol=);ij58_Fq}-13H0o+4wW{DPq(YH=7_O9!;09 zBMkej*RJCZR5i8t5*RPS;oRkLEgbBy3KiLNnRPc=z+};;nqWnRN$su0890vSs6D2j z1zq>Q@4R?c7A+KyiFGEkNA(F;zdlW;n3xuY(`xb|w*{d$KT zDiakv=(K1E@M^)XN>e}}tA@uN-`}BSlD3s0l7Kuy`!|vMVw(~n8dcl}KcbV2DVh+AbjU=S=WAZvqL53jv=BGV?twrSSk|u7=@H@I4OVhF zN2jJ__C;Qd#JfnCg%+%ABPqYsjQ*_8^+%Lfek8b21hd$Bf4sPtwic@i&{~3HFZ~y~ zmro$6^w;q2ttAq{R=+A6pBR%H;V`Jbxno2R00NwCoqZ(a@d+S?j#aqb%R~0sdTaB3 zb4s6-SN8RipxC-h@Bp%^@287y_fctoC@jt{n-t#5vra2Bjs6%KV2kp~JCk8vv%<@5 z`4`UZgEkZZYA#ql|HWJwb+EZJ4RcY9=uoo77VT%Xv1OBgg+z{6oFlVzjnNfvbDwL5 zt<2Y@A$)%d%wOH1z56JC-E8?!Vas z&$kHJZOp^%UM{Q)%Cy>piGX+@@H}O^V%TW`JDD@T#_8>6I?_9#-Uxnr90y4GntjoD zh2!OJ(k|>3%$xYx1D}lw<~EW4JnCyDO&6S$dNV7Q29#+xV!?>hm{n<^)*R7EK_%S2UQO))ZDhgUEXmB3e( zLR^Wt2OSFmFbSQBKqPmC{4p3Fzpanh=^R5)sLa6qBnKmAVrfu1gp3*9;daGj#{0>E zOQE0-5wEGkECl`-dCT{=?Y}hEf*8TJnY7RzWz(SufEND|&hW5zD2{>8TSIn}heq?g zpA~PZ0K$#~&(H`C)$7=pZx~J*&;~-(JjDV3zw&sJY{6dT)0RGF{3QBKi(Oy^fJmsscPJnpWJH#L~iA_2$5Ft<%n$VhE@# zgtMC6S1W=K{e}#nT#a4jknqZRWnigL36yg;igeuO9}noaBws7bCu@t&j=X$0 zikH*chGN4-=i&UPZD5tgwi)N36bz*S8IB2MS}@+XeoEj(K=1&z@)~Vo^94sP!=`Y~`&%`H4LTwiUR8#k)d?h%vvfQLd(@ z2O20*D)Xp%iWBL^RIz6iA%$I@+|fDb5WDOZ(Ibi7oAW{#KxR!xE*g?@n0)b%b`#+s zXfaEf{>~|*s9|Hh0Rc|ONI1E_EXF4#Y%yBt`z&b}x~Fu9Vpw?*0i{261!jG$q)q>( z@RMKiAM#IeYAW2r{8&l)@?ikZ08nrqV?)$?>#~~aWYrvpQZm!^@A)ApQ1K3*Fh3TQ zGnJVG{H3!wb7Q5?0K;5#*}gu)h0J+2*xey0h_ZsWn(*(-SA_OWl=1l*m23oenozlH zzK>}2`W&6ht9nbYxUAjWy`TM$vqa`k6XBd-pE2ZX=BwYcDfIcZawu6cYK#+ZqsCbcAm1|dIkleSz1tk2YwE{1Szh%6F zGwo5HmGf|$;9kIY{5?P}GZ((2E6DN&$MyQqNVHN>Dc--MjP5koi#2?o9qbPjZQf-5 zGCCSyYln&;bna-UsPj$F*TQ=WK~-`wX89XWV8A6_0;H`Bskn8H|6I4$x=M<-ytL^G zjm&#@i+{;NcMLBB*eIvv-GLzFsmFkO07MapqQ&L(&6Z)S~fI# z*4-v>h4+UD2oxMb9QPyRVJR8bipy~ON@WHdS{)WtTS%V)_$^eeg`vggUx8r}H0{VC z`{%&!F15}8zSr~CvxlkOH}-KePi0cbfkR;%QCsTqEQ8-W^1OJbl4_iUIZcY}DC}tr zNS-!1dx76pEUhD!(hXAlEg>xD$FzvoK8vQSii@a*pgu(^kr1n}zhpQ()|3q3XOmx*Seg(R5` zYDBw4!iP4tN!4C0kXTUeM)O2XH_c2PELE3wWk81_kOtNLupt|_f)>zI_?yj-nrjw1 ztZ1JC&G+3E&vdNvw;Z)n#2pKB>%ynWuI+t<-T2JZ8Ek~)X8O({pgL5)>VK9fY=Pt$ zG-`#=rIq18`WsjrP;HK$q{W7heW@7?ky8t=e~5tf{_@4=n?>yctfMfze?G*tpb$} zP7ckKXB^bk=G(Y(6^gMLWnQL&*U9_%a$=q??3Hy^Fm^@7SXvwo$5uofvGwfIO0mnG zApws;w}bWzY~jhnu2I!1ygqWs&02-7e!q8L)8R4MyT?8zwAg9QPs(0T0D@pYC8-<{ zyaVAq=~`T?4L^H{v$5|6w#fnpz_MsUrATySI07aCh@|`el=*oOWbotko!LAlP-U52 zZsM4g8EcFZZwYh4##*YoKa`5iG}lbJS0-&y2(R%^riQV4gcY-!3j8jnjeh)2!r8K+N?TebaOz@p zxB`RY@8Kd^1w5hvj}gz-pvOllf3*53Y~|Bj{on1Ze!=J3$kdnlG8k-`9S1IL_6KxY z=j6+iMI}iI+Oa@>Y!C`MX8w-5F6ow@Srv%&Ki(bhGeNWo3jt}e=`Qon5Z~giyMq?X z(N$Fo0BDr``{gFCtH=zqKOZXrp2Tf{jgQQ3of8hK9k!nL>tiF_+J$tZq#s4f=Ar&A zkiFY7l`7v{|7pSgni$shyt@s57`?0i~oywWX)*+g*yJziOOS_Q?OZYHE zfQukUzxu(eJ7&9sp*#~T1CiGfBfD9YGosrr-U`g2qj*dI@P-s6#g=}S>38=$vfuhj zI|f3Lxr`K63kE{h3O8Su+_J3I6%)W+{@s%TQBr}H@wquHlWkP>6aS;JyUtH~o3yLc zm(bx`3ehD(8{ud+V73oCAI8eKo0x7t*uzRD=p{uTm}_t$b%)TE-@CU0BGh)FRO`7q z+aj#DGUByz^)Oj`s>3h8GFxI(lbywTcytJAS+BxBi$W=bt55GQ`g}}DZv&D4X2{l@ zR#a0imrok94uk-PGLpIRa;qjgZnujeN1a+W^KXjP0=l0M?r5pO(zYLch3uWeWycL( zZ;|-FU{O#INW*{%wDk^=ykT1~WT! z(}3MY?vP4rc%K?}#{IimfcJ8v>SG|6bpP((vm>=$Ap;5%Dn#~c>_{_d11!FgIJu8` zl`4L#6L@odBP~Av+lPsr{;92Dy2;Gm;{+N9I&;=*ibh~mQIsgibPG7e?IXjZ)g>us zw97R4bT_1$K5XTD(d-lJ>3Ce@7ogAPrGd?;FAouDz9nHlmF)|1aO%%KQbl&44XPh; zha7n?6C*|ItOt=!vuWKd&yKLkR9WwKA@M&VANCVt2zrYUVy45ZTGt=j!3|gYoiFBQ zVhbi9boy}}Q-dJi(9-rYqz_oxN#7xUI5KIX&c0y%p#?rb^9s|Sy{n zPPIWO%Gjw5m}I8*6GLRU8pSQh7(koXtf>V1fB4zX+q#>qkbu<(btHi^GGm|h2~J50 zt#pclNhm{VvPqmdU}pNup~7wy4CMJV;@;o62Gt`8P^ArKkBolK4z*}InG_<`q^s4e z`T{c0hbeD>19pXuPYOPR{eoYI?X95??YW9bU;S*AQpabyl^F?9Oz@n6r z&7l1HjH`Q%@MRQ|%w#xgc+%!5CAdl6tf!5Q0FCp&a}0&LU&x1^ECfFSyiIGhCR`{i z0Dyt{Jm_@~ySj5zZPD5=%^atf)8G-LiV< zi<@ZgYx~SMxEveu@EdrF>z9tBQon<;^K_O>WB@SW`%O!UqTWAAM->F9XUKTT;l6=Y zXVs?)28X!snlC0YBKL?bgj@Y#Tt~pndLX)bIyWzf6z+!d@JCmhcZ}F-tZ?A((N_gi zVe1t(V7HgII~0!;Cp7-FG*wvUogw8GbeuZ1>#D9mphS3F#8Q7X$c+c|Q%$Q_U4?)x zSy&cb*=2&J8V1GBX}jFhN>MeF2rFz*!#xvcBm#{U%$L4GD&q;Cjs5AhTk(%=^4OTy zZaAO^8Li=w!V1AC)Kf=vV)$$|mWRbr*9bB?hmyF^i}>V+ENj_?P%o9Q9(US%hhKQ^ zQoe*rJ!o!xTEcYBT?B8!31KVt$n}O}q_&&xDzK-ZK8+ggx-0nh$tL@e+ybaCgO8Ab z!7~D5C|&O9mM)2eDD@`KzEPG0^+sy=)oiVRfHN?&V~TlF{`UDsP*{@J03i;Y=!!&?i4-2Up*g}~_pJF9QUqB5TQ%_H{gwGt@+{8e0Kh~eC{@~gBxV~<1yt&o& zIvmX`!+r3#!#N*^w9;;Jg7~^jurQ3g{^IxhqJsv#K6^Jd(|2Sw%+^~x0OJAp6Ri$y zkqWMGI#`0Kd$6wziaTcaOw7oIbe}z3^jBJLgar42tbTkXC*#L}WQ=3tHJJ{obzJ zq{C{KjF@92?fqHL4ZF5~RHNX&LwL*lK|+5?T}3qt)AEOxmEI|Y&o8ym z@AORE;mJRbk*jaRl1yKY3%EbwJR#Kxi$#D$8#P^JN{Jy~%W<8b_}AFD!fmO=eXW6; zmK$?`#BmT}ERpA81fEA^8 z`gt!bs9X#sCH~*;=2pfNzwZ=Mq}UHVR_psqmc*h$Z?>7|!RIj;pJ`txPL!AXUF$+# zo~m)g=xqajiI`RWrAFO))7>Oc7u0Bjc>IT4IMmUoPLhs%CQjsddaDC_M;5xzdH&-ZC#qqq03 z(4_GPvA}@xiOROwW{?S+L^uFM&$qXzTdBwO0ZCT=K_TEgElnPc5&UOjI!sAkM<2JV z%}AGSaUC+eEZJG|BBpXrr(er@6_n9oyUEQRv8?Avvsu_vR9RdXx(- zV$;Dpmg%&k7?0u+)@hLtcS{HW0X~HI*Ur@|;{{>v7Q~!(7F;BX50Zz9^JazZ2G>W* z0&|Aik}Uc?V#~R)Z@9NuwY@WDkU&l5r2>>M!;`ECbAxn@;qjC|Qm(QC=eOQ+w=`SLHn9VCOroH+ zZ<2ZYb1*FJhyJ)s3gQD^o|q?OfB;2Bg7ydUrxHRc7h3iLHbo;t^!9taB!|j(=&O{N zupvC7$#!IcN1BTEX_s#s#E*FV0eSO)l{aUfb!^?Vp$>1FjI(5qPUnoFD8#Ho_A=?eO>@q>`jqI%h}m zGrtq1#iG=*7F`XZ59FX-4;yNw2a=_7dP>$~qQm5dt^&Uog!!DK5@a5@^Q0S@+OVeqf ztD$TnV|qKLZ0G*{HhTQLuD?#6iANM@-*vED=Sn0+hSmP-Xm~kvneon+<@W!wb#1 zCckvNN$1=8+(<>D*shoyqGu2ZmQJ}>Du)Qk!EgW%1Jh;aQGmE9@L*x{!^h3~$#Vz* zB_Xza6j58kWiK%v`dGvQJbrPEfR`T;IELHb-L$no?zUXGHuH&xX&$)=`sTa){1x?z z*5>2goT@-7q+%=12l691Ti)8zH&`?ec1m7Ppl#%wNak`ubzg_Ja?n8M`Pq4>)LE2y zpuji3iIJ~&O-0ycDG%A-cJXBYzhtIuGI0SCQa7H|Agr={WDo{x#AY2T zZT*@S*c$?RSVE$b;)Q49XOwJ9w-GRWPHVQH$J4Y})GfMzvze23$pAW>cx}oeQ1_;{ zjfJ@)py!QsKZM)~H+|1KsWzy_JC48jTC;5k9OUj_v`w^6dY-Un{tQ%`?YK)LSzcOz z0^OY$*c7xyu zXb@EINK9K{Dlnf*)z5qL?+b7Iej0@6P4Ax3+GS)sH+fwRO&qP7RH@lukxTH1Y#A^pE$arTc%^f9 zHfQ5AoCrM@SWoa^7*8MZO(O#r2a$8A{He;nj?O>izG9JyQlOsQf{xQ!f`$_|M9?u2 zD-q z&R3(YPhIfCcK|1wssht1&3DUuM*B+$t zyB`{UPSVM<)u^C@!4}rS#c1$_Kd=~Gs=&Y)?;=O^oqQ$x##mKngeW4u>mNeE^_+j( zZ^18{xLAT!bR=sj0yN}ypu#pA%>+KKAylRkZUsJD69+1bjX>B%H_qh#zM(2e$-Azu z$QhFFroh~@=yoil27_ebDXaeVLhVtHXW`DU!0tvhMp2nQ-r;L!?wwL4(Y58)2MDcV z?v4+oO>#XJ+&jg&)S5C@HnQyhF%0deipFklC`~HR=%R;KE*W;vZafxtY7UlXv`VoS zGyfPUIY3aYOj*WmDRxp&nOVCX$Og-h6cAazm=h~}rUPNA%UI)2JrQz9bUlhDwZns7 zLUA+$3;Lzz*dD?~Q^r?gs^Wn$?;M{|j|_|?rQ|FTA>vbM>M=Z!(E*tiZj>ZP=Z0Bo zC?_In?A}I=6+c_=D9Lix9ZTa}+`uK*WK0>Zg51XkWkD!tY(Pnloj!R*--D@I$WYpYhc8^O z+M7YQ1OCwRPog{oXn&z00XvhQ^78h-%ht{)(d1hi=>GTm*p=>(dfR#lOCuE4M%A!c zM(PT6swvgNWL}QE`1JODToCp$a{GvhHc{D*>8|I%iZy|sEGK7WdZ=8|3Iof*6Qh_k zqSsv2@Y_HrONDIX$>T=d%5lPV=r0%Pd48}GuASiDQ|xK`&T?tXN5n92SxN!FO?gfU zJop9XkG4GfW$KPlt*supy4J#RkfVP!(l`xLXKIE9e4eL3I}FKwPf*u${l(>&S{o_B zA(w=V6a_Z`7Ry+hVwQ@w62Zi3C{jBYG7Ryin#u()bkUcETL9h?jbyIPC)bCQAZ9@N z-tVeW)Ri|^?fKye{Xe+glWZbBAKRa;kC_{m0`Qe0um6?#)}*t&0XxZ3b6RHo9Pzuy z%jZqJDJyS(znNT2Sp3D_G&?D%*I}3ooZY-{nUseQvt4~*6IhrTW{o4uy?GMo^Y=f4 z2nD)hiLLU>$;6#vb+{?6kt@l$@R8D}5TIQ$SAIIGv=J?%MOM?8BLzT_wrI=pT-z=6 zkE~#!JE1|5N&#ytta0KFT~UkJ`$#2q{~~t9WPvY1{{jf6>%N{+Xk^h7e1Del>DPE% zz}07&z&<3np(kkV+**&(>uPl=4f9f)=RMJYEa@3kUdUZU-BaVCiAQ+w5U;{|Jr8Ud z3K0#TaH~ge4d)=r97W7@U`f4ND7erY;j@fg{3~XNB|a1nPDsx^^9Y%w>(iRcNJuF# zGC1)2{4iA)d7#SS@d`Bx4hx6$F4#--ahhug^uB+*g(*O41el>SARKT&IiIj*Kf`U&o^(YG)Pwr zCBLk|h4fA@(Y;iY7U93wuKx15-I2zcr1worBLxH>7fuwF9OI49o-cxGI{3=15SBh| z-GGE#kbqFdAv$MG%jCEj0hcMKN|@q){_NR^r$ zLikVC&9hS$h|0_3<)$oE4N1S>2Jcq0I>D%GBT8hoPZye*f z;aXjhG+fWz`{+eGHQvCxPfNO2v7RcCxZE;nDSUH z8{8POUvs~wulQ&V;?oMtg0|Dc(`d#@vaUoH)#q3<4HC!k4#X(%`d#hN*G$}Tl7Kq7 zkD8tlkSvBSjGTzInfY%=f6>kI()${PU4;N>B11w^Q*cV!$U=vQ=%lw2_t&*V5RpAP z*R6m3=9YFxCBHzbl=3&qKwWbL=ch&m$Q9k7zC)OeXs^tnoDh3p2 z5~S@Lp#wAX$T=f#4`=D3_r1_Uuj&m<7ha*-w6VEf2NUA}s?*}FmYhwndqBa<5RrGz%VBpv?VV@C!O?h76rJX+C72iy=Uv77(2v!Wu^$a%{d8}gj(R~ORZ=qtPjeYpOY z0}&oDtfng`f=uL2>$?0^e?B^xj+w-ow3!khtTt#3lDqpK8WbuIkCw?tCw}uYzdHg* z@g)(diFg?V$qyDt=G&{U)WyTTVt!J1#PqJf?aGOtQfmB^KM-$F_8f=FC4~V)y)d&Q zI2<*F${z0Aj)*Gd(f13)oi0AY1$9rq%XSbj-+QNOZ7G8fvq{r=_xA~4)`ARV29tg; zVGfP7+&xl@0YivZB^}7HH|Dc_GS!D&8)#KTUC+@n>s}Z0*qgs*=?dM*47q2+k8KzB zz{?|s5q(ogQmuJUYT#@AW#~Decl6jIxd<^HB~~*15?Eug;!AXajJkV~J(48Q2p1Zo zl7B1*1wQ|n&2lM6Ne*DsR$y~!g>Fn1eUgab!?g;oNKrr58~@Kajelo1_8SJtbEm8# z+R-fjZ`ouP$8eP|j#Z0@$MdUk`f*X_P9^Cd^q;a81HmrfZAx9w8vKx#C{2$?+j%@% z4tHWhmM6b^&zBp)DHUk!K;^@&;hO{E-o&-yN{EG&WtlpO$_yuB2aWXE9$xK6M{nx^ zx@Z(2eJcY4&j%3Hzruzu$n4_^4z9FaUjPg;-msk>Mnbp(|H`$Ex%{X*0kt&CZ>1Am z!Lr6nGY?3TD$VBb17Sk>c(%&5EDsZJpN)NaxOR=!GAE&rP2|pG-E-Mzwu3&8m|IVi zl4rK5BTQog>%+sfJ zC;YpOn|6UwrEHK6zRv3?^KdqqNtx>jA?GBB11K1{8iSeMgKmLeG+CSYp#jdDr_eG; z3)zkz5~btKjWBd+V5Q7~OVy>4c#}=FvzI@iSJIu3yANbumtCrhLs0CoEFR@fcXMuY zDaBLjY-_Y5Ki-dHQO$m^#v3I94Dgk=&;CqWxE_SUXK!{+;KPRQDf-i% z;Udr2XNCbIO%;90u}YCHQ3RlSgnavgS2-ME5i++7^#%^@*E&JRrvM$n7sVN6?!<54 z$pcYk{5|>NTWsE|NK4(<+Y8d61>)}Tv3au^l*?-sA!j^E(qDbD$^5!BuF#EkHh(o( z3Ovc)yL>T$s@b(&{t9`}!DD6|u=kABh8?A=2>Z@j`{nh_ux6W~-UD})wx@qF7k-vp zc&E#GwJ4v3&A+LNZJSy!o4j84-4?s6b{wqT03+mxX4A@`T(4@H?@>j~v=gaLY3EUp zuf26p1hS6%Wu|G4u z^x+u$;|ebk$GvaDhaoj^1gCmhgWag)f7ie}EEWn_w9?qP zNg1!U)?erSNTLyUcIfG_=7CgdUo9S%=J$_(n>34meFgVFL=!-$yog&^>zFj7!W1&z znhtbRWb)~mNUrl%HS21eK58|95@}w0D&Y}s!S2Jpgsw+>6utat0!dsAk9AoRjDtXb zm8e$o@Nr?VbrA}ME|3FWV5UN+bH;z^N4M0yujmE-f5G&v_!aWepMzN^Ig+KnJDF;UKW>*y8suyn<>g%$nxbs`^{!k$S~7R8dlD^8Tbkg zEhGEK*ps|Ii3Uc^b-=Cmu-WwFZY@ zWK?LBccyAR3dxIsASF&)0@rN`+RobaQ5=uy^#G9&x-BK3EX<>=tx7S@)p#J!BK~qd z=k@G6>~?`^V51p1tieo)k^fP&O`V`yYT;sekafV=QCy`7|NpUG@$jI1A!^mGp4$(+~6k(z!3C zm-Pmza$N9IE}ttM9c8#yjJcCobtSr4f94{|rTwQB&ajju6Ct(+G;4<`OmWBL75VL9 zej7|lE2MH=o^kLp@HK_`l6W(tJjz=vPwAkbEwumpFbK>QW_HFmRs>R~?CLDZrb3YeE?BKI_aO3}&3?Mbh{yWD`#M=tzF9zG& zdEl#>=~8+ZPEBPkBrX(A{gN1*KB9yWDUv$0APO( z0p8U~KHcgBFE^SLim;O9T_L~M=K=Hg@h8S$coZZRdPa0nGDrK=%iL7po}1g?W^fcs zaI!2P!zRK2y~21SO*dkA_F*f*P-Dt78!7SHeH;k)673NGu{B$iX$sF$z3F$Yp_vAp znOO_@iXdAAd<`KuTIrR?aK6i6LJKRMB+-xlg%qNnLm`!GRMuOY{XpjZ*3_Ss{R0Vz z3t~U|dOsaF#j1) zUQ;eS1>Ff%n+7aBN(UbE5uz9ViPoJy#jLy0WRW_FC ze($>+BCorl%*Q3JNu+yMCIxiew~LoGa()yCG{ZfWA2^Luuk|`h)f#M3{;UM%uNc@z zjflv}@fvw1#b2x0cpklli%J%HbhTsedO@FaSnck*)ecV9uS zQd+m^`oOOta3Ah7644p7HdX{dh?_$M^Tas(l$|NyKZ&^@T zGGh{KQK0;Eh2Re1S`UIis4tNhb&$#5Hdc~hAk_lkOFhjjg>S|+F?XOluoY1jAwoZ? zP5rd+133rk*-a#e)^bJl6F=cefk!ZbqZ% z*T%7%V#(*WGDCImZQqEU~bFLNkzU>heJFOES4?o$yLYRTrDEYbFC z&CE6!pjsu{u9t_lNto@$#=P8(-utw7LXIl^UxK>!d*c0K&2+ASwS&c^CL2LD=fr2y z(|GE47ff=(Xss5bGm{Zh9ehFehAmP1N%IK!)|#EN+@EeTTi{T{XtXjmr;p3AU3l56 zwgvc@?ZhekRWqO|AD2tF=ZnAsjLM~g<17rZhanSBt6aCtiqj&#^35kUTkguyZd8|-g@{>;<2!l~=+M0B=hz{yHfNU3HByU!r!!~evOF^$AtkKunR{xycWCC9$# z9#1Wvz$Ce<2c#z3IxWSRje}XMloz(rl7yxz7Dt`Gk@!eR`e*Y8rn3)6&>#|QX$xJR zN4_)Z4x6m0?%I)PXE0LZ)kVU!5uwsM(+mZ0)NSATc#=|?z@mIZ2ouFQz_Z+`N`@Jw z7crAFgu(2QZO5*|TSXL&=nLYr*A0giBMm+i<{}#3!b$2O9x6cG@V%cpOu6v08!1O0 z3y?S!4kX;jv8Q_}z->ZhTm|t3HigfMOY!%z!LhI=6I{ImrfuDmKsiC z7v6k{bE*XrOti!Hqd!olJ2FC{VB^V*j-6}WL*5DUkyr%ncf_u!MoC=zAh7XXORfXN z-4vN71s@93!;PHWIpUtpU&aKZ!ZBCoxJyy&3rpG&uyjb`2ex(MC4|hsPoq5JMp@gV zB|+G^0&Imym`?4)hJ_xPd5v9u`*L95nxcHxbG!z+>)|=Ql~G+HfpD4q81)+%-r$A<-d9#&!tR)R z(cVp-3+U(RjE3<;t#k6)A$0&oE3fkfywdwneRNNq52}vu%wk*@z~;;jGG|k~P2g?m z+JsDpVAu~2gVfatQ)Y`1Qb5hh#!VoZ#v!&Y&R zbu$I#h74|vutLB5W2X+5Pc!r z*|o6#*V&AZR8F_AcdCOo%uTRURFd{b_CqW|59xg+!t3fEs>kmX>PC2!n2Xee4#+;r-l z{l3y&TAF^a0>0gvL76@27(a6|UcQD^N@_AidK9PTnh9#IRoGn`WBpmS5%5FT5c+j@ z*61DMCQj{(li|;0!PC?39JM1#om~Uw%M^Zt@PmT=7k}#sDi5aB-m$YDxYs5!;Sb6) z4G*ct$85KjgaIT*>JBfxLsC)aL8EeSg7bC^g#uHmyr9inQ5W$%x&6^lK-|Rgi*aB6b_T zqBkdUdogMMz=GK~P|@Tt9<=yEq*YV_^G8X7N>~j6cG{5k7yj#2{~IK`Mg{EnVB7kX|o&Cb1m3*_5pq zi2{B~MMjlJ&EsW!;HsvB&TaeFD|H@lDsWwi$;hO+mKWB%dA3X2e#md420ljX37Pz; zQ~yKM5WhtDN}J)M<%(=D$J%?yUT2SReZ||uTYzLcNIjj>oGb8TX2S|Djvrh-C>yzS zX>}MQ)It+FNl|ee$#K0s+)umYs@2!C;xY6m!Z(;UeI}-_wa+hR7h@$;8VDIupSXh3 zxpp=jr!A68BwecJeay^Ph^&YW1C3jm7eq!MxZzLBKdyf?9$9*T9}-|G>tA*}=Jhm^ z&Y7p)|3VtX601r_;s914G)KmT3Y)jA3xKpxlMEZw>X%l*WjfcJJj6YFHr%e>j}+;! z0WkZRCY4$RFo!VQDN{T6%sONCovbtp<7?qsZ6I=&PI6ChPn*3s*jz5%n>sK~WHlE) z&z!1v4J>Vb%X7Z#k68+jVZSUhrJWFh1>_Y20W^Tm0HN4)a77RR1b}k#ITBl`g+}N# zU;t1~CbTo;=YEd+iP3pEK!C~{B|v@y0OCjVn*spkn!!%GcaZ&uBnt@e{!8-8>>m;c zC!Mqjz#oEqAfV_kft>LF5WqUu&&emi64KAVQN6O6_(K&A_$kkth|7Zr%$>7tMtn9xmKnea=7Fj@0iN6H@e_8=`_+L5X z0729K5TpbCGZQGM1dm1jjeoQO+Vhv>|81KVAn55|g8z>cP7XuWy?M`B>1w&XTGV=m z6sT;tfl@_foWUr!;!q2Kyd5AI_S>JK|8Tee<701zef@JmWW&qp%Z~^aGhF5y@|nEO z-~*>8lp!3YqWl%9q#9&lc9zSpcl+;+VBK^KqKUn!Jhedr5-3DdIS~{!pqh9s(+sPvT3g0!=Fo?Fp5#W`Hs4ubB_| z`O{Zm#RhR{aL`~P@s2}^x4@~h9 zCeuIVAj!fi)LAv~85-=@p*g|;dAC3?t3S#tM}Af2>#h>ptTLhTzq4G2EFa;c%G^9-_#Tut~~a_XE!}B<4eI(Xbf509qLxtXOyIu${?jGpefCvx@tgt8JZ{ z^i^B%${i9HDi>qlx>m0@ak?pw%mDEuFQ`tz_FQK|;tlnF>D$8z!QX!fSF>tEDYbnXm-r!`y(4yM+Tzqmiz4&x@P~o6d^#Ka32tC|4k(HSEmK{ zbNQ?TL3&>GTPgs!_8`@|{pURN-X`;1uAjU%0p&T#_#H>Wa$U=|k21xsbsT`AYH$O; znei$;wUpYCk!8Y~B#p*I`HW7Phw+Y~zVT?lBSqG| z+{CgK9FIf<&QlHaGXlcr1WpfU(2RmMgkxG(N?G?7xI3LWPB6`<1rU4GX0so=XL1)4 zy;g`=Qn^h8^0{Izs&7jSSCLRojqM$VVClTM+)`64o>XNH4U<}$SSC8ZnR%6Ij?#x? zcH1%q72lpvE}flId;>|hv{iuPE(YpZ-Gi}?>Wxc^axj{A?JxBCoL$wBjVT#959IGB ztV6>NM6{q(@BH#fL?1t~YSufGXtjgldx#$o0d7`=+PyUE5sTkYO#)DSJzH_tgCr*j z+_QZ1kO=R)Nx=IpG4Ut92j^@!VZ|xjgO&e+PY8IijCiVFR-*g}K7aeG;_i&q<~*a) zJCmtvny6D7V~LeX>|m?$2X$y3dMw1oEas;L^xh5_(6vb01zQ@r z_#|A4XmfZ;C?}2WoV}p~>Gw4h3R2~?D_J`Pcdx}K?>N6q(__W*L?I;ACpoAp;Fl*%4Fu=?H#q;V@$Dm&2Rnu+ zhKgg3j0@md@!BB;`ZRc_6C4Q)Mss04cHb!Q)#W+*DgWITyT&dgZ&Yol>cZFrOiqCw~ zv%mW3<4e$|i_XZ&<7{>#EFzSOfj9x8TyN#gbVTX@Tdj)7vZX zQF@**L70tVx4XQ}v4Agz+ajupoF`)h5eh+vW4BtNvrqgctM+zJx#M#GEJ4B_08`h? zx&&sMHXgaq0-1oO@|o{z09@j@o>yDp&o#Mw9`xH0b*HRR5i7n~JuY_zFq7i;EBDgQ zts)#>ro2fYxXGKU-qxc(vGx{h2EohFNrHp-9On<}Y zVceB+qJO=}q9^9w+v?EZ5u?S={ z@Qd2-zSsvsP1`_@2!s~@bkCD>YD|>aaaM`U!x89Bb#a8e;mmFw8}6nF)WxzFl+)27 zhdottjLXg|NM_BJk`y*F_+-h&WcTT5xLrcl9f8)t%f(OF&9jU~d6uyto=VpK)Z()M zVmJGMvt{*u(v15jtjy=9u}Lz*U+-dpJFqib!<3tAwm_Em6Zs-n_`EYw-1v^}p~>d@ zGtGt6lEA01qPz5yKxI0&vA!{zue6_6zev5#%ohmW_^-BJb0&K6ABDZSw#P3>+*F#ikX_$Nx7JT=*!Zj>ZZE@FtvDMUZlFBWk6 zPr3bba()KLb+ft>1%M9By8hlYuLVqH@WPOD&$bORn@%-8JowiX*#HFJe3jsJUH_dT z|8Z?7?Fl6JMXzl8NX)r*eV6ItkEaZ3)#L)7k}jYvXDLKV53c*6li6?1cfuc(8KhJI#(&KPVW4i1EyRLwzn( zQ43TMV3`9g()A($b0SLKSA8D@(Rn0pNVjJOPhsQIr*ZzGBtKKmiF zWUJ=y#`vs!3#ap*KjStdhLXL?ddWPo%+>n!qjO+W-N1N9^BghYS-aYvsjFOATpgGK z)jZ64F)Uj(^Z@v+A)$OA#PT2ZXTD!+tqp>Q<4I%pMCKd3}?a);rIuZcRDUf|11MbZ(UReFdjZ{`>VK zgv!X>Z>XS%(C7+c>+ki`kn?^5AtC=rSq}flE^lJ08%|?dmwc(-c+^1BbPlAIcAeni zD|}ZO*x3jFIkK{d3OnaQoP{1stUxat5~*EqEK=+=8~X>WX>tNcnR0GkOr)hX>LL#g zpqEN(;Nz^79^nx~#ONQr`~X5SzG)r)KX&FZa0GG@*DF#nrW3XJwDsTGHVA`XeDdBC z_ewd6B>mFLjMCBio{9hZ7Lo)~!W62(8B@!BFr@9u*8I1!XMm7eZ@Oj{de!xtH}WY- zP3>fF-cKvccg!Ko%^di7%o{u%UjqU_a&fBcE8{5ey(JASKtY)oKxoZA&Zbd ztSS@?YzP5QlE*NaIP~7rIO;>M>hXP7DHQict0NkMDtU;|NEFSb+E$ z1zX`11K#41&^+BN4sNBOUg&4_oD(FZ>boezUUDA2`%#1F?NP}t4pwwcAx1W9qz_-( zuGITdw4(=-V4;o0u^_!SUSV3MT+2W6w&bI~LD@i+Y*?l99`>6e+J+{t8}T2dwGmCh`W#bRyAs!xZGDe`wa9>3Qax5|S<^(M7OB&vvpS~Y(GdeMf11fbP4M8Kq5Av;pc8b{u zB`B~5S$=7?n@<9Ob?RSfSrIzWQ5>4bx`GQLPUnG&Q*GKB}z*=)$2#t1nL)3I~g`c>5ro@Qe zcqDt$DuE$qe+P(4f+cyW#!8(f)>crG#zN;!-w>%2{}6*&=*&t$YE6O>?!mz9;WwEU zcb=TI*6LAf@UrWIlH!c<(_kV?r^-^M+rXbOu=7Y>)}-WfQf-{Od|)`jzR5`Lzyr$M zOiZ+@TB9fU2lFA<7m#+-7&5|JTZ8wL8rQuz?D%`-UxlMoTtxZw-I@sL;UP@keYTb0 zu&~a)@^$H+lohOhFSZ^8nA)6)%%Kv-pefi30q~Cf+(Mi10GG?I_fzJudM$N(fl$AroMY*o9y@t>2aeUrS`Q52RLj&v8ENjr@yjm+N6Gh zO$g?_m~mO*56C@b2Vr9vN9OK|{Q)~)Sj^WKEGMzpzAeYb3J!`Wh@m!K`yBe1d165aFOS;AKpMsUOb z%r1bR_~Oo1V`iaey~mzV=HtYTMV~X+%FQ>fU|u|H%YEAcw(Rp#09s?GeUD>8zsQxa z$4~VTGFvSnl)em8axfexcd;jHp3bN`+czuY@ebuNiUc(3B3^2V2w5y7@uN&RdawUS zvTMd`wK1=lb&!I>$CX2kmdVyyezKdoT6T< zMFN&KmMvc8J{iIu$<;3X+J^9aC+ax*PYH%}-V2=#XMc=q8n2f*5+*!canW1z2! z^EskAX6KN1?B2v}wkitK5@>Evk0@>p1|cj)&qxC0XfHUn(mnYWr`3$xI_!jq{!#e? z`5D^{HrjZ4pEvif2VK1I8Y^)F17@lIjg9X?3e+kO(EvYUQ+La#~S?diU?u$xhVEhh5 zJjg3Gmm!l4V(K^)Pf-CzAo|7+#?L*`mlb2DbpGzFe?wjQok|=JDC&)sCQArS+NI|S zn$#y9Ipq@ou^`oiW@8-8@&s>02a(Xn^s@xdR#3O`OqLrb2wo8Ev|;{ursrO_g>2O2 z6pe6vr0J|ZNq}Rt$~h|v?#OFd9tMOee9L{mS1VY1Ak%N%0qiXXLe~}e`#~iHco%um z5%xh~d{lm{^Zn=ziHJhmGU*FYk80)mpe!+J$ja`vJIiD zJ8e}xry_}iyW$h`B;2I&sGoQbExg!;n<)vCdK1g#9=;oT#pfnNK7AL(n#au;RW$Qu zu;K^3tH+xOBZ%WjoTpJ*G^SnVxVhcrwp>OG>LCm6O3(dS$gNULYH(jyJk{zF*^Q}d zw}V`g-_k%*^#G!j*(WiBnJd&&mwhp~$bBX}Ygsd~%SRf!_auEt_`LpeVqD~j7XG!x z({E)$Gs-&{FNz+FouEG*)cIVv@r6kK(~A`;NvJBJZt!px=Y6q}P>BE&Cl;*=|ArBh zj02w~XSM7c(W%17LmwABk$y; zU?83tu`61@;3bSLpNfCrkbzOV4l7em1Wuvh;FczL48=?*Mv-f}!JK_Y+9-BYV=y({ z9@)cGC61EeowSG~P|$m+S^Da3Isq^%RpFOiA5!3&F!$3cTT10LbC^14$;C5^q}dG41rRJ7SIM?|P* zz?(!A6k-TDhy?})6e-Zm#qHVxxyh`iLCf2?hReKN++a)Us*{iOMV&@@#@;YmQ$Awn zVDI@YiBYoa!j;TJnevW-P;dQ)^#2}9E9?7U#Qq|zs1HoieGU*?V|lt_Y8(D|pV5TXfY79`0sC48GQXCAu=ZuxzvC5zMwU%I1cyO&pm&db_t{KbTTrFlK_I3* z{3kYK5e%-Ux~`IH@TpKGId3xw^b(e?w#9)XhqOxxBwqBE)@ZGuz(LiyTACU`i%KUDxK_7J*yqv0%R(QcH>qR zdsEiIx0#X&=^U>0@e~%or=#1&j{}8tz1$KIr?MHek#hX6DcTPR{nqsPV@5E5%jXY% zzcmXm(hxGv>Mdd9bCbSBb#(G+0sBFFH)P5{NQ?$(7nP?S2 z5~Zd#xD$DF?(!3I>NQ3>QH=Mc*$gNO{V zmcaU&izk85Z_T81=s#_qw`#Utmx%Xqd^%&CZpw4Zz(6VfQrPNLl*liO4YNV}dww*F z-!3)3p2D7_mE}<|yO4MV#Mg`R5Jb7i0;y^`v`P0>HSojY4ub-<4`y;6$cdOCumrfdXEk>7_0{Z3$~+^Ed@2Y`B0&Y1~;D8 zp`5Dzn|O9UZSl7TQtgZ>{>P-ynBoFkAf6*~113OVkS8U#o;hSL45oP?<&+t5uN3gJ0wF zVg%cCWwl7bv1*nTp+OfknC2zyn>9_1iH;3I-@))&#UmV&BKolUVe!{H-M94L zo4{8IV~BnW2$<9$tpJ%p#5K}4W`M85P#*N*n5buTS2=86gnnUWW~R#`sm2I2VMauI zeQ;_Gp0AboQ9_WK7X(>p)n+JD=Dcsrxtdl5CL5DHweo&~I0n3Qr9Js{GB8!*JN zm5=Uzz&Mt~wr(HrcfwzR&~M$u^#5+N<26{aukbM7f(L$amJ8{pM|^}EJI`k@0|!5d zbBmdRaAp=NSWXm^w1yo`td%Di?zB96{eYE(9z$z+D|EJ%uct7avFV|BQdY&Gv%SD- zK+^NJ3?Qkq)*E4x1Xu4$B2=l1<*{5WzXW87tHKqHm=KGwr!6QD9YSf@266h3?Hq$L z$^X*Vvv9;$^~DBy0Y9N&`LD#ndO#TbHzz{=_v8*lL@y^M;$m?*j*<5#?uRyI$#Bv% zeXG;~<_jVw-i61|8e3YQ35Mzj3@Y>*o>a1gLWW>Jgn9DInleJZy*>53y1j!Mr+eqy z?nB8s^iOnD32~4-CDeLROD5@w;^+F-pf67)!I%d>mgR*mHZxt(+Xs9B1Prj4Gx!H5>2J27xmG=V|;Z>}9Wg*!Wkc#x- zlT{MuyYixj4pCI1I@sle41ol(&W7Z%0%UDT8vcQw=neW07|GV>+vLjYJMTk9P{h1< zk9kX56;Y`PHv7n8iv4#4$F8t7Q>^S6qO0u1&JcAk_yZvAw=r~bWjsixtLVhfzD*u8 zm%UfPl#ecDLN~E;1P|iGpy~5To0bVjgDJ#?yT-rZsPN0r#9<)0>K~K;$0M47FmKJ_ zvp+``Z+RzMmP+Of?Ab4jRAW2pr9<;Zhg-oII^27nJb&JyE?SuzVQIc)n@}!P!@ zZpWHKbKBM{rf&xJii0^K8yf7iI7rnr3Mc#g7R1O-5Xq{DICanWAQ!B!SJGB02~r0v z;h^PLRVc&|w)SqP2Jc~2b=6I%geJ>q zT3L#Ehe^}yKFc1Ygiv9vP@d2(5az9M{ZIU&cm9drK@FDtsj;HuxgL)-bRf(Y zIbMm`Z;)ac&P5UL14iLj-=g5ZaoeiFWF)?}Cu)40k>R_N z*=YEHuC4lYV2iF|{H&i>`lm5oHtX%@bH$WJakaX!`pb^*$4}+h2A`jVh{_GErKfr< z_-Chx{33ORg18RQo);#j_87(dxjsF%5tG748)<8DN72=Imq;J2rPh-Lv%K)R%uh-d z%RLvpd?KUm6lQ}W$tmM;Yk*wkmz__MZ!XK|Y$pYqdo>ac3LL!nmz%1b$bYx~V z)h(rKzFWPXYZuyF8Zq9s_wxEDzGHzfzi|V9YZ`dpRZy|2R~v#jDr(m9FyDqoEu@yb zUub2HaqaFrC7N!;YAzG0#Z=cc8!ouDng}5M3g^MKY4=!0J3dU>!KQW!xa`oXUaQKr z-nsv2jLq1g{7%L;Tkc3oN08K_wlgoYtc;?ftg!xu7oiUpyQ`K3F^K7;S} zS&KM~sa!>ID~70@ax?`A3%NQs?(0x1zD$Oz+iQLn@jR9Cb5!40zxJE+DxalFGcNT< zx__LCli2W3O2P1bH*;+@7NU(MH5)YA>9Y^r!W)L{`O9FRz_~;bjGP|U!vx0Ky1Y#W zRly&Y2kijLLjIY2q>pjP1RVEQM@RG4;FYxlXa3OkOiD6|#C^R2iWz0V9 z#cNil#&U~BK%H+)xwFV5FuzpR+Qi`cPbAS`K4$0#uh@{3rbvbms_G+_WG$k1LSnvu zyf6?17O+D1jDR@EbSVRyHnE^=MFoLGe^;$mAS~9K_rD>G-jY;~&V6-v>yiq1KAqMY z{QH)}E{UqWy9jUs8%!M@;x4$JlSU%fL@yKb144m)&LB86V-*gVmKx~X{1A$X{!(nz zL&}(&jhiIJ;#J`?9H*MdDhlNLDB$iDBtQD(x3QUes_&FK1R~d-AcUWhTs4_`iV}VM z+m~zEQ3<^+1{~^oEA@5@O(Rn+EX+vp%~E&hb-dmg>NmM}<(~dlQNH4mS>v=`HuRMG zZXW~Pu2OLL`V?A{=_&R2X;N6&5ZHb3<+jAn@FAJ~P!G*bH-;=x1t$dnZ^=~n0}%Ev zECl*>rTyKdJN5e=iSKnkQHCrlDWkihN>%OoL0U9d&U%k^DU_Q-c6>WKe>dD&SX^k( zj@oyu?VRme8(ei)sJQrJ(rYQdRB3BYLbJlGtl*T9ROT9Eb9Rju1%z?j9irTC)Q8Nq zW|v(^)Y7r%u1?c1dWQ(Wbq)VG2nI8uOzyp;_-lraVe0EeOUU|@I>F|ZnfUvz<^2Hc z&03i0DoIIqw;_rsGtefz1h%%iv?71;+|#og2WSvTo|OfW1EMc%URYywpLE{WY+ZRW zs!V7_v*wA}uBU}bymJ!jgPd(NGJrh1XPm(w2Gm(gr%5x%hJ& z9gG;bJqc->4|C(@KF@yat|QD)l|PQ_`)v;XX1hxuZ2TWj_|xThRdFiUpY=*zgMBMG zF-QT-G{q+`K6lU@iDxywb{GlTg9ZWY2Z8Db#Up`^qhT<;AA6_oj`QdE?+WLTY?T=& zEVjXz(1Qfr#~=3@7nT}#@0qtNS?huec3sNU-2MLgUj-2MFJ$F!m5V;u`Fq%C4QLk} zTQL0yw?Sw6i{T%{O4fOSYym9IT>w_PXopOZEw9@_C#DZ)#DmziDs?+^M4gG8TayaQ zJb^A-fySDwcm2B;w`1}q&zETu=~=|Qy5)M(R2bb&*KV3fR~a7#sJ$N_yRn!T!aw{CBZ4Q4g~k z0!c9Pk7spCN#K;oWRE&hmf=NyObwm(QPDlAuL|v73k5GLu*%4l#RxL(Gp^DIRubXv z<$~(Pj@K0qcHF=Co`R}$j!FO(^8`bUd^O2#6`M-@5I-cvfz#iWU)M8L^^XW=Ksc1Q zjPte~`(q`*j0P`@^XCyC_ez!)`J10N&LU#M&TfKmnm`po2BN|3bKYt`kN(hncI>R( z55&d#`<5vVAD2~y^gII~hE>kbDwrn^$x>)?F4~42uLnx1yM|r$Huo$oxFS$X(vC%! zhfT2`l`3C92P+I?f3MZwDBN1WO}-}KQg!FGxQ7=;f2m~lRl~KyyplObwq4{3OX?%9 zzUY7{`!-T3xjC{c?|1C^r=MTj;ES{k<>+e;Tp9sM!qfFg7U3M(1R=lHsWWgVYnB;w zPgDivCUIApN6bn+3Rk25ddUwU++Xy^f3^Pl!IV*Zp~swPRq@R)YIKzM!xCE-cshWQ zt_U?wy%-w*%sC;2r13q5ZX)Kj{CJ4j{emNw=QBLio!E?HcY$vv7~&gP@Kz$|7f$bE zu0YqX7j{{rL3Qs#nzSWd2SVp(B3*ZJ_f9Q=+w!VqR>MS?qQ0b&bBppGr^e=s&~+_U zeB<6^Hp4?0a<#N=+)1DXSp($JgYXKGOG(s_l3*MH7YCa^Xn2GCzqHK}wxDh$A!wVQ zL@ez6HKa&15bifnl$?EdhI~YT`Y)H_GU$e=%X&^CLw9$uExOMvu%w%+bXDnvHWFeLg$rL| zS#!dSav*s;=O1!y<2)J57_x08!lxaxH}Nay;DPPdB6o=p-MTzm2itOUp2IxWMe>4u zHhyKQEdyuF==wk{dKUepQOIOze=eP}C=g$^Ft0KH#lf3Q&SJ7hr6yMDa|cvcYp^Ahf)&J_u{%xEK1 z2!xhs4QendXCdN)9j0o#7H$t*#Yb#|=Uw)-3p$OFkxxs09Qm#>wDPecob)k08 z^`3#kk{eVFOwR5$@|U1uRTxOYSty*v5JJ7)l_OLL@0dQ(Zr3mR{0*}ichZEILv&)Z z`Yasr>w}uP52f-$JibMEQ&2DOr#9HHPPf6x>*BLdglBm|tw6ZQx7_kO&_Mls^WL&c zrrM>$Iq}N=*d&}%(x^$0?5OuUsJuExsyMC}>r*}o#vZqfgOR-j(ktCE7=N6j=l!^1 z0VVKs^|-B}-k4^+cZGmG3fx!sJ3cw2~SUvFpWc zg-@yBdPN18Jg57Z^I1u1(-XbO#o|LC*1Ok14Z@p^ezcinjwGQ^9U+_hRRcg^MU+Bh zLYV-_HT`hx1muaB0O8)iJb!x8u)ZOGqz9Ffzsq|dQ@?Fs_r)_^$#n81h!4$0#IT?bw?K7VP#WesQKq>{C40c%HY-E3QG@A!QyGh}eTGTVXqIAxYY zi=5in=O#x*J0ZCiIARNsyk`%HcP4EOAN%1^Vz`?QetmAL8R2}z#*d*h9XL0p4 zNHYarL15{@=Q2d_p( zQy%IoAk1=nXyjwaI3pWj3r>{|e@EDA81?hlpFROY->-W@V`S_SZ2Wx4wHExbpETHN zpv+nx7P8IU(gh*ovG7$ zR`lMjnww+xJUTL2v{()37a8L&44>i5MA;lR)0AA_Q-3IIz*5=Vl1DTKfB|*jKlXhz8vWgeQs2>G0iazBXWmbmHVG`UZ)KWK5S~#utA>{g4SMsY?La0auA&WY4 zScIH$0f6b`PBv~nr6+34VA=nYV0`DG6nS3($mIfI?HdY1Eh{^3u4@14Y`6T6v`IjC zxWCf=+vW3=(WEP=qO0eug5$xGbjzCnbIncqSfVhL7L^c|w+SuyVu>=Oe*HDRzR3BT z7upia;Z(w>`HU!E7YI_@0zq!<9WRBGzBW-gQJ}NvzNO9!oMM;{;Ni98I8nNPdz7m`OBI< z>cm|+-b;Jsyg@~Zq2Sa(3X&Wnf7Y3a6&Dz0Qt4V9Tn4{$uFO>m3UNK@%8Qp*2zA7k z;vtmx?12X?k}FhfAhY8`aMhE?4V+K5zrbJX4Fcica8_r`|Duk*%IJJ_lx)l>DkS(u zLJMVmuoC)KQi`GhTLr68vf&dMPI&8*@On zKwUvk$%ls9V+eVhNl>202f~ZT>c(Vt3Dw74zV*R5I>~+=LzMXJ)>b85_J>t>=i{pv z;kd_lrxe7{Z*y4(!iW5uum8agzE`_48(|_XcRVVZf@+23l$<)->02)I^jb1CTNIhv z*lt#87;j0unQv~fZQetM+W&Jm&3Y`Ms|3bk23r3n! z!^pct4g`I$eRt1!Y`7sc^pOT_fyQZ?7=Ki2_Y=`=DUtcauO7Il2a}l`*Qh{8!>$_x z0kf9c6rJzUYE<+VL{r&tqM^`=wa`O8BLv6&*i?diAK550Qk#Urf9%^6X}5ob=)pLL z+AZcfL8qz3DNo_@(CnC1*VQtt08@NVKGrg!+#(OM98B68y10{uCA2sH!Y-#}^q*Ew{i+QV9OxM#HnO-%LSmo@%`@%6@OR8vUy5yj~>C3+a!lc501 z+l_c({7c2__B<5i{&jkRAnI~M7x4%$zHMPan7C`e5nEY_7+@0Jml- zaZ{zykCG!UNfcTM)%ZaOI{y(~qzZ@t@#+w-HRW$K7OKllB7+!_>_QXIn=Vx#r&rWr`~+) z-Vgi^2&FVci?jp-UtB8$E4SlxAoEBJ zvUjxNH{;@w7TFxN>qV0#u@eS05;ex8Dg<@vqHak;@9$zFKqd^{8MG>?Na%F0CMf_N zRW zX_c*Ga(+_tttxwYl|4N!&m#Mh$9@VS73tqY6^Rd5tLOV@0vbmuj#et|&$BNV7#woE z^Ki05ml_w%*ylR88;a0RVu(!QZ9ClVEjbzZRr9=e<1>7s>;N|O@Miq zM_oTrD27qN(y!(TbjV1G&M^+`YX!NtA3CR#?COaS#Qp;>VT%>0<#n)c2Xyw=UIUp5WN*#Sts_%}tq9_W1rBK$=={zjtxlf$eu_A1QL z9Lr-xTX+KZrqooYNe1Rx!n~<_{V(^HZ%fgd6bkT1@jb4zdq-1&8ZXwKtHsyJ6K%+^ z*WGke26xqgVR?)NcM-EZQ5U^H;YEY(Po|ky%$KK!Jf0eEv)M=!9Q#$y-+kAx>;$OL z#8qPE3AF@daX?Df!UJEH#3no4(ds%QF-AXJ2;?Pkt?tZUA!I$>vrU&VVJG+&Kl%ar$q;zu)k7L`V3F55ZO~QJlP|Z zY`KB&enOZ0wo1BT266eagfyt`Yj%j9O0W5M0_i{m|9{I8REIlBaB=Ly1LzcD-?!j+De!UE}KKAmkvFOfW`h8!F92y#Q(2{>Ky!a7&Mu@YG5V!xX!` zgElODhf5kLE*?$0Hf^~68YwguFvkYzyZm538P+Rt&DMUBujx=m z@Fk3^a8r8M*%4)xGEo6Tn}PJlH>BclcIt+Es{`3=cVr~=4hr;|o^ROieEdo~S~&0T z43^aT*ST2=8H};b7N|ZY|%?*-Lb%}Y>pvFbS#ic)gRLWHLq6;~vr)0n; zq87UtT;$_8(OlaGjl(Iqo8+XK`=?-)LYz9 zr-DL4W~oJ8d{QOWwizI)x^jOc!+o3Py}zZ1FZ*e-8IbJW6}Gg3eQ1Qr;--$JYc;`W zQ+b=si}^Or5)k3#&Hq4t7n*NdM`7tQ6I|8G_>k6&?WyrT%-Z)cV>1u3@-91es(j&a zOR5*Q_V~B%j`CtT&}yt0U0K?Y{l+3cjbdN!3Um2$r%{C7nAZRi|H2~Q(ge{T_E@T8 zEEj@1Qx&eog*q7Y_2r&wBygF+lG6RPpIZzF`u^;&8k9E@nvxf$6RHWy6}UW5l)v( ziDseo96#2smx0|bE@)t4E7Oz3v~qsWxOj;q4=|vgZzW5!M!Sk@mjdVE8(fnE*ED!= zs|}bu7G=6@j45H4oKpCyEa%s1GQ zvRIN4>a=&~xMTFWK)Txl!)UAot4_Yh{Zdwlz_=H)-{)R=Ch@tYnHd?@R~f~wWqI9v z0bL5jmP)B#WxOpCeJRtgPFucMv-p#qL4X3TM!AD4?*Ad|9iv3qnk~_?ZQFM3vTfV8 zZQHhO+r}>2)-K!i_U-Sy9=G3k=l1^`u_7WPb7iiWbB5CQBDISg$J4&+OL~o;E_m$IX`LHDE$t9XBp;i(WI=h+)UMlsH7|>TyrZ=Q zNur?sU+M*%_kX^l|2y>pL?(RK|38)VxBC7IkNEey3h-|sf&HiWFvQj^zf?R5+r;#+ zw&Ce~tcw8vO5+@c(A??*(uFSry>F@u&Zf3*i3?RK=f|{jV0_ z|3L@+cYgc-X~Fe>v``QL{|^x6ztg9|adtLziKS-2+_u}E7MgG^u`Je3k zA6_8*1C;m|3k3lP|JCXL%YphIyg;!3A0x>NK=`K*{ja$|`2V4S|2H@PdU5s768{U& z@t-*UKe<5sf7^Zk>tp5rCsf4$G}N;H)eA(+|4ST*|7pZz|7%qs{?jx0*K8u5{$I0+ z^iLDvUu_`$bL9R%+z}+>|4SrD|C}cNYc7!fA>aLr7kL3l`~NKQZ%+vFf6g}k{Ru(- zhlTa8J|OGRWD(uYOz zP(0h-`Qnef`e73ZQMngZfs<9&bvvx^n>y*}^-{N0bPULW964*o3Y$yvMeID=n&r!xto0p+#A+_N5o-7!nLuAG`h z2O(J1!O|uwPBlib{C z1HL`IN$V&-Exs%OQ-qdVy$1@=cPPJYw&N9nqvr%RVa;cBW5cdll?Ox!lo1Lwz>=H; ze0(dDCRv{k3@ipe$&Jp@1pn>d`y3zDI-troZ6`l)>N@-s24+p<(|4fgqKRRjPHAEm zMlE?Qrvml~%O923im`G_tREP^49Uby`zrSPIiN@7sq{1yqz8-*GaJJ%-DV>D%3 zJB*M6nqlGqj8?Aj=Wx3vFpWj(XHK7<$Mps_%L+q`Zk^S9i7j!F@JfSp+H>g~DvSU% z)S!RCHj&?b+tfd&Q$e_SZC?r^K_tCXpE{?G=4d`DMw)D2}d^sN@*DA)s5T&Gkf|XNi7@d4rf*8@P^C?!A->sv zGA;h7pLWE+tp1(mt+vp@JSz*K=2pSse7u3jm^$O^g6#rf)^tM2jN$z& zorXju+AFhWBbCt9sRLwCVS$6*P%$h!TSs~X>tp>?`5 zb`PdKx|WJOm@~dgQHPQvm*|%iFO+dBGDG+kpmbBFOLmg3SpJW%(7J~qo+FZ6$>PBk zsu?JI-I|W7_=H(^@VgDLNC1ZvZZ7ffGXyKXxg$9rb~m$FuN+l}ejWoEwy_Yjcc%Sf zq(>7HpX?Mx&Z_;NX{^xOsDbwG=MQMWLls&)nndiJLk*0KV~Pi-`{B$6}p1!2cqmjDVnNB=Bc)Tv_Ux7Nzjs+iA1U{u;dZWz$jdI z<)_gtkFhDie}}*-?Iz7TT7~0zrbBT~Bgw5l zBl-&`s!nhE$o&z92hxp8)c{6bS%zrK6&?%Z^g@hV8=Hw+TnXTGejYVTz{tKZFLui8@NYIUYb zuAj2z2Acl(_^Zb5oDEO#{TRy3{kM`Y)D{;P;-&1`wLhd+E1@ih;9mU89R9weiB8G8k{r7U9UsJzUfzqMP3ih zRK-fSqAI!C?C6=4OUR)VxtlbEbeQfF^KS{iLJIm_!PB>8V_~$w({HM*7|v{zdu1VH zQnNFT>&(d`CNjxRV;Hor3NDNv!c2_-EMceIP7X=tYvoST zjOI`&55C)p6W`Q9**HX4u>cY$7z*uPsIZk=fWz)i1q0-mm?Qd@^1FU_*tP1Ik)U%# zrVxe5VP0m$1|M^o;!N=1r$AT+(NHr$nQ@>5G4vZPri#ur#`x{jPwj<4124SP;aW0i zWHhA1qS_^nlg*@>ZjwI$cdqbt})Nv(rqh*+G^z6+!gZTowGM80RwQ^8z z>6e{cINFvoT>ZSRBcxu!zkJ6Yv@F&>S3 zrFnuo2^I9CspXSE9WwC}MdT`*4MUb@nH9odCqNX*j9!4@cnbHg9nkLufrEsfBn^3n zU+sCuk=S>j?O+_NHSTU)0L}f_O!!WgP=J~kyuDkER@wEUarm~l^V{6<%xT085_H*$ z|3XZJe=ck=7=vptXOP4Jeso_d=rJ$btHC`_2bt?e8Z@7O?l@yf6|=o{|d zae`sh6IuUCK6Fj*XqlC7DGpu+?-v`T5JLCjmm#OcFlbnTc4@9=9{LPcxFB;PmfcHw zN>)FJ@4VlD{O%8mNlUcjhiok6x2lVWELTU{5 zNwRpm5$_5qF$THDD75$;h8*;R-qY9ad>Q)Tqc*X}RTRMUWG;GgT z7q6WAge0GLMYnwq39Vr2P08d3tzAYFtH7=Ac7R}HxOrJ~Q|@V8?s31Ts*dmO!b`I1 zvgP9whR1`hH`IX_N!tki7P>^sBJw45kN5vL*814mto7E(bf4Ny=B!a#^{v}&DN`2TF zHF&0$1`rm(wwWrQxX8>sq!mzmUcN6cOpG|Jm1)Ov`vdLxb~=R`RaA!bRFxa$pzjsl z$9UxXCipeQR(Grg%70YWDdMik0Qh!}E_lrS<4|Yr?U7j48S-)PZkKj+JJjrc>D|dM zE>$67PW(<8^MOLnhMSK~x46Gr^0{H(*xL(=cNBCKk~`p*&{rK&Dbt;mKG{kT@eDr` z&(Q{e$UO3P_$vf>iNL%KD}r|7xNourOL`+u4@Bt!?f}GnQjsAqhXyqcd86h$xVD|C z%WL7j!CyqS2o2-KO6{eAP;23a4W?af2?t(C1-3Gu z8_0Yd$c*^jJW+u8jco&rWSI*;>->YAI?0TU{Q&ss}8zAFoV?x&VJ7It)D> zRTfk)q*^(<9Zzjps0Apb6#aF0D!}sjd$*y4cR6uP|IB$29pPw- zrynGVq!o!n3GJfgg#kF)il3E2x6?fqdJ6nup;I({?N(wa&?t1t4gAJ!ZgYFMX#I`*r^il z67wh!+e^LSX6lQ(4CENN(SLV?pe@ioxp`J85(%RQAzGFqgZQp%uVnTXzj$Bvd-{ukN)@|dFCSp2;>IZA!+ z0;1tKehELpYi{L_-yyBj*<Jb&Bj+n*= zEA7Ir|JYYKYv7&@QkF2pl0au}P_Q=<%a}9@FW%snV2ZSGCLD|LJt`zKi;{kj0vSP; z*7;Ytz*0H?vn?AIm#J+uJ$PU0@+?E?rrSj1H;9ubtPI3w%*#ez^OA4X2bUhxEOJ_I2b}7M1xG0P=7@5NScaP6U`$V3y1Iq1|SHS`mF!d8o zfTx;JM4+nooe=sNXw9CbjU*jhDq_KbH>PijA|zuD%R0ignnWEp zeAS)9zXLOnLnING2zYrLB9|o7UOvfikj^{m zO%9Uca^{nbMTgX0UQ7ZlM(jA*YHocF_U`?D;XVQoN{GuK_j?m0s*w^7i4+w{9SnsI zB@Cozu@4{z6ddR^ofF+Oz0%uG>H^D0DL){Yj@1h>1p{E~Y!*Z^f7?qc0Ho=sav6}| zjUS)5gxd!qgGMkJXueK|&nV_^v2`W{*!Dl3aiwZN!Ia%Obpf zs;?tan*tdpWSupqmYpd-L`{9krTLXe53M+J)}w26{*h2)TWhKbH{I`dK;AUWmUQm} zozUj&X@Z9q!h(c;C-5wnEcBQp#i{?Qx>HZx=$L`q zRqhak-tGFUr1Z4_-?bB~m_)wc*YiR-Tcx_luQnTlG49SsgH0hHlVQgGSFGjS>x6GZ z7(7=&oj}cMdR;7afX4cy{fdf!vt6G;TNI@(e)F|srlS;;Q%c;svxZa;h-`t#GG4$; z8zsFzqiyRZI1PzM0V(iYFZ1?hruv3KXaivn&EP+k-e_e|9`W@LF9ZhBZA?&BgL!ye zfUHC>L;XlSgENt`(Y?mLqcJj;&A>%~J{S(}tqCRNuL?d3ka3|+hMe(VuYA}`l{SP94H?raWWvnl7c#8C2yluJnd z?S~w`<`I3sr@K+T=)Qu*lge#Twjk&?ii>rvbXrpL;?zcuG~%JterExd#e02B72l{( zcCH$VEV=3;hkV;5TzrPG43{=1kG`nyaMkTeE7?hQ8|a0vi$B z6)WzCyH`UgjOEL_a+ZobbyQRb?9A4)zo3~3a&I+H8s1g}u>oPXP4Di}*e3J=8FAzq6e=%N7V!v|w+^Zg@s2KtpsFPS zITI};>XFrL$!*PpZX{8WMTv;KQ)o6*i5Zcx%qCZGnl)Eqc`Am!SfStPy5& zGt!_F9p%WYOySi@z2Og%xNM%=6IU80N|n-=4i87qZk!2U;79^~^>*v+V~WhdJb`}e z<`J}0A#YtvZ|ym;(Tt6|r!apq2^2M+0;BIM`X+Oc5eUDu7aEJqJ+-I^rsP}ze-d4- zut@mxeHc0Jw+T7MdX8S*uSH9Sq&T85edGTe;Jl`sxIVAb&z6gxJnDMz(Iyuo^-UKo zQ;`;=he&f2*omz<$gNY)Su#h04xjKlxPth&h!bea?9cDt_OqDV`%OD9>FBr+%IGCr zv1WVPcnHS>;nPkrh(})i{ZbN$6#MMTP?) z8K>_yq<+b>}aurZ5; zTV~rR3HH#L^cd{0pOZAJ;hJaaN`%XxYj_y{JQwe3S=3KNWRM%zyd?NKQU)~&YfkMg z>QWX^aH6|&H4sbi;*a_SWiIClYv{tojRjs@+*lWS4n7#HGH_*0zkt2OvD;zb;GtIX zC`mY4#bdFO;WnLSZ^XdP4Q2B+MPWcH*8ocja<#?3J0xsGX|%$Z!>68loh20!dU^!t zZs^#LJ}XTWh5v*Hq?vV7l|A)8S-}}>$ias`IsqI)`eo$B=>2PD0Ym;tTZW<6Gk8bG zciVzkMrf3BNFGd5skvbL^Da-&dO(oHPCoCm&SusIEuB*g)doeLkKNA3~^@LzUx$kfh6sYmHTYN!G zIgg)qOSvT=N6is~*CR(j%o()mqDq!m0bNdL9(Am6NII;SA`8Ze3?M7(wJD_vNZb_H`Z zK1k*2AKB4BqS*VsX>cl$vI06~yT{EW2c7ery7iHHMKQjOTF$f2F;+s9-J-hQQ1|wN zS^ucBkFM70nc9<-Y#FfF-3|~ag)gt>;4H|6V-VF(JuoR{=WJZwbZoNB=-=~OjCX`* z%oMO(au_|b_=hOFAzbE-T6tb@NRy1^hf$Z{6lRi{f{{5N64XcE73@PpxkVOQBNG&dRXvZ1 z_ILrO%1)=2mzDStP~blxP9Ekd`JRt!iJ&>xsK|gw9Yhb0iR~9}$N=*{gfvUlKRWeC zbufY?sqa?Q*= zWwH)Xer4oJ_=+yUIWL)Wb8=)eP@Xwz6Fp-c$fTb^kt~X&)>ArZ zMNiE-Q>vV~!SawvT9jdxNKB9UEk-hyC43#gIc5jfgwJEZo))|Qixy5(o8zesxt9Bg z10x;)ViH{hHC9T~5F-8EjH8>KCEQ@6KPjEzU~RnX9^JH=Orl zk;r(S2N72Fosz`lOM$`tF}$^x9+M=@R46=N2?F8H8A{#G`a72;BD!NCE81b)x)Doz z%u_YcaJ^IAq~A{H%+{5Gzo|;;BrEwhW?@xl zj|e!qRB^h_!sWI0QbC$jOXrLpR^tGmN%BL%_dO}DL@{iwC~1_F^4*~0KWVaZapJ7s zhxjZ-^RHfSru5?H`y^MgVKkjk?AO$}(XCPSf@#cLNvs%Dax}L_Nl23xKG#|GrxjGQ z>4X$e#Ib}hQ`C9X2jhN<;WAMyvMkl>oL@qlx@XT7<({7hcXqLHcw`DZ-G=s2w2FYR z-+()9FjmhzpO0!UTxU@KP%0;pFC!@xG$MaZ&6QC8gH+i z&l8}Ems3flc)dg^t;y4?&VhWGEHw>GPFUi95srMt5{bT99fkr#PuFY(&Z@kzU-rVS z4vW?1?Ub;uXoWv83rM}h{C6w`^jpe<5gW!Enjxc#uhu%$feQgER*f^U^AFbrxN=^} zwOXC2Dm*8CyZ`G{>&+YAiHol#HF{1B5!JBb<2;@(`q=8?nqf$QQkFw+N7Y@$NA4P) z1v+_QI^yyQT4CxPvl6~ZdwLR3V9Td-=0Y?nVo2L!tG*hLIVl_1Z-P5`3p|=sJQOI_ zOw$Wb64}$mJ5Ucm-}?@C6lwv;&>pl%XRj*9fQqJq0=;t@`)@pDuZSSuQ>jM>A>1Nen9tCQ*V%lhIhca$!^eI z<2sSPZ6g7x*uD!~Vvwgb40t|-h;pzW7;r@45URT>X~6A;2+bULi;f8)gK5X`QA&Z7O6 znV_Lmm-IC4e@FbR=dT&*3WXo5bT}snr*!kt=2g6jR217O^vBw=^_toA(N_@309po2 z^_W`+Nfgxg!Qqds1K!j;mynVz1yH)fH1Qub|Kg}Fx5-qvzn*IM=Wy>(6bn548Kw&} zsk$h_tGmcWUoX|^`pt~_R2yof=b36_><Ww zlNy6S1fU=5qTiDk4-P_jvTP+=Qcin6$KPy0_R{m`ff|F`iunC@^=aaadkFZoCl0R3 z(a3GOZ2CYT1^Z@WufS5bm=FQhcf7O+=1FVUoR-$1#Xy8gMp?&q5 zt76XC6L)2QJ$`?JUHA)zS8i4KH=Eew4%w~4Nah-J6opEKDGyeG@=wDvtU8r`*|4vl zSCZF=vLh?9<2=k`015tln9S*%HB&byY|F5`ZxiLaOR=&ueKICoYr85NX)Okv|C{Yd z|xO!s_$3+X#FuzWn^>yR~mTH?+q1glvZ)tj2`gYC=ENN!3hcPSmis>SygII zhTU<&GITc^a?@&Z8>Y*}U)Y*(YJTKy38tk>^mJq>BjSgWqtQrRJ)k97<(Z|n6AH)Il76RJ|HC@g7_s}HE?JDzVAqRh~^O_o^_KD&fT7@mGRFe z?_9#ENnDUPja>!S$m9FP@*Z3vF7m`jdMsZUgp)r<%ctNGIjG=awWVyA>(7tKg>rF1 z?b4OfReQ=tZXb}+jp0TqPW7`qxHcZWrJZ!c*U*E6iuNTM9@Rkv_?P2zWCF9(etaz% zfN~p4SzJPzG*MQtLBy3_H8cdD^IPJ#@<<;I$P;9AzYK*%qr!%*3X+XO%dlAt>$Ih$ zH`CgI3TW*uD&Im~ZisK0j}OfhXseOy`lRSejJIdAJd*)yn3!>I!_h_o?vxCmu?rxR zC}GHt18{!_B};U7HmAcwPEmLZbBu5nWgG{90R85s0LoDofJ|3JPE?fNCaVV&;=wZH z0Q56o*(dY;J~YJsEBp+b@^#fH?t_6*I9h5{o|cqi8VPGeZD4fcwA43OIoKF8YrKjw z5#)0qr`^8UKbt|3RF9_;mtPdO^b5UfmnORiiul^0=#B1~cfO@+V{4eeH@y&Zm4J_CB|qWcCnq2t@!K#gx2h>EkpY#a*X$}dUSBP&X~JL92Mf) z-Pg+{7nFuI}TyJMg&6Cet2telW?Iqg<%?N%PrLT2w(>`y99CD^pqIor3@n{fX zHh}m24;PrIn(ugD+ena|r@0newVsa?54ICKnY0RjU|b6pX((N7Q56L-T?Per!$OXx zm}#s(Fts5zVYtQCjJ$KlL|Z$20N2W{2fKRxsqv7lscZj}1(f!ETskYBeoNji=F346 z?DdB(^l@|;;B)+0*oYioz}k6sO?YUrr z17E*@3@81wQ`N8WKao*C2_aP5st&YuRc#eY@&d)z`yrj8A}`h0gwwtEN0LQrWB}=e z%<9(GT7%>Y;p8_IV}G9A3&(`+c@|Pt^N3F6)oLhQzW?e;5^8&Dv-xyF?wIxJ0!|_k zOeo`EOTF@Btgi~k7Dgk7`U*57mt)?Udc~*j?}_o(D`$tyRrZ=nQ=B#Ia|s%9Of) zj#knIAY@4WNqhyOpU|xchJqu8now7@qObeBMHsV~zX55nDR>^4-A%Fz3UFqB*<|fv!DYex{cZ4}@6{j)i)L8tXqLq? zVcwUAVQ!aEFNNI01gO!I^}FBE0vS>g542tg3Ya~jhdLzJsFVFAjgu~sJr<9Y@Po}4 z?MjG^NA67Rd!GCmYBQI6>#-!qQ$7uV{c>?1+$_pwgN)oa&mV%lPkU^RsBHI-Xfso7 zrLvV&x1hF2FbJzw&mIE&@OD(Ksp_2rj)qO}V-_S1y9cH-PVd#^OxzD0000`%Y9lDq zGJ))-*(F6ToX_iiq)~WjO8niDet6xaB!SGTngVDR$!i>(Qsumk)7$fIDgK*O4{k$e z{t&LBn&au%-ky8!y*JPE0qZaGSfH>a6&6;Owmv#@Y1U1Yc8WY?z7GgPiI_VzN1uca zr>^Jty-_hG!E9?bdo=$z`80Dz9_ZVq$h)U$D@Xuiqwa+3Y}d?AdIwr5i!b8vo=8$4 z>k0gxp*|D+g)0SFwj85piKC(2<4hstq_v&?z@p$9VL&U1+0Ikt%e4~HW#4{b!rCr4eLikUouP^4y8 zP`4kfo88`icjJDpjL)qggX&FC#+=|4xui{*b%Ns}PCtdF8CNy$+}4eOgzTU zrX2JX%5w2g$55C*lXB9ig+y2#(H&}@1?+aa zdkAWnl|odxTeY()65g*)f5+pZ?RjDdoZRbk4Hm>44Se-NjI=4FTU4$iXEIwtO`_ehGt9D#pSb z3fWO|i~h|yTfG7OXJn|j7dDp%|5d4cI6iIjgEKJ`UJRz+SIok*LtW#g9G^Jg92@{p z=2qCx>$)GBnEM?Iaayl!8ZJl~`AEG7v_rVz>Gd{7`-o1m!3PAVP%GF$Q^vg!9*-89 zhv|%^CFPK&DS*>SX^ne=PbDSsIcf{&wrRKr%EXC=U{FNHg}~3fgE=An#_k`>`@Mjc zNp9El0qRFKH-D-)LBPr;_3=xEEuosQCOw(D2}aJ7n%z)%ZfSjn2bT07GVyt8FiNW%qw1o6`SkB3vc(d|EbwDg8g7yXwW zM!aDd{&+9YEmD;e5;ckqNyZtbfEZonD$YWTepuWU8X*jgR1h6TYI*5L{2dRTIXa~F zW*7a-`K*w?K6729@ybX3{=5&7g^kJ7yiCF=HwZJdJV&31)R3RouFkx*AbFkas2wdu z`8`&7i`pGq|GjGZ{W{!+qQT=!e0=N>$ zFl`qe?G-8CJ)_bZA70Ui5Ca}??$u~#XkbkqY1W`_fq^Qc=27O>mhXW;2kt8MLk@=N ziJA29q$kY&QLzIk@~7K zdc6S7nU%}H$Q>UiwQ)Fyd~3=+`pBC;7EX}{qdJTfv#HwLL*pOce=UcZE5PRJyMqK8 zjr7a@bA)Ea5CZIp;#Xg4fp-Rn@mMQj2WQ{>Z-8g~0-4{KO4GDA`^!s`Iw79MQGRP- zwD!?#@AW?VC|*I9AsoACccfXyZ9(8I=YX!OKlE=|UvOrb<%G)QpvgoTY};};WeQWg zelt3-X1z^$Hvm)Ibz03}V9C?BME?v4 zb$Qg63diY

R@i=0uuF-spLNd_9sX7GcExw$~=yI_r=no9&5PX`dwABVmF3PN#qh zJt~P|zfDZ*({~K6JXfdnQPb=lUp6h9p;p zy;qap!o4e#MX5`|9=C6vJL=1$#nN4!Q{abE)NLO$eP^I^fV7He$REd(iw%<|MEIK5 zoc(GHWGQ;}q-|&=_fdmZUVY6m8BJH~N~~c<%(Wlw)--Lkzb&!@<89Irdk>IEh~z&JKxm>IS_jmEe5z5$d%>&}L)E)*78Cvs-H zzYoz_U7*wB#3BHFB-&{dyz_m64h<(35vTfuU-{}~eC$$N(Np|e)bUEMrW=}D*OGRp z16-||T2#eU@IL9<%`Mxi+`{@07&%nO(IW?Fr%%!#ko5Y?lBYKPk5%87EJy-D;-XVl z7JI5!?CjuH$oQgwGazFpCFquCzwe!D(C|zoYqr2zG(5M?F@QAF(Gbka5@1X*=I?T_ z&_w>^U+@bE4=Qkf42OC{>ZX}If8K585fT27XvAig+UW+k78W(49haIreJZ?j*_3|J zm|+d_^9m!Svqhwu6Ngext9Lr;-NkUBym0G8bY2#>-*X^bVw$il>#1c)ycR=zH!_gq zgE6qaz0E(%+S`HP*TLN0I}Q4oQfWw%=1Tzb&%Fea^<;6v#4jJUoC zJn{|yoq&$|Dt_76(DCz*J)l>jgNpv+QcLXlCC?|osa7%K9las3t*2e@CoFE07VoX( z)B%1jhE}S6+5?7;@z4eNam?jXDQh!=#f_FiAm-JghI}Jm;0$gLL ziJ-;&u#V#~8=)BCr3ZeMS*P6;BT2>aLmO2wZkWylai@)%H4^-SG3Lo3(p=v`o*O-=be?r%;O;??=~CFR&-aK&YZKh47O{ibvXNX z*f>5*OM;O7lh1X>SxQL-3P{`@9PcUzVjmsn?NzFUzFA*et5)@mv``<2H6Z<;PZ7IL zf|p(3?+%>)#n1|u5;9`ItXm#v{thf6=vgSPGW5zv{hVPE6(5L{|q z#?APwUKj}ch=lM<3~m6=Qr_fZ`T5WD?g*A)S~QhbcHT$%<8#4-QXAhWI7%U4Ty|kLPButLwu#~2$#jM zJd{y7o4n+n8RRHMmuG_muYRdE#o&`D=D)tS)17tRBD3dNapD0`?1@V5OkLNQmm=%< z!7KW@fE+3`8}^d1l(Fr+{W#<3V|=4qlVo6+AVGW2qAmxY@pZE1eZ* z(o(&gxsukj(^x6|6PL6~n@&7V#tmkgS!ZOtc*p=>%WfD7IhI(}u~4}2PmX}z=Q=F$AwCup#NiPaA`Zn2@vF5d5li*d92UeXyR!&D2{*bY?1)owd)=E;Y_{7BS zG6!(Rk+jCX(21tfaNfOsJsmk}y^UDinMNM2=y^HSNo=$CR!Q6KeB#>tSa`7NUD}08 zLF;caHQ+mGSoJ`UhMycN3w> zb}>I5K>($D_(WeMR8QL-OH6AG3qb=vYeA3P^7j7*xeUKwjgTZ*=XO;$8-1Re`%^1O zllk#=FG3PIrJmfM4;A9MR?7&OiZosw1aPezA5>thUEYLcw=vr%7G-v@p*9qC)|g=M z7gdpEa+bS)Lr$W$?FIo$$TWgNWWu7+kg{g_lUX=#x^|}mTKAV@G z=nwMrCxEXOEg;Hjp-h8vKOa8_h1p3S0OB+rIJ}Mxqt8|{s5FZeEP>5Jz%HcWmztY4 zTFEToZV@&)(^JF+t^{>=2`pPw%<$ZH;}X)rp5<-n52Ttj6JLVt67mZeCv(DnKsys0 z)w#r6NS^qpKb;nnv?VQezeGQ$ut{+tP$Ssu1(C&%mK~9Mg7X-IURJ`l<>THcLpQ6j zOUCCAxd~Qrkec2v1;FNm(cp@BBi^F+o8ZhWfZhRSb9rDD_n#eFeBe&OM%WIfNJK&L zjgJn;iBt+4ykLA~R0!x1v(W=n^V|82@lkpO8UddGB1<3C`(Ei(EuH`kdWYfKc(gN( z3|nzqJQV`+@m95bZa*`!e1DYq2Sw{vtcBNWq&djNSFSghA?G%OpCwJJ36I*_p{zvd zfzUE?-)BT=eSRoaZFeil4BMOb>_F*$N;>l!#O6H`^nMA%0s+hI?RX;1x!4Q->9$2N zWAk4geSH~I^Gfy$!E;zkLy=WZ==(0{&WkK#hHk!X;9HZNsn{~#UYOAi$X+T$_m>@z~; zvCw95IjrK?2gU0INZI?6eG$E%Op!!JpEJoigk2g115S1H?xm8(v%i|?;s^?BTL@UdZrJ3=^Fm5<#QOq6d0OQU@bVvJj@j=M`ndPyGtbb%q~oFXP3JJX zupP|TnGrGg=(BwSr^VRpeEd?4?ToMh|BE%6b9(g1fbvVPi^j|!f#kc%I=Kxa`EqVA zjrZ=^)xH;T2id<&BS*G?OIj1&BA^*(P)yC~K`^wQv(>|{%tMN9Vr~`2kit zt~7ZVK3{oV3f=n*4hv(=F~SU61(m&!tDB2#4oA&JG`c7r{xuDxxdJT__QMrv)t4=kda;^X=oIRzkeL{s$n=^wzY87!oW3IHyAg;2%Y&dGzYhTTd z5DjK$wh6r#s2>f}Roloyqr&}NTd#rU>N`k+8@|YiLNuxZZt<(Kl`jO|di^Sowo*i6 zB^-qEr@zn*Y+iXB9oEL+uxvV*`49wKvG+O-V?qpzsrC*WRQFwg*Cg)FS7P!ZxIDYr z0=n*zK~r`MVTMG14X=EG3G<-X<&0#Wz%HWH5h*dChn?m(+M$#>ypmxVi$)<5hTf4< zPT}#f3Ip9NA!sTTxZ?zfKh)r_I~v9{%=vb$brZKnHE{4qJ=gk{2q%paM`|W~ooC(( zU)3$dZAwKd*eD=^k${kT;lIbP&=C|t?Lyt0^6umSL{ zPu1+^VIsZS2b7jgis=1nvEe#_yp<$S&ym*A=N2>%@(X-%xV;iOSk#BDWI4JL;s#DB zO6c6hW|!3g>6k`x*`U+!YIN#~$2bLRM}~_t;ZGVGEuQAA8%>zm)+L#QVzmh(R@s@u zl%(7$UrehJbN>DUXGRU3>u+H0?;e=Oi|hGgy%bD!O@(hrFKdTXUlmWUonb1&FYN&U zl7P#5VTr*6#SFJV-OF^Hp9K{|p@jAP;>uPie~F!+y%LLB_#HL%thwgXYlmImTLQ{` zc@fh&Ml~9jXb#KzUQnrWMO)e@`B{PP1Rc*r-^RJp`#n-#Yf`tJDST@lRrnMEZ5so0 zM4bnXJFxqKqGc5b^f$Re$>WXid0mx6AlpS0=;+xX^-79aGhWJwol_; zOmczfXAK%~Ua=S3L>+fgM?pf@k^WLs!@lWV$HEH>_D?juB8V37TGpDw^_l!F4;6c( zUuNGGa)#!}A8&;%+HX(M*sfJpE;(R zmn|}D>^Ok!Bxaz(o^McU2M!gVd39Kl2%qKg7pf^_ox^drn`gG}JYpG$~ zeYJ{0E%O`Nsc0y)d-{R1{N{dPv_GEGjE%FkKA4%hS-DQrP$Xw2ee6Av*z(7qDNvrZ zPR=#f=l;azc5$5TQyx6oM@+CRak=U5R+L)0y2Zx#m-@)>+y;168ow{oEjEr7_K-t@ z;%QuQf?p`nN(0_)qeg+wBI4o*5%Ro~k$m<=;}J<}o@^}W>%~4iT--AgN8u6;h}G+`-l_YEsOGZikoy0hn`E_?<+%F3oHjiTn>#{oMum3qRU%Xxc>)B zK(xOgHmHHCyD`#Gvu_rz8{*Yr((+M<-Tz@$&sQ`ysa}vO;0H1NuD!6&Ixb&c2$A!G z#)TFo7RzzNjxr`Q%3&&M%tAHf#u=bm;T%)xXt%^{jrxt;F;6vbE@$e3hf%1juJHh@ z>LYk39(Qyj|19UPw^Y;vvdpiK`7?$&@-Tr7kKnm=-i!A2*vulKlWpnIo*SINfOo`2 zFZA9!GkFS)@agp|d;h3cdjc7i%DQWmbKWldTIi2qHotf46MCMe*g&ricp4dI0is^S z&B2p!M-+9s`&A=79G+%szB#;d3W5Y*EJ{==YM3V`$$@$%b zYfuQjGnhxcr#^WZwqZ8YvAT;M275`FKtuDU-T^U4$odi=6Jv&AM>`}s2FI^Sd@af$ zjo_@5#=__T0AbqSx4er_kdk&xN&tS9qSHiVKi3BrHG`c*ulqxic|(uAapJJ=s8Fya zUQFFe2`4yiTThR<>E#M{pQg0+K^)cU<-I+`xC@r1%p14wNMwd`(yxm@VOpf^nwi^w zy^-bgyTmyuNNR@z-3MKF>>}tmca`36&B`4Ne2m%38>3LXmWV$N*!(%=M_4&6cD!d>>ja0J{K~!5EDp#IcTerft{B%h*(z zI~ee4odc!+XM{;JxDNo-A3bbMBc?Y1^m1ECl)FP4tIAkLMMl)jn04}Wt4mvr<5@>l zp^4@EkDijd$IJ7P*d9#$a!wia9BHd=PmW9cns*zPNkEWfYy}pGsQzxu$wmaGdF|Pu zsJ;8azOM=qHM{v6uD=enFt&*DC9Nc?Vc2WaEjx%25RcqzdZ*srTE7yArIV2+kjmcW z5rK(Dh8FsW(P4dITWptXe!MhSX3i1%t`@6V-`-|HEzj!Bd@snGG}b6WO6z`NrWtRf zTWmhox)D?VF8Ys$QYUh{n7DGkgsDlX&6lrzoQ@1>H>BNCQD{0q>CePbc_onvbp5ge zI{~m<_Czf>OiE|r)D@qk_Qs7|M7Im(7R>lYO+i*#FWJu?3Q)eH$vZ(|z6(Mb1SI6C zIzHx0jw0nG8Ml;?+#92GcLSbU{l9!*Z<#yf=cU=EQm1)F-`G$wsE~tZUIh5-w=y6R zhaKvSk`I4H_1O+rjJpsa{^zc3Rr4*~Zmiu#L_*H*3p@0Wo=#~x&JxfYMGEzbiM~Wf z0&=bYymULcFe;6)XTx#3dC)u|Pzs=h7p5h!*IaD8{2c1a$#bg&nkjPuzATWcrv906 z2T(2b$~C9LmJ!^NxtQdw0$EoTyl(FU#6HIbQ6#Q`s_L615@E$J>j zO~(nIGbbe=%dYtV2~lmO!fttZ8plTQA~sXPXRWJ zoQ2q`n}KmWa@vEBYpDddg8V@F*uDj&7j**ynFcQq(evvVdT*^LES1)R6IjxVuz2C5 zvEu@zks_jzXR+7>lr7XRLl@0j?l@IAok{WX{vzK{+0xxcO&gv*l?SkN=3Jey39H6t zW$pxA*R;6IJ1>2?AiChE%{?;HFy@4x1^X7&99;!R%5cF^Hz@Qz4yD`qEuah!Ra_P% zdP}};)*Ad6*iR^*Z+ltNNbTH19ux(Fgu8D1XmG$^-D%(M|H`-O^gEE!LjQb{9E#L_ zlpq~J_KCtUS*$ze{1o0N&VKO8YLsva(Qti^LDIksgSTq(BoyL6b9fr!&sDo)3-8w8 z;Y}cEhqH#N&lTgj&)9&&x)L{S1N0M-~A(p@wXw#|eo|a|_{>y22XlG6O-CCL{f}x**vTTE6mcdHJHR z8YagL+hIKUCI~e;yV~>nfgB!P3kz=0P!`u1I&$NMM8WEAs_H_&Q^}dO6npA0qBDFm zGm7n7d3vX3bY}3Uk{ennPEwuWvbXkIsot$_iJ>N9iJqA^292D==Q`cSqc$va=Iq!E zbvpPmLAIY)LGHmnkb6Jw1kA?hLD-79 z4k}Gyp-#4WD}c)OcsM8zCIg2IDj0L7#K?vu+aUL2&x+Fn0dFgL+7!JC3XYp%+c`q- zhhlOUUNN-H1*$qgM}61B#KrM7$^)7*g*|+48%^iZ=%`mq^|_h)mfIou2(2d^?}(mX z0P#rQ7u#|dXI0o&$u$fXQ`#IBza_}r$|^Pd{aIYAYl+OUDvy;BE>x>-2h)L1e&Zgw z3PEM0)QkBnT4Cd@^pYbm$1Kgt+{7Sw3qrWJmf2M+rM3)Ze<-eW~F1g%F=6q1=BAyVeq_-R4{7Zw`NR?f`KG;b# z+5+Y?k!pCBa=Oqi>!U5*{TMWOWj^-bHR{XmHG})7BJSijtTE^Tt}ZpHr;_=!EAz`( zm1n2TsBQf9U!vUD;z!mT(H>5;z%zxxMLd#$+uz8Z-~aZ!cehfF+l={ ze`B6iD=kANlt<+SpPA|yo)NzolYffQk`K78D+0a}9F2;VxQTrX^gJG0i)P3x&wAbJ zZ2>*cD;R6QW-mX@!a-uFOCdCGS!bxES_|WB%N4V6kIBN(u@PIY&P07opa>_Ds+vouu0~g7AFfls zS}p8VUAAyzGgJqeSjTIEmv7?=t=@f1)P)ijip1v#JCn`IFjPeJY=9}xf%lR`i6?V< z@*9W0yEgfIwZoaw7O&x{7K?NIrLm3bz3kTZJSE)YQnR_)(@Z@Z`*Nbh_ZB8p3eo%7 zLDl^U2;%bAK$_j0Q`j*=4Zqd!GNjW-+0+j>gai-h)jwGoTyWjiqJT#q#FA*=?Dy#E zi>TPDJr@3XHi5kft^<*K7-<$l6?axj_Nz>dhMq zEIR!g&lpx!vJKI0GewJG76`O}OvOxFtQM&RG*iwZ_`)^k&@Bh!B81sSGkv)Zsc;Eg zYlCsYRR4-Gknqe_1?WH7Bb3;RzCzpl>X!-N6Av7S@nF3GCDK&+of7PW+eMKia3I2 z6Db#Ke&F_rO4O>JZLLP)#AO9uXz^b{Vk#{rLLVno~v~ST(B~2`<+|? z(sYu~8e8>V1r|tFY`bq#+T(M&9%j}7cyR6ik{aG+6GKCl(ob#An~X^`7-c!W-5XzZ zmY|N`@eu$xzgf4=)(l{Zv+!PR2u;76P&6NMQ8{Kf!%;Zl<>^@$wY0^l+W)sL-QUFq z=KXXS>&`7Wp;yNkSztQ`Nwui+)Dh63=x|`2aAA31!9@MBhpFx{LrlaI0$h=w+St6S zr~>gY#OHOs)sugdaesYg92xBA=+n!7c;0kJ8Vc#t0)t1mp1Nc5`bJ{#w--EF`TjJ1 z4~%okF`ggXZT3*H)Koq^8+R9Fv8}0nH`*28rWrSn9Vn?!bEwDn^L$PY-)oGhJ$PuF zS&8sgQMHr|_%7NSWd9bzk3t*39>NC?OBaESh>~p#jx||9VhVO&HmOlg?~{}@sH9As z%v8C!d~&{rz_e>Rcc86K%+dn}3J|W=G`|BrpH|BA@^JQc{m; zUzq35!?0Y1!I3Eb#qHY&Q{1gN@rff!^Jo2pN zJ%YmUHuJelzAS>wbFI>sh)OZi+Nbo+-C`$qOI-K}Aj_Z{`pmgaicNuE6cO0B;zpHa z$Uco#scYhTKrQh0v$@nCZ~noodxOXf>q^7%&ygr*=C?B})+h}vlXsQjFL znlLT|`UbA9`E{VKjWk1_xzfo$mKDMX!{(7V_Nou@fOS!eyU9&yxbT*LMAbJpJBSLK(qU=Be>c#0yD`rPWBd+)s`eAZVdd|@0!q0(t ztdmrSgBAnHO(@~mnfikfZ~2rl$4Wu))mJeGIRig-y*^DJcxCB5TqGz)xC4l^_=hz= z>9Fqm=TXVG%BmvY-e{^uU)!cX%d_g1y+1x8-fGQTi0Z|f^XZDB52j-NyqsV+QIsTqhYY+VnCj(W+;co1^ za*Uk$1Qr(vFf_hO@B9n zt@jHyn_{asJ@TbliMgBls#FJVEyzn6zyI|h+CA-&YAs8pO#eIyC5uHcBT7IA91e^# zV5o@d$Um`s3rMAW3m`-itf?u40ZUgKvxT;(hoUe3E{0e_lJhDy93F6RHPIk|NHkfI^40=;)Hz4)PV*lmHH!VetvMdL zAb|6Fq)JMwuR1kFpw5f{4+Et-+clI$#MF{Z6M@8w>v|ZB%&XyozZZod6y4 zTIRrKb0HQ+U^?+8xyg@|;2x`)1<8o150mq|d_QUI#Iq)B;bvnufY-XFlWTm-(I2%# zBwQ2(%_6?-mZiaTJNHxt#GP4DL}bO0AF_e-OGpZ}_@&9r4eYUMkg;}Fq3=zy4(s5m z2>6)dFeu?vS3b*G0Mwd=!ec`ca7$X9tXk_Bk4hAOdtx}*F^TaLA_u?B)u4Ny$ur0a z|Aof@-86{JJWlLeFAn1{sEl_gSUG)h*iES>@uYi3$&p-dfQ|c`1^Ce55A4srmV0yA5 zd7H$OD7Nz7eH4%a;IHU<3Dlt0mZN*`iGO%y^g}oSxOFEEBSa0b#c(&jD9tfT6%%a% z5##iDHVRPXE|C8}eVa2LykDml!7v9@p)K&xJg893xBFKfitgEwMUdReTz3&~;EGSYq4F{I)a5STx=3Sx+s;CdPJMvy_k?~Q;?7n!}N zzEe_(Sd}QY?fpW|uN3FNKbTlqTmAJ}BL#@X!6nR~=I*b^k=BFl{I2$g)g?|H){O$|mtSq*wJwp`c3E>sCWsBwZMx%*a&UT!)9LPxU=7JgFWS zdFCmr9SMYH&`igANvoUH`$EZef1EtywK0?k#a1A{Ril6`Qtz#>d@d_8&+psHMk^U1N=%Hr|2>BdL zt38wd91Ktvp1Yo#`pXpxc`czSLXTvw3+9?Ad&-fKGQB;}mUZ(Zl~#Xy-zYMgQ9g&dUf~0;dW{O8ySC z&sO7UQUii}_14-VbTJ=o+rOzQI#qtoI1xF_Fn>I%7YxCg!PBYV8do4NngfTf0XhVj zriC8Ood54y;e+}#E8c4A?NUZ1Y-L?o#o@s0Cq+w>VzO<%K_r&8d%Fu}N1d!T^NHEc(ku=XM#&BiS5UzU<62x#C+WGaAxI=Cs`dX>+RktcX zmh@eZ?wqSPK@)ZIk#V0hgZMEofM0=jTf54`_U;nf0#CF}DMlxT-VEKL13Kl*+a|Jp zPt->blUuVS{x>(*TQS4Y6ozbRz2^C)T?(L9K)r=A(*|IZ8Q*EHqG_S0fdh3s$vyzA z;yfZ0qP?r~E-l0FEjBr4uO7PaWj1dj7vkFv`-CB9wlm^5G8n zPR&vbr#pLgh})`F4@M5llJIS(8Anp?WDIs?)$k?2B&l86_JGIt#=rfj`Ar$h8pdPN z;Rf%}vJC;&B_(*3l+t%f zvCCzk>0MA_Z|ZxDSNXz4Fc?fb`&|L}mwu1q6I;>C4xI^u(rM7pzPQ?B$YH-xKgk74 zgZHN4XauG)BgBI-OwKQ`lpcBUEUT8{?>R)lT_;x2m(W?iajC(G$E#qDVnMJgUItg^z=@oTZ$s1R zcHsYo<}+`luhI4Y?Vg-{UC6@Z0JCMa9vf}~-9lDiQrunD%;iM~Vcv{WirQ47dIJD1 zLh?zv6u!gXxL&a&7im2{7PAgB7KrTNM@muGnvwy0C*=@y#pp#r7#16!wl&9z`waGP zZMpGBuV}Z!jNIl>yTJjNR`t5rgW}Kx>dn&U(5s1*R!WdAHUS@B+g#D;IQ7SSWNDMg zFt4a3Q{wW59^nJ!L5k(gc4=SAKe-EkIWCkQs=V{iD#LEsm1MMnyT3aCp1-3-R)*c# zmPh@d#>e!Tn35PIjdSyj62u1hi=H>m5%hGJnZD>tn8u8IX4(bHdCOm%MW3}WhnG~{ z_guq5NSO{wDrGzjm!)0*eXVB2Hq7bQ4+Ds=e5<^0XN8%_VoU~4y8BcrZ4y-o5C@p$ zS%}8;0=3~mMal`tqw;DiOa{c`%?)>?cw7v;+DUP$!9b>KHk3#bgMS#=d`N5XWpz3_ z)qUY{Y8J`IMHAC03AYPZL}`c#84P zh&Q?Qi8`MHL$}?tZRCDX)%X8E$pDlKXJ@`Ee&S7VZxNz6{vxsA%=_66E`mqav%wBMd#bl1?ej!n| zbMu;tVO=o{Om+K(sWAtb6;MzGFZ9rl4}k$GFwgI_-5;3g0g%2E6CHD%dKdDn9=iGf z6p$>LbJw~2o6W`wNlJjNfnL;Z)5`MwziefgO$r5o_-I*xVgpN=?3xKA|G~BuR(lne zJ5@*Wv%gG3@Z-iS#|1M3;Tp!1$$$NUZ)o1n2dUu7LTrca3;e_fArt@X3w!H&p!z|z zcgK&z0&f8oO4k%|@xZQ|*ohYRN~O0eL6tbX@Dp%bg5bh!X@jePZvLAB9#@nhg_1U^ z()6+o-Y1fR1-LbJf}5pDOPpTiBKP0tNMDKALL%K>aMmcI0&wup7{C9`GJ^#pYRfVo z6R?};)XJ}V9+qk7Pr>@ej13;g-X9*zBet z+F+pNW)k5}MkMgUqIaJilKu;YDDqo);BT1`clx_at0wHCOeuqfm|-~MI@*bjKob4z zZ!@Kqqi$_fuyi?#wu3%NFY9W8thgXb8>)#W$oz-ENPp&aMV)5~I&ZDCuzm0P2@=Uk zfn*#T-`{}`E6NbU$rzdr==7_D>JHK{T*Sh(0mKy*INVr}Wq74iM@4hDQDK3>N%^|I zYBYhbjNU6FOF%bP)l*MTqE$TK*^WoJAIYzErxL)?5uUvatEh$!(rM_G>w7fJEo2$T z*#FLeEy{0GR*_7P6=a= zfyIT9$4HNk$QzX!3_xirgWqhsua9BCd+g;ys!9-34mLSCDTGHc~90U`8)Q?2D8D@x~O6xWM1LQK(L zO_HMCkIv5=QueQI2$+w)3o%~y?DGl%ws_+F%+G%3-Ziiz_>nrA6VP$hdrxUP360sy zACv6NP95k8YjQJww)CLgY(Kmx`Kx4*Cdfs+TuKrG*v>h}eIGu@ZZO5%At*&-Q|GU= zd3=aMvr2R(AbLuftxy#1oY#n1+;zd5So6drV3E8L45vZ!-!+2sUx4UG>;=s!6dr2N zcM~Y?Fp7RC%t!2f6bCHev!v-B1sP+q`tyKSxlJBf?eIrYe3U4vAdpz+q=DO=D&Hf9 z`_+>r65l6@}I|Wb@Wj+TD)=Be@4RL49J@cwXExf2>f< z>agZJ_80v1PDzhj_Sh8elTpR2AH_ZHp}2HI5N)8z=Ac)b=VlUg;j?;rJK99A?uUTY z@qdk5P!jghb1QuF$gvus>v#5oGx??ySLAJlvcce02J(Gi&Rmh+`AMK|vO`4xjxHV) zZyM7uQ=WlO&0Fcc9DJp58r?DTSqjCvnn=$c ziGTLYDunkvm7f|Boo!4V5GZ})x0~e#jrW-%SgITS9iOl!m`U`hgo51%&Ch=_)|V?} zs0tyvT8cxssQq!qKw;QD+(tmw$(JW-6dKy1a75p`E!zxW{I|ZxCEj1zsVVaXK!$?r z`C&F%oz}(Xu?+*+S8nLN>$rK(!1x2l*fdPbr^>s;YJXo@a1>NA?xFfG_=DixkaHcn9Ua>5A)CR`TA zI+pj+mYv}pw=~74rv!e7d?Mr{o@ekPtmD@y-QmLhXkzf8>MO_Y5rYijD~>hZN^bC( zgC)o#(;16Eeuq4{#}7%aZ1}e_U%x?QUSx2JiJuDvb-o3gDV{YX7cDCG*gDdMB;y4z zRvpQS%{S91E*JShnIO{$e(Bf|6aaJwK4+-m9pX;JIXeUcLS;se<_6$WikbyzFbAC` zz+gd+L~M{PL2*-K)A39DJ^*Se`D~S{F`$ZvBxiH4w-YIEP(MRW#&e{$;Aa3t zES`-}iDT3u7vvBmG`iwNs(C0}q3;MM0jPi9aipMT`eo-$`skXw?iHDI9LANX$CNR%S-c0XuOcH&xi=#1^e|b=Z;qiKWZXNm4#l| z+SUxRT*;l4yMiZke_vV(QP1v{l&zs>mA+<02$YMPF3Bm{q`idGYzkxmE!#cV0%M&f z0>P)YB~>WZIarJk!+zuvAw(-&OVk(QNQnEW-EwV4a zXz8htrz0-3bST4WQgR$D1@v+&5U?uiHc-1HS52A}-%h>l%Y{G6jE1l3h#-YuU<&Sr zSJyNFv@#WUdg^DqZ+0nG8s!pMVuKLq#Sp5<6EHicT!Hp?NDlj8LH1Z852k|@+injz~mMEPV+{5)1I2o$P#EybJcZQcP{;nS93 zQz4;qx47pX|= zM17e#?+7@eeZc;CbpfM#o&a2wWI&>uVsxLIDJ)VN_7QN2OUY7A#V*uZ;tDX+^J6&Q zdQ4ofr@v7KrXv|(fUq=@xPTfkuQzMXW=3&|Fxy~CF@nZnYb4`!j@b&r?-smv7o(Yv zkXc?a{N&!GPVHr)KtJKpRH`g*J?)NJZm{AJhc+IsI1`WCc4a175iNiW6lZKFscB~} z@IS9Jx&j3SM%G`^a0L655hXtwgaEGp@L)L29pbbZ54u8qE0=6QR3g6YNQDQVaAg3U zLwU`u2q>HUha$=ffz2x_1Dy*9V)LZa{D-=W#(!$LfG%eVn)ZYi2%G9`C@OI1`MH5Al<6C#x#gOowaNyekePlJ)tCkL!}n z4(P9QqPpHx*@}eH&X6l#-*ih#Gyl!}#**Ix>=99;osNNSsYQCaoP*Z_Ya%h+R~J8{ z%|9sFd!2_8VRQ6oY~2FS-hV-NM2RyJGZ06dB5~MijlH17aix7T92;+@;A(w9%o#p?+<_Z1Hb;i7yK=w zu8Aw{$A`&nzG(Wd&=zbOvoIC^9eR%I{C{B>B`8to@`y~!r|09|LZyo&kkFfAul5K? z?aKb=PdWS1Dgr*1l#WhDPBi`p_T#q3+kjG5ExPkTQQ>r0J6o(cj||v2#3FLG{)9jO z#2Oof0qVFN3~%e5j?`Eocl2_QkE_4YNR52MhPpJP1|9z+nJq^AXV2~-UDBOz>+kY}STpJn#{AH&Or7@71+ZU0bBv+lGeU+zLv z-)4y4A55O$Bo@X(W29Td?Ep={YA+|Hu4J z3-Y%k8aHgh=#;7x3VV8^>`pXvN$wFY#9c`}oGY-*CX3VaxH z6-fPaz!1lV$Z-|0lP@YEm13$ zS;>j^m=LCys;QeS#3xQEI)eNT5iRS%u^~xoGp(GK0ha}q<+9&i>$D0Xn(V$vVqngS zb*m#E)iRZ3jegSi4Y%mgO1LVe*RH25pjEtIIYX)&%n3uVYCEw#{nM2T4_2V@!!Deu z`)t`On?xyp{P1ftjW20D+z2ht{OB8e=@I z5lkUdDlWS0!BtcvJ+5oR<)*haM*`k@=POM2|NP4kp`YrP)+@$m|4tq}dh9u>^j!?C zDT|4M5u4Cz>?SL?VX+IcXfCmkcp0PGG?N;iB|?{37L*)eh|Z?K1RjqzvDF7-fQa=0HZ`8lK3a_l8c9q}ugsS*Iye(IHdf{9m&Klmn4$eB z8ZYEfloz{g6-|$iP{Q|tc8NQ%XuwCpXvAd=KQnNoic@3_E|@;dg0eClA1zc%b1P)j z>MfGj`y>xNFHq@U(4M8Q8r=*?CXjtn9QVEs=Lf|1N4seKlRfD}?Jqhv7{~RX*k;Z@ z0y{V8>SXWVjsi%JH(^|4fLL`O4{N_Vr|ied1~)Zs*5WzJ{S-YWrBQ}pe?&NTKp zXDAByR8+^?esR3IC!0#*5~2nb3uILq`Y^!O)I~ehS2(h_sImn2LZixKdn2XCOHx5X zC~cPh-=QaLTSg%NPXh3uDG2H)v+J+W7krf)hrSNrT+qGch+XT0U+3PTnezs5I@C^> zk|4OB-4J8w5J9`elB-FkSW{Kdk7Vna_H22#ARuIi)K<1o$@jlvqzGOnjWfo^Tev zaw?SheJx@{$%M2}n8&pRCi*O?n|1@`g*-=$Gc1^DnsjO-XhK1i8-;mEsaX4I8_G zV;on8bIh>BTJe(1wL`k48U)z7Oz;@tqrmRVx$Nkd+flpyH_+IKFmVj*@nigpyDMXQ zpW5@|M_kaUTL53iav4|0Eu&^TjzB+sZr(>s@@12#xTY+`*}EsR6Oi9gg^NsGH^npZ z!l}RlG7I9ojd~5TzuI%@cvxmUvcu;7Zr)RStSLX9OzV)3|?156fF`~unM-Q6as3*KxFm)wa$+c9c# zc}LC>csP4p=+jg4W34$p+3pALh`Jx*du2j%t=vLaHLbB>V75-9VvPAO9<+w6rkw55 zktNF=ENEbRCeS`L!j19s{p<_Ngv6roW__Jb9GpXl{nAcXd$(e`AMNEy)}^uxs%Ovd*IH7IOzXM@XhC%B-ud4aHRCBfxk4#)`xfGnyAApO2+ zMx{56S?48P`!Z&S3?kTgvP8p>IRmbi=7Z4)VfwA0bHeTosUv@uQiuR5)`R07rxDq+ zL;pDj$HS7`y=YHKq#E`;7DTt}RAPb_90dTj=i)O?y?Y}K+gWAvPkku8EDKt(9pza^ zp?qQyZ2~La|B5K)+`^eL5X)JP1Om=rO|eH`nta0Yi~%-1S9S~*g7e-b%UfwjMGZBZ zBx{Qb_7ekm9&`wMB$LWTw7GPQd_Ly#wkixwj`KbBD!joRc_pnxJI6(F(DPJ-L zz=X}Db`!pSQr8lw&U8-!yURfurEtl$5G4GK@PTUCAGza5EoQlhjL=3gcO@XO&XuOO z=Sega1BDA`aUFJssS=M{KoVt{gUWfL3U2u$ly~sYphd1}36-1^JSP7rK0JTtF0onX zGbeG2tCqrMiFr2*E#RPtf2el zck&m4^XT>H;Ka&@;7`IALux9QM>yo73%JM=2#fX)mwx|d)J8+2DVpE?3XOSzRlb7f zSpdw$jlmMxTJdTJpg!$*IJgs1RX!@53&(g^Ai>dMAn)o5cJ@7;sDdl?3q;STD}2kp zYf{nyMdk+##4U8~%kPy5c!z`MD1wokzMEAv2{_w$Gi=21EzL)D07^;bX|34mJ5U`F zg)Jj_$YNGon$-@k_3ie2xVSvk+Qa4@+OxM*y`?1;y_kG$$L_o((<56Q-p#e?kpdCM zU31?o9lWJortE?YDB#iN{3p|lsbaD(rm=(;DNmNpJ-dWp>5D5 zXD~@OWod5bM})5HPIk8G93F_tE{`gwc?Ec?-p+0@y-B0H6DbI0?mPqR*O9i6FaHbcQH;MsSTIjZ_X z4$EK}1|0QM^ar#MHZCfcrouZ`n)4Gs*PFwy>3%9dG6#UcK4Zk6&yA6xOS_2eP+`Sa zT*Sik78~m8)sg%#7GTOJc9*FaX&4=Ds2S(thryNmeNc8}3B}2*!{lC7=91M_^D^LMQCf-^7Ap2L(fl)*(>L z>R_GFK2c%sv!_!Z*N*ZIOseCpuf~#x4yrJ}j?ux-8$;}AxKaMW?2}J4PpEOQHiEe8 z?nxIlN#R%jB`^(W^3?Lr8I7aMRO#$IY7J>SXx~bxfmrPC#c>s^a(dw2&fv>;;hTQ$ zU)0qG-SgI@XjP$rO?eInI>8gncCODd{3o!a6;~q`Z;`b290E<><;Ji3ydOR``fi%y|8Gh4tKQ*e6!MRV`6Nb-lZGk@y%qGRsu>qhhoAqXEo_D_J?QqsaTpXk)t6!epE3W^GKqGK{L23K2+;T6r7hL)3UN(03Ylr~i1hf5XQBQSSHb)w`>U=2cmKLjmt9;2jaL5i&+ zcFku)SG;FSR~z{pTt$#{Vab*{8q-p~x9>^BR1sGBbKJ+7omLTH_rC^3a1+Z1TL67d6#1CG zI@zFMofyZcOB5!`Xy)1|vpKPW!Q1Hum^n21%guHY@etQa8;Rsppd0bT2+}%0v29sW ze%(bfih){>ve`K6%i7u2^ZRnXmg3U_bYQ(Uhl2i&x^okTiSpw9wIlwip^Ws8p#l%fe@V zb4#Q3+qk{ti-0yt`LMwA4cOXaggyErwpg%bk0nxNo91xwyWOBmvpy$6$XSBuSn<#X zWp90vcq=X_E!+XanuO6=ogm1=+}BiLu8^F!esv8LY%&T`+u2y+}*b>Em=_u?N@3&Sg ze>f$#EU1BC;)@;)Yzy`~8;b>V1i@ucqRKlZ%fe28B^=cpN-G4<5a)Ce49-TEq!u?* za8l{RffYov=B@5~&Du+o0qtMb^xQ!yu1JRDXN3GLw6tfaun=43Zsa(UiPqT_L+-#4 zJ7K8?lV-B{NsM&V0XJn-|3TW9e;>}cKMMV3Ru18~*?!&A4+*t$N$e(dHKhVLFc7tg z7O4RQw44bmm$s0Of$?Y8=jlD<$Qyje>gb-_wNXE)*3jA~<6X>=$t>wEaH&f)gBZa9 z3Wk$acqbG#@k;?f<#>v2f471e$5&FkEqX>isn`@c zOo8=DkN(|<2drbFkBY5CmkL1|W0j2UD|m8SWPs!Zczk6?Cj$h@C-i#gtziA4;A8VSZIDxgMnu_J56-3bKy{9ME7AC59>72 zfS?&e-$!aE;KY;oc?oVHpl0pVVZqhn3+89D2-RmQJHGWvGsZ&3v?I2UIyPT3{2q|v zg}CYp(cEB&B=#lIgCL|+-XWbA<5+{W%Y!syqc3+L^wW9F+Ue=od;hpO<(&pH5?kCV zdfn6w71}p;Enw<`I>JYvqmRpX)VIYsoCiV@lX)BVAuoN~Dta76@$DBE?q91V7gAXv z2-AcvQSlf3?m6OX+Ar(g#NInI|s-W_Y#rcWT$53t-+*^@f8oUW60&U8QvuCHC68P*OBvz3YN$qr1-D9g$4c zGp*zwDOG>Y*D<(QB1D2uUkjY^~U}nbcj5rBIT@e^)9K=Bf2#I2GAQ}#%zKj96d)s;hD@^wVvTq#je#ihX$RgwO%!G7-;i_u z3}>dkS#NF{cMHmfoc;xU)i7dKPZ1Al7M=#QT=gy9CRQ$RBY|$LL19P&Bre_}rmhg< zYPr!WLk#d~C`I`0bZ~!Y2(D!xyhzaoE74XA@f#{HU4B~cc)6;wHD z5eKeH39^h^wpTB~7>DsNgZm zial-GJqOB$4CiYnV5LkK?}W4MkL()GDgpM8y8}D%@XmvTF)XgZNjGX3nIvtL$#V+= z(VDzhw-O>7&2g`bDO!p=pocZ^x_M_)CR`$j_1xi(YJX>NWOq&rb9Fy(7sGP~wPi#b zB63ZH2(ppxlA7xZ>Wy@n)m5%Hr%J2#1x0hUAN}S#^`En_sIJpZ&E>NVH;OWVWvYzM z{tphQQ+vgCS$*G%2PRxEdZos>}4FdWTsV0uUaa|`X^)YBzD-F7P_NVfW&qCNt| zS2}wvJVHf!G@nbfT$j5;L-ba_4B*yjPCpt0W)@;-8|VEf(jD8+~G$ug810H*c#$omb`_P-Tkr6}8JfY4OooGGa3t(gN;3QF?Mv-tlqNGVg%vBB^Uk z1qj)PN&;h+@#tIdfBa66?Qa;5$YDKFgkfT;jj4ki>DHF4^_QFwL|hAa{(p~gXtP|? z&K3+^LlQf*162dOfs@*{EETMo5Tq=v9Dt3{?_OiS%k@d9BxpGv!+0LCS6J&$)DlSZ z_e+%~CTLVG!J# zb`dd*`RY)mD&B2MhWk+~stPf)Lx;HS5)o{A)GTXnFslZTDGfU(Q0qb!VrHz1&h?ar ze+!@&WM#ob@%#L3U!(51#u4~)UY6FPXw}m+tV77b{)wxXA+2Sm^IVFOTaU-;)1o&msJ{T?*{%FLT z$PhINdw^+b&cumVj1d6I5*EDQTy?s1c;)sRMJrMkdE{9%s-7IcwQGJhB^I;DxuQQ@I`hGz16))>9Wd~N(*)O@PY@|CjWXTGSML7dG#~MIwSCgS@xU)n$-q$9{6>h9&y#E=*|2Gv z`bZJp(O4IqFX{GCMmDsCFT?!XTtw5Pypit8jmY#3tdNq^hHyRD%xXCA^4j+f_ZONQ z&tnSZ#Q3uMDn0mq{5`ijN4T7mvZC)9m9c{(fv!E|X|uPQliqBX zGtS@kMNjgF^fUAGoHn|f5!;knA#UONPEgEz)tjUL_(iTCRtCla34Aso_IYn%kom5C zb{MH0-OVUIbYc;Yv6cg?DyUAof!_W}#VlGokM^*o=eb|rO|o<|MPweriW{4^yrr{2 ziE!bNUpQs|m@B5Iz>@l$eYR<7ANQ%sE*lkf4b-zFgCd1^8IM2ZZcj+Iqk2tmPbwbN z>NVX5Bv9`Af=3QW0Il3QvAs+=)28PYJ~=n(;kxW-vev_|p6x!H*8)+hXoPtd&HxSQ zSoR8Lp>cUaKQKyH^EeNp*KBD$`9nP*swolOYL7dU*eZ@r(ps?h(E z1^QWwnR2mffsqQZ{0Ulb1i&Tou4!>Fuvf!ofGX^GV;ExEeoI`w@J`Vb{0NA0lXRiI zS57cTM#m$pHoX0pD7}^yko$3Jx#NYIPZ|Hm;iu;7dOT=Tt=A#0TQqp>cYuxJ;hS@?gPT5vSJADyCf-g3`upQ`KA|N}wh;Qu1$@WRawCugtGuf% z%r!Ldnf{I9ehg!EP&YjRk#hP@Cf`;-DF=3yBRrk26#BmsTM6_D2b03DY^ulF8KKhM zQ4xX%@VnWpGUcQR{#MsFG?Vx1lXj5MH~=w}WrGE3msYtpUQzRSPF-23EEqzVWM0x~ zw$eb*GQeQ{AMY|ijzz#581jer5SCkjtshQ?A;W_xJMtO?ToxFgp}!lOBkmJ|SqU15 zS*(k+XLDB(Zm@+1mkCFW#dm~+o?CD}D9FeRQal7C?lJV6fMMXIO;BsN$yEqqJ#S7_ zlA0AWnQy1C3%&E{uy8ON6Kw~sVtq@3?CYtiV=t4o)Z#Q#hoIDn!udC&ip|M47K}*U zXtz2V!!@V@8}d6Pqkt)t%Lm+wc-pw8DeN+XB+W@yITufoPf}jw&HlBAjKY$@m-iT&B zV{5yK>$p#W{*{$`e}|be`#4siZRx5G>26||2)J`zm*{J(roS{aUUdF|R6N_}8gIdI zSMS>8ekSm&tgO!BF$8~f|L_nIFdZ!*hOT(6vkt|8{?ST!SU+L4r9vAWl zp+KvyT{vh1tb_kDMR;(}gwXWv(CqzM*H^zP#tsIQvdy(|D+hGHk)^HLf+!w@bZW>* zGU4Xcx6c%Kx6e#rFxsz53@!C!V(PxW1 zvzE}v>!uDv8e(f^!Upn7Y3C$~@4`lE<95_OX0jo?PCA|?wKksPqor(~EgAK<$KNWD zvfsvkAHnt4`Jm@2J0Wx;)G-tK4-{9Jgc@<8#u7^*Vnsd;NoT1tioIzWb3~7NEhaty z3IX82*Ebl8=FPk%zB#$_#Lf@Z@;yjd>ANC<*yCv3#=qa9kKq&QP0!_u*cG6IgmBTP zw2bKTx~lguZqD|CiLeJdb@2qzCj~8+&h}<-jX1tgU$uRQ_f=ZSP8O*v4>v~j#xfA% zDN|8HLkqKH*$l5WS|VLNA!~SF1Ej>+p<`$5DGKZUMD3tNK*mDs?gSu1eJqAgl?Q-D zrT)ToqlSsL85fbh73kSb(G5P8Qn?T?ll3T~MlsuTcV_NOr5<(J)k+?7wyY(-*+e2< zPB!&PFe1^gNqwL+6xC>ts6eIP8frWdDjkX7TMv1LLKru<|i$hF3K#*3%Qt7RkOUMMg$Myn zzI`6%_i+Zjvp2@{&L;ZYt;tBK|zc(sq z|M@hoK6A~g^=h6h7iADZm1e%#1Y;`0j`bicok=JaO!V0Fu`*;)&U%W_|t1CLII$y zZ=$URtv^=V4Eo+s`{9Ru?gB+sb?^Ejpvfvr+!9&U{PgSjx{}pk2FFWpFhL`(XVqxyWbvaY+=?in z<)dIE%HB>ef+SP^+KPc%FDS}ry+1V*dAE8uDixw3hMh943F9=xCYvl(n}-|9FH+G=F-@Ov%98qV8yllOh5Vpn98cVBMeM_-hFejb#c-qtDy z`w@kq1gRWHRAgs5%A%X7S&{Wf#qG{ouEsYs>6t{aA?Bf_9(2EcY?{?9Vx>2umI2EU z*1yw|5qpP7%ctY}F*W zfik2CkL)EKEq>TB13utCnd9D1Y?m&m*o5>Zx-E z7dm>vFGQdtZU4AYn$>og&MES}e}Xx(SmnOsg%HTrn-I9)BNym?N$cOlPQ`o6pyU;! z?5D03E77r_v$&eQQSPc#o`{lxhnRq$O4P^x;^5-@rww9nT z${w(VVmsqtnbp94NH(TpoLBQ-HHqEqzS`)x=%KiNyySu{vdMcoR}Gr+OS#Wz6US-= z6XH`xsT0xIAP760LRzY^0)Q$x;;BX8_c@o}LTTdi^j5-k$Bv58HqQzb2#F+Mpt_m5 zvOg5|jj*)kAi=AvP#Ean0ODsM{6OuQ3PwoojyuIZkcu4CQr_QT}rsh+BzWOcz-Ms_@Qqi!Og=y6?2I3iGbKl6oBZolKJc{|efi}sijgR6dusmo&##1bDFnpU`jV?C7KHN%z72pd2-22wuHe4?}y$OFtkl(N}MC^?>k*e&)IL! zct{2tF&pSyia5WWN3OIpvjD2Q7`|~ z$^Lur9Mb zWb)s2qYJuyX-x`X335@cMDBL4>cJ7quk=a|LQLOzIr5&I4=-W1LwV?86zdbwu5EQy+&2p$Y| z_Tg{f$pcqo{6BgITX5d1N@|OwErr6Q5Q5Y1a3))S>mp0Ze~;tL+0st+>-%+Fi270) zqfxB*oo|6D07~kONmuOz z1}x8I!vCr$XKenq%pGXy*QN~h=T!Wx0%e$I?(j><^A4+k%)jS$Wt(TdI|JHNXeVCz z-ibC*^S|S&0Ly%Om5h5S^AZkEQ*LJ?pqoKa7oz7DIxe58P7OtNE)5tzB&S7!i?9Qz< zB-$v+IG(0d{Gp%HsT)0))H%I`^WGWU9O)Z0=1~&?mIA)`&H@6Vax9}GuN>elhu?}z*f@ciz{lQt{yQ>R|n+4e$)+~yvJFH1C)b#|W3<+@pON)C> zwg96O6fj%~nc$MFh_$GE^HlndMw0+f{Z83K7F7L{@;-KZuJpXuCm?7D2d;O3qC0l8 z>teYOj@U3$TUamXiP7ZE(YlGA3E5izvxGUhR%{GYJDy#}Oxg-#fK_4t`ZGK-nuEF1 zOYpmvd$qyU{67+$N;Es+h?&$nM44p&u?$GKZW82A)TY*SUFQ!Ha)5Etuzk}ZNhpHK z!-m0KP4=wk6oZmuX&#EbU_j&^svh*Tte@;H`ag_x58=Qz-zNAxZ zcqN0x&PNH*y>YQo;x}Jd+Y!L&m*D$U*f@6T^YQC?HA{=HzbcSf9H(_NI6BtG2Ct_1 z%jx+e1t^W;Jm~Nf2N+Ssx4vuhXVC$$LjlU^5d>J7b>9T)hObmlw3m-{@h2s}{FdY1 zEY!{!JUk#;<-p7x5N-jR%F#jyG*y^~7`JfIa(mWQr+z7jQV32~n6q3; zmc$y@LzA2$N`ARKqiWCN(%RdB_!7OJEV%fb=bnJN1X|3xW!npo7)OiZJ2^F~U+`=@ zSn!XzlB7rbOzkN=O2u+?{^42v)friYwgj>Nn2RPX6@}Y;e6f#C1&W&hkrE0wrNgB+ z>bxPIH(3RTSFvmvWrm^@GYEbkYw>#we(wr%NrON^mVhV*8==8w=Oncf7zH^MPwOd9UzFQ+T3C0l2 zasgP*kT)}$yD!Zy95E9$VX)r^X);_O*4ZtPd;bwwpWy;UW$J*a(}mevsZPbJOz`n& z0?;p6;AHY3Zo26yuB-wvk7Zt5G)%wMoi@=LDccgl5cgdkuXu}6O#vd96BleKMsoiF z%gWfS-2(+kM&tE3%R-bE2rbxs)m=AWy1k~Vh(k7r%CESEll0D@RTwS43L+_PQHWstI)nZ&4<8xj8^LUQYpV^H8wIvb< zv>x9p+_KH}eC=J6eQJK5^ecb5d&}qyc1Ne`K7ebELgssrEE5?6de01jv=|yG1rLA< zcvpsHoL8UjyZT*n{EZ>%P46hFJeCqT%(PX{QN${FkX&S{306C<;P0XI+STbW z{pIZ2F1Y2i8ixjk25|Sk+U>`FYgLVt^^z@s-!faxZV}VAchtRAoGW);nT$j@ofe}d zx;BEB-v6;m63vqH?PQFS@pzJ1`{Dohs6fJ)J^|El4j9NO9vv;!49Q~4bp-_8{HKNlKR=F{*jEZ zG}C|k=J|!~#OriR_xq~+KZcD(v5jRN)gPfb`TaiN`Y=r&a9kE~&=37Y zdg2o+xon{^6&!x87wt4xXoy1K3BN{K$_3s4zGSBPTtD@OqV3b{3;J3aYH5Qk2{i@% zoHxFRl7Q7WBf@t)?%ZI6RA%qj;WQ9rtaaAgAV|sjtU-Myx3}D2CHNYaGtEp`X4rIQ z?D-0vrr*6_9Qf&A+@(!02iIoOEqcm{@1!CV>h|wUN`^mjp<+o{gyrI3w#{cLka#;~ z>``z%j$zWmk@DX$4>fEom&la=YlHe&AvL!bh7$MMQ7so|)+f2qbPo!vN9j^@mQ|s5 z;oO&-gxG0ZtrtQ}?JN3VyM_uho!t=M1+zp3MRT~HG;L)QIV+@`)p;#^J5EikP)c(? z#k#PCz}nyZTJxm*XPdYx9`Xm>Bx9~5On!_$3qGkJA~b?-r^iXM0PdRa$1=0@>P2*P zxy%&zcPviHs1|Thr1FLHR8^7nPcT*((AR;4>|9TOySYeBFtX;(rqnQ10__kUDbt>VBvP9g!YQn;kgJIA9 zp%Q2#fOhDuuA+vze-i4qpP1&{Q`D!WE)&v&Q3RMBAESdj;kG`?`7?YRI~L8hs8!2( zF{~8Z1b>y^sEBGaXMs|IifFrOBN+;jne366GE;5QZ|$1^tK(s@bCulI+ffJ2wW)hF zJ_G=A>c1E}hi5%c_B_5F#$oaSe?o;eX#$|Fql`V&3<6IzI`3=Y3GrgYV4wQ&V*;E% z4w-DL^$)PA6{}~LJH~F>ngtN~i3nl*AdsCS;@Kx^enySljKn{6OXY~VgANTm%Fqwe zxojOUz0zkm)mTnJQSF;7?-X@6pQRD@7RFKg0kQ330q7McZ#YWKLO3SL?;lrY#sjE3G9A&meBNl2qRbN_Uz z(GS4z#g!(xh&6d%Zyq{(k>ZkYNd?)|RWa~gr0k0}p(#+9sU27l1X1a<56is|lZ8*i z)*S%JRA`0B<*DOvaxV4lW#?5%fOBV2x;%vr) z*vx@pIWyY!3qY)f=Bbxx(=nnv&a>uMvZItrSI*Ap#0xyw1Qjc1MFY{cG?SamT`o=5wBht#9$1cl*2M#6wwi4dGdo6zJ zz%1q`G)9!&a5PiDWj%kw%j`J|3OUU@L>{Ui0HoDsU3{%&J@*6c_DR|QB7ZH6#^%#4 zz3WS3yTbnSWdAbYGsN%z_NyU@4=V??wT)M8Krlkd(e4VrrhUyo=&5V{>lr*0s+xO` zmX~e4V;UhZOG-MgG=C|d$%F*jWcpYjH+{!>*02kOrgU#SxnGwpUG8rcui_cA|8wjw z`@in!XulqoXvy8<;BvY^pT$eoxn%m2IvI?Zt2@%M)`Tt-IAqJP#7w8Rhq-|5g&dv> z3@AT~r)ed(jKq_tK6?}ON%m!NG{M{gryu9*{-w!A56`^>CwF`rTRKZ;j?%2S%YY7! ztN@HD+ru+-PNn|Ht^>{WyTf~s@S4Av{>n^!`!0Ah^LHzg!a}#67)CoV>zLZC;ZZDB zhwgT*Ex>nkVNmNlpzvp{dzJ%<_83fAlm(ERyoHPq~n3)2@O7GxvobU<-AzACMh=5#%?@>p6gmwFV zhG+OHPAI9Lf2wL0fsd|HyMl=VCa@~+mq^AA6!NIO2T`Fz5SWPf;Uj=hYUwN`poac% zq_N>3ZNEdFi*qaZLM)Key=&irX~I^{>zEHqN~I&>WnG3CBnevdB^bBwEG??d#Sx~qSnOO5}0_w)BwCTGEX*wKG!)g~M0Exxhr= zqn?2QcL0U}K$4sQPT>FnfFJ+>00004bMx$QbAtY0qn?2Qpa6ydK$4sQPyqk|fFJ+> z00004bMx$QbAtY098mpArutiZfayJ6#gF!4Ftka4loA<_)h>KO%Y|YUAkqvqm`>e zP$etPlxqSD|8In*QfK`iT9@sP`A>(WGHk9~69mXA+a3EK#8K}4PZR?`AM!t}nyA(qgW-h)WCLPl4BgDw80Z+;=olFo)CK=fED{+ZP^0v}EdGc7dfop+ zPJS&A(0Mds(24EM<@IqUClG+BH7D5B&LkobAS_f(Urm8CI1rHdC^!Vjd%|kJpdknd zt}6&wCMys~c_0u_DW=qDms+ z|0f}5=Ud}}XygBPYV(Oi(c&mU83hSJecAs;h=d0+nopGee=TJE=c4g{E;2Lz=OPn< zjir&Bg9ojf1E={!p=fc0h^msHiln@pNO&>>1K0l)W;!+|R)+r-0DR##S3+>lvp|4i zg2V7Y0DyoX$zUM>f_auU5DpR?3@HS#0RNefzTgvQ_}#gm_^1B+{zH3z^gs5szv|ca zFaE>7^k4SFZ~v|Le)K2*=-;=F{wMsc-}2*c{7e7DU-d8i;2-_R&MV(DKKS=8mpx1W z?sxvhKZ1~QpXI%kJS*6>MMm+87yl}6dE?`=iUs^7JuUoNPx8sf?k{Z=b`HCC{fB>f zz4~M>eZ@Dt{nZt-#nC-xM$RQLl?&QMoCe=>BWkRpP>w0$6&KOd?zFzw6Q$WlMHOE3 z`+kXscUJ~tVYH?V4Jfw|ZPkBJ+Q{z#|82KUCTaiQwRC_CtADZu7D54dipRxiSm23S znBDXvP{wxSBlziB>m-Q&y>OmjVpL07i&Zq;%8{r&@AN3;MH*UW0)2kLLPzlPzqWsz zmkXrN1^3-)|8i9me$kls>t%eP+*X9IY%?Djfit4bRSH0aGMC^(wPquJ55I*y0Uaw# zEszfdMps999!lGehe2Up20M|v=eJB4G}@>J`4d1w9D;e6@3Jl%UX!o8h@|9hcb!9F z&7u6I@_ME?Pdp-H301sxy&wBWFXH(m4dROvYW4d9vkE6Qlkl^m_+~hVS=Eq16t4Nz zoBu%1wml&6olppHlTx7Ms#mIy_XJ;ekq50-QDAi2?k3q zi3~d;E;f$cCUrXYe7now&Q=0(CZR1Ba)8MF=$M~q@2bJ0O6JZtY%JX0;S>)mZsvnd z%Bh%~&|kX?H?h&M@bGbMKXjUyr(ROJ_nzqLvWpV<62u4}Bb)%D*L!+;z9{5?X{GM- z6+s!->^2sAeo1c5 zaNhGO)C9OeGKSb#T1A$Ez=N8nSBG9@$mQVc)R9kq8 zE78dsAY%xwn@)`AZtG&~r+?K;c(6eCBQpgsK1 zkl*N!YFx@{ul#QMEW-_4So@)q*o^UusS|u;!ze%7r1hc0ImIrsB)b8WclGG~z4@hn z1FGlLY*rl?o~EUiT{p_Xc^lVYq^bnRx1o3VgptaCNbJXUfe3>m0}X`{BXlo%7Nj(a z09*c?1U`Yy&qOf|&;ey)w!b;(sw)Y<7H_MXe2{6tPDVU?7& z>5&~3l*-3yIYA}hYJX=!brah)ly;kbw?WB3j}3+h0y}2csi)mS``aQ5xTy-mv^RCc zgk55$dHCq)G~oB&?%Ch_$v)7((7V43Khb1uG0+A`m@O$P z3bsbUDU6|JQw=}-g$|oF947&4T;4N){QG1gX%K~;g8WITcFSC2k%w39C3(urgGxsv8omjHJDH%b=lpeUDI;Dxv zh5Q>w+Woy%8RG1;N+EX@&F>~@G$=8`5x#n= zSqcn%#qcnrU2Owr6pgR8iIVqQ)?6G{6*4k82es*DQhXm|s^d;tvr?w3FGNzkFR3&) zzj9z;W%vXRy5rAKbuErZMlvSMv7#N^_YAUIyWHQ4CQxwkZQ=IrOhF&*Y8iXkYTqE1 z&>pv?cU}o1@5#KuP-F7-YTU4lt^Or2=t`TNQ0UykM8IFmYP8-rFC_-yx@AQWi0GBt zhLC|%{-vO*fPVtg#)CLzH&+618mGrC-!vBw!!CcqK@~`$MY^O)&)w@izwd!$ zR5oeCRse5pm)=?*nXqcO>3?}+WO#f&$2JRkG}tj@?f2@# zWZ(EEBQ`(qM_x{WC5q*!@pm~mg0ZXGy=$CZzfGrZ5aA#~fqbdhu-fE-Rxp#l)S z*GrfuBT85{>-C%J-GAGyNPTJ}8W29_&h9bI@j?M2aYTklV!Ko}5~%7u%m+owEfMAW z=$R^ihDt(6RfnO=QjNn|%jhFOGjrRuD7Idwy)=tW!HSWI?%=8EpmYSB;H)%up9s9Q zLOHw5+V{7lZMQqglkm(7I`mi4uTC!x>|H^yB!W37-1j7>=mJlA;l-b5<9iSstVgH` z%U?5th~xk!5(y!6-Z*a0?2T}h`*}!6k+Oah$K`s+oXL}d!Cd+s;MobtdLz%I@2$3C zCbf{1I205I8~nC;M+eh;F1-Gy5onJqtECSH$w;0GzN5O8c2&s(*fk^+LT6vYHz+ zq?urw?yd7ILWE1Pp!-K<%#rp1SyPL^_3z7jU*{NvdsPCO>W^XeGw6E|mr;2rLzK96 z!f(+Oix00*#(x;@Vq+>5Jzr@DWZBYOxU33<20SenFkj>KWSj|doU^=s6$+)if6D+> zn7|@3NGaZXiXdtlYkUEqcH@Iu)WA9;)w;QyfH9!S1=87YN=_KEjZiUz2rF}0)}Cz* z&VD|?97j6LO;XeW04)@A3wdzq1Sdu}$b}1TI0oVSW4=n^SiQ6??p?C4>t4T5lO#)} z3XOp+ZjZFc2|IUYkN(BP5eJTA?{5#=N;a3)U0Hfze2M27N4kkQO{f4dIKuc{zvA=+ zaSssT_~d%!fDcmvrLxp9&vVBf)-c17{wuF4{LYRK1#S@~+uBf#Y_oP8Ll_Sy<*$ps zD}MS-sr+Q#gHnEd096-VsZ%gr{0`TuZ#6+rwK7`MQt?61LAYE2|47EGG#J{6b}EQ; zI~i@-bfY(bEF;W7$?|ZEh}5@BvUW*{N11*1Jt6dXbtzy0!4%-%A-bpB*D^Ik<~Xv@ zHF9}g(uJqlwW=l`DT|iv*uT_NEvWZ*>bw3q;_dA@bn6d@C|#fB%^g>3fmzMhdeFn^35rVGQQy!;jmrd zHOsS!B<`O(Im3~;F$uLpJ-=@fpfAHl?Jh3G-|~sWl6ocLoQcLGvpS^6*H8HR?;TO7 zBJ_{Oo$imgTjrLeR4kf3ArQ2-XNTCb#Eq$~n!RqczI7P5GOOx_T zVJA@2c92-)**z>mjL=BXIa7+G|0?_?>m(+SEL)^rO-w~>@y=9Og1HTj{zar>! z+SV;9#xKMwVyYCP6yalEkh9GDgcTI|MQeughNE#sonD`jFFU`>f=n64q?P?&kQ!LQ z%3amoMfU;Lvkre&NBs(7KLKdOs}>Vw7=}HSB%gC zK2d)KTyCrUp_+K>?LC}hua}Q658f$A1apEBcdLRq1HPDhK|)w8p#Gs`$8GG?ydna3WtAAol}>sG_bp5Q;|7du?TiPedITmf8efy zev<-`LlT7e(}6U0Gn6}nq*B?R#~FOa|6S(dKm&~rv-BFY^E*JuJgJN-L_CunRLYB? zIG*z(G|%SRH`H5m%5TQ4=8}M5(H4?~Mqy^DnY~;4 zufAsXb+}Ltd&dhesJkuaxkAdhhINk42UQTiqr#$%Osw(2(2zMt$3q_Hc2Yz1W43Hz zV7O!}f$94{&M~TO#ae(}iwJSsF&AdKk=rcU7KhuI9DX-efmsk*)}GEgi_CptMRV4sm$zooQstIr`FG@vS~*lKndiE|JG_0ZEM-%zp0Lh9&T*zSvMrdS zArKaV;51{FHvv?b@k>fERFp(-J9iX_x7D8){4DH=6s6MwsY} z)hw_NEW$LLe``XvEy)XkSud}QgJbb3DkLq@;G(>-1r+X~WAV;veg1Y^$mh6hY|6j0 zFZye;3K*=^@h9211QrU>G7Ad**lunw(}dmH+^v-uc;=Gw(}!|p&6rfOrK2-7B%$=o zM7Dig1sxY4hS|EM>&v6PQN%DlV<(QRSJNL-8s+ql__Ma0wdOWJu5!jzDw`fdKSz_l zse=Q$^_ZD175xg|clUI>H4>&PYog8C z4p;*-NzW$DOmJ{Q`1DanHT57f?_dp_KpUsKCG>5dgjq^{Ocz7Wm>EIHo z)OYKG37Wp5dYtQc{<1|4T;3SnbTm;6>i|p~LfZ}Xe!B@<1eFwpO zKQhdKSMJ#EfFPH}13YRSQeYm#{n%dTmequFv~n0UoWPD|FsLkaR8D_mIc%2KqZ-fn z)x?*OmcZ^WnoQy?^A>&yx3J?(;4#&Uitz-kKZ&w3Uz$Qe)S4F9)}}HA#-_LIyh$S# zTB1-5X5K>p7XR4l5#|bi6DwzLR=(Uq!eiq|S#2CqBh|xrj7M7?M3A5YB4N~vRN!=o z#vivXVLUiqB%ttDrqo;P^kVx#z-u(*E`ws))pq2kM1GX0@6P)>^1d8p`PmVtq*)1Dt2e~IS#Cmrw(ZMWBmpt?x^lsC1J9?1K)$=TxN$f7k4A>3{f{A#Cpj-Tv?{ZrnD!;fELF==*p^ zApaR9Pwez!VR{ZsWZ&^e+&WF>I|+F4X273CfOky@J%ribwZ3vNnxoF>;nYq+r7PKf zuSGitGprZW*e-_kk-{iMuW!7l>ty$qlF^k=&YbnAx!H82V(%iU0gJpA4zzV}`MZCP z-BIAFSZwg>wmEsle9PbIMKT%;CbJCw&NipXm zQO`cWZz+{ypuZaV9B~{|@~F|g&%Xl@2{oF)-%ZlqtOI~;uZDxa&`$;76ngAh#7Yt@ zk_Kfh%Azcu=zacmarO5Ix#jopX(lS(i>T;0f$DT0ko2L^XT`9?Iuvcl*naHw0fgg= zi9WxSHj=L?z_&jDo#eY|f!8Ks-z1+>kxIuPVDBmhA*QlDv_DR{T%m?1tL zAqHoh1^^oRWoDeOGj>;<_ib;O3-K+_g8Lx2@Bz&%44Zidq8cO6CG)An(dejHk?y4c zwxln$^em>k`&D_^h?U%*3C&(D1%p4cfjj-{fl`_1lpmdIIs3o|gx<#l3T5B>SAup@djjjtcEPq$6xKQa*iS>72EI9b$G z2t=0Vp{K-U+SYO?@KZ+{iWa}BZv9+dwnPh zjZ6`86OOs9N@u6$3M+~vGLF|XNAI^8u+hTqu8W(^R_8Mb(z!-GSlt*2`6*I9#Ui))Pc{Ij^t#kq>+@$zRT zw>d_g8D@Q6DC1WPFyI_p_}3Q&ufZh0)5_659CM9>5YZH(j*RLJ=6xsp(J68Pj5vW* zP0RS=e~(9~*eyR+!2O=uu9985>R9l%CkkGJNq)PTqkl-ncuaqMtf}i4Rc}n2g)gH; z8psX~UvwTbH89BA0Ji1qRad=kP?@g;;D;jnXmDuVBI>$!`p3`=>(psqjo-o&&JE%% zr4Gv+)QgA8xjG@xCZe_QmgLvb=B_`K7(r^iA*n45bA|St;p3`mxU#s71p$rsrV)Xt zoX^`-=6xOkrOF#mnr%fD2tZQ02>?gQ|$ssOZlW5zRK@=+qwjVYE3Y$T00^0kTN9y)~*G1#HPgPg=#}Zux zuljduXzpGkcphgQ*j`|Nwm$KJo+MStJRvSNzR5yG2~eg!z_IfR(=10tzfT!udtjz( zrZO{>Srg_l>cAZduT_4{={h>yfGFb=qTIB%bqy;Qk`wgFvcv&~NI@#A^5oGIybZVL zSQK4+7VmQs3LR9JUgjf0Um2Kqb9fg7N|@>#apRV^WD=aXi#j?u-;2CPP`y;NcC@^f z*d$u3Qd7EZb>r<~3H)zVe$)ORM#sta-Y7J)M$egbp2&;9B|23W5zxJygrkM9|4}7( zD_qRtm7|UepRY!rykh42fPOM-93D7b{=eG5cQR&R@KQmUvJiuFnSnm{$A8pX7JbjQ zXNmp{{M0En?wL9_K$%wE6RUdRJ_dg+s8)#BRDbe7*!U)IXZ~Sm7QsJsLcr5oaSD4N z6pMf&9P=ZS6nWxh>w2QSXpQYC%k+?IyB~>EdGeTAk@i9}pCVTR3Kn9^p`BG>2)W#| zmaU0}LT3*r^9zrBaPXYu_qs2~%}aIUZAF{3*zwOSt@fxO#VoK|3eitTnkG_%Kj6L@ z-H)9Gd2NNIMN%Rx^iYEOB+@zo*yxsdG9p9s@rbOr&7&Kk!VHKE|1Q>OtW_S z9AN51g4Ks$-WD!rv4!ycWG7^{w<<NY^K`kOxA&5*rP7)rGM`ggk{YF`dt%dH_$x}QEH35v+cy>PSkVmLkcWCx))q|i zpI{JPYhJMUPGBUDdL*8m4(O={arU%+l}I|M)J3>|&RsxlS$-H2$3WjY(gP-fpeDrA zQ_i?2JKq6OFM08C@nGv>o{w4hPp6>bo2kJ~G!rWaoaoSq4@+ewo~)zO51N*jD0!F5 zin*<>m;Qj6==3EA>!)sZli!3`J$aN%k$!+7mw#ikBaW)LQ@8SZv=_Rftb0ASZI>h{ zOZCMX1zM26sS7)4dAvgkUU)fZJR+G@R1Jm6s!k1U_6Y*HKqW1`YBv@t2O#LpTzKQCuy8JaM*!<>c_8A3nchiXwxv<>u%aJ_7j!w8ry zcYx>MFRM$|Z3BgAKoLY!qHRbOpC4j+4f~LJR+jlf$;TCFFOXRl_|z2#$k2|it>(E zB9rOu|E1d!dh(l;l6H;`^dv))#|OESP(AdfN1y$jX%qWiC;;7dBB4gmIOs@0xnyhl zlVFjkhu(Kt`dlCYwH!Z;t}RX7Dmz^O?WAG8_N#DOFyH`!@sT=A0-;DU;?}VxNS*w` z-gLr0u+b$n_Mo~D#%J!gG$lddNibtp`SN4G;~u{6mT3wFn-Hp*Jl9#mTS<@wdkIYr zT892s1}7o4e&u0Hj>g7sk)|cF=%oh}KbUR9l;I@FW{6{V2= zZ2il_Ge=e(ki2)pzXFDcrQHh zA1U;wt`)g#kc}qlI_1LTu8M$OzB-{c)W-*6&xOLv%q|{M1I8%+1sFmMiEG~-@oXi< z5K*K}Jx}ugSe{+r44^?tV~s3P)SxYg3c+h>ULq%(xa%@*p91TRgbV5lUtd{C`6-n7 z?GNF%OEI|n3N?%C$~+=|{0I7Cro>^K0xv8NIL}zF)E7y6Sz5*etZ``uWuiCvOU#_bmD}L za0Iu6DnJmyc?5x5PQ}P@V%o8sGRH~|klW~Z%#&Vt-C)TOqY7nYJtHwqN;2&Z@cvKr zG8Z|=i(kW8z7RZT;a8CI{N%(6Jw(hd(xszfbh#7YgjO1X8pZuR0H8tg15vO?7DnX;0@ zuJe20({bABV*(yyDhho;6a()7m^&qP4V+JvfYnZ5iiaNsUbb8N<~|xHY+$@mpuxP_LkaT$Sfx zx0@{h84RR^6>t2Fl+4FuUEXO%lgzcpqSLbl$uQoePkwOD4`hvAS9Q|8V6F!IPOHHC zG~+)K0Exj9nY#L4j=T7<8Q92nUPr@^g`I@A07@??C}H+ye%PQxGpU%dwB=%Bh>%8O z{Uatk4qH=e0RT80DH<^I;xl+(X{&=R%CfE?KD=q=`v}|PO^Ad(==l73e}A@0_L{qP zB!Gpo_{>onfTaoJV8GO;Dn2HVR^EXlu!nQ3p17u0xD~*%56uZd0>*zKpi{@Y9lrd3 z8;1*J@NankHER1+`%~qBL%u|$ghP_yC@;#7jkWzr{og;=jV^ssE&!RMS09%K(-Z|S zD)`Q%;~e&GQMBvZx;$zn`@ev9a(Y)oRQJrdD~71E)AUxUg}!*K6&7Qxybi`j?q z`S#1%DC(=;T@4|hA*gPj1Mj$!gcwxQ9G`tUM)bw({w@{h z*;k7O$^xfcIp)<2Hzj6d;AuT)r02|OpKofK@-6vcgWc%_>#INYpmbu`Z zSw7e=;>*(&k9Wci7C4_KQRh;Zv@-AYjpVQ3_SHT-E21Run!2G4$U#x#hrj;xuT_=* z`bAv^pQ#scHHT)q#28^`Ky>q6pR%bDMstG!0?5@n-)$2{-VG~JXXfwPn6;S|Q!%}o z$M*i-&jRZ1u7`|6w?7nl8+d2AQgzXoP>>5!Yueb@`Q+4D<7>U72~IfYG(WfbLiyQP z+m9_^ApM;^=3&7xm)~v;Js2d4q`6m%m`5g(Qsf~04GEm#XJJEHeZfLk6esE<42 ze7wBP;%KZ;y5g4~u!yAIPN{?*WskeXxuV*0jQqsye{SB+U`c&;+efC@z=W_*o$MRnWj(@8t_*OxW%9@)0 zJG@l@1~^N(V?93Tx#J?n>GrNzHN1^1g_*)uV`G{VPh6IvMcg*YeMs4PMgi(W83*^~ zqo0dmOQpYbs?o5@DOEf zL&AD_Cv09HFDM8BYs81a0a%U9{GRnt$~iGa4|46aLi<|NM!w@HT^KCFxE7VJ`IR<` z%dzgl2dB$)Gmrih!)&}fk%b`%jW67UDiFklp#%nT)XF9?XJ$p^Q{@gA>yh~%^ZP<| z*nminJEvl?80!F4@KaW<3Z7MoBd=ux?{SUEUg^Su@_9_7gS2iF0EItcpFG~FPsp&I z$EkUyAAr%*^*PAaq1D^609h0;$?E3Y-QVYHF9$5Z;5h6Vv$_N(GY<;~yZ5uF#}cutH5lIZ}p{Q4ZcFDNDp z)ytnTj@2C^kxGzaWw~^-qmubYE*@O>%j0!Zlx)!|qs>%_DrBj_31NhTi?N5P4OjYd zJr!l=WG>7>o`Od9u#}w?BGG{*!&5qu*ttaGrn~qkoVQJUML~Rq#rIwC1MU~~O!jaL z|F;nKg;E8Haf)3408f69lX{|6OX!B$%q#BOg9%tPOs)hMJKD;Na0Xd950e>&@3Yp} zeBH;-5MTC5k_b;tcjGn5P<%>6?xTk{;9g)|;y%reP%P^tDv=3SGOgUFWT0cNLDLV@G5{y&V^-AM$mOBT3tPR- zu@}SBZs{*}^&~1Oe+=2BqzOHUObcsb)B7l;0f&MAxK+eHpp8XF!QGb^ULDY8ri^^{ zH*`#F3eQsSt@yyh={!m_>J}UeygRRpIHdF{zkc)nm&Q4f&5W{hQ)|VMg*#4RX33*? z0Dg~YPzre6MknF;N{C~!=q86$KWniq%C5O(Ww}+8N^OnvD@fiB*if- z<`cL)$o;XV>_xGk2??NL^>nrfp$C=toi3XlFS*w9`+PcPdRJaaykRQ48^MIBQIpQP?FnIZ6V6WQ<#F-v z6Cg-f@ZeiapdY^;t&12Xjt)&+U|F3Zk;}IzABJ81a9AbM9{+0He9IO|H``u(1gVM+ zWX};POL15)u_e^K@*sZF!sOi3c?pTp*y}qICb-%ngGc*GepmM9i)a^nEE95^j=g(^ zbMQhP;V*?ZY6QvxAm_~ObZGj8%p}~({7It*`0OqL095%`jXtDi$uoifdsrwd4KA(g zQtje(MzrpM-D=O_)VeO(bNm2vF}@hvc=hNQZVt|MvC(*-Mnf?jc;QGq2jkk>6_nD} z#}t8X-dU4Yak}#Xvi^5$r7h^;$#7&McI3ljFyWbCA#^t{m%}_Tt^wa?l;ZpMi)%!6 zbAT8JcTu|>7T+WBwZ34s8D)0*oRsX+;-AG?F&rMn-eK(>=wcrTSMXw?9FA}$dapmv z`j|1}{DT?E9UY4lP!H#~LZyoh=kFiK;N1&VCSq(tq)RBrL7L2iBieM-{Jsu{ zkiU>SgzKo3hrNq&_v*MaY7dm39LLy=-JHw@UZ3Y0vH}>o_ncxYHt}YFmL+_-6~>>G zg;Sr8V5hH!0iUXv38Bvw0k^kc@f{>yZih_4d(_-3Zr z4%D%@SzespzXrf&;*N1|1wbUW6YJ#(udhd^WU{ z>X1$SYB0e6yIaUimrgt7O#sMc)2oxP zaW}t*U<67>C`1wKA^XUluFi^Ss;cj+$_Z@ZO)HjLH@B_ufN$7}f@itTx`CkmA`pKty{< zjV;z*E;STja{29zF+`v^WKzAxwbh%id)ku{W!nR^nS@-enqdN$XK-dNzX^O2$fs8* z)o<1_OHEU8S zfuH$DXJv`j1r(h{L!~)13fS5(z_E91SpvEnTv{2@*&w}ys6w+2F;|DKRS10O>|}(; zHjd37%m#ZkdC*^X*l|)=Efq1fa|Md-j7~AAKtLly{hQt8svivV+ z%l&OHITQG^RarxW8(X{1IG|`0mHcfd=roKRWR-)QOfafaU!mGO-h(Rf;CIK^m_{7f zR9x8Yv`Kk41`xk9&44xw)iM8G|9HafAM{WC&a+E}a6rm5;3N$Y_~Q?3P|>^I1ii0*V zmx)XM#ohDET}t_LOwp;*xzyYHPwL~(h}p*@EAo04Jw7NQ0CZpmk@U4Z1))iic!^Ni zSO7GU8UVOF`X=D}FglhZ_WJxWq#>2dw2D+csCI`X)A`Z{m*k-|Z&77K;#0+g$YlU* z@wPCe9A1j5Vx?t|_@MC`Ow5zSSS%d>F@RxEFm{3#iFuB#5LIXDeHKty{_nh=Hc9YP z=5q4cq2w@G>62fk5pwRQtw<^lq1b(aBsd1fX$blo90dHr;kFY1A*?`3GNQO@n%MRwX~b?vxR2+}DL_l`wN zsfswboiEU-P8-{p0o1YppaM;rd-ShRvS~cl{8&8nJGR-A(&;WLxmH+jyH)EXB%m^! zCsjYpVYvX|hr__!t3B$|Qgbkmb$P|s#;{9f3i}WsPUOBhXeoV#zkV9ki32qp<4(_E zfd7K-7lPgNlW3@!Z%*8l{O)ZApr0`m3hA)UC(es0&nZ>6Zeztxr5V58m zyHSl@Gzb^_hp(voKi5oP^)?XX08&uR;8tv)TU>P|?n&TZbQugo2_f@)HYR>Mr>*<} z^9qS_Y!&s8_lb)fril-E}dqinMd=R}^ml6IN znlXR#|5AO5-KWhe5S%K_?pHLz7p{<4raa4fVHjG9LV4M?hCA9>9TKqtUw^hQnm51$ zP2A)>s0zRYT8z_ciMH16x3?mvbMpgD_ljSJB2Hn;07!B!LMrKNqVP~%Vt#kfb003C zqxe*pC!Gl-p`K27JHDUQfN%2w$@r&j8{QZ%w`7n*>ZAK?XfgKDD z5UjqaoF}z~7bY`=`VV!TqC;R*&KHMv`N2gauQpN?8iSvBx@7`f9u`_}WyJe7Q}NG=426BgtLU(qM=B+NHDSf3 z`LYaKWV@-0@m&ATgUUH=2^O{#b~w8^kr||(_BcxQ1U=LZ=pUi0Jq&K#AEV4QCi*uI|W}m|7+V{;JKwKh2NF4by5Rm}=fF4`asA@|tzT#%(KY7*(ktDsmr+ z+T_j+4~)n+r1k_}JlkJ#JCbH-Z)!#A%;p7Bbs{ZBxTY#(E0>Z}a|@`-{b-#`v`z2v zLSiYD>grCX;j#G383s|y7b$_&@)*ZfoSeFpFFKHg@#z$zfr<-ku%pY%;j-quFF@MH z(ni|f=o?O*zY8B5R>XVq4|X+Y^B6S4Q0x6yIF(YjU~{+lzw-EyBQq+0N%ww}6$>Vb zYzj|s4%BRVg~R)Zkx93uK~fRab9v+pO`l3&&!x6BD|q?9h|TKo76})(oZ72|MBiWA z?jk$iIjoQdVRJk&eLq6~%jV|z@TaYrhnIO0m~cHym^)D`8LUysoP2n(QO-xsHCB9J zsZ&8|AS9MB1Dt_a{wyqZ)#yf)_^A)SyA_c_s^ima?V0!*}Rd7Xz2SR&I=jDc6 z@<-_0{67C%DeHy-ARf=P=d0LRP0!@PI+OVoa{J)A=%)DL>#zWW2>M zv!tPhfTw}y9<=C|2IWD%VQSR#t$yp}rQN@+|97(!10uk)wifEo3J0i!zm8-o#PRbA zJEL?4($EUWDbx{CPb$>PRwjnDmBh}`U@Z9sb7MFyV=3E9GE|*e)|VRG;uWn!L-=$A zUn+|A;g-5TZt*W_I@2Qgq5K>Ed|CgD!;c`c{l|ILZU2SC$;Po*%NHP2*MNvlYDT

-tvYs@3HlR4A#*k^>Viw8<&J`jZ*0&) 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

% zWk;=5`td|dT&?-VfeOoWW&uQwFkgy& zKa$HHlQi`YYgiYo0b!H=HX@hNSFGHMlq)&7c7bpL`WAne~Q)4z@Z#D9rG`Xxy&l#5j;ml1zC93Hq;9OWdUd^vr-K+DA~Ug7@&BkAb^D!k1LGrI6j|Sa14Y) z`Gd?qT5i8|_l%JBvwZ4aAQQ10{8&l-Ck|x$f=xqlqpnkoiaMPeX8|O63UI9kU8J-z z9__VH61g0<@8K+X!A)a4t7Za62S-DR+^dle997$Z(ghVjxW9X}|Ii-hO_s%H5dsI{ zUwRYPb(Ur4!U`7%zfu2c)xMpG!T)h0run5=xSL72Sc?LqMK};jK>!ogD+GJc&>N+U zJDNIps4A@?dL$?IPvAleGfrfIF~;g^*6bpcML6kOCoD6>RtVrw4j!sZwOcVqmeJJ! zpxS{q#U@YLqehm5-t(`wW($WZai^Tl8lxRQtpH{EDHF*g*pnzOHZG+2EzZ{ zGyX?(K>f1nVa@+9RnLcB{^Az)pS3Fj!vEbC{)cw|vVkn@=hv2Bms;Tebb?hrLW!@|{w}^fwjKy* Qj<^GYTn2zlUI4-V4@%@{PXGV_ literal 0 HcmV?d00001 diff --git a/protocol-designer/src/assets/images/placeholder_image_delete.png b/protocol-designer/src/assets/images/placeholder_image_delete.png deleted file mode 100644 index f429a3862dc0c9dd01b5013c30689735e9f20e94..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199922 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 Nu;>*Eega_qu;B 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 tp;vlbPWv% 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 diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx index bfac8f51a19..442f6d0e4ed 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx @@ -1,6 +1,6 @@ -import type * as React from 'react' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' +import { css } from 'styled-components' +import { useState, useLayoutEffect } from 'react' import { ALIGN_CENTER, ALIGN_END, @@ -18,7 +18,12 @@ import { TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' -import temporaryImg from '../../assets/images/placeholder_image_delete.png' +import one from '../../assets/images/onboarding_animation_1.webm' +import two from '../../assets/images/onboarding_animation_2.webm' +import three from '../../assets/images/onboarding_animation_3.webm' +import four from '../../assets/images/onboarding_animation_4.webm' +import five from '../../assets/images/onboarding_animation_5.webm' +import six from '../../assets/images/onboarding_animation_6.webm' import { BUTTON_LINK_STYLE } from '../../atoms' interface WizardBodyProps { @@ -29,9 +34,18 @@ interface WizardBodyProps { disabled?: boolean goBack?: () => void subHeader?: string - imgSrc?: string tooltipOnDisabled?: string } + +const ONBOARDING_ANIMATIONS: Record = { + 1: one, + 2: two, + 3: three, + 4: four, + 5: five, + 6: six, +} + export function WizardBody(props: WizardBodyProps): JSX.Element { const { stepNumber, @@ -41,13 +55,26 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { subHeader, proceed, disabled = false, - imgSrc, tooltipOnDisabled, } = props const { t } = useTranslation('shared') const [targetProps, tooltipProps] = useHoverTooltip({ placement: 'top', }) + const [asset, setAsset] = useState(null) + const [loaded, setLoaded] = useState(false) + + useLayoutEffect(() => { + const videoAsset = ONBOARDING_ANIMATIONS[stepNumber] + setLoaded(false) + setAsset(videoAsset) + const timeout = setTimeout(() => { + setLoaded(true) + }, 100) + return () => { + clearTimeout(timeout) + } + }, [stepNumber]) return ( - - + + ) } - -const StyledImg = styled.img` - border-radius: ${BORDERS.borderRadius16}; - max-height: 844px; -` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx index 085e2e76efc..f0493707774 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx @@ -37,6 +37,6 @@ describe('WizardBody', () => { expect(props.proceed).toHaveBeenCalled() fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(props.goBack).toHaveBeenCalled() - screen.getByRole('img', { name: '' }) + screen.getByLabelText('onboarding animation for page 1') }) }) From 9404144b1a08bb470de8913ddcffbf21f6a413dd Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Fri, 22 Nov 2024 09:18:32 -0500 Subject: [PATCH 52/52] test(api): fix or ignore Decoy related warnings in unit tests (#16944) Selectively ignore or otherwise fix all Decoy related warnings in api tests --- api/pytest.ini | 6 + .../protocol_api/test_instrument_context.py | 108 +++++++++++++++--- .../commands/test_verify_tip_presence.py | 12 +- .../unsafe/test_update_position_estimators.py | 5 - .../protocol_engine/test_protocol_engine.py | 3 +- .../protocol_runner/test_protocol_runner.py | 1 + 6 files changed, 105 insertions(+), 30 deletions(-) diff --git a/api/pytest.ini b/api/pytest.ini index a8e3bbb1933..61288b3f3c1 100644 --- a/api/pytest.ini +++ b/api/pytest.ini @@ -5,3 +5,9 @@ markers = ot3_only: Test only functions using the OT3 hardware addopts = --color=yes --strict-markers asyncio_mode = auto + +# TODO this should be looked into being removed upon updating the Decoy library. The purpose of this warning is to +# catch missing attributes, but it raises for any property referenced in a test which accounts for about ~250 warnings +# which aren't serving any useful purpose and obscure other warnings. +filterwarnings = + ignore::decoy.warnings.MissingSpecAttributeWarning diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1b8445ed7b2..8282f660a44 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1501,6 +1501,7 @@ def test_mix_no_lpd( mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1516,6 +1517,7 @@ def test_mix_no_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1523,19 +1525,63 @@ def test_mix_no_lpd( subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, - ) - decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) + # Slight differences in dispense push-out logic for 2.14 and 2.15 api levels + if subject.api_version < APIVersion(2, 16): + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=10, + ) + else: + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, + ) + decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=0, ) @@ -1551,6 +1597,7 @@ def test_mix_with_lpd( """It should aspirate/dispense to a well several times and do 1 lpd.""" mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1566,6 +1613,7 @@ def test_mix_with_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1577,19 +1625,45 @@ def test_mix_with_lpd( subject.liquid_presence_detection = True subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, ) - decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=1, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py index 53eb1f5a59e..ef6d79629be 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py +++ b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py @@ -23,13 +23,13 @@ async def test_verify_tip_presence_implementation( expectedState=TipPresenceStatus.PRESENT, ) - decoy.when( + result = await subject.execute(data) + + assert result == SuccessData(public=VerifyTipPresenceResult()) + decoy.verify( await tip_handler.verify_tip_presence( pipette_id="pipette-id", expected=TipPresenceStatus.PRESENT, + follow_singular_sensor=None, ) - ).then_return(None) - - result = await subject.execute(data) - - assert result == SuccessData(public=VerifyTipPresenceResult()) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py index 79131994299..da381635ce3 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py @@ -37,11 +37,6 @@ async def test_update_position_estimators_implementation( decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( Axis.Y ) - decoy.when( - await ot3_hardware_api.update_axis_position_estimations( - [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] - ) - ).then_return(None) result = await subject.execute(data) diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index bc581114ab2..d7e4b32e02a 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -997,8 +997,7 @@ async def test_estop_noops_if_invalid( subject.estop() # Should not raise. decoy.verify( - action_dispatcher.dispatch(), # type: ignore - ignore_extra_args=True, + action_dispatcher.dispatch(expected_action), times=0, ) decoy.verify( diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 2f06e27c2c2..15e0192175e 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -448,6 +448,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( await run_func() +@pytest.mark.filterwarnings("ignore::decoy.warnings.RedundantVerifyWarning") @pytest.mark.parametrize( "schema_version, json_protocol", [